diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4e56fac --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - hhvm + +before_script: + - composer install --dev --prefer-source + +script: ./vendor/bin/phpspec run diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6412e25 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +1.0.0 (2014-09-XX) +================== + +New awesome features +-------------------- + +* Drop Guzzle dependency +* Added dependency on Symfony Event Dispatcher Component +* Added specificity tests with phpspec +* Added cache strategy support +* Added transformer strategies +* Made the ApiFactory "IDE friendly" + +Compatibility breaks: +--------------------- + +Almost everything, sorry guys this is a new, very very major version :-). diff --git a/Readme.md b/Readme.md index 8113332..a1bbb3e 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,8 @@ Nekland Base API ================ +[![Build Status](https://travis-ci.org/Nekland/BaseApi.svg)](https://travis-ci.org/Nekland/BaseApi) + Why --- @@ -8,9 +10,16 @@ I made this project because I created a lib for Youtube API and another for Soun Both libs have the same needs. To avoid duplicated code, I made this little project that can be used as a base for the API lib you want to create. -Inspiration ------------ +How +--- + +[x] [Semver](http://semver.org) compliant +[x] [HHVM](http://hhvm.com/) compatible +[x] [Composer](http://packagist) installable + +Documentation +------------- -This project look like php-github-api, and it's not innocent. I used it as model. +This project does not need so much documentation but I wrote some for interested people. Checkout the [doc](doc) folder. -Thanks to KnpLabs & Contributors on this project. \ No newline at end of file +> This project is inspirated by KNPLabs api libs. diff --git a/composer.json b/composer.json index 49cb89a..5057cec 100644 --- a/composer.json +++ b/composer.json @@ -7,20 +7,35 @@ "authors": [ { "name": "Maxime Veber", - "email": "nekland@gmail.com", + "email": "nek.dev@gmail.com", "homepage": "http://nekland.fr" + }, + { + "name": "Nekland Team", + "email": "team@nekland.fr", + "homepage": "http://team.nekland.fr" } ], + "require": { + "php": ">=5.4", + "symfony/event-dispatcher": "~2.3" + }, + "require-dev": { + "phpspec/phpspec": "dev-master", + "guzzlehttp/guzzle": "~4.2" + }, "suggest": { "nekland/youtube-api": "Youtube API made easy !", "nekland/soundcloud-api": "Soundcloud API made easy !", - "guzzle/guzzle": "The default HttpClient will not work without this library." + "guzzlehttp/guzzle": "The only http adapter available for now is for this library." }, "autoload": { "psr-0": { "Nekland\\": "lib/" } }, - "require-dev": { - "php": ">=5.4", - "phpunit/phpunit": ">=3.7" + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } } -} \ No newline at end of file +} diff --git a/doc/api_classes.md b/doc/api_classes.md new file mode 100644 index 0000000..91ece8e --- /dev/null +++ b/doc/api_classes.md @@ -0,0 +1,13 @@ +Api classes +=========== + + +They should simply extends the `AbstractApi` class. + +She provide you useful methods (get/put/post/delete rest methods) and as the class do some job, you just have to create methods like: + +* getResourceById +* deleteResource + + +> You have to register a namespace to your api objects in the api factory diff --git a/doc/api_factory.md b/doc/api_factory.md new file mode 100644 index 0000000..7868b6f --- /dev/null +++ b/doc/api_factory.md @@ -0,0 +1,18 @@ +The ApiFactory +============== + +This class instantiate "[Api classes](api_classes.md)" when you use it like that thanks to the magic method `call`: + +```php +getMyAwesomeApi(); +``` + +So the first thing you have to do when building your API is to extend it and implement the only needed method. + +You can also: + +* Redefine the `getTransformer` method to change the default one. + +Now, build your [API classes](api_classes.md). diff --git a/doc/auth_and_cache.md b/doc/auth_and_cache.md new file mode 100644 index 0000000..433b458 --- /dev/null +++ b/doc/auth_and_cache.md @@ -0,0 +1,21 @@ +Auth & Cache strategies +======================= + +In order to help you manage authentication or cache systems, this lib provides you useful strategies managements. + +> Since theses classes are register as event listener (it use the event dispatcher of Symfony), you can add multiple strategies of each feature. + +The cache strategy +================== + +This are classes that implements the `CacheStrategyInterface`. + +You register them using the method `useCache` of the ApiFactory. + + +The authentication strategy +=========================== + +This are classes implementing the `AuthenticationStrategyInterface`. + +You register them using the method `useAuthentication` of the ApiFactory diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000..761643c --- /dev/null +++ b/doc/index.md @@ -0,0 +1,11 @@ +Nekland base api +================ + +The idea is to made easy build of new PHP api libs. So this lib provide you some abstract class that will work together with other classes. + +You will learn more about things in the dedicated pages of doc: + +* [ApiFactory](api_factory.md) +* [Api classes](api_classes.md) +* [Auth and Cache strategies](auth_and_cache.md) +* [Transformers](transformers.md) diff --git a/doc/transformers.md b/doc/transformers.md new file mode 100644 index 0000000..812c46d --- /dev/null +++ b/doc/transformers.md @@ -0,0 +1,8 @@ +Transformers +============ + +Transformers are classes implementing the `TransformerInterface` interface. + +You can set them by using the `setTransformer` of the `ApiFactory` class. + +> You can only set one transformer because you can't retrieve the same data in many forms in the same request. diff --git a/lib/Nekland/BaseApi/Api.php b/lib/Nekland/BaseApi/Api.php deleted file mode 100644 index 2ecc674..0000000 --- a/lib/Nekland/BaseApi/Api.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * For the full license, take a look to the LICENSE file - * on the root directory of this project - */ - -namespace Nekland\BaseApi; - - -use Nekland\BaseApi\Http\ClientInterface; - -abstract class Api implements ApiInterface -{ - /** - * @var ClientInterface - */ - private $client; - - public function __construct(ClientInterface $httpClient) - { - $this->client = $httpClient; - } - - /** - * @param string $name - * @return \Nekland\BaseApi\Api\AbstractApi - */ - abstract public function api($name); - - /*** - * @param string $method - * @param array $options - */ - public function authenticate($method, array $options) - { - $this->client->authenticate($method, $options); - } - - /** - * @return ClientInterface - */ - public function getClient() - { - return $this->client; - } -} diff --git a/lib/Nekland/BaseApi/Api/AbstractApi.php b/lib/Nekland/BaseApi/Api/AbstractApi.php index 2f45db4..c68666a 100644 --- a/lib/Nekland/BaseApi/Api/AbstractApi.php +++ b/lib/Nekland/BaseApi/Api/AbstractApi.php @@ -11,20 +11,118 @@ namespace Nekland\BaseApi\Api; - use Nekland\BaseApi\Api; +use Nekland\BaseApi\Http\AbstractHttpClient; +use Nekland\BaseApi\Transformer\JsonTransformer; +use Nekland\BaseApi\Transformer\TransformerInterface; abstract class AbstractApi { - protected $api; + /** + * @var AbstractHttpClient + */ + private $client; + + /** + * @var TransformerInterface + */ + private $transformer; + + public function __construct(AbstractHttpClient $client, TransformerInterface $transformer = null) { + $this->client = $client; + $this->transformer = $transformer ?: new JsonTransformer(); + } + + /** + * Set the transformer that will be used to return data + * + * @param TransformerInterface $transformer + * @return self + */ + public function setTransformer(TransformerInterface $transformer) + { + $this->transformer = $transformer; + + return $this; + } + + /** + * Execute a http get query + * + * @param string $path + * @param array $body + * @param array $headers + * @return array|mixed + */ + protected function get($path, array $body = [], array $headers = []) + { + $client = $this->getClient(); + $request = $client::createRequest('GET', $path, $body, $headers); + + return $this->transformer->transform($client->send($request)); + } + + /** + * Execute a http put query + * + * @param string $path + * @param array $body + * @param array $headers + * @return array|mixed + */ + protected function put($path, array $body = [], array $headers = []) + { + $client = $this->getClient(); + $request = $client::createRequest('PUT', $path, $body, $headers); + + return $this->transformer->transform($client->send($request)); + } + + /** + * Execute a http post query + * + * @param string $path + * @param array $body + * @param array $headers + * @return array|mixed + */ + protected function post($path, array $body = [], array $headers = []) + { + $client = $this->getClient(); + $request = $client::createRequest('POST', $path, $body, $headers); + + return $this->transformer->transform($client->send($request)); + } + + /** + * Execute a http delete query + * + * @param string $path + * @param array $body + * @param array $headers + * @return array|mixed + */ + protected function delete($path, array $body = [], array $headers = []) + { + $client = $this->getClient(); + $request = $client::createRequest('DELETE', $path, $body, $headers); + + return $this->transformer->transform($client->send($request)); + } - public function __construct(Api $api) + /** + * @return AbstractHttpClient + */ + protected function getClient() { - $this->api = $api; + return $this->client; } - protected function get($path, array $parameters = [], array $requestHeaders = []) + /** + * @return TransformerInterface + */ + protected function getTransformer() { - return json_decode((string) $this->api->getClient()->get($path, $parameters, $requestHeaders), true); + return $this->transformer; } } diff --git a/lib/Nekland/BaseApi/ApiFactory.php b/lib/Nekland/BaseApi/ApiFactory.php new file mode 100644 index 0000000..7629845 --- /dev/null +++ b/lib/Nekland/BaseApi/ApiFactory.php @@ -0,0 +1,218 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi; + +use Nekland\BaseApi\Api\AbstractApi; +use Nekland\BaseApi\Cache\CacheFactory; +use Nekland\BaseApi\Cache\CacheStrategyInterface; +use Nekland\BaseApi\Exception\MissingApiException; + +use Nekland\BaseApi\Http\Auth\AuthFactory; +use Nekland\BaseApi\Http\Auth\AuthStrategyInterface; +use Nekland\BaseApi\Http\Event\Events; +use Nekland\BaseApi\Http\HttpClientFactory; +use Nekland\BaseApi\Transformer\JsonTransformer; +use Nekland\BaseApi\Transformer\TransformerInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; + +abstract class ApiFactory +{ + /** + * @var HttpClientFactory + */ + private $clientFactory; + + /** + * @var Http\Auth\AuthFactory + */ + private $authFactory; + + /** + * @var TransformerInterface + */ + private $transformer; + + /** + * @var EventDispatcher + */ + private $dispatcher; + + /** + * @var CacheFactory + */ + private $cacheFactory; + + public function __construct( + HttpClientFactory $httpClientFactory = null, + EventDispatcher $dispatcher = null, + TransformerInterface $transformer = null, + AuthFactory $authFactory = null, + CacheFactory $cacheFactory = null + ) { + if ($httpClientFactory !== null) { + $this->clientFactory = $httpClientFactory; + $this->dispatcher = $dispatcher ?: $httpClientFactory->getEventDispatcher(); + } else { + $this->dispatcher = $dispatcher ?: new EventDispatcher(); + $this->clientFactory = new HttpClientFactory($this->dispatcher); + } + + $this->authFactory = $authFactory; + $this->cacheFactory = $cacheFactory ?: new CacheFactory(); + $this->transformer = $transformer; + } + + /** + * Allow the user to add an authentication to the request + * + * @param string|AuthStrategyInterface $auth + * @param array $options + */ + public function useAuthentication($auth, array $options = []) + { + if (!($auth instanceof AuthStrategyInterface)) { + $auth = $this->getAuthFactory()->get($auth); + $auth->setOptions($options); + } + + $this->dispatcher->addListener(Events::ON_REQUEST_EVENT, [ + $auth, + 'auth' + ]); + } + + /** + * @param CacheStrategyInterface|string $cacheStrategy + * @param \Nekland\BaseApi\Cache\Provider\CacheProviderInterface|string $cacheProvider + * @param array $options + */ + public function useCache($cacheStrategy, $cacheProvider = null, array $options = null) + { + $cache = $this->getCacheFactory()->createCacheStrategy($cacheStrategy, $cacheProvider, $options); + $this->dispatcher->addListener( + Events::ON_REQUEST_EVENT, + [ $cache, 'execute' ] + ); + $this->dispatcher->addListener( + Events::AFTER_REQUEST_EVENT, + [ $cache, 'cache' ] + ); + } + + /** + * @return \Nekland\BaseApi\Http\AbstractHttpClient + */ + public function getClient() + { + return $this->clientFactory->createHttpClient(); + } + + /** + * @return HttpClientFactory + */ + public function getHttpClientFactory() + { + return $this->clientFactory; + } + + /** + * @param HttpClientFactory $clientFactory + * @return self + */ + public function setHttpClientFactory(HttpClientFactory $clientFactory) + { + $this->clientFactory = $clientFactory; + + return $this; + } + + /** + * @param TransformerInterface $transformer + */ + public function setTransformer(TransformerInterface $transformer) + { + $this->transformer = $transformer; + } + + /** + * @param string $name + * @param array $parameters + * @return AbstractApi + * @throws \RuntimeException|MissingApiException|\BadMethodCallException + */ + public function __call($name, $parameters) + { + if ($this->isApiMethod($name)) { + $apiName = str_replace(['get', 'Api'], '', str_replace('Api', '', $name)); + + foreach ($this->getApiNamespaces() as $namespace) { + $class = $namespace . '\\' . $apiName; + if (class_exists($class)) { + $api = new $class($this->getClient(), $this->getTransformer()); + + if ($api instanceof AbstractApi) { + return $api; + } + + throw new \RuntimeException( + sprintf('The API %s is found but does not implements AbstractApi.', $apiName) + ); + } + } + + throw new MissingApiException($apiName); + } + + throw new \BadMethodCallException(sprintf('The method %s does not exists.', $name)); + } + + /** + * @param string $name + * @return bool + */ + protected function isApiMethod($name) + { + return (bool) preg_match('/^get[A-Z][a-zA-Z]*Api$/', $name); + } + + /** + * @return AuthFactory + */ + public function getAuthFactory() + { + return $this->authFactory ?: $this->authFactory = new AuthFactory($this->getClient()); + } + + /** + * @return CacheFactory + */ + public function getCacheFactory() + { + return $this->cacheFactory; + } + + /** + * @return TransformerInterface + */ + protected function getTransformer() + { + return $this->transformer ?: new JsonTransformer(); + } + + /** + * Return array of namespaces where AbstractApi instance are localized + * + * + * @return string[] Example: ['Nekland\BaseApi\Api'] + */ + abstract protected function getApiNamespaces(); +} diff --git a/lib/Nekland/BaseApi/Cache/CacheFactory.php b/lib/Nekland/BaseApi/Cache/CacheFactory.php new file mode 100644 index 0000000..749f715 --- /dev/null +++ b/lib/Nekland/BaseApi/Cache/CacheFactory.php @@ -0,0 +1,90 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Cache; + + +use Nekland\BaseApi\Cache\Provider\CacheProviderInterface; +use Nekland\BaseApi\Cache\Provider\FileProvider; + +class CacheFactory +{ + /** + * @var array + */ + private $namespaces; + + public function __construct($namespaces = null) + { + $this->namespaces = $namespaces ?: []; + } + + /** + * @param string[CacheStrategyInterface $cacheStrategy + * @param string[CacheProviderInterface $cacheProvider + * @param array $options + * @return CacheStrategyInterface + * @throws \RuntimeException + */ + public function createCacheStrategy($cacheStrategy, $cacheProvider = null, array $options = []) + { + $provider = $this->createProvider($cacheProvider, $options); + + foreach ($this->namespaces as $namespace) { + $class = $namespace . '\\' . $cacheStrategy; + if (class_exists($class)) { + $class = new $class(); + $class->setProvider($provider); + + return $class; + } + } + + throw new \RuntimeException(sprintf('Impossible to find a cache strategy named %s', $cacheStrategy)); + } + + /** + * @param string|CacheProviderInterface $provider + * @param array $options + * @return CacheProviderInterface + * @throws \RuntimeException + */ + public function createProvider($provider, array $options = null) + { + if ($provider === null) { + return (new FileProvider())->setOptions($options); + } + + if (is_object($provider)) { + if (!($provider instanceof CacheProviderInterface)) { + throw new \RuntimeException( + 'The cache provider must implements Nekland\BaseApi\Cache\Provider\CacheProviderInterface' + ); + } + $provider->setOptions($options); + + return $provider; + } + + foreach ($this->namespaces as $namespace) { + if ( + class_exists($class = $namespace . '\\' . $provider) || + class_exists($class = $namespace . '\\Provider\\' . $provider) + ) { + $provider = new $class; + + return $provider; + } + } + + throw new \RuntimeException(sprintf('Impossible to find the provider %s', $provider)); + } +} diff --git a/lib/Nekland/BaseApi/Cache/CacheStrategyInterface.php b/lib/Nekland/BaseApi/Cache/CacheStrategyInterface.php new file mode 100644 index 0000000..5b6d9a3 --- /dev/null +++ b/lib/Nekland/BaseApi/Cache/CacheStrategyInterface.php @@ -0,0 +1,34 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Cache; + + +use Nekland\BaseApi\Cache\Provider\CacheProviderInterface; +use Nekland\BaseApi\Http\Event\RequestEvent; + +interface CacheStrategyInterface +{ + /** + * @param RequestEvent $event + */ + public function execute(RequestEvent $event); + + /** + * @param RequestEvent $event + */ + public function cache(RequestEvent $event); + + /** + * @param CacheProviderInterface $provider + */ + public function setProvider(CacheProviderInterface $provider); +} diff --git a/lib/Nekland/BaseApi/Cache/Provider/CacheProviderInterface.php b/lib/Nekland/BaseApi/Cache/Provider/CacheProviderInterface.php new file mode 100644 index 0000000..09f8e16 --- /dev/null +++ b/lib/Nekland/BaseApi/Cache/Provider/CacheProviderInterface.php @@ -0,0 +1,42 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Cache\Provider; + + +interface CacheProviderInterface +{ + /** + * Loads the cache + */ + public function load(); + + /** + * Saves the cache + */ + public function save(); + + /** + * @param string $key + */ + public function get($key); + + /** + * @param string $key + * @param mixed $value + */ + public function set($key, $value); + + /** + * @param array $options + */ + public function setOptions(array $options); +} diff --git a/lib/Nekland/BaseApi/Cache/Provider/FileProvider.php b/lib/Nekland/BaseApi/Cache/Provider/FileProvider.php new file mode 100644 index 0000000..7031a1e --- /dev/null +++ b/lib/Nekland/BaseApi/Cache/Provider/FileProvider.php @@ -0,0 +1,86 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Cache\Provider; + + +class FileProvider implements CacheProviderInterface +{ + /** + * @var array + */ + private $options; + + /** + * @var array + */ + private $cache; + + public function setOptions(array $options) + { + $this->options = array_merge($this->getOptions(), $options); + + return $this; + } + + public function load() + { + $path = $this->getPath(); + if (!is_file($path)) { + @file_put_contents($path, serialize([])); + } + + $this->cache = unserialize(file_get_contents($this->getPath())); + } + + public function save() + { + file_put_contents($this->getPath(), serialize($this->cache)); + } + + public function get($key) + { + if (!isset($this->cache[$key])) { + return null; + } + + return $this->cache[$key]; + } + + public function set($key, $value) + { + $this->cache[$key] = $value; + + return $this; + } + + /** + * @return string + */ + protected function getPath() + { + return $this->options['path']; + } + + protected function getOptions() + { + return $this->options ?: [ + 'path' => sys_get_temp_dir() . '/nekland_api_cache_file' + ]; + } + + public function __destruct() + { + if (null !== $this->getPath()) { + $this->save(); + } + } +} diff --git a/lib/Nekland/BaseApi/Exception/MissingApiException.php b/lib/Nekland/BaseApi/Exception/MissingApiException.php new file mode 100644 index 0000000..b734135 --- /dev/null +++ b/lib/Nekland/BaseApi/Exception/MissingApiException.php @@ -0,0 +1,22 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Exception; + +class MissingApiException extends \Exception +{ + public function __construct($name, $message = '', $code = 0, \Exception $previous = null) + { + parent::__construct( + sprintf('The api "%s" does not exists, please check the docs.%s', $name, $message, $code, $previous) + ); + } +} diff --git a/lib/Nekland/BaseApi/ApiInterface.php b/lib/Nekland/BaseApi/Exception/MissingOptionException.php similarity index 53% rename from lib/Nekland/BaseApi/ApiInterface.php rename to lib/Nekland/BaseApi/Exception/MissingOptionException.php index 2bc5916..3d035f9 100644 --- a/lib/Nekland/BaseApi/ApiInterface.php +++ b/lib/Nekland/BaseApi/Exception/MissingOptionException.php @@ -9,16 +9,10 @@ * on the root directory of this project */ -namespace Nekland\BaseApi; +namespace Nekland\BaseApi\Exception; -interface ApiInterface +class MissingOptionException extends \Exception { - /** - * Return an api object - * - * @param string $name - * @return mixed - */ - public function api($name); -} \ No newline at end of file + +} diff --git a/lib/Nekland/BaseApi/Http/AbstractHttpClient.php b/lib/Nekland/BaseApi/Http/AbstractHttpClient.php new file mode 100644 index 0000000..e7accc5 --- /dev/null +++ b/lib/Nekland/BaseApi/Http/AbstractHttpClient.php @@ -0,0 +1,141 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Http; + + +use Nekland\BaseApi\Http\Event\Events; +use Nekland\BaseApi\Http\Event\RequestEvent; +use Nekland\BaseApi\Http\Request; +use Symfony\Component\EventDispatcher\EventDispatcher; + +abstract class AbstractHttpClient +{ + /** + * @var mixed[] + */ + private $options = [ + 'base_url' => '', + 'user_agent' => 'php-base-api (https://github.com/Nekland/BaseApi)' + ]; + + /** + * @var EventDispatcher + */ + private $dispatcher; + + public function __construct(EventDispatcher $eventDispatcher, array $options = []) + { + $this->dispatcher = $eventDispatcher; + $this->options = array_merge($this->options, $options); + } + + /** + * @param Request $request + * @param bool $withEvent if an event send a request, in order to avoid infinite while + * @return string + * @throws \BadMethodCallException + */ + public function send(Request $request, $withEvent = true) + { + $method = $request->getMethod(); + + if (!in_array($method, ['get', 'put', 'post', 'delete'])) { + throw new \BadMethodCallException(sprintf( + 'The http method "%s" does not exists or is not supported.', + $method + )); + } + + $event = new RequestEvent($request, $this); + if ($withEvent) { + $event = $this->getEventDispatcher()->dispatch(Events::ON_REQUEST_EVENT, $event); + } + + if (!$event->requestCompleted()) { + $res = $this->execute($request); + $event->setResponse($res); + } + + if ($withEvent) { + $this->getEventDispatcher()->dispatch(Events::AFTER_REQUEST_EVENT, $event); + } + + return (string) $event->getResponse(); + } + + /** + * Execute a request + * + * @param Request $request + * @return Response + */ + abstract protected function execute(Request $request); + + /** + * Complete headers using options + * + * @param array $headers + * @return array + */ + protected function getHeaders(array $headers = []) + { + return array_merge(['User-Agent' => $this->options['user_agent']], $headers); + } + + /** + * Generate a complete URL using the option "base_url" + * + * @param string $path The api uri + * @return string + */ + protected function getPath($path) + { + $hasHttp = strpos($path, 'http'); + if (false === $hasHttp || $hasHttp !== 0) { + return $this->options['base_url'] . $path; + } + + return $path; + } + + /** + * Generate a request object + * + * @param string $method + * @param string $path + * @param array $parameters + * @param array $headers + * @return Request + */ + public static function createRequest($method, $path, array $parameters = [], array $headers = []) + { + return new Request($method, $path, $parameters, $headers); + } + + /** + * @param string $body + * @param array $headers + * @return Response + */ + public static function createResponse($body, array $headers) + { + return new Response($body, $headers); + } + + /** + * @return EventDispatcher + */ + protected function getEventDispatcher() + { + return $this->dispatcher; + } +} diff --git a/lib/Nekland/BaseApi/Http/Auth/AuthFactory.php b/lib/Nekland/BaseApi/Http/Auth/AuthFactory.php index d6360ff..9f1ea01 100644 --- a/lib/Nekland/BaseApi/Http/Auth/AuthFactory.php +++ b/lib/Nekland/BaseApi/Http/Auth/AuthFactory.php @@ -11,6 +11,7 @@ namespace Nekland\BaseApi\Http\Auth; +use Nekland\BaseApi\Http\AbstractHttpClient; class AuthFactory { @@ -28,13 +29,13 @@ public function __construct() { $this->authentications = []; $this->namespaces = [ - 'Nekland\\BaseApi\\Http\\Auth\\' + 'Nekland\\BaseApi\\Http\\Auth' ]; } /** * @param string $authName - * @return AuthInterface + * @return AuthStrategyInterface * @throws \RuntimeException */ public function get($authName) @@ -47,11 +48,14 @@ public function get($authName) $authClass = $namespace . '\\' . $authName; if (class_exists($authClass)) { - return new $authClass(); + $auth = new $authClass(); + return $this->authentications[$authName] = $auth; } } - throw new \RuntimeException('The method "'.$authName.'" is not supported for authentication.'); + throw new \RuntimeException( + sprintf('The method "%s" is not supported for authentication.', $authName) + ); } /** diff --git a/lib/Nekland/BaseApi/Http/Auth/AuthListener.php b/lib/Nekland/BaseApi/Http/Auth/AuthListener.php deleted file mode 100644 index 18ec3df..0000000 --- a/lib/Nekland/BaseApi/Http/Auth/AuthListener.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full license, take a look to the LICENSE file - * on the root directory of this project - */ - -namespace Nekland\BaseApi\Http\Auth; - -use Guzzle\Common\Event; - -class AuthListener -{ - /** - * @var AuthInterface - */ - private $auth; - - public function __construct(AuthInterface $auth) - { - $this->auth = $auth; - } - - public function onRequestBeforeSend(Event $event) - { - if (null === $this->auth) { - return; - } - - $this->auth->auth($event['request']); - } -} diff --git a/lib/Nekland/BaseApi/Http/Auth/AuthInterface.php b/lib/Nekland/BaseApi/Http/Auth/AuthStrategyInterface.php similarity index 70% rename from lib/Nekland/BaseApi/Http/Auth/AuthInterface.php rename to lib/Nekland/BaseApi/Http/Auth/AuthStrategyInterface.php index ead792b..bbadc64 100644 --- a/lib/Nekland/BaseApi/Http/Auth/AuthInterface.php +++ b/lib/Nekland/BaseApi/Http/Auth/AuthStrategyInterface.php @@ -11,8 +11,9 @@ namespace Nekland\BaseApi\Http\Auth; +use Nekland\BaseApi\Http\Event\RequestEvent; -interface AuthInterface +interface AuthStrategyInterface { /** * @param array $options @@ -21,7 +22,7 @@ interface AuthInterface public function setOptions(array $options); /** - * @param \Guzzle\Http\Message\Request $request + * @param RequestEvent $request */ - public function auth(\Guzzle\Http\Message\Request $request); + public function auth(RequestEvent $request); } diff --git a/lib/Nekland/BaseApi/Http/ClientAdapter/GuzzleAdapter.php b/lib/Nekland/BaseApi/Http/ClientAdapter/GuzzleAdapter.php new file mode 100644 index 0000000..9c5b2c6 --- /dev/null +++ b/lib/Nekland/BaseApi/Http/ClientAdapter/GuzzleAdapter.php @@ -0,0 +1,43 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Http\ClientAdapter; + +use GuzzleHttp\Client; +use Nekland\BaseApi\Http\AbstractHttpClient; +use Nekland\BaseApi\Http\Request; +use Symfony\Component\EventDispatcher\EventDispatcher; + +class GuzzleAdapter extends AbstractHttpClient +{ + /** + * @var \GuzzleHttp\Client + */ + private $guzzle; + + public function __construct(EventDispatcher $dispatcher, array $options = [], Client $client = null) + { + parent::__construct($dispatcher, $options); + $this->guzzle = $client ?: new Client(); + } + + protected function execute(Request $request) + { + $method = $request->getMethod(); + + $response = $this->guzzle->$method($this->getPath($request->getUrl()), [ + 'headers' => $this->getHeaders($request->getHeaders()), + 'body' => $request->getBody() + ]); + + return self::createResponse($response->getBody(), $response->getHeaders()); + } +} diff --git a/lib/Nekland/BaseApi/Http/ClientInterface.php b/lib/Nekland/BaseApi/Http/Event/Events.php similarity index 52% rename from lib/Nekland/BaseApi/Http/ClientInterface.php rename to lib/Nekland/BaseApi/Http/Event/Events.php index e79f4e2..1a5a856 100644 --- a/lib/Nekland/BaseApi/Http/ClientInterface.php +++ b/lib/Nekland/BaseApi/Http/Event/Events.php @@ -9,12 +9,11 @@ * on the root directory of this project */ -namespace Nekland\BaseApi\Http; +namespace Nekland\BaseApi\Http\Event; -interface ClientInterface +class Events { - public function get($path, array $parameters = [], array $headers = []); - - public function authenticate($method, array $options); + const ON_REQUEST_EVENT = 'nekland_api.on_http_request'; + const AFTER_REQUEST_EVENT = 'nekland_api.after_http_request'; } diff --git a/lib/Nekland/BaseApi/Http/Event/RequestEvent.php b/lib/Nekland/BaseApi/Http/Event/RequestEvent.php new file mode 100644 index 0000000..5ec95db --- /dev/null +++ b/lib/Nekland/BaseApi/Http/Event/RequestEvent.php @@ -0,0 +1,82 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Http\Event; + + +use Nekland\BaseApi\Http\AbstractHttpClient; +use Nekland\BaseApi\Http\Request; +use Nekland\BaseApi\Http\Response; +use Symfony\Component\EventDispatcher\Event; + +class RequestEvent extends Event +{ + /** + * @var \Nekland\BaseApi\Http\Request + */ + private $request; + + /** + * @var Response + */ + private $response; + + /** + * @var \Nekland\BaseApi\Http\AbstractHttpClient + */ + private $client; + + public function __construct(Request $request, AbstractHttpClient $client) + { + $this->request = $request; + $this->client = $client; + } + + /** + * @return \Nekland\BaseApi\Http\Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * @param Response $response + */ + public function setResponse(Response $response) + { + $this->response = $response; + } + + /** + * @return bool + */ + public function requestCompleted() + { + return isset($this->response); + } + + /** + * @return Response + */ + public function getResponse() + { + return $this->response; + } + + /** + * @return AbstractHttpClient + */ + public function getClient() + { + return $this->client; + } +} diff --git a/lib/Nekland/BaseApi/Http/HttpClient.php b/lib/Nekland/BaseApi/Http/HttpClient.php deleted file mode 100644 index f253a94..0000000 --- a/lib/Nekland/BaseApi/Http/HttpClient.php +++ /dev/null @@ -1,165 +0,0 @@ - - * - * For the full license, take a look to the LICENSE file - * on the root directory of this project - */ - -namespace Nekland\BaseApi\Http; - -use Guzzle\Http\Client as GuzzleClient; -use Guzzle\Http\Exception\ServerErrorResponseException; -use Nekland\BaseApi\Http\Auth\AuthFactory; -use Nekland\BaseApi\Http\Auth\AuthListener; - -abstract class HttpClient implements ClientInterface -{ - /** - * @var array - */ - private $options = [ - 'base_url' => '', - 'user_agent' => 'php-base-api (https://github.com/Nekland/BaseApi)' - ]; - - /** - * @var array - */ - private $headers = []; - - /** - * @var \Guzzle\Http\Message\Request - */ - private $lastRequest; - - /** - * @var \Guzzle\Http\Message\Response - */ - private $lastResponse; - - /** - * @var AuthFactory - */ - private $authFactory; - - public function __construct(array $options = []) - { - $this->options = array_merge($this->options, $options); - $this->client = new GuzzleClient($this->options['base_url'], $this->options); - $this->authFactory = new AuthFactory(); - - $this->clearHeaders(); - } - - protected function clearHeaders() - { - $this->headers = [ - 'User-Agent' => $this->options['user_agent'] - ]; - } - - /** - * @param $path - * @param array $parameters - * @param array $headers - * @return \Guzzle\Http\Message\Response - */ - public function get($path, array $parameters = [], array $headers = []) - { - return $this->request($path, null, 'GET', $headers, array('query' => $parameters))->getBody(); - } - - - public function request($path, $body = null, $httpMethod = 'GET', array $headers = array(), array $options = array()) - { - $request = $this->createRequest($httpMethod, $path, $body, $headers, $options); - - $response = $this->client->send($request); - - $this->lastRequest = $request; - $this->lastResponse = $response; - - - return $response; - } - - /*** - * @param string $method - * @param array $options - */ - public function authenticate($method, array $options) - { - $auth = $this->authFactory->get($method); - $auth->setOptions($options); - - $this->addListener('request.before_send', array( - new AuthListener($auth), - 'onRequestBeforeSend' - )); - } - - /** - * Add an event on the http client - * - * @param $eventName - * @param $listener - */ - public function addListener($eventName, $listener) - { - $this->client->getEventDispatcher()->addListener($eventName, $listener); - } - - - protected function createRequest($httpMethod, $path, $body = null, array $headers = array(), array $options = array()) - { - return $this->client->createRequest( - $httpMethod, - $path, - array_merge($this->headers, $headers), - $body, - $options - ); - } - - /** - * @return mixed - */ - public function getLastRequest() - { - return $this->lastRequest; - } - - /** - * @return mixed - */ - public function getLastResponse() - { - return $this->lastResponse; - } - - /** - * @return AuthFactory - */ - public function getAuthFactory() - { - return $this->authFactory; - } - - /** - * @param string $name - * @param mixed $value - * - * @return HttpClient - */ - public function setOption($name, $value) - { - $this->options[$name] = $value; - - return $this; - } - -} diff --git a/lib/Nekland/BaseApi/Http/HttpClientAware.php b/lib/Nekland/BaseApi/Http/HttpClientAware.php new file mode 100644 index 0000000..f3c8800 --- /dev/null +++ b/lib/Nekland/BaseApi/Http/HttpClientAware.php @@ -0,0 +1,28 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Http; + +abstract class HttpClientAware +{ + /** + * @var AbstractHttpClient + */ + protected $httpClient; + + /** + * @param AbstractHttpClient $client + */ + public function setClient(AbstractHttpClient $client) + { + $this->httpClient = $client; + } +} diff --git a/lib/Nekland/BaseApi/Http/HttpClientFactory.php b/lib/Nekland/BaseApi/Http/HttpClientFactory.php new file mode 100644 index 0000000..e434646 --- /dev/null +++ b/lib/Nekland/BaseApi/Http/HttpClientFactory.php @@ -0,0 +1,148 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Http; + +use Nekland\BaseApi\Http\Auth\AuthFactory; +use Nekland\BaseApi\Http\Auth\AuthListener; +use Nekland\BaseApi\Http\ClientAdapter\GuzzleAdapter; +use Symfony\Component\EventDispatcher\EventDispatcher; + +class HttpClientFactory +{ + /** + * @var EventDispatcher + */ + private $dispatcher; + + /** + * @var array + */ + private $classes = [ + 'guzzle4' => [ + 'class' => 'Nekland\BaseApi\Http\ClientAdapter\GuzzleAdapter', + 'requirement' => 'GuzzleHttp\Client' + ] + ]; + + /** + * @var array + */ + private $options = []; + + public function __construct(array $options = [], EventDispatcher $eventDispatcher = null) + { + $this->options = array_merge($this->options, $options); + $this->dispatcher = $eventDispatcher ?: new EventDispatcher(); + } + + /** + * Generate the best http client according to the detected configuration + * + * @param string $name + * @param null $client + * @return GuzzleAdapter|AbstractHttpClient + * @throws \InvalidArgumentException + */ + public function createHttpClient($name = '', $client = null) + { + if (empty($name)) { + return $this->createBestClient(); + } + + if (!isset($this->classes[$name])) { + throw new \InvalidArgumentException(sprintf('The client "%s" is not registered.', $name)); + } + + $class = $this->classes[$name]['class']; + $client = new $class($this->dispatcher, $this->options, $client); + + if ($client instanceof AbstractHttpClient) { + return $client; + } + + throw new \InvalidArgumentException('The client must be an implementation of ClientInterface.'); + } + + /** + * @return AbstractHttpClient + * @throws \RuntimeException + */ + public function createBestClient() + { + foreach ($this->classes as $name => $definition) { + + if (is_callable($definition['requirement'])) { + if (!call_user_func($definition['requirement'])) { + continue; + } + } + if (!empty($definition['requirement'])) { + if (!class_exists($definition['requirement'])) { + continue; + } + } + $className = $definition['class']; + + return new $className($this->dispatcher, $this->options); + } + + throw new \RuntimeException('Impossible to find a Client class.'); + } + + /** + * Register a new client that will be able to be use by the ApiFactory + * + * @param string $name + * @param string $class + * @param string|callable $requirement + * @param bool $priority + * @throws \InvalidArgumentException + */ + public function register($name, $class, $requirement = '', $priority = false) + { + if (!class_exists($class)) { + throw new \InvalidArgumentException(sprintf('%s is not a valid class name.', $class)); + } + + $definition = [ + 'class' => $class, + 'requirement' => $requirement + ]; + + if ($priority) { + $this->classes = array_merge_recursive([$name => $definition], $this->classes); + } else { + $this->classes[$name] = $definition; + } + } + + /** + * @param string $name + * @param mixed $value + * + * @return self + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + + return $this; + } + + /** + * @return EventDispatcher + */ + public function getEventDispatcher() + { + return $this->dispatcher; + } +} diff --git a/lib/Nekland/BaseApi/Http/Request.php b/lib/Nekland/BaseApi/Http/Request.php new file mode 100644 index 0000000..d7dbe97 --- /dev/null +++ b/lib/Nekland/BaseApi/Http/Request.php @@ -0,0 +1,246 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Http; + + +class Request +{ + /** + * @var string + */ + private $method; + + /** + * @var string + */ + private $path; + + /** + * Body parameters + * used in PUT, POST and DELETE methods + * + * @var array + */ + private $body; + + /** + * @var array + */ + private $headers; + + /** + * URI parameters + * basically for GET requests + * + * @var array + */ + private $parameters; + + /** + * Type of request, nothing to do with http but useful for transformers + * + * @var null|string + */ + private $type; + + /** + * @param string $method + * @param string $path + * @param array $body if the method is GET it's taken as parameters + * @param array $headers + * @param string $type + */ + public function __construct($method, $path, array $body = [], array $headers = [], $type = null) + { + $this->method = $method; + $this->path = $path; + $this->headers = $headers; + $this->type = $type; + + if ($method === 'GET') { + $this->parameters = $body; + } else { + $this->body = $body; + } + } + + /** + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * @param string $name + * @return bool + */ + public function hasHeader($name) + { + return isset($this->headers[$name]); + } + + /** + * @return string + */ + public function getMethod() + { + return strtolower($this->method); + } + + /** + * @return array + */ + public function getBody() + { + return $this->body; + } + + /** + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * @return string + */ + public function getUrl() + { + $url = $this->path; + + if (!empty($this->parameters)) { + foreach ($this->parameters as $name => $value) { + $start = false === strpos($url, '?') ? '?' : '&'; + $url .= $start . $name . '=' . $value; + } + } + + return $url; + } + + /** + * @param array $body + * @return self + */ + public function setBody($body) + { + $this->body = $body; + return $this; + } + + /** + * @param array $headers + * @return self + */ + public function setHeaders($headers) + { + $this->headers = $headers; + return $this; + } + + /** + * @param string $name + * @param string $content + */ + public function setHeader($name, $content) + { + $this->headers[$name] = $content; + } + + /** + * @param string $method + * @return self + */ + public function setMethod($method) + { + $this->method = $method; + return $this; + } + + /** + * @param string $path + * @return self + */ + public function setPath($path) + { + $this->path = $path; + return $this; + } + + /** + * @param mixed[] $parameters + */ + public function setParameters(array $parameters) + { + $this->parameters = $parameters; + } + + /** + * @return array + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * @param string $name + * @param string $parameter + * @return self + */ + public function setParameter($name, $parameter) + { + $this->parameters[$name] = $parameter; + return $this; + } + + /** + * @param string $name + * @return string + */ + public function getParameter($name) + { + return $this->parameters[$name]; + } + + /** + * @param null|string $type + * @return self + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * Return an id uniq for this request with identicals parameters + * + * @return string + */ + public function getId() + { + return base64_encode($this->path . implode('', $this->parameters) . implode('', $this->body) . $this->method); + } + + /** + * @return null|string + */ + public function getType() + { + return $this->type; + } +} diff --git a/lib/Nekland/BaseApi/Http/Response.php b/lib/Nekland/BaseApi/Http/Response.php new file mode 100644 index 0000000..4898694 --- /dev/null +++ b/lib/Nekland/BaseApi/Http/Response.php @@ -0,0 +1,80 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Http; + + +class Response +{ + /** + * @var string + */ + private $body; + + /** + * @var array + */ + private $headers; + + /** + * @param string $body + * @param array $headers + */ + public function __construct($body, array $headers = []) + { + $this->body = $body; + $this->headers = $headers; + } + + /** + * @return string + */ + public function getBody() + { + return $this->body; + } + + /** + * @param string $body + * @return self + */ + public function setBody($body) + { + $this->body = $body; + return $this; + } + + /** + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * @param array $headers + * @return self + */ + public function setHeaders(array $headers) + { + $this->headers = $headers; + return $this; + } + + /** + * @return string + */ + public function __toString() + { + return (string) $this->body; + } +} diff --git a/lib/Nekland/BaseApi/Transformer/JsonTransformer.php b/lib/Nekland/BaseApi/Transformer/JsonTransformer.php new file mode 100644 index 0000000..2cddd6e --- /dev/null +++ b/lib/Nekland/BaseApi/Transformer/JsonTransformer.php @@ -0,0 +1,28 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Transformer; + + +class JsonTransformer implements TransformerInterface +{ + /** + * Depending on what formatter will be used, the data will be transform. + * + * @param string $data + * @param string $type Type of data that is sent + * @return array + */ + public function transform($data, $type = self::UNKNOWN) + { + return json_decode($data, true); + } +} \ No newline at end of file diff --git a/lib/Nekland/BaseApi/Transformer/TransformerInterface.php b/lib/Nekland/BaseApi/Transformer/TransformerInterface.php new file mode 100644 index 0000000..c1093fd --- /dev/null +++ b/lib/Nekland/BaseApi/Transformer/TransformerInterface.php @@ -0,0 +1,26 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace Nekland\BaseApi\Transformer; + +interface TransformerInterface +{ + const UNKNOWN = 'unknown'; + + /** + * Depending on what formatter will be used, the data will be transform. + * + * @param string $data + * @param string $type Type of data that is sent + * @return mixed + */ + public function transform($data, $type = self::UNKNOWN); +} diff --git a/spec/Nekland/BaseApi/Cache/Provider/FileProviderSpec.php b/spec/Nekland/BaseApi/Cache/Provider/FileProviderSpec.php new file mode 100644 index 0000000..816dbec --- /dev/null +++ b/spec/Nekland/BaseApi/Cache/Provider/FileProviderSpec.php @@ -0,0 +1,46 @@ + + * + * For the full license, take a look to the LICENSE file + * on the root directory of this project + */ + +namespace spec\Nekland\BaseApi\Cache\Provider; + +use PhpSpec\ObjectBehavior; + +class FileProviderSpec extends ObjectBehavior +{ + public function it_is_initializable() + { + $this->shouldHaveType('Nekland\BaseApi\Cache\Provider\FileProvider'); + $this->shouldHaveType('Nekland\BaseApi\Cache\Provider\CacheProviderInterface'); + } + + public function it_should_return_an_array_stored_in_a_file() + { + $this->setOptions(['path' => __DIR__ . '/../../../../fixture/cache_file']); + $this->load(); + $this->get('something')->shouldReturn(['foz', 'baz']); + } + + public function it_should_save_the_cache() + { + $final = sys_get_temp_dir() . '/nekland_test_cache_file'; + $this->setOptions(['path' => __DIR__ . '/../../../../fixture/cache_file']); + $this->load(); + $this->set('element', ['foo' => 'bar']); + $this->setOptions(['path' => $final]); + $this->save(); + + if ( + trim(file_get_contents($final)) !== + trim(file_get_contents(__DIR__ . '/../../../../fixture/cache_file_comp')) + ) { + throw new \Exception('The cache file is not as expected'); + } + } +} diff --git a/spec/Nekland/BaseApi/Http/Auth/AuthFactorySpec.php b/spec/Nekland/BaseApi/Http/Auth/AuthFactorySpec.php new file mode 100644 index 0000000..afca6ed --- /dev/null +++ b/spec/Nekland/BaseApi/Http/Auth/AuthFactorySpec.php @@ -0,0 +1,34 @@ +shouldHaveType('Nekland\BaseApi\Http\Auth\AuthFactory'); + } + + public function let() + { + $this->beConstructedWith(); + } + + public function it_should_return_an_auth_strategy_after_registered_namespace() + { + $this->addNamespace('spec\fixture'); + $this->get('MyAuthStrategy')->shouldHaveType('spec\fixture\MyAuthStrategy'); + } + + public function it_should_return_an_auth_strategy_after_added_it_to_classes() + { + $this->addAuth('my_auth', 'spec\fixture\MyAuthStrategy'); + $this->get('my_auth')->shouldHaveType('spec\fixture\MyAuthStrategy'); + } +} diff --git a/spec/Nekland/BaseApi/Http/ClientAdapter/GuzzleAdapterSpec.php b/spec/Nekland/BaseApi/Http/ClientAdapter/GuzzleAdapterSpec.php new file mode 100644 index 0000000..1a6b558 --- /dev/null +++ b/spec/Nekland/BaseApi/Http/ClientAdapter/GuzzleAdapterSpec.php @@ -0,0 +1,74 @@ +shouldHaveType('Nekland\BaseApi\Http\ClientAdapter\GuzzleAdapter'); + $this->shouldHaveType('Nekland\BaseApi\Http\AbstractHttpClient'); + } + + public function let(EventDispatcher $dispatcher, Client $guzzle) + { + $this->beConstructedWith($dispatcher, [], $guzzle); + } + + public function it_should_not_send_real_request_if_the_event_completed_request( + Request $request, + EventDispatcher $dispatcher, + RequestEvent $requestEvent, + Client $guzzle + ) { + $requestEvent->requestCompleted()->willReturn(true); + $requestEvent->getResponse()->willReturn('the response'); + $request->getMethod()->willReturn('get'); + $guzzle->get()->shouldNotBeCalled(); + + $dispatcher + ->dispatch(Argument::any(), Argument::type('Nekland\BaseApi\Http\Event\RequestEvent')) + ->willReturn($requestEvent) + ; + + $this->send($request); + } + + public function it_should_send_real_request_when_event_request_not_completed( + Client $guzzle, + EventDispatcher $dispatcher, + Request $request, + RequestEvent $requestEvent, + ResponseInterface $result + ) { + $guzzle->get('api.com', Argument::any())->shouldBeCalled(); + $guzzle->get('api.com', Argument::any())->willReturn($result); + $result->getHeaders()->willReturn([]); + $result->getBody()->willReturn(''); + + $requestEvent->requestCompleted()->willReturn(false); + $dispatcher + ->dispatch(Argument::any(), Argument::type('Nekland\BaseApi\Http\Event\RequestEvent')) + ->willReturn($requestEvent) + ; + + $request->getMethod()->willReturn('get'); + $request->getUrl()->willReturn('api.com'); + $request->getHeaders()->willReturn([]); + $request->getBody()->willReturn([]); + + $requestEvent->setResponse(Argument::any())->shouldBeCalled(); + $requestEvent->getResponse()->shouldBeCalled(); + + $this->send($request); + } +} diff --git a/spec/Nekland/BaseApi/Http/HttpClientFactorySpec.php b/spec/Nekland/BaseApi/Http/HttpClientFactorySpec.php new file mode 100644 index 0000000..21ace95 --- /dev/null +++ b/spec/Nekland/BaseApi/Http/HttpClientFactorySpec.php @@ -0,0 +1,36 @@ +shouldHaveType('Nekland\BaseApi\Http\HttpClientFactory'); + } + + public function it_should_return_a_guzzle_http_client_by_default() + { + $this->createHttpClient()->shouldHaveType('Nekland\BaseApi\Http\ClientAdapter\GuzzleAdapter'); + } + + public function it_should_be_able_to_create_user_http_client() + { + $this->register('MyHttpClient', 'spec\fixture\MyHttpClient'); + $this->createHttpClient('MyHttpClient')->shouldHaveType('spec\fixture\MyHttpClient'); + } + + public function it_should_not_register_class_that_does_not_exists() + { + + $this + ->shouldThrow('\InvalidArgumentException') + ->duringRegister('ClassThatDoesNotExists', 'this\namespace\does\not\Exists') + ; + } +} diff --git a/spec/Nekland/BaseApi/Http/RequestSpec.php b/spec/Nekland/BaseApi/Http/RequestSpec.php new file mode 100644 index 0000000..2f83112 --- /dev/null +++ b/spec/Nekland/BaseApi/Http/RequestSpec.php @@ -0,0 +1,48 @@ + 'bar']; + private $headers = ['User-Agent' => 'phpspec']; + + public function it_is_initializable() + { + $this->shouldHaveType('Nekland\BaseApi\Http\Request'); + } + + public function let() + { + $this->beConstructedWith('GET', $this->url, $this->body, $this->headers); + } + + public function it_should_return_values_setted_in_constructor() + { + $this->getHeaders()->shouldReturn($this->headers); + $this->getParameters()->shouldReturn($this->body); + $this->getPath()->shouldReturn($this->url); + $this->getUrl()->shouldReturn($this->url . '?foo=bar'); + $this->getMethod()->shouldReturn('get'); + } + + public function headers_should_be_editable() + { + $this->hasHeader('User-Agent')->shouldReturn(true); + $this->hasHeader('Something-Else')->shouldReturn(false); + $this->addHeader('Hello', 'Content'); + $this->hasHeader('Hello')->shouldReturn('true'); + } + + public function parameters_should_be_editable() + { + $this->hasParameter('foo')->shouldReturn(true); + $this->hasParameter('key')->shouldReturn(false); + $this->setParameter('key', 'some_key'); + $this->getParameter('key')->shouldReturn('some_key'); + } +} diff --git a/spec/Nekland/BaseApi/Transformer/JsonTransformerSpec.php b/spec/Nekland/BaseApi/Transformer/JsonTransformerSpec.php new file mode 100644 index 0000000..cd8f31b --- /dev/null +++ b/spec/Nekland/BaseApi/Transformer/JsonTransformerSpec.php @@ -0,0 +1,21 @@ +shouldHaveType('Nekland\BaseApi\Transformer\JsonTransformer'); + $this->shouldHaveType('Nekland\BaseApi\Transformer\TransformerInterface'); + } + + public function it_should_transform_json_string_to_array() + { + $a = ['hello' => 'world']; + $this->transform('{"hello":"world"}')->shouldReturn($a); + } +} diff --git a/spec/fixture/MyAuthStrategy.php b/spec/fixture/MyAuthStrategy.php new file mode 100644 index 0000000..af8bdc8 --- /dev/null +++ b/spec/fixture/MyAuthStrategy.php @@ -0,0 +1,26 @@ +