diff --git a/config/services/adapter.yaml b/config/services/adapter.yaml index 9154bfc..76e97de 100644 --- a/config/services/adapter.yaml +++ b/config/services/adapter.yaml @@ -42,6 +42,14 @@ services: tags: - { name: i18n.adapter.path.generator, alias: document } + I18nBundle\Adapter\PathGenerator\DataObject: + parent: I18nBundle\Adapter\PathGenerator\AbstractPathGenerator + arguments: + - '@router' + - '@event_dispatcher' + tags: + - { name: i18n.adapter.path.generator, alias: data_object } + # # Adapter: ReDirector @@ -63,4 +71,4 @@ services: I18nBundle\Adapter\Redirector\FallbackRedirector: parent: I18nBundle\Adapter\Redirector\AbstractRedirector tags: - - { name: i18n.adapter.redirector, alias: fallback, priority: 100 } \ No newline at end of file + - { name: i18n.adapter.redirector, alias: fallback, priority: 100 } diff --git a/src/Adapter/PathGenerator/DataObject.php b/src/Adapter/PathGenerator/DataObject.php new file mode 100644 index 0000000..25cae90 --- /dev/null +++ b/src/Adapter/PathGenerator/DataObject.php @@ -0,0 +1,63 @@ +buildAlternateRoutesStack( + $i18nContext, + RouteItemInterface::DATA_OBJECT_ROUTE, + I18nEvents::PATH_ALTERNATE_DATA_OBJECT_ROUTE + ); + } + + /** + * @throws \Exception + */ + protected function generateLink(AlternateRouteItemInterface $routeItem): string + { + $routeParameters = $this->alternateRouteItemTransformer->reverseTransformToArray($routeItem, [ + 'type' => RouteItemInterface::DATA_OBJECT_ROUTE, + ]); + + if ($routeItem->getEntity() === null || $routeItem->getUrlSlug() === null) { + throw new \RuntimeException( + 'Cannot create Data Object route URL. Object and/or UrlSlug is missing!' + ); + } + + return $this->router->generate( + $routeItem->getRouteName(), + $routeParameters, + UrlGeneratorInterface::ABSOLUTE_URL + ); + } +} diff --git a/src/Builder/RouteItemBuilder.php b/src/Builder/RouteItemBuilder.php index 5b54913..99b87c2 100644 --- a/src/Builder/RouteItemBuilder.php +++ b/src/Builder/RouteItemBuilder.php @@ -83,6 +83,10 @@ public function buildRouteItemByRequest(Request $baseRequest, ?Document $baseDoc $routeItem = $this->routeItemFactory->create(RouteItemInterface::STATIC_ROUTE, false); } elseif (str_starts_with($currentRouteName, 'document_')) { $routeItem = $this->routeItemFactory->create(RouteItemInterface::DOCUMENT_ROUTE, false); + } elseif (str_starts_with($currentRouteName, 'data_object_')) { + $routeItem = $this->routeItemFactory->create(RouteItemInterface::DATA_OBJECT_ROUTE, true); + $routeItem->setEntity($baseRequest->attributes->get('object')); + $routeItem->setUrlSlug($baseRequest->attributes->get('urlSlug')); } elseif ($baseRequest->attributes->has(Definitions::ATTRIBUTE_I18N_ROUTE_IDENTIFIER)) { $routeItem = $this->routeItemFactory->create(RouteItemInterface::SYMFONY_ROUTE, false); } diff --git a/src/I18nEvents.php b/src/I18nEvents.php index 3719d8e..f3a5730 100644 --- a/src/I18nEvents.php +++ b/src/I18nEvents.php @@ -18,6 +18,7 @@ final class I18nEvents public const CONTEXT_SWITCH = 'i18n.switch'; public const PATH_ALTERNATE_STATIC_ROUTE = 'i18n.path.static_route.alternate'; public const PATH_ALTERNATE_SYMFONY_ROUTE = 'i18n.path.symfony_route.alternate'; + public const PATH_ALTERNATE_DATA_OBJECT_ROUTE = 'i18n.path.data_object_route.alternate'; public const PREVIEW_CONFIG_GENERATION = 'i18n.preview.config_generation'; public const PREVIEW_URL_GENERATION = 'i18n.preview.url_generation'; } diff --git a/src/Model/RouteItem/BaseRouteItem.php b/src/Model/RouteItem/BaseRouteItem.php index 7ed31a2..5535a6f 100644 --- a/src/Model/RouteItem/BaseRouteItem.php +++ b/src/Model/RouteItem/BaseRouteItem.php @@ -13,6 +13,7 @@ namespace I18nBundle\Model\RouteItem; +use Pimcore\Model\DataObject\Data\UrlSlug; use Pimcore\Model\Element\ElementInterface; use Symfony\Component\HttpFoundation\ParameterBag; @@ -25,6 +26,7 @@ abstract class BaseRouteItem protected ParameterBag $routeParameters; protected ParameterBag $routeAttributes; protected ParameterBag $routeContext; + protected ?UrlSlug $urlSlug = null; public function __construct(string $type, bool $headless) { @@ -32,6 +34,7 @@ public function __construct(string $type, bool $headless) RouteItemInterface::DOCUMENT_ROUTE, RouteItemInterface::SYMFONY_ROUTE, RouteItemInterface::STATIC_ROUTE, + RouteItemInterface::DATA_OBJECT_ROUTE, ], true)) { throw new \Exception(sprintf('Invalid RouteItem type "%s"', $type)); } @@ -127,4 +130,14 @@ public function setRouteName(?string $routeName): void { $this->routeName = $routeName; } + + public function getUrlSlug(): ?UrlSlug + { + return $this->urlSlug; + } + + public function setUrlSlug(?UrlSlug $urlSlug): void + { + $this->urlSlug = $urlSlug; + } } diff --git a/src/Model/RouteItem/BaseRouteItemInterface.php b/src/Model/RouteItem/BaseRouteItemInterface.php index eb0a8bc..37e8963 100644 --- a/src/Model/RouteItem/BaseRouteItemInterface.php +++ b/src/Model/RouteItem/BaseRouteItemInterface.php @@ -13,6 +13,7 @@ namespace I18nBundle\Model\RouteItem; +use Pimcore\Model\DataObject\Data\UrlSlug; use Pimcore\Model\Element\ElementInterface; use Symfony\Component\HttpFoundation\ParameterBag; @@ -51,4 +52,8 @@ public function hasRouteName(): bool; public function getRouteName(): ?string; public function setRouteName(?string $routeName): void; + + public function getUrlSlug(): ?UrlSlug; + + public function setUrlSlug(?UrlSlug $urlSlug): void; } diff --git a/src/Model/RouteItem/RouteItemInterface.php b/src/Model/RouteItem/RouteItemInterface.php index 23d6756..b966ba9 100644 --- a/src/Model/RouteItem/RouteItemInterface.php +++ b/src/Model/RouteItem/RouteItemInterface.php @@ -18,4 +18,5 @@ interface RouteItemInterface extends BaseRouteItemInterface public const STATIC_ROUTE = 'static_route'; public const SYMFONY_ROUTE = 'symfony'; public const DOCUMENT_ROUTE = 'document'; + public const DATA_OBJECT_ROUTE = 'data_object'; } diff --git a/src/Modifier/RouteModifier.php b/src/Modifier/RouteModifier.php index 060502c..0e03d14 100644 --- a/src/Modifier/RouteModifier.php +++ b/src/Modifier/RouteModifier.php @@ -20,6 +20,7 @@ use I18nBundle\LinkGenerator\I18nLinkGeneratorInterface; use I18nBundle\Manager\I18nContextManager; use I18nBundle\Model\RouteItem\RouteItemInterface; +use I18nBundle\Model\SiteRequestContext; use I18nBundle\Model\ZoneInterface; use I18nBundle\Model\ZoneSiteInterface; use I18nBundle\Tool\System; @@ -192,23 +193,53 @@ public function buildDocumentPath(I18nContextInterface $i18nContext, int $refere return $documentPath; } - $scheme = $zoneSite->getSiteRequestContext()->getScheme(); - $host = $zoneSite->getSiteRequestContext()->getHost(); - $httpPort = $zoneSite->getSiteRequestContext()->getHttpPort(); - $httpsPort = $zoneSite->getSiteRequestContext()->getHttpsPort(); + return $this->generateAbsoluteUrl($zoneSite->getSiteRequestContext(), $documentPath); + } - $port = ''; - if ($scheme === 'http' && $httpPort !== 80) { - $port = ':' . $httpPort; - } elseif ($scheme === 'https' && $httpsPort !== 443) { - $port = ':' . $httpsPort; + /** + * @throws \Exception + */ + public function buildDataObjectPath(I18nContextInterface $i18nContext, int $referenceType): string + { + $routeItem = $i18nContext->getRouteItem(); + $attributes = $routeItem->isHeadless() + ? $routeItem->getRouteParameters() + : $routeItem->getRouteAttributes(); + $object = $routeItem->getEntity(); + $urlSlug = $routeItem->getUrlSlug(); + + if (!$object instanceof Concrete) { + throw new \RuntimeException( + sprintf( + 'Cannot build data object path. Entity must be an instance of "%s", "%s" given!', + Concrete::class, + get_class($object) + ) + ); } - if (!empty($documentPath)) { - $documentPath = '/' . ltrim($documentPath, '/'); + $slugs = $object->get($urlSlug->getFieldname(), $attributes['_locale']); + + if (empty($slugs)) { + throw new \RuntimeException( + sprintf( + 'No slugs found in object field "%s" for locale "%s"', + $urlSlug->getFieldname(), + $attributes['_locale'] + ) + ); } - return sprintf('%s://%s%s%s', $scheme, $host, $port, $documentPath); + $dataObjectPath = $slugs[0]->getSlug(); + + if ($referenceType !== UrlGeneratorInterface::ABSOLUTE_URL) { + return $dataObjectPath; + } + + return $this->generateAbsoluteUrl( + $i18nContext->getCurrentZoneSite()->getSiteRequestContext(), + $dataObjectPath + ); } protected function translateDynamicRouteKey(ZoneInterface $zone, RouteItemInterface $routeItem, string $key, string $locale): string @@ -267,4 +298,25 @@ protected function validateRouteParameters(string $name, string $routeType, arra return $parameters; } + + private function generateAbsoluteUrl(SiteRequestContext $siteRequestContext, string $path): string + { + $scheme = $siteRequestContext->getScheme(); + $host = $siteRequestContext->getHost(); + $httpPort = $siteRequestContext->getHttpPort(); + $httpsPort = $siteRequestContext->getHttpsPort(); + + $port = ''; + if ($scheme === 'http' && $httpPort !== 80) { + $port = ':' . $httpPort; + } elseif ($scheme === 'https' && $httpsPort !== 443) { + $port = ':' . $httpsPort; + } + + if (!empty($path)) { + $path = '/' . ltrim($path, '/'); + } + + return sprintf('%s://%s%s%s', $scheme, $host, $port, $path); + } } diff --git a/src/Routing/I18nRouter.php b/src/Routing/I18nRouter.php index b7499c6..d20d2bb 100644 --- a/src/Routing/I18nRouter.php +++ b/src/Routing/I18nRouter.php @@ -107,6 +107,10 @@ public function generate($name, $parameters = [], $referenceType = self::ABSOLUT return $this->generateDocumentRoute($i18nContext, $referenceType); } + if ($i18nContext->getRouteItem()->getType() === RouteItemInterface::DATA_OBJECT_ROUTE) { + return $this->generateDataObjectRoute($i18nContext, $referenceType); + } + throw new RouteNotFoundException(sprintf('None of the chained routers were able to generate i18n route: %s', $name)); } @@ -141,6 +145,11 @@ protected function generateDocumentRoute(I18nContextInterface $i18nContext, int return $this->routeModifier->buildDocumentPath($i18nContext, $referenceType); } + protected function generateDataObjectRoute(I18nContextInterface $i18nContext, int $referenceType): string + { + return $this->routeModifier->buildDataObjectPath($i18nContext, $referenceType); + } + protected function generateContextAwarePath(I18nContextInterface $i18nContext, RouteItemInterface $routeItem, int $referenceType): string { $this->buildRouteContext($i18nContext, $referenceType); diff --git a/src/Transformer/AlternateRouteItemTransformer.php b/src/Transformer/AlternateRouteItemTransformer.php index cd0aafb..9dcef67 100644 --- a/src/Transformer/AlternateRouteItemTransformer.php +++ b/src/Transformer/AlternateRouteItemTransformer.php @@ -39,6 +39,13 @@ public function transform(RouteItemInterface $routeItem, array $context = []): A $alternateRouteItem->getRouteParametersBag()->set('_locale', $zoneSite->getLocale()); $alternateRouteItem->getRouteContextBag()->set('site', $zoneSite->getPimcoreSite()); + // Data object routes always have a route name, entity and url slug assigned + if ($routeItem->getType() === RouteItemInterface::DATA_OBJECT_ROUTE) { + $alternateRouteItem->setRouteName($routeItem->getRouteName()); + $alternateRouteItem->setEntity($routeItem->getEntity()); + $alternateRouteItem->setUrlSlug($routeItem->getUrlSlug()); + } + return $alternateRouteItem; } @@ -66,7 +73,8 @@ public function reverseTransformToArray(mixed $transformedRouteItem, array $cont 'routeName' => $transformedRouteItem->getRouteName(), 'routeParameters' => $transformedRouteItem->getRouteParameters(), 'routeAttributes' => $transformedRouteItem->getRouteAttributes(), - 'context' => $transformedRouteItem->getRouteContext() + 'context' => $transformedRouteItem->getRouteContext(), + 'urlSlug' => $transformedRouteItem->getUrlSlug(), ] ]; }