Skip to content

Commit 405b576

Browse files
KocaltBibaut
authored andcommitted
Initialize StaticSiteGeneration (SSG) feature
1 parent d824d53 commit 405b576

File tree

19 files changed

+577
-16
lines changed

19 files changed

+577
-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: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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
26+
*/
27+
#[AsCommand(name: 'static-site-generation:generate', description: 'Generates the static site')]
28+
// * @todo better description
29+
final class StaticSiteGenerationGenerateCommand extends Command
30+
{
31+
// @todo dry-run
32+
// @todo fitlerPattern
33+
34+
public function __construct(
35+
private readonly StaticPagesGenerator $staticPagesGenerator,
36+
private readonly StaticPageDumperInterface $staticPageDumper,
37+
private readonly StaticUrisProviderInterface $staticUrisProvider,
38+
) {
39+
parent::__construct();
40+
}
41+
42+
protected function configure(): void
43+
{
44+
$this
45+
->setDescription('Generates static pages')
46+
->addOption(
47+
'filterPattern',
48+
null,
49+
InputOption::VALUE_REQUIRED,
50+
'A pattern to filter the pages to generate',
51+
);
52+
}
53+
54+
protected function execute(InputInterface $input, OutputInterface $output): int
55+
{
56+
$io = new SymfonyStyle($input, $output);
57+
$filterPattern = $input->getOption('filterPattern');
58+
59+
foreach ($this->staticUrisProvider->provide() as $uri) {
60+
if (null !== $filterPattern && !preg_match($filterPattern, $uri)) {
61+
continue;
62+
}
63+
64+
try {
65+
['content' => $content, 'format' => $format] = $this->staticPagesGenerator->generate($filterPattern);
66+
} catch (RuntimeException $e) {
67+
// io error
68+
}
69+
70+
$this->staticPageDumper->dump($uri, $content, $format);
71+
72+
// $output-> $uri
73+
}
74+
75+
return Command::SUCCESS;
76+
}
77+
}

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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
/**
15+
* @author Thomas Bibaut
16+
*/
17+
class RuntimeException extends \RuntimeException implements ExceptionInterface
18+
{
19+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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\Filesystem\Filesystem;
16+
17+
final readonly class FilesystemStaticPageDumper implements StaticPageDumperInterface
18+
{
19+
private Filesystem $filesystem;
20+
21+
public function __construct(
22+
private string $projectDir,
23+
) {
24+
$this->filesystem = new Filesystem();
25+
}
26+
27+
public function dump(string $uri, string $content, ?string $format): void
28+
{
29+
if ($format && !str_ends_with($uri, '.' . $format)) {
30+
$uri = sprintf('%s.%s', $uri, $format);
31+
}
32+
33+
// @todo no .. in $uri
34+
// @todo config
35+
$staticPagesDirectory = sprintf('%s/static-pages', $this->getPublicDirectory());
36+
37+
$this->filesystem->dumpFile(sprintf('%s/%s', $staticPagesDirectory, $uri), $content);
38+
}
39+
40+
private function getPublicDirectory(): string
41+
{
42+
$defaultPublicDir = 'public';
43+
$composerFilePath = sprintf('%s/composer.json', $this->projectDir);
44+
45+
if (!file_exists($composerFilePath)) {
46+
return $defaultPublicDir;
47+
}
48+
49+
$composerConfig = json_decode($this->filesystem->readFile($composerFilePath), true, flags: \JSON_THROW_ON_ERROR);
50+
51+
return $composerConfig['extra']['public-dir'] ?? $defaultPublicDir;
52+
}
53+
}
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: 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
22+
*/
23+
final readonly class StaticPagesGenerator
24+
{
25+
public function __construct(
26+
private HttpKernelInterface $kernel,
27+
) {
28+
}
29+
30+
/**
31+
* @todo write doc everywhere
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+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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\Tests\StaticSiteGeneration;
13+
14+
use PHPUnit\Framework\TestCase;
15+
16+
/*
17+
* test example @see src/Symfony/Component/JsonEncoder/Tests/JsonEncoderTest.php
18+
*/
19+
class FilesystemStaticPageDumperTest extends TestCase
20+
{
21+
}

0 commit comments

Comments
 (0)