Skip to content

Commit 8e29cba

Browse files
KocaltBibaut
authored andcommitted
Initialize StaticSiteGeneration (SSG) feature
1 parent d824d53 commit 8e29cba

File tree

16 files changed

+451
-16
lines changed

16 files changed

+451
-16
lines changed

notes

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// [HttpKernel]
2+
// StaticSiteGeneration\ParamsProviderInterface (__invoke: iterable<list<mixed>>)
3+
// StaticSiteGeneration\StaticSiteGeneratorInterface (generate(): void)
4+
// - checks that: 200, GET, require stateless, no query param, no fragments
5+
// - generate in "public/static" dir (see in composer config - example AssetsInstallCommand)
6+
// DependencyInjection\StaticSiteGenerationPass (service locator for params providers)
7+
//
8+
// [Routing]
9+
// update RouteConfigurator to add "staticGeneration" method (params?: list<list<mixed>>|string}) - string for service id - default to true in Route
10+
// update Attribute\Route to add "staticGeneration" property (array{params: list<list<mixed>>|string}|true) - string for service id - default false in Route
11+
// make it work in YAML and XML in the same way
12+
// - GET method, stateless, staticGeneration depending on a new defined "params" property
13+
// eg: #[Route(name: 'app_blog_article', path: '/blog/{id}', methods: ['GET'], staticGeneration: true)]
14+
//
15+
// [FrameworkBundle]
16+
// Command\GenerateStaticSiteCommand - static-site:generate (--dry-run and fail) (should retrieve all generators)
17+
//
18+
// [Recipe]
19+
// update .gitignore to add "/public/static/"
20+
// update server configurations
21+
//
22+
// [Documentation]
23+
// server configurations example to rewrite
24+
//
25+
// [Other PR]
26+
// debug:router command - add option to see SSG details
27+
// StaticSiteGenerationDataCollector - add SSG section only on SSG routes, showing information, and telling if it will be ready for generation
28+
// Add custom formats
29+
30+
31+
32+
33+
StaticPagesGenerator
34+
-> StaticPagesProviderInterface =>
35+
-> -> RouteStaticPagesProvider => parcourt toutes les routes "staticGeneration"
36+
_> -> -> ParamsProviderInterface => /blog/* -> [ [slug => puasddsa], [ slug => qweqw] ]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
/*
3+
* This file is part of the Symfony package.
4+
*
5+
* (c) Fabien Potencier <[email protected]>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
namespace Symfony\Bundle\FrameworkBundle\Command;
12+
13+
use Symfony\Component\Console\Attribute\AsCommand;
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Output\OutputInterface;
17+
use Symfony\Component\HttpKernel\StaticSiteGeneration\StaticPagesGenerator;
18+
use Symfony\Component\HttpKernel\StaticSiteGeneration\StaticPageDumperInterface;
19+
20+
/**
21+
* @author Thomas Bibaut
22+
* @author Mathias Arlaud <[email protected]>
23+
* @author Hugo Alliaume <[email protected]>
24+
*/
25+
#[AsCommand(name: 'static-site-generation:generate', description: 'Generates the static site')]
26+
// * @todo better description
27+
final class StaticSiteGenerationGenerateCommand extends Command
28+
{
29+
// @todo dry-run
30+
// @todo fitlerPattern
31+
32+
public function __construct(
33+
private readonly StaticPagesGenerator $staticPagesGenerator,
34+
private readonly StaticPageDumperInterface $staticPageDumper,
35+
) {
36+
parent::__construct();
37+
}
38+
39+
protected function execute(InputInterface $input, OutputInterface $output): int
40+
{
41+
foreach ($this->staticPagesGenerator->generate() as $uri => $content) {
42+
$this->staticPageDumper->dump($uri, $content);
43+
}
44+
45+
return Command::SUCCESS;
46+
}
47+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Composer\InstalledVersions;
1515
use Http\Client\HttpAsyncClient;
1616
use Http\Client\HttpClient;
17+
use Symfony\Component\Routing\StaticSiteGeneration\ParamsProviderInterface;
1718
use phpDocumentor\Reflection\DocBlockFactoryInterface;
1819
use phpDocumentor\Reflection\Types\ContextFactory;
1920
use PhpParser\Parser;
@@ -1247,6 +1248,9 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co
12471248
$container->getDefinition('router.request_context')
12481249
->replaceArgument(0, $config['default_uri']);
12491250
}
1251+
1252+
$container->registerForAutoconfiguration(ParamsProviderInterface::class)
1253+
->addTag('routing.static_site.params_provider');
12501254
}
12511255

12521256
private function registerSessionConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void

src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
use Symfony\Bundle\FrameworkBundle\Command\TranslationExtractCommand;
4040
use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand;
4141
use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand;
42+
use Symfony\Bundle\FrameworkBundle\Command\StaticSiteGenerationGenerateCommand;
4243
use Symfony\Bundle\FrameworkBundle\Console\Application;
4344
use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber;
4445
use Symfony\Component\Console\EventListener\ErrorListener;
@@ -397,5 +398,13 @@
397398
service('console.messenger.application'),
398399
])
399400
->tag('messenger.message_handler')
401+
402+
403+
->set('console.command.static_site_generation_generate', StaticSiteGenerationGenerateCommand::class)
404+
->args([
405+
service('static_site.pages_generator'),
406+
service('static_site.page_dumper'),
407+
])
408+
->tag('console.command')
400409
;
401410
};

src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
use Symfony\Component\Routing\RequestContext;
4040
use Symfony\Component\Routing\RequestContextAwareInterface;
4141
use Symfony\Component\Routing\RouterInterface;
42+
use Symfony\Component\Routing\StaticSiteGeneration\StaticUrisProvider;
4243

4344
return static function (ContainerConfigurator $container) {
4445
$container->parameters()
@@ -208,5 +209,11 @@
208209
service('twig')->ignoreOnInvalid(),
209210
])
210211
->public()
212+
213+
->set('routing.static_site.uri_provider', StaticUrisProvider::class)
214+
->args([
215+
service('router'),
216+
tagged_locator('routing.static_site.params_provider'),
217+
])
211218
;
212219
};

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
use Symfony\Component\HttpKernel\EventListener\LocaleListener;
3535
use Symfony\Component\HttpKernel\EventListener\ResponseListener;
3636
use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener;
37+
use Symfony\Component\HttpKernel\StaticSiteGeneration\FilesystemStaticPageDumper;
38+
use Symfony\Component\HttpKernel\StaticSiteGeneration\StaticPageDumperInterface;
39+
use Symfony\Component\HttpKernel\StaticSiteGeneration\StaticPagesGenerator;
3740

3841
return static function (ContainerConfigurator $container) {
3942
$container->services()
@@ -145,5 +148,16 @@
145148
->set('controller.cache_attribute_listener', CacheAttributeListener::class)
146149
->tag('kernel.event_subscriber')
147150

151+
->set('static_site.pages_generator', StaticPagesGenerator::class)
152+
->args([
153+
service('http_kernel'),
154+
service('routing.static_site.uri_provider'),
155+
])
156+
157+
->set('static_site.page_dumper', FilesystemStaticPageDumper::class)
158+
->args([
159+
param('kernel.project_dir'),
160+
])
161+
->alias(StaticPageDumperInterface::class, 'static_static.page_dumper')
148162
;
149163
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\StaticSiteGeneration;
13+
14+
use Symfony\Component\Filesystem\Filesystem;
15+
16+
final readonly class FilesystemStaticPageDumper implements StaticPageDumperInterface
17+
{
18+
private Filesystem $filesystem;
19+
20+
public function __construct(
21+
private string $projectDir,
22+
) {
23+
$this->filesystem = new Filesystem();
24+
}
25+
26+
public function dump(string $uri, string $content): void
27+
{
28+
// @todo pas de .. dans $uri
29+
30+
$staticPagesDirectory = sprintf('%s/static-pages', $this->getPublicDirectory());
31+
32+
$this->filesystem->dumpFile(sprintf('%s/%s', $staticPagesDirectory, $uri), $content);
33+
}
34+
35+
private function getPublicDirectory(): string
36+
{
37+
$defaultPublicDir = 'public';
38+
$composerFilePath = sprintf('%s/composer.json', $this->projectDir);
39+
40+
if (!file_exists($composerFilePath)) {
41+
return $defaultPublicDir;
42+
}
43+
44+
$composerConfig = json_decode($this->filesystem->readFile($composerFilePath), true, flags: \JSON_THROW_ON_ERROR);
45+
46+
return $composerConfig['extra']['public-dir'] ?? $defaultPublicDir;
47+
}
48+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\StaticSiteGeneration;
13+
14+
interface StaticPageDumperInterface
15+
{
16+
public function dump(string $uri, string $content): void;
17+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\StaticSiteGeneration;
13+
14+
// @todo dependency compsoer.json symfony/filesystem
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\HttpKernel\HttpKernelInterface;
18+
use Symfony\Component\HttpKernel\TerminableInterface;
19+
use Symfony\Component\Routing\StaticSiteGeneration\StaticUrisProviderInterface;
20+
21+
/**
22+
* @author Thomas Bibaut
23+
* @author Mathias Arlaud <[email protected]>
24+
* @author Hugo Alliaume <[email protected]>
25+
*/
26+
final readonly class StaticPagesGenerator
27+
{
28+
public function __construct(
29+
private HttpKernelInterface $kernel,
30+
private StaticUrisProviderInterface $staticUrisProvider,
31+
) {
32+
}
33+
34+
/**
35+
* @return iterable<string, string>
36+
*/
37+
public function generate(?string $filterPattern = null): iterable
38+
{
39+
foreach ($this->staticUrisProvider->provide() as $uri) {
40+
if (null !== $filterPattern && !preg_match($filterPattern, $uri)) {
41+
continue;
42+
}
43+
44+
// @todo try/catch
45+
$request = Request::create($uri);
46+
$response = $this->kernel->handle($request, HttpKernelInterface::MAIN_REQUEST);
47+
48+
if ($this->kernel instanceof TerminableInterface) {
49+
$this->kernel->terminate($request, $response);
50+
}
51+
52+
if (Response::HTTP_OK !== $response->getStatusCode()) {
53+
// @todo throws
54+
}
55+
56+
$format = $request->getFormat($response->headers->get('Content-Type')) ?? '';
57+
if (!str_ends_with($uri, '.' . $format)) {
58+
$uri = sprintf('%s.%s', $uri, $format);
59+
}
60+
61+
yield $uri => $response->getContent();
62+
}
63+
}
64+
}

src/Symfony/Component/Routing/Attribute/Route.php

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,23 @@ class Route
2828
private array $aliases = [];
2929

3030
/**
31-
* @param string|array<string,string>|null $path The route path (i.e. "/user/login")
32-
* @param string|null $name The route name (i.e. "app_user_login")
33-
* @param array<string|\Stringable> $requirements Requirements for the route attributes, @see https://symfony.com/doc/current/routing.html#parameters-validation
34-
* @param array<string, mixed> $options Options for the route (i.e. ['prefix' => '/api'])
35-
* @param array<string, mixed> $defaults Default values for the route attributes and query parameters
36-
* @param string|null $host The host for which this route should be active (i.e. "localhost")
37-
* @param string|string[] $methods The list of HTTP methods allowed by this route
38-
* @param string|string[] $schemes The list of schemes allowed by this route (i.e. "https")
39-
* @param string|null $condition An expression that must evaluate to true for the route to be matched, @see https://symfony.com/doc/current/routing.html#matching-expressions
40-
* @param int|null $priority The priority of the route if multiple ones are defined for the same path
41-
* @param string|null $locale The locale accepted by the route
42-
* @param string|null $format The format returned by the route (i.e. "json", "xml")
43-
* @param bool|null $utf8 Whether the route accepts UTF-8 in its parameters
44-
* @param bool|null $stateless Whether the route is defined as stateless or stateful, @see https://symfony.com/doc/current/routing.html#stateless-routes
45-
* @param string|null $env The env in which the route is defined (i.e. "dev", "test", "prod")
46-
* @param string|DeprecatedAlias|(string|DeprecatedAlias)[] $alias The list of aliases for this route
31+
* @param string|array<string,string>|null $path The route path (i.e. "/user/login")
32+
* @param string|null $name The route name (i.e. "app_user_login")
33+
* @param array<string|\Stringable> $requirements Requirements for the route attributes, @see https://symfony.com/doc/current/routing.html#parameters-validation
34+
* @param array<string, mixed> $options Options for the route (i.e. ['prefix' => '/api'])
35+
* @param array<string, mixed> $defaults Default values for the route attributes and query parameters
36+
* @param string|null $host The host for which this route should be active (i.e. "localhost")
37+
* @param string|string[] $methods The list of HTTP methods allowed by this route
38+
* @param string|string[] $schemes The list of schemes allowed by this route (i.e. "https")
39+
* @param string|null $condition An expression that must evaluate to true for the route to be matched, @see https://symfony.com/doc/current/routing.html#matching-expressions
40+
* @param int|null $priority The priority of the route if multiple ones are defined for the same path
41+
* @param string|null $locale The locale accepted by the route
42+
* @param string|null $format The format returned by the route (i.e. "json", "xml")
43+
* @param bool|null $utf8 Whether the route accepts UTF-8 in its parameters
44+
* @param bool|null $stateless Whether the route is defined as stateless or stateful, @see https://symfony.com/doc/current/routing.html#stateless-routes
45+
* @param string|null $env The env in which the route is defined (i.e. "dev", "test", "prod")
46+
* @param string|DeprecatedAlias|(string|DeprecatedAlias)[] $alias The list of aliases for this route
47+
* @param bool|array{params?: string|iterable<array<mixed>>} $staticGeneration The static generation configuration, params : route parameters
4748
*/
4849
public function __construct(
4950
string|array|null $path = null,
@@ -62,6 +63,7 @@ public function __construct(
6263
?bool $stateless = null,
6364
private ?string $env = null,
6465
string|DeprecatedAlias|array $alias = [],
66+
bool|array $staticGeneration = false,
6567
) {
6668
if (\is_array($path)) {
6769
$this->localizedPaths = $path;
@@ -87,6 +89,10 @@ public function __construct(
8789
if (null !== $stateless) {
8890
$this->defaults['_stateless'] = $stateless;
8991
}
92+
93+
if ($staticGeneration) {
94+
$this->defaults['_static_generation'] = $staticGeneration;
95+
}
9096
}
9197

9298
public function setPath(string $path): void

src/Symfony/Component/Routing/Loader/Configurator/Traits/RouteTrait.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,14 @@ final public function stateless(bool $stateless = true): static
169169

170170
return $this;
171171
}
172+
173+
/**
174+
* @param bool|array{params?: string|iterable<array<mixed>>} $staticGeneration
175+
*/
176+
final public function staticGeneration(bool|array $staticGeneration = true): static
177+
{
178+
$this->route->addDefaults(['_static_generation' => $staticGeneration]);
179+
180+
return $this;
181+
}
172182
}

src/Symfony/Component/Routing/Loader/XmlFileLoader.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,16 @@ private function parseConfigs(\DOMElement $node, string $path): array
321321

322322
$defaults['_stateless'] = XmlUtils::phpize($stateless);
323323
}
324+
if ($staticGeneration = $node->getAttribute('static-generation')) {
325+
if (isset($defaults['_static_generation'])) {
326+
$name = $node->hasAttribute('id') ? \sprintf('"%s".', $node->getAttribute('id')) : \sprintf('the "%s" tag.', $node->tagName);
327+
328+
throw new \InvalidArgumentException(\sprintf('The routing file "%s" must not specify both the "static-generation" attribute and the defaults key "_static_generation" for ', $path).$name);
329+
}
330+
331+
332+
$defaults['_static_generation'] = XmlUtils::phpize($staticGeneration);
333+
}
324334

325335
if (!$hosts) {
326336
$hosts = $node->hasAttribute('host') ? $node->getAttribute('host') : null;

0 commit comments

Comments
 (0)