SortableListener   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 155
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 31
eloc 68
dl 0
loc 155
rs 9.92
c 1
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 2
A preUpdate() 0 13 3
A getSubscribedEvents() 0 7 1
A preRemove() 0 6 2
A movePosition() 0 21 3
A prePersist() 0 7 2
A loadClassMetadata() 0 14 4
A uniquePosition() 0 10 5
A getNextPosition() 0 23 4
A addGroupFilter() 0 10 5
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 Core23\Doctrine\Util\ClassUtils;
17
use Doctrine\Common\EventSubscriber;
18
use Doctrine\ORM\EntityManager;
19
use Doctrine\ORM\Event\LifecycleEventArgs;
20
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
21
use Doctrine\ORM\Event\PreUpdateEventArgs;
22
use Doctrine\ORM\Events;
23
use Doctrine\ORM\Mapping\MappingException;
24
use Doctrine\ORM\NonUniqueResultException;
25
use Doctrine\ORM\QueryBuilder;
26
use Doctrine\ORM\UnitOfWork;
27
use Symfony\Component\PropertyAccess\PropertyAccess;
28
use Symfony\Component\PropertyAccess\PropertyAccessor;
29
30
final class SortableListener implements EventSubscriber
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
    public function getSubscribedEvents()
50
    {
51
        return [
52
            Events::prePersist,
53
            Events::preUpdate,
54
            Events::preRemove,
55
            Events::loadClassMetadata,
56
        ];
57
    }
58
59
    public function prePersist(LifecycleEventArgs $args): void
60
    {
61
        if (!$args->getEntity() instanceof PositionAwareInterface) {
62
            return;
63
        }
64
65
        $this->uniquePosition($args);
66
    }
67
68
    public function preUpdate(PreUpdateEventArgs $args): void
69
    {
70
        if (!$args->getEntity() instanceof PositionAwareInterface) {
71
            return;
72
        }
73
74
        $position = $args->getEntity()->getPosition();
75
76
        if ($args->hasChangedField('position')) {
77
            $position = $args->getOldValue('position');
78
        }
79
80
        $this->uniquePosition($args, $position);
81
    }
82
83
    public function preRemove(LifecycleEventArgs $args): void
84
    {
85
        $entity = $args->getEntity();
86
87
        if ($entity instanceof PositionAwareInterface) {
88
            $this->movePosition($args->getEntityManager(), $entity, -1);
89
        }
90
    }
91
92
    /**
93
     * @throws MappingException
94
     */
95
    public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void
96
    {
97
        $meta = $eventArgs->getClassMetadata();
98
99
        $reflClass = $meta->getReflectionClass();
100
101
        if (null === $reflClass || !ClassUtils::containsTrait($reflClass, SortableTrait::class)) {
102
            return;
103
        }
104
105
        if (!$meta->hasField('position')) {
106
            $meta->mapField([
107
                'type'      => 'integer',
108
                'fieldName' => 'position',
109
            ]);
110
        }
111
    }
112
113
    private function uniquePosition(LifecycleEventArgs $args, ?int $oldPosition = null): void
114
    {
115
        $entity = $args->getEntity();
116
117
        if ($entity instanceof PositionAwareInterface) {
118
            if (null === $entity->getPosition()) {
119
                $position = $this->getNextPosition($args->getEntityManager(), $entity);
120
                $entity->setPosition($position);
121
            } elseif (null !== $oldPosition && $oldPosition !== $entity->getPosition()) {
122
                $this->movePosition($args->getEntityManager(), $entity);
123
            }
124
        }
125
    }
126
127
    private function movePosition(EntityManager $em, PositionAwareInterface $entity, int $direction = 1): void
128
    {
129
        $uow  = $em->getUnitOfWork();
130
        $meta = $em->getClassMetadata(\get_class($entity));
131
132
        $qb = $em->createQueryBuilder()
133
            ->update($meta->getName(), 'e')
134
            ->set('e.position', 'e.position + '.$direction)
135
        ;
136
137
        if ($direction > 0) {
138
            $qb->andWhere('e.position <= :position')->setParameter('position', $entity->getPosition());
139
        } elseif ($direction < 0) {
140
            $qb->andWhere('e.position >= :position')->setParameter('position', $entity->getPosition());
141
        } else {
142
            return;
143
        }
144
145
        $this->addGroupFilter($qb, $entity, $uow);
146
147
        $qb->getQuery()->execute();
148
    }
149
150
    private function getNextPosition(EntityManager $em, PositionAwareInterface $entity): int
151
    {
152
        $meta = $em->getClassMetadata(\get_class($entity));
153
154
        $qb = $em->createQueryBuilder()
155
            ->select('e')
156
            ->from($meta->getName(), 'e')
157
            ->addOrderBy('e.position', 'DESC')
158
            ->setMaxResults(1)
159
        ;
160
161
        $this->addGroupFilter($qb, $entity);
162
163
        try {
164
            $result = $qb->getQuery()->getOneOrNullResult();
165
166
            if ($result instanceof PositionAwareInterface && null !== $result->getPosition()) {
167
                return $result->getPosition() + 1;
168
            }
169
        } catch (NonUniqueResultException $ignored) {
0 ignored issues
show
Coding Style Comprehensibility introduced by core23
Consider adding a comment why this CATCH block is empty.
Loading history...
170
        }
171
172
        return 0;
173
    }
174
175
    private function addGroupFilter(QueryBuilder $qb, PositionAwareInterface $entity, UnitOfWork $uow = null): void
176
    {
177
        foreach ($entity->getPositionGroup() as $field) {
178
            $value = $this->propertyAccessor->getValue($entity, $field);
179
180
            if (\is_object($value) && (null === $uow || null === $uow->getSingleIdentifierValue($value))) {
181
                continue;
182
            }
183
184
            $qb->andWhere('e.'.$field.' = :'.$field)->setParameter($field, $value);
185
        }
186
    }
187
}
188