Issues (281)

Branch: master

Validator/UniqueDataTransferObjectValidator.php (1 issue)

1
<?php
2
3
namespace ForkCMS\Bundle\CoreBundle\Validator;
4
5
use Doctrine\Persistence\ManagerRegistry;
6
use Doctrine\Persistence\Mapping\ClassMetadata;
7
use Doctrine\Persistence\ObjectManager;
8
use Doctrine\Persistence\ObjectRepository;
9
use Symfony\Component\Validator\Constraint;
10
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
11
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
12
use Symfony\Component\Validator\ConstraintValidator;
13
14
/**
15
 * Unique Entity Validator checks if one or a set of fields contain unique values.
16
 */
17
final class UniqueDataTransferObjectValidator extends ConstraintValidator
18
{
19
    /** @var ManagerRegistry */
20
    private $registry;
21
22
    public function __construct(ManagerRegistry $registry)
23
    {
24
        $this->registry = $registry;
25
    }
26
27
    /**
28
     * @param object $dataTransferObject
29
     * @param Constraint $constraint
30
     *
31
     * @throws UnexpectedTypeException
32
     * @throws ConstraintDefinitionException
33
     */
34
    public function validate($dataTransferObject, Constraint $constraint): void
35
    {
36
        if (!$constraint instanceof UniqueDataTransferObject) {
37
            throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\UniqueDataTransferObject');
38
        }
39
        if (!\is_array($constraint->fields) && !\is_string($constraint->fields)) {
40
            throw new UnexpectedTypeException($constraint->fields, 'array');
41
        }
42
        if ($constraint->errorPath !== null && !\is_string($constraint->errorPath)) {
43
            throw new UnexpectedTypeException($constraint->errorPath, 'string or null');
44
        }
45
        $fields = (array) $constraint->fields;
46
        if (\count($fields) === 0) {
47
            throw new ConstraintDefinitionException('At least one field has to be specified.');
48
        }
49
        if ($dataTransferObject === null) {
50
            return;
51
        }
52
        $om = $this->getObjectManager($dataTransferObject, $constraint);
53
        $class = $om->getClassMetadata($constraint->entityClass ?? \get_class($dataTransferObject->getEntity()));
54
        $criteria = [];
55
        $hasNullValue = false;
56
        foreach ($fields as $fieldName) {
57
            if (!$class->hasField($fieldName) && !$class->hasAssociation($fieldName)) {
58
                throw new ConstraintDefinitionException(
59
                    sprintf(
60
                        'The field "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.',
61
                        $fieldName
62
                    )
63
                );
64
            }
65
            $fieldValue = $dataTransferObject->$fieldName;
66
            if ($fieldValue === null) {
67
                $hasNullValue = true;
68
            }
69
            if ($constraint->ignoreNull && $fieldValue === null) {
70
                continue;
71
            }
72
            $criteria[$fieldName] = $fieldValue;
73
            if ($criteria[$fieldName] !== null && $class->hasAssociation($fieldName)) {
74
                /* Ensure the Proxy is initialized before using reflection to
75
                 * read its identifiers. This is necessary because the wrapped
76
                 * getter methods in the Proxy are being bypassed.
77
                 */
78
                $om->initializeObject($criteria[$fieldName]);
79
            }
80
        }
81
        // validation doesn't fail if one of the fields is null and if null values should be ignored
82
        if ($hasNullValue && $constraint->ignoreNull) {
83
            return;
84
        }
85
        // skip validation if there are no criteria (this can happen when the
86
        // "ignoreNull" option is enabled and fields to be checked are null
87
        if (empty($criteria)) {
88
            return;
89
        }
90
        $repository = $this->getRepository($dataTransferObject, $constraint, $om, $class);
91
        $result = $repository->{$constraint->repositoryMethod}($criteria);
92
        if ($result instanceof \IteratorAggregate) {
93
            $result = $result->getIterator();
94
        }
95
        /* If the result is a MongoCursor, it must be advanced to the first
96
         * element. Rewinding should have no ill effect if $result is another
97
         * iterator implementation.
98
         */
99
        if ($result instanceof \Iterator) {
100
            $result->rewind();
101
        } elseif (\is_array($result)) {
102
            reset($result);
103
        }
104
        /* If no entity matched the query criteria or a single entity matched,
105
         * which is the same as the entity being validated, the criteria is
106
         * unique.
107
         */
108
        if (\count($result) === 0
109
            || (
110
                \count($result) === 1
111
                && $dataTransferObject->getEntity() === ($result instanceof \Iterator
112
                    ? $result->current() : current($result))
113
            )
114
        ) {
115
            return;
116
        }
117
        $errorPath = $constraint->errorPath ?? $fields[0];
118
        $invalidValue = $criteria[$errorPath] ?? $criteria[$fields[0]];
119
        $this->context->buildViolation($constraint->message)
120
            ->atPath($errorPath)
121
            ->setParameter('{{ value }}', $this->formatWithIdentifiers($om, $class, $invalidValue))
122
            ->setInvalidValue($invalidValue)
123
            ->setCode(UniqueDataTransferObject::NOT_UNIQUE_ERROR)
124
            ->setCause($result)
125
            ->addViolation();
126
    }
127
128
    private function formatWithIdentifiers(ObjectManager $em, ClassMetadata $class, $value): string
129
    {
130
        if (!\is_object($value) || $value instanceof \DateTimeInterface) {
131
            return $this->formatValue($value, self::PRETTY_DATE);
132
        }
133
        $idClass = \get_class($value);
134
        $identifiers = $this->getIdentifiers($em, $class, $value, $idClass);
135
        if (!$identifiers) {
136
            return sprintf('object("%s")', $idClass);
137
        }
138
        array_walk(
139
            $identifiers,
140
            function (&$id, $field) {
141
                if (!\is_object($id) || $id instanceof \DateTimeInterface) {
142
                    $idAsString = $this->formatValue($id, self::PRETTY_DATE);
143
                } else {
144
                    $idAsString = sprintf('object("%s")', \get_class($id));
145
                }
146
                $id = sprintf('%s => %s', $field, $idAsString);
147
            }
148
        );
149
150
        return sprintf('object("%s") identified by (%s)', $idClass, implode(', ', $identifiers));
151
    }
152
153
    private function getIdentifiers(ObjectManager $om, ClassMetadata $class, $value, string $idClass): array
154
    {
155
        if ($class->getName() === $idClass) {
156
            return $class->getIdentifierValues($value);
157
        }
158
        // non unique value might be a composite PK that consists of other entity objects
159
        if ($om->getMetadataFactory()->hasMetadataFor($idClass)) {
160
            return $om->getClassMetadata($idClass)->getIdentifierValues($value);
161
        }
162
        // this case might happen if the non unique column has a custom doctrine type and its value is an object
163
        // in which case we cannot get any identifiers for it
164
        return [];
165
    }
166
167
    private function getRepository(
168
        $dataTransferObject,
169
        Constraint $constraint,
170
        ObjectManager $om,
171
        ClassMetadata $class
172
    ): ObjectRepository {
173
        if ($constraint->entityClass === null) {
174
            return $om->getRepository(\get_class($dataTransferObject->getEntity()));
175
        }
176
        /* Retrieve repository from given entity name.
177
         * We ensure the retrieved repository can handle the entity
178
         * by checking the entity is the same, or subclass of the supported entity.
179
         */
180
        $repository = $om->getRepository($constraint->entityClass);
181
        $supportedClass = $repository->getClassName();
182
        if ($dataTransferObject->getEntity() !== null
183
            && !$dataTransferObject->getEntity() instanceof $supportedClass) {
184
            throw new ConstraintDefinitionException(
185
                sprintf(
186
                    'The "%s" entity repository does not support the "%s" entity. The entity should be an instance of or extend "%s".',
187
                    $constraint->entityClass,
188
                    $class->getName(),
189
                    $supportedClass
190
                )
191
            );
192
        }
193
194
        return $repository;
195
    }
196
197
    private function getObjectManager($dataTransferObject, Constraint $constraint): ObjectManager
198
    {
199
        if ($constraint->em) {
200
            $om = $this->registry->getManager($constraint->em);
201
            if (!$om) {
0 ignored issues
show
$om is of type Doctrine\Persistence\ObjectManager, thus it always evaluated to true.
Loading history...
202
                throw new ConstraintDefinitionException(
203
                    sprintf('Object manager "%s" does not exist.', $constraint->em)
204
                );
205
            }
206
207
            return $om;
208
        }
209
210
        $om = $this->registry->getManagerForClass(
211
            $constraint->entityClass ?? \get_class($dataTransferObject->getEntity())
212
        );
213
214
        if (!$om) {
215
            return $this->registry->getManager();
216
        }
217
218
        return $om;
219
    }
220
}
221