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