Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sitemap.xml #183

Merged
merged 16 commits into from
Dec 8, 2023
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
"license": "MIT",
"require": {
"php": "^8.2",
"ext-zlib": "*",
"bentools/cartesian-product": "^1.4",
"league/commonmark": "^2.3",
"phpdocumentor/type-resolver": "^1.0",
"phpstan/phpdoc-parser": "^1.0",
"presta/sitemap-bundle": "^3.0",
"spatie/commonmark-highlighter": "^3.0",
"symfony/console": "^6.4 || ^7.0",
"symfony/expression-language": "^6.4 || ^7.0",
Expand Down
1 change: 1 addition & 0 deletions config/bundles.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Presta\SitemapBundle\PrestaSitemapBundle::class => ['all' => true],
];
27 changes: 19 additions & 8 deletions psalm.baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,11 @@
<code>__construct</code>
</PossiblyUnusedMethod>
</file>
<file src="src/DatabaseProvider.php">
<PossiblyUnusedMethod>
<code>__construct</code>
</PossiblyUnusedMethod>
</file>
<file src="src/Decoder/CachingFileDecoder.php">
<UnusedClass>
<code>CachingFileDecoder</code>
Expand All @@ -536,6 +541,11 @@
<code>YamlFileDecoder</code>
</UnusedClass>
</file>
<file src="src/Generator.php">
<PossiblyUnusedMethod>
<code>__construct</code>
</PossiblyUnusedMethod>
</file>
<file src="src/Permutator.php">
<MixedArgument>
<code>$expression</code>
Expand Down Expand Up @@ -570,6 +580,9 @@
<code><![CDATA[$spec['defaults']]]></code>
<code><![CDATA[$spec['options']]]></code>
</PossiblyUndefinedStringArrayOffset>
<PossiblyUnusedMethod>
<code>__construct</code>
</PossiblyUnusedMethod>
</file>
<file src="src/Storage/DenormalizingStorage.php">
<MissingTemplateParam>
Expand Down Expand Up @@ -635,6 +648,11 @@
<code>array</code>
</PossiblyUnusedReturnValue>
</file>
<file src="tests/functional/site/src/Bridge/Symfony/EventSubscriber.php">
<UnusedClass>
<code>EventSubscriber</code>
</UnusedClass>
</file>
<file src="tests/functional/site/src/Controller/TestController.php">
<UnusedClass>
<code>TestController</code>
Expand All @@ -643,6 +661,7 @@
<file src="tests/functional/site/src/Model/Article.php">
<MissingConstructor>
<code>$body</code>
<code>$image</code>
<code>$publishedAt</code>
<code>$slug</code>
<code>$title</code>
Expand Down Expand Up @@ -705,14 +724,6 @@
<code>$slug</code>
</PossiblyUnusedProperty>
</file>
<file src="tests/unit/GeneratorTest.php">
<InternalMethod>
<code>numberOfInvocations</code>
</InternalMethod>
<MixedAssignment>
<code>$params</code>
</MixedAssignment>
</file>
<file src="web/index.php">
<MixedArgument>
<code><![CDATA[$GLOBALS['YASSG_BASEDIR'] ?? throw new LogicException('YASSG base dir not found')]]></code>
Expand Down
39 changes: 39 additions & 0 deletions src/Bridge/PrestaSitemap/Urlset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sigwin Yassg project.
*
* (c) sigwin.hr
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Sigwin\YASSG\Bridge\PrestaSitemap;

use Presta\SitemapBundle\Sitemap\Url\Url;
use Presta\SitemapBundle\Sitemap\Url\UrlConcrete;

final class Urlset extends \Presta\SitemapBundle\Sitemap\Urlset
{
public function __construct(string $loc)
{
parent::__construct($loc);

$this->lastmod = new \DateTimeImmutable('1970-01-01 00:00:00');
}

public function addUrl(Url $url): void
{
parent::addUrl($url);

if ($url instanceof UrlConcrete) {
$lastModification = $url->getLastmod();
if ($lastModification !== null && $lastModification->getTimestamp() > $this->lastmod->getTimestamp()) {
$this->lastmod = $lastModification;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

namespace Sigwin\YASSG\Bridge\Symfony\Routing\Generator;

use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext;

Expand All @@ -24,7 +23,7 @@ public function __construct(private readonly UrlGeneratorInterface $urlGenerator
public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
{
if (! isset($this->routes[$name])) {
throw new RouteNotFoundException();
return $this->urlGenerator->generate($name, $parameters, $referenceType);
}

$this->stripParameters($this->stripParameters[$name] ?? [], $parameters);
Expand Down
85 changes: 82 additions & 3 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@

namespace Sigwin\YASSG;

use Presta\SitemapBundle\Sitemap\Sitemapindex;
use Presta\SitemapBundle\Sitemap\Url\UrlConcrete;
use Sigwin\YASSG\Bridge\PrestaSitemap\Urlset;
use Sigwin\YASSG\Bridge\Symfony\Routing\Request;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
Expand All @@ -32,19 +36,76 @@ public function generate(callable $callable): void

$indexFile = (bool) ($requestContext->getParameter('index-file') ?? false);

$deflate = true;
$index = new Sitemapindex();
$offset = 0;
$urlSet = null;
$urlSetAdded = true;
foreach ($this->permutator->permute() as $location) {
if ($urlSet !== null) {
if ($this->generateSitemapPath($deflate, $location->getRoute()->getName(), $offset) !== $urlSet->getLoc()) {
$this->dumpSitemap($urlSet, $deflate);
$index->addSitemap($urlSet);
$urlSet = null;
$urlSetAdded = false;
} elseif ($urlSet->isFull()) {
$this->dumpSitemap($urlSet, $deflate);
$index->addSitemap($urlSet);
$urlSet = null;
$urlSetAdded = false;
++$offset;
}
}
if ($urlSet === null) {
$urlSet = new Urlset($this->generateSitemapPath($deflate, $location->getRoute()->getName(), $offset));
$urlSetAdded = false;
$offset = 0;
}

$route = $location->getRoute();
$url = $this->urlGenerator->generate($route->getName(), $route->getParameters() + ($indexFile ? ['_filename' => 'index.html'] : []), UrlGeneratorInterface::ABSOLUTE_URL);
$request = Request::create(rtrim($url, '/'))->withBaseUrl($requestContext->getBaseUrl());
$request = $this->createRequest($url);
if (($buildHeaders = $location->getBuildOptions()->getRequestHeaders()) !== null) {
$request->headers->add($buildHeaders);
}

$this->dumpFile($callable, $request);
$response = $this->dumpRequest($callable, $request);
$urlSet->addUrl(new UrlConcrete($url, new \DateTimeImmutable($response->headers->get('Last-Modified', 'now'))));
}
if ($urlSet !== null) {
$this->dumpSitemap($urlSet, $deflate);
if ($urlSetAdded === false) {
$index->addSitemap($urlSet);
}
}
$this->dumpSitemap($index, $deflate);

// dump static files
$this->dumpRequest($callable, $this->createRequest($this->urlGenerator->generate('error404', [], UrlGeneratorInterface::ABSOLUTE_URL)), 404);
}

private function createRequest(string $path): Request
{
return Request::create(rtrim($path, '/'))->withBaseUrl($this->urlGenerator->getContext()->getBaseUrl());
}

private function generateSitemapPath(bool $deflate, ?string $name = null, ?int $offset = null): string
{
if ($name === null) {
return $this->generateUrl('/sitemap.xml'.($deflate ? '.gz' : ''));
}

return $this->generateUrl(sprintf('/sitemap-%1$s-%2$d.xml'.($deflate ? '.gz' : ''), $name, $offset ?? throw new \LogicException('Offset must be set when name is set')));
}

private function dumpFile(callable $callable, Request $request, int $expectedStatusCode = 200): void
private function generateUrl(string $path): string
{
$context = $this->urlGenerator->getContext();

return sprintf('%1$s://%2$s%3$s%4$s', $context->getScheme(), $context->getHost(), $context->getBaseUrl(), $path);
}

private function dumpRequest(callable $callable, Request $request, int $expectedStatusCode = 200): Response
{
try {
$response = $this->kernel->handle($request);
Expand All @@ -71,5 +132,23 @@ private function dumpFile(callable $callable, Request $request, int $expectedSta
$this->filesystem->dumpFile($path, $body);

$callable($request, $response, $path);

return $response;
}

private function dumpSitemap(Sitemapindex|Urlset $sitemap, bool $deflate): void
{
if ($sitemap->count() === 0) {
return;
}

$path = $this->generateSitemapPath($deflate);
if ($sitemap instanceof Urlset) {
$path = $sitemap->getLoc();
}

/** @var string $content */
$content = $deflate ? gzdeflate($sitemap->toXml()) : $sitemap->toXml();
$this->filesystem->dumpFile($this->buildDir.str_replace($this->generateUrl(''), '', $path), $content);
}
}
24 changes: 24 additions & 0 deletions tests/functional/init/fixtures/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex,nofollow,noarchive">
<title>An Error Occurred: Not Found</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>❌</text></svg>">
<style>body { background-color: #fff; color: #222; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; }
.container { margin: 30px; max-width: 600px; }
h1 { color: #dc3545; font-size: 24px; }
h2 { font-size: 18px; }</style>
</head>
<body>
<div class="container">
<h1>Oops! An Error Occurred</h1>
<h2>The server returned a "404 Not Found".</h2>

<p>
Something is broken. Please let us know what you were doing when this error occurred.
We will fix it as soon as possible. Sorry for any inconvenience caused.
</p>
</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
include ../default.mk

self/test:
cp public/sitemap* fixtures/
sh -c "${PHPQA_DOCKER_COMMAND} diff -r fixtures/ public/"
24 changes: 24 additions & 0 deletions tests/functional/site/fixtures/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex,nofollow,noarchive">
<title>An Error Occurred: Not Found</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>❌</text></svg>">
<style>body { background-color: #fff; color: #222; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; }
.container { margin: 30px; max-width: 600px; }
h1 { color: #dc3545; font-size: 24px; }
h2 { font-size: 18px; }</style>
</head>
<body>
<div class="container">
<h1>Oops! An Error Occurred</h1>
<h2>The server returned a "404 Not Found".</h2>

<p>
Something is broken. Please let us know what you were doing when this error occurred.
We will fix it as soon as possible. Sorry for any inconvenience caused.
</p>
</div>
</body>
</html>
Binary file not shown.
Binary file not shown.
Binary file added tests/functional/site/fixtures/sitemap-file-0.xml.gz
Binary file not shown.
3 changes: 3 additions & 0 deletions tests/functional/site/fixtures/sitemap-homepage-0.xml.gz
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
½M
ƒ0F¯"Ù3ÑnÚã®'°°qP!?’‰ÕãWS/Ð.
ÃÀ|¼#ë՚셁Fï*VpÁ2tÚw£ë+öhîù…ÕJÎÁÆlcUlˆqº,ËÂiŒhۉ¸=¶ƒàAð+ËRYIãµÚ{´qmídkoæ'tc€Öù8`€AÂKÓR´¾S¥(‹¼(ósÑqKsJ{ãDBr|%ŸDè~Áçñê
Binary file not shown.
1 change: 1 addition & 0 deletions tests/functional/site/fixtures/sitemap-product-0.xml.gz
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
½‘=ƒ0 F¯‚²V‰º´U[O@@Á¤ü 8Ž_ \ *Y–lOo°.g³F‚/˜’eè›Ð¾+Ø£ºó +ž¢%LٚõT°>¥ñ0ϳ !¡«G!v@M¿Ç¤¸²l‡¶¡1G+ˆKíF‹¢ hzB;D¨}H=F@)aˏhØXmkJ.´&—¹â*çgUIyÛë´÷5wD4ìÊo½ÇYýݘÿn„ÏgÌ
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-ŽÝ
ƒ0 F_Ez;lª»ÙF­w{w=j[´ÐiêÜã¯:!ÎùÞ½«>&¡¡#e¤2AEmÃԑ×ð¬o¤|MM®
°#sÎË`Û6Š6/¤1M€j.¹Fï¤:dÁ]Tb÷°ˆæ+ýâ UÑ®#h›@†˜g“ÀXRÔ«ÊïÍæù--‰rT8ì!ÜIÌ>jѲ¶©›¶¾6c£.G/܉p8nÃÿ}ñ
Expand Down
2 changes: 2 additions & 0 deletions tests/functional/site/fixtures/sitemap.xml.gz
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
͔Akà †ÿJð:¢&½lÁ¤·ºÓ:Ø-8µ‰`4ø™%ì×ÏÚ¬ôÚ1È@Dä}|=ÈöË`²OåA;[£S”)+œÔ¶«ÑÛñ9Dû†jࣶR-Y$,T èõ!Œ!ó<ãy‡ïHIiAÞ_¯¢D®-n…BYÌW6NðênðµÒ!—u“Pü”Ý‘Më䊐è"|Wº^¹aƉæŒBdÕ‡Ñ(,Ü@`ú R{­ ½ò?xžŠsŠc+î¾9óÌpƒ“MIË"/Ê|W)­ÒxHsÌ­F®Õ¿v8i£6VèÝ FÞm­Á}ÐbóÇX-`cÑ;9‰ð?,ÚY‡¾õn
ª•êÄ'óGbäö·j¾
31 changes: 31 additions & 0 deletions tests/functional/site/src/Bridge/Symfony/EventSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sigwin Yassg project.
*
* (c) sigwin.hr
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Sigwin\YASSG\Test\Functional\Site\Bridge\Symfony;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;

final class EventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return ['kernel.response' => 'onResponse'];
}

public function onResponse(ResponseEvent $event): void
{
$response = $event->getResponse();
$response->headers->add(['Last-Modified' => gmdate('D, d M Y H:i:s', strtotime('2021-12-31 00:00:00')).' GMT']);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

use PHPUnit\Framework\TestCase;
use Sigwin\YASSG\Bridge\Symfony\Routing\Generator\FilenameUrlGenerator;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
Expand All @@ -31,9 +30,7 @@ final class FilenameUrlGeneratorTest extends TestCase
{
public function testCannotGenerateUnknownRoute(): void
{
$this->expectException(RouteNotFoundException::class);

$generator = new FilenameUrlGenerator($this->getMockBuilder(UrlGeneratorInterface::class)->getMock(), [], []);
$generator->generate('unknown');
self::assertEmpty($generator->generate('unknown'));
}
}
Loading
Loading