From 765876fdfe7e3c4c4fc6660ab3d11e2cb53dcd01 Mon Sep 17 00:00:00 2001 From: Michael Roterman Date: Mon, 4 Jan 2021 21:44:27 +0100 Subject: [PATCH] Release 4.0.0 (#29) Massive changes that implement many PSR standards, and now only supports PHP7.3+, and Symfony 4 and 5. --- .gitattributes | 7 + .github/workflows/coding-standards.yml | 45 +++ .github/workflows/continuous-integration.yml | 89 +++++ .github/workflows/static-analysis.yml.bak | 56 +++ .gitignore | 5 +- .travis.yml | 37 -- ClientConfiguration.php | 54 ++- .../CompilerPass/ConfigurationPass.php | 170 +++++++++ .../CompilerPass/EventDispatchingPass.php | 279 ++++++++++++++ DependencyInjection/Configuration.php | 209 +++++++++-- DependencyInjection/TmdbSymfonyExtension.php | 169 +++++++-- README.md | 246 +++++++++---- Resources/config/repositories.xml | 113 +++--- Resources/config/services.xml | 103 ++++-- Resources/config/twig.xml | 8 +- Resources/test/configuration.json | 97 +++++ .../CompilerPass/ConfigurationPassTest.php | 197 ++++++++++ .../CompilerPass/EventDispatchingPassTest.php | 341 ++++++++++++++++++ Tests/DependencyInjection/TestCase.php | 212 +++++++++++ .../TmdbSymfonyExtensionTest.php | 119 +++++- Tests/TestKernel.php | 39 -- Tests/Twig/TmdbExtensionTest.php | 105 ++++++ Tests/config.yml | 4 - TmdbSymfonyBundle.php | 48 +++ Twig/TmdbExtension.php | 52 ++- composer.json | 38 +- phpcs.xml.dist | 13 + phpstan.neon | 10 + phpunit.xml.dist | 39 +- psalm.xml | 16 + 30 files changed, 2565 insertions(+), 355 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/coding-standards.yml create mode 100644 .github/workflows/continuous-integration.yml create mode 100644 .github/workflows/static-analysis.yml.bak delete mode 100644 .travis.yml create mode 100644 DependencyInjection/CompilerPass/ConfigurationPass.php create mode 100644 DependencyInjection/CompilerPass/EventDispatchingPass.php create mode 100644 Resources/test/configuration.json create mode 100644 Tests/DependencyInjection/CompilerPass/ConfigurationPassTest.php create mode 100644 Tests/DependencyInjection/CompilerPass/EventDispatchingPassTest.php create mode 100644 Tests/DependencyInjection/TestCase.php delete mode 100644 Tests/TestKernel.php create mode 100644 Tests/Twig/TmdbExtensionTest.php delete mode 100644 Tests/config.yml create mode 100644 phpcs.xml.dist create mode 100644 phpstan.neon create mode 100644 psalm.xml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..22c1526 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +.editorconfig export-ignore +.gitattributes export-ignore +.github export-ignore +.gitignore export-ignore +.php_cs export-ignore +phpstan.neon.dist export-ignore +Tests export-ignore diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml new file mode 100644 index 0000000..8c69385 --- /dev/null +++ b/.github/workflows/coding-standards.yml @@ -0,0 +1,45 @@ +name: "Coding Standards" + +on: ["pull_request"] + +jobs: + coding-standards: + name: "Coding Standards" + runs-on: "ubuntu-20.04" + + strategy: + matrix: + php-version: + - "7.4" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + with: + fetch-depth: 10 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: "cs2pr" + + - name: "Cache dependencies installed with Composer" + uses: "actions/cache@v2" + with: + path: "~/.composer/cache" + key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}" + restore-keys: "php-${{ matrix.php-version }}-composer-locked-" + + - name: "Install dependencies with Composer" + run: "composer update --no-interaction --no-progress --prefer-dist --prefer-stable" + + - name: "Install git-phpcs" + run: "wget https://github.com/diff-sniffer/git/releases/download/0.3.2/git-phpcs.phar" + + - name: "Fetch head branch" + run: "git remote set-branches --add origin $GITHUB_BASE_REF && git fetch origin $GITHUB_BASE_REF" + + - name: "Run git-phpcs" + run: "php git-phpcs.phar origin/$GITHUB_BASE_REF...$GITHUB_SHA --report=checkstyle | cs2pr" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..18be910 --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,89 @@ +name: "Continuous Integration" + +on: ["pull_request", "push"] + +env: + fail-fast: true + +jobs: + phpunit: + name: "PHPUnit" + runs-on: "ubuntu-20.04" + env: + SYMFONY_REQUIRE: ${{matrix.symfony-require}} + SYMFONY_DEPRECATIONS_HELPER: ${{matrix.symfony-deprecations-helper}} + + strategy: + matrix: + php-version: + - "7.3" + - "7.4" + deps: + - "normal" + symfony-require: + - "" + symfony-deprecations-helper: + - "" + include: + # Test against latest Symfony 4.3 stable + - symfony-require: "4.3.*" + php-version: "7.3" + deps: "normal" + + # Test against latest Symfony 4.4 dev + - symfony-require: "4.4.*" + php-version: "7.3" + deps: "dev" + + # Test against latest Symfony 5.2 dev + - symfony-require: "5.2.*" + php-version: "7.3" + deps: "dev" + + - php-version: "8.0" + deps: "dev" + symfony-deprecations-helper: "weak" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + with: + fetch-depth: 2 + + - name: "Install PHP with PCOV" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php-version }}" + coverage: "pcov" + + - name: "Cache dependencies installed with composer" + uses: "actions/cache@v2" + with: + path: "~/.composer/cache" + key: "php-${{ matrix.php-version }}-composer-locked-${{ hashFiles('composer.lock') }}" + restore-keys: "php-${{ matrix.php-version }}-composer-locked-" + + - name: "Install stable dependencies with composer" + run: "composer update --no-interaction --prefer-dist --prefer-stable" + if: "${{ matrix.deps == 'normal' }}" + + - name: "Install dev dependencies with composer" + run: "composer update --no-interaction --prefer-dist" + if: "${{ matrix.deps == 'dev' }}" + + - name: "Install lowest possible dependencies with composer" + run: "composer update --no-interaction --prefer-dist --prefer-stable --prefer-lowest" + if: "${{ matrix.deps == 'low' }}" + + - name: "Run PHPUnit" + run: "vendor/bin/phpunit --coverage-clover=coverage.xml" + + - name: "Upload coverage file" + uses: "actions/upload-artifact@v2" + with: + name: "phpunit-${{ matrix.php-version }}-${{ matrix.deps }}-${{ hashFiles('composer.lock') }}.coverage" + path: "coverage.xml" + + - uses: codecov/codecov-action@v1 + with: + verbose: true diff --git a/.github/workflows/static-analysis.yml.bak b/.github/workflows/static-analysis.yml.bak new file mode 100644 index 0000000..1fd3f32 --- /dev/null +++ b/.github/workflows/static-analysis.yml.bak @@ -0,0 +1,56 @@ +name: Static Analysis + +on: + pull_request: + +jobs: + static-analysis-phpstan: + name: "PHPStan" + runs-on: "ubuntu-latest" + + strategy: + matrix: + php-version: + - "7.4" + + steps: + - name: "Checkout code" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: cs2pr + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v1" + + - name: "Run PHPStan" + run: "vendor/bin/phpstan analyse --error-format=checkstyle --no-progress -c phpstan.neon | cs2pr" + + static-analysis-psalm: + name: "Psalm" + runs-on: "ubuntu-latest" + + strategy: + matrix: + php-version: + - "7.4" + + steps: + - name: "Checkout code" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v1" + + - name: "Run a static analysis with vimeo/psalm" + run: "vendor/bin/psalm --show-info=false --stats --output-format=github --threads=$(nproc)" diff --git a/.gitignore b/.gitignore index 63c79b6..6ae778b 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ composer.lock .idea/ vendor/ - +coverage/ +phpcs.cache +.phpunit.result.cache +.phpcs-cache diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 519f6a7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,37 +0,0 @@ -sudo: false -language: php - -php: - - 7.3 - - 7.4 - - nightly - - hhvm - -cache: - directories: - - ~/.composer/cache - -before_script: - - composer self-update - - if [ "$SYMFONY_VERSION" != "" ]; then composer require --no-update symfony/config:${SYMFONY_VERSION} symfony/dependency-injection:${SYMFONY_VERSION} symfony/event-dispatcher:${SYMFONY_VERSION} symfony/http-kernel:${SYMFONY_VERSION} symfony/framework-bundle:${SYMFONY_VERSION}; fi; - - if [ "$SYMFONY_EVENT_DISPATCHER_VERSION" != "" ]; then composer require --no-update symfony/event-dispatcher:${SYMFONY_EVENT_DISPATCHER_VERSION}; fi; - - composer install --no-interaction --prefer-source --dev - -script: vendor/bin/phpunit --verbose - -matrix: - include: - - php: 7.3 - env: [SYMFONY_VERSION="^4.4", SYMFONY_EVENT_DISPATCHER_VERSION="^4.4"] - - php: 7.3 - env: [SYMFONY_VERSION="^5.0", SYMFONY_EVENT_DISPATCHER_VERSION="^5.0"] - - - php: 7.4 - env: [SYMFONY_VERSION="^4.4", SYMFONY_EVENT_DISPATCHER_VERSION="^4.4"] - - php: 7.4 - env: [SYMFONY_VERSION="^5.0", SYMFONY_EVENT_DISPATCHER_VERSION="^5.0"] - - allow_failures: - - php: nightly - - php: hhvm - fast_finish: true diff --git a/ClientConfiguration.php b/ClientConfiguration.php index f509d19..6ddf0ba 100644 --- a/ClientConfiguration.php +++ b/ClientConfiguration.php @@ -1,35 +1,55 @@ parameters = $options; - - $this->parameters['event_dispatcher'] = $eventDispatcher; - } + ) { + $options['api_token'] = $apiToken; + $options['event_dispatcher']['adapter'] = $eventDispatcher; + $options['http']['client'] = $client; + $options['http']['request_factory'] = $requestFactory; + $options['http']['response_factory'] = $responseFactory; + $options['http']['stream_factory'] = $streamFactory; + $options['http']['uri_factory'] = $uriFactory; - public function setCacheHandler(Cache $handler = null) - { - $this->parameters['cache']['handler'] = $handler; - } + // Library handles it as an api_token + unset($options['bearer_token']); - /** - * @return array - */ - public function all() - { - return $this->parameters; + parent::__construct($options); } } diff --git a/DependencyInjection/CompilerPass/ConfigurationPass.php b/DependencyInjection/CompilerPass/ConfigurationPass.php new file mode 100644 index 0000000..842bdde --- /dev/null +++ b/DependencyInjection/CompilerPass/ConfigurationPass.php @@ -0,0 +1,170 @@ +getParameter('tmdb.options'); + $configDefinition = $container->getDefinition(ClientConfiguration::class); + + // By default the first argument is always referenced to the ApiToken. + if (null !== $bearerToken = $parameters['options']['bearer_token']) { + $configDefinition->replaceArgument(0, new Reference(BearerToken::class)); + } + + $this->setupEventDispatcher($container, $configDefinition, $parameters); + $this->setupHttpClient($container, $configDefinition, $parameters); + } + + /** + * @param ContainerBuilder $container + * @param Definition $configDefinition + * @param array $parameters + * + * @return void + */ + private function setupEventDispatcher( + ContainerBuilder $container, + Definition $configDefinition, + array $parameters + ): void { + if (!$container->hasDefinition($parameters['options']['event_dispatcher']['adapter'])) { + $this->tryToAliasAutowiredInterfacesIfPossible( + $container, + $parameters['options']['event_dispatcher']['adapter'], + TmdbSymfonyBundle::PSR14_EVENT_DISPATCHERS, + 'tmdb_symfony.options.event_dispatcher.adapter' + ); + } + + $configDefinition->replaceArgument(1, new Reference($parameters['options']['event_dispatcher']['adapter'])); + } + + /** + * @param ContainerBuilder $container + * @param Definition $configDefinition + * @param array $parameters + * + * @return void + */ + private function setupHttpClient( + ContainerBuilder $container, + Definition $configDefinition, + array $parameters + ): void { + if (!$container->hasDefinition($parameters['options']['http']['client'])) { + $this->tryToAliasAutowiredInterfacesIfPossible( + $container, + $parameters['options']['http']['client'], + TmdbSymfonyBundle::PSR18_CLIENTS, + 'tmdb_symfony.options.http.client' + ); + } + + if (!$container->hasDefinition($parameters['options']['http']['request_factory'])) { + $this->tryToAliasAutowiredInterfacesIfPossible( + $container, + $parameters['options']['http']['request_factory'], + TmdbSymfonyBundle::PSR17_REQUEST_FACTORIES, + 'tmdb_symfony.options.http.request_factory' + ); + } + + if (!$container->hasDefinition($parameters['options']['http']['response_factory'])) { + $this->tryToAliasAutowiredInterfacesIfPossible( + $container, + $parameters['options']['http']['response_factory'], + TmdbSymfonyBundle::PSR17_RESPONSE_FACTORIES, + 'tmdb_symfony.options.http.response_factory' + ); + } + + if (!$container->hasDefinition($parameters['options']['http']['stream_factory'])) { + $this->tryToAliasAutowiredInterfacesIfPossible( + $container, + $parameters['options']['http']['stream_factory'], + TmdbSymfonyBundle::PSR17_STREAM_FACTORIES, + 'tmdb_symfony.options.http.stream_factory' + ); + } + + if (!$container->hasDefinition($parameters['options']['http']['uri_factory'])) { + $this->tryToAliasAutowiredInterfacesIfPossible( + $container, + $parameters['options']['http']['uri_factory'], + TmdbSymfonyBundle::PSR17_URI_FACTORIES, + 'tmdb_symfony.options.http.uri_factory' + ); + } + + $configDefinition->replaceArgument(2, new Reference($parameters['options']['http']['client'])); + $configDefinition->replaceArgument(3, new Reference($parameters['options']['http']['request_factory'])); + $configDefinition->replaceArgument(4, new Reference($parameters['options']['http']['response_factory'])); + $configDefinition->replaceArgument(5, new Reference($parameters['options']['http']['stream_factory'])); + $configDefinition->replaceArgument(6, new Reference($parameters['options']['http']['uri_factory'])); + } + + /** + * @param ContainerBuilder $container + * @param string $alias + * @param string $tag + * @param string $configurationPath + * @return void + * @throws \RuntimeException + */ + protected function tryToAliasAutowiredInterfacesIfPossible( + ContainerBuilder $container, + string $alias, + string $tag, + string $configurationPath + ): void { + $services = $container->findTaggedServiceIds($tag); + + if (!empty($services)) { + if (count($services) > 1) { + throw new RuntimeException( + sprintf( + 'Trying to automatically configure tmdb symfony bundle, however we found %d applicable services' + . ' ( %s ) for tag "%s", please set one of these explicitly in your configuration under "%s".', + count($services), + implode(', ', array_keys($services)), + $tag, + $configurationPath + ) + ); + } + + $serviceIds = array_keys($services); + $serviceId = array_shift($serviceIds); + + $container->setAlias($alias, $serviceId); + return; + } + + throw new RuntimeException( + sprintf( + 'Unable to find any services tagged with "%s", ' . + 'please set it in the configuration explicitly under "%s".', + $tag, + $configurationPath + ) + ); + } +} diff --git a/DependencyInjection/CompilerPass/EventDispatchingPass.php b/DependencyInjection/CompilerPass/EventDispatchingPass.php new file mode 100644 index 0000000..9a3c1e6 --- /dev/null +++ b/DependencyInjection/CompilerPass/EventDispatchingPass.php @@ -0,0 +1,279 @@ +getParameter('tmdb.options'); + $clientOptions = $parameters['options']; + + if ($container->hasAlias($clientOptions['event_dispatcher']['adapter'])) { + $definition = $container->getDefinition( + $container->getAlias($clientOptions['event_dispatcher']['adapter']) + ); + } else { + $definition = $container->getDefinition($clientOptions['event_dispatcher']['adapter']); + } + + if ($definition->getClass() === EventDispatcher::class) { + $this->handleSymfonyEventDispatcherRegistration($container, $definition, $parameters); + } + } + + /** + * @param ContainerBuilder $container + * @param Definition $eventDispatcher + * @param array $parameters + * + * @return void + */ + private function handleSymfonyEventDispatcherRegistration( + ContainerBuilder $container, + Definition $eventDispatcher, + array $parameters + ): void { + $cacheEnabled = $parameters['cache']['enabled']; + $logEnabled = $parameters['log']['enabled']; + + $requestListener = $cacheEnabled ? + $this->getPsr6CacheRequestListener($container, $parameters) : + $this->getRequestListener($container, $parameters); + + if ($logEnabled) { + $this->handleLoggerListeners($container, $eventDispatcher, $parameters); + } + + $this->registerEventListener( + $eventDispatcher, + RequestEvent::class, + $requestListener->getClass() + ); + + if (null !== $bearerToken = $parameters['options']['bearer_token']) { + $definition = $container->getDefinition(ApiTokenRequestListener::class); + $definition->replaceArgument(0, new Reference(BearerToken::class)); + } + + $this->registerEventListener( + $eventDispatcher, + BeforeRequestEvent::class, + ApiTokenRequestListener::class + ); + + $this->registerEventListener( + $eventDispatcher, + BeforeRequestEvent::class, + ContentTypeJsonRequestListener::class + ); + + $this->registerEventListener( + $eventDispatcher, + BeforeRequestEvent::class, + AcceptJsonRequestListener::class + ); + + $definition = $container->getDefinition(UserAgentRequestListener::class); + $definition->replaceArgument( + 0, + sprintf( + 'php-tmdb/symfony/%s php-tmdb/api/%s', + TmdbSymfonyBundle::VERSION, + Client::VERSION + ) + ); + + $this->registerEventListener( + $eventDispatcher, + BeforeRequestEvent::class, + UserAgentRequestListener::class + ); + } + + /** + * @param ContainerBuilder $container + * @param array $parameters + * @return Definition + */ + private function getRequestListener( + ContainerBuilder $container, + array $parameters + ): Definition { + return $container->getDefinition(RequestListener::class) + ->replaceArgument( + 1, + new Reference($parameters['options']['event_dispatcher']['adapter']) + ); + } + + /** + * @param ContainerBuilder $container + * @param array $parameters + * @return Definition + */ + private function getPsr6CacheRequestListener( + ContainerBuilder $container, + array $parameters + ): Definition { + return $container->getDefinition(Psr6CachedRequestListener::class) + ->replaceArgument(1, new Reference($parameters['options']['event_dispatcher']['adapter'])) + ->replaceArgument(2, new Reference($parameters['cache']['adapter'])) + ->replaceArgument(3, new Reference($parameters['options']['http']['stream_factory'])); + } + + /** + * @param string $event + * @param string $listener + * @param Definition $eventDispatcher + * @param ContainerBuilder $container + * @param array $parameters + * + * @return void + */ + private function handleLogging( + string $event, + string $listener, + Definition $eventDispatcher, + ContainerBuilder $container, + array $parameters + ) { + $options = $parameters[$listener]; + $configEntry = sprintf('tmdb_symfony.log.%s', $listener); + + if (!$options['enabled']) { + return; + } + + if (!$options['adapter']) { + $options['adapter'] = $parameters['adapter']; + } + + if (!$container->hasDefinition($options['adapter']) && !$container->hasAlias($options['adapter'])) { + throw new \RuntimeException(sprintf( + 'Unable to find a definition for the adapter to provide tmdb request logging, you gave "%s" for "%s".', + $options['adapter'], + sprintf('%s.%s', $configEntry, 'adapter') + )); + } + + if (!$container->hasDefinition($options['listener']) && !$container->hasAlias($options['listener'])) { + throw new \RuntimeException(sprintf( + 'Unable to find a definition for the listener to provide tmdb request logging, you gave "%s" for "%s".', + $options['listener'], + sprintf('%s.%s', $configEntry, 'listener') + )); + } + + if (!$container->hasDefinition($options['formatter']) && !$container->hasAlias($options['formatter'])) { + throw new \RuntimeException(sprintf( + 'Unable to find a definition for the formatter to provide tmdb request logging, ' . + 'you gave "%s" for "%s".', + $options['formatter'], + sprintf('%s.%s', $configEntry, 'formatter') + )); + } + + $adapter = $container->hasAlias($options['adapter']) ? + $container->getAlias($options['adapter']) : + $options['adapter']; + + $listenerDefinition = $container->getDefinition($options['listener']); + $listenerDefinition->replaceArgument(0, new Reference($adapter)); + $listenerDefinition->replaceArgument(1, new Reference($options['formatter'])); + + // Cannot assume if this was replaced this parameter will be kept. + if ($listenerDefinition->getClass() === LogHydrationListener::class) { + $listenerDefinition->replaceArgument( + 2, + $parameters['hydration']['with_hydration_data'] + ); + } + + $this->registerEventListener( + $eventDispatcher, + $event, + $listenerDefinition->getClass() + ); + } + + /** + * Register listeners for logging. + * + * @param ContainerBuilder $container + * @param Definition $eventDispatcher + * @param array $parameters + * + * @return void + */ + private function handleLoggerListeners(ContainerBuilder $container, Definition $eventDispatcher, array $parameters): void + { + $listeners = [ + BeforeRequestEvent::class => 'request_logging', + ResponseEvent::class => 'response_logging', + HttpClientExceptionEvent::class => 'client_exception_logging', + TmdbExceptionEvent::class => 'api_exception_logging', + BeforeHydrationEvent::class => 'hydration' + ]; + + foreach ($listeners as $event => $listener) { + $this->handleLogging( + $event, + $listener, + $eventDispatcher, + $container, + $parameters['log'] + ); + } + } + + /** + * @param Definition $eventDispatcher + * @param string $event + * @param string $reference + */ + private function registerEventListener( + Definition $eventDispatcher, + string $event, + string $reference + ): void { + $eventDispatcher->addMethodCall( + 'addListener', + [ + $event, + new Reference($reference) + ] + ); + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 038831d..4397a8f 100755 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -1,16 +1,18 @@ getRootNode(); + $this->addRootChildren($rootNode); + $this->addOptionsSection($rootNode); + $this->addLogSection($rootNode); + $this->addCacheSection($rootNode); + + return $treeBuilder; + } + + /** + * @param ArrayNodeDefinition $rootNode + * + * @return void + */ + private function addRootChildren(ArrayNodeDefinition $rootNode): void + { $rootNode + ->beforeNormalization() + ->ifTrue(function ($v) { + return isset($v['api_key']) && !empty($v['api_key']); + }) + ->then(function ($v) { + $v['options']['api_token'] = $v['api_key']; + + return $v; + }) + ->end() + ->addDefaultsIfNotSet() ->children() ->scalarNode('api_key')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('session_token')->defaultValue(null)->end() ->arrayNode('repositories')->canBeDisabled()->end() ->arrayNode('twig_extension')->canBeDisabled()->end() + ->booleanNode('disable_legacy_aliases')->defaultFalse()->end() + ->end() + ; + } + + /** + * @param ArrayNodeDefinition $rootNode + * + * @return void + */ + private function addOptionsSection(ArrayNodeDefinition $rootNode): void + { + $rootNode + ->children() ->arrayNode('options') ->addDefaultsIfNotSet() ->children() - ->scalarNode('adapter')->defaultValue(null)->end() - ->scalarNode('secure')->defaultValue(true)->end() + ->scalarNode('api_token') + ->defaultValue(null) + ->info('Will be set by root api_key') + ->end() + ->scalarNode('bearer_token') + ->defaultValue(null) + ->info('If set will be used instead of api token') + ->end() + ->scalarNode('secure')->defaultTrue()->end() ->scalarNode('host')->defaultValue(Client::TMDB_URI)->end() - ->scalarNode('session_token')->defaultValue(null)->end() - ->arrayNode('cache') - ->canBeDisabled() + ->scalarNode('guest_session_token')->defaultValue(null)->end() + ->arrayNode('event_dispatcher') + ->info('Reference to a service which implements PSR-14 Event Dispatcher') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('adapter') + ->isRequired()->cannotBeEmpty() + ->defaultValue('Psr\EventDispatcher\EventDispatcherInterface') + ->end() + ->end() + ->end() + ->arrayNode('http') + ->addDefaultsIfNotSet() ->children() - ->scalarNode('path')->defaultValue('%kernel.cache_dir%/themoviedb')->end() - ->scalarNode('handler')->defaultValue(null)->end() - ->scalarNode('subscriber')->defaultValue(null)->end() + ->scalarNode('client') + ->defaultValue('Psr\Http\Client\ClientInterface') + ->info('Reference to a service which implements PSR-18 HTTP Client') + ->end() + ->scalarNode('request_factory') + ->defaultValue('Psr\Http\Message\RequestFactoryInterface') + ->info('Reference to a service which implements PSR-17 HTTP Factories') + ->end() + ->scalarNode('response_factory') + ->defaultValue('Psr\Http\Message\ResponseFactoryInterface') + ->info('Reference to a service which implements PSR-17 HTTP Factories') + ->end() + ->scalarNode('stream_factory') + ->defaultValue('Psr\Http\Message\StreamFactoryInterface') + ->info('Reference to a service which implements PSR-17 HTTP Factories') + ->end() + ->scalarNode('uri_factory') + ->defaultValue('Psr\Http\Message\UriFactoryInterface') + ->info('Reference to a service which implements PSR-17 HTTP Factories') + ->end() ->end() ->end() - ->arrayNode('log') - ->canBeEnabled() + ->arrayNode('hydration') + ->addDefaultsIfNotSet() ->children() - ->scalarNode('level')->defaultValue('DEBUG')->end() - ->scalarNode('path')->defaultValue('%kernel.logs_dir%/themoviedb.log')->end() - ->scalarNode('handler')->defaultValue(null)->end() - ->scalarNode('subscriber')->defaultValue(null)->end() + ->booleanNode('event_listener_handles_hydration')->defaultFalse()->end() + ->arrayNode('only_for_specified_models') + ->scalarPrototype()->end() + ->end() ->end() ->end() ->end() ->end() - ->end(); + ->end() + ; + } - return $treeBuilder; + /** + * @param ArrayNodeDefinition $rootNode + * + * @return void + */ + private function addLogSection(ArrayNodeDefinition $rootNode): void + { + $rootNode + ->children() + ->arrayNode('log') + ->addDefaultsIfNotSet() + ->canBeEnabled() + ->children() + ->scalarNode('adapter') + ->defaultValue('Psr\Log\LoggerInterface') + ->info('When registering a channel in monolog as "tmdb" for example, monolog.logger.tmdb') + ->end() + ->arrayNode('request_logging') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('enabled')->defaultValue('%kernel.debug%')->end() + ->scalarNode('listener')->defaultValue(LogHttpMessageListener::class)->end() + ->scalarNode('adapter')->defaultValue(null)->end() + ->scalarNode('formatter')->defaultValue(SimpleHttpMessageFormatter::class)->end() + ->end() + ->end() + ->arrayNode('response_logging') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('enabled')->defaultValue('%kernel.debug%')->end() + ->scalarNode('listener')->defaultValue(LogHttpMessageListener::class)->end() + ->scalarNode('adapter')->defaultValue(null)->end() + ->scalarNode('formatter')->defaultValue(SimpleHttpMessageFormatter::class)->end() + ->end() + ->end() + ->arrayNode('api_exception_logging') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('enabled')->defaultValue('%kernel.debug%')->end() + ->scalarNode('listener')->defaultValue(LogApiErrorListener::class)->end() + ->scalarNode('adapter')->defaultValue(null)->end() + ->scalarNode('formatter')->defaultValue(SimpleTmdbApiExceptionFormatter::class)->end() + ->end() + ->end() + ->arrayNode('client_exception_logging') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('enabled')->defaultValue('%kernel.debug%')->end() + ->scalarNode('listener')->defaultValue(LogHttpMessageListener::class)->end() + ->scalarNode('adapter')->defaultValue(null)->end() + ->scalarNode('formatter')->defaultValue(SimpleHttpMessageFormatter::class)->end() + ->end() + ->end() + ->arrayNode('hydration') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('enabled')->defaultValue('%kernel.debug%')->end() + ->scalarNode('listener')->defaultValue(LogHydrationListener::class)->end() + ->scalarNode('adapter')->defaultValue(null)->end() + ->scalarNode('formatter')->defaultValue(SimpleHydrationFormatter::class)->end() + ->booleanNode('with_hydration_data')->defaultFalse()->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } + + /** + * @param ArrayNodeDefinition $rootNode + * + * @return void + */ + private function addCacheSection(ArrayNodeDefinition $rootNode): void + { + $rootNode + ->children() + ->arrayNode('cache') + ->addDefaultsIfNotSet() + ->canBeEnabled() + ->children() + ->scalarNode('adapter')->defaultValue('Psr\Cache\CacheItemPoolInterface')->end() + ->end() + ->end() + ->end() + ; } } diff --git a/DependencyInjection/TmdbSymfonyExtension.php b/DependencyInjection/TmdbSymfonyExtension.php index 6194347..c295250 100644 --- a/DependencyInjection/TmdbSymfonyExtension.php +++ b/DependencyInjection/TmdbSymfonyExtension.php @@ -2,70 +2,183 @@ namespace Tmdb\SymfonyBundle\DependencyInjection; +use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\Loader; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Tmdb\Token\Api\ApiToken; +use Tmdb\Client; +use Tmdb\Repository\AccountRepository; +use Tmdb\Repository\AuthenticationRepository; +use Tmdb\Repository\CertificationRepository; +use Tmdb\Repository\ChangesRepository; +use Tmdb\Repository\CollectionRepository; +use Tmdb\Repository\CompanyRepository; +use Tmdb\Repository\ConfigurationRepository; +use Tmdb\Repository\CreditsRepository; +use Tmdb\Repository\DiscoverRepository; +use Tmdb\Repository\FindRepository; +use Tmdb\Repository\GenreRepository; +use Tmdb\Repository\JobsRepository; +use Tmdb\Repository\KeywordRepository; +use Tmdb\Repository\ListRepository; +use Tmdb\Repository\MovieRepository; +use Tmdb\Repository\NetworkRepository; +use Tmdb\Repository\PeopleRepository; +use Tmdb\Repository\ReviewRepository; +use Tmdb\Repository\SearchRepository; +use Tmdb\Repository\TvEpisodeRepository; +use Tmdb\Repository\TvRepository; +use Tmdb\Repository\TvSeasonRepository; +use Tmdb\SymfonyBundle\ClientConfiguration; +use Tmdb\SymfonyBundle\Twig\TmdbExtension; /** - * This is the class that loads and manages your bundle configuration - * - * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html} + * Class TmdbSymfonyExtension + * @package Tmdb\SymfonyBundle\DependencyInjection */ class TmdbSymfonyExtension extends Extension { /** - * {@inheritDoc} + * @param array $configs + * @param ContainerBuilder $container + * @return void */ public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.xml'); - $container->setParameter('tmdb.api_key', $config['api_key']); + $container->setParameter('tmdb.api_token', $config['options']['api_token']); + $container->setParameter('tmdb.bearer_token', $config['options']['bearer_token']); + + if (!$config['disable_legacy_aliases']) { + $this->handleLegacyGeneralAliases($container); + } if ($config['repositories']['enabled']) { $loader->load('repositories.xml'); + + if (!$config['disable_legacy_aliases']) { + $this->handleLegacyRepositoryAliases($container); + } } if ($config['twig_extension']['enabled']) { $loader->load('twig.xml'); - } - - $options = $config['options']; - - if ($options['cache']['enabled']) { - $options = $this->handleCache($container, $options); - } - if ($options['log']['enabled']) { - $options = $this->handleLog($options); + if (!$config['disable_legacy_aliases']) { + $this->handleLegacyTwigExtensionAlias($container); + } } - $container->setParameter('tmdb.options', $options); + $container->setParameter('tmdb.options', $config); + $container->setParameter('tmdb.client.options', $config['options']); } - protected function handleCache(ContainerBuilder $container, $options) + /** + * Alias mapping for legacy constructs; public to abuse within test suite. + * + * @return array + */ + public function getLegacyAliasMapping() { - if (null !== $handler = $options['cache']['handler']) { - $serviceId = sprintf('doctrine_cache.providers.%s', $options['cache']['handler']); + return [ + 'repositories' => [ + 'tmdb.authentication_repository' => AuthenticationRepository::class, + 'tmdb.account_repository' => AccountRepository::class, + 'tmdb.certification_repository' => CertificationRepository::class, + 'tmdb.changes_repository' => ChangesRepository::class, + 'tmdb.collection_repository' => CollectionRepository::class, + 'tmdb.company_repository' => CompanyRepository::class, + 'tmdb.configuration_repository' => ConfigurationRepository::class, + 'tmdb.credits_repository' => CreditsRepository::class, + 'tmdb.discover_repository' => DiscoverRepository::class, + 'tmdb.find_repository' => FindRepository::class, + 'tmdb.genre_repository' => GenreRepository::class, + 'tmdb.jobs_repository' => JobsRepository::class, + 'tmdb.keyword_repository' => KeywordRepository::class, + 'tmdb.list_repository' => ListRepository::class, + 'tmdb.movie_repository' => MovieRepository::class, + 'tmdb.network_repository' => NetworkRepository::class, + 'tmdb.people_repository' => PeopleRepository::class, + 'tmdb.review_repository' => ReviewRepository::class, + 'tmdb.search_repository' => SearchRepository::class, + 'tmdb.tv_repository' => TvRepository::class, + 'tmdb.tv_episode_repository' => TvEpisodeRepository::class, + 'tmdb.tv_season_repository' => TvSeasonRepository::class, + ], + 'general' => [ + 'tmdb.client' => Client::class, + 'tmdb.api_token' => ApiToken::class, + 'tmdb.configuration' => ClientConfiguration::class + ], + 'twig' => [ + 'tmdb.twig.image_extension' => TmdbExtension::class + ] + ]; + } - $container->setAlias('tmdb.cache_handler', new Alias($serviceId, false)); + /** + * Performs mapping of legacy aliases to their new service identifiers. + * + * @todo major release remove alias mapping of legacy muck :-) + * + * @param ContainerBuilder $container + * @param array $mapping + * + * @return void + */ + protected function performAliasMapping(ContainerBuilder $container, array $mapping = []): void + { + foreach ($mapping as $legacyAlias => $newAlias) { + // @todo fix alias with public/private properties + $container + ->setAlias($legacyAlias, new Alias($newAlias)) + ; } + } - return $options; + /** + * Handle general lgeacy aliases. + * + * @param ContainerBuilder $container + * + * @return void + */ + protected function handleLegacyGeneralAliases(ContainerBuilder $container): void + { + $mapping = $this->getLegacyAliasMapping(); + $this->performAliasMapping($container, $mapping['general']); } - protected function handleLog($options) + /** + * Map repository legacy aliases + * + * @param ContainerBuilder $container + * + * @return void + */ + protected function handleLegacyRepositoryAliases(ContainerBuilder $container): void { - if (null !== $handler = $options['log']['handler']) { - $options['log']['handler'] = !is_string($handler) ? $handler: new $handler(); - } + $mapping = $this->getLegacyAliasMapping(); + $this->performAliasMapping($container, $mapping['repositories']); + } - return $options; + /** + * Map twig legacy aliases + * + * @param ContainerBuilder $container + * + * @return void + */ + protected function handleLegacyTwigExtensionAlias(ContainerBuilder $container): void + { + $mapping = $this->getLegacyAliasMapping(); + $this->performAliasMapping($container, $mapping['twig']); } } diff --git a/README.md b/README.md index b2847b7..3f3e142 100755 --- a/README.md +++ b/README.md @@ -1,87 +1,155 @@ -Description ----------------- +# A Symfony Bundle for use together with the [php-tmdb/api](https://github.com/php-tmdb/api) TMDB API Wrapper. + +[![License](https://poser.pugx.org/php-tmdb/symfony/license.png)](https://packagist.org/packages/php-tmdb/symfony) +[![License](https://img.shields.io/github/v/tag/php-tmdb/symfony)](https://github.com/php-tmdb/symfony/releases) +[![Build Status](https://img.shields.io/github/workflow/status/php-tmdb/symfony/Continuous%20Integration?label=phpunit)](https://github.com/php-tmdb/symfony/actions?query=workflow%3A%22Continuous+Integration%22) +[![Build Status](https://img.shields.io/github/workflow/status/php-tmdb/symfony/Coding%20Standards?label=phpcs)](https://github.com/php-tmdb/symfony/actions?query=workflow%3A%22Coding+Standards%22) +[![codecov](https://img.shields.io/codecov/c/github/php-tmdb/symfony?token=gTM9AiO5vH)](https://codecov.io/gh/php-tmdb/symfony) +[![PHP](https://img.shields.io/badge/php->=7.3,%20>=8.0-8892BF.svg)](https://packagist.org/packages/php-tmdb/symfony) +[![Total Downloads](https://poser.pugx.org/php-tmdb/symfony/downloads.svg)](https://packagist.org/packages/php-tmdb/symfony) -A Symfony Bundle for use together with the [php-tmdb/api](https://github.com/php-tmdb/api) TMDB API Wrapper. +Compatible with Symfony 4 and 5, PHP 7.3 and up. Installation ------------ -[Install Composer](https://getcomposer.org/doc/00-intro.md) +- [Install Composer](https://getcomposer.org/doc/00-intro.md) +- [Install php-tmdb/api dependencies](https://github.com/php-tmdb/api/tree/release/4.0.0#installation) + - For development within Symfony we recommend making use of Symfony's PSR-18 HTTP Client _`Symfony\Component\HttpClient\Psr18Client`_, + as when non-cached results pass your profiler will be filled with data. -Then require the package: +Then require the bundle: ``` -composer require php-tmdb/symfony +composer require php-tmdb/symfony:^4 ``` Configuration ---------------- -Register the bundle in `app/AppKernel.php`: +Register the bundle in `app/bundles.php`: ```php - public function registerBundles() - { - ... - new Tmdb\SymfonyBundle\TmdbSymfonyBundle() - ... - } -``` + ['all' => true], +]; ``` - -Add to your `app/config/config.yml` the following: +Add to your `app/config/config.yml` the following, or replace values with services of your choice ( PSR-18 Http Client / PSR-17 Factories ): ```yaml tmdb_symfony: api_key: YOUR_API_KEY_HERE + options: + http: + client: Symfony\Component\HttpClient\Psr18Client + request_factory: Nyholm\Psr7\Factory\Psr17Factory + response_factory: Nyholm\Psr7\Factory\Psr17Factory + stream_factory: Nyholm\Psr7\Factory\Psr17Factory + uri_factory: Nyholm\Psr7\Factory\Psr17Factory +``` + +`services.yaml`: + +```yaml +services: + Symfony\Component\HttpClient\Psr18Client: + class: Symfony\Component\HttpClient\Psr18Client + + Nyholm\Psr7\Factory\Psr17Factory: + class: Nyholm\Psr7\Factory\Psr17Factory ``` __Configure caching__ -First create a new doctrine_cache provider with a caching provider of your preference. +You can use any PSR-6 cache you wish to use, we will simply use symfony's cache. + +When making use of caching, make sure to also include `php-http/cache-plugin` in composer, this plugin handles the logic for us, +so we don't have to re-invent the wheel. + +You are however also free to choose to implement your own cache listener, or add the caching logic inside the http client of your choice. + +```shell script +composer require php-http/cache-plugin:^1.7 +``` + +First off configure the cache pool in symfony `config/cache.yaml`: ```yaml -doctrine_cache: - providers: - tmdb_cache: - file_system: - directory: %kernel.cache_dir%/tmdb +framework: + cache: + pools: + cache.tmdb: + adapter: cache.adapter.filesystem + default_lifetime: 86400 ``` -Then update the tmdb configuration with the alias: +Then in your `tmdb_symfony.yaml` configuration enable the cache and reference this cache pool: ```yaml tmdb_symfony: - options: - cache: - enabled: true - handler: tmdb_cache + api_key: YOUR_API_KEY_HERE + cache: + enabled: true + adapter: cache.tmdb ``` -This caching system will adhere to the TMDB API max-age values, if you have different needs like long TTL's -you'd have to make your own implementation. We would be happy to integrate more options, so please contribute. - __Want to make use of logging?__ +Logging capabilities as of `4.0` allow you to make a fine-grained configuration. + +You can use any PSR-3 logger you wish to use, we will simply use monolog. + +First off configure the monolog and add a channel and handler: + +```yaml +monolog: + channels: + - tmdb + handlers: + tmdb: + type: stream + path: "%kernel.logs_dir%/php-tmdb--symfony.%kernel.environment%.log" + level: info + channels: ["tmdb"] +``` + +Then in your `tmdb_symfony.yaml` configuration: + ```yaml tmdb_symfony: - api_key: YOUR_API_KEY_HERE - options: - cache: - enabled: true - log: - enabled: true - #path: "%kernel.logs_dir%/tmdb.log" + api_key: YOUR_API_KEY_HERE + log: + enabled: true + adapter: monolog.logger.tmdb + hydration: + enabled: true + with_hydration_data: false # We would only recommend to enable this with an in-memory logger, so you have access to the hydration data within the profiler. + adapter: null # you can set different adapters for different logs, leave null to use the main adapter. + listener: Tmdb\Event\Listener\Logger\LogHydrationListener + formatter: Tmdb\Formatter\Hydration\SimpleHydrationFormatter + request_logging: + enabled: true + adapter: null # you can set different adapters for different logs, leave null to use the main adapter. + listener: Tmdb\Event\Listener\Logger\LogHttpMessageListener + formatter: Tmdb\Formatter\HttpMessage\SimpleHttpMessageFormatter + response_logging: + enabled: true + adapter: null # you can set different adapters for different logs, leave null to use the main adapter. + listener: Tmdb\Event\Listener\Logger\LogHttpMessageListener + formatter: Tmdb\Formatter\HttpMessage\SimpleHttpMessageFormatter + api_exception_logging: + enabled: true + adapter: null # you can set different adapters for different logs, leave null to use the main adapter. + listener: Tmdb\Event\Listener\Logger\LogApiErrorListener + formatter: Tmdb\Formatter\TmdbApiException\SimpleTmdbApiExceptionFormatter + client_exception_logging: + enabled: true + adapter: null # you can set different adapters for different logs, leave null to use the main adapter. + listener: Tmdb\Event\Listener\Logger\LogHttpMessageListener + formatter: Tmdb\Formatter\HttpMessage\SimpleHttpMessageFormatter ``` __Disable repositories :__ @@ -111,30 +179,75 @@ tmdb_symfony: enabled: false ``` +__Disable legacy aliases :__ + +_Set to true to remove all legacy alises ( e.g. `tmdb.client` or `tmdb.movie_repository` )._ + +```yaml +tmdb_symfony: + api_key: YOUR_API_KEY_HERE + disable_legacy_aliases: true +``` + __Full configuration with defaults :__ ```yaml tmdb_symfony: api_key: YOUR_API_KEY_HERE + cache: + enabled: true + adapter: cache.tmdb + log: + enabled: true + adapter: monolog.logger.tmdb + hydration: + enabled: true + with_hydration_data: false + adapter: null + listener: Tmdb\Event\Listener\Logger\LogHydrationListener + formatter: Tmdb\Formatter\Hydration\SimpleHydrationFormatter + request_logging: + enabled: true + adapter: null + listener: Tmdb\Event\Listener\Logger\LogHttpMessageListener + formatter: Tmdb\Formatter\HttpMessage\SimpleHttpMessageFormatter + response_logging: + enabled: true + adapter: null + listener: Tmdb\Event\Listener\Logger\LogHttpMessageListener + formatter: Tmdb\Formatter\HttpMessage\SimpleHttpMessageFormatter + api_exception_logging: + enabled: true + adapter: null + listener: Tmdb\Event\Listener\Logger\LogApiErrorListener + formatter: Tmdb\Formatter\TmdbApiException\SimpleTmdbApiExceptionFormatter + client_exception_logging: + enabled: true + adapter: null + listener: Tmdb\Event\Listener\Logger\LogHttpMessageListener + formatter: Tmdb\Formatter\HttpMessage\SimpleHttpMessageFormatter + options: + bearer_token: YOUR_BEARER_TOKEN_HERE + http: + client: Symfony\Component\HttpClient\Psr18Client + request_factory: Nyholm\Psr7\Factory\Psr17Factory + response_factory: Nyholm\Psr7\Factory\Psr17Factory + stream_factory: Nyholm\Psr7\Factory\Psr17Factory + uri_factory: Nyholm\Psr7\Factory\Psr17Factory + secure: true + host: api.themoviedb.org/3 + guest_session_token: null + event_dispatcher: + adapter: event_dispatcher + hydration: + event_listener_handles_hydration: false + only_for_specified_models: { } + api_token: YOUR_API_KEY_HERE # you don't have to set this if you set it at the root level + session_token: null repositories: - enabled: true # Set to false to disable repositories + enabled: true twig_extension: - enabled: true # Set to false to disable twig extensions - options: - adapter: null - secure: true # Set to false to disable https - host: "api.themoviedb.org/3/" - session_token: null - cache: - enabled: true # Set to false to disable cache - path: "%kernel.cache_dir%/themoviedb" - handler: null - subscriber: null - log: - enabled: false # Set to true to enable log - path: "%kernel.logs_dir%/themoviedb.log" - level: DEBUG - handler: null - subscriber: null + enabled: true + disable_legacy_aliases: false ``` Usage @@ -143,13 +256,13 @@ Usage Obtaining the client ```php -$client = $this->get('tmdb.client'); +$client = $this->get(Tmdb\Client::class); ``` Obtaining repositories ```php -$movie = $this->get('tmdb.movie_repository')->load(13); +$movie = $this->get(\Tmdb\Repository\MovieRepository::class)->load(13); ``` An overview of all the repositories can be found in the services configuration [repositories.xml](https://github.com/php-tmdb/symfony/blob/master/Resources/config/repositories.xml). @@ -163,3 +276,4 @@ There is also a Twig helper that makes use of the `Tmdb\Helper\ImageHelper` to o ``` **For all all other interactions take a look at [php-tmdb/api](https://github.com/php-tmdb/api).** + diff --git a/Resources/config/repositories.xml b/Resources/config/repositories.xml index c1390e5..d1848eb 100755 --- a/Resources/config/repositories.xml +++ b/Resources/config/repositories.xml @@ -4,110 +4,93 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - Tmdb\Repository\AuthenticationRepository - Tmdb\Repository\AccountRepository - Tmdb\Repository\CertificationRepository - Tmdb\Repository\ChangesRepository - Tmdb\Repository\CollectionRepository - Tmdb\Repository\CompanyRepository - Tmdb\Repository\ConfigurationRepository - Tmdb\Repository\CreditsRepository - Tmdb\Repository\DiscoverRepository - Tmdb\Repository\FindRepository - Tmdb\Repository\GenreRepository - Tmdb\Repository\JobsRepository - Tmdb\Repository\KeywordRepository - Tmdb\Repository\ListRepository - Tmdb\Repository\MovieRepository - Tmdb\Repository\NetworkRepository - Tmdb\Repository\PeopleRepository - Tmdb\Repository\ReviewRepository - Tmdb\Repository\SearchRepository - Tmdb\Repository\TvRepository - Tmdb\Repository\TvEpisodeRepository - Tmdb\Repository\TvSeasonRepository - - - - + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/Resources/config/services.xml b/Resources/config/services.xml index a32e439..e6777ca 100755 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -4,34 +4,93 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - - - - Tmdb\Client - Tmdb\SymfonyBundle\ClientConfiguration - Tmdb\ApiToken - Tmdb\RequestToken - Tmdb\SessionToken - - - - - + + + + + + %tmdb.api_token% + + + + %tmdb.bearer_token% - - %tmdb.api_key% + + - - - %tmdb.options% + + + + + + + + + + + + + + + - - - + + + + + + + + + + %tmdb.client.options% + + + + + + + + %tmdb.client.options% + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/config/twig.xml b/Resources/config/twig.xml index 13a908c..9c9a039 100755 --- a/Resources/config/twig.xml +++ b/Resources/config/twig.xml @@ -4,13 +4,9 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - - Tmdb\SymfonyBundle\Twig\TmdbExtension - - - - + + diff --git a/Resources/test/configuration.json b/Resources/test/configuration.json new file mode 100644 index 0000000..cda945d --- /dev/null +++ b/Resources/test/configuration.json @@ -0,0 +1,97 @@ +{ + "images": { + "base_url": "http://image.tmdb.org/t/p/", + "secure_base_url": "https://image.tmdb.org/t/p/", + "backdrop_sizes": [ + "w300", + "w780", + "w1280", + "original" + ], + "logo_sizes": [ + "w45", + "w92", + "w154", + "w185", + "w300", + "w500", + "original" + ], + "poster_sizes": [ + "w92", + "w154", + "w185", + "w342", + "w500", + "w780", + "original" + ], + "profile_sizes": [ + "w45", + "w185", + "h632", + "original" + ], + "still_sizes": [ + "w92", + "w185", + "w300", + "original" + ] + }, + "change_keys": [ + "adult", + "air_date", + "also_known_as", + "alternative_titles", + "biography", + "birthday", + "budget", + "cast", + "certifications", + "character_names", + "created_by", + "crew", + "deathday", + "episode", + "episode_number", + "episode_run_time", + "freebase_id", + "freebase_mid", + "general", + "genres", + "guest_stars", + "homepage", + "images", + "imdb_id", + "languages", + "name", + "network", + "origin_country", + "original_name", + "original_title", + "overview", + "parts", + "place_of_birth", + "plot_keywords", + "production_code", + "production_companies", + "production_countries", + "releases", + "revenue", + "runtime", + "season", + "season_number", + "season_regular", + "spoken_languages", + "status", + "tagline", + "title", + "translations", + "tvdb_id", + "tvrage_id", + "type", + "video", + "videos" + ] +} \ No newline at end of file diff --git a/Tests/DependencyInjection/CompilerPass/ConfigurationPassTest.php b/Tests/DependencyInjection/CompilerPass/ConfigurationPassTest.php new file mode 100644 index 0000000..f523a0e --- /dev/null +++ b/Tests/DependencyInjection/CompilerPass/ConfigurationPassTest.php @@ -0,0 +1,197 @@ +createFullConfiguration(); + $this->registerBasicServices($container); + + $pass = new ConfigurationPass(); + $pass->process($container); + + $this->doBasicAssertionsBasedOnFullOrMinimalConfig($container); + } + + /** + * @test + * @group DependencyInjection + */ + public function testProcessMinimalConfiguration() + { + $container = $this->createMinimalConfiguration(); + $this->registerBasicServices($container); + + $pass = new ConfigurationPass(); + $pass->process($container); + + $this->doBasicAssertionsBasedOnFullOrMinimalConfig($container); + } + + /** + * @test + * @group DependencyInjection + */ + public function testAutowiring() + { + $container = new ContainerBuilder(); + + $eventDispatcherMock = $this->createMock(EventDispatcherInterface::class); + $container->register(get_class($eventDispatcherMock))->addTag(TmdbSymfonyBundle::PSR14_EVENT_DISPATCHERS); + + $httpClientMock = $this->createMock(ClientInterface::class); + $container->register(get_class($httpClientMock))->addTag(TmdbSymfonyBundle::PSR18_CLIENTS); + + $requestFactoryMock = $this->createMock(RequestFactoryInterface::class); + $container->register(get_class($requestFactoryMock))->addTag(TmdbSymfonyBundle::PSR17_REQUEST_FACTORIES); + + $responseFactoryMock = $this->createMock(ResponseFactoryInterface::class); + $container->register(get_class($responseFactoryMock))->addTag(TmdbSymfonyBundle::PSR17_RESPONSE_FACTORIES); + + $streamFactoryMock = $this->createMock(StreamFactoryInterface::class); + $container->register(get_class($streamFactoryMock))->addTag(TmdbSymfonyBundle::PSR17_STREAM_FACTORIES); + + $uriFactoryMock = $this->createMock(UriFactoryInterface::class); + $container->register(get_class($uriFactoryMock))->addTag(TmdbSymfonyBundle::PSR17_URI_FACTORIES); + + $loader = new TmdbSymfonyExtension(); + $config = $this->getMinimalConfig(); + $loader->load([$config], $container); + + $pass = new ConfigurationPass(); + $pass->process($container); + + $this->assertAlias($container, get_class($eventDispatcherMock), EventDispatcherInterface::class); + $this->assertAlias($container, get_class($httpClientMock), ClientInterface::class); + $this->assertAlias($container, get_class($requestFactoryMock), RequestFactoryInterface::class); + $this->assertAlias($container, get_class($responseFactoryMock), ResponseFactoryInterface::class); + $this->assertAlias($container, get_class($streamFactoryMock), StreamFactoryInterface::class); + $this->assertAlias($container, get_class($uriFactoryMock), UriFactoryInterface::class); + } + + /** + * @test + * @group DependencyInjection + */ + public function testAutowiringFailsWithUndiscoveredServices() + { + $this->expectException(\RuntimeException::class); + $container = new ContainerBuilder(); + + $loader = new TmdbSymfonyExtension(); + $config = $this->getMinimalConfig(); + $loader->load([$config], $container); + + $pass = new ConfigurationPass(); + $pass->process($container); + } + + /** + * @test + * @group DependencyInjection + */ + public function testAutowiringFailsWithSeveralDiscoveredServices() + { + $this->expectException(\RuntimeException::class); + + $container = new ContainerBuilder(); + + $eventDispatcherMock = $this->createMock(EventDispatcherInterface::class); + // mocking the same interface results in the same object? Doesn't matter for test though. + $eventDispatcherMockTwo = $this->createMock(ClientInterface::class); + + $container->register(get_class($eventDispatcherMock))->addTag(TmdbSymfonyBundle::PSR14_EVENT_DISPATCHERS); + $container->register(get_class($eventDispatcherMockTwo))->addTag(TmdbSymfonyBundle::PSR14_EVENT_DISPATCHERS); + + $loader = new TmdbSymfonyExtension(); + $config = $this->getMinimalConfig(); + $loader->load([$config], $container); + + $pass = new ConfigurationPass(); + $pass->process($container); + } + + /** + * @test + * @group DependencyInjection + */ + public function testProcessBearerToken() + { + $config = $this->getFullConfig(); + $config['options']['bearer_token'] = 'bearer_token'; + + $container = $this->createManualConfiguration($config); + $this->registerBasicServices($container); + + $pass = new ConfigurationPass(); + $pass->process($container); + + $this->assertEquals( + BearerToken::class, + $container->getDefinition('Tmdb\SymfonyBundle\ClientConfiguration')->getArgument(0)->__toString() + ); + } + + /** + * @param ContainerBuilder $container + */ + protected function registerBasicServices(ContainerBuilder $container) + { + $container->register(EventDispatcherInterface::class); + $container->register(ClientInterface::class); + $container->register(RequestFactoryInterface::class); + $container->register(ResponseFactoryInterface::class); + $container->register(StreamFactoryInterface::class); + $container->register(UriFactoryInterface::class); + } + + /** + * @param ContainerBuilder $container + */ + protected function doBasicAssertionsBasedOnFullOrMinimalConfig(ContainerBuilder $container) + { + $this->assertClientConfigurationEquals($container, ApiToken::class, 0); + $this->assertClientConfigurationEquals($container, EventDispatcherInterface::class, 1); + $this->assertClientConfigurationEquals($container, ClientInterface::class, 2); + $this->assertClientConfigurationEquals($container, RequestFactoryInterface::class, 3); + $this->assertClientConfigurationEquals($container, ResponseFactoryInterface::class, 4); + $this->assertClientConfigurationEquals($container, StreamFactoryInterface::class, 5); + $this->assertClientConfigurationEquals($container, UriFactoryInterface::class, 6); + } + + /** + * @param ContainerBuilder $container + * @param string $expectedServiceId + * @param int $argument + */ + protected function assertClientConfigurationEquals( + ContainerBuilder $container, + string $expectedServiceId, + int $argument + ) { + $this->assertEquals( + $expectedServiceId, + $container->getDefinition('Tmdb\SymfonyBundle\ClientConfiguration')->getArgument($argument)->__toString() + ); + } +} diff --git a/Tests/DependencyInjection/CompilerPass/EventDispatchingPassTest.php b/Tests/DependencyInjection/CompilerPass/EventDispatchingPassTest.php new file mode 100644 index 0000000..b472e5b --- /dev/null +++ b/Tests/DependencyInjection/CompilerPass/EventDispatchingPassTest.php @@ -0,0 +1,341 @@ +setParameter('kernel.debug', true); + + $loader = new TmdbSymfonyExtension(); + $config = $this->getFullConfig(); + + $this->registerBasicServices($container); + $this->registerListenerServices($container); + $config['options']['event_dispatcher']['adapter'] = EventDispatcher::class; + + $loader->load([$config], $container); + + $pass = new ConfigurationPass(); + $pass->process($container); + + $pass = new EventDispatchingPass(); + $pass->process($container); + + $container->compile(); + $this->doAssertCountListenersRegistered( + $container, + 5, + 1, + 1, + 1, + 1 + ); + } + + /** + * @test + * @group DependencyInjection + */ + public function testProcessFullConfigurationWithSingleLogItemDisabled() + { + $container = $this->containerWithConfig([ + 'log' => [ + 'request_logging' => [ + 'enabled' => false + ] + ] + ]); + + $container->compile(); + $this->doAssertCountListenersRegistered( + $container, + 4, + 1, + 1, + 1, + 1 + ); + } + + /** + * @test + * @group DependencyInjection + */ + public function testProcessMinimalConfiguration() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', true); + + $loader = new TmdbSymfonyExtension(); + $config = $this->getMinimalConfig(); + + $this->registerBasicServices($container); + $this->registerListenerServices($container); + + $loader->load([$config], $container); + + $pass = new ConfigurationPass(); + $pass->process($container); + + $pass = new EventDispatchingPass(); + $pass->process($container); + + $container->compile(); + $this->doAssertCountListenersRegistered( + $container, + 4 + ); + } + + /** + * @test + * @group DependencyInjection + */ + public function testBearerToken() + { + $container = $this->containerWithConfig(['options' => ['bearer_token' => 'foobar']]); + + $definition = $container->getDefinition(ApiTokenRequestListener::class); + $this->assertEquals(BearerToken::class, $definition->getArgument(0)->__toString()); + } + + /** + * @test + * @group DependencyInjection + */ + public function testWithFaultyAdapter() + { + $this->expectException(\RuntimeException::class); + + $this->containerWithConfig(['log' => ['request_logging' => ['adapter' => 'foobar']]]); + } + + /** + * @test + * @group DependencyInjection + */ + public function testWithFaultyFormatter() + { + $this->expectException(\RuntimeException::class); + + $this->containerWithConfig(['log' => ['request_logging' => ['formatter' => 'foobar']]]); + } + + /** + * @test + * @group DependencyInjection + */ + public function testWithFaultyListener() + { + $this->expectException(\RuntimeException::class); + + $this->containerWithConfig(['log' => ['request_logging' => ['listener' => 'foobar']]]); + } + + /** + * @test + * @group DependencyInjection + */ + public function testWithLogItemAliases() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', true); + + $loader = new TmdbSymfonyExtension(); + $config = $this->getFullConfig(); + + $this->registerBasicServices($container); + $this->registerListenerServices($container); + $config['options']['event_dispatcher']['adapter'] = EventDispatcher::class; + + $loader->load([$config], $container); + + $pass = new ConfigurationPass(); + $pass->process($container); + + $pass = new EventDispatchingPass(); + $pass->process($container); + + $container->compile(); + $this->doAssertCountListenersRegistered( + $container, + 5, + 1, + 1, + 1, + 1 + ); + } + + /** + * @param array $faulty + * @return ContainerBuilder + */ + private function containerWithConfig(array $faulty = []) + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.debug', true); + + $loader = new TmdbSymfonyExtension(); + $config = $this->getFullConfig(); + + $this->registerBasicServices($container); + $this->registerListenerServices($container); + $config['options']['event_dispatcher']['adapter'] = EventDispatcher::class; + + $loader->load([array_merge($config, $faulty)], $container); + + $pass = new ConfigurationPass(); + $pass->process($container); + + $pass = new EventDispatchingPass(); + $pass->process($container); + + return $container; + } + + /** + * @param ContainerBuilder $container + */ + protected function registerBasicServices(ContainerBuilder $container) + { + $container->register(EventDispatcher::class, EventDispatcher::class)->addTag( + TmdbSymfonyBundle::PSR14_EVENT_DISPATCHERS + ); + $container->setAlias(EventDispatcherInterface::class, EventDispatcher::class); + + $httpClientMock = $this->createMock(ClientInterface::class); + $container->register(get_class($httpClientMock), get_class($httpClientMock))->addTag( + TmdbSymfonyBundle::PSR18_CLIENTS + ); + + $requestFactoryMock = $this->createMock(RequestFactoryInterface::class); + $container->register(get_class($requestFactoryMock), get_class($requestFactoryMock))->addTag( + TmdbSymfonyBundle::PSR17_REQUEST_FACTORIES + ); + + $responseFactoryMock = $this->createMock(ResponseFactoryInterface::class); + $container->register(get_class($responseFactoryMock), get_class($responseFactoryMock))->addTag( + TmdbSymfonyBundle::PSR17_RESPONSE_FACTORIES + ); + + $streamFactoryMock = $this->createMock(StreamFactoryInterface::class); + $container->register(get_class($streamFactoryMock), get_class($streamFactoryMock))->addTag( + TmdbSymfonyBundle::PSR17_STREAM_FACTORIES + ); + + $uriFactoryMock = $this->createMock(UriFactoryInterface::class); + $container->register(get_class($uriFactoryMock), get_class($uriFactoryMock))->addTag( + TmdbSymfonyBundle::PSR17_URI_FACTORIES + ); + + $cacheItemPoolMock = $this->createMock(CacheItemPoolInterface::class); + $container->register(get_class($cacheItemPoolMock), get_class($cacheItemPoolMock)); + $container->setAlias(CacheItemPoolInterface::class, get_class($cacheItemPoolMock)); + + $loggerMock = $this->createMock(LoggerInterface::class); + $container->register(get_class($loggerMock), get_class($loggerMock)); + $container->setAlias(LoggerInterface::class, get_class($loggerMock)); + } + + /** + * @param ContainerBuilder $container + */ + protected function registerListenerServices(ContainerBuilder $container) + { + $container->register(RequestListener::class, RequestListener::class); + $container->register(Psr6CachedRequestListener::class, Psr6CachedRequestListener::class); + $container->register(HydrationListener::class, HydrationListener::class); + $container->register(AcceptJsonRequestListener::class, AcceptJsonRequestListener::class); + $container->register(ContentTypeJsonRequestListener::class, ContentTypeJsonRequestListener::class); + $container->register(ApiTokenRequestListener::class, ApiTokenRequestListener::class); + $container->register(SessionTokenRequestListener::class, SessionTokenRequestListener::class); + } + + /** + * @param ContainerBuilder $container + * @param int $beforeRequestEventCount + * @param int $responseEventCount + * @param int $httpClientExceptionEventCount + * @param int $tmdbExceptionEventCount + * @param int $beforeHydrationEventCount + * @throws Exception + */ + protected function doAssertCountListenersRegistered( + ContainerBuilder $container, + int $beforeRequestEventCount = 0, + int $responseEventCount = 0, + int $httpClientExceptionEventCount = 0, + int $tmdbExceptionEventCount = 0, + int $beforeHydrationEventCount = 0 + ) { + /** @var Client $client */ + $client = $container->get(Client::class); + + /** @var EventDispatcher $eventDispatcher */ + $eventDispatcher = $client->getEventDispatcher(); + + $this->assertEquals( + $beforeRequestEventCount, + count($eventDispatcher->getListeners(BeforeRequestEvent::class)) + ); + + $this->assertEquals( + $responseEventCount, + count($eventDispatcher->getListeners(ResponseEvent::class)) + ); + + $this->assertEquals( + $httpClientExceptionEventCount, + count($eventDispatcher->getListeners(HttpClientExceptionEvent::class)) + ); + + $this->assertEquals( + $tmdbExceptionEventCount, + count($eventDispatcher->getListeners(TmdbExceptionEvent::class)) + ); + + $this->assertEquals( + $beforeHydrationEventCount, + count($eventDispatcher->getListeners(BeforeHydrationEvent::class)) + ); + } +} diff --git a/Tests/DependencyInjection/TestCase.php b/Tests/DependencyInjection/TestCase.php new file mode 100644 index 0000000..d093b2a --- /dev/null +++ b/Tests/DependencyInjection/TestCase.php @@ -0,0 +1,212 @@ +assertTrue(($this->container->hasDefinition($id) ?: $this->container->hasAlias($id))); + } + + /** + * @param ContainerBuilder $container + * @param string $value + * @param string $key + */ + protected function assertAlias(ContainerBuilder $container, $value, $key): void + { + $this->assertSame($value, (string)$container->getAlias($key), sprintf('%s alias is correct', $key)); + } + + /** + * @param string $key + */ + protected function assertNotAlias($key): void + { + $this->assertFalse( + $this->container->hasAlias($key), + sprintf('%s alias is expected not to be registered', $key) + ); + } + + /** + * @param string $id + */ + protected function assertNotHasDefinition($id): void + { + $this->assertFalse(($this->container->hasDefinition($id) ?: $this->container->hasAlias($id))); + } + + protected function tearDown(): void + { + $this->container = null; + } + + /** + * @param mixed $value + * @param string $key + */ + protected function assertParameter($value, $key): void + { + $this->assertSame($value, $this->container->getParameter($key), sprintf('%s parameter is correct', $key)); + } + + /** + * @return ContainerBuilder + */ + protected function createEmptyConfiguration(): ContainerBuilder + { + $this->container = new ContainerBuilder(); + $loader = new TmdbSymfonyExtension(); + $config = $this->getEmptyConfig(); + $loader->load([$config], $this->container); + + return $this->container; + } + + /** + * getEmptyConfig. + * + * @return array + */ + protected function getEmptyConfig(): array + { + return []; + } + + /** + * @param array $config + * @return ContainerBuilder + */ + protected function createManualConfiguration(array $config = array()): ContainerBuilder + { + $this->container = new ContainerBuilder(); + $loader = new TmdbSymfonyExtension(); + $loader->load([$config], $this->container); + + return $this->container; + } + + /** + * @return ContainerBuilder + */ + protected function createMinimalConfiguration(): ContainerBuilder + { + $this->container = new ContainerBuilder(); + $loader = new TmdbSymfonyExtension(); + $config = $this->getMinimalConfig(); + $loader->load([$config], $this->container); + + return $this->container; + } + + /** + * getEmptyConfig. + * + * @return array + */ + protected function getMinimalConfig(): array + { + $yaml = <<parse($yaml); + } + + /** + * @return ContainerBuilder + */ + protected function createFullConfiguration(): ContainerBuilder + { + $this->container = new ContainerBuilder(); + $loader = new TmdbSymfonyExtension(); + $config = $this->getFullConfig(); + $loader->load([$config], $this->container); + + return $this->container; + } + + /** + * @return mixed + */ + protected function getFullConfig(): array + { + $yaml = <<parse($yaml); + } +} diff --git a/Tests/DependencyInjection/TmdbSymfonyExtensionTest.php b/Tests/DependencyInjection/TmdbSymfonyExtensionTest.php index 9182d92..d731d69 100644 --- a/Tests/DependencyInjection/TmdbSymfonyExtensionTest.php +++ b/Tests/DependencyInjection/TmdbSymfonyExtensionTest.php @@ -2,9 +2,9 @@ namespace Tmdb\SymfonyBundle\Tests\DependencyInjection; -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\Container; -use Tmdb\SymfonyBundle\Tests\TestKernel; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Tmdb\SymfonyBundle\DependencyInjection\TmdbSymfonyExtension; final class TmdbSymfonyExtensionTest extends TestCase { @@ -12,14 +12,113 @@ final class TmdbSymfonyExtensionTest extends TestCase * @test * @group DependencyInjection */ - public function all_tmdb_services_can_be_loaded() + public function testDefaultConfigurationWithoutApiKeyThrowsException(): void { - $kernel = new TestKernel('test', true); - $kernel->boot(); + $this->expectException(InvalidConfigurationException::class); + $loader = new TmdbSymfonyExtension(); + $config = $this->getEmptyConfig(); + $loader->load([$config], new ContainerBuilder()); + } + + /** + * @test + * @group DependencyInjection + */ + public function testDefaultConfigurationWithApiKey(): void + { + $this->container = new ContainerBuilder(); + $loader = new TmdbSymfonyExtension(); + $config = $this->getMinimalConfig(); + $loader->load([$config], $this->container); + + $this->assertHasDefinition('Tmdb\Client'); + $this->assertHasDefinition('Tmdb\Repository\MovieRepository'); + $this->assertHasDefinition('Tmdb\SymfonyBundle\Twig\TmdbExtension'); + } + + /** + * @test + * @group DependencyInjection + */ + public function testDefaultConfigurationHasLegacyAliases(): void + { + $this->container = new ContainerBuilder(); + $loader = new TmdbSymfonyExtension(); + $config = $this->getMinimalConfig(); + $loader->load([$config], $this->container); + + $this->assertAlias($this->container, 'Tmdb\Client', 'tmdb.client'); + $this->assertAlias($this->container, 'Tmdb\Repository\MovieRepository', 'tmdb.movie_repository'); + $this->assertAlias($this->container, 'Tmdb\SymfonyBundle\Twig\TmdbExtension', 'tmdb.twig.image_extension'); + } + + /** + * @test + * @group DependencyInjection + */ + public function testDisablingRepositories(): void + { + $this->container = new ContainerBuilder(); + $loader = new TmdbSymfonyExtension(); + $config = $this->getMinimalConfig(); + $config['repositories']['enabled'] = false; + $loader->load([$config], $this->container); + + $this->assertAlias($this->container, 'Tmdb\Client', 'tmdb.client'); + $this->assertNotAlias('tmdb.movie_repository'); + $this->assertNotHasDefinition('Tmdb\Repository\MovieRepository'); + } + + /** + * @test + * @group DependencyInjection + */ + public function testDisablingTwig(): void + { + $this->container = new ContainerBuilder(); + $loader = new TmdbSymfonyExtension(); + $config = $this->getMinimalConfig(); + $config['twig_extension']['enabled'] = false; + $loader->load([$config], $this->container); + + $this->assertAlias($this->container, 'Tmdb\Client', 'tmdb.client'); + $this->assertHasDefinition('Tmdb\Repository\MovieRepository'); + $this->assertNotHasDefinition('Tmdb\SymfonyBundle\Twig\TmdbExtension'); + } + + /** + * @test + * @group DependencyInjection + */ + public function testDisablingLegacyAliasesRemovesLegacyAliases(): void + { + $this->container = new ContainerBuilder(); + $loader = new TmdbSymfonyExtension(); + $config = $this->getMinimalConfig(); + $config['disable_legacy_aliases'] = true; + $loader->load([$config], $this->container); + + $this->assertNotAlias('tmdb.client'); + $this->assertNotAlias('tmdb.movie_repository'); + $this->assertNotAlias('tmdb.twig.image_extension'); + } + + /** + * @test + * @group DependencyInjection + */ + public function testLegacyMappingMapsCorrectly(): void + { + $this->container = new ContainerBuilder(); + $loader = new TmdbSymfonyExtension(); + $config = $this->getMinimalConfig(); + $loader->load([$config], $this->container); - /** @var Container $container */ - $container = $kernel->getContainer(); - $this->assertInstanceOf('Tmdb\Client', $container->get('tmdb.client')); - $this->assertInstanceOf('Tmdb\Repository\MovieRepository', $container->get('tmdb.movie_repository')); + foreach ($loader->getLegacyAliasMapping() as $group => $mapping) { + foreach ($mapping as $alias => $serviceIdentifier) { + $this->assertHasDefinition($serviceIdentifier); + $this->assertAlias($this->container, $serviceIdentifier, $alias); + } + } } } diff --git a/Tests/TestKernel.php b/Tests/TestKernel.php deleted file mode 100644 index 0249a58..0000000 --- a/Tests/TestKernel.php +++ /dev/null @@ -1,39 +0,0 @@ -load(__DIR__ . '/config.yml'); - } - - public function getRootDir() - { - return sys_get_temp_dir() . '/php-tmdb-symfony-test'; - } - - public function getCacheDir() - { - return $this->getRootDir() . '/cache'; - } - - public function getLogDir() - { - return $this->getRootDir() . '/logs'; - } -} diff --git a/Tests/Twig/TmdbExtensionTest.php b/Tests/Twig/TmdbExtensionTest.php new file mode 100644 index 0000000..4124a0d --- /dev/null +++ b/Tests/Twig/TmdbExtensionTest.php @@ -0,0 +1,105 @@ +createMock(Client::class); + $responseData = json_decode( + file_get_contents(__DIR__ . '/../../Resources/test/configuration.json'), + true + ); + + $configuration = new Configuration(); + $configuration->setImages($responseData['images']); + + $helper = new ImageHelper($configuration); + + $extension = new TmdbExtension($client); + $this->assertEquals($client, $extension->getClient()); + + $extension->setHelper($helper); + $extension->setClient($client); + $this->assertEquals($client, $extension->getClient()); + + $image = new Image(); + $image + ->setAspectRatio(1) + ->setFilePath('/foo.jpg') + ->setHeight(null) + ->setWidth(null) + ->setIso6391('foobar') + ->setMedia('dunno') + ->setVoteAverage(4.7) + ->setVoteCount(666); + + $this->assertEquals('//image.tmdb.org/t/p/original/foo.jpg', $extension->getUrl($image)); + $this->assertEquals( + '', + $extension->getHtml($image) + ); + $this->assertEquals('tmdb_extension', $extension->getName()); + $this->assertEquals(2, count($extension->getFilters())); + } + + /** + * @test + * @group Twig + */ + public function testRepository() + { + $client = $this->createMock(Client::class); + $responseData = json_decode( + file_get_contents(__DIR__ . '/../../Resources/test/configuration.json'), + true + ); + + $configuration = new Configuration(); + $configuration->setImages($responseData['images']); + + $helper = new ImageHelper($configuration); + + $repository = $this->getMockBuilder(AbstractRepository::class) + ->disableOriginalConstructor() + ->setMethods(['load', 'getApi', 'getFactory']) + ->getMock() + ; + + $repository->method('load')->willReturn($configuration); + + $extension = new TmdbExtension($client, $repository); + $this->assertEquals($client, $extension->getClient()); + + $image = new Image(); + $image + ->setAspectRatio(1) + ->setFilePath('/foo.jpg') + ->setHeight(null) + ->setWidth(null) + ->setIso6391('foobar') + ->setMedia('dunno') + ->setVoteAverage(4.7) + ->setVoteCount(666); + + $this->assertEquals('//image.tmdb.org/t/p/original/foo.jpg', $extension->getUrl($image)); + $this->assertEquals( + '', + $extension->getHtml($image) + ); + $this->assertEquals('tmdb_extension', $extension->getName()); + $this->assertEquals(2, count($extension->getFilters())); + } +} diff --git a/Tests/config.yml b/Tests/config.yml deleted file mode 100644 index 8d8d68c..0000000 --- a/Tests/config.yml +++ /dev/null @@ -1,4 +0,0 @@ -framework: - secret: NopeChuckTesta -tmdb_symfony: - api_key: invalidapikey diff --git a/TmdbSymfonyBundle.php b/TmdbSymfonyBundle.php index 7bcec9c..cf29829 100644 --- a/TmdbSymfonyBundle.php +++ b/TmdbSymfonyBundle.php @@ -2,8 +2,56 @@ namespace Tmdb\SymfonyBundle; +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\UriFactoryInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Tmdb\SymfonyBundle\DependencyInjection\CompilerPass\ConfigurationPass; +use Tmdb\SymfonyBundle\DependencyInjection\CompilerPass\EventDispatchingPass; +/** + * Class TmdbSymfonyBundle + * @package Tmdb\SymfonyBundle + * @codeCoverageIgnore + */ class TmdbSymfonyBundle extends Bundle { + public const VERSION = '4.0.0'; + public const PSR18_CLIENTS = 'tmdb_symfony.psr18.clients'; + public const PSR17_REQUEST_FACTORIES = 'tmdb_symfony.psr17.request_factories'; + public const PSR17_RESPONSE_FACTORIES = 'tmdb_symfony.psr17.response_factories'; + public const PSR17_STREAM_FACTORIES = 'tmdb_symfony.psr17.stream_factories'; + public const PSR17_URI_FACTORIES = 'tmdb_symfony.psr17.uri_factories'; + public const PSR14_EVENT_DISPATCHERS = 'tmdb_symfony.psr17.event_dispatchers'; + + /** + * @param ContainerBuilder $container + */ + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container->addCompilerPass(new ConfigurationPass()); + $container->addCompilerPass(new EventDispatchingPass()); + + $targets = [ + ClientInterface::class => self::PSR18_CLIENTS, + RequestFactoryInterface::class => self::PSR17_REQUEST_FACTORIES, + ResponseFactoryInterface::class => self::PSR17_RESPONSE_FACTORIES, + StreamFactoryInterface::class => self::PSR17_STREAM_FACTORIES, + UriFactoryInterface::class => self::PSR17_URI_FACTORIES, + EventDispatcherInterface::class => self::PSR14_EVENT_DISPATCHERS + ]; + + foreach ($targets as $interface => $tag) { + $container + ->registerForAutoconfiguration($interface) + ->addTag($tag) + ; + } + } } diff --git a/Twig/TmdbExtension.php b/Twig/TmdbExtension.php index 6de674e..673ac8d 100644 --- a/Twig/TmdbExtension.php +++ b/Twig/TmdbExtension.php @@ -1,8 +1,10 @@ client = $client; + $this->repository = $repository ?? new ConfigurationRepository($client); } + /** + * @return array|TwigFilter[] + */ public function getFilters() { return array( @@ -32,26 +48,41 @@ public function getFilters() ); } - public function getHtml($image, $size = 'original', $width = null, $height = null) + /** + * @param string $image + * @param string $size + * @param int|null $width + * @param int|null $height + * @return string + */ + public function getHtml(string $image, string $size = 'original', int $width = null, int $height = null): string { return $this->getHelper()->getHtml($image, $size, $width, $height); } - public function getUrl($image, $size = 'original') + /** + * @param string $image + * @param string $size + * @return string + */ + public function getUrl(string $image, string $size = 'original'): string { return $this->getHelper()->getUrl($image, $size); } - public function getName() + /** + * @return string + */ + public function getName(): string { return 'tmdb_extension'; } /** - * @param null $client + * @param Client $client * @return $this */ - public function setClient($client) + public function setClient(Client $client) { $this->client = $client; @@ -59,9 +90,9 @@ public function setClient($client) } /** - * @return null + * @return Client|null */ - public function getClient() + public function getClient(): ?Client { return $this->client; } @@ -86,10 +117,7 @@ public function getHelper() return $this->helper; } - $repository = new ConfigurationRepository($this->client); - $config = $repository->load(); - - $this->helper = new ImageHelper($config); + $this->helper = new ImageHelper($this->repository->load()); return $this->helper; } diff --git a/composer.json b/composer.json index bd56246..eab7454 100755 --- a/composer.json +++ b/composer.json @@ -3,8 +3,8 @@ "license": "MIT", "type": "symfony-bundle", "description": "Symfony Bundle for TMDB (The Movie Database) API. Provides easy access to the php-tmdb/api library.", - "homepage": "https://github.com/wtfzdotnet/php-tmdb-api", - "keywords": ["tmdb", "api", "php","wrapper", "movie", "cinema", "tv", "tv show", "tvdb", "symfony", "symfony2", "symfony3"], + "homepage": "https://github.com/php-tmdb/symfony", + "keywords": ["tmdb", "api", "php","wrapper", "movie", "cinema", "tv", "tv show", "tvdb", "symfony", "symfony4", "symfony5"], "authors": [ { "name": "Michael Roterman", @@ -13,18 +13,34 @@ } ], "require": { - "php": ">=5.5.0", - "symfony/config": "^4.3.7 || ^5.0", - "symfony/dependency-injection": "^4.3.7 || ^5.0", - "symfony/event-dispatcher": "^4.3.7 || ^5.0", - "symfony/http-kernel": "^4.3.7 || ^5.0", + "php": "^7.3 || ^8.0", + "php-tmdb/api": "^4", + "symfony/config": "^4.3.7 || <6", + "symfony/dependency-injection": "^4.3.7 || <6", + "symfony/event-dispatcher": "^4.3.7 || <6", + "symfony/http-kernel": "^4.3.7 || >=5.1.5", + "symfony/phpunit-bridge": "^4.2", "symfony/yaml": "^4.3.7 || ^5.0", - "php-tmdb/api": "^3.0", - "twig/twig": "^2.0|^3.0" + "twig/twig": "^2.0 || ^3.0" + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml coverage", + "test-coverage": "php -d xdebug.mode=coverage vendor/bin/phpunit --coverage-html build/coverage", + "test-cs": "vendor/bin/phpcs", + "test-phpstan": "vendor/bin/phpstan analyse -c phpstan.neon . --level 7 --no-progress", + "test-psalm": "vendor/bin/psalm --show-info=true ." }, "require-dev": { - "phpunit/phpunit": ">=5.7", - "symfony/framework-bundle": "^4.3.7 || ^5.0" + "nyholm/psr7": "^1.2", + "slevomat/coding-standard": "^6.4.1", + "squizlabs/php_codesniffer": "^3.5.8", + "php-http/guzzle7-adapter": "^0.1", + "phpstan/phpstan": "^0.12.18", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.3", + "symfony/framework-bundle": "^4.3.7 || ^5.0", + "vimeo/psalm": "^4", + "php-http/cache-plugin": "^1.7" }, "autoload": { "psr-4": { "Tmdb\\SymfonyBundle\\": "" } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..c851d73 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,13 @@ + + + + + + + + + + . + vendor/* + + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..7450740 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,10 @@ +parameters: + level: 2 + inferPrivatePropertyTypeFromConstructor: true + ignoreErrors: + - '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::children\(\)\.#' + paths: + - . + excludes_analyse: + - Tests + - vendor diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 133e76d..5d6685c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,16 +1,25 @@ - - - - ./Tests/ - - + + + + + ./ + + + ./Resources + ./Tests + ./vendor + + + + + + + + + + + + ./Tests/ + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..d1fc7bf --- /dev/null +++ b/psalm.xml @@ -0,0 +1,16 @@ + + + + + + + + + +