Passed
Push — main ( 39b0f6...4f7abe )
by Sammy
22:35
created

Row::isNew()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
namespace HexMakina\Crudites;
4
5
use HexMakina\BlackBox\Database\ConnectionInterface;
6
use HexMakina\BlackBox\Database\RowInterface;
7
8
use HexMakina\Crudites\CruditesException;
9
use HexMakina\Crudites\Grammar\Clause\Where;
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
    private ?Result $last_result = null;
27
    private ?Result $last_alter_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
    public function __toString()
46
    {
47
        return PHP_EOL . 'load: '
48
        . json_encode($this->load)
49
        . PHP_EOL . 'alterations: '
50
        . json_encode(array_keys($this->alterations));
51
    }
52
53
    public function __debugInfo()
54
    {
55
        return [
56
            'table' => $this->table,
57
            'load' => $this->load,
58
            'fresh' => $this->fresh,
59
            'alterations' => $this->alterations,
60
            'last_result' => $this->last_result,
61
            'last_alter_result' => $this->last_alter_result,
62
        ];
63
    }
64
65
    public function get($name)
66
    {
67
        return $this->alterations[$name]
68
            ?? $this->fresh[$name]
69
            ?? $this->load[$name]
70
            ?? null;
71
    }
72
73
    public function set($name, $value) : void
74
    {
75
        $this->alterations[$name] = $value;
76
    }
77
78
    public function table(): string
79
    {
80
        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...
81
    }
82
83
    public function isNew(): bool
84
    {
85
        return empty($this->load);
86
    }
87
88
    public function isAltered(): bool
89
    {
90
        return !empty($this->alterations);
91
    }
92
93
    /**
94
     * @return array<int|string,mixed>
95
     * merges the initial database load with the constructor and the alterations
96
     * the result is an associative array containing all data, all up-to-date
97
     */
98
    public function export(): array
99
    {
100
        return array_merge((array)$this->load, $this->fresh, $this->alterations);
101
    }
102
103
    public function load(array $datass = null): Rowinterface
104
    {
105
        $unique_match = $this->connection->schema()->matchUniqueness($this->table, $datass ?? $this->export());
106
        
107
        if (empty($unique_match)) {
108
            return $this;
109
        }
110
111
        $where = (new Where())->andFields($unique_match, $this->table, '=');
112
113
        $query = $this->connection->schema()->select($this->table)->add($where);
114
        $this->last_result = new Result($this->connection->pdo(), $query);
115
        
116
        $res = $this->last_result->ret(\PDO::FETCH_ASSOC);
117
        $this->load = (is_array($res) && count($res) === 1) ? current($res) : null;
118
119
        return $this;
120
    }
121
122
    /**
123
     * loops the associative data and records changes vis-à-vis loaded data
124
     * 
125
     * 1. skips non existing field name and A_I column
126
     * 2. replaces empty strings with null or default value
127
     * 3. checks for changes with loaded data. using == instead of === is risky but needed
128
     * 4. pushes the changes in the alterations tracker
129
     *
130
     *
131
     * @param  array<int|string,mixed> $datass an associative array containing the new data
132
     */
133
    public function alter(array $datass): Rowinterface
134
    {
135
        foreach (array_keys($datass) as $field_name) {
136
            // skips non existing field name and A_I column
137
            if ($this->connection->schema()->hasColumn($this->table, $field_name)) {
138
                continue;
139
            }
140
141
            $attributes = $this->connection->schema()->attributes($this->table, $field_name);
142
143
            if ($attributes->isAuto()) {
144
                continue;
145
            }
146
147
            // replaces empty strings with null or default value
148
            if (trim('' . $datass[$field_name]) === '' && $attributes->nullable()) {
149
                $datass[$field_name] = null;
150
            }
151
152
            // checks for changes with loaded data. using == instead of === is risky but needed
153
            if (!is_array($this->load) || $this->load[$field_name] != $datass[$field_name]) {
154
                $this->set($field_name, $datass[$field_name]);
155
            }
156
        }
157
158
        return $this;
159
    }
160
161
    /**
162
      * @return array<string,string> an array of errors, column name => message
163
      */
164
    public function persist(): array
165
    {
166
        if (!$this->isNew() && !$this->isAltered()) { // existing record with no alterations
167
            return [];
168
        }
169
170
        if (!empty($errors = $this->validate())) { // Table level validation
171
            return $errors;
172
        }
173
        try {
174
            if ($this->isNew()) {
175
                $this->create();
176
            } else {
177
                $this->update();
178
            }
179
        } catch (CruditesException $cruditesException) {
180
            return [$this->table => $cruditesException->getMessage()];
181
        }
182
183
        return [];
184
    }
185
186
    private function create(): void
187
    {
188
        $query = $this->connection->schema()->insert($this->table, $this->export());
189
        $this->last_result = $this->last_alter_result = new Result($this->connection->pdo(), $query);
190
191
        // creation might lead to auto_incremented changes
192
        // recovering auto_incremented value and pushing it in alterations tracker
193
        $aipk = $this->connection->schema()->autoIncrementedPrimaryKey($this->table);
194
        if ($aipk !== null) {
195
            $this->alterations[$aipk] = $this->connection->lastInsertId();
196
        }
197
    }
198
199
    private function update(): void
200
    {
201
        
202
        $unique_match = $this->connection->schema()->matchUniqueness($this->table, $this->load);
203
204
        if(empty($unique_match)){
205
            throw new CruditesException('UNIQUE_MATCH_NOT_FOUND');
206
        }
207
208
        $query = $this->connection->schema()->update($this->table, $this->alterations, $unique_match);
209
        $this->last_result = $this->last_alter_result = new Result($this->connection->pdo(), $query);
210
    }
211
212
    public function wipe(): bool
213
    {
214
        $datass = $this->load ?? $this->fresh ?? $this->alterations;
215
216
        // need The Primary key, then you can wipe at ease
217
        if (!empty($pk_match = $this->connection->schema()->matchPrimaryKeys($this->table, $datass))) {
218
            $query = $this->connection->schema()->delete($this->table, $pk_match);
219
            
220
            try {
221
                $this->last_result = $this->last_alter_result = new Result($this->connection->pdo(), $query);
222
223
            } catch (CruditesException $cruditesException) {
224
                return false;
225
            }
226
227
            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

227
            return $this->last_alter_result->/** @scrutinizer ignore-call */ isSuccess();
Loading history...
228
        }
229
230
        return false;
231
    }
232
233
    //------------------------------------------------------------  type:data validation
234
    /**
235
     * @return array<mixed,string> containing all invalid data, indexed by field name, or empty if all valid
236
     */
237
    public function validate(): array
238
    {
239
        $errors = [];
240
        $datass = $this->export();
241
242
        foreach ($this->connection->schema()->columns($this->table) as $column_name) {
243
244
            $attribute = $this->connection->schema()->attributes($this->table, $column_name);
245
            $errors = $attribute->validateValue($datass[$column_name] ?? null);
246
247
            if (!empty($errors)) {
248
                $errors[$column_name] = $errors;
249
            }
250
        }
251
252
        return $errors;
253
    }
254
}
255