Completed
Push — master ( 6067eb...3f355c )
by Samuel
15:57 queued 01:01
created

Repository::configure()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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