default services conflit ?

This commit is contained in:
armansansd
2022-04-27 11:30:43 +02:00
parent 28190a5749
commit 8bb1064a3b
8132 changed files with 900138 additions and 426 deletions

View File

@@ -0,0 +1,108 @@
CHANGELOG
=========
4.4.0
-----
* added a way to exclude patterns of resources from being imported by the `import()` method
4.3.0
-----
* deprecated using environment variables with `cannotBeEmpty()` if the value is validated with `validate()`
* made `Resource\*` classes final and not implement `Serializable` anymore
* deprecated the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead
4.2.0
-----
* deprecated constructing a `TreeBuilder` without passing root node information
* renamed `FileLoaderLoadException` to `LoaderLoadException`
4.1.0
-----
* added `setPathSeparator` method to `NodeBuilder` class
* added third `$pathSeparator` constructor argument to `BaseNode`
* the `Processor` class has been made final
4.0.0
-----
* removed `ConfigCachePass`
3.4.0
-----
* added `setDeprecated()` method to indicate a deprecated node
* added `XmlUtils::parse()` method to parse an XML string
* deprecated `ConfigCachePass`
3.3.0
-----
* added `ReflectionClassResource` class
* added second `$exists` constructor argument to `ClassExistenceResource`
* made `ClassExistenceResource` work with interfaces and traits
* added `ConfigCachePass` (originally in FrameworkBundle)
* added `castToArray()` helper to turn any config value into an array
3.0.0
-----
* removed `ReferenceDumper` class
* removed the `ResourceInterface::isFresh()` method
* removed `BCResourceInterfaceChecker` class
* removed `ResourceInterface::getResource()` method
2.8.0
-----
The edge case of defining just one value for nodes of type Enum is now allowed:
```php
$rootNode
->children()
->enumNode('variable')
->values(['value'])
->end()
->end()
;
```
Before: `InvalidArgumentException` (variable must contain at least two
distinct elements).
After: the code will work as expected and it will restrict the values of the
`variable` option to just `value`.
* deprecated the `ResourceInterface::isFresh()` method. If you implement custom resource types and they
can be validated that way, make them implement the new `SelfCheckingResourceInterface`.
* deprecated the getResource() method in ResourceInterface. You can still call this method
on concrete classes implementing the interface, but it does not make sense at the interface
level as you need to know about the particular type of resource at hand to understand the
semantics of the returned value.
2.7.0
-----
* added `ConfigCacheInterface`, `ConfigCacheFactoryInterface` and a basic `ConfigCacheFactory`
implementation to delegate creation of ConfigCache instances
2.2.0
-----
* added `ArrayNodeDefinition::canBeEnabled()` and `ArrayNodeDefinition::canBeDisabled()`
to ease configuration when some sections are respectively disabled / enabled
by default.
* added a `normalizeKeys()` method for array nodes (to avoid key normalization)
* added numerical type handling for config definitions
* added convenience methods for optional configuration sections to `ArrayNodeDefinition`
* added a utils class for XML manipulations
2.1.0
-----
* added a way to add documentation on configuration
* implemented `Serializable` on resources
* `LoaderResolverInterface` is now used instead of `LoaderResolver` for type
hinting

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config;
use Symfony\Component\Config\Resource\SelfCheckingResourceChecker;
/**
* ConfigCache caches arbitrary content in files on disk.
*
* When in debug mode, those metadata resources that implement
* \Symfony\Component\Config\Resource\SelfCheckingResourceInterface will
* be used to check cache freshness.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Matthias Pigulla <mp@webfactory.de>
*/
class ConfigCache extends ResourceCheckerConfigCache
{
private $debug;
/**
* @param string $file The absolute cache path
* @param bool $debug Whether debugging is enabled or not
*/
public function __construct(string $file, bool $debug)
{
$this->debug = $debug;
$checkers = [];
if (true === $this->debug) {
$checkers = [new SelfCheckingResourceChecker()];
}
parent::__construct($file, $checkers);
}
/**
* Checks if the cache is still fresh.
*
* This implementation always returns true when debug is off and the
* cache file exists.
*
* @return bool true if the cache is fresh, false otherwise
*/
public function isFresh()
{
if (!$this->debug && is_file($this->getPath())) {
return true;
}
return parent::isFresh();
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config;
/**
* Basic implementation of ConfigCacheFactoryInterface that
* creates an instance of the default ConfigCache.
*
* This factory and/or cache <em>do not</em> support cache validation
* by means of ResourceChecker instances (that is, service-based).
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
class ConfigCacheFactory implements ConfigCacheFactoryInterface
{
private $debug;
/**
* @param bool $debug The debug flag to pass to ConfigCache
*/
public function __construct(bool $debug)
{
$this->debug = $debug;
}
/**
* {@inheritdoc}
*/
public function cache($file, $callback)
{
if (!\is_callable($callback)) {
throw new \InvalidArgumentException(sprintf('Invalid type for callback argument. Expected callable, but got "%s".', \gettype($callback)));
}
$cache = new ConfigCache($file, $this->debug);
if (!$cache->isFresh()) {
$callback($cache);
}
return $cache;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config;
/**
* Interface for a ConfigCache factory. This factory creates
* an instance of ConfigCacheInterface and initializes the
* cache if necessary.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
interface ConfigCacheFactoryInterface
{
/**
* Creates a cache instance and (re-)initializes it if necessary.
*
* @param string $file The absolute cache file path
* @param callable $callable The callable to be executed when the cache needs to be filled (i. e. is not fresh). The cache will be passed as the only parameter to this callback
*
* @return ConfigCacheInterface The cache instance
*/
public function cache($file, $callable);
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config;
use Symfony\Component\Config\Resource\ResourceInterface;
/**
* Interface for ConfigCache.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
interface ConfigCacheInterface
{
/**
* Gets the cache file path.
*
* @return string The cache file path
*/
public function getPath();
/**
* Checks if the cache is still fresh.
*
* This check should take the metadata passed to the write() method into consideration.
*
* @return bool Whether the cache is still fresh
*/
public function isFresh();
/**
* Writes the given content into the cache file. Metadata will be stored
* independently and can be used to check cache freshness at a later time.
*
* @param string $content The content to write into the cache
* @param ResourceInterface[]|null $metadata An array of ResourceInterface instances
*
* @throws \RuntimeException When the cache file cannot be written
*/
public function write($content, array $metadata = null);
}

View File

@@ -0,0 +1,399 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
/**
* Represents an Array node in the config tree.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ArrayNode extends BaseNode implements PrototypeNodeInterface
{
protected $xmlRemappings = [];
protected $children = [];
protected $allowFalse = false;
protected $allowNewKeys = true;
protected $addIfNotSet = false;
protected $performDeepMerging = true;
protected $ignoreExtraKeys = false;
protected $removeExtraKeys = true;
protected $normalizeKeys = true;
public function setNormalizeKeys($normalizeKeys)
{
$this->normalizeKeys = (bool) $normalizeKeys;
}
/**
* {@inheritdoc}
*
* Namely, you mostly have foo_bar in YAML while you have foo-bar in XML.
* After running this method, all keys are normalized to foo_bar.
*
* If you have a mixed key like foo-bar_moo, it will not be altered.
* The key will also not be altered if the target key already exists.
*/
protected function preNormalize($value)
{
if (!$this->normalizeKeys || !\is_array($value)) {
return $value;
}
$normalized = [];
foreach ($value as $k => $v) {
if (str_contains($k, '-') && !str_contains($k, '_') && !\array_key_exists($normalizedKey = str_replace('-', '_', $k), $value)) {
$normalized[$normalizedKey] = $v;
} else {
$normalized[$k] = $v;
}
}
return $normalized;
}
/**
* Retrieves the children of this node.
*
* @return array<string, NodeInterface>
*/
public function getChildren()
{
return $this->children;
}
/**
* Sets the xml remappings that should be performed.
*
* @param array $remappings An array of the form [[string, string]]
*/
public function setXmlRemappings(array $remappings)
{
$this->xmlRemappings = $remappings;
}
/**
* Gets the xml remappings that should be performed.
*
* @return array an array of the form [[string, string]]
*/
public function getXmlRemappings()
{
return $this->xmlRemappings;
}
/**
* Sets whether to add default values for this array if it has not been
* defined in any of the configuration files.
*
* @param bool $boolean
*/
public function setAddIfNotSet($boolean)
{
$this->addIfNotSet = (bool) $boolean;
}
/**
* Sets whether false is allowed as value indicating that the array should be unset.
*
* @param bool $allow
*/
public function setAllowFalse($allow)
{
$this->allowFalse = (bool) $allow;
}
/**
* Sets whether new keys can be defined in subsequent configurations.
*
* @param bool $allow
*/
public function setAllowNewKeys($allow)
{
$this->allowNewKeys = (bool) $allow;
}
/**
* Sets if deep merging should occur.
*
* @param bool $boolean
*/
public function setPerformDeepMerging($boolean)
{
$this->performDeepMerging = (bool) $boolean;
}
/**
* Whether extra keys should just be ignored without an exception.
*
* @param bool $boolean To allow extra keys
* @param bool $remove To remove extra keys
*/
public function setIgnoreExtraKeys($boolean, $remove = true)
{
$this->ignoreExtraKeys = (bool) $boolean;
$this->removeExtraKeys = $this->ignoreExtraKeys && $remove;
}
/**
* {@inheritdoc}
*/
public function setName($name)
{
$this->name = $name;
}
/**
* {@inheritdoc}
*/
public function hasDefaultValue()
{
return $this->addIfNotSet;
}
/**
* {@inheritdoc}
*/
public function getDefaultValue()
{
if (!$this->hasDefaultValue()) {
throw new \RuntimeException(sprintf('The node at path "%s" has no default value.', $this->getPath()));
}
$defaults = [];
foreach ($this->children as $name => $child) {
if ($child->hasDefaultValue()) {
$defaults[$name] = $child->getDefaultValue();
}
}
return $defaults;
}
/**
* Adds a child node.
*
* @throws \InvalidArgumentException when the child node has no name
* @throws \InvalidArgumentException when the child node's name is not unique
*/
public function addChild(NodeInterface $node)
{
$name = $node->getName();
if ('' === $name) {
throw new \InvalidArgumentException('Child nodes must be named.');
}
if (isset($this->children[$name])) {
throw new \InvalidArgumentException(sprintf('A child node named "%s" already exists.', $name));
}
$this->children[$name] = $node;
}
/**
* {@inheritdoc}
*
* @throws UnsetKeyException
* @throws InvalidConfigurationException if the node doesn't have enough children
*/
protected function finalizeValue($value)
{
if (false === $value) {
throw new UnsetKeyException(sprintf('Unsetting key for path "%s", value: "%s".', $this->getPath(), json_encode($value)));
}
foreach ($this->children as $name => $child) {
if (!\array_key_exists($name, $value)) {
if ($child->isRequired()) {
$ex = new InvalidConfigurationException(sprintf('The child node "%s" at path "%s" must be configured.', $name, $this->getPath()));
$ex->setPath($this->getPath());
throw $ex;
}
if ($child->hasDefaultValue()) {
$value[$name] = $child->getDefaultValue();
}
continue;
}
if ($child->isDeprecated()) {
@trigger_error($child->getDeprecationMessage($name, $this->getPath()), \E_USER_DEPRECATED);
}
try {
$value[$name] = $child->finalize($value[$name]);
} catch (UnsetKeyException $e) {
unset($value[$name]);
}
}
return $value;
}
/**
* {@inheritdoc}
*/
protected function validateType($value)
{
if (!\is_array($value) && (!$this->allowFalse || false !== $value)) {
$ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected array, but got %s', $this->getPath(), \gettype($value)));
if ($hint = $this->getInfo()) {
$ex->addHint($hint);
}
$ex->setPath($this->getPath());
throw $ex;
}
}
/**
* {@inheritdoc}
*
* @throws InvalidConfigurationException
*/
protected function normalizeValue($value)
{
if (false === $value) {
return $value;
}
$value = $this->remapXml($value);
$normalized = [];
foreach ($value as $name => $val) {
if (isset($this->children[$name])) {
try {
$normalized[$name] = $this->children[$name]->normalize($val);
} catch (UnsetKeyException $e) {
}
unset($value[$name]);
} elseif (!$this->removeExtraKeys) {
$normalized[$name] = $val;
}
}
// if extra fields are present, throw exception
if (\count($value) && !$this->ignoreExtraKeys) {
$proposals = array_keys($this->children);
sort($proposals);
$guesses = [];
foreach (array_keys($value) as $subject) {
$minScore = \INF;
foreach ($proposals as $proposal) {
$distance = levenshtein($subject, $proposal);
if ($distance <= $minScore && $distance < 3) {
$guesses[$proposal] = $distance;
$minScore = $distance;
}
}
}
$msg = sprintf('Unrecognized option%s "%s" under "%s"', 1 === \count($value) ? '' : 's', implode(', ', array_keys($value)), $this->getPath());
if (\count($guesses)) {
asort($guesses);
$msg .= sprintf('. Did you mean "%s"?', implode('", "', array_keys($guesses)));
} else {
$msg .= sprintf('. Available option%s %s "%s".', 1 === \count($proposals) ? '' : 's', 1 === \count($proposals) ? 'is' : 'are', implode('", "', $proposals));
}
$ex = new InvalidConfigurationException($msg);
$ex->setPath($this->getPath());
throw $ex;
}
return $normalized;
}
/**
* Remaps multiple singular values to a single plural value.
*
* @param array $value The source values
*
* @return array The remapped values
*/
protected function remapXml($value)
{
foreach ($this->xmlRemappings as [$singular, $plural]) {
if (!isset($value[$singular])) {
continue;
}
$value[$plural] = Processor::normalizeConfig($value, $singular, $plural);
unset($value[$singular]);
}
return $value;
}
/**
* {@inheritdoc}
*
* @throws InvalidConfigurationException
* @throws \RuntimeException
*/
protected function mergeValues($leftSide, $rightSide)
{
if (false === $rightSide) {
// if this is still false after the last config has been merged the
// finalization pass will take care of removing this key entirely
return false;
}
if (false === $leftSide || !$this->performDeepMerging) {
return $rightSide;
}
foreach ($rightSide as $k => $v) {
// no conflict
if (!\array_key_exists($k, $leftSide)) {
if (!$this->allowNewKeys) {
$ex = new InvalidConfigurationException(sprintf('You are not allowed to define new elements for path "%s". Please define all elements for this path in one config file. If you are trying to overwrite an element, make sure you redefine it with the same name.', $this->getPath()));
$ex->setPath($this->getPath());
throw $ex;
}
$leftSide[$k] = $v;
continue;
}
if (!isset($this->children[$k])) {
if (!$this->ignoreExtraKeys || $this->removeExtraKeys) {
throw new \RuntimeException('merge() expects a normalized config array.');
}
$leftSide[$k] = $v;
continue;
}
$leftSide[$k] = $this->children[$k]->merge($leftSide[$k], $v);
}
return $leftSide;
}
/**
* {@inheritdoc}
*/
protected function allowPlaceholders(): bool
{
return false;
}
}

View File

@@ -0,0 +1,561 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
/**
* The base node class.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
abstract class BaseNode implements NodeInterface
{
public const DEFAULT_PATH_SEPARATOR = '.';
private static $placeholderUniquePrefixes = [];
private static $placeholders = [];
protected $name;
protected $parent;
protected $normalizationClosures = [];
protected $finalValidationClosures = [];
protected $allowOverwrite = true;
protected $required = false;
protected $deprecationMessage = null;
protected $equivalentValues = [];
protected $attributes = [];
protected $pathSeparator;
private $handlingPlaceholder;
/**
* @throws \InvalidArgumentException if the name contains a period
*/
public function __construct(?string $name, NodeInterface $parent = null, string $pathSeparator = self::DEFAULT_PATH_SEPARATOR)
{
if (str_contains($name = (string) $name, $pathSeparator)) {
throw new \InvalidArgumentException('The name must not contain ".'.$pathSeparator.'".');
}
$this->name = $name;
$this->parent = $parent;
$this->pathSeparator = $pathSeparator;
}
/**
* Register possible (dummy) values for a dynamic placeholder value.
*
* Matching configuration values will be processed with a provided value, one by one. After a provided value is
* successfully processed the configuration value is returned as is, thus preserving the placeholder.
*
* @internal
*/
public static function setPlaceholder(string $placeholder, array $values): void
{
if (!$values) {
throw new \InvalidArgumentException('At least one value must be provided.');
}
self::$placeholders[$placeholder] = $values;
}
/**
* Adds a common prefix for dynamic placeholder values.
*
* Matching configuration values will be skipped from being processed and are returned as is, thus preserving the
* placeholder. An exact match provided by {@see setPlaceholder()} might take precedence.
*
* @internal
*/
public static function setPlaceholderUniquePrefix(string $prefix): void
{
self::$placeholderUniquePrefixes[] = $prefix;
}
/**
* Resets all current placeholders available.
*
* @internal
*/
public static function resetPlaceholders(): void
{
self::$placeholderUniquePrefixes = [];
self::$placeholders = [];
}
/**
* @param string $key
*/
public function setAttribute($key, $value)
{
$this->attributes[$key] = $value;
}
/**
* @param string $key
*
* @return mixed
*/
public function getAttribute($key, $default = null)
{
return $this->attributes[$key] ?? $default;
}
/**
* @param string $key
*
* @return bool
*/
public function hasAttribute($key)
{
return isset($this->attributes[$key]);
}
/**
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
public function setAttributes(array $attributes)
{
$this->attributes = $attributes;
}
/**
* @param string $key
*/
public function removeAttribute($key)
{
unset($this->attributes[$key]);
}
/**
* Sets an info message.
*
* @param string $info
*/
public function setInfo($info)
{
$this->setAttribute('info', $info);
}
/**
* Returns info message.
*
* @return string|null The info text
*/
public function getInfo()
{
return $this->getAttribute('info');
}
/**
* Sets the example configuration for this node.
*
* @param string|array $example
*/
public function setExample($example)
{
$this->setAttribute('example', $example);
}
/**
* Retrieves the example configuration for this node.
*
* @return string|array|null The example
*/
public function getExample()
{
return $this->getAttribute('example');
}
/**
* Adds an equivalent value.
*
* @param mixed $originalValue
* @param mixed $equivalentValue
*/
public function addEquivalentValue($originalValue, $equivalentValue)
{
$this->equivalentValues[] = [$originalValue, $equivalentValue];
}
/**
* Set this node as required.
*
* @param bool $boolean Required node
*/
public function setRequired($boolean)
{
$this->required = (bool) $boolean;
}
/**
* Sets this node as deprecated.
*
* You can use %node% and %path% placeholders in your message to display,
* respectively, the node name and its complete path.
*
* @param string|null $message Deprecated message
*/
public function setDeprecated($message)
{
$this->deprecationMessage = $message;
}
/**
* Sets if this node can be overridden.
*
* @param bool $allow
*/
public function setAllowOverwrite($allow)
{
$this->allowOverwrite = (bool) $allow;
}
/**
* Sets the closures used for normalization.
*
* @param \Closure[] $closures An array of Closures used for normalization
*/
public function setNormalizationClosures(array $closures)
{
$this->normalizationClosures = $closures;
}
/**
* Sets the closures used for final validation.
*
* @param \Closure[] $closures An array of Closures used for final validation
*/
public function setFinalValidationClosures(array $closures)
{
$this->finalValidationClosures = $closures;
}
/**
* {@inheritdoc}
*/
public function isRequired()
{
return $this->required;
}
/**
* Checks if this node is deprecated.
*
* @return bool
*/
public function isDeprecated()
{
return null !== $this->deprecationMessage;
}
/**
* Returns the deprecated message.
*
* @param string $node the configuration node name
* @param string $path the path of the node
*
* @return string
*/
public function getDeprecationMessage($node, $path)
{
return strtr($this->deprecationMessage, ['%node%' => $node, '%path%' => $path]);
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function getPath()
{
if (null !== $this->parent) {
return $this->parent->getPath().$this->pathSeparator.$this->name;
}
return $this->name;
}
/**
* {@inheritdoc}
*/
final public function merge($leftSide, $rightSide)
{
if (!$this->allowOverwrite) {
throw new ForbiddenOverwriteException(sprintf('Configuration path "%s" cannot be overwritten. You have to define all options for this path, and any of its sub-paths in one configuration section.', $this->getPath()));
}
if ($leftSide !== $leftPlaceholders = self::resolvePlaceholderValue($leftSide)) {
foreach ($leftPlaceholders as $leftPlaceholder) {
$this->handlingPlaceholder = $leftSide;
try {
$this->merge($leftPlaceholder, $rightSide);
} finally {
$this->handlingPlaceholder = null;
}
}
return $rightSide;
}
if ($rightSide !== $rightPlaceholders = self::resolvePlaceholderValue($rightSide)) {
foreach ($rightPlaceholders as $rightPlaceholder) {
$this->handlingPlaceholder = $rightSide;
try {
$this->merge($leftSide, $rightPlaceholder);
} finally {
$this->handlingPlaceholder = null;
}
}
return $rightSide;
}
$this->doValidateType($leftSide);
$this->doValidateType($rightSide);
return $this->mergeValues($leftSide, $rightSide);
}
/**
* {@inheritdoc}
*/
final public function normalize($value)
{
$value = $this->preNormalize($value);
// run custom normalization closures
foreach ($this->normalizationClosures as $closure) {
$value = $closure($value);
}
// resolve placeholder value
if ($value !== $placeholders = self::resolvePlaceholderValue($value)) {
foreach ($placeholders as $placeholder) {
$this->handlingPlaceholder = $value;
try {
$this->normalize($placeholder);
} finally {
$this->handlingPlaceholder = null;
}
}
return $value;
}
// replace value with their equivalent
foreach ($this->equivalentValues as $data) {
if ($data[0] === $value) {
$value = $data[1];
}
}
// validate type
$this->doValidateType($value);
// normalize value
return $this->normalizeValue($value);
}
/**
* Normalizes the value before any other normalization is applied.
*
* @param mixed $value
*
* @return mixed The normalized array value
*/
protected function preNormalize($value)
{
return $value;
}
/**
* Returns parent node for this node.
*
* @return NodeInterface|null
*/
public function getParent()
{
return $this->parent;
}
/**
* {@inheritdoc}
*/
final public function finalize($value)
{
if ($value !== $placeholders = self::resolvePlaceholderValue($value)) {
foreach ($placeholders as $placeholder) {
$this->handlingPlaceholder = $value;
try {
$this->finalize($placeholder);
} finally {
$this->handlingPlaceholder = null;
}
}
return $value;
}
$this->doValidateType($value);
$value = $this->finalizeValue($value);
// Perform validation on the final value if a closure has been set.
// The closure is also allowed to return another value.
foreach ($this->finalValidationClosures as $closure) {
try {
$value = $closure($value);
} catch (Exception $e) {
if ($e instanceof UnsetKeyException && null !== $this->handlingPlaceholder) {
continue;
}
throw $e;
} catch (\Exception $e) {
throw new InvalidConfigurationException(sprintf('Invalid configuration for path "%s": ', $this->getPath()).$e->getMessage(), $e->getCode(), $e);
}
}
return $value;
}
/**
* Validates the type of a Node.
*
* @param mixed $value The value to validate
*
* @throws InvalidTypeException when the value is invalid
*/
abstract protected function validateType($value);
/**
* Normalizes the value.
*
* @param mixed $value The value to normalize
*
* @return mixed The normalized value
*/
abstract protected function normalizeValue($value);
/**
* Merges two values together.
*
* @param mixed $leftSide
* @param mixed $rightSide
*
* @return mixed The merged value
*/
abstract protected function mergeValues($leftSide, $rightSide);
/**
* Finalizes a value.
*
* @param mixed $value The value to finalize
*
* @return mixed The finalized value
*/
abstract protected function finalizeValue($value);
/**
* Tests if placeholder values are allowed for this node.
*/
protected function allowPlaceholders(): bool
{
return true;
}
/**
* Tests if a placeholder is being handled currently.
*/
protected function isHandlingPlaceholder(): bool
{
return null !== $this->handlingPlaceholder;
}
/**
* Gets allowed dynamic types for this node.
*/
protected function getValidPlaceholderTypes(): array
{
return [];
}
private static function resolvePlaceholderValue($value)
{
if (\is_string($value)) {
if (isset(self::$placeholders[$value])) {
return self::$placeholders[$value];
}
foreach (self::$placeholderUniquePrefixes as $placeholderUniquePrefix) {
if (str_starts_with($value, $placeholderUniquePrefix)) {
return [];
}
}
}
return $value;
}
private function doValidateType($value): void
{
if (null !== $this->handlingPlaceholder && !$this->allowPlaceholders()) {
$e = new InvalidTypeException(sprintf('A dynamic value is not compatible with a "%s" node type at path "%s".', static::class, $this->getPath()));
$e->setPath($this->getPath());
throw $e;
}
if (null === $this->handlingPlaceholder || null === $value) {
$this->validateType($value);
return;
}
$knownTypes = array_keys(self::$placeholders[$this->handlingPlaceholder]);
$validTypes = $this->getValidPlaceholderTypes();
if ($validTypes && array_diff($knownTypes, $validTypes)) {
$e = new InvalidTypeException(sprintf(
'Invalid type for path "%s". Expected %s, but got %s.',
$this->getPath(),
1 === \count($validTypes) ? '"'.reset($validTypes).'"' : 'one of "'.implode('", "', $validTypes).'"',
1 === \count($knownTypes) ? '"'.reset($knownTypes).'"' : 'one of "'.implode('", "', $knownTypes).'"'
));
if ($hint = $this->getInfo()) {
$e->addHint($hint);
}
$e->setPath($this->getPath());
throw $e;
}
$this->validateType($value);
}
}

View File

@@ -0,0 +1,55 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
/**
* This node represents a Boolean value in the config tree.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class BooleanNode extends ScalarNode
{
/**
* {@inheritdoc}
*/
protected function validateType($value)
{
if (!\is_bool($value)) {
$ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected boolean, but got %s.', $this->getPath(), \gettype($value)));
if ($hint = $this->getInfo()) {
$ex->addHint($hint);
}
$ex->setPath($this->getPath());
throw $ex;
}
}
/**
* {@inheritdoc}
*/
protected function isValueEmpty($value)
{
// a boolean value cannot be empty
return false;
}
/**
* {@inheritdoc}
*/
protected function getValidPlaceholderTypes(): array
{
return ['bool'];
}
}

View File

@@ -0,0 +1,548 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;
use Symfony\Component\Config\Definition\PrototypedArrayNode;
/**
* This class provides a fluent interface for defining an array node.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinitionInterface
{
protected $performDeepMerging = true;
protected $ignoreExtraKeys = false;
protected $removeExtraKeys = true;
protected $children = [];
protected $prototype;
protected $atLeastOne = false;
protected $allowNewKeys = true;
protected $key;
protected $removeKeyItem;
protected $addDefaults = false;
protected $addDefaultChildren = false;
protected $nodeBuilder;
protected $normalizeKeys = true;
/**
* {@inheritdoc}
*/
public function __construct(?string $name, NodeParentInterface $parent = null)
{
parent::__construct($name, $parent);
$this->nullEquivalent = [];
$this->trueEquivalent = [];
}
/**
* {@inheritdoc}
*/
public function setBuilder(NodeBuilder $builder)
{
$this->nodeBuilder = $builder;
}
/**
* {@inheritdoc}
*/
public function children()
{
return $this->getNodeBuilder();
}
/**
* Sets a prototype for child nodes.
*
* @param string $type The type of node
*
* @return NodeDefinition
*/
public function prototype($type)
{
return $this->prototype = $this->getNodeBuilder()->node(null, $type)->setParent($this);
}
/**
* @return VariableNodeDefinition
*/
public function variablePrototype()
{
return $this->prototype('variable');
}
/**
* @return ScalarNodeDefinition
*/
public function scalarPrototype()
{
return $this->prototype('scalar');
}
/**
* @return BooleanNodeDefinition
*/
public function booleanPrototype()
{
return $this->prototype('boolean');
}
/**
* @return IntegerNodeDefinition
*/
public function integerPrototype()
{
return $this->prototype('integer');
}
/**
* @return FloatNodeDefinition
*/
public function floatPrototype()
{
return $this->prototype('float');
}
/**
* @return ArrayNodeDefinition
*/
public function arrayPrototype()
{
return $this->prototype('array');
}
/**
* @return EnumNodeDefinition
*/
public function enumPrototype()
{
return $this->prototype('enum');
}
/**
* Adds the default value if the node is not set in the configuration.
*
* This method is applicable to concrete nodes only (not to prototype nodes).
* If this function has been called and the node is not set during the finalization
* phase, it's default value will be derived from its children default values.
*
* @return $this
*/
public function addDefaultsIfNotSet()
{
$this->addDefaults = true;
return $this;
}
/**
* Adds children with a default value when none are defined.
*
* This method is applicable to prototype nodes only.
*
* @param int|string|array|null $children The number of children|The child name|The children names to be added
*
* @return $this
*/
public function addDefaultChildrenIfNoneSet($children = null)
{
$this->addDefaultChildren = $children;
return $this;
}
/**
* Requires the node to have at least one element.
*
* This method is applicable to prototype nodes only.
*
* @return $this
*/
public function requiresAtLeastOneElement()
{
$this->atLeastOne = true;
return $this;
}
/**
* Disallows adding news keys in a subsequent configuration.
*
* If used all keys have to be defined in the same configuration file.
*
* @return $this
*/
public function disallowNewKeysInSubsequentConfigs()
{
$this->allowNewKeys = false;
return $this;
}
/**
* Sets a normalization rule for XML configurations.
*
* @param string $singular The key to remap
* @param string $plural The plural of the key for irregular plurals
*
* @return $this
*/
public function fixXmlConfig($singular, $plural = null)
{
$this->normalization()->remap($singular, $plural);
return $this;
}
/**
* Sets the attribute which value is to be used as key.
*
* This is useful when you have an indexed array that should be an
* associative array. You can select an item from within the array
* to be the key of the particular item. For example, if "id" is the
* "key", then:
*
* [
* ['id' => 'my_name', 'foo' => 'bar'],
* ];
*
* becomes
*
* [
* 'my_name' => ['foo' => 'bar'],
* ];
*
* If you'd like "'id' => 'my_name'" to still be present in the resulting
* array, then you can set the second argument of this method to false.
*
* This method is applicable to prototype nodes only.
*
* @param string $name The name of the key
* @param bool $removeKeyItem Whether or not the key item should be removed
*
* @return $this
*/
public function useAttributeAsKey($name, $removeKeyItem = true)
{
$this->key = $name;
$this->removeKeyItem = $removeKeyItem;
return $this;
}
/**
* Sets whether the node can be unset.
*
* @param bool $allow
*
* @return $this
*/
public function canBeUnset($allow = true)
{
$this->merge()->allowUnset($allow);
return $this;
}
/**
* Adds an "enabled" boolean to enable the current section.
*
* By default, the section is disabled. If any configuration is specified then
* the node will be automatically enabled:
*
* enableableArrayNode: {enabled: true, ...} # The config is enabled & default values get overridden
* enableableArrayNode: ~ # The config is enabled & use the default values
* enableableArrayNode: true # The config is enabled & use the default values
* enableableArrayNode: {other: value, ...} # The config is enabled & default values get overridden
* enableableArrayNode: {enabled: false, ...} # The config is disabled
* enableableArrayNode: false # The config is disabled
*
* @return $this
*/
public function canBeEnabled()
{
$this
->addDefaultsIfNotSet()
->treatFalseLike(['enabled' => false])
->treatTrueLike(['enabled' => true])
->treatNullLike(['enabled' => true])
->beforeNormalization()
->ifArray()
->then(function (array $v) {
$v['enabled'] = $v['enabled'] ?? true;
return $v;
})
->end()
->children()
->booleanNode('enabled')
->defaultFalse()
;
return $this;
}
/**
* Adds an "enabled" boolean to enable the current section.
*
* By default, the section is enabled.
*
* @return $this
*/
public function canBeDisabled()
{
$this
->addDefaultsIfNotSet()
->treatFalseLike(['enabled' => false])
->treatTrueLike(['enabled' => true])
->treatNullLike(['enabled' => true])
->children()
->booleanNode('enabled')
->defaultTrue()
;
return $this;
}
/**
* Disables the deep merging of the node.
*
* @return $this
*/
public function performNoDeepMerging()
{
$this->performDeepMerging = false;
return $this;
}
/**
* Allows extra config keys to be specified under an array without
* throwing an exception.
*
* Those config values are ignored and removed from the resulting
* array. This should be used only in special cases where you want
* to send an entire configuration array through a special tree that
* processes only part of the array.
*
* @param bool $remove Whether to remove the extra keys
*
* @return $this
*/
public function ignoreExtraKeys($remove = true)
{
$this->ignoreExtraKeys = true;
$this->removeExtraKeys = $remove;
return $this;
}
/**
* Sets key normalization.
*
* @param bool $bool Whether to enable key normalization
*
* @return $this
*/
public function normalizeKeys($bool)
{
$this->normalizeKeys = (bool) $bool;
return $this;
}
/**
* {@inheritdoc}
*/
public function append(NodeDefinition $node)
{
$this->children[$node->name] = $node->setParent($this);
return $this;
}
/**
* Returns a node builder to be used to add children and prototype.
*
* @return NodeBuilder The node builder
*/
protected function getNodeBuilder()
{
if (null === $this->nodeBuilder) {
$this->nodeBuilder = new NodeBuilder();
}
return $this->nodeBuilder->setParent($this);
}
/**
* {@inheritdoc}
*/
protected function createNode()
{
if (null === $this->prototype) {
$node = new ArrayNode($this->name, $this->parent, $this->pathSeparator);
$this->validateConcreteNode($node);
$node->setAddIfNotSet($this->addDefaults);
foreach ($this->children as $child) {
$child->parent = $node;
$node->addChild($child->getNode());
}
} else {
$node = new PrototypedArrayNode($this->name, $this->parent, $this->pathSeparator);
$this->validatePrototypeNode($node);
if (null !== $this->key) {
$node->setKeyAttribute($this->key, $this->removeKeyItem);
}
if (true === $this->atLeastOne || false === $this->allowEmptyValue) {
$node->setMinNumberOfElements(1);
}
if ($this->default) {
$node->setDefaultValue($this->defaultValue);
}
if (false !== $this->addDefaultChildren) {
$node->setAddChildrenIfNoneSet($this->addDefaultChildren);
if ($this->prototype instanceof static && null === $this->prototype->prototype) {
$this->prototype->addDefaultsIfNotSet();
}
}
$this->prototype->parent = $node;
$node->setPrototype($this->prototype->getNode());
}
$node->setAllowNewKeys($this->allowNewKeys);
$node->addEquivalentValue(null, $this->nullEquivalent);
$node->addEquivalentValue(true, $this->trueEquivalent);
$node->addEquivalentValue(false, $this->falseEquivalent);
$node->setPerformDeepMerging($this->performDeepMerging);
$node->setRequired($this->required);
$node->setDeprecated($this->deprecationMessage);
$node->setIgnoreExtraKeys($this->ignoreExtraKeys, $this->removeExtraKeys);
$node->setNormalizeKeys($this->normalizeKeys);
if (null !== $this->normalization) {
$node->setNormalizationClosures($this->normalization->before);
$node->setXmlRemappings($this->normalization->remappings);
}
if (null !== $this->merge) {
$node->setAllowOverwrite($this->merge->allowOverwrite);
$node->setAllowFalse($this->merge->allowFalse);
}
if (null !== $this->validation) {
$node->setFinalValidationClosures($this->validation->rules);
}
return $node;
}
/**
* Validate the configuration of a concrete node.
*
* @throws InvalidDefinitionException
*/
protected function validateConcreteNode(ArrayNode $node)
{
$path = $node->getPath();
if (null !== $this->key) {
throw new InvalidDefinitionException(sprintf('->useAttributeAsKey() is not applicable to concrete nodes at path "%s".', $path));
}
if (false === $this->allowEmptyValue) {
throw new InvalidDefinitionException(sprintf('->cannotBeEmpty() is not applicable to concrete nodes at path "%s".', $path));
}
if (true === $this->atLeastOne) {
throw new InvalidDefinitionException(sprintf('->requiresAtLeastOneElement() is not applicable to concrete nodes at path "%s".', $path));
}
if ($this->default) {
throw new InvalidDefinitionException(sprintf('->defaultValue() is not applicable to concrete nodes at path "%s".', $path));
}
if (false !== $this->addDefaultChildren) {
throw new InvalidDefinitionException(sprintf('->addDefaultChildrenIfNoneSet() is not applicable to concrete nodes at path "%s".', $path));
}
}
/**
* Validate the configuration of a prototype node.
*
* @throws InvalidDefinitionException
*/
protected function validatePrototypeNode(PrototypedArrayNode $node)
{
$path = $node->getPath();
if ($this->addDefaults) {
throw new InvalidDefinitionException(sprintf('->addDefaultsIfNotSet() is not applicable to prototype nodes at path "%s".', $path));
}
if (false !== $this->addDefaultChildren) {
if ($this->default) {
throw new InvalidDefinitionException(sprintf('A default value and default children might not be used together at path "%s".', $path));
}
if (null !== $this->key && (null === $this->addDefaultChildren || \is_int($this->addDefaultChildren) && $this->addDefaultChildren > 0)) {
throw new InvalidDefinitionException(sprintf('->addDefaultChildrenIfNoneSet() should set default children names as ->useAttributeAsKey() is used at path "%s".', $path));
}
if (null === $this->key && (\is_string($this->addDefaultChildren) || \is_array($this->addDefaultChildren))) {
throw new InvalidDefinitionException(sprintf('->addDefaultChildrenIfNoneSet() might not set default children names as ->useAttributeAsKey() is not used at path "%s".', $path));
}
}
}
/**
* @return NodeDefinition[]
*/
public function getChildNodeDefinitions()
{
return $this->children;
}
/**
* Finds a node defined by the given $nodePath.
*
* @param string $nodePath The path of the node to find. e.g "doctrine.orm.mappings"
*/
public function find(string $nodePath): NodeDefinition
{
$firstPathSegment = (false === $pathSeparatorPos = strpos($nodePath, $this->pathSeparator))
? $nodePath
: substr($nodePath, 0, $pathSeparatorPos);
if (null === $node = ($this->children[$firstPathSegment] ?? null)) {
throw new \RuntimeException(sprintf('Node with name "%s" does not exist in the current node "%s".', $firstPathSegment, $this->name));
}
if (false === $pathSeparatorPos) {
return $node;
}
return $node->find(substr($nodePath, $pathSeparatorPos + \strlen($this->pathSeparator)));
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
use Symfony\Component\Config\Definition\BooleanNode;
use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;
/**
* This class provides a fluent interface for defining a node.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class BooleanNodeDefinition extends ScalarNodeDefinition
{
/**
* {@inheritdoc}
*/
public function __construct(?string $name, NodeParentInterface $parent = null)
{
parent::__construct($name, $parent);
$this->nullEquivalent = true;
}
/**
* Instantiate a Node.
*
* @return BooleanNode The node
*/
protected function instantiateNode()
{
return new BooleanNode($this->name, $this->parent, $this->pathSeparator);
}
/**
* {@inheritdoc}
*
* @throws InvalidDefinitionException
*/
public function cannotBeEmpty()
{
throw new InvalidDefinitionException('->cannotBeEmpty() is not applicable to BooleanNodeDefinition.');
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
/**
* An interface that can be implemented by nodes which build other nodes.
*
* @author Roland Franssen <franssen.roland@gmail.com>
*/
interface BuilderAwareInterface
{
/**
* Sets a custom children builder.
*/
public function setBuilder(NodeBuilder $builder);
}

View File

@@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
use Symfony\Component\Config\Definition\EnumNode;
/**
* Enum Node Definition.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class EnumNodeDefinition extends ScalarNodeDefinition
{
private $values;
/**
* @return $this
*/
public function values(array $values)
{
$values = array_unique($values);
if (empty($values)) {
throw new \InvalidArgumentException('->values() must be called with at least one value.');
}
$this->values = $values;
return $this;
}
/**
* Instantiate a Node.
*
* @return EnumNode The node
*
* @throws \RuntimeException
*/
protected function instantiateNode()
{
if (null === $this->values) {
throw new \RuntimeException('You must call ->values() on enum nodes.');
}
return new EnumNode($this->name, $this->parent, $this->values, $this->pathSeparator);
}
}

View File

@@ -0,0 +1,248 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
/**
* This class builds an if expression.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
* @author Christophe Coevoet <stof@notk.org>
*/
class ExprBuilder
{
protected $node;
public $ifPart;
public $thenPart;
public function __construct(NodeDefinition $node)
{
$this->node = $node;
}
/**
* Marks the expression as being always used.
*
* @return $this
*/
public function always(\Closure $then = null)
{
$this->ifPart = function () { return true; };
if (null !== $then) {
$this->thenPart = $then;
}
return $this;
}
/**
* Sets a closure to use as tests.
*
* The default one tests if the value is true.
*
* @return $this
*/
public function ifTrue(\Closure $closure = null)
{
if (null === $closure) {
$closure = function ($v) { return true === $v; };
}
$this->ifPart = $closure;
return $this;
}
/**
* Tests if the value is a string.
*
* @return $this
*/
public function ifString()
{
$this->ifPart = function ($v) { return \is_string($v); };
return $this;
}
/**
* Tests if the value is null.
*
* @return $this
*/
public function ifNull()
{
$this->ifPart = function ($v) { return null === $v; };
return $this;
}
/**
* Tests if the value is empty.
*
* @return $this
*/
public function ifEmpty()
{
$this->ifPart = function ($v) { return empty($v); };
return $this;
}
/**
* Tests if the value is an array.
*
* @return $this
*/
public function ifArray()
{
$this->ifPart = function ($v) { return \is_array($v); };
return $this;
}
/**
* Tests if the value is in an array.
*
* @return $this
*/
public function ifInArray(array $array)
{
$this->ifPart = function ($v) use ($array) { return \in_array($v, $array, true); };
return $this;
}
/**
* Tests if the value is not in an array.
*
* @return $this
*/
public function ifNotInArray(array $array)
{
$this->ifPart = function ($v) use ($array) { return !\in_array($v, $array, true); };
return $this;
}
/**
* Transforms variables of any type into an array.
*
* @return $this
*/
public function castToArray()
{
$this->ifPart = function ($v) { return !\is_array($v); };
$this->thenPart = function ($v) { return [$v]; };
return $this;
}
/**
* Sets the closure to run if the test pass.
*
* @return $this
*/
public function then(\Closure $closure)
{
$this->thenPart = $closure;
return $this;
}
/**
* Sets a closure returning an empty array.
*
* @return $this
*/
public function thenEmptyArray()
{
$this->thenPart = function () { return []; };
return $this;
}
/**
* Sets a closure marking the value as invalid at processing time.
*
* if you want to add the value of the node in your message just use a %s placeholder.
*
* @param string $message
*
* @return $this
*
* @throws \InvalidArgumentException
*/
public function thenInvalid($message)
{
$this->thenPart = function ($v) use ($message) { throw new \InvalidArgumentException(sprintf($message, json_encode($v))); };
return $this;
}
/**
* Sets a closure unsetting this key of the array at processing time.
*
* @return $this
*
* @throws UnsetKeyException
*/
public function thenUnset()
{
$this->thenPart = function () { throw new UnsetKeyException('Unsetting key.'); };
return $this;
}
/**
* Returns the related node.
*
* @return NodeDefinition|ArrayNodeDefinition|VariableNodeDefinition
*
* @throws \RuntimeException
*/
public function end()
{
if (null === $this->ifPart) {
throw new \RuntimeException('You must specify an if part.');
}
if (null === $this->thenPart) {
throw new \RuntimeException('You must specify a then part.');
}
return $this->node;
}
/**
* Builds the expressions.
*
* @param ExprBuilder[] $expressions An array of ExprBuilder instances to build
*
* @return array
*/
public static function buildExpressions(array $expressions)
{
foreach ($expressions as $k => $expr) {
if ($expr instanceof self) {
$if = $expr->ifPart;
$then = $expr->thenPart;
$expressions[$k] = function ($v) use ($if, $then) {
return $if($v) ? $then($v) : $v;
};
}
}
return $expressions;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
use Symfony\Component\Config\Definition\FloatNode;
/**
* This class provides a fluent interface for defining a float node.
*
* @author Jeanmonod David <david.jeanmonod@gmail.com>
*/
class FloatNodeDefinition extends NumericNodeDefinition
{
/**
* Instantiates a Node.
*
* @return FloatNode The node
*/
protected function instantiateNode()
{
return new FloatNode($this->name, $this->parent, $this->min, $this->max, $this->pathSeparator);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
use Symfony\Component\Config\Definition\IntegerNode;
/**
* This class provides a fluent interface for defining an integer node.
*
* @author Jeanmonod David <david.jeanmonod@gmail.com>
*/
class IntegerNodeDefinition extends NumericNodeDefinition
{
/**
* Instantiates a Node.
*
* @return IntegerNode The node
*/
protected function instantiateNode()
{
return new IntegerNode($this->name, $this->parent, $this->min, $this->max, $this->pathSeparator);
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
/**
* This class builds merge conditions.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class MergeBuilder
{
protected $node;
public $allowFalse = false;
public $allowOverwrite = true;
public function __construct(NodeDefinition $node)
{
$this->node = $node;
}
/**
* Sets whether the node can be unset.
*
* @param bool $allow
*
* @return $this
*/
public function allowUnset($allow = true)
{
$this->allowFalse = $allow;
return $this;
}
/**
* Sets whether the node can be overwritten.
*
* @param bool $deny Whether the overwriting is forbidden or not
*
* @return $this
*/
public function denyOverwrite($deny = true)
{
$this->allowOverwrite = !$deny;
return $this;
}
/**
* Returns the related node.
*
* @return NodeDefinition|ArrayNodeDefinition|VariableNodeDefinition
*/
public function end()
{
return $this->node;
}
}

View File

@@ -0,0 +1,238 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
/**
* This class provides a fluent interface for building a node.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class NodeBuilder implements NodeParentInterface
{
protected $parent;
protected $nodeMapping;
public function __construct()
{
$this->nodeMapping = [
'variable' => VariableNodeDefinition::class,
'scalar' => ScalarNodeDefinition::class,
'boolean' => BooleanNodeDefinition::class,
'integer' => IntegerNodeDefinition::class,
'float' => FloatNodeDefinition::class,
'array' => ArrayNodeDefinition::class,
'enum' => EnumNodeDefinition::class,
];
}
/**
* Set the parent node.
*
* @return $this
*/
public function setParent(ParentNodeDefinitionInterface $parent = null)
{
$this->parent = $parent;
return $this;
}
/**
* Creates a child array node.
*
* @param string $name The name of the node
*
* @return ArrayNodeDefinition The child node
*/
public function arrayNode($name)
{
return $this->node($name, 'array');
}
/**
* Creates a child scalar node.
*
* @param string $name The name of the node
*
* @return ScalarNodeDefinition The child node
*/
public function scalarNode($name)
{
return $this->node($name, 'scalar');
}
/**
* Creates a child Boolean node.
*
* @param string $name The name of the node
*
* @return BooleanNodeDefinition The child node
*/
public function booleanNode($name)
{
return $this->node($name, 'boolean');
}
/**
* Creates a child integer node.
*
* @param string $name The name of the node
*
* @return IntegerNodeDefinition The child node
*/
public function integerNode($name)
{
return $this->node($name, 'integer');
}
/**
* Creates a child float node.
*
* @param string $name The name of the node
*
* @return FloatNodeDefinition The child node
*/
public function floatNode($name)
{
return $this->node($name, 'float');
}
/**
* Creates a child EnumNode.
*
* @param string $name
*
* @return EnumNodeDefinition
*/
public function enumNode($name)
{
return $this->node($name, 'enum');
}
/**
* Creates a child variable node.
*
* @param string $name The name of the node
*
* @return VariableNodeDefinition The builder of the child node
*/
public function variableNode($name)
{
return $this->node($name, 'variable');
}
/**
* Returns the parent node.
*
* @return NodeDefinition&ParentNodeDefinitionInterface The parent node
*/
public function end()
{
return $this->parent;
}
/**
* Creates a child node.
*
* @param string|null $name The name of the node
* @param string $type The type of the node
*
* @return NodeDefinition The child node
*
* @throws \RuntimeException When the node type is not registered
* @throws \RuntimeException When the node class is not found
*/
public function node($name, $type)
{
$class = $this->getNodeClass($type);
$node = new $class($name);
$this->append($node);
return $node;
}
/**
* Appends a node definition.
*
* Usage:
*
* $node = new ArrayNodeDefinition('name')
* ->children()
* ->scalarNode('foo')->end()
* ->scalarNode('baz')->end()
* ->append($this->getBarNodeDefinition())
* ->end()
* ;
*
* @return $this
*/
public function append(NodeDefinition $node)
{
if ($node instanceof BuilderAwareInterface) {
$builder = clone $this;
$builder->setParent(null);
$node->setBuilder($builder);
}
if (null !== $this->parent) {
$this->parent->append($node);
// Make this builder the node parent to allow for a fluid interface
$node->setParent($this);
}
return $this;
}
/**
* Adds or overrides a node Type.
*
* @param string $type The name of the type
* @param string $class The fully qualified name the node definition class
*
* @return $this
*/
public function setNodeClass($type, $class)
{
$this->nodeMapping[strtolower($type)] = $class;
return $this;
}
/**
* Returns the class name of the node definition.
*
* @param string $type The node type
*
* @return string The node definition class name
*
* @throws \RuntimeException When the node type is not registered
* @throws \RuntimeException When the node class is not found
*/
protected function getNodeClass($type)
{
$type = strtolower($type);
if (!isset($this->nodeMapping[$type])) {
throw new \RuntimeException(sprintf('The node type "%s" is not registered.', $type));
}
$class = $this->nodeMapping[$type];
if (!class_exists($class)) {
throw new \RuntimeException(sprintf('The node class "%s" does not exist.', $class));
}
return $class;
}
}

View File

@@ -0,0 +1,375 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;
use Symfony\Component\Config\Definition\NodeInterface;
/**
* This class provides a fluent interface for defining a node.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
abstract class NodeDefinition implements NodeParentInterface
{
protected $name;
protected $normalization;
protected $validation;
protected $defaultValue;
protected $default = false;
protected $required = false;
protected $deprecationMessage = null;
protected $merge;
protected $allowEmptyValue = true;
protected $nullEquivalent;
protected $trueEquivalent = true;
protected $falseEquivalent = false;
protected $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR;
protected $parent;
protected $attributes = [];
public function __construct(?string $name, NodeParentInterface $parent = null)
{
$this->parent = $parent;
$this->name = $name;
}
/**
* Sets the parent node.
*
* @return $this
*/
public function setParent(NodeParentInterface $parent)
{
$this->parent = $parent;
return $this;
}
/**
* Sets info message.
*
* @param string $info The info text
*
* @return $this
*/
public function info($info)
{
return $this->attribute('info', $info);
}
/**
* Sets example configuration.
*
* @param string|array $example
*
* @return $this
*/
public function example($example)
{
return $this->attribute('example', $example);
}
/**
* Sets an attribute on the node.
*
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function attribute($key, $value)
{
$this->attributes[$key] = $value;
return $this;
}
/**
* Returns the parent node.
*
* @return NodeParentInterface|NodeBuilder|NodeDefinition|ArrayNodeDefinition|VariableNodeDefinition|null The builder of the parent node
*/
public function end()
{
return $this->parent;
}
/**
* Creates the node.
*
* @param bool $forceRootNode Whether to force this node as the root node
*
* @return NodeInterface
*/
public function getNode($forceRootNode = false)
{
if ($forceRootNode) {
$this->parent = null;
}
if (null !== $this->normalization) {
$this->normalization->before = ExprBuilder::buildExpressions($this->normalization->before);
}
if (null !== $this->validation) {
$this->validation->rules = ExprBuilder::buildExpressions($this->validation->rules);
}
$node = $this->createNode();
if ($node instanceof BaseNode) {
$node->setAttributes($this->attributes);
}
return $node;
}
/**
* Sets the default value.
*
* @param mixed $value The default value
*
* @return $this
*/
public function defaultValue($value)
{
$this->default = true;
$this->defaultValue = $value;
return $this;
}
/**
* Sets the node as required.
*
* @return $this
*/
public function isRequired()
{
$this->required = true;
return $this;
}
/**
* Sets the node as deprecated.
*
* You can use %node% and %path% placeholders in your message to display,
* respectively, the node name and its complete path.
*
* @param string $message Deprecation message
*
* @return $this
*/
public function setDeprecated($message = 'The child node "%node%" at path "%path%" is deprecated.')
{
$this->deprecationMessage = $message;
return $this;
}
/**
* Sets the equivalent value used when the node contains null.
*
* @param mixed $value
*
* @return $this
*/
public function treatNullLike($value)
{
$this->nullEquivalent = $value;
return $this;
}
/**
* Sets the equivalent value used when the node contains true.
*
* @param mixed $value
*
* @return $this
*/
public function treatTrueLike($value)
{
$this->trueEquivalent = $value;
return $this;
}
/**
* Sets the equivalent value used when the node contains false.
*
* @param mixed $value
*
* @return $this
*/
public function treatFalseLike($value)
{
$this->falseEquivalent = $value;
return $this;
}
/**
* Sets null as the default value.
*
* @return $this
*/
public function defaultNull()
{
return $this->defaultValue(null);
}
/**
* Sets true as the default value.
*
* @return $this
*/
public function defaultTrue()
{
return $this->defaultValue(true);
}
/**
* Sets false as the default value.
*
* @return $this
*/
public function defaultFalse()
{
return $this->defaultValue(false);
}
/**
* Sets an expression to run before the normalization.
*
* @return ExprBuilder
*/
public function beforeNormalization()
{
return $this->normalization()->before();
}
/**
* Denies the node value being empty.
*
* @return $this
*/
public function cannotBeEmpty()
{
$this->allowEmptyValue = false;
return $this;
}
/**
* Sets an expression to run for the validation.
*
* The expression receives the value of the node and must return it. It can
* modify it.
* An exception should be thrown when the node is not valid.
*
* @return ExprBuilder
*/
public function validate()
{
return $this->validation()->rule();
}
/**
* Sets whether the node can be overwritten.
*
* @param bool $deny Whether the overwriting is forbidden or not
*
* @return $this
*/
public function cannotBeOverwritten($deny = true)
{
$this->merge()->denyOverwrite($deny);
return $this;
}
/**
* Gets the builder for validation rules.
*
* @return ValidationBuilder
*/
protected function validation()
{
if (null === $this->validation) {
$this->validation = new ValidationBuilder($this);
}
return $this->validation;
}
/**
* Gets the builder for merging rules.
*
* @return MergeBuilder
*/
protected function merge()
{
if (null === $this->merge) {
$this->merge = new MergeBuilder($this);
}
return $this->merge;
}
/**
* Gets the builder for normalization rules.
*
* @return NormalizationBuilder
*/
protected function normalization()
{
if (null === $this->normalization) {
$this->normalization = new NormalizationBuilder($this);
}
return $this->normalization;
}
/**
* Instantiate and configure the node according to this definition.
*
* @return NodeInterface The node instance
*
* @throws InvalidDefinitionException When the definition is invalid
*/
abstract protected function createNode();
/**
* Set PathSeparator to use.
*
* @return $this
*/
public function setPathSeparator(string $separator)
{
if ($this instanceof ParentNodeDefinitionInterface) {
if (method_exists($this, 'getChildNodeDefinitions')) {
foreach ($this->getChildNodeDefinitions() as $child) {
$child->setPathSeparator($separator);
}
} else {
@trigger_error(sprintf('Not implementing the "%s::getChildNodeDefinitions()" method in "%s" is deprecated since Symfony 4.1.', ParentNodeDefinitionInterface::class, static::class), \E_USER_DEPRECATED);
}
}
$this->pathSeparator = $separator;
return $this;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
/**
* An interface that must be implemented by all node parents.
*
* @author Victor Berchet <victor@suumit.com>
*/
interface NodeParentInterface
{
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
/**
* This class builds normalization conditions.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class NormalizationBuilder
{
protected $node;
public $before = [];
public $remappings = [];
public function __construct(NodeDefinition $node)
{
$this->node = $node;
}
/**
* Registers a key to remap to its plural form.
*
* @param string $key The key to remap
* @param string $plural The plural of the key in case of irregular plural
*
* @return $this
*/
public function remap($key, $plural = null)
{
$this->remappings[] = [$key, null === $plural ? $key.'s' : $plural];
return $this;
}
/**
* Registers a closure to run before the normalization or an expression builder to build it if null is provided.
*
* @return ExprBuilder|$this
*/
public function before(\Closure $closure = null)
{
if (null !== $closure) {
$this->before[] = $closure;
return $this;
}
return $this->before[] = new ExprBuilder($this->node);
}
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;
/**
* Abstract class that contains common code of integer and float node definitions.
*
* @author David Jeanmonod <david.jeanmonod@gmail.com>
*/
abstract class NumericNodeDefinition extends ScalarNodeDefinition
{
protected $min;
protected $max;
/**
* Ensures that the value is smaller than the given reference.
*
* @param int|float $max
*
* @return $this
*
* @throws \InvalidArgumentException when the constraint is inconsistent
*/
public function max($max)
{
if (isset($this->min) && $this->min > $max) {
throw new \InvalidArgumentException(sprintf('You cannot define a max(%s) as you already have a min(%s).', $max, $this->min));
}
$this->max = $max;
return $this;
}
/**
* Ensures that the value is bigger than the given reference.
*
* @param int|float $min
*
* @return $this
*
* @throws \InvalidArgumentException when the constraint is inconsistent
*/
public function min($min)
{
if (isset($this->max) && $this->max < $min) {
throw new \InvalidArgumentException(sprintf('You cannot define a min(%s) as you already have a max(%s).', $min, $this->max));
}
$this->min = $min;
return $this;
}
/**
* {@inheritdoc}
*
* @throws InvalidDefinitionException
*/
public function cannotBeEmpty()
{
throw new InvalidDefinitionException('->cannotBeEmpty() is not applicable to NumericNodeDefinition.');
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
/**
* An interface that must be implemented by nodes which can have children.
*
* @author Victor Berchet <victor@suumit.com>
*
* @method NodeDefinition[] getChildNodeDefinitions() Gets the child node definitions - not implementing it is deprecated since Symfony 4.2
*/
interface ParentNodeDefinitionInterface extends BuilderAwareInterface
{
/**
* Returns a builder to add children nodes.
*
* @return NodeBuilder
*/
public function children();
/**
* Appends a node definition.
*
* Usage:
*
* $node = $parentNode
* ->children()
* ->scalarNode('foo')->end()
* ->scalarNode('baz')->end()
* ->append($this->getBarNodeDefinition())
* ->end()
* ;
*
* @return $this
*/
public function append(NodeDefinition $node);
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
use Symfony\Component\Config\Definition\ScalarNode;
/**
* This class provides a fluent interface for defining a node.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ScalarNodeDefinition extends VariableNodeDefinition
{
/**
* Instantiate a Node.
*
* @return ScalarNode The node
*/
protected function instantiateNode()
{
return new ScalarNode($this->name, $this->parent, $this->pathSeparator);
}
}

View File

@@ -0,0 +1,106 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
use Symfony\Component\Config\Definition\Exception\TreeWithoutRootNodeException;
use Symfony\Component\Config\Definition\NodeInterface;
/**
* This is the entry class for building a config tree.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class TreeBuilder implements NodeParentInterface
{
protected $tree;
protected $root;
public function __construct(string $name = null, string $type = 'array', NodeBuilder $builder = null)
{
if (null === $name) {
@trigger_error('A tree builder without a root node is deprecated since Symfony 4.2 and will not be supported anymore in 5.0.', \E_USER_DEPRECATED);
} else {
$builder = $builder ?? new NodeBuilder();
$this->root = $builder->node($name, $type)->setParent($this);
}
}
/**
* Creates the root node.
*
* @param string $name The name of the root node
* @param string $type The type of the root node
*
* @return ArrayNodeDefinition|NodeDefinition The root node (as an ArrayNodeDefinition when the type is 'array')
*
* @throws \RuntimeException When the node type is not supported
*
* @deprecated since Symfony 4.3, pass the root name to the constructor instead
*/
public function root($name, $type = 'array', NodeBuilder $builder = null)
{
@trigger_error(sprintf('The "%s()" method called for the "%s" configuration is deprecated since Symfony 4.3, pass the root name to the constructor instead.', __METHOD__, $name), \E_USER_DEPRECATED);
$builder = $builder ?? new NodeBuilder();
return $this->root = $builder->node($name, $type)->setParent($this);
}
/**
* @return NodeDefinition|ArrayNodeDefinition The root node (as an ArrayNodeDefinition when the type is 'array')
*/
public function getRootNode(): NodeDefinition
{
if (null === $this->root) {
throw new \RuntimeException(sprintf('Calling "%s()" before creating the root node is not supported, migrate to the new constructor signature instead.', __METHOD__));
}
return $this->root;
}
/**
* Builds the tree.
*
* @return NodeInterface
*
* @throws \RuntimeException
*/
public function buildTree()
{
$this->assertTreeHasRootNode();
if (null !== $this->tree) {
return $this->tree;
}
return $this->tree = $this->root->getNode(true);
}
public function setPathSeparator(string $separator)
{
$this->assertTreeHasRootNode();
// unset last built as changing path separator changes all nodes
$this->tree = null;
$this->root->setPathSeparator($separator);
}
/**
* @throws \RuntimeException if root node is not defined
*/
private function assertTreeHasRootNode()
{
if (null === $this->root) {
throw new TreeWithoutRootNodeException('The configuration tree has no root node.');
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
/**
* This class builds validation conditions.
*
* @author Christophe Coevoet <stof@notk.org>
*/
class ValidationBuilder
{
protected $node;
public $rules = [];
public function __construct(NodeDefinition $node)
{
$this->node = $node;
}
/**
* Registers a closure to run as normalization or an expression builder to build it if null is provided.
*
* @return ExprBuilder|$this
*/
public function rule(\Closure $closure = null)
{
if (null !== $closure) {
$this->rules[] = $closure;
return $this;
}
return $this->rules[] = new ExprBuilder($this->node);
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Builder;
use Symfony\Component\Config\Definition\VariableNode;
/**
* This class provides a fluent interface for defining a node.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class VariableNodeDefinition extends NodeDefinition
{
/**
* Instantiate a Node.
*
* @return VariableNode The node
*/
protected function instantiateNode()
{
return new VariableNode($this->name, $this->parent, $this->pathSeparator);
}
/**
* {@inheritdoc}
*/
protected function createNode()
{
$node = $this->instantiateNode();
if (null !== $this->normalization) {
$node->setNormalizationClosures($this->normalization->before);
}
if (null !== $this->merge) {
$node->setAllowOverwrite($this->merge->allowOverwrite);
}
if (true === $this->default) {
$node->setDefaultValue($this->defaultValue);
}
$node->setAllowEmptyValue($this->allowEmptyValue);
$node->addEquivalentValue(null, $this->nullEquivalent);
$node->addEquivalentValue(true, $this->trueEquivalent);
$node->addEquivalentValue(false, $this->falseEquivalent);
$node->setRequired($this->required);
$node->setDeprecated($this->deprecationMessage);
if (null !== $this->validation) {
$node->setFinalValidationClosures($this->validation->rules);
}
return $node;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
/**
* Configuration interface.
*
* @author Victor Berchet <victor@suumit.com>
*/
interface ConfigurationInterface
{
/**
* Generates the configuration tree builder.
*
* @return \Symfony\Component\Config\Definition\Builder\TreeBuilder The tree builder
*/
public function getConfigTreeBuilder();
}

View File

@@ -0,0 +1,305 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Dumper;
use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\EnumNode;
use Symfony\Component\Config\Definition\NodeInterface;
use Symfony\Component\Config\Definition\PrototypedArrayNode;
/**
* Dumps an XML reference configuration for the given configuration/node instance.
*
* @author Wouter J <waldio.webdesign@gmail.com>
*/
class XmlReferenceDumper
{
private $reference;
public function dump(ConfigurationInterface $configuration, $namespace = null)
{
return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree(), $namespace);
}
public function dumpNode(NodeInterface $node, $namespace = null)
{
$this->reference = '';
$this->writeNode($node, 0, true, $namespace);
$ref = $this->reference;
$this->reference = null;
return $ref;
}
private function writeNode(NodeInterface $node, int $depth = 0, bool $root = false, string $namespace = null)
{
$rootName = ($root ? 'config' : $node->getName());
$rootNamespace = ($namespace ?: ($root ? 'http://example.org/schema/dic/'.$node->getName() : null));
// xml remapping
if ($node->getParent()) {
$remapping = array_filter($node->getParent()->getXmlRemappings(), function (array $mapping) use ($rootName) {
return $rootName === $mapping[1];
});
if (\count($remapping)) {
[$singular] = current($remapping);
$rootName = $singular;
}
}
$rootName = str_replace('_', '-', $rootName);
$rootAttributes = [];
$rootAttributeComments = [];
$rootChildren = [];
$rootComments = [];
if ($node instanceof ArrayNode) {
$children = $node->getChildren();
// comments about the root node
if ($rootInfo = $node->getInfo()) {
$rootComments[] = $rootInfo;
}
if ($rootNamespace) {
$rootComments[] = 'Namespace: '.$rootNamespace;
}
// render prototyped nodes
if ($node instanceof PrototypedArrayNode) {
$prototype = $node->getPrototype();
$info = 'prototype';
if (null !== $prototype->getInfo()) {
$info .= ': '.$prototype->getInfo();
}
array_unshift($rootComments, $info);
if ($key = $node->getKeyAttribute()) {
$rootAttributes[$key] = str_replace('-', ' ', $rootName).' '.$key;
}
if ($prototype instanceof PrototypedArrayNode) {
$prototype->setName($key ?? '');
$children = [$key => $prototype];
} elseif ($prototype instanceof ArrayNode) {
$children = $prototype->getChildren();
} else {
if ($prototype->hasDefaultValue()) {
$prototypeValue = $prototype->getDefaultValue();
} else {
switch (\get_class($prototype)) {
case 'Symfony\Component\Config\Definition\ScalarNode':
$prototypeValue = 'scalar value';
break;
case 'Symfony\Component\Config\Definition\FloatNode':
case 'Symfony\Component\Config\Definition\IntegerNode':
$prototypeValue = 'numeric value';
break;
case 'Symfony\Component\Config\Definition\BooleanNode':
$prototypeValue = 'true|false';
break;
case 'Symfony\Component\Config\Definition\EnumNode':
$prototypeValue = implode('|', array_map('json_encode', $prototype->getValues()));
break;
default:
$prototypeValue = 'value';
}
}
}
}
// get attributes and elements
foreach ($children as $child) {
if ($child instanceof ArrayNode) {
// get elements
$rootChildren[] = $child;
continue;
}
// get attributes
// metadata
$name = str_replace('_', '-', $child->getName());
$value = '%%%%not_defined%%%%'; // use a string which isn't used in the normal world
// comments
$comments = [];
if ($child instanceof BaseNode && $info = $child->getInfo()) {
$comments[] = $info;
}
if ($child instanceof BaseNode && $example = $child->getExample()) {
$comments[] = 'Example: '.$example;
}
if ($child->isRequired()) {
$comments[] = 'Required';
}
if ($child instanceof BaseNode && $child->isDeprecated()) {
$comments[] = sprintf('Deprecated (%s)', $child->getDeprecationMessage($child->getName(), $node->getPath()));
}
if ($child instanceof EnumNode) {
$comments[] = 'One of '.implode('; ', array_map('json_encode', $child->getValues()));
}
if (\count($comments)) {
$rootAttributeComments[$name] = implode(";\n", $comments);
}
// default values
if ($child->hasDefaultValue()) {
$value = $child->getDefaultValue();
}
// append attribute
$rootAttributes[$name] = $value;
}
}
// render comments
// root node comment
if (\count($rootComments)) {
foreach ($rootComments as $comment) {
$this->writeLine('<!-- '.$comment.' -->', $depth);
}
}
// attribute comments
if (\count($rootAttributeComments)) {
foreach ($rootAttributeComments as $attrName => $comment) {
$commentDepth = $depth + 4 + \strlen($attrName) + 2;
$commentLines = explode("\n", $comment);
$multiline = (\count($commentLines) > 1);
$comment = implode(\PHP_EOL.str_repeat(' ', $commentDepth), $commentLines);
if ($multiline) {
$this->writeLine('<!--', $depth);
$this->writeLine($attrName.': '.$comment, $depth + 4);
$this->writeLine('-->', $depth);
} else {
$this->writeLine('<!-- '.$attrName.': '.$comment.' -->', $depth);
}
}
}
// render start tag + attributes
$rootIsVariablePrototype = isset($prototypeValue);
$rootIsEmptyTag = (0 === \count($rootChildren) && !$rootIsVariablePrototype);
$rootOpenTag = '<'.$rootName;
if (1 >= ($attributesCount = \count($rootAttributes))) {
if (1 === $attributesCount) {
$rootOpenTag .= sprintf(' %s="%s"', current(array_keys($rootAttributes)), $this->writeValue(current($rootAttributes)));
}
$rootOpenTag .= $rootIsEmptyTag ? ' />' : '>';
if ($rootIsVariablePrototype) {
$rootOpenTag .= $prototypeValue.'</'.$rootName.'>';
}
$this->writeLine($rootOpenTag, $depth);
} else {
$this->writeLine($rootOpenTag, $depth);
$i = 1;
foreach ($rootAttributes as $attrName => $attrValue) {
$attr = sprintf('%s="%s"', $attrName, $this->writeValue($attrValue));
$this->writeLine($attr, $depth + 4);
if ($attributesCount === $i++) {
$this->writeLine($rootIsEmptyTag ? '/>' : '>', $depth);
if ($rootIsVariablePrototype) {
$rootOpenTag .= $prototypeValue.'</'.$rootName.'>';
}
}
}
}
// render children tags
foreach ($rootChildren as $child) {
$this->writeLine('');
$this->writeNode($child, $depth + 4);
}
// render end tag
if (!$rootIsEmptyTag && !$rootIsVariablePrototype) {
$this->writeLine('');
$rootEndTag = '</'.$rootName.'>';
$this->writeLine($rootEndTag, $depth);
}
}
/**
* Outputs a single config reference line.
*/
private function writeLine(string $text, int $indent = 0)
{
$indent = \strlen($text) + $indent;
$format = '%'.$indent.'s';
$this->reference .= sprintf($format, $text).\PHP_EOL;
}
/**
* Renders the string conversion of the value.
*
* @param mixed $value
*/
private function writeValue($value): string
{
if ('%%%%not_defined%%%%' === $value) {
return '';
}
if (\is_string($value) || is_numeric($value)) {
return $value;
}
if (false === $value) {
return 'false';
}
if (true === $value) {
return 'true';
}
if (null === $value) {
return 'null';
}
if (empty($value)) {
return '';
}
if (\is_array($value)) {
return implode(',', $value);
}
return '';
}
}

View File

@@ -0,0 +1,250 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Dumper;
use Symfony\Component\Config\Definition\ArrayNode;
use Symfony\Component\Config\Definition\BaseNode;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\EnumNode;
use Symfony\Component\Config\Definition\NodeInterface;
use Symfony\Component\Config\Definition\PrototypedArrayNode;
use Symfony\Component\Config\Definition\ScalarNode;
use Symfony\Component\Config\Definition\VariableNode;
use Symfony\Component\Yaml\Inline;
/**
* Dumps a Yaml reference configuration for the given configuration/node instance.
*
* @author Kevin Bond <kevinbond@gmail.com>
*/
class YamlReferenceDumper
{
private $reference;
public function dump(ConfigurationInterface $configuration)
{
return $this->dumpNode($configuration->getConfigTreeBuilder()->buildTree());
}
public function dumpAtPath(ConfigurationInterface $configuration, $path)
{
$rootNode = $node = $configuration->getConfigTreeBuilder()->buildTree();
foreach (explode('.', $path) as $step) {
if (!$node instanceof ArrayNode) {
throw new \UnexpectedValueException(sprintf('Unable to find node at path "%s.%s".', $rootNode->getName(), $path));
}
/** @var NodeInterface[] $children */
$children = $node instanceof PrototypedArrayNode ? $this->getPrototypeChildren($node) : $node->getChildren();
foreach ($children as $child) {
if ($child->getName() === $step) {
$node = $child;
continue 2;
}
}
throw new \UnexpectedValueException(sprintf('Unable to find node at path "%s.%s".', $rootNode->getName(), $path));
}
return $this->dumpNode($node);
}
public function dumpNode(NodeInterface $node)
{
$this->reference = '';
$this->writeNode($node);
$ref = $this->reference;
$this->reference = null;
return $ref;
}
private function writeNode(NodeInterface $node, NodeInterface $parentNode = null, int $depth = 0, bool $prototypedArray = false)
{
$comments = [];
$default = '';
$defaultArray = null;
$children = null;
$example = null;
if ($node instanceof BaseNode) {
$example = $node->getExample();
}
// defaults
if ($node instanceof ArrayNode) {
$children = $node->getChildren();
if ($node instanceof PrototypedArrayNode) {
$children = $this->getPrototypeChildren($node);
}
if (!$children) {
if ($node->hasDefaultValue() && \count($defaultArray = $node->getDefaultValue())) {
$default = '';
} elseif (!\is_array($example)) {
$default = '[]';
}
}
} elseif ($node instanceof EnumNode) {
$comments[] = 'One of '.implode('; ', array_map('json_encode', $node->getValues()));
$default = $node->hasDefaultValue() ? Inline::dump($node->getDefaultValue()) : '~';
} elseif (VariableNode::class === \get_class($node) && \is_array($example)) {
// If there is an array example, we are sure we dont need to print a default value
$default = '';
} else {
$default = '~';
if ($node->hasDefaultValue()) {
$default = $node->getDefaultValue();
if (\is_array($default)) {
if (\count($defaultArray = $node->getDefaultValue())) {
$default = '';
} elseif (!\is_array($example)) {
$default = '[]';
}
} else {
$default = Inline::dump($default);
}
}
}
// required?
if ($node->isRequired()) {
$comments[] = 'Required';
}
// deprecated?
if ($node instanceof BaseNode && $node->isDeprecated()) {
$comments[] = sprintf('Deprecated (%s)', $node->getDeprecationMessage($node->getName(), $parentNode ? $parentNode->getPath() : $node->getPath()));
}
// example
if ($example && !\is_array($example)) {
$comments[] = 'Example: '.Inline::dump($example);
}
$default = '' != (string) $default ? ' '.$default : '';
$comments = \count($comments) ? '# '.implode(', ', $comments) : '';
$key = $prototypedArray ? '-' : $node->getName().':';
$text = rtrim(sprintf('%-21s%s %s', $key, $default, $comments), ' ');
if ($node instanceof BaseNode && $info = $node->getInfo()) {
$this->writeLine('');
// indenting multi-line info
$info = str_replace("\n", sprintf("\n%".($depth * 4).'s# ', ' '), $info);
$this->writeLine('# '.$info, $depth * 4);
}
$this->writeLine($text, $depth * 4);
// output defaults
if ($defaultArray) {
$this->writeLine('');
$message = \count($defaultArray) > 1 ? 'Defaults' : 'Default';
$this->writeLine('# '.$message.':', $depth * 4 + 4);
$this->writeArray($defaultArray, $depth + 1);
}
if (\is_array($example)) {
$this->writeLine('');
$message = \count($example) > 1 ? 'Examples' : 'Example';
$this->writeLine('# '.$message.':', $depth * 4 + 4);
$this->writeArray(array_map([Inline::class, 'dump'], $example), $depth + 1);
}
if ($children) {
foreach ($children as $childNode) {
$this->writeNode($childNode, $node, $depth + 1, $node instanceof PrototypedArrayNode && !$node->getKeyAttribute());
}
}
}
/**
* Outputs a single config reference line.
*/
private function writeLine(string $text, int $indent = 0)
{
$indent = \strlen($text) + $indent;
$format = '%'.$indent.'s';
$this->reference .= sprintf($format, $text)."\n";
}
private function writeArray(array $array, int $depth)
{
$isIndexed = array_values($array) === $array;
foreach ($array as $key => $value) {
if (\is_array($value)) {
$val = '';
} else {
$val = $value;
}
if ($isIndexed) {
$this->writeLine('- '.$val, $depth * 4);
} else {
$this->writeLine(sprintf('%-20s %s', $key.':', $val), $depth * 4);
}
if (\is_array($value)) {
$this->writeArray($value, $depth + 1);
}
}
}
private function getPrototypeChildren(PrototypedArrayNode $node): array
{
$prototype = $node->getPrototype();
$key = $node->getKeyAttribute();
// Do not expand prototype if it isn't an array node nor uses attribute as key
if (!$key && !$prototype instanceof ArrayNode) {
return $node->getChildren();
}
if ($prototype instanceof ArrayNode) {
$keyNode = new ArrayNode($key, $node);
$children = $prototype->getChildren();
if ($prototype instanceof PrototypedArrayNode && $prototype->getKeyAttribute()) {
$children = $this->getPrototypeChildren($prototype);
}
// add children
foreach ($children as $childNode) {
$keyNode->addChild($childNode);
}
} else {
$keyNode = new ScalarNode($key, $node);
}
$info = 'Prototype';
if (null !== $prototype->getInfo()) {
$info .= ': '.$prototype->getInfo();
}
$keyNode->setInfo($info);
return [$key => $keyNode];
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
/**
* Node which only allows a finite set of values.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class EnumNode extends ScalarNode
{
private $values;
public function __construct(?string $name, NodeInterface $parent = null, array $values = [], string $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR)
{
$values = array_unique($values);
if (empty($values)) {
throw new \InvalidArgumentException('$values must contain at least one element.');
}
parent::__construct($name, $parent, $pathSeparator);
$this->values = $values;
}
public function getValues()
{
return $this->values;
}
/**
* {@inheritdoc}
*/
protected function finalizeValue($value)
{
$value = parent::finalizeValue($value);
if (!\in_array($value, $this->values, true)) {
$ex = new InvalidConfigurationException(sprintf('The value %s is not allowed for path "%s". Permissible values: %s', json_encode($value), $this->getPath(), implode(', ', array_map('json_encode', $this->values))));
$ex->setPath($this->getPath());
throw $ex;
}
return $value;
}
/**
* {@inheritdoc}
*/
protected function allowPlaceholders(): bool
{
return false;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Exception;
/**
* This exception is thrown whenever the key of an array is not unique. This can
* only be the case if the configuration is coming from an XML file.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class DuplicateKeyException extends InvalidConfigurationException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Exception;
/**
* Base exception for all configuration exceptions.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class Exception extends \RuntimeException
{
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Exception;
/**
* This exception is thrown when a configuration path is overwritten from a
* subsequent configuration file, but the entry node specifically forbids this.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ForbiddenOverwriteException extends InvalidConfigurationException
{
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Exception;
/**
* A very general exception which can be thrown whenever non of the more specific
* exceptions is suitable.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class InvalidConfigurationException extends Exception
{
private $path;
private $containsHints = false;
public function setPath($path)
{
$this->path = $path;
}
public function getPath()
{
return $this->path;
}
/**
* Adds extra information that is suffixed to the original exception message.
*
* @param string $hint
*/
public function addHint($hint)
{
if (!$this->containsHints) {
$this->message .= "\nHint: ".$hint;
$this->containsHints = true;
} else {
$this->message .= ', '.$hint;
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Exception;
/**
* Thrown when an error is detected in a node Definition.
*
* @author Victor Berchet <victor.berchet@suumit.com>
*/
class InvalidDefinitionException extends Exception
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Exception;
/**
* This exception is thrown if an invalid type is encountered.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class InvalidTypeException extends InvalidConfigurationException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Exception;
/**
* @author Roland Franssen <franssen.roland@gmail.com>
*
* @internal
*/
class TreeWithoutRootNodeException extends \RuntimeException
{
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition\Exception;
/**
* This exception is usually not encountered by the end-user, but only used
* internally to signal the parent scope to unset a key.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class UnsetKeyException extends Exception
{
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
/**
* This node represents a float value in the config tree.
*
* @author Jeanmonod David <david.jeanmonod@gmail.com>
*/
class FloatNode extends NumericNode
{
/**
* {@inheritdoc}
*/
protected function validateType($value)
{
// Integers are also accepted, we just cast them
if (\is_int($value)) {
$value = (float) $value;
}
if (!\is_float($value)) {
$ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected float, but got %s.', $this->getPath(), \gettype($value)));
if ($hint = $this->getInfo()) {
$ex->addHint($hint);
}
$ex->setPath($this->getPath());
throw $ex;
}
}
/**
* {@inheritdoc}
*/
protected function getValidPlaceholderTypes(): array
{
return ['float'];
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
/**
* This node represents an integer value in the config tree.
*
* @author Jeanmonod David <david.jeanmonod@gmail.com>
*/
class IntegerNode extends NumericNode
{
/**
* {@inheritdoc}
*/
protected function validateType($value)
{
if (!\is_int($value)) {
$ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected int, but got %s.', $this->getPath(), \gettype($value)));
if ($hint = $this->getInfo()) {
$ex->addHint($hint);
}
$ex->setPath($this->getPath());
throw $ex;
}
}
/**
* {@inheritdoc}
*/
protected function getValidPlaceholderTypes(): array
{
return ['int'];
}
}

View File

@@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
use Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
/**
* Common Interface among all nodes.
*
* In most cases, it is better to inherit from BaseNode instead of implementing
* this interface yourself.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface NodeInterface
{
/**
* Returns the name of the node.
*
* @return string The name of the node
*/
public function getName();
/**
* Returns the path of the node.
*
* @return string The node path
*/
public function getPath();
/**
* Returns true when the node is required.
*
* @return bool If the node is required
*/
public function isRequired();
/**
* Returns true when the node has a default value.
*
* @return bool If the node has a default value
*/
public function hasDefaultValue();
/**
* Returns the default value of the node.
*
* @return mixed The default value
*
* @throws \RuntimeException if the node has no default value
*/
public function getDefaultValue();
/**
* Normalizes a value.
*
* @param mixed $value The value to normalize
*
* @return mixed The normalized value
*
* @throws InvalidTypeException if the value type is invalid
*/
public function normalize($value);
/**
* Merges two values together.
*
* @param mixed $leftSide
* @param mixed $rightSide
*
* @return mixed The merged value
*
* @throws ForbiddenOverwriteException if the configuration path cannot be overwritten
* @throws InvalidTypeException if the value type is invalid
*/
public function merge($leftSide, $rightSide);
/**
* Finalizes a value.
*
* @param mixed $value The value to finalize
*
* @return mixed The finalized value
*
* @throws InvalidTypeException if the value type is invalid
* @throws InvalidConfigurationException if the value is invalid configuration
*/
public function finalize($value);
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
/**
* This node represents a numeric value in the config tree.
*
* @author David Jeanmonod <david.jeanmonod@gmail.com>
*/
class NumericNode extends ScalarNode
{
protected $min;
protected $max;
/**
* @param int|float|null $min
* @param int|float|null $max
*/
public function __construct(?string $name, NodeInterface $parent = null, $min = null, $max = null, string $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR)
{
parent::__construct($name, $parent, $pathSeparator);
$this->min = $min;
$this->max = $max;
}
/**
* {@inheritdoc}
*/
protected function finalizeValue($value)
{
$value = parent::finalizeValue($value);
$errorMsg = null;
if (isset($this->min) && $value < $this->min) {
$errorMsg = sprintf('The value %s is too small for path "%s". Should be greater than or equal to %s', $value, $this->getPath(), $this->min);
}
if (isset($this->max) && $value > $this->max) {
$errorMsg = sprintf('The value %s is too big for path "%s". Should be less than or equal to %s', $value, $this->getPath(), $this->max);
}
if (isset($errorMsg)) {
$ex = new InvalidConfigurationException($errorMsg);
$ex->setPath($this->getPath());
throw $ex;
}
return $value;
}
/**
* {@inheritdoc}
*/
protected function isValueEmpty($value)
{
// a numeric value cannot be empty
return false;
}
}

View File

@@ -0,0 +1,97 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
/**
* This class is the entry point for config normalization/merging/finalization.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @final since version 4.1
*/
class Processor
{
/**
* Processes an array of configurations.
*
* @param array $configs An array of configuration items to process
*
* @return array The processed configuration
*/
public function process(NodeInterface $configTree, array $configs)
{
$currentConfig = [];
foreach ($configs as $config) {
$config = $configTree->normalize($config);
$currentConfig = $configTree->merge($currentConfig, $config);
}
return $configTree->finalize($currentConfig);
}
/**
* Processes an array of configurations.
*
* @param array $configs An array of configuration items to process
*
* @return array The processed configuration
*/
public function processConfiguration(ConfigurationInterface $configuration, array $configs)
{
return $this->process($configuration->getConfigTreeBuilder()->buildTree(), $configs);
}
/**
* Normalizes a configuration entry.
*
* This method returns a normalize configuration array for a given key
* to remove the differences due to the original format (YAML and XML mainly).
*
* Here is an example.
*
* The configuration in XML:
*
* <twig:extension>twig.extension.foo</twig:extension>
* <twig:extension>twig.extension.bar</twig:extension>
*
* And the same configuration in YAML:
*
* extensions: ['twig.extension.foo', 'twig.extension.bar']
*
* @param array $config A config array
* @param string $key The key to normalize
* @param string $plural The plural form of the key if it is irregular
*
* @return array
*/
public static function normalizeConfig($config, $key, $plural = null)
{
if (null === $plural) {
$plural = $key.'s';
}
if (isset($config[$plural])) {
return $config[$plural];
}
if (isset($config[$key])) {
if (\is_string($config[$key]) || !\is_int(key($config[$key]))) {
// only one
return [$config[$key]];
}
return $config[$key];
}
return [];
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
/**
* This interface must be implemented by nodes which can be used as prototypes.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface PrototypeNodeInterface extends NodeInterface
{
/**
* Sets the name of the node.
*
* @param string $name The name of the node
*/
public function setName($name);
}

View File

@@ -0,0 +1,360 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
use Symfony\Component\Config\Definition\Exception\DuplicateKeyException;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Exception\UnsetKeyException;
/**
* Represents a prototyped Array node in the config tree.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class PrototypedArrayNode extends ArrayNode
{
protected $prototype;
protected $keyAttribute;
protected $removeKeyAttribute = false;
protected $minNumberOfElements = 0;
protected $defaultValue = [];
protected $defaultChildren;
/**
* @var NodeInterface[] An array of the prototypes of the simplified value children
*/
private $valuePrototypes = [];
/**
* Sets the minimum number of elements that a prototype based node must
* contain. By default this is zero, meaning no elements.
*
* @param int $number
*/
public function setMinNumberOfElements($number)
{
$this->minNumberOfElements = $number;
}
/**
* Sets the attribute which value is to be used as key.
*
* This is useful when you have an indexed array that should be an
* associative array. You can select an item from within the array
* to be the key of the particular item. For example, if "id" is the
* "key", then:
*
* [
* ['id' => 'my_name', 'foo' => 'bar'],
* ];
*
* becomes
*
* [
* 'my_name' => ['foo' => 'bar'],
* ];
*
* If you'd like "'id' => 'my_name'" to still be present in the resulting
* array, then you can set the second argument of this method to false.
*
* @param string $attribute The name of the attribute which value is to be used as a key
* @param bool $remove Whether or not to remove the key
*/
public function setKeyAttribute($attribute, $remove = true)
{
$this->keyAttribute = $attribute;
$this->removeKeyAttribute = $remove;
}
/**
* Retrieves the name of the attribute which value should be used as key.
*
* @return string|null The name of the attribute
*/
public function getKeyAttribute()
{
return $this->keyAttribute;
}
/**
* Sets the default value of this node.
*
* @param string $value
*
* @throws \InvalidArgumentException if the default value is not an array
*/
public function setDefaultValue($value)
{
if (!\is_array($value)) {
throw new \InvalidArgumentException($this->getPath().': the default value of an array node has to be an array.');
}
$this->defaultValue = $value;
}
/**
* {@inheritdoc}
*/
public function hasDefaultValue()
{
return true;
}
/**
* Adds default children when none are set.
*
* @param int|string|array|null $children The number of children|The child name|The children names to be added
*/
public function setAddChildrenIfNoneSet($children = ['defaults'])
{
if (null === $children) {
$this->defaultChildren = ['defaults'];
} else {
$this->defaultChildren = \is_int($children) && $children > 0 ? range(1, $children) : (array) $children;
}
}
/**
* {@inheritdoc}
*
* The default value could be either explicited or derived from the prototype
* default value.
*/
public function getDefaultValue()
{
if (null !== $this->defaultChildren) {
$default = $this->prototype->hasDefaultValue() ? $this->prototype->getDefaultValue() : [];
$defaults = [];
foreach (array_values($this->defaultChildren) as $i => $name) {
$defaults[null === $this->keyAttribute ? $i : $name] = $default;
}
return $defaults;
}
return $this->defaultValue;
}
/**
* Sets the node prototype.
*/
public function setPrototype(PrototypeNodeInterface $node)
{
$this->prototype = $node;
}
/**
* Retrieves the prototype.
*
* @return PrototypeNodeInterface The prototype
*/
public function getPrototype()
{
return $this->prototype;
}
/**
* Disable adding concrete children for prototyped nodes.
*
* @throws Exception
*/
public function addChild(NodeInterface $node)
{
throw new Exception('A prototyped array node can not have concrete children.');
}
/**
* {@inheritdoc}
*/
protected function finalizeValue($value)
{
if (false === $value) {
throw new UnsetKeyException(sprintf('Unsetting key for path "%s", value: "%s".', $this->getPath(), json_encode($value)));
}
foreach ($value as $k => $v) {
$prototype = $this->getPrototypeForChild($k);
try {
$value[$k] = $prototype->finalize($v);
} catch (UnsetKeyException $e) {
unset($value[$k]);
}
}
if (\count($value) < $this->minNumberOfElements) {
$ex = new InvalidConfigurationException(sprintf('The path "%s" should have at least %d element(s) defined.', $this->getPath(), $this->minNumberOfElements));
$ex->setPath($this->getPath());
throw $ex;
}
return $value;
}
/**
* {@inheritdoc}
*
* @throws DuplicateKeyException
*/
protected function normalizeValue($value)
{
if (false === $value) {
return $value;
}
$value = $this->remapXml($value);
$isList = array_is_list($value);
$normalized = [];
foreach ($value as $k => $v) {
if (null !== $this->keyAttribute && \is_array($v)) {
if (!isset($v[$this->keyAttribute]) && \is_int($k) && $isList) {
$ex = new InvalidConfigurationException(sprintf('The attribute "%s" must be set for path "%s".', $this->keyAttribute, $this->getPath()));
$ex->setPath($this->getPath());
throw $ex;
} elseif (isset($v[$this->keyAttribute])) {
$k = $v[$this->keyAttribute];
if (\is_float($k)) {
$k = var_export($k, true);
}
// remove the key attribute when required
if ($this->removeKeyAttribute) {
unset($v[$this->keyAttribute]);
}
// if only "value" is left
if (array_keys($v) === ['value']) {
$v = $v['value'];
if ($this->prototype instanceof ArrayNode && ($children = $this->prototype->getChildren()) && \array_key_exists('value', $children)) {
$valuePrototype = current($this->valuePrototypes) ?: clone $children['value'];
$valuePrototype->parent = $this;
$originalClosures = $this->prototype->normalizationClosures;
if (\is_array($originalClosures)) {
$valuePrototypeClosures = $valuePrototype->normalizationClosures;
$valuePrototype->normalizationClosures = \is_array($valuePrototypeClosures) ? array_merge($originalClosures, $valuePrototypeClosures) : $originalClosures;
}
$this->valuePrototypes[$k] = $valuePrototype;
}
}
}
if (\array_key_exists($k, $normalized)) {
$ex = new DuplicateKeyException(sprintf('Duplicate key "%s" for path "%s".', $k, $this->getPath()));
$ex->setPath($this->getPath());
throw $ex;
}
}
$prototype = $this->getPrototypeForChild($k);
if (null !== $this->keyAttribute || !$isList) {
$normalized[$k] = $prototype->normalize($v);
} else {
$normalized[] = $prototype->normalize($v);
}
}
return $normalized;
}
/**
* {@inheritdoc}
*/
protected function mergeValues($leftSide, $rightSide)
{
if (false === $rightSide) {
// if this is still false after the last config has been merged the
// finalization pass will take care of removing this key entirely
return false;
}
if (false === $leftSide || !$this->performDeepMerging) {
return $rightSide;
}
$isList = array_is_list($rightSide);
foreach ($rightSide as $k => $v) {
// prototype, and key is irrelevant there are no named keys, append the element
if (null === $this->keyAttribute && $isList) {
$leftSide[] = $v;
continue;
}
// no conflict
if (!\array_key_exists($k, $leftSide)) {
if (!$this->allowNewKeys) {
$ex = new InvalidConfigurationException(sprintf('You are not allowed to define new elements for path "%s". Please define all elements for this path in one config file.', $this->getPath()));
$ex->setPath($this->getPath());
throw $ex;
}
$leftSide[$k] = $v;
continue;
}
$prototype = $this->getPrototypeForChild($k);
$leftSide[$k] = $prototype->merge($leftSide[$k], $v);
}
return $leftSide;
}
/**
* Returns a prototype for the child node that is associated to $key in the value array.
* For general child nodes, this will be $this->prototype.
* But if $this->removeKeyAttribute is true and there are only two keys in the child node:
* one is same as this->keyAttribute and the other is 'value', then the prototype will be different.
*
* For example, assume $this->keyAttribute is 'name' and the value array is as follows:
*
* [
* [
* 'name' => 'name001',
* 'value' => 'value001'
* ]
* ]
*
* Now, the key is 0 and the child node is:
*
* [
* 'name' => 'name001',
* 'value' => 'value001'
* ]
*
* When normalizing the value array, the 'name' element will removed from the child node
* and its value becomes the new key of the child node:
*
* [
* 'name001' => ['value' => 'value001']
* ]
*
* Now only 'value' element is left in the child node which can be further simplified into a string:
*
* ['name001' => 'value001']
*
* Now, the key becomes 'name001' and the child node becomes 'value001' and
* the prototype of child node 'name001' should be a ScalarNode instead of an ArrayNode instance.
*
* @return mixed The prototype instance
*/
private function getPrototypeForChild(string $key)
{
$prototype = $this->valuePrototypes[$key] ?? $this->prototype;
$prototype->setName($key);
return $prototype;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
use Symfony\Component\Config\Definition\Exception\InvalidTypeException;
/**
* This node represents a scalar value in the config tree.
*
* The following values are considered scalars:
* * booleans
* * strings
* * null
* * integers
* * floats
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ScalarNode extends VariableNode
{
/**
* {@inheritdoc}
*/
protected function validateType($value)
{
if (!is_scalar($value) && null !== $value) {
$ex = new InvalidTypeException(sprintf('Invalid type for path "%s". Expected scalar, but got %s.', $this->getPath(), \gettype($value)));
if ($hint = $this->getInfo()) {
$ex->addHint($hint);
}
$ex->setPath($this->getPath());
throw $ex;
}
}
/**
* {@inheritdoc}
*/
protected function isValueEmpty($value)
{
// assume environment variables are never empty (which in practice is likely to be true during runtime)
// not doing so breaks many configs that are valid today
if ($this->isHandlingPlaceholder()) {
return false;
}
return null === $value || '' === $value;
}
/**
* {@inheritdoc}
*/
protected function getValidPlaceholderTypes(): array
{
return ['bool', 'int', 'float', 'string'];
}
}

View File

@@ -0,0 +1,143 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Definition;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
/**
* This node represents a value of variable type in the config tree.
*
* This node is intended for values of arbitrary type.
* Any PHP type is accepted as a value.
*
* @author Jeremy Mikola <jmikola@gmail.com>
*/
class VariableNode extends BaseNode implements PrototypeNodeInterface
{
protected $defaultValueSet = false;
protected $defaultValue;
protected $allowEmptyValue = true;
public function setDefaultValue($value)
{
$this->defaultValueSet = true;
$this->defaultValue = $value;
}
/**
* {@inheritdoc}
*/
public function hasDefaultValue()
{
return $this->defaultValueSet;
}
/**
* {@inheritdoc}
*/
public function getDefaultValue()
{
$v = $this->defaultValue;
return $v instanceof \Closure ? $v() : $v;
}
/**
* Sets if this node is allowed to have an empty value.
*
* @param bool $boolean True if this entity will accept empty values
*/
public function setAllowEmptyValue($boolean)
{
$this->allowEmptyValue = (bool) $boolean;
}
/**
* {@inheritdoc}
*/
public function setName($name)
{
$this->name = $name;
}
/**
* {@inheritdoc}
*/
protected function validateType($value)
{
}
/**
* {@inheritdoc}
*/
protected function finalizeValue($value)
{
// deny environment variables only when using custom validators
// this avoids ever passing an empty value to final validation closures
if (!$this->allowEmptyValue && $this->isHandlingPlaceholder() && $this->finalValidationClosures) {
@trigger_error(sprintf('Setting path "%s" to an environment variable is deprecated since Symfony 4.3. Remove "cannotBeEmpty()", "validate()" or include a prefix/suffix value instead.', $this->getPath()), \E_USER_DEPRECATED);
// $e = new InvalidConfigurationException(sprintf('The path "%s" cannot contain an environment variable when empty values are not allowed by definition and are validated.', $this->getPath()));
// if ($hint = $this->getInfo()) {
// $e->addHint($hint);
// }
// $e->setPath($this->getPath());
//
// throw $e;
}
if (!$this->allowEmptyValue && $this->isValueEmpty($value)) {
$ex = new InvalidConfigurationException(sprintf('The path "%s" cannot contain an empty value, but got %s.', $this->getPath(), json_encode($value)));
if ($hint = $this->getInfo()) {
$ex->addHint($hint);
}
$ex->setPath($this->getPath());
throw $ex;
}
return $value;
}
/**
* {@inheritdoc}
*/
protected function normalizeValue($value)
{
return $value;
}
/**
* {@inheritdoc}
*/
protected function mergeValues($leftSide, $rightSide)
{
return $rightSide;
}
/**
* Evaluates if the given value is to be treated as empty.
*
* By default, PHP's empty() function is used to test for emptiness. This
* method may be overridden by subtypes to better match their understanding
* of empty data.
*
* @param mixed $value
*
* @return bool
*
* @see finalizeValue()
*/
protected function isValueEmpty($value)
{
return empty($value);
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Exception;
/**
* Exception class for when a circular reference is detected when importing resources.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class FileLoaderImportCircularReferenceException extends LoaderLoadException
{
public function __construct(array $resources, ?int $code = 0, \Throwable $previous = null)
{
$message = sprintf('Circular reference detected in "%s" ("%s" > "%s").', $this->varToString($resources[0]), implode('" > "', $resources), $resources[0]);
\Exception::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,111 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Exception;
/**
* Exception class for when a resource cannot be loaded or imported.
*
* @author Ryan Weaver <ryan@thatsquality.com>
*
* @deprecated since Symfony 4.2, use LoaderLoadException instead.
*/
class FileLoaderLoadException extends \Exception
{
/**
* @param string $resource The resource that could not be imported
* @param string|null $sourceResource The original resource importing the new resource
* @param int|null $code The error code
* @param \Throwable|null $previous A previous exception
* @param string|null $type The type of resource
*/
public function __construct(string $resource, string $sourceResource = null, ?int $code = 0, \Throwable $previous = null, string $type = null)
{
$message = '';
if ($previous) {
// Include the previous exception, to help the user see what might be the underlying cause
// Trim the trailing period of the previous message. We only want 1 period remove so no rtrim...
if (str_ends_with($previous->getMessage(), '.')) {
$trimmedMessage = substr($previous->getMessage(), 0, -1);
$message .= sprintf('%s', $trimmedMessage).' in ';
} else {
$message .= sprintf('%s', $previous->getMessage()).' in ';
}
$message .= $resource.' ';
// show tweaked trace to complete the human readable sentence
if (null === $sourceResource) {
$message .= sprintf('(which is loaded in resource "%s")', $resource);
} else {
$message .= sprintf('(which is being imported from "%s")', $sourceResource);
}
$message .= '.';
// if there's no previous message, present it the default way
} elseif (null === $sourceResource) {
$message .= sprintf('Cannot load resource "%s".', $resource);
} else {
$message .= sprintf('Cannot import resource "%s" from "%s".', $resource, $sourceResource);
}
// Is the resource located inside a bundle?
if ('@' === $resource[0]) {
$parts = explode(\DIRECTORY_SEPARATOR, $resource);
$bundle = substr($parts[0], 1);
$message .= sprintf(' Make sure the "%s" bundle is correctly registered and loaded in the application kernel class.', $bundle);
$message .= sprintf(' If the bundle is registered, make sure the bundle path "%s" is not empty.', $resource);
} elseif (null !== $type) {
// maybe there is no loader for this specific type
if ('annotation' === $type) {
$message .= ' Make sure annotations are installed and enabled.';
} else {
$message .= sprintf(' Make sure there is a loader supporting the "%s" type.', $type);
}
}
parent::__construct($message, $code, $previous);
}
protected function varToString($var)
{
if (\is_object($var)) {
return sprintf('Object(%s)', \get_class($var));
}
if (\is_array($var)) {
$a = [];
foreach ($var as $k => $v) {
$a[] = sprintf('%s => %s', $k, $this->varToString($v));
}
return sprintf('Array(%s)', implode(', ', $a));
}
if (\is_resource($var)) {
return sprintf('Resource(%s)', get_resource_type($var));
}
if (null === $var) {
return 'null';
}
if (false === $var) {
return 'false';
}
if (true === $var) {
return 'true';
}
return (string) $var;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Exception;
/**
* File locator exception if a file does not exist.
*
* @author Leo Feyer <https://github.com/leofeyer>
*/
class FileLocatorFileNotFoundException extends \InvalidArgumentException
{
private $paths;
public function __construct(string $message = '', int $code = 0, \Throwable $previous = null, array $paths = [])
{
parent::__construct($message, $code, $previous);
$this->paths = $paths;
}
public function getPaths()
{
return $this->paths;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Exception;
/**
* Exception class for when a resource cannot be loaded or imported.
*
* @author Ryan Weaver <ryan@thatsquality.com>
*/
class LoaderLoadException extends FileLoaderLoadException
{
}

View File

@@ -0,0 +1,94 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config;
use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
/**
* FileLocator uses an array of pre-defined paths to find files.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class FileLocator implements FileLocatorInterface
{
protected $paths;
/**
* @param string|string[] $paths A path or an array of paths where to look for resources
*/
public function __construct($paths = [])
{
$this->paths = (array) $paths;
}
/**
* {@inheritdoc}
*/
public function locate($name, $currentPath = null, $first = true)
{
if ('' == $name) {
throw new \InvalidArgumentException('An empty file name is not valid to be located.');
}
if ($this->isAbsolutePath($name)) {
if (!file_exists($name)) {
throw new FileLocatorFileNotFoundException(sprintf('The file "%s" does not exist.', $name), 0, null, [$name]);
}
return $name;
}
$paths = $this->paths;
if (null !== $currentPath) {
array_unshift($paths, $currentPath);
}
$paths = array_unique($paths);
$filepaths = $notfound = [];
foreach ($paths as $path) {
if (@file_exists($file = $path.\DIRECTORY_SEPARATOR.$name)) {
if (true === $first) {
return $file;
}
$filepaths[] = $file;
} else {
$notfound[] = $file;
}
}
if (!$filepaths) {
throw new FileLocatorFileNotFoundException(sprintf('The file "%s" does not exist (in: "%s").', $name, implode('", "', $paths)), 0, null, $notfound);
}
return $filepaths;
}
/**
* Returns whether the file path is an absolute path.
*/
private function isAbsolutePath(string $file): bool
{
if ('/' === $file[0] || '\\' === $file[0]
|| (\strlen($file) > 3 && ctype_alpha($file[0])
&& ':' === $file[1]
&& ('\\' === $file[2] || '/' === $file[2])
)
|| null !== parse_url($file, \PHP_URL_SCHEME)
) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config;
use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
interface FileLocatorInterface
{
/**
* Returns a full path for a given file name.
*
* @param string $name The file name to locate
* @param string|null $currentPath The current path
* @param bool $first Whether to return the first occurrence or an array of filenames
*
* @return string|array The full path to the file or an array of file paths
*
* @throws \InvalidArgumentException If $name is empty
* @throws FileLocatorFileNotFoundException If a file is not found
*/
public function locate($name, $currentPath = null, $first = true);
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-2022 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Loader;
use Symfony\Component\Config\Exception\LoaderLoadException;
/**
* DelegatingLoader delegates loading to other loaders using a loader resolver.
*
* This loader acts as an array of LoaderInterface objects - each having
* a chance to load a given resource (handled by the resolver)
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class DelegatingLoader extends Loader
{
public function __construct(LoaderResolverInterface $resolver)
{
$this->resolver = $resolver;
}
/**
* {@inheritdoc}
*/
public function load($resource, $type = null)
{
if (false === $loader = $this->resolver->resolve($resource, $type)) {
throw new LoaderLoadException($resource, null, 0, null, $type);
}
return $loader->load($resource, $type);
}
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
{
return false !== $this->resolver->resolve($resource, $type);
}
}

View File

@@ -0,0 +1,186 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Loader;
use Symfony\Component\Config\Exception\FileLoaderImportCircularReferenceException;
use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
use Symfony\Component\Config\Exception\LoaderLoadException;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Config\Resource\FileExistenceResource;
use Symfony\Component\Config\Resource\GlobResource;
/**
* FileLoader is the abstract class used by all built-in loaders that are file based.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class FileLoader extends Loader
{
protected static $loading = [];
protected $locator;
private $currentDir;
public function __construct(FileLocatorInterface $locator)
{
$this->locator = $locator;
}
/**
* Sets the current directory.
*
* @param string $dir
*/
public function setCurrentDir($dir)
{
$this->currentDir = $dir;
}
/**
* Returns the file locator used by this loader.
*
* @return FileLocatorInterface
*/
public function getLocator()
{
return $this->locator;
}
/**
* Imports a resource.
*
* @param mixed $resource A Resource
* @param string|null $type The resource type or null if unknown
* @param bool $ignoreErrors Whether to ignore import errors or not
* @param string|null $sourceResource The original resource importing the new resource
* @param string|string[]|null $exclude Glob patterns to exclude from the import
*
* @return mixed
*
* @throws LoaderLoadException
* @throws FileLoaderImportCircularReferenceException
* @throws FileLocatorFileNotFoundException
*/
public function import($resource, $type = null, $ignoreErrors = false, $sourceResource = null/*, $exclude = null*/)
{
if (\func_num_args() < 5 && __CLASS__ !== static::class && !str_starts_with(static::class, 'Symfony\Component\\') && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) {
@trigger_error(sprintf('The "%s()" method will have a new "$exclude = null" argument in version 5.0, not defining it is deprecated since Symfony 4.4.', __METHOD__), \E_USER_DEPRECATED);
}
$exclude = \func_num_args() >= 5 ? func_get_arg(4) : null;
if (\is_string($resource) && \strlen($resource) !== ($i = strcspn($resource, '*?{[')) && !str_contains($resource, "\n")) {
$excluded = [];
foreach ((array) $exclude as $pattern) {
foreach ($this->glob($pattern, true, $_, false, true) as $path => $info) {
// normalize Windows slashes and remove trailing slashes
$excluded[rtrim(str_replace('\\', '/', $path), '/')] = true;
}
}
$ret = [];
$isSubpath = 0 !== $i && str_contains(substr($resource, 0, $i), '/');
foreach ($this->glob($resource, false, $_, $ignoreErrors || !$isSubpath, false, $excluded) as $path => $info) {
if (null !== $res = $this->doImport($path, 'glob' === $type ? null : $type, $ignoreErrors, $sourceResource)) {
$ret[] = $res;
}
$isSubpath = true;
}
if ($isSubpath) {
return isset($ret[1]) ? $ret : ($ret[0] ?? null);
}
}
return $this->doImport($resource, $type, $ignoreErrors, $sourceResource);
}
/**
* @internal
*/
protected function glob(string $pattern, bool $recursive, &$resource = null, bool $ignoreErrors = false, bool $forExclusion = false, array $excluded = [])
{
if (\strlen($pattern) === $i = strcspn($pattern, '*?{[')) {
$prefix = $pattern;
$pattern = '';
} elseif (0 === $i || !str_contains(substr($pattern, 0, $i), '/')) {
$prefix = '.';
$pattern = '/'.$pattern;
} else {
$prefix = \dirname(substr($pattern, 0, 1 + $i));
$pattern = substr($pattern, \strlen($prefix));
}
try {
$prefix = $this->locator->locate($prefix, $this->currentDir, true);
} catch (FileLocatorFileNotFoundException $e) {
if (!$ignoreErrors) {
throw $e;
}
$resource = [];
foreach ($e->getPaths() as $path) {
$resource[] = new FileExistenceResource($path);
}
return;
}
$resource = new GlobResource($prefix, $pattern, $recursive, $forExclusion, $excluded);
yield from $resource;
}
private function doImport($resource, string $type = null, bool $ignoreErrors = false, $sourceResource = null)
{
try {
$loader = $this->resolve($resource, $type);
if ($loader instanceof self && null !== $this->currentDir) {
$resource = $loader->getLocator()->locate($resource, $this->currentDir, false);
}
$resources = \is_array($resource) ? $resource : [$resource];
for ($i = 0; $i < $resourcesCount = \count($resources); ++$i) {
if (isset(self::$loading[$resources[$i]])) {
if ($i == $resourcesCount - 1) {
throw new FileLoaderImportCircularReferenceException(array_keys(self::$loading));
}
} else {
$resource = $resources[$i];
break;
}
}
self::$loading[$resource] = true;
try {
$ret = $loader->load($resource, $type);
} finally {
unset(self::$loading[$resource]);
}
return $ret;
} catch (FileLoaderImportCircularReferenceException $e) {
throw $e;
} catch (\Exception $e) {
if (!$ignoreErrors) {
// prevent embedded imports from nesting multiple exceptions
if ($e instanceof LoaderLoadException) {
throw $e;
}
throw new LoaderLoadException($resource, $sourceResource, 0, $e, $type);
}
}
return null;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Loader;
/**
* GlobFileLoader loads files from a glob pattern.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class GlobFileLoader extends FileLoader
{
/**
* {@inheritdoc}
*/
public function load($resource, $type = null)
{
return $this->import($resource);
}
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
{
return 'glob' === $type;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Loader;
use Symfony\Component\Config\Exception\LoaderLoadException;
/**
* Loader is the abstract class used by all built-in loaders.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
abstract class Loader implements LoaderInterface
{
protected $resolver;
/**
* {@inheritdoc}
*/
public function getResolver()
{
return $this->resolver;
}
/**
* {@inheritdoc}
*/
public function setResolver(LoaderResolverInterface $resolver)
{
$this->resolver = $resolver;
}
/**
* Imports a resource.
*
* @param mixed $resource A resource
* @param string|null $type The resource type or null if unknown
*
* @return mixed
*/
public function import($resource, $type = null)
{
return $this->resolve($resource, $type)->load($resource, $type);
}
/**
* Finds a loader able to load an imported resource.
*
* @param mixed $resource A resource
* @param string|null $type The resource type or null if unknown
*
* @return $this|LoaderInterface
*
* @throws LoaderLoadException If no loader is found
*/
public function resolve($resource, $type = null)
{
if ($this->supports($resource, $type)) {
return $this;
}
$loader = null === $this->resolver ? false : $this->resolver->resolve($resource, $type);
if (false === $loader) {
throw new LoaderLoadException($resource, null, 0, null, $type);
}
return $loader;
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Loader;
/**
* LoaderInterface is the interface implemented by all loader classes.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface LoaderInterface
{
/**
* Loads a resource.
*
* @param mixed $resource The resource
* @param string|null $type The resource type or null if unknown
*
* @return mixed
*
* @throws \Exception If something went wrong
*/
public function load($resource, $type = null);
/**
* Returns whether this class supports the given resource.
*
* @param mixed $resource A resource
* @param string|null $type The resource type or null if unknown
*
* @return bool True if this class supports the given resource, false otherwise
*/
public function supports($resource, $type = null);
/**
* Gets the loader resolver.
*
* @return LoaderResolverInterface A LoaderResolverInterface instance
*/
public function getResolver();
/**
* Sets the loader resolver.
*/
public function setResolver(LoaderResolverInterface $resolver);
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Loader;
/**
* LoaderResolver selects a loader for a given resource.
*
* A resource can be anything (e.g. a full path to a config file or a Closure).
* Each loader determines whether it can load a resource and how.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class LoaderResolver implements LoaderResolverInterface
{
/**
* @var LoaderInterface[] An array of LoaderInterface objects
*/
private $loaders = [];
/**
* @param LoaderInterface[] $loaders An array of loaders
*/
public function __construct(array $loaders = [])
{
foreach ($loaders as $loader) {
$this->addLoader($loader);
}
}
/**
* {@inheritdoc}
*/
public function resolve($resource, $type = null)
{
foreach ($this->loaders as $loader) {
if ($loader->supports($resource, $type)) {
return $loader;
}
}
return false;
}
public function addLoader(LoaderInterface $loader)
{
$this->loaders[] = $loader;
$loader->setResolver($this);
}
/**
* Returns the registered loaders.
*
* @return LoaderInterface[] An array of LoaderInterface instances
*/
public function getLoaders()
{
return $this->loaders;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Loader;
/**
* LoaderResolverInterface selects a loader for a given resource.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface LoaderResolverInterface
{
/**
* Returns a loader able to load the resource.
*
* @param mixed $resource A resource
* @param string|null $type The resource type or null if unknown
*
* @return LoaderInterface|false The loader or false if none is able to load the resource
*/
public function resolve($resource, $type = null);
}

View File

@@ -0,0 +1,15 @@
Config Component
================
The Config component helps find, load, combine, autofill and validate
configuration values of any kind, whatever their source may be (YAML, XML, INI
files, or for instance a database).
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/config.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -0,0 +1,235 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Resource;
/**
* ClassExistenceResource represents a class existence.
* Freshness is only evaluated against resource existence.
*
* The resource must be a fully-qualified class name.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final since Symfony 4.3
*/
class ClassExistenceResource implements SelfCheckingResourceInterface
{
private $resource;
private $exists;
private static $autoloadLevel = 0;
private static $autoloadedClass;
private static $existsCache = [];
/**
* @param string $resource The fully-qualified class name
* @param bool|null $exists Boolean when the existency check has already been done
*/
public function __construct(string $resource, bool $exists = null)
{
$this->resource = $resource;
if (null !== $exists) {
$this->exists = [$exists, null];
}
}
public function __toString()
{
return $this->resource;
}
/**
* @return string The file path to the resource
*/
public function getResource()
{
return $this->resource;
}
/**
* {@inheritdoc}
*
* @throws \ReflectionException when a parent class/interface/trait is not found
*/
public function isFresh($timestamp)
{
$loaded = class_exists($this->resource, false) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
if (null !== $exists = &self::$existsCache[$this->resource]) {
if ($loaded) {
$exists = [true, null];
} elseif (0 >= $timestamp && !$exists[0] && null !== $exists[1]) {
throw new \ReflectionException($exists[1]);
}
} elseif ([false, null] === $exists = [$loaded, null]) {
if (!self::$autoloadLevel++) {
spl_autoload_register(__CLASS__.'::throwOnRequiredClass');
}
$autoloadedClass = self::$autoloadedClass;
self::$autoloadedClass = ltrim($this->resource, '\\');
try {
$exists[0] = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false);
} catch (\Exception $e) {
$exists[1] = $e->getMessage();
try {
self::throwOnRequiredClass($this->resource, $e);
} catch (\ReflectionException $e) {
if (0 >= $timestamp) {
throw $e;
}
}
} catch (\Throwable $e) {
$exists[1] = $e->getMessage();
throw $e;
} finally {
self::$autoloadedClass = $autoloadedClass;
if (!--self::$autoloadLevel) {
spl_autoload_unregister(__CLASS__.'::throwOnRequiredClass');
}
}
}
if (null === $this->exists) {
$this->exists = $exists;
}
return $this->exists[0] xor !$exists[0];
}
/**
* @internal
*/
public function __sleep(): array
{
if (null === $this->exists) {
$this->isFresh(0);
}
return ['resource', 'exists'];
}
/**
* @internal
*/
public function __wakeup()
{
if (\is_bool($this->exists)) {
$this->exists = [$this->exists, null];
}
}
/**
* Throws a reflection exception when the passed class does not exist but is required.
*
* A class is considered "not required" when it's loaded as part of a "class_exists" or similar check.
*
* This function can be used as an autoload function to throw a reflection
* exception if the class was not found by previous autoload functions.
*
* A previous exception can be passed. In this case, the class is considered as being
* required totally, so if it doesn't exist, a reflection exception is always thrown.
* If it exists, the previous exception is rethrown.
*
* @throws \ReflectionException
*
* @internal
*/
public static function throwOnRequiredClass($class, \Exception $previous = null)
{
// If the passed class is the resource being checked, we shouldn't throw.
if (null === $previous && self::$autoloadedClass === $class) {
return;
}
if (class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false)) {
if (null !== $previous) {
throw $previous;
}
return;
}
if ($previous instanceof \ReflectionException) {
throw $previous;
}
$message = sprintf('Class "%s" not found.', $class);
if (self::$autoloadedClass !== $class) {
$message = substr_replace($message, sprintf(' while loading "%s"', self::$autoloadedClass), -1, 0);
}
if (null !== $previous) {
$message = $previous->getMessage();
}
$e = new \ReflectionException($message, 0, $previous);
if (null !== $previous) {
throw $e;
}
$trace = debug_backtrace();
$autoloadFrame = [
'function' => 'spl_autoload_call',
'args' => [$class],
];
if (\PHP_VERSION_ID >= 80000 && isset($trace[1])) {
$callerFrame = $trace[1];
$i = 2;
} elseif (false !== $i = array_search($autoloadFrame, $trace, true)) {
$callerFrame = $trace[++$i];
} else {
throw $e;
}
if (isset($callerFrame['function']) && !isset($callerFrame['class'])) {
switch ($callerFrame['function']) {
case 'get_class_methods':
case 'get_class_vars':
case 'get_parent_class':
case 'is_a':
case 'is_subclass_of':
case 'class_exists':
case 'class_implements':
case 'class_parents':
case 'trait_exists':
case 'defined':
case 'interface_exists':
case 'method_exists':
case 'property_exists':
case 'is_callable':
return;
}
$props = [
'file' => $callerFrame['file'] ?? null,
'line' => $callerFrame['line'] ?? null,
'trace' => \array_slice($trace, 1 + $i),
];
foreach ($props as $p => $v) {
if (null !== $v) {
$r = new \ReflectionProperty(\Exception::class, $p);
$r->setAccessible(true);
$r->setValue($e, $v);
}
}
}
throw $e;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Resource;
/**
* ComposerResource tracks the PHP version and Composer dependencies.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @final since Symfony 4.3
*/
class ComposerResource implements SelfCheckingResourceInterface
{
private $vendors;
private static $runtimeVendors;
public function __construct()
{
self::refresh();
$this->vendors = self::$runtimeVendors;
}
public function getVendors()
{
return array_keys($this->vendors);
}
public function __toString()
{
return __CLASS__;
}
/**
* {@inheritdoc}
*/
public function isFresh($timestamp)
{
self::refresh();
return array_values(self::$runtimeVendors) === array_values($this->vendors);
}
private static function refresh()
{
self::$runtimeVendors = [];
foreach (get_declared_classes() as $class) {
if ('C' === $class[0] && str_starts_with($class, 'ComposerAutoloaderInit')) {
$r = new \ReflectionClass($class);
$v = \dirname($r->getFileName(), 2);
if (file_exists($v.'/composer/installed.json')) {
self::$runtimeVendors[$v] = @filemtime($v.'/composer/installed.json');
}
}
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Resource;
/**
* DirectoryResource represents a resources stored in a subdirectory tree.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final since Symfony 4.3
*/
class DirectoryResource implements SelfCheckingResourceInterface
{
private $resource;
private $pattern;
/**
* @param string $resource The file path to the resource
* @param string|null $pattern A pattern to restrict monitored files
*
* @throws \InvalidArgumentException
*/
public function __construct(string $resource, string $pattern = null)
{
$this->resource = realpath($resource) ?: (file_exists($resource) ? $resource : false);
$this->pattern = $pattern;
if (false === $this->resource || !is_dir($this->resource)) {
throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist.', $resource));
}
}
public function __toString()
{
return md5(serialize([$this->resource, $this->pattern]));
}
/**
* @return string The file path to the resource
*/
public function getResource()
{
return $this->resource;
}
/**
* Returns the pattern to restrict monitored files.
*
* @return string|null
*/
public function getPattern()
{
return $this->pattern;
}
/**
* {@inheritdoc}
*/
public function isFresh($timestamp)
{
if (!is_dir($this->resource)) {
return false;
}
if ($timestamp < filemtime($this->resource)) {
return false;
}
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->resource), \RecursiveIteratorIterator::SELF_FIRST) as $file) {
// if regex filtering is enabled only check matching files
if ($this->pattern && $file->isFile() && !preg_match($this->pattern, $file->getBasename())) {
continue;
}
// always monitor directories for changes, except the .. entries
// (otherwise deleted files wouldn't get detected)
if ($file->isDir() && str_ends_with($file, '/..')) {
continue;
}
// for broken links
try {
$fileMTime = $file->getMTime();
} catch (\RuntimeException $e) {
continue;
}
// early return if a file's mtime exceeds the passed timestamp
if ($timestamp < $fileMTime) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Resource;
/**
* FileExistenceResource represents a resource stored on the filesystem.
* Freshness is only evaluated against resource creation or deletion.
*
* The resource can be a file or a directory.
*
* @author Charles-Henri Bruyand <charleshenri.bruyand@gmail.com>
*
* @final since Symfony 4.3
*/
class FileExistenceResource implements SelfCheckingResourceInterface
{
private $resource;
private $exists;
/**
* @param string $resource The file path to the resource
*/
public function __construct(string $resource)
{
$this->resource = $resource;
$this->exists = file_exists($resource);
}
public function __toString()
{
return $this->resource;
}
/**
* @return string The file path to the resource
*/
public function getResource()
{
return $this->resource;
}
/**
* {@inheritdoc}
*/
public function isFresh($timestamp)
{
return file_exists($this->resource) === $this->exists;
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Resource;
/**
* FileResource represents a resource stored on the filesystem.
*
* The resource can be a file or a directory.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final since Symfony 4.3
*/
class FileResource implements SelfCheckingResourceInterface
{
/**
* @var string|false
*/
private $resource;
/**
* @param string $resource The file path to the resource
*
* @throws \InvalidArgumentException
*/
public function __construct(string $resource)
{
$this->resource = realpath($resource) ?: (file_exists($resource) ? $resource : false);
if (false === $this->resource) {
throw new \InvalidArgumentException(sprintf('The file "%s" does not exist.', $resource));
}
}
public function __toString()
{
return $this->resource;
}
/**
* @return string The canonicalized, absolute path to the resource
*/
public function getResource()
{
return $this->resource;
}
/**
* {@inheritdoc}
*/
public function isFresh($timestamp)
{
return false !== ($filemtime = @filemtime($this->resource)) && $filemtime <= $timestamp;
}
}

View File

@@ -0,0 +1,239 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Resource;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\Glob;
/**
* GlobResource represents a set of resources stored on the filesystem.
*
* Only existence/removal is tracked (not mtimes.)
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @final since Symfony 4.3
*/
class GlobResource implements \IteratorAggregate, SelfCheckingResourceInterface
{
private $prefix;
private $pattern;
private $recursive;
private $hash;
private $forExclusion;
private $excludedPrefixes;
private $globBrace;
/**
* @param string $prefix A directory prefix
* @param string $pattern A glob pattern
* @param bool $recursive Whether directories should be scanned recursively or not
*
* @throws \InvalidArgumentException
*/
public function __construct(string $prefix, string $pattern, bool $recursive, bool $forExclusion = false, array $excludedPrefixes = [])
{
ksort($excludedPrefixes);
$this->prefix = realpath($prefix) ?: (file_exists($prefix) ? $prefix : false);
$this->pattern = $pattern;
$this->recursive = $recursive;
$this->forExclusion = $forExclusion;
$this->excludedPrefixes = $excludedPrefixes;
$this->globBrace = \defined('GLOB_BRACE') ? \GLOB_BRACE : 0;
if (false === $this->prefix) {
throw new \InvalidArgumentException(sprintf('The path "%s" does not exist.', $prefix));
}
}
public function getPrefix()
{
return $this->prefix;
}
public function __toString()
{
return 'glob.'.$this->prefix.(int) $this->recursive.$this->pattern.(int) $this->forExclusion.implode("\0", $this->excludedPrefixes);
}
/**
* {@inheritdoc}
*/
public function isFresh($timestamp)
{
$hash = $this->computeHash();
if (null === $this->hash) {
$this->hash = $hash;
}
return $this->hash === $hash;
}
/**
* @internal
*/
public function __sleep(): array
{
if (null === $this->hash) {
$this->hash = $this->computeHash();
}
return ['prefix', 'pattern', 'recursive', 'hash', 'forExclusion', 'excludedPrefixes'];
}
/**
* @internal
*/
public function __wakeup(): void
{
$this->globBrace = \defined('GLOB_BRACE') ? \GLOB_BRACE : 0;
}
/**
* @return \Traversable
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
if (!file_exists($this->prefix) || (!$this->recursive && '' === $this->pattern)) {
return;
}
$prefix = str_replace('\\', '/', $this->prefix);
$paths = null;
if (!str_starts_with($this->prefix, 'phar://') && !str_contains($this->pattern, '/**/')) {
if ($this->globBrace || !str_contains($this->pattern, '{')) {
$paths = glob($this->prefix.$this->pattern, \GLOB_NOSORT | $this->globBrace);
} elseif (!str_contains($this->pattern, '\\') || !preg_match('/\\\\[,{}]/', $this->pattern)) {
foreach ($this->expandGlob($this->pattern) as $p) {
$paths[] = glob($this->prefix.$p, \GLOB_NOSORT);
}
$paths = array_merge(...$paths);
}
}
if (null !== $paths) {
natsort($paths);
foreach ($paths as $path) {
if ($this->excludedPrefixes) {
$normalizedPath = str_replace('\\', '/', $path);
do {
if (isset($this->excludedPrefixes[$dirPath = $normalizedPath])) {
continue 2;
}
} while ($prefix !== $dirPath && $dirPath !== $normalizedPath = \dirname($dirPath));
}
if (is_file($path)) {
yield $path => new \SplFileInfo($path);
}
if (!is_dir($path)) {
continue;
}
if ($this->forExclusion) {
yield $path => new \SplFileInfo($path);
continue;
}
if (!$this->recursive || isset($this->excludedPrefixes[str_replace('\\', '/', $path)])) {
continue;
}
$files = iterator_to_array(new \RecursiveIteratorIterator(
new \RecursiveCallbackFilterIterator(
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
function (\SplFileInfo $file, $path) {
return !isset($this->excludedPrefixes[str_replace('\\', '/', $path)]) && '.' !== $file->getBasename()[0];
}
),
\RecursiveIteratorIterator::LEAVES_ONLY
));
uksort($files, 'strnatcmp');
foreach ($files as $path => $info) {
if ($info->isFile()) {
yield $path => $info;
}
}
}
return;
}
if (!class_exists(Finder::class)) {
throw new \LogicException(sprintf('Extended glob pattern "%s" cannot be used as the Finder component is not installed.', $this->pattern));
}
$finder = new Finder();
$regex = Glob::toRegex($this->pattern);
if ($this->recursive) {
$regex = substr_replace($regex, '(/|$)', -2, 1);
}
$prefixLen = \strlen($this->prefix);
foreach ($finder->followLinks()->sortByName()->in($this->prefix) as $path => $info) {
$normalizedPath = str_replace('\\', '/', $path);
if (!preg_match($regex, substr($normalizedPath, $prefixLen)) || !$info->isFile()) {
continue;
}
if ($this->excludedPrefixes) {
do {
if (isset($this->excludedPrefixes[$dirPath = $normalizedPath])) {
continue 2;
}
} while ($prefix !== $dirPath && $dirPath !== $normalizedPath = \dirname($dirPath));
}
yield $path => $info;
}
}
private function computeHash(): string
{
$hash = hash_init('md5');
foreach ($this->getIterator() as $path => $info) {
hash_update($hash, $path."\n");
}
return hash_final($hash);
}
private function expandGlob(string $pattern): array
{
$segments = preg_split('/\{([^{}]*+)\}/', $pattern, -1, \PREG_SPLIT_DELIM_CAPTURE);
$paths = [$segments[0]];
$patterns = [];
for ($i = 1; $i < \count($segments); $i += 2) {
$patterns = [];
foreach (explode(',', $segments[$i]) as $s) {
foreach ($paths as $p) {
$patterns[] = $p.$s.$segments[1 + $i];
}
}
$paths = $patterns;
}
$j = 0;
foreach ($patterns as $i => $p) {
if (str_contains($p, '{')) {
$p = $this->expandGlob($p);
array_splice($paths, $i + $j, 1, $p);
$j += \count($p) - 1;
}
}
return $paths;
}
}

View File

@@ -0,0 +1,268 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Resource;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface as LegacyServiceSubscriberInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Handler\MessageSubscriberInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @final since Symfony 4.3
*/
class ReflectionClassResource implements SelfCheckingResourceInterface
{
private $files = [];
private $className;
private $classReflector;
private $excludedVendors = [];
private $hash;
public function __construct(\ReflectionClass $classReflector, array $excludedVendors = [])
{
$this->className = $classReflector->name;
$this->classReflector = $classReflector;
$this->excludedVendors = $excludedVendors;
}
public function isFresh($timestamp)
{
if (null === $this->hash) {
$this->hash = $this->computeHash();
$this->loadFiles($this->classReflector);
}
foreach ($this->files as $file => $v) {
if (false === $filemtime = @filemtime($file)) {
return false;
}
if ($filemtime > $timestamp) {
return $this->hash === $this->computeHash();
}
}
return true;
}
public function __toString()
{
return 'reflection.'.$this->className;
}
/**
* @internal
*/
public function __sleep(): array
{
if (null === $this->hash) {
$this->hash = $this->computeHash();
$this->loadFiles($this->classReflector);
}
return ['files', 'className', 'hash'];
}
private function loadFiles(\ReflectionClass $class)
{
foreach ($class->getInterfaces() as $v) {
$this->loadFiles($v);
}
do {
$file = $class->getFileName();
if (false !== $file && file_exists($file)) {
foreach ($this->excludedVendors as $vendor) {
if (str_starts_with($file, $vendor) && false !== strpbrk(substr($file, \strlen($vendor), 1), '/'.\DIRECTORY_SEPARATOR)) {
$file = false;
break;
}
}
if ($file) {
$this->files[$file] = null;
}
}
foreach ($class->getTraits() as $v) {
$this->loadFiles($v);
}
} while ($class = $class->getParentClass());
}
private function computeHash(): string
{
if (null === $this->classReflector) {
try {
$this->classReflector = new \ReflectionClass($this->className);
} catch (\ReflectionException $e) {
// the class does not exist anymore
return false;
}
}
$hash = hash_init('md5');
foreach ($this->generateSignature($this->classReflector) as $info) {
hash_update($hash, $info);
}
return hash_final($hash);
}
private function generateSignature(\ReflectionClass $class): iterable
{
if (\PHP_VERSION_ID >= 80000) {
$attributes = [];
foreach ($class->getAttributes() as $a) {
$attributes[] = [$a->getName(), \PHP_VERSION_ID >= 80100 ? (string) $a : $a->getArguments()];
}
yield print_r($attributes, true);
$attributes = [];
}
yield $class->getDocComment();
yield (int) $class->isFinal();
yield (int) $class->isAbstract();
if ($class->isTrait()) {
yield print_r(class_uses($class->name), true);
} else {
yield print_r(class_parents($class->name), true);
yield print_r(class_implements($class->name), true);
yield print_r($class->getConstants(), true);
}
if (!$class->isInterface()) {
$defaults = $class->getDefaultProperties();
foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED) as $p) {
if (\PHP_VERSION_ID >= 80000) {
foreach ($p->getAttributes() as $a) {
$attributes[] = [$a->getName(), \PHP_VERSION_ID >= 80100 ? (string) $a : $a->getArguments()];
}
yield print_r($attributes, true);
$attributes = [];
}
yield $p->getDocComment();
yield $p->isDefault() ? '<default>' : '';
yield $p->isPublic() ? 'public' : 'protected';
yield $p->isStatic() ? 'static' : '';
yield '$'.$p->name;
yield print_r(isset($defaults[$p->name]) && !\is_object($defaults[$p->name]) ? $defaults[$p->name] : null, true);
}
}
$defined = \Closure::bind(static function ($c) { return \defined($c); }, null, $class->name);
foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) {
if (\PHP_VERSION_ID >= 80000) {
foreach ($m->getAttributes() as $a) {
$attributes[] = [$a->getName(), \PHP_VERSION_ID >= 80100 ? (string) $a : $a->getArguments()];
}
yield print_r($attributes, true);
$attributes = [];
}
$defaults = [];
$parametersWithUndefinedConstants = [];
foreach ($m->getParameters() as $p) {
if (\PHP_VERSION_ID >= 80000) {
foreach ($p->getAttributes() as $a) {
$attributes[] = [$a->getName(), \PHP_VERSION_ID >= 80100 ? (string) $a : $a->getArguments()];
}
yield print_r($attributes, true);
$attributes = [];
}
if (!$p->isDefaultValueAvailable()) {
$defaults[$p->name] = null;
continue;
}
if (\PHP_VERSION_ID >= 80100) {
$defaults[$p->name] = (string) $p;
continue;
}
if (!$p->isDefaultValueConstant() || $defined($p->getDefaultValueConstantName())) {
$defaults[$p->name] = $p->getDefaultValue();
continue;
}
$defaults[$p->name] = $p->getDefaultValueConstantName();
$parametersWithUndefinedConstants[$p->name] = true;
}
if (!$parametersWithUndefinedConstants) {
yield preg_replace('/^ @@.*/m', '', $m);
} else {
$t = $m->getReturnType();
$stack = [
$m->getDocComment(),
$m->getName(),
$m->isAbstract(),
$m->isFinal(),
$m->isStatic(),
$m->isPublic(),
$m->isPrivate(),
$m->isProtected(),
$m->returnsReference(),
$t instanceof \ReflectionNamedType ? ((string) $t->allowsNull()).$t->getName() : (string) $t,
];
foreach ($m->getParameters() as $p) {
if (!isset($parametersWithUndefinedConstants[$p->name])) {
$stack[] = (string) $p;
} else {
$t = $p->getType();
$stack[] = $p->isOptional();
$stack[] = $t instanceof \ReflectionNamedType ? ((string) $t->allowsNull()).$t->getName() : (string) $t;
$stack[] = $p->isPassedByReference();
$stack[] = $p->isVariadic();
$stack[] = $p->getName();
}
}
yield implode(',', $stack);
}
yield print_r($defaults, true);
}
if ($class->isAbstract() || $class->isInterface() || $class->isTrait()) {
return;
}
if (interface_exists(EventSubscriberInterface::class, false) && $class->isSubclassOf(EventSubscriberInterface::class)) {
yield EventSubscriberInterface::class;
yield print_r($class->name::getSubscribedEvents(), true);
}
if (interface_exists(MessageSubscriberInterface::class, false) && $class->isSubclassOf(MessageSubscriberInterface::class)) {
yield MessageSubscriberInterface::class;
foreach ($class->name::getHandledMessages() as $key => $value) {
yield $key.print_r($value, true);
}
}
if (interface_exists(LegacyServiceSubscriberInterface::class, false) && $class->isSubclassOf(LegacyServiceSubscriberInterface::class)) {
yield LegacyServiceSubscriberInterface::class;
yield print_r([$class->name, 'getSubscribedServices'](), true);
} elseif (interface_exists(ServiceSubscriberInterface::class, false) && $class->isSubclassOf(ServiceSubscriberInterface::class)) {
yield ServiceSubscriberInterface::class;
yield print_r($class->name::getSubscribedServices(), true);
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Resource;
/**
* ResourceInterface is the interface that must be implemented by all Resource classes.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface ResourceInterface
{
/**
* Returns a string representation of the Resource.
*
* This method is necessary to allow for resource de-duplication, for example by means
* of array_unique(). The string returned need not have a particular meaning, but has
* to be identical for different ResourceInterface instances referring to the same
* resource; and it should be unlikely to collide with that of other, unrelated
* resource instances.
*/
public function __toString();
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Resource;
use Symfony\Component\Config\ResourceCheckerInterface;
/**
* Resource checker for instances of SelfCheckingResourceInterface.
*
* As these resources perform the actual check themselves, we can provide
* this class as a standard way of validating them.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
class SelfCheckingResourceChecker implements ResourceCheckerInterface
{
public function supports(ResourceInterface $metadata)
{
return $metadata instanceof SelfCheckingResourceInterface;
}
public function isFresh(ResourceInterface $resource, $timestamp)
{
/* @var SelfCheckingResourceInterface $resource */
return $resource->isFresh($timestamp);
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Resource;
/**
* Interface for Resources that can check for freshness autonomously,
* without special support from external services.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
interface SelfCheckingResourceInterface extends ResourceInterface
{
/**
* Returns true if the resource has not been updated since the given timestamp.
*
* @param int $timestamp The last time the resource was loaded
*
* @return bool True if the resource has not been updated, false otherwise
*/
public function isFresh($timestamp);
}

View File

@@ -0,0 +1,188 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config;
use Symfony\Component\Config\Resource\ResourceInterface;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
/**
* ResourceCheckerConfigCache uses instances of ResourceCheckerInterface
* to check whether cached data is still fresh.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
class ResourceCheckerConfigCache implements ConfigCacheInterface
{
/**
* @var string
*/
private $file;
/**
* @var iterable|ResourceCheckerInterface[]
*/
private $resourceCheckers;
/**
* @param string $file The absolute cache path
* @param iterable|ResourceCheckerInterface[] $resourceCheckers The ResourceCheckers to use for the freshness check
*/
public function __construct(string $file, iterable $resourceCheckers = [])
{
$this->file = $file;
$this->resourceCheckers = $resourceCheckers;
}
/**
* {@inheritdoc}
*/
public function getPath()
{
return $this->file;
}
/**
* Checks if the cache is still fresh.
*
* This implementation will make a decision solely based on the ResourceCheckers
* passed in the constructor.
*
* The first ResourceChecker that supports a given resource is considered authoritative.
* Resources with no matching ResourceChecker will silently be ignored and considered fresh.
*
* @return bool true if the cache is fresh, false otherwise
*/
public function isFresh()
{
if (!is_file($this->file)) {
return false;
}
if ($this->resourceCheckers instanceof \Traversable && !$this->resourceCheckers instanceof \Countable) {
$this->resourceCheckers = iterator_to_array($this->resourceCheckers);
}
if (!\count($this->resourceCheckers)) {
return true; // shortcut - if we don't have any checkers we don't need to bother with the meta file at all
}
$metadata = $this->getMetaFile();
if (!is_file($metadata)) {
return false;
}
$meta = $this->safelyUnserialize($metadata);
if (false === $meta) {
return false;
}
$time = filemtime($this->file);
foreach ($meta as $resource) {
/* @var ResourceInterface $resource */
foreach ($this->resourceCheckers as $checker) {
if (!$checker->supports($resource)) {
continue; // next checker
}
if ($checker->isFresh($resource, $time)) {
break; // no need to further check this resource
}
return false; // cache is stale
}
// no suitable checker found, ignore this resource
}
return true;
}
/**
* Writes cache.
*
* @param string $content The content to write in the cache
* @param ResourceInterface[] $metadata An array of metadata
*
* @throws \RuntimeException When cache file can't be written
*/
public function write($content, array $metadata = null)
{
$mode = 0666;
$umask = umask();
$filesystem = new Filesystem();
$filesystem->dumpFile($this->file, $content);
try {
$filesystem->chmod($this->file, $mode, $umask);
} catch (IOException $e) {
// discard chmod failure (some filesystem may not support it)
}
if (null !== $metadata) {
$filesystem->dumpFile($this->getMetaFile(), serialize($metadata));
try {
$filesystem->chmod($this->getMetaFile(), $mode, $umask);
} catch (IOException $e) {
// discard chmod failure (some filesystem may not support it)
}
}
if (\function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
@opcache_invalidate($this->file, true);
}
}
/**
* Gets the meta file path.
*/
private function getMetaFile(): string
{
return $this->file.'.meta';
}
private function safelyUnserialize(string $file)
{
$meta = false;
$content = file_get_contents($file);
$signalingException = new \UnexpectedValueException();
$prevUnserializeHandler = ini_set('unserialize_callback_func', self::class.'::handleUnserializeCallback');
$prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler, $signalingException) {
if (__FILE__ === $file) {
throw $signalingException;
}
return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false;
});
try {
$meta = unserialize($content);
} catch (\Throwable $e) {
if ($e !== $signalingException) {
throw $e;
}
} finally {
restore_error_handler();
ini_set('unserialize_callback_func', $prevUnserializeHandler);
}
return $meta;
}
/**
* @internal
*/
public static function handleUnserializeCallback($class)
{
trigger_error('Class not found: '.$class);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config;
/**
* A ConfigCacheFactory implementation that validates the
* cache with an arbitrary set of ResourceCheckers.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
class ResourceCheckerConfigCacheFactory implements ConfigCacheFactoryInterface
{
private $resourceCheckers = [];
/**
* @param iterable|ResourceCheckerInterface[] $resourceCheckers
*/
public function __construct(iterable $resourceCheckers = [])
{
$this->resourceCheckers = $resourceCheckers;
}
/**
* {@inheritdoc}
*/
public function cache($file, $callback)
{
if (!\is_callable($callback)) {
throw new \InvalidArgumentException(sprintf('Invalid type for callback argument. Expected callable, but got "%s".', \gettype($callback)));
}
$cache = new ResourceCheckerConfigCache($file, $this->resourceCheckers);
if (!$cache->isFresh()) {
$callback($cache);
}
return $cache;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config;
use Symfony\Component\Config\Resource\ResourceInterface;
/**
* Interface for ResourceCheckers.
*
* When a ResourceCheckerConfigCache instance is checked for freshness, all its associated
* metadata resources are passed to ResourceCheckers. The ResourceCheckers
* can then inspect the resources and decide whether the cache can be considered
* fresh or not.
*
* @author Matthias Pigulla <mp@webfactory.de>
* @author Benjamin Klotz <bk@webfactory.de>
*/
interface ResourceCheckerInterface
{
/**
* Queries the ResourceChecker whether it can validate a given
* resource or not.
*
* @return bool True if the ResourceChecker can handle this resource type, false if not
*/
public function supports(ResourceInterface $metadata);
/**
* Validates the resource.
*
* @param int $timestamp The timestamp at which the cache associated with this resource was created
*
* @return bool True if the resource has not changed since the given timestamp, false otherwise
*/
public function isFresh(ResourceInterface $resource, $timestamp);
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Util\Exception;
/**
* Exception class for when XML parsing with an XSD schema file path or a callable validator produces errors unrelated
* to the actual XML parsing.
*
* @author Ole Rößner <ole@roessner.it>
*/
class InvalidXmlException extends XmlParsingException
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Util\Exception;
/**
* Exception class for when XML cannot be parsed properly.
*
* @author Ole Rößner <ole@roessner.it>
*/
class XmlParsingException extends \InvalidArgumentException
{
}

View File

@@ -0,0 +1,289 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Config\Util;
use Symfony\Component\Config\Util\Exception\InvalidXmlException;
use Symfony\Component\Config\Util\Exception\XmlParsingException;
/**
* XMLUtils is a bunch of utility methods to XML operations.
*
* This class contains static methods only and is not meant to be instantiated.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Martin Hasoň <martin.hason@gmail.com>
* @author Ole Rößner <ole@roessner.it>
*/
class XmlUtils
{
/**
* This class should not be instantiated.
*/
private function __construct()
{
}
/**
* Parses an XML string.
*
* @param string $content An XML string
* @param string|callable|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
*
* @return \DOMDocument
*
* @throws XmlParsingException When parsing of XML file returns error
* @throws InvalidXmlException When parsing of XML with schema or callable produces any errors unrelated to the XML parsing itself
* @throws \RuntimeException When DOM extension is missing
*/
public static function parse($content, $schemaOrCallable = null)
{
if (!\extension_loaded('dom')) {
throw new \LogicException('Extension DOM is required.');
}
$internalErrors = libxml_use_internal_errors(true);
if (\LIBXML_VERSION < 20900) {
$disableEntities = libxml_disable_entity_loader(true);
}
libxml_clear_errors();
$dom = new \DOMDocument();
$dom->validateOnParse = true;
if (!$dom->loadXML($content, \LIBXML_NONET | (\defined('LIBXML_COMPACT') ? \LIBXML_COMPACT : 0))) {
if (\LIBXML_VERSION < 20900) {
libxml_disable_entity_loader($disableEntities);
}
throw new XmlParsingException(implode("\n", static::getXmlErrors($internalErrors)));
}
$dom->normalizeDocument();
libxml_use_internal_errors($internalErrors);
if (\LIBXML_VERSION < 20900) {
libxml_disable_entity_loader($disableEntities);
}
foreach ($dom->childNodes as $child) {
if (\XML_DOCUMENT_TYPE_NODE === $child->nodeType) {
throw new XmlParsingException('Document types are not allowed.');
}
}
if (null !== $schemaOrCallable) {
$internalErrors = libxml_use_internal_errors(true);
libxml_clear_errors();
$e = null;
if (\is_callable($schemaOrCallable)) {
try {
$valid = $schemaOrCallable($dom, $internalErrors);
} catch (\Exception $e) {
$valid = false;
}
} elseif (!\is_array($schemaOrCallable) && is_file((string) $schemaOrCallable)) {
$schemaSource = file_get_contents((string) $schemaOrCallable);
$valid = @$dom->schemaValidateSource($schemaSource);
} else {
libxml_use_internal_errors($internalErrors);
throw new XmlParsingException('The schemaOrCallable argument has to be a valid path to XSD file or callable.');
}
if (!$valid) {
$messages = static::getXmlErrors($internalErrors);
if (empty($messages)) {
throw new InvalidXmlException('The XML is not valid.', 0, $e);
}
throw new XmlParsingException(implode("\n", $messages), 0, $e);
}
}
libxml_clear_errors();
libxml_use_internal_errors($internalErrors);
return $dom;
}
/**
* Loads an XML file.
*
* @param string $file An XML file path
* @param string|callable|null $schemaOrCallable An XSD schema file path, a callable, or null to disable validation
*
* @return \DOMDocument
*
* @throws \InvalidArgumentException When loading of XML file returns error
* @throws XmlParsingException When XML parsing returns any errors
* @throws \RuntimeException When DOM extension is missing
*/
public static function loadFile($file, $schemaOrCallable = null)
{
if (!is_file($file)) {
throw new \InvalidArgumentException(sprintf('Resource "%s" is not a file.', $file));
}
if (!is_readable($file)) {
throw new \InvalidArgumentException(sprintf('File "%s" is not readable.', $file));
}
$content = @file_get_contents($file);
if ('' === trim($content)) {
throw new \InvalidArgumentException(sprintf('File "%s" does not contain valid XML, it is empty.', $file));
}
try {
return static::parse($content, $schemaOrCallable);
} catch (InvalidXmlException $e) {
throw new XmlParsingException(sprintf('The XML file "%s" is not valid.', $file), 0, $e->getPrevious());
}
}
/**
* Converts a \DOMElement object to a PHP array.
*
* The following rules applies during the conversion:
*
* * Each tag is converted to a key value or an array
* if there is more than one "value"
*
* * The content of a tag is set under a "value" key (<foo>bar</foo>)
* if the tag also has some nested tags
*
* * The attributes are converted to keys (<foo foo="bar"/>)
*
* * The nested-tags are converted to keys (<foo><foo>bar</foo></foo>)
*
* @param \DOMElement $element A \DOMElement instance
* @param bool $checkPrefix Check prefix in an element or an attribute name
*
* @return mixed
*/
public static function convertDomElementToArray(\DOMElement $element, $checkPrefix = true)
{
$prefix = (string) $element->prefix;
$empty = true;
$config = [];
foreach ($element->attributes as $name => $node) {
if ($checkPrefix && !\in_array((string) $node->prefix, ['', $prefix], true)) {
continue;
}
$config[$name] = static::phpize($node->value);
$empty = false;
}
$nodeValue = false;
foreach ($element->childNodes as $node) {
if ($node instanceof \DOMText) {
if ('' !== trim($node->nodeValue)) {
$nodeValue = trim($node->nodeValue);
$empty = false;
}
} elseif ($checkPrefix && $prefix != (string) $node->prefix) {
continue;
} elseif (!$node instanceof \DOMComment) {
$value = static::convertDomElementToArray($node, $checkPrefix);
$key = $node->localName;
if (isset($config[$key])) {
if (!\is_array($config[$key]) || !\is_int(key($config[$key]))) {
$config[$key] = [$config[$key]];
}
$config[$key][] = $value;
} else {
$config[$key] = $value;
}
$empty = false;
}
}
if (false !== $nodeValue) {
$value = static::phpize($nodeValue);
if (\count($config)) {
$config['value'] = $value;
} else {
$config = $value;
}
}
return !$empty ? $config : null;
}
/**
* Converts an xml value to a PHP type.
*
* @param mixed $value
*
* @return mixed
*/
public static function phpize($value)
{
$value = (string) $value;
$lowercaseValue = strtolower($value);
switch (true) {
case 'null' === $lowercaseValue:
return null;
case ctype_digit($value):
case isset($value[1]) && '-' === $value[0] && ctype_digit(substr($value, 1)):
$raw = $value;
$cast = (int) $value;
return self::isOctal($value) ? \intval($value, 8) : (($raw === (string) $cast) ? $cast : $raw);
case 'true' === $lowercaseValue:
return true;
case 'false' === $lowercaseValue:
return false;
case isset($value[1]) && '0b' == $value[0].$value[1] && preg_match('/^0b[01]*$/', $value):
return bindec($value);
case is_numeric($value):
return '0x' === $value[0].$value[1] ? hexdec($value) : (float) $value;
case preg_match('/^0x[0-9a-f]++$/i', $value):
return hexdec($value);
case preg_match('/^[+-]?[0-9]+(\.[0-9]+)?$/', $value):
return (float) $value;
default:
return $value;
}
}
protected static function getXmlErrors($internalErrors)
{
$errors = [];
foreach (libxml_get_errors() as $error) {
$errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
\LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
$error->code,
trim($error->message),
$error->file ?: 'n/a',
$error->line,
$error->column
);
}
libxml_clear_errors();
libxml_use_internal_errors($internalErrors);
return $errors;
}
private static function isOctal(string $str): bool
{
if ('-' === $str[0]) {
$str = substr($str, 1);
}
return $str === '0'.decoct(\intval($str, 8));
}
}

View File

@@ -0,0 +1,45 @@
{
"name": "symfony/config",
"type": "library",
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=7.1.3",
"symfony/filesystem": "^3.4|^4.0|^5.0",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-php80": "^1.16",
"symfony/polyfill-php81": "^1.22"
},
"require-dev": {
"symfony/event-dispatcher": "^3.4|^4.0|^5.0",
"symfony/finder": "^3.4|^4.0|^5.0",
"symfony/messenger": "^4.1|^5.0",
"symfony/service-contracts": "^1.1|^2",
"symfony/yaml": "^3.4|^4.0|^5.0"
},
"conflict": {
"symfony/finder": "<3.4"
},
"suggest": {
"symfony/yaml": "To use the yaml reference dumper"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Config\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,162 @@
CHANGELOG
=========
4.4.0
-----
* deprecated finding hidden commands using an abbreviation, use the full name instead
* added `Question::setTrimmable` default to true to allow the answer to be trimmed
* added method `minSecondsBetweenRedraws()` and `maxSecondsBetweenRedraws()` on `ProgressBar`
* `Application` implements `ResetInterface`
* marked all dispatched event classes as `@final`
* added support for displaying table horizontally
* deprecated returning `null` from `Command::execute()`, return `0` instead
* Deprecated the `Application::renderException()` and `Application::doRenderException()` methods,
use `renderThrowable()` and `doRenderThrowable()` instead.
* added support for the `NO_COLOR` env var (https://no-color.org/)
4.3.0
-----
* added support for hyperlinks
* added `ProgressBar::iterate()` method that simplify updating the progress bar when iterating
* added `Question::setAutocompleterCallback()` to provide a callback function
that dynamically generates suggestions as the user types
4.2.0
-----
* allowed passing commands as `[$process, 'ENV_VAR' => 'value']` to
`ProcessHelper::run()` to pass environment variables
* deprecated passing a command as a string to `ProcessHelper::run()`,
pass it the command as an array of its arguments instead
* made the `ProcessHelper` class final
* added `WrappableOutputFormatterInterface::formatAndWrap()` (implemented in `OutputFormatter`)
* added `capture_stderr_separately` option to `CommandTester::execute()`
4.1.0
-----
* added option to run suggested command if command is not found and only 1 alternative is available
* added option to modify console output and print multiple modifiable sections
* added support for iterable messages in output `write` and `writeln` methods
4.0.0
-----
* `OutputFormatter` throws an exception when unknown options are used
* removed `QuestionHelper::setInputStream()/getInputStream()`
* removed `Application::getTerminalWidth()/getTerminalHeight()` and
`Application::setTerminalDimensions()/getTerminalDimensions()`
* removed `ConsoleExceptionEvent`
* removed `ConsoleEvents::EXCEPTION`
3.4.0
-----
* added `SHELL_VERBOSITY` env var to control verbosity
* added `CommandLoaderInterface`, `FactoryCommandLoader` and PSR-11
`ContainerCommandLoader` for commands lazy-loading
* added a case-insensitive command name matching fallback
* added static `Command::$defaultName/getDefaultName()`, allowing for
commands to be registered at compile time in the application command loader.
Setting the `$defaultName` property avoids the need for filling the `command`
attribute on the `console.command` tag when using `AddConsoleCommandPass`.
3.3.0
-----
* added `ExceptionListener`
* added `AddConsoleCommandPass` (originally in FrameworkBundle)
* [BC BREAK] `Input::getOption()` no longer returns the default value for options
with value optional explicitly passed empty
* added console.error event to catch exceptions thrown by other listeners
* deprecated console.exception event in favor of console.error
* added ability to handle `CommandNotFoundException` through the
`console.error` event
* deprecated default validation in `SymfonyQuestionHelper::ask`
3.2.0
------
* added `setInputs()` method to CommandTester for ease testing of commands expecting inputs
* added `setStream()` and `getStream()` methods to Input (implement StreamableInputInterface)
* added StreamableInputInterface
* added LockableTrait
3.1.0
-----
* added truncate method to FormatterHelper
* added setColumnWidth(s) method to Table
2.8.3
-----
* remove readline support from the question helper as it caused issues
2.8.0
-----
* use readline for user input in the question helper when available to allow
the use of arrow keys
2.6.0
-----
* added a Process helper
* added a DebugFormatter helper
2.5.0
-----
* deprecated the dialog helper (use the question helper instead)
* deprecated TableHelper in favor of Table
* deprecated ProgressHelper in favor of ProgressBar
* added ConsoleLogger
* added a question helper
* added a way to set the process name of a command
* added a way to set a default command instead of `ListCommand`
2.4.0
-----
* added a way to force terminal dimensions
* added a convenient method to detect verbosity level
* [BC BREAK] made descriptors use output instead of returning a string
2.3.0
-----
* added multiselect support to the select dialog helper
* added Table Helper for tabular data rendering
* added support for events in `Application`
* added a way to normalize EOLs in `ApplicationTester::getDisplay()` and `CommandTester::getDisplay()`
* added a way to set the progress bar progress via the `setCurrent` method
* added support for multiple InputOption shortcuts, written as `'-a|-b|-c'`
* added two additional verbosity levels, VERBOSITY_VERY_VERBOSE and VERBOSITY_DEBUG
2.2.0
-----
* added support for colorization on Windows via ConEmu
* add a method to Dialog Helper to ask for a question and hide the response
* added support for interactive selections in console (DialogHelper::select())
* added support for autocompletion as you type in Dialog Helper
2.1.0
-----
* added ConsoleOutputInterface
* added the possibility to disable a command (Command::isEnabled())
* added suggestions when a command does not exist
* added a --raw option to the list command
* added support for STDERR in the console output class (errors are now sent
to STDERR)
* made the defaults (helper set, commands, input definition) in Application
more easily customizable
* added support for the shell even if readline is not available
* added support for process isolation in Symfony shell via
`--process-isolation` switch
* added support for `--`, which disables options parsing after that point
(tokens will be parsed as arguments)

View File

@@ -0,0 +1,667 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Base class for all commands.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Command
{
/**
* @var string|null The default command name
*/
protected static $defaultName;
private $application;
private $name;
private $processTitle;
private $aliases = [];
private $definition;
private $hidden = false;
private $help = '';
private $description = '';
private $ignoreValidationErrors = false;
private $applicationDefinitionMerged = false;
private $applicationDefinitionMergedWithArgs = false;
private $code;
private $synopsis = [];
private $usages = [];
private $helperSet;
/**
* @return string|null The default command name or null when no default name is set
*/
public static function getDefaultName()
{
$class = static::class;
$r = new \ReflectionProperty($class, 'defaultName');
return $class === $r->class ? static::$defaultName : null;
}
/**
* @param string|null $name The name of the command; passing null means it must be set in configure()
*
* @throws LogicException When the command name is empty
*/
public function __construct(string $name = null)
{
$this->definition = new InputDefinition();
if (null !== $name || null !== $name = static::getDefaultName()) {
$this->setName($name);
}
$this->configure();
}
/**
* Ignores validation errors.
*
* This is mainly useful for the help command.
*/
public function ignoreValidationErrors()
{
$this->ignoreValidationErrors = true;
}
public function setApplication(Application $application = null)
{
$this->application = $application;
if ($application) {
$this->setHelperSet($application->getHelperSet());
} else {
$this->helperSet = null;
}
}
public function setHelperSet(HelperSet $helperSet)
{
$this->helperSet = $helperSet;
}
/**
* Gets the helper set.
*
* @return HelperSet|null A HelperSet instance
*/
public function getHelperSet()
{
return $this->helperSet;
}
/**
* Gets the application instance for this command.
*
* @return Application|null An Application instance
*/
public function getApplication()
{
return $this->application;
}
/**
* Checks whether the command is enabled or not in the current environment.
*
* Override this to check for x or y and return false if the command can not
* run properly under the current conditions.
*
* @return bool
*/
public function isEnabled()
{
return true;
}
/**
* Configures the current command.
*/
protected function configure()
{
}
/**
* Executes the current command.
*
* This method is not abstract because you can use this class
* as a concrete class. In this case, instead of defining the
* execute() method, you set the code to execute by passing
* a Closure to the setCode() method.
*
* @return int 0 if everything went fine, or an exit code
*
* @throws LogicException When this abstract method is not implemented
*
* @see setCode()
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
throw new LogicException('You must override the execute() method in the concrete command class.');
}
/**
* Interacts with the user.
*
* This method is executed before the InputDefinition is validated.
* This means that this is the only place where the command can
* interactively ask for values of missing required arguments.
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
}
/**
* Initializes the command after the input has been bound and before the input
* is validated.
*
* This is mainly useful when a lot of commands extends one main command
* where some things need to be initialized based on the input arguments and options.
*
* @see InputInterface::bind()
* @see InputInterface::validate()
*/
protected function initialize(InputInterface $input, OutputInterface $output)
{
}
/**
* Runs the command.
*
* The code to execute is either defined directly with the
* setCode() method or by overriding the execute() method
* in a sub-class.
*
* @return int The command exit code
*
* @throws \Exception When binding input fails. Bypass this by calling {@link ignoreValidationErrors()}.
*
* @see setCode()
* @see execute()
*/
public function run(InputInterface $input, OutputInterface $output)
{
// force the creation of the synopsis before the merge with the app definition
$this->getSynopsis(true);
$this->getSynopsis(false);
// add the application arguments and options
$this->mergeApplicationDefinition();
// bind the input against the command specific arguments/options
try {
$input->bind($this->definition);
} catch (ExceptionInterface $e) {
if (!$this->ignoreValidationErrors) {
throw $e;
}
}
$this->initialize($input, $output);
if (null !== $this->processTitle) {
if (\function_exists('cli_set_process_title')) {
if (!@cli_set_process_title($this->processTitle)) {
if ('Darwin' === \PHP_OS) {
$output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE);
} else {
cli_set_process_title($this->processTitle);
}
}
} elseif (\function_exists('setproctitle')) {
setproctitle($this->processTitle);
} elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
$output->writeln('<comment>Install the proctitle PECL to be able to change the process title.</comment>');
}
}
if ($input->isInteractive()) {
$this->interact($input, $output);
}
// The command name argument is often omitted when a command is executed directly with its run() method.
// It would fail the validation if we didn't make sure the command argument is present,
// since it's required by the application.
if ($input->hasArgument('command') && null === $input->getArgument('command')) {
$input->setArgument('command', $this->getName());
}
$input->validate();
if ($this->code) {
$statusCode = ($this->code)($input, $output);
} else {
$statusCode = $this->execute($input, $output);
if (!\is_int($statusCode)) {
@trigger_error(sprintf('Return value of "%s::execute()" should always be of the type int since Symfony 4.4, %s returned.', static::class, \gettype($statusCode)), \E_USER_DEPRECATED);
}
}
return is_numeric($statusCode) ? (int) $statusCode : 0;
}
/**
* Sets the code to execute when running this command.
*
* If this method is used, it overrides the code defined
* in the execute() method.
*
* @param callable $code A callable(InputInterface $input, OutputInterface $output)
*
* @return $this
*
* @throws InvalidArgumentException
*
* @see execute()
*/
public function setCode(callable $code)
{
if ($code instanceof \Closure) {
$r = new \ReflectionFunction($code);
if (null === $r->getClosureThis()) {
set_error_handler(static function () {});
try {
if ($c = \Closure::bind($code, $this)) {
$code = $c;
}
} finally {
restore_error_handler();
}
}
}
$this->code = $code;
return $this;
}
/**
* Merges the application definition with the command definition.
*
* This method is not part of public API and should not be used directly.
*
* @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
*/
public function mergeApplicationDefinition($mergeArgs = true)
{
if (null === $this->application || (true === $this->applicationDefinitionMerged && ($this->applicationDefinitionMergedWithArgs || !$mergeArgs))) {
return;
}
$this->definition->addOptions($this->application->getDefinition()->getOptions());
$this->applicationDefinitionMerged = true;
if ($mergeArgs) {
$currentArguments = $this->definition->getArguments();
$this->definition->setArguments($this->application->getDefinition()->getArguments());
$this->definition->addArguments($currentArguments);
$this->applicationDefinitionMergedWithArgs = true;
}
}
/**
* Sets an array of argument and option instances.
*
* @param array|InputDefinition $definition An array of argument and option instances or a definition instance
*
* @return $this
*/
public function setDefinition($definition)
{
if ($definition instanceof InputDefinition) {
$this->definition = $definition;
} else {
$this->definition->setDefinition($definition);
}
$this->applicationDefinitionMerged = false;
return $this;
}
/**
* Gets the InputDefinition attached to this Command.
*
* @return InputDefinition An InputDefinition instance
*/
public function getDefinition()
{
if (null === $this->definition) {
throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
}
return $this->definition;
}
/**
* Gets the InputDefinition to be used to create representations of this Command.
*
* Can be overridden to provide the original command representation when it would otherwise
* be changed by merging with the application InputDefinition.
*
* This method is not part of public API and should not be used directly.
*
* @return InputDefinition An InputDefinition instance
*/
public function getNativeDefinition()
{
return $this->getDefinition();
}
/**
* Adds an argument.
*
* @param string $name The argument name
* @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
* @param string $description A description text
* @param mixed $default The default value (for InputArgument::OPTIONAL mode only)
*
* @throws InvalidArgumentException When argument mode is not valid
*
* @return $this
*/
public function addArgument($name, $mode = null, $description = '', $default = null)
{
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default));
return $this;
}
/**
* Adds an option.
*
* @param string $name The option name
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param int|null $mode The option mode: One of the InputOption::VALUE_* constants
* @param string $description A description text
* @param mixed $default The default value (must be null for InputOption::VALUE_NONE)
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*
* @return $this
*/
public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null)
{
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
return $this;
}
/**
* Sets the name of the command.
*
* This method can set both the namespace and the name if
* you separate them by a colon (:)
*
* $command->setName('foo:bar');
*
* @param string $name The command name
*
* @return $this
*
* @throws InvalidArgumentException When the name is invalid
*/
public function setName($name)
{
$this->validateName($name);
$this->name = $name;
return $this;
}
/**
* Sets the process title of the command.
*
* This feature should be used only when creating a long process command,
* like a daemon.
*
* @param string $title The process title
*
* @return $this
*/
public function setProcessTitle($title)
{
$this->processTitle = $title;
return $this;
}
/**
* Returns the command name.
*
* @return string|null
*/
public function getName()
{
return $this->name;
}
/**
* @param bool $hidden Whether or not the command should be hidden from the list of commands
*
* @return $this
*/
public function setHidden($hidden)
{
$this->hidden = (bool) $hidden;
return $this;
}
/**
* @return bool whether the command should be publicly shown or not
*/
public function isHidden()
{
return $this->hidden;
}
/**
* Sets the description for the command.
*
* @param string $description The description for the command
*
* @return $this
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Returns the description for the command.
*
* @return string The description for the command
*/
public function getDescription()
{
return $this->description;
}
/**
* Sets the help for the command.
*
* @param string $help The help for the command
*
* @return $this
*/
public function setHelp($help)
{
$this->help = $help;
return $this;
}
/**
* Returns the help for the command.
*
* @return string The help for the command
*/
public function getHelp()
{
return $this->help;
}
/**
* Returns the processed help for the command replacing the %command.name% and
* %command.full_name% patterns with the real values dynamically.
*
* @return string The processed help for the command
*/
public function getProcessedHelp()
{
$name = $this->name;
$isSingleCommand = $this->application && $this->application->isSingleCommand();
$placeholders = [
'%command.name%',
'%command.full_name%',
];
$replacements = [
$name,
$isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name,
];
return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription());
}
/**
* Sets the aliases for the command.
*
* @param string[] $aliases An array of aliases for the command
*
* @return $this
*
* @throws InvalidArgumentException When an alias is invalid
*/
public function setAliases($aliases)
{
if (!\is_array($aliases) && !$aliases instanceof \Traversable) {
throw new InvalidArgumentException('$aliases must be an array or an instance of \Traversable.');
}
foreach ($aliases as $alias) {
$this->validateName($alias);
}
$this->aliases = $aliases;
return $this;
}
/**
* Returns the aliases for the command.
*
* @return array An array of aliases for the command
*/
public function getAliases()
{
return $this->aliases;
}
/**
* Returns the synopsis for the command.
*
* @param bool $short Whether to show the short version of the synopsis (with options folded) or not
*
* @return string The synopsis
*/
public function getSynopsis($short = false)
{
$key = $short ? 'short' : 'long';
if (!isset($this->synopsis[$key])) {
$this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
}
return $this->synopsis[$key];
}
/**
* Add a command usage example.
*
* @param string $usage The usage, it'll be prefixed with the command name
*
* @return $this
*/
public function addUsage($usage)
{
if (!str_starts_with($usage, $this->name)) {
$usage = sprintf('%s %s', $this->name, $usage);
}
$this->usages[] = $usage;
return $this;
}
/**
* Returns alternative usages of the command.
*
* @return array
*/
public function getUsages()
{
return $this->usages;
}
/**
* Gets a helper instance by name.
*
* @param string $name The helper name
*
* @return mixed The helper value
*
* @throws LogicException if no HelperSet is defined
* @throws InvalidArgumentException if the helper is not defined
*/
public function getHelper($name)
{
if (null === $this->helperSet) {
throw new LogicException(sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name));
}
return $this->helperSet->get($name);
}
/**
* Validates a command name.
*
* It must be non-empty and parts can optionally be separated by ":".
*
* @throws InvalidArgumentException When the name is invalid
*/
private function validateName(string $name)
{
if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
}
}
}

View File

@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* HelpCommand displays the help for a given command.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class HelpCommand extends Command
{
private $command;
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->ignoreValidationErrors();
$this
->setName('help')
->setDefinition([
new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
])
->setDescription('Display help for a command')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command displays help for a given command:
<info>php %command.full_name% list</info>
You can also output the help in other formats by using the <comment>--format</comment> option:
<info>php %command.full_name% --format=xml list</info>
To display the list of available commands, please use the <info>list</info> command.
EOF
)
;
}
public function setCommand(Command $command)
{
$this->command = $command;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
if (null === $this->command) {
$this->command = $this->getApplication()->find($input->getArgument('command_name'));
}
$helper = new DescriptorHelper();
$helper->describe($output, $this->command, [
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
]);
$this->command = null;
return 0;
}
}

View File

@@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* ListCommand displays the list of all available commands for the application.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ListCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('list')
->setDefinition($this->createDefinition())
->setDescription('List commands')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command lists all commands:
<info>php %command.full_name%</info>
You can also display the commands for a specific namespace:
<info>php %command.full_name% test</info>
You can also output the information in other formats by using the <comment>--format</comment> option:
<info>php %command.full_name% --format=xml</info>
It's also possible to get raw list of commands (useful for embedding command runner):
<info>php %command.full_name% --raw</info>
EOF
)
;
}
/**
* {@inheritdoc}
*/
public function getNativeDefinition()
{
return $this->createDefinition();
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$helper = new DescriptorHelper();
$helper->describe($output, $this->getApplication(), [
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
'namespace' => $input->getArgument('namespace'),
]);
return 0;
}
private function createDefinition(): InputDefinition
{
return new InputDefinition([
new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
]);
}
}

View File

@@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\Store\FlockStore;
use Symfony\Component\Lock\Store\SemaphoreStore;
/**
* Basic lock feature for commands.
*
* @author Geoffrey Brier <geoffrey.brier@gmail.com>
*/
trait LockableTrait
{
/** @var Lock */
private $lock;
/**
* Locks a command.
*/
private function lock(string $name = null, bool $blocking = false): bool
{
if (!class_exists(SemaphoreStore::class)) {
throw new LogicException('To enable the locking feature you must install the symfony/lock component.');
}
if (null !== $this->lock) {
throw new LogicException('A lock is already in place.');
}
if (SemaphoreStore::isSupported()) {
$store = new SemaphoreStore();
} else {
$store = new FlockStore();
}
$this->lock = (new LockFactory($store))->createLock($name ?: $this->getName());
if (!$this->lock->acquire($blocking)) {
$this->lock = null;
return false;
}
return true;
}
/**
* Releases the command lock if there is one.
*/
private function release()
{
if ($this->lock) {
$this->lock->release();
$this->lock = null;
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\CommandLoader;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface CommandLoaderInterface
{
/**
* Loads a command.
*
* @param string $name
*
* @return Command
*
* @throws CommandNotFoundException
*/
public function get($name);
/**
* Checks if a command exists.
*
* @param string $name
*
* @return bool
*/
public function has($name);
/**
* @return string[] All registered command names
*/
public function getNames();
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\CommandLoader;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* Loads commands from a PSR-11 container.
*
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ContainerCommandLoader implements CommandLoaderInterface
{
private $container;
private $commandMap;
/**
* @param array $commandMap An array with command names as keys and service ids as values
*/
public function __construct(ContainerInterface $container, array $commandMap)
{
$this->container = $container;
$this->commandMap = $commandMap;
}
/**
* {@inheritdoc}
*/
public function get($name)
{
if (!$this->has($name)) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
}
return $this->container->get($this->commandMap[$name]);
}
/**
* {@inheritdoc}
*/
public function has($name)
{
return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]);
}
/**
* {@inheritdoc}
*/
public function getNames()
{
return array_keys($this->commandMap);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\CommandLoader;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* A simple command loader using factories to instantiate commands lazily.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class FactoryCommandLoader implements CommandLoaderInterface
{
private $factories;
/**
* @param callable[] $factories Indexed by command names
*/
public function __construct(array $factories)
{
$this->factories = $factories;
}
/**
* {@inheritdoc}
*/
public function has($name)
{
return isset($this->factories[$name]);
}
/**
* {@inheritdoc}
*/
public function get($name)
{
if (!isset($this->factories[$name])) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
}
$factory = $this->factories[$name];
return $factory();
}
/**
* {@inheritdoc}
*/
public function getNames()
{
return array_keys($this->factories);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console;
/**
* Contains all events dispatched by an Application.
*
* @author Francesco Levorato <git@flevour.net>
*/
final class ConsoleEvents
{
/**
* The COMMAND event allows you to attach listeners before any command is
* executed by the console. It also allows you to modify the command, input and output
* before they are handed to the command.
*
* @Event("Symfony\Component\Console\Event\ConsoleCommandEvent")
*/
public const COMMAND = 'console.command';
/**
* The TERMINATE event allows you to attach listeners after a command is
* executed by the console.
*
* @Event("Symfony\Component\Console\Event\ConsoleTerminateEvent")
*/
public const TERMINATE = 'console.terminate';
/**
* The ERROR event occurs when an uncaught exception or error appears.
*
* This event allows you to deal with the exception/error or
* to modify the thrown exception.
*
* @Event("Symfony\Component\Console\Event\ConsoleErrorEvent")
*/
public const ERROR = 'console.error';
}

View File

@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\DependencyInjection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\TypedReference;
/**
* Registers console commands.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class AddConsoleCommandPass implements CompilerPassInterface
{
private $commandLoaderServiceId;
private $commandTag;
public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command')
{
$this->commandLoaderServiceId = $commandLoaderServiceId;
$this->commandTag = $commandTag;
}
public function process(ContainerBuilder $container)
{
$commandServices = $container->findTaggedServiceIds($this->commandTag, true);
$lazyCommandMap = [];
$lazyCommandRefs = [];
$serviceIds = [];
foreach ($commandServices as $id => $tags) {
$definition = $container->getDefinition($id);
$class = $container->getParameterBag()->resolveValue($definition->getClass());
if (isset($tags[0]['command'])) {
$commandName = $tags[0]['command'];
} else {
if (!$r = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
if (!$r->isSubclassOf(Command::class)) {
throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class));
}
$commandName = $class::getDefaultName();
}
if (null === $commandName) {
if (!$definition->isPublic() || $definition->isPrivate()) {
$commandId = 'console.command.public_alias.'.$id;
$container->setAlias($commandId, $id)->setPublic(true);
$id = $commandId;
}
$serviceIds[] = $id;
continue;
}
unset($tags[0]);
$lazyCommandMap[$commandName] = $id;
$lazyCommandRefs[$id] = new TypedReference($id, $class);
$aliases = [];
foreach ($tags as $tag) {
if (isset($tag['command'])) {
$aliases[] = $tag['command'];
$lazyCommandMap[$tag['command']] = $id;
}
}
$definition->addMethodCall('setName', [$commandName]);
if ($aliases) {
$definition->addMethodCall('setAliases', [$aliases]);
}
}
$container
->register($this->commandLoaderServiceId, ContainerCommandLoader::class)
->setPublic(true)
->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]);
$container->setParameter('console.command.ids', $serviceIds);
}
}

View File

@@ -0,0 +1,143 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class ApplicationDescription
{
public const GLOBAL_NAMESPACE = '_global';
private $application;
private $namespace;
private $showHidden;
/**
* @var array
*/
private $namespaces;
/**
* @var Command[]
*/
private $commands;
/**
* @var Command[]
*/
private $aliases;
public function __construct(Application $application, string $namespace = null, bool $showHidden = false)
{
$this->application = $application;
$this->namespace = $namespace;
$this->showHidden = $showHidden;
}
public function getNamespaces(): array
{
if (null === $this->namespaces) {
$this->inspectApplication();
}
return $this->namespaces;
}
/**
* @return Command[]
*/
public function getCommands(): array
{
if (null === $this->commands) {
$this->inspectApplication();
}
return $this->commands;
}
/**
* @throws CommandNotFoundException
*/
public function getCommand(string $name): Command
{
if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
}
return $this->commands[$name] ?? $this->aliases[$name];
}
private function inspectApplication()
{
$this->commands = [];
$this->namespaces = [];
$all = $this->application->all($this->namespace ? $this->application->findNamespace($this->namespace) : null);
foreach ($this->sortCommands($all) as $namespace => $commands) {
$names = [];
/** @var Command $command */
foreach ($commands as $name => $command) {
if (!$command->getName() || (!$this->showHidden && $command->isHidden())) {
continue;
}
if ($command->getName() === $name) {
$this->commands[$name] = $command;
} else {
$this->aliases[$name] = $command;
}
$names[] = $name;
}
$this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names];
}
}
private function sortCommands(array $commands): array
{
$namespacedCommands = [];
$globalCommands = [];
$sortedCommands = [];
foreach ($commands as $name => $command) {
$key = $this->application->extractNamespace($name, 1);
if (\in_array($key, ['', self::GLOBAL_NAMESPACE], true)) {
$globalCommands[$name] = $command;
} else {
$namespacedCommands[$key][$name] = $command;
}
}
if ($globalCommands) {
ksort($globalCommands);
$sortedCommands[self::GLOBAL_NAMESPACE] = $globalCommands;
}
if ($namespacedCommands) {
ksort($namespacedCommands);
foreach ($namespacedCommands as $key => $commandsSet) {
ksort($commandsSet);
$sortedCommands[$key] = $commandsSet;
}
}
return $sortedCommands;
}
}

View File

@@ -0,0 +1,97 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
abstract class Descriptor implements DescriptorInterface
{
/**
* @var OutputInterface
*/
protected $output;
/**
* {@inheritdoc}
*/
public function describe(OutputInterface $output, $object, array $options = [])
{
$this->output = $output;
switch (true) {
case $object instanceof InputArgument:
$this->describeInputArgument($object, $options);
break;
case $object instanceof InputOption:
$this->describeInputOption($object, $options);
break;
case $object instanceof InputDefinition:
$this->describeInputDefinition($object, $options);
break;
case $object instanceof Command:
$this->describeCommand($object, $options);
break;
case $object instanceof Application:
$this->describeApplication($object, $options);
break;
default:
throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', \get_class($object)));
}
}
/**
* Writes content to output.
*
* @param string $content
* @param bool $decorated
*/
protected function write($content, $decorated = false)
{
$this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW);
}
/**
* Describes an InputArgument instance.
*/
abstract protected function describeInputArgument(InputArgument $argument, array $options = []);
/**
* Describes an InputOption instance.
*/
abstract protected function describeInputOption(InputOption $option, array $options = []);
/**
* Describes an InputDefinition instance.
*/
abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []);
/**
* Describes a Command instance.
*/
abstract protected function describeCommand(Command $command, array $options = []);
/**
* Describes an Application instance.
*/
abstract protected function describeApplication(Application $application, array $options = []);
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Descriptor interface.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
interface DescriptorInterface
{
/**
* Describes an object if supported.
*
* @param object $object
*/
public function describe(OutputInterface $output, $object, array $options = []);
}

View File

@@ -0,0 +1,156 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
/**
* JSON descriptor.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*
* @internal
*/
class JsonDescriptor extends Descriptor
{
/**
* {@inheritdoc}
*/
protected function describeInputArgument(InputArgument $argument, array $options = [])
{
$this->writeData($this->getInputArgumentData($argument), $options);
}
/**
* {@inheritdoc}
*/
protected function describeInputOption(InputOption $option, array $options = [])
{
$this->writeData($this->getInputOptionData($option), $options);
}
/**
* {@inheritdoc}
*/
protected function describeInputDefinition(InputDefinition $definition, array $options = [])
{
$this->writeData($this->getInputDefinitionData($definition), $options);
}
/**
* {@inheritdoc}
*/
protected function describeCommand(Command $command, array $options = [])
{
$this->writeData($this->getCommandData($command), $options);
}
/**
* {@inheritdoc}
*/
protected function describeApplication(Application $application, array $options = [])
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace, true);
$commands = [];
foreach ($description->getCommands() as $command) {
$commands[] = $this->getCommandData($command);
}
$data = [];
if ('UNKNOWN' !== $application->getName()) {
$data['application']['name'] = $application->getName();
if ('UNKNOWN' !== $application->getVersion()) {
$data['application']['version'] = $application->getVersion();
}
}
$data['commands'] = $commands;
if ($describedNamespace) {
$data['namespace'] = $describedNamespace;
} else {
$data['namespaces'] = array_values($description->getNamespaces());
}
$this->writeData($data, $options);
}
/**
* Writes data as json.
*/
private function writeData(array $data, array $options)
{
$flags = $options['json_encoding'] ?? 0;
$this->write(json_encode($data, $flags));
}
private function getInputArgumentData(InputArgument $argument): array
{
return [
'name' => $argument->getName(),
'is_required' => $argument->isRequired(),
'is_array' => $argument->isArray(),
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()),
'default' => \INF === $argument->getDefault() ? 'INF' : $argument->getDefault(),
];
}
private function getInputOptionData(InputOption $option): array
{
return [
'name' => '--'.$option->getName(),
'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '',
'accept_value' => $option->acceptValue(),
'is_value_required' => $option->isValueRequired(),
'is_multiple' => $option->isArray(),
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()),
'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(),
];
}
private function getInputDefinitionData(InputDefinition $definition): array
{
$inputArguments = [];
foreach ($definition->getArguments() as $name => $argument) {
$inputArguments[$name] = $this->getInputArgumentData($argument);
}
$inputOptions = [];
foreach ($definition->getOptions() as $name => $option) {
$inputOptions[$name] = $this->getInputOptionData($option);
}
return ['arguments' => $inputArguments, 'options' => $inputOptions];
}
private function getCommandData(Command $command): array
{
$command->getSynopsis();
$command->mergeApplicationDefinition(false);
return [
'name' => $command->getName(),
'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()),
'description' => $command->getDescription(),
'help' => $command->getProcessedHelp(),
'definition' => $this->getInputDefinitionData($command->getNativeDefinition()),
'hidden' => $command->isHidden(),
];
}
}

View File

@@ -0,0 +1,182 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Markdown descriptor.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*
* @internal
*/
class MarkdownDescriptor extends Descriptor
{
/**
* {@inheritdoc}
*/
public function describe(OutputInterface $output, $object, array $options = [])
{
$decorated = $output->isDecorated();
$output->setDecorated(false);
parent::describe($output, $object, $options);
$output->setDecorated($decorated);
}
/**
* {@inheritdoc}
*/
protected function write($content, $decorated = true)
{
parent::write($content, $decorated);
}
/**
* {@inheritdoc}
*/
protected function describeInputArgument(InputArgument $argument, array $options = [])
{
$this->write(
'#### `'.($argument->getName() ?: '<none>')."`\n\n"
.($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '')
.'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n"
.'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n"
.'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`'
);
}
/**
* {@inheritdoc}
*/
protected function describeInputOption(InputOption $option, array $options = [])
{
$name = '--'.$option->getName();
if ($option->getShortcut()) {
$name .= '|-'.str_replace('|', '|-', $option->getShortcut()).'';
}
$this->write(
'#### `'.$name.'`'."\n\n"
.($option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $option->getDescription())."\n\n" : '')
.'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n"
.'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
.'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n"
.'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`'
);
}
/**
* {@inheritdoc}
*/
protected function describeInputDefinition(InputDefinition $definition, array $options = [])
{
if ($showArguments = \count($definition->getArguments()) > 0) {
$this->write('### Arguments');
foreach ($definition->getArguments() as $argument) {
$this->write("\n\n");
$this->write($this->describeInputArgument($argument));
}
}
if (\count($definition->getOptions()) > 0) {
if ($showArguments) {
$this->write("\n\n");
}
$this->write('### Options');
foreach ($definition->getOptions() as $option) {
$this->write("\n\n");
$this->write($this->describeInputOption($option));
}
}
}
/**
* {@inheritdoc}
*/
protected function describeCommand(Command $command, array $options = [])
{
$command->getSynopsis();
$command->mergeApplicationDefinition(false);
$this->write(
'`'.$command->getName()."`\n"
.str_repeat('-', Helper::strlen($command->getName()) + 2)."\n\n"
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
.'### Usage'."\n\n"
.array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), function ($carry, $usage) {
return $carry.'* `'.$usage.'`'."\n";
})
);
if ($help = $command->getProcessedHelp()) {
$this->write("\n");
$this->write($help);
}
if ($command->getNativeDefinition()) {
$this->write("\n\n");
$this->describeInputDefinition($command->getNativeDefinition());
}
}
/**
* {@inheritdoc}
*/
protected function describeApplication(Application $application, array $options = [])
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace);
$title = $this->getApplicationTitle($application);
$this->write($title."\n".str_repeat('=', Helper::strlen($title)));
foreach ($description->getNamespaces() as $namespace) {
if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
$this->write("\n\n");
$this->write('**'.$namespace['id'].':**');
}
$this->write("\n\n");
$this->write(implode("\n", array_map(function ($commandName) use ($description) {
return sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName()));
}, $namespace['commands'])));
}
foreach ($description->getCommands() as $command) {
$this->write("\n\n");
$this->write($this->describeCommand($command));
}
}
private function getApplicationTitle(Application $application): string
{
if ('UNKNOWN' !== $application->getName()) {
if ('UNKNOWN' !== $application->getVersion()) {
return sprintf('%s %s', $application->getName(), $application->getVersion());
}
return $application->getName();
}
return 'Console Tool';
}
}

View File

@@ -0,0 +1,342 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
/**
* Text descriptor.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*
* @internal
*/
class TextDescriptor extends Descriptor
{
/**
* {@inheritdoc}
*/
protected function describeInputArgument(InputArgument $argument, array $options = [])
{
if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) {
$default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($argument->getDefault()));
} else {
$default = '';
}
$totalWidth = $options['total_width'] ?? Helper::strlen($argument->getName());
$spacingWidth = $totalWidth - \strlen($argument->getName());
$this->writeText(sprintf(' <info>%s</info> %s%s%s',
$argument->getName(),
str_repeat(' ', $spacingWidth),
// + 4 = 2 spaces before <info>, 2 spaces after </info>
preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $argument->getDescription()),
$default
), $options);
}
/**
* {@inheritdoc}
*/
protected function describeInputOption(InputOption $option, array $options = [])
{
if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) {
$default = sprintf('<comment> [default: %s]</comment>', $this->formatDefaultValue($option->getDefault()));
} else {
$default = '';
}
$value = '';
if ($option->acceptValue()) {
$value = '='.strtoupper($option->getName());
if ($option->isValueOptional()) {
$value = '['.$value.']';
}
}
$totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]);
$synopsis = sprintf('%s%s',
$option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ',
sprintf('--%s%s', $option->getName(), $value)
);
$spacingWidth = $totalWidth - Helper::strlen($synopsis);
$this->writeText(sprintf(' <info>%s</info> %s%s%s%s',
$synopsis,
str_repeat(' ', $spacingWidth),
// + 4 = 2 spaces before <info>, 2 spaces after </info>
preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $option->getDescription()),
$default,
$option->isArray() ? '<comment> (multiple values allowed)</comment>' : ''
), $options);
}
/**
* {@inheritdoc}
*/
protected function describeInputDefinition(InputDefinition $definition, array $options = [])
{
$totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
foreach ($definition->getArguments() as $argument) {
$totalWidth = max($totalWidth, Helper::strlen($argument->getName()));
}
if ($definition->getArguments()) {
$this->writeText('<comment>Arguments:</comment>', $options);
$this->writeText("\n");
foreach ($definition->getArguments() as $argument) {
$this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth]));
$this->writeText("\n");
}
}
if ($definition->getArguments() && $definition->getOptions()) {
$this->writeText("\n");
}
if ($definition->getOptions()) {
$laterOptions = [];
$this->writeText('<comment>Options:</comment>', $options);
foreach ($definition->getOptions() as $option) {
if (\strlen($option->getShortcut() ?? '') > 1) {
$laterOptions[] = $option;
continue;
}
$this->writeText("\n");
$this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
}
foreach ($laterOptions as $option) {
$this->writeText("\n");
$this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth]));
}
}
}
/**
* {@inheritdoc}
*/
protected function describeCommand(Command $command, array $options = [])
{
$command->getSynopsis(true);
$command->getSynopsis(false);
$command->mergeApplicationDefinition(false);
if ($description = $command->getDescription()) {
$this->writeText('<comment>Description:</comment>', $options);
$this->writeText("\n");
$this->writeText(' '.$description);
$this->writeText("\n\n");
}
$this->writeText('<comment>Usage:</comment>', $options);
foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) {
$this->writeText("\n");
$this->writeText(' '.OutputFormatter::escape($usage), $options);
}
$this->writeText("\n");
$definition = $command->getNativeDefinition();
if ($definition->getOptions() || $definition->getArguments()) {
$this->writeText("\n");
$this->describeInputDefinition($definition, $options);
$this->writeText("\n");
}
$help = $command->getProcessedHelp();
if ($help && $help !== $description) {
$this->writeText("\n");
$this->writeText('<comment>Help:</comment>', $options);
$this->writeText("\n");
$this->writeText(' '.str_replace("\n", "\n ", $help), $options);
$this->writeText("\n");
}
}
/**
* {@inheritdoc}
*/
protected function describeApplication(Application $application, array $options = [])
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace);
if (isset($options['raw_text']) && $options['raw_text']) {
$width = $this->getColumnWidth($description->getCommands());
foreach ($description->getCommands() as $command) {
$this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options);
$this->writeText("\n");
}
} else {
if ('' != $help = $application->getHelp()) {
$this->writeText("$help\n\n", $options);
}
$this->writeText("<comment>Usage:</comment>\n", $options);
$this->writeText(" command [options] [arguments]\n\n", $options);
$this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options);
$this->writeText("\n");
$this->writeText("\n");
$commands = $description->getCommands();
$namespaces = $description->getNamespaces();
if ($describedNamespace && $namespaces) {
// make sure all alias commands are included when describing a specific namespace
$describedNamespaceInfo = reset($namespaces);
foreach ($describedNamespaceInfo['commands'] as $name) {
$commands[$name] = $description->getCommand($name);
}
}
// calculate max. width based on available commands per namespace
$width = $this->getColumnWidth(array_merge(...array_values(array_map(function ($namespace) use ($commands) {
return array_intersect($namespace['commands'], array_keys($commands));
}, array_values($namespaces)))));
if ($describedNamespace) {
$this->writeText(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options);
} else {
$this->writeText('<comment>Available commands:</comment>', $options);
}
foreach ($namespaces as $namespace) {
$namespace['commands'] = array_filter($namespace['commands'], function ($name) use ($commands) {
return isset($commands[$name]);
});
if (!$namespace['commands']) {
continue;
}
if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
$this->writeText("\n");
$this->writeText(' <comment>'.$namespace['id'].'</comment>', $options);
}
foreach ($namespace['commands'] as $name) {
$this->writeText("\n");
$spacingWidth = $width - Helper::strlen($name);
$command = $commands[$name];
$commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : '';
$this->writeText(sprintf(' <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options);
}
}
$this->writeText("\n");
}
}
/**
* {@inheritdoc}
*/
private function writeText(string $content, array $options = [])
{
$this->write(
isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content,
isset($options['raw_output']) ? !$options['raw_output'] : true
);
}
/**
* Formats command aliases to show them in the command description.
*/
private function getCommandAliasesText(Command $command): string
{
$text = '';
$aliases = $command->getAliases();
if ($aliases) {
$text = '['.implode('|', $aliases).'] ';
}
return $text;
}
/**
* Formats input option/argument default value.
*
* @param mixed $default
*/
private function formatDefaultValue($default): string
{
if (\INF === $default) {
return 'INF';
}
if (\is_string($default)) {
$default = OutputFormatter::escape($default);
} elseif (\is_array($default)) {
foreach ($default as $key => $value) {
if (\is_string($value)) {
$default[$key] = OutputFormatter::escape($value);
}
}
}
return str_replace('\\\\', '\\', json_encode($default, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE));
}
/**
* @param array<Command|string> $commands
*/
private function getColumnWidth(array $commands): int
{
$widths = [];
foreach ($commands as $command) {
if ($command instanceof Command) {
$widths[] = Helper::strlen($command->getName());
foreach ($command->getAliases() as $alias) {
$widths[] = Helper::strlen($alias);
}
} else {
$widths[] = Helper::strlen($command);
}
}
return $widths ? max($widths) + 2 : 0;
}
/**
* @param InputOption[] $options
*/
private function calculateTotalWidthForOptions(array $options): int
{
$totalWidth = 0;
foreach ($options as $option) {
// "-" + shortcut + ", --" + name
$nameLength = 1 + max(Helper::strlen($option->getShortcut()), 1) + 4 + Helper::strlen($option->getName());
if ($option->acceptValue()) {
$valueLength = 1 + Helper::strlen($option->getName()); // = + value
$valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
$nameLength += $valueLength;
}
$totalWidth = max($totalWidth, $nameLength);
}
return $totalWidth;
}
}

View File

@@ -0,0 +1,231 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
/**
* XML descriptor.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*
* @internal
*/
class XmlDescriptor extends Descriptor
{
public function getInputDefinitionDocument(InputDefinition $definition): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($definitionXML = $dom->createElement('definition'));
$definitionXML->appendChild($argumentsXML = $dom->createElement('arguments'));
foreach ($definition->getArguments() as $argument) {
$this->appendDocument($argumentsXML, $this->getInputArgumentDocument($argument));
}
$definitionXML->appendChild($optionsXML = $dom->createElement('options'));
foreach ($definition->getOptions() as $option) {
$this->appendDocument($optionsXML, $this->getInputOptionDocument($option));
}
return $dom;
}
public function getCommandDocument(Command $command): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($commandXML = $dom->createElement('command'));
$command->getSynopsis();
$command->mergeApplicationDefinition(false);
$commandXML->setAttribute('id', $command->getName());
$commandXML->setAttribute('name', $command->getName());
$commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0);
$commandXML->appendChild($usagesXML = $dom->createElement('usages'));
foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) {
$usagesXML->appendChild($dom->createElement('usage', $usage));
}
$commandXML->appendChild($descriptionXML = $dom->createElement('description'));
$descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription())));
$commandXML->appendChild($helpXML = $dom->createElement('help'));
$helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp())));
$definitionXML = $this->getInputDefinitionDocument($command->getNativeDefinition());
$this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0));
return $dom;
}
public function getApplicationDocument(Application $application, string $namespace = null): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($rootXml = $dom->createElement('symfony'));
if ('UNKNOWN' !== $application->getName()) {
$rootXml->setAttribute('name', $application->getName());
if ('UNKNOWN' !== $application->getVersion()) {
$rootXml->setAttribute('version', $application->getVersion());
}
}
$rootXml->appendChild($commandsXML = $dom->createElement('commands'));
$description = new ApplicationDescription($application, $namespace, true);
if ($namespace) {
$commandsXML->setAttribute('namespace', $namespace);
}
foreach ($description->getCommands() as $command) {
$this->appendDocument($commandsXML, $this->getCommandDocument($command));
}
if (!$namespace) {
$rootXml->appendChild($namespacesXML = $dom->createElement('namespaces'));
foreach ($description->getNamespaces() as $namespaceDescription) {
$namespacesXML->appendChild($namespaceArrayXML = $dom->createElement('namespace'));
$namespaceArrayXML->setAttribute('id', $namespaceDescription['id']);
foreach ($namespaceDescription['commands'] as $name) {
$namespaceArrayXML->appendChild($commandXML = $dom->createElement('command'));
$commandXML->appendChild($dom->createTextNode($name));
}
}
}
return $dom;
}
/**
* {@inheritdoc}
*/
protected function describeInputArgument(InputArgument $argument, array $options = [])
{
$this->writeDocument($this->getInputArgumentDocument($argument));
}
/**
* {@inheritdoc}
*/
protected function describeInputOption(InputOption $option, array $options = [])
{
$this->writeDocument($this->getInputOptionDocument($option));
}
/**
* {@inheritdoc}
*/
protected function describeInputDefinition(InputDefinition $definition, array $options = [])
{
$this->writeDocument($this->getInputDefinitionDocument($definition));
}
/**
* {@inheritdoc}
*/
protected function describeCommand(Command $command, array $options = [])
{
$this->writeDocument($this->getCommandDocument($command));
}
/**
* {@inheritdoc}
*/
protected function describeApplication(Application $application, array $options = [])
{
$this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null));
}
/**
* Appends document children to parent node.
*/
private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent)
{
foreach ($importedParent->childNodes as $childNode) {
$parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true));
}
}
/**
* Writes DOM document.
*/
private function writeDocument(\DOMDocument $dom)
{
$dom->formatOutput = true;
$this->write($dom->saveXML());
}
private function getInputArgumentDocument(InputArgument $argument): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($objectXML = $dom->createElement('argument'));
$objectXML->setAttribute('name', $argument->getName());
$objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0);
$objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0);
$objectXML->appendChild($descriptionXML = $dom->createElement('description'));
$descriptionXML->appendChild($dom->createTextNode($argument->getDescription()));
$objectXML->appendChild($defaultsXML = $dom->createElement('defaults'));
$defaults = \is_array($argument->getDefault()) ? $argument->getDefault() : (\is_bool($argument->getDefault()) ? [var_export($argument->getDefault(), true)] : ($argument->getDefault() ? [$argument->getDefault()] : []));
foreach ($defaults as $default) {
$defaultsXML->appendChild($defaultXML = $dom->createElement('default'));
$defaultXML->appendChild($dom->createTextNode($default));
}
return $dom;
}
private function getInputOptionDocument(InputOption $option): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($objectXML = $dom->createElement('option'));
$objectXML->setAttribute('name', '--'.$option->getName());
$pos = strpos($option->getShortcut() ?? '', '|');
if (false !== $pos) {
$objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos));
$objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut()));
} else {
$objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : '');
}
$objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0);
$objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0);
$objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0);
$objectXML->appendChild($descriptionXML = $dom->createElement('description'));
$descriptionXML->appendChild($dom->createTextNode($option->getDescription()));
if ($option->acceptValue()) {
$defaults = \is_array($option->getDefault()) ? $option->getDefault() : (\is_bool($option->getDefault()) ? [var_export($option->getDefault(), true)] : ($option->getDefault() ? [$option->getDefault()] : []));
$objectXML->appendChild($defaultsXML = $dom->createElement('defaults'));
if (!empty($defaults)) {
foreach ($defaults as $default) {
$defaultsXML->appendChild($defaultXML = $dom->createElement('default'));
$defaultXML->appendChild($dom->createTextNode($default));
}
}
}
return $dom;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Event;
/**
* Allows to do things before the command is executed, like skipping the command or changing the input.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final since Symfony 4.4
*/
class ConsoleCommandEvent extends ConsoleEvent
{
/**
* The return code for skipped commands, this will also be passed into the terminate event.
*/
public const RETURN_CODE_DISABLED = 113;
/**
* Indicates if the command should be run or skipped.
*/
private $commandShouldRun = true;
/**
* Disables the command, so it won't be run.
*
* @return bool
*/
public function disableCommand()
{
return $this->commandShouldRun = false;
}
/**
* Enables the command.
*
* @return bool
*/
public function enableCommand()
{
return $this->commandShouldRun = true;
}
/**
* Returns true if the command is runnable, false otherwise.
*
* @return bool
*/
public function commandShouldRun()
{
return $this->commandShouldRun;
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Event;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Allows to handle throwables thrown while running a command.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class ConsoleErrorEvent extends ConsoleEvent
{
private $error;
private $exitCode;
public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, Command $command = null)
{
parent::__construct($command, $input, $output);
$this->error = $error;
}
public function getError(): \Throwable
{
return $this->error;
}
public function setError(\Throwable $error): void
{
$this->error = $error;
}
public function setExitCode(int $exitCode): void
{
$this->exitCode = $exitCode;
$r = new \ReflectionProperty($this->error, 'code');
$r->setAccessible(true);
$r->setValue($this->error, $this->exitCode);
}
public function getExitCode(): int
{
return $this->exitCode ?? (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1);
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Event;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Allows to inspect input and output of a command.
*
* @author Francesco Levorato <git@flevour.net>
*/
class ConsoleEvent extends Event
{
protected $command;
private $input;
private $output;
public function __construct(?Command $command, InputInterface $input, OutputInterface $output)
{
$this->command = $command;
$this->input = $input;
$this->output = $output;
}
/**
* Gets the command that is executed.
*
* @return Command|null A Command instance
*/
public function getCommand()
{
return $this->command;
}
/**
* Gets the input instance.
*
* @return InputInterface An InputInterface instance
*/
public function getInput()
{
return $this->input;
}
/**
* Gets the output instance.
*
* @return OutputInterface An OutputInterface instance
*/
public function getOutput()
{
return $this->output;
}
}

Some files were not shown because too many files have changed in this diff Show More