Completed
Push — develop ( 67e5f1...4da4e3 )
by Neomerx
08:20
created

BaseRepository::getLastInsertId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 0
crap 1
1
<?php namespace Limoncello\Passport\Repositories;
2
3
/**
4
 * Copyright 2015-2018 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Closure;
20
use DateTimeInterface;
21
use Doctrine\DBAL\Connection;
22
use Doctrine\DBAL\ConnectionException as ConEx;
23
use Doctrine\DBAL\DBALException;
24
use Doctrine\DBAL\Query\QueryBuilder;
25
use Doctrine\DBAL\Types\Type;
26
use Exception;
27
use Limoncello\Passport\Contracts\Entities\DatabaseSchemaInterface;
28
use Limoncello\Passport\Exceptions\RepositoryException;
29
use PDO;
30
31
/**
32
 * @package Limoncello\Passport
33
 *
34
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
35
 */
36
abstract class BaseRepository
37
{
38
    /**
39
     * @return string
40
     */
41
    abstract protected function getTableNameForReading(): string;
42
43
    /**
44
     * @return string
45
     */
46
    abstract protected function getTableNameForWriting(): string;
47
48
    /**
49
     * @return string
50
     */
51
    abstract protected function getClassName(): string;
52
53
    /**
54
     * @return string
55
     */
56
    abstract protected function getPrimaryKeyName(): string;
57
58
    /**
59
     * @var Connection
60
     */
61
    private $connection;
62
63
    /**
64
     * @var DatabaseSchemaInterface
65
     */
66
    private $databaseSchema;
67
68
    /**
69
     * @param Closure $closure
70
     *
71
     * @return void
72
     *
73
     * @throws RepositoryException
74
     */
75 21
    public function inTransaction(Closure $closure): void
76
    {
77 21
        $connection = $this->getConnection();
78 21
        $connection->beginTransaction();
79
        try {
80 21
            $isOk = ($closure() === false ? null : true);
81 19
        } finally {
82 21
            $isCommitting = (isset($isOk) === true);
83
            try {
84 21
                $isCommitting === true ? $connection->commit() : $connection->rollBack();
85 1
            } /** @noinspection PhpRedundantCatchClauseInspection */ catch (ConEx | DBALException $exception) {
86 1
                throw new RepositoryException(
87 1
                    $isCommitting === true ? 'Failed to commit a transaction.' : 'Failed to rollback a transaction.',
88 1
                    0,
89 1
                    $exception
90
                );
91
            }
92
        }
93
    }
94
95
    /**
96
     * @return Connection
97
     */
98 61
    protected function getConnection(): Connection
99
    {
100 61
        return $this->connection;
101
    }
102
103
    /**
104
     * @param Connection $connection
105
     *
106
     * @return self
107
     */
108 71
    protected function setConnection(Connection $connection): self
109
    {
110 71
        $this->connection = $connection;
111
112 71
        return $this;
113
    }
114
115
    /**
116
     * @param array $columns
117
     *
118
     * @return array
119
     *
120
     * @throws RepositoryException
121
     */
122 4
    protected function indexResources(array $columns = ['*']): array
123
    {
124
        try {
125 4
            $query = $this->getConnection()->createQueryBuilder();
126
127
            $statement = $query
128 4
                ->select($columns)
129 4
                ->from($this->getTableNameForReading())
130 4
                ->execute();
131
132 2
            $statement->setFetchMode(PDO::FETCH_CLASS, $this->getClassName());
133 2
            $result = $statement->fetchAll();
134
135 2
            return $result;
136 2
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
137 2
            $message = 'Resource reading failed.';
138 2
            throw new RepositoryException($message, 0, $exception);
139
        }
140
    }
141
142
    /**
143
     * @param iterable $values
144
     *
145
     * @return void
146
     *
147
     * @throws RepositoryException
148
     */
149 30
    protected function createResource(iterable $values): void
150
    {
151
        try {
152 30
            $query = $this->getConnection()->createQueryBuilder();
153
154 30
            $query->insert($this->getTableNameForWriting());
155 30
            foreach ($values as $key => $value) {
156 30
                $query->setValue($key, $this->createTypedParameter($query, $value));
157
            }
158
159 30
            $numberOfAdded = $query->execute();
160 25
            assert(is_int($numberOfAdded) === true);
161 5
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
162 5
            $message = 'Resource creation failed.';
163 5
            throw new RepositoryException($message, 0, $exception);
164
        }
165
    }
166
167
    /**
168
     * @return string
169
     */
170 22
    protected function getLastInsertId(): string
171
    {
172 22
        $lastInsertId = $this->getConnection()->lastInsertId();
173
174 22
        return $lastInsertId;
175
    }
176
177
    /**
178
     * @param string|int $identifier
179
     * @param array      $columns
180
     *
181
     * @return mixed
182
     *
183
     * @throws RepositoryException
184
     */
185 26
    protected function readResource($identifier, array $columns = ['*'])
186
    {
187 26
        return $this->readResourceByColumn($identifier, $this->getPrimaryKeyName(), $columns);
188
    }
189
190
    /**
191
     * @param string|int $identifier
192
     * @param string     $column
193
     * @param array      $columns
194
     *
195
     * @return mixed
196
     *
197
     * @throws RepositoryException
198
     */
199 26
    protected function readResourceByColumn($identifier, string $column, array $columns = ['*'])
200
    {
201
        try {
202 26
            $query = $this->getConnection()->createQueryBuilder();
203
204
            $statement = $query
205 26
                ->select($columns)
206 26
                ->from($this->getTableNameForReading())
207 26
                ->where($column . '=' . $this->createTypedParameter($query, $identifier))
208 26
                ->execute();
209
210 22
            $statement->setFetchMode(PDO::FETCH_CLASS, $this->getClassName());
211 22
            $result = $statement->fetch();
212
213 22
            return $result === false ? null : $result;
214 4
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
215 4
            $message = 'Resource reading failed.';
216 4
            throw new RepositoryException($message, 0, $exception);
217
        }
218
    }
219
220
    /**
221
     * @param string|int $identifier
222
     * @param array      $values
223
     *
224
     * @return int
225
     *
226
     * @throws RepositoryException
227
     */
228 7
    protected function updateResource($identifier, array $values): int
229
    {
230
        try {
231 7
            $query = $this->getConnection()->createQueryBuilder();
232
233
            $query
234 7
                ->update($this->getTableNameForWriting())
235 7
                ->where($this->getPrimaryKeyName() . '=' . $this->createTypedParameter($query, $identifier));
236 7
            foreach ($values as $key => $value) {
237 7
                $query->set($key, $this->createTypedParameter($query, $value));
238
            }
239
240 7
            $numberOfUpdated = $query->execute();
241 4
            assert(is_int($numberOfUpdated) === true);
242
243 4
            return $numberOfUpdated;
244 3
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
245 3
            $message = 'Resource update failed.';
246 3
            throw new RepositoryException($message, 0, $exception);
247
        }
248
    }
249
250
    /**
251
     * @param string|int $identifier
252
     *
253
     * @return int
254
     *
255
     * @throws RepositoryException
256
     */
257 7
    protected function deleteResource($identifier): int
258
    {
259
        try {
260 7
            $query = $this->getConnection()->createQueryBuilder();
261
262
            $query
263 7
                ->delete($this->getTableNameForWriting())
264 7
                ->where($this->getPrimaryKeyName() . '=' . $this->createTypedParameter($query, $identifier));
265
266 7
            $numberOfDeleted = $query->execute();
267 4
            assert(is_int($numberOfDeleted) === true);
268
269 4
            return $numberOfDeleted;
270 3
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
271 3
            $message = 'Resource deletion failed.';
272 3
            throw new RepositoryException($message, 0, $exception);
273
        }
274
    }
275
276
    /**
277
     * @param string|int $primaryKey
278
     * @param iterable   $foreignKeys
279
     * @param string     $intTableName
280
     * @param string     $intPrimaryKeyName
281
     * @param string     $intForeignKeyName
282
     *
283
     * @return void
284
     *
285
     * @throws RepositoryException
286
     */
287 20
    protected function createBelongsToManyRelationship(
288
        $primaryKey,
289
        iterable $foreignKeys,
290
        string $intTableName,
291
        string $intPrimaryKeyName,
292
        string $intForeignKeyName
293
    ): void {
294 20
        assert(is_string($primaryKey) === true || is_int($primaryKey) === true);
295
296
        try {
297
            $this->inTransaction(function () use (
298 20
                $intTableName,
299 20
                $intPrimaryKeyName,
300 20
                $intForeignKeyName,
301 20
                $primaryKey,
302 20
                $foreignKeys
303
            ): void {
304 20
                $connection = $this->getConnection();
305 20
                $query      = $connection->createQueryBuilder();
306
307 20
                $query->insert($intTableName)->values([$intPrimaryKeyName => '?', $intForeignKeyName => '?']);
308 20
                $statement = $connection->prepare($query->getSQL());
309
310 18
                foreach ($foreignKeys as $value) {
311 18
                    assert(is_string($value) === true || is_int($value) === true);
312 18
                    $statement->bindValue(1, $primaryKey);
313 18
                    $statement->bindValue(2, $value);
314 18
                    $statement->execute();
315
                }
316 20
            });
317 2
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
318 2
            $message = 'Belongs-to-Many relationship creation failed.';
319 2
            throw new RepositoryException($message, 0, $exception);
320
        }
321
    }
322
323
    /**
324
     * @param string|int $identifier
325
     * @param string     $intTableName
326
     * @param string     $intPrimaryKeyName
327
     * @param string     $intForeignKeyName
328
     *
329
     * @return string[]
330
     *
331
     * @throws RepositoryException
332
     */
333 22
    protected function readBelongsToManyRelationshipIdentifiers(
334
        $identifier,
335
        string $intTableName,
336
        string $intPrimaryKeyName,
337
        string $intForeignKeyName
338
    ): array {
339
        try {
340 22
            $connection = $this->getConnection();
341 22
            $query      = $connection->createQueryBuilder();
342
343
            $query
344 22
                ->select($intForeignKeyName)
345 22
                ->from($intTableName)
346 22
                ->where($intPrimaryKeyName . '=' . $this->createTypedParameter($query, $identifier));
347
348 22
            $statement = $query->execute();
349 20
            $statement->setFetchMode(PDO::FETCH_NUM);
350 20
            $result = array_column($statement->fetchAll(), 0);
351
352 20
            return $result;
353 2
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
354 2
            $message = 'Belongs-to-Many relationship reading failed.';
355 2
            throw new RepositoryException($message, 0, $exception);
356
        }
357
    }
358
359
    /**
360
     * @param string     $intTableName
361
     * @param string     $intPrimaryKeyName
362
     * @param string|int $identifier
363
     *
364
     * @return int
365
     *
366
     * @throws RepositoryException
367
     */
368 5
    protected function deleteBelongsToManyRelationshipIdentifiers(
369
        string $intTableName,
370
        string $intPrimaryKeyName,
371
        $identifier
372
    ): int {
373
        try {
374 5
            $connection = $this->getConnection();
375 5
            $query      = $connection->createQueryBuilder();
376
377
            $query
378 5
                ->delete($intTableName)
379 5
                ->where($intPrimaryKeyName . '=' . $this->createTypedParameter($query, $identifier));
380
381 5
            $numberOfDeleted = $query->execute();
382 3
            assert(is_int($numberOfDeleted) === true);
383
384 3
            return $numberOfDeleted;
385 2
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
386 2
            $message = 'Belongs-to-Many relationship deletion failed.';
387 2
            throw new RepositoryException($message, 0, $exception);
388
        }
389
    }
390
391
    /**
392
     * @param string|int $identifier
393
     * @param string     $hasManyTableName
394
     * @param string     $hasManyColumn
395
     * @param string     $hasManyFkName
396
     *
397
     * @return string[]
398
     *
399
     * @throws RepositoryException
400
     */
401 16
    protected function readHasManyRelationshipColumn(
402
        $identifier,
403
        string $hasManyTableName,
404
        string $hasManyColumn,
405
        string $hasManyFkName
406
    ): array {
407
        try {
408 16
            $connection = $this->getConnection();
409 16
            $query      = $connection->createQueryBuilder();
410
411
            $query
412 16
                ->select($hasManyColumn)
413 16
                ->from($hasManyTableName)
414 16
                ->where($hasManyFkName . '=' . $this->createTypedParameter($query, $identifier));
415
416 16
            $statement = $query->execute();
417 15
            $statement->setFetchMode(PDO::FETCH_NUM);
418 15
            $result = array_column($statement->fetchAll(), 0);
419
420 15
            return $result;
421 1
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
422 1
            $message = 'Has-Many relationship reading failed.';
423 1
            throw new RepositoryException($message, 0, $exception);
424
        }
425
    }
426
427
    /**
428
     * @param DateTimeInterface $dateTime
429
     *
430
     * @return string
431
     *
432
     * @throws RepositoryException
433
     */
434 43
    protected function getDateTimeForDb(DateTimeInterface $dateTime): string
435
    {
436
        try {
437 43
            return Type::getType(Type::DATETIME)
438 43
                ->convertToDatabaseValue($dateTime, $this->getConnection()->getDatabasePlatform());
439 1
        } catch (DBALException $exception) {
440 1
            $message = 'DateTime conversion to database format failed.';
441 1
            throw new RepositoryException($message, 0, $exception);
442
        }
443
    }
444
445
    /**
446
     * @return DatabaseSchemaInterface
447
     */
448 63
    protected function getDatabaseSchema(): DatabaseSchemaInterface
449
    {
450 63
        return $this->databaseSchema;
451
    }
452
453
    /**
454
     * @param DatabaseSchemaInterface $databaseSchema
455
     *
456
     * @return self
457
     */
458 71
    protected function setDatabaseSchema(DatabaseSchemaInterface $databaseSchema): self
459
    {
460 71
        $this->databaseSchema = $databaseSchema;
461
462 71
        return $this;
463
    }
464
465
    /**
466
     * @param QueryBuilder $query
467
     * @param mixed        $value
468
     *
469
     * @return string
470
     *
471
     * @SuppressWarnings(PHPMD.ElseExpression)
472
     *
473
     * @throws RepositoryException
474
     */
475 55
    protected function createTypedParameter(QueryBuilder $query, $value): string
476
    {
477 55
        if (is_bool($value) === true) {
478 28
            $type = PDO::PARAM_BOOL;
479 55
        } elseif (is_int($value) === true) {
480 17
            $type = PDO::PARAM_INT;
481 50
        } elseif ($value === null) {
482 35
            $type = PDO::PARAM_NULL;
483 50
        } elseif ($value instanceof DateTimeInterface) {
484 42
            $value = $this->getDateTimeForDb($value);
485 42
            $type  = PDO::PARAM_STR;
486
        } else {
487 45
            $type = PDO::PARAM_STR;
488
        }
489
490 55
        return $query->createNamedParameter($value, $type);
491
    }
492
493
    /**
494
     * Helps to ignore exception handling for cases when they do not arise (e.g. having current date and time).
495
     *
496
     * @param Closure $closure
497
     * @param mixed   $defaultValue
498
     *
499
     * @return mixed|null
500
     */
501 43
    protected function ignoreException(Closure $closure, $defaultValue = null)
0 ignored issues
show
Unused Code introduced by
The parameter $defaultValue is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
502
    {
503
        try {
504 43
            $defaultValue = call_user_func($closure);
505 1
        } catch (Exception $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
506
        }
507
508 43
        return $defaultValue;
509
    }
510
}
511