diff --git a/app/bootstrap.php b/app/bootstrap.php index 0a16687b..01e06953 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -22,10 +22,12 @@ use Aura\Session\SessionFactory; use Aviat\AnimeClient\API\Kitsu\{ Auth as KitsuAuth, ListItem as KitsuListItem, + KitsuRequestBuilder, Model as KitsuModel }; use Aviat\AnimeClient\API\MAL\{ ListItem as MALListItem, + MALRequestBuilder, Model as MALModel }; use Aviat\AnimeClient\Model; @@ -48,13 +50,13 @@ return function(array $config_array = []) { $app_logger = new Logger('animeclient'); $app_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/app.log', Logger::NOTICE)); - $kitsu_request_logger = new Logger('kitsu_request'); + $kitsu_request_logger = new Logger('kitsu-request'); $kitsu_request_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/kitsu_request.log', Logger::NOTICE)); - $mal_request_logger = new Logger('mal_request'); + $mal_request_logger = new Logger('mal-request'); $mal_request_logger->pushHandler(new RotatingFileHandler(__DIR__ . '/logs/mal_request.log', Logger::NOTICE)); $container->setLogger($app_logger, 'default'); - $container->setLogger($kitsu_request_logger, 'kitsu_request'); - $container->setLogger($mal_request_logger, 'mal_request'); + $container->setLogger($kitsu_request_logger, 'kitsu-request'); + $container->setLogger($mal_request_logger, 'mal-request'); // ------------------------------------------------------------------------- // Injected Objects @@ -115,21 +117,35 @@ return function(array $config_array = []) { // Models $container->set('kitsu-model', function($container) { + $requestBuilder = new KitsuRequestBuilder(); + $requestBuilder->setLogger($container->getLogger('kitsu-request')); + $listItem = new KitsuListItem(); $listItem->setContainer($container); + $listItem->setRequestBuilder($requestBuilder); + $model = new KitsuModel($listItem); $model->setContainer($container); + $model->setRequestBuilder($requestBuilder); + $cache = $container->get('cache'); $model->setCache($cache); return $model; }); $container->set('mal-model', function($container) { + $requestBuilder = new MALRequestBuilder(); + $requestBuilder->setLogger($container->getLogger('mal-request')); + $listItem = new MALListItem(); $listItem->setContainer($container); + $listItem->setRequestBuilder($requestBuilder); + $model = new MALModel($listItem); $model->setContainer($container); + $model->setRequestBuilder($requestBuilder); return $model; }); + $container->set('api-model', function($container) { return new Model\API($container); }); diff --git a/src/API/APIRequestBuilder.php b/src/API/APIRequestBuilder.php new file mode 100644 index 00000000..52fc91c2 --- /dev/null +++ b/src/API/APIRequestBuilder.php @@ -0,0 +1,217 @@ + + * @copyright 2015 - 2017 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.0 + * @link https://github.com/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\API; + +use Amp\Artax\{ + Client, + FormBody, + Request +}; +use Aviat\Ion\Di\ContainerAware; +use InvalidArgumentException; +use Psr\Log\LoggerAwareTrait; + +/** + * Wrapper around Artex to make it easier to build API requests + */ +class APIRequestBuilder { + use LoggerAwareTrait; + + /** + * Url prefix for making url requests + * @var string + */ + protected $baseUrl = ''; + + /** + * Url path of the request + * @var string + */ + protected $path = ''; + + /** + * Query string for the request + * @var string + */ + protected $query = ''; + + /** + * Default request headers + * @var array + */ + protected $defaultHeaders = []; + + /** + * Valid HTTP request methos + * @var array + */ + protected $validMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']; + + /** + * The current request + * @var \Amp\Promise + */ + protected $request; + + /** + * Set body as form fields + * + * @param array $fields Mapping of field names to values + * @return self + */ + public function setFormFields(array $fields): self + { + $body = $this->fixBody((new FormBody)->addFields($createData)); + $this->setBody($body); + return $this; + } + + /** + * Set the request body + * + * @param FormBody|string $body + * @return self + */ + public function setBody($body): self + { + $this->request->setBody($body); + return $this; + } + + /** + * Set a request header + * + * @param string $name + * @param string $value + * @return self + */ + public function setHeader(string $name, string $value): self + { + $this->request->setHeader($name, $value); + return $this; + } + + /** + * Set multiple request headers + * + * name => value + * + * @param array $headers + * @return self + */ + public function setHeaders(array $headers): self + { + foreach ($headers as $name => $value) + { + $this->setHeader($name, $value); + } + + return $this; + } + + /** + * Append a query string in array format + * + * @param array $params + * @return self + */ + public function setQuery(array $params): self + { + $this->query = http_build_query($params); + return $this; + } + + /** + * Return the promise for the current request + * + * @return \Amp\Promise + */ + public function getFullRequest() + { + $this->buildUri(); + return $this->request; + } + + /** + * Create a new http request + * + * @param string $type + * @param string $uri + * @return self + */ + public function newRequest(string $type, string $uri): self + { + if ( ! in_array($type, $this->validMethods)) + { + throw new InvalidArgumentException('Invalid HTTP methods'); + } + + $this->resetState(); + + $this->request + ->setMethod($type) + ->setProtocol('1.1'); + + return $this; + } + + /** + * Create the full request url + * + * @return void + */ + private function buildUri() + { + $url = (strpos($this->path, '//') !== FALSE) + ? $this->path + : $this->baseUrl . $url; + + if ( ! empty($this->query)) + { + $url .= '?' . $this->query; + } + + $this->request->setUri($url); + } + + /** + * Unencode the dual-encoded ampersands in the body + * + * This is a dirty hack until I can fully track down where + * the dual-encoding happens + * + * @param FormBody $formBody The form builder object to fix + * @return string + */ + private function fixBody(FormBody $formBody): string + { + $rawBody = \Amp\wait($formBody->getBody()); + return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8'); + } + + /** + * Reset the class state for a new request + * + * @return void + */ + private function resetState() + { + $this->path = ''; + $this->query = ''; + $this->request = new Request(); + } +} \ No newline at end of file diff --git a/src/API/Kitsu/KitsuRequestBuilder.php b/src/API/Kitsu/KitsuRequestBuilder.php new file mode 100644 index 00000000..c155794a --- /dev/null +++ b/src/API/Kitsu/KitsuRequestBuilder.php @@ -0,0 +1,55 @@ + + * @copyright 2015 - 2017 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.0 + * @link https://github.com/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\API\Kitsu; + +use Aviat\AnimeClient\API\APIRequestBuilder; +use Aviat\AnimeClient\API\Kitsu as K; +use Aviat\Ion\Json; + +class KitsuRequestBuilder extends APIRequestBuilder { + + /** + * The base url for api requests + * @var string $base_url + */ + protected $baseUrl = "https://kitsu.io/api/edge/"; + + /** + * HTTP headers to send with every request + * + * @var array + */ + protected $defaultHeaders = [ + 'User-Agent' => "Tim's Anime Client/4.0", + 'Accept-Encoding' => 'application/vnd.api+json', + 'Content-Type' => 'application/vnd.api+json', + 'client_id' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd', + 'client_secret' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151', + ]; + + /** + * Set the request body + * + * @param array|FormBody|string $body + * @return self + */ + public function setJsonBody(array $body): self + { + $requestBody = Json::encode($body); + return $this->setBody($requestBody); + } +} diff --git a/src/API/Kitsu/KitsuTrait.php b/src/API/Kitsu/KitsuTrait.php index 79d219af..148c35c5 100644 --- a/src/API/Kitsu/KitsuTrait.php +++ b/src/API/Kitsu/KitsuTrait.php @@ -27,7 +27,24 @@ use InvalidArgumentException; use RuntimeException; trait KitsuTrait { - use GuzzleTrait; + + /** + * The request builder for the MAL API + * @var MALRequestBuilder + */ + protected $requestBuilder; + + /** + * The Guzzle http client object + * @var object + */ + protected $client; + + /** + * Cookie jar object for api requests + * @var object + */ + protected $cookieJar; /** * The base url for api requests @@ -47,6 +64,18 @@ trait KitsuTrait { 'client_id' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd', 'client_secret' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151', ]; + + /** + * Set the request builder object + * + * @param KitsuRequestBuilder $requestBuilder + * @return self + */ + public function setRequestBuilder($requestBuilder): self + { + $this->requestBuilder = $requestBuilder; + return $this; + } /** * Set up the class properties @@ -93,7 +122,7 @@ trait KitsuTrait { 'headers' => $this->defaultHeaders ]; - $logger = $this->container->getLogger('kitsu_request'); + $logger = $this->container->getLogger('kitsu-request'); $sessionSegment = $this->getContainer() ->get('session') ->getSegment(AnimeClient::SESSION_SEGMENT); @@ -134,7 +163,7 @@ trait KitsuTrait { $logger = null; if ($this->getContainer()) { - $logger = $this->container->getLogger('kitsu_request'); + $logger = $this->container->getLogger('kitsu-request'); } $response = $this->getResponse($type, $url, $options); @@ -183,7 +212,7 @@ trait KitsuTrait { $logger = null; if ($this->getContainer()) { - $logger = $this->container->getLogger('kitsu_request'); + $logger = $this->container->getLogger('kitsu-request'); } $response = $this->getResponse('POST', ...$args); diff --git a/src/API/MAL/MALRequestBuilder.php b/src/API/MAL/MALRequestBuilder.php new file mode 100644 index 00000000..ca0d88b1 --- /dev/null +++ b/src/API/MAL/MALRequestBuilder.php @@ -0,0 +1,50 @@ + + * @copyright 2015 - 2017 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.0 + * @link https://github.com/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\API\MAL; + +use Aviat\AnimeClient\API\{ + APIRequestBuilder, + MAL as M, + XML +}; + +class MALRequestBuilder extends APIRequestBuilder { + + /** + * The base url for api requests + * @var string $base_url + */ + protected $baseUrl = M::BASE_URL; + + /** + * HTTP headers to send with every request + * + * @var array + */ + protected $defaultHeaders = [ + 'Accept' => 'text/xml', + 'Accept-Encoding' => 'gzip', + 'Content-type' => 'application/x-www-form-urlencoded', + 'User-Agent' => "Tim's Anime Client/4.0" + ]; + + /** + * Valid HTTP request methos + * @var array + */ + protected $validMethods = ['GET', 'POST', 'DELETE']; +} \ No newline at end of file diff --git a/src/API/MAL/MALTrait.php b/src/API/MAL/MALTrait.php index 15ac7757..5d8e84dc 100644 --- a/src/API/MAL/MALTrait.php +++ b/src/API/MAL/MALTrait.php @@ -19,12 +19,20 @@ namespace Aviat\AnimeClient\API\MAL; use Amp\Artax\{Client, FormBody, Request}; use Aviat\AnimeClient\API\{ MAL as M, + APIRequestBuilder, XML }; +use Aviat\AnimeClient\API\MALRequestBuilder; use Aviat\Ion\Json; use InvalidArgumentException; trait MALTrait { + + /** + * The request builder for the MAL API + * @var MALRequestBuilder + */ + protected $requestBuilder; /** * The base url for api requests @@ -44,6 +52,18 @@ trait MALTrait { 'User-Agent' => "Tim's Anime Client/4.0" ]; + /** + * Set the request builder object + * + * @param MALRequestBuilder $requestBuilder + * @return self + */ + public function setRequestBuilder($requestBuilder): self + { + $this->requestBuilder = $requestBuilder; + return $this; + } + /** * Unencode the dual-encoded ampersands in the body * @@ -58,16 +78,16 @@ trait MALTrait { $rawBody = \Amp\wait($formBody->getBody()); return html_entity_decode($rawBody, \ENT_HTML5, 'UTF-8'); } - + /** - * Make a request via Guzzle + * Create a request object * * @param string $type * @param string $url * @param array $options - * @return Response + * @return \Amp\Promise */ - private function getResponse(string $type, string $url, array $options = []) + public function setUpRequest(string $type, string $url, array $options = []) { $this->defaultHeaders['User-Agent'] = $_SERVER['HTTP_USER_AGENT'] ?? $this->defaultHeaders; @@ -80,7 +100,7 @@ trait MALTrait { } $config = $this->container->get('config'); - $logger = $this->container->getLogger('mal_request'); + $logger = $this->container->getLogger('mal-request'); $headers = array_merge($this->defaultHeaders, $options['headers'] ?? [], [ 'Authorization' => 'Basic ' . @@ -108,7 +128,27 @@ trait MALTrait { { $request->setBody($options['body']); } + + return $request; + } + /** + * Make a request + * + * @param string $type + * @param string $url + * @param array $options + * @return \Amp\Artax\Response + */ + private function getResponse(string $type, string $url, array $options = []) + { + $logger = null; + if ($this->getContainer()) + { + $logger = $this->container->getLogger('mal-request'); + } + + $request = $this->setUpRequest($type, $url, $options); $response = \Amp\wait((new Client)->request($request)); $logger->debug('MAL api request', [ @@ -126,7 +166,7 @@ trait MALTrait { } /** - * Make a request via Guzzle + * Make a request * * @param string $type * @param string $url @@ -138,7 +178,7 @@ trait MALTrait { $logger = null; if ($this->getContainer()) { - $logger = $this->container->getLogger('mal_request'); + $logger = $this->container->getLogger('mal-request'); } $response = $this->getResponse($type, $url, $options); @@ -176,7 +216,7 @@ trait MALTrait { $logger = null; if ($this->getContainer()) { - $logger = $this->container->getLogger('mal_request'); + $logger = $this->container->getLogger('mal-request'); } $response = $this->getResponse('POST', ...$args);