Completed
Push — master ( a343ad...f24189 )
by Samuel
29:24 queued 14:26
created

Repository::prefixColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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