├── Makefile ├── composer.json ├── phpunit.xml.dist ├── src ├── AlternateLocaleProviderInterface.php ├── Cache │ ├── CacheInterface.php │ ├── ExtractorCollection.php │ └── FileCache.php ├── CmfSeoBundle.php ├── Controller │ ├── SitemapController.php │ └── SuggestionProviderController.php ├── DependencyInjection │ ├── CmfSeoExtension.php │ ├── Compiler │ │ ├── RegisterExtractorsPass.php │ │ ├── RegisterSuggestionProviderPass.php │ │ └── RegisterUrlInformationProviderPass.php │ ├── ConfigValues.php │ └── Configuration.php ├── Doctrine │ └── Phpcr │ │ ├── AlternateLocaleProvider.php │ │ ├── BaseSuggestionProvider.php │ │ ├── ParentSuggestionProvider.php │ │ ├── SeoMetadata.php │ │ ├── SiblingSuggestionProvider.php │ │ └── SitemapDocumentProvider.php ├── EventListener │ ├── ContentListener.php │ └── LanguageListener.php ├── Exception │ ├── ExtractorStrategyException.php │ └── InvalidArgumentException.php ├── Extractor │ ├── DescriptionExtractor.php │ ├── DescriptionReadInterface.php │ ├── ExtractorInterface.php │ ├── ExtrasExtractor.php │ ├── ExtrasReadInterface.php │ ├── KeywordsExtractor.php │ ├── KeywordsReadInterface.php │ ├── OriginalRouteExtractor.php │ ├── OriginalRouteReadInterface.php │ ├── OriginalUrlExtractor.php │ ├── OriginalUrlReadInterface.php │ ├── TitleExtractor.php │ ├── TitleReadExtractor.php │ └── TitleReadInterface.php ├── Form │ └── Type │ │ └── SeoMetadataType.php ├── Matcher │ └── ExclusionMatcher.php ├── Model │ ├── AlternateLocale.php │ ├── AlternateLocaleCollection.php │ ├── SeoMetadata.php │ ├── SeoMetadataInterface.php │ └── UrlInformation.php ├── Resources │ ├── config │ │ ├── content-listener.xml │ │ ├── doctrine-model │ │ │ ├── SeoMetadata.orm.xml │ │ │ └── SeoMetadata.phpcr.xml │ │ ├── doctrine-phpcr │ │ │ └── SeoMetadata.phpcr.xml │ │ ├── extractors.xml │ │ ├── matcher_phpcr.xml │ │ ├── phpcr-alternate-locale.xml │ │ ├── phpcr-sitemap.xml │ │ ├── routing │ │ │ └── sitemap.xml │ │ ├── schema │ │ │ └── seo-1.0.xsd │ │ ├── services.xml │ │ └── sitemap.xml │ ├── meta │ │ └── LICENSE │ └── views │ │ ├── Exception │ │ └── error.html.twig │ │ └── Sitemap │ │ ├── index.html.twig │ │ └── index.xml.twig ├── SeoAwareInterface.php ├── SeoAwareTrait.php ├── SeoPresentation.php ├── SeoPresentationInterface.php ├── Sitemap │ ├── AbstractChain.php │ ├── AlternateLocalesGuesser.php │ ├── DefaultChangeFrequencyGuesser.php │ ├── DepthGuesser.php │ ├── GuesserChain.php │ ├── GuesserInterface.php │ ├── LastModifiedGuesser.php │ ├── LoaderChain.php │ ├── LoaderInterface.php │ ├── LocationGuesser.php │ ├── PublishWorkflowVoter.php │ ├── SeoMetadataTitleGuesser.php │ ├── SitemapAwareDocumentVoter.php │ ├── UrlInformationProvider.php │ ├── VoterChain.php │ └── VoterInterface.php ├── SitemapAwareInterface.php ├── SuggestionProviderInterface.php └── Twig │ └── Extension │ └── CmfSeoExtension.php ├── tests ├── Fixtures │ ├── App │ │ ├── Controller │ │ │ └── TestController.php │ │ ├── DataFixtures │ │ │ └── Phpcr │ │ │ │ ├── LoadContentData.php │ │ │ │ └── LoadSitemapData.php │ │ ├── Document │ │ │ ├── AllStrategiesDocument.php │ │ │ ├── AlternateLocaleContent.php │ │ │ ├── ContentBase.php │ │ │ ├── ContentWithExtractors.php │ │ │ ├── SeoAwareContent.php │ │ │ ├── SitemapAwareContent.php │ │ │ ├── SitemapAwareWithLastModifiedDateContent.php │ │ │ └── SitemapAwareWithPublishWorkflowContent.php │ │ ├── Entity │ │ │ └── SeoAwareOrmContent.php │ │ ├── Kernel.php │ │ ├── Model │ │ │ └── SeoAwareOrmContent.php │ │ ├── Resources │ │ │ └── views │ │ │ │ ├── base.html.twig │ │ │ │ └── tests │ │ │ │ ├── exception.html.twig │ │ │ │ └── index.html.twig │ │ ├── config │ │ │ ├── bundles.php │ │ │ ├── cmf_seo.orm.yml │ │ │ ├── cmf_seo.phpcr.yml │ │ │ ├── cmf_seo.yml │ │ │ ├── config.php │ │ │ ├── config_orm.php │ │ │ ├── config_phpcr.php │ │ │ ├── routing.php │ │ │ ├── tree_browser_1.x.yml │ │ │ └── tree_browser_2.x.yml │ │ └── translations │ │ │ ├── mesages.de.xliff │ │ │ └── mesages.en.xliff │ └── fixtures │ │ └── config │ │ ├── config.php │ │ ├── config.xml │ │ ├── config.yml │ │ ├── config1.xml │ │ ├── config2.xml │ │ ├── config3.xml │ │ ├── config4.xml │ │ ├── config_error.xml │ │ └── config_sitemap.xml ├── Functional │ └── Doctrine │ │ ├── Orm │ │ └── SeoMetadataTest.php │ │ └── Phpcr │ │ ├── SeoMetadataTest.php │ │ └── SitemapDocumentProviderTest.php ├── Unit │ ├── Cache │ │ └── FileCacheTest.php │ ├── Controller │ │ └── SitemapControllerTest.php │ ├── DependencyInjection │ │ ├── CmfSeoExtensionTest.php │ │ ├── Compiler │ │ │ ├── RegisterExtractorsPassTest.php │ │ │ ├── RegisterSuggestionProviderPassTest.php │ │ │ └── RegisterUrlInformationProviderPassTest.php │ │ ├── ConfigValuesTest.php │ │ ├── ConfigurationTest.php │ │ └── XmlSchemaTest.php │ ├── EventListener │ │ ├── ContentListenerTest.php │ │ └── LanguageListenerTest.php │ ├── Extractor │ │ ├── BaseTestCase.php │ │ ├── DescriptionExtractorTest.php │ │ ├── ExtrasExtractorTest.php │ │ ├── KeywordsExtractorTest.php │ │ ├── OriginalRouteExtractorTest.php │ │ ├── OriginalUrlExtractorTest.php │ │ ├── TitleExtractorTest.php │ │ └── TitleReadExtractorTest.php │ ├── Form │ │ └── Type │ │ │ └── SeoMetadataTypeTest.php │ ├── Matcher │ │ └── ExclusionMatcherTest.php │ ├── Model │ │ └── UrlInformationTest.php │ ├── SeoPresentationTest.php │ └── Sitemap │ │ ├── AbstractChainTest.php │ │ ├── AlternateLocalesGuesserTest.php │ │ ├── DefaultChangeFrequencyGuesserTest.php │ │ ├── DepthGuesserTest.php │ │ ├── GuesserTestCase.php │ │ ├── LastModifiedGuesserTest.php │ │ ├── LocationGuesserTest.php │ │ ├── SeoMetadataTitleGuesserTest.php │ │ ├── SitemapAwareDocumentVoterTest.php │ │ └── UrlInformationProviderTest.php ├── WebTest │ ├── SeoFrontendTest.php │ └── SitemapTest.php └── bootstrap.php └── travis.php.ini /Makefile: -------------------------------------------------------------------------------- 1 | ####################################################### 2 | # DO NOT EDIT THIS FILE! # 3 | # # 4 | # It's auto-generated by symfony-cmf/dev-kit package. # 5 | ####################################################### 6 | 7 | ############################################################################ 8 | # This file is part of the Symfony CMF package. # 9 | # # 10 | # (c) 2011-2017 Symfony CMF # 11 | # # 12 | # For the full copyright and license information, please view the LICENSE # 13 | # file that was distributed with this source code. # 14 | ############################################################################ 15 | 16 | TESTING_SCRIPTS_DIR=vendor/symfony-cmf/testing/bin 17 | CONSOLE=${TESTING_SCRIPTS_DIR}/console 18 | VERSION=dev-master 19 | ifdef BRANCH 20 | VERSION=dev-${BRANCH} 21 | endif 22 | PACKAGE=symfony-cmf/seo-bundle 23 | export KERNEL_CLASS=Symfony\Cmf\Bundle\SeoBundle\Tests\Fixtures\App\Kernel 24 | list: 25 | @echo 'test: will run all tests' 26 | @echo 'unit_tests: will run unit tests only' 27 | @echo 'functional_tests_phpcr: will run functional tests with PHPCR' 28 | @echo 'functional_tests_orm: will run functional tests with ORM' 29 | 30 | include ${TESTING_SCRIPTS_DIR}/make/unit_tests.mk 31 | include ${TESTING_SCRIPTS_DIR}/make/functional_tests_phpcr.mk 32 | include ${TESTING_SCRIPTS_DIR}/make/functional_tests_orm.mk 33 | 34 | .PHONY: test 35 | test: unit_tests functional_tests_phpcr functional_tests_orm 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony-cmf/seo-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Symfony CMF Search Engine Optimization Bundle", 5 | "keywords": [ 6 | "Symfony CMF", 7 | "SEO" 8 | ], 9 | "homepage": "http://cmf.symfony.com", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Symfony CMF Community", 14 | "homepage": "https://github.com/symfony-cmf/SeoBundle/contributors" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.2", 19 | "symfony/framework-bundle": "^3.4 || ^4.0", 20 | "sonata-project/seo-bundle": "^2.2", 21 | "doctrine/collections": "^1.0" 22 | }, 23 | "require-dev": { 24 | "doctrine/phpcr-odm": "^1.4.2 || ^2.0", 25 | "doctrine/orm": "^2.4", 26 | "symfony-cmf/core-bundle": "^2.1", 27 | "symfony-cmf/routing-bundle": "^1.4.0 || ^2.1", 28 | "matthiasnoback/symfony-dependency-injection-test": "^1.1", 29 | "matthiasnoback/symfony-config-test": "^2.1", 30 | "symfony/phpunit-bridge": "^3.4 || ^4.0", 31 | "burgov/key-value-form-bundle": "^1.0", 32 | "symfony-cmf/testing": "^2.1.9", 33 | "symfony/security-bundle": "^3.4 || ^4.0", 34 | "symfony/translation": "^3.4 || ^4.0", 35 | "symfony/validator": "^3.4 || ^4.0", 36 | "symfony/asset": "^3.4 || ^4.0", 37 | "symfony/templating": "^3.4 || ^4.0", 38 | "symfony/browser-kit": "^3.4 || ^4.0", 39 | "symfony/css-selector": "^3.4 || ^4.0" 40 | }, 41 | "suggest": { 42 | "symfony-cmf/core-bundle": "When using the provided PHPCR-ODM document", 43 | "symfony-cmf/routing-bundle": "To make use of the alternate locale provider.", 44 | "doctrine/phpcr-bundle": "To persist the metadata in PHPCR ODM documents", 45 | "doctrine/doctrine-bundle": "To persist the metadata in ORM entities", 46 | "doctrine/orm": "To persist the metadata in ORM entities, ~2.4", 47 | "burgov/key-value-form-bundle": "To have advanced editing options in the SEO form type, ^1.0", 48 | "symfony-cmf/sonata-admin-integration-bundle": "To provide an admin interface for the PHPCR ODM documents." 49 | }, 50 | "autoload": { 51 | "psr-4": { 52 | "Symfony\\Cmf\\Bundle\\SeoBundle\\": "src/" 53 | } 54 | }, 55 | "autoload-dev": { 56 | "psr-4": { 57 | "Symfony\\Cmf\\Bundle\\SeoBundle\\Tests\\": "tests/" 58 | } 59 | }, 60 | "extra": { 61 | "branch-alias": { 62 | "dev-master": "2.2-dev" 63 | } 64 | }, 65 | "conflict": { 66 | "phpcr/phpcr-utils": "<1.3.0", 67 | "sebastian/exporter": "<2.0.0", 68 | "burgov/key-value-form-bundle": "<1.3.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | 13 | ./tests/WebTest 14 | ./tests/Functional/Doctrine/Phpcr 15 | 16 | 17 | 18 | ./tests/Functional/Doctrine/Orm 19 | 20 | 21 | 22 | 23 | 24 | . 25 | 26 | Resources/ 27 | tests 28 | vendor/ 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/AlternateLocaleProviderInterface.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | interface AlternateLocaleProviderInterface 22 | { 23 | /** 24 | * Creates a collection of AlternateLocales for one content object. 25 | * 26 | * @param object $content 27 | * 28 | * @return AlternateLocaleCollection 29 | */ 30 | public function createForContent($content); 31 | 32 | /** 33 | * Creates a collection of AlternateLocales for many content object. 34 | * 35 | * @param array|object[] $contents 36 | * 37 | * @return AlternateLocaleCollection[] 38 | */ 39 | public function createForContents(array $contents); 40 | } 41 | -------------------------------------------------------------------------------- /src/Cache/CacheInterface.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | interface CacheInterface 21 | { 22 | /** 23 | * Fetches extractors from the cache. 24 | * 25 | * @param string $class 26 | * 27 | * @return ExtractorCollection|null 28 | */ 29 | public function loadExtractorsFromCache($class); 30 | 31 | /** 32 | * Saves extractors into the cache. 33 | * 34 | * @param string $class 35 | * @param array $extractors 36 | */ 37 | public function putExtractorsInCache($class, array $extractors); 38 | } 39 | -------------------------------------------------------------------------------- /src/Cache/ExtractorCollection.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class ExtractorCollection implements \IteratorAggregate, \Serializable 20 | { 21 | /** 22 | * @var array 23 | */ 24 | private $extractors; 25 | 26 | /** 27 | * @var null|string 28 | */ 29 | private $resource; 30 | 31 | /** 32 | * @var int 33 | */ 34 | private $createdAt; 35 | 36 | /** 37 | * @param array $extractors 38 | * @param null|string $resource The path to the file of the content object, this is 39 | * used to determine if the cache needs to be updated 40 | */ 41 | public function __construct(array $extractors, $resource = null) 42 | { 43 | $this->extractors = $extractors; 44 | $this->resource = $resource; 45 | $this->createdAt = time(); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function getIterator() 52 | { 53 | return new \ArrayIterator($this->extractors); 54 | } 55 | 56 | /** 57 | * Checks if the cache needs to be updated or not. 58 | * 59 | * @param null|int $timestamp 60 | * 61 | * @return bool whether cache needs to be updated 62 | */ 63 | public function isFresh($timestamp = null) 64 | { 65 | if (null === $timestamp) { 66 | $timestamp = $this->createdAt; 67 | } 68 | 69 | if (!file_exists($this->resource)) { 70 | return false; 71 | } 72 | 73 | if ($timestamp < filemtime($this->resource)) { 74 | return false; 75 | } 76 | 77 | return true; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function serialize() 84 | { 85 | return serialize([ 86 | $this->extractors, 87 | $this->resource, 88 | $this->createdAt, 89 | ]); 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function unserialize($data) 96 | { 97 | list( 98 | $this->extractors, 99 | $this->resource, 100 | $this->createdAt 101 | ) = unserialize($data); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/CmfSeoBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new RegisterExtractorsPass()); 27 | $container->addCompilerPass(new RegisterSuggestionProviderPass()); 28 | $container->addCompilerPass(new RegisterUrlInformationProviderPass()); 29 | 30 | $this->buildPhpcrCompilerPass($container); 31 | $this->buildOrmCompilerPass($container); 32 | } 33 | 34 | /** 35 | * Creates and registers compiler passes for PHPCR-ODM mapping if both the 36 | * phpcr-odm and the phpcr-bundle are present. 37 | * 38 | * @param ContainerBuilder $container 39 | */ 40 | private function buildPhpcrCompilerPass(ContainerBuilder $container) 41 | { 42 | if (!class_exists('Doctrine\Bundle\PHPCRBundle\DependencyInjection\Compiler\DoctrinePhpcrMappingsPass') 43 | || !class_exists('Doctrine\ODM\PHPCR\Version') 44 | ) { 45 | return; 46 | } 47 | 48 | $container->addCompilerPass( 49 | DoctrinePhpcrMappingsPass::createXmlMappingDriver( 50 | [ 51 | $this->getPath().'/Resources/config/doctrine-model' => 'Symfony\Cmf\Bundle\SeoBundle\Model', 52 | $this->getPath().'/Resources/config/doctrine-phpcr' => 'Symfony\Cmf\Bundle\SeoBundle\Doctrine\Phpcr', 53 | ], 54 | ['cmf_seo.dynamic.persistence.phpcr.manager_name'], 55 | 'cmf_seo.backend_type_phpcr', 56 | ['CmfSeoBundle' => 'Symfony\Cmf\Bundle\SeoBundle\Doctrine\Phpcr'] 57 | ) 58 | ); 59 | } 60 | 61 | /** 62 | * Creates and registers compiler passes for ORM mappings if both doctrine 63 | * ORM and a suitable compiler pass implementation are available. 64 | * 65 | * @param ContainerBuilder $container 66 | */ 67 | private function buildOrmCompilerPass(ContainerBuilder $container) 68 | { 69 | if (!class_exists('Doctrine\ORM\Version') 70 | || !class_exists('Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\RegisterMappingsPass') 71 | || !class_exists('Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass') 72 | ) { 73 | return; 74 | } 75 | 76 | $container->addCompilerPass( 77 | DoctrineOrmMappingsPass::createXmlMappingDriver( 78 | [ 79 | $this->getPath().'/Resources/config/doctrine-model' => 'Symfony\Cmf\Bundle\SeoBundle\Model', 80 | ], 81 | ['cmf_seo.dynamic.persistence.orm.manager_name'], 82 | 'cmf_seo.backend_type_orm', 83 | ['CmfSeoBundle' => 'Symfony\Cmf\Bundle\SeoBundle\Model'] 84 | ) 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Controller/SitemapController.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class SitemapController 27 | { 28 | /** 29 | * @var EngineInterface 30 | */ 31 | private $templating; 32 | 33 | /** 34 | * The complete configurations for all sitemap with its 35 | * definitions for their templates. 36 | * 37 | * @var array 38 | */ 39 | private $configurations; 40 | 41 | /** 42 | * @var UrlInformationProvider 43 | */ 44 | private $sitemapProvider; 45 | 46 | /** 47 | * You should provide templates for html and xml. 48 | * 49 | * Json is serialized by default, but can be customized with a template 50 | * 51 | * @param UrlInformationProvider $sitemapProvider 52 | * @param EngineInterface $templating 53 | * @param array $configurations list of available sitemap configurations 54 | */ 55 | public function __construct( 56 | UrlInformationProvider $sitemapProvider, 57 | EngineInterface $templating, 58 | array $configurations 59 | ) { 60 | $this->templating = $templating; 61 | $this->configurations = $configurations; 62 | $this->sitemapProvider = $sitemapProvider; 63 | } 64 | 65 | /** 66 | * @param string $_format the format of the sitemap 67 | * @param string $sitemap the sitemap to show 68 | * 69 | * @return Response 70 | */ 71 | public function indexAction($_format, $sitemap) 72 | { 73 | if (!isset($this->configurations[$sitemap])) { 74 | throw new NotFoundHttpException(sprintf('Unknown sitemap "%s"', $sitemap)); 75 | } 76 | 77 | $templates = $this->configurations[$sitemap]['templates']; 78 | 79 | $supportedFormats = array_merge(['json'], array_keys($templates)); 80 | if (!in_array($_format, $supportedFormats)) { 81 | $text = sprintf( 82 | 'Unknown format %s, use one of %s.', 83 | $_format, 84 | implode(', ', $supportedFormats) 85 | ); 86 | 87 | return new Response($text, 406); 88 | } 89 | 90 | $urlInformation = $this->sitemapProvider->getUrlInformation($sitemap); 91 | 92 | if (isset($templates[$_format])) { 93 | return new Response($this->templating->render($templates[$_format], ['urls' => $urlInformation])); 94 | } 95 | 96 | return $this->createJsonResponse($urlInformation); 97 | } 98 | 99 | /** 100 | * @param array|UrlInformation[] $urls 101 | * 102 | * @return JsonResponse 103 | */ 104 | private function createJsonResponse($urls) 105 | { 106 | $result = []; 107 | 108 | foreach ($urls as $url) { 109 | $result[] = $url->toArray(); 110 | } 111 | 112 | return new JsonResponse($result); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/RegisterExtractorsPass.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class RegisterExtractorsPass implements CompilerPassInterface 26 | { 27 | /** 28 | * {@inheritdoc} 29 | * 30 | * @throws LogicException if a tagged service is not public 31 | */ 32 | public function process(ContainerBuilder $container) 33 | { 34 | if (!$container->hasDefinition('cmf_seo.presentation')) { 35 | return; 36 | } 37 | 38 | $strategyDefinition = $container->getDefinition('cmf_seo.presentation'); 39 | $taggedServices = $container->findTaggedServiceIds('cmf_seo.extractor'); 40 | 41 | foreach ($taggedServices as $id => $attributes) { 42 | $definition = $container->getDefinition($id); 43 | if (!$definition->isPublic()) { 44 | throw new LogicException(sprintf('Strategy "%s" must be public.', $id)); 45 | } 46 | 47 | $priority = 0; 48 | foreach ($attributes as $attribute) { 49 | if (isset($attribute['priority'])) { 50 | $priority = $attribute['priority']; 51 | 52 | break; 53 | } 54 | } 55 | 56 | $strategyDefinition->addMethodCall('addExtractor', [ 57 | new Reference($id), 58 | $priority, 59 | ]); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/RegisterSuggestionProviderPass.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class RegisterSuggestionProviderPass implements CompilerPassInterface 23 | { 24 | /** 25 | * {@inheritdoc} 26 | * 27 | * @throws LogicException if a tagged service is not public 28 | */ 29 | public function process(ContainerBuilder $container) 30 | { 31 | if (!$container->hasDefinition('cmf_seo.error.suggestion_provider.controller')) { 32 | return; 33 | } 34 | 35 | $presentationDefinition = $container->getDefinition('cmf_seo.error.suggestion_provider.controller'); 36 | $taggedServices = $container->findTaggedServiceIds('cmf_seo.suggestion_provider'); 37 | $provider = []; 38 | foreach ($taggedServices as $id => $attributes) { 39 | $definition = $container->getDefinition($id); 40 | if (!$definition->isPublic()) { 41 | throw new LogicException(sprintf('Matcher "%s" must be public.', $id)); 42 | } 43 | 44 | $group = null; 45 | foreach ($attributes as $attribute) { 46 | if (isset($attribute['group'])) { 47 | $group = $attribute['group']; 48 | 49 | break; 50 | } 51 | } 52 | $group = $group ?: 'default'; 53 | $provider[] = ['provider' => new Reference($id), 'group' => $group]; 54 | } 55 | 56 | $presentationDefinition->replaceArgument(4, $provider); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/DependencyInjection/ConfigValues.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class ConfigValues 24 | { 25 | /** 26 | * Specific translation domain to use for title and description. 27 | * 28 | * @var string 29 | */ 30 | private $translationDomain; 31 | 32 | /** 33 | * The title translation key. The extracted title will be passed to the 34 | * translator as %content_title%. 35 | * 36 | * @var string 37 | */ 38 | private $title; 39 | 40 | /** 41 | * The description translation key. The extracted description will be 42 | * passed to the translator as %content_description%. 43 | * 44 | * @var string 45 | */ 46 | private $description; 47 | 48 | /** 49 | * The original URL behaviour decides on how to handle content with several URLs. 50 | * 51 | * This value needs to be one of the ORIGINAL_URL_* constants in SeoPresentation. 52 | * 53 | * @var string 54 | */ 55 | private $originalUrlBehaviour; 56 | 57 | /** 58 | * @param string $description 59 | */ 60 | public function setDescription($description) 61 | { 62 | $this->description = $description; 63 | } 64 | 65 | /** 66 | * @return string 67 | */ 68 | public function getDescription() 69 | { 70 | return $this->description; 71 | } 72 | 73 | /** 74 | * @param string $behaviour one of the constants from SeoPresentation 75 | * 76 | * @throws ExtractorStrategyException if $behaviour is not supported 77 | */ 78 | public function setOriginalUrlBehaviour($behaviour) 79 | { 80 | if (!in_array($behaviour, SeoPresentation::$originalUrlBehaviours)) { 81 | throw new ExtractorStrategyException( 82 | sprintf('Behaviour "%s" not supported by SeoPresentation.', $behaviour) 83 | ); 84 | } 85 | $this->originalUrlBehaviour = $behaviour; 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function getOriginalUrlBehaviour() 92 | { 93 | return $this->originalUrlBehaviour; 94 | } 95 | 96 | /** 97 | * @param string $title 98 | */ 99 | public function setTitle($title) 100 | { 101 | $this->title = $title; 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | public function getTitle() 108 | { 109 | return $this->title; 110 | } 111 | 112 | /** 113 | * @param string $translationDomain 114 | */ 115 | public function setTranslationDomain($translationDomain) 116 | { 117 | $this->translationDomain = $translationDomain; 118 | } 119 | 120 | /** 121 | * @return string 122 | */ 123 | public function getTranslationDomain() 124 | { 125 | return $this->translationDomain; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Doctrine/Phpcr/BaseSuggestionProvider.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | abstract class BaseSuggestionProvider implements SuggestionProviderInterface 26 | { 27 | /** 28 | * @var ManagerRegistry 29 | */ 30 | protected $managerRegistry; 31 | 32 | /** 33 | * By concatenating the routeBasePaths and the url 34 | * we will get the absolute path a route document 35 | * should be persisted with. 36 | * 37 | * @var array 38 | */ 39 | protected $routeBasePaths; 40 | 41 | /** 42 | * @param ManagerRegistry $managerRegistry 43 | * @param array $routeBasePath 44 | */ 45 | public function __construct(ManagerRegistry $managerRegistry, $routeBasePaths) 46 | { 47 | $this->managerRegistry = $managerRegistry; 48 | $this->routeBasePaths = (array) $routeBasePaths; 49 | } 50 | 51 | /** 52 | * @param string $class 53 | * 54 | * @return null|DocumentManager 55 | */ 56 | public function getManagerForClass($class) 57 | { 58 | return $this->managerRegistry->getManagerForClass($class); 59 | } 60 | 61 | /** 62 | * Finds the parent route document by concatenating the basepaths with the 63 | * requested path. 64 | * 65 | * @return null|object 66 | */ 67 | protected function findParentRoute($requestedPath) 68 | { 69 | $manager = $this->getManagerForClass('Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route'); 70 | $parentPaths = []; 71 | 72 | foreach ($this->routeBasePaths as $basepath) { 73 | $parentPaths[] = PathHelper::getParentPath($basepath.$requestedPath); 74 | } 75 | 76 | $parentRoutes = $manager->findMany(null, $parentPaths); 77 | if (0 === count($parentRoutes)) { 78 | return; 79 | } 80 | 81 | return $parentRoutes->first(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Doctrine/Phpcr/ParentSuggestionProvider.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class ParentSuggestionProvider extends BaseSuggestionProvider 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function create(Request $request) 28 | { 29 | $routes = []; 30 | $parentRoute = $this->findParentRoute($request->getPathInfo()); 31 | 32 | if (null === $parentRoute) { 33 | return []; 34 | } 35 | 36 | if ($parentRoute instanceof Route) { 37 | $routes[$parentRoute->getName()] = $parentRoute; 38 | } 39 | 40 | return $routes; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Doctrine/Phpcr/SeoMetadata.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class SeoMetadata extends SeoMetadataModel implements HierarchyInterface, TranslatableInterface 22 | { 23 | /** 24 | * @var string 25 | */ 26 | protected $name; 27 | 28 | /** 29 | * @var object 30 | */ 31 | protected $parentDocument; 32 | 33 | /** 34 | * @param string $name 35 | * 36 | * @return SeoMetadata 37 | */ 38 | public function setName($name) 39 | { 40 | $this->name = $name; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | public function getName() 49 | { 50 | return $this->name; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function setParentDocument($parentDocument) 57 | { 58 | $this->parentDocument = $parentDocument; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function getParentDocument() 67 | { 68 | return $this->parentDocument; 69 | } 70 | 71 | /** 72 | * @deprecated For compatibility with Doctrine PHPCR ODM 1.4, to be removed in version 3.0. 73 | * @see setParentDocument 74 | */ 75 | public function setParent($parent) 76 | { 77 | @trigger_error('The '.__METHOD__.'() method is deprecated and will be removed in version 3.0. Use setParentDocument() instead.', E_USER_DEPRECATED); 78 | 79 | return $this->setParentDocument($parent); 80 | } 81 | 82 | /** 83 | * @deprecated For compatibility with Doctrine PHPCR ODM 1.4, to be removed in version 3.0. 84 | * @see getParentDocument 85 | */ 86 | public function getParent() 87 | { 88 | @trigger_error('The '.__METHOD__.'() method is deprecated and will be removed in version 3.0. Use getParentDocument() instead.', E_USER_DEPRECATED); 89 | 90 | return $this->getParentDocument(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Doctrine/Phpcr/SiblingSuggestionProvider.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class SiblingSuggestionProvider extends BaseSuggestionProvider 24 | { 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function create(Request $request) 29 | { 30 | $parentRoute = $this->findParentRoute($request->getPathInfo()); 31 | 32 | if (null === $parentRoute) { 33 | return []; 34 | } 35 | 36 | $routes = []; 37 | $manager = $this->getManagerForClass('Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route'); 38 | $childRoutes = $manager->getChildren($parentRoute); 39 | 40 | foreach ($childRoutes->toArray() as $childRoute) { 41 | if ($childRoute instanceof Route) { 42 | $routes[$childRoute->getName()] = $childRoute; 43 | } 44 | } 45 | 46 | return $routes; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Doctrine/Phpcr/SitemapDocumentProvider.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class SitemapDocumentProvider implements LoaderInterface 24 | { 25 | /** 26 | * @var DocumentManager 27 | */ 28 | private $manager; 29 | 30 | /** 31 | * @param DocumentManager $manager 32 | */ 33 | public function __construct(DocumentManager $manager) 34 | { 35 | $this->manager = $manager; 36 | } 37 | 38 | /** 39 | * {@inheritdoc}. 40 | */ 41 | public function load($sitemap) 42 | { 43 | // todo rewrite query when https://github.com/symfony-cmf/CoreBundle/issues/126 is ready 44 | $documentsCollection = $this->manager->createQuery( 45 | 'SELECT * FROM [nt:unstructured] WHERE (visible_for_sitemap = true) ORDER BY [jcr:path]', 46 | QueryInterface::JCR_SQL2 47 | )->execute(); 48 | 49 | $documents = []; 50 | // the chain provider does not like collections as we array_merge in there 51 | foreach ($documentsCollection as $document) { 52 | $documents[] = $document; 53 | } 54 | 55 | return $documents; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/EventListener/ContentListener.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class ContentListener 29 | { 30 | /** 31 | * @var SeoPresentationInterface 32 | */ 33 | private $seoPresentation; 34 | 35 | /** 36 | * @var string The key to look up the content in the request attributes 37 | */ 38 | private $requestKey; 39 | 40 | /** 41 | * @var AlternateLocaleProviderInterface|null 42 | */ 43 | private $alternateLocaleProvider; 44 | 45 | /** 46 | * @param SeoPresentationInterface $seoPage service Handling SEO information 47 | * @param string $requestKey the key to look up the content in the request attributes 48 | */ 49 | public function __construct(SeoPresentationInterface $seoPage, $requestKey) 50 | { 51 | $this->seoPresentation = $seoPage; 52 | $this->requestKey = $requestKey; 53 | } 54 | 55 | /** 56 | * @param GetResponseEvent $event 57 | */ 58 | public function onKernelRequest(GetResponseEvent $event) 59 | { 60 | if ($event->getRequest()->attributes->has($this->requestKey)) { 61 | $content = $event->getRequest()->attributes->get($this->requestKey); 62 | $this->seoPresentation->updateSeoPage($content); 63 | 64 | // look if the strategy is redirectResponse and if there is a route to redirectResponse to 65 | $response = $this->seoPresentation->getRedirectResponse(); 66 | if (false !== $response && $this->canBeRedirected($event->getRequest(), $response)) { 67 | $event->setResponse($response); 68 | } 69 | 70 | if (null !== $this->alternateLocaleProvider) { 71 | $this->seoPresentation->updateAlternateLocales( 72 | $this->alternateLocaleProvider->createForContent($content) 73 | ); 74 | } 75 | } 76 | } 77 | 78 | protected function canBeRedirected(Request $request, RedirectResponse $response) 79 | { 80 | $targetRequest = Request::create($response->getTargetUrl()); 81 | $stripUrl = function ($path) { 82 | return preg_replace('/#.+$/', '', $path); 83 | }; 84 | $targetPath = $stripUrl($targetRequest->getBaseUrl().$targetRequest->getPathInfo()); 85 | $currentPath = $stripUrl($request->getBaseUrl().$request->getPathInfo()); 86 | 87 | return $targetPath !== $currentPath; 88 | } 89 | 90 | /** 91 | * @param AlternateLocaleProviderInterface $alternateLocaleProvider 92 | */ 93 | public function setAlternateLocaleProvider($alternateLocaleProvider) 94 | { 95 | $this->alternateLocaleProvider = $alternateLocaleProvider; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/EventListener/LanguageListener.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class LanguageListener implements EventSubscriberInterface 24 | { 25 | public static function getSubscribedEvents() 26 | { 27 | return [ 28 | KernelEvents::RESPONSE => 'onKernelResponse', 29 | ]; 30 | } 31 | 32 | public function onKernelResponse(FilterResponseEvent $event) 33 | { 34 | if ($event->getResponse()->headers->has('Content-Language')) { 35 | return; 36 | } 37 | 38 | $locale = $event->getRequest()->getLocale(); 39 | $language = current(explode('_', $locale, 2)); 40 | $event->getResponse()->headers->set('Content-Language', $language); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Exception/ExtractorStrategyException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class ExtractorStrategyException extends \RuntimeException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class InvalidArgumentException extends \RuntimeException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/Extractor/DescriptionExtractor.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class DescriptionExtractor implements ExtractorInterface 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function supports($content) 28 | { 29 | return $content instanceof DescriptionReadInterface; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | * 35 | * @param DescriptionReadInterface $content 36 | */ 37 | public function updateMetadata($content, SeoMetadataInterface $seoMetadata) 38 | { 39 | $seoMetadata->setMetaDescription($content->getSeoDescription()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Extractor/DescriptionReadInterface.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | interface DescriptionReadInterface 25 | { 26 | /** 27 | * Provide a description of this page to be used in SEO context. 28 | * 29 | * @return string 30 | */ 31 | public function getSeoDescription(); 32 | } 33 | -------------------------------------------------------------------------------- /src/Extractor/ExtractorInterface.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | interface ExtractorInterface 22 | { 23 | /** 24 | * Check whether the strategy supports this content. 25 | * 26 | * The decision could be based on the content implementing 27 | * an interface or being instance of a specific class, 28 | * or introspection to see if a certain method exists. 29 | * 30 | * @param object $content 31 | * 32 | * @return bool 33 | */ 34 | public function supports($content); 35 | 36 | /** 37 | * Update the metadata with information from this content. 38 | * 39 | * It is up to the strategy to check if certain fields 40 | * are already set by previous strategies and decide on a merge strategy. 41 | * 42 | * This method should only be called if supports returned true. 43 | * 44 | * @param object $content 45 | * @param SeoMetadataInterface $seoMetadata 46 | */ 47 | public function updateMetadata($content, SeoMetadataInterface $seoMetadata); 48 | } 49 | -------------------------------------------------------------------------------- /src/Extractor/ExtrasExtractor.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class ExtrasExtractor implements ExtractorInterface 24 | { 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function supports($content) 29 | { 30 | return $content instanceof ExtrasReadInterface; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | * 36 | * @param ExtrasReadInterface $content 37 | */ 38 | public function updateMetadata($content, SeoMetadataInterface $seoMetadata) 39 | { 40 | $allowedTypesMethodMapping = [ 41 | 'property' => 'addExtraProperty', 42 | 'name' => 'addExtraName', 43 | 'http-equiv' => 'addExtraHttp', 44 | ]; 45 | 46 | $contentExtras = $content->getSeoExtras(); 47 | 48 | foreach ($contentExtras as $type => $extras) { 49 | if (!array_key_exists($type, $allowedTypesMethodMapping)) { 50 | throw new InvalidArgumentException( 51 | printf( 52 | 'Extras type %s not in the list of allowed ones %s.', 53 | $type, 54 | implode(', ', $allowedTypesMethodMapping) 55 | ) 56 | ); 57 | } 58 | 59 | foreach ($extras as $key => $value) { 60 | $seoMetadata->{$allowedTypesMethodMapping[$type]}($key, $value); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Extractor/ExtrasReadInterface.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | interface ExtrasReadInterface 26 | { 27 | /** 28 | * Provides a list of extras as key-values pairs 29 | * for this page's SEO context and meta type property. 30 | * 31 | * Different types should be grouped in different arrays. 32 | * 33 | * Example return: 34 | * array( 35 | * 'property' => array('og:title' => 'Extra Title'), 36 | * 'name' => array('robots' => 'index, follow'), 37 | * 'http-equiv' => array('Content-Type' => 'text/html; charset=utf-8'), 38 | * ) 39 | * 40 | * @return array 41 | */ 42 | public function getSeoExtras(); 43 | } 44 | -------------------------------------------------------------------------------- /src/Extractor/KeywordsExtractor.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class KeywordsExtractor implements ExtractorInterface 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function supports($object) 28 | { 29 | return $object instanceof KeywordsReadInterface; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | * 35 | * @param KeywordsReadInterface $content 36 | */ 37 | public function updateMetadata($content, SeoMetadataInterface $seoMetadata) 38 | { 39 | $keywords = $content->getSeoKeywords(); 40 | if (is_array($keywords)) { 41 | $keywords = implode(', ', $keywords); 42 | } 43 | 44 | $seoMetadata->setMetaKeywords($keywords); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Extractor/KeywordsReadInterface.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | interface KeywordsReadInterface 25 | { 26 | /** 27 | * Provides a list of keywords for this page to be 28 | * used in SEO context. 29 | * 30 | * @return string|array 31 | */ 32 | public function getSeoKeywords(); 33 | } 34 | -------------------------------------------------------------------------------- /src/Extractor/OriginalRouteExtractor.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class OriginalRouteExtractor implements ExtractorInterface 26 | { 27 | /** 28 | * @var UrlGeneratorInterface 29 | */ 30 | private $urlGenerator; 31 | 32 | public function __construct(UrlGeneratorInterface $urlGenerator) 33 | { 34 | $this->urlGenerator = $urlGenerator; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function supports($content) 41 | { 42 | return $content instanceof OriginalRouteReadInterface; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | * 48 | * @param OriginalRouteReadInterface $content 49 | */ 50 | public function updateMetadata($content, SeoMetadataInterface $seoMetadata) 51 | { 52 | $route = $content->getSeoOriginalRoute(); 53 | 54 | try { 55 | $seoMetadata->setOriginalUrl($this->urlGenerator->generate($route)); 56 | } catch (RouteNotFoundException $e) { 57 | throw new ExtractorStrategyException('Unable to create a url.', 0, $e); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Extractor/OriginalRouteReadInterface.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | interface OriginalRouteReadInterface 27 | { 28 | /** 29 | * Returns something that can be used to generate an absolute URL. 30 | * 31 | * This may be a Symfony route name or - when using the Symfony CMF 32 | * DynamicRouter - a Route object. 33 | * 34 | * @return Route|string 35 | */ 36 | public function getSeoOriginalRoute(); 37 | } 38 | -------------------------------------------------------------------------------- /src/Extractor/OriginalUrlExtractor.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class OriginalUrlExtractor implements ExtractorInterface 24 | { 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function supports($content) 29 | { 30 | return $content instanceof OriginalUrlReadInterface; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function updateMetadata($content, SeoMetadataInterface $seoMetadata) 37 | { 38 | $seoMetadata->setOriginalUrl($content->getSeoOriginalUrl()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Extractor/OriginalUrlReadInterface.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | interface OriginalUrlReadInterface 24 | { 25 | /** 26 | * The method returns the absolute URL as a string to redirect to or set to 27 | * the canonical link. 28 | * 29 | * @return string an absolute URL 30 | */ 31 | public function getSeoOriginalUrl(); 32 | } 33 | -------------------------------------------------------------------------------- /src/Extractor/TitleExtractor.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class TitleExtractor implements ExtractorInterface 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function supports($content) 28 | { 29 | return $content instanceof TitleReadInterface; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | * 35 | * @param TitleReadInterface $content 36 | */ 37 | public function updateMetadata($content, SeoMetadataInterface $seoMetadata) 38 | { 39 | $seoMetadata->setTitle($content->getSeoTitle()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Extractor/TitleReadExtractor.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class TitleReadExtractor implements ExtractorInterface 22 | { 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function supports($content) 27 | { 28 | return method_exists($content, 'getTitle'); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function updateMetadata($content, SeoMetadataInterface $seoMetadata) 35 | { 36 | if (!$seoMetadata->getTitle()) { 37 | $seoMetadata->setTitle($content->getTitle()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Extractor/TitleReadInterface.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | interface TitleReadInterface 24 | { 25 | /** 26 | * Provides a title of this page to be used in SEO context. 27 | * 28 | * @return string 29 | */ 30 | public function getSeoTitle(); 31 | } 32 | -------------------------------------------------------------------------------- /src/Matcher/ExclusionMatcher.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class ExclusionMatcher implements RequestMatcherInterface 23 | { 24 | /** 25 | * @var RequestMatcherInterface[] a list of rule matchers 26 | */ 27 | private $matchersMap = []; 28 | 29 | /** 30 | * @param RequestMatcherInterface $ruleMatcher 31 | */ 32 | public function addRequestMatcher(RequestMatcherInterface $ruleMatcher) 33 | { 34 | $this->matchersMap[] = $ruleMatcher; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function matches(Request $request) 41 | { 42 | foreach ($this->matchersMap as $matcher) { 43 | if ($matcher->matches($request)) { 44 | return true; 45 | } 46 | } 47 | 48 | return false; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Model/AlternateLocale.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class AlternateLocale 20 | { 21 | const REL = 'alternate'; 22 | 23 | /** 24 | * @var string the absolute url for that locale 25 | */ 26 | public $href; 27 | 28 | /** 29 | * @var string The locale/language in the following formats: de, de-DE 30 | */ 31 | public $hrefLocale; 32 | 33 | public function __construct($href, $hrefLocale) 34 | { 35 | $this->href = $href; 36 | $this->hrefLocale = $hrefLocale; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Model/AlternateLocaleCollection.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class AlternateLocaleCollection extends ArrayCollection 22 | { 23 | } 24 | -------------------------------------------------------------------------------- /src/Model/SeoMetadataInterface.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | interface SeoMetadataInterface 20 | { 21 | /** 22 | * Updates the description. 23 | * 24 | * @param string $metaDescription 25 | */ 26 | public function setMetaDescription($metaDescription); 27 | 28 | /** 29 | * Gets the description for the meta tag. 30 | * 31 | * @return string 32 | */ 33 | public function getMetaDescription(); 34 | 35 | /** 36 | * Sets the keywords. 37 | * 38 | * @param string $metaKeywords 39 | */ 40 | public function setMetaKeywords($metaKeywords); 41 | 42 | /** 43 | * Gets the Keywords for the meta tag. 44 | * 45 | * @return string 46 | */ 47 | public function getMetaKeywords(); 48 | 49 | /** 50 | * Sets the original URL for content that has several URLs. 51 | * 52 | * @param string $originalUrl 53 | */ 54 | public function setOriginalUrl($originalUrl); 55 | 56 | /** 57 | * Gets the original URL of this content. 58 | * 59 | * This will be used for the canonical link or to redirect to the original 60 | * URL, depending on your settings. 61 | * 62 | * @return string 63 | */ 64 | public function getOriginalUrl(); 65 | 66 | /** 67 | * Sets the title. 68 | * 69 | * @param string $title 70 | */ 71 | public function setTitle($title); 72 | 73 | /** 74 | * Gets the title. 75 | * 76 | * @return string 77 | */ 78 | public function getTitle(); 79 | 80 | /** 81 | * @param array|\Traversable|KeyValueContainer 82 | */ 83 | public function setExtraProperties($extraProperties); 84 | 85 | /** 86 | * @param array|\Traversable|KeyValueContainer 87 | */ 88 | public function setExtraNames($extraNames); 89 | 90 | /** 91 | * @param array|\Traversable|KeyValueContainer 92 | */ 93 | public function setExtraHttp($extraHttp); 94 | 95 | /** 96 | * @return array 97 | */ 98 | public function getExtraProperties(); 99 | 100 | /** 101 | * @return array 102 | */ 103 | public function getExtraNames(); 104 | 105 | /** 106 | * @return array 107 | */ 108 | public function getExtraHttp(); 109 | 110 | /** 111 | * Add a key-value pair for meta attribute property. 112 | * 113 | * @param string $key 114 | * @param string $value 115 | */ 116 | public function addExtraProperty($key, $value); 117 | 118 | /** 119 | * Add a key-value pair for meta attribute name. 120 | * 121 | * @param string $key 122 | * @param string $value 123 | */ 124 | public function addExtraName($key, $value); 125 | 126 | /** 127 | * Add a key-value pair for meta attribute http-equiv. 128 | * 129 | * @param string $key 130 | * @param string $value 131 | */ 132 | public function addExtraHttp($key, $value); 133 | } 134 | -------------------------------------------------------------------------------- /src/Resources/config/content-listener.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | %cmf_seo.content_key% 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-model/SeoMetadata.orm.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-model/SeoMetadata.phpcr.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-phpcr/SeoMetadata.phpcr.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Resources/config/extractors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Resources/config/matcher_phpcr.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | %cmf_routing.dynamic.persistence.phpcr.route_basepaths% 11 | 12 | 13 | 14 | 15 | 16 | %cmf_routing.dynamic.persistence.phpcr.route_basepaths% 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Resources/config/phpcr-alternate-locale.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Resources/config/phpcr-sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | %cmf_seo.persistence.phpcr.content_basepath% 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Resources/config/routing/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | cmf_seo.sitemap.controller:indexAction 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | %cmf_seo.title% 11 | %cmf_seo.description% 12 | %cmf_seo.translation_domain% 13 | %cmf_seo.original_route_pattern% 14 | 15 | 16 | 17 | %cmf_seo.form.data_class.seo_metadata% 18 | %cmf_seo.form.options% 19 | 20 | 21 | 22 | 23 | %kernel.cache_dir% 24 | cmf_seo 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | %kernel.debug% 42 | 43 | %cmf_seo.error.templates% 44 | 45 | 46 | 47 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/Resources/config/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | %cmf_seo.sitemap.configurations% 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | %cmf_seo.sitemap.default_change_frequency% 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/Resources/meta/LICENSE: -------------------------------------------------------------------------------- 1 | Symfony CMF Seo Bundle 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2011-2017 Symfony CMF 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/Resources/views/Exception/error.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | An Error Occurred: {{ status_text }} 6 | 7 | 8 |

Oops! An Error Occurred

9 |

The server returned a "{{ status_code }} {{ status_text }}".

10 | 11 |

12 | Something is broken. Please let us know what you were doing when this error occurred. 13 | We will fix it as soon as possible. Sorry for any inconvenience caused. 14 |

15 | 16 | {% if best_matches not is empty %} 17 |

Suggested pages

18 | 19 | {% for group, matches in best_matches %} 20 |

{{ group|capitalize }}

21 | 26 | {% endfor %} 27 | {% endif %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Resources/views/Sitemap/index.html.twig: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/Resources/views/Sitemap/index.xml.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for url in urls %} 4 | 5 | {{ url.location }} 6 | {% if url.lastModification %} 7 | {{ url.lastModification }} 8 | {% endif %} 9 | {{ url.changeFrequency }} 10 | {% if url.alternateLocales is defined and url.alternateLocales|length > 0 %} 11 | {% for locale in url.alternateLocales %} 12 | 13 | {% endfor %} 14 | {% endif %} 15 | 16 | {% endfor %} 17 | 18 | -------------------------------------------------------------------------------- /src/SeoAwareInterface.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | interface SeoAwareInterface 23 | { 24 | /** 25 | * Gets the SEO metadata for this content. 26 | * 27 | * @return SeoMetadataInterface 28 | */ 29 | public function getSeoMetadata(); 30 | 31 | /** 32 | * Sets the SEO metadata for this content. 33 | * 34 | * This method is used by a listener, which converts the metadata to a 35 | * plain array in order to persist it and converts it back when the content 36 | * is fetched. 37 | * 38 | * @param array|SeoMetadataInterface $metadata 39 | */ 40 | public function setSeoMetadata($metadata); 41 | } 42 | -------------------------------------------------------------------------------- /src/SeoAwareTrait.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | trait SeoAwareTrait 23 | { 24 | /** 25 | * @PHPCRODM\Child 26 | */ 27 | private $seoMetadata; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function getSeoMetadata() 33 | { 34 | return $this->seoMetadata; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function setSeoMetadata($metadata) 41 | { 42 | $this->seoMetadata = $metadata; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/SeoPresentationInterface.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | interface SeoPresentationInterface 24 | { 25 | /** 26 | * Updates the Sonata SeoPage service with the data retrieved from the $content. 27 | * 28 | * @param object $content the content to load data from 29 | */ 30 | public function updateSeoPage($content); 31 | 32 | /** 33 | * Returns the redirect response if the bundle is configured to redirect to 34 | * the canonical URL and this content provided a canonical URL different 35 | * from the current URL. Returns false in all other cases. 36 | * 37 | * @return bool|RedirectResponse 38 | */ 39 | public function getRedirectResponse(); 40 | 41 | /** 42 | * Updates alternate locale information on the Sonata SeoPage service. 43 | * 44 | * @param AlternateLocaleCollection $collection 45 | */ 46 | public function updateAlternateLocales(AlternateLocaleCollection $collection); 47 | } 48 | -------------------------------------------------------------------------------- /src/Sitemap/AbstractChain.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | abstract class AbstractChain 20 | { 21 | /** 22 | * The list of entries by sitemap name and priority. 23 | * 24 | * @var array 25 | */ 26 | private $items = []; 27 | 28 | /** 29 | * The list of default entries by priority. 30 | * 31 | * @var array 32 | */ 33 | private $defaultItems = []; 34 | 35 | /** 36 | * Add an entry to the chain. 37 | * 38 | * An entry can be added to a specific sitemap or to all of them. The 39 | * higher the priority, the earlier the entry is in the chain. Sitemap 40 | * specific entries come before general entries of the same priority. 41 | * 42 | * @param object $item the entry 43 | * @param int $priority the higher the value, the earlier the item comes in the chain 44 | * @param string $sitemap Name of the sitemap to add the entry for. Null adds the entry to all sitemaps. 45 | */ 46 | public function addItem($item, $priority = 0, $sitemap = null) 47 | { 48 | if ($sitemap) { 49 | if (!isset($this->items[$sitemap])) { 50 | $this->items[$sitemap] = []; 51 | } 52 | $entries = &$this->items[$sitemap]; 53 | } else { 54 | $entries = &$this->defaultItems; 55 | } 56 | 57 | if (!isset($entries[$priority])) { 58 | $entries[$priority] = []; 59 | } 60 | 61 | $entries[$priority][] = $item; 62 | } 63 | 64 | /** 65 | * Get the sorted list of chain entries for the specified sitemap. 66 | * 67 | * @param string $sitemap Name of the sitemap 68 | * 69 | * @return object[] priority sorted list of chain entries 70 | */ 71 | protected function getSortedEntriesForSitemap($sitemap) 72 | { 73 | $priorities = array_keys($this->defaultItems); 74 | if (isset($this->items[$sitemap])) { 75 | $priorities = array_unique(array_merge($priorities, array_keys($this->items[$sitemap]))); 76 | } 77 | 78 | rsort($priorities); 79 | 80 | $sortedItems = []; 81 | foreach ($priorities as $priority) { 82 | if (isset($this->items[$sitemap][$priority])) { 83 | // never happens if the sitemap has no specific entries at all 84 | $sortedItems = array_merge($sortedItems, $this->items[$sitemap][$priority]); 85 | } 86 | if (isset($this->defaultItems[$priority])) { 87 | $sortedItems = array_merge($sortedItems, $this->defaultItems[$priority]); 88 | } 89 | } 90 | 91 | return $sortedItems; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Sitemap/AlternateLocalesGuesser.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class AlternateLocalesGuesser implements GuesserInterface 23 | { 24 | /** 25 | * @var AlternateLocaleProviderInterface 26 | */ 27 | private $alternateLocaleProvider; 28 | 29 | public function __construct(AlternateLocaleProviderInterface $alternateLocaleProvider) 30 | { 31 | $this->alternateLocaleProvider = $alternateLocaleProvider; 32 | } 33 | 34 | /** 35 | * {@inheritdoc}. 36 | */ 37 | public function guessValues(UrlInformation $urlInformation, $object, $sitemap) 38 | { 39 | if ($urlInformation->getAlternateLocales()) { 40 | return; 41 | } 42 | 43 | $collection = $this->alternateLocaleProvider->createForContent($object); 44 | $urlInformation->setAlternateLocales($collection->toArray()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Sitemap/DefaultChangeFrequencyGuesser.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class DefaultChangeFrequencyGuesser implements GuesserInterface 22 | { 23 | /** 24 | * @var string 25 | */ 26 | private $defaultChangeFrequency; 27 | 28 | /** 29 | * @param string $defaultChangeFrequency 30 | * 31 | * @see UrlInformation::setChangeFrequency for information on this parameter. 32 | */ 33 | public function __construct($defaultChangeFrequency) 34 | { 35 | $this->defaultChangeFrequency = $defaultChangeFrequency; 36 | } 37 | 38 | /** 39 | * {@inheritdoc}. 40 | */ 41 | public function guessValues(UrlInformation $urlInformation, $object, $sitemap) 42 | { 43 | if ($urlInformation->getChangeFrequency()) { 44 | return; 45 | } 46 | 47 | $urlInformation->setChangeFrequency($this->defaultChangeFrequency); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Sitemap/DepthGuesser.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class DepthGuesser implements GuesserInterface 25 | { 26 | /** 27 | * @var ManagerRegistry 28 | */ 29 | private $managerRegistry; 30 | 31 | /** 32 | * @var int the depth of the content base path as the depth offset 33 | */ 34 | private $offset; 35 | 36 | /** 37 | * DepthGuesser constructor. 38 | * 39 | * @param ManagerRegistry $managerRegistry 40 | * @param $contentBasePath 41 | */ 42 | public function __construct(ManagerRegistry $managerRegistry, $contentBasePath) 43 | { 44 | $this->managerRegistry = $managerRegistry; 45 | $this->offset = ('/' === $contentBasePath) ? 1 : substr_count($contentBasePath, '/') + 1; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function guessValues(UrlInformation $urlInformation, $object, $sitemap) 52 | { 53 | if (null !== $urlInformation->getDepth()) { 54 | return; 55 | } 56 | 57 | $manager = $this->managerRegistry->getManagerForClass(ClassUtils::getRealClass(get_class($object))); 58 | if (!$manager instanceof DocumentManager) { 59 | return; 60 | } 61 | 62 | $node = $manager->getNodeForDocument($object); 63 | if (null === $node) { 64 | return; 65 | } 66 | $urlInformation->setDepth($node->getDepth() - $this->offset); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Sitemap/GuesserChain.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class GuesserChain extends AbstractChain 22 | { 23 | /** 24 | * {@inheritdoc}. 25 | */ 26 | public function guessValues(UrlInformation $urlInformation, $object, $sitemap) 27 | { 28 | /** @var $guesser GuesserInterface */ 29 | foreach ($this->getSortedEntriesForSitemap($sitemap) as $guesser) { 30 | $guesser->guessValues($urlInformation, $object, $sitemap); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Sitemap/GuesserInterface.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | interface GuesserInterface 25 | { 26 | /** 27 | * Updates UrlInformation with new values if they are not already set. 28 | * 29 | * @param UrlInformation $urlInformation the value object to update 30 | * @param object $object the sitemap element to get values from 31 | * @param string $sitemap name of the sitemap being built 32 | */ 33 | public function guessValues(UrlInformation $urlInformation, $object, $sitemap); 34 | } 35 | -------------------------------------------------------------------------------- /src/Sitemap/LastModifiedGuesser.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class LastModifiedGuesser implements GuesserInterface 27 | { 28 | /** 29 | * @var ManagerRegistry 30 | */ 31 | private $managerRegistry; 32 | 33 | /** 34 | * LastModifiedGuesser constructor. 35 | * 36 | * @param ManagerRegistry $manager 37 | */ 38 | public function __construct(ManagerRegistry $manager) 39 | { 40 | $this->managerRegistry = $manager; 41 | } 42 | 43 | /** 44 | * Updates UrlInformation with new values if they are not already set. 45 | * 46 | * @param UrlInformation $urlInformation the value object to update 47 | * @param object $object the sitemap element to get values from 48 | * @param string $sitemap name of the sitemap being built 49 | */ 50 | public function guessValues(UrlInformation $urlInformation, $object, $sitemap) 51 | { 52 | if (null !== $urlInformation->getLastModification()) { 53 | return; 54 | } 55 | 56 | $className = ClassUtils::getRealClass(get_class($object)); 57 | $manager = $this->managerRegistry->getManagerForClass($className); 58 | if (!$manager instanceof DocumentManager) { 59 | return; 60 | } 61 | 62 | /** @var ClassMetadata $metadata */ 63 | $metadata = $manager->getClassMetadata($className); 64 | $mixins = $metadata->getMixins(); 65 | 66 | if (!in_array('mix:lastModified', $mixins)) { 67 | return; 68 | } 69 | 70 | $fieldName = $this->getFieldName($metadata); 71 | if (null === $fieldName) { 72 | return; 73 | } 74 | 75 | $urlInformation->setLastModification($metadata->getFieldValue($object, $fieldName)); 76 | } 77 | 78 | private function getFieldName(ClassMetadata $metadata) 79 | { 80 | foreach ($metadata->getFieldNames() as $fieldName) { 81 | $field = $metadata->getFieldMapping($fieldName); 82 | if ('jcr:lastModified' === $field['property']) { 83 | return $fieldName; 84 | } 85 | } 86 | 87 | return; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Sitemap/LoaderChain.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class LoaderChain extends AbstractChain 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function load($sitemap) 25 | { 26 | $documents = []; 27 | 28 | /** @var $loader LoaderInterface */ 29 | foreach ($this->getSortedEntriesForSitemap($sitemap) as $loader) { 30 | $documents = array_merge($documents, $loader->load($sitemap)); 31 | } 32 | 33 | return $documents; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Sitemap/LoaderInterface.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | interface LoaderInterface 22 | { 23 | /** 24 | * @param string $sitemap the name of the sitemap 25 | * 26 | * @return Route[] 27 | */ 28 | public function load($sitemap); 29 | } 30 | -------------------------------------------------------------------------------- /src/Sitemap/LocationGuesser.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class LocationGuesser implements GuesserInterface 25 | { 26 | /** 27 | * @var UrlGeneratorInterface 28 | */ 29 | private $urlGenerator; 30 | 31 | public function __construct(UrlGeneratorInterface $router) 32 | { 33 | $this->urlGenerator = $router; 34 | } 35 | 36 | /** 37 | * {@inheritdoc}. 38 | */ 39 | public function guessValues(UrlInformation $urlInformation, $object, $sitemap) 40 | { 41 | if ($urlInformation->getLocation()) { 42 | return; 43 | } 44 | 45 | $urlInformation->setLocation($this->urlGenerator->generate($object, [], UrlGeneratorInterface::ABSOLUTE_URL)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Sitemap/PublishWorkflowVoter.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class PublishWorkflowVoter implements VoterInterface 24 | { 25 | /** 26 | * @var AuthorizationCheckerInterface 27 | */ 28 | private $publishWorkflowChecker; 29 | 30 | public function __construct(AuthorizationCheckerInterface $publishWorkflowChecker) 31 | { 32 | $this->publishWorkflowChecker = $publishWorkflowChecker; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function exposeOnSitemap($content, $sitemap) 39 | { 40 | return $this->publishWorkflowChecker->isGranted(PublishWorkflowChecker::VIEW_ATTRIBUTE, $content); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Sitemap/SeoMetadataTitleGuesser.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class SeoMetadataTitleGuesser implements GuesserInterface 23 | { 24 | /** 25 | * @var SeoPresentation 26 | */ 27 | private $seoPresentation; 28 | 29 | public function __construct( 30 | SeoPresentation $seoPresentation 31 | ) { 32 | $this->seoPresentation = $seoPresentation; 33 | } 34 | 35 | /** 36 | * {@inheritdoc}. 37 | */ 38 | public function guessValues(UrlInformation $urlInformation, $object, $sitemap) 39 | { 40 | if ($urlInformation->getLabel()) { 41 | return; 42 | } 43 | 44 | $seoMetadata = $this->seoPresentation->getSeoMetadata($object); 45 | if ($seoMetadata->getTitle()) { 46 | $urlInformation->setLabel($seoMetadata->getTitle()); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Sitemap/SitemapAwareDocumentVoter.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class SitemapAwareDocumentVoter implements VoterInterface 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function exposeOnSitemap($content, $sitemap) 28 | { 29 | if (!$content instanceof SitemapAwareInterface) { 30 | return true; 31 | } 32 | 33 | return $content->isVisibleInSitemap($sitemap); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Sitemap/UrlInformationProvider.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class UrlInformationProvider 25 | { 26 | /** 27 | * @var LoaderChain 28 | */ 29 | private $loader; 30 | 31 | /** 32 | * @var GuesserChain 33 | */ 34 | private $guesser; 35 | 36 | /** 37 | * @var VoterChain 38 | */ 39 | private $voter; 40 | 41 | public function __construct( 42 | LoaderChain $loader, 43 | VoterChain $voter, 44 | GuesserChain $guesser 45 | ) { 46 | $this->loader = $loader; 47 | $this->voter = $voter; 48 | $this->guesser = $guesser; 49 | } 50 | 51 | /** 52 | * Get the UrlInformation for the specified sitemap. 53 | * 54 | * @param string $sitemap Name of the sitemap to create, "default" if not specified 55 | * 56 | * @return UrlInformation[] 57 | */ 58 | public function getUrlInformation($sitemap = 'default') 59 | { 60 | $urlInformationList = []; 61 | 62 | $contentObjects = $this->loader->load($sitemap); 63 | foreach ($contentObjects as $content) { 64 | if (!$this->voter->exposeOnSitemap($content, $sitemap)) { 65 | continue; 66 | } 67 | $urlInformation = new UrlInformation(); 68 | $this->guesser->guessValues($urlInformation, $content, $sitemap); 69 | $urlInformationList[] = $urlInformation; 70 | } 71 | 72 | return $urlInformationList; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Sitemap/VoterChain.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class VoterChain extends AbstractChain 20 | { 21 | /** 22 | * Decide whether the voters want to expose the content. 23 | * 24 | * @param object $content The content to expose 25 | * @param string $sitemap Name of the sitemap 26 | * 27 | * @return bool whether to expose this content 28 | */ 29 | public function exposeOnSitemap($content, $sitemap) 30 | { 31 | /** @var $voter VoterInterface */ 32 | foreach ($this->getSortedEntriesForSitemap($sitemap) as $voter) { 33 | if (!$voter->exposeOnSitemap($content, $sitemap)) { 34 | return false; 35 | } 36 | } 37 | 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Sitemap/VoterInterface.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | interface VoterInterface 20 | { 21 | /** 22 | * Decide whether this content is visible on the specified sitemap. 23 | * 24 | * @param object $content the content object 25 | * @param string $sitemap name of the sitemap 26 | * 27 | * @return bool true if the content should be visible on the sitemap, false otherwise 28 | */ 29 | public function exposeOnSitemap($content, $sitemap); 30 | } 31 | -------------------------------------------------------------------------------- /src/SitemapAwareInterface.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | interface SitemapAwareInterface 21 | { 22 | /** 23 | * Decision whether a document should be visible 24 | * in sitemap or not. 25 | * 26 | * @param $sitemap 27 | * 28 | * @return bool 29 | */ 30 | public function isVisibleInSitemap($sitemap); 31 | } 32 | -------------------------------------------------------------------------------- /src/SuggestionProviderInterface.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | interface SuggestionProviderInterface 26 | { 27 | /** 28 | * Based on the current request this method creates a list 29 | * of suggestions as an array of routes. 30 | * 31 | * @param Request $request 32 | * 33 | * @return array|Route[] 34 | */ 35 | public function create(Request $request); 36 | } 37 | -------------------------------------------------------------------------------- /src/Twig/Extension/CmfSeoExtension.php: -------------------------------------------------------------------------------- 1 | seoPresentation = $seoPresentation; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getFunctions() 35 | { 36 | return [ 37 | new \Twig_SimpleFunction('cmf_seo_update_metadata', [$this, 'updateMetadata']), 38 | ]; 39 | } 40 | 41 | /** 42 | * @see SeoPresentationInterface 43 | */ 44 | public function updateMetadata($content) 45 | { 46 | $this->seoPresentation->updateSeoPage($content); 47 | } 48 | 49 | public function getName() 50 | { 51 | return 'cmf_seo'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Controller/TestController.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'title' => $contentDocument->getTitle(), 23 | 'body' => $contentDocument->getBody(), 24 | ], 25 | ]; 26 | 27 | return $this->render('::tests/index.html.twig', $params); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Document/AllStrategiesDocument.php: -------------------------------------------------------------------------------- 1 | seoMetadata = new SeoMetadata(); 28 | } 29 | 30 | /** 31 | * Provide a description of this page to be used in SEO context. 32 | * 33 | * @return string 34 | */ 35 | public function getSeoDescription() 36 | { 37 | return 'Test Description.'; 38 | } 39 | 40 | /** 41 | * Provide the original url of this page to be used in SEO context. 42 | * 43 | * @return Route the original route 44 | */ 45 | public function getSeoOriginalUrl() 46 | { 47 | return '/test-route'; 48 | } 49 | 50 | /** 51 | * Provide a title of this page to be used in SEO context. 52 | * 53 | * @return string 54 | */ 55 | public function getSeoTitle() 56 | { 57 | return 'Test title'; 58 | } 59 | 60 | /** 61 | * To let a content be seo aware means in the SeoBundle to serve the SeoMetadata. 62 | * This SeoMetadata contains the information to fill some meta tags or has 63 | * the information of the original url of the content. 64 | * 65 | * @return SeoMetadata 66 | */ 67 | public function getSeoMetadata() 68 | { 69 | return $this->seoMetadata; 70 | } 71 | 72 | public function setSeoMetadata($metadata) 73 | { 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Document/AlternateLocaleContent.php: -------------------------------------------------------------------------------- 1 | routes = new ArrayCollection(); 52 | } 53 | 54 | /** 55 | * Add a route to the collection. 56 | * 57 | * @param Route $route 58 | */ 59 | public function addRoute($route) 60 | { 61 | $this->routes->add($route); 62 | } 63 | 64 | /** 65 | * Remove a route from the collection. 66 | * 67 | * @param Route $route 68 | */ 69 | public function removeRoute($route) 70 | { 71 | $this->routes->removeElement($route); 72 | } 73 | 74 | /** 75 | * Get the routes that point to this content. 76 | * 77 | * @return Route[] Route instances that point to this content 78 | */ 79 | public function getRoutes() 80 | { 81 | return $this->routes; 82 | } 83 | 84 | /** 85 | * @return string|bool the locale of this model or false if 86 | * translations are disabled in this project 87 | */ 88 | public function getLocale() 89 | { 90 | return $this->locale; 91 | } 92 | 93 | /** 94 | * @param string|bool $locale the local for this model, or false if 95 | * translations are disabled in this project 96 | */ 97 | public function setLocale($locale) 98 | { 99 | $this->locale = $locale; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Document/ContentBase.php: -------------------------------------------------------------------------------- 1 | id; 64 | } 65 | 66 | /** 67 | * Gets the underlying PHPCR node of this content. 68 | * 69 | * @return NodeInterface 70 | */ 71 | public function getNode() 72 | { 73 | return $this->node; 74 | } 75 | 76 | /** 77 | * Sets the parent document. 78 | * 79 | * @param object $parent the parent document 80 | */ 81 | public function setParentDocument($parent) 82 | { 83 | $this->parentDocument = $parent; 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Gets the parent document. 90 | * 91 | * @return object 92 | */ 93 | public function getParentDocument() 94 | { 95 | return $this->parentDocument; 96 | } 97 | 98 | /** 99 | * Sets the document name. 100 | * 101 | * @param string $name 102 | */ 103 | public function setName($name) 104 | { 105 | $this->name = $name; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * Gets the document name. 112 | * 113 | * @return string 114 | */ 115 | public function getName() 116 | { 117 | return $this->name; 118 | } 119 | 120 | /** 121 | * @return string 122 | */ 123 | public function getTitle() 124 | { 125 | return $this->title; 126 | } 127 | 128 | /** 129 | * @param string $title 130 | */ 131 | public function setTitle($title) 132 | { 133 | $this->title = $title; 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * @return string 140 | */ 141 | public function getBody() 142 | { 143 | return $this->body; 144 | } 145 | 146 | /** 147 | * @param string $body 148 | */ 149 | public function setBody($body) 150 | { 151 | $this->body = $body; 152 | 153 | return $this; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Document/ContentWithExtractors.php: -------------------------------------------------------------------------------- 1 | getTitle(); 33 | } 34 | 35 | /** 36 | * Provide a description of this page to be used in SEO context. 37 | * 38 | * @return string 39 | */ 40 | public function getSeoDescription() 41 | { 42 | return substr($this->getBody(), 0, 200).' ...'; 43 | } 44 | 45 | /** 46 | * The method returns the absolute url as a string to redirect to 47 | * or set to the canonical link. 48 | * 49 | * @return string 50 | */ 51 | public function getSeoOriginalUrl() 52 | { 53 | return '/home'; 54 | } 55 | 56 | /** 57 | * Provide a list of the content's keywords for this page to be 58 | * used in SEO context. 59 | * 60 | * @return string 61 | */ 62 | public function getSeoKeywords() 63 | { 64 | return 'test, key'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Document/SeoAwareContent.php: -------------------------------------------------------------------------------- 1 | seoMetadata; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function setSeoMetadata($seoMetadata) 43 | { 44 | $this->seoMetadata = $seoMetadata; 45 | 46 | return $this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Document/SitemapAwareContent.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class SitemapAwareContent extends ContentBase implements RouteReferrersReadInterface, TranslatableInterface, SitemapAwareInterface 27 | { 28 | /** 29 | * @var string 30 | * 31 | * @PHPCRODM\Locale 32 | */ 33 | protected $locale; 34 | 35 | /** 36 | * @var ArrayCollection|Route[] 37 | * 38 | * @PHPCRODM\Referrers( 39 | * referringDocument="Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route", 40 | * referencedBy="content" 41 | * ) 42 | */ 43 | protected $routes; 44 | 45 | /** 46 | * @var bool 47 | * 48 | * @PHPCRODM\Field(type="boolean",property="visible_for_sitemap") 49 | */ 50 | private $isVisibleForSitemap; 51 | 52 | /** 53 | * @var string 54 | * 55 | * @PHPCRODM\Field(type="string",translated=true) 56 | */ 57 | protected $title; 58 | 59 | public function __construct() 60 | { 61 | $this->routes = new ArrayCollection(); 62 | } 63 | 64 | /** 65 | * @param string $sitemap 66 | * 67 | * @return bool 68 | */ 69 | public function isVisibleInSitemap($sitemap) 70 | { 71 | return $this->isVisibleForSitemap; 72 | } 73 | 74 | /** 75 | * @param bool $isVisibleForSitemap 76 | * 77 | * @return SitemapAwareContent 78 | */ 79 | public function setIsVisibleForSitemap($isVisibleForSitemap) 80 | { 81 | $this->isVisibleForSitemap = $isVisibleForSitemap; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Add a route to the collection. 88 | * 89 | * @param Route $route 90 | */ 91 | public function addRoute($route) 92 | { 93 | $this->routes->add($route); 94 | } 95 | 96 | /** 97 | * Remove a route from the collection. 98 | * 99 | * @param Route $route 100 | */ 101 | public function removeRoute($route) 102 | { 103 | $this->routes->removeElement($route); 104 | } 105 | 106 | /** 107 | * Get the routes that point to this content. 108 | * 109 | * @return Route[] Route instances that point to this content 110 | */ 111 | public function getRoutes() 112 | { 113 | return $this->routes; 114 | } 115 | 116 | /** 117 | * @return string|bool the locale of this model or false if 118 | * translations are disabled in this project 119 | */ 120 | public function getLocale() 121 | { 122 | return $this->locale; 123 | } 124 | 125 | /** 126 | * @param string|bool $locale the local for this model, or false if 127 | * translations are disabled in this project 128 | */ 129 | public function setLocale($locale) 130 | { 131 | $this->locale = $locale; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Document/SitemapAwareWithLastModifiedDateContent.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class SitemapAwareWithLastModifiedDateContent extends ContentBase implements RouteReferrersReadInterface, SitemapAwareInterface 26 | { 27 | /** 28 | * @var ArrayCollection|Route[] 29 | * 30 | * @PHPCRODM\Referrers( 31 | * referringDocument="Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route", 32 | * referencedBy="content" 33 | * ) 34 | */ 35 | protected $routes; 36 | 37 | /** 38 | * @var bool 39 | * 40 | * @PHPCRODM\Field(type="boolean",property="visible_for_sitemap") 41 | */ 42 | private $isVisibleForSitemap; 43 | 44 | /** 45 | * @var \DateTime 46 | * @PHPCRODM\Field(type="date", property="jcr:lastModified") 47 | */ 48 | private $lastModified; 49 | 50 | public function __construct() 51 | { 52 | $this->routes = new ArrayCollection(); 53 | } 54 | 55 | /** 56 | * @param string $sitemap 57 | * 58 | * @return bool 59 | */ 60 | public function isVisibleInSitemap($sitemap) 61 | { 62 | return $this->isVisibleForSitemap; 63 | } 64 | 65 | /** 66 | * @param bool $isVisibleForSitemap 67 | * 68 | * @return SitemapAwareContent 69 | */ 70 | public function setIsVisibleForSitemap($isVisibleForSitemap) 71 | { 72 | $this->isVisibleForSitemap = $isVisibleForSitemap; 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function getRoutes() 81 | { 82 | return $this->routes; 83 | } 84 | 85 | /** 86 | * @return \DateTime 87 | */ 88 | public function getLastModified() 89 | { 90 | return $this->lastModified; 91 | } 92 | 93 | /** 94 | * @param \DateTime $lastModified 95 | * 96 | * @return SitemapAwareWithLastModifiedDateContent 97 | */ 98 | public function setLastModified($lastModified) 99 | { 100 | $this->lastModified = $lastModified; 101 | 102 | return $this; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Entity/SeoAwareOrmContent.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class SeoAwareOrmContent implements SeoAwareInterface 23 | { 24 | /** 25 | * @ORM\Id() 26 | * @ORM\Column(type="integer") 27 | * @ORM\GeneratedValue(strategy="AUTO") 28 | */ 29 | private $id; 30 | 31 | /** 32 | * @ORM\Column(type="string") 33 | */ 34 | private $title; 35 | 36 | /** 37 | * @ORM\Column(type="text") 38 | */ 39 | private $body; 40 | 41 | /** 42 | * @ORM\Column(type="object") 43 | */ 44 | private $seoMetadata; 45 | 46 | /** 47 | * @param mixed $body 48 | */ 49 | public function setBody($body) 50 | { 51 | $this->body = $body; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * @return mixed 58 | */ 59 | public function getBody() 60 | { 61 | return $this->body; 62 | } 63 | 64 | /** 65 | * @param mixed $id 66 | */ 67 | public function setId($id) 68 | { 69 | $this->id = $id; 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * @return mixed 76 | */ 77 | public function getId() 78 | { 79 | return $this->id; 80 | } 81 | 82 | /** 83 | * @param mixed $title 84 | */ 85 | public function setTitle($title) 86 | { 87 | $this->title = $title; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * @return mixed 94 | */ 95 | public function getTitle() 96 | { 97 | return $this->title; 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function getSeoMetadata() 104 | { 105 | return $this->seoMetadata; 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function setSeoMetadata($metadata) 112 | { 113 | $this->seoMetadata = $metadata; 114 | 115 | return $this; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Kernel.php: -------------------------------------------------------------------------------- 1 | requireBundleSet('default'); 22 | 23 | if ('phpcr' === $this->environment) { 24 | $this->requireBundleSets(['phpcr_odm']); 25 | } elseif ('orm' === $this->environment) { 26 | $this->requireBundleSet('doctrine_orm'); 27 | } 28 | 29 | $this->registerConfiguredBundles(); 30 | } 31 | 32 | public function registerContainerConfiguration(LoaderInterface $loader) 33 | { 34 | $loader->load(__DIR__.'/config/config.php'); 35 | $loader->load(__DIR__.'/config/config_'.$this->environment.'.php'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Model/SeoAwareOrmContent.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class SeoAwareOrmContent implements SeoAwareInterface 23 | { 24 | /** 25 | * @ORM\Id() 26 | * @ORM\Column(type="integer") 27 | */ 28 | private $id; 29 | 30 | /** 31 | * @ORM\String(type="string") 32 | */ 33 | private $title; 34 | 35 | /** 36 | * @ORM\Column(type="text") 37 | */ 38 | private $body; 39 | 40 | /** 41 | * @ORM\Column(type="object") 42 | */ 43 | private $seoMetadata; 44 | 45 | /** 46 | * @param mixed $body 47 | */ 48 | public function setBody($body) 49 | { 50 | $this->body = $body; 51 | } 52 | 53 | /** 54 | * @return mixed 55 | */ 56 | public function getBody() 57 | { 58 | return $this->body; 59 | } 60 | 61 | /** 62 | * @param mixed $id 63 | */ 64 | public function setId($id) 65 | { 66 | $this->id = $id; 67 | } 68 | 69 | /** 70 | * @return mixed 71 | */ 72 | public function getId() 73 | { 74 | return $this->id; 75 | } 76 | 77 | /** 78 | * @param mixed $title 79 | */ 80 | public function setTitle($title) 81 | { 82 | $this->title = $title; 83 | } 84 | 85 | /** 86 | * @return mixed 87 | */ 88 | public function getTitle() 89 | { 90 | return $this->title; 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | public function getSeoMetadata() 97 | { 98 | return $this->seoMetadata; 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function setSeoMetadata($metadata) 105 | { 106 | $this->seoMetadata = $metadata; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Resources/views/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block seo_head %} 7 | {% block title %}{{ cmfMainContent.title|default('CMF Sandbox')|striptags }}{% endblock %} 8 | {% endblock %} 9 | 10 | 11 | 12 |
13 | {% block content %} 14 | {% endblock %} 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Resources/views/tests/exception.html.twig: -------------------------------------------------------------------------------- 1 |

Exception-Test

2 | 3 |
4 | {{ status_code }} {{ status_text }} - {{ message }} - {{ exception.class|abbr_class }} 5 |
6 |
7 | 14 |
15 | -------------------------------------------------------------------------------- /tests/Fixtures/App/Resources/views/tests/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "::base.html.twig" %} 2 | 3 | {% block title %}{{ cmfMainContent.title|striptags }}{% endblock %} 4 | 5 | {% block seo_head -%} 6 | {{ sonata_seo_title() }} 7 | {{ sonata_seo_metadatas() }} 8 | {{ sonata_seo_link_canonical() }} 9 | {{ sonata_seo_lang_alternates() }} 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
14 |

This is a template used by the sandbox controller

15 |

{{ cmfMainContent.title }}

16 |
17 | {{ cmfMainContent.body|raw }} 18 |
19 |
20 | {{ parent() }} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 14 | Symfony\Cmf\Bundle\SeoBundle\CmfSeoBundle::class => ['all' => true], 15 | Symfony\Cmf\Bundle\RoutingBundle\CmfRoutingBundle::class => ['all' => true], 16 | Symfony\Cmf\Bundle\CoreBundle\CmfCoreBundle::class => ['all' => true], 17 | ]; 18 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/cmf_seo.orm.yml: -------------------------------------------------------------------------------- 1 | cmf_seo: 2 | persistence: { orm: true } 3 | 4 | cmf_routing: 5 | dynamic: 6 | enabled: true 7 | persistence: 8 | orm: 9 | enabled: true 10 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/cmf_seo.phpcr.yml: -------------------------------------------------------------------------------- 1 | cmf_seo: 2 | persistence: { phpcr: true } 3 | alternate_locale: ~ 4 | 5 | cmf_routing: 6 | dynamic: 7 | locales: [de, en] 8 | enabled: true 9 | persistence: 10 | phpcr: 11 | route_basepaths: [/test/routes] 12 | enabled: true 13 | cmf_core: 14 | persistence: 15 | phpcr: 16 | basepath: /test 17 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/cmf_seo.yml: -------------------------------------------------------------------------------- 1 | framework: 2 | translator: { enabled: true, fallback: en } 3 | serializer: true 4 | 5 | sonata_seo: 6 | page: 7 | metas: 8 | name: 9 | keywords: "testkey" 10 | 11 | cmf_seo: 12 | title: "Default | %%content_title%%" 13 | description: "Default description. %%content_description%%" 14 | original_route_pattern: canonical 15 | error: 16 | enable_parent_provider: true 17 | enable_sibling_provider: true 18 | templates: 19 | html: ":tests:exception.html.twig" 20 | exclusion_rules: 21 | - { path: 'excluded' } 22 | sitemap: 23 | defaults: 24 | default_change_frequency: never 25 | templates: 26 | xml: 'CmfSeoBundle:Sitemap:index.xml.twig' 27 | html: 'CmfSeoBundle:Sitemap:index.html.twig' 28 | configurations: 29 | sitemap: ~ 30 | frequent: 31 | default_change_frequency: always 32 | 33 | cmf_routing: 34 | chain: 35 | routers_by_id: 36 | cmf_routing.dynamic_router: 20 37 | router.default: 100 38 | 39 | twig: 40 | exception_controller: cmf_seo.error.suggestion_provider.controller:listAction 41 | 42 | cmf_core: 43 | multilang: 44 | locales: [en, de] 45 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/config.php: -------------------------------------------------------------------------------- 1 | import(CMF_TEST_CONFIG_DIR.'/default.php'); 13 | $loader->import(__DIR__.'/cmf_seo.yml'); 14 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/config_orm.php: -------------------------------------------------------------------------------- 1 | setParameter('cmf_testing.bundle_fqn', 'Symfony\Cmf\Bundle\SeoBundle'); 13 | $loader->import(CMF_TEST_CONFIG_DIR.'/doctrine_orm.php'); 14 | $loader->import(__DIR__.'/cmf_seo.orm.yml'); 15 | 16 | $container->loadFromExtension('doctrine', [ 17 | 'orm' => [ 18 | 'mappings' => [ 19 | 'tests_fixtures' => [ 20 | 'type' => 'annotation', 21 | 'prefix' => 'Symfony\Cmf\Bundle\SeoBundle\Tests\Fixtures\App\Entity', 22 | 'dir' => $container->getParameter('kernel.root_dir').'/Entity', 23 | 'is_bundle' => false, 24 | ], 25 | ], 26 | ], 27 | ]); 28 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/config_phpcr.php: -------------------------------------------------------------------------------- 1 | setParameter('cmf_testing.bundle_fqn', 'Symfony\Cmf\Bundle\SeoBundle'); 13 | $loader->import(CMF_TEST_CONFIG_DIR.'/phpcr_odm.php'); 14 | $loader->import(__DIR__.'/cmf_seo.phpcr.yml'); 15 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/routing.php: -------------------------------------------------------------------------------- 1 | addCollection($loader->import(__DIR__.'/../../../../src/Resources/config/routing/sitemap.xml')); 17 | 18 | return $collection; 19 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/tree_browser_1.x.yml: -------------------------------------------------------------------------------- 1 | cmf_tree: 2 | resource: . 3 | type: 'cmf_tree' 4 | 5 | fos_js_routing: 6 | resource: '@FOSJsRoutingBundle/Resources/config/routing/routing.xml' 7 | -------------------------------------------------------------------------------- /tests/Fixtures/App/config/tree_browser_2.x.yml: -------------------------------------------------------------------------------- 1 | _cmf_resource: 2 | resource: '@CmfResourceRestBundle/Resources/config/routing.yml' 3 | -------------------------------------------------------------------------------- /tests/Fixtures/App/translations/mesages.de.xliff: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | default_title 7 | %content_title% | Default title 8 | 9 | 10 | default_description 11 | Default description. %content_description% 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/Fixtures/App/translations/mesages.en.xliff: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | default_title 7 | %title% | Default title 8 | 9 | 10 | default_description 11 | Default description. %description% 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/Fixtures/fixtures/config/config.php: -------------------------------------------------------------------------------- 1 | loadFromExtension('cmf_seo', [ 13 | 'title' => 'default_title', 14 | 'description' => 'description_key', 15 | ]); 16 | -------------------------------------------------------------------------------- /tests/Fixtures/fixtures/config/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/Fixtures/fixtures/config/config.yml: -------------------------------------------------------------------------------- 1 | cmf_seo: 2 | title: default_title 3 | description: default_description 4 | -------------------------------------------------------------------------------- /tests/Fixtures/fixtures/config/config1.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/Fixtures/fixtures/config/config2.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | _none 17 | _none 18 | _none 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | /My/Class 27 | 28 |
30 |
31 | -------------------------------------------------------------------------------- /tests/Fixtures/fixtures/config/config3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/Fixtures/fixtures/config/config4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/Fixtures/fixtures/config/config_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/Fixtures/fixtures/config/config_sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/Functional/Doctrine/Orm/SeoMetadataTest.php: -------------------------------------------------------------------------------- 1 | getDbManager('ORM')->getOm()))->purge(); 24 | } 25 | 26 | protected function getKernelConfiguration() 27 | { 28 | return [ 29 | 'environment' => 'orm', 30 | ]; 31 | } 32 | 33 | protected function getEm() 34 | { 35 | return $this->db('ORM')->getOm(); 36 | } 37 | 38 | public function testSeoMetadata() 39 | { 40 | $content = new SeoAwareOrmContent(); 41 | $content 42 | ->setTitle('Seo Aware test') 43 | ->setBody('Content for SeoAware Test') 44 | ; 45 | 46 | $seoMetadata = new SeoMetadata(); 47 | $seoMetadata 48 | ->setTitle('Seo Title') 49 | ->setMetaDescription('Seo Description') 50 | ->setMetaKeywords('Seo, Keys') 51 | ->setOriginalUrl('/test') 52 | ->setExtraProperties(['og:title' => 'Extra title']) 53 | ->setExtraNames(['robots' => 'index, follow']) 54 | ->setExtraHttp(['content-type' => 'text/html']) 55 | ; 56 | 57 | $content->setSeoMetadata($seoMetadata); 58 | 59 | $this->getEm()->persist($content); 60 | $this->getEm()->flush(); 61 | $this->getEm()->clear(); 62 | 63 | $content = $this->getEm() 64 | ->getRepository('Symfony\Cmf\Bundle\SeoBundle\Tests\Fixtures\App\Entity\SeoAwareOrmContent') 65 | ->findOneByTitle('Seo Aware test'); 66 | 67 | $this->assertNotNull($content); 68 | 69 | $persistedSeoMetadata = $content->getSeoMetadata(); 70 | $this->assertEquals($seoMetadata, $persistedSeoMetadata); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Functional/Doctrine/Phpcr/SeoMetadataTest.php: -------------------------------------------------------------------------------- 1 | getDbManager('PHPCR')->getOm()))->purge(); 25 | $this->db('PHPCR')->createTestNode(); 26 | $this->dm = $this->db('PHPCR')->getOm(); 27 | $this->base = $this->dm->find(null, '/test'); 28 | } 29 | 30 | public function testSeoMetadataMapping() 31 | { 32 | $content = new SeoAwareContent(); 33 | $content 34 | ->setTitle('Seo Aware test') 35 | ->setName('seo-aware') 36 | ->setParentDocument($this->dm->find(null, '/test')) 37 | ->setBody('Content for SeoAware Test') 38 | ; 39 | 40 | $seoMetadata = new SeoMetadata(); 41 | $seoMetadata 42 | ->setTitle('Seo Title') 43 | ->setMetaDescription('Seo Description') 44 | ->setMetaKeywords('Seo, Keys') 45 | ->setOriginalUrl('/test') 46 | ->setExtraProperties(['og:title' => 'Extra title']) 47 | ->setExtraNames(['robots' => 'index, follow']) 48 | ->setExtraHttp(['content-type' => 'text/html']) 49 | ; 50 | 51 | $content->setSeoMetadata($seoMetadata); 52 | 53 | $this->dm->persist($content); 54 | $this->dm->flush(); 55 | $this->dm->clear(); 56 | 57 | $content = $this->dm->find(null, '/test/seo-aware'); 58 | 59 | $this->assertNotNull($content); 60 | 61 | $persistedSeoMetadata = $content->getSeoMetadata(); 62 | $this->assertEquals($seoMetadata->getTitle(), $persistedSeoMetadata->getTitle()); 63 | $this->assertEquals($seoMetadata->getMetaDescription(), $persistedSeoMetadata->getMetaDescription()); 64 | $this->assertEquals($seoMetadata->getMetaKeywords(), $persistedSeoMetadata->getMetaKeywords()); 65 | $this->assertEquals($seoMetadata->getOriginalUrl(), $persistedSeoMetadata->getOriginalUrl()); 66 | $this->assertEquals($seoMetadata->getExtraProperties(), $persistedSeoMetadata->getExtraProperties()); 67 | $this->assertEquals($seoMetadata->getExtraNames(), $persistedSeoMetadata->getExtraNames()); 68 | $this->assertEquals($seoMetadata->getExtraHttp(), $persistedSeoMetadata->getExtraHttp()); 69 | } 70 | 71 | /** 72 | * @expectedException \Doctrine\ODM\PHPCR\Exception\OutOfBoundsException 73 | * @expectedExceptionMessage It cannot have children 74 | */ 75 | public function testAddSeoMetadataChild() 76 | { 77 | $seoMetadata = new SeoMetadata(); 78 | $seoMetadata->setName('seo-metadata'); 79 | $seoMetadata->setParentDocument($this->dm->find(null, '/test')); 80 | $this->dm->persist($seoMetadata); 81 | 82 | $document = new Generic(); 83 | $document->setParentDocument($seoMetadata); 84 | $document->setNodename('invalid'); 85 | $this->dm->persist($document); 86 | 87 | $this->dm->flush(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Functional/Doctrine/Phpcr/SitemapDocumentProviderTest.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class SitemapDocumentProviderTest extends BaseTestCase 21 | { 22 | /** 23 | * @var SitemapDocumentProvider 24 | */ 25 | private $documentProvider; 26 | 27 | public function setUp() 28 | { 29 | $this->db('PHPCR')->createTestNode(); 30 | $this->dm = $this->db('PHPCR')->getOm(); 31 | $this->base = $this->dm->find(null, '/test'); 32 | $this->db('PHPCR')->loadFixtures( 33 | ['Symfony\Cmf\Bundle\SeoBundle\Tests\Fixtures\App\DataFixtures\Phpcr\LoadSitemapData'] 34 | ); 35 | 36 | $this->documentProvider = new SitemapDocumentProvider($this->dm); 37 | } 38 | 39 | public function testDocumentOrder() 40 | { 41 | $documents = $this->documentProvider->load('default'); 42 | 43 | $paths = []; 44 | foreach ($documents as $document) { 45 | $paths[] = $document->getId(); 46 | } 47 | 48 | $this->assertEquals( 49 | [ 50 | '/test/content/sitemap-aware', 51 | '/test/content/sitemap-aware-last-mod-date', 52 | '/test/content/sitemap-aware-non-publish', 53 | '/test/content/sitemap-aware-publish', 54 | ], 55 | $paths 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Unit/Cache/FileCacheTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class FileCacheTest extends \PHPUnit_Framework_TestCase 18 | { 19 | public function testThrowingExceptionForUnknownBaseDir() 20 | { 21 | $thrown = false; 22 | $baseDir = __DIR__.'/unknown'; 23 | $dir = 'test'; 24 | 25 | try { 26 | new FileCache($baseDir, $dir); 27 | } catch (\InvalidArgumentException $e) { 28 | $message = $e->getMessage(); 29 | $thrown = true; 30 | } 31 | 32 | $this->assertTrue($thrown); 33 | $this->assertEquals($message, sprintf('The directory "%s" does not exist.', $baseDir)); 34 | } 35 | 36 | public function testDirectoryCreation() 37 | { 38 | $baseDir = __DIR__.'/base'; 39 | $dir = 'test'; 40 | mkdir($baseDir); 41 | new FileCache($baseDir, $dir); 42 | 43 | $expectedDirectory = $baseDir.'/'.$dir; 44 | $this->assertTrue(is_dir($expectedDirectory)); 45 | 46 | rmdir($expectedDirectory); 47 | rmdir($baseDir); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Unit/DependencyInjection/Compiler/RegisterExtractorsPassTest.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new RegisterExtractorsPass()); 25 | } 26 | 27 | public function testRegistersServicesWithExtractorTagAndDefaultPriority() 28 | { 29 | $nonExtractorService = new Definition(); 30 | $this->setDefinition('some_service', $nonExtractorService); 31 | 32 | $extractorService = new Definition(); 33 | $extractorService->addTag('cmf_seo.extractor'); 34 | $this->setDefinition('extractor_service', $extractorService); 35 | 36 | $presentationService = new Definition(); 37 | $this->setDefinition('cmf_seo.presentation', $presentationService); 38 | 39 | $this->compile(); 40 | 41 | $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( 42 | 'cmf_seo.presentation', 43 | 'addExtractor', 44 | [new Reference('extractor_service'), 0] 45 | ); 46 | } 47 | 48 | public function testRegistersServicesWithExtractorTagAndPriority() 49 | { 50 | $nonExtractorService = new Definition(); 51 | $this->setDefinition('some_service', $nonExtractorService); 52 | 53 | $extractorService = new Definition(); 54 | $extractorService->addTag('cmf_seo.extractor', ['priority' => 1]); 55 | $this->setDefinition('extractor_service', $extractorService); 56 | 57 | $presentationService = new Definition(); 58 | $this->setDefinition('cmf_seo.presentation', $presentationService); 59 | 60 | $this->compile(); 61 | 62 | $this->assertContainerBuilderHasServiceDefinitionWithMethodCall( 63 | 'cmf_seo.presentation', 64 | 'addExtractor', 65 | [new Reference('extractor_service'), 1] 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Unit/DependencyInjection/Compiler/RegisterSuggestionProviderPassTest.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class RegisterSuggestionProviderPassTest extends AbstractCompilerPassTestCase 24 | { 25 | /** 26 | * Register the compiler pass under test, just like you would do inside a bundle's load() 27 | * method:. 28 | * 29 | * $container->addCompilerPass(new MyCompilerPass()); 30 | */ 31 | protected function registerCompilerPass(ContainerBuilder $container) 32 | { 33 | $container->addCompilerPass(new RegisterSuggestionProviderPass()); 34 | } 35 | 36 | public function testRegistersServicesWithMatcherTag() 37 | { 38 | $nonMatcherService = new Definition(); 39 | $this->setDefinition('some_service', $nonMatcherService); 40 | 41 | $matcherServiceWithGroup = new Definition(); 42 | $matcherServiceWithGroup->addTag('cmf_seo.suggestion_provider', ['group' => 'some-group']); 43 | $this->setDefinition('matcher.with_group', $matcherServiceWithGroup); 44 | 45 | $matcherPresentationService = new Definition(); 46 | $matcherPresentationService->setArguments([1, 1, 2, 3, []]); 47 | $this->setDefinition('cmf_seo.error.suggestion_provider.controller', $matcherPresentationService); 48 | 49 | $this->compile(); 50 | 51 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 52 | 'cmf_seo.error.suggestion_provider.controller', 53 | 4, 54 | [['provider' => new Reference('matcher.with_group'), 'group' => 'some-group']] 55 | ); 56 | } 57 | 58 | public function testRegistersServicesWithMatcherTagWithoutGroup() 59 | { 60 | $nonMatcherService = new Definition(); 61 | $this->setDefinition('some_service', $nonMatcherService); 62 | 63 | $matcherServiceWithOutGroup = new Definition(); 64 | $matcherServiceWithOutGroup->addTag('cmf_seo.suggestion_provider'); 65 | $this->setDefinition('matcher.without_group', $matcherServiceWithOutGroup); 66 | 67 | $matcherPresentationService = new Definition(); 68 | $matcherPresentationService->setArguments([1, 1, 2, 3, []]); 69 | $this->setDefinition('cmf_seo.error.suggestion_provider.controller', $matcherPresentationService); 70 | 71 | $this->compile(); 72 | 73 | $this->assertContainerBuilderHasServiceDefinitionWithArgument( 74 | 'cmf_seo.error.suggestion_provider.controller', 75 | 4, 76 | [['provider' => new Reference('matcher.without_group'), 'group' => 'default']] 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Unit/DependencyInjection/ConfigValuesTest.php: -------------------------------------------------------------------------------- 1 | setOriginalUrlBehaviour('nonexistent'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Unit/DependencyInjection/XmlSchemaTest.php: -------------------------------------------------------------------------------- 1 | assertSchemaAcceptsXml([$xmlFile], __DIR__.'/../../../src/Resources/config/schema/seo-1.0.xsd'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Unit/EventListener/ContentListenerTest.php: -------------------------------------------------------------------------------- 1 | seoPresentation = $this->createMock(SeoPresentationInterface::class); 35 | $this->request = $this->createMock(Request::class); 36 | $this->event = $this->createMock(GetResponseEvent::class); 37 | $this->listener = new ContentListener($this->seoPresentation, DynamicRouter::CONTENT_KEY); 38 | } 39 | 40 | /** 41 | * @dataProvider getRedirectRouteData 42 | */ 43 | public function testRedirectRoute($targetUrl, $redirect = true, $currentPath = '/test') 44 | { 45 | $redirectResponse = $this->createMock(RedirectResponse::class); 46 | $redirectResponse->expects($this->any()) 47 | ->method('getTargetUrl') 48 | ->will($this->returnValue($targetUrl)); 49 | 50 | $content = new \stdClass(); 51 | $this->seoPresentation 52 | ->expects($this->once()) 53 | ->method('updateSeoPage') 54 | ->with($content) 55 | ; 56 | $this->seoPresentation 57 | ->expects($this->any()) 58 | ->method('getRedirectResponse') 59 | ->will($this->returnValue($redirectResponse)) 60 | ; 61 | 62 | $this->event 63 | ->expects($this->any()) 64 | ->method('getRequest') 65 | ->will($this->returnValue($this->request)) 66 | ; 67 | if ($redirect) { 68 | $this->event 69 | ->expects($this->once()) 70 | ->method('setResponse') 71 | ->with($redirectResponse) 72 | ; 73 | } else { 74 | $this->event 75 | ->expects($this->never()) 76 | ->method('setResponse') 77 | ; 78 | } 79 | 80 | $attributes = $this->createMock(ParameterBag::class); 81 | $attributes 82 | ->expects($this->any()) 83 | ->method('has') 84 | ->with(DynamicRouter::CONTENT_KEY) 85 | ->will($this->returnValue(true)) 86 | ; 87 | $attributes 88 | ->expects($this->any()) 89 | ->method('get') 90 | ->with(DynamicRouter::CONTENT_KEY) 91 | ->will($this->returnValue($content)) 92 | ; 93 | $this->request->attributes = $attributes; 94 | $this->request 95 | ->expects($this->any()) 96 | ->method('getBaseUrl') 97 | ->will($this->returnValue('')) 98 | ; 99 | $this->request 100 | ->expects($this->any()) 101 | ->method('getPathInfo') 102 | ->will($this->returnValue($currentPath)) 103 | ; 104 | 105 | $this->listener->onKernelRequest($this->event); 106 | } 107 | 108 | public function getRedirectRouteData() 109 | { 110 | return [ 111 | ['/test_redirect'], 112 | ['/a/test'], 113 | ['/test', false], 114 | ['/test?a', false], 115 | ['/test', false, '/test#b'], 116 | ['/test?a', false, '/test#b'], 117 | ]; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/Unit/EventListener/LanguageListenerTest.php: -------------------------------------------------------------------------------- 1 | listener = new LanguageListener(); 26 | } 27 | 28 | /** 29 | * @dataProvider provideRequestLocales 30 | */ 31 | public function testSetsContentLanguageHeader($locale) 32 | { 33 | $request = Request::create('/'); 34 | $request->setLocale($locale); 35 | 36 | $response = new Response(); 37 | 38 | $event = $this->createMock(FilterResponseEvent::class); 39 | $event->expects($this->any())->method('getRequest')->will($this->returnValue($request)); 40 | $event->expects($this->any())->method('getResponse')->will($this->returnValue($response)); 41 | 42 | $this->listener->onKernelResponse($event); 43 | 44 | $this->assertEquals('en', $response->headers->get('Content-Language')); 45 | } 46 | 47 | public function provideRequestLocales() 48 | { 49 | return [ 50 | ['en'], 51 | ['en_US'], 52 | ]; 53 | } 54 | 55 | public function testDoesNotOverridePreSetContentLanguage() 56 | { 57 | $request = Request::create('/'); 58 | $request->setLocale('en'); 59 | 60 | $response = new Response(); 61 | $response->headers->set('Content-Language', 'nl'); 62 | 63 | $event = $this->createMock(FilterResponseEvent::class); 64 | $event->expects($this->any())->method('getRequest')->will($this->returnValue($request)); 65 | $event->expects($this->any())->method('getResponse')->will($this->returnValue($response)); 66 | 67 | $this->listener->onKernelResponse($event); 68 | 69 | $this->assertEquals('nl', $response->headers->get('Content-Language')); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Unit/Extractor/BaseTestCase.php: -------------------------------------------------------------------------------- 1 | seoMetadata = $this->createMock(SeoMetadataInterface::class); 30 | } 31 | 32 | /** 33 | * @dataProvider getSupportsData 34 | */ 35 | public function testSupports($object, $supports = true) 36 | { 37 | $result = $this->extractor->supports($object); 38 | 39 | if ($supports) { 40 | $this->assertTrue($result); 41 | } else { 42 | $this->assertFalse($result); 43 | } 44 | } 45 | 46 | abstract public function getSupportsData(); 47 | 48 | public function testExtracting() 49 | { 50 | $document = $this->getMockBuilder('ExtractedDocument')->setMethods([$this->extractMethod])->getMock(); 51 | $document->expects($this->any()) 52 | ->method($this->extractMethod) 53 | ->will($this->returnValue('extracted')); 54 | 55 | $this->seoMetadata->expects($this->once()) 56 | ->method($this->metadataMethod) 57 | ->with($this->equalTo('extracted')) 58 | ; 59 | 60 | $this->extractor->updateMetadata($document, $this->seoMetadata); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Unit/Extractor/DescriptionExtractorTest.php: -------------------------------------------------------------------------------- 1 | extractor = new DescriptionExtractor(); 25 | $this->extractMethod = 'getSeoDescription'; 26 | $this->metadataMethod = 'setMetaDescription'; 27 | } 28 | 29 | public function getSupportsData() 30 | { 31 | return [ 32 | [$this->createMock(DescriptionReadInterface::class)], 33 | [$this->createMock(SeoAwareInterface::class), false], 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/Extractor/ExtrasExtractorTest.php: -------------------------------------------------------------------------------- 1 | extractor = new ExtrasExtractor(); 25 | } 26 | 27 | public function getSupportsData() 28 | { 29 | return [ 30 | [$this->createMock(ExtrasReadInterface::class)], 31 | [$this->createMock(SeoAwareInterface::class), false], 32 | ]; 33 | } 34 | 35 | public function testExtracting() 36 | { 37 | $document = $this->getMockBuilder('ExtractedDocument')->setMethods(['getSeoExtras'])->getMock(); 38 | $document->expects($this->any()) 39 | ->method('getSeoExtras') 40 | ->will($this->returnValue([ 41 | 'property' => ['og:title' => 'Extra Title'], 42 | 'name' => ['robots' => 'index, follow'], 43 | 'http-equiv' => ['Content-Type' => 'text/html; charset=utf-8'], 44 | ])); 45 | 46 | $this->seoMetadata->expects($this->once()) 47 | ->method('addExtraProperty') 48 | ->with($this->equalTo('og:title'), $this->equalTo('Extra Title')) 49 | ; 50 | 51 | $this->seoMetadata->expects($this->once()) 52 | ->method('addExtraName') 53 | ->with($this->equalTo('robots'), $this->equalTo('index, follow')) 54 | ; 55 | 56 | $this->seoMetadata->expects($this->once()) 57 | ->method('addExtraHttp') 58 | ->with($this->equalTo('Content-Type'), $this->equalTo('text/html; charset=utf-8')) 59 | ; 60 | 61 | $this->extractor->updateMetadata($document, $this->seoMetadata); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Unit/Extractor/KeywordsExtractorTest.php: -------------------------------------------------------------------------------- 1 | extractor = new KeywordsExtractor(); 25 | $this->extractMethod = 'getSeoKeywords'; 26 | $this->metadataMethod = 'setMetaKeywords'; 27 | } 28 | 29 | public function getSupportsData() 30 | { 31 | return [ 32 | [$this->createMock(KeywordsReadInterface::class)], 33 | [$this->createMock(SeoAwareInterface::class), false], 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/Extractor/OriginalRouteExtractorTest.php: -------------------------------------------------------------------------------- 1 | urlGenerator = $this->createMock(UrlGeneratorInterface::class); 28 | $this->extractor = new OriginalRouteExtractor($this->urlGenerator); 29 | $this->extractMethod = 'getSeoOriginalRoute'; 30 | $this->metadataMethod = 'setOriginalUrl'; 31 | } 32 | 33 | public function getSupportsData() 34 | { 35 | return [ 36 | [$this->createMock(OriginalRouteReadInterface::class)], 37 | [$this->createMock(SeoAwareInterface::class), false], 38 | ]; 39 | } 40 | 41 | public function testExtracting() 42 | { 43 | $this->urlGenerator->expects($this->any()) 44 | ->method('generate') 45 | ->with($this->equalTo('extracted')) 46 | ->will($this->returnValue('extracted')) 47 | ; 48 | 49 | parent::testExtracting(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Unit/Extractor/OriginalUrlExtractorTest.php: -------------------------------------------------------------------------------- 1 | extractor = new OriginalUrlExtractor(); 25 | $this->extractMethod = 'getSeoOriginalUrl'; 26 | $this->metadataMethod = 'setOriginalUrl'; 27 | } 28 | 29 | public function getSupportsData() 30 | { 31 | return [ 32 | [$this->createMock(OriginalUrlReadInterface::class)], 33 | [$this->createMock(SeoAwareInterface::class), false], 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/Extractor/TitleExtractorTest.php: -------------------------------------------------------------------------------- 1 | extractor = new TitleExtractor(); 25 | $this->extractMethod = 'getSeoTitle'; 26 | $this->metadataMethod = 'setTitle'; 27 | } 28 | 29 | public function getSupportsData() 30 | { 31 | return [ 32 | [$this->createMock(TitleReadInterface::class)], 33 | [$this->createMock(SeoAwareInterface::class), false], 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/Extractor/TitleReadExtractorTest.php: -------------------------------------------------------------------------------- 1 | extractor = new TitleReadExtractor(); 24 | $this->extractMethod = 'getTitle'; 25 | $this->metadataMethod = 'setTitle'; 26 | } 27 | 28 | public function getSupportsData() 29 | { 30 | return [ 31 | [$this->getMockBuilder('Foo')->setMethods(['getTitle'])->getMock()], 32 | [$this->createMock(SeoAwareInterface::class), false], 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Unit/Matcher/ExclusionMatcherTest.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class ExclusionMatcherTest extends PHPUnit_Framework_TestCase 20 | { 21 | /** 22 | * @var RequestMatcherInterface 23 | */ 24 | private $matcherB; 25 | 26 | /** 27 | * @var RequestMatcherInterface 28 | */ 29 | private $matcherA; 30 | 31 | /** 32 | * @var ExclusionMatcher 33 | */ 34 | private $exclusionMatcher; 35 | 36 | public function setUp() 37 | { 38 | $this->matcherA = $this->createMock(RequestMatcherInterface::class); 39 | $this->matcherB = $this->createMock(RequestMatcherInterface::class); 40 | 41 | $this->exclusionMatcher = new ExclusionMatcher(); 42 | $this->exclusionMatcher->addRequestMatcher($this->matcherA); 43 | $this->exclusionMatcher->addRequestMatcher($this->matcherB); 44 | } 45 | 46 | public function testReturnTrueMatcherAReturnsTrue() 47 | { 48 | $this->matcherA->expects($this->once())->method('matches')->will($this->returnValue(true)); 49 | 50 | $this->assertTrue($this->exclusionMatcher->matches(new Request())); 51 | } 52 | 53 | public function testReturnTrueMatcherBReturnsTrue() 54 | { 55 | $this->matcherA->expects($this->once())->method('matches')->will($this->returnValue(false)); 56 | $this->matcherB->expects($this->once())->method('matches')->will($this->returnValue(true)); 57 | 58 | $this->assertTrue($this->exclusionMatcher->matches(new Request())); 59 | } 60 | 61 | public function testReturnTrueBothReturningFalse() 62 | { 63 | $this->matcherA->expects($this->once())->method('matches')->will($this->returnValue(false)); 64 | $this->matcherB->expects($this->once())->method('matches')->will($this->returnValue(false)); 65 | 66 | $this->assertFalse($this->exclusionMatcher->matches(new Request())); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Unit/Model/UrlInformationTest.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class UrlInformationTest extends \PHPUnit_Framework_Testcase 20 | { 21 | /** 22 | * @var UrlInformation 23 | */ 24 | private $model; 25 | 26 | public function setUp() 27 | { 28 | $this->model = new UrlInformation(); 29 | } 30 | 31 | /** 32 | * @expectedException \Symfony\Cmf\Bundle\SeoBundle\Exception\InvalidArgumentException 33 | * @expectedExceptionMessage Invalid change frequency "some one", use one of always, hourly, daily, weekly, monthly, yearly, never. 34 | */ 35 | public function testSetChangeFrequencyShouldThrowExceptionForInvalidArguments() 36 | { 37 | $this->model->setChangeFrequency('some one'); 38 | } 39 | 40 | public function testValidChangeFrequency() 41 | { 42 | $this->model->setChangeFrequency('never'); 43 | 44 | $this->assertEquals('never', $this->model->getChangeFrequency()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/Sitemap/AbstractChainTest.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class AbstractChainTest extends \PHPUnit_Framework_Testcase 20 | { 21 | /** @var TestChain */ 22 | private $chain; 23 | 24 | public function setUp() 25 | { 26 | $this->chain = new TestChain(); 27 | } 28 | 29 | public function testAllChains() 30 | { 31 | $one = new TestEntry('info-one'); 32 | $two = new TestEntry('info-two'); 33 | 34 | $this->chain->addItem($one, 0); 35 | $this->chain->addItem($two, 0); 36 | 37 | $expectedList = ['info-one', 'info-two']; 38 | 39 | $this->assertEquals($expectedList, $this->chain->getValues('test')); 40 | } 41 | 42 | public function testSpecificChain() 43 | { 44 | $one = new TestEntry('info-one'); 45 | $two = new TestEntry('info-two'); 46 | 47 | $this->chain->addItem($one, 0, 'test'); 48 | $this->chain->addItem($two, 0, 'test'); 49 | 50 | $expectedList = ['info-one', 'info-two']; 51 | 52 | $this->assertEquals($expectedList, $this->chain->getValues('test')); 53 | } 54 | 55 | public function testPrioritisedInput() 56 | { 57 | $first = new TestEntry('info-first'); 58 | $earlySpecific = new TestEntry('info-early-specific'); 59 | $specific = new TestEntry('info-specific'); 60 | $early = new TestEntry('info-early'); 61 | $last = new TestEntry('info-last'); 62 | 63 | $this->chain->addItem($early, 5); 64 | $this->chain->addItem($specific, 5, 'test'); 65 | $this->chain->addItem($last, 0); 66 | $this->chain->addItem($first, 15); 67 | $this->chain->addItem($earlySpecific, 10, 'test'); 68 | 69 | $this->assertEquals( 70 | ['info-first', 'info-early-specific', 'info-specific', 'info-early', 'info-last'], 71 | $this->chain->getValues('test') 72 | ); 73 | 74 | $this->assertEquals( 75 | ['info-first', 'info-early', 'info-last'], 76 | $this->chain->getValues('foobar') 77 | ); 78 | } 79 | } 80 | 81 | class TestChain extends AbstractChain 82 | { 83 | public function getValues($sitemap) 84 | { 85 | $values = []; 86 | /** @var $entry TestEntry */ 87 | foreach ($this->getSortedEntriesForSitemap($sitemap) as $entry) { 88 | $values[] = $entry->name; 89 | } 90 | 91 | return $values; 92 | } 93 | } 94 | 95 | class TestEntry 96 | { 97 | public $name; 98 | 99 | public function __construct($name) 100 | { 101 | $this->name = $name; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/Unit/Sitemap/AlternateLocalesGuesserTest.php: -------------------------------------------------------------------------------- 1 | add(new AlternateLocale('http://symfony.com/fr', 'fr')); 28 | 29 | $localeProvider = $this->createMock(AlternateLocaleProviderInterface::class); 30 | $localeProvider 31 | ->expects($this->any()) 32 | ->method('createForContent') 33 | ->with($this) 34 | ->will($this->returnValue($collection)) 35 | ; 36 | 37 | return new AlternateLocalesGuesser($localeProvider); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function createData() 44 | { 45 | return $this; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | protected function getFields() 52 | { 53 | return ['AlternateLocales']; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Unit/Sitemap/DefaultChangeFrequencyGuesserTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('weekly', $urlInformation->getChangeFrequency()); 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function createGuesser() 28 | { 29 | return new DefaultChangeFrequencyGuesser('weekly'); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function createData() 36 | { 37 | return $this; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function getFields() 44 | { 45 | return ['ChangeFrequency']; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Unit/Sitemap/GuesserTestCase.php: -------------------------------------------------------------------------------- 1 | guesser = $this->createGuesser(); 35 | $this->data = $this->createData(); 36 | } 37 | 38 | /** 39 | * Information is created. 40 | */ 41 | public function testGuessCreate() 42 | { 43 | $urlInformation = new UrlInformation(); 44 | 45 | $this->guesser->guessValues($urlInformation, $this->data, 'default'); 46 | foreach ($this->getFields() as $field) { 47 | $this->assertNotNull($urlInformation->{'get'.$field}()); 48 | } 49 | 50 | return $urlInformation; 51 | } 52 | 53 | /** 54 | * If the information is already set, it must not be overwritten. 55 | */ 56 | public function testGuessNoOverwrite() 57 | { 58 | $urlInformation = new UrlInformation(); 59 | foreach ($this->getFields() as $field) { 60 | $urlInformation->{'set'.$field}('always'); 61 | } 62 | 63 | $this->guesser->guessValues($urlInformation, $this->data, 'default'); 64 | foreach ($this->getFields() as $field) { 65 | $this->assertEquals('always', $urlInformation->{'get'.$field}()); 66 | } 67 | } 68 | 69 | /** 70 | * Create the guesser for this test. 71 | * 72 | * @return GuesserInterface 73 | */ 74 | abstract protected function createGuesser(); 75 | 76 | /** 77 | * @return object 78 | */ 79 | abstract protected function createData(); 80 | 81 | /** 82 | * Provide list of fields in UrlInformation covered by this guesser. 83 | * 84 | * @return array 85 | */ 86 | abstract protected function getFields(); 87 | } 88 | -------------------------------------------------------------------------------- /tests/Unit/Sitemap/LastModifiedGuesserTest.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class LastModifiedGuesserTest extends GuesserTestCase 25 | { 26 | /** 27 | * @var ManagerRegistry 28 | */ 29 | private $registry; 30 | 31 | /** 32 | * @var DocumentManager 33 | */ 34 | private $manager; 35 | 36 | /** 37 | * @var ClassMetadata 38 | */ 39 | private $metadata; 40 | 41 | public function testGuessCreate() 42 | { 43 | $urlInformation = parent::testGuessCreate(); 44 | $this->assertEquals('2016-07-06T00:00:00+02:00', $urlInformation->getLastModification()); 45 | } 46 | 47 | /** 48 | * Create the guesser for this test. 49 | * 50 | * @return GuesserInterface 51 | */ 52 | protected function createGuesser() 53 | { 54 | $this->registry = $this->createMock(ManagerRegistry::class); 55 | $this->manager = $this->createMock(DocumentManager::class); 56 | $this->metadata = $this->createMock(ClassMetadata::class); 57 | $this->registry 58 | ->expects($this->any()) 59 | ->method('getManagerForClass') 60 | ->with($this->equalTo('Symfony\Cmf\Bundle\SeoBundle\Tests\Unit\Sitemap\LastModifiedGuesserTest')) 61 | ->will($this->returnValue($this->manager)); 62 | $this->manager 63 | ->expects($this->any()) 64 | ->method('getClassMetadata') 65 | ->with($this->equalTo('Symfony\Cmf\Bundle\SeoBundle\Tests\Unit\Sitemap\LastModifiedGuesserTest')) 66 | ->will($this->returnValue($this->metadata)); 67 | $this->metadata 68 | ->expects($this->any()) 69 | ->method('getMixins') 70 | ->will($this->returnValue(['mix:lastModified'])); 71 | $this->metadata 72 | ->expects($this->any()) 73 | ->method('getFieldNames') 74 | ->will($this->returnValue(['lastModified'])); 75 | $this->metadata 76 | ->expects($this->any()) 77 | ->method('getFieldMapping') 78 | ->with($this->equalTo('lastModified')) 79 | ->will($this->returnValue(['property' => 'jcr:lastModified'])); 80 | $this->metadata 81 | ->expects($this->any()) 82 | ->method('getFieldValue') 83 | ->with($this->equalTo($this), $this->equalTo('lastModified')) 84 | ->will($this->returnValue(new \DateTime('2016-07-06', new \DateTimeZone('Europe/Berlin')))); 85 | 86 | return new LastModifiedGuesser($this->registry); 87 | } 88 | 89 | /** 90 | * @return object 91 | */ 92 | protected function createData() 93 | { 94 | return $this; 95 | } 96 | 97 | /** 98 | * Provide list of fields in UrlInformation covered by this guesser. 99 | * 100 | * @return array 101 | */ 102 | protected function getFields() 103 | { 104 | return ['LastModification']; 105 | } 106 | 107 | public function testGuessNoOverwrite() 108 | { 109 | $urlInformation = new UrlInformation(); 110 | $urlInformation->setLastModification(new \DateTime('2016-06-06', new \DateTimeZone('Europe/Berlin'))); 111 | 112 | $this->guesser->guessValues($urlInformation, $this->data, 'default'); 113 | $this->assertEquals('2016-06-06T00:00:00+02:00', $urlInformation->getLastModification()); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/Unit/Sitemap/LocationGuesserTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('http://symfony.com', $urlInformation->getLocation()); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | protected function createGuesser() 29 | { 30 | $urlGenerator = $this->createMock(UrlGeneratorInterface::class); 31 | $urlGenerator 32 | ->expects($this->any()) 33 | ->method('generate') 34 | ->with($this, [], UrlGeneratorInterface::ABSOLUTE_URL) 35 | ->will($this->returnValue('http://symfony.com')) 36 | ; 37 | 38 | return new LocationGuesser($urlGenerator); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function createData() 45 | { 46 | return $this; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | protected function getFields() 53 | { 54 | return ['Location']; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Unit/Sitemap/SeoMetadataTitleGuesserTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('Symfony CMF', $urlInformation->getLabel()); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | protected function createGuesser() 30 | { 31 | $seoMetadata = new SeoMetadata(); 32 | $seoMetadata->setTitle('Symfony CMF'); 33 | $seoPresentation = $this->createMock(SeoPresentation::class); 34 | $seoPresentation 35 | ->expects($this->any()) 36 | ->method('getSeoMetadata') 37 | ->with($this) 38 | ->will($this->returnValue($seoMetadata)) 39 | ; 40 | 41 | return new SeoMetadataTitleGuesser($seoPresentation); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | protected function createData() 48 | { 49 | return $this; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | protected function getFields() 56 | { 57 | return ['Label']; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Unit/Sitemap/SitemapAwareDocumentVoterTest.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class SitemapAwareDocumentVoterTest extends \PHPUnit_Framework_TestCase 22 | { 23 | /** 24 | * @var VoterInterface 25 | */ 26 | protected $voter; 27 | 28 | protected $sitemapAwareDocument; 29 | 30 | public function setUp() 31 | { 32 | $this->voter = new SitemapAwareDocumentVoter(); 33 | $this->sitemapAwareDocument = $this->createMock(SitemapAwareInterface::class); 34 | } 35 | 36 | public function testSitemapAwareDocumentShouldReturnTrueWhenDocumentIsExposed() 37 | { 38 | $this->sitemapAwareDocument 39 | ->expects($this->once()) 40 | ->method('isVisibleInSitemap') 41 | ->will($this->returnValue(true)); 42 | $this->assertTrue($this->voter->exposeOnSitemap($this->sitemapAwareDocument, 'some-sitemap')); 43 | } 44 | 45 | public function testSitemapAwareDocumentShouldReturnFalseWhenDocumentIsNotExposed() 46 | { 47 | $this->sitemapAwareDocument 48 | ->expects($this->once()) 49 | ->method('isVisibleInSitemap') 50 | ->will($this->returnValue(false)); 51 | $this->assertFalse($this->voter->exposeOnSitemap($this->sitemapAwareDocument, 'some-sitemap')); 52 | } 53 | 54 | public function testInvalidDocumentShouldReturnTrueToBeAwareForTheOtherVoters() 55 | { 56 | $this->assertTrue($this->voter->exposeOnSitemap(new \stdClass(), 'some-sitemap')); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Unit/Sitemap/UrlInformationProviderTest.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class UrlInformationProviderTest extends \PHPUnit_Framework_Testcase 24 | { 25 | /** 26 | * @var UrlInformationProvider 27 | */ 28 | private $provider; 29 | 30 | public function setUp() 31 | { 32 | $accepted = new TestModel('accepted'); 33 | $refused = new TestModel('refused'); 34 | 35 | $loader = $this->createMock(LoaderChain::class); 36 | $loader 37 | ->expects($this->once()) 38 | ->method('load') 39 | ->with('default') 40 | ->will( 41 | $this->returnValue([$accepted, $refused]) 42 | ) 43 | ; 44 | 45 | $voter = $this->createMock(VoterChain::class); 46 | $voter 47 | ->expects($this->at(0)) 48 | ->method('exposeOnSitemap') 49 | ->with($accepted, 'default') 50 | ->will($this->returnValue(true)) 51 | ; 52 | $voter 53 | ->expects($this->at(1)) 54 | ->method('exposeOnSitemap') 55 | ->with($refused, 'default') 56 | ->will($this->returnValue(false)) 57 | ; 58 | 59 | $guesser = $this->createMock(GuesserChain::class); 60 | $guesser 61 | ->expects($this->once()) 62 | ->method('guessValues') 63 | ->with( 64 | $this->isInstanceOf(UrlInformation::class), 65 | $this->equalTo($accepted), 66 | $this->equalTo('default')) 67 | ->will($this->returnCallback(function (UrlInformation $info) { 68 | $info->setLocation('http://symfony.com'); 69 | })) 70 | ; 71 | 72 | $this->provider = new UrlInformationProvider($loader, $voter, $guesser); 73 | } 74 | 75 | public function testProvider() 76 | { 77 | $urlInformations = $this->provider->getUrlInformation(); 78 | $this->assertCount(1, $urlInformations); 79 | $urlInformation = reset($urlInformations); 80 | $this->assertInstanceof('Symfony\Cmf\Bundle\SeoBundle\Model\UrlInformation', $urlInformation); 81 | $this->assertEquals('http://symfony.com', $urlInformation->getLocation()); 82 | } 83 | } 84 | 85 | class TestModel 86 | { 87 | public $name; 88 | 89 | public function __construct($name) 90 | { 91 | $this->name = $name; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |