Completed
Pull Request — master (#9)
by Samuel
03:17
created

Repository::update()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 20
ccs 10
cts 10
cp 1
rs 8.8571
cc 5
eloc 10
nc 2
nop 2
crap 5
1
<?php
2
3
namespace SimpleMapper;
4
5
use Nette\Database\Context;
6
use Nette\Database\DriverException;
7
use Nette\Database\Table\ActiveRow as NetteDatabaseActiveRow;
8
use Nette\Database\Table\Selection as NetteDatabaseSelection;
9
use SimpleMapper\Behaviour\Behaviour;
10
use SimpleMapper\Exception\RepositoryException;
11
use SimpleMapper\Structure\EmptyStructure;
12
use SimpleMapper\Structure\Structure;
13
use Traversable;
14
use Exception;
15
use PDOException;
16
17
/**
18
 * Base repository class
19
 */
20
abstract class Repository
21
{
22
    /** @var Context */
23
    protected $databaseContext;
24
25
    /** @var Structure|null */
26
    protected $structure;
27
28
    /** @var string             Soft delete field, if empty soft delete is disabled */
29
    protected $softDelete = '';
30
31
    /** @var array */
32
    private $behaviours = [];
33
34
    /** @var string */
35
    protected static $tableName = 'unknown';
36
37
    /**
38
     * @param Context $databaseContext
39
     */
40 68
    public function __construct(Context $databaseContext)
41
    {
42 68
        $this->databaseContext = $databaseContext;
43 68
        $this->structure = new EmptyStructure();
44 68
        $this->configure();
45 68
    }
46
47
    /**
48
     * @param Structure $structure
49
     */
50 68
    public function setStructure(Structure $structure)
51
    {
52 68
        $this->structure = $structure;
53 68
        if (count($this->getScopes())) {
54 68
            $this->structure->registerScopes(static::getTableName(), $this->getScopes());
55
        }
56 68
    }
57
58
    /**
59
     * @return string
60
     */
61 68
    public static function getTableName(): string
62
    {
63 68
        return static::$tableName;
64
    }
65
66
    /**
67
     * @return Context
68
     */
69 8
    public function getDatabaseContext(): Context
70
    {
71 8
        return $this->databaseContext;
72
    }
73
74
    /********************************************************************\
75
    | Magic methods
76
    \********************************************************************/
77
78
    /**
79
     * @param string $name
80
     * @param array $arguments
81
     * @return mixed
82
     * @throws RepositoryException
83
     */
84 6
    public function __call(string $name, array $arguments)
85
    {
86 6
        if (substr($name, 0, 5) === 'scope') {
87 4
            $scopeName = lcfirst(substr($name, 5));
88 4
            $scope = $this->structure->getScope(static::$tableName, $scopeName);
89 4
            if (!$scope) {
90 2
                throw new RepositoryException('Scope ' . $scopeName . ' is not defined for table ' . static::$tableName);
91
            }
92
93 2
            $scopeNameToCall = 'scope' . ucfirst($scope->getName());
94 2
            return call_user_func_array([$this->findAll(), $scopeNameToCall], $arguments);
95
        }
96
97 2
        throw new RepositoryException('Call to undefined method ' . get_class($this) . '::' . $name . '()');
98
    }
99
100
    /********************************************************************\
101
    | Wrapper methods
102
    \********************************************************************/
103
104
    /**
105
     * Find all records
106
     * @return Selection
107
     */
108 42
    public function findAll(): Selection
109
    {
110 42
        return $this->prepareSelection($this->getTable());
111
    }
112
113
    /**
114
     * Find by conditions
115
     * @param array $by
116
     * @return Selection
117
     */
118 20
    public function findBy(array $by): Selection
119
    {
120 20
        return $this->prepareSelection($this->getTable()->where($by));
121
    }
122
123
    /**
124
     * Returns all rows as associative array
125
     * @param string|null $key
126
     * @param string|null $value
127
     * @param string|null $order
128
     * @param array $where
129
     * @return array
130
     */
131
    public function fetchPairs(string $key = null, string $value = null, string $order = null, array $where = []): array
132
    {
133
        $result = [];
134
        $pairs = $this->findBy($where);
135
        if ($order) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $order of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
136
            $pairs->order($order);
137
        }
138
139
        foreach ($pairs->fetchPairs($key, $value) as $k => $v) {
140
            $result[$k] = $v instanceof NetteDatabaseActiveRow ? $this->prepareRecord($v) : $v;
141
        }
142
        return $result;
143
    }
144
145
    /**
146
     * Insert one record
147
     * @param array|Traversable $data
148
     * @return ActiveRow|null
149
     * @throws Exception
150
     */
151 2
    public function insert(array $data): ?ActiveRow
152
    {
153
        $result = $this->transaction(function () use ($data) {
154 2
            foreach ($this->behaviours as $behaviour) {
155 2
                $data = $behaviour->beforeInsert($data);
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $data, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
156
            }
157
158 2
            $record = $this->getTable()->insert($data);
159 2
            if (!($record instanceof NetteDatabaseActiveRow)) {
160
                return null;
161
            }
162
163 2
            $record = $this->prepareRecord($record);
164
165 2
            foreach ($this->behaviours as $behaviour) {
166 2
                $behaviour->afterInsert($record, $data);
167
            }
168
169 2
            return $record;
170 2
        });
171
172 2
        return $result instanceof NetteDatabaseActiveRow ? $this->prepareRecord($result) : $result;
173
    }
174
175
    /**
176
     * Update one record
177
     * @param ActiveRow $record
178
     * @param array $data
179
     * @return ActiveRow|null
180
     */
181 2
    public function update(ActiveRow $record, array $data): ?ActiveRow
182
    {
183
        $result = $this->transaction(function () use ($record, $data) {
184 2
            $oldRecord = clone $record;
185
186 2
            foreach ($this->behaviours as $behaviour) {
187 2
                $data = $behaviour->beforeUpdate($record, $data);
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $data, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
188
            }
189
190 2
            $result = $record->update($data);
191
192 2
            foreach ($this->behaviours as $behaviour) {
193 2
                $behaviour->afterUpdate($oldRecord, $record, $data);
194
            }
195
196 2
            return $result ? $record : null;
197 2
        });
198
199 2
        return $result instanceof NetteDatabaseActiveRow ? $this->prepareRecord($result) : $result;
200
    }
201
202
    /**
203
     * Delete one record
204
     * @param ActiveRow $record
205
     * @return bool
206
     */
207
    public function delete(ActiveRow $record): bool
208
    {
209 4
        $result = $this->transaction(function () use ($record): bool {
210 4
            $oldRecord = clone $record;
211
212 4
            foreach ($this->behaviours as $behaviour) {
213 2
                $behaviour->beforeDelete($record, (bool) $this->softDelete);
214
            }
215
216 4
            if ($this->softDelete) {
217 2
                $result = $record->update([
218 2
                    $this->softDelete => true
219
                ]);
220
            } else {
221 2
                $result = $record->delete();
222
            }
223
224 4
            foreach ($this->behaviours as $behaviour) {
225 2
                $behaviour->afterDelete($oldRecord, (bool) $this->softDelete);
226
            }
227
228 4
            return (bool) $result;
229 4
        });
230
231 4
        return $result;
232
    }
233
234
    /********************************************************************\
235
    | Internal methods
236
    \********************************************************************/
237
238
    /**
239
     * @return NetteDatabaseSelection
240
     */
241 62
    protected function getTable(): NetteDatabaseSelection
242
    {
243 62
        return $this->databaseContext->table(static::getTableName());
244
    }
245
246
    /**
247
     * @param Behaviour $behaviour
248
     * @return Repository
249
     */
250 68
    protected function registerBehaviour(Behaviour $behaviour): Repository
251
    {
252 68
        $this->behaviours[get_class($behaviour)] = $behaviour;
253 68
        return $this;
254
    }
255
256
    /**
257
     * Get behaviour by class
258
     * @param string $class
259
     * @return Behaviour|null
260
     */
261
    protected function getBehaviour($class): ?Behaviour
262
    {
263
        return $this->behaviours[$class] ?? null;
264
    }
265
266
    /**
267
     * Configure repository
268
     */
269 68
    protected function configure(): void
270
    {
271
        // override in child
272 68
    }
273
274
    /**
275
     * Define table scopes
276
     * @return array
277
     */
278 68
    protected function getScopes(): array
279
    {
280
        // override in child
281 68
        return [];
282
    }
283
284
    /********************************************************************\
285
    | Builder methods
286
    \********************************************************************/
287
288
    /**
289
     * @param NetteDatabaseSelection $selection
290
     * @return Selection
291
     */
292 60
    private function prepareSelection(NetteDatabaseSelection $selection): Selection
293
    {
294 60
        $selectionClass = $this->structure->getSelectionClass($selection->getName());
295 60
        return new $selectionClass($selection, $this->structure);
296
    }
297
298
    /**
299
     * @param NetteDatabaseActiveRow $row
300
     * @return ActiveRow
301
     */
302 2
    private function prepareRecord(NetteDatabaseActiveRow $row): ActiveRow
303
    {
304 2
        $rowClass = $this->structure->getActiveRowClass($row->getTable()->getName());
305 2
        return new $rowClass($row, $this->structure);
306
    }
307
308
    /********************************************************************\
309
    | Helper methods
310
    \********************************************************************/
311
312
    /**
313
     * Run new transaction if no transaction is running, do nothing otherwise
314
     * @param callable $callback
315
     * @return mixed
316
     */
317 8
    public function transaction(callable $callback)
318
    {
319
        try {
320
            // Check if transaction already running
321 8
            $inTransaction = $this->getDatabaseContext()->getConnection()->getPdo()->inTransaction();
322 8
            if (!$inTransaction) {
323 8
                $this->getDatabaseContext()->beginTransaction();
324
            }
325
326 8
            $result = $callback($this);
327
328 8
            if (!$inTransaction) {
329 8
                $this->getDatabaseContext()->commit();
330
            }
331
        } catch (Exception $e) {
332
            if (isset($inTransaction) && !$inTransaction && $e instanceof PDOException) {
333
                $this->getDatabaseContext()->rollBack();
334
            }
335
            throw $e;
336
        }
337
338 8
        return $result;
339
    }
340
341
    /**
342
     * @param callable $callback
343
     * @param int $retryTimes
344
     * @return mixed
345
     * @throws DriverException
346
     */
347
    public function ensure(callable $callback, int $retryTimes = 1)
348
    {
349
        try {
350
            return $callback($this);
351
        } catch (DriverException $e) {
352
            if ($retryTimes == 0) {
353
                throw $e;
354
            }
355
            $this->getDatabaseContext()->getConnection()->reconnect();
356
            return $this->ensure($callback, $retryTimes - 1);
357
        }
358
    }
359
360
    /**
361
     * Try call callback X times
362
     * @param callable $callback
363
     * @param int $retryTimes
364
     * @return mixed
365
     * @throws DriverException
366
     */
367
    public function retry(callable $callback, int $retryTimes = 3)
368
    {
369
        try {
370
            return $callback($this);
371
        } catch (DriverException $e) {
372
            if ($retryTimes == 0) {
373
                throw $e;
374
            }
375
            return $this->retry($callback, $retryTimes - 1);
376
        }
377
    }
378
379
    /**
380
     * Paginate callback
381
     * @param Selection $selection
382
     * @param int $limit
383
     * @param callable $callback
384
     */
385
    public function chunk(Selection $selection, int $limit, callable $callback)
386
    {
387
        $count = $selection->count('*');
388
        $pages = ceil($count / $limit);
389
        for ($i = 0; $i < $pages; $i++) {
390
            $callback($selection->page($i + 1, $limit));
391
        }
392
    }
393
}
394