From ea1aea7d4853c008e3f32f48241391417158a269 Mon Sep 17 00:00:00 2001 From: Michael Babker Date: Mon, 19 Feb 2024 18:37:25 -0500 Subject: [PATCH] Flip the inheritance chain between annotation and attribute drivers for the extensions --- src/Blameable/Mapping/Driver/Annotation.php | 79 +---- src/Blameable/Mapping/Driver/Attribute.php | 88 ++++- src/IpTraceable/Mapping/Driver/Annotation.php | 70 +--- src/IpTraceable/Mapping/Driver/Attribute.php | 79 ++++- src/Loggable/Mapping/Driver/Annotation.php | 129 +------- src/Loggable/Mapping/Driver/Attribute.php | 140 +++++++- .../Driver/AbstractAnnotationDriver.php | 19 +- src/Mapping/ExtensionMetadataFactory.php | 4 +- .../Mapping/Driver/Annotation.php | 47 +-- .../Mapping/Driver/Attribute.php | 56 +++- src/References/Mapping/Driver/Annotation.php | 94 +----- src/References/Mapping/Driver/Attribute.php | 71 ++++- src/Sluggable/Mapping/Driver/Annotation.php | 161 +--------- src/Sluggable/Mapping/Driver/Attribute.php | 172 +++++++++- .../Mapping/Driver/Annotation.php | 52 +-- .../Mapping/Driver/Attribute.php | 58 +++- src/Sortable/Mapping/Driver/Annotation.php | 82 +---- src/Sortable/Mapping/Driver/Attribute.php | 85 ++++- .../Mapping/Driver/Annotation.php | 79 +---- .../Mapping/Driver/Attribute.php | 89 +++++- .../Mapping/Driver/Annotation.php | 113 +------ src/Translatable/Mapping/Driver/Attribute.php | 132 +++++++- src/Tree/Mapping/Driver/Annotation.php | 256 +-------------- src/Tree/Mapping/Driver/Attribute.php | 300 +++++++++++++++++- src/Uploadable/Mapping/Driver/Annotation.php | 96 +----- src/Uploadable/Mapping/Driver/Attribute.php | 118 ++++++- 26 files changed, 1382 insertions(+), 1287 deletions(-) diff --git a/src/Blameable/Mapping/Driver/Annotation.php b/src/Blameable/Mapping/Driver/Annotation.php index ca267db69f..7dae3a81ca 100644 --- a/src/Blameable/Mapping/Driver/Annotation.php +++ b/src/Blameable/Mapping/Driver/Annotation.php @@ -9,88 +9,15 @@ namespace Gedmo\Blameable\Mapping\Driver; -use Gedmo\Exception\InvalidMappingException; -use Gedmo\Mapping\Annotation\Blameable; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; /** - * This is an annotation mapping driver for Blameable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Blameable - * extension. - * - * @author David Buchmann + * Mapping driver for the blamable extension which reads extended metadata from annotations on a blamable class. * * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. * * @internal */ -class Annotation extends AbstractAnnotationDriver +class Annotation extends Attribute implements AnnotationDriverInterface { - /** - * Annotation field is blameable - */ - public const BLAMEABLE = Blameable::class; - - /** - * List of types which are valid for blame - * - * @var string[] - */ - protected $validTypes = [ - 'one', - 'string', - 'int', - ]; - - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - // property annotations - foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() - || $meta->isInheritedField($property->name) - || isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - if ($blameable = $this->reader->getPropertyAnnotation($property, self::BLAMEABLE)) { - $field = $property->getName(); - - if (!$meta->hasField($field) && !$meta->hasAssociation($field)) { - throw new InvalidMappingException("Unable to find blameable [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if ($meta->hasField($field)) { - if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a one-to-many relation in class - {$meta->getName()}"); - } - } else { - // association - if (!$meta->isSingleValuedAssociation($field)) { - throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->getName()}"); - } - } - if (!in_array($blameable->on, ['update', 'create', 'change'], true)) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); - } - if ('change' === $blameable->on) { - if (!isset($blameable->field)) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); - } - if (is_array($blameable->field) && isset($blameable->value)) { - throw new InvalidMappingException('Blameable extension does not support multiple value changeset detection yet.'); - } - $field = [ - 'field' => $field, - 'trackedField' => $blameable->field, - 'value' => $blameable->value, - ]; - } - // properties are unique and mapper checks that, no risk here - $config[$blameable->on][] = $field; - } - } - - return $config; - } } diff --git a/src/Blameable/Mapping/Driver/Attribute.php b/src/Blameable/Mapping/Driver/Attribute.php index 18b4ce3712..ccffcf5fff 100644 --- a/src/Blameable/Mapping/Driver/Attribute.php +++ b/src/Blameable/Mapping/Driver/Attribute.php @@ -9,16 +9,92 @@ namespace Gedmo\Blameable\Mapping\Driver; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\Blameable; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an attribute mapping driver for Blameable - * behavioral extension. Used for extraction of extended - * metadata from attribute specifically for Blameable - * extension. + * Mapping driver for the blameable extension which reads extended metadata from attributes on a blameable class. + * + * @author David Buchmann * * @internal */ -final class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { + /** + * Mapping object for the blameable extension. + */ + public const BLAMEABLE = Blameable::class; + + /** + * List of types which are valid for blame + * + * @var string[] + */ + protected $validTypes = [ + 'one', + 'string', + 'int', + ]; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // property annotations + foreach ($class->getProperties() as $property) { + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) + ) { + continue; + } + + if ($blameable = $this->reader->getPropertyAnnotation($property, self::BLAMEABLE)) { + \assert($blameable instanceof Blameable); + + $field = $property->getName(); + + if (!$meta->hasField($field) && !$meta->hasAssociation($field)) { + throw new InvalidMappingException("Unable to find blameable [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if ($meta->hasField($field)) { + if (!$this->isValidField($meta, $field)) { + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a one-to-many relation in class - {$meta->getName()}"); + } + } else { + // association + if (!$meta->isSingleValuedAssociation($field)) { + throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->getName()}"); + } + } + + if (!in_array($blameable->on, ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); + } + + if ('change' === $blameable->on) { + if (!isset($blameable->field)) { + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); + } + + if (is_array($blameable->field) && isset($blameable->value)) { + throw new InvalidMappingException('Blameable extension does not support multiple value changeset detection yet.'); + } + + $field = [ + 'field' => $field, + 'trackedField' => $blameable->field, + 'value' => $blameable->value, + ]; + } + // properties are unique and mapper checks that, no risk here + $config[$blameable->on][] = $field; + } + } + + return $config; + } } diff --git a/src/IpTraceable/Mapping/Driver/Annotation.php b/src/IpTraceable/Mapping/Driver/Annotation.php index 4656d62182..03d7537db1 100644 --- a/src/IpTraceable/Mapping/Driver/Annotation.php +++ b/src/IpTraceable/Mapping/Driver/Annotation.php @@ -9,79 +9,15 @@ namespace Gedmo\IpTraceable\Mapping\Driver; -use Gedmo\Exception\InvalidMappingException; -use Gedmo\Mapping\Annotation\IpTraceable; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; /** - * This is an annotation mapping driver for IpTraceable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for IpTraceable - * extension. - * - * @author Pierre-Charles Bertineau + * Mapping driver for the IP traceable extension which reads extended metadata from annotations on an IP traceable class. * * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. * * @internal */ -class Annotation extends AbstractAnnotationDriver +class Annotation extends Attribute implements AnnotationDriverInterface { - /** - * Annotation field is ipTraceable - */ - public const IP_TRACEABLE = IpTraceable::class; - - /** - * List of types which are valid for IP - * - * @var string[] - */ - protected $validTypes = [ - 'string', - ]; - - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - // property annotations - foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() - || $meta->isInheritedField($property->name) - || isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - if ($ipTraceable = $this->reader->getPropertyAnnotation($property, self::IP_TRACEABLE)) { - $field = $property->getName(); - - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find ipTraceable [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' - {$meta->getName()}"); - } - if (!in_array($ipTraceable->on, ['update', 'create', 'change'], true)) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); - } - if ('change' === $ipTraceable->on) { - if (!isset($ipTraceable->field)) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); - } - if (is_array($ipTraceable->field) && isset($ipTraceable->value)) { - throw new InvalidMappingException('IpTraceable extension does not support multiple value changeset detection yet.'); - } - $field = [ - 'field' => $field, - 'trackedField' => $ipTraceable->field, - 'value' => $ipTraceable->value, - ]; - } - // properties are unique and mapper checks that, no risk here - $config[$ipTraceable->on][] = $field; - } - } - - return $config; - } } diff --git a/src/IpTraceable/Mapping/Driver/Attribute.php b/src/IpTraceable/Mapping/Driver/Attribute.php index 64a578ed89..f1503baf20 100644 --- a/src/IpTraceable/Mapping/Driver/Attribute.php +++ b/src/IpTraceable/Mapping/Driver/Attribute.php @@ -9,17 +9,84 @@ namespace Gedmo\IpTraceable\Mapping\Driver; +use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\IpTraceable; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an attribute mapping driver for IpTraceable - * behavioral extension. Used for extraction of extended - * metadata from attribute specifically for IpTraceable - * extension. + * Mapping driver for the IP traceable extension which reads extended metadata from attributes on an IP traceable class. + * + * @author Pierre-Charles Bertineau * * @internal */ -final class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { + /** + * Mapping object for the IP traceable extension. + */ + public const IP_TRACEABLE = IpTraceable::class; + + /** + * List of types which are valid for IP + * + * @var string[] + */ + protected $validTypes = [ + 'string', + ]; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // property annotations + foreach ($class->getProperties() as $property) { + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) + ) { + continue; + } + + if ($ipTraceable = $this->reader->getPropertyAnnotation($property, self::IP_TRACEABLE)) { + \assert($ipTraceable instanceof IpTraceable); + + $field = $property->getName(); + + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find ipTraceable [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$this->isValidField($meta, $field)) { + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' - {$meta->getName()}"); + } + + if (!in_array($ipTraceable->on, ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); + } + + if ('change' === $ipTraceable->on) { + if (!isset($ipTraceable->field)) { + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); + } + + if (is_array($ipTraceable->field) && isset($ipTraceable->value)) { + throw new InvalidMappingException('IpTraceable extension does not support multiple value changeset detection yet.'); + } + + $field = [ + 'field' => $field, + 'trackedField' => $ipTraceable->field, + 'value' => $ipTraceable->value, + ]; + } + + // properties are unique and mapper checks that, no risk here + $config[$ipTraceable->on][] = $field; + } + } + + return $config; + } } diff --git a/src/Loggable/Mapping/Driver/Annotation.php b/src/Loggable/Mapping/Driver/Annotation.php index 86f4293180..45f895952a 100644 --- a/src/Loggable/Mapping/Driver/Annotation.php +++ b/src/Loggable/Mapping/Driver/Annotation.php @@ -9,138 +9,15 @@ namespace Gedmo\Loggable\Mapping\Driver; -use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ClassMetadataODM; -use Doctrine\Persistence\Mapping\ClassMetadata; -use Gedmo\Exception\InvalidMappingException; -use Gedmo\Mapping\Annotation\Loggable; -use Gedmo\Mapping\Annotation\Versioned; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; /** - * This is an annotation mapping driver for Loggable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Loggable - * extension. - * - * @author Boussekeyt Jules - * @author Gediminas Morkevicius + * Mapping driver for the loggable extension which reads extended metadata from annotations on a loggable class. * * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. * * @internal */ -class Annotation extends AbstractAnnotationDriver +class Annotation extends Attribute implements AnnotationDriverInterface { - /** - * Annotation to define that this object is loggable - */ - public const LOGGABLE = Loggable::class; - - /** - * Annotation to define that this property is versioned - */ - public const VERSIONED = Versioned::class; - - public function validateFullMetadata(ClassMetadata $meta, array $config) - { - if ($config && $meta instanceof ClassMetadataODM && is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { - throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->getName()}"); - } - if (isset($config['versioned']) && !isset($config['loggable'])) { - throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->getName()}"); - } - } - - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - // class annotations - if ($annot = $this->reader->getClassAnnotation($class, self::LOGGABLE)) { - $config['loggable'] = true; - if ($annot->logEntryClass) { - if (!$cl = $this->getRelatedClassName($meta, $annot->logEntryClass)) { - throw new InvalidMappingException("LogEntry class: {$annot->logEntryClass} does not exist."); - } - $config['logEntryClass'] = $cl; - } - } - - // property annotations - foreach ($class->getProperties() as $property) { - $field = $property->getName(); - if ($meta->isMappedSuperclass && !$property->isPrivate()) { - continue; - } - - // versioned property - if ($this->reader->getPropertyAnnotation($property, self::VERSIONED)) { - if (!$this->isMappingValid($meta, $field)) { - throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); - } - if (isset($meta->embeddedClasses[$field])) { - $this->inspectEmbeddedForVersioned($field, $config, $meta); - - continue; - } - // fields cannot be overrided and throws mapping exception - if (!in_array($field, $config['versioned'] ?? [], true)) { - $config['versioned'][] = $field; - } - } - } - - if (!$meta->isMappedSuperclass && $config) { - if ($meta instanceof ClassMetadataODM && is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { - throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->getName()}"); - } - if ($this->isClassAnnotationInValid($meta, $config)) { - throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->getName()}"); - } - } - - return $config; - } - - /** - * @param string $field - * - * @return bool - */ - protected function isMappingValid(ClassMetadata $meta, $field) - { - return false == $meta->isCollectionValuedAssociation($field); - } - - /** - * @param array $config - * - * @return bool - */ - protected function isClassAnnotationInValid(ClassMetadata $meta, array &$config) - { - return isset($config['versioned']) && !isset($config['loggable']) && (!isset($meta->isEmbeddedClass) || !$meta->isEmbeddedClass); - } - - /** - * Searches properties of embedded object for versioned fields - * - * @param array $config - */ - private function inspectEmbeddedForVersioned(string $field, array &$config, \Doctrine\ORM\Mapping\ClassMetadata $meta): void - { - $class = new \ReflectionClass($meta->embeddedClasses[$field]['class']); - - // property annotations - foreach ($class->getProperties() as $property) { - // versioned property - if ($this->reader->getPropertyAnnotation($property, self::VERSIONED)) { - $embeddedField = $field.'.'.$property->getName(); - $config['versioned'][] = $embeddedField; - - if (isset($meta->embeddedClasses[$embeddedField])) { - $this->inspectEmbeddedForVersioned($embeddedField, $config, $meta); - } - } - } - } } diff --git a/src/Loggable/Mapping/Driver/Attribute.php b/src/Loggable/Mapping/Driver/Attribute.php index c1d07f54c6..e0c8739612 100644 --- a/src/Loggable/Mapping/Driver/Attribute.php +++ b/src/Loggable/Mapping/Driver/Attribute.php @@ -9,16 +9,144 @@ namespace Gedmo\Loggable\Mapping\Driver; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ClassMetadataODM; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Loggable\LogEntryInterface; +use Gedmo\Mapping\Annotation\Loggable; +use Gedmo\Mapping\Annotation\Versioned; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an attribute mapping driver for Loggable - * behavioral extension. Used for extraction of extended - * metadata from attributes specifically for Loggable - * extension. + * Mapping driver for the loggable extension which reads extended metadata from attributes on a loggable class. + * + * @author Boussekeyt Jules + * @author Gediminas Morkevicius * * @internal */ -final class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { + /** + * Mapping object defining a loggable class. + */ + public const LOGGABLE = Loggable::class; + + /** + * Mapping object defining a versioned property from a loggable class. + */ + public const VERSIONED = Versioned::class; + + public function validateFullMetadata(ClassMetadata $meta, array $config) + { + if ($config && $meta instanceof ClassMetadataODM && is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->getName()}"); + } + + if (isset($config['versioned']) && !isset($config['loggable'])) { + throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->getName()}"); + } + } + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // class annotations + if ($annot = $this->reader->getClassAnnotation($class, self::LOGGABLE)) { + \assert($annot instanceof Loggable); + + $config['loggable'] = true; + + if ($annot->logEntryClass) { + if (!$cl = $this->getRelatedClassName($meta, $annot->logEntryClass)) { + throw new InvalidMappingException("LogEntry class: {$annot->logEntryClass} does not exist."); + } + + $config['logEntryClass'] = $cl; + } + } + + // property annotations + foreach ($class->getProperties() as $property) { + $field = $property->getName(); + + if ($meta->isMappedSuperclass && !$property->isPrivate()) { + continue; + } + + // versioned property + if ($this->reader->getPropertyAnnotation($property, self::VERSIONED)) { + if (!$this->isMappingValid($meta, $field)) { + throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); + } + + if (isset($meta->embeddedClasses[$field])) { + $this->inspectEmbeddedForVersioned($field, $config, $meta); + + continue; + } + + // fields cannot be overridden and throws mapping exception + if (!in_array($field, $config['versioned'] ?? [], true)) { + $config['versioned'][] = $field; + } + } + } + + if (!$meta->isMappedSuperclass && $config) { + if ($meta instanceof ClassMetadataODM && is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->getName()}"); + } + + if ($this->isClassAnnotationInValid($meta, $config)) { + throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->getName()}"); + } + } + + return $config; + } + + /** + * @param string $field + * + * @return bool + */ + protected function isMappingValid(ClassMetadata $meta, $field) + { + return false == $meta->isCollectionValuedAssociation($field); + } + + /** + * @param array $config + * + * @return bool + */ + protected function isClassAnnotationInValid(ClassMetadata $meta, array &$config) + { + return isset($config['versioned']) && !isset($config['loggable']) && (!isset($meta->isEmbeddedClass) || !$meta->isEmbeddedClass); + } + + /** + * Searches properties of embedded objects for versioned fields + * + * @param array $config + */ + private function inspectEmbeddedForVersioned(string $field, array &$config, \Doctrine\ORM\Mapping\ClassMetadata $meta): void + { + $class = new \ReflectionClass($meta->embeddedClasses[$field]['class']); + + // property annotations + foreach ($class->getProperties() as $property) { + // versioned property + if ($this->reader->getPropertyAnnotation($property, self::VERSIONED)) { + $embeddedField = $field.'.'.$property->getName(); + $config['versioned'][] = $embeddedField; + + if (isset($meta->embeddedClasses[$embeddedField])) { + $this->inspectEmbeddedForVersioned($embeddedField, $config, $meta); + } + } + } + } } diff --git a/src/Mapping/Driver/AbstractAnnotationDriver.php b/src/Mapping/Driver/AbstractAnnotationDriver.php index b2a26a0269..70d3cbbcd7 100644 --- a/src/Mapping/Driver/AbstractAnnotationDriver.php +++ b/src/Mapping/Driver/AbstractAnnotationDriver.php @@ -20,7 +20,7 @@ * * @author Derek J. Lambert */ -abstract class AbstractAnnotationDriver implements AnnotationDriverInterface +abstract class AbstractAnnotationDriver implements AttributeDriverInterface { /** * Annotation reader instance @@ -45,6 +45,23 @@ abstract class AbstractAnnotationDriver implements AnnotationDriverInterface */ protected $validTypes = []; + /** + * Set the annotation reader instance + * + * When originally implemented, `Doctrine\Common\Annotations\Reader` was not available, + * therefore this method may accept any object implementing these methods from the interface: + * + * getClassAnnotations([reflectionClass]) + * getClassAnnotation([reflectionClass], [name]) + * getPropertyAnnotations([reflectionProperty]) + * getPropertyAnnotation([reflectionProperty], [name]) + * + * @param Reader|AttributeReader|object $reader + * + * @return void + * + * @note Providing any object is deprecated, as of 4.0 an {@see AttributeReader} will be required + */ public function setAnnotationReader($reader) { if ($reader instanceof Reader) { diff --git a/src/Mapping/ExtensionMetadataFactory.php b/src/Mapping/ExtensionMetadataFactory.php index 85a325cd36..54f9d470b8 100644 --- a/src/Mapping/ExtensionMetadataFactory.php +++ b/src/Mapping/ExtensionMetadataFactory.php @@ -258,13 +258,13 @@ protected function getDriver($omDriver) } if ($driver instanceof AnnotationDriverInterface) { + $driver->setAnnotationReader($this->annotationReader); + } else { if ($this->annotationReader instanceof AttributeReader) { $driver->setAnnotationReader($this->annotationReader); } else { $driver->setAnnotationReader(new AttributeAnnotationReader(new AttributeReader(), $this->annotationReader)); } - } else { - $driver->setAnnotationReader($this->annotationReader); } } } diff --git a/src/ReferenceIntegrity/Mapping/Driver/Annotation.php b/src/ReferenceIntegrity/Mapping/Driver/Annotation.php index 48b80e2c43..8d095c1e29 100644 --- a/src/ReferenceIntegrity/Mapping/Driver/Annotation.php +++ b/src/ReferenceIntegrity/Mapping/Driver/Annotation.php @@ -12,57 +12,16 @@ use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\ReferenceIntegrity; use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; use Gedmo\ReferenceIntegrity\Mapping\Validator; /** - * This is an annotation mapping driver for ReferenceIntegrity - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for ReferenceIntegrity - * extension. - * - * @author Evert Harmeling + * Mapping driver for the reference integrity extension which reads extended metadata from annotations on a class with referential integrity. * * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. * * @internal */ -class Annotation extends AbstractAnnotationDriver +class Annotation extends Attribute implements AnnotationDriverInterface { - /** - * Annotation to identify the fields which manages the reference integrity - */ - public const REFERENCE_INTEGRITY = ReferenceIntegrity::class; - - /** - * ReferenceIntegrityAction extension annotation - */ - public const ACTION = 'Gedmo\\Mapping\\Annotation\\ReferenceIntegrityAction'; - - public function readExtendedMetadata($meta, array &$config) - { - $validator = new Validator(); - $reflClass = $this->getMetaReflectionClass($meta); - - foreach ($reflClass->getProperties() as $reflProperty) { - if ($referenceIntegrity = $this->reader->getPropertyAnnotation($reflProperty, self::REFERENCE_INTEGRITY)) { - $property = $reflProperty->getName(); - if (!$meta->hasField($property)) { - throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $property, $meta->getName())); - } - - $fieldMapping = $meta->getFieldMapping($property); - if (!isset($fieldMapping['mappedBy'])) { - throw new InvalidMappingException(sprintf("'mappedBy' should be set on '%s' in '%s'", $property, $meta->getName())); - } - - if (!in_array($referenceIntegrity->value, $validator->getIntegrityActions(), true)) { - throw new InvalidMappingException(sprintf('Field - [%s] does not have a valid integrity option, [%s] in class - %s', $property, implode(', ', $validator->getIntegrityActions()), $meta->getName())); - } - - $config['referenceIntegrity'][$property] = $referenceIntegrity->value; - } - } - - return $config; - } } diff --git a/src/ReferenceIntegrity/Mapping/Driver/Attribute.php b/src/ReferenceIntegrity/Mapping/Driver/Attribute.php index 46b0a5cab9..e2b7387e01 100644 --- a/src/ReferenceIntegrity/Mapping/Driver/Attribute.php +++ b/src/ReferenceIntegrity/Mapping/Driver/Attribute.php @@ -9,17 +9,61 @@ namespace Gedmo\ReferenceIntegrity\Mapping\Driver; +use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\ReferenceIntegrity; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\ReferenceIntegrity\Mapping\Validator; /** - * This is an attribute mapping driver for ReferenceIntegrity - * behavioral extension. Used for extraction of extended - * metadata from attributes specifically for ReferenceIntegrity - * extension. + * Mapping driver for the reference integrity extension which reads extended metadata from attributes on a class with referential integrity. + * + * @author Evert Harmeling * * @internal */ -final class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { + /** + * Mapping object for the reference integrity extension. + */ + public const REFERENCE_INTEGRITY = ReferenceIntegrity::class; + + /** + * Unimplemented mapping object for actions within the reference integrity extension. + * + * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0; actions are defined as {@see Validator} class constants instead. + */ + public const ACTION = 'Gedmo\\Mapping\\Annotation\\ReferenceIntegrityAction'; + + public function readExtendedMetadata($meta, array &$config) + { + $validator = new Validator(); + $reflClass = $this->getMetaReflectionClass($meta); + + foreach ($reflClass->getProperties() as $reflProperty) { + if ($referenceIntegrity = $this->reader->getPropertyAnnotation($reflProperty, self::REFERENCE_INTEGRITY)) { + \assert($referenceIntegrity instanceof ReferenceIntegrity); + + $property = $reflProperty->getName(); + + if (!$meta->hasField($property)) { + throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $property, $meta->getName())); + } + + $fieldMapping = $meta->getFieldMapping($property); + + if (!isset($fieldMapping['mappedBy'])) { + throw new InvalidMappingException(sprintf("'mappedBy' should be set on '%s' in '%s'", $property, $meta->getName())); + } + + if (!in_array($referenceIntegrity->value, $validator->getIntegrityActions(), true)) { + throw new InvalidMappingException(sprintf('Field - [%s] does not have a valid integrity option, [%s] in class - %s', $property, implode(', ', $validator->getIntegrityActions()), $meta->getName())); + } + + $config['referenceIntegrity'][$property] = $referenceIntegrity->value; + } + } + + return $config; + } } diff --git a/src/References/Mapping/Driver/Annotation.php b/src/References/Mapping/Driver/Annotation.php index 7d30285d4f..3382d254e8 100644 --- a/src/References/Mapping/Driver/Annotation.php +++ b/src/References/Mapping/Driver/Annotation.php @@ -9,105 +9,15 @@ namespace Gedmo\References\Mapping\Driver; -use Doctrine\Common\Annotations\Reader; -use Doctrine\Persistence\Mapping\Driver\MappingDriver; -use Gedmo\Mapping\Annotation\ReferenceMany; -use Gedmo\Mapping\Annotation\ReferenceManyEmbed; -use Gedmo\Mapping\Annotation\ReferenceOne; use Gedmo\Mapping\Driver\AnnotationDriverInterface; -use Gedmo\Mapping\Driver\AttributeReader; /** - * This is an annotation mapping driver for References - * behavioral extension. - * - * @author Gediminas Morkevicius - * @author Bulat Shakirzyanov - * @author Jonathan H. Wage + * Mapping driver for the references extension which reads extended metadata from annotations on a class with references. * * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. * * @internal */ -class Annotation implements AnnotationDriverInterface +class Annotation extends Attribute implements AnnotationDriverInterface { - /** - * Annotation to mark field as reference to one - */ - public const REFERENCE_ONE = ReferenceOne::class; - - /** - * Annotation to mark field as reference to many - */ - public const REFERENCE_MANY = ReferenceMany::class; - - /** - * Annotation to mark field as reference to many - */ - public const REFERENCE_MANY_EMBED = ReferenceManyEmbed::class; - - /** - * @var array - */ - private const ANNOTATIONS = [ - 'referenceOne' => self::REFERENCE_ONE, - 'referenceMany' => self::REFERENCE_MANY, - 'referenceManyEmbed' => self::REFERENCE_MANY_EMBED, - ]; - - /** - * original driver if it is available - * - * @var MappingDriver - */ - protected $_originalDriver; - - /** - * Annotation reader instance - * - * @var Reader|AttributeReader|object - */ - private $reader; - - public function setAnnotationReader($reader) - { - $this->reader = $reader; - } - - public function readExtendedMetadata($meta, array &$config) - { - $class = $meta->getReflectionClass(); - foreach (self::ANNOTATIONS as $key => $annotation) { - $config[$key] = []; - foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() - || $meta->isInheritedField($property->name) - || isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - - if ($reference = $this->reader->getPropertyAnnotation($property, $annotation)) { - $config[$key][$property->getName()] = [ - 'field' => $property->getName(), - 'type' => $reference->type, - 'class' => $reference->class, - 'identifier' => $reference->identifier, - 'mappedBy' => $reference->mappedBy, - 'inversedBy' => $reference->inversedBy, - ]; - } - } - } - - return $config; - } - - /** - * Passes in the mapping read by original driver - */ - public function setOriginalDriver($driver) - { - $this->_originalDriver = $driver; - } } diff --git a/src/References/Mapping/Driver/Attribute.php b/src/References/Mapping/Driver/Attribute.php index 57e3f7de11..0e1450c709 100644 --- a/src/References/Mapping/Driver/Attribute.php +++ b/src/References/Mapping/Driver/Attribute.php @@ -9,14 +9,77 @@ namespace Gedmo\References\Mapping\Driver; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Mapping\Annotation\Reference; +use Gedmo\Mapping\Annotation\ReferenceMany; +use Gedmo\Mapping\Annotation\ReferenceManyEmbed; +use Gedmo\Mapping\Annotation\ReferenceOne; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an attribute mapping driver for References - * behavioral extension. + * Mapping driver for the references extension which reads extended metadata from attributes on a class with references. + * + * @author Gediminas Morkevicius + * @author Bulat Shakirzyanov + * @author Jonathan H. Wage * * @internal */ -final class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { + /** + * Mapping object declaring a field as having a reference to one object. + */ + public const REFERENCE_ONE = ReferenceOne::class; + + /** + * Mapping object declaring a field as having a reference to many objects. + */ + public const REFERENCE_MANY = ReferenceMany::class; + + /** + * Mapping object declaring a field as having a reference to an embedded collection of many objects. + */ + public const REFERENCE_MANY_EMBED = ReferenceManyEmbed::class; + + /** + * @var array + */ + private const ANNOTATIONS = [ + 'referenceOne' => self::REFERENCE_ONE, + 'referenceMany' => self::REFERENCE_MANY, + 'referenceManyEmbed' => self::REFERENCE_MANY_EMBED, + ]; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $meta->getReflectionClass(); + + foreach (self::ANNOTATIONS as $key => $annotation) { + $config[$key] = []; + + foreach ($class->getProperties() as $property) { + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) + ) { + continue; + } + + if ($reference = $this->reader->getPropertyAnnotation($property, $annotation)) { + \assert($reference instanceof Reference); + + $config[$key][$property->getName()] = [ + 'field' => $property->getName(), + 'type' => $reference->type, + 'class' => $reference->class, + 'identifier' => $reference->identifier, + 'mappedBy' => $reference->mappedBy, + 'inversedBy' => $reference->inversedBy, + ]; + } + } + } + + return $config; + } } diff --git a/src/Sluggable/Mapping/Driver/Annotation.php b/src/Sluggable/Mapping/Driver/Annotation.php index b329997bbc..6d8c2b4088 100644 --- a/src/Sluggable/Mapping/Driver/Annotation.php +++ b/src/Sluggable/Mapping/Driver/Annotation.php @@ -15,12 +15,11 @@ use Gedmo\Mapping\Annotation\SlugHandler; use Gedmo\Mapping\Annotation\SlugHandlerOption; use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; +use Gedmo\Sluggable\Handler\SlugHandlerInterface; /** - * This is an annotation mapping driver for Sluggable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Sluggable - * extension. + * Mapping driver for the sluggable extension which reads extended metadata from annotations on a sluggable class. * * @author Gediminas Morkevicius * @@ -28,74 +27,10 @@ * * @internal */ -class Annotation extends AbstractAnnotationDriver +class Annotation extends Attribute implements AnnotationDriverInterface { /** - * Annotation to identify field as one which holds the slug - * together with slug options - */ - public const SLUG = Slug::class; - - /** - * SlugHandler extension annotation - */ - public const HANDLER = SlugHandler::class; - - /** - * SlugHandler option annotation - */ - public const HANDLER_OPTION = SlugHandlerOption::class; - - /** - * List of types which are valid for slug and sluggable fields - * - * @var string[] - */ - protected $validTypes = [ - 'string', - 'text', - 'integer', - 'int', - 'date', - 'date_immutable', - 'datetime', - 'datetime_immutable', - 'datetimetz', - 'datetimetz_immutable', - 'citext', - ]; - - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - // property annotations - foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() - || $meta->isInheritedField($property->name) - || isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - $config = $this->retrieveSlug($meta, $config, $property); - } - - // Embedded entity - if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) { - foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) { - $embeddedClass = new \ReflectionClass($embeddedClassInfo['class']); - foreach ($embeddedClass->getProperties() as $embeddedProperty) { - $config = $this->retrieveSlug($meta, $config, $embeddedProperty, $propertyName); - } - } - } - - return $config; - } - - /** - * @internal - * - * @return array + * @return array, SlugHandler[]> */ protected function getSlugHandlers(\ReflectionProperty $property, Slug $slug, ClassMetadata $meta): array { @@ -109,101 +44,31 @@ protected function getSlugHandlers(\ReflectionProperty $property, Slug $slug, Cl if (!$handler instanceof SlugHandler) { throw new InvalidMappingException("SlugHandler: {$handler} should be instance of SlugHandler annotation in entity - {$meta->getName()}"); } + if (!class_exists($handler->class)) { throw new InvalidMappingException("SlugHandler class: {$handler->class} should be a valid class name in entity - {$meta->getName()}"); } + + /** @var class-string $class */ $class = $handler->class; + $handlers[$class] = []; + foreach ($handler->options as $option) { if (!$option instanceof SlugHandlerOption) { throw new InvalidMappingException("SlugHandlerOption: {$option} should be instance of SlugHandlerOption annotation in entity - {$meta->getName()}"); } + if ('' === $option->name) { throw new InvalidMappingException("SlugHandlerOption name: {$option->name} should be valid name in entity - {$meta->getName()}"); } + $handlers[$class][$option->name] = $option->value; } + $class::validate($handlers[$class], $meta); } return $handlers; } - - /** - * @param array $config - * - * @return array> - */ - private function retrieveSlug(ClassMetadata $meta, array &$config, \ReflectionProperty $property, ?string $fieldNamePrefix = null): array - { - $fieldName = null !== $fieldNamePrefix ? ($fieldNamePrefix.'.'.$property->getName()) : $property->getName(); - - // slug property - $slug = $this->reader->getPropertyAnnotation($property, self::SLUG); - - if (null === $slug) { - return $config; - } - - assert($slug instanceof Slug); - - if (!$meta->hasField($fieldName)) { - throw new InvalidMappingException("Unable to find slug [{$fieldName}] as mapped property in entity - {$meta->getName()}"); - } - if (!$this->isValidField($meta, $fieldName)) { - throw new InvalidMappingException("Cannot use field - [{$fieldName}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); - } - // process slug handlers - $handlers = $this->getSlugHandlers($property, $slug, $meta); - - // process slug fields - if ([] === $slug->fields || !is_array($slug->fields)) { - throw new InvalidMappingException("Slug must contain at least one field for slug generation in class - {$meta->getName()}"); - } - foreach ($slug->fields as $slugField) { - $slugFieldWithPrefix = null !== $fieldNamePrefix ? ($fieldNamePrefix.'.'.$slugField) : $slugField; - if (!$meta->hasField($slugFieldWithPrefix)) { - throw new InvalidMappingException("Unable to find slug [{$slugFieldWithPrefix}] as mapped property in entity - {$meta->getName()}"); - } - if (!$this->isValidField($meta, $slugFieldWithPrefix)) { - throw new InvalidMappingException("Cannot use field - [{$slugFieldWithPrefix}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); - } - } - if (!is_bool($slug->updatable)) { - throw new InvalidMappingException("Slug annotation [updatable], type is not valid and must be 'boolean' in class - {$meta->getName()}"); - } - if (!is_bool($slug->unique)) { - throw new InvalidMappingException("Slug annotation [unique], type is not valid and must be 'boolean' in class - {$meta->getName()}"); - } - if ([] !== $meta->getIdentifier() && $meta->isIdentifier($fieldName) && !(bool) $slug->unique) { - throw new InvalidMappingException("Identifier field - [{$fieldName}] slug must be unique in order to maintain primary key in class - {$meta->getName()}"); - } - if (false === $slug->unique && $slug->unique_base) { - throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'"); - } - if ($slug->unique_base && !$meta->hasField($slug->unique_base) && !$meta->hasAssociation($slug->unique_base)) { - throw new InvalidMappingException("Unable to find [{$slug->unique_base}] as mapped property in entity - {$meta->getName()}"); - } - $sluggableFields = []; - foreach ($slug->fields as $field) { - $sluggableFields[] = null !== $fieldNamePrefix ? ($fieldNamePrefix.'.'.$field) : $field; - } - - // set all options - $config['slugs'][$fieldName] = [ - 'fields' => $sluggableFields, - 'slug' => $fieldName, - 'style' => $slug->style, - 'dateFormat' => $slug->dateFormat, - 'updatable' => $slug->updatable, - 'unique' => $slug->unique, - 'unique_base' => $slug->unique_base, - 'separator' => $slug->separator, - 'prefix' => $slug->prefix, - 'suffix' => $slug->suffix, - 'handlers' => $handlers, - ]; - - return $config; - } } diff --git a/src/Sluggable/Mapping/Driver/Attribute.php b/src/Sluggable/Mapping/Driver/Attribute.php index a804d0e96e..fb6c4874a0 100644 --- a/src/Sluggable/Mapping/Driver/Attribute.php +++ b/src/Sluggable/Mapping/Driver/Attribute.php @@ -13,23 +13,179 @@ use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\Slug; use Gedmo\Mapping\Annotation\SlugHandler; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Mapping\Annotation\SlugHandlerOption; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Sluggable\Handler\SlugHandlerInterface; /** - * This is an attribute mapping driver for Sluggable - * behavioral extension. Used for extraction of extended - * metadata from attribute specifically for Sluggable - * extension. + * Mapping driver for the sluggable extension which reads extended metadata from attributes on a sluggable class. + * + * @author Gediminas Morkevicius * * @internal */ -final class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { /** - * @return array + * Mapping object configuring a field which should have a slug computed. + */ + public const SLUG = Slug::class; + + /** + * Mapping object configuring a slug handler for a sluggable field. + */ + public const HANDLER = SlugHandler::class; + + /** + * Mapping object configuring an option for a slug handler. + */ + public const HANDLER_OPTION = SlugHandlerOption::class; + + /** + * List of types which are valid for slug and sluggable fields + * + * @var string[] + */ + protected $validTypes = [ + 'string', + 'text', + 'integer', + 'int', + 'date', + 'date_immutable', + 'datetime', + 'datetime_immutable', + 'datetimetz', + 'datetimetz_immutable', + 'citext', + ]; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // property annotations + foreach ($class->getProperties() as $property) { + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) + ) { + continue; + } + + $config = $this->retrieveSlug($meta, $config, $property); + } + + // Embedded entity + if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) { + foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) { + $embeddedClass = new \ReflectionClass($embeddedClassInfo['class']); + + foreach ($embeddedClass->getProperties() as $embeddedProperty) { + $config = $this->retrieveSlug($meta, $config, $embeddedProperty, $propertyName); + } + } + } + + return $config; + } + + /** + * @param array $config + * + * @return array> + */ + private function retrieveSlug(ClassMetadata $meta, array &$config, \ReflectionProperty $property, ?string $fieldNamePrefix = null): array + { + $fieldName = null !== $fieldNamePrefix ? ($fieldNamePrefix.'.'.$property->getName()) : $property->getName(); + + // slug property + $slug = $this->reader->getPropertyAnnotation($property, self::SLUG); + + if (null === $slug) { + return $config; + } + + assert($slug instanceof Slug); + + if (!$meta->hasField($fieldName)) { + throw new InvalidMappingException("Unable to find slug [{$fieldName}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$this->isValidField($meta, $fieldName)) { + throw new InvalidMappingException("Cannot use field - [{$fieldName}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); + } + + // process slug handlers + $handlers = $this->getSlugHandlers($property, $slug, $meta); + + // process slug fields + if ([] === $slug->fields || !is_array($slug->fields)) { + throw new InvalidMappingException("Slug must contain at least one field for slug generation in class - {$meta->getName()}"); + } + + foreach ($slug->fields as $slugField) { + $slugFieldWithPrefix = null !== $fieldNamePrefix ? ($fieldNamePrefix.'.'.$slugField) : $slugField; + + if (!$meta->hasField($slugFieldWithPrefix)) { + throw new InvalidMappingException("Unable to find slug [{$slugFieldWithPrefix}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$this->isValidField($meta, $slugFieldWithPrefix)) { + throw new InvalidMappingException("Cannot use field - [{$slugFieldWithPrefix}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); + } + } + + if (!is_bool($slug->updatable)) { + throw new InvalidMappingException("Slug annotation [updatable], type is not valid and must be 'boolean' in class - {$meta->getName()}"); + } + + if (!is_bool($slug->unique)) { + throw new InvalidMappingException("Slug annotation [unique], type is not valid and must be 'boolean' in class - {$meta->getName()}"); + } + + if ([] !== $meta->getIdentifier() && $meta->isIdentifier($fieldName) && !(bool) $slug->unique) { + throw new InvalidMappingException("Identifier field - [{$fieldName}] slug must be unique in order to maintain primary key in class - {$meta->getName()}"); + } + + if (false === $slug->unique && $slug->unique_base) { + throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'"); + } + + if ($slug->unique_base && !$meta->hasField($slug->unique_base) && !$meta->hasAssociation($slug->unique_base)) { + throw new InvalidMappingException("Unable to find [{$slug->unique_base}] as mapped property in entity - {$meta->getName()}"); + } + + $sluggableFields = []; + + foreach ($slug->fields as $field) { + $sluggableFields[] = null !== $fieldNamePrefix ? ($fieldNamePrefix.'.'.$field) : $field; + } + + // set all options + $config['slugs'][$fieldName] = [ + 'fields' => $sluggableFields, + 'slug' => $fieldName, + 'style' => $slug->style, + 'dateFormat' => $slug->dateFormat, + 'updatable' => $slug->updatable, + 'unique' => $slug->unique, + 'unique_base' => $slug->unique_base, + 'separator' => $slug->separator, + 'prefix' => $slug->prefix, + 'suffix' => $slug->suffix, + 'handlers' => $handlers, + ]; + + return $config; + } + + /** + * @return array, SlugHandler[]> */ protected function getSlugHandlers(\ReflectionProperty $property, Slug $slug, ClassMetadata $meta): array { + /** @var list|null $attributeHandlers */ $attributeHandlers = $this->reader->getPropertyAnnotation($property, self::HANDLER); if (null === $attributeHandlers) { @@ -43,9 +199,11 @@ protected function getSlugHandlers(\ReflectionProperty $property, Slug $slug, Cl throw new InvalidMappingException("SlugHandler class: {$handler->class} should be a valid class name in entity - {$meta->getName()}"); } + /** @var class-string $class */ $class = $handler->class; $handlers[$class] = []; + foreach ($handler->options as $name => $value) { $handlers[$class][$name] = $value; } diff --git a/src/SoftDeleteable/Mapping/Driver/Annotation.php b/src/SoftDeleteable/Mapping/Driver/Annotation.php index 14fcac75b6..a45df5a33c 100644 --- a/src/SoftDeleteable/Mapping/Driver/Annotation.php +++ b/src/SoftDeleteable/Mapping/Driver/Annotation.php @@ -9,61 +9,15 @@ namespace Gedmo\SoftDeleteable\Mapping\Driver; -use Gedmo\Exception\InvalidMappingException; -use Gedmo\Mapping\Annotation\SoftDeleteable; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; -use Gedmo\SoftDeleteable\Mapping\Validator; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; /** - * This is an annotation mapping driver for SoftDeleteable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for SoftDeleteable - * extension. - * - * @author Gustavo Falco - * @author Gediminas Morkevicius + * Mapping driver for the soft-deletable extension which reads extended metadata from annotations on a soft-deletable class. * * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. * * @internal */ -class Annotation extends AbstractAnnotationDriver +class Annotation extends Attribute implements AnnotationDriverInterface { - /** - * Annotation to define that this object is loggable - */ - public const SOFT_DELETEABLE = SoftDeleteable::class; - - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - // class annotations - if (null !== $class && $annot = $this->reader->getClassAnnotation($class, self::SOFT_DELETEABLE)) { - $config['softDeleteable'] = true; - - Validator::validateField($meta, $annot->fieldName); - - $config['fieldName'] = $annot->fieldName; - - $config['timeAware'] = false; - if (isset($annot->timeAware)) { - if (!is_bool($annot->timeAware)) { - throw new InvalidMappingException('timeAware must be boolean. '.gettype($annot->timeAware).' provided.'); - } - $config['timeAware'] = $annot->timeAware; - } - - $config['hardDelete'] = true; - if (isset($annot->hardDelete)) { - if (!is_bool($annot->hardDelete)) { - throw new InvalidMappingException('hardDelete must be boolean. '.gettype($annot->hardDelete).' provided.'); - } - $config['hardDelete'] = $annot->hardDelete; - } - } - - $this->validateFullMetadata($meta, $config); - - return $config; - } } diff --git a/src/SoftDeleteable/Mapping/Driver/Attribute.php b/src/SoftDeleteable/Mapping/Driver/Attribute.php index e5c97a8bb0..8358f1b35b 100644 --- a/src/SoftDeleteable/Mapping/Driver/Attribute.php +++ b/src/SoftDeleteable/Mapping/Driver/Attribute.php @@ -9,17 +9,63 @@ namespace Gedmo\SoftDeleteable\Mapping\Driver; +use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\SoftDeleteable; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\SoftDeleteable\Mapping\Validator; /** - * This is an attribute mapping driver for SoftDeleteable - * behavioral extension. Used for extraction of extended - * metadata from attributes specifically for SoftDeleteable - * extension. + * Mapping driver for the soft-deletable extension which reads extended metadata from attributes on a soft-deletable class. + * + * @author Gustavo Falco + * @author Gediminas Morkevicius * * @internal */ -final class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { + /** + * Mapping object for the soft-deletable extension. + */ + public const SOFT_DELETEABLE = SoftDeleteable::class; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // class annotations + if (null !== $class && $annot = $this->reader->getClassAnnotation($class, self::SOFT_DELETEABLE)) { + \assert($annot instanceof SoftDeleteable); + + $config['softDeleteable'] = true; + + Validator::validateField($meta, $annot->fieldName); + + $config['fieldName'] = $annot->fieldName; + + $config['timeAware'] = false; + + if (isset($annot->timeAware)) { + if (!is_bool($annot->timeAware)) { + throw new InvalidMappingException('timeAware must be boolean. '.gettype($annot->timeAware).' provided.'); + } + + $config['timeAware'] = $annot->timeAware; + } + + $config['hardDelete'] = true; + + if (isset($annot->hardDelete)) { + if (!is_bool($annot->hardDelete)) { + throw new InvalidMappingException('hardDelete must be boolean. '.gettype($annot->hardDelete).' provided.'); + } + + $config['hardDelete'] = $annot->hardDelete; + } + } + + $this->validateFullMetadata($meta, $config); + + return $config; + } } diff --git a/src/Sortable/Mapping/Driver/Annotation.php b/src/Sortable/Mapping/Driver/Annotation.php index 137bb762c6..a1367c4c06 100644 --- a/src/Sortable/Mapping/Driver/Annotation.php +++ b/src/Sortable/Mapping/Driver/Annotation.php @@ -9,91 +9,15 @@ namespace Gedmo\Sortable\Mapping\Driver; -use Gedmo\Exception\InvalidMappingException; -use Gedmo\Mapping\Annotation\SortableGroup; -use Gedmo\Mapping\Annotation\SortablePosition; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; /** - * This is an annotation mapping driver for Sortable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Sortable - * extension. - * - * @author Lukas Botsch + * Mapping driver for the sortable extension which reads extended metadata from annotations on a sortable class. * * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. * * @internal */ -class Annotation extends AbstractAnnotationDriver +class Annotation extends Attribute implements AnnotationDriverInterface { - /** - * Annotation to mark field as one which will store node position - */ - public const POSITION = SortablePosition::class; - - /** - * Annotation to mark field as sorting group - */ - public const GROUP = SortableGroup::class; - - /** - * List of types which are valid for position fields - * - * @var string[] - */ - protected $validTypes = [ - 'int', - 'integer', - 'smallint', - 'bigint', - ]; - - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - - // property annotations - foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() - || $meta->isInheritedField($property->name) - || isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - - // position - if ($this->reader->getPropertyAnnotation($property, self::POSITION)) { - $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'position' - [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Sortable position field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); - } - $config['position'] = $field; - } - - // group - if ($this->reader->getPropertyAnnotation($property, self::GROUP)) { - $field = $property->getName(); - if (!$meta->hasField($field) && !$meta->hasAssociation($field)) { - throw new InvalidMappingException("Unable to find 'group' - [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if (!isset($config['groups'])) { - $config['groups'] = []; - } - $config['groups'][] = $field; - } - } - - if (!$meta->isMappedSuperclass && $config) { - if (!isset($config['position'])) { - throw new InvalidMappingException("Missing property: 'position' in class - {$meta->getName()}"); - } - } - - return $config; - } } diff --git a/src/Sortable/Mapping/Driver/Attribute.php b/src/Sortable/Mapping/Driver/Attribute.php index beab4cbde1..61ac71e06c 100644 --- a/src/Sortable/Mapping/Driver/Attribute.php +++ b/src/Sortable/Mapping/Driver/Attribute.php @@ -9,18 +9,91 @@ namespace Gedmo\Sortable\Mapping\Driver; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\SortableGroup; +use Gedmo\Mapping\Annotation\SortablePosition; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an attribute mapping driver for Sortable - * behavioral extension. Used for extraction of extended - * metadata from attributes specifically for Sortable - * extension. + * Mapping driver for the sortable extension which reads extended metadata from attributes on a sortable class. + * + * @author Lukas Botsch * * @license MIT License (http://www.opensource.org/licenses/mit-license.php) * * @internal */ -final class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { + /** + * Mapping object to mark a field as the one which will store the node position on a sortable object. + */ + public const POSITION = SortablePosition::class; + + /** + * Mapping object to mark a field as part of a sorting group for a sortable object. + */ + public const GROUP = SortableGroup::class; + + /** + * List of types which are valid for position fields + * + * @var string[] + */ + protected $validTypes = [ + 'int', + 'integer', + 'smallint', + 'bigint', + ]; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // property annotations + foreach ($class->getProperties() as $property) { + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) + ) { + continue; + } + + // position + if ($this->reader->getPropertyAnnotation($property, self::POSITION)) { + $field = $property->getName(); + + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find 'position' - [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$this->isValidField($meta, $field)) { + throw new InvalidMappingException("Sortable position field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); + } + + $config['position'] = $field; + } + + // group + if ($this->reader->getPropertyAnnotation($property, self::GROUP)) { + $field = $property->getName(); + + if (!$meta->hasField($field) && !$meta->hasAssociation($field)) { + throw new InvalidMappingException("Unable to find 'group' - [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + $config['groups'] ??= []; + $config['groups'][] = $field; + } + } + + if (!$meta->isMappedSuperclass && $config) { + if (!isset($config['position'])) { + throw new InvalidMappingException("Missing property: 'position' in class - {$meta->getName()}"); + } + } + + return $config; + } } diff --git a/src/Timestampable/Mapping/Driver/Annotation.php b/src/Timestampable/Mapping/Driver/Annotation.php index cbde9ad315..52d4d4bc18 100644 --- a/src/Timestampable/Mapping/Driver/Annotation.php +++ b/src/Timestampable/Mapping/Driver/Annotation.php @@ -9,88 +9,15 @@ namespace Gedmo\Timestampable\Mapping\Driver; -use Gedmo\Exception\InvalidMappingException; -use Gedmo\Mapping\Annotation\Timestampable; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; /** - * This is an annotation mapping driver for Timestampable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Timestampable - * extension. - * - * @author Gediminas Morkevicius + * Mapping driver for the timestampable extension which reads extended metadata from annotations on a timestampable class. * * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. * * @internal */ -class Annotation extends AbstractAnnotationDriver +class Annotation extends Attribute implements AnnotationDriverInterface { - /** - * Annotation field is timestampable - */ - public const TIMESTAMPABLE = Timestampable::class; - - /** - * List of types which are valid for timestamp - * - * @var string[] - */ - protected $validTypes = [ - 'date', - 'date_immutable', - 'time', - 'time_immutable', - 'datetime', - 'datetime_immutable', - 'datetimetz', - 'datetimetz_immutable', - 'timestamp', - 'vardatetime', - 'integer', - ]; - - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - // property annotations - foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() - || $meta->isInheritedField($property->name) - || isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - if ($timestampable = $this->reader->getPropertyAnnotation($property, self::TIMESTAMPABLE)) { - $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find timestampable [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if (!$this->isValidField($meta, $field)) { - throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'date', 'datetime' or 'time' in class - {$meta->getName()}"); - } - if (!in_array($timestampable->on, ['update', 'create', 'change'], true)) { - throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); - } - if ('change' === $timestampable->on) { - if (!isset($timestampable->field)) { - throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); - } - if (is_array($timestampable->field) && isset($timestampable->value)) { - throw new InvalidMappingException('Timestampable extension does not support multiple value changeset detection yet.'); - } - $field = [ - 'field' => $field, - 'trackedField' => $timestampable->field, - 'value' => $timestampable->value, - ]; - } - // properties are unique and mapper checks that, no risk here - $config[$timestampable->on][] = $field; - } - } - - return $config; - } } diff --git a/src/Timestampable/Mapping/Driver/Attribute.php b/src/Timestampable/Mapping/Driver/Attribute.php index 9cad942b41..cf4c1c099e 100644 --- a/src/Timestampable/Mapping/Driver/Attribute.php +++ b/src/Timestampable/Mapping/Driver/Attribute.php @@ -9,20 +9,97 @@ namespace Gedmo\Timestampable\Mapping\Driver; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\Timestampable; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an attribute mapping driver for Timestampable - * behavioral extension. Used for extraction of extended - * metadata from attributes specifically for Timestampable - * extension. + * Mapping driver for the timestampable extension which reads extended metadata from attributes on a timestampable class. * + * @author Gediminas Morkevicius * @author Kevin Mian Kraiker * * @license MIT License (http://www.opensource.org/licenses/mit-license.php) * * @internal */ -final class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { + /** + * Mapping object for the timestampable extension. + */ + public const TIMESTAMPABLE = Timestampable::class; + + /** + * List of types which are valid for timestamp + * + * @var string[] + */ + protected $validTypes = [ + 'date', + 'date_immutable', + 'time', + 'time_immutable', + 'datetime', + 'datetime_immutable', + 'datetimetz', + 'datetimetz_immutable', + 'timestamp', + 'vardatetime', + 'integer', + ]; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // property annotations + foreach ($class->getProperties() as $property) { + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) + ) { + continue; + } + + if ($timestampable = $this->reader->getPropertyAnnotation($property, self::TIMESTAMPABLE)) { + \assert($timestampable instanceof Timestampable); + + $field = $property->getName(); + + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find timestampable [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$this->isValidField($meta, $field)) { + throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'date', 'datetime' or 'time' in class - {$meta->getName()}"); + } + + if (!in_array($timestampable->on, ['update', 'create', 'change'], true)) { + throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); + } + + if ('change' === $timestampable->on) { + if (!isset($timestampable->field)) { + throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); + } + + if (is_array($timestampable->field) && isset($timestampable->value)) { + throw new InvalidMappingException('Timestampable extension does not support multiple value changeset detection yet.'); + } + + $field = [ + 'field' => $field, + 'trackedField' => $timestampable->field, + 'value' => $timestampable->value, + ]; + } + + // properties are unique and mapper checks that, no risk here + $config[$timestampable->on][] = $field; + } + } + + return $config; + } } diff --git a/src/Translatable/Mapping/Driver/Annotation.php b/src/Translatable/Mapping/Driver/Annotation.php index 8e0ff47bc1..83cb4ecb45 100644 --- a/src/Translatable/Mapping/Driver/Annotation.php +++ b/src/Translatable/Mapping/Driver/Annotation.php @@ -9,122 +9,15 @@ namespace Gedmo\Translatable\Mapping\Driver; -use Gedmo\Exception\InvalidMappingException; -use Gedmo\Mapping\Annotation\Language; -use Gedmo\Mapping\Annotation\Locale; -use Gedmo\Mapping\Annotation\Translatable; -use Gedmo\Mapping\Annotation\TranslationEntity; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; /** - * This is an annotation mapping driver for Translatable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Translatable - * extension. - * - * @author Gediminas Morkevicius + * Mapping driver for the translatable extension which reads extended metadata from annotations on a translatable class. * * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. * * @internal */ -class Annotation extends AbstractAnnotationDriver +class Annotation extends Attribute implements AnnotationDriverInterface { - /** - * Annotation to identity translation entity to be used for translation storage - */ - public const ENTITY_CLASS = TranslationEntity::class; - - /** - * Annotation to identify field as translatable - */ - public const TRANSLATABLE = Translatable::class; - - /** - * Annotation to identify field which can store used locale or language - * alias is LANGUAGE - */ - public const LOCALE = Locale::class; - - /** - * Annotation to identify field which can store used locale or language - * alias is LOCALE - */ - public const LANGUAGE = Language::class; - - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - // class annotations - if ($annot = $this->reader->getClassAnnotation($class, self::ENTITY_CLASS)) { - if (!$cl = $this->getRelatedClassName($meta, $annot->class)) { - throw new InvalidMappingException("Translation class: {$annot->class} does not exist."); - } - $config['translationClass'] = $cl; - } - - // property annotations - foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() - || $meta->isInheritedField($property->name) - || isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - // translatable property - if ($translatable = $this->reader->getPropertyAnnotation($property, self::TRANSLATABLE)) { - $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find translatable [{$field}] as mapped property in entity - {$meta->getName()}"); - } - // fields cannot be overrided and throws mapping exception - $config['fields'][] = $field; - if (isset($translatable->fallback)) { - $config['fallback'][$field] = $translatable->fallback; - } - } - // locale property - if ($this->reader->getPropertyAnnotation($property, self::LOCALE)) { - $field = $property->getName(); - if ($meta->hasField($field)) { - throw new InvalidMappingException("Locale field [{$field}] should not be mapped as column property in entity - {$meta->getName()}, since it makes no sense"); - } - $config['locale'] = $field; - } elseif ($this->reader->getPropertyAnnotation($property, self::LANGUAGE)) { - $field = $property->getName(); - if ($meta->hasField($field)) { - throw new InvalidMappingException("Language field [{$field}] should not be mapped as column property in entity - {$meta->getName()}, since it makes no sense"); - } - $config['locale'] = $field; - } - } - - // Embedded entity - if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) { - foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) { - if ($meta->isInheritedEmbeddedClass($propertyName)) { - continue; - } - $embeddedClass = new \ReflectionClass($embeddedClassInfo['class']); - foreach ($embeddedClass->getProperties() as $embeddedProperty) { - if ($translatable = $this->reader->getPropertyAnnotation($embeddedProperty, self::TRANSLATABLE)) { - $field = $propertyName.'.'.$embeddedProperty->getName(); - - $config['fields'][] = $field; - if (isset($translatable->fallback)) { - $config['fallback'][$field] = $translatable->fallback; - } - } - } - } - } - - if (!$meta->isMappedSuperclass && $config) { - if (is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { - throw new InvalidMappingException("Translatable does not support composite identifiers in class - {$meta->getName()}"); - } - } - - return $config; - } } diff --git a/src/Translatable/Mapping/Driver/Attribute.php b/src/Translatable/Mapping/Driver/Attribute.php index ae35862c4d..044a46e6bc 100644 --- a/src/Translatable/Mapping/Driver/Attribute.php +++ b/src/Translatable/Mapping/Driver/Attribute.php @@ -9,19 +9,139 @@ namespace Gedmo\Translatable\Mapping\Driver; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\Language; +use Gedmo\Mapping\Annotation\Locale; use Gedmo\Mapping\Annotation\Translatable; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Mapping\Annotation\TranslationEntity; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** - * This is an attribute mapping driver for Translatable - * behavioral extension. Used for extraction of extended - * metadata from attributes specifically for Translatable - * extension. + * Mapping driver for the translatable extension which reads extended metadata from annotations on a translatable class. * * @author Gediminas Morkevicius * * @internal */ -final class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { + /** + * Mapping object to configure the translation model for a translatable class. + */ + public const ENTITY_CLASS = TranslationEntity::class; + + /** + * Mapping object to identify a field as translatable in a translatable class. + */ + public const TRANSLATABLE = Translatable::class; + + /** + * Mapping object to identify the field which stores the locale or language for the translation. + * + * This object is an alias of {@see self::LANGUAGE} + */ + public const LOCALE = Locale::class; + + /** + * Mapping object to identify the field which stores the locale or language for the translation. + * + * This object is an alias of {@see self::LOCALE} + */ + public const LANGUAGE = Language::class; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // class annotations + if ($annot = $this->reader->getClassAnnotation($class, self::ENTITY_CLASS)) { + \assert($annot instanceof TranslationEntity); + + if (!$cl = $this->getRelatedClassName($meta, $annot->class)) { + throw new InvalidMappingException("Translation class: {$annot->class} does not exist."); + } + + $config['translationClass'] = $cl; + } + + // property annotations + foreach ($class->getProperties() as $property) { + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) + ) { + continue; + } + + // translatable property + if ($translatable = $this->reader->getPropertyAnnotation($property, self::TRANSLATABLE)) { + \assert($translatable instanceof Translatable); + + $field = $property->getName(); + + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find translatable [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + // fields cannot be overrided and throws mapping exception + $config['fields'][] = $field; + + if (isset($translatable->fallback)) { + $config['fallback'][$field] = $translatable->fallback; + } + } + + // locale property + if ($this->reader->getPropertyAnnotation($property, self::LOCALE)) { + $field = $property->getName(); + + if ($meta->hasField($field)) { + throw new InvalidMappingException("Locale field [{$field}] should not be mapped as column property in entity - {$meta->getName()}, since it makes no sense"); + } + + $config['locale'] = $field; + } elseif ($this->reader->getPropertyAnnotation($property, self::LANGUAGE)) { + $field = $property->getName(); + + if ($meta->hasField($field)) { + throw new InvalidMappingException("Language field [{$field}] should not be mapped as column property in entity - {$meta->getName()}, since it makes no sense"); + } + + $config['locale'] = $field; + } + } + + // Embedded entity + if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) { + foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) { + if ($meta->isInheritedEmbeddedClass($propertyName)) { + continue; + } + + $embeddedClass = new \ReflectionClass($embeddedClassInfo['class']); + + foreach ($embeddedClass->getProperties() as $embeddedProperty) { + if ($translatable = $this->reader->getPropertyAnnotation($embeddedProperty, self::TRANSLATABLE)) { + \assert($translatable instanceof Translatable); + + $field = $propertyName.'.'.$embeddedProperty->getName(); + + $config['fields'][] = $field; + + if (isset($translatable->fallback)) { + $config['fallback'][$field] = $translatable->fallback; + } + } + } + } + } + + if (!$meta->isMappedSuperclass && $config) { + if (is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Translatable does not support composite identifiers in class - {$meta->getName()}"); + } + } + + return $config; + } } diff --git a/src/Tree/Mapping/Driver/Annotation.php b/src/Tree/Mapping/Driver/Annotation.php index a87e684fba..d37d20c7d5 100644 --- a/src/Tree/Mapping/Driver/Annotation.php +++ b/src/Tree/Mapping/Driver/Annotation.php @@ -9,265 +9,15 @@ namespace Gedmo\Tree\Mapping\Driver; -use Gedmo\Exception\InvalidMappingException; -use Gedmo\Mapping\Annotation\Tree; -use Gedmo\Mapping\Annotation\TreeClosure; -use Gedmo\Mapping\Annotation\TreeLeft; -use Gedmo\Mapping\Annotation\TreeLevel; -use Gedmo\Mapping\Annotation\TreeLockTime; -use Gedmo\Mapping\Annotation\TreeParent; -use Gedmo\Mapping\Annotation\TreePath; -use Gedmo\Mapping\Annotation\TreePathHash; -use Gedmo\Mapping\Annotation\TreePathSource; -use Gedmo\Mapping\Annotation\TreeRight; -use Gedmo\Mapping\Annotation\TreeRoot; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; -use Gedmo\Tree\Mapping\Validator; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; /** - * This is an annotation mapping driver for Tree - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Tree - * extension. - * - * @author Gediminas Morkevicius - * @author + * Mapping driver for the tree extension which reads extended metadata from annotations on class which is part of a tree. * * @deprecated since gedmo/doctrine-extensions 3.16, will be removed in version 4.0. * * @internal */ -class Annotation extends AbstractAnnotationDriver +class Annotation extends Attribute implements AnnotationDriverInterface { - /** - * Annotation to define the tree type - */ - public const TREE = Tree::class; - - /** - * Annotation to mark field as one which will store left value - */ - public const LEFT = TreeLeft::class; - - /** - * Annotation to mark field as one which will store right value - */ - public const RIGHT = TreeRight::class; - - /** - * Annotation to mark relative parent field - */ - public const PARENT = TreeParent::class; - - /** - * Annotation to mark node level - */ - public const LEVEL = TreeLevel::class; - - /** - * Annotation to mark field as tree root - */ - public const ROOT = TreeRoot::class; - - /** - * Annotation to specify closure tree class - */ - public const CLOSURE = TreeClosure::class; - - /** - * Annotation to specify path class - */ - public const PATH = TreePath::class; - - /** - * Annotation to specify path source class - */ - public const PATH_SOURCE = TreePathSource::class; - - /** - * Annotation to specify path hash class - */ - public const PATH_HASH = TreePathHash::class; - - /** - * Annotation to mark the field to be used to hold the lock time - */ - public const LOCK_TIME = TreeLockTime::class; - - /** - * List of tree strategies available - * - * @var string[] - */ - protected $strategies = [ - 'nested', - 'closure', - 'materializedPath', - ]; - - public function readExtendedMetadata($meta, array &$config) - { - $validator = new Validator(); - $class = $this->getMetaReflectionClass($meta); - // class annotations - if ($annot = $this->reader->getClassAnnotation($class, self::TREE)) { - if (!in_array($annot->type, $this->strategies, true)) { - throw new InvalidMappingException("Tree type: {$annot->type} is not available."); - } - $config['strategy'] = $annot->type; - $config['activate_locking'] = $annot->activateLocking; - $config['locking_timeout'] = (int) $annot->lockingTimeout; - - if ($config['locking_timeout'] < 1) { - throw new InvalidMappingException('Tree Locking Timeout must be at least of 1 second.'); - } - } - if ($annot = $this->reader->getClassAnnotation($class, self::CLOSURE)) { - if (!$cl = $this->getRelatedClassName($meta, $annot->class)) { - throw new InvalidMappingException("Tree closure class: {$annot->class} does not exist."); - } - $config['closure'] = $cl; - } - - // property annotations - foreach ($class->getProperties() as $property) { - if ($meta->isMappedSuperclass && !$property->isPrivate() - || $meta->isInheritedField($property->name) - || isset($meta->associationMappings[$property->name]['inherited']) - ) { - continue; - } - // left - if ($this->reader->getPropertyAnnotation($property, self::LEFT)) { - $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'left' - [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree left field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); - } - $config['left'] = $field; - } - // right - if ($this->reader->getPropertyAnnotation($property, self::RIGHT)) { - $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'right' - [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree right field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); - } - $config['right'] = $field; - } - // ancestor/parent - if ($this->reader->getPropertyAnnotation($property, self::PARENT)) { - $field = $property->getName(); - if (!$meta->isSingleValuedAssociation($field)) { - throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); - } - $config['parent'] = $field; - } - // root - if ($this->reader->getPropertyAnnotation($property, self::ROOT)) { - $field = $property->getName(); - if (!$meta->isSingleValuedAssociation($field)) { - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'root' - [{$field}] as mapped property in entity - {$meta->getName()}"); - } - - if (!$validator->isValidFieldForRoot($meta, $field)) { - throw new InvalidMappingException("Tree root field should be either a literal property ('integer' types or 'string') or a many-to-one association through root field - [{$field}] in class - {$meta->getName()}"); - } - } - $annotation = $this->reader->getPropertyAnnotation($property, self::ROOT); - $config['rootIdentifierMethod'] = $annotation->identifierMethod; - $config['root'] = $field; - } - // level - $levelAnnotation = $this->reader->getPropertyAnnotation($property, self::LEVEL); - if (null !== $levelAnnotation) { - $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'level' - [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if (!$validator->isValidField($meta, $field)) { - throw new InvalidMappingException("Tree level field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); - } - $config['level'] = $field; - $config['level_base'] = (int) $levelAnnotation->base; - } - // path - $pathAnnotation = $this->reader->getPropertyAnnotation($property, self::PATH); - if (null !== $pathAnnotation) { - $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'path' - [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if (!$validator->isValidFieldForPath($meta, $field)) { - throw new InvalidMappingException("Tree Path field - [{$field}] type is not valid. It must be string or text in class - {$meta->getName()}"); - } - if (strlen($pathAnnotation->separator) > 1) { - throw new InvalidMappingException("Tree Path field - [{$field}] Separator {$pathAnnotation->separator} is invalid. It must be only one character long."); - } - $config['path'] = $field; - $config['path_separator'] = $pathAnnotation->separator; - $config['path_append_id'] = $pathAnnotation->appendId; - $config['path_starts_with_separator'] = $pathAnnotation->startsWithSeparator; - $config['path_ends_with_separator'] = $pathAnnotation->endsWithSeparator; - } - // path source - if (null !== $this->reader->getPropertyAnnotation($property, self::PATH_SOURCE)) { - $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'path_source' - [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if (!$validator->isValidFieldForPathSource($meta, $field)) { - throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->getName()}"); - } - $config['path_source'] = $field; - } - - // path hash - if (null !== $this->reader->getPropertyAnnotation($property, self::PATH_HASH)) { - $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'path_hash' - [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if (!$validator->isValidFieldForPathHash($meta, $field)) { - throw new InvalidMappingException("Tree PathHash field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->getName()}"); - } - $config['path_hash'] = $field; - } - // lock time - - if (null !== $this->reader->getPropertyAnnotation($property, self::LOCK_TIME)) { - $field = $property->getName(); - if (!$meta->hasField($field)) { - throw new InvalidMappingException("Unable to find 'lock_time' - [{$field}] as mapped property in entity - {$meta->getName()}"); - } - if (!$validator->isValidFieldForLockTime($meta, $field)) { - throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->getName()}"); - } - $config['lock_time'] = $field; - } - } - - if (isset($config['activate_locking']) && $config['activate_locking'] && !isset($config['lock_time'])) { - throw new InvalidMappingException('You need to map a date field as the tree lock time field to activate locking support.'); - } - - if (!$meta->isMappedSuperclass && $config) { - if (isset($config['strategy'])) { - if (is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { - throw new InvalidMappingException("Tree does not support composite identifiers in class - {$meta->getName()}"); - } - $method = 'validate'.ucfirst($config['strategy']).'TreeMetadata'; - $validator->$method($meta, $config); - } else { - throw new InvalidMappingException("Cannot find Tree type for class: {$meta->getName()}"); - } - } - - return $config; - } } diff --git a/src/Tree/Mapping/Driver/Attribute.php b/src/Tree/Mapping/Driver/Attribute.php index 41d53b3ea4..e8547aac6a 100644 --- a/src/Tree/Mapping/Driver/Attribute.php +++ b/src/Tree/Mapping/Driver/Attribute.php @@ -9,20 +9,308 @@ namespace Gedmo\Tree\Mapping\Driver; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Exception\InvalidMappingException; +use Gedmo\Mapping\Annotation\Tree; +use Gedmo\Mapping\Annotation\TreeClosure; +use Gedmo\Mapping\Annotation\TreeLeft; +use Gedmo\Mapping\Annotation\TreeLevel; +use Gedmo\Mapping\Annotation\TreeLockTime; +use Gedmo\Mapping\Annotation\TreeParent; +use Gedmo\Mapping\Annotation\TreePath; +use Gedmo\Mapping\Annotation\TreePathHash; +use Gedmo\Mapping\Annotation\TreePathSource; +use Gedmo\Mapping\Annotation\TreeRight; +use Gedmo\Mapping\Annotation\TreeRoot; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Tree\Mapping\Validator; /** - * This is an attribute mapping driver for Tree - * behavioral extension. Used for extraction of extended - * metadata from attributes specifically for Tree - * extension. + * Mapping driver for the tree extension which reads extended metadata from attributes on class which is part of a tree. * + * @author Gediminas Morkevicius + * @author * @author Kevin Mian Kraiker * * @license MIT License (http://www.opensource.org/licenses/mit-license.php) * * @internal */ -final class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { + /** + * Mapping object to configure the type of tree. + */ + public const TREE = Tree::class; + + /** + * Mapping object to mark the field which will store the left value of a tree node. + */ + public const LEFT = TreeLeft::class; + + /** + * Mapping object to mark the field which will store the right value of a tree node. + */ + public const RIGHT = TreeRight::class; + + /** + * Mapping object to mark the field which will store the reference to the parent of a tree node. + */ + public const PARENT = TreeParent::class; + + /** + * Mapping object to mark the field which will store the level of a tree node. + */ + public const LEVEL = TreeLevel::class; + + /** + * Mapping object to mark the field which will store the reference to the root of a tree node. + */ + public const ROOT = TreeRoot::class; + + /** + * Mapping object to configure a closure tree object. + */ + public const CLOSURE = TreeClosure::class; + + /** + * Mapping object to configure a tree path field. + */ + public const PATH = TreePath::class; + + /** + * Mapping object to specify the source for a tree path. + */ + public const PATH_SOURCE = TreePathSource::class; + + /** + * Mapping object to configure the hash for a tree path. + */ + public const PATH_HASH = TreePathHash::class; + + /** + * Mapping object to configure the lock time for a tree. + */ + public const LOCK_TIME = TreeLockTime::class; + + /** + * List of tree strategies available + * + * @var string[] + */ + protected $strategies = [ + 'nested', + 'closure', + 'materializedPath', + ]; + + public function readExtendedMetadata($meta, array &$config) + { + $validator = new Validator(); + $class = $this->getMetaReflectionClass($meta); + + // class annotations + if ($annot = $this->reader->getClassAnnotation($class, self::TREE)) { + \assert($annot instanceof Tree); + + if (!in_array($annot->type, $this->strategies, true)) { + throw new InvalidMappingException("Tree type: {$annot->type} is not available."); + } + + $config['strategy'] = $annot->type; + $config['activate_locking'] = $annot->activateLocking; + $config['locking_timeout'] = (int) $annot->lockingTimeout; + + if ($config['locking_timeout'] < 1) { + throw new InvalidMappingException('Tree Locking Timeout must be at least of 1 second.'); + } + } + + if ($annot = $this->reader->getClassAnnotation($class, self::CLOSURE)) { + \assert($annot instanceof TreeClosure); + + if (!$cl = $this->getRelatedClassName($meta, $annot->class)) { + throw new InvalidMappingException("Tree closure class: {$annot->class} does not exist."); + } + + $config['closure'] = $cl; + } + + // property annotations + foreach ($class->getProperties() as $property) { + if ($meta->isMappedSuperclass && !$property->isPrivate() + || $meta->isInheritedField($property->name) + || isset($meta->associationMappings[$property->name]['inherited']) + ) { + continue; + } + + // left + if ($this->reader->getPropertyAnnotation($property, self::LEFT)) { + $field = $property->getName(); + + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find 'left' - [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$validator->isValidField($meta, $field)) { + throw new InvalidMappingException("Tree left field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); + } + + $config['left'] = $field; + } + + // right + if ($this->reader->getPropertyAnnotation($property, self::RIGHT)) { + $field = $property->getName(); + + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find 'right' - [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$validator->isValidField($meta, $field)) { + throw new InvalidMappingException("Tree right field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); + } + + $config['right'] = $field; + } + + // ancestor/parent + if ($this->reader->getPropertyAnnotation($property, self::PARENT)) { + $field = $property->getName(); + + if (!$meta->isSingleValuedAssociation($field)) { + throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); + } + + $config['parent'] = $field; + } + + // root + if ($annot = $this->reader->getPropertyAnnotation($property, self::ROOT)) { + \assert($annot instanceof TreeRoot); + + $field = $property->getName(); + + if (!$meta->isSingleValuedAssociation($field)) { + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find 'root' - [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$validator->isValidFieldForRoot($meta, $field)) { + throw new InvalidMappingException("Tree root field should be either a literal property ('integer' types or 'string') or a many-to-one association through root field - [{$field}] in class - {$meta->getName()}"); + } + } + + $config['rootIdentifierMethod'] = $annot->identifierMethod; + $config['root'] = $field; + } + + // level + if ($annot = $this->reader->getPropertyAnnotation($property, self::LEVEL)) { + \assert($annot instanceof TreeLevel); + + $field = $property->getName(); + + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find 'level' - [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$validator->isValidField($meta, $field)) { + throw new InvalidMappingException("Tree level field - [{$field}] type is not valid and must be 'integer' in class - {$meta->getName()}"); + } + + $config['level'] = $field; + $config['level_base'] = (int) $annot->base; + } + + // path + if ($annot = $this->reader->getPropertyAnnotation($property, self::PATH)) { + \assert($annot instanceof TreePath); + + $field = $property->getName(); + + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find 'path' - [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$validator->isValidFieldForPath($meta, $field)) { + throw new InvalidMappingException("Tree Path field - [{$field}] type is not valid. It must be string or text in class - {$meta->getName()}"); + } + + if (strlen($annot->separator) > 1) { + throw new InvalidMappingException("Tree Path field - [{$field}] Separator {$annot->separator} is invalid. It must be only one character long."); + } + + $config['path'] = $field; + $config['path_separator'] = $annot->separator; + $config['path_append_id'] = $annot->appendId; + $config['path_starts_with_separator'] = $annot->startsWithSeparator; + $config['path_ends_with_separator'] = $annot->endsWithSeparator; + } + + // path source + if (null !== $this->reader->getPropertyAnnotation($property, self::PATH_SOURCE)) { + $field = $property->getName(); + + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find 'path_source' - [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$validator->isValidFieldForPathSource($meta, $field)) { + throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->getName()}"); + } + + $config['path_source'] = $field; + } + + // path hash + if (null !== $this->reader->getPropertyAnnotation($property, self::PATH_HASH)) { + $field = $property->getName(); + + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find 'path_hash' - [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$validator->isValidFieldForPathHash($meta, $field)) { + throw new InvalidMappingException("Tree PathHash field - [{$field}] type is not valid. It can be any of the integer variants, double, float or string in class - {$meta->getName()}"); + } + + $config['path_hash'] = $field; + } + + // lock time + if (null !== $this->reader->getPropertyAnnotation($property, self::LOCK_TIME)) { + $field = $property->getName(); + + if (!$meta->hasField($field)) { + throw new InvalidMappingException("Unable to find 'lock_time' - [{$field}] as mapped property in entity - {$meta->getName()}"); + } + + if (!$validator->isValidFieldForLockTime($meta, $field)) { + throw new InvalidMappingException("Tree PathSource field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->getName()}"); + } + + $config['lock_time'] = $field; + } + } + + if (isset($config['activate_locking']) && $config['activate_locking'] && !isset($config['lock_time'])) { + throw new InvalidMappingException('You need to map a date field as the tree lock time field to activate locking support.'); + } + + if (!$meta->isMappedSuperclass && $config) { + if (isset($config['strategy'])) { + if (is_array($meta->getIdentifier()) && count($meta->getIdentifier()) > 1) { + throw new InvalidMappingException("Tree does not support composite identifiers in class - {$meta->getName()}"); + } + + $method = 'validate'.ucfirst($config['strategy']).'TreeMetadata'; + $validator->$method($meta, $config); + } else { + throw new InvalidMappingException("Cannot find Tree type for class: {$meta->getName()}"); + } + } + + return $config; + } } diff --git a/src/Uploadable/Mapping/Driver/Annotation.php b/src/Uploadable/Mapping/Driver/Annotation.php index f84573f252..2d9172a38b 100644 --- a/src/Uploadable/Mapping/Driver/Annotation.php +++ b/src/Uploadable/Mapping/Driver/Annotation.php @@ -9,19 +9,10 @@ namespace Gedmo\Uploadable\Mapping\Driver; -use Gedmo\Mapping\Annotation\Uploadable; -use Gedmo\Mapping\Annotation\UploadableFileMimeType; -use Gedmo\Mapping\Annotation\UploadableFileName; -use Gedmo\Mapping\Annotation\UploadableFilePath; -use Gedmo\Mapping\Annotation\UploadableFileSize; -use Gedmo\Mapping\Driver\AbstractAnnotationDriver; -use Gedmo\Uploadable\Mapping\Validator; +use Gedmo\Mapping\Driver\AnnotationDriverInterface; /** - * This is an annotation mapping driver for Uploadable - * behavioral extension. Used for extraction of extended - * metadata from Annotations specifically for Uploadable - * extension. + * Mapping driver for the uploaded extension which reads extended metadata from annotations on an uploadable class. * * @author Gustavo Falco * @author Gediminas Morkevicius @@ -30,87 +21,6 @@ * * @internal */ -class Annotation extends AbstractAnnotationDriver +class Annotation extends Attribute implements AnnotationDriverInterface { - /** - * Annotation to define that this object is loggable - */ - public const UPLOADABLE = Uploadable::class; - public const UPLOADABLE_FILE_MIME_TYPE = UploadableFileMimeType::class; - public const UPLOADABLE_FILE_NAME = UploadableFileName::class; - public const UPLOADABLE_FILE_PATH = UploadableFilePath::class; - public const UPLOADABLE_FILE_SIZE = UploadableFileSize::class; - - public function readExtendedMetadata($meta, array &$config) - { - $class = $this->getMetaReflectionClass($meta); - - // class annotations - if ($annot = $this->reader->getClassAnnotation($class, self::UPLOADABLE)) { - $config['uploadable'] = true; - $config['allowOverwrite'] = $annot->allowOverwrite; - $config['appendNumber'] = $annot->appendNumber; - $config['path'] = $annot->path; - $config['pathMethod'] = $annot->pathMethod; - $config['fileMimeTypeField'] = false; - $config['fileNameField'] = false; - $config['filePathField'] = false; - $config['fileSizeField'] = false; - $config['callback'] = $annot->callback; - $config['filenameGenerator'] = $annot->filenameGenerator; - $config['maxSize'] = (float) $annot->maxSize; - $config['allowedTypes'] = $annot->allowedTypes; - $config['disallowedTypes'] = $annot->disallowedTypes; - - foreach ($class->getProperties() as $prop) { - if ($this->reader->getPropertyAnnotation($prop, self::UPLOADABLE_FILE_MIME_TYPE)) { - $config['fileMimeTypeField'] = $prop->getName(); - } - - if ($this->reader->getPropertyAnnotation($prop, self::UPLOADABLE_FILE_NAME)) { - $config['fileNameField'] = $prop->getName(); - } - - if ($this->reader->getPropertyAnnotation($prop, self::UPLOADABLE_FILE_PATH)) { - $config['filePathField'] = $prop->getName(); - } - - if ($this->reader->getPropertyAnnotation($prop, self::UPLOADABLE_FILE_SIZE)) { - $config['fileSizeField'] = $prop->getName(); - } - } - - $config = Validator::validateConfiguration($meta, $config); - } - - /* - // Code in case we need to identify entities which are not Uploadables, but have associations - // with other Uploadable entities - - } else { - // We need to check if this class has a relation with Uploadable entities - $associations = $meta->getAssociationMappings(); - - foreach ($associations as $field => $association) { - $refl = new \ReflectionClass($association['targetEntity']); - - if ($annot = $this->reader->getClassAnnotation($refl, self::UPLOADABLE)) { - $config['hasUploadables'] = true; - - if (!isset($config['uploadables'])) { - $config['uploadables'] = array(); - } - - $config['uploadables'][] = array( - 'class' => $association['targetEntity'], - 'property' => $association['fieldName'] - ); - } - } - }*/ - - $this->validateFullMetadata($meta, $config); - - return $config; - } } diff --git a/src/Uploadable/Mapping/Driver/Attribute.php b/src/Uploadable/Mapping/Driver/Attribute.php index ce26c4ae92..e8f12f87e0 100644 --- a/src/Uploadable/Mapping/Driver/Attribute.php +++ b/src/Uploadable/Mapping/Driver/Attribute.php @@ -10,16 +10,122 @@ namespace Gedmo\Uploadable\Mapping\Driver; use Gedmo\Mapping\Annotation\Uploadable; -use Gedmo\Mapping\Driver\AttributeDriverInterface; +use Gedmo\Mapping\Annotation\UploadableFileMimeType; +use Gedmo\Mapping\Annotation\UploadableFileName; +use Gedmo\Mapping\Annotation\UploadableFilePath; +use Gedmo\Mapping\Annotation\UploadableFileSize; +use Gedmo\Mapping\Driver\AbstractAnnotationDriver; +use Gedmo\Uploadable\Mapping\Validator; /** - * This is an attribute mapping driver for Uploadable - * behavioral extension. Used for extraction of extended - * metadata from attribute specifically for Uploadable - * extension. + * Mapping driver for the uploaded extension which reads extended metadata from attributes on an uploadable class. + * + * @author Gustavo Falco + * @author Gediminas Morkevicius * * @internal */ -class Attribute extends Annotation implements AttributeDriverInterface +class Attribute extends AbstractAnnotationDriver { + /** + * Mapping object for the uploadable extension. + */ + public const UPLOADABLE = Uploadable::class; + + /** + * Mapping object to mark the field which will store the MIME type for an upload. + */ + public const UPLOADABLE_FILE_MIME_TYPE = UploadableFileMimeType::class; + + /** + * Mapping object to mark the field which will store the file name for an upload. + */ + public const UPLOADABLE_FILE_NAME = UploadableFileName::class; + + /** + * Mapping object to mark the field which will store the filesystem path for an upload. + */ + public const UPLOADABLE_FILE_PATH = UploadableFilePath::class; + + /** + * Mapping object to mark the field which will store the file size for an upload. + */ + public const UPLOADABLE_FILE_SIZE = UploadableFileSize::class; + + public function readExtendedMetadata($meta, array &$config) + { + $class = $this->getMetaReflectionClass($meta); + + // class annotations + if ($annot = $this->reader->getClassAnnotation($class, self::UPLOADABLE)) { + \assert($annot instanceof Uploadable); + + $config['uploadable'] = true; + $config['allowOverwrite'] = $annot->allowOverwrite; + $config['appendNumber'] = $annot->appendNumber; + $config['path'] = $annot->path; + $config['pathMethod'] = $annot->pathMethod; + $config['fileMimeTypeField'] = false; + $config['fileNameField'] = false; + $config['filePathField'] = false; + $config['fileSizeField'] = false; + $config['callback'] = $annot->callback; + $config['filenameGenerator'] = $annot->filenameGenerator; + $config['maxSize'] = (float) $annot->maxSize; + $config['allowedTypes'] = $annot->allowedTypes; + $config['disallowedTypes'] = $annot->disallowedTypes; + + foreach ($class->getProperties() as $prop) { + if ($this->reader->getPropertyAnnotation($prop, self::UPLOADABLE_FILE_MIME_TYPE)) { + $config['fileMimeTypeField'] = $prop->getName(); + } + + if ($this->reader->getPropertyAnnotation($prop, self::UPLOADABLE_FILE_NAME)) { + $config['fileNameField'] = $prop->getName(); + } + + if ($this->reader->getPropertyAnnotation($prop, self::UPLOADABLE_FILE_PATH)) { + $config['filePathField'] = $prop->getName(); + } + + if ($this->reader->getPropertyAnnotation($prop, self::UPLOADABLE_FILE_SIZE)) { + $config['fileSizeField'] = $prop->getName(); + } + } + + $config = Validator::validateConfiguration($meta, $config); + } + + /* + // Code in case we need to identify entities which are not Uploadables, but have associations + // with other Uploadable entities + + } else { + // We need to check if this class has a relation with Uploadable entities + $associations = $meta->getAssociationMappings(); + + foreach ($associations as $field => $association) { + $refl = new \ReflectionClass($association['targetEntity']); + + if ($annot = $this->reader->getClassAnnotation($refl, self::UPLOADABLE)) { + \assert($annot instanceof Uploadable); + + $config['hasUploadables'] = true; + + if (!isset($config['uploadables'])) { + $config['uploadables'] = array(); + } + + $config['uploadables'][] = array( + 'class' => $association['targetEntity'], + 'property' => $association['fieldName'] + ); + } + } + }*/ + + $this->validateFullMetadata($meta, $config); + + return $config; + } }