1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Data\Persistence\Array_\Db;
6:
7: use Atk4\Data\Exception;
8: use Atk4\Data\Model;
9:
10: class Table
11: {
12: /** @var string Immutable */
13: private $tableName;
14: /** @var array<string, string> */
15: private $columnNames = [];
16: /** @var array<int, Row> */
17: private $rows = [];
18:
19: public function __construct(string $tableName)
20: {
21: $this->tableName = $tableName;
22: }
23:
24: /**
25: * @return array<string, mixed>
26: */
27: public function __debugInfo(): array
28: {
29: return [
30: 'table_name' => $this->getTableName(),
31: 'column_names' => $this->getColumnNames(),
32: 'row_count' => count($this->rows),
33: ];
34: }
35:
36: /**
37: * @param string $name
38: */
39: protected function assertValidIdentifier($name): void
40: {
41: if (!is_string($name) || $name === '' || is_numeric($name)) { // @phpstan-ignore-line
42: throw (new Exception('Name must be a non-empty non-numeric string'))
43: ->addMoreInfo('name', $name);
44: }
45: }
46:
47: /**
48: * @param mixed $value
49: */
50: protected function assertValidValue($value): void
51: {
52: if ($value instanceof self || $value instanceof Row) {
53: throw new Exception('Value cannot be an ' . get_class($value) . ' object');
54: } elseif (!is_scalar($value) && $value !== null) {
55: throw (new Exception('Value must be scalar'))
56: ->addMoreInfo('value', $value);
57: }
58: }
59:
60: public function getTableName(): string
61: {
62: return $this->tableName;
63: }
64:
65: public function hasColumnName(string $columnName): bool
66: {
67: return isset($this->columnNames[$columnName]);
68: }
69:
70: public function assertHasColumnName(string $columnName): void
71: {
72: if (!isset($this->columnNames[$columnName])) {
73: throw (new Exception('Column name does not exist'))
74: ->addMoreInfo('table_name', $this->getTableName())
75: ->addMoreInfo('column_name', $columnName);
76: }
77: }
78:
79: /**
80: * @return $this
81: */
82: public function addColumnName(string $columnName): self
83: {
84: $this->assertValidIdentifier($columnName);
85: if (isset($this->columnNames[$columnName])) {
86: throw (new Exception('Column name is already present'))
87: ->addMoreInfo('table_name', $this->getTableName())
88: ->addMoreInfo('column_name', $columnName);
89: }
90:
91: $this->columnNames[$columnName] = $columnName;
92:
93: foreach ($this->getRows() as $row) {
94: \Closure::bind(static function () use ($row, $columnName) {
95: $row->initValue($columnName);
96: }, null, $row)();
97: }
98:
99: return $this;
100: }
101:
102: /**
103: * @return array<int, string>
104: */
105: public function getColumnNames(): array
106: {
107: return array_values($this->columnNames);
108: }
109:
110: public function hasRow(int $rowIndex): bool
111: {
112: return isset($this->rows[$rowIndex]);
113: }
114:
115: public function getRow(int $rowIndex): Row
116: {
117: if (!isset($this->rows[$rowIndex])) {
118: throw (new Exception('Row with given index was not found'))
119: ->addMoreInfo('table_name', $this->getTableName())
120: ->addMoreInfo('row_index', $rowIndex);
121: }
122:
123: return $this->rows[$rowIndex];
124: }
125:
126: /**
127: * @param class-string<Row> $rowClass
128: * @param array<string, mixed> $rowData
129: */
130: public function addRow(string $rowClass, array $rowData): Row
131: {
132: $that = $this;
133: $columnNames = $this->getColumnNames();
134: /** @var Row $row */
135: $row = \Closure::bind(static function () use ($that, $rowClass, $columnNames) {
136: $row = new $rowClass($that);
137: foreach ($columnNames as $columnName) {
138: $row->initValue($columnName);
139: }
140:
141: return $row;
142: }, null, $rowClass)();
143: $this->rows[$row->getRowIndex()] = $row;
144:
145: foreach ($rowData as $columnName => $value) {
146: if (!$this->hasColumnName($columnName)) {
147: $this->addColumnName($columnName);
148: }
149: }
150:
151: $row->updateValues($rowData);
152:
153: return $row;
154: }
155:
156: public function deleteRow(Row $row): void
157: {
158: \Closure::bind(static function () use ($row) {
159: $row->beforeDelete();
160: }, null, $row)();
161:
162: unset($this->rows[$row->getRowIndex()]);
163: }
164:
165: /**
166: * @return \Traversable<Row>
167: */
168: public function getRows(): \Traversable
169: {
170: return new \ArrayIterator($this->rows);
171: }
172:
173: /**
174: * @param array<string, mixed> $newRowData
175: */
176: protected function beforeValuesSet(Row $childRow, $newRowData): void
177: {
178: foreach ($newRowData as $columnName => $newValue) {
179: $this->assertValidValue($newValue);
180:
181: // update index here
182: }
183: }
184:
185: /**
186: * TODO rewrite with hash index support.
187: *
188: * @param mixed $idRaw
189: */
190: public function getRowById(Model $model, $idRaw): ?Row
191: {
192: foreach ($this->getRows() as $row) {
193: if ($row->getValue($model->getField($model->idField)->getPersistenceName()) === $idRaw) {
194: return $row;
195: }
196: }
197:
198: return null;
199: }
200: }
201: