Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.08% covered (warning)
76.08%
159 / 209
40.00% covered (danger)
40.00%
4 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComponentFactory
76.08% covered (warning)
76.08%
159 / 209
40.00% covered (danger)
40.00%
4 / 10
33.56
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
 createRawContents
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createResource
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
1
 createResourceOverview
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
2
 createFilterColumns
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
4.01
 createWrapLayout
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 createFormForMethod
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
4
 createFormForResourceRemoval
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 createFormForResourceCreation
48.89% covered (danger)
48.89%
22 / 45
0.00% covered (danger)
0.00%
0 / 1
6.14
 createFormForResourceModification
48.89% covered (danger)
48.89%
22 / 45
0.00% covered (danger)
0.00%
0 / 1
6.14
1<?php
2namespace Apie\HtmlBuilders\Factories;
3
4use Apie\Core\Actions\ActionResponse;
5use Apie\Core\Attributes\Context;
6use Apie\Core\BoundedContext\BoundedContextHashmap;
7use Apie\Core\BoundedContext\BoundedContextId;
8use Apie\Core\Context\ApieContext;
9use Apie\Core\ContextConstants;
10use Apie\Core\Datalayers\ApieDatalayerWithFilters;
11use Apie\Core\Datalayers\Lists\PaginatedResult;
12use Apie\Core\Entities\EntityInterface;
13use Apie\Core\Enums\RequestMethod;
14use Apie\Core\Lists\StringSet;
15use Apie\Core\Session\CsrfTokenProvider;
16use Apie\Core\Utils\EntityUtils;
17use Apie\Core\ValueObjects\Utils;
18use Apie\HtmlBuilders\Columns\ColumnSelector;
19use Apie\HtmlBuilders\Components\Dashboard\RawContents;
20use Apie\HtmlBuilders\Components\Forms\Csrf;
21use Apie\HtmlBuilders\Components\Forms\Form;
22use Apie\HtmlBuilders\Components\Forms\PolymorphicForm;
23use Apie\HtmlBuilders\Components\Forms\RemoveConfirm;
24use Apie\HtmlBuilders\Components\Layout;
25use Apie\HtmlBuilders\Components\Resource\Detail;
26use Apie\HtmlBuilders\Components\Resource\FilterColumns;
27use Apie\HtmlBuilders\Components\Resource\Overview;
28use Apie\HtmlBuilders\Components\Resource\Pagination;
29use Apie\HtmlBuilders\Components\Resource\ResourceActionList;
30use Apie\HtmlBuilders\Components\Resource\SingleResourceActionList;
31use Apie\HtmlBuilders\Configuration\ApplicationConfiguration;
32use Apie\HtmlBuilders\Enums\LayoutEnum;
33use Apie\HtmlBuilders\Interfaces\ComponentInterface;
34use Apie\HtmlBuilders\Lists\ComponentHashmap;
35use Psr\Http\Message\RequestInterface;
36use ReflectionClass;
37use ReflectionMethod;
38use Stringable;
39
40class ComponentFactory
41{
42    private ColumnSelector $columnSelector;
43
44    public function __construct(
45        private readonly ApplicationConfiguration $applicationConfiguration,
46        private readonly BoundedContextHashmap $boundedContextHashmap,
47        private readonly FormComponentFactory $formComponentFactory,
48        private readonly FieldDisplayComponentFactory $fieldDisplayComponentFactory,
49        private readonly ResourceActionFactory $resourceActionFactory,
50        ?ColumnSelector $columnSelector = null
51    ) {
52        $this->columnSelector = $columnSelector ?? new ColumnSelector();
53    }
54
55    public function createRawContents(Stringable|string $dashboardContents): ComponentInterface
56    {
57        return new RawContents($dashboardContents);
58    }
59
60    /**
61     * @param ReflectionClass<EntityInterface> $className
62     */
63    public function createResource(
64        ActionResponse $actionResponse,
65        ReflectionClass $className,
66        ?BoundedContextId $boundedContextId
67    ): ComponentInterface {
68        assert($actionResponse->result instanceof EntityInterface);
69        $id = $actionResponse->apieContext->getContext(ContextConstants::RESOURCE_ID);
70        $configuration = $this->applicationConfiguration->createConfiguration(
71            $actionResponse->apieContext,
72            $this->boundedContextHashmap,
73            $boundedContextId
74        );
75        $actionList = $this->resourceActionFactory->createResourceActionForDetail(
76            $actionResponse->result,
77            $className,
78            $actionResponse->apieContext
79        );
80        $resourceActionList = new SingleResourceActionList(
81            $configuration,
82            $actionList,
83            $id
84        );
85        return $this->createWrapLayout(
86            $className->getShortName() . ' details of ' . $id,
87            $boundedContextId,
88            $actionResponse->apieContext,
89            new Detail(
90                $actionResponse->result,
91                $resourceActionList,
92                $this->fieldDisplayComponentFactory->createDisplayFor($actionResponse->result, $actionResponse->apieContext)
93            )
94        );
95    }
96
97    /**
98     * @param ReflectionClass<EntityInterface> $className
99     */
100    public function createResourceOverview(
101        ActionResponse $actionResponse,
102        ReflectionClass $className,
103        ?BoundedContextId $boundedContextId
104    ): ComponentInterface {
105        assert($actionResponse->result instanceof PaginatedResult);
106        $listData = Utils::toArray($actionResponse->getResultAsNativeData()['list']);
107        $columns = $this->columnSelector->getColumns($className, $actionResponse->apieContext);
108        $pagination = $this->createRawContents('');
109        if ($actionResponse->result->totalCount > $actionResponse->result->pageSize) {
110            $pagination = new Pagination($actionResponse->result);
111        }
112        $configuration = $this->applicationConfiguration->createConfiguration(
113            $actionResponse->apieContext,
114            $this->boundedContextHashmap,
115            $boundedContextId
116        );
117        $filterColumns = $this->createFilterColumns($className, $actionResponse->apieContext);
118
119        $actionList = new ResourceActionList(
120            $configuration,
121            $this->resourceActionFactory->createResourceActionForOverview($className, $actionResponse->apieContext),
122            $filterColumns
123        );
124        return $this->createWrapLayout(
125            $className->getShortName() . ' overview',
126            $boundedContextId,
127            $actionResponse->apieContext,
128            new Overview($listData, $columns, $actionList, $pagination)
129        );
130    }
131
132    /**
133     * @param ReflectionClass<EntityInterface> $className
134     */
135    public function createFilterColumns(ReflectionClass $className, ApieContext $apieContext): ComponentInterface
136    {
137        $searchFilters = [];
138        if ($apieContext->hasContext(RequestInterface::class)) {
139            $request = $apieContext->getContext(RequestInterface::class);
140            assert($request instanceof RequestInterface);
141            $query = $request->getUri()->getQuery();
142            parse_str($query, $searchFilters);
143        }
144        if ($apieContext->hasContext(ApieDatalayerWithFilters::class) && $apieContext->hasContext(ContextConstants::BOUNDED_CONTEXT_ID)) {
145            $datalayer = $apieContext->getContext(ApieDatalayerWithFilters::class);
146            assert($datalayer instanceof ApieDatalayerWithFilters);
147            $boundedContextId = $apieContext->getContext(ContextConstants::BOUNDED_CONTEXT_ID);
148            $filterColumns = $datalayer->getFilterColumns($className, new BoundedContextId($boundedContextId));
149            return new FilterColumns($filterColumns, $searchFilters['search'] ?? '', Utils::toArray($searchFilters['query'] ?? []));
150        }
151        return new FilterColumns(new StringSet(), $searchFilters['search'] ?? '', []);
152    }
153
154    public function createWrapLayout(
155        string $pageTitle,
156        ?BoundedContextId $boundedContextId,
157        ApieContext $context,
158        ComponentInterface $contents,
159        LayoutEnum $layoutEnum = LayoutEnum::LAYOUT
160    ): ComponentInterface {
161        if ($layoutEnum === LayoutEnum::SIDEBAR) {
162            return $contents;
163        }
164        $configuration = $this->applicationConfiguration->createConfiguration($context, $this->boundedContextHashmap, $boundedContextId);
165        return new Layout(
166            $pageTitle,
167            $configuration,
168            $contents
169        );
170    }
171
172    public function createFormForMethod(
173        string $pageTitle,
174        ReflectionMethod $method,
175        ?BoundedContextId $boundedContextId,
176        ApieContext $context,
177        LayoutEnum $layoutEnum = LayoutEnum::LAYOUT
178    ): ComponentInterface {
179        /** @var CsrfTokenProvider $csrfTokenProvider */
180        $csrfTokenProvider = $context->getContext(CsrfTokenProvider::class);
181        $csrfToken = $csrfTokenProvider->createToken();
182        $formFields = ['_csrf' => new Csrf($csrfToken)];
183        $filledIn = $context->hasContext(ContextConstants::RAW_CONTENTS)
184            ? $context->getContext(ContextConstants::RAW_CONTENTS)
185            : null;
186        $formBuildContext = $this->formComponentFactory->createFormBuildContext($context, Utils::toArray($filledIn));
187        foreach ($method->getParameters() as $parameter) {
188            if ($parameter->getAttributes(Context::class)) {
189                continue;
190            }
191            $formFields[$parameter->name] = $this->formComponentFactory->createFromParameter($parameter, $formBuildContext);
192        }
193        return $this->createWrapLayout(
194            $pageTitle,
195            $boundedContextId,
196            $context,
197            new Form(
198                RequestMethod::POST,
199                $formBuildContext->getValidationError(),
200                $formBuildContext->getValidationErrorsInContext(),
201                $filledIn,
202                $formBuildContext->isMultipart(),
203                ...$formFields
204            ),
205            $layoutEnum
206        );
207    }
208
209    /**
210     * @param ReflectionClass<EntityInterface> $class
211     */
212    public function createFormForResourceRemoval(
213        string $pageTitle,
214        ReflectionClass $class,
215        ?BoundedContextId $boundedContextId,
216        ApieContext $context,
217        LayoutEnum $layoutEnum = LayoutEnum::LAYOUT
218    ): ComponentInterface {
219        $filledIn = $context->hasContext(ContextConstants::RAW_CONTENTS)
220            ? $context->getContext(ContextConstants::RAW_CONTENTS)
221            : null;
222        /** @var CsrfTokenProvider $csrfTokenProvider */
223        $csrfTokenProvider = $context->getContext(CsrfTokenProvider::class);
224        $csrfToken = $csrfTokenProvider->createToken();
225
226        $formBuildContext = $this->formComponentFactory->createFormBuildContext($context, Utils::toArray($filledIn));
227        return $this->createWrapLayout(
228            $pageTitle,
229            $boundedContextId,
230            $context,
231            new Form(
232                RequestMethod::POST,
233                $formBuildContext->getValidationError(),
234                $formBuildContext->getValidationErrorsInContext(),
235                null,
236                $formBuildContext->isMultipart(),
237                new RemoveConfirm($class),
238                new Csrf($csrfToken)
239            ),
240            $layoutEnum
241        );
242    }
243
244    /**
245     * @param ReflectionClass<EntityInterface> $class
246     */
247    public function createFormForResourceCreation(
248        string $pageTitle,
249        ReflectionClass $class,
250        ?BoundedContextId $boundedContextId,
251        ApieContext $context,
252        LayoutEnum $layoutEnum = LayoutEnum::LAYOUT
253    ): ComponentInterface {
254        $filledIn = $context->hasContext(ContextConstants::RAW_CONTENTS)
255            ? $context->getContext(ContextConstants::RAW_CONTENTS)
256            : null;
257        /** @var CsrfTokenProvider $csrfTokenProvider */
258        $csrfTokenProvider = $context->getContext(CsrfTokenProvider::class);
259        $csrfToken = $csrfTokenProvider->createToken();
260
261        $formBuildContext = $this->formComponentFactory->createFormBuildContext($context, Utils::toArray($filledIn));
262        if (EntityUtils::isPolymorphicEntity($class)) {
263            $subClasses = EntityUtils::getDiscriminatorClasses($class);
264            $subForms = [];
265            foreach ($subClasses as $subClass) {
266                $subForms[$subClass->getShortName()] = $this->formComponentFactory->createFromClass(
267                    $subClass,
268                    $formBuildContext
269                );
270            }
271            return $this->createWrapLayout(
272                $pageTitle,
273                $boundedContextId,
274                $context,
275                new PolymorphicForm(
276                    RequestMethod::POST,
277                    $class,
278                    $formBuildContext->getValidationError(),
279                    [], // TODO
280                    $filledIn,
281                    $formBuildContext->isMultipart(),
282                    new Csrf($csrfToken),
283                    new ComponentHashmap($subForms)
284                )
285            );
286        }
287        $form = $this->formComponentFactory->createFromClass($class, $formBuildContext);
288        return $this->createWrapLayout(
289            $pageTitle,
290            $boundedContextId,
291            $context,
292            new Form(
293                RequestMethod::POST,
294                $formBuildContext->getValidationError(),
295                $formBuildContext->getValidationErrorsInContext(),
296                $filledIn,
297                $formBuildContext->isMultipart(),
298                new Csrf($csrfToken),
299                $form
300            ),
301            $layoutEnum
302        );
303    }
304
305    /**
306     * @param ReflectionClass<EntityInterface> $class
307     */
308    public function createFormForResourceModification(
309        string $pageTitle,
310        ReflectionClass $class,
311        ?BoundedContextId $boundedContextId,
312        ApieContext $context,
313        LayoutEnum $layoutEnum = LayoutEnum::LAYOUT
314    ): ComponentInterface {
315        $filledIn = $context->hasContext(ContextConstants::RAW_CONTENTS)
316            ? $context->getContext(ContextConstants::RAW_CONTENTS)
317            : null;
318        /** @var CsrfTokenProvider $csrfTokenProvider */
319        $csrfTokenProvider = $context->getContext(CsrfTokenProvider::class);
320        $csrfToken = $csrfTokenProvider->createToken();
321
322        $formBuildContext = $this->formComponentFactory->createFormBuildContext($context, Utils::toArray($filledIn));
323        if (EntityUtils::isPolymorphicEntity($class)) {
324            $subClasses = EntityUtils::getDiscriminatorClasses($class);
325            $subForms = [];
326            foreach ($subClasses as $subClass) {
327                $subForms[$subClass->getShortName()] = $this->formComponentFactory->createFromClass(
328                    $subClass,
329                    $formBuildContext
330                );
331            }
332            return $this->createWrapLayout(
333                $pageTitle,
334                $boundedContextId,
335                $context,
336                new PolymorphicForm(
337                    RequestMethod::POST,
338                    $class,
339                    $formBuildContext->getValidationError(),
340                    [], // TODO
341                    $filledIn,
342                    $formBuildContext->isMultipart(),
343                    new Csrf($csrfToken),
344                    new ComponentHashmap($subForms)
345                )
346            );
347        }
348        $form = $this->formComponentFactory->createFromClass($class, $formBuildContext);
349        return $this->createWrapLayout(
350            $pageTitle,
351            $boundedContextId,
352            $context,
353            new Form(
354                RequestMethod::POST,
355                $formBuildContext->getValidationError(),
356                $formBuildContext->getValidationErrorsInContext(),
357                $filledIn,
358                $formBuildContext->isMultipart(),
359                new Csrf($csrfToken),
360                $form
361            ),
362            $layoutEnum
363        );
364    }
365}