Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.09% covered (success)
99.09%
109 / 110
100.00% covered (success)
100.00%
16 / 16
CRAP
100.00% covered (success)
100.00%
1 / 1
ModuleMetadata
100.00% covered (success)
100.00%
109 / 109
100.00% covered (success)
100.00%
16 / 16
36
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requiresSuperuserAccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 hasOwnerScope
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isWritable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isClientModule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasExtensionTabs
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 getExtensionTabs
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 withMergedExtensionTabs
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
6
 isQuickCreateDefault
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDrawerPresentationDefault
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isPagePresentationDefault
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIconClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getAvailableViews
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 fromRow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAvailableForHost
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 withTableName
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Engine\Domain\Model;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Domain\Model\Support\ModuleMetadataHydrator;
12
13/**
14 * Module Metadata Value Object.
15 *
16 * Immutable value object representing a system module catalog record
17 * from a_core_module_records. Drives the central engine logic.
18 *
19 * @package App\Core\Engine\Domain\Model
20 */
21final readonly class ModuleMetadata
22{
23    /** @var string Default fallback module icon. */
24    public const string DEFAULT_ICON = 'bi bi-box-seam';
25
26    public const string GANTT_LABEL = 'Wykres Gantta';
27    public const string GANTT_ICON = 'bi bi-bar-chart-steps';
28
29    /**
30     * ModuleMetadata constructor.
31     *
32     * @param int                              $id                     Module primary key.
33     * @param string                           $type                   Module type (crud|dynamic).
34     * @param string                           $name                   Machine-readable module name (unique slug).
35     * @param string                           $label                  Human-readable display label.
36     * @param string                           $routeUrl               Web URL for the module index page.
37     * @param string                           $apiEndpoint            REST API endpoint for the module list action.
38     * @param string                           $iconClass              Bootstrap Icon CSS class.
39     * @param string                           $version                Semantic version string.
40     * @param bool                             $isActive               Whether the module is currently active.
41     * @param string                           $description            Short description of the module purpose.
42     * @param string                           $tableName              Database table name for this module records.
43     * @param string                           $tableAlias             SQL alias used in query building.
44     * @param string                           $primaryKey             Primary key column name.
45     * @param bool                             $hasList                Whether module supports DataGrid list view.
46     * @param bool                             $hasGrid                Whether module supports Gridstack GRID view.
47     * @param bool                             $hasCalendar            Whether module supports calendar view.
48     * @param bool                             $hasKanban              Whether module supports Kanban board view.
49     * @param string|null                      $kanbanFieldName        Kanban grouping field name.
50     * @param bool                             $hasGantt               Whether module supports Gantt chart view.
51     * @param bool                             $hasInventory           Whether module supports inventory table.
52     * @param string                           $defaultView            Default initial view ('list' or 'grid').
53     * @param string                           $defaultCreateMode      Default create mode ('quick' or 'standard').
54     * @param string                           $createPresentationMode Default presentation ('drawer'|'modal').
55     * @param array<int, array<string, mixed>> $extensionTabs          Registered spatial extension tabs.
56     * @param list<int|string>                 $allowedHosts           Target hosts where module is enabled.
57     */
58    public function __construct(
59        public readonly int    $id,
60        public readonly string $type,
61        public readonly string $name,
62        public readonly string $label,
63        public readonly string $routeUrl,
64        public readonly string $apiEndpoint,
65        public readonly string $iconClass,
66        public readonly string $version,
67        public readonly bool   $isActive,
68        public readonly string $description,
69        public readonly string $tableName,
70        public readonly string $tableAlias,
71        public readonly string $primaryKey,
72        public readonly bool   $hasList = true,
73        public readonly bool   $hasGrid = true,
74        public readonly bool   $hasCalendar = false,
75        public readonly bool   $hasKanban = false,
76        public readonly ?string $kanbanFieldName = null,
77        public readonly bool   $hasGantt = false,
78        public readonly bool   $hasInventory = false,
79        public readonly string $defaultView = 'list',
80        public readonly string $defaultCreateMode = 'standard',
81        public readonly string $createPresentationMode = 'drawer',
82        public readonly array  $extensionTabs = [],
83        public readonly array  $allowedHosts = [],
84    ) {
85    }
86
87    /**
88     * Checks whether this module requires superuser access.
89     *
90     * @return bool True if superuser-only access is required.
91     */
92    public function requiresSuperuserAccess(): bool
93    {
94        return $this->type === 'dynamic' && $this->name !== 'dashboard';
95    }
96
97    /**
98     * Checks whether this module applies owner-based record scoping.
99     *
100     * @return bool True if owner scope should be applied.
101     */
102    public function hasOwnerScope(): bool
103    {
104        return $this->type === 'crud';
105    }
106
107    /**
108     * Checks whether write operations (create/update/delete) are allowed.
109     *
110     * @return bool True if write operations are permitted.
111     */
112    public function isWritable(): bool
113    {
114        return $this->type === 'crud';
115    }
116
117    /**
118     * Checks whether this module belongs to client SaaS or tenant configuration domain.
119     *
120     * @return bool True if module database table uses client prefix 'c_'.
121     */
122    public function isClientModule(): bool
123    {
124        return str_starts_with($this->tableName, 'c_');
125    }
126
127    /**
128     * Checks whether this module has active spatial extension tabs in block 8/12.
129     *
130     * @param string $viewMode Optional view mode ('detail', 'edit', 'create').
131     * @return bool True if extension tabs are present for the given view mode.
132     */
133    public function hasExtensionTabs(string $viewMode = ''): bool
134    {
135        if ($viewMode === '') {
136            return !empty($this->extensionTabs);
137        }
138        foreach ($this->extensionTabs as $tab) {
139            $views = $tab['views'] ?? ['all'];
140            if (in_array($viewMode, $views, true) || in_array('all', $views, true)) {
141                return true;
142            }
143        }
144        return false;
145    }
146
147    /**
148     * Returns extension tabs filtered by view mode.
149     *
150     * @param string $viewMode Optional view mode filter ('detail', 'edit', 'create').
151     * @return array<int, array<string, mixed>> List of matching extension tabs.
152     */
153    public function getExtensionTabs(string $viewMode = ''): array
154    {
155        if ($viewMode === '') {
156            return $this->extensionTabs;
157        }
158        return array_values(array_filter($this->extensionTabs, static function (array $tab) use ($viewMode): bool {
159            $views = $tab['views'] ?? ['all'];
160            return in_array($viewMode, $views, true) || in_array('all', $views, true);
161        }));
162    }
163
164    /**
165     * Returns a clone of this metadata instance with additional extension tabs merged.
166     *
167     * @param array<int, array<string, mixed>> $additionalTabs Additional extension tabs.
168     * @return self New instance with merged tabs.
169     */
170    public function withMergedExtensionTabs(array $additionalTabs): self
171    {
172        if ($additionalTabs === []) {
173            return $this;
174        }
175
176        $current = $this->extensionTabs;
177        $timelineTab = null;
178        $tabMap = [];
179
180        foreach ($current as $tab) {
181            $tid = (string) ($tab['id'] ?? '');
182            if ($tid === 'timeline') {
183                $timelineTab = $tab;
184            } else {
185                $tabMap[$tid] = $tab;
186            }
187        }
188
189        foreach ($additionalTabs as $addTab) {
190            $tid = (string) ($addTab['id'] ?? '');
191            $tabMap[$tid] = $addTab;
192        }
193
194        $merged = array_values($tabMap);
195        if ($timelineTab !== null) {
196            $merged[] = $timelineTab;
197        }
198
199        return new self(
200            id:                     $this->id,
201            type:                   $this->type,
202            name:                   $this->name,
203            label:                  $this->label,
204            routeUrl:               $this->routeUrl,
205            apiEndpoint:            $this->apiEndpoint,
206            iconClass:              $this->iconClass,
207            version:                $this->version,
208            isActive:               $this->isActive,
209            description:            $this->description,
210            tableName:              $this->tableName,
211            tableAlias:             $this->tableAlias,
212            primaryKey:             $this->primaryKey,
213            hasList:                $this->hasList,
214            hasGrid:                $this->hasGrid,
215            hasCalendar:            $this->hasCalendar,
216            hasKanban:              $this->hasKanban,
217            kanbanFieldName:        $this->kanbanFieldName,
218            hasGantt:               $this->hasGantt,
219            hasInventory:           $this->hasInventory,
220            defaultView:            $this->defaultView,
221            defaultCreateMode:      $this->defaultCreateMode,
222            createPresentationMode: $this->createPresentationMode,
223            extensionTabs:          $merged,
224            allowedHosts:           $this->allowedHosts,
225        );
226    }
227
228    /**
229     * Checks whether this module opens create forms in quick mode by default.
230     *
231     * @return bool True if default create mode is quick.
232     */
233    public function isQuickCreateDefault(): bool
234    {
235        return $this->defaultCreateMode === 'quick';
236    }
237
238    /**
239     * Checks whether this module presents create forms in a docked drawer/sidebar.
240     *
241     * @return bool True if presentation mode is drawer.
242     */
243    public function isDrawerPresentationDefault(): bool
244    {
245        return $this->createPresentationMode === 'drawer';
246    }
247
248    /**
249     * Checks whether this module presents create forms in a dedicated full page.
250     *
251     * @return bool True if presentation mode is page.
252     */
253    public function isPagePresentationDefault(): bool
254    {
255        return $this->createPresentationMode === 'page';
256    }
257
258    /**
259     * Returns a valid icon class for the module with default fallback.
260     *
261     * @return string Bootstrap icon CSS class.
262     */
263    public function getIconClass(): string
264    {
265        return $this->iconClass !== '' ? $this->iconClass : self::DEFAULT_ICON;
266    }
267
268    /**
269     * Obtains list of available view definitions for this module (list, grid, calendar, kanban, gantt).
270     *
271     * @return array<int, array{id: string, label: string, icon: string}> Available view definitions.
272     */
273    public function getAvailableViews(): array
274    {
275        $views = [];
276        if ($this->hasList) {
277            $views[] = ['id' => 'list', 'label' => 'Lista', 'icon' => 'bi bi-list-ul'];
278        }
279        if ($this->hasGrid) {
280            $views[] = ['id' => 'grid', 'label' => 'Siatka', 'icon' => 'bi bi-grid-3x3-gap-fill'];
281        }
282        if ($this->hasCalendar) {
283            $views[] = ['id' => 'calendar', 'label' => 'Kalendarz', 'icon' => 'bi bi-calendar3'];
284        }
285        if ($this->hasKanban) {
286            $views[] = ['id' => 'kanban', 'label' => 'Kanban', 'icon' => 'bi bi-kanban'];
287        }
288        if ($this->hasGantt) {
289            $views[] = ['id' => 'gantt', 'label' => self::GANTT_LABEL, 'icon' => self::GANTT_ICON];
290        }
291
292        return $views;
293    }
294
295    /**
296     * Creates a ModuleMetadata instance from a raw database row array.
297     *
298     * @param array<string, mixed> $row Raw database row.
299     * @return self Hydrated ModuleMetadata value object.
300     */
301    public static function fromRow(array $row): self
302    {
303        return (new ModuleMetadataHydrator())->hydrateFromRow($row);
304    }
305
306    /**
307     * Checks whether this module is available for the given host ID.
308     *
309     * @param int $hostId Target host identifier (1=app_admin, 2=app_client, etc.).
310     * @return bool True if module is enabled on this host.
311     */
312    public function isAvailableForHost(int $hostId): bool
313    {
314        if ($this->allowedHosts === []) {
315            return true;
316        }
317
318        return in_array($hostId, $this->allowedHosts, true)
319            || in_array((string) $hostId, $this->allowedHosts, true);
320    }
321
322    /**
323     * Clones the module metadata with a different physical database table name.
324     *
325     * @param string $tableName New physical database table name.
326     * @return self New immutable instance with the updated table name.
327     */
328    public function withTableName(string $tableName): self
329    {
330        return new self(
331            $this->id,
332            $this->type,
333            $this->name,
334            $this->label,
335            $this->routeUrl,
336            $this->apiEndpoint,
337            $this->iconClass,
338            $this->version,
339            $this->isActive,
340            $this->description,
341            $tableName,
342            $this->tableAlias,
343            $this->primaryKey,
344            $this->hasList,
345            $this->hasGrid,
346            $this->hasCalendar,
347            $this->hasKanban,
348            $this->kanbanFieldName,
349            $this->hasGantt,
350            $this->hasInventory,
351            $this->defaultView,
352            $this->defaultCreateMode,
353            $this->createPresentationMode,
354            $this->extensionTabs,
355            $this->allowedHosts,
356        );
357    }
358}
359