1 | <?php |
||||||
2 | |||||||
3 | namespace HexMakina\Crudites; |
||||||
4 | |||||||
5 | use HexMakina\BlackBox\Database\ConnectionInterface; |
||||||
6 | use HexMakina\BlackBox\Database\RowInterface; |
||||||
7 | use HexMakina\BlackBox\Database\ResultInterface; |
||||||
8 | |||||||
9 | use HexMakina\Crudites\CruditesException; |
||||||
10 | |||||||
11 | class Row implements RowInterface |
||||||
12 | { |
||||||
13 | private string $table; |
||||||
14 | |||||||
15 | private ConnectionInterface $connection; |
||||||
16 | |||||||
17 | /** @var array<int|string,mixed>|null $load from database */ |
||||||
18 | private ?array $load = null; |
||||||
19 | |||||||
20 | /** @var array<int|string,mixed> $fresh from the constructor */ |
||||||
21 | private array $fresh = []; |
||||||
22 | |||||||
23 | /** @var array<int|string,mixed> $alterations during lifecycle */ |
||||||
24 | private array $alterations = []; |
||||||
25 | |||||||
26 | /** @var ResultInterface|null $result the result from the last executed query */ |
||||||
27 | private ?ResultInterface $result = null; |
||||||
28 | |||||||
29 | |||||||
30 | /** @param array<string,mixed> $datass */ |
||||||
31 | /** |
||||||
32 | * Represents a row in a table. |
||||||
33 | * |
||||||
34 | * @param ConnectionInterface $connection The database connection. |
||||||
35 | * @param string $table The table name. |
||||||
36 | * @param array $fresh The fresh data for the row. |
||||||
37 | */ |
||||||
38 | public function __construct(ConnectionInterface $connection, string $table, array $fresh = []) |
||||||
39 | { |
||||||
40 | $this->connection = $connection; |
||||||
41 | $this->table = $table; |
||||||
42 | $this->fresh = $fresh; |
||||||
43 | } |
||||||
44 | |||||||
45 | // property overloading |
||||||
46 | public function __get($name) |
||||||
47 | { |
||||||
48 | return $this->alterations[$name] |
||||||
49 | ?? $this->fresh[$name] |
||||||
50 | ?? $this->load[$name] |
||||||
51 | ?? null; |
||||||
52 | } |
||||||
53 | |||||||
54 | public function __isset($name) |
||||||
55 | { |
||||||
56 | return isset($this->alterations[$name]) |
||||||
57 | || isset($this->fresh[$name]) |
||||||
58 | || isset($this->load[$name]); |
||||||
59 | } |
||||||
60 | |||||||
61 | public function __set(string $name, $value = null) |
||||||
62 | { |
||||||
63 | if ( |
||||||
64 | $value === $this->$name |
||||||
65 | || !$this->connection->schema()->hasColumn($this->table, $name) |
||||||
66 | ) { |
||||||
67 | return; |
||||||
68 | } |
||||||
69 | |||||||
70 | $attributes = $this->connection->schema()->attributes($this->table, $name); |
||||||
71 | |||||||
72 | // skip auto_incremented columns |
||||||
73 | if ($attributes->isAuto()) { |
||||||
74 | return; |
||||||
75 | } |
||||||
76 | |||||||
77 | // Replace empty strings with null if the column is nullable |
||||||
78 | if (trim((string)$value) === '' && $attributes->nullable()) { |
||||||
79 | $value = null; |
||||||
80 | } |
||||||
81 | |||||||
82 | // checks for changes with loaded data. using == instead of === is risky but needed |
||||||
83 | if ($this->isNew() || $this->load[$name] != $value) { |
||||||
84 | $this->alterations[$name] = $value; |
||||||
85 | } |
||||||
86 | |||||||
87 | } |
||||||
88 | |||||||
89 | public function __unset($name) |
||||||
90 | { |
||||||
91 | unset($this->alterations[$name]); |
||||||
92 | } |
||||||
93 | |||||||
94 | // output |
||||||
95 | public function __toString() |
||||||
96 | { |
||||||
97 | return PHP_EOL . 'load: ' |
||||||
98 | . json_encode($this->load) |
||||||
99 | . PHP_EOL . 'alterations: ' |
||||||
100 | . json_encode(array_keys($this->alterations)); |
||||||
101 | } |
||||||
102 | |||||||
103 | public function __debugInfo() |
||||||
104 | { |
||||||
105 | return [ |
||||||
106 | 'table' => $this->table, |
||||||
107 | 'load' => $this->load, |
||||||
108 | 'fresh' => $this->fresh, |
||||||
109 | 'alterations' => $this->alterations, |
||||||
110 | 'result' => $this->result, |
||||||
111 | ]; |
||||||
112 | } |
||||||
113 | |||||||
114 | public function import(array $dat_ass): RowInterface |
||||||
115 | { |
||||||
116 | foreach ($dat_ass as $k => $v) { |
||||||
117 | $this->$k = $v; |
||||||
118 | } |
||||||
119 | |||||||
120 | return $this; |
||||||
121 | } |
||||||
122 | |||||||
123 | |||||||
124 | public function export(): array |
||||||
125 | { |
||||||
126 | return array_merge((array)$this->load, $this->fresh, $this->alterations); |
||||||
127 | } |
||||||
128 | |||||||
129 | |||||||
130 | |||||||
131 | public function table(): string |
||||||
132 | { |
||||||
133 | return $this->table; |
||||||
134 | } |
||||||
135 | |||||||
136 | public function isNew(): bool |
||||||
137 | { |
||||||
138 | return empty($this->load); |
||||||
139 | } |
||||||
140 | |||||||
141 | public function isAltered(): bool |
||||||
142 | { |
||||||
143 | return !empty($this->alterations); |
||||||
144 | } |
||||||
145 | |||||||
146 | public function load(?array $datass = null): Rowinterface |
||||||
147 | { |
||||||
148 | $unique_match = $this->connection->schema()->matchUniqueness($this->table, $datass ?? $this->export()); |
||||||
149 | if (empty($unique_match)) { |
||||||
150 | return $this; |
||||||
151 | } |
||||||
152 | |||||||
153 | $query = $this->connection->schema()->select($this->table); |
||||||
154 | $query->where()->andFields($unique_match, $this->table, '='); |
||||||
155 | |||||||
156 | try{ |
||||||
157 | $this->result = $this->connection->result($query); |
||||||
158 | } |
||||||
159 | catch(\Throwable $t){ |
||||||
160 | } |
||||||
161 | |||||||
162 | $res = $this->result->retOne(\PDO::FETCH_ASSOC); |
||||||
163 | $this->load = $res === false ? null : $res; |
||||||
164 | |||||||
165 | return $this; |
||||||
166 | } |
||||||
167 | |||||||
168 | /** |
||||||
169 | * @return array<string,string> an array of errors, column name => message |
||||||
170 | */ |
||||||
171 | public function save(): array |
||||||
172 | { |
||||||
173 | if (!$this->isNew() && !$this->isAltered()) { // existing record with no alterations |
||||||
174 | return []; |
||||||
175 | } |
||||||
176 | |||||||
177 | if (!empty($errors = $this->validate())) { // Table level validation |
||||||
178 | return $errors; |
||||||
179 | } |
||||||
180 | try { |
||||||
181 | if ($this->isNew()) { |
||||||
182 | $this->create($this->connection); |
||||||
0 ignored issues
–
show
|
|||||||
183 | } else { |
||||||
184 | $this->update($this->connection); |
||||||
0 ignored issues
–
show
The call to
HexMakina\Crudites\Row::update() has too many arguments starting with $this->connection .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above. ![]() |
|||||||
185 | } |
||||||
186 | } catch (CruditesException $cruditesException) { |
||||||
187 | return [$this->table => $cruditesException->getMessage()]; |
||||||
188 | } |
||||||
189 | |||||||
190 | return []; |
||||||
191 | } |
||||||
192 | |||||||
193 | |||||||
194 | /** |
||||||
195 | * Deletes the current record from the database. |
||||||
196 | * |
||||||
197 | * @return bool true if the record was deleted, false otherwise. |
||||||
198 | * @throws CruditesException if a unique match is not found. |
||||||
199 | */ |
||||||
200 | public function wipe(): bool |
||||||
201 | { |
||||||
202 | $datass = $this->load ?? $this->fresh ?? $this->alterations; |
||||||
203 | |||||||
204 | // need The Primary key, then you can wipe at ease |
||||||
205 | if (!empty($pk_match = $this->connection->schema()->matchPrimaryKeys($this->table, $datass))) { |
||||||
206 | $query = $this->connection->schema()->delete($this->table, $pk_match); |
||||||
207 | |||||||
208 | $this->result = $this->connection->result($query); |
||||||
209 | return $this->result->ran(); |
||||||
210 | } |
||||||
211 | |||||||
212 | return false; |
||||||
213 | } |
||||||
214 | |||||||
215 | |||||||
216 | /** |
||||||
217 | * Creates a new record in the database. |
||||||
218 | * Executes an insert query with the current data and updates the alterations tracker with the auto-incremented primary key value if applicable. |
||||||
219 | */ |
||||||
220 | private function create(): void |
||||||
221 | { |
||||||
222 | $query = $this->connection->schema()->insert($this->table, $this->export()); |
||||||
223 | $this->result = $this->connection->result($query); |
||||||
224 | |||||||
225 | // creation might lead to auto_incremented changes |
||||||
226 | // recovering auto_incremented value and pushing it in alterations tracker |
||||||
227 | $aipk = $this->connection->schema()->autoIncrementedPrimaryKey($this->table); |
||||||
228 | if ($aipk !== null) { |
||||||
229 | $this->$aipk = $this->result->lastInsertId(); |
||||||
230 | } |
||||||
231 | } |
||||||
232 | |||||||
233 | /** |
||||||
234 | * Updates the existing record in the database with the current alterations. |
||||||
235 | * |
||||||
236 | * @throws CruditesException if a unique match is not found. |
||||||
237 | */ |
||||||
238 | private function update(): void |
||||||
239 | { |
||||||
240 | $unique_match = $this->connection->schema()->matchUniqueness($this->table, $this->load); |
||||||
241 | |||||||
242 | if (empty($unique_match)) { |
||||||
243 | throw new CruditesException('NO_UNIQUE_MATCH_IN_LOAD_ARRAY'); |
||||||
244 | } |
||||||
245 | |||||||
246 | $query = $this->connection->schema()->update($this->table, $this->alterations, $unique_match); |
||||||
247 | $this->result = $this->connection->result($query); |
||||||
248 | } |
||||||
249 | |||||||
250 | |||||||
251 | //------------------------------------------------------------ type:data validation |
||||||
252 | /** |
||||||
253 | * @return array<mixed,string> containing all invalid data, indexed by field name, or empty if all valid |
||||||
254 | */ |
||||||
255 | public function validate(): array |
||||||
256 | { |
||||||
257 | $errors = []; |
||||||
258 | $datass = $this->export(); |
||||||
259 | |||||||
260 | foreach ($this->connection->schema()->columns($this->table) as $column_name) { |
||||||
261 | |||||||
262 | $attribute = $this->connection->schema()->attributes($this->table, $column_name); |
||||||
263 | $column_errors = $attribute->validateValue($datass[$column_name] ?? null); |
||||||
264 | |||||||
265 | if (!empty($column_errors)) { |
||||||
266 | $errors[$column_name] = $column_errors; |
||||||
267 | } |
||||||
268 | } |
||||||
269 | |||||||
270 | return $errors; |
||||||
271 | } |
||||||
272 | } |
||||||
273 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.