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