diff --git a/AlternateLocaleProviderInterface.php b/AlternateLocaleProviderInterface.php new file mode 100644 index 00000000..6df24073 --- /dev/null +++ b/AlternateLocaleProviderInterface.php @@ -0,0 +1,38 @@ + + */ +interface AlternateLocaleProviderInterface +{ + /** + * Creates a collection of AlternateLocales for one content object. + * + * @param object $content + * @return AlternateLocaleCollection + */ + public function createForContent($content); + + /** + * Creates a collection of AlternateLocales for many content object. + * + * @param array|object[] $contents + * @return AlternateLocaleCollection + */ + public function createForManyContent(array $contents); +} diff --git a/CHANGELOG.md b/CHANGELOG.md index b9d4e0a3..aa39c2b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changelog 1.1.0-RC1 --------- +* **2014-02-08**: Implement alternate locale support and its configuration * **2014-06-06**: Updated to PSR-4 autoloading 1.0.0 diff --git a/CmfSeoBundle.php b/CmfSeoBundle.php index f570cf85..e45e147d 100644 --- a/CmfSeoBundle.php +++ b/CmfSeoBundle.php @@ -15,7 +15,6 @@ use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Cmf\Bundle\SeoBundle\DependencyInjection\Compiler\RegisterExtractorsPass; diff --git a/DependencyInjection/CmfSeoExtension.php b/DependencyInjection/CmfSeoExtension.php index 5a3f9910..23d5210c 100644 --- a/DependencyInjection/CmfSeoExtension.php +++ b/DependencyInjection/CmfSeoExtension.php @@ -13,6 +13,8 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Cmf\Bundle\RoutingBundle\Routing\DynamicRouter; @@ -56,6 +58,8 @@ public function load(array $configs, ContainerBuilder $container) $config['persistence']['phpcr']['manager_name'] ); $sonataBundles[] = 'SonataDoctrinePHPCRAdminBundle'; + + $this->loadPhpcr($config['persistence']['phpcr'], $loader, $container); } if ($this->isConfigEnabled($container, $config['persistence']['orm'])) { @@ -70,6 +74,10 @@ public function load(array $configs, ContainerBuilder $container) if (count($sonataBundles) && $config['sonata_admin_extension']['enabled']) { $this->loadSonataAdmin($config['sonata_admin_extension'], $loader, $container, $sonataBundles); } + + if ($this->isConfigEnabled($container, $config['alternate_locale'])) { + $this->loadAlternateLocaleProvider($config['alternate_locale'], $container); + } } /** @@ -131,4 +139,51 @@ public function getXsdValidationBasePath() { return __DIR__.'/../Resources/config/schema'; } + + /** + * @param $config + * @param XmlFileLoader $loader + * @param ContainerBuilder $container + */ + private function loadPhpcr($config, XmlFileLoader $loader, ContainerBuilder $container) + { + $loader->load('phpcr.xml'); + + // chose the default alternate locale provider, when nothing else was set + if (!isset($config['alternate_locale.provider_id']) || null === $config['alternate_locale.provider_id']) { + $container->setParameter( + $this->getAlias().'.alternate_locale.provider_id', + 'cmf_seo.alternate_locale.provider' + ); + } + } + + /** + * If somebody (event loading of phpcr configuration) set the + * cmf_seo.alternate_locale.provider_id this one will be used + * to get the locale provider. + * + * @param $config + * @param ContainerBuilder $container + */ + private function loadAlternateLocaleProvider($config, ContainerBuilder $container) + { + if ($container->hasParameter($this->getAlias().'.alternate_locale.provider_id') + && null !== $container->getParameter($this->getAlias().'.alternate_locale.provider_id') + && $container->hasDefinition( + $container->getParameter($this->getAlias().'.alternate_locale.provider_id') + ) + ) { + $definition = $container->getDefinition('cmf_seo.event_listener.seo_content'); + $definition + ->addMethodCall( + 'setAlternateLocaleProvider', + array( + $container->getDefinition( + $container->getParameter($this->getAlias().'.alternate_locale.provider_id') + ) + ) + ); + } + } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 8d08975e..5aa8e9bb 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -72,6 +72,13 @@ public function getConfigTreeBuilder() ->scalarNode('form_group')->defaultValue('form.group_seo')->end() ->end() ->end() + ->arrayNode('alternate_locale') + ->addDefaultsIfNotSet() + ->canBeEnabled() + ->children() + ->scalarNode('provider_id')->defaultNull()->end() + ->end() + ->end() ->end() ; diff --git a/EventListener/ContentListener.php b/EventListener/ContentListener.php index ce7a2048..26a7dbd3 100644 --- a/EventListener/ContentListener.php +++ b/EventListener/ContentListener.php @@ -11,7 +11,10 @@ namespace Symfony\Cmf\Bundle\SeoBundle\EventListener; +use Symfony\Cmf\Bundle\CoreBundle\Translatable\TranslatableInterface; +use Symfony\Cmf\Bundle\SeoBundle\AlternateLocaleProviderInterface; use Symfony\Cmf\Bundle\SeoBundle\SeoPresentationInterface; +use Symfony\Cmf\Component\Routing\RouteReferrersInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -37,8 +40,13 @@ class ContentListener private $requestKey; /** - * @param SeoPresentationInterface $seoPage Service Handling SEO information. - * @param string $requestKey The key to look up the content in the request attributes. + * @var AlternateLocaleProviderInterface|null + */ + private $alternateLocaleProvider; + + /** + * @param SeoPresentationInterface $seoPage Service Handling SEO information. + * @param string $requestKey The key to look up the content in the request attributes. */ public function __construct(SeoPresentationInterface $seoPage, $requestKey) { @@ -52,13 +60,20 @@ public function __construct(SeoPresentationInterface $seoPage, $requestKey) public function onKernelRequest(GetResponseEvent $event) { if ($event->getRequest()->attributes->has($this->requestKey)) { - $this->seoPresentation->updateSeoPage($event->getRequest()->attributes->get($this->requestKey)); + $content = $event->getRequest()->attributes->get($this->requestKey); + $this->seoPresentation->updateSeoPage($content); // look if the strategy is redirectResponse and if there is a route to redirectResponse to $response = $this->seoPresentation->getRedirectResponse(); if (false !== $response && $this->canBeRedirected($event->getRequest(), $response)) { $event->setResponse($response); } + + if (null !== $this->alternateLocaleProvider) { + $this->seoPresentation->updateAlternateLocales( + $this->alternateLocaleProvider->createForContent($content) + ); + } } } @@ -73,4 +88,12 @@ protected function canBeRedirected(Request $request, RedirectResponse $response) return $targetPath !== $currentPath; } + + /** + * @param AlternateLocaleProviderInterface $alternateLocaleProvider + */ + public function setAlternateLocaleProvider($alternateLocaleProvider) + { + $this->alternateLocaleProvider = $alternateLocaleProvider; + } } diff --git a/Model/AlternateLocale.php b/Model/AlternateLocale.php new file mode 100644 index 00000000..a23e884f --- /dev/null +++ b/Model/AlternateLocale.php @@ -0,0 +1,39 @@ + + */ +class AlternateLocale +{ + const REL = 'alternate'; + + /** + * @var string The complete url for that locale. + */ + public $href; + + /** + * @var string The locale/language in the following formats: de, de-DE + */ + public $hrefLocale; + + public function __construct($href, $hrefLocale) + { + + $this->href = $href; + $this->hrefLocale = $hrefLocale; + } +} diff --git a/Model/AlternateLocaleCollection.php b/Model/AlternateLocaleCollection.php new file mode 100644 index 00000000..c243bb80 --- /dev/null +++ b/Model/AlternateLocaleCollection.php @@ -0,0 +1,24 @@ + + */ +class AlternateLocaleCollection extends ArrayCollection +{ + +} diff --git a/PhpcrAlternateLocaleProvider.php b/PhpcrAlternateLocaleProvider.php new file mode 100644 index 00000000..ca9fcd12 --- /dev/null +++ b/PhpcrAlternateLocaleProvider.php @@ -0,0 +1,107 @@ + + */ +class PhpcrAlternateLocaleProvider implements AlternateLocaleProviderInterface +{ + /** + * @var UrlGeneratorInterface + */ + private $urlGenerator; + + /** + * @var ManagerRegistry + */ + private $managerRegistry; + + /** + * @param ManagerRegistry $managerRegistry + * @param UrlGeneratorInterface $urlGenerator + */ + public function __construct(ManagerRegistry $managerRegistry, UrlGeneratorInterface $urlGenerator) + { + $this->managerRegistry = $managerRegistry; + $this->urlGenerator = $urlGenerator; + } + + /** + * Creates a collection of AlternateLocales for one content object. + * + * @param object $content + * @return AlternateLocaleCollection + */ + public function createForContent($content) + { + $documentManager = $this->getDocumentManagerForClass(get_class($content)); + $alternateLocaleCollection = new AlternateLocaleCollection(); + if (null === $documentManager + || !$content instanceof TranslatableInterface + || !$content instanceof RouteReferrersReadInterface + ) { + return $alternateLocaleCollection; + } + + $alternateLocales = $documentManager->getLocalesFor($content); + $currentLocale = $content->getLocale(); + foreach ($alternateLocales as $locale) { + if ($locale === $currentLocale) { + continue; + } + + $alternateLocaleCollection->add( + new AlternateLocale( + $this->urlGenerator->generate($content, array('_locale' => $locale)), + $locale + ) + ); + } + + return $alternateLocaleCollection; + } + + /** + * Creates a collection of AlternateLocales for many content object. + * + * @param array|object[] $contents + * @return AlternateLocaleCollection + */ + public function createForManyContent(array $contents) + { + // todo[max] implement for sitemap + } + + /** + * When the registry was set, this method figure out + * the document manager of a given class. + * + * @param $class + * @return DocumentManager|null|object + */ + private function getDocumentManagerForClass($class) + { + return $this->managerRegistry->getManagerForClass($class); + } +} diff --git a/Resources/config/phpcr.xml b/Resources/config/phpcr.xml new file mode 100644 index 00000000..68404ab5 --- /dev/null +++ b/Resources/config/phpcr.xml @@ -0,0 +1,18 @@ + + + + + + Symfony\Cmf\Bundle\SeoBundle\PhpcrAlternateLocaleProvider + + + + + + + + + + diff --git a/SeoPresentation.php b/SeoPresentation.php index 62ceb181..e942a917 100644 --- a/SeoPresentation.php +++ b/SeoPresentation.php @@ -12,9 +12,11 @@ namespace Symfony\Cmf\Bundle\SeoBundle; use Sonata\SeoBundle\Seo\SeoPage; +use Symfony\Cmf\Bundle\SeoBundle\Model\AlternateLocaleCollection; use Symfony\Cmf\Bundle\SeoBundle\Model\SeoMetadata; use Symfony\Cmf\Bundle\SeoBundle\Model\SeoMetadataInterface; use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Translation\TranslatorInterface; use Symfony\Cmf\Bundle\SeoBundle\Extractor\ExtractorInterface; use Symfony\Cmf\Bundle\SeoBundle\DependencyInjection\ConfigValues; @@ -112,6 +114,14 @@ private function setRedirectResponse(RedirectResponse $redirect) $this->redirectResponse = $redirect; } + /** + * @param UrlGeneratorInterface $urlGenerator + */ + public function setUrlGenerator(UrlGeneratorInterface $urlGenerator) + { + $this->urlGenerator = $urlGenerator; + } + /** * {@inheritDoc} */ @@ -309,4 +319,17 @@ private function copyMetadata(SeoMetadataInterface $contentSeoMetadata) return $metadata; } + + /** + * {inheritDoc} + */ + public function updateAlternateLocales(AlternateLocaleCollection $collection) + { + foreach ($collection->toArray() as $alternateLocale) { + $this->sonataPage->addLangAlternate( + $alternateLocale->href, + $alternateLocale->hrefLocale + ); + } + } } diff --git a/SeoPresentationInterface.php b/SeoPresentationInterface.php index 30b809df..269b4a1c 100644 --- a/SeoPresentationInterface.php +++ b/SeoPresentationInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Cmf\Bundle\SeoBundle; +use Symfony\Cmf\Bundle\SeoBundle\Model\AlternateLocaleCollection; +use Symfony\Cmf\Component\Routing\RouteReferrersInterface; use Symfony\Component\HttpFoundation\RedirectResponse; /** @@ -36,4 +38,11 @@ public function updateSeoPage($content); * @return boolean|RedirectResponse */ public function getRedirectResponse(); + + /** + * Updates alternate locale information on the Sonata SeoPage service. + * + * @param Model\AlternateLocaleCollection $collection + */ + public function updateAlternateLocales(AlternateLocaleCollection $collection); } diff --git a/Tests/Resources/DataFixtures/Phpcr/LoadContentData.php b/Tests/Resources/DataFixtures/Phpcr/LoadContentData.php index 15ceca9e..3c185fad 100644 --- a/Tests/Resources/DataFixtures/Phpcr/LoadContentData.php +++ b/Tests/Resources/DataFixtures/Phpcr/LoadContentData.php @@ -16,6 +16,7 @@ use PHPCR\Util\NodeHelper; use Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route; use Symfony\Cmf\Bundle\SeoBundle\Doctrine\Phpcr\SeoMetadata; +use Symfony\Cmf\Bundle\SeoBundle\Tests\Resources\Document\AlternateLocaleContent; use Symfony\Cmf\Bundle\SeoBundle\Tests\Resources\Document\SeoAwareContent; use Symfony\Cmf\Bundle\SeoBundle\Tests\Resources\Document\ContentWithExtractors; @@ -89,6 +90,42 @@ public function load(ObjectManager $manager) $manager->persist($route); + $alternateLocaleContent = new AlternateLocaleContent(); + $alternateLocaleContent->setName('alternate-locale-content'); + $alternateLocaleContent->setTitle('Alternate locale content'); + $alternateLocaleContent->setBody('Body of some alternate locate content'); + $alternateLocaleContent->setParentDocument($contentRoot); + $manager->persist($alternateLocaleContent); + $manager->bindTranslation($alternateLocaleContent, 'en'); + + // creating the locale base routes as generic for now + NodeHelper::createPath($manager->getPhpcrSession(), '/test/routes/de'); + NodeHelper::createPath($manager->getPhpcrSession(), '/test/routes/en'); + + $deRoute = $manager->find(null, '/test/routes/de'); + $enRoute = $manager->find(null, '/test/routes/en'); + + $alternateLocaleRoute = new Route(); + $alternateLocaleRoute->setPosition($enRoute, 'alternate-locale-content'); + $alternateLocaleRoute->setContent($alternateLocaleContent); + $alternateLocaleRoute->setDefault( + '_controller', + 'Symfony\Cmf\Bundle\SeoBundle\Tests\Resources\Controller\TestController::indexAction' + ); + $manager->persist($alternateLocaleRoute); + + $alternateLocaleContent->setTitle('Alternative Sprachen'); + $manager->bindTranslation($alternateLocaleContent, 'de'); + + $alternateLocaleRoute = new Route(); + $alternateLocaleRoute->setPosition($deRoute, 'alternate-locale-content'); + $alternateLocaleRoute->setContent($alternateLocaleContent); + $alternateLocaleRoute->setDefault( + '_controller', + 'Symfony\Cmf\Bundle\SeoBundle\Tests\Resources\Controller\TestController::indexAction' + ); + $manager->persist($alternateLocaleRoute); + $manager->flush(); } } diff --git a/Tests/Resources/Document/AlternateLocaleContent.php b/Tests/Resources/Document/AlternateLocaleContent.php new file mode 100644 index 00000000..632af4dd --- /dev/null +++ b/Tests/Resources/Document/AlternateLocaleContent.php @@ -0,0 +1,102 @@ +routes = new ArrayCollection(); + } + /** + * Add a route to the collection. + * + * @param Route $route + */ + public function addRoute($route) + { + $this->routes->add($route); + } + + /** + * Remove a route from the collection. + * + * @param Route $route + */ + public function removeRoute($route) + { + $this->routes->removeElement($route); + } + + /** + * Get the routes that point to this content. + * + * @return Route[] Route instances that point to this content + */ + public function getRoutes() + { + return $this->routes; + } + + /** + * @return string|boolean The locale of this model or false if + * translations are disabled in this project. + */ + public function getLocale() + { + return $this->locale; + } + + /** + * @param string|boolean $locale The local for this model, or false if + * translations are disabled in this project. + */ + public function setLocale($locale) + { + $this->locale = $locale; + } +} diff --git a/Tests/Resources/app/config/cmf_seo.phpcr.yml b/Tests/Resources/app/config/cmf_seo.phpcr.yml index d2ef39ee..d0a395ed 100644 --- a/Tests/Resources/app/config/cmf_seo.phpcr.yml +++ b/Tests/Resources/app/config/cmf_seo.phpcr.yml @@ -1,9 +1,11 @@ cmf_seo: persistence: { phpcr: true } + alternate_locale: ~ sonata_admin_extension: true cmf_routing: dynamic: + locales: [de, en] enabled: true persistence: phpcr: diff --git a/Tests/Unit/DependencyInjection/CmfSeoExtensionTest.php b/Tests/Unit/DependencyInjection/CmfSeoExtensionTest.php index 3d618221..3ddb1368 100644 --- a/Tests/Unit/DependencyInjection/CmfSeoExtensionTest.php +++ b/Tests/Unit/DependencyInjection/CmfSeoExtensionTest.php @@ -38,6 +38,7 @@ public function testPersistencePHPCR() array( 'CmfRoutingBundle' => true, 'SonataDoctrinePHPCRAdminBundle' => true, + 'DoctrinePHPCRBundle' => true, ) ); $this->load(array( @@ -75,6 +76,7 @@ public function testAdminExtension() array( 'CmfRoutingBundle' => true, 'SonataDoctrineORMBundle' => true, + 'DoctrinePHPCRBundle' => true, ) ); @@ -89,4 +91,32 @@ public function testAdminExtension() $this->assertContainerBuilderHasService('cmf_seo.admin_extension', 'Symfony\Cmf\Bundle\SeoBundle\Admin\Extension\SeoContentAdminExtension'); } + + public function testAlternateLocaleWithPhpcr() + { + $this->container->setParameter( + 'kernel.bundles', + array( + 'DoctrinePHPCRBundle' => true, + ) + ); + $this->load(array( + 'persistence' => array( + 'phpcr' => true, + ), + 'alternate_locale' => array( + 'enabled' => true + ), + )); + + + $this->assertContainerBuilderHasService( + 'cmf_seo.alternate_locale.provider', + 'Symfony\Cmf\Bundle\SeoBundle\PhpcrAlternateLocaleProvider' + ); + $this->assertContainerBuilderHasParameter( + 'cmf_seo.alternate_locale.provider_id', + 'cmf_seo.alternate_locale.provider' + ); + } } diff --git a/Tests/Unit/DependencyInjection/ConfigurationTest.php b/Tests/Unit/DependencyInjection/ConfigurationTest.php index e1e4e5c9..3e509afa 100644 --- a/Tests/Unit/DependencyInjection/ConfigurationTest.php +++ b/Tests/Unit/DependencyInjection/ConfigurationTest.php @@ -56,6 +56,10 @@ public function testDefaultsForAllConfigFormats() 'enabled' => 'auto', 'form_group' => 'form.group_seo', ), + 'alternate_locale' => array( + 'enabled' => false, + 'provider_id' => null, + ), ); $sources = array_map(function ($path) { diff --git a/Tests/WebTest/SeoFrontendTest.php b/Tests/WebTest/SeoFrontendTest.php index 8bdd0299..69820278 100644 --- a/Tests/WebTest/SeoFrontendTest.php +++ b/Tests/WebTest/SeoFrontendTest.php @@ -140,4 +140,27 @@ public function getExtraProperties() array('http-equiv', 'Content-Type', 'text/html; charset=utf-8'), ); } + + public function testAlternateLanguages() + { + $crawler = $this->client->request('GET', '/en/alternate-locale-content'); + $res = $this->client->getResponse(); + + $this->assertEquals(200, $res->getStatusCode()); + $this->assertCount(1, $crawler->filter('html:contains("Alternate locale content")')); + + $linkCrawler = $crawler->filter('head > link'); + $expectedArray = array(array('alternate', '/de/alternate-locale-content', 'de')); + $this->assertEquals($expectedArray, $linkCrawler->extract(array('rel', 'href', 'hreflang'))); + + $crawler = $this->client->request('GET', '/de/alternate-locale-content'); + $res = $this->client->getResponse(); + + $this->assertEquals(200, $res->getStatusCode()); + $this->assertCount(1, $crawler->filter('html:contains("Alternative Sprachen")')); + + $linkCrawler = $crawler->filter('head > link'); + $expectedArray = array(array('alternate', '/en/alternate-locale-content', 'en')); + $this->assertEquals($expectedArray, $linkCrawler->extract(array('rel', 'href', 'hreflang'))); + } }