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