[BUGFIX] Extbase: Optional arguments must not throw an Exception
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Mvc / Controller / Argument.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Mvc\Controller;
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
17 use TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException;
18
19 /**
20 * A controller argument
21 *
22 * @api
23 */
24 class Argument {
25
26 /**
27 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
28 * @inject
29 */
30 protected $objectManager;
31
32 /**
33 * @var \TYPO3\CMS\Extbase\Persistence\Generic\QueryFactory
34 * @inject
35 */
36 protected $queryFactory;
37
38 /**
39 * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
40 * @inject
41 */
42 protected $configurationManager;
43
44 /**
45 * This is the old property mapper, which has been completely rewritten for 1.4.
46 * @inject
47 *
48 * @var \TYPO3\CMS\Extbase\Property\Mapper
49 */
50 protected $deprecatedPropertyMapper;
51
52 /**
53 * The new, completely rewritten property mapper since Extbase 1.4.
54 *
55 * @var \TYPO3\CMS\Extbase\Property\PropertyMapper
56 * @inject
57 */
58 protected $propertyMapper;
59
60 /**
61 * @var \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfiguration
62 * @inject
63 */
64 protected $propertyMappingConfiguration;
65
66 /**
67 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
68 */
69 protected $reflectionService;
70
71 /**
72 * @var \TYPO3\CMS\Extbase\Service\TypeHandlingService
73 */
74 protected $typeHandlingService;
75
76 /**
77 * Name of this argument
78 *
79 * @var string
80 */
81 protected $name = '';
82
83 /**
84 * Short name of this argument
85 *
86 * @var string
87 */
88 protected $shortName = NULL;
89
90 /**
91 * Data type of this argument's value
92 *
93 * @var string
94 */
95 protected $dataType = NULL;
96
97 /**
98 * If the data type is an object, the class schema of the data type class is resolved
99 *
100 * @var \TYPO3\CMS\Extbase\Reflection\ClassSchema
101 */
102 protected $dataTypeClassSchema;
103
104 /**
105 * TRUE if this argument is required
106 *
107 * @var boolean
108 */
109 protected $isRequired = FALSE;
110
111 /**
112 * Actual value of this argument
113 *
114 * @var object
115 */
116 protected $value = NULL;
117
118 /**
119 * Default value. Used if argument is optional.
120 *
121 * @var mixed
122 */
123 protected $defaultValue = NULL;
124
125 /**
126 * A custom validator, used supplementary to the base validation
127 *
128 * @var \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface
129 */
130 protected $validator = NULL;
131
132 /**
133 * The validation results. This can be asked if the argument has errors.
134 *
135 * @var \TYPO3\CMS\Extbase\Error\Result
136 */
137 protected $validationResults = NULL;
138
139 /**
140 * Uid for the argument, if it has one
141 *
142 * @var string
143 */
144 protected $uid = NULL;
145
146 const ORIGIN_CLIENT = 0;
147 const ORIGIN_PERSISTENCE = 1;
148 const ORIGIN_PERSISTENCE_AND_MODIFIED = 2;
149 const ORIGIN_NEWLY_CREATED = 3;
150
151 /**
152 * The origin of the argument value. This is only meaningful after argument mapping.
153 *
154 * One of the ORIGIN_* constants above
155 *
156 * @var integer
157 */
158 protected $origin = 0;
159
160 /**
161 * @var \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface
162 * @inject
163 */
164 protected $persistenceManager;
165
166 /**
167 * Constructs this controller argument
168 *
169 * @param string $name Name of this argument
170 * @param string $dataType The data type of this argument
171 * @throws \InvalidArgumentException if $name is not a string or empty
172 * @api
173 */
174 public function __construct($name, $dataType) {
175 if (!is_string($name)) {
176 throw new \InvalidArgumentException('$name must be of type string, ' . gettype($name) . ' given.', 1187951688);
177 }
178 if (strlen($name) === 0) {
179 throw new \InvalidArgumentException('$name must be a non-empty string, ' . strlen($name) . ' characters given.', 1232551853);
180 }
181 $this->name = $name;
182 $this->dataType = $dataType;
183 }
184
185 /**
186 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
187 * @return void
188 */
189 public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService) {
190 $this->reflectionService = $reflectionService;
191 // Check for classnames (which have at least one underscore or backslash)
192 $this->dataTypeClassSchema = strpbrk($this->dataType, '_\\') !== FALSE ? $this->reflectionService->getClassSchema($this->dataType) : NULL;
193 }
194
195 /**
196 * @param \TYPO3\CMS\Extbase\Service\TypeHandlingService $typeHandlingService
197 * @return void
198 */
199 public function injectTypeHandlingService(\TYPO3\CMS\Extbase\Service\TypeHandlingService $typeHandlingService) {
200 $this->typeHandlingService = $typeHandlingService;
201 $this->dataType = $this->typeHandlingService->normalizeType($this->dataType);
202 }
203
204 /**
205 * Returns the name of this argument
206 *
207 * @return string This argument's name
208 * @api
209 */
210 public function getName() {
211 return $this->name;
212 }
213
214 /**
215 * Sets the short name of this argument.
216 *
217 * @param string $shortName A "short name" - a single character
218 * @throws \InvalidArgumentException if $shortName is not a character
219 * @return \TYPO3\CMS\Extbase\Mvc\Controller\Argument $this
220 * @api
221 */
222 public function setShortName($shortName) {
223 if ($shortName !== NULL && (!is_string($shortName) || strlen($shortName) !== 1)) {
224 throw new \InvalidArgumentException('$shortName must be a single character or NULL', 1195824959);
225 }
226 $this->shortName = $shortName;
227 return $this;
228 }
229
230 /**
231 * Returns the short name of this argument
232 *
233 * @return string This argument's short name
234 * @api
235 */
236 public function getShortName() {
237 return $this->shortName;
238 }
239
240 /**
241 * Sets the data type of this argument's value
242 *
243 * @param string $dataType The data type. Can be either a built-in type such as "Text" or "Integer" or a fully qualified object name
244 * @return \TYPO3\CMS\Extbase\Mvc\Controller\Argument $this
245 * @api
246 */
247 public function setDataType($dataType) {
248 $this->dataType = $dataType;
249 $this->dataTypeClassSchema = $this->reflectionService->getClassSchema($dataType);
250 return $this;
251 }
252
253 /**
254 * Returns the data type of this argument's value
255 *
256 * @return string The data type
257 * @api
258 */
259 public function getDataType() {
260 return $this->dataType;
261 }
262
263 /**
264 * Marks this argument to be required
265 *
266 * @param boolean $required TRUE if this argument should be required
267 * @return \TYPO3\CMS\Extbase\Mvc\Controller\Argument $this
268 * @api
269 */
270 public function setRequired($required) {
271 $this->isRequired = (boolean) $required;
272 return $this;
273 }
274
275 /**
276 * Returns TRUE if this argument is required
277 *
278 * @return boolean TRUE if this argument is required
279 * @api
280 */
281 public function isRequired() {
282 return $this->isRequired;
283 }
284
285 /**
286 * Sets the default value of the argument
287 *
288 * @param mixed $defaultValue Default value
289 * @return \TYPO3\CMS\Extbase\Mvc\Controller\Argument $this
290 * @api
291 */
292 public function setDefaultValue($defaultValue) {
293 $this->defaultValue = $defaultValue;
294 return $this;
295 }
296
297 /**
298 * Returns the default value of this argument
299 *
300 * @return mixed The default value
301 * @api
302 */
303 public function getDefaultValue() {
304 return $this->defaultValue;
305 }
306
307 /**
308 * Sets a custom validator which is used supplementary to the base validation
309 *
310 * @param \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface $validator The actual validator object
311 * @return \TYPO3\CMS\Extbase\Mvc\Controller\Argument Returns $this (used for fluent interface)
312 * @api
313 */
314 public function setValidator(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface $validator) {
315 $this->validator = $validator;
316 return $this;
317 }
318
319 /**
320 * Create and set a validator chain
321 *
322 * @param array $objectNames Object names of the validators
323 * @return \TYPO3\CMS\Extbase\Mvc\Controller\Argument Returns $this (used for fluent interface)
324 * @api
325 * @deprecated since Extbase 1.4.0, will be removed two versions after Extbase 6.1
326 */
327 public function setNewValidatorConjunction(array $objectNames) {
328 if ($this->validator === NULL) {
329 $this->validator = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Validation\\Validator\\ConjunctionValidator');
330 }
331 foreach ($objectNames as $objectName) {
332 if (!class_exists($objectName)) {
333 $objectName = 'Tx_Extbase_Validation_Validator_' . $objectName;
334 }
335 $this->validator->addValidator($this->objectManager->get($objectName));
336 }
337 return $this;
338 }
339
340 /**
341 * Returns the set validator
342 *
343 * @return \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface The set validator, NULL if none was set
344 * @api
345 */
346 public function getValidator() {
347 return $this->validator;
348 }
349
350 /**
351 * Get the origin of the argument value. This is only meaningful after argument mapping.
352 *
353 * @return integer one of the ORIGIN_* constants
354 * @deprecated since Extbase 1.4.0, will be removed two versions after Extbase 6.1
355 */
356 public function getOrigin() {
357 return $this->origin;
358 }
359
360 /**
361 * Sets the value of this argument.
362 *
363 * @param mixed $rawValue The value of this argument
364 * @return \TYPO3\CMS\Extbase\Mvc\Controller\Argument
365 * @throws \TYPO3\CMS\Extbase\Property\Exception
366 */
367 public function setValue($rawValue) {
368 if ($this->configurationManager->isFeatureEnabled('rewrittenPropertyMapper')) {
369 if ($rawValue === NULL) {
370 $this->value = NULL;
371 return $this;
372 }
373 if (is_object($rawValue) && $rawValue instanceof $this->dataType) {
374 $this->value = $rawValue;
375 return $this;
376 }
377 try {
378 $this->value = $this->propertyMapper->convert($rawValue, $this->dataType, $this->propertyMappingConfiguration);
379 } catch (TargetNotFoundException $e) {
380 // for optional arguments no exeption is thrown.
381 if ($this->isRequired()) {
382 throw $e;
383 }
384 }
385 $this->validationResults = $this->propertyMapper->getMessages();
386 if ($this->validator !== NULL) {
387 // TODO: Validation API has also changed!!!
388 $validationMessages = $this->validator->validate($this->value);
389 $this->validationResults->merge($validationMessages);
390 }
391 return $this;
392 } else {
393 if ($rawValue === NULL || is_object($rawValue) && $rawValue instanceof $this->dataType) {
394 $this->value = $rawValue;
395 } else {
396 $this->value = $this->transformValue($rawValue);
397 }
398 return $this;
399 }
400 }
401
402 /**
403 * Checks if the value is a UUID or an array but should be an object, i.e.
404 * the argument's data type class schema is set. If that is the case, this
405 * method tries to look up the corresponding object instead.
406 *
407 * Additionally, it maps arrays to objects in case it is a normal object.
408 *
409 * @param mixed $value The value of an argument
410 * @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentValueException
411 * @return mixed
412 * @deprecated since Extbase 1.4.0, will be removed two versions after Extbase 6.1
413 */
414 protected function transformValue($value) {
415 if (!class_exists($this->dataType)) {
416 return $value;
417 }
418 $transformedValue = NULL;
419 if ($this->dataTypeClassSchema !== NULL) {
420 // The target object is an Entity or ValueObject.
421 if (is_numeric($value)) {
422 $this->origin = self::ORIGIN_PERSISTENCE;
423 $transformedValue = $this->findObjectByUid($value);
424 } elseif (is_array($value)) {
425 $this->origin = self::ORIGIN_PERSISTENCE_AND_MODIFIED;
426 $transformedValue = $this->deprecatedPropertyMapper->map(array_keys($value), $value, $this->dataType);
427 }
428 } else {
429 if (!is_array($value)) {
430 throw new \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentValueException('The value was a simple type, so we could not map it to an object. Maybe the @entity or @valueobject annotations are missing?', 1251730701);
431 }
432 $this->origin = self::ORIGIN_NEWLY_CREATED;
433 $transformedValue = $this->deprecatedPropertyMapper->map(array_keys($value), $value, $this->dataType);
434 }
435 if (!$transformedValue instanceof $this->dataType && ($transformedValue !== NULL || $this->isRequired())) {
436 throw new \TYPO3\CMS\Extbase\Mvc\Exception\InvalidArgumentValueException('The value must be of type "' . $this->dataType . '", but was of type "' . (is_object($transformedValue) ? get_class($transformedValue) : gettype($transformedValue)) . '".' . ($this->deprecatedPropertyMapper->getMappingResults()->hasErrors() ? '<p>' . implode('<br />', $this->deprecatedPropertyMapper->getMappingResults()->getErrors()) . '</p>' : ''), 1251730702);
437 }
438 return $transformedValue;
439 }
440
441 /**
442 * Finds an object from the repository by searching for its technical UID.
443 *
444 * @param integer $uid The object's uid
445 * @return object Either the object matching the uid or, if none or more than one object was found, NULL
446 */
447 protected function findObjectByUid($uid) {
448 $query = $this->queryFactory->create($this->dataType);
449 $query->getQuerySettings()->setRespectSysLanguage(FALSE);
450 $query->getQuerySettings()->setRespectStoragePage(FALSE);
451 return $query->matching($query->equals('uid', $uid))->execute()->getFirst();
452 }
453
454 /**
455 * Returns the value of this argument
456 *
457 * @return object The value of this argument - if none was set, NULL is returned
458 * @api
459 */
460 public function getValue() {
461 if ($this->value === NULL) {
462 return $this->defaultValue;
463 } else {
464 return $this->value;
465 }
466 }
467
468 /**
469 * Checks if this argument has a value set.
470 *
471 * @return boolean TRUE if a value was set, otherwise FALSE
472 * @deprecated since Extbase 1.4.0, will be removed two versions after Extbase 6.1
473 */
474 public function isValue() {
475 return $this->value !== NULL;
476 }
477
478 /**
479 * Return the Property Mapping Configuration used for this argument; can be used by the initialize*action to modify the Property Mapping.
480 *
481 * @return \TYPO3\CMS\Extbase\Mvc\Controller\MvcPropertyMappingConfiguration
482 * @api
483 */
484 public function getPropertyMappingConfiguration() {
485 return $this->propertyMappingConfiguration;
486 }
487
488 /**
489 * @return boolean TRUE if the argument is valid, FALSE otherwise
490 * @api
491 */
492 public function isValid() {
493 return !$this->validationResults->hasErrors();
494 }
495
496 /**
497 * @return array<\TYPO3\CMS\Extbase\Error\Result> Validation errors which have occurred.
498 * @api
499 */
500 public function getValidationResults() {
501 return $this->validationResults;
502 }
503
504 /**
505 * Returns a string representation of this argument's value
506 *
507 * @return string
508 * @api
509 */
510 public function __toString() {
511 return (string) $this->value;
512 }
513 }