Issues (65)

src/Row.php (2 issues)

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
The call to HexMakina\Crudites\Row::create() 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 ignore-call  annotation

182
                $this->/** @scrutinizer ignore-call */ 
183
                       create($this->connection);

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.

Loading history...
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 ignore-call  annotation

184
                $this->/** @scrutinizer ignore-call */ 
185
                       update($this->connection);

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.

Loading history...
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