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

Repository::delete()   B

Complexity

Conditions 4
Paths 1

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 26
ccs 13
cts 13
cp 1
rs 8.5806
cc 4
eloc 14
nc 1
nop 1
crap 4
1
<?php
2
declare(strict_types=1);
3
4
namespace SimpleMapper;
5
6
use Nette\Database\Context;
7
use Nette\Database\DriverException;
8
use Nette\Database\Table\ActiveRow as NetteDatabaseActiveRow;
9
use Nette\Database\Table\Selection as NetteDatabaseSelection;
10
use SimpleMapper\Behaviour\Behaviour;
11
use SimpleMapper\Exception\RepositoryException;
12
use SimpleMapper\Structure\EmptyStructure;
13
use SimpleMapper\Structure\Structure;
14
use Traversable;
15
use Exception;
16
use PDOException;
17
18
/**
19
 * Base repository class
20
 */
21
abstract class Repository
22
{
23
    /** @var Context */
24
    protected $databaseContext;
25
26
    /** @var Structure|null */
27
    protected $structure;
28
29
    /** @var string             Soft delete field, if empty soft delete is disabled */
30
    protected $softDelete = '';
31
32
    /** @var array */
33
    private $behaviours = [];
34
35
    /** @var string */
36
    protected static $tableName = 'unknown';
37
38
    /**
39
     * @param Context $databaseContext
40
     */
41 68
    public function __construct(Context $databaseContext)
42
    {
43 68
        $this->databaseContext = $databaseContext;
44 68
        $this->structure = new EmptyStructure();
45 68
        $this->configure();
46 68
    }
47
48
    /**
49
     * @param Structure $structure
50
     */
51 68
    public function setStructure(Structure $structure): void
52
    {
53 68
        $this->structure = $structure;
54 68
        if (count($this->getScopes())) {
55 68
            $this->structure->registerScopes(static::getTableName(), $this->getScopes());
56
        }
57 68
    }
58
59
    /**
60
     * @return string
61
     */
62 68
    public static function getTableName(): string
63
    {
64 68
        return static::$tableName;
65
    }
66
67
    /**
68
     * @return Context
69
     */
70 8
    public function getDatabaseContext(): Context
71
    {
72 8
        return $this->databaseContext;
73
    }
74
75
    /********************************************************************\
76
    | Magic methods
77
    \********************************************************************/
78
79
    /**
80
     * @param string $name
81
     * @param array $arguments
82
     * @return mixed
83
     * @throws RepositoryException
84
     */
85 6
    public function __call(string $name, array $arguments)
86
    {
87 6
        if (substr($name, 0, 5) === 'scope') {
88 4
            $scopeName = lcfirst(substr($name, 5));
89 4
            $scope = $this->structure->getScope(static::$tableName, $scopeName);
90 4
            if (!$scope) {
91 2
                throw new RepositoryException('Scope ' . $scopeName . ' is not defined for table ' . static::$tableName);
92
            }
93
94 2
            $scopeNameToCall = 'scope' . ucfirst($scope->getName());
95 2
            return call_user_func_array([$this->findAll(), $scopeNameToCall], $arguments);
96
        }
97
98 2
        throw new RepositoryException('Call to undefined method ' . get_class($this) . '::' . $name . '()');
99
    }
100
101
    /********************************************************************\
102
    | Wrapper methods
103
    \********************************************************************/
104
105
    /**
106
     * Find all records
107
     * @return Selection
108
     */
109 42
    public function findAll(): Selection
110
    {
111 42
        return $this->prepareSelection($this->getTable());
112
    }
113
114
    /**
115
     * Find by conditions
116
     * @param array $by
117
     * @return Selection
118
     */
119 20
    public function findBy(array $by): Selection
120
    {
121 20
        return $this->prepareSelection($this->getTable()->where($by));
122
    }
123
124
    /**
125
     * Returns all rows as associative array
126
     * @param string|null $key
127
     * @param string|null $value
128
     * @param string|null $order
129
     * @param array $where
130
     * @return array
131
     */
132
    public function fetchPairs(string $key = null, string $value = null, string $order = null, array $where = []): array
133
    {
134
        $result = [];
135
        $pairs = $this->findBy($where);
136
        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...
137
            $pairs->order($order);
138
        }
139
140
        foreach ($pairs->fetchPairs($key, $value) as $k => $v) {
141
            $result[$k] = $v instanceof NetteDatabaseActiveRow ? $this->prepareRecord($v) : $v;
142
        }
143
        return $result;
144
    }
145
146
    /**
147
     * Insert one record
148
     * @param array|Traversable $data
149
     * @return ActiveRow|null
150
     * @throws Exception
151
     */
152 2
    public function insert(array $data): ?ActiveRow
153
    {
154
        $result = $this->transaction(function () use ($data) {
155 2
            foreach ($this->behaviours as $behaviour) {
156 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...
157
            }
158
159 2
            $record = $this->getTable()->insert($data);
160 2
            if (!($record instanceof NetteDatabaseActiveRow)) {
161
                return null;
162
            }
163
164 2
            $record = $this->prepareRecord($record);
165
166 2
            foreach ($this->behaviours as $behaviour) {
167 2
                $behaviour->afterInsert($record, $data);
168
            }
169
170 2
            return $record;
171 2
        });
172
173 2
        return $result instanceof NetteDatabaseActiveRow ? $this->prepareRecord($result) : $result;
174
    }
175
176
    /**
177
     * Update one record
178
     * @param ActiveRow $record
179
     * @param array $data
180
     * @return ActiveRow|null
181
     */
182 2
    public function update(ActiveRow $record, array $data): ?ActiveRow
183
    {
184
        $result = $this->transaction(function () use ($record, $data) {
185 2
            $oldRecord = clone $record;
186
187 2
            foreach ($this->behaviours as $behaviour) {
188 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...
189
            }
190
191 2
            $result = $record->update($data);
192
193 2
            foreach ($this->behaviours as $behaviour) {
194 2
                $behaviour->afterUpdate($oldRecord, $record, $data);
195
            }
196
197 2
            return $result ? $record : null;
198 2
        });
199
200 2
        return $result instanceof NetteDatabaseActiveRow ? $this->prepareRecord($result) : $result;
201
    }
202
203
    /**
204
     * Delete one record
205
     * @param ActiveRow $record
206
     * @return bool
207
     */
208
    public function delete(ActiveRow $record): bool
209
    {
210 4
        $result = $this->transaction(function () use ($record): bool {
211 4
            $oldRecord = clone $record;
212
213 4
            foreach ($this->behaviours as $behaviour) {
214 2
                $behaviour->beforeDelete($record, (bool) $this->softDelete);
215
            }
216
217 4
            if ($this->softDelete) {
218 2
                $result = $record->update([
219 2
                    $this->softDelete => true
220
                ]);
221
            } else {
222 2
                $result = $record->delete();
223
            }
224
225 4
            foreach ($this->behaviours as $behaviour) {
226 2
                $behaviour->afterDelete($oldRecord, (bool) $this->softDelete);
227
            }
228
229 4
            return (bool) $result;
230 4
        });
231
232 4
        return $result;
233
    }
234
235
    /********************************************************************\
236
    | Internal methods
237
    \********************************************************************/
238
239
    /**
240
     * @return NetteDatabaseSelection
241
     */
242 62
    protected function getTable(): NetteDatabaseSelection
243
    {
244 62
        return $this->databaseContext->table(static::getTableName());
245
    }
246
247
    /**
248
     * @param Behaviour $behaviour
249
     * @return Repository
250
     */
251 68
    protected function registerBehaviour(Behaviour $behaviour): Repository
252
    {
253 68
        $this->behaviours[get_class($behaviour)] = $behaviour;
254 68
        return $this;
255
    }
256
257
    /**
258
     * Get behaviour by class
259
     * @param string $class
260
     * @return Behaviour|null
261
     */
262
    protected function getBehaviour($class): ?Behaviour
263
    {
264
        return $this->behaviours[$class] ?? null;
265
    }
266
267
    /**
268
     * Configure repository
269
     */
270 68
    protected function configure(): void
271
    {
272
        // override in child
273 68
    }
274
275
    /**
276
     * Define table scopes
277
     * @return array
278
     */
279 68
    protected function getScopes(): array
280
    {
281
        // override in child
282 68
        return [];
283
    }
284
285
    /********************************************************************\
286
    | Builder methods
287
    \********************************************************************/
288
289
    /**
290
     * @param NetteDatabaseSelection $selection
291
     * @return Selection
292
     */
293 60
    private function prepareSelection(NetteDatabaseSelection $selection): Selection
294
    {
295 60
        $selectionClass = $this->structure->getSelectionClass($selection->getName());
296 60
        return new $selectionClass($selection, $this->structure);
297
    }
298
299
    /**
300
     * @param NetteDatabaseActiveRow $row
301
     * @return ActiveRow
302
     */
303 2
    private function prepareRecord(NetteDatabaseActiveRow $row): ActiveRow
304
    {
305 2
        $rowClass = $this->structure->getActiveRowClass($row->getTable()->getName());
306 2
        return new $rowClass($row, $this->structure);
307
    }
308
309
    /********************************************************************\
310
    | Helper methods
311
    \********************************************************************/
312
313
    /**
314
     * Run new transaction if no transaction is running, do nothing otherwise
315
     * @param callable $callback
316
     * @return mixed
317
     */
318 8
    public function transaction(callable $callback)
319
    {
320
        try {
321
            // Check if transaction already running
322 8
            $inTransaction = $this->getDatabaseContext()->getConnection()->getPdo()->inTransaction();
323 8
            if (!$inTransaction) {
324 8
                $this->getDatabaseContext()->beginTransaction();
325
            }
326
327 8
            $result = $callback($this);
328
329 8
            if (!$inTransaction) {
330 8
                $this->getDatabaseContext()->commit();
331
            }
332
        } catch (Exception $e) {
333
            if (isset($inTransaction) && !$inTransaction && $e instanceof PDOException) {
334
                $this->getDatabaseContext()->rollBack();
335
            }
336
            throw $e;
337
        }
338
339 8
        return $result;
340
    }
341
342
    /**
343
     * @param callable $callback
344
     * @param int $retryTimes
345
     * @return mixed
346
     * @throws DriverException
347
     */
348
    public function ensure(callable $callback, int $retryTimes = 1)
349
    {
350
        try {
351
            return $callback($this);
352
        } catch (DriverException $e) {
353
            if ($retryTimes == 0) {
354
                throw $e;
355
            }
356
            $this->getDatabaseContext()->getConnection()->reconnect();
357
            return $this->ensure($callback, $retryTimes - 1);
358
        }
359
    }
360
361
    /**
362
     * Try call callback X times
363
     * @param callable $callback
364
     * @param int $retryTimes
365
     * @return mixed
366
     * @throws DriverException
367
     */
368
    public function retry(callable $callback, int $retryTimes = 3)
369
    {
370
        try {
371
            return $callback($this);
372
        } catch (DriverException $e) {
373
            if ($retryTimes == 0) {
374
                throw $e;
375
            }
376
            return $this->retry($callback, $retryTimes - 1);
377
        }
378
    }
379
380
    /**
381
     * Paginate callback
382
     * @param Selection $selection
383
     * @param int $limit
384
     * @param callable $callback
385
     */
386
    public function chunk(Selection $selection, int $limit, callable $callback)
387
    {
388
        $count = $selection->count('*');
389
        $pages = ceil($count / $limit);
390
        for ($i = 0; $i < $pages; $i++) {
391
            $callback($selection->page($i + 1, $limit));
392
        }
393
    }
394
}
395