Passed
Push — main ( 85995a...64fb94 )
by Sammy
07:44
created

Row::__debugInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 9
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 11
rs 9.9666
1
<?php
2
3
namespace HexMakina\Crudites\Table;
4
5
use HexMakina\BlackBox\Database\ConnectionInterface;
6
use HexMakina\BlackBox\Database\RowInterface;
7
use HexMakina\BlackBox\Database\QueryInterface;
8
9
use HexMakina\Crudites\CruditesException;
10
use HexMakina\Crudites\Result;
11
use HexMakina\Crudites\Grammar\Clause\Where;
12
13
class Row implements RowInterface
14
{
15
    private string $table;
16
17
    private ConnectionInterface $connection;
18
19
    /** @var array<int|string,mixed>|null $load from database */
20
    private ?array $load = null;
21
22
    /** @var array<int|string,mixed> $fresh from the constructor */
23
    private array $fresh = [];
24
25
    /** @var array<int|string,mixed> $alterations during lifecycle */
26
    private array $alterations = [];
27
28
    private ?QueryInterface $last_query = null;
29
    private ?QueryInterface $last_alter_query = null;
30
31
    private ?Result $last_result = null;
32
    private ?Result $last_alter_result = null;
33
34
35
    /** @param array<string,mixed> $datass */
36
    /**
37
     * Represents a row in a table.
38
     *
39
     * @param ConnectionInterface $connection The database connection.
40
     * @param string $table The table name.
41
     * @param array $fresh The fresh data for the row.
42
     */
43
    public function __construct(ConnectionInterface $connection, string $table, array $fresh = [])
44
    {
45
        $this->connection = $connection;
46
        $this->table = $table;
47
        $this->fresh = $fresh;
48
    }
49
50
    public function __toString()
51
    {
52
        return PHP_EOL . 'load: '
53
        . json_encode($this->load)
54
        . PHP_EOL . 'alterations: '
55
        . json_encode(array_keys($this->alterations));
56
    }
57
58
    public function __debugInfo()
59
    {
60
        return [
61
            'table' => $this->table,
62
            'load' => $this->load,
63
            'fresh' => $this->fresh,
64
            'alterations' => $this->alterations,
65
            'last_query' => $this->last_query,
66
            'last_alter_query' => $this->last_alter_query,
67
            'last_result' => $this->last_result,
68
            'last_alter_result' => $this->last_alter_result,
69
        ];
70
    }
71
72
    public function get($name)
73
    {
74
        return $this->alterations[$name]
75
            ?? $this->fresh[$name]
76
            ?? $this->load[$name]
77
            ?? null;
78
    }
79
80
    public function set($name, $value) : void
81
    {
82
        $this->alterations[$name] = $value;
83
    }
84
85
    public function table(): string
86
    {
87
        return $this->table;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->table returns the type string which is incompatible with the return type mandated by HexMakina\BlackBox\Database\RowInterface::table() of HexMakina\BlackBox\Database\TableInterface.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
88
    }
89
90
    public function lastQuery(): ?QueryInterface
91
    {
92
        return $this->last_query;
93
    }
94
95
    public function lastAlterQuery(): ?QueryInterface
96
    {
97
        return $this->last_alter_query;
98
    }
99
100
    public function isNew(): bool
101
    {
102
        return empty($this->load);
103
    }
104
105
    public function isAltered(): bool
106
    {
107
        return !empty($this->alterations);
108
    }
109
110
    /**
111
     * @return array<int|string,mixed>
112
     * merges the initial database load with the constructor and the alterations
113
     * the result is an associative array containing all data, all up-to-date
114
     */
115
    public function export(): array
116
    {
117
        return array_merge((array)$this->load, $this->fresh, $this->alterations);
118
    }
119
120
    public function load(array $datass = null): Rowinterface
121
    {
122
        $unique_match = $this->connection->schema()->matchUniqueness($this->table, $datass ?? $this->export());
123
        
124
        if (empty($unique_match)) {
125
            return $this;
126
        }
127
128
        $where = (new Where())->andFields($unique_match, $this->table, '=');
129
130
        $this->last_query = $this->connection->schema()->select($this->table)->add($where);
131
        $this->last_result = new Result($this->connection->pdo(), $this->last_query);
132
        
133
        $res = $this->last_result->ret(\PDO::FETCH_ASSOC);
134
        $this->load = (is_array($res) && count($res) === 1) ? current($res) : null;
135
136
        return $this;
137
    }
138
139
    /**
140
     * loops the associative data and records changes vis-à-vis loaded data
141
     * 
142
     * 1. skips non existing field name and A_I column
143
     * 2. replaces empty strings with null or default value
144
     * 3. checks for changes with loaded data. using == instead of === is risky but needed
145
     * 4. pushes the changes in the alterations tracker
146
     *
147
     *
148
     * @param  array<int|string,mixed> $datass an associative array containing the new data
149
     */
150
    public function alter(array $datass): Rowinterface
151
    {
152
        foreach (array_keys($datass) as $field_name) {
153
            // skips non existing field name and A_I column
154
            if ($this->connection->schema()->hasColumn($this->table, $field_name)) {
155
                continue;
156
            }
157
158
            $attributes = $this->connection->schema()->attributes($this->table, $field_name);
159
160
            if ($attributes->isAuto()) {
161
                continue;
162
            }
163
164
            // replaces empty strings with null or default value
165
            if (trim('' . $datass[$field_name]) === '' && $attributes->nullable()) {
166
                $datass[$field_name] = $attributes->nullable() ? null : $attributes->default();
167
            }
168
169
            // checks for changes with loaded data. using == instead of === is risky but needed
170
            if (!is_array($this->load) || $this->load[$field_name] != $datass[$field_name]) {
171
                $this->set($field_name, $datass[$field_name]);
172
            }
173
        }
174
175
        return $this;
176
    }
177
178
    /**
179
      * @return array<string,string> an array of errors, column name => message
180
      */
181
    public function persist(): array
182
    {
183
        if (!$this->isNew() && !$this->isAltered()) { // existing record with no alterations
184
            return [];
185
        }
186
187
        if (!empty($errors = $this->validate())) { // Table level validation
188
            return $errors;
189
        }
190
        try {
191
            if ($this->isNew()) {
192
                $this->create();
193
            } else {
194
                $this->update();
195
            }
196
            $this->last_query = $this->lastAlterQuery();
197
198
        } catch (CruditesException $cruditesException) {
199
            return [$this->table => $cruditesException->getMessage()];
200
        }
201
202
        return [];
203
    }
204
205
    private function create(): void
206
    {
207
        $this->last_alter_query = $this->connection->schema()->insert($this->table, $this->export());
208
        $this->last_alter_result = new Result($this->connection->pdo(), $this->last_alter_query);
209
210
        // creation might lead to auto_incremented changes
211
        // recovering auto_incremented value and pushing it in alterations tracker
212
        $aipk = $this->connection->schema()->autoIncrementedPrimaryKey($this->table);
213
        if ($aipk !== null) {
214
            $this->alterations[$aipk] = $this->connection->lastInsertId();
215
        }
216
    }
217
218
    private function update(): void
219
    {
220
        
221
        $unique_match = $this->connection->schema()->matchUniqueness($this->table, $this->load);
222
223
        if(empty($unique_match)){
224
            throw new CruditesException('UNIQUE_MATCH_NOT_FOUND');
225
        }
226
227
        $this->last_alter_query = $this->connection->schema()->update($this->table, $this->alterations, $unique_match);
228
        $this->last_alter_result = new Result($this->connection->pdo(), $this->last_alter_query);
229
    }
230
231
    public function wipe(): bool
232
    {
233
        $datass = $this->load ?? $this->fresh ?? $this->alterations;
234
235
        // need The Primary key, then you can wipe at ease
236
        if (!empty($pk_match = $this->connection->schema()->matchPrimaryKeys($this->table, $datass))) {
237
            $this->last_alter_query = $this->connection->schema()->delete($this->table, $pk_match);
238
            
239
            try {
240
                
241
                $this->last_alter_result = new Result($this->connection->pdo(), $this->last_alter_query);
242
                $this->last_query = $this->lastAlterQuery();
243
244
            } catch (CruditesException $cruditesException) {
245
                return false;
246
            }
247
248
            return $this->last_alter_result->isSuccess();
0 ignored issues
show
Bug introduced by
The method isSuccess() does not exist on HexMakina\Crudites\Result. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

248
            return $this->last_alter_result->/** @scrutinizer ignore-call */ isSuccess();
Loading history...
249
        }
250
251
        return false;
252
    }
253
254
    //------------------------------------------------------------  type:data validation
255
    /**
256
     * @return array<mixed,string> containing all invalid data, indexed by field name, or empty if all valid
257
     */
258
    public function validate(): array
259
    {
260
        $errors = [];
261
        $datass = $this->export();
262
263
        foreach ($this->connection->schema()->columns($this->table) as $column_name) {
264
265
            $attribute = $this->connection->schema()->attributes($this->table, $column_name);
266
            $errors = $attribute->validateValue($datass[$column_name] ?? null);
267
268
            if (!empty($errors)) {
269
                $errors[$column_name] = $errors;
270
            }
271
        }
272
273
        return $errors;
274
    }
275
}
276