Completed
Pull Request — master (#9)
by Samuel
15:22
created

Repository::transaction()   C

Complexity

Conditions 7
Paths 16

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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