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