vendor/symfony/config/Builder/ConfigBuilderGenerator.php line 54

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Config\Builder;
  11. use Symfony\Component\Config\Definition\ArrayNode;
  12. use Symfony\Component\Config\Definition\BaseNode;
  13. use Symfony\Component\Config\Definition\BooleanNode;
  14. use Symfony\Component\Config\Definition\ConfigurationInterface;
  15. use Symfony\Component\Config\Definition\EnumNode;
  16. use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
  17. use Symfony\Component\Config\Definition\FloatNode;
  18. use Symfony\Component\Config\Definition\IntegerNode;
  19. use Symfony\Component\Config\Definition\NodeInterface;
  20. use Symfony\Component\Config\Definition\PrototypedArrayNode;
  21. use Symfony\Component\Config\Definition\ScalarNode;
  22. use Symfony\Component\Config\Definition\VariableNode;
  23. use Symfony\Component\Config\Loader\ParamConfigurator;
  24. /**
  25.  * Generate ConfigBuilders to help create valid config.
  26.  *
  27.  * @author Tobias Nyholm <tobias.nyholm@gmail.com>
  28.  */
  29. class ConfigBuilderGenerator implements ConfigBuilderGeneratorInterface
  30. {
  31.     /**
  32.      * @var ClassBuilder[]
  33.      */
  34.     private array $classes = [];
  35.     private string $outputDir;
  36.     public function __construct(string $outputDir)
  37.     {
  38.         $this->outputDir $outputDir;
  39.     }
  40.     /**
  41.      * @return \Closure that will return the root config class
  42.      */
  43.     public function build(ConfigurationInterface $configuration): \Closure
  44.     {
  45.         $this->classes = [];
  46.         $rootNode $configuration->getConfigTreeBuilder()->buildTree();
  47.         $rootClass = new ClassBuilder('Symfony\\Config'$rootNode->getName());
  48.         $path $this->getFullPath($rootClass);
  49.         if (!is_file($path)) {
  50.             // Generate the class if the file not exists
  51.             $this->classes[] = $rootClass;
  52.             $this->buildNode($rootNode$rootClass$this->getSubNamespace($rootClass));
  53.             $rootClass->addImplements(ConfigBuilderInterface::class);
  54.             $rootClass->addMethod('getExtensionAlias''
  55. public function NAME(): string
  56. {
  57.     return \'ALIAS\';
  58. }', ['ALIAS' => $rootNode->getPath()]);
  59.             $this->writeClasses();
  60.         }
  61.         return function () use ($path$rootClass) {
  62.             require_once $path;
  63.             $className $rootClass->getFqcn();
  64.             return new $className();
  65.         };
  66.     }
  67.     private function getFullPath(ClassBuilder $class): string
  68.     {
  69.         $directory $this->outputDir.\DIRECTORY_SEPARATOR.$class->getDirectory();
  70.         if (!is_dir($directory)) {
  71.             @mkdir($directory0777true);
  72.         }
  73.         return $directory.\DIRECTORY_SEPARATOR.$class->getFilename();
  74.     }
  75.     private function writeClasses(): void
  76.     {
  77.         foreach ($this->classes as $class) {
  78.             $this->buildConstructor($class);
  79.             $this->buildToArray($class);
  80.             if ($class->getProperties()) {
  81.                 $class->addProperty('_usedProperties'null'[]');
  82.             }
  83.             $this->buildSetExtraKey($class);
  84.             file_put_contents($this->getFullPath($class), $class->build());
  85.         }
  86.         $this->classes = [];
  87.     }
  88.     private function buildNode(NodeInterface $nodeClassBuilder $classstring $namespace): void
  89.     {
  90.         if (!$node instanceof ArrayNode) {
  91.             throw new \LogicException('The node was expected to be an ArrayNode. This Configuration includes an edge case not supported yet.');
  92.         }
  93.         foreach ($node->getChildren() as $child) {
  94.             switch (true) {
  95.                 case $child instanceof ScalarNode:
  96.                     $this->handleScalarNode($child$class);
  97.                     break;
  98.                 case $child instanceof PrototypedArrayNode:
  99.                     $this->handlePrototypedArrayNode($child$class$namespace);
  100.                     break;
  101.                 case $child instanceof VariableNode:
  102.                     $this->handleVariableNode($child$class);
  103.                     break;
  104.                 case $child instanceof ArrayNode:
  105.                     $this->handleArrayNode($child$class$namespace);
  106.                     break;
  107.                 default:
  108.                     throw new \RuntimeException(sprintf('Unknown node "%s".'\get_class($child)));
  109.             }
  110.         }
  111.     }
  112.     private function handleArrayNode(ArrayNode $nodeClassBuilder $classstring $namespace): void
  113.     {
  114.         $childClass = new ClassBuilder($namespace$node->getName());
  115.         $childClass->setAllowExtraKeys($node->shouldIgnoreExtraKeys());
  116.         $class->addRequire($childClass);
  117.         $this->classes[] = $childClass;
  118.         $hasNormalizationClosures $this->hasNormalizationClosures($node);
  119.         $comment $this->getComment($node);
  120.         if ($hasNormalizationClosures) {
  121.             $comment .= sprintf(' * @return %s|$this'."\n "$childClass->getFqcn());
  122.         }
  123.         if ('' !== $comment) {
  124.             $comment "/**\n$comment*/\n";
  125.         }
  126.         $property $class->addProperty(
  127.             $node->getName(),
  128.             $this->getType($childClass->getFqcn(), $hasNormalizationClosures)
  129.         );
  130.         $body $hasNormalizationClosures '
  131. COMMENTpublic function NAME(mixed $value = []): CLASS|static
  132. {
  133.     if (!\is_array($value)) {
  134.         $this->_usedProperties[\'PROPERTY\'] = true;
  135.         $this->PROPERTY = $value;
  136.         return $this;
  137.     }
  138.     if (!$this->PROPERTY instanceof CLASS) {
  139.         $this->_usedProperties[\'PROPERTY\'] = true;
  140.         $this->PROPERTY = new CLASS($value);
  141.     } elseif (0 < \func_num_args()) {
  142.         throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
  143.     }
  144.     return $this->PROPERTY;
  145. }' '
  146. COMMENTpublic function NAME(array $value = []): CLASS
  147. {
  148.     if (null === $this->PROPERTY) {
  149.         $this->_usedProperties[\'PROPERTY\'] = true;
  150.         $this->PROPERTY = new CLASS($value);
  151.     } elseif (0 < \func_num_args()) {
  152.         throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
  153.     }
  154.     return $this->PROPERTY;
  155. }';
  156.         $class->addUse(InvalidConfigurationException::class);
  157.         $class->addMethod($node->getName(), $body, ['COMMENT' => $comment'PROPERTY' => $property->getName(), 'CLASS' => $childClass->getFqcn()]);
  158.         $this->buildNode($node$childClass$this->getSubNamespace($childClass));
  159.     }
  160.     private function handleVariableNode(VariableNode $nodeClassBuilder $class): void
  161.     {
  162.         $comment $this->getComment($node);
  163.         $property $class->addProperty($node->getName());
  164.         $class->addUse(ParamConfigurator::class);
  165.         $body '
  166. /**
  167. COMMENT *
  168.  * @return $this
  169.  */
  170. public function NAME(mixed $valueDEFAULT): static
  171. {
  172.     $this->_usedProperties[\'PROPERTY\'] = true;
  173.     $this->PROPERTY = $value;
  174.     return $this;
  175. }';
  176.         $class->addMethod($node->getName(), $body, [
  177.             'PROPERTY' => $property->getName(),
  178.             'COMMENT' => $comment,
  179.             'DEFAULT' => $node->hasDefaultValue() ? ' = '.var_export($node->getDefaultValue(), true) : '',
  180.         ]);
  181.     }
  182.     private function handlePrototypedArrayNode(PrototypedArrayNode $nodeClassBuilder $classstring $namespace): void
  183.     {
  184.         $name $this->getSingularName($node);
  185.         $prototype $node->getPrototype();
  186.         $methodName $name;
  187.         $hasNormalizationClosures $this->hasNormalizationClosures($node) || $this->hasNormalizationClosures($prototype);
  188.         $parameterType $this->getParameterType($prototype);
  189.         if (null !== $parameterType || $prototype instanceof ScalarNode) {
  190.             $class->addUse(ParamConfigurator::class);
  191.             $property $class->addProperty($node->getName());
  192.             if (null === $key $node->getKeyAttribute()) {
  193.                 // This is an array of values; don't use singular name
  194.                 $body '
  195. /**
  196.  * @param PHPDOC_TYPE $value
  197.  *
  198.  * @return $this
  199.  */
  200. public function NAME(TYPE $value): static
  201. {
  202.     $this->_usedProperties[\'PROPERTY\'] = true;
  203.     $this->PROPERTY = $value;
  204.     return $this;
  205. }';
  206.                 $class->addMethod($node->getName(), $body, [
  207.                     'PROPERTY' => $property->getName(),
  208.                     'TYPE' => $hasNormalizationClosures 'mixed' 'ParamConfigurator|array',
  209.                     'PHPDOC_TYPE' => $hasNormalizationClosures 'mixed' sprintf('ParamConfigurator|list<ParamConfigurator|%s>''' === $parameterType 'mixed' $parameterType),
  210.                 ]);
  211.             } else {
  212.                 $body '
  213. /**
  214.  * @return $this
  215.  */
  216. public function NAME(string $VAR, TYPE $VALUE): static
  217. {
  218.     $this->_usedProperties[\'PROPERTY\'] = true;
  219.     $this->PROPERTY[$VAR] = $VALUE;
  220.     return $this;
  221. }';
  222.                 $class->addMethod($methodName$body, [
  223.                     'PROPERTY' => $property->getName(),
  224.                     'TYPE' => $hasNormalizationClosures || '' === $parameterType 'mixed' 'ParamConfigurator|'.$parameterType,
  225.                     'VAR' => '' === $key 'key' $key,
  226.                     'VALUE' => 'value' === $key 'data' 'value',
  227.                 ]);
  228.             }
  229.             return;
  230.         }
  231.         $childClass = new ClassBuilder($namespace$name);
  232.         if ($prototype instanceof ArrayNode) {
  233.             $childClass->setAllowExtraKeys($prototype->shouldIgnoreExtraKeys());
  234.         }
  235.         $class->addRequire($childClass);
  236.         $this->classes[] = $childClass;
  237.         $property $class->addProperty(
  238.             $node->getName(),
  239.             $this->getType($childClass->getFqcn().'[]'$hasNormalizationClosures)
  240.         );
  241.         $comment $this->getComment($node);
  242.         if ($hasNormalizationClosures) {
  243.             $comment .= sprintf(' * @return %s|$this'."\n "$childClass->getFqcn());
  244.         }
  245.         if ('' !== $comment) {
  246.             $comment "/**\n$comment*/\n";
  247.         }
  248.         if (null === $key $node->getKeyAttribute()) {
  249.             $body $hasNormalizationClosures '
  250. COMMENTpublic function NAME(mixed $value = []): CLASS|static
  251. {
  252.     $this->_usedProperties[\'PROPERTY\'] = true;
  253.     if (!\is_array($value)) {
  254.         $this->PROPERTY[] = $value;
  255.         return $this;
  256.     }
  257.     return $this->PROPERTY[] = new CLASS($value);
  258. }' '
  259. COMMENTpublic function NAME(array $value = []): CLASS
  260. {
  261.     $this->_usedProperties[\'PROPERTY\'] = true;
  262.     return $this->PROPERTY[] = new CLASS($value);
  263. }';
  264.             $class->addMethod($methodName$body, ['COMMENT' => $comment'PROPERTY' => $property->getName(), 'CLASS' => $childClass->getFqcn()]);
  265.         } else {
  266.             $body $hasNormalizationClosures '
  267. COMMENTpublic function NAME(string $VAR, mixed $VALUE = []): CLASS|static
  268. {
  269.     if (!\is_array($VALUE)) {
  270.         $this->_usedProperties[\'PROPERTY\'] = true;
  271.         $this->PROPERTY[$VAR] = $VALUE;
  272.         return $this;
  273.     }
  274.     if (!isset($this->PROPERTY[$VAR]) || !$this->PROPERTY[$VAR] instanceof CLASS) {
  275.         $this->_usedProperties[\'PROPERTY\'] = true;
  276.         $this->PROPERTY[$VAR] = new CLASS($VALUE);
  277.     } elseif (1 < \func_num_args()) {
  278.         throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
  279.     }
  280.     return $this->PROPERTY[$VAR];
  281. }' '
  282. COMMENTpublic function NAME(string $VAR, array $VALUE = []): CLASS
  283. {
  284.     if (!isset($this->PROPERTY[$VAR])) {
  285.         $this->_usedProperties[\'PROPERTY\'] = true;
  286.         $this->PROPERTY[$VAR] = new CLASS($VALUE);
  287.     } elseif (1 < \func_num_args()) {
  288.         throw new InvalidConfigurationException(\'The node created by "NAME()" has already been initialized. You cannot pass values the second time you call NAME().\');
  289.     }
  290.     return $this->PROPERTY[$VAR];
  291. }';
  292.             $class->addUse(InvalidConfigurationException::class);
  293.             $class->addMethod($methodName$body, [
  294.                 'COMMENT' => $comment'PROPERTY' => $property->getName(),
  295.                 'CLASS' => $childClass->getFqcn(),
  296.                 'VAR' => '' === $key 'key' $key,
  297.                 'VALUE' => 'value' === $key 'data' 'value',
  298.             ]);
  299.         }
  300.         $this->buildNode($prototype$childClass$namespace.'\\'.$childClass->getName());
  301.     }
  302.     private function handleScalarNode(ScalarNode $nodeClassBuilder $class): void
  303.     {
  304.         $comment $this->getComment($node);
  305.         $property $class->addProperty($node->getName());
  306.         $class->addUse(ParamConfigurator::class);
  307.         $body '
  308. /**
  309. COMMENT * @return $this
  310.  */
  311. public function NAME($value): static
  312. {
  313.     $this->_usedProperties[\'PROPERTY\'] = true;
  314.     $this->PROPERTY = $value;
  315.     return $this;
  316. }';
  317.         $class->addMethod($node->getName(), $body, ['PROPERTY' => $property->getName(), 'COMMENT' => $comment]);
  318.     }
  319.     private function getParameterType(NodeInterface $node): ?string
  320.     {
  321.         if ($node instanceof BooleanNode) {
  322.             return 'bool';
  323.         }
  324.         if ($node instanceof IntegerNode) {
  325.             return 'int';
  326.         }
  327.         if ($node instanceof FloatNode) {
  328.             return 'float';
  329.         }
  330.         if ($node instanceof EnumNode) {
  331.             return '';
  332.         }
  333.         if ($node instanceof PrototypedArrayNode && $node->getPrototype() instanceof ScalarNode) {
  334.             // This is just an array of variables
  335.             return 'array';
  336.         }
  337.         if ($node instanceof VariableNode) {
  338.             // mixed
  339.             return '';
  340.         }
  341.         return null;
  342.     }
  343.     private function getComment(BaseNode $node): string
  344.     {
  345.         $comment '';
  346.         if ('' !== $info = (string) $node->getInfo()) {
  347.             $comment .= ' * '.$info."\n";
  348.         }
  349.         if (!$node instanceof ArrayNode) {
  350.             foreach ((array) ($node->getExample() ?? []) as $example) {
  351.                 $comment .= ' * @example '.$example."\n";
  352.             }
  353.             if ('' !== $default $node->getDefaultValue()) {
  354.                 $comment .= ' * @default '.(null === $default 'null' var_export($defaulttrue))."\n";
  355.             }
  356.             if ($node instanceof EnumNode) {
  357.                 $comment .= sprintf(' * @param ParamConfigurator|%s $value'implode('|'array_map(function ($a) {
  358.                     return var_export($atrue);
  359.                 }, $node->getValues())))."\n";
  360.             } else {
  361.                 $parameterType $this->getParameterType($node);
  362.                 if (null === $parameterType || '' === $parameterType) {
  363.                     $parameterType 'mixed';
  364.                 }
  365.                 $comment .= ' * @param ParamConfigurator|'.$parameterType.' $value'."\n";
  366.             }
  367.         } else {
  368.             foreach ((array) ($node->getExample() ?? []) as $example) {
  369.                 $comment .= ' * @example '.json_encode($example)."\n";
  370.             }
  371.             if ($node->hasDefaultValue() && [] != $default $node->getDefaultValue()) {
  372.                 $comment .= ' * @default '.json_encode($default)."\n";
  373.             }
  374.         }
  375.         if ($node->isDeprecated()) {
  376.             $comment .= ' * @deprecated '.$node->getDeprecation($node->getName(), $node->getParent()->getName())['message']."\n";
  377.         }
  378.         return $comment;
  379.     }
  380.     /**
  381.      * Pick a good singular name.
  382.      */
  383.     private function getSingularName(PrototypedArrayNode $node): string
  384.     {
  385.         $name $node->getName();
  386.         if (!str_ends_with($name's')) {
  387.             return $name;
  388.         }
  389.         $parent $node->getParent();
  390.         $mappings $parent instanceof ArrayNode $parent->getXmlRemappings() : [];
  391.         foreach ($mappings as $map) {
  392.             if ($map[1] === $name) {
  393.                 $name $map[0];
  394.                 break;
  395.             }
  396.         }
  397.         return $name;
  398.     }
  399.     private function buildToArray(ClassBuilder $class): void
  400.     {
  401.         $body '$output = [];';
  402.         foreach ($class->getProperties() as $p) {
  403.             $code '$this->PROPERTY';
  404.             if (null !== $p->getType()) {
  405.                 if ($p->isArray()) {
  406.                     $code $p->areScalarsAllowed()
  407.                         ? 'array_map(function ($v) { return $v instanceof CLASS ? $v->toArray() : $v; }, $this->PROPERTY)'
  408.                         'array_map(function ($v) { return $v->toArray(); }, $this->PROPERTY)'
  409.                     ;
  410.                 } else {
  411.                     $code $p->areScalarsAllowed()
  412.                         ? '$this->PROPERTY instanceof CLASS ? $this->PROPERTY->toArray() : $this->PROPERTY'
  413.                         '$this->PROPERTY->toArray()'
  414.                     ;
  415.                 }
  416.             }
  417.             $body .= strtr('
  418.     if (isset($this->_usedProperties[\'PROPERTY\'])) {
  419.         $output[\'ORG_NAME\'] = '.$code.';
  420.     }', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName(), 'CLASS' => $p->getType()]);
  421.         }
  422.         $extraKeys $class->shouldAllowExtraKeys() ? ' + $this->_extraKeys' '';
  423.         $class->addMethod('toArray''
  424. public function NAME(): array
  425. {
  426.     '.$body.'
  427.     return $output'.$extraKeys.';
  428. }');
  429.     }
  430.     private function buildConstructor(ClassBuilder $class): void
  431.     {
  432.         $body '';
  433.         foreach ($class->getProperties() as $p) {
  434.             $code '$value[\'ORG_NAME\']';
  435.             if (null !== $p->getType()) {
  436.                 if ($p->isArray()) {
  437.                     $code $p->areScalarsAllowed()
  438.                         ? 'array_map(function ($v) { return \is_array($v) ? new '.$p->getType().'($v) : $v; }, $value[\'ORG_NAME\'])'
  439.                         'array_map(function ($v) { return new '.$p->getType().'($v); }, $value[\'ORG_NAME\'])'
  440.                     ;
  441.                 } else {
  442.                     $code $p->areScalarsAllowed()
  443.                         ? '\is_array($value[\'ORG_NAME\']) ? new '.$p->getType().'($value[\'ORG_NAME\']) : $value[\'ORG_NAME\']'
  444.                         'new '.$p->getType().'($value[\'ORG_NAME\'])'
  445.                     ;
  446.                 }
  447.             }
  448.             $body .= strtr('
  449.     if (array_key_exists(\'ORG_NAME\', $value)) {
  450.         $this->_usedProperties[\'PROPERTY\'] = true;
  451.         $this->PROPERTY = '.$code.';
  452.         unset($value[\'ORG_NAME\']);
  453.     }
  454. ', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName()]);
  455.         }
  456.         if ($class->shouldAllowExtraKeys()) {
  457.             $body .= '
  458.     $this->_extraKeys = $value;
  459. ';
  460.         } else {
  461.             $body .= '
  462.     if ([] !== $value) {
  463.         throw new InvalidConfigurationException(sprintf(\'The following keys are not supported by "%s": \', __CLASS__).implode(\', \', array_keys($value)));
  464.     }';
  465.             $class->addUse(InvalidConfigurationException::class);
  466.         }
  467.         $class->addMethod('__construct''
  468. public function __construct(array $value = [])
  469. {'.$body.'
  470. }');
  471.     }
  472.     private function buildSetExtraKey(ClassBuilder $class): void
  473.     {
  474.         if (!$class->shouldAllowExtraKeys()) {
  475.             return;
  476.         }
  477.         $class->addUse(ParamConfigurator::class);
  478.         $class->addProperty('_extraKeys');
  479.         $class->addMethod('set''
  480. /**
  481.  * @param ParamConfigurator|mixed $value
  482.  *
  483.  * @return $this
  484.  */
  485. public function NAME(string $key, mixed $value): static
  486. {
  487.     $this->_extraKeys[$key] = $value;
  488.     return $this;
  489. }');
  490.     }
  491.     private function getSubNamespace(ClassBuilder $rootClass): string
  492.     {
  493.         return sprintf('%s\\%s'$rootClass->getNamespace(), substr($rootClass->getName(), 0, -6));
  494.     }
  495.     private function hasNormalizationClosures(NodeInterface $node): bool
  496.     {
  497.         try {
  498.             $r = new \ReflectionProperty($node'normalizationClosures');
  499.         } catch (\ReflectionException) {
  500.             return false;
  501.         }
  502.         $r->setAccessible(true);
  503.         return [] !== $r->getValue($node);
  504.     }
  505.     private function getType(string $classTypebool $hasNormalizationClosures): string
  506.     {
  507.         return $classType.($hasNormalizationClosures '|scalar' '');
  508.     }
  509. }