diff --git a/src/Aggregate/ArrayElementBuilder.php b/src/Aggregate/ArrayElementBuilder.php
index 23ce742..23cd23a 100644
--- a/src/Aggregate/ArrayElementBuilder.php
+++ b/src/Aggregate/ArrayElementBuilder.php
@@ -103,7 +103,7 @@ public function satisfy(Constraint|callable $constraint, ?string $message = null
* Define a transformer on the inner element
*/
#[Override]
- public function transformer(callable|TransformerInterface $transformer, bool $append = true): static
+ public function transformer(callable|TransformerInterface|string $transformer, bool $append = true): static
{
$this->getElementBuilder()->transformer($transformer, $append);
@@ -440,14 +440,18 @@ final public function required(string|Constraint|null $message = null, ?bool $al
*/
final public function choices(ChoiceInterface|array|string|callable $choices, ?string $message = null, ?bool $multiple = null, ?bool $strict = null, ?int $min = null, ?int $max = null, ?string $minMessage = null, ?string $maxMessage = null): static
{
- /** @psalm-suppress MissingConstructor */
- $builder = new class {
+ /** @psalm-suppress PropertyNotSetInConstructor */
+ $builder = new class($this->registry) {
use ChoiceBuilderTrait {
getChoices as public;
}
public ChoiceConstraint $constraint;
+ public function __construct(
+ private readonly RegistryInterface $registry,
+ ) {}
+
#[Override]
public function satisfy(Constraint|callable $constraint, ?string $message = null, bool $append = true): static
{
@@ -455,6 +459,12 @@ public function satisfy(Constraint|callable $constraint, ?string $message = null
$this->constraint = $constraint;
return $this;
}
+
+ #[Override]
+ protected function registry(): RegistryInterface
+ {
+ return $this->registry;
+ }
};
// Force the multiple option to true
diff --git a/src/Attribute/Aggregate/ArrayTransformer.php b/src/Attribute/Aggregate/ArrayTransformer.php
index 7dfef5f..87fa3cb 100644
--- a/src/Attribute/Aggregate/ArrayTransformer.php
+++ b/src/Attribute/Aggregate/ArrayTransformer.php
@@ -20,8 +20,12 @@
*
* class MyForm extends AttributeForm
* {
- * #[ArrayTransformer(MyTransformer::class, ['foo', 'bar']), ElementType(IntegerElement::class)]
+ * #[ArrayTransformer(new MyTransformer(['foo', 'bar']), ElementType(IntegerElement::class)]
* private ArrayElement $foo;
+ *
+ * // Load the transformer from the registry/container
+ * #[ArrayTransformer(TransformerService::class)]
+ * private ArrayElement $bar;
* }
*
*
@@ -35,11 +39,11 @@
class ArrayTransformer extends Transformer
{
/**
- * @param class-string $transformerClass The transformer class name
+ * @param class-string|TransformerInterface $transformer The transformer class name or instance
* @param array $constructorArguments Arguments to provide on the transformer constructor
*/
- public function __construct(string $transformerClass, array $constructorArguments = [])
+ public function __construct(string|TransformerInterface $transformer, array $constructorArguments = [])
{
- parent::__construct($transformerClass, $constructorArguments, true);
+ parent::__construct($transformer, $constructorArguments, true);
}
}
diff --git a/src/Attribute/Constraint/Satisfy.php b/src/Attribute/Constraint/Satisfy.php
index c8cbef1..46a4037 100644
--- a/src/Attribute/Constraint/Satisfy.php
+++ b/src/Attribute/Constraint/Satisfy.php
@@ -21,18 +21,14 @@
*
* This attribute is equivalent to call :
*
- * $builder->integer('foo')->satisfy(MyConstraint::class, $options);
+ * $builder->integer('foo')->satisfy(new MyConstraint(foo: 'bar'));
*
*
* Usage:
*
* class MyForm extends AttributeForm
* {
- * #[Satisfy(MyConstraint::class, ['foo' => 'bar'])]
- * private IntegerElement $foo;
- *
- * // or on PHP 8.1
- * #[Satisfy(new MyConstraint(['foo' => 'bar']))]
+ * #[Satisfy(new MyConstraint(foo: 'bar'))]
* private IntegerElement $foo;
* }
*
diff --git a/src/Attribute/Element/Choices.php b/src/Attribute/Element/Choices.php
index 21fbee4..f46ac07 100644
--- a/src/Attribute/Element/Choices.php
+++ b/src/Attribute/Element/Choices.php
@@ -29,6 +29,7 @@
* - a simple array of values (without labels)
* - an associative array for provide a label (in key), and inner value (in value)
* - a method name for resolving choices in lazy way
+ * - a choice class name to load from the registry/container
*
* Note: this attribute is not repeatable
*
@@ -50,6 +51,9 @@
* #[Choices('loadBazValues', 'Invalid value')]
* private StringElement $baz;
*
+ * #[Choices(MyChoices::class)]
+ * private StringElement $oof;
+ *
* // For dynamic choices, or with complex logic
* public function loadBazValues(): array
* {
@@ -86,7 +90,7 @@ public function __construct(
* If the value is an array, the key will be used as label (displayed value), and the value as real value
* The label is not required.
*
- * @var literal-string|array
+ * @var literal-string|class-string|array
* @readonly
*/
private string|array $choices,
@@ -118,7 +122,7 @@ public function applyOnChildBuilder(object|string $context, ChildBuilderInterfac
$choices = $this->choices;
- if (is_string($choices)) {
+ if (is_string($choices) && !class_exists($choices)) {
$choices = is_object($context)
? new LazyChoice($context->{$this->choices}(...))
: new LazyChoice($context::{$this->choices}(...))
@@ -139,7 +143,7 @@ public function generateCodeForChildBuilder(string $name, AttributesProcessorGen
$options['message'] = $this->message;
}
- if (is_string($this->choices)) {
+ if (is_string($this->choices) && !class_exists($this->choices)) {
$generator->use(LazyChoice::class);
if (is_object($context)) {
diff --git a/src/Attribute/Element/Transformer.php b/src/Attribute/Element/Transformer.php
index 9ecb12b..e4018f5 100644
--- a/src/Attribute/Element/Transformer.php
+++ b/src/Attribute/Element/Transformer.php
@@ -8,13 +8,19 @@
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\ChildBuilderAttributeInterface;
use Bdf\Form\Attribute\Processor\CodeGenerator\AttributesProcessorGenerator;
+use Bdf\Form\Attribute\Processor\CodeGenerator\ObjectInstantiation;
use Bdf\Form\Attribute\Processor\GenerateConfiguratorStrategy;
use Bdf\Form\Child\ChildBuilderInterface;
use Bdf\Form\ElementBuilderInterface;
use Bdf\Form\Transformer\TransformerInterface;
+use InvalidArgumentException;
use Nette\PhpGenerator\Literal;
use Override;
+use function is_object;
+use function is_string;
+use function trigger_error;
+
/**
* Add a transformer on the element, using a transformer class
*
@@ -29,8 +35,12 @@
*
* class MyForm extends AttributeForm
* {
- * #[Transformer(MyTransformer::class, ['foo', 'bar'])]
+ * #[Transformer(new MyTransformer('foo', 'bar'))]
* private IntegerElement $foo;
+ *
+ * // Use a transformer loaded from the registry/container
+ * #[Transformer(TransformerService::class)]
+ * private IntegerElement $bar;
* }
*
*
@@ -45,19 +55,30 @@
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
class Transformer implements ChildBuilderAttributeInterface
{
+ /**
+ * The transformer class name or instance
+ *
+ * @var class-string|TransformerInterface
+ */
+ private readonly string|TransformerInterface $transformer;
+
+ /**
+ * @param class-string|TransformerInterface|null $transformer
+ * @param class-string|TransformerInterface|null $transformerClass
+ */
public function __construct(
/**
- * The transformer class name
+ * The transformer class name or instance
*
- * @var class-string
- * @readonly
+ * @var class-string|TransformerInterface|null
*/
- private readonly string $transformerClass,
+ string|TransformerInterface|null $transformer = null,
/**
- * Arguments to provide on the transformer constructor
+ * Arguments to provide on the transformer constructor.
+ * Only used if first parameter is a class name, and the transformer is instantiable
*
* @var array
- * @readonly
+ * @deprecated Use object parameter instead
*/
private readonly array $constructorArguments = [],
/**
@@ -74,12 +95,47 @@ public function __construct(
* @see ArrayTransformer Prefer use this attribute for array element, instead of manually set this flag
*/
private readonly bool $array = false,
- ) {}
+ /**
+ * @var class-string|TransformerInterface
+ * @deprecated For compatiblity only. Use first parameter instead.
+ */
+ string|TransformerInterface|null $transformerClass = null,
+ ) {
+ $transformer ??= $transformerClass;
+
+ if ($transformerClass !== null) {
+ @trigger_error('The transformerClass parameter is deprecated since 2.0, use transformer parameter instead', E_USER_DEPRECATED);
+ }
+
+ if ($transformer === null) {
+ throw new InvalidArgumentException('The transformer parameter must not be null.');
+ }
+
+ $this->transformer = $transformer;
+
+ if ($this->constructorArguments) {
+ if (!is_string($this->transformer)) {
+ throw new \InvalidArgumentException('Constructor arguments can be used only with transformer class name');
+ }
+
+ @trigger_error('The constructorArguments parameter is deprecated since 2.0, use object parameter instead', E_USER_DEPRECATED);
+ }
+ }
#[Override]
public function applyOnChildBuilder(object|string $context, ChildBuilderInterface $builder): void
{
- $transformer = new $this->transformerClass(...$this->constructorArguments);
+ $transformer = $this->transformer;
+
+ if (is_string($transformer)) {
+ $shouldBeInstantiated = !empty($this->constructorArguments) || self::canBeInstantiatedWithoutParameters($transformer);
+
+ if ($shouldBeInstantiated) {
+ @trigger_error('Passing a transformer class instead of object for inline instantiation is deprecated since 2.0, will use the registry in 3.0. Use object instead.', E_USER_DEPRECATED);
+
+ $transformer = new $transformer(...$this->constructorArguments);
+ }
+ }
if ($this->array) {
/** @var ChildBuilderInterface $builder */
@@ -92,9 +148,45 @@ public function applyOnChildBuilder(object|string $context, ChildBuilderInterfac
#[Override]
public function generateCodeForChildBuilder(string $name, AttributesProcessorGenerator $generator, object|string $context): void
{
- $transformer = $generator->useAndSimplifyType($this->transformerClass);
- $code = $this->array ? '$?->arrayTransformer(new ?(...?));' : '$?->transformer(new ?(...?));';
+ $transformer = $this->transformer;
+
+ if (is_object($transformer)) {
+ $transformer = ObjectInstantiation::promotedProperties($transformer)->render($generator);
+ $code = $this->array ? '$?->arrayTransformer(?);' : '$?->transformer(?);';
+ $generator->line($code, [$name, $transformer]);
+ return;
+ }
+
+ // Transformer is the class name
+ $shouldBeInstantiated = !empty($this->constructorArguments) || self::canBeInstantiatedWithoutParameters($transformer);
+ $transformer = $generator->useAndSimplifyType($transformer);
+
+ if ($shouldBeInstantiated) {
+ @trigger_error('Passing a transformer class instead of object for inline instantiation is deprecated since 2.0, will use the registry in 3.0. Use object instead.', E_USER_DEPRECATED);
+
+ $code = $this->array ? '$?->arrayTransformer(new ?(...?));' : '$?->transformer(new ?(...?));';
+ $generator->line($code, [$name, new Literal($transformer), $this->constructorArguments]);
+ return;
+ }
+
+ $code = $this->array ? '$?->arrayTransformer(?::class);' : '$?->transformer(?::class);';
+ $generator->line($code, [$name, new Literal($transformer)]);
+ }
+
+ /**
+ * @param class-string $class
+ * @return bool
+ */
+ private static function canBeInstantiatedWithoutParameters(string $class): bool
+ {
+ $r = new \ReflectionClass($class);
+
+ if (!$r->isInstantiable()) {
+ return false;
+ }
+
+ $constructor = $r->getConstructor();
- $generator->line($code, [$name, new Literal($transformer), $this->constructorArguments]);
+ return $constructor === null || $constructor->getNumberOfRequiredParameters() === 0;
}
}
diff --git a/src/Child/ChildBuilder.php b/src/Child/ChildBuilder.php
index c378109..d462bcb 100644
--- a/src/Child/ChildBuilder.php
+++ b/src/Child/ChildBuilder.php
@@ -433,13 +433,13 @@ final public function configure(callable $configurator): static
/**
* Forward call to element builder
*
- * @param callable|TransformerInterface $transformer
+ * @param callable|TransformerInterface|class-string $transformer
* @param bool $append
* @return $this
*
* @see ElementBuilderInterface::transformer()
*/
- public function transformer(callable|TransformerInterface $transformer, bool $append = true): static
+ public function transformer(callable|TransformerInterface|string $transformer, bool $append = true): static
{
$this->elementBuilder->transformer($transformer, $append);
diff --git a/src/Child/ChildBuilderInterface.php b/src/Child/ChildBuilderInterface.php
index 746c503..8e1cb94 100644
--- a/src/Child/ChildBuilderInterface.php
+++ b/src/Child/ChildBuilderInterface.php
@@ -160,14 +160,14 @@ public function depends(string ...$inputNames): static;
* });
*
*
- * @param callable|TransformerInterface $transformer The transformer
+ * @param callable|TransformerInterface|class-string $transformer The transformer
* @param bool $append Append the transformer. Prepend if false
*
* @return $this
*
* @see TransformerInterface
*/
- public function modelTransformer(callable|TransformerInterface $transformer, bool $append = true): static;
+ public function modelTransformer(callable|TransformerInterface|string $transformer, bool $append = true): static;
/**
* Creates the child instance
diff --git a/src/Choice/ChoiceBuilderTrait.php b/src/Choice/ChoiceBuilderTrait.php
index 65ec263..a6df4c6 100644
--- a/src/Choice/ChoiceBuilderTrait.php
+++ b/src/Choice/ChoiceBuilderTrait.php
@@ -3,7 +3,9 @@
namespace Bdf\Form\Choice;
use BackedEnum;
+use Bdf\Form\AbstractElementBuilder;
use Bdf\Form\ElementBuilderInterface;
+use Bdf\Form\Registry\RegistryInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Choice as ChoiceConstraint;
@@ -45,11 +47,14 @@ trait ChoiceBuilderTrait
* // Using enum
* $builder->choices(MyEnum::class);
*
+ * // Using choice from service (e.g. if a container is used, the choice will be loaded from the container)
+ * $builder->choices(MyChoice::class);
+ *
* $builder->choices(['foo', 'bar'], 'my error'); // With message
* $builder->choices(['foo', 'bar'], min: 2, max: 6); // With custom options
*
*
- * @param ChoiceInterface|array|class-string|callable $choices The allowed values in PHP form.
+ * @param ChoiceInterface|array|class-string|callable $choices The allowed values in PHP form.
* @param string|null $message The error message.
* @param non-negative-int|null $min
* @param positive-int|null $max
@@ -63,6 +68,7 @@ final public function choices(ChoiceInterface|array|string|callable $choices, ?s
$choices = match (true) {
is_array($choices) => new ArrayChoice($choices),
is_string($choices) && is_subclass_of($choices, BackedEnum::class) => new EnumChoice($choices),
+ is_string($choices) && is_subclass_of($choices, ChoiceInterface::class) => new LazyChoice(fn () => $this->registry()->service($choices)),
is_callable($choices) => new LazyChoice($choices),
};
}
@@ -102,4 +108,11 @@ final protected function getChoices(): ?ChoiceInterface
* @see ElementBuilderInterface::satisfy()
*/
abstract public function satisfy(Constraint|callable $constraint, ?string $message = null, bool $append = true): static;
+
+ /**
+ * {@inheritdoc}
+ *
+ * @see AbstractElementBuilder::registry()
+ */
+ abstract protected function registry(): RegistryInterface;
}
diff --git a/src/Csrf/CsrfElementBuilder.php b/src/Csrf/CsrfElementBuilder.php
index 1285127..d5c00d2 100644
--- a/src/Csrf/CsrfElementBuilder.php
+++ b/src/Csrf/CsrfElementBuilder.php
@@ -136,7 +136,7 @@ public function satisfy(Constraint|callable $constraint, ?string $message = null
}
#[Override]
- public function transformer(callable|TransformerInterface $transformer, bool $append = true): static
+ public function transformer(callable|TransformerInterface|string $transformer, bool $append = true): static
{
throw new BadMethodCallException();
}
diff --git a/src/ElementBuilderInterface.php b/src/ElementBuilderInterface.php
index de3cd55..f425ab6 100644
--- a/src/ElementBuilderInterface.php
+++ b/src/ElementBuilderInterface.php
@@ -88,16 +88,18 @@ public function satisfy(Constraint|callable $constraint, ?string $message = null
* return $value->export();
* }
* });
+ *
+ * $builder->transformer(TransformerService::class); // Add a transformer, loaded from the registry/container
*
*
- * @param callable|TransformerInterface $transformer The transformer.
+ * @param callable|TransformerInterface|class-string $transformer The transformer.
* @param bool $append Append the transformer. Prepend if false
*
* @return $this
*
* @see TransformerInterface
*/
- public function transformer(callable|TransformerInterface $transformer, bool $append = true): static;
+ public function transformer(callable|TransformerInterface|string $transformer, bool $append = true): static;
/**
* Define the initial value of the element
diff --git a/src/Filter/ClosureFilter.php b/src/Filter/ClosureFilter.php
index 362263c..3a17b9b 100755
--- a/src/Filter/ClosureFilter.php
+++ b/src/Filter/ClosureFilter.php
@@ -2,6 +2,7 @@
namespace Bdf\Form\Filter;
+use Attribute;
use Bdf\Form\Child\ChildBuilderInterface;
use Bdf\Form\Child\ChildInterface;
use Override;
@@ -18,6 +19,7 @@
*
* @see ChildBuilderInterface::filter()
*/
+#[Attribute(Attribute::TARGET_PROPERTY)]
final readonly class ClosureFilter implements FilterInterface
{
/**
diff --git a/src/Registry/AbstractRegistry.php b/src/Registry/AbstractRegistry.php
new file mode 100755
index 0000000..94c01fc
--- /dev/null
+++ b/src/Registry/AbstractRegistry.php
@@ -0,0 +1,172 @@
+[]|callable[]
+ */
+ private array $elementBuilderFactories = [
+ StringElement::class => StringElementBuilder::class,
+ IntegerElement::class => IntegerElementBuilder::class,
+ FloatElement::class => FloatElementBuilder::class,
+ BooleanElement::class => BooleanElementBuilder::class,
+ AnyElement::class => AnyElementBuilder::class,
+ BackedEnumElement::class => EnumElementBuilder::class,
+ UnitEnumElement::class => EnumElementBuilder::class,
+
+ EmailElement::class => EmailElementBuilder::class,
+ UrlElement::class => UrlElementBuilder::class,
+
+ CsrfElement::class => CsrfElementBuilder::class,
+ PhoneElement::class => PhoneElementBuilder::class,
+
+ DateTimeElement::class => DateTimeElementBuilder::class,
+
+ ArrayElement::class => ArrayElementBuilder::class,
+ Form::class => FormBuilder::class,
+ ];
+
+ /**
+ * @var class-string[]|callable[]
+ */
+ private array $childBuilderFactories = [
+ DateTimeElement::class => DateTimeChildBuilder::class,
+ PhoneElement::class => PhoneChildBuilder::class,
+ ArrayElement::class => ArrayChildBuilder::class,
+ ];
+
+ #[Override]
+ public function childBuilder(string $element, string $name): ChildBuilderInterface
+ {
+ $elementBuilder = $this->elementBuilder($element);
+
+ $builderFactory = $this->childBuilderFactories[$element] ?? ChildBuilder::class;
+
+ if (is_string($builderFactory)) {
+ /** @var class-string $builderFactory */
+ return new $builderFactory($name, $elementBuilder, $this);
+ }
+
+ return $builderFactory($name, $elementBuilder, $this);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @psalm-param class-string $element
+ * @psalm-template E as \Bdf\Form\ElementInterface
+ * @psalm-return ElementBuilderInterface
+ */
+ #[Override]
+ public function elementBuilder(string $element): ElementBuilderInterface
+ {
+ $builderFactory = null;
+
+ if (isset($this->elementBuilderFactories[$element])) {
+ $builderFactory = $this->elementBuilderFactories[$element];
+ } else {
+ foreach ($this->elementBuilderFactories as $builderElement => $factory) {
+ if (is_subclass_of($element, $builderElement, true)) {
+ $builderFactory = $factory;
+ break;
+ }
+ }
+ }
+
+ if ($builderFactory === null) {
+ throw new InvalidArgumentException('The element '.$element.' is not registered');
+ }
+
+ if (is_string($builderFactory)) {
+ /** @var class-string> $builderFactory */
+ return new $builderFactory($this, $element);
+ }
+
+ return ($builderFactory)($this, $element);
+ }
+
+ #[Override]
+ public function buttonBuilder(string $name): ButtonBuilderInterface
+ {
+ return new SubmitButtonBuilder($name);
+ }
+
+ /**
+ * Register a new element builder
+ *
+ *
+ * // Register MyCustomBuilder as builder for MyCustomElement
+ * $registry->register(MyCustomElement::class, MyCustomBuilder::class);
+ *
+ * // Register a factory builder. The factory takes as parameters the registry, and the element class name
+ * $registry->register(MyCustomElement::class, function (Registry $registry, string $element) {
+ * return new MyCustomBuilder($registry);
+ * });
+ *
+ * // Register with a custom child builder
+ * $registry->register(MyCustomElement::class, MyCustomBuilder::class, function (string $name, ElementBuilderInterface $builder, Registry $registry) {
+ * return new MyCustomChildBuilder($registry, new ChildBuilder($name, $builder, $registry));
+ * });
+ *
+ *
+ * @param string $elementType The element class name
+ * @param class-string|callable $builderFactory The builder factory, or builder class name
+ * @param class-string|callable|null $childBuilderFactory The builder factory for child, or builder class name. If null, use default child builder
+ *
+ * @see Registry::elementBuilder()
+ */
+ public function register(string $elementType, string|callable $builderFactory, string|callable|null $childBuilderFactory = null): void
+ {
+ $this->elementBuilderFactories[$elementType] = $builderFactory;
+
+ if ($childBuilderFactory !== null) {
+ $this->childBuilderFactories[$elementType] = $childBuilderFactory;
+ }
+ }
+}
diff --git a/src/Registry/Registry.php b/src/Registry/Registry.php
index cacef9c..73d737b 100755
--- a/src/Registry/Registry.php
+++ b/src/Registry/Registry.php
@@ -2,88 +2,26 @@
namespace Bdf\Form\Registry;
-use Bdf\Form\Aggregate\ArrayChildBuilder;
-use Bdf\Form\Aggregate\ArrayElement;
-use Bdf\Form\Aggregate\ArrayElementBuilder;
use Bdf\Form\Aggregate\Form;
-use Bdf\Form\Aggregate\FormBuilder;
-use Bdf\Form\Button\ButtonBuilderInterface;
-use Bdf\Form\Button\SubmitButtonBuilder;
-use Bdf\Form\Child\ChildBuilder;
-use Bdf\Form\Child\ChildBuilderInterface;
-use Bdf\Form\Csrf\CsrfElement;
-use Bdf\Form\Csrf\CsrfElementBuilder;
use Bdf\Form\Custom\CustomForm;
use Bdf\Form\Custom\CustomFormBuilder;
-use Bdf\Form\ElementBuilderInterface;
-use Bdf\Form\Leaf\AnyElement;
-use Bdf\Form\Leaf\AnyElementBuilder;
-use Bdf\Form\Leaf\BackedEnumElement;
-use Bdf\Form\Leaf\BooleanElement;
-use Bdf\Form\Leaf\BooleanElementBuilder;
-use Bdf\Form\Leaf\Date\DateTimeChildBuilder;
-use Bdf\Form\Leaf\Date\DateTimeElement;
-use Bdf\Form\Leaf\Date\DateTimeElementBuilder;
-use Bdf\Form\Leaf\EnumElementBuilder;
-use Bdf\Form\Leaf\FloatElement;
-use Bdf\Form\Leaf\FloatElementBuilder;
-use Bdf\Form\Leaf\Helper\EmailElement;
-use Bdf\Form\Leaf\Helper\EmailElementBuilder;
-use Bdf\Form\Leaf\Helper\UrlElement;
-use Bdf\Form\Leaf\Helper\UrlElementBuilder;
-use Bdf\Form\Leaf\IntegerElement;
-use Bdf\Form\Leaf\IntegerElementBuilder;
-use Bdf\Form\Leaf\StringElement;
-use Bdf\Form\Leaf\StringElementBuilder;
-use Bdf\Form\Leaf\UnitEnumElement;
-use Bdf\Form\Phone\PhoneChildBuilder;
-use Bdf\Form\Phone\PhoneElement;
-use Bdf\Form\Phone\PhoneElementBuilder;
use Bdf\Form\Struct\StructForm;
use Bdf\Form\Struct\StructFormBuilder;
use InvalidArgumentException;
use Override;
-use function is_string;
-use function is_subclass_of;
+use function sprintf;
/**
* Base registry interface
*/
-final class Registry implements RegistryInterface
+final class Registry extends AbstractRegistry
{
/**
- * @var class-string[]|callable[]
+ * @var array
+ * @psalm-var class-string-map
*/
- private array $elementBuilderFactories = [
- StringElement::class => StringElementBuilder::class,
- IntegerElement::class => IntegerElementBuilder::class,
- FloatElement::class => FloatElementBuilder::class,
- BooleanElement::class => BooleanElementBuilder::class,
- AnyElement::class => AnyElementBuilder::class,
- BackedEnumElement::class => EnumElementBuilder::class,
- UnitEnumElement::class => EnumElementBuilder::class,
-
- EmailElement::class => EmailElementBuilder::class,
- UrlElement::class => UrlElementBuilder::class,
-
- CsrfElement::class => CsrfElementBuilder::class,
- PhoneElement::class => PhoneElementBuilder::class,
-
- DateTimeElement::class => DateTimeElementBuilder::class,
-
- ArrayElement::class => ArrayElementBuilder::class,
- Form::class => FormBuilder::class,
- ];
-
- /**
- * @var class-string[]|callable[]
- */
- private array $childBuilderFactories = [
- DateTimeElement::class => DateTimeChildBuilder::class,
- PhoneElement::class => PhoneChildBuilder::class,
- ArrayElement::class => ArrayChildBuilder::class,
- ];
+ private array $services = [];
/**
* Registry constructor.
@@ -100,91 +38,28 @@ public function __construct()
}
#[Override]
- public function childBuilder(string $element, string $name): ChildBuilderInterface
+ public function service(string $class): object
{
- $elementBuilder = $this->elementBuilder($element);
-
- $builderFactory = $this->childBuilderFactories[$element] ?? ChildBuilder::class;
-
- if (is_string($builderFactory)) {
- /** @var class-string $builderFactory */
- return new $builderFactory($name, $elementBuilder, $this);
+ if (!isset($this->services[$class])) {
+ throw new InvalidArgumentException(sprintf('Service "%s" is not registered.', $class));
}
- return $builderFactory($name, $elementBuilder, $this);
+ return $this->services[$class];
}
/**
- * {@inheritdoc}
+ * Register a new service, associated to the given type
*
- * @psalm-param class-string $element
- * @psalm-template E as \Bdf\Form\ElementInterface
- * @psalm-return ElementBuilderInterface
- */
- #[Override]
- public function elementBuilder(string $element): ElementBuilderInterface
- {
- $builderFactory = null;
-
- if (isset($this->elementBuilderFactories[$element])) {
- $builderFactory = $this->elementBuilderFactories[$element];
- } else {
- foreach ($this->elementBuilderFactories as $builderElement => $factory) {
- if (is_subclass_of($element, $builderElement, true)) {
- $builderFactory = $factory;
- break;
- }
- }
- }
-
- if ($builderFactory === null) {
- throw new InvalidArgumentException('The element '.$element.' is not registered');
- }
-
- if (is_string($builderFactory)) {
- /** @var class-string> $builderFactory */
- return new $builderFactory($this, $element);
- }
-
- return ($builderFactory)($this, $element);
- }
-
- #[Override]
- public function buttonBuilder(string $name): ButtonBuilderInterface
- {
- return new SubmitButtonBuilder($name);
- }
-
- /**
- * Register a new element builder
- *
- *
- * // Register MyCustomBuilder as builder for MyCustomElement
- * $registry->register(MyCustomElement::class, MyCustomBuilder::class);
- *
- * // Register a factory builder. The factory takes as parameters the registry, and the element class name
- * $registry->register(MyCustomElement::class, function (Registry $registry, string $element) {
- * return new MyCustomBuilder($registry);
- * });
+ * @param T $service
+ * @param class-string|null $class The registered class name. If not set, the serice class name will be used
*
- * // Register with a custom child builder
- * $registry->register(MyCustomElement::class, MyCustomBuilder::class, function (string $name, ElementBuilderInterface $builder, Registry $registry) {
- * return new MyCustomChildBuilder($registry, new ChildBuilder($name, $builder, $registry));
- * });
- *
- *
- * @param string $elementType The element class name
- * @param class-string|callable $builderFactory The builder factory, or builder class name
- * @param class-string|callable|null $childBuilderFactory The builder factory for child, or builder class name. If null, use default child builder
- *
- * @see Registry::elementBuilder()
+ * @return void
+ * @template T as object
*/
- public function register(string $elementType, string|callable $builderFactory, string|callable|null $childBuilderFactory = null): void
+ public function registerService(object $service, ?string $class = null): void
{
- $this->elementBuilderFactories[$elementType] = $builderFactory;
+ $class ??= $service::class;
- if ($childBuilderFactory !== null) {
- $this->childBuilderFactories[$elementType] = $childBuilderFactory;
- }
+ $this->services[$class] = $service;
}
}
diff --git a/src/Registry/RegistryInterface.php b/src/Registry/RegistryInterface.php
index ab46583..1af2f50 100644
--- a/src/Registry/RegistryInterface.php
+++ b/src/Registry/RegistryInterface.php
@@ -60,4 +60,14 @@ public function elementBuilder(string $element): ElementBuilderInterface;
* @return ButtonBuilderInterface
*/
public function buttonBuilder(string $name): ButtonBuilderInterface;
+
+ /**
+ * Get a service instance
+ *
+ * @param class-string $class
+ *
+ * @return T
+ * @template T as object
+ */
+ public function service(string $class): object;
}
diff --git a/src/Util/DelegateElementBuilderTrait.php b/src/Util/DelegateElementBuilderTrait.php
index d05b0d3..31f233c 100644
--- a/src/Util/DelegateElementBuilderTrait.php
+++ b/src/Util/DelegateElementBuilderTrait.php
@@ -27,8 +27,10 @@ final public function satisfy(Constraint|callable $constraint, ?string $message
/**
* {@inheritdoc}
+ *
+ * @param callable|TransformerInterface|class-string $transformer
*/
- final public function transformer(callable|TransformerInterface $transformer, bool $append = true): static
+ final public function transformer(callable|TransformerInterface|string $transformer, bool $append = true): static
{
$this->getElementBuilder()->transformer($transformer, $append);
diff --git a/src/Util/TransformerBuilderTrait.php b/src/Util/TransformerBuilderTrait.php
index 8e3f9a1..362c3a4 100644
--- a/src/Util/TransformerBuilderTrait.php
+++ b/src/Util/TransformerBuilderTrait.php
@@ -12,6 +12,7 @@
use function array_unshift;
use function count;
use function is_callable;
+use function is_string;
/**
* Trait for implements builder of transformer
@@ -31,12 +32,15 @@ trait TransformerBuilderTrait
/**
* {@inheritdoc}
*
+ * @param callable|TransformerInterface|class-string $transformer
* @see ElementBuilderInterface::transformer()
*/
- final public function transformer(callable|TransformerInterface $transformer, bool $append = true): static
+ final public function transformer(callable|TransformerInterface|string $transformer, bool $append = true): static
{
if (is_callable($transformer)) {
$transformer = new ClosureTransformer($transformer);
+ } elseif (is_string($transformer)) {
+ $transformer = $this->registry()->service($transformer);
}
if ($append === true) {
diff --git a/tests/Aggregate/ArrayElementBuilderTest.php b/tests/Aggregate/ArrayElementBuilderTest.php
index 2a1bdfa..c1a71ba 100644
--- a/tests/Aggregate/ArrayElementBuilderTest.php
+++ b/tests/Aggregate/ArrayElementBuilderTest.php
@@ -3,6 +3,8 @@
namespace Bdf\Form\Aggregate;
use Bdf\Form\Choice\EnumChoice;
+use Bdf\Form\ElementInterface;
+use Bdf\Form\Transformer\TransformerInterface;
use Bdf\Form\Choice\LazyChoice;
use Bdf\Form\Leaf\AnyElementBuilder;
use Bdf\Form\Leaf\BooleanElementBuilder;
@@ -10,12 +12,16 @@
use Bdf\Form\Leaf\EnumElementBuilder;
use Bdf\Form\Leaf\FloatElementBuilder;
use Bdf\Form\Leaf\IntegerElementBuilder;
+use Bdf\Form\Leaf\MyServiceChoice;
use Bdf\Form\Leaf\MyStringEnum;
+use Bdf\Form\Leaf\MyTransformerService;
use Bdf\Form\Leaf\StringElement;
use Bdf\Form\Leaf\StringElementBuilder;
use Bdf\Form\Phone\PhoneElementBuilder;
+use Bdf\Form\Registry\Registry;
use Bdf\Form\Struct\Fixtures\SimpleDto;
use Bdf\Form\Struct\StructFormBuilder;
+use InvalidArgumentException;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
@@ -260,6 +266,31 @@ public function test_arrayTransformer()
$this->assertSame(['bar' => 'foo'], $element->submit(['foo' => 'bar'])->value());
}
+ /**
+ *
+ */
+ public function test_arrayTransformer_from_service()
+ {
+ $registry = new Registry();
+ $registry->registerService(new MyArrayTransformerService());
+
+ $builder = new ArrayElementBuilder($registry);
+ $element = $builder->arrayTransformer(MyArrayTransformerService::class)->buildElement();
+
+ $this->assertSame(['bar' => 'foo'], $element->submit(['foo' => 'bar'])->value());
+ }
+
+ /**
+ *
+ */
+ public function test_arrayTransformer_from_service_not_registered()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Service "'.MyArrayTransformerService::class.'" is not registered.');
+
+ $this->builder->arrayTransformer(MyArrayTransformerService::class);
+ }
+
/**
*
*/
@@ -324,6 +355,31 @@ public function test_transformer_order()
$this->assertSame(['BAC'], $element->submit([''])->value());
}
+ /**
+ *
+ */
+ public function test_transformer_from_service()
+ {
+ $registry = new Registry();
+ $registry->registerService(new MyTransformerService());
+
+ $builder = new ArrayElementBuilder($registry);
+ $element = $builder->transformer(MyTransformerService::class)->buildElement();
+
+ $this->assertSame(['foo>', 'bar>'], $element->submit(['foo', 'bar'])->value());
+ }
+
+ /**
+ *
+ */
+ public function test_transformer_from_service_not_registered()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Service "'.MyTransformerService::class.'" is not registered.');
+
+ $this->builder->transformer(MyTransformerService::class);
+ }
+
/**
*
*/
@@ -393,4 +449,51 @@ public function test_choices()
$this->assertTrue($element->submit(['foo', 'bar'])->valid());
}
+
+ /**
+ *
+ */
+ public function test_choices_from_service()
+ {
+ $choice = new MyServiceChoice();
+
+ $registry = new Registry();
+ $registry->registerService($choice);
+
+ $builder = new ArrayElementBuilder($registry);
+ $element = $builder->choices(MyServiceChoice::class)->buildElement();
+
+ $this->assertInstanceOf(LazyChoice::class, $element->choices());
+ $this->assertSame(['foo', 'bar'], $element->choices()->values());
+
+ $this->assertFalse($element->submit(['foo', 'aaa'])->valid());
+ $this->assertEquals('One or more of the given values is invalid.', $element->error()->global());
+
+ $this->assertTrue($element->submit(['foo', 'bar'])->valid());
+ }
+
+ /**
+ *
+ */
+ public function test_choices_from_service_not_registered()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Service "'.MyServiceChoice::class.'" is not registered.');
+
+ $element = $this->builder->choices(MyServiceChoice::class)->buildElement();
+ $element->submit(['foo', 'bar']);
+ }
+}
+
+class MyArrayTransformerService implements TransformerInterface
+{
+ public function transformToHttp(mixed $value, ElementInterface $input): mixed
+ {
+ return array_flip($value);
+ }
+
+ public function transformFromHttp(mixed $value, ElementInterface $input): mixed
+ {
+ return array_flip($value);
+ }
}
diff --git a/tests/Attribute/Aggregate/ArrayTransformerTest.php b/tests/Attribute/Aggregate/ArrayTransformerTest.php
index a5d8f15..0c3da0c 100644
--- a/tests/Attribute/Aggregate/ArrayTransformerTest.php
+++ b/tests/Attribute/Aggregate/ArrayTransformerTest.php
@@ -3,10 +3,12 @@
namespace Tests\Form\Attribute\Aggregate;
use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilder;
use Bdf\Form\Attribute\Aggregate\ArrayTransformer;
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\ElementInterface;
+use Bdf\Form\Registry\Registry;
use Bdf\Form\Struct\StructForm;
use Bdf\Form\Transformer\TransformerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
@@ -42,6 +44,178 @@ public function struct(AttributesProcessorInterface $processor)
$this->assertEquals(['A_A', 'A-A'], $view['foo']->value());
}
+ #[DataProvider('provideAttributesProcessor')]
+ public function test_object(AttributesProcessorInterface $processor)
+ {
+ $form = new class(null, $processor) extends AttributeForm {
+ #[ArrayTransformer(new AArrayTransformer('A'))]
+ public ArrayElement $foo;
+ };
+
+ $form->submit(['foo' => ['_', '-']]);
+ $this->assertEquals(['A_', 'A-'], $form->foo->value());
+
+ $view = $form->view();
+ $this->assertEquals(['A_A', 'A-A'], $view['foo']->value());
+ }
+
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct_object(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestArrayTransformerObjectStruct::class, processor: $processor);
+
+ $form->submit(['foo' => ['_', '-']]);
+ $this->assertEquals(['A_', 'A-'], $form['foo']->element()->value());
+
+ $view = $form->view();
+ $this->assertEquals(['A_A', 'A-A'], $view['foo']->value());
+ }
+
+ #[DataProvider('provideAttributesProcessor')]
+ public function test_from_service(AttributesProcessorInterface $processor)
+ {
+ $registry = new Registry();
+ $registry->registerService(new ServiceArrayTransformer('S'));
+
+ $form = new class(new FormBuilder($registry), $processor) extends AttributeForm {
+ #[ArrayTransformer(ServiceArrayTransformer::class)]
+ public ArrayElement $foo;
+ };
+
+ $form->submit(['foo' => ['_', '-']]);
+ $this->assertEquals(['S_', 'S-'], $form->foo->value());
+
+ $view = $form->view();
+ $this->assertEquals(['S_S', 'S-S'], $view['foo']->value());
+ }
+
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct_from_service(AttributesProcessorInterface $processor)
+ {
+ $registry = new Registry();
+ $registry->registerService(new ServiceArrayTransformer('S'));
+
+ $form = new StructForm(TestArrayTransformerServiceStruct::class, new FormBuilder($registry), processor: $processor);
+
+ $form->submit(['foo' => ['_', '-']]);
+ $this->assertEquals(['S_', 'S-'], $form['foo']->element()->value());
+
+ $view = $form->view();
+ $this->assertEquals(['S_S', 'S-S'], $view['foo']->value());
+ }
+
+ #[DataProvider('provideAttributesProcessor')]
+ public function test_from_service_not_registered(AttributesProcessorInterface $processor)
+ {
+ $form = new class(new FormBuilder(new Registry()), $processor) extends AttributeForm {
+ #[ArrayTransformer(ServiceArrayTransformer::class)]
+ public ArrayElement $foo;
+ };
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Service "'.ServiceArrayTransformer::class.'" is not registered.');
+
+ $form->submit(['foo' => ['_', '-']]);
+ }
+
+ public function test_object_with_constructor_arguments_throws()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Constructor arguments can be used only with transformer class name');
+
+ new ArrayTransformer(new AArrayTransformer('A'), ['B']);
+ }
+
+ public function test_code_generator_object()
+ {
+ $form = new class extends AttributeForm {
+ #[ArrayTransformer(new AArrayTransformer('A'))]
+ public ArrayElement $foo;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Tests\Form\Attribute\Aggregate\AArrayTransformer;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->arrayTransformer(new AArrayTransformer(c: 'A'));
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+
+ public function test_code_generator_from_service()
+ {
+ $form = new class extends AttributeForm {
+ #[ArrayTransformer(ServiceArrayTransformer::class)]
+ public ArrayElement $foo;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Tests\Form\Attribute\Aggregate\ServiceArrayTransformer;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->arrayTransformer(ServiceArrayTransformer::class);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -147,3 +321,37 @@ class TestArrayTransformerStruct
#[ArrayTransformer(AArrayTransformer::class, ['A'])]
public array $foo;
}
+
+class TestArrayTransformerObjectStruct
+{
+ #[ArrayTransformer(new AArrayTransformer('A'))]
+ public array $foo;
+}
+
+class TestArrayTransformerServiceStruct
+{
+ #[ArrayTransformer(ServiceArrayTransformer::class)]
+ public array $foo;
+}
+
+/**
+ * Transformer with a required constructor dependency: not instantiable without parameters,
+ * so it must be resolved from the registry/container.
+ */
+class ServiceArrayTransformer implements TransformerInterface
+{
+ public function __construct(
+ public string $prefix
+ ) {
+ }
+
+ public function transformToHttp($value, ElementInterface $input)
+ {
+ return array_map(fn($v) => $v . $this->prefix, $value);
+ }
+
+ public function transformFromHttp($value, ElementInterface $input)
+ {
+ return array_map(fn($v) => $this->prefix . $v, $value);
+ }
+}
diff --git a/tests/Attribute/Element/ChoicesTest.php b/tests/Attribute/Element/ChoicesTest.php
index 5861e97..6877d30 100644
--- a/tests/Attribute/Element/ChoicesTest.php
+++ b/tests/Attribute/Element/ChoicesTest.php
@@ -3,11 +3,16 @@
namespace Tests\Form\Attribute\Element;
use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilder;
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Element\Choices;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\Choice\ArrayChoice;
+use Bdf\Form\Choice\ChoiceInterface;
+use Bdf\Form\Choice\ChoiceView;
+use Bdf\Form\Choice\LazyChoice;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Registry\Registry;
use Bdf\Form\Struct\StructForm;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
@@ -67,6 +72,110 @@ public function struct(AttributesProcessorInterface $processor)
$this->assertEquals(['bar' => 'You must select at least 2 choices.'], $form->error()->toArray());
}
+ #[Test, DataProvider('provideAttributesProcessor')]
+ public function from_service(AttributesProcessorInterface $processor)
+ {
+ $registry = new Registry();
+ $registry->registerService(new TestChoicesService());
+
+ $form = new class(new FormBuilder($registry), $processor) extends AttributeForm {
+ #[Choices(TestChoicesService::class, message: 'my error')]
+ public StringElement $foo;
+
+ #[Choices(TestChoicesService::class, message: 'my error', options: ['min' => 2])]
+ public ArrayElement $bar;
+ };
+
+ $form->submit(['foo' => 'a', 'bar' => ['b']]);
+
+ $this->assertInstanceOf(LazyChoice::class, $form->foo->choices());
+ $this->assertSame(['aaa', 'bbb', 'ccc'], $form->foo->choices()->values());
+ $this->assertInstanceOf(LazyChoice::class, $form->bar->choices());
+ $this->assertSame(['aaa', 'bbb', 'ccc'], $form->bar->choices()->values());
+ $this->assertEquals(['foo' => 'my error', 'bar' => 'my error'], $form->error()->toArray());
+
+ $form->submit(['foo' => 'aaa', 'bar' => ['aaa', 'bbb']]);
+ $this->assertTrue($form->valid());
+
+ $form->submit(['foo' => 'aaa', 'bar' => ['aaa']]);
+ $this->assertEquals(['bar' => 'You must select at least 2 choices.'], $form->error()->toArray());
+ }
+
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct_from_service(AttributesProcessorInterface $processor)
+ {
+ $registry = new Registry();
+ $registry->registerService(new TestChoicesService());
+
+ $form = new StructForm(TestChoicesServiceStruct::class, new FormBuilder($registry), processor: $processor);
+
+ $form->submit(['foo' => 'a', 'bar' => ['b']]);
+
+ $this->assertInstanceOf(LazyChoice::class, $form['foo']->element()->choices());
+ $this->assertSame(['aaa', 'bbb', 'ccc'], $form['foo']->element()->choices()->values());
+ $this->assertInstanceOf(LazyChoice::class, $form['bar']->element()->choices());
+ $this->assertSame(['aaa', 'bbb', 'ccc'], $form['bar']->element()->choices()->values());
+ $this->assertEquals(['foo' => 'my error', 'bar' => 'my error'], $form->error()->toArray());
+
+ $form->submit(['foo' => 'aaa', 'bar' => ['aaa', 'bbb']]);
+ $this->assertTrue($form->valid());
+
+ $form->submit(['foo' => 'aaa', 'bar' => ['aaa']]);
+ $this->assertEquals(['bar' => 'You must select at least 2 choices.'], $form->error()->toArray());
+ }
+
+ public function test_code_generator_from_service()
+ {
+ $form = new class extends AttributeForm {
+ #[Choices(TestChoicesService::class, message: 'my error')]
+ public StringElement $foo;
+
+ #[Choices(TestChoicesService::class, message: 'my error', options: ['min' => 2])]
+ public ArrayElement $bar;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\StringElement;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->choices('Tests\Form\Attribute\Element\TestChoicesService', message: 'my error');
+
+ $bar = $builder->add('bar', ArrayElement::class);
+ $bar->choices('Tests\Form\Attribute\Element\TestChoicesService', min: 2, message: 'my error');
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ $form->bar = $inner['bar']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -194,3 +303,28 @@ public static function generateChoices()
return ['aaa', 'bbb', 'ccc'];
}
}
+
+class TestChoicesServiceStruct
+{
+ public function __construct(
+ #[Choices(TestChoicesService::class, message: 'my error')]
+ public ?string $foo,
+ #[Choices(TestChoicesService::class, message: 'my error', options: ['min' => 2])]
+ public array $bar,
+ ) {}
+}
+
+class TestChoicesService implements ChoiceInterface
+{
+ #[\Override]
+ public function values(): array
+ {
+ return ['aaa', 'bbb', 'ccc'];
+ }
+
+ #[\Override]
+ public function view(?callable $configuration = null): array
+ {
+ return new ArrayChoice($this->view())->view($configuration);
+ }
+}
diff --git a/tests/Attribute/Element/TransformerTest.php b/tests/Attribute/Element/TransformerTest.php
index 82beb76..20b7c5c 100644
--- a/tests/Attribute/Element/TransformerTest.php
+++ b/tests/Attribute/Element/TransformerTest.php
@@ -3,11 +3,13 @@
namespace Tests\Form\Attribute\Element;
use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilder;
use Bdf\Form\Attribute\AttributeForm;
use Bdf\Form\Attribute\Element\Transformer;
use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
use Bdf\Form\ElementInterface;
use Bdf\Form\Leaf\StringElement;
+use Bdf\Form\Registry\Registry;
use Bdf\Form\Struct\StructForm;
use Bdf\Form\Transformer\TransformerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
@@ -70,6 +72,309 @@ public function testWithArrayStruct(AttributesProcessorInterface $processor)
$this->assertEquals(['A_A', 'A-A'], $view['foo']->value());
}
+ #[DataProvider('provideAttributesProcessor')]
+ public function test_object(AttributesProcessorInterface $processor)
+ {
+ $form = new class(null, $processor) extends AttributeForm {
+ #[Transformer(new ATransformer('A'))]
+ public StringElement $foo;
+ };
+
+ $form->submit(['foo' => '_']);
+ $this->assertEquals('A_', $form->foo->value());
+
+ $view = $form->view();
+ $this->assertEquals('A_A', $view['foo']->value());
+ }
+
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct_object(AttributesProcessorInterface $processor)
+ {
+ $form = new StructForm(TestTransformerObjectStruct::class, processor: $processor);
+
+ $form->submit(['foo' => '_']);
+ $this->assertEquals('A_', $form->value()->foo);
+
+ $view = $form->view();
+ $this->assertEquals('A_A', $view['foo']->value());
+ }
+
+ #[DataProvider('provideAttributesProcessor')]
+ public function test_object_array(AttributesProcessorInterface $processor)
+ {
+ $form = new class(null, $processor) extends AttributeForm {
+ #[Transformer(new AArrayTransformer('A'), array: true)]
+ public ArrayElement $foo;
+ };
+
+ $form->submit(['foo' => ['_', '-']]);
+ $this->assertEquals(['A_', 'A-'], $form->foo->value());
+
+ $view = $form->view();
+ $this->assertEquals(['A_A', 'A-A'], $view['foo']->value());
+ }
+
+ #[DataProvider('provideAttributesProcessor')]
+ public function test_from_service(AttributesProcessorInterface $processor)
+ {
+ $registry = new Registry();
+ $registry->registerService(new ServiceTransformer('S'));
+
+ $form = new class(new FormBuilder($registry), $processor) extends AttributeForm {
+ #[Transformer(ServiceTransformer::class)]
+ public StringElement $foo;
+ };
+
+ $form->submit(['foo' => '_']);
+ $this->assertEquals('S_', $form->foo->value());
+
+ $view = $form->view();
+ $this->assertEquals('S_S', $view['foo']->value());
+ }
+
+ #[Test, DataProvider('provideStructAttributesProcessor')]
+ public function struct_from_service(AttributesProcessorInterface $processor)
+ {
+ $registry = new Registry();
+ $registry->registerService(new ServiceTransformer('S'));
+
+ $form = new StructForm(TestTransformerServiceStruct::class, new FormBuilder($registry), processor: $processor);
+
+ $form->submit(['foo' => '_']);
+ $this->assertEquals('S_', $form->value()->foo);
+
+ $view = $form->view();
+ $this->assertEquals('S_S', $view['foo']->value());
+ }
+
+ #[DataProvider('provideAttributesProcessor')]
+ public function test_from_service_array(AttributesProcessorInterface $processor)
+ {
+ $registry = new Registry();
+ $registry->registerService(new ServiceArrayTransformer('S'));
+
+ $form = new class(new FormBuilder($registry), $processor) extends AttributeForm {
+ #[Transformer(ServiceArrayTransformer::class, array: true)]
+ public ArrayElement $foo;
+ };
+
+ $form->submit(['foo' => ['_', '-']]);
+ $this->assertEquals(['S_', 'S-'], $form->foo->value());
+
+ $view = $form->view();
+ $this->assertEquals(['S_S', 'S-S'], $view['foo']->value());
+ }
+
+ #[DataProvider('provideAttributesProcessor')]
+ public function test_from_service_not_registered(AttributesProcessorInterface $processor)
+ {
+ $form = new class(new FormBuilder(new Registry()), $processor) extends AttributeForm {
+ #[Transformer(ServiceTransformer::class)]
+ public StringElement $foo;
+ };
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Service "'.ServiceTransformer::class.'" is not registered.');
+
+ $form->submit(['foo' => '_']);
+ }
+
+ public function test_object_with_constructor_arguments_throws()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Constructor arguments can be used only with transformer class name');
+
+ new Transformer(new ATransformer('A'), ['B']);
+ }
+
+ public function test_null_transformer_throws()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The transformer parameter must not be null.');
+
+ new Transformer();
+ }
+
+ public function test_code_generator_object()
+ {
+ $form = new class extends AttributeForm {
+ #[Transformer(new ATransformer('A'))]
+ public StringElement $foo;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\StringElement;
+use Tests\Form\Attribute\Element\ATransformer;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->transformer(new ATransformer(c: 'A'));
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+
+ public function test_code_generator_object_array()
+ {
+ $form = new class extends AttributeForm {
+ #[Transformer(new AArrayTransformer('A'), array: true)]
+ public ArrayElement $foo;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Tests\Form\Attribute\Element\AArrayTransformer;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->arrayTransformer(new AArrayTransformer(c: 'A'));
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+
+ public function test_code_generator_from_service()
+ {
+ $form = new class extends AttributeForm {
+ #[Transformer(ServiceTransformer::class)]
+ public StringElement $foo;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Bdf\Form\Leaf\StringElement;
+use Tests\Form\Attribute\Element\ServiceTransformer;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', StringElement::class);
+ $foo->transformer(ServiceTransformer::class);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+
+ public function test_code_generator_from_service_array()
+ {
+ $form = new class extends AttributeForm {
+ #[Transformer(ServiceArrayTransformer::class, array: true)]
+ public ArrayElement $foo;
+ };
+
+ $this->assertGenerated(<<<'PHP'
+namespace Generated;
+
+use Bdf\Form\Aggregate\ArrayElement;
+use Bdf\Form\Aggregate\FormBuilderInterface;
+use Bdf\Form\Aggregate\FormInterface;
+use Bdf\Form\Attribute\AttributeForm;
+use Bdf\Form\Attribute\Processor\AttributesProcessorInterface;
+use Bdf\Form\Attribute\Processor\PostConfigureInterface;
+use Tests\Form\Attribute\Element\ServiceArrayTransformer;
+
+class GeneratedConfigurator implements AttributesProcessorInterface, PostConfigureInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ function configureBuilder(object|string $context, FormBuilderInterface $builder): ?PostConfigureInterface
+ {
+ $foo = $builder->add('foo', ArrayElement::class);
+ $foo->arrayTransformer(ServiceArrayTransformer::class);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ function postConfigure(AttributeForm $form, FormInterface $inner): void
+ {
+ $form->foo = $inner['foo']->element();
+ }
+}
+
+PHP
+ , $form
+);
+ }
+
public function test_code_generator()
{
$form = new class extends AttributeForm {
@@ -220,7 +525,7 @@ function configureBuilder(object|string $context, FormBuilderInterface $builder)
$builder->generates(TestTransformerArrayStruct::class);
$foo = $builder->add('foo', ArrayElement::class);
- $foo->arrayTransformer(new AArrayTransformer('A'));
+ $foo->arrayTransformer(new AArrayTransformer(c: 'A'));
$foo->hydrator(new Setter(null))->extractor(new Getter(null));
return null;
@@ -275,8 +580,60 @@ class TestTransformerStruct
public ?string $foo;
}
+class TestTransformerObjectStruct
+{
+ #[Transformer(new ATransformer('A'))]
+ public ?string $foo;
+}
+
+class TestTransformerServiceStruct
+{
+ #[Transformer(ServiceTransformer::class)]
+ public ?string $foo;
+}
+
+/**
+ * Transformer with a required constructor dependency: not instantiable without parameters,
+ * so it must be resolved from the registry/container.
+ */
+class ServiceTransformer implements TransformerInterface
+{
+ public function __construct(
+ public string $prefix
+ ) {
+ }
+
+ public function transformToHttp($value, ElementInterface $input)
+ {
+ return $value . $this->prefix;
+ }
+
+ public function transformFromHttp($value, ElementInterface $input)
+ {
+ return $this->prefix . $value;
+ }
+}
+
+class ServiceArrayTransformer implements TransformerInterface
+{
+ public function __construct(
+ public string $prefix
+ ) {
+ }
+
+ public function transformToHttp($value, ElementInterface $input)
+ {
+ return array_map(fn($v) => $v . $this->prefix, $value);
+ }
+
+ public function transformFromHttp($value, ElementInterface $input)
+ {
+ return array_map(fn($v) => $this->prefix . $v, $value);
+ }
+}
+
class TestTransformerArrayStruct
{
- #[Transformer(AArrayTransformer::class, ['A'], array: true)]
+ #[Transformer(new AArrayTransformer('A'), array: true)]
public array $foo;
}
diff --git a/tests/Leaf/FloatElementBuilderTest.php b/tests/Leaf/FloatElementBuilderTest.php
index 5a9cc89..9184292 100644
--- a/tests/Leaf/FloatElementBuilderTest.php
+++ b/tests/Leaf/FloatElementBuilderTest.php
@@ -3,6 +3,11 @@
namespace Bdf\Form\Leaf;
use Bdf\Form\Choice\ArrayChoice;
+use Bdf\Form\Choice\ChoiceInterface;
+use Bdf\Form\Choice\ChoiceView;
+use Bdf\Form\Choice\LazyChoice;
+use Bdf\Form\Registry\Registry;
+use InvalidArgumentException;
use Locale;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\Extension\Core\DataTransformer\IntegerToLocalizedStringTransformer;
@@ -340,4 +345,64 @@ public function test_choices_custom_message()
$this->assertTrue($element->failed());
$this->assertEquals('my error', $element->error()->global());
}
+
+ /**
+ *
+ */
+ public function test_choices_from_service()
+ {
+ $choice = new MyFloatServiceChoice();
+
+ $registry = new Registry();
+ $registry->registerService($choice);
+
+ $builder = new FloatElementBuilder($registry);
+ $element = $builder->choices(MyFloatServiceChoice::class)->buildElement();
+
+ $this->assertInstanceOf(LazyChoice::class, $element->choices());
+ $this->assertSame([12.3, 45.6, 78.9], $element->choices()->values());
+
+ $element->submit('14.7');
+ $this->assertFalse($element->valid());
+ $this->assertTrue($element->failed());
+ $this->assertEquals('The value you selected is not a valid choice.', $element->error()->global());
+
+ $element->submit('45.6');
+ $this->assertTrue($element->valid());
+ }
+
+ /**
+ *
+ */
+ public function test_choices_from_service_not_registered()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Service "'.MyFloatServiceChoice::class.'" is not registered.');
+
+ $element = $this->builder->choices(MyFloatServiceChoice::class)->buildElement();
+ $element->submit('45.6');
+ }
+}
+
+class MyFloatServiceChoice implements ChoiceInterface
+{
+ public function values(): array
+ {
+ return [12.3, 45.6, 78.9];
+ }
+
+ public function view(?callable $configuration = null): array
+ {
+ $view = [];
+
+ foreach ($this->values() as $value) {
+ $view[] = $choice = new ChoiceView($value, $value);
+
+ if ($configuration !== null) {
+ $configuration($choice);
+ }
+ }
+
+ return $view;
+ }
}
diff --git a/tests/Leaf/IntegerElementBuilderTest.php b/tests/Leaf/IntegerElementBuilderTest.php
index 82e525f..db12300 100644
--- a/tests/Leaf/IntegerElementBuilderTest.php
+++ b/tests/Leaf/IntegerElementBuilderTest.php
@@ -3,7 +3,12 @@
namespace Bdf\Form\Leaf;
use Bdf\Form\Choice\ArrayChoice;
+use Bdf\Form\Choice\ChoiceInterface;
+use Bdf\Form\Choice\ChoiceView;
use Bdf\Form\Choice\EnumChoice;
+use Bdf\Form\Choice\LazyChoice;
+use Bdf\Form\Registry\Registry;
+use InvalidArgumentException;
use Locale;
use NumberFormatter;
use PHPUnit\Framework\TestCase;
@@ -331,6 +336,43 @@ public function test_choices_enum()
$this->assertSame('Bar', $view[1]->label);
$this->assertSame('121', $view[1]->value);
}
+
+ /**
+ *
+ */
+ public function test_choices_from_service()
+ {
+ $choice = new MyIntServiceChoice();
+
+ $registry = new Registry();
+ $registry->registerService($choice);
+
+ $builder = new IntegerElementBuilder($registry);
+ $element = $builder->choices(MyIntServiceChoice::class)->buildElement();
+
+ $this->assertInstanceOf(LazyChoice::class, $element->choices());
+ $this->assertSame([12, 34, 56], $element->choices()->values());
+
+ $element->submit(22);
+ $this->assertFalse($element->valid());
+ $this->assertTrue($element->failed());
+ $this->assertEquals('The value you selected is not a valid choice.', $element->error()->global());
+
+ $element->submit(34);
+ $this->assertTrue($element->valid());
+ }
+
+ /**
+ *
+ */
+ public function test_choices_from_service_not_registered()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Service "'.MyIntServiceChoice::class.'" is not registered.');
+
+ $element = $this->builder->choices(MyIntServiceChoice::class)->buildElement();
+ $element->submit(34);
+ }
}
enum MyIntEnum: int
@@ -338,3 +380,26 @@ enum MyIntEnum: int
case Foo = 42;
case Bar = 121;
}
+
+class MyIntServiceChoice implements ChoiceInterface
+{
+ public function values(): array
+ {
+ return [12, 34, 56];
+ }
+
+ public function view(?callable $configuration = null): array
+ {
+ $view = [];
+
+ foreach ($this->values() as $value) {
+ $view[] = $choice = new ChoiceView($value, $value);
+
+ if ($configuration !== null) {
+ $configuration($choice);
+ }
+ }
+
+ return $view;
+ }
+}
diff --git a/tests/Leaf/StringElementBuilderTest.php b/tests/Leaf/StringElementBuilderTest.php
index 94b7b6f..844d885 100644
--- a/tests/Leaf/StringElementBuilderTest.php
+++ b/tests/Leaf/StringElementBuilderTest.php
@@ -3,7 +3,14 @@
namespace Bdf\Form\Leaf;
use Bdf\Form\Choice\ArrayChoice;
+use Bdf\Form\Choice\ChoiceInterface;
+use Bdf\Form\Choice\ChoiceView;
use Bdf\Form\Choice\EnumChoice;
+use Bdf\Form\Choice\LazyChoice;
+use Bdf\Form\ElementInterface;
+use Bdf\Form\Registry\Registry;
+use Bdf\Form\Transformer\TransformerInterface;
+use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\NotEqualTo;
use Symfony\Component\Validator\Constraints\Positive;
@@ -91,6 +98,35 @@ public function test_transformer_prepend()
$this->assertEquals('_ab', $element->submit('_')->value());
}
+ /**
+ *
+ */
+ public function test_transformer_from_service()
+ {
+ $registry = new Registry();
+ $registry->registerService(new MyTransformerService());
+
+ $builder = new StringElementBuilder($registry);
+ $element = $builder
+ ->transformer(MyTransformerService::class)
+ ->transformer(function ($value) { return $value . 'a'; })
+ ->buildElement()
+ ;
+
+ $this->assertEquals('_a>', $element->submit('_')->value());
+ }
+
+ /**
+ *
+ */
+ public function test_transformer_from_service_not_registered()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Service "'.MyTransformerService::class.'" is not registered.');
+
+ $this->builder->transformer(MyTransformerService::class);
+ }
+
/**
*
*/
@@ -209,6 +245,43 @@ public function test_choices_enum()
$this->assertSame('Bar', $view[1]->label);
$this->assertSame('bar', $view[1]->value);
}
+
+ /**
+ *
+ */
+ public function test_choices_from_service()
+ {
+ $choice = new MyServiceChoice();
+
+ $registry = new Registry();
+ $registry->registerService($choice);
+
+ $builder = new StringElementBuilder($registry);
+ $element = $builder->choices(MyServiceChoice::class)->buildElement();
+
+ $this->assertInstanceOf(LazyChoice::class, $element->choices());
+ $this->assertSame(['foo', 'bar'], $element->choices()->values());
+
+ $element->submit('aaa');
+ $this->assertFalse($element->valid());
+ $this->assertTrue($element->failed());
+ $this->assertEquals('The value you selected is not a valid choice.', $element->error()->global());
+
+ $element->submit('foo');
+ $this->assertTrue($element->valid());
+ }
+
+ /**
+ *
+ */
+ public function test_choices_from_service_not_registered()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Service "'.MyServiceChoice::class.'" is not registered.');
+
+ $element = $this->builder->choices(MyServiceChoice::class)->buildElement();
+ $element->submit('foo');
+ }
}
enum MyStringEnum: string
@@ -216,3 +289,39 @@ enum MyStringEnum: string
case Foo = 'foo';
case Bar = 'bar';
}
+
+class MyTransformerService implements TransformerInterface
+{
+ public function transformToHttp(mixed $value, ElementInterface $input): mixed
+ {
+ return $value;
+ }
+
+ public function transformFromHttp(mixed $value, ElementInterface $input): mixed
+ {
+ return $value . '>';
+ }
+}
+
+class MyServiceChoice implements ChoiceInterface
+{
+ public function values(): array
+ {
+ return ['foo', 'bar'];
+ }
+
+ public function view(?callable $configuration = null): array
+ {
+ $view = [];
+
+ foreach ($this->values() as $value) {
+ $view[] = $choice = new ChoiceView($value, $value);
+
+ if ($configuration !== null) {
+ $configuration($choice);
+ }
+ }
+
+ return $view;
+ }
+}
diff --git a/tests/Registry/RegistryTest.php b/tests/Registry/RegistryTest.php
index 57f6881..f41f4b0 100644
--- a/tests/Registry/RegistryTest.php
+++ b/tests/Registry/RegistryTest.php
@@ -156,6 +156,18 @@ public function test_buttonBuilder()
$this->assertInstanceOf(SubmitButtonBuilder::class, $this->registry->buttonBuilder('btn'));
$this->assertEquals('btn', $this->registry->buttonBuilder('btn')->buildButton()->name());
}
+
+ public function test_service()
+ {
+ $service = new \stdClass();
+ $this->registry->registerService($service);
+
+ $this->assertSame($service, $this->registry->service(\stdClass::class));
+
+ $otherService = new \stdClass();
+ $this->registry->registerService($otherService, 'other');
+ $this->assertSame($otherService, $this->registry->service('other'));
+ }
}
class MyCustomTestElement extends LeafElement