Skip to content

Commit 7816ddf

Browse files
KocaltBibaut
authored andcommitted
Initialize StaticSiteGeneration (SSG) feature
1 parent d824d53 commit 7816ddf

File tree

24 files changed

+789
-16
lines changed

24 files changed

+789
-16
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* asd
8+
49
7.3
510
---
611

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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\Input\InputOption;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Component\Console\Style\SymfonyStyle;
19+
use Symfony\Component\HttpKernel\StaticSiteGeneration\StaticPagesGenerator;
20+
use Symfony\Component\HttpKernel\StaticSiteGeneration\StaticPageDumperInterface;
21+
use Symfony\Component\HttpKernel\Exception\RuntimeException;
22+
use Symfony\Component\Routing\StaticSiteGeneration\StaticUrisProviderInterface;
23+
24+
/**
25+
* @author Thomas Bibaut <[email protected]>
26+
*/
27+
#[AsCommand(name: 'static-site-generation:generate', description: 'Generates the static site')]
28+
final class StaticSiteGenerationGenerateCommand extends Command
29+
{
30+
public function __construct(
31+
private readonly StaticPagesGenerator $staticPagesGenerator,
32+
private readonly StaticPageDumperInterface $staticPageDumper,
33+
private readonly StaticUrisProviderInterface $staticUrisProvider,
34+
) {
35+
parent::__construct();
36+
}
37+
38+
protected function configure(): void
39+
{
40+
$this
41+
->setDescription('Generates static pages')
42+
->addOption(
43+
'filterPattern',
44+
null,
45+
InputOption::VALUE_REQUIRED,
46+
'A pattern to filter the pages to generate',
47+
)
48+
->addOption(
49+
'dryRun',
50+
null,
51+
InputOption::VALUE_NONE,
52+
'Do not dump pages',
53+
);
54+
}
55+
56+
protected function execute(InputInterface $input, OutputInterface $output): int
57+
{
58+
$io = new SymfonyStyle($input, $output);
59+
$filterPattern = $input->getOption('filterPattern');
60+
$dryRun = $input->getOption('dryRun');
61+
62+
foreach ($this->staticUrisProvider->provide() as $uri)
63+
{
64+
if (null !== $filterPattern && !preg_match($filterPattern, $uri)) {
65+
continue;
66+
}
67+
68+
try {
69+
['content' => $content, 'format' => $format] = $this->staticPagesGenerator->generate($uri);
70+
} catch (RuntimeException $exception) {
71+
$io->error(sprintf('Generating page for URI "%s" failed : %s', $uri, $exception->getMessage()));
72+
73+
// PROBLEM : even with -vvv we don't get the stack trace for this
74+
// @todo printTrace in SymfonyStyle ???
75+
// @see src/Symfony/Component/Console/Application.php:899
76+
//
77+
// if ($io->isVerbose()) {
78+
// $io->comment('Exception trace', OutputInterface::VERBOSITY_QUIET);
79+
// }
80+
}
81+
82+
if (false === $dryRun) {
83+
$this->staticPageDumper->dump($uri, $content, $format);
84+
}
85+
86+
// @todo output path ?
87+
$io->info(sprintf('Generated static page for URI "%s"', $uri));
88+
}
89+
90+
return Command::SUCCESS;
91+
}
92+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class UnusedTagsPass implements CompilerPassInterface
8383
'routing.expression_language_provider',
8484
'routing.loader',
8585
'routing.route_loader',
86+
'routing.static_site.params_provider',
8687
'scheduler.schedule_provider',
8788
'scheduler.task',
8889
'security.access_token_handler.oidc.encryption_algorithm',

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: 10 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,14 @@
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+
service('routing.static_site.uri_provider'),
408+
])
409+
->tag('console.command')
400410
;
401411
};

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
};

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* asd
8+
9+
410
7.3
511
---
612

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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\Exception;
13+
14+
class RuntimeException extends \RuntimeException
15+
{
16+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
/**
17+
* @author Thomas Bibaut <[email protected]>
18+
*/
19+
final readonly class FilesystemStaticPageDumper implements StaticPageDumperInterface
20+
{
21+
private Filesystem $filesystem;
22+
23+
public function __construct(
24+
private string $projectDir,
25+
) {
26+
$this->filesystem = new Filesystem();
27+
}
28+
29+
public function dump(string $uri, string $content, ?string $format = null): void
30+
{
31+
$fileName = $uri === '/'
32+
? 'index.html'
33+
: $uri;
34+
35+
if ($format && !str_ends_with($uri, '.' . $format)) {
36+
$fileName = sprintf('%s.%s', $uri, $format);
37+
}
38+
39+
$staticPagesDirectory = sprintf('%s/%s/static-pages', $this->projectDir, $this->getPublicDirectory());
40+
$this->filesystem->dumpFile(sprintf('%s/%s', $staticPagesDirectory, $fileName), $content);
41+
}
42+
43+
private function getPublicDirectory(): string
44+
{
45+
$defaultPublicDir = 'public';
46+
$composerFilePath = sprintf('%s/composer.json', $this->projectDir);
47+
48+
if (!file_exists($composerFilePath)) {
49+
return $defaultPublicDir;
50+
}
51+
52+
$composerConfig = json_decode($this->filesystem->readFile($composerFilePath), true, flags: \JSON_THROW_ON_ERROR);
53+
54+
return $composerConfig['extra']['public-dir'] ?? $defaultPublicDir;
55+
}
56+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
/**
15+
* @author Thomas Bibaut <[email protected]>
16+
*/
17+
interface StaticPageDumperInterface
18+
{
19+
public function dump(string $uri, string $content, ?string $format): void;
20+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\HttpKernel\HttpKernelInterface;
17+
use Symfony\Component\HttpKernel\TerminableInterface;
18+
use Symfony\Component\HttpKernel\Exception\RuntimeException;
19+
20+
/**
21+
* @author Thomas Bibaut <[email protected]>
22+
*/
23+
final readonly class StaticPagesGenerator
24+
{
25+
public function __construct(
26+
private HttpKernelInterface $kernel,
27+
) {
28+
}
29+
30+
/**
31+
* Generate page content for URI.
32+
*
33+
* @return array{content: string, format: ?string}
34+
*
35+
* @throws RuntimeException
36+
*/
37+
public function generate(string $uri): array
38+
{
39+
$request = Request::create($uri);
40+
try {
41+
$response = $this->kernel->handle($request, HttpKernelInterface::MAIN_REQUEST);
42+
43+
if ($this->kernel instanceof TerminableInterface) {
44+
$this->kernel->terminate($request, $response);
45+
}
46+
} catch (\Exception $e) {
47+
throw new RuntimeException(sprintf('Cannot generate page for URI "%s".', $uri), $e->getCode(), $e);
48+
}
49+
50+
if (Response::HTTP_OK !== $response->getStatusCode()) {
51+
throw new RuntimeException(sprintf('Expected URI "%s" to return status code 200, got %d.', $uri, $response->getStatusCode()));
52+
}
53+
54+
return [
55+
'content' => $response->getContent(),
56+
'format' => $request->getFormat($response->headers->get('Content-Type'))
57+
];
58+
}
59+
}

0 commit comments

Comments
 (0)