Completed
Push — development ( 9250e3...3fd01d )
by Romain
01:14
created

ValidatorResolver::addMixedTypeValidators()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 23
rs 8.7972
cc 4
eloc 7
nc 4
nop 2
1
<?php
2
/*
3
 * 2017 Romain CANON <[email protected]>
4
 *
5
 * This file is part of the TYPO3 Configuration Object project.
6
 * It is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License, either
8
 * version 3 of the License, or any later version.
9
 *
10
 * For the full copyright and license information, see:
11
 * http://www.gnu.org/licenses/gpl-3.0.html
12
 */
13
14
namespace Romm\ConfigurationObject\Validation;
15
16
use Romm\ConfigurationObject\Core\Core;
17
use Romm\ConfigurationObject\Core\Service\ObjectService;
18
use Romm\ConfigurationObject\Service\Items\MixedTypes\MixedTypesInterface;
19
use Romm\ConfigurationObject\Service\Items\MixedTypes\MixedTypesService;
20
use Romm\ConfigurationObject\Validation\Validator\Internal\ConfigurationObjectValidator;
21
use Romm\ConfigurationObject\Validation\Validator\Internal\MixedTypeCollectionValidator;
22
use Romm\ConfigurationObject\Validation\Validator\Internal\MixedTypeValidator;
23
use TYPO3\CMS\Extbase\Reflection\ReflectionService as ExtbaseReflectionService;
24
use TYPO3\CMS\Extbase\Validation\Validator\CollectionValidator;
25
use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
26
use TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator;
27
use TYPO3\CMS\Extbase\Validation\Validator\ObjectValidatorInterface;
28
29
/**
30
 * Customized validator resolver, which it mostly used to support the mixed
31
 * types.
32
 *
33
 * When an instance of validator is created, we check if the type of this
34
 * validator is `CollectionValidator`: in this case we use a custom one instead:
35
 * `MixedTypeCollectionValidator` which will support the mixed types feature.
36
 */
37
class ValidatorResolver extends \TYPO3\CMS\Extbase\Validation\ValidatorResolver
38
{
39
40
    /**
41
     * @var array
42
     */
43
    protected $baseValidatorConjunctionsWithChecks = [];
44
45
    /**
46
     * @var ObjectService
47
     */
48
    protected $objectService;
49
50
    /**
51
     * @inheritdoc
52
     */
53
    public function createValidator($validatorType, array $validatorOptions = [])
54
    {
55
        return (CollectionValidator::class === $validatorType)
56
            ? parent::createValidator(MixedTypeCollectionValidator::class)
57
            : parent::createValidator($validatorType, $validatorOptions);
58
    }
59
60
    /**
61
     * @inheritdoc
62
     */
63
    protected function buildBaseValidatorConjunction($indexKey, $targetClassName, array $validationGroups = [])
64
    {
65
        parent::buildBaseValidatorConjunction($indexKey, $targetClassName, $validationGroups);
66
67
        /*
68
         * The code below is DIRTY: in order to use `SilentExceptionInterface`
69
         * feature we need an extended version of the `GenericObjectValidator`,
70
         * but this is hardcoded in:
71
         * \TYPO3\CMS\Extbase\Validation\ValidatorResolver::buildBaseValidatorConjunction()
72
         *
73
         * Here we replace every `GenericObjectValidator` by our own instance.
74
         *
75
         * Please, do not try this at home.
76
         */
77
        /** @var ConjunctionValidator $conjunctionValidator */
78
        $conjunctionValidator = $this->baseValidatorConjunctions[$indexKey];
79
80
        foreach ($conjunctionValidator->getValidators() as $validator) {
81
            if ($validator instanceof GenericObjectValidator) {
82
                /*
83
                 * A full check is processed on the properties to check for
84
                 * mixed types, in which case a validator is added to these
85
                 * properties.
86
                 */
87
                $this->addMixedTypeValidators($targetClassName, $validator);
88
89
                /** @var ConfigurationObjectValidator $newValidator */
90
                $newValidator = $this->objectManager->get(ConfigurationObjectValidator::class, []);
0 ignored issues
show
Unused Code introduced by
The call to ObjectManagerInterface::get() has too many arguments starting with array().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
91
92
                foreach ($validator->getPropertyValidators() as $propertyName => $propertyValidators) {
93
                    foreach ($propertyValidators as $propertyValidator) {
94
                        $newValidator->addPropertyValidator($propertyName, $propertyValidator);
95
                    }
96
                }
97
98
                // Replacing the old validator with the new one...
99
                $conjunctionValidator->removeValidator($validator);
100
                unset($validator);
101
                $conjunctionValidator->addValidator($newValidator);
102
            }
103
        }
104
    }
105
106
    /**
107
     * This function will list the properties of the given class, and filter on
108
     * the ones that do not have a validator assigned yet.
109
     *
110
     * @param string                 $targetClassName
111
     * @param GenericObjectValidator $validator
112
     */
113
    protected function addMixedTypeValidators($targetClassName, GenericObjectValidator $validator)
114
    {
115
        foreach ($this->reflectionService->getClassPropertyNames($targetClassName) as $property) {
116
            /*
117
             * If the property already is already bound to an object validator,
118
             * there is no need to do further checks.
119
             */
120
            if ($this->propertyHasObjectValidator($validator, $property)) {
121
                continue;
122
            }
123
124
            if ($this->propertyIsMixedType($targetClassName, $property)) {
125
                /*
126
                 * The property is mixed, a validator with the `mixedTypes`
127
                 * option is added, to delegate the validator resolving to
128
                 * later (when the property is actually filled).
129
                 */
130
                $objectValidator = $this->createValidator(MixedTypeValidator::class);
131
132
                $validator->addPropertyValidator($property, $objectValidator);
0 ignored issues
show
Bug introduced by
It seems like $objectValidator defined by $this->createValidator(\...edTypeValidator::class) on line 130 can be null; however, TYPO3\CMS\Extbase\Valida...:addPropertyValidator() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
133
            }
134
        }
135
    }
136
137
    /**
138
     * Checks among the existing validators of the given property if it does
139
     * already has an object validator (can be several types, like the classes
140
     * `ObjectValidator` or `ConjunctionValidator`, as long as they implement
141
     * the interface `ObjectValidatorInterface`).
142
     *
143
     * If one is found, `true` is returned.
144
     *
145
     * @param GenericObjectValidator $validator
146
     * @param string                 $property
147
     * @return bool
148
     */
149
    protected function propertyHasObjectValidator(GenericObjectValidator $validator, $property)
150
    {
151
        $propertiesValidators = $validator->getPropertyValidators();
152
153
        if (isset($propertiesValidators[$property])) {
154
            foreach ($propertiesValidators[$property] as $validator) {
155
                if ($validator instanceof ObjectValidatorInterface) {
156
                    return true;
157
                }
158
            }
159
        }
160
161
        return false;
162
    }
163
164
    /**
165
     * Checks if the given property is a mixed type.
166
     *
167
     * First, we check if the type of the property is a class that implements
168
     * the interface `MixedTypesInterface`.
169
     *
170
     * If not, we check if the property has the annotation `@mixedTypesResolver`
171
     * with a class name that implements the interface `MixedTypesInterface`.
172
     *
173
     * If one was found, `true` is returned.
174
     *
175
     * @param string $className
176
     * @param string $property
177
     * @return bool
178
     */
179
    protected function propertyIsMixedType($className, $property)
180
    {
181
        $mixedType = false;
182
183
        $propertySchema = $this->reflectionService->getClassSchema($className)->getProperty($property);
184
185
        if ($this->classIsMixedType($propertySchema['type'])) {
186
            $mixedType = true;
187
        } else {
188
            if ($this->reflectionService->isPropertyTaggedWith($className, $property, MixedTypesService::PROPERTY_ANNOTATION_MIXED_TYPE)) {
189
                $tags = $this->reflectionService->getPropertyTagValues($className, $property, MixedTypesService::PROPERTY_ANNOTATION_MIXED_TYPE);
190
                $mixedTypeClassName = reset($tags);
191
192
                if ($this->classIsMixedType($mixedTypeClassName)) {
193
                    $mixedType = true;
194
                }
195
            }
196
        }
197
198
        return $mixedType;
199
    }
200
201
    /**
202
     * @param string $className
203
     * @return bool
204
     */
205
    protected function classIsMixedType($className)
206
    {
207
        return Core::get()->classExists($className)
208
            && array_key_exists(MixedTypesInterface::class, class_implements($className));
209
    }
210
211
    /**
212
     * @param ExtbaseReflectionService $reflectionService
213
     */
214
    public function injectReflectionService(ExtbaseReflectionService $reflectionService)
215
    {
216
        $this->reflectionService = Core::get()->getReflectionService();
217
    }
218
219
    /**
220
     * @param ObjectService $objectService
221
     */
222
    public function injectObjectService(ObjectService $objectService)
223
    {
224
        $this->objectService = $objectService;
225
    }
226
}
227