diff --git a/README.md b/README.md index 577dc14..ca366ee 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,13 @@ [![](https://img.shields.io/travis/terminal42/contao-url-rewrite/master.svg?style=flat-square)](https://travis-ci.org/terminal42/contao-url-rewrite/) [![](https://img.shields.io/coveralls/terminal42/contao-url-rewrite/master.svg?style=flat-square)](https://coveralls.io/github/terminal42/contao-url-rewrite) -The extension provides a new backend module for Contao that allows to set various URL rewrites. Behind the scenes -the rules are added as routes to the internal application router which allows to use all the features provided -by the Symfony Routing component. +The extension provides a new way for Contao to set various URL rewrites. The available config providers are: + +- Bundle config provider – the entries are taken from `config.yml` file +- Database provider – the entries are taken from backend module + +Behind the scenes the rules are added as routes to the internal application router which allows to use all the features +provided by the Symfony Routing component. ## Installation @@ -18,6 +22,38 @@ composer require terminal42/contao-url-rewrite ## Configuration +### Bundle configuration + +The bundle configuration is optional. Here you can define the entries and disable the backend management module. + +```yaml +# config/config.yml +terminal42_url_rewrite: + backend_management: false # Disable backend management of entries (true by default) + entries: # Optional entries + - + request: { path: 'find/{address}' } + response: { code: 303, uri: 'https://www.google.com/maps?q={address}' } + + - + request: + path: 'news/{news}' + requirements: {news: '\d+'} + response: + code: 301 + uri: '{{news_url::{news}|absolute}}' + + - + request: + path: 'home.php' + hosts: ['localhost'] + condition: "context.getMethod() == 'GET' and request.query.has('page')" + response: + uri: '{{link_url::{page}|absolute}}' +``` + +### Running under non Contao managed edition + If you are running the Contao Managed Edition then the extension should work out of the box. For all the other systems you have to additionally register the routing configuration in the config files: @@ -65,6 +101,21 @@ Response URI: {{link_url::{page}|absolute}} Result: domain.tld/home.php?page=123 → domain.tld/foobar-123.html ``` +## Create a custom config provider + +In addition to the existing providers you can create your own class that provides the rewrite configurations. +The new service must extend the [Terminal42\UrlRewriteBundle\ConfigProvider\ConfigProviderInterface](src/ConfigProvider/ConfigProviderInterface.php) +interface and be registered with the appropriate tag: + +```yaml +services: + app.my_rewrite_provider: + class: AppBundle\RewriteProvider\MyRewriteProvider + public: false + tags: + - { name: terminal42_url_rewrite.provider, priority: 128 } +``` + ## Resources 1. [Symfony Routing](https://symfony.com/doc/current/routing.html) diff --git a/src/ConfigProvider/BundleConfigProvider.php b/src/ConfigProvider/BundleConfigProvider.php new file mode 100644 index 0000000..6511a2d --- /dev/null +++ b/src/ConfigProvider/BundleConfigProvider.php @@ -0,0 +1,103 @@ + + * @license MIT + */ + +namespace Terminal42\UrlRewriteBundle\ConfigProvider; + +use Terminal42\UrlRewriteBundle\RewriteConfig; +use Terminal42\UrlRewriteBundle\RewriteConfigInterface; + +class BundleConfigProvider implements ConfigProviderInterface +{ + /** + * @var array + */ + private $entries = []; + + /** + * BundleConfigProvider constructor. + * + * @param array $entries + */ + public function __construct(array $entries = []) + { + $this->entries = $entries; + } + + /** + * {@inheritdoc} + */ + public function find(string $id): ?RewriteConfigInterface + { + if (!array_key_exists($id, $this->entries)) { + return null; + } + + return $this->createConfig($id, $this->entries[$id]); + } + + /** + * {@inheritdoc} + */ + public function findAll(): array + { + if (count($this->entries) === 0) { + return []; + } + + $configs = []; + + foreach ($this->entries as $id => $entry) { + if (($config = $this->createConfig((string) $id, $entry)) !== null) { + $configs[] = $config; + } + } + + return $configs; + } + + /** + * Create the config. + * + * @param string $id + * @param array $data + * + * @return null|RewriteConfig + */ + private function createConfig(string $id, array $data): ?RewriteConfig + { + if (!isset($data['request']['path'], $data['response']['code'])) { + return null; + } + + $config = new RewriteConfig($id, $data['request']['path'], (int) $data['response']['code']); + + // Request hosts + if (isset($data['request']['hosts'])) { + $config->setRequestHosts($data['request']['hosts']); + } + + // Request condition + if (isset($data['request']['condition'])) { + $config->setRequestCondition($data['request']['condition']); + } + + // Request requirements + if (isset($data['request']['requirements'])) { + $config->setRequestRequirements($data['request']['requirements']); + } + + // Response URI + if (isset($data['response']['uri'])) { + $config->setResponseUri($data['response']['uri']); + } + + return $config; + } +} diff --git a/src/ConfigProvider/ChainConfigProvider.php b/src/ConfigProvider/ChainConfigProvider.php new file mode 100644 index 0000000..b9b479e --- /dev/null +++ b/src/ConfigProvider/ChainConfigProvider.php @@ -0,0 +1,82 @@ + + * @license MIT + */ + +namespace Terminal42\UrlRewriteBundle\ConfigProvider; + +use Terminal42\UrlRewriteBundle\RewriteConfigInterface; + +class ChainConfigProvider implements ConfigProviderInterface +{ + /** + * @var array + */ + private $providers = []; + + /** + * Add the config provider. + * + * @param ConfigProviderInterface $provider + */ + public function addProvider(ConfigProviderInterface $provider): void + { + $this->providers[] = $provider; + } + + /** + * {@inheritdoc} + */ + public function find(string $id): ?RewriteConfigInterface + { + list($class, $id) = explode(':', $id); + + /** @var ConfigProviderInterface $provider */ + foreach ($this->providers as $provider) { + if ($class === $this->getProviderIdentifier($provider) && ($config = $provider->find($id)) !== null) { + return $config; + } + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function findAll(): array + { + $configs = []; + + /** @var ConfigProviderInterface $provider */ + foreach ($this->providers as $provider) { + $providerConfigs = $provider->findAll(); + + /** @var RewriteConfigInterface $config */ + foreach ($providerConfigs as $config) { + $config->setIdentifier($this->getProviderIdentifier($provider) . ':' . $config->getIdentifier()); + } + + $configs = array_merge($configs, $providerConfigs); + } + + return $configs; + } + + /** + * Get the provider identifier + * + * @param ConfigProviderInterface $provider + * + * @return string + */ + private function getProviderIdentifier(ConfigProviderInterface $provider): string + { + return get_class($provider); + } +} diff --git a/src/ConfigProvider/ConfigProviderInterface.php b/src/ConfigProvider/ConfigProviderInterface.php new file mode 100644 index 0000000..ec6b93b --- /dev/null +++ b/src/ConfigProvider/ConfigProviderInterface.php @@ -0,0 +1,32 @@ + + * @license MIT + */ + +namespace Terminal42\UrlRewriteBundle\ConfigProvider; + +use Terminal42\UrlRewriteBundle\RewriteConfigInterface; + +interface ConfigProviderInterface +{ + /** + * Find the config. + * + * @param string $id + * + * @return RewriteConfigInterface|null + */ + public function find(string $id): ?RewriteConfigInterface; + + /** + * Find all configs. + * + * @return array + */ + public function findAll(): array; +} diff --git a/src/ConfigProvider/DatabaseConfigProvider.php b/src/ConfigProvider/DatabaseConfigProvider.php new file mode 100644 index 0000000..ce181a5 --- /dev/null +++ b/src/ConfigProvider/DatabaseConfigProvider.php @@ -0,0 +1,133 @@ + + * @license MIT + */ + +namespace Terminal42\UrlRewriteBundle\ConfigProvider; + +use Contao\StringUtil; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Exception\TableNotFoundException; +use Terminal42\UrlRewriteBundle\RewriteConfig; +use Terminal42\UrlRewriteBundle\RewriteConfigInterface; + +class DatabaseConfigProvider implements ConfigProviderInterface +{ + /** + * @var Connection + */ + private $connection; + + /** + * DatabaseConfigProvider constructor. + * + * @param Connection $connection + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * {@inheritdoc} + */ + public function find(string $id): ?RewriteConfigInterface + { + try { + $data = $this->connection->fetchAssoc('SELECT * FROM tl_url_rewrite WHERE id=?', [$id]); + } catch (\PDOException | TableNotFoundException $e) { + return null; + } + + if (false === $data) { + return null; + } + + return $this->createConfig($data); + } + + /** + * {@inheritdoc} + */ + public function findAll(): array + { + try { + $records = $this->connection->fetchAll('SELECT * FROM tl_url_rewrite'); + } catch (\PDOException | TableNotFoundException $e) { + return []; + } + + if (count($records) === 0) { + return []; + } + + $configs = []; + + foreach ($records as $record) { + if (($config = $this->createConfig($record)) !== null) { + $configs[] = $config; + } + } + + return $configs; + } + + /** + * Create the config. + * + * @param array $data + * + * @return null|RewriteConfig + */ + private function createConfig(array $data): ?RewriteConfig + { + if (!isset($data['id'], $data['type'], $data['requestPath'], $data['responseCode'])) { + return null; + } + + $config = new RewriteConfig((string) $data['id'], $data['requestPath'], (int) $data['responseCode']); + + // Hosts + if (isset($data['requestHosts'])) { + $config->setRequestHosts(StringUtil::deserialize($data['requestHosts'], true)); + } + + // Response URI + if (isset($data['responseUri'])) { + $config->setResponseUri($data['responseUri']); + } + + switch ($data['type']) { + // Basic type + case 'basic': + if (isset($data['requestRequirements'])) { + $requirements = []; + + foreach (StringUtil::deserialize($data['requestRequirements'], true) as $requirement) { + if ($requirement['key'] !== '' && $requirement['value'] !== '') { + $requirements[$requirement['key']] = $requirement['value']; + } + } + + $config->setRequestRequirements($requirements); + } + break; + // Expert type + case 'expert': + if (isset($data['requestCondition'])) { + $config->setRequestCondition($data['requestCondition']); + } + break; + // Unsupported type + default: + throw new \RuntimeException(sprintf('Unsupported database record config type: %s', $data['type'])); + } + + return $config; + } +} diff --git a/src/Controller/RewriteController.php b/src/Controller/RewriteController.php index 8db21d4..cff8138 100644 --- a/src/Controller/RewriteController.php +++ b/src/Controller/RewriteController.php @@ -12,18 +12,19 @@ use Contao\CoreBundle\Framework\ContaoFrameworkInterface; use Contao\InsertTags; -use Doctrine\DBAL\Connection; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Terminal42\UrlRewriteBundle\ConfigProvider\ConfigProviderInterface; +use Terminal42\UrlRewriteBundle\RewriteConfigInterface; class RewriteController { /** - * @var Connection + * @var ConfigProviderInterface */ - private $db; + private $configProvider; /** * @var ContaoFrameworkInterface @@ -33,12 +34,12 @@ class RewriteController /** * RewriteController constructor. * - * @param Connection $db + * @param ConfigProviderInterface $configProvider * @param ContaoFrameworkInterface $framework */ - public function __construct(Connection $db, ContaoFrameworkInterface $framework) + public function __construct(ConfigProviderInterface $configProvider, ContaoFrameworkInterface $framework) { - $this->db = $db; + $this->configProvider = $configProvider; $this->framework = $framework; } @@ -53,17 +54,18 @@ public function __construct(Connection $db, ContaoFrameworkInterface $framework) */ public function indexAction(Request $request): Response { - if (!$request->attributes->has('_url_rewrite') || !($rewriteId = $request->attributes->getInt('_url_rewrite'))) { - throw new RouteNotFoundException('There _url_rewrite attribute is missing'); + if (!$request->attributes->has('_url_rewrite')) { + throw new RouteNotFoundException('The _url_rewrite attribute is missing'); } - $config = $this->db->fetchAssoc('SELECT * FROM tl_url_rewrite WHERE id=?', [$rewriteId]); + $rewriteId = $request->attributes->get('_url_rewrite'); + $config = $this->configProvider->find($rewriteId); - if (false === $config || !isset($config['responseCode'])) { + if (null === $config) { throw new RouteNotFoundException(sprintf('URL rewrite config ID %s does not exist', $rewriteId)); } - $responseCode = (int) $config['responseCode']; + $responseCode = $config->getResponseCode(); if (410 === $responseCode) { return new Response(Response::$statusTexts[$responseCode], $responseCode); @@ -77,20 +79,17 @@ public function indexAction(Request $request): Response /** * Generate the URI. * - * @param Request $request - * @param array $config + * @param Request $request + * @param RewriteConfigInterface $config * * @return string|null */ - private function generateUri(Request $request, array $config): ?string + private function generateUri(Request $request, RewriteConfigInterface $config): ?string { - if (!isset($config['responseUri'])) { + if (($uri = $config->getResponseUri()) === null) { return null; } - $uri = $config['responseUri']; - - // Parse the URI $uri = $this->replaceWildcards($request, $uri); $uri = $this->replaceInsertTags($uri); diff --git a/src/DependencyInjection/Compiler/ConfigProviderPass.php b/src/DependencyInjection/Compiler/ConfigProviderPass.php new file mode 100644 index 0000000..dfe90d0 --- /dev/null +++ b/src/DependencyInjection/Compiler/ConfigProviderPass.php @@ -0,0 +1,71 @@ + + * @license MIT + */ + +namespace Terminal42\UrlRewriteBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class ConfigProviderPass implements CompilerPassInterface +{ + use PriorityTaggedServiceTrait; + + /** + * @var string + */ + private $alias; + + /** + * @var string + */ + private $chain; + + /** + * @var string + */ + private $tag; + + /** + * ConfigProviderPass constructor. + * + * @param string $alias + * @param string $chain + * @param $tag + */ + public function __construct($alias, $chain, $tag) + { + $this->alias = $alias; + $this->chain = $chain; + $this->tag = $tag; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + $services = $this->findAndSortTaggedServices($this->tag, $container); + + // If there's only one service or chain service is not present alias the first service + if ((count($services) === 1 && count($services[0]) === 1) || !$container->hasDefinition($this->chain)) { + $container->setAlias($this->alias, (string) $services[0]); + + return; + } + + $definition = $container->findDefinition($this->chain); + + // Add providers to the chain + foreach ($services as $service) { + $definition->addMethodCall('addProvider', [$service]); + } + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php new file mode 100644 index 0000000..eef4136 --- /dev/null +++ b/src/DependencyInjection/Configuration.php @@ -0,0 +1,79 @@ + + * @license MIT + */ + +namespace Terminal42\UrlRewriteBundle\DependencyInjection; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Terminal42\UrlRewriteBundle\RewriteConfigInterface; + +class Configuration implements ConfigurationInterface +{ + /** + * {@inheritdoc} + */ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + + $rootNode = $treeBuilder->root('terminal42_url_rewrite'); + $rootNode + ->children() + ->booleanNode('backend_management') + ->info('Enable the rewrites management in Contao backend.') + ->defaultTrue() + ->end() + ->arrayNode('entries') + ->arrayPrototype() + ->children() + ->arrayNode('request') + ->children() + ->scalarNode('path') + ->info('The request path to match.') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->arrayNode('hosts') + ->info('An array of hosts to match.') + ->scalarPrototype()->end() + ->end() + ->arrayNode('requirements') + ->info('Additional requirements to match.') + ->scalarPrototype()->end() + ->end() + ->scalarNode('condition') + ->info('Request condition in Symfony\'s Expression Language to match.') + ->end() + ->end() + ->end() + ->arrayNode('response') + ->children() + ->integerNode('code') + ->info('The response code.') + ->defaultValue(301) + ->validate() + ->ifNotInArray(RewriteConfigInterface::VALID_RESPONSE_CODES) + ->thenInvalid('Invalid response code %s.') + ->end() + ->end() + ->scalarNode('uri') + ->info('The response redirect URI. Irrelevant if response code is set to 410.') + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + + return $treeBuilder; + } +} diff --git a/src/DependencyInjection/Terminal42UrlRewriteExtension.php b/src/DependencyInjection/Terminal42UrlRewriteExtension.php index 3fa4465..27b952f 100644 --- a/src/DependencyInjection/Terminal42UrlRewriteExtension.php +++ b/src/DependencyInjection/Terminal42UrlRewriteExtension.php @@ -14,18 +14,33 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; +use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension; -class Terminal42UrlRewriteExtension extends Extension +class Terminal42UrlRewriteExtension extends ConfigurableExtension { /** * {@inheritdoc} */ - public function load(array $configs, ContainerBuilder $container): void + protected function loadInternal(array $mergedConfig, ContainerBuilder $container) { $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('listener.yml'); $loader->load('services.yml'); + + $hasBackendManagement = (bool) $mergedConfig['backend_management']; + + // Set the "backend management" parameter + $container->setParameter('terminal42_url_rewrite.backend_management', $hasBackendManagement); + + // Remove the database provider if backend management is not available + if (!$hasBackendManagement) { + $container->removeDefinition('terminal42_url_rewrite.provider.database'); + } + + // Set the entries as argument for bundle config provider + if (isset($mergedConfig['entries']) && $container->hasDefinition('terminal42_url_rewrite.provider.bundle')) { + $container->getDefinition('terminal42_url_rewrite.provider.bundle')->setArguments([$mergedConfig['entries']]); + } } } diff --git a/src/EventListener/RewriteContainerListener.php b/src/EventListener/RewriteContainerListener.php index 8c21bac..b0375ff 100644 --- a/src/EventListener/RewriteContainerListener.php +++ b/src/EventListener/RewriteContainerListener.php @@ -13,6 +13,7 @@ use Symfony\Bundle\FrameworkBundle\Routing\Router; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Response; +use Terminal42\UrlRewriteBundle\RewriteConfigInterface; class RewriteContainerListener { @@ -66,8 +67,6 @@ public function onRecordsModified(): void */ public function onGenerateLabel(array $row): string { - $request = $row['requestPath']; - if ((int) $row['responseCode'] === 410) { $response = $row['responseCode']; } else { @@ -77,7 +76,7 @@ public function onGenerateLabel(array $row): string return sprintf( '%s [%s → %s]', $row['name'], - $request, + $row['requestPath'], $response ); } @@ -91,7 +90,7 @@ public function getResponseCodes(): array { $options = []; - foreach ([301, 302, 303, 307, 410] as $code) { + foreach (RewriteConfigInterface::VALID_RESPONSE_CODES as $code) { $options[$code] = $code.' '.Response::$statusTexts[$code]; } @@ -99,7 +98,7 @@ public function getResponseCodes(): array } /** - * Generate the examples + * Generate the examples. * * @return string */ diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml index 4a00bc3..182e3e7 100644 --- a/src/Resources/config/services.yml +++ b/src/Resources/config/services.yml @@ -2,12 +2,33 @@ services: terminal42_url_rewrite.rewrite_controller: class: Terminal42\UrlRewriteBundle\Controller\RewriteController arguments: - - "@database_connection" + - "@terminal42_url_rewrite.provider" - "@contao.framework" terminal42_url_rewrite.rewrite_loader: class: Terminal42\UrlRewriteBundle\Routing\UrlRewriteLoader arguments: - - "@database_connection" + - "@terminal42_url_rewrite.provider" tags: - { name: routing.loader } + + terminal42_url_rewrite.provider: + alias: terminal42_url_rewrite.provider.chain + + terminal42_url_rewrite.provider.bundle: + class: Terminal42\UrlRewriteBundle\ConfigProvider\BundleConfigProvider + public: false + tags: + - { name: terminal42_url_rewrite.provider, priority: 64 } + + terminal42_url_rewrite.provider.chain: + class: Terminal42\UrlRewriteBundle\ConfigProvider\ChainConfigProvider + public: false + + terminal42_url_rewrite.provider.database: + class: Terminal42\UrlRewriteBundle\ConfigProvider\DatabaseConfigProvider + public: false + arguments: + - "@database_connection" + tags: + - { name: terminal42_url_rewrite.provider, priority: 32 } diff --git a/src/Resources/contao/config/config.php b/src/Resources/contao/config/config.php index 65e682e..ceafcca 100644 --- a/src/Resources/contao/config/config.php +++ b/src/Resources/contao/config/config.php @@ -8,9 +8,12 @@ * @license MIT */ -$GLOBALS['BE_MOD']['system']['url_rewrites'] = [ - 'tables' => ['tl_url_rewrite'], -]; +/** + * Add the backend module if allowed. + */ +if (\System::getContainer()->getParameter('terminal42_url_rewrite.backend_management')) { + $GLOBALS['BE_MOD']['system']['url_rewrites'] = ['tables' => ['tl_url_rewrite']]; +} /* * Hooks diff --git a/src/Resources/contao/dca/tl_url_rewrite.php b/src/Resources/contao/dca/tl_url_rewrite.php index 6cd8e2f..788132f 100644 --- a/src/Resources/contao/dca/tl_url_rewrite.php +++ b/src/Resources/contao/dca/tl_url_rewrite.php @@ -181,3 +181,10 @@ ], ], ]; + +/* + * Remove the DCA if not allowed + */ +if (!\System::getContainer()->getParameter('terminal42_url_rewrite.backend_management')) { + unset($GLOBALS['TL_DCA']['tl_url_rewrite']); +} diff --git a/src/Resources/contao/languages/de/tl_url_rewrite.php b/src/Resources/contao/languages/de/tl_url_rewrite.php index 73a440b..2c91d34 100644 --- a/src/Resources/contao/languages/de/tl_url_rewrite.php +++ b/src/Resources/contao/languages/de/tl_url_rewrite.php @@ -42,7 +42,7 @@ 'expert' => ['komplex', 'Erlaubt die Anfrage-Zuordnung auf Basis der Symfony Expression Language. Für weitere Informationen besuchen Sie bitte diese Seite.'], ]; -/** +/* * Examples */ $GLOBALS['TL_LANG']['tl_url_rewrite']['examples'] = [ diff --git a/src/Resources/contao/languages/en/tl_url_rewrite.php b/src/Resources/contao/languages/en/tl_url_rewrite.php index ac0c201..aae444c 100644 --- a/src/Resources/contao/languages/en/tl_url_rewrite.php +++ b/src/Resources/contao/languages/en/tl_url_rewrite.php @@ -42,7 +42,7 @@ 'expert' => ['Expert', 'Allows to define the request condition using the Expression Language. For more information please visit this link.'], ]; -/** +/* * Examples */ $GLOBALS['TL_LANG']['tl_url_rewrite']['examples'] = [ diff --git a/src/RewriteConfig.php b/src/RewriteConfig.php new file mode 100644 index 0000000..630da15 --- /dev/null +++ b/src/RewriteConfig.php @@ -0,0 +1,181 @@ + + * @license MIT + */ + +namespace Terminal42\UrlRewriteBundle; + +class RewriteConfig implements RewriteConfigInterface +{ + /** + * @var string + */ + private $identifier; + + /** + * @var string + */ + private $requestPath = ''; + + /** + * @var array + */ + private $requestHosts = []; + + /** + * @var array + */ + private $requestRequirements = []; + + /** + * @var string|null + */ + private $requestCondition; + + /** + * @var int + */ + private $responseCode = 301; + + /** + * @var string|null + */ + private $responseUri; + + /** + * RewriteConfig constructor. + * + * @param string $identifier + * @param string $requestPath + * @param int $responseCode + */ + public function __construct(string $identifier, string $requestPath, int $responseCode = 301) + { + $this->identifier = $identifier; + $this->setRequestPath($requestPath); + $this->setResponseCode($responseCode); + } + + /** + * @return string + */ + public function getIdentifier(): string + { + return $this->identifier; + } + + /** + * @inheritDoc + */ + public function setIdentifier(string $identifier): void + { + $this->identifier = $identifier; + } + + /** + * @return string + */ + public function getRequestPath(): string + { + return $this->requestPath; + } + + /** + * @param string $requestPath + */ + public function setRequestPath(string $requestPath): void + { + $this->requestPath = $requestPath; + } + + /** + * @return array + */ + public function getRequestHosts(): array + { + return $this->requestHosts; + } + + /** + * @param array $requestHosts + */ + public function setRequestHosts(array $requestHosts): void + { + $this->requestHosts = array_values(array_unique(array_filter($requestHosts))); + } + + /** + * @return array + */ + public function getRequestRequirements(): array + { + return $this->requestRequirements; + } + + /** + * @param array $requestRequirements + */ + public function setRequestRequirements(array $requestRequirements): void + { + $this->requestRequirements = $requestRequirements; + } + + /** + * @return null|string + */ + public function getRequestCondition(): ?string + { + return $this->requestCondition; + } + + /** + * @param null|string $requestCondition + */ + public function setRequestCondition(string $requestCondition): void + { + $this->requestCondition = $requestCondition; + } + + /** + * @return int + */ + public function getResponseCode(): int + { + return $this->responseCode; + } + + /** + * @param int $responseCode + * + * @throws \InvalidArgumentException + */ + public function setResponseCode(int $responseCode): void + { + if (!\in_array($responseCode, self::VALID_RESPONSE_CODES, true)) { + throw new \InvalidArgumentException(sprintf('Invalid response code: %s', $responseCode)); + } + + $this->responseCode = $responseCode; + } + + /** + * @return null|string + */ + public function getResponseUri(): ?string + { + return $this->responseUri; + } + + /** + * @param null|string $responseUri + */ + public function setResponseUri(string $responseUri): void + { + $this->responseUri = $responseUri; + } +} diff --git a/src/RewriteConfigInterface.php b/src/RewriteConfigInterface.php new file mode 100644 index 0000000..eaed98b --- /dev/null +++ b/src/RewriteConfigInterface.php @@ -0,0 +1,56 @@ + + * @license MIT + */ + +namespace Terminal42\UrlRewriteBundle; + +interface RewriteConfigInterface +{ + const VALID_RESPONSE_CODES = [301, 302, 303, 307, 410]; + + /** + * @return string + */ + public function getIdentifier(): string; + + /** + * @param string $identifier + */ + public function setIdentifier(string $identifier): void; + + /** + * @return string + */ + public function getRequestPath(): string; + + /** + * @return array + */ + public function getRequestHosts(): array; + + /** + * @return array + */ + public function getRequestRequirements(): array; + + /** + * @return null|string + */ + public function getRequestCondition(): ?string; + + /** + * @return int + */ + public function getResponseCode(): int; + + /** + * @return null|string + */ + public function getResponseUri(): ?string; +} diff --git a/src/Routing/UrlRewriteLoader.php b/src/Routing/UrlRewriteLoader.php index 0a2f587..6606ec4 100644 --- a/src/Routing/UrlRewriteLoader.php +++ b/src/Routing/UrlRewriteLoader.php @@ -12,19 +12,18 @@ namespace Terminal42\UrlRewriteBundle\Routing; -use Contao\StringUtil; -use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Exception\TableNotFoundException; use Symfony\Component\Config\Loader\Loader; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; +use Terminal42\UrlRewriteBundle\ConfigProvider\ConfigProviderInterface; +use Terminal42\UrlRewriteBundle\RewriteConfigInterface; class UrlRewriteLoader extends Loader { /** - * @var Connection + * @var ConfigProviderInterface */ - private $db; + private $configProvider; /** * Has been already loaded? @@ -36,11 +35,11 @@ class UrlRewriteLoader extends Loader /** * UrlRewriteLoader constructor. * - * @param Connection $db + * @param ConfigProviderInterface $configProvider */ - public function __construct(Connection $db) + public function __construct(ConfigProviderInterface $configProvider) { - $this->db = $db; + $this->configProvider = $configProvider; } /** @@ -54,22 +53,18 @@ public function load($resource, $type = null): RouteCollection $this->loaded = true; $collection = new RouteCollection(); + $configs = $this->configProvider->findAll(); - try { - $rewrites = $this->db->fetchAll('SELECT * FROM tl_url_rewrite'); - } catch (\PDOException | TableNotFoundException $e) { - return $collection; - } - - if (0 === count($rewrites)) { + if (0 === count($configs)) { return $collection; } $count = 0; - foreach ($rewrites as $rewrite) { + /** @var RewriteConfigInterface $config */ + foreach ($configs as $config) { /** @var Route $route */ - foreach ($this->generateRoutes($rewrite) as $route) { + foreach ($this->generateRoutes($config) as $route) { if ($route !== null) { $collection->add('url_rewrite_'.$count++, $route); } @@ -90,19 +85,13 @@ public function supports($resource, $type = null): bool /** * Generate the routes. * - * @param array $config + * @param RewriteConfigInterface $config * * @return \Generator */ - private function generateRoutes(array $config): \Generator + private function generateRoutes(RewriteConfigInterface $config): \Generator { - $hosts = []; - - // Parse the hosts from config - if (isset($config['requestHosts'])) { - /** @var array $hosts */ - $hosts = array_unique(array_filter(StringUtil::deserialize($config['requestHosts'], true))); - } + $hosts = $config->getRequestHosts(); if (count($hosts) > 0) { foreach ($hosts as $host) { @@ -116,30 +105,28 @@ private function generateRoutes(array $config): \Generator /** * Create the route object. * - * @param array $config - * @param string|null $host + * @param RewriteConfigInterface $config + * @param string|null $host * * @return Route|null */ - private function createRoute(array $config, string $host = null): ?Route + private function createRoute(RewriteConfigInterface $config, string $host = null): ?Route { - if (!isset($config['id'], $config['type'], $config['requestPath'])) { + if (!$config->getRequestPath()) { return null; } - switch ($config['type']) { - case 'basic': - $route = $this->createBasicRoute($config); - break; - case 'expert': - $route = $this->createExpertRoute($config); - break; - default: - throw new \InvalidArgumentException(sprintf('Unsupported database record config type: %s', $config['type'])); - } - + $route = new Route($config->getRequestPath()); $route->setDefault('_controller', 'terminal42_url_rewrite.rewrite_controller:indexAction'); - $route->setDefault('_url_rewrite', $config['id']); + $route->setDefault('_url_rewrite', $config->getIdentifier()); + $route->setRequirements($config->getRequestRequirements()); + + // Set the condition + if (($condition = $config->getRequestCondition()) !== null) { + $route->setCondition($condition); + } else { + $route->setMethods('GET'); + } // Set the host if (null !== $host) { @@ -148,49 +135,4 @@ private function createRoute(array $config, string $host = null): ?Route return $route; } - - /** - * Create the basic route. - * - * @param array $config - * - * @return Route - */ - private function createBasicRoute(array $config): Route - { - $route = new Route($config['requestPath']); - $route->setMethods('GET'); - - // Set the requirements - if (isset($config['requestRequirements'])) { - /** @var array $requirements */ - $requirements = StringUtil::deserialize($config['requestRequirements'], true); - $requirements = array_filter($requirements, function ($item) { - return $item['key'] !== '' && $item['value'] !== ''; - }); - - if (count($requirements) > 0) { - foreach ($requirements as $requirement) { - $route->setRequirement($requirement['key'], $requirement['value']); - } - } - } - - return $route; - } - - /** - * Create the expert route. - * - * @param array $config - * - * @return Route - */ - private function createExpertRoute(array $config): Route - { - $route = new Route($config['requestPath']); - $route->setCondition($config['requestCondition']); - - return $route; - } } diff --git a/src/Terminal42UrlRewriteBundle.php b/src/Terminal42UrlRewriteBundle.php index 139a01b..4d31768 100644 --- a/src/Terminal42UrlRewriteBundle.php +++ b/src/Terminal42UrlRewriteBundle.php @@ -14,6 +14,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Terminal42\UrlRewriteBundle\DependencyInjection\Compiler\ConfigProviderPass; class Terminal42UrlRewriteBundle extends Bundle { @@ -22,5 +23,10 @@ class Terminal42UrlRewriteBundle extends Bundle */ public function build(ContainerBuilder $container): void { + $container->addCompilerPass(new ConfigProviderPass( + 'terminal42_url_rewrite.provider', + 'terminal42_url_rewrite.provider.chain', + 'terminal42_url_rewrite.provider' + )); } } diff --git a/tests/ConfigProvider/BundleConfigProviderTest.php b/tests/ConfigProvider/BundleConfigProviderTest.php new file mode 100644 index 0000000..217c96f --- /dev/null +++ b/tests/ConfigProvider/BundleConfigProviderTest.php @@ -0,0 +1,70 @@ +provider = new BundleConfigProvider([ + // Invalid entry + ['request' => [], 'response' => []], + + // Valid entry + [ + 'request' => [ + 'path' => 'foo/bar', + 'hosts' => ['domain1.tld', 'domain2.tld'], + 'condition' => "request.query.has('foobar')", + 'requirements' => ['foo' => '\d+', 'bar' => '\s+'], + ], + 'response' => [ + 'code' => 303, + 'uri' => 'foo/baz' + ], + ], + ]); + } + + public function testInstantiation() + { + $this->assertInstanceOf(BundleConfigProvider::class, $this->provider); + } + + public function testFind() + { + $config = $this->provider->find('1'); + + $this->assertNull($this->provider->find('123')); + + $this->assertInstanceOf(RewriteConfig::class, $config); + $this->assertSame('1', $config->getIdentifier()); + $this->assertSame('foo/bar', $config->getRequestPath()); + $this->assertSame(['domain1.tld', 'domain2.tld'], $config->getRequestHosts()); + $this->assertSame(['foo' => '\d+', 'bar' => '\s+'], $config->getRequestRequirements()); + $this->assertSame("request.query.has('foobar')", $config->getRequestCondition()); + $this->assertSame(303, $config->getResponseCode()); + $this->assertSame('foo/baz', $config->getResponseUri()); + } + + public function testFindAll() + { + $this->assertCount(0, (new BundleConfigProvider())->findAll()); + $this->assertCount(1, $this->provider->findAll()); + } +} diff --git a/tests/ConfigProvider/ChainConfigProviderTest.php b/tests/ConfigProvider/ChainConfigProviderTest.php new file mode 100644 index 0000000..d70e5f9 --- /dev/null +++ b/tests/ConfigProvider/ChainConfigProviderTest.php @@ -0,0 +1,61 @@ +assertInstanceOf(ChainConfigProvider::class, new ChainConfigProvider()); + } + + public function testFind() + { + $config1 = new RewriteConfig('1', 'path/1'); + $config2 = new RewriteConfig('2', 'path/2'); + $config3 = new RewriteConfig('3', 'path/3'); + + $chain = new ChainConfigProvider(); + $chain->addProvider($this->mockProvider([1 => $config1, 2 => $config2])); + $chain->addProvider($this->mockProvider([3 => $config3])); + + $configs = $chain->findAll(); + + $this->assertCount(3, $configs); + $this->assertSame('path/1', $chain->find($configs[0]->getIdentifier())->getRequestPath()); + $this->assertSame('path/2', $chain->find($configs[1]->getIdentifier())->getRequestPath()); + $this->assertSame('path/3', $chain->find($configs[2]->getIdentifier())->getRequestPath()); + $this->assertNull($chain->find('bar:baz')); + } + + /** + * @param array $configs + * + * @return \PHPUnit_Framework_MockObject_MockObject|ConfigProviderInterface + */ + private function mockProvider(array $configs) + { + $provider = $this->createMock(ConfigProviderInterface::class); + + $provider + ->method('find') + ->willReturnCallback(function($key) use ($configs) { + return isset($configs[$key]) ? $configs[$key] : null; + }) + ; + + $provider + ->method('findAll') + ->willReturn($configs) + ; + + return $provider; + } +} diff --git a/tests/ConfigProvider/DatabaseConfigProviderTest.php b/tests/ConfigProvider/DatabaseConfigProviderTest.php new file mode 100644 index 0000000..3b0541b --- /dev/null +++ b/tests/ConfigProvider/DatabaseConfigProviderTest.php @@ -0,0 +1,229 @@ +assertInstanceOf(DatabaseConfigProvider::class, new DatabaseConfigProvider($this->createMock(Connection::class))); + } + + /** + * @dataProvider findDataProvider + */ + public function testFind($row, $expected) + { + $connection = $this->createMock(Connection::class); + + $connection + ->method('fetchAssoc') + ->willReturn($row) + ; + + $provider = new DatabaseConfigProvider($connection); + + // Handle the exception + if (is_array($expected) && isset($expected['exception'])) { + $this->expectException($expected['exception']); + $provider->find('foobar'); + + return; + } + + // Compare the values + if (is_array($expected)) { + $config = $provider->find('foobar'); + + foreach ($expected as $method => $value) { + $this->assertSame($value, $config->$method()); + } + + return; + } + + $this->assertSame($expected, $provider->find('foobar')); + } + + public function findDataProvider() + { + return [ + 'Row not found' => [ + false, + null, + ], + + 'Invalid config – missing ID' => [ + ['type' => 'basic', 'requestPath' => 'foo/bar', 'responseCode' => 301], + null, + ], + + 'Invalid config – missing type' => [ + ['id' => 123, 'requestPath' => 'foo/bar', 'responseCode' => 301], + null, + ], + + 'Invalid config – missing request path' => [ + ['id' => 123, 'type' => 'basic', 'responseCode' => 301], + null, + ], + + 'Invalid config – missing response code' => [ + ['id' => 123, 'type' => 'basic', 'requestPath' => 'foo/bar'], + null, + ], + + 'Invalid config – unsupported data type' => [ + ['id' => 123, 'type' => 'foobar', 'requestPath' => 'foo/bar', 'responseCode' => 301], + ['exception' => \RuntimeException::class], + ], + + 'Valid config – basic' => [ + [ + 'id' => 123, + 'type' => 'basic', + 'requestPath' => 'foo/bar', + 'requestHosts' => serialize(['domain1.tld', 'domain2.tld']), + 'requestRequirements' => serialize([ + ['key' => 'foo', 'value' => '\d+'], + ['key' => 'bar', 'value' => '\s+'] + ]), + 'responseUri' => 'foo/baz', + 'responseCode' => 301, + ], + [ + 'getIdentifier' => '123', + 'getRequestPath' => 'foo/bar', + 'getRequestHosts' => ['domain1.tld', 'domain2.tld'], + 'getRequestRequirements' => ['foo' => '\d+', 'bar' => '\s+'], + 'getRequestCondition' => null, + 'getResponseCode' => 301, + 'getResponseUri' => 'foo/baz', + ], + ], + + 'Valid config – expert' => [ + [ + 'id' => 123, + 'type' => 'expert', + 'requestPath' => 'foo/bar', + 'requestHosts' => serialize(['domain1.tld', 'domain2.tld']), + 'requestCondition' => "request.query.has('foobar')", + 'responseUri' => 'foo/baz', + 'responseCode' => 301, + ], + [ + 'getIdentifier' => '123', + 'getRequestPath' => 'foo/bar', + 'getRequestHosts' => ['domain1.tld', 'domain2.tld'], + 'getRequestRequirements' => [], + 'getRequestCondition' => "request.query.has('foobar')", + 'getResponseCode' => 301, + 'getResponseUri' => 'foo/baz', + ], + ], + ]; + } + + public function testFindAll() + { + $connection = $this->createMock(Connection::class); + + $connection + ->method('fetchAll') + ->willReturn([ + [ + 'id' => 123, + 'type' => 'basic', + 'requestPath' => 'foo/bar', + 'responseUri' => 'foo/baz', + 'responseCode' => 301, + ], + [ + 'id' => 456, + 'type' => 'expert', + 'requestPath' => 'foo/bar', + 'responseUri' => 'foo/baz', + 'responseCode' => 301, + ], + ]) + ; + + $provider = new DatabaseConfigProvider($connection); + + $this->assertCount(2, $provider->findAll()); + } + + public function testFindAllNoRecords() + { + $connection = $this->createMock(Connection::class); + + $connection + ->method('fetchAll') + ->willReturn([]) + ; + + $provider = new DatabaseConfigProvider($connection); + + $this->assertCount(0, $provider->findAll()); + } + + /** + * @dataProvider connectionExceptionDataProvider + */ + public function testConnectionException($method, $connMethod, $exception, $expected) + { + $connection = $this->createMock(Connection::class); + + $connection + ->method($connMethod) + ->willThrowException($exception) + ; + + $provider = new DatabaseConfigProvider($connection); + + if (is_array($expected) && isset($expected['exception'])) { + $this->expectException($expected['exception']); + } + + $this->assertSame($expected, $provider->$method('foobar')); + } + + public function connectionExceptionDataProvider() + { + $pdoException = $this->createMock(\PDOException::class); + $tableNotFoundException = $this->createMock(TableNotFoundException::class); + $runtimeException = $this->createMock(\RuntimeException::class); + + return [ + // find() + 'Find - PDO exception' => [ + 'find', 'fetchAssoc', $pdoException, null + ], + 'Find – Table exception' => [ + 'find', 'fetchAssoc', $tableNotFoundException, null + ], + 'Find – Runtime exception (uncaught)' => [ + 'find', 'fetchAssoc', $runtimeException, ['exception' => \RuntimeException::class] + ], + + // findAll() + 'Find all - PDO exception' => [ + 'findAll', 'fetchAll', $pdoException, [] + ], + 'Find all - Table exception' => [ + 'findAll', 'fetchAll', $tableNotFoundException, [] + ], + 'Find all - Runtime exception (uncaught)' => [ + 'findAll', 'fetchAll', $runtimeException, ['exception' => \RuntimeException::class] + ], + ]; + } +} diff --git a/tests/Controller/RewriteControllerTest.php b/tests/Controller/RewriteControllerTest.php index 6b8b156..5c78de5 100644 --- a/tests/Controller/RewriteControllerTest.php +++ b/tests/Controller/RewriteControllerTest.php @@ -1,35 +1,35 @@ assertInstanceOf(RewriteController::class, new RewriteController( - $this->createMock(Connection::class), - $this->createMock(ContaoFramework::class) + $this->mockConfigProvider(), + $this->mockContaoFramework() )); } public function testIndexActionNoUrlRewriteAttribute() { - $controller = new RewriteController( - $this->createMock(Connection::class), - $this->createMock(ContaoFramework::class) - ); - - $request = $this->createRequest(null); + $controller = new RewriteController($this->mockConfigProvider(), $this->mockContaoFramework()); + $request = $this->mockRequest(null); $this->expectException(RouteNotFoundException::class); $controller->indexAction($request); @@ -37,15 +37,9 @@ public function testIndexActionNoUrlRewriteAttribute() public function testIndexActionNoUrlRewriteRecord() { - $db = $this->createMock(Connection::class); - - $db - ->method('fetchAssoc') - ->willReturn(false) - ; - - $controller = new RewriteController($db, $this->createMock(ContaoFramework::class)); - $request = $this->createRequest(1); + $provider = $this->mockConfigProvider(); + $controller = new RewriteController($provider, $this->mockContaoFramework()); + $request = $this->mockRequest(1); $this->expectException(RouteNotFoundException::class); $controller->indexAction($request); @@ -56,15 +50,9 @@ public function testIndexActionNoUrlRewriteRecord() */ public function testIndexActionRedirect($provided, $expected) { - $db = $this->createMock(Connection::class); - - $db - ->method('fetchAssoc') - ->willReturn($provided[0]) - ; - - $controller = new RewriteController($db, $this->createFrameworkMock()); - $request = $this->createRequest(1, $provided[1], $provided[2]); + $provider = $this->mockConfigProvider($provided[0]); + $controller = new RewriteController($provider, $this->mockContaoFramework()); + $request = $this->mockRequest(1, $provided[1], $provided[2]); $response = $controller->indexAction($request); $this->assertInstanceOf(RedirectResponse::class, $response); @@ -74,10 +62,16 @@ public function testIndexActionRedirect($provided, $expected) public function indexActionRedirectDataProvider() { + $config1 = new RewriteConfig('1', 'foobar'); + $config1->setResponseUri('{{link_url::{bar}|absolute}}/foo///{baz}/{quux}'); + + $config2 = new RewriteConfig('2', 'foobar', 302); + $config2->setResponseUri('foo///{baz}/{quux}'); + return [ 'Insert tags' => [ [ - ['responseUri' => '{{link_url::{bar}|absolute}}/foo///{baz}/{quux}', 'responseCode' => 301], + $config1, ['bar' => 1, 'baz' => 'bar'], ['quux' => 'quuz'] ], @@ -88,7 +82,7 @@ public function indexActionRedirectDataProvider() ], 'Absolute ' => [ [ - ['responseUri' => 'foo///{baz}/{quux}', 'responseCode' => 302], + $config2, ['baz' => 'bar'], ['quux' => 'quuz'] ], @@ -102,17 +96,9 @@ public function indexActionRedirectDataProvider() public function testIndexActionGone() { - $db = $this->createMock(Connection::class); - - $db - ->method('fetchAssoc') - ->willReturn([ - 'responseCode' => 410, - ]) - ; - - $controller = new RewriteController($db, $this->createFrameworkMock()); - $request = $this->createRequest(1); + $provider = $this->mockConfigProvider(new RewriteConfig('1', 'foobar', 410)); + $controller = new RewriteController($provider, $this->mockContaoFramework()); + $request = $this->mockRequest(1); $response = $controller->indexAction($request); $this->assertInstanceOf(Response::class, $response); @@ -122,16 +108,9 @@ public function testIndexActionGone() public function testIndexActionInternalServerError() { - $db = $this->createMock(Connection::class); - - $db - ->method('fetchAssoc') - ->willReturn(['responseCode' => 301]) - ; - - $controller = new RewriteController($db, $this->createFrameworkMock()); - - $request = $this->createRequest(1); + $provider = $this->mockConfigProvider(new RewriteConfig('1', 'foobar')); + $controller = new RewriteController($provider, $this->mockContaoFramework()); + $request = $this->mockRequest(1); $response = $controller->indexAction($request); $this->assertInstanceOf(Response::class, $response); @@ -139,7 +118,10 @@ public function testIndexActionInternalServerError() $this->assertEquals('Internal Server Error', $response->getContent()); } - private function createFrameworkMock() + /** + * @return \PHPUnit_Framework_MockObject_MockObject|ContaoFrameworkInterface + */ + private function mockContaoFramework() { $insertTags = $this ->getMockBuilder(Adapter::class) @@ -165,17 +147,27 @@ private function createFrameworkMock() return $framework; } - private function createRequest($urlRewrite, $routeParams = [], $query = []) + /** + * @param mixed $urlRewrite + * @param array $routeParams + * @param array $query + * + * @return \PHPUnit_Framework_MockObject_MockObject|Request + */ + private function mockRequest($urlRewrite = null, $routeParams = [], $query = []) { + $attributes = ['_route_params' => $routeParams]; + + if (null !== $urlRewrite) { + $attributes['_url_rewrite'] = $urlRewrite; + } + $request = $this ->getMockBuilder(Request::class) ->setConstructorArgs([ $query, [], - [ - '_url_rewrite' => $urlRewrite, - '_route_params' => $routeParams, - ] + $attributes ]) ->setMethods(['getSchemeAndHttpHost', 'getBasePath']) ->getMock() @@ -193,4 +185,21 @@ private function createRequest($urlRewrite, $routeParams = [], $query = []) return $request; } + + /** + * @param RewriteConfig|null $config + * + * @return \PHPUnit_Framework_MockObject_MockObject|ConfigProviderInterface + */ + private function mockConfigProvider(RewriteConfig $config = null) + { + $provider = $this->createMock(ConfigProviderInterface::class); + + $provider + ->method('find') + ->willReturn($config) + ; + + return $provider; + } } diff --git a/tests/DependencyInjection/Compiler/ConfigProviderPassTest.php b/tests/DependencyInjection/Compiler/ConfigProviderPassTest.php new file mode 100644 index 0000000..9d961f8 --- /dev/null +++ b/tests/DependencyInjection/Compiler/ConfigProviderPassTest.php @@ -0,0 +1,100 @@ +pass = new ConfigProviderPass('alias', 'chain', 'tag'); + } + + public function testInstantiation() + { + $this->assertInstanceOf(ConfigProviderPass::class, $this->pass); + } + + public function testProcessSingleProvider() + { + $chainDefinition = new Definition(); + + $providerDefinition = new Definition(); + $providerDefinition->addTag('tag', ['priority' => 32]); + + $container = new ContainerBuilder(); + $container->addDefinitions([ + 'alias' => $chainDefinition, + 'chain' => $chainDefinition, + 'provider' => $providerDefinition, + ]); + + $this->pass->process($container); + + $this->assertEquals('provider', (string) $container->getAlias('alias')); + $this->assertEmpty($chainDefinition->getMethodCalls()); + } + + public function testProcessMultipleProviders() + { + $chainDefinition = new Definition(); + + $providerDefinition1 = new Definition(); + $providerDefinition1->addTag('tag', ['priority' => 32]); + + $providerDefinition2 = new Definition(); + $providerDefinition2->addTag('tag', ['priority' => 64]); + + $container = new ContainerBuilder(); + $container->addDefinitions([ + 'alias' => $chainDefinition, + 'chain' => $chainDefinition, + 'provider1' => $providerDefinition1, + 'provider2' => $providerDefinition2, + ]); + + $this->pass->process($container); + + $calls = $chainDefinition->getMethodCalls(); + + $this->assertEquals($chainDefinition, $container->getDefinition('alias')); + $this->assertEquals('addProvider', $calls[0][0]); + $this->assertInstanceOf(Reference::class, $calls[0][1][0]); + $this->assertEquals('provider2', (string) $calls[0][1][0]); + $this->assertEquals('provider1', (string) $calls[1][1][0]); + } + + public function testProcessMultipleProvidersNoChain() + { + $aliasDefinition = new Definition(); + + $providerDefinition1 = new Definition(); + $providerDefinition1->addTag('tag', ['priority' => 32]); + + $providerDefinition2 = new Definition(); + $providerDefinition2->addTag('tag', ['priority' => 64]); + + $container = new ContainerBuilder(); + $container->addDefinitions([ + 'alias' => $aliasDefinition, + 'provider1' => $providerDefinition1, + 'provider2' => $providerDefinition2, + ]); + + $this->pass->process($container); + + $this->assertEquals('provider2', (string) $container->getAlias('alias')); + } +} diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000..f356159 --- /dev/null +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,75 @@ +assertInstanceOf(Configuration::class, new Configuration()); + } + + public function testValidConfig() + { + $yaml = Yaml::parse(file_get_contents(__DIR__ . '/../Fixtures/config_valid.yml')); + $config = (new Processor())->processConfiguration(new Configuration(), $yaml); + + $expected = [ + 'backend_management' => true, + 'entries' => [ + [ + 'request' => [ + 'path' => 'find/{address}', + 'hosts' => [], + 'requirements' => [], + ], + 'response' => [ + 'code' => 303, + 'uri' => 'https://www.google.com/maps?q={address}', + ], + ], + [ + 'request' => [ + 'path' => 'news/{news}', + 'requirements' => ['news' => '\d+'], + 'hosts' => [], + ], + 'response' => [ + 'code' => 301, + 'uri' => '{{news_url::{news}|absolute}}' + ], + ], + [ + 'request' => [ + 'path' => 'home.php', + 'hosts' => ['localhost'], + 'condition' => "context.getMethod() == 'GET' and request.query.has('page')", + 'requirements' => [], + ], + 'response' => [ + 'uri' => '{{link_url::{page}|absolute}}', + 'code' => 301, + ], + ], + ], + ]; + + $this->assertSame($expected, $config); + } + + public function testInvalidConfig() + { + $this->expectException(InvalidConfigurationException::class); + + $yaml = Yaml::parse(file_get_contents(__DIR__ . '/../Fixtures/config_invalid.yml')); + (new Processor())->processConfiguration(new Configuration(), $yaml); + } +} diff --git a/tests/DependencyInjection/Terminal42UrlRewriteExtensionTest.php b/tests/DependencyInjection/Terminal42UrlRewriteExtensionTest.php index d917305..e4c6724 100644 --- a/tests/DependencyInjection/Terminal42UrlRewriteExtensionTest.php +++ b/tests/DependencyInjection/Terminal42UrlRewriteExtensionTest.php @@ -15,11 +15,28 @@ public function testInstantiation() $this->assertInstanceOf(Terminal42UrlRewriteExtension::class, new Terminal42UrlRewriteExtension()); } - public function testLoad() + public function testLoadWithBackendManagement() { $container = new ContainerBuilder(); $extension = new Terminal42UrlRewriteExtension(); - $extension->load([], $container); + $extension->load(['terminal42_url_rewrite' => ['backend_management' => true]], $container); + + $this->assertTrue($container->getParameter('terminal42_url_rewrite.backend_management')); + $this->assertTrue($container->hasDefinition('terminal42_url_rewrite.provider.database')); + + $this->assertTrue($container->hasDefinition('terminal42_url_rewrite.listener.insert_tags')); + $this->assertTrue($container->hasDefinition('terminal42_url_rewrite.listener.rewrite_container')); + $this->assertTrue($container->hasDefinition('terminal42_url_rewrite.rewrite_controller')); + } + + public function testLoadWithoutBackendManagement() + { + $container = new ContainerBuilder(); + $extension = new Terminal42UrlRewriteExtension(); + $extension->load(['terminal42_url_rewrite' => ['backend_management' => false]], $container); + + $this->assertFalse($container->getParameter('terminal42_url_rewrite.backend_management')); + $this->assertFalse($container->hasDefinition('terminal42_url_rewrite.provider.database')); $this->assertTrue($container->hasDefinition('terminal42_url_rewrite.listener.insert_tags')); $this->assertTrue($container->hasDefinition('terminal42_url_rewrite.listener.rewrite_container')); diff --git a/tests/Fixtures/config_invalid.yml b/tests/Fixtures/config_invalid.yml new file mode 100644 index 0000000..cea45df --- /dev/null +++ b/tests/Fixtures/config_invalid.yml @@ -0,0 +1,5 @@ +terminal42_url_rewrite: + entries: + - + request: { path: 'find/{address}' } + response: { code: 404, uri: 'https://www.google.com/maps?q={address}' } diff --git a/tests/Fixtures/config_valid.yml b/tests/Fixtures/config_valid.yml new file mode 100644 index 0000000..8f538e8 --- /dev/null +++ b/tests/Fixtures/config_valid.yml @@ -0,0 +1,18 @@ +terminal42_url_rewrite: + backend_management: true + entries: + - + request: { path: 'find/{address}' } + response: { code: 303, uri: 'https://www.google.com/maps?q={address}' } + + - + request: { path: 'news/{news}', requirements: {news: '\d+'} } + response: { code: 301, uri: '{{news_url::{news}|absolute}}' } + + - + request: + path: 'home.php' + hosts: ['localhost'] + condition: "context.getMethod() == 'GET' and request.query.has('page')" + response: + uri: '{{link_url::{page}|absolute}}' diff --git a/tests/RewriteConfigTest.php b/tests/RewriteConfigTest.php new file mode 100644 index 0000000..b693c01 --- /dev/null +++ b/tests/RewriteConfigTest.php @@ -0,0 +1,41 @@ +setRequestHosts(['domain1.tld', 'domain1.tld', 'domain2.tld', '']); + $config->setRequestRequirements(['foo' => '\d+', 'bar' => '\s+']); + $config->setRequestCondition("request.query.has('foobar')"); + $config->setResponseCode(303); + $config->setResponseUri('foo/baz'); + + $this->assertSame('1', $config->getIdentifier()); + $this->assertSame('foo/bar', $config->getRequestPath()); + $this->assertSame(['domain1.tld', 'domain2.tld'], $config->getRequestHosts()); + $this->assertSame(['foo' => '\d+', 'bar' => '\s+'], $config->getRequestRequirements()); + $this->assertSame("request.query.has('foobar')", $config->getRequestCondition()); + $this->assertSame(303, $config->getResponseCode()); + $this->assertSame('foo/baz', $config->getResponseUri()); + + $config->setIdentifier('2'); + $config->setRequestPath('foo/baz'); + + $this->assertSame('2', $config->getIdentifier()); + $this->assertSame('foo/baz', $config->getRequestPath()); + } + + public function testInvalidResponseCode() + { + $this->expectException(\InvalidArgumentException::class); + new RewriteConfig('1', 'foobar', 500); + } +} diff --git a/tests/Routing/UrlRewriteLoaderTest.php b/tests/Routing/UrlRewriteLoaderTest.php index f9a3079..6dc036d 100644 --- a/tests/Routing/UrlRewriteLoaderTest.php +++ b/tests/Routing/UrlRewriteLoaderTest.php @@ -1,23 +1,26 @@ assertInstanceOf(UrlRewriteLoader::class, new UrlRewriteLoader($this->createMock(Connection::class))); + $this->assertInstanceOf(UrlRewriteLoader::class, new UrlRewriteLoader($this->mockConfigProvider())); } public function testSupports() { - $loader = new UrlRewriteLoader($this->createMock(Connection::class)); + $loader = new UrlRewriteLoader($this->mockConfigProvider()); $this->assertTrue($loader->supports('', 'terminal42_url_rewrite')); $this->assertFalse($loader->supports('', 'foobar')); @@ -28,88 +31,27 @@ public function testLoadedTwice() { $this->expectException(\RuntimeException::class); - $loader = new UrlRewriteLoader($this->createMock(Connection::class)); - $loader->load(''); + $loader = new UrlRewriteLoader($this->mockConfigProvider()); $loader->load(''); - } - - public function testLoadDatabaseCaughtException() - { - $db = $this->createMock(Connection::class); - - $db - ->method('fetchAll') - ->willThrowException(new \PDOException()) - ; - - $loader = new UrlRewriteLoader($db); - $collection = $loader->load(''); - - $this->assertInstanceOf(RouteCollection::class, $collection); - $this->assertCount(0, $collection->getIterator()); - } - - public function testLoadDatabaseUncaughtException() - { - $this->expectException(\RuntimeException::class); - - $db = $this->createMock(Connection::class); - - $db - ->method('fetchAll') - ->willThrowException(new \RuntimeException()) - ; - - $loader = new UrlRewriteLoader($db); $loader->load(''); } - public function testLoadNoDatabaseRecords() + public function testNoRoutes() { - $db = $this->createMock(Connection::class); - - $db - ->method('fetchAll') - ->willReturn([]) - ; - - $loader = new UrlRewriteLoader($db); + $loader = new UrlRewriteLoader($this->mockConfigProvider()); $collection = $loader->load(''); $this->assertInstanceOf(RouteCollection::class, $collection); $this->assertCount(0, $collection->getIterator()); } - public function testLoadUnsupportedConfigType() - { - $this->expectException(\InvalidArgumentException::class); - - $db = $this->createMock(Connection::class); - - $db - ->method('fetchAll') - ->willReturn([ - ['id' => 1, 'type' => 'foobar', 'requestPath' => 'foobar'] - ]) - ; - - $loader = new UrlRewriteLoader($db); - $loader->load(''); - } - /** * @dataProvider getRouteCollectionProvider */ public function testLoad($provided, $expected) { - $db = $this->createMock(Connection::class); - - $db - ->method('fetchAll') - ->willReturn([$provided]) - ; - - $loader = new UrlRewriteLoader($db); + $provider = $this->mockConfigProvider([$provided]); + $loader = new UrlRewriteLoader($provider); $collection = $loader->load(''); $routes = $collection->getIterator(); @@ -134,13 +76,22 @@ public function testLoad($provided, $expected) public function getRouteCollectionProvider() { + $config1 = new RewriteConfig('1', 'foo/bar'); + + $config2 = new RewriteConfig('2', 'foo/baz'); + $config2->setRequestHosts(['domain1.tld', 'domain2.tld']); + $config2->setRequestRequirements(['foo' => '\d+', 'baz' => '\s+']); + + $config3 = new RewriteConfig('3', 'foo/bar'); + $config3->setRequestCondition('context.getMethod() in [\'GET\']'); + + $config4 = new RewriteConfig('4', 'foo/baz'); + $config4->setRequestHosts(['domain1.tld', 'domain2.tld']); + $config4->setRequestCondition('context.getMethod() in [\'GET\']'); + return [ 'Basic – single route' => [ - [ - 'id' => 1, - 'type' => 'basic', - 'requestPath' => 'foo/bar' - ], + $config1, [ [ 'path' => '/foo/bar', @@ -153,16 +104,7 @@ public function getRouteCollectionProvider() ], 'Basic – multiple hosts' => [ - [ - 'id' => 1, - 'type' => 'basic', - 'requestPath' => 'foo/baz', - 'requestHosts' => ['domain1.tld', 'domain2.tld'], - 'requestRequirements' => [ - ['key' => 'foo', 'value' => '\d+'], - ['key' => 'baz', 'value' => '\s+'] - ], - ], + $config2, [ [ 'path' => '/foo/baz', @@ -182,12 +124,7 @@ public function getRouteCollectionProvider() ], 'Expert – single route' => [ - [ - 'id' => 1, - 'type' => 'expert', - 'requestPath' => 'foo/bar', - 'requestCondition' => 'context.getMethod() in [\'GET\']', - ], + $config3, [ [ 'path' => '/foo/bar', @@ -200,13 +137,7 @@ public function getRouteCollectionProvider() ], 'Expert – multiple hosts' => [ - [ - 'id' => 1, - 'type' => 'expert', - 'requestPath' => 'foo/baz', - 'requestHosts' => ['domain1.tld', 'domain2.tld'], - 'requestCondition' => 'context.getMethod() in [\'GET\']', - ], + $config4, [ [ 'path' => '/foo/baz', @@ -225,20 +156,27 @@ public function getRouteCollectionProvider() ] ], - 'Invalid #1' => [ - ['id' => 1], + 'Invalid' => [ + new RewriteConfig('1', ''), [] ], + ]; + } - 'Invalid #2' => [ - ['requestPath' => 'invalid'], - [] - ], + /** + * @param array $configs + * + * @return \PHPUnit_Framework_MockObject_MockObject|ConfigProviderInterface + */ + private function mockConfigProvider(array $configs = []) + { + $provider = $this->createMock(ConfigProviderInterface::class); - 'Invalid #3' => [ - [], - [] - ], - ]; + $provider + ->method('findAll') + ->willReturn($configs) + ; + + return $provider; } } diff --git a/tests/Terminal42UrlRewriteBundleTest.php b/tests/Terminal42UrlRewriteBundleTest.php index ff87443..fc1e9aa 100644 --- a/tests/Terminal42UrlRewriteBundleTest.php +++ b/tests/Terminal42UrlRewriteBundleTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Terminal42\UrlRewriteBundle\DependencyInjection\Compiler\ConfigProviderPass; use Terminal42\UrlRewriteBundle\Terminal42UrlRewriteBundle; class Terminal42UrlRewriteBundleTest extends TestCase @@ -21,6 +22,15 @@ public function testBuild() $bundle = new Terminal42UrlRewriteBundle(); $bundle->build($container); - $this->assertEquals($container, new ContainerBuilder()); + $found = false; + + foreach ($container->getCompiler()->getPassConfig()->getPasses() as $pass) { + if ($pass instanceof ConfigProviderPass) { + $found = true; + break; + } + } + + $this->assertTrue($found); } }