diff --git a/README.md b/README.md index abe9666a..38a3a507 100644 --- a/README.md +++ b/README.md @@ -305,92 +305,6 @@ The `authorization-guards.global.php` file provides configuration to restrict ac ] ``` -## Slug - -In terms of business logic, any application launched in production needs to be indexed by search engines as friendly as possible. - -Here comes the *Slug module* that works together with the application's routing system, which adds the option to customize each route replacing the old pattern such as the name of the route as well as its action with a chosen alias. On top of that, it allows you to replace the attributes with a more coherent version without losing their main functionality. - -The `slug.global.php.dist` is the main configuration for *Slug module* . - -```php - // How to add a registered route to your slug configuration. - 'slug_configuration' => [ - // Detect a duplicate alias to avoid confusion. - // We may have duplicate aliases but it is not recommended - // that it requires future development on each project. - 'detect_duplicates' => true, - // Main slug declaration. - 'slug_route' => [ - [ - 'route' => '[route name]', // <- Specify the route name. - 'action' => '[route action]', // <- Specify the route action. - 'alias' => '/[alias]', // <- Here you must add an alias for the - // specific route ex: /list/detail -> /product-detail. - 'exchange' => [ // <- If you want to exchange your route - // attribute specify the exchange configuration or leave it empty. - '[attribute name]' => [ // <- Attribute name. - 'table' => '[table name]', // <- The main table from which the attribute belongs. - 'identifier' => '[table identifier]', // <- This must be the main attribute afferent column. - 'exchangeColumn' => '[exchange column name]', // <- Specify the main column from which the slug will be generated. - 'slugColumn' => '[slug column]' // <- This will be the main storage column for the generated slug. - ], - . - . - // You can add here more attribute. - ] - ], - . - . - // You can add here more routes. - ] - ] -``` -The main functionality of the `exchange` key is to replace one or more attributes that play the role of unique identifier later being used in controllers to access certain information, these unique identifiers can have an unfriendly format, irrelevant to the end user. - -Therefore, we can replace the attribute with an auto-generated value from the column specified in the `exchangeColumn` key and then saved in the `slugColumn`. - -Of course, we will have to make certain changes in the respective table requiring a field in which the generated value will be saved. - -```php - // Example of slug configuration. - 'slug_configuration' => [ - 'detect_duplicates' => true, - 'slug_route' => [ - [ - 'route' => 'page', - 'action' => 'home', - 'alias' => '/home-page', - 'exchange' => [] - ], - // The slug configuration will replace the basic route pattern with a specified alias so - // the route path will be changed from `/page/home` to `/home-page`. - [ - 'route' => 'product', - 'action' => 'detail', - 'alias' => '/product', - 'exchange' => [ - 'uuid' => [ - 'table' => 'products', - 'identifier' => 'uuid', - 'exchangeColumn' => 'name', - 'slugColumn' => 'slug' - ], - ] - ], - // The above configuration will replace the `uuid` attribute from - // the route declaration path `/product[/{action}[/{uuid}]]` with a generated value from - // `exchangeColumn` and stored in `slugColumn` key without changing his main functionality - // Therefore, changing the path with a more friendly format from - // `/product/detail/2e4c49d2-8187-11eb-a1c1-0c4de9a75a56` to `/product/product-name`. - ] - ] -``` - -*Remember that you can add the slug configuration only to a valid registered route !* - -For redirect response you must use `UrlHelperPlugin::class` to generate the url, this class can detect the slug configuration. - ## Languages The `local.php.dist` file provides an example for working with multiple languages. The `translator` variable can be expanded to other languages using [Poedit](https://poedit.net/) which can edit `.po` files like the example in `data/language/da_DK/LC_MESSAGES/messages.po`. The compiled file will have the extension `.mo` diff --git a/composer.json b/composer.json index 66acd2c1..bcb13b01 100644 --- a/composer.json +++ b/composer.json @@ -87,8 +87,7 @@ "Frontend\\Contact\\": "src/Contact/src/", "Frontend\\Page\\": "src/Page/src/", "Frontend\\Plugin\\": "src/Plugin/src/", - "Frontend\\User\\": "src/User/src/", - "Frontend\\Slug\\": "src/Slug/src/" + "Frontend\\User\\": "src/User/src/" } }, "autoload-dev": { diff --git a/config/autoload/slug.global.php.dist b/config/autoload/slug.global.php.dist deleted file mode 100644 index e36afe9b..00000000 --- a/config/autoload/slug.global.php.dist +++ /dev/null @@ -1,36 +0,0 @@ - [ - * [ - * 'route' => 'contact', <- route name - * 'action' => 'form', <- route action - * 'alias' => '/contact', <- route alias, this will replace /routePath/action - * 'exchange' => [ <- if you want to exchange your route attribute specify - * the exchange configuration or leave it empty - * 'hash' => [ <- attribute name - * 'table' => 'user', <- table name - * 'identifier' => 'uuid', <- main attribute identifier ex. id, uuid - * 'exchangeColumn' => 'identity', <- exchange value from witch the slug will be generated - * 'slugColumn' => 'slug' <- slug column where the slug will be stored - * ] - * ] - * ], - * - * Use UrlHelperPlugin::class to generate url, this class can detect slug. - */ - - 'slug_configuration' => [ - 'detect_duplicates' => true, - 'slug_route' => [ - ] - ] -]; diff --git a/config/autoload/templates.global.php b/config/autoload/templates.global.php index e0fd36e0..49f90d76 100644 --- a/config/autoload/templates.global.php +++ b/config/autoload/templates.global.php @@ -3,7 +3,6 @@ declare(strict_types=1); use Dot\DebugBar\Extension\DebugBarExtension; -use Frontend\Slug\Factory\RouteExtensionFactory; use Twig\Environment; use Mezzio\Twig\TwigEnvironmentFactory; use Mezzio\Twig\TwigRendererFactory; @@ -11,7 +10,6 @@ use Mezzio\Template\TemplateRendererInterface; use Dot\Twig\Extension\DateExtension; use Dot\Twig\Extension\TranslationExtension; -use Frontend\Slug\TwigExtension\RouteExtension; return [ 'dependencies' => [ @@ -20,7 +18,6 @@ TemplateRendererInterface::class => TwigRendererFactory::class, DateExtension::class => InvokableFactory::class, TranslationExtension::class => InvokableFactory::class, - RouteExtension::class => RouteExtensionFactory::class, ], ], 'debug' => false, @@ -36,7 +33,6 @@ 'extensions' => [ DateExtension::class, TranslationExtension::class, - RouteExtension::class, DebugBarExtension::class, ], 'optimizations' => -1, diff --git a/config/config.php b/config/config.php index fba34cb9..9b99ac21 100644 --- a/config/config.php +++ b/config/config.php @@ -51,7 +51,6 @@ class_exists(\Mezzio\Swoole\ConfigProvider::class) \Frontend\Page\ConfigProvider::class, \Frontend\Plugin\ConfigProvider::class, \Frontend\User\ConfigProvider::class, - \Frontend\Slug\ConfigProvider::class, // Load application config in a pre-defined order in such a way that local settings // overwrite global settings. (Loaded as first to last): diff --git a/config/pipeline.php b/config/pipeline.php index 9fc5e302..8f4b9e58 100644 --- a/config/pipeline.php +++ b/config/pipeline.php @@ -23,7 +23,6 @@ use Dot\Rbac\Guard\Middleware\ForbiddenHandler; use Dot\Rbac\Guard\Middleware\RbacGuardMiddleware; use Frontend\App\Middleware\AuthMiddleware; -use Frontend\Slug\Middleware\SlugMiddleware; /** * Setup middleware pipeline: @@ -55,9 +54,6 @@ // - $app->pipe('/docs', $apiDocMiddleware); // - $app->pipe('/files', $filesMiddleware); - // Slug Middleware must be triggered before RouteMiddleware! - $app->pipe(SlugMiddleware::class); - // Register the routing middleware in the middleware pipeline. // This middleware registers the Mezzio\Router\RouteResult request attribute. $app->pipe(RouteMiddleware::class); diff --git a/src/Plugin/src/Factory/PluginManagerFactory.php b/src/Plugin/src/Factory/PluginManagerFactory.php index 8af01d6a..256c2605 100644 --- a/src/Plugin/src/Factory/PluginManagerFactory.php +++ b/src/Plugin/src/Factory/PluginManagerFactory.php @@ -4,10 +4,9 @@ namespace Frontend\Plugin\Factory; +use Dot\Controller\Plugin\UrlHelperPlugin; use Frontend\Plugin\PluginManager; use Frontend\Plugin\TemplatePlugin; -use Frontend\Plugin\UrlHelperPlugin; -use Frontend\Slug\SlugInterface; use Mezzio\Helper\UrlHelper; use Mezzio\Template\TemplateRendererInterface; use Psr\Container\ContainerExceptionInterface; @@ -31,12 +30,9 @@ public function __invoke(ContainerInterface $container): PluginManager $pluginManager = new PluginManager($container, $container->get('config')['dot_controller']['plugin_manager']); //register the built-in plugins, if the required component is present - if ($container->has(UrlHelper::class) && $container->has(SlugInterface::class)) { + if ($container->has(UrlHelper::class)) { $pluginManager->setFactory('url', function (ContainerInterface $container) { - return new UrlHelperPlugin( - $container->get(UrlHelper::class), - $container->get(SlugInterface::class) - ); + return new UrlHelperPlugin($container->get(UrlHelper::class)); }); } diff --git a/src/Plugin/src/UrlHelperPlugin.php b/src/Plugin/src/UrlHelperPlugin.php deleted file mode 100644 index 34df033e..00000000 --- a/src/Plugin/src/UrlHelperPlugin.php +++ /dev/null @@ -1,83 +0,0 @@ -urlHelper = $helper; - $this->slugAdapter = $slugAdapter; - } - - /** - * @param string|null $routeName - * @param array $routeParams - * @param array $queryParams - * @param $fragmentIdentifier - * @param array $options - * @return string|$this - * @throws Exception - * @throws MissingConfigurationException - */ - public function __invoke( - string $routeName = null, - array $routeParams = [], - array $queryParams = [], - $fragmentIdentifier = null, - array $options = [] - ): UrlHelperPlugin|string { - $args = func_get_args(); - if (empty($args)) { - return $this; - } - - return $this->generate($routeName, $routeParams, $queryParams, $fragmentIdentifier, $options); - } - - /** - * @param string|null $routeName - * @param array $routeParams - * @param array $queryParams - * @param null $fragmentIdentifier - * @param array $options - * @return string - * @throws Exception - * @throws MissingConfigurationException - */ - public function generate( - string $routeName = null, - array $routeParams = [], - array $queryParams = [], - $fragmentIdentifier = null, - array $options = [] - ): string { - - $response = $this->slugAdapter->match($routeName, $routeParams, $queryParams, $fragmentIdentifier, $options); - - if ($response->isSuccess()) { - return $response->getUrl(); - } - - return $this->urlHelper->generate($routeName, $routeParams, $queryParams, $fragmentIdentifier, $options); - } -} diff --git a/src/Slug/src/ConfigProvider.php b/src/Slug/src/ConfigProvider.php deleted file mode 100644 index f5d2b453..00000000 --- a/src/Slug/src/ConfigProvider.php +++ /dev/null @@ -1,49 +0,0 @@ - $this->getDependencies(), - ]; - } - - /** - * @return array - */ - public function getDependencies(): array - { - return [ - 'factories' => [ - SlugCollector::class => SlugCollectorFactory::class, - SlugMiddleware::class => AnnotatedServiceFactory::class, - SlugService::class => AnnotatedServiceFactory::class, - RouteExtension::class => RouteExtensionFactory::class, - ], - 'aliases' => [ - SlugInterface::class => SlugCollector::class, - SlugServiceInterface::class => SlugService::class - ], - ]; - } -} diff --git a/src/Slug/src/DuplicateSlugDetector.php b/src/Slug/src/DuplicateSlugDetector.php deleted file mode 100644 index 4f4c9e7a..00000000 --- a/src/Slug/src/DuplicateSlugDetector.php +++ /dev/null @@ -1,68 +0,0 @@ -throwOnDuplicate($slug); - $this->remember($slug); - } - - /** - * @param Slug $slug - */ - private function remember(Slug $slug): void - { - $this->slugs[$slug->getAlias()] = $slug; - } - - /** - * @param Slug $slug - * @throws DuplicateSlugException - */ - private function throwOnDuplicate(Slug $slug): void - { - if (isset($this->slugs[$slug->getAlias()])) { - $this->duplicateRouteDetected($slug); - } - } - - /** - * @param Slug $slug - * @throws DuplicateSlugException - */ - private function duplicateRouteDetected(Slug $slug): void - { - throw new DuplicateSlugException( - sprintf( - 'Duplicate slug detected; alias "%s" ', - $slug->getAlias() - ) - ); - } -} diff --git a/src/Slug/src/Exception/DuplicateSlugException.php b/src/Slug/src/Exception/DuplicateSlugException.php deleted file mode 100644 index cf35b955..00000000 --- a/src/Slug/src/Exception/DuplicateSlugException.php +++ /dev/null @@ -1,16 +0,0 @@ -get(UrlHelper::class), - $container->get(SlugInterface::class), - $container->get(ServerUrlHelper::class) - ); - } -} diff --git a/src/Slug/src/Factory/SlugCollectorFactory.php b/src/Slug/src/Factory/SlugCollectorFactory.php deleted file mode 100644 index 0d8569be..00000000 --- a/src/Slug/src/Factory/SlugCollectorFactory.php +++ /dev/null @@ -1,40 +0,0 @@ -get('config')['slug_configuration'] ?? []; - - return new $requestedName( - $container->get(RouterInterface::class), - $container->get(UrlHelper::class), - $container->get(SlugServiceInterface::class), - $config, - $config['detect_duplicates'] ?? true - ); - } -} diff --git a/src/Slug/src/Middleware/SlugMiddleware.php b/src/Slug/src/Middleware/SlugMiddleware.php deleted file mode 100644 index cd67564d..00000000 --- a/src/Slug/src/Middleware/SlugMiddleware.php +++ /dev/null @@ -1,54 +0,0 @@ -slugAdapter = $slugAdapter; - } - - /** - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface - * @throws Exception - * @throws MissingConfigurationException - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - $result = $this->slugAdapter->matchRequest($request); - - if ($result->isSuccess()) { - $request = $request->withUri(new Uri($result->getUrl())); - } - - return $handler->handle($request); - } -} diff --git a/src/Slug/src/Service/SlugService.php b/src/Slug/src/Service/SlugService.php deleted file mode 100644 index 81ae673b..00000000 --- a/src/Slug/src/Service/SlugService.php +++ /dev/null @@ -1,259 +0,0 @@ -em = $em; - } - - /** - * @param Slug $slug - * @param string $attribute - * @param string $value - * @return mixed - * @throws MissingConfigurationException - */ - public function slugManipulation(Slug $slug, string $attribute, string $value): mixed - { - $exchange = $slug->getExchange(); - $exchange = array_reduce( - $exchange, - function ($matched, $exchange) use ($attribute) { - if (isset($exchange[$attribute])) { - return $matched; - } - - return $exchange; - }, - false - ); - - if (is_array($exchange)) { - $this->checkExchange($slug, $exchange, $attribute); - - $result = $this->proceedSlug($slug, $value, $exchange); - if ($result) { - if ($slug->getType() === Slug::REQUEST_TYPE) { - return $this->processUuidToString($result[$exchange['identifier']]); - } - if ($result[$exchange['exchangeColumn']]) { - if (isset($result[$exchange['slugColumn']]) && !is_null($result[$exchange['slugColumn']])) { - return $result[$exchange['slugColumn']]; - } else { - return $this->generateSlug($result, $exchange); - } - } else { - return $value; - } - } - } - return false; - } - - /** - * @param array $param - * @param array $exchange - * @return string - */ - protected function generateSlug(array $param, array $exchange): string - { - $exchangeValue = $param[$exchange['exchangeColumn']]; - $exchangeValue = strtolower($exchangeValue); - $exchangeValue = str_replace(' ', '-', $exchangeValue); - $exchangeValue = preg_replace('/[^A-Za-z0-9\-]/', '', $exchangeValue); - $exchangeValue = str_replace('.com', '', $exchangeValue); - - $response = $this->checkDuplicateSlug($exchangeValue, $exchange); - - if ($response) { - $exchangeValue .= '-' . $this->getSlugSuffix($param, $exchange); - } - - try { - $stmt = $this->em->getConnection()->prepare( - 'UPDATE `' . $exchange['table'] . '` SET `' . $exchange['slugColumn'] . - '` = :slug WHERE `' . $exchange['identifier'] . '` = :identifier' - ); - $stmt->bindValue('slug', $this->clean($exchangeValue)); - $stmt->bindValue('identifier', $param[$exchange['identifier']]); - $stmt->executeStatement(); - return $exchangeValue; - } catch (Exception $exception) { - throw new RuntimeException($exception->getMessage()); - } - } - - /** - * @param $param - * @param array $exchange - * @return int - */ - protected function getSlugSuffix($param, array $exchange): int - { - try { - $stmt = $this->em->getConnection()->prepare( - 'SELECT ' . $exchange['exchangeColumn'] . ' FROM `' . $exchange['table'] . '` WHERE `' . - $exchange['exchangeColumn'] . '` = :exchangeColumn AND `' . - $exchange['slugColumn'] . '` IS NOT NULL' - ); - $stmt->bindValue('exchangeColumn', $param[$exchange['exchangeColumn']]); - return $stmt->executeQuery()->rowCount(); - } catch (Exception $exception) { - throw new RuntimeException($exception->getMessage()); - } - } - - /** - * @param $input - * @return string - */ - protected function clean($input): string - { - return preg_replace('/[^A-Za-z0-9. -]/', '', $input); - } - - /** - * @param string $slug - * @param array $exchange - * @return int - */ - protected function checkDuplicateSlug(string $slug, array $exchange): int - { - try { - $stmt = $this->em->getConnection()->prepare( - 'SELECT ' . $exchange['slugColumn'] . ' FROM `' . $exchange['table'] . '` WHERE `' . - $exchange['slugColumn'] . '` = :slug' - ); - $stmt->bindValue('slug', $this->clean($slug)); - return $stmt->executeQuery()->rowCount(); - } catch (Exception $exception) { - throw new RuntimeException($exception->getMessage()); - } - } - - /** - * @param Slug $slug - * @param string $param - * @param array $db - * @return bool|array - */ - protected function proceedSlug(Slug $slug, string $param, array $db): bool|array - { - $searchParam = $db['identifier']; - if ($slug->getType() === Slug::REQUEST_TYPE) { - $searchParam = $db['slugColumn']; - } - - try { - $table = $db['table']; - unset($db['table']); - $column = array_values($db); - $column = implode(',', $column); - - $stmt = $this->em->getConnection()->prepare( - 'SELECT ' . $column . ' FROM `' . $table . '` WHERE `' . $searchParam . '` = :searchParam' - ); - if ($slug->getType() === Slug::REQUEST_TYPE) { - $stmt->bindValue('searchParam', $this->escapeCharacter($param)); - } else { - $stmt->bindValue('searchParam', $param, UuidBinaryOrderedTimeType::NAME); - } - - return $stmt->executeQuery()->fetchAssociative(); - } catch (Exception $exception) { - throw new RuntimeException($exception->getMessage()); - } - } - - /** - * @param $input - * @return string|string[] - */ - protected function escapeCharacter($input): array|string - { - return str_replace( - ['\\', "\0", "\n", "\r", "'", '"', "\x1a"], - ['\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'], - $input - ); - } - - /** - * @param string $attributeUuid - * @return string - */ - protected function processUuidToString(string $attributeUuid): string - { - return $this->getUuidGenerator()->fromBytes($attributeUuid)->toString(); - } - - /** - * @return UuidFactory - */ - private function getUuidGenerator(): UuidFactory - { - /** @var UuidFactory $factory */ - $factory = clone Uuid::getFactory(); - $codec = new OrderedTimeCodec($factory->getUuidBuilder()); - $factory->setCodec($codec); - - return $factory; - } - - /** - * @param Slug $slug - * @param array $exchange - * @param string $attribute - * @throws MissingConfigurationException - */ - private function checkExchange(Slug $slug, array $exchange, string $attribute) - { - foreach (self::CONFIGURATION as $configuration) { - if (!isset($exchange[$configuration])) { - throw new MissingConfigurationException( - sprintf( - 'Missing "%s" configuration , trace --> slug : "%s" , exchange attribute : "%s"', - $configuration, - $slug->getAlias(), - $attribute - ) - ); - } - } - } -} diff --git a/src/Slug/src/Service/SlugServiceInterface.php b/src/Slug/src/Service/SlugServiceInterface.php deleted file mode 100644 index d131643f..00000000 --- a/src/Slug/src/Service/SlugServiceInterface.php +++ /dev/null @@ -1,24 +0,0 @@ -alias = $alias; - $this->routeName = $routeName; - $this->params = $params; - $this->exchange = $exchange; - $this->type = self::URL_TYPE; - } - - /** - * @return string - */ - public function getAlias(): string - { - return $this->alias; - } - - /** - * @param string $alias - */ - public function setAlias(string $alias): void - { - $this->alias = $alias; - } - - /** - * @return array - */ - public function getParams(): array - { - return $this->params; - } - - /** - * @param array $params - */ - public function setParams(array $params): void - { - $this->params = $params; - } - - /** - * @return string - */ - public function getRouteName(): string - { - return $this->routeName; - } - - /** - * @param string $routeName - */ - public function setRouteName(string $routeName): void - { - $this->routeName = $routeName; - } - - /** - * @return array - */ - public function getExchange(): array - { - return $this->exchange; - } - - /** - * @param array $exchange - */ - public function setExchange(array $exchange): void - { - $this->exchange = $exchange; - } - - /** - * @return string - */ - public function getType(): string - { - return $this->type; - } - - /** - * @param string $type - */ - public function setType(string $type): void - { - $this->type = $type; - } -} diff --git a/src/Slug/src/SlugCollector.php b/src/Slug/src/SlugCollector.php deleted file mode 100644 index bb76e6a9..00000000 --- a/src/Slug/src/SlugCollector.php +++ /dev/null @@ -1,443 +0,0 @@ -router = $router; - $this->url = $url; - $this->slugService = $slugService; - $this->config = $config; - $this->detectDuplicates = $detectDuplicates; - - $this->duplicateSlugDetector = new DuplicateSlugDetector(); - - try { - $this->loadConfig($config); - } catch (DuplicateSlugException $e) { - throw new DuplicateSlugException($e->getMessage(), $e->getCode()); - } - } - - /** - * Load configuration parameters - * - * @param array $config Array of custom configuration options. - * @throws DuplicateSlugException - */ - public function loadConfig(array $config) - { - if (empty($config)) { - return; - } - - if (isset($this->config['slug_route'])) { - foreach ($this->config['slug_route'] as $slugRoute) { - $params = []; - $params['action'] = $slugRoute['action']; - $this->slug($slugRoute['alias'], $slugRoute['route'], $params, $slugRoute['exchange'] ?? []); - } - } - } - - /** - * Add a slug for the slug middleware to match. - * @param string $alias - * @param string $routeName - * @param array $params - * @param array $exchange - * @return Slug - * @throws DuplicateSlugException - */ - public function slug( - string $alias, - string $routeName, - array $params, - array $exchange - ): Slug { - $slug = new Slug($alias, $routeName, $params, $exchange); - if ($this->detectDuplicates) { - $this->detectDuplicate($slug); - } - $this->slugs[] = $slug; - return $slug; - } - - /** - * Retrieve all directly registered slugs with the application. - * - * @return Slug[] - */ - public function getSlugs(): array - { - return $this->slugs; - } - - /** - * @param $routeName - * @param $routeParams - * @param $queryParams - * @param $fragmentIdentifier - * @param $options - * @return SlugResult - * @throws Exception - * @throws MissingConfigurationException - */ - public function match( - $routeName, - $routeParams, - $queryParams, - $fragmentIdentifier, - $options - ): SlugResult { - /** @var Slug $slug */ - $slug = array_reduce($this->slugs, function ($matched, $slug) use ($routeName, $routeParams, $queryParams) { - if ($routeName !== $slug->getRouteName()) { - return $matched; - } - if ($routeParams['action'] !== $slug->getParams()['action']) { - return $matched; - } - return $slug; - }, false); - - - if (!($slug instanceof Slug)) { - return SlugResult::fromSlugFailure(); - } - - $routerOptions = $options['router'] ?? []; - $path = $this->router->generateUri($routeName, $routeParams, $routerOptions); - - $request = new ServerRequest(); - $request = $request->withUri(new Uri($path)); - $match = $this->router->match($request); - $slug->setType(Slug::URL_TYPE); - - if ($match->isSuccess()) { - $path = $this->generateUri($slug, $match); - $path = $this->appendQueryStringArguments($path, $queryParams); - $path = $this->appendFragment($path, $fragmentIdentifier); - } else { - $path = $this->url->generate($routeName, $routeParams, $queryParams, $fragmentIdentifier, $options); - } - - return SlugResult::fromSlug($slug, $path); - } - - /** - * @param ServerRequestInterface $request - * @return SlugResult - * @throws Exception - * @throws MissingConfigurationException - */ - public function matchRequest(ServerRequestInterface $request): SlugResult - { - $path = rawurldecode($request->getUri()->getPath()); - $fragments = explode('/', $path); - $path = '/' . $fragments[1]; - - /** @var Slug $slug */ - $slug = array_reduce($this->slugs, function ($matched, $slug) use ($path) { - if ($path !== $slug->getAlias()) { - return $matched; - } - return $slug; - }, false); - - if (!($slug instanceof Slug)) { - return SlugResult::fromSlugFailure(); - } - - $path = $this->router->generateUri($slug->getRouteName(), $slug->getParams(), $fragments); - - $serverRequest = new ServerRequest(); - $serverRequest = $serverRequest->withUri(new Uri($path)); - - $match = $this->router->match($serverRequest); - - $matchParams = $this->matchParams($request, $match); - - $slug->setType(Slug::REQUEST_TYPE); - - if ($match->isSuccess()) { - $path = $this->generateUri($slug, $match, $matchParams); - } else { - return SlugResult::fromSlugFailure(); - } - - return SlugResult::fromSlug($slug, $path, $matchParams); - } - - /** - * @param Slug $slug - * @param RouteResult $routeResult - * @param array $matchParams - * @return string - * @throws MissingConfigurationException - */ - public function generateUri(Slug $slug, RouteResult $routeResult, array $matchParams = []): string - { - $route = $routeResult->getMatchedRoute(); - $substitutions = $routeResult->getMatchedParams(); - - if ($slug->getType() === Slug::REQUEST_TYPE) { - $substitutions = array_merge($routeResult->getMatchedParams(), $matchParams); - } - - $routeParser = new Std(); - $routes = array_reverse($routeParser->parse($route->getPath())); - $missingParameters = []; - foreach ($routes as $parts) { - // Check if all parameters can be substituted - $missingParameters = $this->missingParameters($parts, $substitutions); - // If not all parameters can be substituted, try the next route - if (! empty($missingParameters)) { - continue; - } - $path = $slug->getAlias(); - if ($slug->getType() === Slug::REQUEST_TYPE) { - $path = ''; - } - - foreach ($parts as $p => $part) { - if ($slug->getType() === Slug::URL_TYPE) { - if (is_string($part)) { - // Append the string - $path .= ''; - continue; - } - - if ($part[0] !== self::REMOVABLE_PART) { - // Check substitute value with regex - /** @psalm-suppress UndefinedVariable */ - if (!empty($addOns)) { - $substitutions[$part[0]] = $addOns[$p]; - } - - if (!preg_match('~^' . $part[1] . '$~', (string)$substitutions[$part[0]])) { - throw new RuntimeException( - sprintf( - 'Parameter value for [%s] did not match the regex `%s`', - $part[0], - $part[1] - ) - ); - } - - $attribute = $substitutions[$part[0]]; - if (!empty($slug->getExchange())) { - $attribute = $this->slugService->slugManipulation( - $slug, - $part[0], - $attribute - ); - } - - // Append the substituted value - $path .= '/' . $attribute; - } - } else { - if (is_string($part)) { - // Append the string - if ($part !== '/') { - $path .= $part; - } - continue; - } - - if (!preg_match('~^' . $part[1] . '$~', (string)$substitutions[$part[0]])) { - throw new RuntimeException( - sprintf( - 'Parameter value for [%s] did not match the regex `%s`', - $part[0], - $part[1] - ) - ); - } - - $attribute = $substitutions[$part[0]]; - if ($part[0] !== self::REMOVABLE_PART) { - if (!empty($slug->getExchange())) { - $attribute = $this->slugService->slugManipulation( - $slug, - $part[0], - $attribute - ); - } - if ($attribute) { - $path .= '/' . $attribute; - } - } else { - $path .= $attribute; - } - } - } - // Return generated path - return $path; - } - - // No valid route was found: list minimal required parameters - throw new RuntimeException(sprintf( - 'Route `%s` expects at least parameter values for [%s], but received [%s]', - $routeResult->getMatchedRouteName(), - implode(',', $missingParameters), - implode(',', array_keys($substitutions)) - )); - } - - /** - * Checks for any missing route parameters - * @param array $parts - * @param array $substitutions - * @return array - */ - private function missingParameters(array $parts, array $substitutions): array - { - $missingParameters = []; - - foreach ($parts as $part) { - if (is_string($part)) { - continue; - } - - $missingParameters[] = $part[0]; - } - - foreach ($missingParameters as $param) { - if (! isset($substitutions[$param])) { - return $missingParameters; - } - } - return []; - } - - /** - * @param string $uriString - * @param array $queryParams - * @return string - */ - private function appendQueryStringArguments(string $uriString, array $queryParams): string - { - if (count($queryParams) > 0) { - return sprintf('%s?%s', $uriString, http_build_query($queryParams)); - } - return $uriString; - } - - /** - * @param string $uriString - * @param string|null $fragmentIdentifier - * @return string - */ - private function appendFragment(string $uriString, ?string $fragmentIdentifier): string - { - if ($fragmentIdentifier !== null) { - if (! preg_match(self::FRAGMENT_IDENTIFIER_REGEX, $fragmentIdentifier)) { - throw new InvalidArgumentException('Fragment identifier must conform to RFC 3986', 400); - } - - return sprintf('%s#%s', $uriString, $fragmentIdentifier); - } - return $uriString; - } - - /** - * @param ServerRequestInterface $request - * @param RouteResult $routeResult - * @return array - */ - public function matchParams(ServerRequestInterface $request, RouteResult $routeResult): array - { - $route = $routeResult->getMatchedRoute(); - $routeParser = new Std(); - $routes = array_reverse($routeParser->parse($route->getPath())); - - $fragments = explode('/', $request->getUri()->getPath()); - - $mParams = []; - foreach ($routes as $parts) { - // Generate the path - $validParams = []; - - foreach ($parts as $part) { - if ($part[0] !== '/' && $part[0] !== 'action') { - $validParams[] = $part[0]; - } - } - foreach ($validParams as $p => $part) { - if (isset($fragments[$p + 2])) { - $mParams[$part] = $fragments[$p + 2]; - } - } - return $mParams; - } - return []; - } - - /** - * @param Slug $slug - * @throws DuplicateSlugException - */ - private function detectDuplicate(Slug $slug): void - { - $this->duplicateSlugDetector?->detectDuplicate($slug); - } -} diff --git a/src/Slug/src/SlugInterface.php b/src/Slug/src/SlugInterface.php deleted file mode 100644 index 2c39691c..00000000 --- a/src/Slug/src/SlugInterface.php +++ /dev/null @@ -1,48 +0,0 @@ -success = true; - $result->slug = $slug; - $result->url = $url; - $result->matchedParams = $matchedParams; - - return $result; - } - - /** - * Create an instance representing a slug failure. - * - * @return SlugResult - */ - public static function fromSlugFailure(): self - { - $result = new self(); - $result->success = false; - - return $result; - } - - /** - * Does the result represent successful route match? - */ - public function isSuccess(): bool - { - return $this->success; - } - - /** - * Retrieve the slug that resulted in the slug match. - * - * @return bool|Slug|null - */ - public function getMatchedSlug(): bool|Slug|null - { - return $this->isFailure() ? false : $this->slug; - } - - /** - * Returns the generated Url. - */ - public function getUrl(): string - { - return $this->url; - } - - /** - * Returns the matched params. - */ - public function getMatchedParams(): array - { - return $this->matchedParams; - } - - /** - * Is this a slug match failure result? - */ - public function isFailure(): bool - { - return ! $this->success; - } -} diff --git a/src/Slug/src/TwigExtension/RouteExtension.php b/src/Slug/src/TwigExtension/RouteExtension.php deleted file mode 100644 index 6cb6b249..00000000 --- a/src/Slug/src/TwigExtension/RouteExtension.php +++ /dev/null @@ -1,99 +0,0 @@ -urlHelper = $urlHelper; - $this->slugAdapter = $slugAdapter; - $this->serverUrlHelper = $serverUrlHelper; - } - - /** - * @return TwigFunction[] - */ - public function getFunctions(): array - { - return [ - new TwigFunction('path', [$this, 'renderUri']), - new TwigFunction('url', [$this, 'renderUrl']), - ]; - } - - /** - * @param string|null $route - * @param array $routeParams - * @param array $queryParams - * @param string|null $fragmentIdentifier - * @param array $options - * @return string - * @throws Exception - * @throws MissingConfigurationException - */ - public function renderUri( - ?string $route = null, - array $routeParams = [], - array $queryParams = [], - ?string $fragmentIdentifier = null, - array $options = [] - ): string { - $response = $this->slugAdapter->match($route, $routeParams, $queryParams, $fragmentIdentifier, $options); - - if ($response->isSuccess()) { - return $response->getUrl(); - } - - return $this->urlHelper->generate($route, $routeParams, $queryParams, $fragmentIdentifier, $options); - } - - /** - * @param string|null $route - * @param array $routeParams - * @param array $queryParams - * @param string|null $fragmentIdentifier - * @param array $options - * @return string - * @throws Exception - * @throws MissingConfigurationException - */ - public function renderUrl( - ?string $route = null, - array $routeParams = [], - array $queryParams = [], - ?string $fragmentIdentifier = null, - array $options = [] - ): string { - return $this->serverUrlHelper->generate( - $this->renderUri($route, $routeParams, $queryParams, $fragmentIdentifier, $options) - ); - } -}