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