Completed
Push — master ( a293b3...45cee5 )
by Maximilian
03:05
created

UserRepository::getFollowingIdsByUser()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 27
rs 8.8571
cc 3
eloc 17
nc 4
nop 2
1
<?php
2
3
/*
4
 * This file is part of the Sententiaregum project.
5
 *
6
 * (c) Maximilian Bosch <[email protected]>
7
 * (c) Ben Bieler <[email protected]>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
declare(strict_types=1);
14
15
namespace AppBundle\Service\Doctrine\Repository;
16
17
use AppBundle\Model\Core\DTO\PaginatableDTO;
18
use AppBundle\Model\User\User;
19
use AppBundle\Model\User\UserReadRepositoryInterface;
20
use AppBundle\Model\User\UserWriteRepositoryInterface;
21
use DateTime;
22
use Doctrine\DBAL\Types\Type;
23
use Doctrine\ORM\EntityRepository;
24
use Doctrine\ORM\Query;
25
use Doctrine\ORM\Query\Expr\Join;
26
27
/**
28
 * Repository that contains custom dql calls for the user model.
29
 *
30
 * @author Maximilian Bosch <[email protected]>
31
 */
32
class UserRepository extends EntityRepository implements UserReadRepositoryInterface, UserWriteRepositoryInterface
33
{
34
    /**
35
     * {@inheritdoc}
36
     *
37
     * @throws \Exception If something went wrong with the transaction
38
     */
39
    public function deletePendingActivationsByDate(DateTime $dateTime): int
40
    {
41
        $connection = $this->_em->getConnection();
42
        try {
43
            // we have to wrap all the deletion logic into a transaction
44
            // although the deletion statement will be executed in a new nested transaction.
45
            // This is because all tables must be locked in order to prevent users from activating
46
            // their accounts when the purger has already started.
47
            // The activation must be done before that.
48
            $connection->beginTransaction();
49
50
            $query = $this->buildQueryForUserIdsWithOldPendingActivations($dateTime);
51
            $query->setHydrationMode(Query::HYDRATE_ARRAY);
52
53
            $qb       = $this->_em->createQueryBuilder();
54
            $affected = 0;
55
56
            if ($ids = $query->getResult()) {
57
                $qb
58
                    ->delete('Account:User', 'user')
59
                    ->where($qb->expr()->in('user.id', ':ids'))
60
                    ->setParameter(':ids', $ids);
61
62
                $affected = $qb->getQuery()->execute();
63
            }
64
65
            $connection->commit();
66
67
            return $affected;
68
        } catch (\Exception $ex) {
69
            $connection->rollBack();
70
71
            throw $ex;
72
        }
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     *
78
     * @throws \Exception If something in the DB transaction went wrong.
79
     */
80
    public function deleteAncientAttemptData(DateTime $dateTime): int
81
    {
82
        $connection = $this->_em->getConnection();
83
84
        // build a transaction on top of the rest to avoid side-effects
85
        try {
86
            $connection->beginTransaction();
87
88
            $qb     = $this->_em->createQueryBuilder();
89
            $search = clone $qb;
90
            $expr   = $qb->expr();
91
92
            // unfortunately DQL can't do joins on DELETE queries
93
            $idQuery = $search
94
                ->select('authentication_attempt.id')
95
                ->distinct()
96
                ->from('Account:AuthenticationAttempt', 'authentication_attempt')
97
                ->join('Account:User', 'user', Join::WITH, $expr->isMemberOf(
98
                    'authentication_attempt',
99
                    'user.failedAuthentications'
100
                ))
101
                ->where($expr->lt(
102
                    'authentication_attempt.latestDateTime',
103
                    ':date_time'
104
                ))
105
                ->setParameter(':date_time', $dateTime, Type::DATETIME)
106
                ->getQuery()
107
                ->setHydrationMode(Query::HYDRATE_ARRAY);
108
109
            $ids = array_column($idQuery->getResult(), 'id');
110
111
            if (!empty($ids)) {
112
                $list          = implode(',', array_fill(0, count($ids), '?'));
113
                $relationQuery = $connection->prepare("DELETE FROM `FailedAuthAttempt2User` WHERE `attemptId` IN ({$list})");
114
115
                // drop relations manually before removing the models
116
                $result = $relationQuery->execute($ids);
117
118
                if ($result) {
119
                    $affected = $qb
120
                        ->delete('Account:AuthenticationAttempt', 'attempt')
121
                        ->where($expr->in('attempt.id', ':ids'))
122
                        ->setParameter(':ids', $ids)
123
                        ->getQuery()
124
                        ->execute();
125
126
                    return $affected + $result;
127
                }
128
            }
129
130
            $connection->commit();
131
132
            return 0;
133
        } catch (\Exception $ex) {
134
            $connection->rollBack();
135
136
            throw $ex;
137
        }
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    public function getFollowingIdsByUser(User $user, PaginatableDTO $dto): array
144
    {
145
        $qb = $this->_em->createQueryBuilder();
146
147
        $qb
148
            ->select('partial user.{id}')
149
            ->distinct()
150
            ->from('Account:User', 'user')
151
            ->join('Account:User', 'current_user', Join::WITH, $qb->expr()->eq('current_user.id', ':user_id'))
152
            ->where($qb->expr()->isMemberOf('user', 'current_user.following'))
153
            ->setParameter(':user_id', $user->getId());
154
155
        // copy pagination parameters
156
        // into the query builder.
157
        if (null !== $limit = $dto->limit) {
158
            $qb->setMaxResults($limit);
159
        }
160
        if (null !== $offset = $dto->offset) {
161
            $qb->setFirstResult($offset);
162
        }
163
164
        $result = $qb
165
            ->getQuery()
166
            ->getResult(Query::HYDRATE_ARRAY);
167
168
        return array_column($result, 'id');
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174
    public function findUserByUsernameAndActivationKey(string $username, string $activationKey)
175
    {
176
        return $this->findOneBy(['username' => $username, 'pendingActivation.key' => $activationKey]);
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182
    public function filterUniqueUsernames(array $names): array
183
    {
184
        $qb     = $this->_em->createQueryBuilder();
185
        $result = $qb
186
            ->select('user.username')
187
            ->from('Account:User', 'user')
188
            ->where($qb->expr()->in('user.username', ':names'))
189
            ->setParameter(':names', $names)
190
            ->getQuery()
191
            ->getResult(Query::HYDRATE_ARRAY);
192
193
        $nonUnique = array_column($result, 'username');
194
195
        return array_values(// re-index array after filter process
196
            array_filter(
197
                $names,
198
                function ($username) use ($nonUnique) {
199
                    return !in_array($username, $nonUnique, true);
200
                }
201
            )
202
        );
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208
    public function save(User $user): string
209
    {
210
        $this->_em->persist($user);
211
212
        return $user->getId();
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218
    public function remove(User $user)
219
    {
220
        $this->_em->remove($user);
221
    }
222
223
    /**
224
     * Creates a list of old entity ids that should be removed.
225
     *
226
     * @param DateTime $dateTime
227
     *
228
     * @return Query
229
     */
230
    private function buildQueryForUserIdsWithOldPendingActivations(DateTime $dateTime): Query
231
    {
232
        $qb = $this->_em->createQueryBuilder();
233
234
        $qb
235
            ->select('partial user.{id}')
236
            ->distinct()
237
            ->from('Account:User', 'user')
238
            ->where($qb->expr()->lt('user.pendingActivation.activationDate', ':date_time'))
239
            ->setParameter(':date_time', $dateTime, Type::DATETIME);
240
241
        return $qb->getQuery();
242
    }
243
}
244