TokenRepository::disable()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 8
cts 8
cp 1
rs 9.8333
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php declare(strict_types=1);
2
3
namespace Limoncello\Passport\Repositories;
4
5
/**
6
 * Copyright 2015-2019 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use DateInterval;
22
use DateTimeImmutable;
23
use Doctrine\DBAL\DBALException;
24
use Doctrine\DBAL\Query\QueryBuilder;
25
use Limoncello\Passport\Contracts\Entities\ScopeInterface;
26
use Limoncello\Passport\Contracts\Entities\TokenInterface;
27
use Limoncello\Passport\Contracts\Repositories\TokenRepositoryInterface;
28
use Limoncello\Passport\Exceptions\RepositoryException;
29
use PDO;
30
use function assert;
31
use function is_int;
32
33
/**
34
 * @package Limoncello\Passport
35
 *
36
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
37
 */
38
abstract class TokenRepository extends BaseRepository implements TokenRepositoryInterface
39
{
40
    /**
41
     * @inheritdoc
42
     *
43
     * @throws RepositoryException
44
     *
45
     * @SuppressWarnings(PHPMD.ElseExpression)
46
     */
47 3
    public function createCode(TokenInterface $code): TokenInterface
48
    {
49
        try {
50
            $now    = $this->ignoreException(function (): DateTimeImmutable {
51 3
                return new DateTimeImmutable();
52 3
            });
53 3
            $schema = $this->getDatabaseSchema();
54
            $values = [
55 3
                $schema->getTokensClientIdentityColumn() => $code->getClientIdentifier(),
56 3
                $schema->getTokensUserIdentityColumn()   => $code->getUserIdentifier(),
57 3
                $schema->getTokensCodeColumn()           => $code->getCode(),
58 3
                $schema->getTokensIsScopeModified()      => $code->isScopeModified(),
59 3
                $schema->getTokensCodeCreatedAtColumn()  => $now,
60
            ];
61
62 3
            $tokenIdentifier = null;
63 3
            if (empty($scopeIdentifiers = $code->getScopeIdentifiers()) === false) {
64
                $this->inTransaction(function () use ($values, $scopeIdentifiers, &$tokenIdentifier) {
65 1
                    $this->createResource($values);
0 ignored issues
show
Documentation introduced by
$values is of type array<string,*>, but the function expects a object<Limoncello\Passport\Repositories\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
66 1
                    $tokenIdentifier = $this->getLastInsertId();
67 1
                    $this->bindScopeIdentifiers($tokenIdentifier, $scopeIdentifiers);
68 1
                });
69
            } else {
70 2
                $this->createResource($values);
0 ignored issues
show
Documentation introduced by
$values is of type array<string,*>, but the function expects a object<Limoncello\Passport\Repositories\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
71 1
                $tokenIdentifier = $this->getLastInsertId();
72
            }
73
74 2
            $code->setIdentifier($tokenIdentifier)->setCodeCreatedAt($now);
75
76 2
            return $code;
77 1
        } catch (RepositoryException $exception) {
78 1
            $message = 'Token code creation failed.';
79 1
            throw new RepositoryException($message, 0, $exception);
80
        }
81
    }
82
83
    /**
84
     * @inheritdoc
85
     *
86
     * @throws RepositoryException
87
     */
88 3
    public function assignValuesToCode(TokenInterface $token, int $expirationInSeconds): void
89
    {
90
        try {
91 3
            $query = $this->getConnection()->createQueryBuilder();
92
93
            $now   = $this->ignoreException(function (): DateTimeImmutable {
94 3
                return new DateTimeImmutable();
95 3
            });
96 3
            $dbNow = $this->createTypedParameter($query, $now);
97
98
            $earliestExpired = $this->ignoreException(function () use ($now, $expirationInSeconds) : DateTimeImmutable {
99
                /** @var DateTimeImmutable $now */
100 3
                return $now->sub(new DateInterval("PT{$expirationInSeconds}S"));
101 3
            });
102
103 3
            $schema = $this->getDatabaseSchema();
104
            $query
105 3
                ->update($this->getTableNameForWriting())
106 3
                ->where($schema->getTokensCodeColumn() . '=' . $this->createTypedParameter($query, $token->getCode()))
107 3
                ->andWhere(
108 3
                    $schema->getTokensCodeCreatedAtColumn() . '>' .
109 3
                    $this->createTypedParameter($query, $earliestExpired)
110
                )
111 3
                ->set($schema->getTokensValueColumn(), $this->createTypedParameter($query, $token->getValue()))
112 3
                ->set($schema->getTokensTypeColumn(), $this->createTypedParameter($query, $token->getType()))
113 3
                ->set($schema->getTokensValueCreatedAtColumn(), $dbNow);
114
115 3
            if ($token->getRefreshValue() !== null) {
116
                $query
117 2
                    ->set(
118 2
                        $schema->getTokensRefreshColumn(),
119 2
                        $this->createTypedParameter($query, $token->getRefreshValue())
120 2
                    )->set($schema->getTokensRefreshCreatedAtColumn(), $dbNow);
121
            }
122
123 3
            $numberOfUpdated = $query->execute();
124 2
            assert(is_int($numberOfUpdated) === true);
125 1
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
126 1
            $message = 'Assigning token values by code failed.';
127 1
            throw new RepositoryException($message, 0, $exception);
128
        }
129
    }
130
131
    /**
132
     * @inheritdoc
133
     *
134
     * @throws RepositoryException
135
     *
136
     * @SuppressWarnings(PHPMD.ElseExpression)
137
     */
138 10
    public function createToken(TokenInterface $token): TokenInterface
139
    {
140
        try {
141
            $now        = $this->ignoreException(function (): DateTimeImmutable {
142 10
                return new DateTimeImmutable();
143 10
            });
144 10
            $schema     = $this->getDatabaseSchema();
145 10
            $hasRefresh = $token->getRefreshValue() !== null;
146 10
            $values     = $hasRefresh === false ? [
147 4
                $schema->getTokensClientIdentityColumn() => $token->getClientIdentifier(),
148 4
                $schema->getTokensUserIdentityColumn()   => $token->getUserIdentifier(),
149 4
                $schema->getTokensValueColumn()          => $token->getValue(),
150 4
                $schema->getTokensTypeColumn()           => $token->getType(),
151 4
                $schema->getTokensIsScopeModified()      => $token->isScopeModified(),
152 4
                $schema->getTokensIsEnabledColumn()      => $token->isEnabled(),
153 4
                $schema->getTokensValueCreatedAtColumn() => $now,
154
            ] : [
155 6
                $schema->getTokensClientIdentityColumn()   => $token->getClientIdentifier(),
156 6
                $schema->getTokensUserIdentityColumn()     => $token->getUserIdentifier(),
157 6
                $schema->getTokensValueColumn()            => $token->getValue(),
158 6
                $schema->getTokensTypeColumn()             => $token->getType(),
159 6
                $schema->getTokensIsScopeModified()        => $token->isScopeModified(),
160 6
                $schema->getTokensIsEnabledColumn()        => $token->isEnabled(),
161 6
                $schema->getTokensValueCreatedAtColumn()   => $now,
162 6
                $schema->getTokensRefreshColumn()          => $token->getRefreshValue(),
163 10
                $schema->getTokensRefreshCreatedAtColumn() => $now,
164
            ];
165
166 10
            $tokenIdentifier = null;
167 10
            if (empty($scopeIdentifiers = $token->getScopeIdentifiers()) === false) {
168
                $this->inTransaction(function () use ($values, $scopeIdentifiers, &$tokenIdentifier) {
169 5
                    $this->createResource($values);
0 ignored issues
show
Documentation introduced by
$values is of type array<string,*>, but the function expects a object<Limoncello\Passport\Repositories\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
170 5
                    $tokenIdentifier = $this->getLastInsertId();
171 5
                    $this->bindScopeIdentifiers($tokenIdentifier, $scopeIdentifiers);
172 5
                });
173
            } else {
174 5
                $this->createResource($values);
0 ignored issues
show
Documentation introduced by
$values is of type array<string,*>, but the function expects a object<Limoncello\Passport\Repositories\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
175 4
                $tokenIdentifier = $this->getLastInsertId();
176
            }
177
178 9
            $token->setIdentifier($tokenIdentifier)->setValueCreatedAt($now);
179 9
            if ($hasRefresh === true) {
180 6
                $token->setRefreshCreatedAt($now);
181
            }
182
183 9
            return $token;
184 1
        } catch (RepositoryException $exception) {
185 1
            $message = 'Token creation failed';
186 1
            throw new RepositoryException($message, 0, $exception);
187
        }
188
    }
189
190
    /**
191
     * @inheritdoc
192
     *
193
     * @throws RepositoryException
194
     */
195 1
    public function bindScopes(int $identifier, iterable $scopes): void
196
    {
197
        $getIdentifiers = function (iterable $scopes): iterable {
198 1
            foreach ($scopes as $scope) {
199
                /** @var ScopeInterface $scope */
200 1
                assert($scope instanceof ScopeInterface);
201 1
                yield $scope->getIdentifier();
202
            }
203 1
        };
204
205 1
        $this->bindScopeIdentifiers($identifier, $getIdentifiers($scopes));
206
    }
207
208
    /**
209
     * @inheritdoc
210
     *
211
     * @throws RepositoryException
212
     */
213 8
    public function bindScopeIdentifiers(int $identifier, iterable $scopeIdentifiers): void
214
    {
215
        try {
216 8
            $schema = $this->getDatabaseSchema();
217 8
            $this->createBelongsToManyRelationship(
218 8
                $identifier,
219 8
                $scopeIdentifiers,
0 ignored issues
show
Bug introduced by
It seems like $scopeIdentifiers defined by parameter $scopeIdentifiers on line 213 can also be of type array<integer,string>; however, Limoncello\Passport\Repo...ngsToManyRelationship() does only seem to accept object<Limoncello\Passport\Repositories\iterable>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
220 8
                $schema->getTokensScopesTable(),
221 8
                $schema->getTokensScopesTokenIdentityColumn(),
222 8
                $schema->getTokensScopesScopeIdentityColumn()
223
            );
224 1
        } catch (RepositoryException $exception) {
225 1
            $message = 'Binding token scopes failed.';
226 1
            throw new RepositoryException($message, 0, $exception);
227
        }
228
    }
229
230
    /**
231
     * @inheritdoc
232
     *
233
     * @throws RepositoryException
234
     */
235 3
    public function unbindScopes(int $identifier): void
236
    {
237
        try {
238 3
            $schema = $this->getDatabaseSchema();
239 3
            $this->deleteBelongsToManyRelationshipIdentifiers(
240 3
                $schema->getTokensScopesTable(),
241 3
                $schema->getTokensScopesTokenIdentityColumn(),
242 3
                $identifier
243
            );
244 1
        } catch (RepositoryException $exception) {
245 1
            $message = 'Unbinding token scopes failed.';
246 1
            throw new RepositoryException($message, 0, $exception);
247
        }
248
    }
249
250
    /**
251
     * @inheritdoc
252
     *
253
     * @throws RepositoryException
254
     */
255 4
    public function read(int $identifier): ?TokenInterface
256
    {
257
        try {
258 4
            return $this->readResource($identifier);
259 1
        } catch (RepositoryException $exception) {
260 1
            $message = 'Token reading failed.';
261 1
            throw new RepositoryException($message, 0, $exception);
262
        }
263
    }
264
265
    /**
266
     * @inheritdoc
267
     *
268
     * @throws RepositoryException
269
     */
270 3
    public function readByCode(string $code, int $expirationInSeconds): ?TokenInterface
271
    {
272 3
        $schema = $this->getDatabaseSchema();
273 3
        return $this->readEnabledTokenByColumnWithExpirationCheck(
274 3
            $code,
275 3
            $schema->getTokensCodeColumn(),
276 3
            $expirationInSeconds,
277 3
            $schema->getTokensCodeCreatedAtColumn()
278
        );
279
    }
280
281
    /**
282
     * @inheritdoc
283
     *
284
     * @throws RepositoryException
285
     */
286 4
    public function readByValue(string $tokenValue, int $expirationInSeconds): ?TokenInterface
287
    {
288 4
        $schema = $this->getDatabaseSchema();
289 4
        return $this->readEnabledTokenByColumnWithExpirationCheck(
290 4
            $tokenValue,
291 4
            $schema->getTokensValueColumn(),
292 4
            $expirationInSeconds,
293 4
            $schema->getTokensValueCreatedAtColumn()
294
        );
295
    }
296
297
    /**
298
     * @inheritdoc
299
     *
300
     * @throws RepositoryException
301
     */
302 4
    public function readByRefresh(string $refreshValue, int $expirationInSeconds): ?TokenInterface
303
    {
304 4
        $schema = $this->getDatabaseSchema();
305 4
        return $this->readEnabledTokenByColumnWithExpirationCheck(
306 4
            $refreshValue,
307 4
            $schema->getTokensRefreshColumn(),
308 4
            $expirationInSeconds,
309 4
            $schema->getTokensRefreshCreatedAtColumn()
310
        );
311
    }
312
313
    /**
314
     * @inheritdoc
315
     *
316
     * @throws RepositoryException
317
     */
318 2
    public function readByUser(int $userId, int $expirationInSeconds, int $limit = null): array
319
    {
320 2
        $schema = $this->getDatabaseSchema();
321
        /** @var TokenInterface[] $tokens */
322 2
        $tokens = $this->readEnabledTokensByColumnWithExpirationCheck(
323 2
            (string)$userId,
324 2
            $schema->getTokensUserIdentityColumn(),
325 2
            $expirationInSeconds,
326 2
            $schema->getTokensValueCreatedAtColumn(),
327 2
            ['*'],
328 2
            $limit
329
        );
330
331 1
        return $tokens;
332
    }
333
334
    /**
335
     * @inheritdoc
336
     *
337
     * @throws RepositoryException
338
     */
339 11
    public function readScopeIdentifiers(int $identifier): array
340
    {
341
        try {
342 11
            $schema = $this->getDatabaseSchema();
343 11
            return $this->readBelongsToManyRelationshipIdentifiers(
344 11
                $identifier,
345 11
                $schema->getTokensScopesTable(),
346 11
                $schema->getTokensScopesTokenIdentityColumn(),
347 11
                $schema->getTokensScopesScopeIdentityColumn()
348
            );
349 1
        } catch (RepositoryException $exception) {
350 1
            $message = 'Reading scopes for a token failed.';
351 1
            throw new RepositoryException($message, 0, $exception);
352
        }
353
    }
354
355
    /**
356
     * @inheritdoc
357
     *
358
     * @throws RepositoryException
359
     */
360 3
    public function updateValues(TokenInterface $token): void
361
    {
362
        try {
363 3
            $query = $this->getConnection()->createQueryBuilder();
364
365 3
            $schema = $this->getDatabaseSchema();
366
            $now    = $this->ignoreException(function (): DateTimeImmutable {
367 3
                return new DateTimeImmutable();
368 3
            });
369 3
            $dbNow  = $this->createTypedParameter($query, $now);
370
            $query
371 3
                ->update($this->getTableNameForWriting())
372 3
                ->where($this->getPrimaryKeyName() . '=' . $this->createTypedParameter($query, $token->getIdentifier()))
373 3
                ->set($schema->getTokensValueColumn(), $this->createTypedParameter($query, $token->getValue()))
374 3
                ->set($schema->getTokensValueCreatedAtColumn(), $dbNow);
375 3
            if ($token->getRefreshValue() !== null) {
376
                $query
377 2
                    ->set(
378 2
                        $schema->getTokensRefreshColumn(),
379 2
                        $this->createTypedParameter($query, $token->getRefreshValue())
380 2
                    )->set($schema->getTokensRefreshCreatedAtColumn(), $dbNow);
381
            }
382
383 3
            $numberOfUpdated = $query->execute();
384 2
            assert(is_int($numberOfUpdated) === true);
385 2
            if ($numberOfUpdated > 0) {
386 2
                $token->setValueCreatedAt($now);
387 2
                if ($token->getRefreshValue() !== null) {
388 2
                    $token->setRefreshCreatedAt($now);
389
                }
390
            }
391 1
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
392 1
            $message = 'Token update failed.';
393 1
            throw new RepositoryException($message, 0, $exception);
394
        }
395
    }
396
397
    /**
398
     * @inheritdoc
399
     *
400
     * @throws RepositoryException
401
     */
402 1
    public function delete(int $identifier): void
403
    {
404 1
        $this->deleteResource($identifier);
405
    }
406
407
    /**
408
     * @inheritdoc
409
     *
410
     * @throws RepositoryException
411
     */
412 3
    public function disable(int $identifier): void
413
    {
414 3
        $query = $this->getConnection()->createQueryBuilder();
415
416 3
        $schema = $this->getDatabaseSchema();
417
        $query
418 3
            ->update($this->getTableNameForWriting())
419 3
            ->where($this->getPrimaryKeyName() . '=' . $this->createTypedParameter($query, $identifier))
420 3
            ->set($schema->getTokensIsEnabledColumn(), $this->createTypedParameter($query, false));
421
422 3
        $numberOfUpdated = $query->execute();
423 3
        assert(is_int($numberOfUpdated) === true);
424
    }
425
426
    /**
427
     * @inheritdoc
428
     */
429 19
    protected function getTableNameForWriting(): string
430
    {
431 19
        return $this->getDatabaseSchema()->getTokensTable();
432
    }
433
434
    /**
435
     * @inheritdoc
436
     */
437 8
    protected function getPrimaryKeyName(): string
438
    {
439 8
        return $this->getDatabaseSchema()->getTokensIdentityColumn();
440
    }
441
442
    /**
443
     * @param string $identifier
444
     * @param string $column
445
     * @param int    $expirationInSeconds
446
     * @param string $createdAtColumn
447
     * @param array  $columns
448
     *
449
     * @return TokenInterface|null
450
     *
451
     * @throws RepositoryException
452
     */
453 9
    protected function readEnabledTokenByColumnWithExpirationCheck(
454
        string $identifier,
455
        string $column,
456
        int $expirationInSeconds,
457
        string $createdAtColumn,
458
        array $columns = ['*']
459
    ): ?TokenInterface {
460
        try {
461 9
            $query = $this->createEnabledTokenByColumnWithExpirationCheckQuery(
462 9
                $identifier,
463 9
                $column,
464 9
                $expirationInSeconds,
465 9
                $createdAtColumn,
466 9
                $columns
467
            );
468
469 9
            $statement = $query->execute();
470 8
            $statement->setFetchMode(PDO::FETCH_CLASS, $this->getClassName());
471 8
            $result = $statement->fetch();
472
473 8
            return $result === false ? null : $result;
474 1
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
475 1
            $message = 'Reading token failed.';
476 1
            throw new RepositoryException($message, 0, $exception);
477
        }
478
    }
479
480
    /** @noinspection PhpTooManyParametersInspection
481
     * @param string   $identifier
482
     * @param string   $column
483
     * @param int      $expirationInSeconds
484
     * @param string   $createdAtColumn
485
     * @param array    $columns
486
     * @param int|null $limit
487
     *
488
     * @return array
489
     *
490
     * @throws RepositoryException
491
     */
492 2
    protected function readEnabledTokensByColumnWithExpirationCheck(
493
        string $identifier,
494
        string $column,
495
        int $expirationInSeconds,
496
        string $createdAtColumn,
497
        array $columns = ['*'],
498
        int $limit = null
499
    ): array {
500
        try {
501 2
            $query = $this->createEnabledTokenByColumnWithExpirationCheckQuery(
502 2
                $identifier,
503 2
                $column,
504 2
                $expirationInSeconds,
505 2
                $createdAtColumn,
506 2
                $columns
507
            );
508 2
            $limit === null ?: $query->setMaxResults($limit);
509
510 2
            $statement = $query->execute();
511 1
            $statement->setFetchMode(PDO::FETCH_CLASS, $this->getClassName());
512
513 1
            $result = [];
514 1
            while (($token = $statement->fetch()) !== false) {
515
                /** @var TokenInterface $token */
516 1
                $result[$token->getIdentifier()] = $token;
517
            }
518
519 1
            return $result;
520 1
        } /** @noinspection PhpRedundantCatchClauseInspection */ catch (DBALException $exception) {
521 1
            $message = 'Reading tokens failed.';
522 1
            throw new RepositoryException($message, 0, $exception);
523
        }
524
    }
525
526
    /**
527
     * @param string $identifier
528
     * @param string $column
529
     * @param int    $expirationInSeconds
530
     * @param string $createdAtColumn
531
     * @param array  $columns
532
     *
533
     * @return QueryBuilder
534
     */
535 11
    protected function createEnabledTokenByColumnWithExpirationCheckQuery(
536
        string $identifier,
537
        string $column,
538
        int $expirationInSeconds,
539
        string $createdAtColumn,
540
        array $columns = ['*']
541
    ): QueryBuilder {
542 11
        $query = $this->getConnection()->createQueryBuilder();
543 11
        $query = $this->addExpirationCondition(
544 11
            $query->select($columns)
545 11
                ->from($this->getTableNameForReading())
546 11
                ->where($column . '=' . $this->createTypedParameter($query, $identifier))
547
                // SQLite and MySQL work fine with just 1 but PostgreSQL wants it to be a string '1'
548 11
                ->andWhere($query->expr()->eq($this->getDatabaseSchema()->getTokensIsEnabledColumn(), "'1'")),
549 11
            $expirationInSeconds,
550 11
            $createdAtColumn
551
        );
552
553 11
        return $query;
554
    }
555
556
    /**
557
     * @param QueryBuilder $query
558
     * @param int          $expirationInSeconds
559
     * @param string       $createdAtColumn
560
     *
561
     * @return QueryBuilder
562
     */
563 15
    protected function addExpirationCondition(
564
        QueryBuilder $query,
565
        int $expirationInSeconds,
566
        string $createdAtColumn
567
    ): QueryBuilder {
568
        $earliestExpired = $this->ignoreException(function () use ($expirationInSeconds) : DateTimeImmutable {
569 15
            return (new DateTimeImmutable())->sub(new DateInterval("PT{$expirationInSeconds}S"));
570 15
        });
571
572 15
        $query->andWhere($createdAtColumn . '>' . $this->createTypedParameter($query, $earliestExpired));
573
574 15
        return $query;
575
    }
576
}
577