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

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