packages/ztd-query-core/src/Shadow/CascadedChildren.php

1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Shadow;
6
7use ZtdQuery\Schema\RowSet;
8use ZtdQuery\Schema\TableDefinition;
9use ZtdQuery\Shadow\Row\RowChange;
10
11/**
12 * The rows of one child table as a cascade leaves them.
13 *
14 * Following a constraint reads the child rows once and then rewrites some and
15 * drops others, and the caller needs both the rows to write back and an
16 * account of what happened to them. Keeping the two together is what makes
17 * the account impossible to disagree with the rows.
18 *
19 * @phpstan-import-type Row from TableDefinition
20 */
21final class CascadedChildren
22{
23    private RowSet $rows;
24
25    private RowSet $deleted;
26
27    /** @var list<RowChange> */
28    private array $updated = [];
29
30    /**
31     * @param array<int, Row> $rows The child rows as they stood
32     */
33    public function __construct(array $rows)
34    {
35        $this->rows = new RowSet($rows);
36        $this->deleted = new RowSet();
37    }
38
39    /**
40     * Answers the child rows as they stand.
41     *
42     * @return array<int, Row> The rows
43     */
44    public function rows(): array
45    {
46        return $this->rows->rows;
47    }
48
49    /**
50     * Writes a row over the one in that position, and records the change.
51     *
52     * @param int $index Position of the row being written over
53     * @param Row $row The row as it should now be
54     */
55    public function replace(int $index, array $row): void
56    {
57        $rows = $this->rows->rows;
58        $this->updated[] = new RowChange($rows[$index], $row);
59        $rows[$index] = $row;
60        $this->rows = new RowSet($rows);
61    }
62
63    /**
64     * Drops the rows in those positions, and records that they went.
65     *
66     * @param list<int> $indexes Positions of the rows that went
67     */
68    public function remove(array $indexes): void
69    {
70        $remaining = [];
71        $deleted = $this->deleted->rows;
72        foreach ($this->rows->rows as $index => $row) {
73            if (in_array($index, $indexes, true)) {
74                $deleted[] = $row;
75                continue;
76            }
77            $remaining[] = $row;
78        }
79        $this->rows = new RowSet($remaining);
80        $this->deleted = new RowSet($deleted);
81    }
82
83    /**
84     * Answers the rows that went.
85     *
86     * @return list<Row> The rows
87     */
88    public function deleted(): array
89    {
90        return array_values($this->deleted->rows);
91    }
92
93    /**
94     * Answers what happened to the rows that stayed.
95     *
96     * @return list<RowChange> The changes
97     */
98    public function updated(): array
99    {
100        return $this->updated;
101    }
102
103    /**
104     * Reports whether the cascade reached this table at all.
105     *
106     * @return bool True when nothing went and nothing changed
107     */
108    public function areUnchanged(): bool
109    {
110        return $this->deleted->rows === [] && $this->updated === [];
111    }
112}
113