Stand: SMTP-Test, Admin-Mail-Tab, Notifiable-Fix, Lazy-Quill

- Fix: Notifiable-Trait zum User-Model hinzugefuegt (behebt notify()-500er)
- Installer: SMTP-Verbindungstest mit EsmtpTransport + Ueberspringen-Link
- Admin: Neuer E-Mail-Tab mit SMTP-Konfiguration + Verbindungstest
- Admin: Lazy Quill-Initialisierung (nur sichtbare Locale wird geladen)
- Uebersetzungen: 17 neue Mail-Keys in allen 6 Sprachen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Rhino
2026-03-02 07:30:37 +01:00
commit 2e24a40d68
9633 changed files with 1300799 additions and 0 deletions

147
vendor/symfony/console/Attribute/Argument.php vendored Executable file
View File

@@ -0,0 +1,147 @@
<?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\Attribute;
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\String\UnicodeString;
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
class Argument
{
private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array'];
private string|bool|int|float|array|null $default = null;
private array|\Closure $suggestedValues;
private ?int $mode = null;
/**
* @var string|class-string<\BackedEnum>
*/
private string $typeName = '';
private ?InteractiveAttributeInterface $interactiveAttribute = null;
/**
* Represents a console command <argument> definition.
*
* If unset, the `name` value will be inferred from the parameter definition.
*
* @param array<string|Suggestion>|callable(CompletionInput):list<string|Suggestion> $suggestedValues The values used for input completion
*/
public function __construct(
public string $description = '',
public string $name = '',
array|callable $suggestedValues = [],
) {
$this->suggestedValues = \is_callable($suggestedValues) ? $suggestedValues(...) : $suggestedValues;
}
/**
* @internal
*/
public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member): ?self
{
$reflection = new ReflectionMember($member);
if (!$self = $reflection->getAttribute(self::class)) {
return null;
}
$type = $reflection->getType();
$name = $reflection->getName();
if (!$type instanceof \ReflectionNamedType) {
throw new LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $reflection->getMemberName(), $name, $reflection->getSourceName()));
}
$self->typeName = $type->getName();
$isBackedEnum = is_subclass_of($self->typeName, \BackedEnum::class);
if (!\in_array($self->typeName, self::ALLOWED_TYPES, true) && !$isBackedEnum) {
throw new LogicException(\sprintf('The type "%s" on %s "$%s" of "%s" is not supported as a command argument. Only "%s" types and backed enums are allowed.', $self->typeName, $reflection->getMemberName(), $name, $reflection->getSourceName(), implode('", "', self::ALLOWED_TYPES)));
}
if (!$self->name) {
$self->name = (new UnicodeString($name))->kebab();
}
$self->default = $reflection->hasDefaultValue() ? $reflection->getDefaultValue() : null;
$isOptional = $reflection->hasDefaultValue() || $reflection->isNullable();
$self->mode = $isOptional ? InputArgument::OPTIONAL : InputArgument::REQUIRED;
if ('array' === $self->typeName) {
$self->mode |= InputArgument::IS_ARRAY;
}
if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $reflection->getSourceThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) {
// In case that the callback is declared as a static method `[Foo::class, 'methodName']` - yet it is not callable,
// while non-static method `[Foo $instance, 'methodName']` would be callable, we transform the callback on the fly into a non-static version.
$self->suggestedValues = [$instance, $self->suggestedValues[1]];
}
if ($isBackedEnum && !$self->suggestedValues) {
$self->suggestedValues = array_column($self->typeName::cases(), 'value');
}
$self->interactiveAttribute = Ask::tryFrom($member, $self->name);
if ($self->interactiveAttribute && $isOptional) {
throw new LogicException(\sprintf('The %s "$%s" argument of "%s" cannot be both interactive and optional.', $reflection->getMemberName(), $self->name, $reflection->getSourceName()));
}
return $self;
}
/**
* @internal
*/
public function toInputArgument(): InputArgument
{
$suggestedValues = \is_callable($this->suggestedValues) ? ($this->suggestedValues)(...) : $this->suggestedValues;
return new InputArgument($this->name, $this->mode, $this->description, $this->default, $suggestedValues);
}
/**
* @internal
*/
public function resolveValue(InputInterface $input): mixed
{
$value = $input->getArgument($this->name);
if (is_subclass_of($this->typeName, \BackedEnum::class) && (\is_string($value) || \is_int($value))) {
return $this->typeName::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($this->name, $value, $this->suggestedValues);
}
return $value;
}
/**
* @internal
*/
public function getInteractiveAttribute(): ?InteractiveAttributeInterface
{
return $this->interactiveAttribute;
}
/**
* @internal
*/
public function isRequired(): bool
{
return InputArgument::REQUIRED === (InputArgument::REQUIRED & $this->mode);
}
}

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\Console\Attribute;
/**
* Service tag to autoconfigure commands.
*
* @final since Symfony 7.3
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class AsCommand
{
/**
* @param string $name The name of the command, used when calling it (i.e. "cache:clear")
* @param string|null $description The description of the command, displayed with the help page
* @param string[] $aliases The list of aliases of the command. The command will be executed when using one of them (i.e. "cache:clean")
* @param bool $hidden If true, the command won't be shown when listing all the available commands, but it can still be run as any other command
* @param string|null $help The help content of the command, displayed with the help page
* @param string[] $usages The list of usage examples, displayed with the help page
*/
public function __construct(
public string $name,
public ?string $description = null,
array $aliases = [],
bool $hidden = false,
public ?string $help = null,
public array $usages = [],
) {
if (!$hidden && !$aliases) {
return;
}
$name = explode('|', $name);
$name = array_merge($name, $aliases);
if ($hidden && '' !== $name[0]) {
array_unshift($name, '');
}
$this->name = implode('|', $name);
}
}

147
vendor/symfony/console/Attribute/Ask.php vendored Executable file
View File

@@ -0,0 +1,147 @@
<?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\Attribute;
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
class Ask implements InteractiveAttributeInterface
{
public ?\Closure $normalizer;
public ?\Closure $validator;
private \Closure $closure;
/**
* @param string $question The question to ask the user
* @param string|bool|int|float|null $default The default answer to return if the user enters nothing
* @param bool $hidden Whether the user response must be hidden or not
* @param bool $multiline Whether the user response should accept newline characters
* @param bool $trimmable Whether the user response must be trimmed or not
* @param int|null $timeout The maximum time the user has to answer the question in seconds
* @param callable|null $validator The validator for the question
* @param int|null $maxAttempts The maximum number of attempts allowed to answer the question.
* Null means an unlimited number of attempts
*/
public function __construct(
public string $question,
public string|bool|int|float|null $default = null,
public bool $hidden = false,
public bool $multiline = false,
public bool $trimmable = true,
public ?int $timeout = null,
?callable $normalizer = null,
?callable $validator = null,
public ?int $maxAttempts = null,
) {
$this->normalizer = $normalizer ? $normalizer(...) : null;
$this->validator = $validator ? $validator(...) : null;
}
/**
* @internal
*/
public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member, string $name): ?self
{
$reflection = new ReflectionMember($member);
if (!$self = $reflection->getAttribute(self::class)) {
return null;
}
$type = $reflection->getType();
if (!$type instanceof \ReflectionNamedType) {
throw new LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped, Union or Intersection types are not supported for interactive questions.', $reflection->getMemberName(), $name, $reflection->getSourceName()));
}
$self->closure = function (SymfonyStyle $io, InputInterface $input) use ($self, $reflection, $name, $type) {
if ($reflection->isProperty() && isset($this->{$reflection->getName()})) {
return;
}
if ($reflection->isParameter() && !\in_array($input->getArgument($name), [null, []], true)) {
return;
}
if ('bool' === $type->getName()) {
$self->default ??= false;
if (!\is_bool($self->default)) {
throw new LogicException(\sprintf('The "%s::$default" value for the %s "$%s" of "%s" must be a boolean.', self::class, $reflection->getMemberName(), $name, $reflection->getSourceName()));
}
$question = new ConfirmationQuestion($self->question, $self->default);
} else {
$question = new Question($self->question, $self->default);
}
$question->setHidden($self->hidden);
$question->setMultiline($self->multiline);
$question->setTrimmable($self->trimmable);
$question->setTimeout($self->timeout);
if (!$self->validator && $reflection->isProperty() && 'array' !== $type->getName()) {
$self->validator = function (mixed $value) use ($reflection): mixed {
return $this->{$reflection->getName()} = $value;
};
}
$question->setValidator($self->validator);
$question->setMaxAttempts($self->maxAttempts);
if ($self->normalizer) {
$question->setNormalizer($self->normalizer);
} elseif (is_subclass_of($type->getName(), \BackedEnum::class)) {
/** @var class-string<\BackedEnum> $backedType */
$backedType = $reflection->getType()->getName();
$question->setNormalizer(fn (string|int $value) => $backedType::tryFrom($value) ?? throw InvalidArgumentException::fromEnumValue($reflection->getName(), $value, array_column($backedType::cases(), 'value')));
}
if ('array' === $type->getName()) {
$value = [];
while ($v = $io->askQuestion($question)) {
if ("\x4" === $v || \PHP_EOL === $v || ($question->isTrimmable() && '' === $v = trim($v))) {
break;
}
$value[] = $v;
}
} else {
$value = $io->askQuestion($question);
}
if (null === $value && !$reflection->isNullable()) {
return;
}
if ($reflection->isProperty()) {
$this->{$reflection->getName()} = $value;
} else {
$input->setArgument($name, $value);
}
};
return $self;
}
/**
* @internal
*/
public function getFunction(object $instance): \ReflectionFunction
{
return new \ReflectionFunction($this->closure->bindTo($instance, $instance::class));
}
}

51
vendor/symfony/console/Attribute/Interact.php vendored Executable file
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\Console\Attribute;
use Symfony\Component\Console\Exception\LogicException;
#[\Attribute(\Attribute::TARGET_METHOD)]
class Interact implements InteractiveAttributeInterface
{
private \ReflectionMethod $method;
/**
* @internal
*/
public static function tryFrom(\ReflectionMethod $method): ?self
{
/** @var self|null $self */
if (!$self = ($method->getAttributes(self::class)[0] ?? null)?->newInstance()) {
return null;
}
if (!$method->isPublic() || $method->isStatic()) {
throw new LogicException(\sprintf('The interactive method "%s::%s()" must be public and non-static.', $method->getDeclaringClass()->getName(), $method->getName()));
}
if ('__invoke' === $method->getName()) {
throw new LogicException(\sprintf('The "%s::__invoke()" method cannot be used as an interactive method.', $method->getDeclaringClass()->getName()));
}
$self->method = $method;
return $self;
}
/**
* @internal
*/
public function getFunction(object $instance): \ReflectionFunction
{
return new \ReflectionFunction($this->method->getClosure($instance));
}
}

View File

@@ -0,0 +1,20 @@
<?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\Attribute;
/**
* @internal
*/
interface InteractiveAttributeInterface
{
public function getFunction(object $instance): \ReflectionFunction;
}

188
vendor/symfony/console/Attribute/MapInput.php vendored Executable file
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\Console\Attribute;
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Interaction\Interaction;
/**
* Maps a command input into an object (DTO).
*/
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
final class MapInput
{
/**
* @var array<string, Argument|Option|self>
*/
private array $definition = [];
private \ReflectionClass $class;
/**
* @var list<Interact>
*/
private array $interactiveAttributes = [];
/**
* @internal
*/
public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member): ?self
{
$reflection = new ReflectionMember($member);
if (!$self = $reflection->getAttribute(self::class)) {
return null;
}
$type = $reflection->getType();
if (!$type instanceof \ReflectionNamedType) {
throw new LogicException(\sprintf('The input %s "%s" must have a named type.', $reflection->getMemberName(), $member->name));
}
if (!class_exists($class = $type->getName())) {
throw new LogicException(\sprintf('The input class "%s" does not exist.', $type->getName()));
}
$self->class = new \ReflectionClass($class);
foreach ($self->class->getProperties() as $property) {
if ($argument = Argument::tryFrom($property)) {
$self->definition[$property->name] = $argument;
} elseif ($option = Option::tryFrom($property)) {
$self->definition[$property->name] = $option;
} elseif ($input = self::tryFrom($property)) {
$self->definition[$property->name] = $input;
}
if (isset($self->definition[$property->name]) && (!$property->isPublic() || $property->isStatic())) {
throw new LogicException(\sprintf('The input property "%s::$%s" must be public and non-static.', $self->class->name, $property->name));
}
}
if (!$self->definition) {
throw new LogicException(\sprintf('The input class "%s" must have at least one argument or option.', $self->class->name));
}
foreach ($self->class->getMethods() as $method) {
if ($attribute = Interact::tryFrom($method)) {
$self->interactiveAttributes[] = $attribute;
}
}
return $self;
}
/**
* @internal
*/
public function resolveValue(InputInterface $input): object
{
$instance = $this->class->newInstanceWithoutConstructor();
foreach ($this->definition as $name => $spec) {
// ignore required arguments that are not set yet (may happen in interactive mode)
if ($spec instanceof Argument && $spec->isRequired() && \in_array($input->getArgument($spec->name), [null, []], true)) {
continue;
}
$instance->$name = $spec->resolveValue($input);
}
return $instance;
}
/**
* @internal
*/
public function setValue(InputInterface $input, object $object): void
{
foreach ($this->definition as $name => $spec) {
$property = $this->class->getProperty($name);
if (!$property->isInitialized($object) || \in_array($value = $property->getValue($object), [null, []], true)) {
continue;
}
match (true) {
$spec instanceof Argument => $input->setArgument($spec->name, $value),
$spec instanceof Option => $input->setOption($spec->name, $value),
$spec instanceof self => $spec->setValue($input, $value),
default => throw new LogicException('Unexpected specification type.'),
};
}
}
/**
* @return iterable<Argument>
*/
public function getArguments(): iterable
{
foreach ($this->definition as $spec) {
if ($spec instanceof Argument) {
yield $spec;
} elseif ($spec instanceof self) {
yield from $spec->getArguments();
}
}
}
/**
* @return iterable<Option>
*/
public function getOptions(): iterable
{
foreach ($this->definition as $spec) {
if ($spec instanceof Option) {
yield $spec;
} elseif ($spec instanceof self) {
yield from $spec->getOptions();
}
}
}
/**
* @internal
*
* @return iterable<Interaction>
*/
public function getPropertyInteractions(): iterable
{
foreach ($this->definition as $spec) {
if ($spec instanceof self) {
yield from $spec->getPropertyInteractions();
} elseif ($spec instanceof Argument && $attribute = $spec->getInteractiveAttribute()) {
yield new Interaction($this, $attribute);
}
}
}
/**
* @internal
*
* @return iterable<Interaction>
*/
public function getMethodInteractions(): iterable
{
foreach ($this->definition as $spec) {
if ($spec instanceof self) {
yield from $spec->getMethodInteractions();
}
}
foreach ($this->interactiveAttributes as $attribute) {
yield new Interaction($this, $attribute);
}
}
}

194
vendor/symfony/console/Attribute/Option.php vendored Executable file
View File

@@ -0,0 +1,194 @@
<?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\Attribute;
use Symfony\Component\Console\Attribute\Reflection\ReflectionMember;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\String\UnicodeString;
#[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::TARGET_PROPERTY)]
class Option
{
private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array'];
private const ALLOWED_UNION_TYPES = ['bool|string', 'bool|int', 'bool|float'];
private string|bool|int|float|array|null $default = null;
private array|\Closure $suggestedValues;
private ?int $mode = null;
/**
* @var string|class-string<\BackedEnum>
*/
private string $typeName = '';
private bool $allowNull = false;
private string $memberName = '';
private string $sourceName = '';
/**
* Represents a console command --option definition.
*
* If unset, the `name` value will be inferred from the parameter definition.
*
* @param array|string|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param array<string|Suggestion>|callable(CompletionInput):list<string|Suggestion> $suggestedValues The values used for input completion
*/
public function __construct(
public string $description = '',
public string $name = '',
public array|string|null $shortcut = null,
array|callable $suggestedValues = [],
) {
$this->suggestedValues = \is_callable($suggestedValues) ? $suggestedValues(...) : $suggestedValues;
}
/**
* @internal
*/
public static function tryFrom(\ReflectionParameter|\ReflectionProperty $member): ?self
{
$reflection = new ReflectionMember($member);
if (!$self = $reflection->getAttribute(self::class)) {
return null;
}
$self->memberName = $reflection->getMemberName();
$self->sourceName = $reflection->getSourceName();
$name = $reflection->getName();
$type = $reflection->getType();
if (!$reflection->hasDefaultValue()) {
throw new LogicException(\sprintf('The option %s "$%s" of "%s" must declare a default value.', $self->memberName, $name, $self->sourceName));
}
if (!$self->name) {
$self->name = (new UnicodeString($name))->kebab();
}
$self->default = $reflection->getDefaultValue();
$self->allowNull = $reflection->isNullable();
if ($type instanceof \ReflectionUnionType) {
return $self->handleUnion($type);
}
if (!$type instanceof \ReflectionNamedType) {
throw new LogicException(\sprintf('The %s "$%s" of "%s" must have a named type. Untyped or Intersection types are not supported for command options.', $self->memberName, $name, $self->sourceName));
}
$self->typeName = $type->getName();
$isBackedEnum = is_subclass_of($self->typeName, \BackedEnum::class);
if (!\in_array($self->typeName, self::ALLOWED_TYPES, true) && !$isBackedEnum) {
throw new LogicException(\sprintf('The type "%s" on %s "$%s" of "%s" is not supported as a command option. Only "%s" types and BackedEnum are allowed.', $self->typeName, $self->memberName, $name, $self->sourceName, implode('", "', self::ALLOWED_TYPES)));
}
if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) {
throw new LogicException(\sprintf('The option %s "$%s" of "%s" must not be nullable when it has a default boolean value.', $self->memberName, $name, $self->sourceName));
}
if ($self->allowNull && null !== $self->default) {
throw new LogicException(\sprintf('The option %s "$%s" of "%s" must either be not-nullable or have a default of null.', $self->memberName, $name, $self->sourceName));
}
if ('bool' === $self->typeName) {
$self->mode = InputOption::VALUE_NONE;
if (false !== $self->default) {
$self->mode |= InputOption::VALUE_NEGATABLE;
}
} elseif ('array' === $self->typeName) {
$self->mode = InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY;
} else {
$self->mode = InputOption::VALUE_REQUIRED;
}
if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $reflection->getSourceThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) {
$self->suggestedValues = [$instance, $self->suggestedValues[1]];
}
if ($isBackedEnum && !$self->suggestedValues) {
$self->suggestedValues = array_column($self->typeName::cases(), 'value');
}
return $self;
}
/**
* @internal
*/
public function toInputOption(): InputOption
{
$default = InputOption::VALUE_NONE === (InputOption::VALUE_NONE & $this->mode) ? null : $this->default;
$suggestedValues = \is_callable($this->suggestedValues) ? ($this->suggestedValues)(...) : $this->suggestedValues;
return new InputOption($this->name, $this->shortcut, $this->mode, $this->description, $default, $suggestedValues);
}
/**
* @internal
*/
public function resolveValue(InputInterface $input): mixed
{
$value = $input->getOption($this->name);
if (null === $value && \in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) {
return true;
}
if (is_subclass_of($this->typeName, \BackedEnum::class) && (\is_string($value) || \is_int($value))) {
return $this->typeName::tryFrom($value) ?? throw InvalidOptionException::fromEnumValue($this->name, $value, $this->suggestedValues);
}
if ('array' === $this->typeName && $this->allowNull && [] === $value) {
return null;
}
if ('bool' !== $this->typeName) {
return $value;
}
if ($this->allowNull && null === $value) {
return null;
}
return $value ?? $this->default;
}
private function handleUnion(\ReflectionUnionType $type): self
{
$types = array_map(
static fn (\ReflectionType $t) => $t instanceof \ReflectionNamedType ? $t->getName() : null,
$type->getTypes(),
);
sort($types);
$this->typeName = implode('|', array_filter($types));
if (!\in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) {
throw new LogicException(\sprintf('The union type for %s "$%s" of "%s" is not supported as a command option. Only "%s" types are allowed.', $this->memberName, $this->name, $this->sourceName, implode('", "', self::ALLOWED_UNION_TYPES)));
}
if (false !== $this->default) {
throw new LogicException(\sprintf('The option %s "$%s" of "%s" must have a default value of false.', $this->memberName, $this->name, $this->sourceName));
}
$this->mode = InputOption::VALUE_OPTIONAL;
return $this;
}
}

View File

@@ -0,0 +1,109 @@
<?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\Attribute\Reflection;
/**
* @internal
*/
class ReflectionMember
{
public function __construct(
private readonly \ReflectionParameter|\ReflectionProperty $member,
) {
}
/**
* @template T of object
*
* @param class-string<T> $class
*
* @return T|null
*/
public function getAttribute(string $class): ?object
{
return ($this->member->getAttributes($class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)?->newInstance();
}
public function getSourceName(): string
{
if ($this->member instanceof \ReflectionProperty) {
return $this->member->getDeclaringClass()->name;
}
$function = $this->member->getDeclaringFunction();
if ($function instanceof \ReflectionMethod) {
return $function->class.'::'.$function->name.'()';
}
return $function->name.'()';
}
public function getSourceThis(): ?object
{
if ($this->member instanceof \ReflectionParameter) {
return $this->member->getDeclaringFunction()->getClosureThis();
}
return null;
}
public function getType(): ?\ReflectionType
{
return $this->member->getType();
}
public function getName(): string
{
return $this->member->getName();
}
public function hasDefaultValue(): bool
{
if ($this->member instanceof \ReflectionParameter) {
return $this->member->isDefaultValueAvailable();
}
return $this->member->hasDefaultValue();
}
public function getDefaultValue(): mixed
{
$defaultValue = $this->member->getDefaultValue();
if ($defaultValue instanceof \BackedEnum) {
return $defaultValue->value;
}
return $defaultValue;
}
public function isNullable(): bool
{
return (bool) $this->member->getType()?->allowsNull();
}
public function getMemberName(): string
{
return $this->member instanceof \ReflectionParameter ? 'parameter' : 'property';
}
public function isParameter(): bool
{
return $this->member instanceof \ReflectionParameter;
}
public function isProperty(): bool
{
return $this->member instanceof \ReflectionProperty;
}
}