• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

AJenbo / agcms / 20972217862

13 Jan 2026 08:53PM UTC coverage: 53.678% (+0.1%) from 53.541%
20972217862

Pull #74

github

web-flow
Merge 4fdfac7ee into 498ff829e
Pull Request #74: Add PHP versions 8.4 to 8.5 to CI matrix

248 of 345 new or added lines in 40 files covered. (71.88%)

6 existing lines in 5 files now uncovered.

2780 of 5179 relevant lines covered (53.68%)

13.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.26
/application/inc/Models/Table.php
1
<?php
2

3
namespace App\Models;
4

5
use App\DTO\TableColumn;
6
use App\Enums\ColumnType;
7
use App\Exceptions\Exception;
8
use App\Services\DbService;
9
use App\Services\OrmService;
10

11
class Table extends AbstractEntity
12
{
13
    /** Table name in database. */
14
    public const TABLE_NAME = 'lists';
15

16
    // Backed by DB
17

18
    /** @var int Parent page id. */
19
    private int $pageId;
20

21
    /** @var string Table caption. */
22
    private string $title = '';
23

24
    /** @var int The default column to order by, starting from 0. */
25
    private int $orderBy = 0;
26

27
    /** @var bool If rows can be linked to pages. */
28
    private bool $hasLinks = false;
29

30
    /** @var bool Indicate if there is a column with sales prices. */
31
    private bool $hasPrices = false;
32

33
    // Runtime
34

35
    /** @var array<int, TableColumn> Decoded column data. */
36
    private array $columns = [];
37

38
    public function __construct(array $data = [])
39
    {
40
        $this->setPageId(valint($data['page_id']))
6✔
41
            ->setTitle(valstring($data['title']))
6✔
42
            ->setColumnData(valstring($data['column_data']))
6✔
43
            ->setOrderBy(valint($data['order_by']))
6✔
44
            ->setHasLinks(valbool($data['has_links']))
6✔
45
            ->setId(intOrNull($data['id'] ?? null));
6✔
46
    }
47

48
    public static function mapFromDB(array $data): array
49
    {
50
        $columnSortings = explode('<', $data['sorts']);
6✔
51
        $columnSortings = array_map('intval', $columnSortings);
6✔
52
        $columnTypes = explode('<', $data['cells']);
6✔
53
        $columnTypes = array_map('intval', $columnTypes);
6✔
54
        $columnTitles = explode('<', $data['cell_names']);
6✔
55
        $columnTitles = array_map('html_entity_decode', $columnTitles);
6✔
56

57
        $orm = app(OrmService::class);
6✔
58

59
        $columns = [];
6✔
60
        foreach ($columnTitles as $key => $title) {
6✔
61
            $sorting = $columnSortings[$key] ?? 0;
6✔
62
            $options = [];
6✔
63
            if ($sorting) {
6✔
64
                $tablesort = $orm->getOne(CustomSorting::class, $sorting);
6✔
65
                if ($tablesort) {
6✔
66
                    $options = $tablesort->getItems();
6✔
67
                }
68
            }
69

70
            $columns[] = [
6✔
71
                'title'   => $title,
72
                'type'    => $columnTypes[$key] ?? 0,
6✔
73
                'sorting' => $sorting,
74
                'options' => $options,
75
            ];
76
        }
77
        $columns = json_encode($columns, JSON_THROW_ON_ERROR);
6✔
78

79
        return [
80
            'id'          => $data['id'],
6✔
81
            'page_id'     => $data['page_id'],
6✔
82
            'title'       => $data['title'],
6✔
83
            'column_data' => $columns,
84
            'order_by'    => $data['sort'],
6✔
85
            'has_links'   => (bool)$data['link'],
6✔
86
        ];
87
    }
88

89
    // Getters and setters
90

91
    /**
92
     * Set parent page id.
93
     *
94
     * @return $this
95
     */
96
    private function setPageId(int $pageId): self
97
    {
98
        $this->pageId = $pageId;
6✔
99

100
        return $this;
6✔
101
    }
102

103
    /**
104
     * Get page id.
105
     */
106
    public function getPageId(): int
107
    {
108
        return $this->pageId;
×
109
    }
110

111
    /**
112
     * Set the table caption.
113
     *
114
     * @return $this
115
     */
116
    public function setTitle(string $title): self
117
    {
118
        $this->title = $title;
6✔
119

120
        return $this;
6✔
121
    }
122

123
    /**
124
     * Get the table caption.
125
     */
126
    public function getTitle(): string
127
    {
128
        return $this->title;
4✔
129
    }
130

131
    /**
132
     * Set the column data.
133
     *
134
     * @param string $columnData Array encoded as JSON
135
     *
136
     * @return $this
137
     */
138
    public function setColumnData(string $columnData): self
139
    {
140
        $columns = json_decode($columnData, true);
6✔
141

142
        if (!is_array($columns)) {
6✔
NEW
143
            return $this;
×
144
        }
145
        foreach ($columns as $columnData) {
6✔
146
            if (!is_array($columnData)) {
6✔
NEW
147
                continue;
×
148
            }
149
            $rawOptions = $columnData['options'] ?? [];
6✔
150
            if (!is_array($rawOptions)) {
6✔
NEW
151
                continue;
×
152
            }
153
            $options = [];
6✔
154
            foreach ($rawOptions as $key => $rawOption) {
6✔
155
                $options[$key] = is_string($rawOption) ? $rawOption : '';
6✔
156
            }
157
            $column = new TableColumn(
6✔
158
                valstring($columnData['title']),
6✔
159
                ColumnType::from(valint($columnData['type'])),
6✔
160
                valint($columnData['sorting']),
6✔
161
                $options,
162
            );
163

164
            if (in_array($column->type, [ColumnType::Price, ColumnType::SalesPrice], true)) {
6✔
165
                $this->hasPrices = true;
6✔
166
            }
167

168
            $this->columns[] = $column;
6✔
169
        }
170

171
        return $this;
6✔
172
    }
173

174
    /**
175
     * Get tabel colum structure.
176
     *
177
     * @return array<int, TableColumn>
178
     */
179
    public function getColumns(): array
180
    {
181
        return $this->columns;
5✔
182
    }
183

184
    /**
185
     * Set the default sorting column.
186
     *
187
     * @param int $orderBy First column = 0
188
     *
189
     * @return $this
190
     */
191
    private function setOrderBy(int $orderBy): self
192
    {
193
        $this->orderBy = $orderBy;
6✔
194

195
        return $this;
6✔
196
    }
197

198
    /**
199
     * Get the default sort by column (zero index).
200
     */
201
    public function getOrderBy(): int
202
    {
203
        return $this->orderBy;
1✔
204
    }
205

206
    /**
207
     * Allow rows to link to pages.
208
     *
209
     * @return $this
210
     */
211
    private function setHasLinks(bool $hasLinks): self
212
    {
213
        $this->hasLinks = $hasLinks;
6✔
214

215
        return $this;
6✔
216
    }
217

218
    /**
219
     * Allow rows to link to pages.
220
     */
221
    public function hasLinks(): bool
222
    {
223
        return $this->hasLinks;
5✔
224
    }
225

226
    // ORM related functions
227

228
    /**
229
     * Indicate if there is a column with sales prices.
230
     */
231
    public function hasPrices(): bool
232
    {
233
        return $this->hasPrices;
4✔
234
    }
235

236
    /**
237
     * Get table rows.
238
     *
239
     * @return array<int, array<int|string, mixed>>
240
     */
241
    public function getRows(?int $orderBy = null): array
242
    {
243
        $db = app(DbService::class);
4✔
244

245
        $dataRows = $db->fetchArray(
4✔
246
            '
247
            SELECT *
248
            FROM `list_rows`
249
            WHERE `list_id` = ' . $this->getId()
4✔
250
        );
251
        $db->addLoadedTable('list_rows');
4✔
252

253
        $orm = app(OrmService::class);
4✔
254

255
        // Cells are indexed by id, this is needed for sorting the rows
256
        $rows = [];
4✔
257
        foreach ($dataRows as $rowData) {
4✔
258
            $page = null;
4✔
259
            if ($this->hasLinks() && $rowData['link']) {
4✔
260
                $page = $orm->getOne(Page::class, (int)$rowData['link']);
4✔
261
            }
262
            $cells = explode('<', $rowData['cells']);
4✔
263
            $cells = array_map('html_entity_decode', $cells);
4✔
264

265
            $row = [
4✔
266
                'id'   => (int)$rowData['id'],
4✔
267
                'page' => $page,
268
            ];
269

270
            foreach ($this->columns as $key => $column) {
4✔
271
                $row[$key] = $cells[$key] ?? '';
4✔
272
                if ($column->type !== ColumnType::String) {
4✔
273
                    $row[$key] = (int)$row[$key];
4✔
274
                }
275
            }
276

277
            $rows[] = $row;
4✔
278
        }
279

280
        return $this->orderRows($rows, $orderBy);
4✔
281
    }
282

283
    /**
284
     * Add a new row to the table.
285
     *
286
     * @param string[] $cells
287
     *
288
     * @return int Id of the new row
289
     */
290
    public function addRow(array $cells, ?int $link = null): int
291
    {
292
        $cells = array_map('htmlspecialchars', $cells);
×
293
        $cells = implode('<', $cells);
×
294

295
        $db = app(DbService::class);
×
296

297
        return $db->query(
×
298
            '
299
            INSERT INTO `list_rows`(`list_id`, `cells`, `link`)
300
            VALUES (' . $this->getId() . ', ' . $db->quote($cells) . ', ' . (null === $link ? 'NULL' : $link) . ')
×
301
            '
302
        );
303
    }
304

305
    /**
306
     * Update an existing row.
307
     *
308
     * @param string[] $cells
309
     */
310
    public function updateRow(int $rowId, array $cells, ?int $link = null): void
311
    {
312
        $cells = array_map('htmlspecialchars', $cells);
×
313
        $cells = implode('<', $cells);
×
314

315
        $db = app(DbService::class);
×
316

317
        $db->query(
×
318
            '
319
            UPDATE `list_rows` SET
320
                `cells` = ' . $db->quote($cells) . ',
×
321
                `link` = ' . (null === $link ? 'NULL' : $link) . '
×
322
            WHERE list_id = ' . $this->getId() . '
×
323
              AND id = ' . $rowId
324
        );
325
    }
326

327
    /**
328
     * Remove a row from the table.
329
     */
330
    public function removeRow(int $rowId): void
331
    {
332
        app(DbService::class)->query('DELETE FROM `list_rows` WHERE list_id = ' . $this->id . ' AND `id` = ' . $rowId);
×
333
    }
334

335
    /**
336
     * Sort a 2D array based on a custome sort order.
337
     *
338
     * @template T of array<int|string, mixed>
339
     *
340
     * @param array<int, T> $rows
341
     *
342
     * @return list<T>
343
     */
344
    private function orderRows(array $rows, ?int $orderBy = null): array
345
    {
346
        $orderBy ??= $this->orderBy;
4✔
347
        $orderBy = max($orderBy, 0);
4✔
348
        $orderBy = min($orderBy, count($this->columns) - 1);
4✔
349

350
        if (!$this->columns[$orderBy]->sorting) {
4✔
351
            return arrayNatsort($rows, (string)$orderBy); // Alpha numeric
4✔
352
        }
353

354
        $options = $this->columns[$orderBy]->options;
1✔
355
        $options = array_flip($options);
1✔
356

357
        $tempArray = [];
1✔
358
        foreach ($rows as $rowKey => $row) {
1✔
359
            $optionKey = $row[$orderBy];
1✔
360
            if (!is_string($optionKey) && !is_int($optionKey)) {
1✔
NEW
361
                throw new Exception('Invalid order key: ' . gettype($optionKey));
×
362
            }
363
            $tempArray[$rowKey] = $options[$optionKey] ?? -1;
1✔
364
        }
365

366
        asort($tempArray);
1✔
367

368
        $result = [];
1✔
369
        foreach (array_keys($tempArray) as $rowKey) {
1✔
370
            $result[] = $rows[$rowKey];
1✔
371
        }
372

373
        return $result;
1✔
374
    }
375

376
    /**
377
     * Get the page this table belongs to.
378
     *
379
     * @throws Exception
380
     */
381
    public function getPage(): Page
382
    {
383
        $page = app(OrmService::class)->getOne(Page::class, $this->pageId);
1✔
384
        if (!$page) {
1✔
385
            throw new Exception(_('Page not found.'));
×
386
        }
387

388
        return $page;
1✔
389
    }
390

391
    public function getDbArray(): array
392
    {
393
        $columnSortings = [];
×
394
        $columnTypes = [];
×
395
        $columnTitles = [];
×
396
        foreach ($this->columns as $column) {
×
397
            $columnSortings[] = $column->sorting;
×
398
            $columnTypes[] = $column->type->value;
×
399
            $columnTitles[] = $column->title;
×
400
        }
401

402
        $columnSortings = implode('<', $columnSortings);
×
403
        $columnTypes = implode('<', $columnTypes);
×
404
        $columnTitles = array_map('htmlspecialchars', $columnTitles);
×
405
        $columnTitles = implode('<', $columnTitles);
×
406

407
        $db = app(DbService::class);
×
408

409
        return [
410
            'page_id'    => (string)$this->pageId,
×
411
            'title'      => $db->quote($this->title),
×
412
            'sorts'      => $db->quote($columnSortings),
×
413
            'cells'      => $db->quote($columnTypes),
×
414
            'cell_names' => $db->quote($columnTitles),
×
415
            'sort'       => (string)$this->orderBy,
×
416
            'link'       => (string)(int)$this->hasLinks,
×
417
        ];
418
    }
419
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc