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