Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.21% covered (warning)
86.21%
50 / 58
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SqlMenuRepository
85.96% covered (warning)
85.96%
49 / 57
60.00% covered (warning)
60.00%
3 / 5
22.22
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
 getActiveMenuTree
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 fetchRawRows
53.85% covered (warning)
53.85%
7 / 13
0.00% covered (danger)
0.00%
0 / 1
3.88
 assembleTree
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 mapRowToEntity
90.00% covered (success)
90.00%
18 / 20
0.00% covered (danger)
0.00%
0 / 1
7.05
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\Modules\Menu\Infrastructure\Repository;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Menu\Domain\Model\MenuItem;
12use App\Modules\Menu\Domain\Repository\MenuRepositoryInterface;
13use DateTimeImmutable;
14use PDO;
15
16/**
17 * SQL Menu Repository Implementation.
18 *
19 * Interacts with prefixed database table a_mod_menu_records to construct nested menu trees.
20 *
21 * @package App\Modules\Menu\Infrastructure\Repository
22 */
23final class SqlMenuRepository implements MenuRepositoryInterface
24{
25    /** @var array<string, array<int, MenuItem>> In-memory cache of assembled active menu trees by profile. */
26    private array $treeCache = [];
27
28    /**
29     * SqlMenuRepository constructor.
30     *
31     * @param PDO $pdo Database PDO instance.
32     * @param string $tablePrefix Database table prefix (default 'a_').
33     * @param string $appProfile Default application profile ('admin' or 'client').
34     */
35    public function __construct(
36        private readonly PDO $pdo,
37        private readonly string $tablePrefix = 'a_',
38        private readonly string $appProfile = 'admin'
39    ) {
40    }
41
42    /**
43     * {@inheritdoc}
44     */
45    public function getActiveMenuTree(?string $profile = null): array
46    {
47        $targetProfile = $profile ?? $this->appProfile;
48        if (isset($this->treeCache[$targetProfile])) {
49            return $this->treeCache[$targetProfile];
50        }
51
52        $rows = $this->fetchRawRows($targetProfile);
53        if (empty($rows)) {
54            $this->treeCache[$targetProfile] = [];
55            return [];
56        }
57
58        $this->treeCache[$targetProfile] = $this->assembleTree($rows);
59        return $this->treeCache[$targetProfile];
60    }
61
62    /**
63     * Fetches raw database rows for active menu items for the target profile.
64     *
65     * @param string $profile Target application profile.
66     * @return array<int, array<string, mixed>> Raw rows array.
67     */
68    private function fetchRawRows(string $profile): array
69    {
70        $tableName = $this->tablePrefix . 'mod_menu_records';
71        $sql = "SELECT id, parent_id, type, label, route_url, icon_class, sort_order, is_active,
72                       created_at, updated_at, created_by, owner
73                FROM {$tableName}
74                WHERE is_active = 1
75                  AND (app_profile = :profile OR app_profile = 'all')
76                ORDER BY sort_order ASC, id ASC";
77
78        try {
79            $stmt = $this->pdo->prepare($sql);
80            $stmt->execute(['profile' => $profile]);
81            return $stmt->fetchAll(PDO::FETCH_ASSOC);
82        } catch (\PDOException) {
83            $fallbackSql = "SELECT id, parent_id, type, label, route_url, icon_class, sort_order, is_active,
84                                   created_at, updated_at, created_by, owner
85                            FROM {$tableName}
86                            WHERE is_active = 1
87                            ORDER BY sort_order ASC, id ASC";
88            $stmt = $this->pdo->query($fallbackSql);
89            return $stmt !== false ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
90        }
91    }
92
93    /**
94     * Assembles raw row array into nested MenuItem tree.
95     *
96     * @param array<int, array<string, mixed>> $rows Raw database rows.
97     * @return array<int, MenuItem> Nested MenuItem tree.
98     */
99    private function assembleTree(array $rows): array
100    {
101        /** @var array<int, MenuItem> $itemsById */
102        $itemsById = [];
103        /** @var array<int, int|null> $parentIdsById */
104        $parentIdsById = [];
105
106        foreach ($rows as $row) {
107            $id = (int)$row['id'];
108            $parentId = isset($row['parent_id']) && $row['parent_id'] !== null ? (int)$row['parent_id'] : null;
109            $itemsById[$id] = $this->mapRowToEntity($row, $id, $parentId);
110            $parentIdsById[$id] = $parentId;
111        }
112
113        /** @var array<int, MenuItem> $rootItems */
114        $rootItems = [];
115
116        foreach ($itemsById as $id => $item) {
117            $parentId = $parentIdsById[$id];
118            if ($parentId !== null && isset($itemsById[$parentId])) {
119                $itemsById[$parentId]->addChild($item);
120            } else {
121                $rootItems[] = $item;
122            }
123        }
124
125        return $rootItems;
126    }
127
128    /**
129     * Maps single database row to MenuItem domain entity.
130     *
131     * @param array<string, mixed> $row Database row.
132     * @param int $id Record ID.
133     * @param int|null $parentId Parent ID.
134     * @return MenuItem Mapped MenuItem instance.
135     */
136    private function mapRowToEntity(array $row, int $id, ?int $parentId): MenuItem
137    {
138        $createdAt = isset($row['created_at']) && is_string($row['created_at'])
139            ? new DateTimeImmutable($row['created_at'])
140            : null;
141
142        $updatedAt = isset($row['updated_at']) && is_string($row['updated_at'])
143            ? new DateTimeImmutable($row['updated_at'])
144            : null;
145
146        return new MenuItem(
147            id: $id,
148            parentId: $parentId,
149            type: (string)$row['type'],
150            label: (string)$row['label'],
151            routeUrl: isset($row['route_url']) ? (string)$row['route_url'] : null,
152            iconClass: isset($row['icon_class']) ? (string)$row['icon_class'] : null,
153            sortOrder: (int)$row['sort_order'],
154            isActive: (bool)$row['is_active'],
155            createdAt: $createdAt,
156            updatedAt: $updatedAt,
157            createdBy: (int)($row['created_by'] ?? 1),
158            owner: (int)($row['owner'] ?? 1)
159        );
160    }
161}