Passed
Push — master ( d70c89...2d756e )
by Christian
01:55
created

SortableListener::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * (c) Christian Gripp <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Core23\Doctrine\EventListener\ORM;
13
14
use Core23\Doctrine\Model\PositionAwareInterface;
15
use Core23\Doctrine\Model\Traits\SortableTrait;
16
use Doctrine\ORM\EntityManager;
17
use Doctrine\ORM\Event\LifecycleEventArgs;
18
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
19
use Doctrine\ORM\Event\PreUpdateEventArgs;
20
use Doctrine\ORM\Events;
21
use Doctrine\ORM\Mapping\ClassMetadata;
22
use Doctrine\ORM\Mapping\MappingException;
23
use Doctrine\ORM\NonUniqueResultException;
24
use Doctrine\ORM\QueryBuilder;
25
use Doctrine\ORM\UnitOfWork;
26
use LogicException;
27
use Symfony\Component\PropertyAccess\PropertyAccess;
28
use Symfony\Component\PropertyAccess\PropertyAccessor;
29
30
final class SortableListener extends AbstractListener
31
{
32
    /**
33
     * @var PropertyAccessor
34
     */
35
    private $propertyAccessor;
36
37
    /**
38
     * @param PropertyAccessor $propertyAccessor
39
     */
40
    public function __construct(PropertyAccessor $propertyAccessor = null)
41
    {
42
        if (null === $propertyAccessor) {
43
            $propertyAccessor = PropertyAccess::createPropertyAccessor();
44
        }
45
46
        $this->propertyAccessor = $propertyAccessor;
47
    }
48
49
    /**
50
     * {@inheritdoc}
51
     */
52
    public function getSubscribedEvents()
53
    {
54
        return [
55
            Events::prePersist,
56
            Events::preUpdate,
57
            Events::preRemove,
58
            Events::loadClassMetadata,
59
        ];
60
    }
61
62
    /**
63
     * @param LifecycleEventArgs $args
64
     */
65
    public function prePersist(LifecycleEventArgs $args): void
66
    {
67
        if (!$args->getEntity() instanceof PositionAwareInterface) {
68
            return;
69
        }
70
71
        $this->uniquePosition($args);
72
    }
73
74
    /**
75
     * @param PreUpdateEventArgs $args
76
     */
77
    public function preUpdate(PreUpdateEventArgs $args): void
78
    {
79
        if (!$args->getEntity() instanceof PositionAwareInterface) {
80
            return;
81
        }
82
83
        $position = $args->getEntity()->getPosition();
84
85
        if ($args->hasChangedField('position')) {
86
            $position = $args->getOldValue('position');
87
        }
88
89
        $this->uniquePosition($args, $position);
90
    }
91
92
    /**
93
     * @param LifecycleEventArgs $args
94
     */
95
    public function preRemove(LifecycleEventArgs $args): void
96
    {
97
        $entity = $args->getEntity();
98
99
        if ($entity instanceof PositionAwareInterface) {
100
            $this->movePosition($args->getEntityManager(), $entity, -1);
101
        }
102
    }
103
104
    /**
105
     * @param LoadClassMetadataEventArgs $eventArgs
106
     *
107
     * @throws MappingException
108
     */
109
    public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void
110
    {
111
        $meta = $eventArgs->getClassMetadata();
112
113
        if (!$meta instanceof ClassMetadata) {
0 ignored issues
show
introduced by core23
$meta is always a sub-type of Doctrine\ORM\Mapping\ClassMetadata.
Loading history...
114
            throw new LogicException(sprintf('Class metadata was no ORM but %s', \get_class($meta)));
115
        }
116
117
        $reflClass = $meta->getReflectionClass();
118
119
        if (null === $reflClass || !$this->containsTrait($reflClass, SortableTrait::class)) {
120
            return;
121
        }
122
123
        if (!$meta->hasField('position')) {
124
            $meta->mapField([
125
                'type'      => 'integer',
126
                'fieldName' => 'position',
127
            ]);
128
        }
129
    }
130
131
    /**
132
     * @param LifecycleEventArgs $args
133
     * @param int|null           $oldPosition
134
     */
135
    private function uniquePosition(LifecycleEventArgs $args, ?int $oldPosition = null): void
136
    {
137
        $entity = $args->getEntity();
138
139
        if ($entity instanceof PositionAwareInterface) {
140
            if (null === $entity->getPosition()) {
141
                $position = $this->getNextPosition($args->getEntityManager(), $entity);
142
                $entity->setPosition($position);
143
            } elseif ($oldPosition && $oldPosition !== $entity->getPosition()) {
0 ignored issues
show
Bug Best Practice introduced by core23
The expression $oldPosition of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
144
                $this->movePosition($args->getEntityManager(), $entity);
145
            }
146
        }
147
    }
148
149
    /**
150
     * @param EntityManager          $em
151
     * @param PositionAwareInterface $entity
152
     * @param int                    $direction
153
     */
154
    private function movePosition(EntityManager $em, PositionAwareInterface $entity, int $direction = 1): void
155
    {
156
        $uow  = $em->getUnitOfWork();
157
        $meta = $em->getClassMetadata(\get_class($entity));
158
159
        $qb = $em->createQueryBuilder()
160
            ->update($meta->getName(), 'e')
161
            ->set('e.position', 'e.position + '.$direction)
162
        ;
163
164
        if ($direction > 0) {
165
            $qb->andWhere('e.position <= :position')->setParameter('position', $entity->getPosition());
166
        } elseif ($direction < 0) {
167
            $qb->andWhere('e.position >= :position')->setParameter('position', $entity->getPosition());
168
        } else {
169
            return;
170
        }
171
172
        $this->addGroupFilter($qb, $entity, $uow);
173
174
        $qb->getQuery()->execute();
175
    }
176
177
    /**
178
     * @param EntityManager          $em
179
     * @param PositionAwareInterface $entity
180
     *
181
     * @return int
182
     */
183
    private function getNextPosition(EntityManager $em, PositionAwareInterface $entity): int
184
    {
185
        $meta = $em->getClassMetadata(\get_class($entity));
186
187
        $qb = $em->createQueryBuilder()
188
            ->select('e')
189
            ->from($meta->getName(), 'e')
190
            ->addOrderBy('e.position', 'DESC')
191
            ->setMaxResults(1)
192
        ;
193
194
        $this->addGroupFilter($qb, $entity);
195
196
        try {
197
            $result = $qb->getQuery()->getOneOrNullResult();
198
199
            return ($result instanceof PositionAwareInterface ? $result->getPosition() : 0) + 1;
200
        } catch (NonUniqueResultException $ignored) {
201
            return 0;
202
        }
203
    }
204
205
    /**
206
     * @param QueryBuilder           $qb
207
     * @param PositionAwareInterface $entity
208
     * @param UnitOfWork             $uow
209
     */
210
    private function addGroupFilter(QueryBuilder $qb, PositionAwareInterface $entity, UnitOfWork $uow = null): void
211
    {
212
        foreach ($entity->getPositionGroup() as $field) {
213
            $value = $this->propertyAccessor->getValue($entity, $field);
214
215
            if (\is_object($value) && (null === $uow || null === $uow->getSingleIdentifierValue($value))) {
216
                continue;
217
            }
218
219
            $qb->andWhere('e.'.$field.' = :'.$field)->setParameter($field, $value);
220
        }
221
    }
222
}
223