1<?php
2namespace TYPO3\CMS\Extbase\Validation\Validator;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
18
19/**
20 * A generic object validator which allows for specifying property validators
21 */
22class GenericObjectValidator extends AbstractValidator implements ObjectValidatorInterface
23{
24    /**
25     * @var \SplObjectStorage[]
26     */
27    protected $propertyValidators = [];
28
29    /**
30     * Checks if the given value is valid according to the validator, and returns
31     * the Error Messages object which occurred.
32     *
33     * @param mixed $value The value that should be validated
34     * @return \TYPO3\CMS\Extbase\Error\Result
35     */
36    public function validate($value)
37    {
38        if (is_object($value) && $this->isValidatedAlready($value)) {
39            return $this->result;
40        }
41
42        $this->result = new \TYPO3\CMS\Extbase\Error\Result();
43        if ($this->acceptsEmptyValues === false || $this->isEmpty($value) === false) {
44            if (!is_object($value)) {
45                $this->addError('Object expected, %1$s given.', 1241099149, [gettype($value)]);
46            } elseif ($this->isValidatedAlready($value) === false) {
47                $this->markInstanceAsValidated($value);
48                $this->isValid($value);
49            }
50        }
51
52        return $this->result;
53    }
54
55    /**
56     * Load the property value to be used for validation.
57     *
58     * In case the object is a doctrine proxy, we need to load the real instance first.
59     *
60     * @param object $object
61     * @param string $propertyName
62     * @return mixed
63     */
64    protected function getPropertyValue($object, $propertyName)
65    {
66        // @todo add support for lazy loading proxies, if needed
67        if (ObjectAccess::isPropertyGettable($object, $propertyName)) {
68            return ObjectAccess::getProperty($object, $propertyName);
69        }
70        return ObjectAccess::getProperty($object, $propertyName, true);
71    }
72
73    /**
74     * Checks if the specified property of the given object is valid, and adds
75     * found errors to the $messages object.
76     *
77     * @param mixed $value The value to be validated
78     * @param \Traversable $validators The validators to be called on the value
79     * @param string $propertyName Name of ther property to check
80     */
81    protected function checkProperty($value, $validators, $propertyName)
82    {
83        /** @var \TYPO3\CMS\Extbase\Error\Result $result */
84        $result = null;
85        foreach ($validators as $validator) {
86            if ($validator instanceof ObjectValidatorInterface) {
87                $validator->setValidatedInstancesContainer($this->validatedInstancesContainer);
88            }
89            $currentResult = $validator->validate($value);
90            if ($currentResult->hasMessages()) {
91                if ($result == null) {
92                    $result = $currentResult;
93                } else {
94                    $result->merge($currentResult);
95                }
96            }
97        }
98        if ($result != null) {
99            $this->result->forProperty($propertyName)->merge($result);
100        }
101    }
102
103    /**
104     * Checks if the given value is valid according to the property validators.
105     *
106     * @param mixed $object The value that should be validated
107     */
108    protected function isValid($object)
109    {
110        foreach ($this->propertyValidators as $propertyName => $validators) {
111            $propertyValue = $this->getPropertyValue($object, $propertyName);
112            $this->checkProperty($propertyValue, $validators, $propertyName);
113        }
114    }
115
116    /**
117     * Checks the given object can be validated by the validator implementation
118     *
119     * @param mixed $object The object to be checked
120     * @return bool TRUE if the given value is an object
121     */
122    public function canValidate($object)
123    {
124        return is_object($object);
125    }
126
127    /**
128     * Adds the given validator for validation of the specified property.
129     *
130     * @param string $propertyName Name of the property to validate
131     * @param ValidatorInterface $validator The property validator
132     */
133    public function addPropertyValidator($propertyName, ValidatorInterface $validator)
134    {
135        if (!isset($this->propertyValidators[$propertyName])) {
136            $this->propertyValidators[$propertyName] = new \SplObjectStorage();
137        }
138        $this->propertyValidators[$propertyName]->attach($validator);
139    }
140
141    /**
142     * @param object $object
143     * @return bool
144     */
145    protected function isValidatedAlready($object)
146    {
147        if ($this->validatedInstancesContainer === null) {
148            $this->validatedInstancesContainer = new \SplObjectStorage();
149        }
150        if ($this->validatedInstancesContainer->contains($object)) {
151            return true;
152        }
153
154        return false;
155    }
156
157    /**
158     * @param $object
159     */
160    protected function markInstanceAsValidated($object): void
161    {
162        $this->validatedInstancesContainer->attach($object);
163    }
164
165    /**
166     * Returns all property validators - or only validators of the specified property
167     *
168     * @param string $propertyName Name of the property to return validators for
169     * @return array An array of validators
170     */
171    public function getPropertyValidators($propertyName = null)
172    {
173        if ($propertyName !== null) {
174            return $this->propertyValidators[$propertyName] ?? [];
175        }
176        return $this->propertyValidators;
177    }
178
179    /**
180     * @var \SplObjectStorage
181     */
182    protected $validatedInstancesContainer;
183
184    /**
185     * Allows to set a container to keep track of validated instances.
186     *
187     * @param \SplObjectStorage $validatedInstancesContainer A container to keep track of validated instances
188     */
189    public function setValidatedInstancesContainer(\SplObjectStorage $validatedInstancesContainer)
190    {
191        $this->validatedInstancesContainer = $validatedInstancesContainer;
192    }
193}
194