Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.64% covered (warning)
76.64%
164 / 214
40.00% covered (danger)
40.00%
4 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComponentFactory
76.64% covered (warning)
76.64%
164 / 214
40.00% covered (danger)
40.00%
4 / 10
34.62
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
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.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\Layout\MenuItem;
27use Apie\HtmlBuilders\Components\Resource\Detail;
28use Apie\HtmlBuilders\Components\Resource\FilterColumns;
29use Apie\HtmlBuilders\Components\Resource\Overview;
30use Apie\HtmlBuilders\Components\Resource\Pagination;
31use Apie\HtmlBuilders\Components\Resource\ResourceActionList;
32use Apie\HtmlBuilders\Components\Resource\SingleResourceActionList;
33use Apie\HtmlBuilders\Configuration\ApplicationConfiguration;
34use Apie\HtmlBuilders\Enums\LayoutEnum;
35use Apie\HtmlBuilders\Interfaces\ComponentInterface;
36use Apie\HtmlBuilders\Lists\ComponentHashmap;
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<covariant 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<covariant 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<covariant 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
173        return new Layout(
174            $pageTitle,
175            $configuration,
176            $contents,
177            $menu ? new MenuItem($menu) : null,
178        );
179    }
180
181    public function createFormForMethod(
182        string $pageTitle,
183        ReflectionMethod $method,
184        ?BoundedContextId $boundedContextId,
185        ApieContext $context,
186        LayoutEnum $layoutEnum = LayoutEnum::LAYOUT
187    ): ComponentInterface {
188        /** @var CsrfTokenProvider $csrfTokenProvider */
189        $csrfTokenProvider = $context->getContext(CsrfTokenProvider::class);
190        $csrfToken = $csrfTokenProvider->createToken();
191        $formFields = ['_csrf' => new Csrf($csrfToken)];
192        $filledIn = $context->hasContext(ContextConstants::RAW_CONTENTS)
193            ? $context->getContext(ContextConstants::RAW_CONTENTS)
194            : null;
195        $formBuildContext = $this->formComponentFactory->createFormBuildContext($context, Utils::toArray($filledIn));
196        foreach ($method->getParameters() as $parameter) {
197            if ($parameter->getAttributes(Context::class)) {
198                continue;
199            }
200            $formFields[$parameter->name] = $this->formComponentFactory->createFromParameter($parameter, $formBuildContext);
201        }
202        return $this->createWrapLayout(
203            $pageTitle,
204            $boundedContextId,
205            $context,
206            new Form(
207                RequestMethod::POST,
208                $formBuildContext->getValidationError(),
209                $formBuildContext->getValidationErrorsInContext(),
210                $filledIn,
211                $formBuildContext->isMultipart(),
212                ...$formFields
213            ),
214            $layoutEnum
215        );
216    }
217
218    /**
219     * @param ReflectionClass<covariant EntityInterface> $class
220     */
221    public function createFormForResourceRemoval(
222        string $pageTitle,
223        ReflectionClass $class,
224        ?BoundedContextId $boundedContextId,
225        ApieContext $context,
226        LayoutEnum $layoutEnum = LayoutEnum::LAYOUT
227    ): ComponentInterface {
228        $filledIn = $context->hasContext(ContextConstants::RAW_CONTENTS)
229            ? $context->getContext(ContextConstants::RAW_CONTENTS)
230            : null;
231        /** @var CsrfTokenProvider $csrfTokenProvider */
232        $csrfTokenProvider = $context->getContext(CsrfTokenProvider::class);
233        $csrfToken = $csrfTokenProvider->createToken();
234
235        $formBuildContext = $this->formComponentFactory->createFormBuildContext($context, Utils::toArray($filledIn));
236        return $this->createWrapLayout(
237            $pageTitle,
238            $boundedContextId,
239            $context,
240            new Form(
241                RequestMethod::POST,
242                $formBuildContext->getValidationError(),
243                $formBuildContext->getValidationErrorsInContext(),
244                null,
245                $formBuildContext->isMultipart(),
246                new RemoveConfirm($class),
247                new Csrf($csrfToken)
248            ),
249            $layoutEnum
250        );
251    }
252
253    /**
254     * @param ReflectionClass<covariant EntityInterface> $class
255     */
256    public function createFormForResourceCreation(
257        string $pageTitle,
258        ReflectionClass $class,
259        ?BoundedContextId $boundedContextId,
260        ApieContext $context,
261        LayoutEnum $layoutEnum = LayoutEnum::LAYOUT
262    ): ComponentInterface {
263        $filledIn = $context->hasContext(ContextConstants::RAW_CONTENTS)
264            ? $context->getContext(ContextConstants::RAW_CONTENTS)
265            : null;
266        /** @var CsrfTokenProvider $csrfTokenProvider */
267        $csrfTokenProvider = $context->getContext(CsrfTokenProvider::class);
268        $csrfToken = $csrfTokenProvider->createToken();
269
270        $formBuildContext = $this->formComponentFactory->createFormBuildContext($context, Utils::toArray($filledIn));
271        if (EntityUtils::isPolymorphicEntity($class)) {
272            $subClasses = EntityUtils::getDiscriminatorClasses($class);
273            $subForms = [];
274            foreach ($subClasses as $subClass) {
275                $subForms[$subClass->getShortName()] = $this->formComponentFactory->createFromClass(
276                    $subClass,
277                    $formBuildContext
278                );
279            }
280            return $this->createWrapLayout(
281                $pageTitle,
282                $boundedContextId,
283                $context,
284                new PolymorphicForm(
285                    RequestMethod::POST,
286                    $class,
287                    $formBuildContext->getValidationError(),
288                    [], // TODO
289                    $filledIn,
290                    $formBuildContext->isMultipart(),
291                    new Csrf($csrfToken),
292                    new ComponentHashmap($subForms)
293                )
294            );
295        }
296        $form = $this->formComponentFactory->createFromClass($class, $formBuildContext);
297        return $this->createWrapLayout(
298            $pageTitle,
299            $boundedContextId,
300            $context,
301            new Form(
302                RequestMethod::POST,
303                $formBuildContext->getValidationError(),
304                $formBuildContext->getValidationErrorsInContext(),
305                $filledIn,
306                $formBuildContext->isMultipart(),
307                new Csrf($csrfToken),
308                $form
309            ),
310            $layoutEnum
311        );
312    }
313
314    /**
315     * @param ReflectionClass<covariant EntityInterface> $class
316     */
317    public function createFormForResourceModification(
318        string $pageTitle,
319        ReflectionClass $class,
320        ?BoundedContextId $boundedContextId,
321        ApieContext $context,
322        LayoutEnum $layoutEnum = LayoutEnum::LAYOUT
323    ): ComponentInterface {
324        $filledIn = $context->hasContext(ContextConstants::RAW_CONTENTS)
325            ? $context->getContext(ContextConstants::RAW_CONTENTS)
326            : null;
327        /** @var CsrfTokenProvider $csrfTokenProvider */
328        $csrfTokenProvider = $context->getContext(CsrfTokenProvider::class);
329        $csrfToken = $csrfTokenProvider->createToken();
330
331        $formBuildContext = $this->formComponentFactory->createFormBuildContext($context, Utils::toArray($filledIn));
332        if (EntityUtils::isPolymorphicEntity($class)) {
333            $subClasses = EntityUtils::getDiscriminatorClasses($class);
334            $subForms = [];
335            foreach ($subClasses as $subClass) {
336                $subForms[$subClass->getShortName()] = $this->formComponentFactory->createFromClass(
337                    $subClass,
338                    $formBuildContext
339                );
340            }
341            return $this->createWrapLayout(
342                $pageTitle,
343                $boundedContextId,
344                $context,
345                new PolymorphicForm(
346                    RequestMethod::POST,
347                    $class,
348                    $formBuildContext->getValidationError(),
349                    [], // TODO
350                    $filledIn,
351                    $formBuildContext->isMultipart(),
352                    new Csrf($csrfToken),
353                    new ComponentHashmap($subForms)
354                )
355            );
356        }
357        $form = $this->formComponentFactory->createFromClass($class, $formBuildContext);
358        return $this->createWrapLayout(
359            $pageTitle,
360            $boundedContextId,
361            $context,
362            new Form(
363                RequestMethod::POST,
364                $formBuildContext->getValidationError(),
365                $formBuildContext->getValidationErrorsInContext(),
366                $filledIn,
367                $formBuildContext->isMultipart(),
368                new Csrf($csrfToken),
369                $form
370            ),
371            $layoutEnum
372        );
373    }
374}