Skip to content

Commit 003b6f9

Browse files
committed
Generate JSON schema for config
1 parent 1e74e69 commit 003b6f9

File tree

14 files changed

+996
-68
lines changed

14 files changed

+996
-68
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ CHANGELOG
1212
* Add `RateLimiterFactoryInterface` as an alias of the `limiter` service
1313
* Add `framework.validation.disable_translation` option
1414
* Add support for signal plain name in the `messenger.stop_worker_on_signals` configuration
15+
* Generate JSON schema for YAML configuration
1516

1617
7.2
1718
---
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Bundle\FrameworkBundle\CacheWarmer;
13+
14+
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Config\Definition\ArrayNode;
16+
use Symfony\Component\Config\Definition\ConfigurationInterface;
17+
use Symfony\Component\Config\Definition\JsonSchemaGenerator;
18+
use Symfony\Component\DependencyInjection\Container;
19+
use Symfony\Component\DependencyInjection\ContainerBuilder;
20+
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
21+
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag;
22+
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
23+
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
24+
use Symfony\Component\HttpKernel\Kernel;
25+
use Symfony\Component\HttpKernel\KernelInterface;
26+
27+
/**
28+
* Generate config json schema.
29+
*/
30+
final readonly class ConfigSchemaCacheWarmer implements CacheWarmerInterface
31+
{
32+
public function __construct(
33+
private KernelInterface $kernel,
34+
private ?LoggerInterface $logger = null,
35+
) {
36+
}
37+
38+
public function warmUp(string $cacheDir, ?string $buildDir = null): array
39+
{
40+
if (!$buildDir || !$this->kernel->isDebug() || !class_exists(JsonSchemaGenerator::class)) {
41+
return [];
42+
}
43+
44+
$generator = new JsonSchemaGenerator(\dirname($this->kernel->getBuildDir()).'/config.schema.json');
45+
46+
if ($this->kernel instanceof Kernel) {
47+
/** @var ContainerBuilder $container */
48+
$container = \Closure::bind(function (Kernel $kernel) {
49+
$containerBuilder = $kernel->getContainerBuilder();
50+
$kernel->prepareContainer($containerBuilder);
51+
52+
return $containerBuilder;
53+
}, null, $this->kernel)($this->kernel);
54+
55+
$extensions = $container->getExtensions();
56+
} else {
57+
$extensions = [];
58+
foreach ($this->kernel->getBundles() as $bundle) {
59+
$extension = $bundle->getContainerExtension();
60+
if (null !== $extension) {
61+
$extensions[] = $extension;
62+
}
63+
}
64+
}
65+
66+
$tree = new ArrayNode($this->kernel->getEnvironment());
67+
foreach ($extensions as $extension) {
68+
try {
69+
$configuration = null;
70+
if ($extension instanceof ConfigurationInterface) {
71+
$configuration = $extension;
72+
} elseif ($extension instanceof ConfigurationExtensionInterface) {
73+
$container = $this->kernel->getContainer();
74+
$configuration = $extension->getConfiguration([], new ContainerBuilder($container instanceof Container ? new ContainerBag($container) : new ParameterBag()));
75+
}
76+
77+
if (!$extensionConfigNode = $configuration?->getConfigTreeBuilder()->buildTree()) {
78+
continue;
79+
}
80+
81+
$tree->addChild($extensionConfigNode);
82+
} catch (\Exception $e) {
83+
$this->logger?->warning('Failed to generate JSON schema for extension {extensionClass}: '.$e->getMessage(), ['exception' => $e, 'extensionClass' => $extension::class]);
84+
}
85+
}
86+
87+
try {
88+
$generator->merge($tree, (object) [
89+
'description' => 'Symfony configuration',
90+
'properties' => (object) [
91+
'when@'.$tree->getName() => ['$ref' => '#/definitions/'.$tree->getName()],
92+
],
93+
'patternProperties' => (object) [
94+
'when@[a-zA-Z0-9]+' => ['$ref' => '#/'],
95+
],
96+
]);
97+
} catch (\Exception $e) {
98+
$this->logger?->warning('Failed to generate JSON schema for the configuration: '.$e->getMessage(), ['exception' => $e]);
99+
}
100+
101+
// No need to preload anything
102+
return [];
103+
}
104+
105+
public function isOptional(): bool
106+
{
107+
return true;
108+
}
109+
110+
private function getRelativePath(string $path, string $projectDir): string
111+
{
112+
return './'.ltrim(substr($path, strspn($path ^ $projectDir, "\0")), '/');
113+
}
114+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Psr\Clock\ClockInterface as PsrClockInterface;
1515
use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;
1616
use Symfony\Bundle\FrameworkBundle\CacheWarmer\ConfigBuilderCacheWarmer;
17+
use Symfony\Bundle\FrameworkBundle\CacheWarmer\ConfigSchemaCacheWarmer;
1718
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
1819
use Symfony\Component\Clock\Clock;
1920
use Symfony\Component\Clock\ClockInterface;
@@ -233,6 +234,10 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : []
233234
->args([service(KernelInterface::class), service('logger')->nullOnInvalid()])
234235
->tag('kernel.cache_warmer')
235236

237+
->set('config_schema.warmer', ConfigSchemaCacheWarmer::class)
238+
->args([service(KernelInterface::class), service('logger')->nullOnInvalid()])
239+
->tag('kernel.cache_warmer')
240+
236241
->set('clock', Clock::class)
237242
->alias(ClockInterface::class, 'clock')
238243
->alias(PsrClockInterface::class, 'clock')

src/Symfony/Component/Config/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add `ExprBuilder::ifFalse()`
88
* Add support for info on `ArrayNodeDefinition::canBeEnabled()` and `ArrayNodeDefinition::canBeDisabled()`
9+
* Add `JsonSchemaGenerator`
910

1011
7.2
1112
---

src/Symfony/Component/Config/Definition/BaseNode.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,14 @@ public function addEquivalentValue(mixed $originalValue, mixed $equivalentValue)
167167
$this->equivalentValues[] = [$originalValue, $equivalentValue];
168168
}
169169

170+
/**
171+
* @internal
172+
*/
173+
public function getEquivalentValues(): array
174+
{
175+
return $this->equivalentValues;
176+
}
177+
170178
/**
171179
* Set this node as required.
172180
*/

src/Symfony/Component/Config/Definition/BooleanNode.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ public function __construct(
2929
parent::__construct($name, $parent, $pathSeparator);
3030
}
3131

32+
/**
33+
* @internal
34+
*/
35+
public function isNullable(): bool
36+
{
37+
return $this->nullable;
38+
}
39+
3240
protected function validateType(mixed $value): void
3341
{
3442
if (!\is_bool($value)) {

0 commit comments

Comments
 (0)