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\Property;
17
18/**
19 * Concrete configuration object for the PropertyMapper.
20 */
21class PropertyMappingConfiguration implements PropertyMappingConfigurationInterface
22{
23    /**
24     * Placeholder in property paths for multi-valued types
25     */
26    const PROPERTY_PATH_PLACEHOLDER = '*';
27
28    /**
29     * multi-dimensional array which stores type-converter specific configuration:
30     * 1. Dimension: Fully qualified class name of the type converter
31     * 2. Dimension: Configuration Key
32     * Value: Configuration Value
33     *
34     * @var array
35     */
36    protected $configuration;
37
38    /**
39     * Stores the configuration for specific child properties.
40     *
41     * @var \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface[]
42     */
43    protected $subConfigurationForProperty = [];
44
45    /**
46     * Keys which should be renamed
47     *
48     * @var array
49     */
50    protected $mapping = [];
51
52    /**
53     * @var \TYPO3\CMS\Extbase\Property\TypeConverterInterface
54     */
55    protected $typeConverter;
56
57    /**
58     * List of allowed property names to be converted
59     *
60     * @var array
61     */
62    protected $propertiesToBeMapped = [];
63
64    /**
65     * List of property names to be skipped during property mapping
66     *
67     * @var array
68     */
69    protected $propertiesToSkip = [];
70
71    /**
72     * List of disallowed property names which will be ignored while property mapping
73     *
74     * @var array
75     */
76    protected $propertiesNotToBeMapped = [];
77
78    /**
79     * If TRUE, unknown properties will be skipped during property mapping
80     *
81     * @var bool
82     */
83    protected $skipUnknownProperties = false;
84
85    /**
86     * If TRUE, unknown properties will be mapped.
87     *
88     * @var bool
89     */
90    protected $mapUnknownProperties = false;
91
92    /**
93     * The behavior is as follows:
94     *
95     * - if a property has been explicitly forbidden using allowAllPropertiesExcept(...), it is directly rejected
96     * - if a property has been allowed using allowProperties(...), it is directly allowed.
97     * - if allowAllProperties* has been called, we allow unknown properties
98     * - else, return FALSE.
99     *
100     * @param string $propertyName
101     * @return bool TRUE if the given propertyName should be mapped, FALSE otherwise.
102     */
103    public function shouldMap($propertyName)
104    {
105        if (isset($this->propertiesNotToBeMapped[$propertyName])) {
106            return false;
107        }
108
109        if (isset($this->propertiesToBeMapped[$propertyName])) {
110            return true;
111        }
112
113        if (isset($this->subConfigurationForProperty[self::PROPERTY_PATH_PLACEHOLDER])) {
114            return true;
115        }
116
117        return $this->mapUnknownProperties;
118    }
119
120    /**
121     * Check if the given $propertyName should be skipped during mapping.
122     *
123     * @param string $propertyName
124     * @return bool
125     */
126    public function shouldSkip($propertyName)
127    {
128        return isset($this->propertiesToSkip[$propertyName]);
129    }
130
131    /**
132     * Allow all properties in property mapping, even unknown ones.
133     *
134     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration this
135     */
136    public function allowAllProperties()
137    {
138        $this->mapUnknownProperties = true;
139        return $this;
140    }
141
142    /**
143     * Allow a list of specific properties. All arguments of
144     * allowProperties are used here (varargs).
145     *
146     * Example: allowProperties('title', 'content', 'author')
147     *
148     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration
149     */
150    public function allowProperties()
151    {
152        foreach (func_get_args() as $propertyName) {
153            $this->propertiesToBeMapped[$propertyName] = $propertyName;
154        }
155        return $this;
156    }
157
158    /**
159     * Skip a list of specific properties. All arguments of
160     * skipProperties are used here (varargs).
161     *
162     * Example: skipProperties('unused', 'dummy')
163     *
164     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration this
165     */
166    public function skipProperties()
167    {
168        foreach (func_get_args() as $propertyName) {
169            $this->propertiesToSkip[$propertyName] = $propertyName;
170        }
171        return $this;
172    }
173
174    /**
175     * Allow all properties during property mapping, but reject a few
176     * selected ones (blacklist).
177     *
178     * Example: allowAllPropertiesExcept('password', 'userGroup')
179     *
180     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration this
181     */
182    public function allowAllPropertiesExcept()
183    {
184        $this->mapUnknownProperties = true;
185
186        foreach (func_get_args() as $propertyName) {
187            $this->propertiesNotToBeMapped[$propertyName] = $propertyName;
188        }
189        return $this;
190    }
191
192    /**
193     * When this is enabled, properties that are disallowed will be skipped
194     * instead of triggering an error during mapping.
195     *
196     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration this
197     */
198    public function skipUnknownProperties()
199    {
200        $this->skipUnknownProperties = true;
201        return $this;
202    }
203
204    /**
205     * Whether unknown (unconfigured) properties should be skipped during
206     * mapping, instead if causing an error.
207     *
208     * @return bool
209     */
210    public function shouldSkipUnknownProperties()
211    {
212        return $this->skipUnknownProperties;
213    }
214
215    /**
216     * Returns the sub-configuration for the passed $propertyName. Must ALWAYS return a valid configuration object!
217     *
218     * @param string $propertyName
219     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface the property mapping configuration for the given $propertyName.
220     */
221    public function getConfigurationFor($propertyName)
222    {
223        if (isset($this->subConfigurationForProperty[$propertyName])) {
224            return $this->subConfigurationForProperty[$propertyName];
225        }
226        if (isset($this->subConfigurationForProperty[self::PROPERTY_PATH_PLACEHOLDER])) {
227            return $this->subConfigurationForProperty[self::PROPERTY_PATH_PLACEHOLDER];
228        }
229
230        return new self();
231    }
232
233    /**
234     * Maps the given $sourcePropertyName to a target property name.
235     *
236     * @param string $sourcePropertyName
237     * @return string property name of target
238     */
239    public function getTargetPropertyName($sourcePropertyName)
240    {
241        if (isset($this->mapping[$sourcePropertyName])) {
242            return $this->mapping[$sourcePropertyName];
243        }
244        return $sourcePropertyName;
245    }
246
247    /**
248     * @param string $typeConverterClassName
249     * @param string $key
250     * @return mixed configuration value for the specific $typeConverterClassName. Can be used by Type Converters to fetch converter-specific configuration.
251     */
252    public function getConfigurationValue($typeConverterClassName, $key)
253    {
254        if (!isset($this->configuration[$typeConverterClassName][$key])) {
255            return null;
256        }
257
258        return $this->configuration[$typeConverterClassName][$key];
259    }
260
261    /**
262     * Define renaming from Source to Target property.
263     *
264     * @param string $sourcePropertyName
265     * @param string $targetPropertyName
266     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration this
267     */
268    public function setMapping($sourcePropertyName, $targetPropertyName)
269    {
270        $this->mapping[$sourcePropertyName] = $targetPropertyName;
271        return $this;
272    }
273
274    /**
275     * Set all options for the given $typeConverter.
276     *
277     * @param string $typeConverter class name of type converter
278     * @param array $options
279     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration this
280     */
281    public function setTypeConverterOptions($typeConverter, array $options)
282    {
283        foreach ($this->getTypeConvertersWithParentClasses($typeConverter) as $typeConverter) {
284            $this->configuration[$typeConverter] = $options;
285        }
286        return $this;
287    }
288
289    /**
290     * Set a single option (denoted by $optionKey) for the given $typeConverter.
291     *
292     * @param string $typeConverter class name of type converter
293     * @param string $optionKey
294     * @param mixed $optionValue
295     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration this
296     */
297    public function setTypeConverterOption($typeConverter, $optionKey, $optionValue)
298    {
299        foreach ($this->getTypeConvertersWithParentClasses($typeConverter) as $typeConverter) {
300            $this->configuration[$typeConverter][$optionKey] = $optionValue;
301        }
302        return $this;
303    }
304
305    /**
306     * Get type converter classes including parents for the given type converter
307     *
308     * When setting an option on a subclassed type converter, this option must also be set on
309     * all its parent type converters.
310     *
311     * @param string $typeConverter The type converter class
312     * @return array Class names of type converters
313     */
314    protected function getTypeConvertersWithParentClasses($typeConverter)
315    {
316        $typeConverterClasses = class_parents($typeConverter);
317        $typeConverterClasses = $typeConverterClasses ?: [];
318        $typeConverterClasses[] = $typeConverter;
319        return $typeConverterClasses;
320    }
321
322    /**
323     * Returns the configuration for the specific property path, ready to be modified. Should be used
324     * inside a fluent interface like:
325     * $configuration->forProperty('foo.bar')->setTypeConverterOption(....)
326     *
327     * @param string $propertyPath
328     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration (or a subclass thereof)
329     */
330    public function forProperty($propertyPath)
331    {
332        $splittedPropertyPath = explode('.', $propertyPath);
333        return $this->traverseProperties($splittedPropertyPath);
334    }
335
336    /**
337     * Traverse the property configuration. Only used by forProperty().
338     *
339     * @param array $splittedPropertyPath
340     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration (or a subclass thereof)
341     */
342    public function traverseProperties(array $splittedPropertyPath)
343    {
344        if (empty($splittedPropertyPath)) {
345            return $this;
346        }
347
348        $currentProperty = array_shift($splittedPropertyPath);
349        if (!isset($this->subConfigurationForProperty[$currentProperty])) {
350            $type = static::class;
351            if (isset($this->subConfigurationForProperty[self::PROPERTY_PATH_PLACEHOLDER])) {
352                $this->subConfigurationForProperty[$currentProperty] = clone $this->subConfigurationForProperty[self::PROPERTY_PATH_PLACEHOLDER];
353            } else {
354                $this->subConfigurationForProperty[$currentProperty] = new $type();
355            }
356        }
357        return $this->subConfigurationForProperty[$currentProperty]->traverseProperties($splittedPropertyPath);
358    }
359
360    /**
361     * Return the type converter set for this configuration.
362     *
363     * @return \TYPO3\CMS\Extbase\Property\TypeConverterInterface|null
364     */
365    public function getTypeConverter()
366    {
367        return $this->typeConverter;
368    }
369
370    /**
371     * Set a type converter which should be used for this specific conversion.
372     *
373     * @param \TYPO3\CMS\Extbase\Property\TypeConverterInterface $typeConverter
374     * @return \TYPO3\CMS\Extbase\Property\PropertyMappingConfiguration this
375     */
376    public function setTypeConverter(TypeConverterInterface $typeConverter)
377    {
378        $this->typeConverter = $typeConverter;
379        return $this;
380    }
381}
382