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

Repository::prepareSelection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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