TYPO3 API  SVNRelease
Mapper.php
Go to the documentation of this file.
00001 <?php
00002 /***************************************************************
00003 *  Copyright notice
00004 *
00005 *  (c) 2009 Jochen Rau <jochen.rau@typoplanet.de>
00006 *  All rights reserved
00007 *
00008 *  This class is a backport of the corresponding class of FLOW3.
00009 *  All credits go to the v5 team.
00010 *
00011 *  This script is part of the TYPO3 project. The TYPO3 project is
00012 *  free software; you can redistribute it and/or modify
00013 *  it under the terms of the GNU General Public License as published by
00014 *  the Free Software Foundation; either version 2 of the License, or
00015 *  (at your option) any later version.
00016 *
00017 *  The GNU General Public License can be found at
00018 *  http://www.gnu.org/copyleft/gpl.html.
00019 *
00020 *  This script is distributed in the hope that it will be useful,
00021 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00022 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00023 *  GNU General Public License for more details.
00024 *
00025 *  This copyright notice MUST APPEAR in all copies of the script!
00026 ***************************************************************/
00027 
00028 /**
00029  * The Property Mapper maps properties from a source onto a given target object, often a
00030  * (domain-) model. Which properties are required and how they should be filtered can
00031  * be customized.
00032  *
00033  * During the mapping process, the property values are validated and the result of this
00034  * validation can be queried.
00035  *
00036  * The following code would map the property of the source array to the target:
00037  *
00038  * $target = new ArrayObject();
00039  * $source = new ArrayObject(
00040  *    array(
00041  *       'someProperty' => 'SomeValue'
00042  *    )
00043  * );
00044  * $mapper->mapAndValidate(array('someProperty'), $source, $target);
00045  *
00046  * Now the target object equals the source object.
00047  *
00048  * @package Extbase
00049  * @subpackage Property
00050  * @version $Id: Mapper.php 2259 2010-04-29 07:53:46Z jocrau $
00051  * @api
00052  */
00053 class Tx_Extbase_Property_Mapper implements t3lib_Singleton {
00054 
00055     /**
00056      * Results of the last mapping operation
00057      * @var Tx_Extbase_Property_MappingResults
00058      */
00059     protected $mappingResults;
00060 
00061     /**
00062      * @var Tx_Extbase_Validation_ValidatorResolver
00063      */
00064     protected $validatorResolver;
00065 
00066     /**
00067      * @var Tx_Extbase_Reflection_Service
00068      */
00069     protected $reflectionService;
00070 
00071     /**
00072      * @var Tx_Extbase_Persistence_ManagerInterface
00073      */
00074     protected $persistenceManager;
00075 
00076     /**
00077      * @var Tx_Extbase_Persistence_QueryFactory
00078      */
00079     protected $queryFactory;
00080 
00081     /**
00082      * @param Tx_Extbase_Validation_ValidatorResolver $validatorResolver
00083      * @return void
00084      */
00085     public function injectValidatorResolver(Tx_Extbase_Validation_ValidatorResolver $validatorResolver) {
00086         $this->validatorResolver = $validatorResolver;
00087     }
00088 
00089     /**
00090      *
00091      * @param Tx_Extbase_Persistence_QueryFactory $queryFactory
00092      * @return void
00093      */
00094     public function injectQueryFactory(Tx_Extbase_Persistence_QueryFactory $queryFactory) {
00095         $this->queryFactory = $queryFactory;
00096     }
00097 
00098     /**
00099      * @param Tx_Extbase_Persistence_Manager $persistenceManager
00100      * @return void
00101      */
00102     public function injectPersistenceManager(Tx_Extbase_Persistence_Manager $persistenceManager) {
00103         $this->persistenceManager = $persistenceManager;
00104     }
00105 
00106     /**
00107      * Injects the Reflection Service
00108      *
00109      * @param Tx_Extbase_Reflection_Service
00110      * @return void
00111      */
00112     public function injectReflectionService(Tx_Extbase_Reflection_Service $reflectionService) {
00113         $this->reflectionService = $reflectionService;
00114     }
00115 
00116     /**
00117      * Maps the given properties to the target object and validates the properties according to the defined
00118      * validators. If the result object is not valid, the operation will be undone (the target object remains
00119      * unchanged) and this method returns FALSE.
00120      *
00121      * If in doubt, always prefer this method over the map() method because skipping validation can easily become
00122      * a security issue.
00123      *
00124      * @param array $propertyNames Names of the properties to map.
00125      * @param mixed $source Source containing the properties to map to the target object. Must either be an array, ArrayObject or any other object.
00126      * @param object $target The target object
00127      * @param Tx_Extbase_Validation_Validator_ObjectValidatorInterface $targetObjectValidator A validator used for validating the target object
00128      * @param array $optionalPropertyNames Names of optional properties. If a property is specified here and it doesn't exist in the source, no error is issued.
00129      * @return boolean TRUE if the mapped properties are valid, otherwise FALSE
00130      * @see getMappingResults()
00131      * @see map()
00132      * @api
00133      */
00134     public function mapAndValidate(array $propertyNames, $source, $target, $optionalPropertyNames = array(), Tx_Extbase_Validation_Validator_ObjectValidatorInterface $targetObjectValidator) {
00135         $backupProperties = array();
00136 
00137         $this->map($propertyNames, $source, $backupProperties, $optionalPropertyNames);
00138         if ($this->mappingResults->hasErrors()) return FALSE;
00139 
00140         $this->map($propertyNames, $source, $target, $optionalPropertyNames);
00141         if ($this->mappingResults->hasErrors()) return FALSE;
00142 
00143         if ($targetObjectValidator->isValid($target) !== TRUE) {
00144             $this->addErrorsFromObjectValidator($targetObjectValidator->getErrors());
00145             $backupMappingResult = $this->mappingResults;
00146             $this->map($propertyNames, $backupProperties, $source, $optionalPropertyNames);
00147             $this->mappingResults = $backupMappingResult;
00148         }
00149         return (!$this->mappingResults->hasErrors());
00150     }
00151 
00152     /**
00153      * Add errors to the mapping result from an object validator (property errors).
00154      *
00155      * @param array Array of Tx_Extbase_Validation_PropertyError
00156      * @return void
00157      */
00158     protected function addErrorsFromObjectValidator($errors) {
00159         foreach ($errors as $error) {
00160             if ($error instanceof Tx_Extbase_Validation_PropertyError) {
00161                 $propertyName = $error->getPropertyName();
00162                 $this->mappingResults->addError($error, $propertyName);
00163             }
00164         }
00165     }
00166 
00167     /**
00168      * Maps the given properties to the target object WITHOUT VALIDATING THE RESULT.
00169      * If the properties could be set, this method returns TRUE, otherwise FALSE.
00170      * Returning TRUE does not mean that the target object is valid and secure!
00171      *
00172      * Only use this method if you're sure that you don't need validation!
00173      *
00174      * @param array $propertyNames Names of the properties to map.
00175      * @param mixed $source Source containing the properties to map to the target object. Must either be an array, ArrayObject or any other object.
00176      * @param object $target The target object
00177      * @param array $optionalPropertyNames Names of optional properties. If a property is specified here and it doesn't exist in the source, no error is issued.
00178      * @return boolean TRUE if the properties could be mapped, otherwise FALSE
00179      * @see mapAndValidate()
00180      * @api
00181      */
00182     public function map(array $propertyNames, $source, $target, $optionalPropertyNames = array()) {
00183         if (!is_object($source) && !is_array($source)) throw new Tx_Extbase_Property_Exception_InvalidSource('The source object must be a valid object or array, ' . gettype($target) . ' given.', 1187807099);
00184 
00185         if (is_string($target) && strpos($target, '_') !== FALSE) {
00186             return $this->transformToObject($source, $target, '--none--');
00187         }
00188 
00189         if (!is_object($target) && !is_array($target)) throw new Tx_Extbase_Property_Exception_InvalidTarget('The target object must be a valid object or array, ' . gettype($target) . ' given.', 1187807099);
00190 
00191         $this->mappingResults = new Tx_Extbase_Property_MappingResults();
00192         if (is_object($target)) {
00193             $targetClassSchema = $this->reflectionService->getClassSchema(get_class($target));
00194         } else {
00195             $targetClassSchema = NULL;
00196         }
00197 
00198         foreach ($propertyNames as $propertyName) {
00199             $propertyValue = NULL;
00200             if (is_array($source) || $source instanceof ArrayAccess) {
00201                 if (isset($source[$propertyName])) {
00202                     $propertyValue = $source[$propertyName];
00203                 }
00204             } else {
00205                 $propertyValue = Tx_Extbase_Reflection_ObjectAccess::getProperty($source, $propertyName);
00206             }
00207 
00208             if ($propertyValue === NULL && !in_array($propertyName, $optionalPropertyNames)) {
00209                 $this->mappingResults->addError(new Tx_Extbase_Error_Error("Required property '$propertyName' does not exist." , 1236785359), $propertyName);
00210             } else {
00211                 if ($targetClassSchema !== NULL && $targetClassSchema->hasProperty($propertyName)) {
00212                     $propertyMetaData = $targetClassSchema->getProperty($propertyName);
00213 
00214                     if (in_array($propertyMetaData['type'], array('array', 'ArrayObject', 'Tx_Extbase_Persistence_ObjectStorage')) && (strpos($propertyMetaData['elementType'], '_') !== FALSE || $propertyValue === '')) {
00215                         $objects = array();
00216                         if (is_array($propertyValue)) {
00217                             foreach ($propertyValue as $value) {
00218                                 $objects[] = $this->transformToObject($value, $propertyMetaData['elementType'], $propertyName);
00219                             }
00220                         }
00221 
00222                             // make sure we hand out what is expected
00223                         if ($propertyMetaData['type'] === 'ArrayObject') {
00224                             $propertyValue = new ArrayObject($objects);
00225                         } elseif ($propertyMetaData['type'] === 'Tx_Extbase_Persistence_ObjectStorage') {
00226                             $propertyValue = new Tx_Extbase_Persistence_ObjectStorage();
00227                             foreach ($objects as $object) {
00228                                 $propertyValue->attach($object);
00229                             }
00230                         } else {
00231                             $propertyValue = $objects;
00232                         }
00233                     } elseif ($propertyMetaData['type'] === 'DateTime' || strpos($propertyMetaData['type'], '_') !== FALSE) {
00234                         $propertyValue = $this->transformToObject($propertyValue, $propertyMetaData['type'], $propertyName);
00235                         if ($propertyValue === NULL) {
00236                             continue;
00237                         }
00238                     }
00239                 } elseif ($targetClassSchema !== NULL) {
00240                     $this->mappingResults->addError(new Tx_Extbase_Error_Error("Property '$propertyName' does not exist in target class schema." , 1251813614), $propertyName);
00241                 }
00242 
00243                 if (is_array($target)) {
00244                     $target[$propertyName] = $propertyValue;
00245                 } elseif (Tx_Extbase_Reflection_ObjectAccess::setProperty($target, $propertyName, $propertyValue) === FALSE) {
00246                     $this->mappingResults->addError(new Tx_Extbase_Error_Error("Property '$propertyName' could not be set." , 1236783102), $propertyName);
00247                 }
00248             }
00249         }
00250 
00251         return !$this->mappingResults->hasErrors();
00252     }
00253 
00254     /**
00255      * Transforms strings with UUIDs or arrays with UUIDs/identity properties
00256      * into the requested type, if possible.
00257      *
00258      * @param mixed $propertyValue The value to transform, string or array
00259      * @param string $targetType The type to transform to
00260      * @param string $propertyName In case of an error we add this to the error message
00261      * @return object The object, when no transformation was possible this may return NULL as well
00262      */
00263     protected function transformToObject($propertyValue, $targetType, $propertyName) {
00264         if ($targetType === 'DateTime' || is_subclass_of($targetType, 'DateTime')) {
00265             // TODO replace this with converter implementation of FLOW3
00266             if ($propertyValue === '') {
00267                 $propertyValue = NULL;
00268             } else {
00269                 try {
00270                     $propertyValue = new $targetType($propertyValue);
00271                 } catch (Exception $e) {
00272                     $propertyValue = NULL;
00273                 }
00274             }
00275         } else {
00276             if (is_numeric($propertyValue)) {
00277                 $propertyValue = $this->findObjectByUid($targetType, $propertyValue);
00278                 if ($propertyValue === FALSE) {
00279                     $this->mappingResults->addError(new Tx_Extbase_Error_Error('Querying the repository for the specified object with UUID ' . $propertyValue . ' was not successful.' , 1249379517), $propertyName);
00280                 }
00281             } elseif (is_array($propertyValue)) {
00282                 if (isset($propertyValue['__identity'])) {
00283                     $existingObject = $this->findObjectByUid($targetType, $propertyValue['__identity']);
00284                     if ($existingObject === FALSE) throw new Tx_Extbase_Property_Exception_TargetNotFound('Querying the repository for the specified object was not successful.', 1237305720);
00285                     unset($propertyValue['__identity']);
00286                     if (count($propertyValue) === 0) {
00287                         $propertyValue = $existingObject;
00288                     } elseif ($existingObject !== NULL) {
00289                         $newObject = clone $existingObject;
00290                         if ($this->map(array_keys($propertyValue), $propertyValue, $newObject)) {
00291                             $propertyValue = $newObject;
00292                         } else {
00293                             $propertyValue = NULL;
00294                         }
00295                     }
00296                 } else {
00297                     $newObject = new $targetType;
00298                     if ($this->map(array_keys($propertyValue), $propertyValue, $newObject)) {
00299                         $propertyValue = $newObject;
00300                     } else {
00301                         $propertyValue = NULL;
00302                     }
00303                 }
00304             } else {
00305                 throw new InvalidArgumentException('transformToObject() accepts only numeric values and arrays.', 1251814355);
00306             }
00307         }
00308 
00309         return $propertyValue;
00310     }
00311 
00312     /**
00313      * Returns the results of the last mapping operation.
00314      *
00315      * @return Tx_Extbase_Property_MappingResults The mapping results (or NULL if no mapping has been carried out yet)
00316      * @api
00317      */
00318     public function getMappingResults() {
00319         return $this->mappingResults;
00320     }
00321 
00322     /**
00323      * Finds an object from the repository by searching for its technical UID.
00324      *
00325      * @param string $dataType the data type to fetch
00326      * @param int $uid The object's uid
00327      * @return object Either the object matching the uid or, if none or more than one object was found, NULL
00328      */
00329     // TODO This is duplicated code; see Argument class
00330     protected function findObjectByUid($dataType, $uid) {
00331         $query = $this->queryFactory->create($dataType);
00332         $query->getQuerySettings()->setRespectSysLanguage(FALSE);
00333         $query->getQuerySettings()->setRespectStoragePage(FALSE);
00334         return $query->matching(
00335             $query->equals('uid', intval($uid)))
00336             ->execute()
00337             ->getFirst();
00338     }
00339 }
00340 
00341 ?>