getIdentifiers()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 0
cts 10
cp 0
rs 9.7666
c 0
b 0
f 0
cc 3
nc 3
nop 4
crap 12
1
<?php
2
3
namespace SumoCoders\FrameworkCoreBundle\Validator;
4
5
use Doctrine\Common\Persistence\ManagerRegistry;
6
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
7
use Doctrine\Common\Persistence\ObjectManager;
8
use Doctrine\Common\Persistence\ObjectRepository;
9
use Doctrine\ORM\EntityManagerInterface;
10
use Symfony\Component\Validator\Constraint;
11
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
12
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
13
use Symfony\Component\Validator\ConstraintValidator;
14
15
/**
16
 * Unique Entity Validator checks if one or a set of fields contain unique values.
17
 */
18
class UniqueDataTransferObjectValidator extends ConstraintValidator
19
{
20
    /** @var ManagerRegistry */
21
    private $registry;
22
23
    public function __construct(ManagerRegistry $registry)
24
    {
25
        $this->registry = $registry;
26
    }
27
28
    /**
29
     * @param object $dataTransferObject
30
     * @param Constraint $constraint
31
     *
32
     * @throws UnexpectedTypeException
33
     * @throws ConstraintDefinitionException
34
     */
35
    public function validate($dataTransferObject, Constraint $constraint): void
36
    {
37
        if (!$constraint instanceof UniqueDataTransferObject) {
38
            throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\UniqueDataTransferObject');
39
        }
40
41
        if (!\is_array($constraint->fields) && !\is_string($constraint->fields)) {
42
            throw new UnexpectedTypeException($constraint->fields, 'array');
43
        }
44
45
        if ($constraint->errorPath !== null && !\is_string($constraint->errorPath)) {
46
            throw new UnexpectedTypeException($constraint->errorPath, 'string or null');
47
        }
48
49
        $fields = (array) $constraint->fields;
50
51
        if (\count($fields) === 0) {
52
            throw new ConstraintDefinitionException('At least one field has to be specified.');
53
        }
54
55
        if ($dataTransferObject === null) {
56
            return;
57
        }
58
59
        $om = $this->getObjectManager($dataTransferObject, $constraint);
60
61
        $class = $om->getClassMetadata($constraint->entityClass ?? \get_class($dataTransferObject->getEntity()));
62
63
        $criteria = array();
64
        $hasNullValue = false;
65
66
        foreach ($fields as $fieldName) {
67
            if (!$class->hasField($fieldName) && !$class->hasAssociation($fieldName)) {
68
                throw new ConstraintDefinitionException(
69
                    sprintf(
70
                        'The field "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.',
71
                        $fieldName
72
                    )
73
                );
74
            }
75
76
            $fieldValue = $dataTransferObject->$fieldName;
77
78
            if ($fieldValue === null) {
79
                $hasNullValue = true;
80
            }
81
82
            if ($constraint->ignoreNull && $fieldValue === null) {
83
                continue;
84
            }
85
86
            $criteria[$fieldName] = $fieldValue;
87
88
            if ($criteria[$fieldName] !== null && $class->hasAssociation($fieldName)) {
89
                /* Ensure the Proxy is initialized before using reflection to
90
                 * read its identifiers. This is necessary because the wrapped
91
                 * getter methods in the Proxy are being bypassed.
92
                 */
93
                $om->initializeObject($criteria[$fieldName]);
94
            }
95
        }
96
97
        // validation doesn't fail if one of the fields is null and if null values should be ignored
98
        if ($hasNullValue && $constraint->ignoreNull) {
99
            return;
100
        }
101
102
        // skip validation if there are no criteria (this can happen when the
103
        // "ignoreNull" option is enabled and fields to be checked are null
104
        if (empty($criteria)) {
105
            return;
106
        }
107
108
        $repository = $this->getRepository($dataTransferObject, $constraint, $om, $class);
109
110
        $result = $repository->{$constraint->repositoryMethod}($criteria);
111
112
        if ($result instanceof \IteratorAggregate) {
113
            $result = $result->getIterator();
114
        }
115
116
        /* If the result is a MongoCursor, it must be advanced to the first
117
         * element. Rewinding should have no ill effect if $result is another
118
         * iterator implementation.
119
         */
120
        if ($result instanceof \Iterator) {
121
            $result->rewind();
122
        } elseif (\is_array($result)) {
123
            reset($result);
124
        }
125
126
        /* If no entity matched the query criteria or a single entity matched,
127
         * which is the same as the entity being validated, the criteria is
128
         * unique.
129
         */
130
        if (\count($result) === 0
131
            || (
132
                \count($result) === 1
133
                && $dataTransferObject->getEntity() === ($result instanceof \Iterator ? $result->current() : current($result))
134
            )) {
135
            return;
136
        }
137
138
        $errorPath = $constraint->errorPath ?? $fields[0];
139
        $invalidValue = $criteria[$errorPath] ?? $criteria[$fields[0]];
140
141
        $this->context->buildViolation($constraint->message)
142
            ->atPath($errorPath)
143
            ->setParameter('{{ value }}', $this->formatWithIdentifiers($om, $class, $invalidValue))
144
            ->setInvalidValue($invalidValue)
145
            ->setCode(UniqueDataTransferObject::NOT_UNIQUE_ERROR)
146
            ->setCause($result)
147
            ->addViolation();
148
    }
149
150
    private function formatWithIdentifiers(ObjectManager $em, ClassMetadata $class, $value): string
151
    {
152
        if (!\is_object($value) || $value instanceof \DateTimeInterface) {
153
            return $this->formatValue($value, self::PRETTY_DATE);
154
        }
155
156
        $idClass = \get_class($value);
157
        $identifiers = $this->getIdentifiers($em, $class, $value, $idClass);
158
159
        if (!$identifiers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $identifiers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
160
            return sprintf('object("%s")', $idClass);
161
        }
162
163
        array_walk(
164
            $identifiers,
165
            function (&$id, $field) {
166
                if (!\is_object($id) || $id instanceof \DateTimeInterface) {
167
                    $idAsString = $this->formatValue($id, self::PRETTY_DATE);
168
                } else {
169
                    $idAsString = sprintf('object("%s")', \get_class($id));
170
                }
171
172
                $id = sprintf('%s => %s', $field, $idAsString);
173
            }
174
        );
175
176
        return sprintf('object("%s") identified by (%s)', $idClass, implode(', ', $identifiers));
177
    }
178
179
    private function getIdentifiers(ObjectManager $om, ClassMetadata $class, $value, string $idClass): array
180
    {
181
        if ($class->getName() === $idClass) {
182
            return $class->getIdentifierValues($value);
183
        }
184
185
        // non unique value might be a composite PK that consists of other entity objects
186
        if ($om->getMetadataFactory()->hasMetadataFor($idClass)) {
187
            return $om->getClassMetadata($idClass)->getIdentifierValues($value);
188
        }
189
190
        // this case might happen if the non unique column has a custom doctrine type and its value is an object
191
        // in which case we cannot get any identifiers for it
192
        return array();
193
    }
194
195
    private function getRepository($dataTransferObject, Constraint $constraint, ObjectManager $om, ClassMetadata $class): ObjectRepository
196
    {
197
        if ($constraint->entityClass === null) {
198
            return $om->getRepository(\get_class($dataTransferObject->getEntity()));
199
        }
200
201
        /* Retrieve repository from given entity name.
202
         * We ensure the retrieved repository can handle the entity
203
         * by checking the entity is the same, or subclass of the supported entity.
204
         */
205
        $repository = $om->getRepository($constraint->entityClass);
206
        $supportedClass = $repository->getClassName();
207
208
        if ($dataTransferObject->getEntity() !== null
209
            && !$dataTransferObject->getEntity() instanceof $supportedClass) {
210
            throw new ConstraintDefinitionException(
211
                sprintf(
212
                    'The "%s" entity repository does not support the "%s" entity. The entity should be an instance of or extend "%s".',
213
                    $constraint->entityClass,
214
                    $class->getName(),
215
                    $supportedClass
216
                )
217
            );
218
        }
219
220
        return $repository;
221
    }
222
223
    private function getObjectManager($dataTransferObject, Constraint $constraint): ObjectManager
224
    {
225
        if ($constraint->em) {
226
            $om = $this->registry->getManager($constraint->em);
227
228
            if (!$om) {
229
                throw new ConstraintDefinitionException(
230
                    sprintf('Object manager "%s" does not exist.', $constraint->em)
231
                );
232
            }
233
234
            return $om;
235
        }
236
237
        $om = $this->registry->getManagerForClass(
238
            $constraint->entityClass ?? \get_class($dataTransferObject->getEntity())
239
        );
240
241
        if (!$om) {
242
            return $this->registry->getManager();
243
        }
244
245
        return $om;
246
    }
247
}
248