Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.87% covered (warning)
77.87%
95 / 122
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
FormComponentFactory
77.87% covered (warning)
77.87%
95 / 122
50.00% covered (danger)
50.00%
4 / 8
46.53
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
1
 createFormBuildContext
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 createFromType
80.77% covered (warning)
80.77%
21 / 26
0.00% covered (danger)
0.00%
0 / 1
10.71
 createFromParameter
43.75% covered (danger)
43.75%
7 / 16
0.00% covered (danger)
0.00%
0 / 1
4.60
 createFromClass
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 makeOptional
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 createFromMetadata
75.76% covered (warning)
75.76%
25 / 33
0.00% covered (danger)
0.00%
0 / 1
12.72
1<?php
2namespace Apie\HtmlBuilders\Factories;
3
4use Apie\Core\Attributes\AllowMultipart;
5use Apie\Core\Attributes\CmsSingleInput;
6use Apie\Core\Context\ApieContext;
7use Apie\Core\ContextConstants;
8use Apie\Core\Dto\CmsInputOption;
9use Apie\Core\Exceptions\InvalidTypeException;
10use Apie\Core\Metadata\CompositeMetadata;
11use Apie\Core\Metadata\Fields\DiscriminatorColumn;
12use Apie\Core\Metadata\Fields\SetterMethod;
13use Apie\Core\Metadata\Fields\StaticDiscriminatorColumn;
14use Apie\Core\Metadata\MetadataFactory;
15use Apie\Core\Metadata\MetadataInterface;
16use Apie\Core\ValueObjects\Utils;
17use Apie\HtmlBuilders\Components\BaseComponent;
18use Apie\HtmlBuilders\Components\Forms\FormGroup;
19use Apie\HtmlBuilders\Components\Forms\FormPrototypeList;
20use Apie\HtmlBuilders\Components\Forms\SingleInput;
21use Apie\HtmlBuilders\Factories\Concrete\ApieSingleInputComponentProvider;
22use Apie\HtmlBuilders\Factories\Concrete\ArrayComponentProvider;
23use Apie\HtmlBuilders\Factories\Concrete\BooleanComponentProvider;
24use Apie\HtmlBuilders\Factories\Concrete\DateTimeComponentProvider;
25use Apie\HtmlBuilders\Factories\Concrete\EntityComponentProvider;
26use Apie\HtmlBuilders\Factories\Concrete\FileUploadComponentProvider;
27use Apie\HtmlBuilders\Factories\Concrete\FloatComponentProvider;
28use Apie\HtmlBuilders\Factories\Concrete\HiddenIdComponentProvider;
29use Apie\HtmlBuilders\Factories\Concrete\HideUuidAsIdComponentProvider;
30use Apie\HtmlBuilders\Factories\Concrete\IntComponentProvider;
31use Apie\HtmlBuilders\Factories\Concrete\ItemHashmapComponentProvider;
32use Apie\HtmlBuilders\Factories\Concrete\ItemListComponentProvider;
33use Apie\HtmlBuilders\Factories\Concrete\MixedComponentProvider;
34use Apie\HtmlBuilders\Factories\Concrete\MultiSelectComponentProvider;
35use Apie\HtmlBuilders\Factories\Concrete\NullComponentProvider;
36use Apie\HtmlBuilders\Factories\Concrete\OptionsComponentProvider;
37use Apie\HtmlBuilders\Factories\Concrete\PolymorphicEntityComponentProvider;
38use Apie\HtmlBuilders\Factories\Concrete\UnionTypehintComponentProvider;
39use Apie\HtmlBuilders\Factories\Concrete\VerifyOtpInputComponentProvider;
40use Apie\HtmlBuilders\FormBuildContext;
41use Apie\HtmlBuilders\Interfaces\ComponentInterface;
42use Apie\HtmlBuilders\Interfaces\FormComponentProviderInterface;
43use Apie\TypeConverter\ReflectionTypeFactory;
44use ReflectionClass;
45use ReflectionNamedType;
46use ReflectionParameter;
47use ReflectionProperty;
48use ReflectionType;
49use SensitiveParameter;
50use Throwable;
51use Time\Duration;
52
53final class FormComponentFactory
54{
55    /** @var FormComponentProviderInterface[] */
56    private $formComponentProviders;
57
58    public function __construct(FormComponentProviderInterface... $formComponentProviders)
59    {
60        $this->formComponentProviders = $formComponentProviders;
61    }
62
63    /**
64     * @param FormComponentProviderInterface[] $formComponentProviders
65     */
66    public static function create(iterable $formComponentProviders = []): FormComponentFactory
67    {
68        return new self(
69            ...[
70                ...$formComponentProviders,
71                new VerifyOtpInputComponentProvider(),
72                new ApieSingleInputComponentProvider(),
73                new FileUploadComponentProvider(),
74                new HideUuidAsIdComponentProvider(),
75                new HiddenIdComponentProvider(),
76                new MixedComponentProvider(),
77                new MultiSelectComponentProvider(),
78                new OptionsComponentProvider(),
79                new UnionTypehintComponentProvider(),
80                new PolymorphicEntityComponentProvider(),
81                new ArrayComponentProvider(),
82                new ItemListComponentProvider(),
83                new ItemHashmapComponentProvider(),
84                new NullComponentProvider(),
85                new BooleanComponentProvider(),
86                new FloatComponentProvider(),
87                new IntComponentProvider(),
88                new DateTimeComponentProvider(),
89                new EntityComponentProvider(),
90            ]
91        );
92    }
93
94    /**
95     * @param array<string|int, mixed> $filledIn
96     */
97    public function createFormBuildContext(ApieContext $context, array $filledIn = []): FormBuildContext
98    {
99        $multipart = false;
100        $resourceName = $context->getContext(ContextConstants::RESOURCE_NAME, false);
101        if ($resourceName && class_exists($resourceName)) {
102            $refl = new ReflectionClass($resourceName);
103            $multipart = !empty($refl->getAttributes(AllowMultipart::class));
104        }
105        return new FormBuildContext(
106            $this,
107            $context->withContext(FormComponentFactory::class, $this),
108            $filledIn,
109            $multipart
110        );
111    }
112
113    public function createFromType(?ReflectionType $typehint, FormBuildContext $context): ComponentInterface
114    {
115        foreach ($this->formComponentProviders as $formComponentProvider) {
116            if ($formComponentProvider->supports($typehint, $context)) {
117                return $formComponentProvider->createComponentFor($typehint, $context);
118            }
119        }
120        $metadata = MetadataFactory::getCreationMetadata(
121            $typehint ?? ReflectionTypeFactory::createReflectionType('mixed'),
122            $context->getApieContext()
123        );
124        if ($metadata instanceof CompositeMetadata) {
125            return $this->createFromMetadata($metadata, $context);
126        }
127        $allowsNull = $typehint === null || $typehint->allowsNull();
128
129        $value = null;
130        try {
131            $value = Utils::toString($context->getFilledInValue($allowsNull ? null : '', true));
132        } catch (Throwable) {
133        }
134        $input = new CmsSingleInput($context->isSensitive() ? ['password'] : ['text']);
135        if ($typehint instanceof ReflectionNamedType && $typehint->getName() === Duration::class) {
136            $input = new CmsSingleInput(
137                ['integer', 'number', 'text']
138            );
139        }
140
141        return new SingleInput(
142            $context->getFormName(),
143            $context->getFilledInValue(),
144            $context->createTranslationLabel(),
145            $allowsNull,
146            $typehint,
147            $input,
148        );
149    }
150
151    public function createFromParameter(ReflectionParameter $parameter, FormBuildContext $context): ComponentInterface
152    {
153        $sensitive = $context->isSensitive();
154        foreach ($parameter->getAttributes(SensitiveParameter::class) as $param) {
155            $sensitive = true;
156        }
157        $childContext = $context->createChildContext($parameter->name, $sensitive);
158        $typehint = $parameter->getType();
159        if ($parameter->isVariadic()) {
160            $prototypeName = $context->getFormName()->getPrototypeName();
161            $component = $this->createFromType($typehint, $context->createChildContext($prototypeName));
162            return new FormPrototypeList(
163                $childContext->getFormName(),
164                $childContext->getFilledInValue([]),
165                $prototypeName,
166                $component
167            );
168        } else {
169            $component = $this->createFromType($typehint, $childContext);
170        }
171        return $component;
172    }
173
174    /**
175     * @param ReflectionClass<covariant object> $class
176     */
177    public function createFromClass(ReflectionClass $class, FormBuildContext $context, ?FormComponentProviderInterface $skipProvider = null): ComponentInterface
178    {
179        $typehint = ReflectionTypeFactory::createReflectionType($class->name);
180        foreach ($this->formComponentProviders as $formComponentProvider) {
181            if ($skipProvider !== $formComponentProvider && $formComponentProvider->supports($typehint, $context)) {
182                return $formComponentProvider->createComponentFor($typehint, $context);
183            }
184        }
185
186        $metadata = MetadataFactory::getCreationMetadata($class, $context->getApieContext());
187        return $this->createFromMetadata($metadata, $context);
188    }
189
190    private function makeOptional(BaseComponent $component): BaseComponent
191    {
192        $attr = new ReflectionProperty(BaseComponent::class, 'attributes');
193        $attributes = $attr->getValue($component);
194        $attributes['optional'] = true;
195        $attr->setValue($component, $attributes);
196        return $component;
197    }
198
199    public function createFromMetadata(MetadataInterface $metadata, FormBuildContext $context): ComponentInterface
200    {
201        if (!$metadata instanceof CompositeMetadata) {
202            throw new InvalidTypeException($metadata, CompositeMetadata::class);
203        }
204
205        $components = [];
206        foreach ($metadata->getHashmap() as $fieldName => $reflectionData) {
207            if (!$reflectionData->isField()) {
208                continue;
209            }
210            $childContext = $context->createChildContext($fieldName);
211            switch (get_class($reflectionData)) {
212                case SetterMethod::class:
213                    foreach ($reflectionData->getMethod()->getParameters() as $parameter) {
214                        if ($parameter->name === $fieldName) {
215                            $component = $this->createFromParameter($parameter, $context);
216                            $components[$fieldName] = $component instanceof BaseComponent ? $this->makeOptional($component) : $component;
217                            break;
218                        }
219                    }
220                    break;
221                case StaticDiscriminatorColumn::class:
222                case DiscriminatorColumn::class:
223                    $components[$fieldName] = new SingleInput(
224                        $childContext->getFormName(),
225                        $childContext->getFilledInValue(),
226                        $childContext->createTranslationLabel(),
227                        false,
228                        ReflectionTypeFactory::createReflectionType('string'),
229                        new CmsSingleInput(
230                            ['forced_hidden', 'hidden'],
231                            new CmsInputOption(forcedValue: $reflectionData->getValueForClass($metadata->toClass()))
232                        ),
233                    );
234                    break;
235                default:
236                    $components[$fieldName] = $this->createFromType($reflectionData->getTypehint(), $childContext);
237            }
238        }
239        return new FormGroup(
240            $context->getFormName(),
241            $context->getValidationError(),
242            $context->getMissingValidationErrors($components),
243            ...array_values($components)
244        );
245    }
246}