├── .github
├── CODEOWNERS
├── dependabot.yml
└── workflows
│ └── build.yml
├── LICENSE
├── UPGRADE-3.0.md
├── bin
└── create_node_symlink.php
├── composer.json
├── docker-compose.yml
├── ecs.php
├── phpstan.neon.dist
├── psalm.xml
└── src
├── Builder
├── BuilderInterface.php
├── SitemapBuilder.php
├── SitemapBuilderInterface.php
├── SitemapIndexBuilder.php
└── SitemapIndexBuilderInterface.php
├── Command
└── GenerateSitemapCommand.php
├── Controller
├── AbstractController.php
├── SitemapController.php
└── SitemapIndexController.php
├── DependencyInjection
├── Compiler
│ └── SitemapProviderPass.php
├── Configuration.php
└── SitemapExtension.php
├── Exception
├── RouteExistsException.php
└── SitemapUrlNotFoundException.php
├── Factory
├── AlternativeUrlFactory.php
├── AlternativeUrlFactoryInterface.php
├── ImageFactory.php
├── ImageFactoryInterface.php
├── IndexUrlFactory.php
├── IndexUrlFactoryInterface.php
├── SitemapFactory.php
├── SitemapFactoryInterface.php
├── SitemapIndexFactory.php
├── SitemapIndexFactoryInterface.php
├── UrlFactory.php
└── UrlFactoryInterface.php
├── Filesystem
├── Reader.php
└── Writer.php
├── Generator
├── ProductImagesToSitemapImagesCollectionGenerator.php
└── ProductImagesToSitemapImagesCollectionGeneratorInterface.php
├── Model
├── AlternativeUrl.php
├── AlternativeUrlInterface.php
├── ChangeFrequency.php
├── Image.php
├── ImageInterface.php
├── IndexUrl.php
├── IndexUrlInterface.php
├── Sitemap.php
├── SitemapIndex.php
├── SitemapInterface.php
├── Url.php
└── UrlInterface.php
├── Provider
├── Data
│ ├── DataProviderInterface.php
│ ├── ProductDataProvider.php
│ ├── ProductDataProviderInterface.php
│ ├── TaxonDataProvider.php
│ └── TaxonDataProviderInterface.php
├── IndexUrlProvider.php
├── IndexUrlProviderInterface.php
├── ProductUrlProvider.php
├── StaticUrlProvider.php
├── TaxonUrlProvider.php
└── UrlProviderInterface.php
├── Renderer
├── RendererAdapterInterface.php
├── SitemapRenderer.php
├── SitemapRendererInterface.php
└── TwigAdapter.php
├── Resources
├── config
│ ├── config.yaml
│ ├── routing.yml
│ ├── services.xml
│ └── services
│ │ ├── providers
│ │ ├── products.xml
│ │ ├── static.xml
│ │ └── taxons.xml
│ │ └── sitemap.xml
└── views
│ ├── Macro
│ ├── language.html.twig
│ └── xml.html.twig
│ ├── index.xml.twig
│ └── show.xml.twig
├── Routing
└── SitemapLoader.php
└── SitemapPlugin.php
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @stefandoorn
2 |
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: composer
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "04:00"
8 | open-pull-requests-limit: 10
9 | allow:
10 | - dependency-type: direct
11 | - dependency-type: indirect
12 | ignore:
13 | - dependency-name: sylius-labs/coding-standard
14 | versions:
15 | - 4.0.2
16 | - dependency-name: symfony/intl
17 | versions:
18 | - 5.2.3
19 | - dependency-name: phpunit/phpunit
20 | versions:
21 | - 9.5.1
22 | - dependency-name: phpstan/phpstan-shim
23 | versions:
24 | - 0.12.0
25 | - dependency-name: lchrusciel/api-test-case
26 | versions:
27 | - 5.0.0
28 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches-ignore:
6 | - 'dependabot/**'
7 | pull_request: ~
8 | release:
9 | types: [created]
10 | schedule:
11 | -
12 | cron: "0 1 * * 6" # Run at 1am every Saturday
13 | workflow_dispatch: ~
14 |
15 | jobs:
16 | tests:
17 | runs-on: ubuntu-latest
18 |
19 | name: "Sylius ${{ matrix.sylius }}, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}"
20 |
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | php: ["8.2", "8.3"]
25 | symfony: ["^6.4", "^7.2"]
26 | sylius: ["~2.0.0", "~2.1.0"]
27 | node: ["18.x"]
28 | mysql: ["8.0"]
29 |
30 | env:
31 | APP_ENV: test
32 | DATABASE_URL: "mysql://root:root@127.0.0.1/sylius?serverVersion=${{ matrix.mysql }}"
33 |
34 | steps:
35 | -
36 | uses: actions/checkout@v4
37 |
38 | -
39 | name: Setup PHP
40 | uses: shivammathur/setup-php@v2
41 | with:
42 | php-version: "${{ matrix.php }}"
43 | extensions: intl
44 | tools: symfony
45 | coverage: none
46 |
47 | -
48 | name: Shutdown default MySQL
49 | run: sudo service mysql stop
50 |
51 | -
52 | name: Setup MySQL
53 | uses: mirromutth/mysql-action@v1.1
54 | with:
55 | mysql version: "${{ matrix.mysql }}"
56 | mysql root password: "root"
57 |
58 | -
59 | name: Output PHP version for Symfony CLI
60 | run: php -v | head -n 1 | awk '{ print $2 }' > .php-version
61 |
62 | -
63 | name: Install certificates
64 | run: symfony server:ca:install
65 |
66 | -
67 | name: Run webserver
68 | run: (cd tests/Application && symfony server:start --port=8080 --dir=public --daemon)
69 |
70 | -
71 | name: Get Composer cache directory
72 | id: composer-cache
73 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
74 |
75 | -
76 | name: Cache Composer
77 | uses: actions/cache@v4
78 | with:
79 | path: ${{ steps.composer-cache.outputs.dir }}
80 | key: ${{ runner.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-sylius-${{ matrix.sylius }}-composer-${{ hashFiles('**/composer.json') }}
81 | restore-keys: |
82 | ${{ runner.os }}-php-${{ matrix.php }}-composer-
83 |
84 | -
85 | name: Allow Composer plugins
86 | run: |
87 | composer global config --no-plugins allow-plugins.symfony/flex true
88 |
89 | -
90 | name: Restrict Symfony version
91 | if: matrix.symfony != ''
92 | run: |
93 | composer global require --no-progress --no-scripts --no-plugins "symfony/flex:^1.10"
94 | composer config extra.symfony.require "${{ matrix.symfony }}"
95 |
96 | -
97 | name: Restrict Sylius version
98 | if: matrix.sylius != ''
99 | run: composer require "sylius/sylius:${{ matrix.sylius }}" --no-update --no-scripts --no-interaction
100 |
101 | -
102 | name: Install PHP dependencies
103 | run: composer install --no-interaction
104 |
105 | -
106 | name: Prepare test application database
107 | run: |
108 | (cd tests/Application && bin/console doctrine:database:create -vvv)
109 | (cd tests/Application && bin/console doctrine:schema:create -vvv)
110 |
111 | -
112 | name: Prepare test application cache
113 | run: (cd tests/Application && bin/console cache:warmup -vvv)
114 |
115 | -
116 | name: Load fixtures in test application
117 | run: (cd tests/Application && bin/console sylius:fixtures:load -n)
118 |
119 | -
120 | name: Validate composer.json
121 | run: composer validate --ansi --strict
122 |
123 | -
124 | name: Run security check
125 | run: symfony security:check
126 |
127 | -
128 | name: Check coding standard
129 | run: composer check-style
130 |
131 | -
132 | name: Run PHPStan
133 | run: composer analyse || true
134 |
135 | -
136 | name: Run tests
137 | run: composer test
138 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Stefan Doorn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/UPGRADE-3.0.md:
--------------------------------------------------------------------------------
1 | # Upgrade 2.x to 3.0
2 |
3 | ## Upgrade
4 |
5 | Upgrading might be as simple as running the following command:
6 |
7 | ```bash
8 | $ composer require stefandoorn/sitemap-plugin:^3.0
9 | ```
10 |
11 | Keep reading to understand the main changes that happened as part of the 3.0 release.
12 |
13 | ## Main changes
14 |
15 | ### Sylius
16 |
17 | The plugin has been upgraded to work with Sylius ^2.0.
18 |
19 | Also, the testing structure has been updated as much possible to reflect `PluginSkeleton^2.0`.
20 |
21 | ### PHP
22 |
23 | Sylius 2.0 requires a minimum of PHP 8.2, and the plugin has been updated similarly.
24 |
25 | ### Filesystem
26 |
27 | Since Nov, 2022 Sylius uses Flysystem as it's default filesystem implementation.
28 |
29 | From Sylius 2.0, this driver has become the default.
30 |
31 | The plugin has been updated to use Flysystem.
32 |
33 | If you did make configuration changes, have a look at `src/Resources/config/config.yaml` for the new configuration.
34 |
35 | #### Breaking change
36 |
37 | `Filesystem/Reader::has` has been removed, as we can rely on Flysystem exceptions now.
38 |
39 | As a side benefit, this also saves an I/O operation.
40 |
41 | `AbstractController::$reader` has been made `private`.
42 |
43 | ### Data providers (potential breaking change)
44 |
45 | Both the `product` & `taxon` URL provider have been changed. The data fetching part of them has been extracted
46 | into separate services.
47 |
48 | This change should make it easier for you to adjust only the data fetching, and not adjust the actual URL provider as well.
49 |
50 | In case you have adjusted these providers, this might incur a breaking change for you. Please do review your implementation.
51 |
52 |
--------------------------------------------------------------------------------
/bin/create_node_symlink.php:
--------------------------------------------------------------------------------
1 | `' . NODE_MODULES_FOLDER_NAME . '` already exists as a link or folder, keeping existing as may be intentional.' . PHP_EOL;
11 | exit(0);
12 | } else {
13 | echo '> Invalid symlink `' . NODE_MODULES_FOLDER_NAME . '` detected, recreating...' . PHP_EOL;
14 | if (!@unlink(NODE_MODULES_FOLDER_NAME)) {
15 | echo '> Could not delete file `' . NODE_MODULES_FOLDER_NAME . '`.' . PHP_EOL;
16 | exit(1);
17 | }
18 | }
19 | }
20 |
21 | /* try to create the symlink using PHP internals... */
22 | $success = @symlink(PATH_TO_NODE_MODULES, NODE_MODULES_FOLDER_NAME);
23 |
24 | /* if case it has failed, but OS is Windows... */
25 | if (!$success && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
26 | /* ...then try a different approach which does not require elevated permissions and folder to exist */
27 | echo '> This system is running Windows, creation of links requires elevated privileges,' . PHP_EOL;
28 | echo '> and target path to exist. Fallback to NTFS Junction:' . PHP_EOL;
29 | exec(sprintf('mklink /J %s %s 2> NUL', NODE_MODULES_FOLDER_NAME, PATH_TO_NODE_MODULES), $output, $returnCode);
30 | $success = $returnCode === 0;
31 | if (!$success) {
32 | echo '> Failed o create the required symlink' . PHP_EOL;
33 | exit(2);
34 | }
35 | }
36 |
37 | $path = @readlink(NODE_MODULES_FOLDER_NAME);
38 | /* check if link points to the intended directory */
39 | if ($path && realpath($path) === realpath(PATH_TO_NODE_MODULES)) {
40 | echo '> Successfully created the symlink.' . PHP_EOL;
41 | exit(0);
42 | }
43 |
44 | echo '> Failed to create the symlink to `' . NODE_MODULES_FOLDER_NAME . '`.' . PHP_EOL;
45 | exit(3);
46 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stefandoorn/sitemap-plugin",
3 | "type": "sylius-plugin",
4 | "description": "Sitemap Plugin for Sylius",
5 | "keywords": [
6 | "sylius",
7 | "sylius-plugin"
8 | ],
9 | "license": "MIT",
10 | "require": {
11 | "php": "^8.2",
12 | "league/flysystem-bundle": "^3.0",
13 | "sylius/sylius": "^2.0"
14 | },
15 | "require-dev": {
16 | "lchrusciel/api-test-case": "^5.1",
17 | "matthiasnoback/symfony-dependency-injection-test": "^6.0",
18 | "nyholm/psr7": "^1.8",
19 | "phpstan/extension-installer": "^1.0",
20 | "phpstan/phpstan": "^2.0",
21 | "phpstan/phpstan-doctrine": "^2.0",
22 | "phpstan/phpstan-strict-rules": "^2.0",
23 | "phpstan/phpstan-symfony": "^2.0",
24 | "phpstan/phpstan-webmozart-assert": "^2.0",
25 | "phpunit/phpunit": "^10.0",
26 | "sylius-labs/coding-standard": "^4.0",
27 | "symfony/browser-kit": "^6.4 || ^7.1",
28 | "symfony/debug-bundle": "^6.4 || ^7.1",
29 | "symfony/dotenv": "^6.4 || ^7.1",
30 | "symfony/intl": "^6.4 || ^7.1",
31 | "symfony/runtime": "^6.4 || ^7.0",
32 | "symfony/ux-icons": "^2.22",
33 | "symfony/web-profiler-bundle": "^6.4 || ^7.1",
34 | "symfony/webpack-encore-bundle": "^1.15 || ^2.2"
35 | },
36 | "config": {
37 | "sort-packages": true,
38 | "bin-dir": "bin",
39 | "allow-plugins": {
40 | "dealerdirect/phpcodesniffer-composer-installer": true,
41 | "symfony/thanks": true,
42 | "phpstan/extension-installer": true,
43 | "symfony/runtime": true,
44 | "php-http/discovery": true
45 | }
46 | },
47 | "extra": {
48 | "branch-alias": {
49 | "dev-master": "3.0-dev"
50 | }
51 | },
52 | "autoload": {
53 | "psr-4": {
54 | "SitemapPlugin\\": "src/",
55 | "Tests\\SitemapPlugin\\": ["tests/", "tests/Application/src"]
56 | }
57 | },
58 | "autoload-dev": {
59 | "classmap": [
60 | "tests/Application/Kernel.php"
61 | ]
62 | },
63 | "scripts": {
64 | "post-install-cmd": [
65 | "php bin/create_node_symlink.php"
66 | ],
67 | "post-update-cmd": [
68 | "php bin/create_node_symlink.php"
69 | ],
70 | "post-create-project-cmd": [
71 | "php bin/create_node_symlink.php"
72 | ],
73 | "analyse": "bin/phpstan analyse",
74 | "check-style": "bin/ecs check --ansi src/ tests/",
75 | "fix-style": "ecs check --ansi src/ tests/ --fix",
76 | "phpunit": "bin/phpunit",
77 | "test": [
78 | "@phpunit"
79 | ]
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | db:
4 | image: mariadb:10.6
5 | environment:
6 | - MYSQL_DATABASE=sylius
7 | - MYSQL_ROOT_PASSWORD=root
8 | - MYSQL_USER=root
9 | - MYSQL_PASSWORD=root
10 | - MYSQL_ROOT_HOST=%
11 | command: --sql_mode=""
12 | ports:
13 | - "3306:3306"
14 | volumes:
15 | - ./docker/volumes/mysql:/var/lib/mysql
16 |
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | import('vendor/sylius-labs/coding-standard/ecs.php');
10 |
11 | $config->paths([
12 | __DIR__ . '/spec',
13 | __DIR__ . '/src',
14 | __DIR__ . '/tests',
15 | 'ecs.php',
16 | ]);
17 |
18 | $config->skip([
19 | VisibilityRequiredFixer::class => ['*Spec.php'],
20 | 'tests/Application/*',
21 | ]);
22 |
23 | $config->ruleWithConfiguration(
24 | NativeFunctionInvocationFixer::class,
25 | ['include' => ['@all'], 'scope' => 'all', 'strict' => \true]);
26 |
27 | $config->ruleWithConfiguration(
28 | TrailingCommaInMultilineFixer::class,
29 | ['elements' => ['arguments', 'array_destructuring', 'arrays', 'match', 'parameters']]);
30 | };
31 |
--------------------------------------------------------------------------------
/phpstan.neon.dist:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 4
3 |
4 | reportUnmatchedIgnoredErrors: true
5 |
6 | paths:
7 | - src
8 |
9 | excludePaths:
10 | # Makes PHPStan crash
11 | - 'src/DependencyInjection/Configuration.php'
12 |
13 | # Test dependencies
14 | - 'tests/Application/app/**.php'
15 | - 'tests/Application/src/**.php'
16 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------
/src/Builder/BuilderInterface.php:
--------------------------------------------------------------------------------
1 | providers[] = $provider;
24 | }
25 |
26 | public function getProviders(): iterable
27 | {
28 | return $this->providers;
29 | }
30 |
31 | public function build(UrlProviderInterface $provider, ChannelInterface $channel): SitemapInterface
32 | {
33 | $urls = [];
34 |
35 | $sitemap = $this->sitemapFactory->createNew();
36 | $urls[] = [...$provider->generate($channel)];
37 |
38 | $sitemap->setUrls(\array_merge(...$urls));
39 |
40 | return $sitemap;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Builder/SitemapBuilderInterface.php:
--------------------------------------------------------------------------------
1 | providers[] = $provider;
27 | }
28 |
29 | public function addIndexProvider(IndexUrlProviderInterface $indexProvider): void
30 | {
31 | foreach ($this->providers as $provider) {
32 | $indexProvider->addProvider($provider);
33 | }
34 |
35 | $this->indexProviders[] = $indexProvider;
36 | }
37 |
38 | public function build(): SitemapInterface
39 | {
40 | $sitemap = $this->sitemapIndexFactory->createNew();
41 | $urls = [];
42 |
43 | foreach ($this->indexProviders as $indexProvider) {
44 | $urls[] = [...$indexProvider->generate()];
45 | }
46 |
47 | $sitemap->setUrls(\array_merge(...$urls));
48 |
49 | return $sitemap;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Builder/SitemapIndexBuilderInterface.php:
--------------------------------------------------------------------------------
1 | addOption('channel', 'c', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Channel codes to generate. If none supplied, all channels will generated.');
36 | }
37 |
38 | protected function execute(InputInterface $input, OutputInterface $output): int
39 | {
40 | foreach ($this->channels($input) as $channel) {
41 | $this->executeChannel($channel, $output);
42 | }
43 |
44 | return 0;
45 | }
46 |
47 | private function executeChannel(ChannelInterface $channel, OutputInterface $output): void
48 | {
49 | $output->writeln(\sprintf('Start generating sitemaps for channel "%s"', $channel->getName()));
50 |
51 | $this->router->getContext()->setHost($channel->getHostname() ?? 'localhost');
52 | // TODO make sure providers are every time emptied (reset call or smth?)
53 | foreach ($this->sitemapBuilder->getProviders() as $provider) {
54 | $output->writeln(\sprintf('Start generating sitemap "%s" for channel "%s"', $provider->getName(), $channel->getCode()));
55 |
56 | $sitemap = $this->sitemapBuilder->build($provider, $channel); // TODO use provider instance, not the name
57 | $xml = $this->sitemapRenderer->render($sitemap);
58 | $path = $this->path($channel, \sprintf('%s.xml', $provider->getName()));
59 |
60 | $this->writer->write(
61 | $path,
62 | $xml,
63 | );
64 |
65 | $output->writeln(\sprintf('Finished generating sitemap "%s" for channel "%s" at path "%s"', $provider->getName(), $channel->getCode(), $path));
66 | }
67 |
68 | $output->writeln(\sprintf('Start generating sitemap index for channel "%s"', $channel->getCode()));
69 |
70 | $sitemap = $this->sitemapIndexBuilder->build();
71 | $xml = $this->sitemapIndexRenderer->render($sitemap);
72 | $path = $this->path($channel, 'sitemap_index.xml');
73 |
74 | $this->writer->write(
75 | $path,
76 | $xml,
77 | );
78 |
79 | $output->writeln(\sprintf('Finished generating sitemap index for channel "%s" at path "%s"', $channel->getCode(), $path));
80 | }
81 |
82 | private function path(ChannelInterface $channel, string $path): string
83 | {
84 | return \sprintf('%s/%s', $channel->getCode(), $path);
85 | }
86 |
87 | /**
88 | * @return iterable
89 | */
90 | private function channels(InputInterface $input): iterable
91 | {
92 | /** @var iterable $channels */
93 | $channels = self::hasChannelInput($input)
94 | ? $this->channelRepository->findBy(['code' => $input->getOption('channel'), 'enabled' => true])
95 | : $this->channelRepository->findBy(['enabled' => true]);
96 |
97 | return $channels;
98 | }
99 |
100 | private static function hasChannelInput(InputInterface $input): bool
101 | {
102 | $inputValue = $input->getOption('channel');
103 |
104 | if (\is_array($inputValue) && 0 === \count($inputValue)) {
105 | return false;
106 | }
107 |
108 | return null !== $inputValue;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Controller/AbstractController.php:
--------------------------------------------------------------------------------
1 | reader->getStream($path);
25 |
26 | while (!\feof($handle)) {
27 | echo \fread($handle, 8192);
28 | }
29 | } catch (UnableToReadFile | FilesystemException) {
30 | throw new NotFoundHttpException(\sprintf('File "%s" not found', $path));
31 | }
32 | });
33 | $response->headers->set('Content-Type', 'application/xml');
34 |
35 | return $response;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Controller/SitemapController.php:
--------------------------------------------------------------------------------
1 | channelContext->getChannel()->getCode(), \sprintf('%s.xml', $name));
23 |
24 | return $this->createResponse($path);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Controller/SitemapIndexController.php:
--------------------------------------------------------------------------------
1 | channelContext->getChannel()->getCode(), 'sitemap_index.xml');
23 |
24 | return $this->createResponse($path);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Compiler/SitemapProviderPass.php:
--------------------------------------------------------------------------------
1 | has('sylius.sitemap_builder')) {
16 | return;
17 | }
18 |
19 | $builderDefinition = $container->findDefinition('sylius.sitemap_builder');
20 | $builderIndexDefinition = $container->findDefinition('sylius.sitemap_index_builder');
21 | $taggedProviders = $container->findTaggedServiceIds('sylius.sitemap_provider');
22 |
23 | foreach ($taggedProviders as $id => $tags) {
24 | $builderIndexDefinition->addMethodCall('addProvider', [(new Reference($id))]);
25 | $builderDefinition->addMethodCall('addProvider', [(new Reference($id))]);
26 | }
27 |
28 | $taggedProvidersIndex = $container->findTaggedServiceIds('sylius.sitemap_index_provider');
29 | foreach ($taggedProvidersIndex as $id => $tags) {
30 | $builderIndexDefinition->addMethodCall('addIndexProvider', [new Reference($id)]);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | getRootNode();
16 |
17 | $rootNode
18 | ->children()
19 | ->arrayNode('providers')
20 | ->addDefaultsIfNotSet()
21 | ->children()
22 | ->booleanNode('products')->defaultTrue()->end()
23 | ->booleanNode('taxons')->defaultTrue()->end()
24 | ->booleanNode('static')->defaultTrue()->end()
25 | ->end()
26 | ->end()
27 | ->scalarNode('template')
28 | ->defaultValue('@SitemapPlugin/show.xml.twig')
29 | ->end()
30 | ->scalarNode('index_template')
31 | ->defaultValue('@SitemapPlugin/index.xml.twig')
32 | ->end()
33 | ->scalarNode('exclude_taxon_root')
34 | ->info('Often you don\'t want to include the root of your taxon tree as it has a generic name as \'products\'.')
35 | ->defaultTrue()
36 | ->end()
37 | ->scalarNode('hreflang')
38 | ->info('Whether to generate alternative URL versions for each locale. Defaults to true. Background: https://support.google.com/webmasters/answer/189077?hl=en.')
39 | ->defaultTrue()
40 | ->end()
41 | ->scalarNode('images')
42 | ->info('Add images to URL output in case the provider adds them. Defaults to true. Background: https://support.google.com/webmasters/answer/178636?hl=en')
43 | ->defaultTrue()
44 | ->end()
45 | ->arrayNode('static_routes')
46 | ->beforeNormalization()->castToArray()->end()
47 | ->info('In case you want to add static routes to your sitemap (e.g. homepage), configure them here. Defaults to homepage & contact page.')
48 | ->prototype('array')
49 | ->children()
50 | ->scalarNode('route')
51 | ->info('Name of route')
52 | ->isRequired()
53 | ->cannotBeEmpty()
54 | ->end()
55 | ->arrayNode('parameters')
56 | ->prototype('variable')->end()
57 | ->info('Add optional parameters to the route.')
58 | ->end()
59 | ->arrayNode('locales')
60 | ->prototype('scalar')
61 | ->info('Define which locales to add. If empty, it uses the default locales for channel context supplied')
62 | ->end()
63 | ->end()
64 | ->end()
65 | ->end()
66 | ->end()
67 | ;
68 |
69 | return $treeBuilder;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/DependencyInjection/SitemapExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration($this->getConfiguration([], $container), $configs);
18 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
19 | $loader->load('services.xml');
20 |
21 | $container->setParameter('sylius.sitemap_template', $config['template']);
22 | $container->setParameter('sylius.sitemap_index_template', $config['index_template']);
23 | $container->setParameter('sylius.sitemap_exclude_taxon_root', $config['exclude_taxon_root']);
24 | $container->setParameter('sylius.sitemap_hreflang', $config['hreflang']);
25 | $container->setParameter('sylius.sitemap_static', $config['static_routes']);
26 | $container->setParameter('sylius.sitemap_images', $config['images']);
27 |
28 | foreach ($config['providers'] as $provider => $setting) {
29 | $parameter = \sprintf('sylius.provider.%s', $provider);
30 | $container->setParameter($parameter, $setting);
31 |
32 | if ($setting === true) {
33 | $loader->load(\sprintf('services/providers/%s.xml', $provider));
34 | }
35 | }
36 | }
37 |
38 | public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface
39 | {
40 | return new Configuration();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Exception/RouteExistsException.php:
--------------------------------------------------------------------------------
1 | getLocation()), 0, $previousException);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Factory/AlternativeUrlFactory.php:
--------------------------------------------------------------------------------
1 | filesystem->readStream($path);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Filesystem/Writer.php:
--------------------------------------------------------------------------------
1 | filesystem->write($path, $contents);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Generator/ProductImagesToSitemapImagesCollectionGenerator.php:
--------------------------------------------------------------------------------
1 | imagePreset = $imagePreset;
25 | }
26 | }
27 |
28 | public function generate(ProductInterface $product): Collection
29 | {
30 | $images = new ArrayCollection();
31 |
32 | /** @var ProductImageInterface $image */
33 | foreach ($product->getImages() as $image) {
34 | $path = $image->getPath();
35 |
36 | if (null === $path) {
37 | continue;
38 | }
39 |
40 | $sitemapImage = $this->sitemapImageUrlFactory->createNew($this->imagineCacheManager->getBrowserPath($path, $this->imagePreset));
41 |
42 | /**
43 | * @psalm-suppress InvalidArgument
44 | */
45 | $images->add($sitemapImage);
46 | }
47 |
48 | return $images;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Generator/ProductImagesToSitemapImagesCollectionGeneratorInterface.php:
--------------------------------------------------------------------------------
1 | setLocation($location);
14 | $this->setLocale($locale);
15 | }
16 |
17 | public function getLocation(): string
18 | {
19 | return $this->location;
20 | }
21 |
22 | public function setLocation(string $location): void
23 | {
24 | $this->location = $location;
25 | }
26 |
27 | public function getLocale(): string
28 | {
29 | return $this->locale;
30 | }
31 |
32 | public function setLocale(string $locale): void
33 | {
34 | $this->locale = $locale;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Model/AlternativeUrlInterface.php:
--------------------------------------------------------------------------------
1 | changeFrequency;
16 | }
17 |
18 | public static function always(): self
19 | {
20 | return new self('always');
21 | }
22 |
23 | public static function hourly(): self
24 | {
25 | return new self('hourly');
26 | }
27 |
28 | public static function daily(): self
29 | {
30 | return new self('daily');
31 | }
32 |
33 | public static function weekly(): self
34 | {
35 | return new self('weekly');
36 | }
37 |
38 | public static function monthly(): self
39 | {
40 | return new self('monthly');
41 | }
42 |
43 | public static function yearly(): self
44 | {
45 | return new self('yearly');
46 | }
47 |
48 | public static function never(): self
49 | {
50 | return new self('never');
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Model/Image.php:
--------------------------------------------------------------------------------
1 | location;
24 | }
25 |
26 | public function setLocation(string $location): void
27 | {
28 | $this->location = $location;
29 | }
30 |
31 | public function getTitle(): ?string
32 | {
33 | return $this->title;
34 | }
35 |
36 | public function setTitle(string $title): void
37 | {
38 | $this->title = $title;
39 | }
40 |
41 | public function getCaption(): ?string
42 | {
43 | return $this->caption;
44 | }
45 |
46 | public function setCaption(string $caption): void
47 | {
48 | $this->caption = $caption;
49 | }
50 |
51 | public function getGeoLocation(): ?string
52 | {
53 | return $this->geoLocation;
54 | }
55 |
56 | public function setGeoLocation(string $geoLocation): void
57 | {
58 | $this->geoLocation = $geoLocation;
59 | }
60 |
61 | public function getLicense(): ?string
62 | {
63 | return $this->license;
64 | }
65 |
66 | public function setLicense(string $license): void
67 | {
68 | $this->license = $license;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Model/ImageInterface.php:
--------------------------------------------------------------------------------
1 | location;
20 | }
21 |
22 | public function setLocation(string $location): void
23 | {
24 | $this->location = $location;
25 | }
26 |
27 | public function getLastModification(): ?DateTimeInterface
28 | {
29 | return $this->lastModification;
30 | }
31 |
32 | public function setLastModification(?DateTimeInterface $lastModification): void
33 | {
34 | $this->lastModification = $lastModification;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Model/IndexUrlInterface.php:
--------------------------------------------------------------------------------
1 | urls = $urls;
21 | }
22 |
23 | public function getUrls(): iterable
24 | {
25 | return $this->urls;
26 | }
27 |
28 | public function addUrl(UrlInterface $url): void
29 | {
30 | $this->urls[] = $url;
31 | }
32 |
33 | public function removeUrl(UrlInterface $url): void
34 | {
35 | $key = \array_search($url, $this->urls, true);
36 | if (false === $key) {
37 | throw new SitemapUrlNotFoundException($url);
38 | }
39 |
40 | unset($this->urls[$key]);
41 | }
42 |
43 | public function setLocalization(string $localization): void
44 | {
45 | $this->localization = $localization;
46 | }
47 |
48 | public function getLocalization(): ?string
49 | {
50 | return $this->localization;
51 | }
52 |
53 | public function setLastModification(DateTimeInterface $lastModification): void
54 | {
55 | $this->lastModification = $lastModification;
56 | }
57 |
58 | public function getLastModification(): ?DateTimeInterface
59 | {
60 | return $this->lastModification;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Model/SitemapIndex.php:
--------------------------------------------------------------------------------
1 | urls = $urls;
21 | }
22 |
23 | public function getUrls(): iterable
24 | {
25 | return $this->urls;
26 | }
27 |
28 | public function addUrl(UrlInterface $url): void
29 | {
30 | $this->urls[] = $url;
31 | }
32 |
33 | public function removeUrl(UrlInterface $url): void
34 | {
35 | $key = \array_search($url, $this->urls, true);
36 | if (false === $key) {
37 | throw new SitemapUrlNotFoundException($url);
38 | }
39 |
40 | unset($this->urls[$key]);
41 | }
42 |
43 | public function setLocalization(string $localization): void
44 | {
45 | $this->localization = $localization;
46 | }
47 |
48 | public function getLocalization(): string
49 | {
50 | return $this->localization;
51 | }
52 |
53 | public function setLastModification(DateTimeInterface $lastModification): void
54 | {
55 | $this->lastModification = $lastModification;
56 | }
57 |
58 | public function getLastModification(): DateTimeInterface
59 | {
60 | return $this->lastModification;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Model/SitemapInterface.php:
--------------------------------------------------------------------------------
1 | alternatives = new ArrayCollection();
28 | $this->images = new ArrayCollection();
29 | }
30 |
31 | public function getLocation(): string
32 | {
33 | return $this->location;
34 | }
35 |
36 | public function setLocation(string $location): void
37 | {
38 | $this->location = $location;
39 | }
40 |
41 | public function getLastModification(): ?DateTimeInterface
42 | {
43 | return $this->lastModification;
44 | }
45 |
46 | public function setLastModification(DateTimeInterface $lastModification): void
47 | {
48 | $this->lastModification = $lastModification;
49 | }
50 |
51 | public function getChangeFrequency(): ?string
52 | {
53 | return $this->changeFrequency;
54 | }
55 |
56 | public function setChangeFrequency(ChangeFrequency $changeFrequency): void
57 | {
58 | $this->changeFrequency = (string) $changeFrequency;
59 | }
60 |
61 | public function getPriority(): ?float
62 | {
63 | return $this->priority;
64 | }
65 |
66 | public function setPriority(float $priority): void
67 | {
68 | if (0 > $priority || 1 < $priority) {
69 | throw new \InvalidArgumentException(\sprintf(
70 | 'The value %s is not supported by the option priority, it must be a number between 0.0 and 1.0.',
71 | $priority,
72 | ));
73 | }
74 |
75 | $this->priority = $priority;
76 | }
77 |
78 | public function getAlternatives(): Collection
79 | {
80 | return $this->alternatives;
81 | }
82 |
83 | public function setAlternatives(iterable $alternatives): void
84 | {
85 | $this->alternatives->clear();
86 |
87 | foreach ($alternatives as $alternative) {
88 | $this->addAlternative($alternative);
89 | }
90 | }
91 |
92 | public function addAlternative(AlternativeUrlInterface $alternative): void
93 | {
94 | $this->alternatives->add($alternative);
95 | }
96 |
97 | public function hasAlternative(AlternativeUrlInterface $alternative): bool
98 | {
99 | return $this->alternatives->contains($alternative);
100 | }
101 |
102 | public function removeAlternative(AlternativeUrlInterface $alternative): void
103 | {
104 | if ($this->hasAlternative($alternative)) {
105 | $this->alternatives->removeElement($alternative);
106 | }
107 | }
108 |
109 | public function hasAlternatives(): bool
110 | {
111 | return !$this->alternatives->isEmpty();
112 | }
113 |
114 | public function getImages(): Collection
115 | {
116 | return $this->images;
117 | }
118 |
119 | public function setImages(iterable $images): void
120 | {
121 | $this->images->clear();
122 |
123 | foreach ($images as $image) {
124 | $this->addImage($image);
125 | }
126 | }
127 |
128 | public function addImage(ImageInterface $image): void
129 | {
130 | $this->images->add($image);
131 | }
132 |
133 | public function hasImage(ImageInterface $image): bool
134 | {
135 | return $this->images->contains($image);
136 | }
137 |
138 | public function removeImage(ImageInterface $image): void
139 | {
140 | if ($this->hasImage($image)) {
141 | $this->images->removeElement($image);
142 | }
143 | }
144 |
145 | public function hasImages(): bool
146 | {
147 | return !$this->images->isEmpty();
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/Model/UrlInterface.php:
--------------------------------------------------------------------------------
1 | repository->createQueryBuilder('o')
24 | ->addSelect('translation')
25 | ->innerJoin('o.translations', 'translation')
26 | ->andWhere(':channel MEMBER OF o.channels')
27 | ->andWhere('o.enabled = :enabled')
28 | ->setParameter('channel', $channel)
29 | ->setParameter('enabled', true)
30 | ->getQuery()
31 | ->getResult()
32 | ;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Provider/Data/ProductDataProviderInterface.php:
--------------------------------------------------------------------------------
1 | repository->findBy(['enabled' => true]);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Provider/Data/TaxonDataProviderInterface.php:
--------------------------------------------------------------------------------
1 | providers[] = $provider;
24 | }
25 |
26 | public function generate(): iterable
27 | {
28 | $urls = [];
29 | foreach ($this->providers as $provider) {
30 | $location = $this->router->generate('sylius_sitemap_' . $provider->getName());
31 | $urls[] = $this->sitemapIndexUrlFactory->createNew($location);
32 | }
33 |
34 | return $urls;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Provider/IndexUrlProviderInterface.php:
--------------------------------------------------------------------------------
1 | */
27 | private array $channelLocaleCodes;
28 |
29 | public function __construct(
30 | private readonly ProductDataProviderInterface $dataProvider,
31 | private readonly RouterInterface $router,
32 | private readonly UrlFactoryInterface $urlFactory,
33 | private readonly AlternativeUrlFactoryInterface $urlAlternativeFactory,
34 | private readonly LocaleContextInterface $localeContext,
35 | private readonly ProductImagesToSitemapImagesCollectionGeneratorInterface $productToImageSitemapArrayGenerator,
36 | ) {
37 | }
38 |
39 | public function getName(): string
40 | {
41 | return 'products';
42 | }
43 |
44 | /**
45 | * @inheritdoc
46 | */
47 | public function generate(ChannelInterface $channel): iterable
48 | {
49 | $this->channel = $channel;
50 | $this->channelLocaleCodes = [];
51 |
52 | $urls = [];
53 | foreach ($this->dataProvider->get($channel) as $product) {
54 | $urls[] = $this->createProductUrl($product);
55 | }
56 |
57 | return $urls;
58 | }
59 |
60 | private function getTranslations(ProductInterface $product): Collection
61 | {
62 | return $product->getTranslations()->filter(function (TranslationInterface $translation): bool {
63 | return $this->localeInLocaleCodes($translation);
64 | });
65 | }
66 |
67 | private function localeInLocaleCodes(TranslationInterface $translation): bool
68 | {
69 | return \in_array($translation->getLocale(), $this->getLocaleCodes(), true);
70 | }
71 |
72 | private function getLocaleCodes(): array
73 | {
74 | if ($this->channelLocaleCodes === []) {
75 | $this->channelLocaleCodes = $this->channel->getLocales()->map(function (LocaleInterface $locale): ?string {
76 | return $locale->getCode();
77 | })->toArray();
78 | }
79 |
80 | return $this->channelLocaleCodes;
81 | }
82 |
83 | private function createProductUrl(ProductInterface $product): UrlInterface
84 | {
85 | $productUrl = $this->urlFactory->createNew(''); // todo bypassing this new constructor right now
86 | $productUrl->setChangeFrequency(ChangeFrequency::always());
87 | $productUrl->setPriority(0.5);
88 | $updatedAt = $product->getUpdatedAt();
89 | if ($updatedAt !== null) {
90 | $productUrl->setLastModification($updatedAt);
91 | }
92 | $productUrl->setImages($this->productToImageSitemapArrayGenerator->generate($product));
93 |
94 | /** @var ProductTranslationInterface $translation */
95 | foreach ($this->getTranslations($product) as $translation) {
96 | $locale = $translation->getLocale();
97 |
98 | if ($locale === null) {
99 | continue;
100 | }
101 |
102 | if (!$this->localeInLocaleCodes($translation)) {
103 | continue;
104 | }
105 |
106 | $location = $this->router->generate('sylius_shop_product_show', [
107 | 'slug' => $translation->getSlug(),
108 | '_locale' => $translation->getLocale(),
109 | ]);
110 |
111 | if ($locale === $this->localeContext->getLocaleCode()) {
112 | $productUrl->setLocation($location);
113 |
114 | continue;
115 | }
116 |
117 | $productUrl->addAlternative($this->urlAlternativeFactory->createNew($location, $locale));
118 | }
119 |
120 | return $productUrl;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/Provider/StaticUrlProvider.php:
--------------------------------------------------------------------------------
1 | */
22 | private readonly array $routes,
23 | ) {
24 | }
25 |
26 | public function getName(): string
27 | {
28 | return 'static';
29 | }
30 |
31 | public function generate(ChannelInterface $channel): iterable
32 | {
33 | $this->channel = $channel;
34 | $urls = [];
35 |
36 | if (0 === \count($this->routes)) {
37 | return $urls;
38 | }
39 |
40 | foreach ($this->transformAndYieldRoutes() as $route) {
41 | $location = $this->router->generate($route['route'], $route['parameters']);
42 |
43 | $staticUrl = $this->sitemapUrlFactory->createNew($location);
44 | $staticUrl->setChangeFrequency(ChangeFrequency::weekly());
45 | $staticUrl->setPriority(0.3);
46 |
47 | foreach ($route['locales'] as $alternativeLocaleCode) {
48 | $route['parameters']['_locale'] = $alternativeLocaleCode;
49 | $alternativeLocation = $this->router->generate($route['route'], $route['parameters']);
50 | $staticUrl->addAlternative($this->urlAlternativeFactory->createNew($alternativeLocation, $alternativeLocaleCode));
51 | }
52 |
53 | $urls[] = $staticUrl;
54 | }
55 |
56 | return $urls;
57 | }
58 |
59 | private function transformAndYieldRoutes(): \Generator
60 | {
61 | foreach ($this->routes as $route) {
62 | yield $this->transformRoute($route);
63 | }
64 | }
65 |
66 | private function transformRoute(array $route): array
67 | {
68 | // Add default locale to route if not set
69 | $route = $this->addDefaultRoute($route);
70 |
71 | // Populate locales array by other enabled locales for current channel if no locales are specified
72 | if (!isset($route['locales']) || 0 === \count($route['locales'])) {
73 | $route['locales'] = $this->getAlternativeLocales();
74 | }
75 |
76 | // Remove the locale that is on the main route from the alternatives to prevent duplicates
77 | $route = $this->excludeMainRouteLocaleFromAlternativeLocales($route);
78 |
79 | return $route;
80 | }
81 |
82 | private function addDefaultRoute(array $route): array
83 | {
84 | if (isset($route['parameters']['_locale'])) {
85 | return $route;
86 | }
87 |
88 | $defaultLocale = $this->channel->getDefaultLocale();
89 |
90 | if (null !== $defaultLocale) {
91 | $route['parameters']['_locale'] = $defaultLocale->getCode();
92 | }
93 |
94 | return $route;
95 | }
96 |
97 | private function excludeMainRouteLocaleFromAlternativeLocales(array $route): array
98 | {
99 | $locales = $route['locales'];
100 | $locale = $route['parameters']['_locale'];
101 |
102 | $key = \array_search($locale, $locales, true);
103 |
104 | if ($key !== false) {
105 | unset($route['locales'][$key]);
106 | }
107 |
108 | return $route;
109 | }
110 |
111 | /**
112 | * @return array
113 | */
114 | private function getAlternativeLocales(): array
115 | {
116 | $locales = [];
117 |
118 | foreach ($this->channel->getLocales() as $locale) {
119 | if ($locale === $this->channel->getDefaultLocale()) {
120 | continue;
121 | }
122 |
123 | $locales[] = $locale->getCode();
124 | }
125 |
126 | return $locales;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Provider/TaxonUrlProvider.php:
--------------------------------------------------------------------------------
1 | dataProvider->get($channel) as $taxon) {
39 | /** @var TaxonInterface $taxon */
40 | if ($this->excludeTaxonRoot && $taxon->isRoot()) {
41 | continue;
42 | }
43 |
44 | $taxonUrl = $this->sitemapUrlFactory->createNew(''); // todo bypassing this new constructor right now
45 | $taxonUrl->setChangeFrequency(ChangeFrequency::always());
46 | $taxonUrl->setPriority(0.5);
47 |
48 | /** @var TaxonTranslationInterface $translation */
49 | foreach ($taxon->getTranslations() as $translation) {
50 | $location = $this->router->generate('sylius_shop_product_index', [
51 | 'slug' => $translation->getSlug(),
52 | '_locale' => $translation->getLocale(),
53 | ]);
54 |
55 | if ($translation->getLocale() === $this->localeContext->getLocaleCode()) {
56 | $taxonUrl->setLocation($location);
57 |
58 | continue;
59 | }
60 |
61 | $locale = $translation->getLocale();
62 | if (null !== $locale) {
63 | $taxonUrl->addAlternative($this->urlAlternativeFactory->createNew($location, $locale));
64 | }
65 | }
66 |
67 | $urls[] = $taxonUrl;
68 | }
69 |
70 | return $urls;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Provider/UrlProviderInterface.php:
--------------------------------------------------------------------------------
1 | adapter->render($sitemap);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Renderer/SitemapRendererInterface.php:
--------------------------------------------------------------------------------
1 | twig->render($this->template, [
19 | 'url_set' => $sitemap->getUrls(),
20 | 'hreflang' => $this->hreflang,
21 | 'images' => $this->images,
22 | ]);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Resources/config/config.yaml:
--------------------------------------------------------------------------------
1 | sitemap:
2 | static_routes:
3 | - { route: sylius_shop_homepage }
4 | - { route: sylius_shop_contact_request }
5 |
6 | parameters:
7 | sylius.sitemap.path: "%kernel.project_dir%/var/sitemap"
8 |
9 | flysystem:
10 | storages:
11 | flysystem.storage.sylius_sitemap:
12 | adapter: 'local'
13 | options:
14 | directory: "%sylius.sitemap.path%"
15 | lazy_root_creation: true
16 |
--------------------------------------------------------------------------------
/src/Resources/config/routing.yml:
--------------------------------------------------------------------------------
1 | # Index file holding references to all generated sitemaps (per provider)
2 | sylius_sitemap_index:
3 | path: /sitemap_index.xml
4 | methods: [GET]
5 | defaults:
6 | _controller: sylius.controller.sitemap_index::showAction
7 |
8 | # Redirect always to the index, as this is the preferred way
9 | sylius_sitemap_no_index:
10 | path: /sitemap.xml
11 | defaults:
12 | _controller: 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction'
13 | route: sylius_sitemap_index
14 | permanent: true
15 |
16 | # Registering routes for each provider
17 | sylius_sitemap_providers:
18 | resource: .
19 | type: sitemap
20 |
--------------------------------------------------------------------------------
/src/Resources/config/services.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/Resources/config/services/providers/products.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Resources/config/services/providers/static.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | %sylius.sitemap_static%
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/Resources/config/services/providers/taxons.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | %sylius.sitemap_exclude_taxon_root%
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/Resources/config/services/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 | %sylius.sitemap_template%
40 | %sylius.sitemap_hreflang%
41 | %sylius.sitemap_images%
42 |
43 |
44 |
45 | %sylius.sitemap_index_template%
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | sylius_shop_product_original
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/Resources/views/Macro/language.html.twig:
--------------------------------------------------------------------------------
1 | {%- macro localeToCode(locale) -%}
2 | {{- locale|split('_')|first -}}
3 | {%- endmacro -%}
4 |
--------------------------------------------------------------------------------
/src/Resources/views/Macro/xml.html.twig:
--------------------------------------------------------------------------------
1 | {%- macro last_modification(url) -%}
2 | {# @var url \SitemapPlugin\Model\SitemapUrlInterface #}
3 | {%- if url.lastModification is not same as(null) -%}
4 | {{ url.lastModification|date('c') }}
5 | {%- endif -%}
6 | {%- endmacro -%}
7 |
8 | {%- macro change_frequency(url) -%}
9 | {# @var url \SitemapPlugin\Model\SitemapUrlInterface #}
10 | {%- if url.changeFrequency is not same as(null) -%}
11 | {{ url.changeFrequency }}
12 | {%- endif -%}
13 | {%- endmacro -%}
14 |
15 | {%- macro priority(url) -%}
16 | {%- if url.priority is not same as(null) -%}
17 | {{ url.priority }}
18 | {%- endif -%}
19 | {%- endmacro -%}
20 |
21 | {%- macro images(url) -%}
22 | {%- if url.getImages is not empty -%}
23 | {%- for image in url.getImages -%}
24 |
25 | {{ image.location }}
26 | {%- if image.title is not empty -%}
27 | {{ image.title }}
28 | {%- endif -%}
29 | {%- if image.caption is not empty -%}
30 | {{ image.caption }}
31 | {%- endif -%}
32 | {%- if image.geoLocation is not empty -%}
33 | {{ image.geoLocation }}
34 | {%- endif -%}
35 | {%- if image.license is not empty -%}
36 | {{ image.license }}
37 | {%- endif -%}
38 |
39 | {%- endfor -%}
40 | {%- endif -%}
41 | {%- endmacro -%}
42 |
--------------------------------------------------------------------------------
/src/Resources/views/index.xml.twig:
--------------------------------------------------------------------------------
1 | {% import '@SitemapPlugin/Macro/xml.html.twig' as xml_helper %}
2 | {% apply spaceless %}
3 |
4 |
5 | {%- for url in url_set -%}
6 |
7 | {{ absolute_url(url.location) }}
8 | {{- xml_helper.last_modification(url) -}}
9 |
10 | {% endfor %}
11 |
12 | {% endapply %}
13 |
--------------------------------------------------------------------------------
/src/Resources/views/show.xml.twig:
--------------------------------------------------------------------------------
1 | {% import '@SitemapPlugin/Macro/language.html.twig' as language_helper %}
2 | {% import '@SitemapPlugin/Macro/xml.html.twig' as xml_helper %}
3 | {% apply spaceless %}
4 |
5 |
6 | {%- for url in url_set -%}
7 |
8 | {{ absolute_url(url.location) }}
9 | {% if hreflang is not same as(false) and url.alternatives is not empty %}
10 |
11 | {% for alternative in url.alternatives %}
12 |
13 | {% endfor %}
14 | {% endif %}
15 | {{ xml_helper.last_modification(url) }}
16 | {{ xml_helper.change_frequency(url) }}
17 | {{ xml_helper.priority(url) }}
18 | {%- if images -%}
19 | {{ xml_helper.images(url) }}
20 | {%- endif -%}
21 |
22 | {% if hreflang is not same as(false) and url.alternatives is not empty %}
23 | {% for alternative in url.alternatives %}
24 |
25 | {{ absolute_url(alternative.location) }}
26 |
27 | {% for alternativeSub in url.alternatives %}
28 |
29 | {% endfor %}
30 | {{ xml_helper.last_modification(url) }}
31 | {{ xml_helper.change_frequency(url) }}
32 | {{ xml_helper.priority(url) }}
33 | {%- if images -%}
34 | {{ xml_helper.images(url) }}
35 | {%- endif -%}
36 |
37 | {% endfor %}
38 | {% endif %}
39 | {%- endfor -%}
40 |
41 | {% endapply %}
42 |
--------------------------------------------------------------------------------
/src/Routing/SitemapLoader.php:
--------------------------------------------------------------------------------
1 | loaded) {
30 | return $routes;
31 | }
32 |
33 | foreach ($this->sitemapBuilder->getProviders() as $provider) {
34 | $name = 'sylius_sitemap_' . $provider->getName();
35 |
36 | if (null !== $routes->get($name)) {
37 | throw new RouteExistsException($name);
38 | }
39 |
40 | $routes->add(
41 | $name,
42 | new Route(
43 | '/sitemap/' . $provider->getName() . '.xml',
44 | [
45 | '_controller' => 'sylius.controller.sitemap::showAction',
46 | 'name' => $provider->getName(),
47 | ],
48 | [],
49 | [],
50 | '',
51 | [],
52 | ['GET'],
53 | ),
54 | );
55 | }
56 |
57 | $this->loaded = true;
58 |
59 | return $routes;
60 | }
61 |
62 | public function supports($resource, $type = null): bool
63 | {
64 | return 'sitemap' === $type;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/SitemapPlugin.php:
--------------------------------------------------------------------------------
1 | addCompilerPass(new SitemapProviderPass());
21 | }
22 | }
23 |
--------------------------------------------------------------------------------