Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 44bb691

Browse files
feature #40600 [Config][DependencyInjection] Add configuration builder for writing PHP config (Nyholm)
This PR was squashed before being merged into the 5.3-dev branch. Discussion ---------- [Config][DependencyInjection] Add configuration builder for writing PHP config | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | | License | MIT | Doc PR | symfony/symfony-docs#15181 I've spent most part of today to generate this PR. It is far from complete but it is ready for review. This PR will build classes and store them in the build_dir. The classes will help you write PHP config. It will basically generate an array for you. ### Before ```php // config/packages/security.php <?php use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $container) { $array = [ 'firewalls' => [ 'main' => [ 'pattern' => '^/*', 'lazy' => true, 'anonymous' => [], ], ], 'access_control' => [ [ 'path' => '^/user', 'roles' => [ 0 => 'ROLE_USER', ], ], [ 'path' => '^/admin', 'roles' => 'ROLE_ADMIN', ], ], 'role_hierarchy' => [ 'ROLE_ADMIN' => ['ROLE_USER'], 'ROLE_SUPER_ADMIN' => ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH', ], ], ]; $container->extension('security', $array); } ``` ### After ```php // config/packages/security.php <?php use Symfony\Config\SecurityConfig; return static function (SecurityConfig $security) { $security ->roleHierarchy('ROLE_ADMIN', ['ROLE_USER']) ->roleHierarchy('ROLE_SUPER_ADMIN', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']) ->accessControl() ->path('^/user') ->role('ROLE_USER'); $security->accessControl(['path' => '^/admin', 'roles' => 'ROLE_ADMIN']); $security->firewall('main') ->pattern('^/*') ->lazy(true) ->anonymous(); }; ``` ### About autogeneration This PR is generating the extension's `ConfigBuilder`s when they are first used. Since the PR is already very large, I prefer to follow up with additional PRs to include a cache warmer and command to rebuild the `ConfigBuilder`s. The generated `ConfigBuilder` uses a "ucfirst() camelCased" extension alias. If the alias is `acme_foo` the root `ConfigBuilder` will be `Symfony\Config\AcmeFooConfig`. The recommended way of using this class is: ```php // config/packages/acme_foo.php use Symfony\Config\AcmeFooConfig; return static function (AcmeFooConfig $foo) { // ... // No need to return } ``` One may also init the class directly, But this will not help you with generation or autoloading ```php // config/packages/acme_foo.php use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $container) { $foo = new \Symfony\Config\AcmeFooConfig(); // ... $container->extension('acme_foo', $foo->toArray()); } ``` **I do think we should only talk about the first way** If a third party bundle like this idea and want to provide their own `ConfigBuilder`, they have two options: 1) Create the class `Symfony\Config\TheBundleConfig` themselves and make sure they configure composer to autoload that file and that the class implements `ConfigBuilderInterface`. We will never regenerate a file that already exists. 2) Create any class implementing `ConfigBuilderInterface` and ask their users to use that class in their config in the same way they would use `Symfony\Config\TheBundleConfig`. The first way is obviously the smoothest way of doing things. ### BC There is a great discussion about backwards compatibility for the generated files. We can assure that the class generator don't introduce a BC break with our tests. However, if the bundle changes their configuration definition it may break BC. Things like renaming, changing type or changing a single value to array is obvious BC breaks, however, these can be fixed in the config definition with normalisation. The generator does not support normalisation. It is way way more complicated to reverse engineer that. I think a future update could fix this in one of two ways: 1) Add extra definition rules to help the class generator 2) Allow the bundle to patch part of the generated code I hate BC breaks as much as the next person, but all the BC breaks in the generated classes will be caught when building the container (not at runtime), so I am fine with not having a 100% complete solution for this issue in the initial PR. ### Other limitations If a bundle is using a custom extension alias, then we cannot guess it.. so a user have to use a cache warmer because we cannot generate the `ConfigBuilder` on the fly. ### TODO - [x] Add tests - [x] Update changelog - [x] Write documentation ------------- The generated code can be found in this example app: https://github.com/Nyholm/sf-issue-40600/tree/main/var/cache/dev/Symfony/Config Commits ------- 460b46f [Config][DependencyInjection] Add configuration builder for writing PHP config
2 parents 48a191c + 460b46f commit 44bb691

28 files changed

+1290
-3
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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\Config\Builder;
13+
14+
/**
15+
* Build PHP classes to generate config.
16+
*
17+
* @internal
18+
*
19+
* @author Tobias Nyholm <[email protected]>
20+
*/
21+
class ClassBuilder
22+
{
23+
/** @var string */
24+
private $namespace;
25+
26+
/** @var string */
27+
private $name;
28+
29+
/** @var Property[] */
30+
private $properties = [];
31+
32+
/** @var Method[] */
33+
private $methods = [];
34+
private $require = [];
35+
private $implements = [];
36+
37+
public function __construct(string $namespace, string $name)
38+
{
39+
$this->namespace = $namespace;
40+
$this->name = ucfirst($this->camelCase($name)).'Config';
41+
}
42+
43+
public function getDirectory(): string
44+
{
45+
return str_replace('\\', \DIRECTORY_SEPARATOR, $this->namespace);
46+
}
47+
48+
public function getFilename(): string
49+
{
50+
return $this->name.'.php';
51+
}
52+
53+
public function build(): string
54+
{
55+
$rootPath = explode(\DIRECTORY_SEPARATOR, $this->getDirectory());
56+
$require = '';
57+
foreach ($this->require as $class) {
58+
// figure out relative path.
59+
$path = explode(\DIRECTORY_SEPARATOR, $class->getDirectory());
60+
$path[] = $class->getFilename();
61+
foreach ($rootPath as $key => $value) {
62+
if ($path[$key] !== $value) {
63+
break;
64+
}
65+
unset($path[$key]);
66+
}
67+
$require .= sprintf('require_once __DIR__.\'%s\';', \DIRECTORY_SEPARATOR.implode(\DIRECTORY_SEPARATOR, $path))."\n";
68+
}
69+
70+
$implements = [] === $this->implements ? '' : 'implements '.implode(', ', $this->implements);
71+
$body = '';
72+
foreach ($this->properties as $property) {
73+
$body .= ' '.$property->getContent()."\n";
74+
}
75+
foreach ($this->methods as $method) {
76+
$lines = explode("\n", $method->getContent());
77+
foreach ($lines as $i => $line) {
78+
$body .= ' '.$line."\n";
79+
}
80+
}
81+
82+
$content = strtr('<?php
83+
84+
namespace NAMESPACE;
85+
86+
REQUIRE
87+
88+
/**
89+
* This class is automatically generated to help creating config.
90+
*
91+
* @experimental in 5.3
92+
*/
93+
class CLASS IMPLEMENTS
94+
{
95+
BODY
96+
}
97+
', ['NAMESPACE' => $this->namespace, 'REQUIRE' => $require, 'CLASS' => $this->getName(), 'IMPLEMENTS' => $implements, 'BODY' => $body]);
98+
99+
return $content;
100+
}
101+
102+
public function addRequire(self $class)
103+
{
104+
$this->require[] = $class;
105+
}
106+
107+
public function addImplements(string $interface)
108+
{
109+
$this->implements[] = '\\'.ltrim($interface, '\\');
110+
}
111+
112+
public function addMethod(string $name, string $body, array $params = []): void
113+
{
114+
$this->methods[] = new Method(strtr($body, ['NAME' => $this->camelCase($name)] + $params));
115+
}
116+
117+
public function addProperty(string $name, string $classType = null): Property
118+
{
119+
$property = new Property($name, $this->camelCase($name));
120+
if (null !== $classType) {
121+
$property->setType($classType);
122+
}
123+
$this->properties[] = $property;
124+
$property->setContent(sprintf('private $%s;', $property->getName()));
125+
126+
return $property;
127+
}
128+
129+
public function getProperties(): array
130+
{
131+
return $this->properties;
132+
}
133+
134+
private function camelCase(string $input): string
135+
{
136+
$output = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input))));
137+
138+
return preg_replace('#\W#', '', $output);
139+
}
140+
141+
public function getName(): string
142+
{
143+
return $this->name;
144+
}
145+
146+
public function getNamespace(): string
147+
{
148+
return $this->namespace;
149+
}
150+
151+
public function getFqcn()
152+
{
153+
return '\\'.$this->namespace.'\\'.$this->name;
154+
}
155+
}

0 commit comments

Comments
 (0)