/** * @param Mapping $mapping * @param string $className * @param string $propertyName * * @throws \Flowpack\ElasticSearch\Exception * @return void */ protected function augmentMappingByProperty(Mapping $mapping, $className, $propertyName) { list($propertyType) = $this->reflectionService->getPropertyTagValues($className, $propertyName, 'var'); if (($transformAnnotation = $this->reflectionService->getPropertyAnnotation($className, $propertyName, 'Flowpack\\ElasticSearch\\Annotations\\Transform')) !== NULL) { $mappingType = $this->transformerFactory->create($transformAnnotation->type)->getTargetMappingType(); } elseif (\TYPO3\Flow\Utility\TypeHandling::isSimpleType($propertyType)) { $mappingType = $propertyType; } elseif ($propertyType === '\\DateTime') { $mappingType = 'date'; } else { throw new \Flowpack\ElasticSearch\Exception('Mapping is only supported for simple types and DateTime objects; "' . $propertyType . '" given but without a Transform directive.'); } $mapping->setPropertyByPath($propertyName, array('type' => $mappingType)); $annotation = $this->reflectionService->getPropertyAnnotation($className, $propertyName, 'Flowpack\\ElasticSearch\\Annotations\\Mapping'); if ($annotation instanceof MappingAnnotation) { $mapping->setPropertyByPath($propertyName, $this->processMappingAnnotation($annotation, $mapping->getPropertyByPath($propertyName))); if ($annotation->getFields()) { foreach ($annotation->getFields() as $multiFieldAnnotation) { $multiFieldIndexName = trim($multiFieldAnnotation->index_name); if ($multiFieldIndexName === '') { throw new \Flowpack\ElasticSearch\Exception('Multi field require an unique index name "' . $className . '::' . $propertyName . '".'); } if (isset($multiFields[$multiFieldIndexName])) { throw new \Flowpack\ElasticSearch\Exception('Duplicate index name in the same multi field is not allowed "' . $className . '::' . $propertyName . '".'); } $multiFieldAnnotation->type = $mappingType; $multiFields[$multiFieldIndexName] = $this->processMappingAnnotation($multiFieldAnnotation); } $mapping->setPropertyByPath(array($propertyName, 'fields'), $multiFields); } } }
/** * Convert a value to the internal object data format * * @param string $identifier The object's identifier * @param object $object The object with the property to flatten * @param string $propertyName The name of the property * @param array $propertyMetaData The property metadata * @param array $propertyData Reference to the property data array * @return void * @api */ protected function flattenValue($identifier, $object, $propertyName, array $propertyMetaData, array &$propertyData) { $propertyValue = \TYPO3\Flow\Reflection\ObjectAccess::getProperty($object, $propertyName, true); if ($propertyValue instanceof \TYPO3\Flow\Persistence\Aspect\PersistenceMagicInterface) { $propertyData[$propertyName] = array('type' => get_class($propertyValue), 'multivalue' => false, 'value' => $this->processObject($propertyValue, $identifier)); } else { switch ($propertyMetaData['type']) { case 'DateTime': $propertyData[$propertyName] = array('multivalue' => false, 'value' => $this->processDateTime($propertyValue)); break; case 'Doctrine\\Common\\Collections\\Collection': case 'Doctrine\\Common\\Collections\\ArrayCollection': $propertyValue = $propertyValue === null ? array() : $propertyValue->toArray(); case 'array': $propertyData[$propertyName] = array('multivalue' => true, 'value' => $this->processArray($propertyValue, $identifier, $this->persistenceSession->getCleanStateOfProperty($object, $propertyName))); break; case 'SplObjectStorage': $propertyData[$propertyName] = array('multivalue' => true, 'value' => $this->processSplObjectStorage($propertyValue, $identifier, $this->persistenceSession->getCleanStateOfProperty($object, $propertyName))); break; default: if ($propertyValue === null && !\TYPO3\Flow\Utility\TypeHandling::isSimpleType($propertyMetaData['type'])) { $this->removeDeletedReference($object, $propertyName, $propertyMetaData); } $propertyData[$propertyName] = array('multivalue' => false, 'value' => $propertyValue); break; } $propertyData[$propertyName]['type'] = $propertyMetaData['type']; } }
/** * Expand shortened class names in "var" and "param" annotations, taking use statements into account. * * @param ClassReflection $class * @param string $type the type inside var/param annotation * @return string the possibly expanded type */ protected function expandType(ClassReflection $class, $type) { // expand "SomeType<SomeElementType>" to "\SomeTypeNamespace\SomeType<\ElementTypeNamespace\ElementType>" if (strpos($type, '<') !== false) { $typeParts = explode('<', $type); $type = $typeParts[0]; $elementType = rtrim($typeParts[1], '>'); return $this->expandType($class, $type) . '<' . $this->expandType($class, $elementType) . '>'; } // skip simple types and types with fully qualified namespaces if ($type === 'mixed' || $type[0] === '\\' || TypeHandling::isSimpleType($type)) { return TypeHandling::normalizeType($type); } // we try to find the class relative to the current namespace... $possibleFullyQualifiedClassName = sprintf('%s\\%s', $class->getNamespaceName(), $type); if (class_exists($possibleFullyQualifiedClassName) || interface_exists($possibleFullyQualifiedClassName)) { return $possibleFullyQualifiedClassName; } // and then we try to find "use" statements for the class. $className = $class->getName(); if (!isset($this->useStatementsForClassCache[$className])) { $this->useStatementsForClassCache[$className] = $this->getDoctrinePhpParser()->parseClass($class); } $useStatementsForClass = $this->useStatementsForClassCache[$className]; // ... and try to expand them $typeParts = explode('\\', $type, 2); $lowercasedFirstTypePart = strtolower($typeParts[0]); if (isset($useStatementsForClass[$lowercasedFirstTypePart])) { $typeParts[0] = $useStatementsForClass[$lowercasedFirstTypePart]; return implode('\\', $typeParts); } return $type; }
/** * Determine the type converter to be used. If no converter has been found, an exception is raised. * * @param mixed $source * @param string $targetType * @param \TYPO3\Flow\Property\PropertyMappingConfigurationInterface $configuration * @return \TYPO3\Flow\Property\TypeConverterInterface Type Converter which should be used to convert between $source and $targetType. * @throws \TYPO3\Flow\Property\Exception\TypeConverterException * @throws \TYPO3\Flow\Property\Exception\InvalidTargetException */ protected function findTypeConverter($source, $targetType, \TYPO3\Flow\Property\PropertyMappingConfigurationInterface $configuration) { if ($configuration->getTypeConverter() !== null) { return $configuration->getTypeConverter(); } if (!is_string($targetType)) { throw new \TYPO3\Flow\Property\Exception\InvalidTargetException('The target type was no string, but of type "' . gettype($targetType) . '"', 1297941727); } $normalizedTargetType = TypeHandling::normalizeType($targetType); $truncatedTargetType = TypeHandling::truncateElementType($normalizedTargetType); $converter = null; $sourceTypes = $this->determineSourceTypes($source); foreach ($sourceTypes as $sourceType) { if (TypeHandling::isSimpleType($truncatedTargetType)) { if (isset($this->typeConverters[$sourceType][$truncatedTargetType])) { $converter = $this->findEligibleConverterWithHighestPriority($this->typeConverters[$sourceType][$truncatedTargetType], $source, $normalizedTargetType); } } else { $converter = $this->findFirstEligibleTypeConverterInObjectHierarchy($source, $sourceType, $normalizedTargetType); } if ($converter !== null) { return $converter; } } throw new \TYPO3\Flow\Property\Exception\TypeConverterException('No converter found which can be used to convert from "' . implode('" or "', $sourceTypes) . '" to "' . $normalizedTargetType . '".'); }
/** * Builds a base validator conjunction for the given data type. * * The base validation rules are those which were declared directly in a class (typically * a model) through some validate annotations on properties. * * If a property holds a class for which a base validator exists, that property will be * checked as well, regardless of a validate annotation * * Additionally, if a custom validator was defined for the class in question, it will be added * to the end of the conjunction. A custom validator is found if it follows the naming convention * "Replace '\Model\' by '\Validator\' and append 'Validator'". * * Example: $targetClassName is TYPO3\Foo\Domain\Model\Quux, then the validator will be found if it has the * name TYPO3\Foo\Domain\Validator\QuuxValidator * * @param string $indexKey The key to use as index in $this->baseValidatorConjunctions; calculated from target class name and validation groups * @param string $targetClassName The data type to build the validation conjunction for. Needs to be the fully qualified class name. * @param array $validationGroups The validation groups to build the validator for * @return void * @throws \TYPO3\Flow\Validation\Exception\NoSuchValidatorException * @throws \InvalidArgumentException */ protected function buildBaseValidatorConjunction($indexKey, $targetClassName, array $validationGroups) { $conjunctionValidator = new ConjunctionValidator(); $this->baseValidatorConjunctions[$indexKey] = $conjunctionValidator; if (!TypeHandling::isSimpleType($targetClassName) && class_exists($targetClassName)) { // Model based validator $objectValidator = new GenericObjectValidator(array()); foreach ($this->reflectionService->getClassPropertyNames($targetClassName) as $classPropertyName) { $classPropertyTagsValues = $this->reflectionService->getPropertyTagsValues($targetClassName, $classPropertyName); if (!isset($classPropertyTagsValues['var'])) { throw new \InvalidArgumentException(sprintf('There is no @var annotation for property "%s" in class "%s".', $classPropertyName, $targetClassName), 1363778104); } try { $parsedType = TypeHandling::parseType(trim(implode('', $classPropertyTagsValues['var']), ' \\')); } catch (\TYPO3\Flow\Utility\Exception\InvalidTypeException $exception) { throw new \InvalidArgumentException(sprintf(' @var annotation of ' . $exception->getMessage(), 'class "' . $targetClassName . '", property "' . $classPropertyName . '"'), 1315564744, $exception); } if ($this->reflectionService->isPropertyAnnotatedWith($targetClassName, $classPropertyName, \TYPO3\Flow\Annotations\IgnoreValidation::class)) { continue; } $propertyTargetClassName = $parsedType['type']; if (TypeHandling::isCollectionType($propertyTargetClassName) === TRUE) { $collectionValidator = $this->createValidator(\TYPO3\Flow\Validation\Validator\CollectionValidator::class, array('elementType' => $parsedType['elementType'], 'validationGroups' => $validationGroups)); $objectValidator->addPropertyValidator($classPropertyName, $collectionValidator); } elseif (!TypeHandling::isSimpleType($propertyTargetClassName) && $this->objectManager->isRegistered($propertyTargetClassName) && $this->objectManager->getScope($propertyTargetClassName) === \TYPO3\Flow\Object\Configuration\Configuration::SCOPE_PROTOTYPE) { $validatorForProperty = $this->getBaseValidatorConjunction($propertyTargetClassName, $validationGroups); if (count($validatorForProperty) > 0) { $objectValidator->addPropertyValidator($classPropertyName, $validatorForProperty); } } $validateAnnotations = $this->reflectionService->getPropertyAnnotations($targetClassName, $classPropertyName, \TYPO3\Flow\Annotations\Validate::class); foreach ($validateAnnotations as $validateAnnotation) { if (count(array_intersect($validateAnnotation->validationGroups, $validationGroups)) === 0) { // In this case, the validation groups for the property do not match current validation context continue; } $newValidator = $this->createValidator($validateAnnotation->type, $validateAnnotation->options); if ($newValidator === NULL) { throw new Exception\NoSuchValidatorException('Invalid validate annotation in ' . $targetClassName . '::' . $classPropertyName . ': Could not resolve class name for validator "' . $validateAnnotation->type . '".', 1241098027); } $objectValidator->addPropertyValidator($classPropertyName, $newValidator); } } if (count($objectValidator->getPropertyValidators()) > 0) { $conjunctionValidator->addValidator($objectValidator); } } $this->addCustomValidators($targetClassName, $conjunctionValidator); }
/** * Convert a value to the internal object data format * * @param string $identifier The object's identifier * @param object $object The object with the property to flatten * @param string $propertyName The name of the property * @param array $propertyMetaData The property metadata * @param array $propertyData Reference to the property data array * @return void * @api */ protected function flattenValue($identifier, $object, $propertyName, array $propertyMetaData, array &$propertyData) { $propertyValue = ObjectAccess::getProperty($object, $propertyName, true); if ($propertyValue instanceof PersistenceMagicInterface) { $propertyData[$propertyName] = ['type' => get_class($propertyValue), 'multivalue' => false, 'value' => $this->processObject($propertyValue, $identifier)]; } else { switch ($propertyMetaData['type']) { case 'DateTime': $propertyData[$propertyName] = ['multivalue' => false, 'value' => $this->processDateTime($propertyValue)]; break; case Collection::class: case ArrayCollection::class: $propertyValue = $propertyValue === null ? [] : $propertyValue->toArray(); case 'array': $propertyData[$propertyName] = ['multivalue' => true, 'value' => $this->processArray($propertyValue, $identifier, $this->persistenceSession->getCleanStateOfProperty($object, $propertyName))]; break; case 'SplObjectStorage': $propertyData[$propertyName] = ['multivalue' => true, 'value' => $this->processSplObjectStorage($propertyValue, $identifier, $this->persistenceSession->getCleanStateOfProperty($object, $propertyName))]; break; default: if ($propertyValue === null && !TypeHandling::isSimpleType($propertyMetaData['type'])) { $this->removeDeletedReference($object, $propertyName, $propertyMetaData); } $propertyData[$propertyName] = ['multivalue' => false, 'value' => $propertyValue]; break; } $propertyData[$propertyName]['type'] = $propertyMetaData['type']; } }
/** * @param mixed $propertyValue * @param string $dataType * @return mixed * @throws PropertyException */ protected function convertValue($propertyValue, $dataType) { $rawType = TypeHandling::truncateElementType($dataType); // This hardcoded handling is to circumvent rewriting PropertyMappers that convert objects. Usually they expect the source to be an object already and break if not. if (!TypeHandling::isSimpleType($rawType) && !is_object($propertyValue) && !is_array($propertyValue)) { return null; } if ($rawType === 'array') { $conversionTargetType = 'array<string>'; } elseif (TypeHandling::isSimpleType($rawType)) { $conversionTargetType = TypeHandling::normalizeType($rawType); } else { $conversionTargetType = 'array'; } $propertyMappingConfiguration = $this->createConfiguration($dataType); $convertedValue = $this->propertyMapper->convert($propertyValue, $conversionTargetType, $propertyMappingConfiguration); if ($convertedValue instanceof \TYPO3\Flow\Error\Error) { throw new PropertyException($convertedValue->getMessage(), $convertedValue->getCode()); } return $convertedValue; }