ヤミRoot VoidGate
User / IP
:
216.73.216.143
Host / Server
:
146.88.233.70 / dev.loger.cm
System
:
Linux hybrid1120.fr.ns.planethoster.net 3.10.0-957.21.2.el7.x86_64 #1 SMP Wed Jun 5 14:26:44 UTC 2019 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
logercm
/
dev.loger.cm
/
fixtures
/
assert
/
Viewing: gedmo.tar
doctrine-extensions/schemas/orm/doctrine-extensions-mapping-2-1.xsd 0000644 00000015641 15117737235 0021464 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping" xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping" elementFormDefault="qualified"> <xs:annotation> <xs:documentation><![CDATA[ This is the XML Schema for the object/relational mapping file used by the Doctrine Extensions by Gedmo extensions Doctrine component version support: 2.1.x ]]></xs:documentation> </xs:annotation> <!-- It would be nice if we could force the gedmo with only necessary elements into each of doctrine elements. Patches that do that are more than welcome. Please note, that marking e.g filed element in xml document with xsi:type is not an option as we need to allow other people to push their own additional attributes/elements into the same field element and they should not extend our schema --> <!-- <xs:complexType name="entity-extension"> <xs:sequence> <xs:element name="translation" type="gedmo:translation" minOccurs="0" maxOccurs="1" /> <xs:element name="tree" type="gedmo:tree" minOccurs="0" maxOccurs="1" /> <xs:element name="tree-closure" type="gedmo:tree-closure" minOccurs="0" maxOccurs="1" /> <xs:element name="loggable" type="gedmo:loggable" minOccurs="0" maxOccurs="1" /> </xs:sequence> </xs:complexType> <xs:complexType name="field-extension"> <xs:sequence> <xs:element name="sluggable" type="gedmo:sluggable" minOccurs="0" maxOccurs="unbounded"/> <xs:element name="slug" type="gedmo:slug" minOccurs="0" maxOccurs="1"/> <xs:element name="translatable" type="gedmo:emptyType" minOccurs="0" maxOccurs="unbounded"/> <xs:element name="timestampable" type="gedmo:timestampable" minOccurs="0" maxOccurs="unbounded"/> <xs:element name="versioned" type="gedmo:emptyType" minOccurs="0" maxOccurs="unbounded"/> <xs:element name="tree-left" type="gedmo:emptyType" minOccurs="0" maxOccurs="1"/> <xs:element name="tree-right" type="gedmo:emptyType" minOccurs="0" maxOccurs="1"/> <xs:element name="tree-level" type="gedmo:emptyType" minOccurs="0" maxOccurs="1"/> <xs:element name="tree-root" type="gedmo:emptyType" minOccurs="0" maxOccurs="1"/> </xs:sequence> </xs:complexType> <xs:complexType name="many-to-one-extension"> <xs:sequence> <xs:element name="versioned" type="gedmo:emptyType" minOccurs="0" maxOccurs="1"/> <xs:element name="tree-parent" type="gedmo:emptyType" minOccurs="0" maxOccurs="1"/> </xs:sequence> </xs:complexType> <xs:complexType name="one-to-one-extension"> <xs:sequence> <xs:element name="versioned" type="gedmo:emptyType" minOccurs="0" maxOccurs="1"/> <xs:element name="tree-parent" type="gedmo:emptyType" minOccurs="0" maxOccurs="1"/> </xs:sequence> </xs:complexType> --> <!-- because of the above we have for now a root element gedmo with all choices --> <!-- entity --> <xs:element name="translation" type="gedmo:translation"/> <xs:element name="tree" type="gedmo:tree"/> <xs:element name="tree-closure" type="gedmo:tree-closure"/> <xs:element name="loggable" type="gedmo:loggable"/> <!-- field --> <xs:element name="sluggable" type="gedmo:sluggable"/> <xs:element name="slug" type="gedmo:slug"/> <xs:element name="translatable" type="gedmo:translatable"/> <xs:element name="timestampable" type="gedmo:timestampable"/> <xs:element name="blameable" type="gedmo:blameable"/> <xs:element name="ip-traceable" type="gedmo:ip-traceable"/> <xs:element name="versioned" type="gedmo:emptyType"/> <xs:element name="tree-left" type="gedmo:emptyType"/> <xs:element name="tree-right" type="gedmo:emptyType"/> <xs:element name="tree-level" type="gedmo:emptyType"/> <xs:element name="tree-root" type="gedmo:emptyType"/> <!-- many-to-one --> <!-- xs:element name="versioned" type="gedmo:emptyType"/--> <xs:element name="tree-parent" type="gedmo:emptyType"/> <!-- one-to-one --> <!-- same as many-to-one <xs:element name="versioned" type="gedmo:emptyType"/> <xs:element name="tree-parent" type="gedmo:emptyType"/> --> <xs:complexType name="translation"> <xs:attribute name="entity" type="xs:string" use="optional" /> <xs:attribute name="locale" type="xs:string" use="optional" /> <xs:attribute name="language" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="tree"> <xs:attribute name="type" type="gedmo:tree-type" default="nested" /> </xs:complexType> <xs:complexType name="tree-closure"> <xs:attribute name="class" type="xs:string" use="required" /> </xs:complexType> <xs:complexType name="loggable"> <xs:attribute name="log-entry-class" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="sluggable"> <xs:attribute name="position" type="xs:integer" use="optional" /> <xs:attribute name="slugField" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="slug"> <xs:attribute name="unique" type="xs:boolean" use="optional" /> <xs:attribute name="updatable" type="xs:boolean" use="optional" /> <xs:attribute name="separator" type="xs:string" use="optional" /> <xs:attribute name="style" type="gedmo:slug-style" use="optional" /> </xs:complexType> <xs:complexType name="timestampable"> <xs:attribute name="on" type="gedmo:timestampable-action" use="optional" /> <xs:attribute name="field" type="xs:string" use="optional" /> <xs:attribute name="value" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="blameable"> <xs:attribute name="on" type="gedmo:timestampable-action" use="optional" /> <xs:attribute name="field" type="xs:string" use="optional" /> <xs:attribute name="value" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="ip-traceable"> <xs:attribute name="on" type="gedmo:timestampable-action" use="optional" /> <xs:attribute name="field" type="xs:string" use="optional" /> <xs:attribute name="value" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="translatable"> <xs:attribute name="fallback" type="xs:boolean" use="optional" /> </xs:complexType> <xs:complexType name="emptyType"> </xs:complexType> <xs:simpleType name="tree-type"> <xs:restriction base="xs:token"> <xs:enumeration value="nested"/> <xs:enumeration value="closure"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="slug-style"> <xs:restriction base="xs:token"> <xs:enumeration value="default"/> <xs:enumeration value="camel"/> <xs:enumeration value="upper"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="timestampable-action"> <xs:restriction base="xs:token"> <xs:enumeration value="create"/> <xs:enumeration value="update"/> <xs:enumeration value="change"/> </xs:restriction> </xs:simpleType> </xs:schema> doctrine-extensions/schemas/orm/doctrine-extensions-mapping-2-2.xsd 0000644 00000017704 15117737235 0021467 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping" xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping" elementFormDefault="qualified"> <xs:annotation> <xs:documentation><![CDATA[ This is the XML Schema for the object/relational mapping file used by the Doctrine Extensions by Gedmo extensions Doctrine component version support: 2.2.x ]]></xs:documentation> </xs:annotation> <!-- It would be nice if we could force the gedmo with only necessary elements into each of doctrine elements. Patches that do that are more than welcome. Please note, that marking e.g filed element in xml document with xsi:type is not an option as we need to allow other people to push their own additional attributes/elements into the same field element and they should not extend our schema --> <!-- entity --> <xs:element name="translation" type="gedmo:translation"/> <xs:element name="tree" type="gedmo:tree"/> <xs:element name="tree-closure" type="gedmo:tree-closure"/> <xs:element name="tree-path" type="gedmo:tree-path"/> <xs:element name="loggable" type="gedmo:loggable"/> <xs:element name="soft-deleteable" type="gedmo:soft-deleteable"/> <xs:element name="uploadable" type="gedmo:uploadable"/> <!-- field --> <xs:element name="slug" type="gedmo:slug"/> <xs:element name="translatable" type="gedmo:translatable"/> <xs:element name="timestampable" type="gedmo:timestampable"/> <xs:element name="blameable" type="gedmo:blameable"/> <xs:element name="ip-traceable" type="gedmo:ip-traceable"/> <xs:element name="versioned" type="gedmo:emptyType"/> <xs:element name="tree-left" type="gedmo:emptyType"/> <xs:element name="tree-right" type="gedmo:emptyType"/> <xs:element name="tree-level" type="gedmo:emptyType"/> <xs:element name="tree-root" type="gedmo:emptyType"/> <xs:element name="tree-parent" type="gedmo:emptyType"/> <xs:element name="tree-path-source" type="gedmo:emptyType"/> <xs:element name="tree-lock-time" type="gedmo:emptyType"/> <xs:element name="tree-path-hash" type="gedmo:emptyType"/> <xs:element name="sortable-group" type="gedmo:emptyType"/> <xs:element name="sortable-position" type="gedmo:emptyType"/> <xs:element name="uploadable-file-mime-type" type="gedmo:emptyType"/> <xs:element name="uploadable-file-path" type="gedmo:emptyType"/> <xs:element name="uploadable-file-size" type="gedmo:emptyType"/> <xs:complexType name="translation"> <xs:attribute name="entity" type="xs:string" use="optional" /> <xs:attribute name="locale" type="xs:string" use="optional" /> <xs:attribute name="language" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="tree"> <xs:attribute name="type" type="gedmo:tree-type" default="nested" /> <xs:attribute name="activate-locking" type="xs:boolean" default="false" /> <xs:attribute name="locking-timeout" type="xs:integer" default="3" /> </xs:complexType> <xs:complexType name="reference"> <xs:attribute name="type" type="gedmo:reference-type" use="required" /> <xs:attribute name="field" type="xs:string" use="required" /> <xs:attribute name="identifier" type="xs:string" use="required" /> <xs:attribute name="class" type="xs:string" use="required" /> <xs:attribute name="mappedBy" type="xs:string" use="optional" /> <xs:attribute name="inversedBy" type="xs:string" use="optional" /> </xs:complexType> <xs:simpleType name="reference-type"> <xs:restriction base="xs:token"> <xs:enumeration value="document"/> <xs:enumeration value="entity"/> </xs:restriction> </xs:simpleType> <xs:complexType name="tree-closure"> <xs:attribute name="class" type="xs:string" use="required" /> </xs:complexType> <xs:complexType name="tree-path"> <xs:attribute name="separator" type="xs:string" use="optional" default="|" /> </xs:complexType> <xs:complexType name="loggable"> <xs:attribute name="log-entry-class" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="slug"> <xs:sequence> <xs:element name="handler" type="gedmo:handler" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> <xs:attribute name="fields" type="xs:string" use="required"/> <xs:attribute name="unique" type="xs:boolean" use="optional" /> <xs:attribute name="unique-base" type="xs:string" use="optional" /> <xs:attribute name="updatable" type="xs:boolean" use="optional" /> <xs:attribute name="separator" type="xs:string" use="optional" /> <xs:attribute name="style" type="gedmo:slug-style" use="optional" /> <xs:attribute name="prefix" type="xs:string" use="optional" /> <xs:attribute name="suffix" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="soft-deleteable"> <xs:attribute name="field-name" type="xs:string" use="required" /> <xs:attribute name="hard-delete" type="xs:boolean" use="optional" /> <xs:attribute name="time-aware" type="xs:boolean" use="optional" /> </xs:complexType> <xs:complexType name="handler"> <xs:sequence> <xs:element name="handler-option" type="gedmo:handler-option" minOccurs="0" maxOccurs="unbounded"/> </xs:sequence> <xs:attribute name="class" type="xs:string" use="required"/> </xs:complexType> <xs:complexType name="handler-option"> <xs:attribute name="name" type="xs:string" use="required"/> <xs:attribute name="value" type="xs:string" use="required"/> </xs:complexType> <xs:complexType name="timestampable"> <xs:attribute name="on" type="gedmo:timestampable-action" use="optional" /> <xs:attribute name="field" type="xs:string" use="optional" /> <xs:attribute name="value" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="blameable"> <xs:attribute name="on" type="gedmo:timestampable-action" use="optional" /> <xs:attribute name="field" type="xs:string" use="optional" /> <xs:attribute name="value" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="ip-traceable"> <xs:attribute name="on" type="gedmo:timestampable-action" use="optional" /> <xs:attribute name="field" type="xs:string" use="optional" /> <xs:attribute name="value" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="translatable"> <xs:attribute name="fallback" type="xs:boolean" use="optional" /> </xs:complexType> <xs:complexType name="emptyType"> </xs:complexType> <xs:simpleType name="tree-type"> <xs:restriction base="xs:token"> <xs:enumeration value="nested"/> <xs:enumeration value="closure"/> <xs:enumeration value="materializedPath"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="slug-style"> <xs:restriction base="xs:token"> <xs:enumeration value="default"/> <xs:enumeration value="camel"/> <xs:enumeration value="upper"/> </xs:restriction> </xs:simpleType> <xs:simpleType name="timestampable-action"> <xs:restriction base="xs:token"> <xs:enumeration value="create"/> <xs:enumeration value="update"/> <xs:enumeration value="change"/> </xs:restriction> </xs:simpleType> <xs:complexType name="uploadable"> <xs:attribute name="allow-overwrite" type="xs:boolean" use="optional" /> <xs:attribute name="append-number" type="xs:boolean" use="optional" /> <xs:attribute name="callback" type="xs:string" use="optional" /> <xs:attribute name="path" type="xs:string" use="optional" /> <xs:attribute name="path-method" type="xs:string" use="optional" /> <xs:attribute name="filename-generator" type="xs:string" use="optional" /> <xs:attribute name="max-size" type="xs:double" use="optional" default="0" /> <xs:attribute name="allowed-types" type="xs:string" use="optional" /> <xs:attribute name="disallowed-types" type="xs:string" use="optional" /> </xs:complexType> </xs:schema> doctrine-extensions/src/Blameable/Mapping/Driver/Annotation.php 0000644 00000007306 15117737235 0020675 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\Blameable; use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** * 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 <mail@davidbu.ch> * * @internal */ class Annotation extends AbstractAnnotationDriver { /** * 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; } } } } doctrine-extensions/src/Blameable/Mapping/Driver/Attribute.php 0000644 00000001245 15117737235 0020522 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable\Mapping\Driver; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for Blameable * behavioral extension. Used for extraction of extended * metadata from attribute specifically for Blameable * extension. * * @internal */ final class Attribute extends Annotation implements AttributeDriverInterface { } doctrine-extensions/src/Blameable/Mapping/Driver/Xml.php 0000644 00000012574 15117737235 0017326 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for Blameable * behavioral extension. Used for extraction of extended * metadata from xml specifically for Blameable * extension. * * @author David Buchmann <mail@davidbu.ch> * * @internal */ class Xml extends BaseXml { /** * List of types which are valid for blame * * @var string[] */ private const VALID_TYPES = [ 'one', 'string', 'int', ]; public function readExtendedMetadata($meta, array &$config) { /** * @var \SimpleXmlElement */ $mapping = $this->_getMapping($meta->getName()); if (isset($mapping->field)) { /** * @var \SimpleXmlElement */ foreach ($mapping->field as $fieldMapping) { $fieldMappingDoctrine = $fieldMapping; $fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($fieldMapping->blameable)) { /** * @var \SimpleXmlElement */ $data = $fieldMapping->blameable; $field = $this->_getAttribute($fieldMappingDoctrine, 'name'); if (!$this->isValidField($meta, $field)) { throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a reference in class - {$meta->getName()}"); } if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, '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' === $this->_getAttribute($data, 'on')) { if (!$this->_isAttributeSet($data, 'field')) { throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $this->_getAttribute($data, 'field'); $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value') : null; $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, ]; } $config[$this->_getAttribute($data, 'on')][] = $field; } } } if (isset($mapping->{'many-to-one'})) { foreach ($mapping->{'many-to-one'} as $fieldMapping) { $field = $this->_getAttribute($fieldMapping, 'field'); $fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($fieldMapping->blameable)) { $data = $fieldMapping->blameable; 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 (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, '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' === $this->_getAttribute($data, 'on')) { if (!$this->_isAttributeSet($data, 'field')) { throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $this->_getAttribute($data, 'field'); $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value') : null; $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, ]; } $config[$this->_getAttribute($data, 'on')][] = $field; } } } } /** * Checks if $field type is valid * * @param ClassMetadata $meta * @param string $field * * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], self::VALID_TYPES, true); } } doctrine-extensions/src/Blameable/Mapping/Driver/Yaml.php 0000644 00000013035 15117737235 0017461 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for Blameable * behavioral extension. Used for extraction of extended * metadata from yaml specifically for Blameable * extension. * * @author David Buchmann <mail@davidbu.ch> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * List of types which are valid for blameable * * @var string[] */ private const VALID_TYPES = [ 'one', 'string', 'int', ]; /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo']['blameable'])) { $mappingProperty = $fieldMapping['gedmo']['blameable']; if (!$this->isValidField($meta, $field)) { throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a reference in class - {$meta->getName()}"); } if (!isset($mappingProperty['on']) || !in_array($mappingProperty['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' === $mappingProperty['on']) { if (!isset($mappingProperty['field'])) { throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $mappingProperty['field']; $valueAttribute = $mappingProperty['value'] ?? null; if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { throw new InvalidMappingException('Blameable extension does not support multiple value changeset detection yet.'); } $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, ]; } $config[$mappingProperty['on']][] = $field; } } } if (isset($mapping['manyToOne'])) { foreach ($mapping['manyToOne'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo']['blameable'])) { $mappingProperty = $fieldMapping['gedmo']['blameable']; 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 (!isset($mappingProperty['on']) || !in_array($mappingProperty['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' === $mappingProperty['on']) { if (!isset($mappingProperty['field'])) { throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $mappingProperty['field']; $valueAttribute = $mappingProperty['value'] ?? null; if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { throw new InvalidMappingException('Blameable extension does not support multiple value changeset detection yet.'); } $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, ]; } $config[$mappingProperty['on']][] = $field; } } } } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } /** * Checks if $field type is valid * * @param ClassMetadata $meta * @param string $field * * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], self::VALID_TYPES, true); } } doctrine-extensions/src/Blameable/Mapping/Event/Adapter/ODM.php 0000644 00000001214 15117737236 0020401 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable\Mapping\Event\Adapter; use Gedmo\Blameable\Mapping\Event\BlameableAdapter; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; /** * Doctrine event adapter for ODM adapted * for Blameable behavior. * * @author David Buchmann <mail@davidbu.ch> */ final class ODM extends BaseAdapterODM implements BlameableAdapter { } doctrine-extensions/src/Blameable/Mapping/Event/Adapter/ORM.php 0000644 00000001214 15117737236 0020417 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable\Mapping\Event\Adapter; use Gedmo\Blameable\Mapping\Event\BlameableAdapter; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; /** * Doctrine event adapter for ORM adapted * for Blameable behavior. * * @author David Buchmann <mail@davidbu.ch> */ final class ORM extends BaseAdapterORM implements BlameableAdapter { } doctrine-extensions/src/Blameable/Mapping/Event/BlameableAdapter.php 0000644 00000001046 15117737236 0021552 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable\Mapping\Event; use Gedmo\Mapping\Event\AdapterInterface; /** * Doctrine event adapter for the Blameable extension. * * @author David Buchmann <mail@davidbu.ch> */ interface BlameableAdapter extends AdapterInterface { } doctrine-extensions/src/Blameable/Traits/Blameable.php 0000644 00000002457 15117737236 0017052 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable\Traits; /** * Blameable Trait, usable with PHP >= 5.4 * * @author David Buchmann <mail@davidbu.ch> */ trait Blameable { /** * @var string */ private $createdBy; /** * @var string */ private $updatedBy; /** * Sets createdBy. * * @param string $createdBy * * @return $this */ public function setCreatedBy($createdBy) { $this->createdBy = $createdBy; return $this; } /** * Returns createdBy. * * @return string */ public function getCreatedBy() { return $this->createdBy; } /** * Sets updatedBy. * * @param string $updatedBy * * @return $this */ public function setUpdatedBy($updatedBy) { $this->updatedBy = $updatedBy; return $this; } /** * Returns updatedBy. * * @return string */ public function getUpdatedBy() { return $this->updatedBy; } } doctrine-extensions/src/Blameable/Traits/BlameableDocument.php 0000644 00000003335 15117737236 0020545 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable\Traits; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** * Blameable Trait, usable with PHP >= 5.4 * * @author David Buchmann <mail@davidbu.ch> */ trait BlameableDocument { /** * @var string * @Gedmo\Blameable(on="create") * @ODM\Field(type="string") */ #[ODM\Field(type: Type::STRING)] #[Gedmo\Blameable(on: 'create')] protected $createdBy; /** * @var string * @Gedmo\Blameable(on="update") * @ODM\Field(type="string") */ #[ODM\Field(type: Type::STRING)] #[Gedmo\Blameable(on: 'update')] protected $updatedBy; /** * Sets createdBy. * * @param string $createdBy * * @return $this */ public function setCreatedBy($createdBy) { $this->createdBy = $createdBy; return $this; } /** * Returns createdBy. * * @return string */ public function getCreatedBy() { return $this->createdBy; } /** * Sets updatedBy. * * @param string $updatedBy * * @return $this */ public function setUpdatedBy($updatedBy) { $this->updatedBy = $updatedBy; return $this; } /** * Returns updatedBy. * * @return string */ public function getUpdatedBy() { return $this->updatedBy; } } doctrine-extensions/src/Blameable/Traits/BlameableEntity.php 0000644 00000003236 15117737236 0020243 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable\Traits; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** * Blameable Trait, usable with PHP >= 5.4 * * @author David Buchmann <mail@davidbu.ch> */ trait BlameableEntity { /** * @var string * @Gedmo\Blameable(on="create") * @ORM\Column(nullable=true) */ #[ORM\Column(nullable: true)] #[Gedmo\Blameable(on: 'create')] protected $createdBy; /** * @var string * @Gedmo\Blameable(on="update") * @ORM\Column(nullable=true) */ #[ORM\Column(nullable: true)] #[Gedmo\Blameable(on: 'update')] protected $updatedBy; /** * Sets createdBy. * * @param string $createdBy * * @return $this */ public function setCreatedBy($createdBy) { $this->createdBy = $createdBy; return $this; } /** * Returns createdBy. * * @return string */ public function getCreatedBy() { return $this->createdBy; } /** * Sets updatedBy. * * @param string $updatedBy * * @return $this */ public function setUpdatedBy($updatedBy) { $this->updatedBy = $updatedBy; return $this; } /** * Returns updatedBy. * * @return string */ public function getUpdatedBy() { return $this->updatedBy; } } doctrine-extensions/src/Blameable/Blameable.php 0000644 00000002632 15117737236 0015577 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable; /** * This interface is not necessary but can be implemented for * Entities which in some cases needs to be identified as * Blameable * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface Blameable { // blameable expects annotations on properties /* * @gedmo:Blameable(on="create") * fields which should be updated on insert only */ /* * @gedmo:Blameable(on="update") * fields which should be updated on update and insert */ /* * @gedmo:Blameable(on="change", field="field", value="value") * fields which should be updated on changed "property" * value and become equal to given "value" */ /* * @gedmo:Blameable(on="change", field="field") * fields which should be updated on changed "property" */ /* * @gedmo:Blameable(on="change", fields={"field1", "field2"}) * fields which should be updated if at least one of the given fields changed */ /* * example * * @gedmo:Blameable(on="create") * @Column(type="string") * $created */ } doctrine-extensions/src/Blameable/BlameableListener.php 0000644 00000004401 15117737236 0017301 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Blameable; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\AbstractTrackingListener; use Gedmo\Exception\InvalidArgumentException; /** * The Blameable listener handles the update of * dates on creation and update. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class BlameableListener extends AbstractTrackingListener { /** * @var mixed */ protected $user; /** * Get the user value to set on a blameable field * * @param ClassMetadata $meta * @param string $field * * @return mixed */ public function getFieldValue($meta, $field, $eventAdapter) { if ($meta->hasAssociation($field)) { if (null !== $this->user && !is_object($this->user)) { throw new InvalidArgumentException('Blame is reference, user must be an object'); } return $this->user; } // ok so it's not an association, then it is a string, or an object if (is_object($this->user)) { if (method_exists($this->user, 'getUserIdentifier')) { return (string) $this->user->getUserIdentifier(); } if (method_exists($this->user, 'getUsername')) { return (string) $this->user->getUsername(); } if (method_exists($this->user, '__toString')) { return $this->user->__toString(); } throw new InvalidArgumentException('Field expects string, user must be a string, or object should have method getUserIdentifier, getUsername or __toString'); } return $this->user; } /** * Set a user value to return * * @param mixed $user * * @return void */ public function setUserValue($user) { $this->user = $user; } protected function getNamespace() { return __NAMESPACE__; } } doctrine-extensions/src/Exception/BadMethodCallException.php 0000644 00000001115 15117737236 0020322 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * BadMethodCallException * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class BadMethodCallException extends \BadMethodCallException implements Exception { } doctrine-extensions/src/Exception/FeatureNotImplementedException.php 0000644 00000001216 15117737236 0022141 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * FeatureNotImplementedException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class FeatureNotImplementedException extends \RuntimeException implements Exception { } doctrine-extensions/src/Exception/InvalidArgumentException.php 0000644 00000001041 15117737236 0020766 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * InvalidArgumentException * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class InvalidArgumentException extends \InvalidArgumentException implements Exception { } doctrine-extensions/src/Exception/InvalidMappingException.php 0000644 00000001232 15117737236 0020601 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * InvalidMappingException * * Triggered when mapping user argument is not * valid or incomplete. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class InvalidMappingException extends InvalidArgumentException implements Exception { } doctrine-extensions/src/Exception/ReferenceIntegrityStrictException.php 0000644 00000001052 15117737236 0022665 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; /** * ReferenceIntegrityStrictException * * @author Evert Harmeling <evert.harmeling@freshheads.com> * * @final since gedmo/doctrine-extensions 3.11 */ class ReferenceIntegrityStrictException extends RuntimeException { } doctrine-extensions/src/Exception/RuntimeException.php 0000644 00000001011 15117737236 0017315 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * RuntimeException * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class RuntimeException extends \RuntimeException implements Exception { } doctrine-extensions/src/Exception/TreeLockingException.php 0000644 00000001116 15117737236 0020106 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; /** * TreeLockingException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class TreeLockingException extends RuntimeException { } doctrine-extensions/src/Exception/UnexpectedValueException.php 0000644 00000001123 15117737236 0020777 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UnexpectedValueException * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UnexpectedValueException extends \UnexpectedValueException implements Exception { } doctrine-extensions/src/Exception/UnsupportedObjectManagerException.php 0000644 00000001133 15117737236 0022651 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UnsupportedObjectManager * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UnsupportedObjectManagerException extends InvalidArgumentException implements Exception { } doctrine-extensions/src/Exception/UploadableCantWriteException.php 0000644 00000001214 15117737236 0021570 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableCantWriteException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableCantWriteException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableCouldntGuessMimeTypeException.php 0000644 00000001242 15117737236 0023762 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableCouldntGuessMimeTypeException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableCouldntGuessMimeTypeException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableDirectoryNotFoundException.php 0000644 00000001234 15117737236 0023313 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableDirectoryNotFoundException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableDirectoryNotFoundException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableException.php 0000644 00000001105 15117737236 0017746 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class UploadableException extends RuntimeException implements Exception { } doctrine-extensions/src/Exception/UploadableExtensionException.php 0000644 00000001214 15117737236 0021644 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableExtensionException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableExtensionException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableFileAlreadyExistsException.php 0000644 00000001234 15117737236 0023253 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableFileAlreadyExistsException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableFileAlreadyExistsException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableFileNotReadableException.php 0000644 00000001230 15117737236 0022646 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableFileNotReadableException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableFileNotReadableException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableFormSizeException.php 0000644 00000001212 15117737236 0021424 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableFormSizeException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableFormSizeException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableIniSizeException.php 0000644 00000001210 15117737236 0021236 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableIniSizeException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableIniSizeException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableInvalidFileException.php 0000644 00000001220 15117737236 0022053 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableInvalidFileException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableInvalidFileException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableInvalidMimeTypeException.php 0000644 00000001230 15117737236 0022726 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableInvalidMimeTypeException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableInvalidMimeTypeException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableInvalidPathException.php 0000644 00000001220 15117737236 0022070 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableInvalidPathException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableInvalidPathException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableMaxSizeException.php 0000644 00000001210 15117737236 0021244 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableMaxSizeException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableMaxSizeException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableNoFileException.php 0000644 00000001206 15117737236 0021045 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableNoFileException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableNoFileException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableNoPathDefinedException.php 0000644 00000001224 15117737236 0022341 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableNoPathDefinedException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableNoPathDefinedException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableNoTmpDirException.php 0000644 00000001212 15117737236 0021362 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableNoTmpDirException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableNoTmpDirException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadablePartialException.php 0000644 00000001210 15117737236 0021260 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadablePartialException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadablePartialException extends UploadableException implements Exception { } doctrine-extensions/src/Exception/UploadableUploadException.php 0000644 00000001206 15117737236 0021115 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Exception; use Gedmo\Exception; /** * UploadableUploadException * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadableUploadException extends UploadableException implements Exception { } doctrine-extensions/src/IpTraceable/Mapping/Driver/Annotation.php 0000644 00000006412 15117737236 0021202 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\IpTraceable; use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** * 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 <pc.bertineau@alterphp.com> * * @internal */ class Annotation extends AbstractAnnotationDriver { /** * 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; } } } } doctrine-extensions/src/IpTraceable/Mapping/Driver/Attribute.php 0000644 00000001325 15117737236 0021031 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable\Mapping\Driver; use Gedmo\Mapping\Annotation\IpTraceable; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for IpTraceable * behavioral extension. Used for extraction of extended * metadata from attribute specifically for IpTraceable * extension. * * @internal */ final class Attribute extends Annotation implements AttributeDriverInterface { } doctrine-extensions/src/IpTraceable/Mapping/Driver/Xml.php 0000644 00000013127 15117737236 0017631 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for IpTraceable * behavioral extension. Used for extraction of extended * metadata from xml specifically for IpTraceable * extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Miha Vrhovnik <miha.vrhovnik@gmail.com> * @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com> * * @internal */ class Xml extends BaseXml { /** * List of types which are valid for IP * * @var string[] */ private const VALID_TYPES = [ 'string', ]; public function readExtendedMetadata($meta, array &$config) { /** * @var \SimpleXmlElement */ $mapping = $this->_getMapping($meta->getName()); if (isset($mapping->field)) { /** * @var \SimpleXmlElement */ foreach ($mapping->field as $fieldMapping) { $fieldMappingDoctrine = $fieldMapping; $fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($fieldMapping->{'ip-traceable'})) { /** * @var \SimpleXmlElement */ $data = $fieldMapping->{'ip-traceable'}; $field = $this->_getAttribute($fieldMappingDoctrine, 'name'); if (!$this->isValidField($meta, $field)) { throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' in class - {$meta->getName()}"); } if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, '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' === $this->_getAttribute($data, 'on')) { if (!$this->_isAttributeSet($data, 'field')) { throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $this->_getAttribute($data, 'field'); $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value') : null; $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, ]; } $config[$this->_getAttribute($data, 'on')][] = $field; } } } if (isset($mapping->{'many-to-one'})) { foreach ($mapping->{'many-to-one'} as $fieldMapping) { $field = $this->_getAttribute($fieldMapping, 'field'); $fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($fieldMapping->{'ip-traceable'})) { /** * @var \SimpleXmlElement */ $data = $fieldMapping->{'ip-traceable'}; 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 (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, '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' === $this->_getAttribute($data, 'on')) { if (!$this->_isAttributeSet($data, 'field')) { throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $this->_getAttribute($data, 'field'); $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value') : null; $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, ]; } $config[$this->_getAttribute($data, 'on')][] = $field; } } } } /** * Checks if $field type is valid * * @param ClassMetadata $meta * @param string $field * * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], self::VALID_TYPES, true); } } doctrine-extensions/src/IpTraceable/Mapping/Driver/Yaml.php 0000644 00000013017 15117737236 0017771 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for IpTraceable * behavioral extension. Used for extraction of extended * metadata from yaml specifically for IpTraceable * extension. * * @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * List of types which are valid for IP * * @var string[] */ private const VALID_TYPES = [ 'string', ]; /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo']['ipTraceable'])) { $mappingProperty = $fieldMapping['gedmo']['ipTraceable']; if (!$this->isValidField($meta, $field)) { throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' in class - {$meta->getName()}"); } if (!isset($mappingProperty['on']) || !in_array($mappingProperty['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' === $mappingProperty['on']) { if (!isset($mappingProperty['field'])) { throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $mappingProperty['field']; $valueAttribute = $mappingProperty['value'] ?? null; if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { throw new InvalidMappingException('IpTraceable extension does not support multiple value changeset detection yet.'); } $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, ]; } $config[$mappingProperty['on']][] = $field; } } } if (isset($mapping['manyToOne'])) { foreach ($mapping['manyToOne'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo']['ipTraceable'])) { $mappingProperty = $fieldMapping['gedmo']['ipTraceable']; 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 (!isset($mappingProperty['on']) || !in_array($mappingProperty['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' === $mappingProperty['on']) { if (!isset($mappingProperty['field'])) { throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $mappingProperty['field']; $valueAttribute = $mappingProperty['value'] ?? null; if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { throw new InvalidMappingException('IpTraceable extension does not support multiple value changeset detection yet.'); } $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, ]; } $config[$mappingProperty['on']][] = $field; } } } } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } /** * Checks if $field type is valid * * @param ClassMetadata $meta * @param string $field * * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], self::VALID_TYPES, true); } } doctrine-extensions/src/IpTraceable/Mapping/Event/Adapter/ODM.php 0000644 00000001251 15117737236 0020711 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable\Mapping\Event\Adapter; use Gedmo\IpTraceable\Mapping\Event\IpTraceableAdapter; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; /** * Doctrine event adapter for ODM adapted * for IpTraceable behavior * * @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com> */ final class ODM extends BaseAdapterODM implements IpTraceableAdapter { } doctrine-extensions/src/IpTraceable/Mapping/Event/Adapter/ORM.php 0000644 00000001251 15117737236 0020727 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable\Mapping\Event\Adapter; use Gedmo\IpTraceable\Mapping\Event\IpTraceableAdapter; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; /** * Doctrine event adapter for ORM adapted * for IpTraceable behavior * * @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com> */ final class ORM extends BaseAdapterORM implements IpTraceableAdapter { } doctrine-extensions/src/IpTraceable/Mapping/Event/IpTraceableAdapter.php 0000644 00000001100 15117737236 0022357 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable\Mapping\Event; use Gedmo\Mapping\Event\AdapterInterface; /** * Doctrine event adapter for the IpTraceable extension. * * @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com> */ interface IpTraceableAdapter extends AdapterInterface { } doctrine-extensions/src/IpTraceable/Traits/IpTraceable.php 0000644 00000002635 15117737236 0017666 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable\Traits; /** * IpTraceable Trait, usable with PHP >= 5.4 * * @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com> */ trait IpTraceable { /** * @var string */ protected $createdFromIp; /** * @var string */ protected $updatedFromIp; /** * Sets createdFromIp. * * @param string $createdFromIp * * @return $this */ public function setCreatedFromIp($createdFromIp) { $this->createdFromIp = $createdFromIp; return $this; } /** * Returns createdFromIp. * * @return string */ public function getCreatedFromIp() { return $this->createdFromIp; } /** * Sets updatedFromIp. * * @param string $updatedFromIp * * @return $this */ public function setUpdatedFromIp($updatedFromIp) { $this->updatedFromIp = $updatedFromIp; return $this; } /** * Returns updatedFromIp. * * @return string */ public function getUpdatedFromIp() { return $this->updatedFromIp; } } doctrine-extensions/src/IpTraceable/Traits/IpTraceableDocument.php 0000644 00000003517 15117737236 0021365 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable\Traits; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** * IpTraceable Trait, usable with PHP >= 5.4 * * @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com> */ trait IpTraceableDocument { /** * @var string * @Gedmo\IpTraceable(on="create") * @ODM\Field(type="string") */ #[ODM\Field(type: Type::STRING)] #[Gedmo\IpTraceable(on: 'create')] protected $createdFromIp; /** * @var string * @Gedmo\IpTraceable(on="update") * @ODM\Field(type="string") */ #[ODM\Field(type: Type::STRING)] #[Gedmo\IpTraceable(on: 'update')] protected $updatedFromIp; /** * Sets createdFromIp. * * @param string $createdFromIp * * @return $this */ public function setCreatedFromIp($createdFromIp) { $this->createdFromIp = $createdFromIp; return $this; } /** * Returns createdFromIp. * * @return string */ public function getCreatedFromIp() { return $this->createdFromIp; } /** * Sets updatedFromIp. * * @param string $updatedFromIp * * @return $this */ public function setUpdatedFromIp($updatedFromIp) { $this->updatedFromIp = $updatedFromIp; return $this; } /** * Returns updatedFromIp. * * @return string */ public function getUpdatedFromIp() { return $this->updatedFromIp; } } doctrine-extensions/src/IpTraceable/Traits/IpTraceableEntity.php 0000644 00000003476 15117737236 0021067 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable\Traits; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** * IpTraceable Trait, usable with PHP >= 5.4 * * @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com> */ trait IpTraceableEntity { /** * @var string * @Gedmo\IpTraceable(on="create") * @ORM\Column(length=45, nullable=true) */ #[ORM\Column(length: 45, nullable: true)] #[Gedmo\IpTraceable(on: 'create')] protected $createdFromIp; /** * @var string * @Gedmo\IpTraceable(on="update") * @ORM\Column(length=45, nullable=true) */ #[ORM\Column(length: 45, nullable: true)] #[Gedmo\IpTraceable(on: 'update')] protected $updatedFromIp; /** * Sets createdFromIp. * * @param string $createdFromIp * * @return $this */ public function setCreatedFromIp($createdFromIp) { $this->createdFromIp = $createdFromIp; return $this; } /** * Returns createdFromIp. * * @return string */ public function getCreatedFromIp() { return $this->createdFromIp; } /** * Sets updatedFromIp. * * @param string $updatedFromIp * * @return $this */ public function setUpdatedFromIp($updatedFromIp) { $this->updatedFromIp = $updatedFromIp; return $this; } /** * Returns updatedFromIp. * * @return string */ public function getUpdatedFromIp() { return $this->updatedFromIp; } } doctrine-extensions/src/IpTraceable/IpTraceable.php 0000644 00000002660 15117737236 0016416 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable; /** * This interface is not necessary but can be implemented for * Entities which in some cases needs to be identified as * IpTraceable * * @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com> */ interface IpTraceable { // ipTraceable expects annotations on properties /* * @gedmo:IpTraceable(on="create") * strings which should be updated on insert only */ /* * @gedmo:IpTraceable(on="update") * strings which should be updated on update and insert */ /* * @gedmo:IpTraceable(on="change", field="field", value="value") * strings which should be updated on changed "property" * value and become equal to given "value" */ /* * @gedmo:IpTraceable(on="change", field="field") * strings which should be updated on changed "property" */ /* * @gedmo:IpTraceable(on="change", fields={"field1", "field2"}) * strings which should be updated if at least one of the given fields changed */ /* * example * * @gedmo:IpTraceable(on="create") * @Column(type="string") * $created */ } doctrine-extensions/src/IpTraceable/IpTraceableListener.php 0000644 00000003156 15117737236 0020125 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\IpTraceable; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\AbstractTrackingListener; use Gedmo\Exception\InvalidArgumentException; use Gedmo\Mapping\Event\AdapterInterface; /** * The IpTraceable listener handles the update of * IPs on creation and update. * * @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com> * * @final since gedmo/doctrine-extensions 3.11 */ class IpTraceableListener extends AbstractTrackingListener { /** * @var string|null */ protected $ip; /** * Get the ipValue value to set on a ip field * * @param ClassMetadata $meta * @param string $field * @param AdapterInterface $eventAdapter * * @return string|null */ public function getFieldValue($meta, $field, $eventAdapter) { return $this->ip; } /** * Set a ip value to return * * @param string|null $ip * * @throws InvalidArgumentException * * @return void */ public function setIpValue($ip = null) { if (isset($ip) && false === filter_var($ip, FILTER_VALIDATE_IP)) { throw new InvalidArgumentException("ip address is not valid $ip"); } $this->ip = $ip; } protected function getNamespace() { return __NAMESPACE__; } } doctrine-extensions/src/Loggable/Document/MappedSuperclass/AbstractLogEntry.php 0000644 00000011007 15117737236 0024057 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Document\MappedSuperclass; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Loggable\LogEntryInterface; /** * @phpstan-template T of object * * @phpstan-implements LogEntryInterface<T> * * @MongoODM\MappedSuperclass */ #[MongoODM\MappedSuperclass] abstract class AbstractLogEntry implements LogEntryInterface { /** * @var string|null * * @MongoODM\Id */ #[MongoODM\Id] protected $id; /** * @var string|null * * @phpstan-var self::ACTION_CREATE|self::ACTION_UPDATE|self::ACTION_REMOVE|null * * @MongoODM\Field(type="string") */ #[MongoODM\Field(type: Type::STRING)] protected $action; /** * @var \DateTime|null * * @MongoODM\Field(type="date") */ #[MongoODM\Field(type: Type::DATE)] protected $loggedAt; /** * @var string|null * * @MongoODM\Field(type="string", nullable=true) */ #[MongoODM\Field(type: Type::STRING, nullable: true)] protected $objectId; /** * @var string|null * * @phpstan-var class-string<T>|null * * @MongoODM\Field(type="string") */ #[MongoODM\Field(type: Type::STRING)] protected $objectClass; /** * @var int|null * * @MongoODM\Field(type="int") */ #[MongoODM\Field(type: Type::INT)] protected $version; /** * @var array<string, mixed>|null * * @MongoODM\Field(type="hash", nullable=true) */ #[MongoODM\Field(type: Type::HASH, nullable: true)] protected $data; /** * @var string|null * * @MongoODM\Field(type="string", nullable=true) */ #[MongoODM\Field(type: Type::STRING, nullable: true)] protected $username; /** * Get id * * @return string|null */ public function getId() { return $this->id; } /** * Get action * * @return string|null */ public function getAction() { return $this->action; } /** * Set action * * @param string $action * * @return void */ public function setAction($action) { $this->action = $action; } /** * Get object class * * @return string|null */ public function getObjectClass() { return $this->objectClass; } /** * Set object class * * @param string $objectClass * * @return void */ public function setObjectClass($objectClass) { $this->objectClass = $objectClass; } /** * Get object id * * @return string|null */ public function getObjectId() { return $this->objectId; } /** * Set object id * * @param string $objectId * * @return void */ public function setObjectId($objectId) { $this->objectId = $objectId; } /** * Get username * * @return string|null */ public function getUsername() { return $this->username; } /** * Set username * * @param string $username * * @return void */ public function setUsername($username) { $this->username = $username; } /** * Get loggedAt * * @return \DateTime|null */ public function getLoggedAt() { return $this->loggedAt; } /** * Set loggedAt to "now" * * @return void */ public function setLoggedAt() { $this->loggedAt = new \DateTime(); } /** * Get data * * @return array<string, mixed>|null */ public function getData() { return $this->data; } /** * Set data * * @param array<string, mixed> $data * * @return void */ public function setData($data) { $this->data = $data; } /** * Set current version * * @param int $version * * @return void */ public function setVersion($version) { $this->version = $version; } /** * Get current version * * @return int|null */ public function getVersion() { return $this->version; } } doctrine-extensions/src/Loggable/Document/Repository/LogEntryRepository.php 0000644 00000012321 15117737236 0023377 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Document\Repository; use Doctrine\ODM\MongoDB\Iterator\Iterator; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; use Gedmo\Loggable\Document\LogEntry; use Gedmo\Loggable\LoggableListener; use Gedmo\Tool\Wrapper\MongoDocumentWrapper; /** * The LogEntryRepository has some useful functions * to interact with log entries. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class LogEntryRepository extends DocumentRepository { /** * Currently used loggable listener * * @var LoggableListener */ private $listener; /** * Loads all log entries for the * given $document * * @param object $document * * @return LogEntry[] */ public function getLogEntries($document) { $wrapped = new MongoDocumentWrapper($document, $this->dm); $objectId = $wrapped->getIdentifier(); $qb = $this->createQueryBuilder(); $qb->field('objectId')->equals($objectId); $qb->field('objectClass')->equals($wrapped->getMetadata()->getName()); $qb->sort('version', 'DESC'); $q = $qb->getQuery(); $result = $q->execute(); if ($result instanceof Iterator) { $result = $result->toArray(); } return $result; } /** * Reverts given $document to $revision by * restoring all fields from that $revision. * After this operation you will need to * persist and flush the $document. * * @param object $document * @param int $version * * @throws \Gedmo\Exception\UnexpectedValueException * * @return void */ public function revert($document, $version = 1) { $wrapped = new MongoDocumentWrapper($document, $this->dm); $objectMeta = $wrapped->getMetadata(); $objectId = $wrapped->getIdentifier(); $qb = $this->createQueryBuilder(); $qb->field('objectId')->equals($objectId); $qb->field('objectClass')->equals($objectMeta->getName()); $qb->field('version')->lte((int) $version); $qb->sort('version', 'ASC'); $q = $qb->getQuery(); $logs = $q->execute(); if ($logs instanceof Iterator) { $logs = $logs->toArray(); } if ([] === $logs) { throw new \Gedmo\Exception\UnexpectedValueException('Count not find any log entries under version: '.$version); } $data = []; while ($log = array_shift($logs)) { $data = array_merge($data, $log->getData()); } $this->fillDocument($document, $data); } /** * Fills a documents versioned fields with data * * @param object $document * * @return void */ protected function fillDocument($document, array $data) { $wrapped = new MongoDocumentWrapper($document, $this->dm); $objectMeta = $wrapped->getMetadata(); $config = $this->getLoggableListener()->getConfiguration($this->dm, $objectMeta->getName()); $fields = $config['versioned']; foreach ($data as $field => $value) { if (!in_array($field, $fields, true)) { continue; } $mapping = $objectMeta->getFieldMapping($field); // Fill the embedded document if ($wrapped->isEmbeddedAssociation($field)) { if (!empty($value)) { $embeddedMetadata = $this->dm->getClassMetadata($mapping['targetDocument']); $document = $embeddedMetadata->newInstance(); $this->fillDocument($document, $value); $value = $document; } } elseif ($objectMeta->isSingleValuedAssociation($field)) { $value = $value ? $this->dm->getReference($mapping['targetDocument'], $value) : null; } $wrapped->setPropertyValue($field, $value); unset($fields[$field]); } /* if (count($fields)) { throw new \Gedmo\Exception\UnexpectedValueException('Cound not fully revert the document to version: '.$version); } */ } /** * Get the currently used LoggableListener * * @throws \Gedmo\Exception\RuntimeException if listener is not found */ private function getLoggableListener(): LoggableListener { if (null === $this->listener) { foreach ($this->dm->getEventManager()->getAllListeners() as $event => $listeners) { foreach ($listeners as $hash => $listener) { if ($listener instanceof LoggableListener) { $this->listener = $listener; break 2; } } } if (null === $this->listener) { throw new \Gedmo\Exception\RuntimeException('The loggable listener could not be found'); } } return $this->listener; } } doctrine-extensions/src/Loggable/Document/LogEntry.php 0000644 00000002356 15117737236 0017127 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; use Gedmo\Loggable\Document\Repository\LogEntryRepository; /** * Gedmo\Loggable\Document\LogEntry * * @MongoODM\Document(repositoryClass="Gedmo\Loggable\Document\Repository\LogEntryRepository") * @MongoODM\Index(keys={"objectId": "asc", "objectClass": "asc", "version": "asc"}) * @MongoODM\Index(keys={"loggedAt": "asc"}) * @MongoODM\Index(keys={"objectClass": "asc"}) * @MongoODM\Index(keys={"username": "asc"}) */ #[MongoODM\Document(repositoryClass: LogEntryRepository::class)] #[MongoODM\Index(keys: ['objectId' => 'asc', 'objectClass' => 'asc', 'version' => 'asc'])] #[MongoODM\Index(keys: ['loggedAt' => 'asc'])] #[MongoODM\Index(keys: ['objectClass' => 'asc'])] #[MongoODM\Index(keys: ['username' => 'asc'])] class LogEntry extends MappedSuperclass\AbstractLogEntry { /* * All required columns are mapped through inherited superclass */ } doctrine-extensions/src/Loggable/Entity/MappedSuperclass/AbstractLogEntry.php 0000644 00000011223 15117737236 0023555 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Entity\MappedSuperclass; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Loggable\LogEntryInterface; /** * @phpstan-template T of object * * @phpstan-implements LogEntryInterface<T> * * @ORM\MappedSuperclass */ #[ORM\MappedSuperclass] abstract class AbstractLogEntry implements LogEntryInterface { /** * @var int|null * * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue */ #[ORM\Column(type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue] protected $id; /** * @var string|null * * @phpstan-var self::ACTION_CREATE|self::ACTION_UPDATE|self::ACTION_REMOVE|null * * @ORM\Column(type="string", length=8) */ #[ORM\Column(type: Types::STRING, length: 8)] protected $action; /** * @var \DateTime|null * * @ORM\Column(name="logged_at", type="datetime") */ #[ORM\Column(name: 'logged_at', type: Types::DATETIME_MUTABLE)] protected $loggedAt; /** * @var string|null * * @ORM\Column(name="object_id", length=64, nullable=true) */ #[ORM\Column(name: 'object_id', length: 64, nullable: true)] protected $objectId; /** * @var string|null * * @phpstan-var class-string<T>|null * * @ORM\Column(name="object_class", type="string", length=191) */ #[ORM\Column(name: 'object_class', type: Types::STRING, length: 191)] protected $objectClass; /** * @var int|null * * @ORM\Column(type="integer") */ #[ORM\Column(type: Types::INTEGER)] protected $version; /** * @var array|null * * @ORM\Column(type="array", nullable=true) */ #[ORM\Column(type: Types::ARRAY, nullable: true)] protected $data; /** * @var string|null * * @ORM\Column(length=191, nullable=true) */ #[ORM\Column(length: 191, nullable: true)] protected $username; /** * Get id * * @return int|null */ public function getId() { return $this->id; } /** * Get action * * @return string|null */ public function getAction() { return $this->action; } /** * Set action * * @param string $action * * @return void */ public function setAction($action) { $this->action = $action; } /** * Get object class * * @return string|null */ public function getObjectClass() { return $this->objectClass; } /** * Set object class * * @param string $objectClass * * @return void */ public function setObjectClass($objectClass) { $this->objectClass = $objectClass; } /** * Get object id * * @return string|null */ public function getObjectId() { return $this->objectId; } /** * Set object id * * @param string $objectId * * @return void */ public function setObjectId($objectId) { $this->objectId = $objectId; } /** * Get username * * @return string|null */ public function getUsername() { return $this->username; } /** * Set username * * @param string $username * * @return void */ public function setUsername($username) { $this->username = $username; } /** * Get loggedAt * * @return \DateTime|null */ public function getLoggedAt() { return $this->loggedAt; } /** * Set loggedAt to "now" * * @return void */ public function setLoggedAt() { $this->loggedAt = new \DateTime(); } /** * Get data * * @return array|null */ public function getData() { return $this->data; } /** * Set data * * @param array $data * * @return void */ public function setData($data) { $this->data = $data; } /** * Set current version * * @param int $version * * @return void */ public function setVersion($version) { $this->version = $version; } /** * Get current version * * @return int|null */ public function getVersion() { return $this->version; } } doctrine-extensions/src/Loggable/Entity/Repository/LogEntryRepository.php 0000644 00000012270 15117737236 0023100 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Entity\Repository; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; use Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry; use Gedmo\Loggable\LoggableListener; use Gedmo\Tool\Wrapper\EntityWrapper; /** * The LogEntryRepository has some useful functions * to interact with log entries. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class LogEntryRepository extends EntityRepository { /** * Currently used loggable listener * * @var LoggableListener */ private $listener; /** * Loads all log entries for the given entity * * @param object $entity * * @return AbstractLogEntry[] */ public function getLogEntries($entity) { $q = $this->getLogEntriesQuery($entity); return $q->getResult(); } /** * Get the query for loading of log entries * * @param object $entity * * @return Query */ public function getLogEntriesQuery($entity) { $wrapped = new EntityWrapper($entity, $this->_em); $objectClass = $wrapped->getMetadata()->getName(); $meta = $this->getClassMetadata(); $dql = "SELECT log FROM {$meta->getName()} log"; $dql .= ' WHERE log.objectId = :objectId'; $dql .= ' AND log.objectClass = :objectClass'; $dql .= ' ORDER BY log.version DESC'; $objectId = (string) $wrapped->getIdentifier(); $q = $this->_em->createQuery($dql); $q->setParameters(compact('objectId', 'objectClass')); return $q; } /** * Reverts given $entity to $revision by * restoring all fields from that $revision. * After this operation you will need to * persist and flush the $entity. * * @param object $entity * @param int $version * * @throws \Gedmo\Exception\UnexpectedValueException * * @return void */ public function revert($entity, $version = 1) { $wrapped = new EntityWrapper($entity, $this->_em); $objectMeta = $wrapped->getMetadata(); $objectClass = $objectMeta->getName(); $meta = $this->getClassMetadata(); $dql = "SELECT log FROM {$meta->getName()} log"; $dql .= ' WHERE log.objectId = :objectId'; $dql .= ' AND log.objectClass = :objectClass'; $dql .= ' AND log.version <= :version'; $dql .= ' ORDER BY log.version ASC'; $objectId = (string) $wrapped->getIdentifier(); $q = $this->_em->createQuery($dql); $q->setParameters(compact('objectId', 'objectClass', 'version')); $logs = $q->getResult(); if ([] === $logs) { throw new \Gedmo\Exception\UnexpectedValueException('Could not find any log entries under version: '.$version); } $config = $this->getLoggableListener()->getConfiguration($this->_em, $objectMeta->getName()); $fields = $config['versioned']; $filled = false; while (($log = array_pop($logs)) && !$filled) { if ($data = $log->getData()) { foreach ($data as $field => $value) { if (in_array($field, $fields, true)) { $this->mapValue($objectMeta, $field, $value); $wrapped->setPropertyValue($field, $value); unset($fields[array_search($field, $fields, true)]); } } } $filled = [] === $fields; } /*if (count($fields)) { throw new \Gedmo\Exception\UnexpectedValueException('Could not fully revert the entity to version: '.$version); }*/ } /** * @param string $field * @param mixed $value * * @return void */ protected function mapValue(ClassMetadata $objectMeta, $field, &$value) { if (!$objectMeta->isSingleValuedAssociation($field)) { return; } $mapping = $objectMeta->getAssociationMapping($field); $value = $value ? $this->_em->getReference($mapping['targetEntity'], $value) : null; } /** * Get the currently used LoggableListener * * @throws \Gedmo\Exception\RuntimeException if listener is not found */ private function getLoggableListener(): LoggableListener { if (null === $this->listener) { foreach ($this->_em->getEventManager()->getAllListeners() as $event => $listeners) { foreach ($listeners as $hash => $listener) { if ($listener instanceof LoggableListener) { $this->listener = $listener; break 2; } } } if (null === $this->listener) { throw new \Gedmo\Exception\RuntimeException('The loggable listener could not be found'); } } return $this->listener; } } doctrine-extensions/src/Loggable/Entity/LogEntry.php 0000644 00000003051 15117737236 0016616 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Entity; use Doctrine\ORM\Mapping as ORM; use Gedmo\Loggable\Entity\Repository\LogEntryRepository; /** * Gedmo\Loggable\Entity\LogEntry * * @ORM\Table( * name="ext_log_entries", * options={"row_format": "DYNAMIC"}, * indexes={ * @ORM\Index(name="log_class_lookup_idx", columns={"object_class"}), * @ORM\Index(name="log_date_lookup_idx", columns={"logged_at"}), * @ORM\Index(name="log_user_lookup_idx", columns={"username"}), * @ORM\Index(name="log_version_lookup_idx", columns={"object_id", "object_class", "version"}) * } * ) * @ORM\Entity(repositoryClass="Gedmo\Loggable\Entity\Repository\LogEntryRepository") */ #[ORM\Entity(repositoryClass: LogEntryRepository::class)] #[ORM\Table(name: 'ext_log_entries', options: ['row_format' => 'DYNAMIC'])] #[ORM\Index(name: 'log_class_lookup_idx', columns: ['object_class'])] #[ORM\Index(name: 'log_date_lookup_idx', columns: ['logged_at'])] #[ORM\Index(name: 'log_user_lookup_idx', columns: ['username'])] #[ORM\Index(name: 'log_version_lookup_idx', columns: ['object_id', 'object_class', 'version'])] class LogEntry extends MappedSuperclass\AbstractLogEntry { /* * All required columns are mapped through inherited superclass */ } doctrine-extensions/src/Loggable/Mapping/Driver/Annotation.php 0000644 00000012246 15117737236 0020545 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\Loggable; use Gedmo\Mapping\Annotation\Versioned; use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** * 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 <jules.boussekeyt@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @internal */ class Annotation extends AbstractAnnotationDriver { /** * 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 && 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 (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()}"); } } } /** * @param string $field * * @return bool */ protected function isMappingValid(ClassMetadata $meta, $field) { return false == $meta->isCollectionValuedAssociation($field); } /** * @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 */ 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); } } } } } doctrine-extensions/src/Loggable/Mapping/Driver/Attribute.php 0000644 00000001243 15117737236 0020371 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Mapping\Driver; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for Loggable * behavioral extension. Used for extraction of extended * metadata from attributes specifically for Loggable * extension. * * @internal */ final class Attribute extends Annotation implements AttributeDriverInterface { } doctrine-extensions/src/Loggable/Mapping/Driver/Xml.php 0000644 00000010504 15117737236 0017166 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for Loggable * behavioral extension. Used for extraction of extended * metadata from xml specifically for Loggable * extension. * * @author Boussekeyt Jules <jules.boussekeyt@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Miha Vrhovnik <miha.vrhovnik@gmail.com> * * @internal */ class Xml extends BaseXml { public function readExtendedMetadata($meta, array &$config) { /** * @var \SimpleXmlElement */ $xml = $this->_getMapping($meta->getName()); $xmlDoctrine = $xml; $xml = $xml->children(self::GEDMO_NAMESPACE_URI); if ('entity' === $xmlDoctrine->getName() || 'document' === $xmlDoctrine->getName() || 'mapped-superclass' === $xmlDoctrine->getName()) { if (isset($xml->loggable)) { /** * @var \SimpleXMLElement; */ $data = $xml->loggable; $config['loggable'] = true; if ($this->_isAttributeSet($data, 'log-entry-class')) { $class = $this->_getAttribute($data, 'log-entry-class'); if (!$cl = $this->getRelatedClassName($meta, $class)) { throw new InvalidMappingException("LogEntry class: {$class} does not exist."); } $config['logEntryClass'] = $cl; } } } if (isset($xmlDoctrine->field)) { $this->inspectElementForVersioned($xmlDoctrine->field, $config, $meta); } foreach ($xmlDoctrine->{'attribute-overrides'}->{'attribute-override'} ?? [] as $overrideMapping) { $this->inspectElementForVersioned($overrideMapping, $config, $meta); } if (isset($xmlDoctrine->{'many-to-one'})) { $this->inspectElementForVersioned($xmlDoctrine->{'many-to-one'}, $config, $meta); } if (isset($xmlDoctrine->{'one-to-one'})) { $this->inspectElementForVersioned($xmlDoctrine->{'one-to-one'}, $config, $meta); } if (isset($xmlDoctrine->{'reference-one'})) { $this->inspectElementForVersioned($xmlDoctrine->{'reference-one'}, $config, $meta); } if (isset($xmlDoctrine->{'embedded'})) { $this->inspectElementForVersioned($xmlDoctrine->{'embedded'}, $config, $meta); } if (!$meta->isMappedSuperclass && $config) { if (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()}"); } } } /** * Searches mappings on element for versioned fields */ private function inspectElementForVersioned(\SimpleXMLElement $element, array &$config, ClassMetadata $meta): void { foreach ($element as $mapping) { $mappingDoctrine = $mapping; /** * @var \SimpleXmlElement */ $mapping = $mapping->children(self::GEDMO_NAMESPACE_URI); $isAssoc = $this->_isAttributeSet($mappingDoctrine, 'field'); $field = $this->_getAttribute($mappingDoctrine, $isAssoc ? 'field' : 'name'); if (isset($mapping->versioned)) { if ($isAssoc && !$meta->associationMappings[$field]['isOwningSide']) { throw new InvalidMappingException("Cannot version [{$field}] as it is not the owning side in object - {$meta->getName()}"); } $config['versioned'][] = $field; } } } } doctrine-extensions/src/Loggable/Mapping/Driver/Yaml.php 0000644 00000014502 15117737236 0017332 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for Loggable * behavioral extension. Used for extraction of extended * metadata from yaml specifically for Loggable * extension. * * @author Boussekeyt Jules <jules.boussekeyt@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['gedmo'])) { $classMapping = $mapping['gedmo']; if (isset($classMapping['loggable'])) { $config['loggable'] = true; if (isset($classMapping['loggable']['logEntryClass'])) { if (!$cl = $this->getRelatedClassName($meta, $classMapping['loggable']['logEntryClass'])) { throw new InvalidMappingException("LogEntry class: {$classMapping['loggable']['logEntryClass']} does not exist."); } $config['logEntryClass'] = $cl; } } } if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { if (in_array('versioned', $fieldMapping['gedmo'], true)) { if ($meta->isCollectionValuedAssociation($field)) { throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); } // fields cannot be overrided and throws mapping exception $config['versioned'][] = $field; } } } } if (isset($mapping['attributeOverride'])) { foreach ($mapping['attributeOverride'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { if (in_array('versioned', $fieldMapping['gedmo'], true)) { if ($meta->isCollectionValuedAssociation($field)) { throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); } // fields cannot be overrided and throws mapping exception $config['versioned'][] = $field; } } } } if (isset($mapping['manyToOne'])) { foreach ($mapping['manyToOne'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { if (in_array('versioned', $fieldMapping['gedmo'], true)) { if ($meta->isCollectionValuedAssociation($field)) { throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); } // fields cannot be overrided and throws mapping exception $config['versioned'][] = $field; } } } } if (isset($mapping['oneToOne'])) { foreach ($mapping['oneToOne'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { if (in_array('versioned', $fieldMapping['gedmo'], true)) { if ($meta->isCollectionValuedAssociation($field)) { throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); } // fields cannot be overrided and throws mapping exception $config['versioned'][] = $field; } } } } if (isset($mapping['embedded'])) { foreach ($mapping['embedded'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { if (in_array('versioned', $fieldMapping['gedmo'], true)) { if ($meta->isCollectionValuedAssociation($field)) { throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->getName()}"); } // fields cannot be overrided and throws mapping exception $mapping = $this->_getMapping($fieldMapping['class']); $this->inspectEmbeddedForVersioned($field, $mapping, $config); } } } } if (!$meta->isMappedSuperclass && $config) { if (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 annoted with Loggable annotation in order to track versioned fields in class - {$meta->getName()}"); } } } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } private function inspectEmbeddedForVersioned(string $field, array $mapping, array &$config): void { if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $property => $fieldMapping) { $config['versioned'][] = $field.'.'.$property; } } } } doctrine-extensions/src/Loggable/Mapping/Event/Adapter/ODM.php 0000644 00000003240 15117737236 0020252 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Mapping\Event\Adapter; use Gedmo\Loggable\Document\LogEntry; use Gedmo\Loggable\Mapping\Event\LoggableAdapter; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; /** * Doctrine event adapter for ODM adapted * for Loggable behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class ODM extends BaseAdapterODM implements LoggableAdapter { public function getDefaultLogEntryClass() { return LogEntry::class; } public function isPostInsertGenerator($meta) { return false; } public function getNewVersion($meta, $object) { $dm = $this->getObjectManager(); $objectMeta = $dm->getClassMetadata(get_class($object)); $identifierField = $this->getSingleIdentifierFieldName($objectMeta); $objectId = $objectMeta->getReflectionProperty($identifierField)->getValue($object); $qb = $dm->createQueryBuilder($meta->getName()); $qb->select('version'); $qb->field('objectId')->equals($objectId); $qb->field('objectClass')->equals($objectMeta->getName()); $qb->sort('version', 'DESC'); $qb->limit(1); $q = $qb->getQuery(); $q->setHydrate(false); $result = $q->getSingleResult(); if ($result) { $result = $result['version'] + 1; } return $result; } } doctrine-extensions/src/Loggable/Mapping/Event/Adapter/ORM.php 0000644 00000003325 15117737236 0020274 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Mapping\Event\Adapter; use Doctrine\ORM\Mapping\ClassMetadata; use Gedmo\Loggable\Entity\LogEntry; use Gedmo\Loggable\Mapping\Event\LoggableAdapter; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; /** * Doctrine event adapter for ORM adapted * for Loggable behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class ORM extends BaseAdapterORM implements LoggableAdapter { public function getDefaultLogEntryClass() { return LogEntry::class; } /** * @param ClassMetadata $meta */ public function isPostInsertGenerator($meta) { return $meta->idGenerator->isPostInsertGenerator(); } public function getNewVersion($meta, $object) { $em = $this->getObjectManager(); $objectMeta = $em->getClassMetadata(get_class($object)); $identifierField = $this->getSingleIdentifierFieldName($objectMeta); $objectId = (string) $objectMeta->getReflectionProperty($identifierField)->getValue($object); $dql = "SELECT MAX(log.version) FROM {$meta->getName()} log"; $dql .= ' WHERE log.objectId = :objectId'; $dql .= ' AND log.objectClass = :objectClass'; $q = $em->createQuery($dql); $q->setParameters([ 'objectId' => $objectId, 'objectClass' => $objectMeta->getName(), ]); return $q->getSingleScalarResult() + 1; } } doctrine-extensions/src/Loggable/Mapping/Event/LoggableAdapter.php 0000644 00000002332 15117737236 0021271 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable\Mapping\Event; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Mapping\Event\AdapterInterface; /** * Doctrine event adapter for the Loggable extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface LoggableAdapter extends AdapterInterface { /** * Get the default object class name used to store the log entries. * * @return string * @phpstan-return class-string */ public function getDefaultLogEntryClass(); /** * Checks whether an identifier should be generated post insert. * * @param ClassMetadata $meta * * @return bool */ public function isPostInsertGenerator($meta); /** * Get the new version number for an object. * * @param ClassMetadata $meta * @param object $object * * @return int */ public function getNewVersion($meta, $object); } doctrine-extensions/src/Loggable/LogEntryInterface.php 0000644 00000004136 15117737236 0017170 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable; /** * Interface to be implemented by log entry models. * * @phpstan-template T of object * * @author Javier Spagnoletti <phansys@gmail.com> */ interface LogEntryInterface { public const ACTION_CREATE = 'create'; public const ACTION_UPDATE = 'update'; public const ACTION_REMOVE = 'remove'; /** * @phpstan-param self::ACTION_CREATE|self::ACTION_UPDATE|self::ACTION_REMOVE $action * * @return void */ public function setAction(string $action); /** * @return string|null * * @phpstan-return self::ACTION_CREATE|self::ACTION_UPDATE|self::ACTION_REMOVE|null */ public function getAction(); /** * @return void */ public function setUsername(string $username); /** * @return string|null */ public function getUsername(); /** * @phpstan-param class-string<T> $objectClass * * @return void */ public function setObjectClass(string $objectClass); /** * @return string|null * * @phpstan-return class-string<T> */ public function getObjectClass(); /** * @return void */ public function setLoggedAt(); /** * @return \DateTimeInterface|null */ public function getLoggedAt(); /** * @return void */ public function setObjectId(string $objectId); /** * @return string|null */ public function getObjectId(); /** * @param array<string, mixed> $data * * @return void */ public function setData(array $data); /** * @return array<string, mixed>|null */ public function getData(); /** * @return void */ public function setVersion(int $version); /** * @return int|null */ public function getVersion(); } doctrine-extensions/src/Loggable/Loggable.php 0000644 00000001756 15117737236 0015325 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable; /** * This interface is not necessary but can be implemented for * Domain Objects which in some cases needs to be identified as * Loggable * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface Loggable { // this interface is not necessary to implement /* * @gedmo:Loggable * to mark the class as loggable use class annotation @gedmo:Loggable * this object will contain now a history * available options: * logEntryClass="My\LogEntryObject" (optional) defaultly will use internal object class * example: * * @gedmo:Loggable(logEntryClass="My\LogEntryObject") * class MyEntity */ } doctrine-extensions/src/Loggable/LoggableListener.php 0000644 00000026602 15117737236 0017030 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Loggable; use Doctrine\Common\EventArgs; use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; use Doctrine\Persistence\ObjectManager; use Gedmo\Loggable\Mapping\Event\LoggableAdapter; use Gedmo\Mapping\MappedEventSubscriber; use Gedmo\Tool\Wrapper\AbstractWrapper; /** * Loggable listener * * @author Boussekeyt Jules <jules.boussekeyt@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @phpstan-type LoggableConfiguration = array{ * loggable?: bool, * logEntryClass?: class-string<LogEntryInterface>, * useObjectClass?: class-string, * versioned?: string[], * } * * @phpstan-method LoggableConfiguration getConfiguration(ObjectManager $objectManager, $class) * * @method LoggableAdapter getEventAdapter(EventArgs $args) */ class LoggableListener extends MappedEventSubscriber { /** * @deprecated use `LogEntryInterface::ACTION_CREATE` instead */ public const ACTION_CREATE = LogEntryInterface::ACTION_CREATE; /** * @deprecated use `LogEntryInterface::ACTION_UPDATE` instead */ public const ACTION_UPDATE = LogEntryInterface::ACTION_UPDATE; /** * @deprecated use `LogEntryInterface::ACTION_REMOVE` instead */ public const ACTION_REMOVE = LogEntryInterface::ACTION_REMOVE; /** * Username for identification * * @var string */ protected $username; /** * List of log entries which do not have the foreign * key generated yet - MySQL case. These entries * will be updated with new keys on postPersist event * * @var array<int, LogEntryInterface> */ protected $pendingLogEntryInserts = []; /** * For log of changed relations we use * its identifiers to avoid storing serialized Proxies. * These are pending relations in case it does not * have an identifier yet * * @var array<int, array<int, array<string, LogEntryInterface|string>>> * * @phpstan-var array<int, array<int, array{log: LogEntryInterface, field: string}>> */ protected $pendingRelatedObjects = []; /** * Set username for identification * * @param mixed $username * * @throws \Gedmo\Exception\InvalidArgumentException Invalid username * * @return void */ public function setUsername($username) { if (is_string($username)) { $this->username = $username; } elseif (is_object($username) && method_exists($username, 'getUserIdentifier')) { $this->username = (string) $username->getUserIdentifier(); } elseif (is_object($username) && method_exists($username, 'getUsername')) { $this->username = (string) $username->getUsername(); } elseif (is_object($username) && method_exists($username, '__toString')) { $this->username = $username->__toString(); } else { throw new \Gedmo\Exception\InvalidArgumentException('Username must be a string, or object should have method getUserIdentifier, getUsername or __toString'); } } /** * @return string[] */ public function getSubscribedEvents() { return [ 'onFlush', 'loadClassMetadata', 'postPersist', ]; } /** * Maps additional metadata * * @param LoadClassMetadataEventArgs $eventArgs * * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } /** * Checks for inserted object to update its logEntry * foreign key * * @return void */ public function postPersist(EventArgs $args) { $ea = $this->getEventAdapter($args); $object = $ea->getObject(); $om = $ea->getObjectManager(); $oid = spl_object_id($object); $uow = $om->getUnitOfWork(); if ($this->pendingLogEntryInserts && array_key_exists($oid, $this->pendingLogEntryInserts)) { $wrapped = AbstractWrapper::wrap($object, $om); $logEntry = $this->pendingLogEntryInserts[$oid]; $logEntryMeta = $om->getClassMetadata(get_class($logEntry)); $id = $wrapped->getIdentifier(); $logEntryMeta->getReflectionProperty('objectId')->setValue($logEntry, $id); $uow->scheduleExtraUpdate($logEntry, [ 'objectId' => [null, $id], ]); $ea->setOriginalObjectProperty($uow, $logEntry, 'objectId', $id); unset($this->pendingLogEntryInserts[$oid]); } if ($this->pendingRelatedObjects && array_key_exists($oid, $this->pendingRelatedObjects)) { $wrapped = AbstractWrapper::wrap($object, $om); $identifiers = $wrapped->getIdentifier(false); foreach ($this->pendingRelatedObjects[$oid] as $props) { $logEntry = $props['log']; $logEntryMeta = $om->getClassMetadata(get_class($logEntry)); $oldData = $data = $logEntry->getData(); $data[$props['field']] = $identifiers; $logEntry->setData($data); $uow->scheduleExtraUpdate($logEntry, [ 'data' => [$oldData, $data], ]); $ea->setOriginalObjectProperty($uow, $logEntry, 'data', $data); } unset($this->pendingRelatedObjects[$oid]); } } /** * Looks for loggable objects being inserted or updated * for further processing * * @return void */ public function onFlush(EventArgs $eventArgs) { $ea = $this->getEventAdapter($eventArgs); $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); foreach ($ea->getScheduledObjectInsertions($uow) as $object) { $this->createLogEntry(LogEntryInterface::ACTION_CREATE, $object, $ea); } foreach ($ea->getScheduledObjectUpdates($uow) as $object) { $this->createLogEntry(LogEntryInterface::ACTION_UPDATE, $object, $ea); } foreach ($ea->getScheduledObjectDeletions($uow) as $object) { $this->createLogEntry(LogEntryInterface::ACTION_REMOVE, $object, $ea); } } /** * Get the LogEntry class * * @param string $class * @phpstan-param class-string $class * * @return string * @phpstan-return class-string<LogEntryInterface> */ protected function getLogEntryClass(LoggableAdapter $ea, $class) { return self::$configurations[$this->name][$class]['logEntryClass'] ?? $ea->getDefaultLogEntryClass(); } /** * Handle any custom LogEntry functionality that needs to be performed * before persisting it * * @param LogEntryInterface $logEntry The LogEntry being persisted * @param object $object The object being Logged * * @return void */ protected function prePersistLogEntry($logEntry, $object) { } protected function getNamespace() { return __NAMESPACE__; } /** * Returns an objects changeset data * * @param LoggableAdapter $ea * @param object $object * @param LogEntryInterface $logEntry * * @return array */ protected function getObjectChangeSetData($ea, $object, $logEntry) { $om = $ea->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $om); $meta = $wrapped->getMetadata(); $config = $this->getConfiguration($om, $meta->getName()); $uow = $om->getUnitOfWork(); $newValues = []; foreach ($ea->getObjectChangeSet($uow, $object) as $field => $changes) { if (empty($config['versioned']) || !in_array($field, $config['versioned'], true)) { continue; } $value = $changes[1]; if ($meta->isSingleValuedAssociation($field) && $value) { if ($wrapped->isEmbeddedAssociation($field)) { $value = $this->getObjectChangeSetData($ea, $value, $logEntry); } else { $oid = spl_object_id($value); $wrappedAssoc = AbstractWrapper::wrap($value, $om); $value = $wrappedAssoc->getIdentifier(false); if (!is_array($value) && !$value) { $this->pendingRelatedObjects[$oid][] = [ 'log' => $logEntry, 'field' => $field, ]; } } } $newValues[$field] = $value; } return $newValues; } /** * Create a new Log instance * * @param string $action * @param object $object * * @phpstan-param LogEntryInterface::ACTION_CREATE|LogEntryInterface::ACTION_UPDATE|LogEntryInterface::ACTION_REMOVE $action * * @return LogEntryInterface|null */ protected function createLogEntry($action, $object, LoggableAdapter $ea) { $om = $ea->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $om); $meta = $wrapped->getMetadata(); // Filter embedded documents if (isset($meta->isEmbeddedDocument) && $meta->isEmbeddedDocument) { return null; } if ($config = $this->getConfiguration($om, $meta->getName())) { $logEntryClass = $this->getLogEntryClass($ea, $meta->getName()); $logEntryMeta = $om->getClassMetadata($logEntryClass); /** @var LogEntryInterface $logEntry */ $logEntry = $logEntryMeta->newInstance(); $logEntry->setAction($action); $logEntry->setUsername($this->username); $logEntry->setObjectClass($meta->getName()); $logEntry->setLoggedAt(); // check for the availability of the primary key $uow = $om->getUnitOfWork(); if (LogEntryInterface::ACTION_CREATE === $action && $ea->isPostInsertGenerator($meta)) { $this->pendingLogEntryInserts[spl_object_id($object)] = $logEntry; } else { $logEntry->setObjectId($wrapped->getIdentifier()); } $newValues = []; if (LogEntryInterface::ACTION_REMOVE !== $action && isset($config['versioned'])) { $newValues = $this->getObjectChangeSetData($ea, $object, $logEntry); $logEntry->setData($newValues); } if (LogEntryInterface::ACTION_UPDATE === $action && [] === $newValues) { return null; } $version = 1; if (LogEntryInterface::ACTION_CREATE !== $action) { $version = $ea->getNewVersion($logEntryMeta, $object); if (empty($version)) { // was versioned later $version = 1; } } $logEntry->setVersion($version); $this->prePersistLogEntry($logEntry, $object); $om->persist($logEntry); $uow->computeChangeSet($logEntryMeta, $logEntry); return $logEntry; } return null; } } doctrine-extensions/src/Mapping/Annotation/All.php 0000644 00000001330 15117737236 0016276 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ @trigger_error(sprintf( 'Requiring the file at "%s" is deprecated since gedmo/doctrine-extensions 3.11, this file will be removed in version 4.0.', __FILE__ ), E_USER_DEPRECATED); // Contains all annotations for extensions // NOTE: should be included with require_once foreach (glob(__DIR__.'/*.php') as $filename) { if ('All' === basename($filename, '.php')) { continue; } include_once $filename; } doctrine-extensions/src/Mapping/Annotation/Annotation.php 0000644 00000000630 15117737236 0017702 0 ustar 00 <?php declare(strict_types=1); /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; /** * @internal */ interface Annotation { } doctrine-extensions/src/Mapping/Annotation/Blameable.php 0000644 00000002703 15117737236 0017437 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Blameable annotation for Blameable behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("PROPERTY") * * @author David Buchmann <mail@davidbu.ch> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Blameable implements GedmoAnnotation { /** @var string */ public $on = 'update'; /** @var string|string[] */ public $field; /** @var mixed */ public $value; /** * @param string|string[]|null $field * @param mixed $value */ public function __construct(array $data = [], string $on = 'update', $field = null, $value = null) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->on = $data['on'] ?? $on; $this->field = $data['field'] ?? $field; $this->value = $data['value'] ?? $value; } } doctrine-extensions/src/Mapping/Annotation/IpTraceable.php 0000644 00000002742 15117737236 0017751 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * IpTraceable annotation for IpTraceable behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("PROPERTY") * * @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class IpTraceable implements GedmoAnnotation { /** @var string */ public $on = 'update'; /** @var string|string[]|null */ public $field; /** @var mixed */ public $value; /** * @param string|string[]|null $field * @param mixed $value */ public function __construct(array $data = [], string $on = 'update', $field = null, $value = null) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->on = $data['on'] ?? $on; $this->field = $data['field'] ?? $field; $this->value = $data['value'] ?? $value; } } doctrine-extensions/src/Mapping/Annotation/Language.php 0000644 00000001331 15117737236 0017312 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Language annotation for Translatable behavioral extension * * @Annotation * @Target("PROPERTY") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Language implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/Locale.php 0000644 00000001325 15117737236 0016771 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Locale annotation for Translatable behavioral extension * * @Annotation * @Target("PROPERTY") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Locale implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/Loggable.php 0000644 00000002632 15117737236 0017310 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Loggable\LogEntryInterface; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Loggable annotation for Loggable behavioral extension * * @phpstan-template T of LogEntryInterface * * @Annotation * @NamedArgumentConstructor * @Target("CLASS") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_CLASS)] final class Loggable implements GedmoAnnotation { /** * @var string|null * * @phpstan-var class-string<T>|null */ public $logEntryClass; /** * @phpstan-param class-string<T>|null $logEntryClass */ public function __construct(array $data = [], ?string $logEntryClass = null) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->logEntryClass = $data['logEntryClass'] ?? $logEntryClass; } } doctrine-extensions/src/Mapping/Annotation/Reference.php 0000644 00000003640 15117737236 0017472 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Reference annotation for ORM -> ODM references extension * to be user like "@ReferenceMany(type="entity", class="MyEntity", identifier="entity_id")" * * @author Bulat Shakirzyanov <mallluhuct@gmail.com> * @Annotation */ abstract class Reference implements GedmoAnnotation { /** * @var string|null * @phpstan-var 'entity'|'document'|null */ public $type; /** * @var string|null * @phpstan-var class-string|null */ public $class; /** * @var string|null */ public $identifier; /** * @var string|null */ public $mappedBy; /** * @var string|null */ public $inversedBy; /** * @phpstan-param class-string|null $class */ public function __construct( array $data = [], ?string $type = null, ?string $class = null, ?string $identifier = null, ?string $mappedBy = null, ?string $inversedBy = null ) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->type = $data['type'] ?? $type; $this->class = $data['class'] ?? $class; $this->identifier = $data['identifier'] ?? $identifier; $this->mappedBy = $data['mappedBy'] ?? $mappedBy; $this->inversedBy = $data['inversedBy'] ?? $inversedBy; } } doctrine-extensions/src/Mapping/Annotation/ReferenceIntegrity.php 0000644 00000002462 15117737236 0021372 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * ReferenceIntegrity annotation for ReferenceIntegrity behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("PROPERTY") * * @author Evert Harmeling <evert.harmeling@freshheads.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class ReferenceIntegrity implements GedmoAnnotation { /** @var string|null */ public $value; /** * @param string|array|null $data */ public function __construct($data = [], ?string $value = null) { if (is_string($data)) { $data = ['value' => $data]; } elseif ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->value = $data['value'] ?? $value; } } doctrine-extensions/src/Mapping/Annotation/ReferenceMany.php 0000644 00000001352 15117737236 0020315 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; /** * Reference annotation for ORM -> ODM references extension * to be user like "@ReferenceMany(type="entity", class="MyEntity", identifier="entity_id")" * * @author Bulat Shakirzyanov <mallluhuct@gmail.com> * @NamedArgumentConstructor * @Annotation * * @final since gedmo/doctrine-extensions 3.11 */ #[Attribute(Attribute::TARGET_PROPERTY)] class ReferenceMany extends Reference { } doctrine-extensions/src/Mapping/Annotation/ReferenceManyEmbed.php 0000644 00000001036 15117737236 0021251 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; /** * @NamedArgumentConstructor * @Annotation * * @final since gedmo/doctrine-extensions 3.11 */ #[Attribute(Attribute::TARGET_PROPERTY)] class ReferenceManyEmbed extends Reference { } doctrine-extensions/src/Mapping/Annotation/ReferenceOne.php 0000644 00000001356 15117737236 0020136 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; /** * Reference annotation for ORM -> ODM references extension * to be user like "@ReferenceOne(type="entity", class="MyEntity", identifier="entity_id")" * * @author Bulat Shakirzyanov <mallluhuct@gmail.com> * * @Annotation * * @NamedArgumentConstructor * * @final since gedmo/doctrine-extensions 3.11 */ #[Attribute(Attribute::TARGET_PROPERTY)] class ReferenceOne extends Reference { } doctrine-extensions/src/Mapping/Annotation/Slug.php 0000644 00000005041 15117737236 0016503 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Slug annotation for Sluggable behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("PROPERTY") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Slug implements GedmoAnnotation { /** @var string[] @Required */ public $fields = []; /** @var bool */ public $updatable = true; /** @var string */ public $style = 'default'; // or "camel" /** @var bool */ public $unique = true; /** @var string|null */ public $unique_base; /** @var string */ public $separator = '-'; /** @var string */ public $prefix = ''; /** @var string */ public $suffix = ''; /** @var SlugHandler[] */ public $handlers = []; /** @var string */ public $dateFormat = 'Y-m-d-H:i'; /** * @param string[] $fields * @param SlugHandler[] $handlers */ public function __construct( array $data = [], array $fields = [], bool $updatable = true, string $style = 'default', bool $unique = true, ?string $unique_base = null, string $separator = '-', string $prefix = '', string $suffix = '', array $handlers = [], string $dateFormat = 'Y-m-d-H:i' ) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->fields = $data['fields'] ?? $fields; $this->updatable = $data['updatable'] ?? $updatable; $this->style = $data['style'] ?? $style; $this->unique = $data['unique'] ?? $unique; $this->unique_base = $data['unique_base'] ?? $unique_base; $this->separator = $data['separator'] ?? $separator; $this->prefix = $data['prefix'] ?? $prefix; $this->suffix = $data['suffix'] ?? $suffix; $this->handlers = $data['handlers'] ?? $handlers; $this->dateFormat = $data['dateFormat'] ?? $dateFormat; } } doctrine-extensions/src/Mapping/Annotation/SlugHandler.php 0000644 00000003112 15117737236 0017776 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; use Gedmo\Sluggable\Handler\SlugHandlerInterface; /** * SlugHandler annotation for Sluggable behavioral extension * * @Annotation * @NamedArgumentConstructor * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class SlugHandler implements GedmoAnnotation { /** * @var string * @phpstan-var string|class-string<SlugHandlerInterface> */ public $class = ''; /** * @var array<SlugHandlerOption>|array<array{string, mixed}> */ public $options = []; /** * @phpstan-param string|class-string<SlugHandlerInterface> $class */ public function __construct( array $data = [], string $class = '', array $options = [] ) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->class = $data['class'] ?? $class; $this->options = $data['options'] ?? $options; } } doctrine-extensions/src/Mapping/Annotation/SlugHandlerOption.php 0000644 00000002433 15117737236 0021174 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * SlugHandlerOption annotation for Sluggable behavioral extension * * @Annotation * @NamedArgumentConstructor * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class SlugHandlerOption implements GedmoAnnotation { /** * @var string */ public $name; /** * @var mixed */ public $value; /** * @param mixed $value */ public function __construct( array $data = [], string $name = '', $value = null ) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->name = $data['name'] ?? $name; $this->value = $data['value'] ?? $value; } } doctrine-extensions/src/Mapping/Annotation/SoftDeleteable.php 0000644 00000002705 15117737236 0020457 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Group annotation for SoftDeleteable extension * * @author Gustavo Falco <comfortablynumb84@gmail.com> * * @Annotation * @NamedArgumentConstructor * @Target("CLASS") */ #[Attribute(Attribute::TARGET_CLASS)] final class SoftDeleteable implements GedmoAnnotation { /** @var string */ public $fieldName = 'deletedAt'; /** @var bool */ public $timeAware = false; /** @var bool */ public $hardDelete = true; public function __construct(array $data = [], string $fieldName = 'deletedAt', bool $timeAware = false, bool $hardDelete = true) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->fieldName = $data['fieldName'] ?? $fieldName; $this->timeAware = $data['timeAware'] ?? $timeAware; $this->hardDelete = $data['hardDelete'] ?? $hardDelete; } } doctrine-extensions/src/Mapping/Annotation/SortableGroup.php 0000644 00000001272 15117737236 0020363 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Group annotation for Sortable extension * * @author Lukas Botsch <lukas.botsch@gmail.com> * * @Annotation * @Target("PROPERTY") */ #[Attribute(Attribute::TARGET_PROPERTY)] final class SortableGroup implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/SortablePosition.php 0000644 00000001300 15117737236 0021063 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Position annotation for Sortable extension * * @author Lukas Botsch <lukas.botsch@gmail.com> * * @Annotation * @Target("PROPERTY") */ #[Attribute(Attribute::TARGET_PROPERTY)] final class SortablePosition implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/Timestampable.php 0000644 00000002734 15117737236 0020366 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Timestampable annotation for Timestampable behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("PROPERTY") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Timestampable implements GedmoAnnotation { /** @var string */ public $on = 'update'; /** @var string|string[] */ public $field; /** @var mixed */ public $value; /** * @param string|string[] $field * @param mixed $value */ public function __construct(array $data = [], string $on = 'update', $field = null, $value = null) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->on = $data['on'] ?? $on; $this->field = $data['field'] ?? $field; $this->value = $data['value'] ?? $value; } } doctrine-extensions/src/Mapping/Annotation/Translatable.php 0000644 00000002264 15117737236 0020211 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Translatable annotation for Translatable behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("PROPERTY") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Translatable implements GedmoAnnotation { /** @var bool|null */ public $fallback; public function __construct(array $data = [], ?bool $fallback = null) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->fallback = $data['fallback'] ?? $fallback; } } doctrine-extensions/src/Mapping/Annotation/TranslationEntity.php 0000644 00000002257 15117737236 0021272 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * TranslationEntity annotation for Translatable behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("CLASS") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_CLASS)] final class TranslationEntity implements GedmoAnnotation { /** @var string @Required */ public $class; public function __construct(array $data = [], string $class = '') { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->class = $data['class'] ?? $class; } } doctrine-extensions/src/Mapping/Annotation/Tree.php 0000644 00000003760 15117737236 0016476 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Tree annotation for Tree behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("CLASS") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_CLASS)] final class Tree implements GedmoAnnotation { /** * @var string * @phpstan-var 'closure'|'materializedPath'|'nested' */ public $type = 'nested'; /** @var bool */ public $activateLocking = false; /** * @var int * @phpstan-var positive-int */ public $lockingTimeout = 3; /** * @var string|null * * @deprecated to be removed in 4.0, unused, configure the property on the TreeRoot annotation instead */ public $identifierMethod; /** * @phpstan-param 'closure'|'materializedPath'|'nested'|null $type */ public function __construct( array $data = [], ?string $type = null, bool $activateLocking = false, int $lockingTimeout = 3, ?string $identifierMethod = null ) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->type = $data['type'] ?? $type; $this->activateLocking = $data['activateLocking'] ?? $activateLocking; $this->lockingTimeout = $data['lockingTimeout'] ?? $lockingTimeout; $this->identifierMethod = $data['identifierMethod'] ?? $identifierMethod; } } doctrine-extensions/src/Mapping/Annotation/TreeClosure.php 0000644 00000002540 15117737236 0020026 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; /** * TreeClosure annotation for Tree behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("CLASS") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_CLASS)] final class TreeClosure implements GedmoAnnotation { /** * @var string * @phpstan-var string|class-string<AbstractClosure> */ public $class; /** * @phpstan-param string|class-string<AbstractClosure> $class */ public function __construct(array $data = [], string $class = '') { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->class = $data['class'] ?? $class; } } doctrine-extensions/src/Mapping/Annotation/TreeLeft.php 0000644 00000001321 15117737236 0017300 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * TreeLeft annotation for Tree behavioral extension * * @Annotation * @Target("PROPERTY") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class TreeLeft implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/TreeLevel.php 0000644 00000001323 15117737236 0017457 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * TreeLevel annotation for Tree behavioral extension * * @Annotation * @Target("PROPERTY") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class TreeLevel implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/TreeLockTime.php 0000644 00000001420 15117737236 0020115 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * TreeLockTime annotation for Tree behavioral extension * * @Annotation * @Target("PROPERTY") * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class TreeLockTime implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/TreeParent.php 0000644 00000001325 15117737236 0017643 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * TreeParent annotation for Tree behavioral extension * * @Annotation * @Target("PROPERTY") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class TreeParent implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/TreePath.php 0000644 00000003442 15117737236 0017310 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * TreePath annotation for Tree behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("PROPERTY") * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author <rocco@roccosportal.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class TreePath implements GedmoAnnotation { /** @var string */ public $separator = ','; /** @var bool|null */ public $appendId; /** @var bool */ public $startsWithSeparator = false; /** @var bool */ public $endsWithSeparator = true; public function __construct( array $data = [], string $separator = ',', ?bool $appendId = null, bool $startsWithSeparator = false, bool $endsWithSeparator = true ) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->separator = $data['separator'] ?? $separator; $this->appendId = $data['appendId'] ?? $appendId; $this->startsWithSeparator = $data['startsWithSeparator'] ?? $startsWithSeparator; $this->endsWithSeparator = $data['endsWithSeparator'] ?? $endsWithSeparator; } } doctrine-extensions/src/Mapping/Annotation/TreePathHash.php 0000644 00000001266 15117737236 0020116 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * TreePath annotation for Tree behavioral extension * * @Annotation * @Target("PROPERTY") * * @author <rocco@roccosportal.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class TreePathHash implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/TreePathSource.php 0000644 00000001416 15117737236 0020470 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * TreePath annotation for Tree behavioral extension * * @Annotation * @Target("PROPERTY") * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class TreePathSource implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/TreeRight.php 0000644 00000001323 15117737236 0017465 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * TreeRight annotation for Tree behavioral extension * * @Annotation * @Target("PROPERTY") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class TreeRight implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/TreeRoot.php 0000644 00000002320 15117737236 0017331 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * TreeRoot annotation for Tree behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("PROPERTY") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class TreeRoot implements GedmoAnnotation { /** @var string|null */ public $identifierMethod; public function __construct(array $data = [], ?string $identifierMethod = null) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->identifierMethod = $data['identifierMethod'] ?? $identifierMethod; } } doctrine-extensions/src/Mapping/Annotation/Uploadable.php 0000644 00000005521 15117737236 0017644 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; use Gedmo\Uploadable\Mapping\Validator; /** * Uploadable annotation for Uploadable behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("CLASS") * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_CLASS)] final class Uploadable implements GedmoAnnotation { /** * @var bool */ public $allowOverwrite = false; /** * @var bool */ public $appendNumber = false; /** * @var string */ public $path = ''; /** * @var string */ public $pathMethod = ''; /** * @var string */ public $callback = ''; /** * @var string */ public $filenameGenerator = Validator::FILENAME_GENERATOR_NONE; /** * @var string */ public $maxSize = '0'; /** * @var string A list of comma separate values of allowed types, like "text/plain,text/css" */ public $allowedTypes = ''; /** * @var string A list of comma separate values of disallowed types, like "video/jpeg,text/html" */ public $disallowedTypes = ''; public function __construct( array $data = [], bool $allowOverwrite = false, bool $appendNumber = false, string $path = '', string $pathMethod = '', string $callback = '', string $filenameGenerator = Validator::FILENAME_GENERATOR_NONE, string $maxSize = '0', string $allowedTypes = '', string $disallowedTypes = '' ) { if ([] !== $data) { @trigger_error(sprintf( 'Passing an array as first argument to "%s()" is deprecated. Use named arguments instead.', __METHOD__ ), E_USER_DEPRECATED); } $this->allowOverwrite = $data['allowOverwrite'] ?? $allowOverwrite; $this->appendNumber = $data['appendNumber'] ?? $appendNumber; $this->path = $data['path'] ?? $path; $this->pathMethod = $data['pathMethod'] ?? $pathMethod; $this->callback = $data['callback'] ?? $callback; $this->filenameGenerator = $data['filenameGenerator'] ?? $filenameGenerator; $this->maxSize = $data['maxSize'] ?? $maxSize; $this->allowedTypes = $data['allowedTypes'] ?? $allowedTypes; $this->disallowedTypes = $data['disallowedTypes'] ?? $disallowedTypes; } } doctrine-extensions/src/Mapping/Annotation/UploadableFileMimeType.php 0000644 00000001452 15117737236 0022115 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * UploadableFileMimeType Annotation for Uploadable behavioral extension * * @Annotation * @Target("PROPERTY") * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class UploadableFileMimeType implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/UploadableFileName.php 0000644 00000001323 15117737236 0021241 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * UploadableFileName Annotation for Uploadable behavioral extension * * @Annotation * @Target("PROPERTY") * * @author tiger-seo <tiger.seo@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class UploadableFileName implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/UploadableFilePath.php 0000644 00000001442 15117737236 0021257 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * UploadableFilePath Annotation for Uploadable behavioral extension * * @Annotation * @Target("PROPERTY") * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class UploadableFilePath implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/UploadableFileSize.php 0000644 00000001442 15117737236 0021275 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * UploadableFileSize Annotation for Uploadable behavioral extension * * @Annotation * @Target("PROPERTY") * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class UploadableFileSize implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Annotation/Versioned.php 0000644 00000001364 15117737236 0017533 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Annotation; use Attribute; use Doctrine\Common\Annotations\Annotation; use Gedmo\Mapping\Annotation\Annotation as GedmoAnnotation; /** * Versioned annotation for Loggable behavioral extension * * @Annotation * @NamedArgumentConstructor * @Target("PROPERTY") * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ #[Attribute(Attribute::TARGET_PROPERTY)] final class Versioned implements GedmoAnnotation { } doctrine-extensions/src/Mapping/Driver/AbstractAnnotationDriver.php 0000644 00000007305 15117737236 0021671 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Driver; use Doctrine\Common\Annotations\Reader; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Mapping\Driver\MappingDriver; /** * This is an abstract class to implement common functionality * for extension annotation mapping drivers. * * @author Derek J. Lambert <dlambert@dereklambert.com> */ abstract class AbstractAnnotationDriver implements AnnotationDriverInterface { /** * Annotation reader instance * * @var Reader|AttributeReader|object * * @todo Remove the support for the `object` type in the next major release. */ protected $reader; /** * Original driver if it is available * * @var MappingDriver */ protected $_originalDriver; /** * List of types which are valid for extension * * @var string[] */ protected $validTypes = []; public function setAnnotationReader($reader) { if (!$reader instanceof Reader && !$reader instanceof AttributeReader) { trigger_deprecation( 'gedmo/doctrine-extensions', '3.11', 'Passing an object not implementing "%s" or "%s" as argument 1 to "%s()" is deprecated and' .' will throw an "%s" error in version 4.0. Instance of "%s" given.', Reader::class, AttributeReader::class, __METHOD__, \TypeError::class, get_class($reader) ); } $this->reader = $reader; } /** * Passes in the mapping read by original driver * * @param MappingDriver $driver * * @return void */ public function setOriginalDriver($driver) { $this->_originalDriver = $driver; } /** * @param ClassMetadata $meta * * @return \ReflectionClass */ public function getMetaReflectionClass($meta) { $class = $meta->getReflectionClass(); if (!$class) { // based on recent doctrine 2.3.0-DEV maybe will be fixed in some way // this happens when running annotation driver in combination with // static reflection services. This is not the nicest fix $class = new \ReflectionClass($meta->getName()); } return $class; } /** * @return void */ public function validateFullMetadata(ClassMetadata $meta, array $config) { } /** * Checks if $field type is valid * * @param ClassMetadata $meta * @param string $field * * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], $this->validTypes, true); } /** * Try to find out related class name out of mapping * * @param ClassMetadata $metadata the mapped class metadata * @param string $name the related object class name * * @return string related class name or empty string if does not exist */ protected function getRelatedClassName($metadata, $name) { if (class_exists($name) || interface_exists($name)) { return $name; } $refl = $metadata->getReflectionClass(); $ns = $refl->getNamespaceName(); $className = $ns.'\\'.$name; return class_exists($className) ? $className : ''; } } doctrine-extensions/src/Mapping/Driver/AnnotationDriverInterface.php 0000644 00000002574 15117737236 0022031 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Driver; use Doctrine\Common\Annotations\Reader; use Gedmo\Mapping\Driver; /** * Annotation driver interface, provides method * to set custom annotation reader. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface AnnotationDriverInterface extends Driver { /** * 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 a `Doctrine\Common\Annotations\Reader` or `Gedmo\Mapping\Driver\AttributeReader` will be required */ public function setAnnotationReader($reader); } doctrine-extensions/src/Mapping/Driver/AttributeAnnotationReader.php 0000644 00000006311 15117737236 0022034 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Driver; use Doctrine\Common\Annotations\Reader; use Gedmo\Mapping\Annotation\Annotation; use ReflectionClass; use ReflectionMethod; /** * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @license MIT License (http://www.opensource.org/licenses/mit-license.php) * * @internal */ final class AttributeAnnotationReader implements Reader { /** * @var Reader */ private $annotationReader; /** * @var AttributeReader */ private $attributeReader; public function __construct(AttributeReader $attributeReader, Reader $annotationReader) { $this->attributeReader = $attributeReader; $this->annotationReader = $annotationReader; } /** * @return Annotation[] */ public function getClassAnnotations(ReflectionClass $class): array { $annotations = $this->attributeReader->getClassAnnotations($class); if ([] !== $annotations) { return $annotations; } return $this->annotationReader->getClassAnnotations($class); } /** * @param class-string<T> $annotationName the name of the annotation * * @return T|null the Annotation or NULL, if the requested annotation does not exist * * @template T */ public function getClassAnnotation(ReflectionClass $class, $annotationName) { $annotation = $this->attributeReader->getClassAnnotation($class, $annotationName); if (null !== $annotation) { return $annotation; } return $this->annotationReader->getClassAnnotation($class, $annotationName); } /** * @return Annotation[] */ public function getPropertyAnnotations(\ReflectionProperty $property): array { $propertyAnnotations = $this->attributeReader->getPropertyAnnotations($property); if ([] !== $propertyAnnotations) { return $propertyAnnotations; } return $this->annotationReader->getPropertyAnnotations($property); } /** * @param class-string<T> $annotationName the name of the annotation * * @return T|null the Annotation or NULL, if the requested annotation does not exist * * @template T */ public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName) { $annotation = $this->attributeReader->getPropertyAnnotation($property, $annotationName); if (null !== $annotation) { return $annotation; } return $this->annotationReader->getPropertyAnnotation($property, $annotationName); } public function getMethodAnnotations(ReflectionMethod $method): array { throw new \BadMethodCallException('Not implemented'); } /** * @return mixed */ public function getMethodAnnotation(ReflectionMethod $method, $annotationName) { throw new \BadMethodCallException('Not implemented'); } } doctrine-extensions/src/Mapping/Driver/AttributeDriverInterface.php 0000644 00000000760 15117737236 0021655 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Driver; /** * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @internal */ interface AttributeDriverInterface extends AnnotationDriverInterface { } doctrine-extensions/src/Mapping/Driver/AttributeReader.php 0000644 00000006200 15117737236 0017776 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Driver; use Attribute; use Gedmo\Mapping\Annotation\Annotation; use ReflectionClass; /** * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @internal */ final class AttributeReader { /** @var array<string,bool> */ private $isRepeatableAttribute = []; /** * @return array<Annotation|Annotation[]> */ public function getClassAnnotations(ReflectionClass $class): array { return $this->convertToAttributeInstances($class->getAttributes()); } /** * @phpstan-param class-string $annotationName * * @return Annotation|Annotation[]|null */ public function getClassAnnotation(ReflectionClass $class, string $annotationName) { return $this->getClassAnnotations($class)[$annotationName] ?? null; } /** * @return array<Annotation|Annotation[]> */ public function getPropertyAnnotations(\ReflectionProperty $property): array { return $this->convertToAttributeInstances($property->getAttributes()); } /** * @phpstan-param class-string $annotationName * * @return Annotation|Annotation[]|null */ public function getPropertyAnnotation(\ReflectionProperty $property, string $annotationName) { return $this->getPropertyAnnotations($property)[$annotationName] ?? null; } /** * @param array<\ReflectionAttribute> $attributes * * @return array<string, Annotation|Annotation[]> */ private function convertToAttributeInstances(array $attributes): array { $instances = []; foreach ($attributes as $attribute) { $attributeName = $attribute->getName(); assert(is_string($attributeName)); // Make sure we only get Gedmo Annotations if (!is_subclass_of($attributeName, Annotation::class)) { continue; } $instance = $attribute->newInstance(); assert($instance instanceof Annotation); if ($this->isRepeatable($attributeName)) { if (!isset($instances[$attributeName])) { $instances[$attributeName] = []; } $instances[$attributeName][] = $instance; } else { $instances[$attributeName] = $instance; } } return $instances; } private function isRepeatable(string $attributeClassName): bool { if (isset($this->isRepeatableAttribute[$attributeClassName])) { return $this->isRepeatableAttribute[$attributeClassName]; } $reflectionClass = new ReflectionClass($attributeClassName); $attribute = $reflectionClass->getAttributes()[0]->newInstance(); return $this->isRepeatableAttribute[$attributeClassName] = ($attribute->flags & Attribute::IS_REPEATABLE) > 0; } } doctrine-extensions/src/Mapping/Driver/Chain.php 0000644 00000004574 15117737236 0015746 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver; /** * The chain mapping driver enables chained * extension mapping driver support * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class Chain implements Driver { /** * The default driver * * @var Driver|null */ private $defaultDriver; /** * List of drivers nested * * @var Driver[] */ private $_drivers = []; /** * Add a nested driver. * * @param string $namespace * * @return void */ public function addDriver(Driver $nestedDriver, $namespace) { $this->_drivers[$namespace] = $nestedDriver; } /** * Get the array of nested drivers. * * @return Driver[] $drivers */ public function getDrivers() { return $this->_drivers; } /** * Get the default driver. * * @return Driver|null */ public function getDefaultDriver() { return $this->defaultDriver; } /** * Set the default driver. * * @return void */ public function setDefaultDriver(Driver $driver) { $this->defaultDriver = $driver; } public function readExtendedMetadata($meta, array &$config) { foreach ($this->_drivers as $namespace => $driver) { if (0 === strpos($meta->getName(), $namespace)) { $driver->readExtendedMetadata($meta, $config); return; } } if (null !== $this->defaultDriver) { $this->defaultDriver->readExtendedMetadata($meta, $config); return; } // commenting it for customized mapping support, debugging of such cases might get harder // throw new \Gedmo\Exception\UnexpectedValueException('Class ' . $meta->getName() . ' is not a valid entity or mapped super class.'); } /** * Passes in the mapping read by original driver */ public function setOriginalDriver($driver) { // not needed here } } doctrine-extensions/src/Mapping/Driver/File.php 0000644 00000007513 15117737236 0015577 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Mapping\Driver\FileDriver; use Doctrine\Persistence\Mapping\Driver\FileLocator; use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Gedmo\Mapping\Driver; /** * The mapping FileDriver abstract class, defines the * metadata extraction function common among * all drivers used on these extensions by file based * drivers. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ abstract class File implements Driver { /** * @var FileLocator */ protected $locator; /** * File extension, must be set in child class * * @var string */ protected $_extension; /** * original driver if it is available * * @var MappingDriver */ protected $_originalDriver; /** * @deprecated since gedmo/doctrine-extensions 3.3, will be removed in version 4.0. * * @var string[] */ protected $_paths = []; /** * @return void */ public function setLocator(FileLocator $locator) { $this->locator = $locator; } /** * Set the paths for file lookup * * @deprecated since gedmo/doctrine-extensions 3.3, will be removed in version 4.0. * * @param string[] $paths * * @return void */ public function setPaths($paths) { $this->_paths = (array) $paths; } /** * Set the file extension * * @param string $extension * * @return void */ public function setExtension($extension) { $this->_extension = $extension; } /** * Passes in the mapping read by original driver * * @param MappingDriver $driver * * @return void */ public function setOriginalDriver($driver) { $this->_originalDriver = $driver; } /** * Loads a mapping file with the given name and returns a map * from class/entity names to their corresponding elements. * * @param string $file the mapping file to load * * @return array */ abstract protected function _loadMappingFile($file); /** * Tries to get a mapping for a given class * * @param string $className * * @return array|object|null */ protected function _getMapping($className) { // try loading mapping from original driver first $mapping = null; if (null !== $this->_originalDriver) { if ($this->_originalDriver instanceof FileDriver) { $mapping = $this->_originalDriver->getElement($className); } } // if no mapping found try to load mapping file again if (null === $mapping) { $yaml = $this->_loadMappingFile($this->locator->findMappingFile($className)); $mapping = $yaml[$className]; } return $mapping; } /** * Try to find out related class name out of mapping * * @param ClassMetadata $metadata the mapped class metadata * @param string $name the related object class name * * @return string related class name or empty string if does not exist */ protected function getRelatedClassName($metadata, $name) { if (class_exists($name) || interface_exists($name)) { return $name; } $refl = $metadata->getReflectionClass(); $ns = $refl->getNamespaceName(); $className = $ns.'\\'.$name; return class_exists($className) ? $className : ''; } } doctrine-extensions/src/Mapping/Driver/Xml.php 0000644 00000007363 15117737236 0015463 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use SimpleXMLElement; /** * The mapping XmlDriver abstract class, defines the * metadata extraction function common among all * all drivers used on these extensions by file based * drivers. * * @author Miha Vrhovnik <miha.vrhovnik@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ abstract class Xml extends File { public const GEDMO_NAMESPACE_URI = 'http://gediminasm.org/schemas/orm/doctrine-extensions-mapping'; public const DOCTRINE_NAMESPACE_URI = 'http://doctrine-project.org/schemas/orm/doctrine-mapping'; /** * File extension * * @var string */ protected $_extension = '.dcm.xml'; /** * Get attribute value. * As we are supporting namespaces the only way to get to the attributes under a node is to use attributes function on it * * @param string $attributeName * * @return string */ protected function _getAttribute(SimpleXmlElement $node, $attributeName) { $attributes = $node->attributes(); return (string) $attributes[$attributeName]; } /** * Get boolean attribute value. * As we are supporting namespaces the only way to get to the attributes under a node is to use attributes function on it * * @param string $attributeName * * @return bool */ protected function _getBooleanAttribute(SimpleXmlElement $node, $attributeName) { $rawValue = strtolower($this->_getAttribute($node, $attributeName)); if ('1' === $rawValue || 'true' === $rawValue) { return true; } if ('0' === $rawValue || 'false' === $rawValue) { return false; } throw new InvalidMappingException(sprintf("Attribute %s must have a valid boolean value, '%s' found", $attributeName, $this->_getAttribute($node, $attributeName))); } /** * does attribute exist under a specific node * As we are supporting namespaces the only way to get to the attributes under a node is to use attributes function on it * * @param string $attributeName * * @return bool */ protected function _isAttributeSet(SimpleXmlElement $node, $attributeName) { $attributes = $node->attributes(); return isset($attributes[$attributeName]); } protected function _loadMappingFile($file) { $result = []; // We avoid calling `simplexml_load_file()` in order to prevent file operations in libXML. // If `libxml_disable_entity_loader(true)` is called before, `simplexml_load_file()` fails, // that's why we use `simplexml_load_string()` instead. // @see https://bugs.php.net/bug.php?id=62577. $xmlElement = simplexml_load_string(file_get_contents($file)); $xmlElement = $xmlElement->children(self::DOCTRINE_NAMESPACE_URI); if (isset($xmlElement->entity)) { foreach ($xmlElement->entity as $entityElement) { $entityName = $this->_getAttribute($entityElement, 'name'); $result[$entityName] = $entityElement; } } elseif (isset($xmlElement->{'mapped-superclass'})) { foreach ($xmlElement->{'mapped-superclass'} as $mappedSuperClass) { $className = $this->_getAttribute($mappedSuperClass, 'name'); $result[$className] = $mappedSuperClass; } } return $result; } } doctrine-extensions/src/Mapping/Event/Adapter/ODM.php 0000644 00000010453 15117737236 0016542 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Event\Adapter; use Doctrine\Common\EventArgs; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Gedmo\Exception\RuntimeException; use Gedmo\Mapping\Event\AdapterInterface; /** * Doctrine event adapter for ODM specific * event arguments * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class ODM implements AdapterInterface { /** * @var \Doctrine\Common\EventArgs */ private $args; /** * @var \Doctrine\ODM\MongoDB\DocumentManager */ private $dm; public function __call($method, $args) { @trigger_error(sprintf( 'Using "%s()" method is deprecated since gedmo/doctrine-extensions 3.5 and will be removed in version 4.0.', __METHOD__ ), E_USER_DEPRECATED); if (null === $this->args) { throw new RuntimeException('Event args must be set before calling its methods'); } $method = str_replace('Object', $this->getDomainObjectName(), $method); return call_user_func_array([$this->args, $method], $args); } public function setEventArgs(EventArgs $args) { $this->args = $args; } public function getDomainObjectName() { return 'Document'; } public function getManagerName() { return 'ODM'; } /** * @param ClassMetadata $meta */ public function getRootObjectClass($meta) { return $meta->rootDocumentName; } /** * Set the document manager * * @return void */ public function setDocumentManager(DocumentManager $dm) { $this->dm = $dm; } /** * @return DocumentManager */ public function getObjectManager() { if (null !== $this->dm) { return $this->dm; } if (null === $this->args) { throw new \LogicException(sprintf('Event args must be set before calling "%s()".', __METHOD__)); } return $this->args->getDocumentManager(); } public function getObject(): object { if (null === $this->args) { throw new \LogicException(sprintf('Event args must be set before calling "%s()".', __METHOD__)); } return $this->args->getDocument(); } public function getObjectState($uow, $object) { return $uow->getDocumentState($object); } public function getObjectChangeSet($uow, $object) { return $uow->getDocumentChangeSet($object); } public function getSingleIdentifierFieldName($meta) { return $meta->getIdentifier()[0]; } public function recomputeSingleObjectChangeSet($uow, $meta, $object) { $uow->recomputeSingleDocumentChangeSet($meta, $object); } public function getScheduledObjectUpdates($uow) { $updates = $uow->getScheduledDocumentUpdates(); $upserts = $uow->getScheduledDocumentUpserts(); return array_merge($updates, $upserts); } public function getScheduledObjectInsertions($uow) { return $uow->getScheduledDocumentInsertions(); } public function getScheduledObjectDeletions($uow) { return $uow->getScheduledDocumentDeletions(); } public function setOriginalObjectProperty($uow, $object, $property, $value) { $uow->setOriginalDocumentProperty(spl_object_hash($object), $property, $value); } public function clearObjectChangeSet($uow, $object) { $uow->clearDocumentChangeSet(spl_object_hash($object)); } /** * Creates a ODM specific LifecycleEventArgs. * * @param object $document * @param \Doctrine\ODM\MongoDB\DocumentManager $documentManager * * @return \Doctrine\ODM\MongoDB\Event\LifecycleEventArgs */ public function createLifecycleEventArgsInstance($document, $documentManager) { return new LifecycleEventArgs($document, $documentManager); } } doctrine-extensions/src/Mapping/Event/Adapter/ORM.php 0000644 00000010310 15117737236 0016550 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Event\Adapter; use Doctrine\Common\EventArgs; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\ORM\Mapping\ClassMetadata; use Gedmo\Exception\RuntimeException; use Gedmo\Mapping\Event\AdapterInterface; /** * Doctrine event adapter for ORM specific * event arguments * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class ORM implements AdapterInterface { /** * @var \Doctrine\Common\EventArgs */ private $args; /** * @var \Doctrine\ORM\EntityManagerInterface */ private $em; public function __call($method, $args) { @trigger_error(sprintf( 'Using "%s()" method is deprecated since gedmo/doctrine-extensions 3.5 and will be removed in version 4.0.', __METHOD__ ), E_USER_DEPRECATED); if (null === $this->args) { throw new RuntimeException('Event args must be set before calling its methods'); } $method = str_replace('Object', $this->getDomainObjectName(), $method); return call_user_func_array([$this->args, $method], $args); } public function setEventArgs(EventArgs $args) { $this->args = $args; } public function getDomainObjectName() { return 'Entity'; } public function getManagerName() { return 'ORM'; } /** * @param ClassMetadata $meta */ public function getRootObjectClass($meta) { return $meta->rootEntityName; } /** * Set the entity manager * * @return void */ public function setEntityManager(EntityManagerInterface $em) { $this->em = $em; } /** * @return EntityManagerInterface */ public function getObjectManager() { if (null !== $this->em) { return $this->em; } if (null === $this->args) { throw new \LogicException(sprintf('Event args must be set before calling "%s()".', __METHOD__)); } return $this->args->getEntityManager(); } public function getObject(): object { if (null === $this->args) { throw new \LogicException(sprintf('Event args must be set before calling "%s()".', __METHOD__)); } return $this->args->getEntity(); } public function getObjectState($uow, $object) { return $uow->getEntityState($object); } public function getObjectChangeSet($uow, $object) { return $uow->getEntityChangeSet($object); } /** * @param ClassMetadata $meta */ public function getSingleIdentifierFieldName($meta) { return $meta->getSingleIdentifierFieldName(); } public function recomputeSingleObjectChangeSet($uow, $meta, $object) { $uow->recomputeSingleEntityChangeSet($meta, $object); } public function getScheduledObjectUpdates($uow) { return $uow->getScheduledEntityUpdates(); } public function getScheduledObjectInsertions($uow) { return $uow->getScheduledEntityInsertions(); } public function getScheduledObjectDeletions($uow) { return $uow->getScheduledEntityDeletions(); } public function setOriginalObjectProperty($uow, $object, $property, $value) { $uow->setOriginalEntityProperty(spl_object_id($object), $property, $value); } public function clearObjectChangeSet($uow, $object) { $uow->clearEntityChangeSet(spl_object_id($object)); } /** * Creates a ORM specific LifecycleEventArgs. * * @param object $document * @param \Doctrine\ORM\EntityManagerInterface $entityManager * * @return \Doctrine\ORM\Event\LifecycleEventArgs */ public function createLifecycleEventArgsInstance($document, $entityManager) { return new LifecycleEventArgs($document, $entityManager); } } doctrine-extensions/src/Mapping/Event/AdapterInterface.php 0000644 00000011433 15117737236 0017743 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping\Event; use Doctrine\Common\EventArgs; use Doctrine\ODM\MongoDB\UnitOfWork as MongoDBUnitOfWork; use Doctrine\ORM\UnitOfWork as ORMUnitOfWork; use Doctrine\Persistence\Event\LifecycleEventArgs; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; /** * Doctrine event adapter for Doctrine extensions. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @method LifecycleEventArgs createLifecycleEventArgsInstance(object $object, ObjectManager $manager) * @method object getObject() */ interface AdapterInterface { /** * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * Calls a method on the event args object. * * @param string $method * @param array $args * * @return mixed */ public function __call($method, $args); /** * Set the event args object. * * @return void */ public function setEventArgs(EventArgs $args); /** * Get the name of the domain object. * * @return string */ public function getDomainObjectName(); /** * Get the name of the manager used by this adapter. * * @return string */ public function getManagerName(); /** * Get the root object class, handles inheritance * * @param ClassMetadata $meta * * @return string * @phpstan-return class-string */ public function getRootObjectClass($meta); /** * Get the object manager. * * @return ObjectManager */ public function getObjectManager(); /** * Gets the state of an object from the unit of work. * * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager * @param object $object * * @return int The object state as reported by the unit of work */ public function getObjectState($uow, $object); /** * Gets the changeset for an object from the unit of work. * * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager * @param object $object * * @return array */ public function getObjectChangeSet($uow, $object); /** * Get the single identifier field name. * * @param ClassMetadata $meta * * @return string */ public function getSingleIdentifierFieldName($meta); /** * Computes the changeset of an individual object, independently of the * computeChangeSets() routine that is used at the beginning of a unit * of work's commit. * * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager * @param ClassMetadata $meta * @param object $object * * @return void */ public function recomputeSingleObjectChangeSet($uow, $meta, $object); /** * Gets the currently scheduled object updates from the unit of work. * * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager * * @return array */ public function getScheduledObjectUpdates($uow); /** * Gets the currently scheduled object insertions in the unit of work. * * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager * * @return array */ public function getScheduledObjectInsertions($uow); /** * Gets the currently scheduled object deletions in the unit of work. * * @param ORMUnitOfWork|MongoDBUnitOfWork $uow The UnitOfWork as provided by the object manager * * @return array */ public function getScheduledObjectDeletions($uow); /** * Sets a property value of the original data array of an object. * * @param ORMUnitOfWork|MongoDBUnitOfWork $uow * @param object $object * @param string $property * @param mixed $value * * @return void */ public function setOriginalObjectProperty($uow, $object, $property, $value); /** * Clears the property changeset of the object with the given OID. * * @param ORMUnitOfWork|MongoDBUnitOfWork $uow * @param object $object * * @return void */ public function clearObjectChangeSet($uow, $object); } doctrine-extensions/src/Mapping/Driver.php 0000644 00000002507 15117737236 0014716 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as OdmClassMetadata; use Doctrine\ORM\Mapping\ClassMetadata as OrmClassMetadata; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Gedmo\Exception\InvalidMappingException; /** * The mapping driver interface defines the metadata extraction functions * common among all drivers used on these extensions. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface Driver { /** * Read the extended metadata configuration for a single mapped class. * * @param ClassMetadata&(OdmClassMetadata|OrmClassMetadata) $meta * * @return void * * @throws InvalidMappingException if the mapping configuration is invalid */ public function readExtendedMetadata($meta, array &$config); /** * Sets the original mapping driver. * * @param MappingDriver $driver * * @return void */ public function setOriginalDriver($driver); } doctrine-extensions/src/Mapping/ExtensionMetadataFactory.php 0000644 00000021022 15117737236 0020421 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping; use Doctrine\Bundle\DoctrineBundle\Mapping\MappingDriver as DoctrineBundleMappingDriver; use Doctrine\Common\Annotations\Reader; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as DocumentClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo as EntityClassMetadata; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Mapping\Driver\DefaultFileLocator; use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; use Doctrine\Persistence\Mapping\Driver\SymfonyFileLocator; use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\Driver\AnnotationDriverInterface; use Gedmo\Mapping\Driver\AttributeAnnotationReader; use Gedmo\Mapping\Driver\AttributeDriverInterface; use Gedmo\Mapping\Driver\AttributeReader; use Gedmo\Mapping\Driver\File as FileDriver; use Psr\Cache\CacheItemPoolInterface; /** * The extension metadata factory is responsible for extension driver * initialization and fully reading the extension metadata * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class ExtensionMetadataFactory { /** * Extension driver * * @var \Gedmo\Mapping\Driver */ protected $driver; /** * Object manager, entity or document * * @var ObjectManager */ protected $objectManager; /** * Extension namespace * * @var string */ protected $extensionNamespace; /** * Custom annotation reader * * @var Reader|AttributeReader|object */ protected $annotationReader; /** * @var CacheItemPoolInterface|null */ private $cacheItemPool; /** * @param Reader|AttributeReader|object $annotationReader */ public function __construct(ObjectManager $objectManager, string $extensionNamespace, object $annotationReader, ?CacheItemPoolInterface $cacheItemPool = null) { if (!$annotationReader instanceof Reader && !$annotationReader instanceof AttributeReader) { trigger_deprecation( 'gedmo/doctrine-extensions', '3.11', 'Providing an annotation reader which does not implement %s or is not an instance of %s to %s is deprecated.', Reader::class, AttributeReader::class, static::class ); } $this->objectManager = $objectManager; $this->annotationReader = $annotationReader; $this->extensionNamespace = $extensionNamespace; $omDriver = $objectManager->getConfiguration()->getMetadataDriverImpl(); $this->driver = $this->getDriver($omDriver); $this->cacheItemPool = $cacheItemPool; } /** * Reads extension metadata * * @param ClassMetadata&(DocumentClassMetadata|EntityClassMetadata) $meta * * @return array the metatada configuration */ public function getExtensionMetadata($meta) { if ($meta->isMappedSuperclass) { return []; // ignore mappedSuperclasses for now } $config = []; $cmf = $this->objectManager->getMetadataFactory(); $useObjectName = $meta->getName(); // collect metadata from inherited classes if (null !== $meta->reflClass) { foreach (array_reverse(class_parents($meta->getName())) as $parentClass) { // read only inherited mapped classes if ($cmf->hasMetadataFor($parentClass)) { /** @var DocumentClassMetadata|EntityClassMetadata $class */ $class = $this->objectManager->getClassMetadata($parentClass); $this->driver->readExtendedMetadata($class, $config); $isBaseInheritanceLevel = !$class->isInheritanceTypeNone() && !$class->parentClasses && $config ; if ($isBaseInheritanceLevel) { $useObjectName = $class->getName(); } } } $this->driver->readExtendedMetadata($meta, $config); } if ($config) { $config['useObjectClass'] = $useObjectName; } $this->storeConfiguration($meta->getName(), $config); return $config; } /** * Get the cache id * * @param string $className * @param string $extensionNamespace * * @return string */ public static function getCacheId($className, $extensionNamespace) { return str_replace('\\', '_', $className).'_$'.strtoupper(str_replace('\\', '_', $extensionNamespace)).'_CLASSMETADATA'; } /** * Get the extended driver instance which will * read the metadata required by extension * * @param MappingDriver $omDriver * * @throws \Gedmo\Exception\RuntimeException if driver was not found in extension * * @return \Gedmo\Mapping\Driver */ protected function getDriver($omDriver) { if ($omDriver instanceof DoctrineBundleMappingDriver) { $omDriver = $omDriver->getDriver(); } $driver = null; $className = get_class($omDriver); $driverName = substr($className, strrpos($className, '\\') + 1); if ($omDriver instanceof MappingDriverChain || 'DriverChain' === $driverName) { $driver = new Driver\Chain(); foreach ($omDriver->getDrivers() as $namespace => $nestedOmDriver) { $driver->addDriver($this->getDriver($nestedOmDriver), $namespace); } if (null !== $omDriver->getDefaultDriver()) { $driver->setDefaultDriver($this->getDriver($omDriver->getDefaultDriver())); } } else { $driverName = substr($driverName, 0, strpos($driverName, 'Driver')); $isSimplified = false; if ('Simplified' === substr($driverName, 0, 10)) { // support for simplified file drivers $driverName = substr($driverName, 10); $isSimplified = true; } // create driver instance $driverClassName = $this->extensionNamespace.'\Mapping\Driver\\'.$driverName; if (!class_exists($driverClassName)) { $driverClassName = $this->extensionNamespace.'\Mapping\Driver\Annotation'; if (!class_exists($driverClassName)) { throw new \Gedmo\Exception\RuntimeException("Failed to fallback to annotation driver: ({$driverClassName}), extension driver was not found."); } } $driver = new $driverClassName(); $driver->setOriginalDriver($omDriver); if ($driver instanceof FileDriver) { if ($omDriver instanceof MappingDriver) { $driver->setLocator($omDriver->getLocator()); // BC for Doctrine 2.2 } elseif ($isSimplified) { $driver->setLocator(new SymfonyFileLocator($omDriver->getNamespacePrefixes(), $omDriver->getFileExtension())); } else { $driver->setLocator(new DefaultFileLocator($omDriver->getPaths(), $omDriver->getFileExtension())); } } if ($driver instanceof AttributeDriverInterface) { if ($this->annotationReader instanceof AttributeReader) { $driver->setAnnotationReader($this->annotationReader); } else { $driver->setAnnotationReader(new AttributeAnnotationReader(new AttributeReader(), $this->annotationReader)); } } elseif ($driver instanceof AnnotationDriverInterface) { $driver->setAnnotationReader($this->annotationReader); } } return $driver; } private function storeConfiguration(string $className, array $config): void { if (null === $this->cacheItemPool) { return; } // Cache the result, even if it's empty, to prevent re-parsing non-existent annotations. $cacheId = self::getCacheId($className, $this->extensionNamespace); $item = $this->cacheItemPool->getItem($cacheId); $this->cacheItemPool->save($item->set($config)); } } doctrine-extensions/src/Mapping/MappedEventSubscriber.php 0000644 00000025467 15117737236 0017731 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Mapping; use function class_exists; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Cache\ArrayCache; use Doctrine\Common\Cache\Psr6\CacheAdapter; use Doctrine\Common\EventArgs; use Doctrine\Common\EventSubscriber; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\Driver\AttributeReader; use Gedmo\Mapping\Event\AdapterInterface; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; /** * This is extension of event subscriber class and is * used specifically for handling the extension metadata * mapping for extensions. * * It dries up some reusable code which is common for * all extensions who maps additional metadata through * extended drivers * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ abstract class MappedEventSubscriber implements EventSubscriber { /** * Static List of cached object configurations * leaving it static for reasons to look into * other listener configuration * * @var array<string, array<string, array<string, mixed>>> * * @phpstan-var array<string, array<class-string, array<string, mixed>>> */ protected static $configurations = []; /** * Listener name, etc: sluggable * * @var string */ protected $name; /** * ExtensionMetadataFactory used to read the extension * metadata through the extension drivers * * @var array<int, ExtensionMetadataFactory> */ private $extensionMetadataFactory = []; /** * List of event adapters used for this listener * * @var array<string, AdapterInterface> */ private $adapters = []; /** * Custom annotation reader * * @var Reader|AttributeReader|object|null */ private $annotationReader; /** * @var AnnotationReader */ private static $defaultAnnotationReader; /** * @var CacheItemPoolInterface|null */ private $cacheItemPool; public function __construct() { $parts = explode('\\', $this->getNamespace()); $this->name = end($parts); } /** * Get the configuration for specific object class * if cache driver is present it scans it also * * @param string $class * @phpstan-param class-string $class * * @return array<string, mixed> */ public function getConfiguration(ObjectManager $objectManager, $class) { if (isset(self::$configurations[$this->name][$class])) { return self::$configurations[$this->name][$class]; } $config = []; $cacheItemPool = $this->getCacheItemPool($objectManager); $cacheId = ExtensionMetadataFactory::getCacheId($class, $this->getNamespace()); $cacheItem = $cacheItemPool->getItem($cacheId); if ($cacheItem->isHit()) { $config = $cacheItem->get(); self::$configurations[$this->name][$class] = $config; } else { // re-generate metadata on cache miss $this->loadMetadataForObjectClass($objectManager, $objectManager->getClassMetadata($class)); if (isset(self::$configurations[$this->name][$class])) { $config = self::$configurations[$this->name][$class]; } } $objectClass = $config['useObjectClass'] ?? $class; if ($objectClass !== $class) { $this->getConfiguration($objectManager, $objectClass); } return $config; } /** * Get extended metadata mapping reader * * @return ExtensionMetadataFactory */ public function getExtensionMetadataFactory(ObjectManager $objectManager) { $oid = spl_object_id($objectManager); if (!isset($this->extensionMetadataFactory[$oid])) { if (null === $this->annotationReader) { // create default annotation reader for extensions $this->annotationReader = $this->getDefaultAnnotationReader(); } $this->extensionMetadataFactory[$oid] = new ExtensionMetadataFactory( $objectManager, $this->getNamespace(), $this->annotationReader, $this->getCacheItemPool($objectManager) ); } return $this->extensionMetadataFactory[$oid]; } /** * 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 a `Doctrine\Common\Annotations\Reader` or `Gedmo\Mapping\Driver\AttributeReader` will be required */ public function setAnnotationReader($reader) { if (!$reader instanceof Reader && !$reader instanceof AttributeReader) { trigger_deprecation( 'gedmo/doctrine-extensions', '3.11', 'Providing an annotation reader which does not implement %s or is not an instance of %s to %s() is deprecated.', Reader::class, AttributeReader::class, __METHOD__ ); } $this->annotationReader = $reader; } final public function setCacheItemPool(CacheItemPoolInterface $cacheItemPool): void { $this->cacheItemPool = $cacheItemPool; } /** * Scans the objects for extended annotations * event subscribers must subscribe to loadClassMetadata event * * @param ClassMetadata $metadata * * @return void */ public function loadMetadataForObjectClass(ObjectManager $objectManager, $metadata) { $factory = $this->getExtensionMetadataFactory($objectManager); try { $config = $factory->getExtensionMetadata($metadata); } catch (\ReflectionException $e) { // entity\document generator is running $config = []; // will not store a cached version, to remap later } if ([] !== $config) { self::$configurations[$this->name][$metadata->getName()] = $config; } } /** * Get an event adapter to handle event specific * methods * * @throws \Gedmo\Exception\InvalidArgumentException if event is not recognized * * @return \Gedmo\Mapping\Event\AdapterInterface */ protected function getEventAdapter(EventArgs $args) { $class = get_class($args); if (preg_match('@Doctrine\\\([^\\\]+)@', $class, $m) && in_array($m[1], ['ODM', 'ORM'], true)) { if (!isset($this->adapters[$m[1]])) { $adapterClass = $this->getNamespace().'\\Mapping\\Event\\Adapter\\'.$m[1]; if (!class_exists($adapterClass)) { $adapterClass = 'Gedmo\\Mapping\\Event\\Adapter\\'.$m[1]; } $this->adapters[$m[1]] = new $adapterClass(); } $this->adapters[$m[1]]->setEventArgs($args); return $this->adapters[$m[1]]; } throw new \Gedmo\Exception\InvalidArgumentException('Event mapper does not support event arg class: '.$class); } /** * Get the namespace of extension event subscriber. * used for cache id of extensions also to know where * to find Mapping drivers and event adapters * * @return string */ abstract protected function getNamespace(); /** * Sets the value for a mapped field * * @param object $object * @param string $field * @param mixed $oldValue * @param mixed $newValue * * @return void */ protected function setFieldValue(AdapterInterface $adapter, $object, $field, $oldValue, $newValue) { $manager = $adapter->getObjectManager(); $meta = $manager->getClassMetadata(get_class($object)); $uow = $manager->getUnitOfWork(); $meta->getReflectionProperty($field)->setValue($object, $newValue); $uow->propertyChanged($object, $field, $oldValue, $newValue); $adapter->recomputeSingleObjectChangeSet($uow, $meta, $object); } /** * Create default annotation reader for extensions */ private function getDefaultAnnotationReader(): Reader { if (null === self::$defaultAnnotationReader) { $reader = new AnnotationReader(); if (class_exists(ArrayAdapter::class)) { $reader = new PsrCachedReader($reader, new ArrayAdapter()); } elseif (class_exists(ArrayCache::class)) { $reader = new PsrCachedReader($reader, CacheAdapter::wrap(new ArrayCache())); } self::$defaultAnnotationReader = $reader; } return self::$defaultAnnotationReader; } private function getCacheItemPool(ObjectManager $objectManager): CacheItemPoolInterface { if (null !== $this->cacheItemPool) { return $this->cacheItemPool; } // TODO: The user should configure its own cache, we are using the one from Doctrine for BC. We should deprecate using // the one from Doctrine when the bundle offers an easy way to configure this cache, otherwise users using the bundle // will see lots of deprecations without an easy way to avoid them. if ($objectManager instanceof EntityManagerInterface || $objectManager instanceof DocumentManager) { $metadataFactory = $objectManager->getMetadataFactory(); $getCache = \Closure::bind(static function (AbstractClassMetadataFactory $metadataFactory): ?CacheItemPoolInterface { return $metadataFactory->getCache(); }, null, \get_class($metadataFactory)); $metadataCache = $getCache($metadataFactory); if (null !== $metadataCache) { $this->cacheItemPool = $metadataCache; return $this->cacheItemPool; } } $this->cacheItemPool = new ArrayAdapter(); return $this->cacheItemPool; } } doctrine-extensions/src/ReferenceIntegrity/Mapping/Driver/Annotation.php 0000644 00000005026 15117737236 0022624 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\ReferenceIntegrity\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\ReferenceIntegrity; use Gedmo\Mapping\Driver\AbstractAnnotationDriver; 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 <evert.harmeling@freshheads.com> * * @internal */ class Annotation extends AbstractAnnotationDriver { /** * 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; } } } } doctrine-extensions/src/ReferenceIntegrity/Mapping/Driver/Attribute.php 0000644 00000001362 15117737236 0022454 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\ReferenceIntegrity\Mapping\Driver; use Gedmo\Mapping\Annotation\ReferenceIntegrity; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for ReferenceIntegrity * behavioral extension. Used for extraction of extended * metadata from attributes specifically for ReferenceIntegrity * extension. * * @internal */ final class Attribute extends Annotation implements AttributeDriverInterface { } doctrine-extensions/src/ReferenceIntegrity/Mapping/Driver/Yaml.php 0000644 00000005023 15117737236 0021411 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\ReferenceIntegrity\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; use Gedmo\ReferenceIntegrity\Mapping\Validator; /** * This is a yaml mapping driver for ReferenceIntegrity * extension. Used for extraction of extended * metadata from yaml specifically for ReferenceIntegrity * extension. * * @author Evert Harmeling <evert.harmeling@freshheads.com> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); $validator = new Validator(); if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $property => $fieldMapping) { if (isset($fieldMapping['gedmo']['referenceIntegrity'])) { if (!$meta->hasField($property)) { throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $property, $meta->getName())); } if (empty($mapping['fields'][$property]['mappedBy'])) { throw new InvalidMappingException(sprintf("'mappedBy' should be set on '%s' in '%s'", $property, $meta->getName())); } if (!in_array($fieldMapping['gedmo']['referenceIntegrity'], $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][$mapping['fields'][$property]['mappedBy']] = $fieldMapping['gedmo']['referenceIntegrity']; } } } } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } } doctrine-extensions/src/ReferenceIntegrity/Mapping/Validator.php 0000644 00000002031 15117737236 0021175 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\ReferenceIntegrity\Mapping; /** * This class is used to validate mapping information * * @author Evert Harmeling <evert.harmeling@freshheads.com> * * @final since gedmo/doctrine-extensions 3.11 */ class Validator { public const NULLIFY = 'nullify'; public const PULL = 'pull'; public const RESTRICT = 'restrict'; /** * List of actions which are valid as integrity check * * @var array */ private $integrityActions = [ self::NULLIFY, self::PULL, self::RESTRICT, ]; /** * Returns a list of available integrity actions * * @return array */ public function getIntegrityActions() { return $this->integrityActions; } } doctrine-extensions/src/ReferenceIntegrity/ReferenceIntegrity.php 0000644 00000002751 15117737236 0021463 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\ReferenceIntegrity; /** * This interface is not necessary but can be implemented for * Entities which in some cases needs to be identified te have * ReferenceIntegrity checks * * @author Evert Harmeling <evert.harmeling@freshheads.com> */ interface ReferenceIntegrity { /* * ReferenceIntegrity expects certain settings to be required * in combination with an association */ /* * example * @ODM\ReferenceOne(targetDocument="Article", nullable="true", mappedBy="type") * @Gedmo\ReferenceIntegrity("nullify") * @var Article */ /* * example * @ODM\ReferenceOne(targetDocument="Article", nullable="true", mappedBy="type") * @Gedmo\ReferenceIntegrity("restrict") * @var Article */ /* * example * @ODM\ReferenceMany(targetDocument="Article", nullable="true", mappedBy="type") * @Gedmo\ReferenceIntegrity("nullify") * @var Doctrine\Common\Collections\ArrayCollection */ /* * example * @ODM\ReferenceMany(targetDocument="Article", nullable="true", mappedBy="type") * @Gedmo\ReferenceIntegrity("restrict") * @var Doctrine\Common\Collections\ArrayCollection */ } doctrine-extensions/src/ReferenceIntegrity/ReferenceIntegrityListener.php 0000644 00000014207 15117737236 0023170 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\ReferenceIntegrity; use Doctrine\Common\EventArgs; use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; use Gedmo\Exception\InvalidMappingException; use Gedmo\Exception\ReferenceIntegrityStrictException; use Gedmo\Mapping\MappedEventSubscriber; use Gedmo\ReferenceIntegrity\Mapping\Validator; /** * The ReferenceIntegrity listener handles the reference integrity on related documents * * @author Evert Harmeling <evert.harmeling@freshheads.com> * * @final since gedmo/doctrine-extensions 3.11 */ class ReferenceIntegrityListener extends MappedEventSubscriber { /** * @return string[] */ public function getSubscribedEvents() { return [ 'loadClassMetadata', 'preRemove', ]; } /** * Maps additional metadata for the Document * * @param LoadClassMetadataEventArgs $eventArgs * * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } /** * Looks for referenced objects being removed * to nullify the relation or throw an exception * * @return void */ public function preRemove(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $class = get_class($object); $meta = $om->getClassMetadata($class); if ($config = $this->getConfiguration($om, $meta->getName())) { foreach ($config['referenceIntegrity'] as $property => $action) { $reflProp = $meta->getReflectionProperty($property); $refDoc = $reflProp->getValue($object); $fieldMapping = $meta->getFieldMapping($property); switch ($action) { case Validator::NULLIFY: if (!isset($fieldMapping['mappedBy'])) { throw new InvalidMappingException(sprintf("Reference '%s' on '%s' should have 'mappedBy' option defined", $property, $meta->getName())); } $subMeta = $om->getClassMetadata($fieldMapping['targetDocument']); if (!$subMeta->hasField($fieldMapping['mappedBy'])) { throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $fieldMapping['mappedBy'], $fieldMapping['targetDocument'])); } $refReflProp = $subMeta->getReflectionProperty($fieldMapping['mappedBy']); if ($meta->isCollectionValuedReference($property)) { foreach ($refDoc as $refObj) { $refReflProp->setValue($refObj, null); $om->persist($refObj); } } else { $refReflProp->setValue($refDoc, null); $om->persist($refDoc); } break; case Validator::PULL: if (!isset($fieldMapping['mappedBy'])) { throw new InvalidMappingException(sprintf("Reference '%s' on '%s' should have 'mappedBy' option defined", $property, $meta->getName())); } $subMeta = $om->getClassMetadata($fieldMapping['targetDocument']); if (!$subMeta->hasField($fieldMapping['mappedBy'])) { throw new InvalidMappingException(sprintf('Unable to find reference integrity [%s] as mapped property in entity - %s', $fieldMapping['mappedBy'], $fieldMapping['targetDocument'])); } if (!$subMeta->isCollectionValuedReference($fieldMapping['mappedBy'])) { throw new InvalidMappingException(sprintf('Reference integrity [%s] mapped property in entity - %s should be a Reference Many', $fieldMapping['mappedBy'], $fieldMapping['targetDocument'])); } $refReflProp = $subMeta->getReflectionProperty($fieldMapping['mappedBy']); if ($meta->isCollectionValuedReference($property)) { foreach ($refDoc as $refObj) { $collection = $refReflProp->getValue($refObj); $collection->removeElement($object); $refReflProp->setValue($refObj, $collection); $om->persist($refObj); } } elseif (is_object($refDoc)) { $collection = $refReflProp->getValue($refDoc); $collection->removeElement($object); $refReflProp->setValue($refDoc, $collection); $om->persist($refDoc); } break; case Validator::RESTRICT: if ($meta->isCollectionValuedReference($property) && $refDoc->count() > 0) { throw new ReferenceIntegrityStrictException(sprintf("The reference integrity for the '%s' collection is restricted", $fieldMapping['targetDocument'])); } if ($meta->isSingleValuedReference($property) && null !== $refDoc) { throw new ReferenceIntegrityStrictException(sprintf("The reference integrity for the '%s' document is restricted", $fieldMapping['targetDocument'])); } break; } } } } protected function getNamespace() { return __NAMESPACE__; } } doctrine-extensions/src/References/Mapping/Driver/Annotation.php 0000644 00000006426 15117737236 0021115 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ 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 <gediminas.morkevicius@gmail.com> * @author Bulat Shakirzyanov <mallluhuct@gmail.com> * @author Jonathan H. Wage <jonwage@gmail.com> * * @internal */ class Annotation 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<string, self::REFERENCE_ONE|self::REFERENCE_MANY|self::REFERENCE_MANY_EMBED> */ 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, ]; } } } } /** * Passes in the mapping read by original driver */ public function setOriginalDriver($driver) { $this->_originalDriver = $driver; } } doctrine-extensions/src/References/Mapping/Driver/Attribute.php 0000644 00000001103 15117737236 0020731 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\References\Mapping\Driver; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for References * behavioral extension. * * @internal */ final class Attribute extends Annotation implements AttributeDriverInterface { } doctrine-extensions/src/References/Mapping/Driver/Xml.php 0000644 00000007711 15117737236 0017541 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\References\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for References * behavioral extension. Used for extraction of extended * metadata from xml specifically for References * extension. * * @author Aram Alipoor <aram.alipoor@gmail.com> * * @internal */ class Xml extends BaseXml { /** * @var string[] */ private const VALID_TYPES = [ 'document', 'entity', ]; /** * @var string[] */ private $validReferences = [ 'referenceOne', 'referenceMany', 'referenceManyEmbed', ]; public function readExtendedMetadata($meta, array &$config) { /** * @var \SimpleXmlElement */ $xml = $this->_getMapping($meta->getName()); $xmlDoctrine = $xml; $xml = $xml->children(self::GEDMO_NAMESPACE_URI); if ('entity' === $xmlDoctrine->getName() || 'document' === $xmlDoctrine->getName() || 'mapped-superclass' === $xmlDoctrine->getName()) { if (isset($xml->reference)) { /** * @var \SimpleXMLElement */ foreach ($xml->reference as $element) { if (!$this->_isAttributeSet($element, 'type')) { throw new InvalidMappingException("Reference type (document or entity) is not set in class - {$meta->getName()}"); } $type = $this->_getAttribute($element, 'type'); if (!in_array($type, self::VALID_TYPES, true)) { throw new InvalidMappingException($type.' is not a valid reference type, valid types are: '.implode(', ', self::VALID_TYPES)); } $reference = $this->_getAttribute($element, 'reference'); if (!in_array($reference, $this->validReferences, true)) { throw new InvalidMappingException($reference.' is not a valid reference, valid references are: '.implode(', ', $this->validReferences)); } if (!$this->_isAttributeSet($element, 'field')) { throw new InvalidMappingException("Reference field is not set in class - {$meta->getName()}"); } $field = $this->_getAttribute($element, 'field'); if (!$this->_isAttributeSet($element, 'class')) { throw new InvalidMappingException("Reference field is not set in class - {$meta->getName()}"); } $class = $this->_getAttribute($element, 'class'); if (!$this->_isAttributeSet($element, 'identifier')) { throw new InvalidMappingException("Reference identifier is not set in class - {$meta->getName()}"); } $identifier = $this->_getAttribute($element, 'identifier'); $config[$reference][$field] = [ 'field' => $field, 'type' => $type, 'class' => $class, 'identifier' => $identifier, ]; if (!$this->_isAttributeSet($element, 'mappedBy')) { $config[$reference][$field]['mappedBy'] = $this->_getAttribute($element, 'mappedBy'); } if (!$this->_isAttributeSet($element, 'inversedBy')) { $config[$reference][$field]['inversedBy'] = $this->_getAttribute($element, 'inversedBy'); } } } } } } doctrine-extensions/src/References/Mapping/Driver/Yaml.php 0000644 00000004724 15117737236 0017704 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\References\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; /** * @author Gonzalo Vilaseca <gonzalo.vilaseca@reiss.com> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; /** * @var array */ private $validReferences = [ 'referenceOne' => [], 'referenceMany' => [], 'referenceManyEmbed' => [], ]; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['gedmo'], $mapping['gedmo']['reference'])) { foreach ($mapping['gedmo']['reference'] as $field => $fieldMapping) { $reference = $fieldMapping['reference']; if (!in_array($reference, array_keys($this->validReferences), true)) { throw new InvalidMappingException($reference.' is not a valid reference, valid references are: '.implode(', ', array_keys($this->validReferences))); } $config[$reference][$field] = [ 'field' => $field, 'type' => $fieldMapping['type'], 'class' => $fieldMapping['class'], ]; if (array_key_exists('mappedBy', $fieldMapping)) { $config[$reference][$field]['mappedBy'] = $fieldMapping['mappedBy']; } if (array_key_exists('identifier', $fieldMapping)) { $config[$reference][$field]['identifier'] = $fieldMapping['identifier']; } if (array_key_exists('inversedBy', $fieldMapping)) { $config[$reference][$field]['inversedBy'] = $fieldMapping['inversedBy']; } } } $config = array_merge($this->validReferences, $config); } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse($file); } } doctrine-extensions/src/References/Mapping/Event/Adapter/ODM.php 0000644 00000005216 15117737236 0020624 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\References\Mapping\Event\Adapter; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Proxy\Proxy as ORMProxy; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\References\Mapping\Event\ReferencesAdapter; use ProxyManager\Proxy\GhostObjectInterface; /** * Doctrine event adapter for ODM references behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Bulat Shakirzyanov <mallluhuct@gmail.com> * @author Jonathan H. Wage <jonwage@gmail.com> */ final class ODM extends BaseAdapterODM implements ReferencesAdapter { public function getIdentifier($om, $object, $single = true) { if ($om instanceof DocumentManager) { return $this->extractIdentifier($om, $object, $single); } if ($om instanceof EntityManagerInterface) { if ($object instanceof ORMProxy) { $id = $om->getUnitOfWork()->getEntityIdentifier($object); } else { $meta = $om->getClassMetadata(get_class($object)); $id = []; foreach ($meta->getIdentifier() as $name) { $id[$name] = $meta->getReflectionProperty($name)->getValue($object); // return null if one of identifiers is missing if (!$id[$name]) { return null; } } } if ($single) { $id = current($id); } return $id; } return null; } public function getSingleReference($om, $class, $identifier) { $meta = $om->getClassMetadata($class); if (!$meta->isInheritanceTypeNone()) { return $om->find($class, $identifier); } return $om->getReference($class, $identifier); } public function extractIdentifier($om, $object, $single = true) { $meta = $om->getClassMetadata(get_class($object)); if ($object instanceof GhostObjectInterface) { $id = $om->getUnitOfWork()->getDocumentIdentifier($object); } else { $id = $meta->getReflectionProperty($meta->getIdentifier()[0])->getValue($object); } if ($single || !$id) { return $id; } return [$meta->getIdentifier()[0] => $id]; } } doctrine-extensions/src/References/Mapping/Event/Adapter/ORM.php 0000644 00000007531 15117737236 0020644 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\References\Mapping\Event\Adapter; use Doctrine\ODM\MongoDB\DocumentManager as MongoDocumentManager; use Doctrine\ODM\PHPCR\DocumentManager as PhpcrDocumentManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Proxy\Proxy as ORMProxy; use Gedmo\Exception\InvalidArgumentException; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\References\Mapping\Event\ReferencesAdapter; use ProxyManager\Proxy\GhostObjectInterface; /** * Doctrine event adapter for ORM references behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Bulat Shakirzyanov <mallluhuct@gmail.com> * @author Jonathan H. Wage <jonwage@gmail.com> */ final class ORM extends BaseAdapterORM implements ReferencesAdapter { public function getIdentifier($om, $object, $single = true) { if ($om instanceof EntityManagerInterface) { return $this->extractIdentifier($om, $object, $single); } if ($om instanceof MongoDocumentManager) { $meta = $om->getClassMetadata(get_class($object)); if ($object instanceof GhostObjectInterface) { $id = $om->getUnitOfWork()->getDocumentIdentifier($object); } else { $id = $meta->getReflectionProperty($meta->getIdentifier()[0])->getValue($object); } if ($single || !$id) { return $id; } return [$meta->getIdentifier()[0] => $id]; } if ($om instanceof PhpcrDocumentManager) { $meta = $om->getClassMetadata(get_class($object)); assert(1 === count($meta->getIdentifier())); $id = $meta->getReflectionProperty($meta->getIdentifier()[0])->getValue($object); if ($single || !$id) { return $id; } return [$meta->getIdentifier()[0] => $id]; } return null; } public function getSingleReference($om, $class, $identifier) { $this->throwIfNotDocumentManager($om); $meta = $om->getClassMetadata($class); if ($om instanceof MongoDocumentManager) { if (!$meta->isInheritanceTypeNone()) { return $om->find($class, $identifier); } } return $om->getReference($class, $identifier); } public function extractIdentifier($om, $object, $single = true) { if ($object instanceof ORMProxy) { $id = $om->getUnitOfWork()->getEntityIdentifier($object); } else { $meta = $om->getClassMetadata(get_class($object)); $id = []; foreach ($meta->getIdentifier() as $name) { $id[$name] = $meta->getReflectionProperty($name)->getValue($object); // return null if one of identifiers is missing if (!$id[$name]) { return null; } } } if ($single) { $id = current($id); } return $id; } /** * Override so we don't get an exception. We want to allow this. * * @param mixed $dm * * @phpstan-assert MongoDocumentManager|PhpcrDocumentManager $dm */ private function throwIfNotDocumentManager($dm): void { if (!($dm instanceof MongoDocumentManager) && !($dm instanceof PhpcrDocumentManager)) { throw new InvalidArgumentException(sprintf('Expected a %s or %s instance but got "%s"', MongoDocumentManager::class, 'Doctrine\ODM\PHPCR\DocumentManager', is_object($dm) ? get_class($dm) : gettype($dm))); } } } doctrine-extensions/src/References/Mapping/Event/ReferencesAdapter.php 0000644 00000003406 15117737236 0022206 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\References\Mapping\Event; use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\Event\AdapterInterface; /** * Doctrine event adapter for the References extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Bulat Shakirzyanov <mallluhuct@gmail.com> * @author Jonathan H. Wage <jonwage@gmail.com> */ interface ReferencesAdapter extends AdapterInterface { /** * Gets the identifier of the given object using the provided object manager. * * @param ObjectManager $om * @param object $object * @param bool $single * * @return array|string|int|null array or single identifier */ public function getIdentifier($om, $object, $single = true); /** * Gets a single reference from the provided object manager for a class and identifier. * * @param ObjectManager $om * @param string $class * @param array|string|int $identifier * * @phpstan-param class-string $class * * @return object|null */ public function getSingleReference($om, $class, $identifier); /** * Extracts identifiers from an object or proxy using the provided object manager. * * @param ObjectManager $om * @param object $object * @param bool $single * * @return array|string|int|null array or single identifier */ public function extractIdentifier($om, $object, $single = true); } doctrine-extensions/src/References/LazyCollection.php 0000644 00000002133 15117737236 0017077 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\References; use Doctrine\Common\Collections\AbstractLazyCollection; /** * Lazy collection for loading reference many associations. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Bulat Shakirzyanov <mallluhuct@gmail.com> * @author Jonathan H. Wage <jonwage@gmail.com> * * @template-extends AbstractLazyCollection<array-key, mixed> * * @final since gedmo/doctrine-extensions 3.11 */ class LazyCollection extends AbstractLazyCollection { /** * @var callable */ private $callback; /** * @param callable $callback */ public function __construct($callback) { $this->callback = $callback; } protected function doInitialize(): void { $this->collection = call_user_func($this->callback); } } doctrine-extensions/src/References/ReferencesListener.php 0000644 00000021263 15117737236 0017740 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\References; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\EventArgs; use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\MappedEventSubscriber; /** * Listener for loading and persisting cross database references. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Bulat Shakirzyanov <mallluhuct@gmail.com> * @author Jonathan H. Wage <jonwage@gmail.com> * * @phpstan-type ReferenceConfiguration = array{ * field?: string, * type?: string, * class?: class-string, * identifier?: string, * mappedBy?: string, * inversedBy?: string, * } * * @phpstan-type ReferencesConfiguration = array{ * referenceMany?: array<string, ReferenceConfiguration>, * referenceManyEmbed?: array<string, ReferenceConfiguration>, * referenceOne?: array<string, ReferenceConfiguration>, * useObjectClass?: class-string, * } * * @phpstan-method ReferencesConfiguration getConfiguration(ObjectManager $objectManager, $class) * * @final since gedmo/doctrine-extensions 3.11 */ class ReferencesListener extends MappedEventSubscriber { /** * @var array<string, ObjectManager> */ private $managers; /** * @param array<string, ObjectManager> $managers */ public function __construct(array $managers = []) { parent::__construct(); $this->managers = $managers; } /** * @param LoadClassMetadataEventArgs $eventArgs * * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { $this->loadMetadataForObjectClass( $eventArgs->getObjectManager(), $eventArgs->getClassMetadata() ); } /** * @return void */ public function postLoad(EventArgs $eventArgs) { $ea = $this->getEventAdapter($eventArgs); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['referenceOne'])) { foreach ($config['referenceOne'] as $mapping) { $property = $meta->reflClass->getProperty($mapping['field']); $property->setAccessible(true); if (isset($mapping['identifier'])) { $referencedObjectId = $meta->getFieldValue($object, $mapping['identifier']); if (null !== $referencedObjectId) { $property->setValue( $object, $ea->getSingleReference( $this->getManager($mapping['type']), $mapping['class'], $referencedObjectId ) ); } } } } if (isset($config['referenceMany'])) { foreach ($config['referenceMany'] as $mapping) { $property = $meta->reflClass->getProperty($mapping['field']); $property->setAccessible(true); if (isset($mapping['mappedBy'])) { $id = $ea->extractIdentifier($om, $object); $manager = $this->getManager($mapping['type']); $class = $mapping['class']; $refMeta = $manager->getClassMetadata($class); $refConfig = $this->getConfiguration($manager, $refMeta->getName()); if (isset($refConfig['referenceOne'][$mapping['mappedBy']])) { $refMapping = $refConfig['referenceOne'][$mapping['mappedBy']]; $identifier = $refMapping['identifier']; $property->setValue( $object, new LazyCollection( static function () use ($id, &$manager, $class, $identifier) { $results = $manager ->getRepository($class) ->findBy([ $identifier => $id, ]); return new ArrayCollection(is_array($results) ? $results : $results->toArray()); } ) ); } } } } $this->updateManyEmbedReferences($eventArgs); } /** * @return void */ public function prePersist(EventArgs $eventArgs) { $this->updateReferences($eventArgs); } /** * @return void */ public function preUpdate(EventArgs $eventArgs) { $this->updateReferences($eventArgs); } /** * @return string[] */ public function getSubscribedEvents() { return [ 'postLoad', 'loadClassMetadata', 'prePersist', 'preUpdate', ]; } /** * @param string $type * @param ObjectManager $manager * * @return void */ public function registerManager($type, $manager) { $this->managers[$type] = $manager; } /** * @param string $type * * @return ObjectManager */ public function getManager($type) { return $this->managers[$type]; } /** * @return void */ public function updateManyEmbedReferences(EventArgs $eventArgs) { $ea = $this->getEventAdapter($eventArgs); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['referenceManyEmbed'])) { foreach ($config['referenceManyEmbed'] as $mapping) { $property = $meta->reflClass->getProperty($mapping['field']); $property->setAccessible(true); $id = $ea->extractIdentifier($om, $object); $manager = $this->getManager('document'); $class = $mapping['class']; $refMeta = $manager->getClassMetadata($class); // Trigger the loading of the configuration to validate the mapping $this->getConfiguration($manager, $refMeta->getName()); $identifier = $mapping['identifier']; $property->setValue( $object, new LazyCollection( static function () use ($id, &$manager, $class, $identifier) { $results = $manager ->getRepository($class) ->findBy([ $identifier => $id, ]); return new ArrayCollection(is_array($results) ? $results : $results->toArray()); } ) ); } } } protected function getNamespace() { return __NAMESPACE__; } private function updateReferences(EventArgs $eventArgs): void { $ea = $this->getEventAdapter($eventArgs); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['referenceOne'])) { foreach ($config['referenceOne'] as $mapping) { if (isset($mapping['identifier'])) { $property = $meta->reflClass->getProperty($mapping['field']); $property->setAccessible(true); $referencedObject = $property->getValue($object); if (is_object($referencedObject)) { $manager = $this->getManager($mapping['type']); $identifier = $ea->getIdentifier($manager, $referencedObject); $meta->setFieldValue( $object, $mapping['identifier'], $identifier ); } } } } $this->updateManyEmbedReferences($eventArgs); } } doctrine-extensions/src/Sluggable/Handler/InversedRelativeSlugHandler.php 0000644 00000012401 15117737236 0022750 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Handler; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Doctrine\Persistence\Proxy; use Gedmo\Exception\InvalidMappingException; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; use Gedmo\Sluggable\SluggableListener; use Gedmo\Tool\Wrapper\AbstractWrapper; /** * Sluggable handler which should be used for inversed relation mapping * used together with RelativeSlugHandler. Updates back related slug on * relation changes * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class InversedRelativeSlugHandler implements SlugHandlerInterface { /** * @var ObjectManager */ protected $om; /** * @var SluggableListener */ protected $sluggable; /** * $options = array( * 'relationClass' => 'objectclass', * 'inverseSlugField' => 'slug', * 'mappedBy' => 'relationField' * ) * {@inheritdoc} */ public function __construct(SluggableListener $sluggable) { $this->sluggable = $sluggable; } public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug) { } public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug) { } public static function validate(array $options, ClassMetadata $meta) { if (!isset($options['relationClass']) || !strlen($options['relationClass'])) { throw new InvalidMappingException("'relationClass' option must be specified for object slug mapping - {$meta->getName()}"); } if (!isset($options['mappedBy']) || !strlen($options['mappedBy'])) { throw new InvalidMappingException("'mappedBy' option must be specified for object slug mapping - {$meta->getName()}"); } if (!isset($options['inverseSlugField']) || !strlen($options['inverseSlugField'])) { throw new InvalidMappingException("'inverseSlugField' option must be specified for object slug mapping - {$meta->getName()}"); } } public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug) { $this->om = $ea->getObjectManager(); $isInsert = $this->om->getUnitOfWork()->isScheduledForInsert($object); if (!$isInsert) { $options = $config['handlers'][static::class]; $wrapped = AbstractWrapper::wrap($object, $this->om); $oldSlug = $wrapped->getPropertyValue($config['slug']); $mappedByConfig = $this->sluggable->getConfiguration( $this->om, $options['relationClass'] ); if ($mappedByConfig) { $meta = $this->om->getClassMetadata($options['relationClass']); if (!$meta->isSingleValuedAssociation($options['mappedBy'])) { throw new InvalidMappingException('Unable to find '.$wrapped->getMetadata()->getName()." relation - [{$options['mappedBy']}] in class - {$meta->getName()}"); } if (!isset($mappedByConfig['slugs'][$options['inverseSlugField']])) { throw new InvalidMappingException("Unable to find slug field - [{$options['inverseSlugField']}] in class - {$meta->getName()}"); } $mappedByConfig['slug'] = $mappedByConfig['slugs'][$options['inverseSlugField']]['slug']; $mappedByConfig['mappedBy'] = $options['mappedBy']; $ea->replaceInverseRelative($object, $mappedByConfig, $slug, $oldSlug); $uow = $this->om->getUnitOfWork(); // update in memory objects foreach ($uow->getIdentityMap() as $className => $objects) { // for inheritance mapped classes, only root is always in the identity map if ($className !== $mappedByConfig['useObjectClass']) { continue; } foreach ($objects as $object) { // @todo: Remove the check against `method_exists()` in the next major release. if (($object instanceof Proxy || method_exists($object, '__isInitialized')) && !$object->__isInitialized()) { continue; } $objectSlug = (string) $meta->getReflectionProperty($mappedByConfig['slug'])->getValue($object); if (preg_match("@^{$oldSlug}@smi", $objectSlug)) { $objectSlug = str_replace($oldSlug, $slug, $objectSlug); $meta->getReflectionProperty($mappedByConfig['slug'])->setValue($object, $objectSlug); $ea->setOriginalObjectProperty($uow, $object, $mappedByConfig['slug'], $objectSlug); } } } } } } public function handlesUrlization() { return false; } } doctrine-extensions/src/Sluggable/Handler/RelativeSlugHandler.php 0000644 00000010552 15117737236 0021255 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Handler; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Gedmo\Exception\InvalidMappingException; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; use Gedmo\Sluggable\SluggableListener; use Gedmo\Tool\Wrapper\AbstractWrapper; /** * Sluggable handler which should be used in order to prefix * a slug of related object. For instance user may belong to a company * in this case user slug could look like 'company-name/user-firstname' * where path separator separates the relative slug * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class RelativeSlugHandler implements SlugHandlerInterface { public const SEPARATOR = '/'; /** * @var ObjectManager */ protected $om; /** * @var SluggableListener */ protected $sluggable; /** * Used options * * @var array */ private $usedOptions; /** * Callable of original transliterator * which is used by sluggable * * @var callable */ private $originalTransliterator; /** * $options = array( * 'separator' => '/', * 'relationField' => 'something', * 'relationSlugField' => 'slug' * ) * {@inheritdoc} */ public function __construct(SluggableListener $sluggable) { $this->sluggable = $sluggable; } public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug) { $this->om = $ea->getObjectManager(); $isInsert = $this->om->getUnitOfWork()->isScheduledForInsert($object); $this->usedOptions = $config['handlers'][static::class]; if (!isset($this->usedOptions['separator'])) { $this->usedOptions['separator'] = self::SEPARATOR; } if (!$isInsert && !$needToChangeSlug) { $changeSet = $ea->getObjectChangeSet($this->om->getUnitOfWork(), $object); if (isset($changeSet[$this->usedOptions['relationField']])) { $needToChangeSlug = true; } } } public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug) { $this->originalTransliterator = $this->sluggable->getTransliterator(); $this->sluggable->setTransliterator([$this, 'transliterate']); } public static function validate(array $options, ClassMetadata $meta) { if (!$meta->isSingleValuedAssociation($options['relationField'])) { throw new InvalidMappingException("Unable to find slug relation through field - [{$options['relationField']}] in class - {$meta->getName()}"); } } public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug) { } /** * Transliterates the slug and prefixes the slug * by relative one * * @param string $text * @param string $separator * @param object $object * * @return string */ public function transliterate($text, $separator, $object) { $result = call_user_func_array( $this->originalTransliterator, [$text, $separator, $object] ); $wrapped = AbstractWrapper::wrap($object, $this->om); $relation = $wrapped->getPropertyValue($this->usedOptions['relationField']); if ($relation) { $wrappedRelation = AbstractWrapper::wrap($relation, $this->om); $slug = $wrappedRelation->getPropertyValue($this->usedOptions['relationSlugField']); if (isset($this->usedOptions['urilize']) && $this->usedOptions['urilize']) { $slug = call_user_func_array( $this->originalTransliterator, [$slug, $separator, $object] ); } $result = $slug.$this->usedOptions['separator'].$result; } $this->sluggable->setTransliterator($this->originalTransliterator); return $result; } public function handlesUrlization() { return true; } } doctrine-extensions/src/Sluggable/Handler/SlugHandlerInterface.php 0000644 00000004203 15117737236 0021376 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Handler; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; use Gedmo\Sluggable\SluggableListener; /** * Interface defining a handler for the sluggable behavior. * Usage is intended only for internal access of the * Sluggable extension and should not be used elsewhere. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface SlugHandlerInterface { /** * Create a new handler instance */ public function __construct(SluggableListener $sluggable); /** * Hook on slug handlers before the decision is made whether * the slug needs to be recalculated. * * @param object $object * @param string $slug * @param bool $needToChangeSlug * * @return void */ public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug); /** * Hook on slug handlers called after the slug is built. * * @param object $object * @param string $slug * * @return void */ public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug); /** * Hook for slug handlers called after the slug is completed. * * @param object $object * @param string $slug * * @return void */ public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug); /** * @return bool Whether this handler has already urlized the slug */ public function handlesUrlization(); /** * Validates the options for the handler. * * @throws InvalidMappingException if the configuration is invalid * * @return void */ public static function validate(array $options, ClassMetadata $meta); } doctrine-extensions/src/Sluggable/Handler/SlugHandlerWithUniqueCallbackInterface.php 0000644 00000001717 15117737236 0025045 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Handler; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; /** * This adds the ability for a slug handler to change the slug just before its * uniqueness is ensured. It is also called if the unique options are _not_ * set. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface SlugHandlerWithUniqueCallbackInterface extends SlugHandlerInterface { /** * Hook for slug handlers called before it is made unique. * * @param object $object * @param string $slug * * @return void */ public function beforeMakingUnique(SluggableAdapter $ea, array &$config, $object, &$slug); } doctrine-extensions/src/Sluggable/Handler/TreeSlugHandler.php 0000644 00000014254 15117737236 0020404 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Handler; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Doctrine\Persistence\Proxy; use Gedmo\Exception\InvalidMappingException; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; use Gedmo\Sluggable\SluggableListener; use Gedmo\Tool\Wrapper\AbstractWrapper; /** * Sluggable handler which slugs all parent nodes * recursively and synchronizes on updates. For instance * category tree slug could look like "food/fruits/apples" * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class TreeSlugHandler implements SlugHandlerWithUniqueCallbackInterface { public const SEPARATOR = '/'; /** * @var ObjectManager */ protected $om; /** * @var SluggableListener */ protected $sluggable; /** * @var string */ private $prefix; /** * @var string */ private $suffix; /** * True if node is being inserted * * @var bool */ private $isInsert = false; /** * Transliterated parent slug * * @var string */ private $parentSlug; /** * Used path separator * * @var string */ private $usedPathSeparator; public function __construct(SluggableListener $sluggable) { $this->sluggable = $sluggable; } public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug) { $this->om = $ea->getObjectManager(); $this->isInsert = $this->om->getUnitOfWork()->isScheduledForInsert($object); $options = $config['handlers'][static::class]; $this->usedPathSeparator = $options['separator'] ?? self::SEPARATOR; $this->prefix = $options['prefix'] ?? ''; $this->suffix = $options['suffix'] ?? ''; if (!$this->isInsert && !$needToChangeSlug) { $changeSet = $ea->getObjectChangeSet($this->om->getUnitOfWork(), $object); if (isset($changeSet[$options['parentRelationField']])) { $needToChangeSlug = true; } } } public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug) { $options = $config['handlers'][static::class]; $this->parentSlug = ''; $wrapped = AbstractWrapper::wrap($object, $this->om); if ($parent = $wrapped->getPropertyValue($options['parentRelationField'])) { $parent = AbstractWrapper::wrap($parent, $this->om); $this->parentSlug = $parent->getPropertyValue($config['slug']); // if needed, remove suffix from parentSlug, so we can use it to prepend it to our slug if (isset($options['suffix'])) { $suffix = $options['suffix']; if (substr($this->parentSlug, -strlen($suffix)) === $suffix) { // endsWith $this->parentSlug = substr_replace($this->parentSlug, '', -1 * strlen($suffix)); } } } } public static function validate(array $options, ClassMetadata $meta) { if (!$meta->isSingleValuedAssociation($options['parentRelationField'])) { throw new InvalidMappingException("Unable to find tree parent slug relation through field - [{$options['parentRelationField']}] in class - {$meta->getName()}"); } } public function beforeMakingUnique(SluggableAdapter $ea, array &$config, $object, &$slug) { $slug = $this->transliterate($slug, $config['separator'], $object); } public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug) { if (!$this->isInsert) { $wrapped = AbstractWrapper::wrap($object, $this->om); $meta = $wrapped->getMetadata(); $target = $wrapped->getPropertyValue($config['slug']); $config['pathSeparator'] = $this->usedPathSeparator; $ea->replaceRelative($object, $config, $target.$config['pathSeparator'], $slug); $uow = $this->om->getUnitOfWork(); // update in memory objects foreach ($uow->getIdentityMap() as $className => $objects) { // for inheritance mapped classes, only root is always in the identity map if ($className !== $wrapped->getRootObjectName()) { continue; } foreach ($objects as $object) { // @todo: Remove the check against `method_exists()` in the next major release. if (($object instanceof Proxy || method_exists($object, '__isInitialized')) && !$object->__isInitialized()) { continue; } $objectSlug = (string) $meta->getReflectionProperty($config['slug'])->getValue($object); if (preg_match("@^{$target}{$config['pathSeparator']}@smi", $objectSlug)) { $objectSlug = str_replace($target, $slug, $objectSlug); $meta->getReflectionProperty($config['slug'])->setValue($object, $objectSlug); $ea->setOriginalObjectProperty($uow, $object, $config['slug'], $objectSlug); } } } } } /** * Transliterates the slug and prefixes the slug * by collection of parent slugs * * @param string $text * @param string $separator * @param object $object * * @return string */ public function transliterate($text, $separator, $object) { $slug = $text.$this->suffix; if (strlen($this->parentSlug)) { $slug = $this->parentSlug.$this->usedPathSeparator.$slug; } else { // if no parentSlug, apply our prefix $slug = $this->prefix.$slug; } return $slug; } public function handlesUrlization() { return false; } } doctrine-extensions/src/Sluggable/Mapping/Driver/Annotation.php 0000644 00000020045 15117737236 0020732 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\Slug; use Gedmo\Mapping\Annotation\SlugHandler; use Gedmo\Mapping\Annotation\SlugHandlerOption; use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** * This is an annotation mapping driver for Sluggable * behavioral extension. Used for extraction of extended * metadata from Annotations specifically for Sluggable * extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @internal */ class Annotation extends AbstractAnnotationDriver { /** * 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); } } } } /** * @internal * * @return array<string, SlugHandler[]> */ protected function getSlugHandlers(\ReflectionProperty $property, Slug $slug, ClassMetadata $meta): array { if (!is_array($slug->handlers) || [] === $slug->handlers) { return []; } $handlers = []; foreach ($slug->handlers as $handler) { 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()}"); } $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; } /** * @return array<string, array<string, mixed>> */ 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; } } doctrine-extensions/src/Sluggable/Mapping/Driver/Attribute.php 0000644 00000003373 15117737236 0020570 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\Slug; use Gedmo\Mapping\Annotation\SlugHandler; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for Sluggable * behavioral extension. Used for extraction of extended * metadata from attribute specifically for Sluggable * extension. * * @internal */ final class Attribute extends Annotation implements AttributeDriverInterface { /** * @return array<string, SlugHandler[]> */ protected function getSlugHandlers(\ReflectionProperty $property, Slug $slug, ClassMetadata $meta): array { $attributeHandlers = $this->reader->getPropertyAnnotation($property, self::HANDLER); if (null === $attributeHandlers) { return []; } $handlers = []; foreach ($attributeHandlers as $handler) { if (!class_exists($handler->class)) { throw new InvalidMappingException("SlugHandler class: {$handler->class} should be a valid class name in entity - {$meta->getName()}"); } $class = $handler->class; $handlers[$class] = []; foreach ($handler->options as $name => $value) { $handlers[$class][$name] = $value; } $class::validate($handlers[$class], $meta); } return $handlers; } } doctrine-extensions/src/Sluggable/Mapping/Driver/Xml.php 0000644 00000014245 15117737236 0017365 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for Sluggable * behavioral extension. Used for extraction of extended * metadata from xml specifically for Sluggable * extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Miha Vrhovnik <miha.vrhovnik@gmail.com> * * @internal */ class Xml extends BaseXml { /** * List of types which are valid for slug and sluggable fields * * @var string[] */ private const VALID_TYPES = [ 'string', 'text', 'integer', 'int', 'datetime', 'citext', ]; public function readExtendedMetadata($meta, array &$config) { /** * @var \SimpleXmlElement */ $xml = $this->_getMapping($meta->getName()); if (isset($xml->field)) { foreach ($xml->field as $mapping) { $field = $this->_getAttribute($mapping, 'name'); $this->buildFieldConfiguration($meta, $field, $mapping, $config); } } if (isset($xml->{'attribute-overrides'})) { foreach ($xml->{'attribute-overrides'}->{'attribute-override'} as $mapping) { $field = $this->_getAttribute($mapping, 'name'); $this->buildFieldConfiguration($meta, $field, $mapping->field, $config); } } } /** * Checks if $field type is valid as Sluggable field * * @param ClassMetadata $meta * @param string $field * * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], self::VALID_TYPES, true); } private function buildFieldConfiguration(ClassMetadata $meta, string $field, \SimpleXMLElement $mapping, array &$config): void { /** * @var \SimpleXmlElement */ $mapping = $mapping->children(self::GEDMO_NAMESPACE_URI); if (isset($mapping->slug)) { /** * @var \SimpleXmlElement */ $slug = $mapping->slug; if (!$this->isValidField($meta, $field)) { throw new InvalidMappingException("Cannot use field - [{$field}] for slug storage, type is not valid and must be 'string' in class - {$meta->getName()}"); } $fields = array_map('trim', explode(',', (string) $this->_getAttribute($slug, 'fields'))); foreach ($fields as $slugField) { if (!$meta->hasField($slugField)) { throw new InvalidMappingException("Unable to find slug [{$slugField}] as mapped property in entity - {$meta->getName()}"); } if (!$this->isValidField($meta, $slugField)) { throw new InvalidMappingException("Cannot use field - [{$slugField}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); } } $handlers = []; if (isset($slug->handler)) { foreach ($slug->handler as $handler) { $class = (string) $this->_getAttribute($handler, 'class'); $handlers[$class] = []; foreach ($handler->{'handler-option'} as $option) { $handlers[$class][(string) $this->_getAttribute($option, 'name')] = (string) $this->_getAttribute($option, 'value') ; } $class::validate($handlers[$class], $meta); } } // set all options $config['slugs'][$field] = [ 'fields' => $fields, 'slug' => $field, 'style' => $this->_isAttributeSet($slug, 'style') ? $this->_getAttribute($slug, 'style') : 'default', 'updatable' => $this->_isAttributeSet($slug, 'updatable') ? $this->_getBooleanAttribute($slug, 'updatable') : true, 'dateFormat' => $this->_isAttributeSet($slug, 'dateFormat') ? $this->_getAttribute($slug, 'dateFormat') : 'Y-m-d-H:i', 'unique' => $this->_isAttributeSet($slug, 'unique') ? $this->_getBooleanAttribute($slug, 'unique') : true, 'unique_base' => $this->_isAttributeSet($slug, 'unique-base') ? $this->_getAttribute($slug, 'unique-base') : null, 'separator' => $this->_isAttributeSet($slug, 'separator') ? $this->_getAttribute($slug, 'separator') : '-', 'prefix' => $this->_isAttributeSet($slug, 'prefix') ? $this->_getAttribute($slug, 'prefix') : '', 'suffix' => $this->_isAttributeSet($slug, 'suffix') ? $this->_getAttribute($slug, 'suffix') : '', 'handlers' => $handlers, ]; if (!$meta->isMappedSuperclass && $meta->isIdentifier($field) && !$config['slugs'][$field]['unique']) { throw new InvalidMappingException("Identifier field - [{$field}] slug must be unique in order to maintain primary key in class - {$meta->getName()}"); } $ubase = $config['slugs'][$field]['unique_base']; if (false === $config['slugs'][$field]['unique'] && $ubase) { throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'"); } if ($ubase && !$meta->hasField($ubase) && !$meta->hasAssociation($ubase)) { throw new InvalidMappingException("Unable to find [{$ubase}] as mapped property in entity - {$meta->getName()}"); } } } } doctrine-extensions/src/Sluggable/Mapping/Driver/Yaml.php 0000644 00000014571 15117737236 0017531 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for Sluggable * behavioral extension. Used for extraction of extended * metadata from yaml specifically for Sluggable * extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * List of types which are valid for slug and sluggable fields * * @var string[] */ private const VALID_TYPES = [ 'string', 'text', 'integer', 'int', 'datetime', 'citext', ]; /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { $this->buildFieldConfiguration($field, $fieldMapping, $meta, $config); } } if (isset($mapping['attributeOverride'])) { foreach ($mapping['attributeOverride'] as $field => $overrideMapping) { $this->buildFieldConfiguration($field, $overrideMapping, $meta, $config); } } } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } /** * Checks if $field type is valid as Sluggable field * * @param ClassMetadata $meta * @param string $field * * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], self::VALID_TYPES, true); } private function buildFieldConfiguration(string $field, array $fieldMapping, ClassMetadata $meta, array &$config): void { if (isset($fieldMapping['gedmo'])) { if (isset($fieldMapping['gedmo']['slug'])) { $slug = $fieldMapping['gedmo']['slug']; if (!$this->isValidField($meta, $field)) { throw new InvalidMappingException("Cannot use field - [{$field}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); } // process slug handlers $handlers = []; if (isset($slug['handlers'])) { foreach ($slug['handlers'] as $handlerClass => $options) { if (!strlen($handlerClass)) { throw new InvalidMappingException("SlugHandler class: {$handlerClass} should be a valid class name in entity - {$meta->getName()}"); } $handlers[$handlerClass] = $options; $handlerClass::validate($handlers[$handlerClass], $meta); } } // process slug fields if (empty($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) { if (!$meta->hasField($slugField)) { throw new InvalidMappingException("Unable to find slug [{$slugField}] as mapped property in entity - {$meta->getName()}"); } if (!$this->isValidField($meta, $slugField)) { throw new InvalidMappingException("Cannot use field - [{$slugField}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->getName()}"); } } $config['slugs'][$field]['fields'] = $slug['fields']; $config['slugs'][$field]['handlers'] = $handlers; $config['slugs'][$field]['slug'] = $field; $config['slugs'][$field]['style'] = isset($slug['style']) ? (string) $slug['style'] : 'default'; $config['slugs'][$field]['dateFormat'] = isset($slug['dateFormat']) ? (string) $slug['dateFormat'] : 'Y-m-d-H:i'; $config['slugs'][$field]['updatable'] = isset($slug['updatable']) ? (bool) $slug['updatable'] : true; $config['slugs'][$field]['unique'] = isset($slug['unique']) ? (bool) $slug['unique'] : true; $config['slugs'][$field]['unique_base'] = $slug['unique_base'] ?? null; $config['slugs'][$field]['separator'] = isset($slug['separator']) ? (string) $slug['separator'] : '-'; $config['slugs'][$field]['prefix'] = isset($slug['prefix']) ? (string) $slug['prefix'] : ''; $config['slugs'][$field]['suffix'] = isset($slug['suffix']) ? (string) $slug['suffix'] : ''; if (!$meta->isMappedSuperclass && $meta->isIdentifier($field) && !$config['slugs'][$field]['unique']) { throw new InvalidMappingException("Identifier field - [{$field}] slug must be unique in order to maintain primary key in class - {$meta->getName()}"); } $ubase = $config['slugs'][$field]['unique_base']; if (false === $config['slugs'][$field]['unique'] && $ubase) { throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'"); } if ($ubase && !$meta->hasField($ubase) && !$meta->hasAssociation($ubase)) { throw new InvalidMappingException("Unable to find [{$ubase}] as mapped property in entity - {$meta->getName()}"); } } } } } doctrine-extensions/src/Sluggable/Mapping/Event/Adapter/ODM.php 0000644 00000011111 15117737236 0020437 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Mapping\Event\Adapter; use Doctrine\ODM\MongoDB\Iterator\Iterator; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; use Gedmo\Tool\Wrapper\AbstractWrapper; use MongoDB\BSON\Regex; /** * Doctrine event adapter for ODM adapted * for sluggable behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class ODM extends BaseAdapterODM implements SluggableAdapter { public function getSimilarSlugs($object, $meta, array $config, $slug) { $dm = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $dm); $qb = $dm->createQueryBuilder($config['useObjectClass']); if (($identifier = $wrapped->getIdentifier()) && !$meta->isIdentifier($config['slug'])) { $qb->field($meta->getIdentifier()[0])->notEqual($identifier); } $qb->field($config['slug'])->equals(new Regex('^'.preg_quote($slug, '/'))); // use the unique_base to restrict the uniqueness check if ($config['unique'] && isset($config['unique_base'])) { if (is_object($ubase = $wrapped->getPropertyValue($config['unique_base']))) { $qb->field($config['unique_base'].'.$id')->equals(new \MongoId($ubase->getId())); } elseif ($ubase) { $qb->where('/^'.preg_quote($ubase, '/').'/.test(this.'.$config['unique_base'].')'); } else { $qb->field($config['unique_base'])->equals(null); } } $q = $qb->getQuery(); $q->setHydrate(false); $result = $q->execute(); if ($result instanceof Iterator) { $result = $result->toArray(); } return $result; } /** * This query can cause some data integrity failures since it does not * execute automatically * * {@inheritdoc} */ public function replaceRelative($object, array $config, $target, $replacement) { $dm = $this->getObjectManager(); $meta = $dm->getClassMetadata($config['useObjectClass']); $q = $dm ->createQueryBuilder($config['useObjectClass']) ->where("function() { return this.{$config['slug']}.indexOf('{$target}') === 0; }") ->getQuery() ; $q->setHydrate(false); $result = $q->execute(); if (!$result instanceof Iterator) { return 0; } $result = $result->toArray(); foreach ($result as $targetObject) { $slug = preg_replace("@^{$target}@smi", $replacement.$config['pathSeparator'], $targetObject[$config['slug']]); $dm ->createQueryBuilder() ->updateMany($config['useObjectClass']) ->field($config['slug'])->set($slug) ->field($meta->getIdentifier()[0])->equals($targetObject['_id']) ->getQuery() ->execute() ; } return count($result); } /** * This query can cause some data integrity failures since it does not * execute atomically * * {@inheritdoc} */ public function replaceInverseRelative($object, array $config, $target, $replacement) { $dm = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $dm); $meta = $dm->getClassMetadata($config['useObjectClass']); $q = $dm ->createQueryBuilder($config['useObjectClass']) ->field($config['mappedBy'].'.'.$meta->getIdentifier()[0])->equals($wrapped->getIdentifier()) ->getQuery() ; $q->setHydrate(false); $result = $q->execute(); if (!$result instanceof Iterator) { return 0; } $result = $result->toArray(); foreach ($result as $targetObject) { $slug = preg_replace("@^{$replacement}@smi", $target, $targetObject[$config['slug']]); $dm ->createQueryBuilder() ->updateMany($config['useObjectClass']) ->field($config['slug'])->set($slug) ->field($meta->getIdentifier()[0])->equals($targetObject['_id']) ->getQuery() ->execute() ; } return count($result); } } doctrine-extensions/src/Sluggable/Mapping/Event/Adapter/ORM.php 0000644 00000011445 15117737236 0020467 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Mapping\Event\Adapter; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Query; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; use Gedmo\Tool\Wrapper\AbstractWrapper; use Gedmo\Tool\Wrapper\EntityWrapper; /** * Doctrine event adapter for ORM adapted * for sluggable behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class ORM extends BaseAdapterORM implements SluggableAdapter { public function getSimilarSlugs($object, $meta, array $config, $slug) { $em = $this->getObjectManager(); /** @var EntityWrapper $wrapped */ $wrapped = AbstractWrapper::wrap($object, $em); $qb = $em->createQueryBuilder(); $qb->select('rec.'.$config['slug']) ->from($config['useObjectClass'], 'rec') ->where($qb->expr()->like( 'rec.'.$config['slug'], ':slug') ) ; $qb->setParameter('slug', $slug.'%'); // use the unique_base to restrict the uniqueness check if ($config['unique'] && isset($config['unique_base'])) { $ubase = $wrapped->getPropertyValue($config['unique_base']); if (array_key_exists($config['unique_base'], $wrapped->getMetadata()->getAssociationMappings())) { $mapping = $wrapped->getMetadata()->getAssociationMapping($config['unique_base']); } else { $mapping = false; } if (($ubase || 0 === $ubase) && !$mapping) { $qb->andWhere('rec.'.$config['unique_base'].' = :unique_base'); $qb->setParameter(':unique_base', $ubase); } elseif ($ubase && $mapping && in_array($mapping['type'], [ClassMetadataInfo::ONE_TO_ONE, ClassMetadataInfo::MANY_TO_ONE], true)) { $mappedAlias = 'mapped_'.$config['unique_base']; $wrappedUbase = AbstractWrapper::wrap($ubase, $em); $metadata = $wrappedUbase->getMetadata(); assert($metadata instanceof ClassMetadataInfo); $qb->innerJoin('rec.'.$config['unique_base'], $mappedAlias); foreach (array_keys($mapping['targetToSourceKeyColumns']) as $i => $mappedKey) { $mappedProp = $metadata->getFieldName($mappedKey); $qb->andWhere($qb->expr()->eq($mappedAlias.'.'.$mappedProp, ':assoc'.$i)); $qb->setParameter(':assoc'.$i, $wrappedUbase->getPropertyValue($mappedProp)); } } else { $qb->andWhere($qb->expr()->isNull('rec.'.$config['unique_base'])); } } // include identifiers foreach ((array) $wrapped->getIdentifier(false) as $id => $value) { if (!$meta->isIdentifier($config['slug'])) { $namedId = str_replace('.', '_', $id); $qb->andWhere($qb->expr()->neq('rec.'.$id, ':'.$namedId)); $qb->setParameter($namedId, $value, $meta->getTypeOfField($namedId)); } } $q = $qb->getQuery(); $q->setHydrationMode(Query::HYDRATE_ARRAY); return $q->execute(); } public function replaceRelative($object, array $config, $target, $replacement) { $em = $this->getObjectManager(); $qb = $em->createQueryBuilder(); $qb->update($config['useObjectClass'], 'rec') ->set('rec.'.$config['slug'], $qb->expr()->concat( $qb->expr()->literal($replacement), $qb->expr()->substring('rec.'.$config['slug'], mb_strlen($target)) )) ->where($qb->expr()->like( 'rec.'.$config['slug'], $qb->expr()->literal($target.'%')) ) ; // update in memory $q = $qb->getQuery(); return $q->execute(); } public function replaceInverseRelative($object, array $config, $target, $replacement) { $em = $this->getObjectManager(); $qb = $em->createQueryBuilder(); $qb->update($config['useObjectClass'], 'rec') ->set('rec.'.$config['slug'], $qb->expr()->concat( $qb->expr()->literal($target), $qb->expr()->substring('rec.'.$config['slug'], mb_strlen($replacement) + 1) )) ->where($qb->expr()->like('rec.'.$config['slug'], $qb->expr()->literal($replacement.'%'))) ; $q = $qb->getQuery(); return $q->execute(); } } doctrine-extensions/src/Sluggable/Mapping/Event/SluggableAdapter.php 0000644 00000003545 15117737236 0021662 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Mapping\Event; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Sluggable\SluggableListener; /** * Doctrine event adapter for the Sluggable extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @phpstan-import-type SluggableConfiguration from SluggableListener */ interface SluggableAdapter extends AdapterInterface { /** * Loads the similar slugs for a managed object. * * @param object $object * @param ClassMetadata $meta * @param string $slug * @phpstan-param SluggableConfiguration $config * * @return array */ public function getSimilarSlugs($object, $meta, array $config, $slug); /** * Replace part of a slug on all objects matching the target pattern. * * @param object $object * @param string $target * @param string $replacement * @phpstan-param SluggableConfiguration $config * * @return int the number of updated records */ public function replaceRelative($object, array $config, $target, $replacement); /** * Replace part of a slug on all objects matching the target pattern * and having a relation to the managed object. * * @param object $object * @param string $target * @param string $replacement * @phpstan-param SluggableConfiguration $config * * @return int the number of updated records */ public function replaceInverseRelative($object, array $config, $target, $replacement); } doctrine-extensions/src/Sluggable/Util/Urlizer.php 0000644 00000000764 15117737236 0016351 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable\Util; use Behat\Transliterator\Transliterator; /** * Transliteration utility * * @final since gedmo/doctrine-extensions 3.11 */ class Urlizer extends Transliterator { } doctrine-extensions/src/Sluggable/Sluggable.php 0000644 00000004145 15117737236 0015702 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable; /** * This interface is not necessary but can be implemented for * Entities which in some cases needs to be identified as * Sluggable * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface Sluggable { // use now annotations instead of predefined methods, this interface is not necessary /* * @gedmo:Sluggable * to mark the field as sluggable use property annotation @gedmo:Sluggable * this field value will be included in built slug */ /* * @gedmo:Slug - to mark property which will hold slug use annotation @gedmo:Slug * available options: * updatable (optional, default=true) - true to update the slug on sluggable field changes, false - otherwise * unique (optional, default=true) - true if slug should be unique and if identical it will be prefixed, false - otherwise * unique_base (optional, default="") - used in conjunction with unique. The name of the entity property that should be used as a key when doing a uniqueness check * separator (optional, default="-") - separator which will separate words in slug * prefix (optional, default="") - prefix which will be added to the generated slug * suffix (optional, default="") - suffix which will be added to the generated slug * style (optional, default="default") - "default" all letters will be lowercase, "camel" - first word letter will be uppercase * dateFormat (optional, default="default") - "default" all letters will be lowercase, "camel" - first word letter will be uppercase * * example: * * @gedmo:Slug(style="camel", separator="_", prefix="", suffix="", updatable=false, unique=false) * @Column(type="string", length=64) * $property */ } doctrine-extensions/src/Sluggable/SluggableListener.php 0000644 00000047177 15117737236 0017424 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sluggable; use Doctrine\Common\EventArgs; use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\MappedEventSubscriber; use Gedmo\Sluggable\Handler\SlugHandlerInterface; use Gedmo\Sluggable\Handler\SlugHandlerWithUniqueCallbackInterface; use Gedmo\Sluggable\Mapping\Event\SluggableAdapter; use Gedmo\Sluggable\Util\Urlizer; /** * The SluggableListener handles the generation of slugs * for documents and entities. * * This behavior can impact the performance of your application * since it does some additional calculations on persisted objects. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Klein Florian <florian.klein@free.fr> * * @phpstan-type SluggableConfiguration = array{ * mappedBy?: string, * pathSeparator?: string, * slug?: string, * slugs?: array<string, array{ * fields?: string[], * slug?: string, * style?: string, * dateFormat?: string, * updatable?: bool, * unique?: bool, * unique_base?: string, * separator?: string, * prefix?: string, * suffix?: string, * handlers?: array<class-string, array{ * mappedBy?: string, * inverseSlugField?: string, * parentRelationField?: string, * relationClass?: class-string, * relationField?: string, * relationSlugField?: string, * separator?: string, * }>, * }>, * unique?: bool, * useObjectClass?: class-string, * } * * @phpstan-method SluggableConfiguration getConfiguration(ObjectManager $objectManager, $class) * * @method SluggableAdapter getEventAdapter(EventArgs $args) */ class SluggableListener extends MappedEventSubscriber { /** * The power exponent to jump * the slug unique number by tens. * * @var int */ private $exponent = 0; /** * Transliteration callback for slugs * * @var callable */ private $transliterator = [Urlizer::class, 'transliterate']; /** * Urlize callback for slugs * * @var callable */ private $urlizer = [Urlizer::class, 'urlize']; /** * List of inserted slugs for each object class. * This is needed in case there are identical slug * composition in number of persisted objects * during the same flush * * @var array */ private $persisted = []; /** * List of initialized slug handlers * * @var array */ private $handlers = []; /** * List of filters which are manipulated when slugs are generated * * @var array */ private $managedFilters = []; /** * Specifies the list of events to listen * * @return string[] */ public function getSubscribedEvents() { return [ 'onFlush', 'loadClassMetadata', 'prePersist', ]; } /** * Set the transliteration callable method * to transliterate slugs * * @param callable $callable * * @throws \Gedmo\Exception\InvalidArgumentException * * @return void */ public function setTransliterator($callable) { if (!is_callable($callable)) { throw new \Gedmo\Exception\InvalidArgumentException('Invalid transliterator callable parameter given'); } $this->transliterator = $callable; } /** * Set the urlization callable method * to urlize slugs * * @param callable $callable * * @return void */ public function setUrlizer($callable) { if (!is_callable($callable)) { throw new \Gedmo\Exception\InvalidArgumentException('Invalid urlizer callable parameter given'); } $this->urlizer = $callable; } /** * Get currently used transliterator callable * * @return callable */ public function getTransliterator() { return $this->transliterator; } /** * Get currently used urlizer callable * * @return callable */ public function getUrlizer() { return $this->urlizer; } /** * Enables or disables the given filter when slugs are generated * * @param string $name * @param bool $disable True by default * * @return void */ public function addManagedFilter($name, $disable = true) { $this->managedFilters[$name] = ['disabled' => $disable]; } /** * Removes a filter from the managed set * * @param string $name * * @return void */ public function removeManagedFilter($name) { unset($this->managedFilters[$name]); } /** * Mapps additional metadata * * @param LoadClassMetadataEventArgs $eventArgs * * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } /** * Allows identifier fields to be slugged as usual * * @return void */ public function prePersist(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); if ($config = $this->getConfiguration($om, $meta->getName())) { foreach ($config['slugs'] as $slugField => $options) { if ($meta->isIdentifier($slugField)) { $meta->getReflectionProperty($slugField)->setValue($object, '__id__'); } } } } /** * Generate slug on objects being updated during flush * if they require changing * * @return void */ public function onFlush(EventArgs $args) { $this->persisted = []; $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); $this->manageFiltersBeforeGeneration($om); // process all objects being inserted, using scheduled insertions instead // of prePersist in case if record will be changed before flushing this will // ensure correct result. No additional overhead is encountered foreach ($ea->getScheduledObjectInsertions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); if ($this->getConfiguration($om, $meta->getName())) { // generate first to exclude this object from similar persisted slugs result $this->generateSlug($ea, $object); $this->persisted[$ea->getRootObjectClass($meta)][] = $object; } } // we use onFlush and not preUpdate event to let other // event listeners be nested together foreach ($ea->getScheduledObjectUpdates($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); if ($this->getConfiguration($om, $meta->getName()) && !$uow->isScheduledForInsert($object)) { $this->generateSlug($ea, $object); $this->persisted[$ea->getRootObjectClass($meta)][] = $object; } } $this->manageFiltersAfterGeneration($om); } protected function getNamespace() { return __NAMESPACE__; } /** * Get the slug handler instance by $class name * * @phpstan-param class-string $class */ private function getHandler(string $class): SlugHandlerInterface { if (!isset($this->handlers[$class])) { $this->handlers[$class] = new $class($this); } return $this->handlers[$class]; } /** * Creates the slug for object being flushed */ private function generateSlug(SluggableAdapter $ea, object $object): void { $om = $ea->getObjectManager(); $meta = $om->getClassMetadata(get_class($object)); $uow = $om->getUnitOfWork(); $changeSet = $ea->getObjectChangeSet($uow, $object); $isInsert = $uow->isScheduledForInsert($object); $config = $this->getConfiguration($om, $meta->getName()); foreach ($config['slugs'] as $slugField => $options) { $hasHandlers = [] !== $options['handlers']; $options['useObjectClass'] = $config['useObjectClass']; // collect the slug from fields $slug = $meta->getReflectionProperty($slugField)->getValue($object); // if slug should not be updated, skip it if (!$options['updatable'] && !$isInsert && (!isset($changeSet[$slugField]) || '__id__' === $slug)) { continue; } // must fetch the old slug from changeset, since $object holds the new version $oldSlug = isset($changeSet[$slugField]) ? $changeSet[$slugField][0] : $slug; $needToChangeSlug = false; // if slug is null, regenerate it, or needs an update if (null === $slug || '__id__' === $slug || !isset($changeSet[$slugField])) { $slug = ''; foreach ($options['fields'] as $sluggableField) { if (isset($changeSet[$sluggableField]) || isset($changeSet[$slugField])) { $needToChangeSlug = true; } $value = $meta->getReflectionProperty($sluggableField)->getValue($object); $slug .= $value instanceof \DateTimeInterface ? $value->format($options['dateFormat']) : $value; $slug .= ' '; } // trim generated slug as it will have unnecessary trailing space $slug = trim($slug); } else { // slug was set manually $needToChangeSlug = true; } // notify slug handlers --> onChangeDecision if ($hasHandlers) { foreach ($options['handlers'] as $class => $handlerOptions) { $this->getHandler($class)->onChangeDecision($ea, $options, $object, $slug, $needToChangeSlug); } } // if slug is changed, do further processing if ($needToChangeSlug) { $mapping = $meta->getFieldMapping($slugField); // notify slug handlers --> postSlugBuild $urlized = false; if ($hasHandlers) { foreach ($options['handlers'] as $class => $handlerOptions) { $this->getHandler($class)->postSlugBuild($ea, $options, $object, $slug); if ($this->getHandler($class)->handlesUrlization()) { $urlized = true; } } } // build the slug // Step 1: transliteration, changing 北京 to 'Bei Jing' $slug = call_user_func_array( $this->transliterator, [$slug, $options['separator'], $object] ); // Step 2: urlization (replace spaces by '-' etc...) if (!$urlized) { $slug = call_user_func_array( $this->urlizer, [$slug, $options['separator'], $object] ); } // add suffix/prefix $slug = $options['prefix'].$slug.$options['suffix']; // Step 3: stylize the slug switch ($options['style']) { case 'camel': $quotedSeparator = preg_quote($options['separator']); $slug = preg_replace_callback('/^[a-z]|'.$quotedSeparator.'[a-z]/smi', static function ($m) { return strtoupper($m[0]); }, $slug); break; case 'lower': if (function_exists('mb_strtolower')) { $slug = mb_strtolower($slug); } else { $slug = strtolower($slug); } break; case 'upper': if (function_exists('mb_strtoupper')) { $slug = mb_strtoupper($slug); } else { $slug = strtoupper($slug); } break; default: // leave it as is break; } // cut slug if exceeded in length if (isset($mapping['length']) && strlen($slug) > $mapping['length']) { $slug = substr($slug, 0, $mapping['length']); } if (isset($mapping['nullable']) && $mapping['nullable'] && 0 === strlen($slug)) { $slug = null; } // notify slug handlers --> beforeMakingUnique if ($hasHandlers) { foreach ($options['handlers'] as $class => $handlerOptions) { $handler = $this->getHandler($class); if ($handler instanceof SlugHandlerWithUniqueCallbackInterface) { $handler->beforeMakingUnique($ea, $options, $object, $slug); } } } // make unique slug if requested if ($options['unique'] && null !== $slug) { $this->exponent = 0; $slug = $this->makeUniqueSlug($ea, $object, $slug, false, $options); } // notify slug handlers --> onSlugCompletion if ($hasHandlers) { foreach ($options['handlers'] as $class => $handlerOptions) { $this->getHandler($class)->onSlugCompletion($ea, $options, $object, $slug); } } // set the final slug $meta->getReflectionProperty($slugField)->setValue($object, $slug); // recompute changeset $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); // overwrite changeset (to set old value) $uow->propertyChanged($object, $slugField, $oldSlug, $slug); } } } /** * Generates the unique slug */ private function makeUniqueSlug(SluggableAdapter $ea, object $object, string $preferredSlug, bool $recursing = false, array $config = []): string { $om = $ea->getObjectManager(); $meta = $om->getClassMetadata(get_class($object)); $similarPersisted = []; // extract unique base $base = false; if ($config['unique'] && isset($config['unique_base'])) { $base = $meta->getReflectionProperty($config['unique_base'])->getValue($object); } // collect similar persisted slugs during this flush if (isset($this->persisted[$class = $ea->getRootObjectClass($meta)])) { foreach ($this->persisted[$class] as $obj) { if (false !== $base && $meta->getReflectionProperty($config['unique_base'])->getValue($obj) !== $base) { continue; // if unique_base field is not the same, do not take slug as similar } $slug = $meta->getReflectionProperty($config['slug'])->getValue($obj); $quotedPreferredSlug = preg_quote($preferredSlug); if (preg_match("@^{$quotedPreferredSlug}.*@smi", $slug)) { $similarPersisted[] = [$config['slug'] => $slug]; } } } // load similar slugs $result = array_merge($ea->getSimilarSlugs($object, $meta, $config, $preferredSlug), $similarPersisted); // leave only right slugs if (!$recursing) { // filter similar slugs $quotedSeparator = preg_quote($config['separator']); $quotedPreferredSlug = preg_quote($preferredSlug); foreach ($result as $key => $similar) { if (!preg_match("@{$quotedPreferredSlug}($|{$quotedSeparator}[\d]+$)@smi", $similar[$config['slug']])) { unset($result[$key]); } } } if ($result) { $generatedSlug = $preferredSlug; $sameSlugs = []; foreach ((array) $result as $list) { $sameSlugs[] = $list[$config['slug']]; } $i = pow(10, $this->exponent); $uniqueSuffix = (string) $i; if ($recursing || in_array($generatedSlug, $sameSlugs, true)) { do { $generatedSlug = $preferredSlug.$config['separator'].$uniqueSuffix; $uniqueSuffix = (string) ++$i; } while (in_array($generatedSlug, $sameSlugs, true)); } $mapping = $meta->getFieldMapping($config['slug']); if (isset($mapping['length']) && strlen($generatedSlug) > $mapping['length']) { $generatedSlug = substr( $generatedSlug, 0, $mapping['length'] - (strlen($uniqueSuffix) + strlen($config['separator'])) ); $this->exponent = strlen($uniqueSuffix) - 1; if (substr($generatedSlug, -strlen($config['separator'])) == $config['separator']) { $generatedSlug = substr($generatedSlug, 0, strlen($generatedSlug) - strlen($config['separator'])); } $generatedSlug = $this->makeUniqueSlug($ea, $object, $generatedSlug, true, $config); } $preferredSlug = $generatedSlug; } return $preferredSlug; } private function manageFiltersBeforeGeneration(ObjectManager $om): void { $collection = $this->getFilterCollectionFromObjectManager($om); $enabledFilters = array_keys($collection->getEnabledFilters()); // set each managed filter to desired status foreach ($this->managedFilters as $name => &$config) { $enabled = in_array($name, $enabledFilters, true); $config['previouslyEnabled'] = $enabled; if ($config['disabled']) { if ($enabled) { $collection->disable($name); } } else { $collection->enable($name); } } } private function manageFiltersAfterGeneration(ObjectManager $om): void { $collection = $this->getFilterCollectionFromObjectManager($om); // Restore managed filters to their original status foreach ($this->managedFilters as $name => &$config) { if (true === $config['previouslyEnabled']) { $collection->enable($name); } unset($config['previouslyEnabled']); } } /** * Retrieves a FilterCollection instance from the given ObjectManager. * * @throws \Gedmo\Exception\InvalidArgumentException * * @return mixed */ private function getFilterCollectionFromObjectManager(ObjectManager $om) { if (is_callable([$om, 'getFilters'])) { return $om->getFilters(); } if (is_callable([$om, 'getFilterCollection'])) { return $om->getFilterCollection(); } throw new \Gedmo\Exception\InvalidArgumentException('ObjectManager does not support filters'); } } doctrine-extensions/src/SoftDeleteable/Filter/ODM/SoftDeleteableFilter.php 0000644 00000007005 15117737236 0022644 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Filter\ODM; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Query\Filter\BsonFilter; use Gedmo\SoftDeleteable\SoftDeleteableListener; /** * @final since gedmo/doctrine-extensions 3.11 */ class SoftDeleteableFilter extends BsonFilter { /** * @var SoftDeleteableListener|null */ protected $listener; /** * @var DocumentManager|null * * @deprecated `BsonFilter::$dm` is a protected property, thus this property is not required */ protected $documentManager; /** * @var array<string, bool> */ protected $disabled = []; /** * Gets the criteria part to add to a query. * * @return array The criteria array, if there is available, empty array otherwise */ public function addFilterCriteria(ClassMetadata $targetEntity): array { $class = $targetEntity->getName(); if (true === ($this->disabled[$class] ?? false)) { return []; } if (true === ($this->disabled[$targetEntity->rootDocumentName] ?? false)) { return []; } $config = $this->getListener()->getConfiguration($this->getDocumentManager(), $targetEntity->name); if (!isset($config['softDeleteable']) || !$config['softDeleteable']) { return []; } $column = $targetEntity->getFieldMapping($config['fieldName']); if (isset($config['timeAware']) && $config['timeAware']) { return [ '$or' => [ [$column['fieldName'] => null], [$column['fieldName'] => ['$gt' => new \DateTime()]], ], ]; } return [ $column['fieldName'] => null, ]; } /** * @param string $class * @phpstan-param class-string $class * * @return void */ public function disableForDocument($class) { $this->disabled[$class] = true; } /** * @param string $class * @phpstan-param class-string $class * * @return void */ public function enableForDocument($class) { $this->disabled[$class] = false; } /** * @return SoftDeleteableListener|null */ protected function getListener() { if (null === $this->listener) { $em = $this->getDocumentManager(); $evm = $em->getEventManager(); foreach ($evm->getAllListeners() as $listeners) { foreach ($listeners as $listener) { if ($listener instanceof SoftDeleteableListener) { $this->listener = $listener; break 2; } } } if (null === $this->listener) { throw new \RuntimeException('Listener "SoftDeleteableListener" was not added to the EventManager!'); } } return $this->listener; } /** * @return DocumentManager */ protected function getDocumentManager() { // Remove the following assignment on the next major release. $this->documentManager = $this->dm; return $this->dm; } } doctrine-extensions/src/SoftDeleteable/Filter/SoftDeleteableFilter.php 0000644 00000010627 15117737236 0022231 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Filter; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Filter\SQLFilter; use Gedmo\SoftDeleteable\SoftDeleteableListener; /** * The SoftDeleteableFilter adds the condition necessary to * filter entities which were deleted "softly" * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Patrik Votoček <patrik@votocek.cz> * * @final since gedmo/doctrine-extensions 3.11 */ class SoftDeleteableFilter extends SQLFilter { /** * @var SoftDeleteableListener */ protected $listener; /** * @var EntityManagerInterface */ protected $entityManager; /** * @var array<string, bool> * @phpstan-var array<class-string, bool> */ protected $disabled = []; /** * @param string $targetTableAlias * * @return string * * @throws \Doctrine\DBAL\Exception */ public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) { $class = $targetEntity->getName(); if (true === ($this->disabled[$class] ?? false)) { return ''; } if (true === ($this->disabled[$targetEntity->rootEntityName] ?? false)) { return ''; } $config = $this->getListener()->getConfiguration($this->getEntityManager(), $targetEntity->name); if (!isset($config['softDeleteable']) || !$config['softDeleteable']) { return ''; } $platform = $this->getConnection()->getDatabasePlatform(); $quoteStrategy = $this->getEntityManager()->getConfiguration()->getQuoteStrategy(); $column = $quoteStrategy->getColumnName($config['fieldName'], $targetEntity, $platform); $addCondSql = $platform->getIsNullExpression($targetTableAlias.'.'.$column); if (isset($config['timeAware']) && $config['timeAware']) { $addCondSql = "({$addCondSql} OR {$targetTableAlias}.{$column} > {$platform->getCurrentTimestampSQL()})"; } return $addCondSql; } /** * @param string $class * * @phpstan-param class-string $class * * @return void */ public function disableForEntity($class) { $this->disabled[$class] = true; // Make sure the hash (@see SQLFilter::__toString()) for this filter will be changed to invalidate the query cache. $this->setParameter(sprintf('disabled_%s', $class), true); } /** * @param string $class * * @phpstan-param class-string $class * * @return void */ public function enableForEntity($class) { $this->disabled[$class] = false; // Make sure the hash (@see SQLFilter::__toString()) for this filter will be changed to invalidate the query cache. $this->setParameter(sprintf('disabled_%s', $class), false); } /** * @return SoftDeleteableListener * * @throws \RuntimeException */ protected function getListener() { if (null === $this->listener) { $em = $this->getEntityManager(); $evm = $em->getEventManager(); foreach ($evm->getAllListeners() as $listeners) { foreach ($listeners as $listener) { if ($listener instanceof SoftDeleteableListener) { $this->listener = $listener; break 2; } } } if (null === $this->listener) { throw new \RuntimeException('Listener "SoftDeleteableListener" was not added to the EventManager!'); } } return $this->listener; } /** * @return EntityManagerInterface */ protected function getEntityManager() { if (null === $this->entityManager) { $getEntityManager = \Closure::bind(function (): EntityManagerInterface { return $this->em; }, $this, parent::class); $this->entityManager = $getEntityManager(); } return $this->entityManager; } } doctrine-extensions/src/SoftDeleteable/Mapping/Driver/Annotation.php 0000644 00000004337 15117737236 0021715 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\SoftDeleteable; use Gedmo\Mapping\Driver\AbstractAnnotationDriver; use Gedmo\SoftDeleteable\Mapping\Validator; /** * 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 <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @internal */ class Annotation extends AbstractAnnotationDriver { /** * 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); } } doctrine-extensions/src/SoftDeleteable/Mapping/Driver/Attribute.php 0000644 00000001342 15117737236 0021537 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Mapping\Driver; use Gedmo\Mapping\Annotation\SoftDeleteable; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for SoftDeleteable * behavioral extension. Used for extraction of extended * metadata from attributes specifically for SoftDeleteable * extension. * * @internal */ final class Attribute extends Annotation implements AttributeDriverInterface { } doctrine-extensions/src/SoftDeleteable/Mapping/Driver/Xml.php 0000644 00000004362 15117737236 0020341 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver\Xml as BaseXml; use Gedmo\SoftDeleteable\Mapping\Validator; /** * This is a xml mapping driver for SoftDeleteable * behavioral extension. Used for extraction of extended * metadata from xml specifically for SoftDeleteable * extension. * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Miha Vrhovnik <miha.vrhovnik@gmail.com> * * @internal */ class Xml extends BaseXml { public function readExtendedMetadata($meta, array &$config) { /** * @var \SimpleXmlElement */ $xml = $this->_getMapping($meta->getName()); $xmlDoctrine = $xml; $xml = $xml->children(self::GEDMO_NAMESPACE_URI); if (in_array($xmlDoctrine->getName(), ['mapped-superclass', 'entity', 'document', 'embedded-document'], true)) { if (isset($xml->{'soft-deleteable'})) { $field = $this->_getAttribute($xml->{'soft-deleteable'}, 'field-name'); if (!$field) { throw new InvalidMappingException('Field name for SoftDeleteable class is mandatory.'); } Validator::validateField($meta, $field); $config['softDeleteable'] = true; $config['fieldName'] = $field; $config['timeAware'] = false; if ($this->_isAttributeSet($xml->{'soft-deleteable'}, 'time-aware')) { $config['timeAware'] = $this->_getBooleanAttribute($xml->{'soft-deleteable'}, 'time-aware'); } $config['hardDelete'] = true; if ($this->_isAttributeSet($xml->{'soft-deleteable'}, 'hard-delete')) { $config['hardDelete'] = $this->_getBooleanAttribute($xml->{'soft-deleteable'}, 'hard-delete'); } } } } } doctrine-extensions/src/SoftDeleteable/Mapping/Driver/Yaml.php 0000644 00000005603 15117737236 0020502 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; use Gedmo\SoftDeleteable\Mapping\Validator; /** * This is a yaml mapping driver for Timestampable * behavioral extension. Used for extraction of extended * metadata from yaml specifically for Timestampable * extension. * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['gedmo'])) { $classMapping = $mapping['gedmo']; if (isset($classMapping['soft_deleteable'])) { $config['softDeleteable'] = true; if (!isset($classMapping['soft_deleteable']['field_name'])) { throw new InvalidMappingException('Field name for SoftDeleteable class is mandatory.'); } $fieldName = $classMapping['soft_deleteable']['field_name']; Validator::validateField($meta, $fieldName); $config['fieldName'] = $fieldName; $config['timeAware'] = false; if (isset($classMapping['soft_deleteable']['time_aware'])) { if (!is_bool($classMapping['soft_deleteable']['time_aware'])) { throw new InvalidMappingException('timeAware must be boolean. '.gettype($classMapping['soft_deleteable']['time_aware']).' provided.'); } $config['timeAware'] = $classMapping['soft_deleteable']['time_aware']; } $config['hardDelete'] = true; if (isset($classMapping['soft_deleteable']['hard_delete'])) { if (!is_bool($classMapping['soft_deleteable']['hard_delete'])) { throw new InvalidMappingException('hardDelete must be boolean. '.gettype($classMapping['soft_deleteable']['hard_delete']).' provided.'); } $config['hardDelete'] = $classMapping['soft_deleteable']['hard_delete']; } } } } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } } doctrine-extensions/src/SoftDeleteable/Mapping/Event/Adapter/ODM.php 0000644 00000002546 15117737236 0021430 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Mapping\Event\Adapter; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\SoftDeleteable\Mapping\Event\SoftDeleteableAdapter; /** * Doctrine event adapter for ORM adapted * for SoftDeleteable behavior. * * @author David Buchmann <mail@davidbu.ch> */ final class ODM extends BaseAdapterODM implements SoftDeleteableAdapter { /** * @param ClassMetadata $meta */ public function getDateValue($meta, $field) { $mapping = $meta->getFieldMapping($field); if (isset($mapping['type']) && 'timestamp' === $mapping['type']) { return time(); } if (isset($mapping['type']) && in_array($mapping['type'], ['date_immutable', 'time_immutable', 'datetime_immutable', 'datetimetz_immutable'], true)) { return new \DateTimeImmutable(); } return \DateTime::createFromFormat('U.u', number_format(microtime(true), 6, '.', '')) ->setTimeZone(new \DateTimeZone(date_default_timezone_get())); } } doctrine-extensions/src/SoftDeleteable/Mapping/Event/Adapter/ORM.php 0000644 00000003632 15117737236 0021443 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Mapping\Event\Adapter; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\ClassMetadata; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\SoftDeleteable\Mapping\Event\SoftDeleteableAdapter; /** * Doctrine event adapter for ORM adapted * for SoftDeleteable behavior. * * @author David Buchmann <mail@davidbu.ch> */ final class ORM extends BaseAdapterORM implements SoftDeleteableAdapter { /** * @param ClassMetadata $meta */ public function getDateValue($meta, $field) { $mapping = $meta->getFieldMapping($field); $converter = Type::getType($mapping['type'] ?? Types::DATETIME_MUTABLE); $platform = $this->getObjectManager()->getConnection()->getDriver()->getDatabasePlatform(); return $converter->convertToPHPValue($this->getRawDateValue($mapping), $platform); } /** * Generates current timestamp for the specified mapping * * @param array<string, mixed> $mapping * * @return \DateTimeInterface|int */ private function getRawDateValue(array $mapping) { if (isset($mapping['type']) && 'integer' === $mapping['type']) { return time(); } if (isset($mapping['type']) && in_array($mapping['type'], ['date_immutable', 'time_immutable', 'datetime_immutable', 'datetimetz_immutable'], true)) { return new \DateTimeImmutable(); } return \DateTime::createFromFormat('U.u', number_format(microtime(true), 6, '.', '')) ->setTimeZone(new \DateTimeZone(date_default_timezone_get())); } } doctrine-extensions/src/SoftDeleteable/Mapping/Event/SoftDeleteableAdapter.php 0000644 00000001521 15117737236 0023604 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Mapping\Event; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Mapping\Event\AdapterInterface; /** * Doctrine event adapter for the SoftDeleteable extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface SoftDeleteableAdapter extends AdapterInterface { /** * Get the date value. * * @param ClassMetadata $meta * @param string $field * * @return int|\DateTimeInterface */ public function getDateValue($meta, $field); } doctrine-extensions/src/SoftDeleteable/Mapping/Validator.php 0000644 00000003124 15117737236 0020266 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Mapping; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; /** * This class is used to validate mapping information * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class Validator { /** * List of types which are valid for timestamp * * @var array */ public static $validTypes = [ 'date', 'date_immutable', 'time', 'time_immutable', 'datetime', 'datetime_immutable', 'datetimetz', 'datetimetz_immutable', 'timestamp', ]; /** * @param mixed $field * * @return void */ public static function validateField(ClassMetadata $meta, $field) { if ($meta->isMappedSuperclass) { return; } $fieldMapping = $meta->getFieldMapping($field); if (!in_array($fieldMapping['type'], self::$validTypes, true)) { throw new InvalidMappingException(sprintf('Field "%s" (type "%s") must be of one of the following types: "%s" in entity %s', $field, $fieldMapping['type'], implode(', ', self::$validTypes), $meta->getName())); } } } doctrine-extensions/src/SoftDeleteable/Query/TreeWalker/Exec/MultiTableDeleteExecutor.php 0000644 00000004041 15117737236 0025707 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Query\TreeWalker\Exec; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\AST\Node; use Doctrine\ORM\Query\Exec\MultiTableDeleteExecutor as BaseMultiTableDeleteExecutor; /** * This class is used when a DELETE DQL query is called for entities * that are part of an inheritance tree * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class MultiTableDeleteExecutor extends BaseMultiTableDeleteExecutor { public function __construct(Node $AST, $sqlWalker, ClassMetadata $meta, AbstractPlatform $platform, array $config) { parent::__construct($AST, $sqlWalker); $sqlStatements = $this->_sqlStatements; $quoteStrategy = $sqlWalker->getEntityManager()->getConfiguration()->getQuoteStrategy(); foreach ($sqlStatements as $index => $stmt) { $matches = []; preg_match('/DELETE FROM (\w+) .+/', $stmt, $matches); if (isset($matches[1]) && $quoteStrategy->getTableName($meta, $platform) === $matches[1]) { $sqlStatements[$index] = str_replace('DELETE FROM', 'UPDATE', $stmt); $sqlStatements[$index] = str_replace( 'WHERE', 'SET '.$config['fieldName'].' = '.$platform->getCurrentTimestampSQL().' WHERE', $sqlStatements[$index] ); } else { // We have to avoid the removal of registers of child entities of a SoftDeleteable entity unset($sqlStatements[$index]); } } $this->_sqlStatements = $sqlStatements; } } doctrine-extensions/src/SoftDeleteable/Query/TreeWalker/SoftDeleteableWalker.php 0000644 00000012731 15117737236 0024154 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Query\TreeWalker; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\QuoteStrategy; use Doctrine\ORM\Query\AST\DeleteClause; use Doctrine\ORM\Query\AST\DeleteStatement; use Doctrine\ORM\Query\Exec\AbstractSqlExecutor; use Doctrine\ORM\Query\Exec\SingleTableDeleteUpdateExecutor; use Doctrine\ORM\Query\SqlWalker; use Gedmo\SoftDeleteable\Query\TreeWalker\Exec\MultiTableDeleteExecutor; use Gedmo\SoftDeleteable\SoftDeleteableListener; /** * This SqlWalker is needed when you need to use a DELETE DQL query. * It will update the "deletedAt" field with the actual date, instead * of actually deleting it. * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class SoftDeleteableWalker extends SqlWalker { /** * @var Connection * * @deprecated to be removed in 4.0, use the `getConnection()` method instead. */ protected $conn; /** * @var AbstractPlatform * * @deprecated to be removed in 4.0, fetch the platform from the connection instead */ protected $platform; /** * @var SoftDeleteableListener */ protected $listener; /** * @var array */ protected $configuration; /** * @var string|null * * @deprecated to be removed in 4.0, unused */ protected $alias; /** * @var string */ protected $deletedAtField; /** * @var ClassMetadata */ protected $meta; /** * @var QuoteStrategy */ private $quoteStrategy; public function __construct($query, $parserResult, array $queryComponents) { parent::__construct($query, $parserResult, $queryComponents); $this->conn = $this->getConnection(); $this->platform = $this->getConnection()->getDatabasePlatform(); $this->listener = $this->getSoftDeleteableListener(); $this->quoteStrategy = $this->getEntityManager()->getConfiguration()->getQuoteStrategy(); $this->extractComponents($this->getQueryComponents()); } /** * @return AbstractSqlExecutor */ public function getExecutor($AST) { switch (true) { case $AST instanceof DeleteStatement: $primaryClass = $this->getEntityManager()->getClassMetadata($AST->deleteClause->abstractSchemaName); return $primaryClass->isInheritanceTypeJoined() ? new MultiTableDeleteExecutor($AST, $this, $this->meta, $this->getConnection()->getDatabasePlatform(), $this->configuration) : new SingleTableDeleteUpdateExecutor($AST, $this); default: throw new \Gedmo\Exception\UnexpectedValueException('SoftDeleteable walker should be used only on delete statement'); } } /** * Change a DELETE clause for an UPDATE clause * * @return string the SQL */ public function walkDeleteClause(DeleteClause $deleteClause) { $em = $this->getEntityManager(); $class = $em->getClassMetadata($deleteClause->abstractSchemaName); $tableName = $class->getTableName(); $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable); $platform = $this->getConnection()->getDatabasePlatform(); $quotedTableName = $this->quoteStrategy->getTableName($class, $platform); $quotedColumnName = $this->quoteStrategy->getColumnName($this->deletedAtField, $class, $platform); return 'UPDATE '.$quotedTableName.' SET '.$quotedColumnName.' = '.$platform->getCurrentTimestampSQL(); } /** * Get the currently used SoftDeleteableListener * * @throws \Gedmo\Exception\RuntimeException if listener is not found */ private function getSoftDeleteableListener(): SoftDeleteableListener { if (null === $this->listener) { $em = $this->getEntityManager(); foreach ($em->getEventManager()->getAllListeners() as $listeners) { foreach ($listeners as $listener) { if ($listener instanceof SoftDeleteableListener) { $this->listener = $listener; break 2; } } } if (null === $this->listener) { throw new \Gedmo\Exception\RuntimeException('The SoftDeleteable listener could not be found.'); } } return $this->listener; } /** * Search for components in the delete clause */ private function extractComponents(array $queryComponents): void { $em = $this->getEntityManager(); foreach ($queryComponents as $comp) { $meta = $comp['metadata']; $config = $this->listener->getConfiguration($em, $meta->getName()); if ($config && isset($config['softDeleteable']) && $config['softDeleteable']) { $this->configuration = $config; $this->deletedAtField = $config['fieldName']; $this->meta = $meta; } } } } doctrine-extensions/src/SoftDeleteable/Traits/SoftDeleteable.php 0000644 00000002405 15117737236 0021077 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Traits; use DateTime; /** * A generic trait to use on your self-deletable entities. * There is no mapping information defined in this trait. * * @author Wesley van Opdorp <wesley.van.opdorp@freshheads.com> */ trait SoftDeleteable { /** * @var DateTime|null */ protected $deletedAt; /** * Set or clear the deleted at timestamp. * * @return self */ public function setDeletedAt(DateTime $deletedAt = null) { $this->deletedAt = $deletedAt; return $this; } /** * Get the deleted at timestamp value. Will return null if * the entity has not been soft deleted. * * @return DateTime|null */ public function getDeletedAt() { return $this->deletedAt; } /** * Check if the entity has been soft deleted. * * @return bool */ public function isDeleted() { return null !== $this->deletedAt; } } doctrine-extensions/src/SoftDeleteable/Traits/SoftDeleteableDocument.php 0000644 00000002645 15117737236 0022604 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Traits; use DateTime; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Types\Type; /** * A soft deletable trait you can apply to your MongoDB entities. * Includes default annotation mapping. * * @author Wesley van Opdorp <wesley.van.opdorp@freshheads.com> */ trait SoftDeleteableDocument { /** * @ODM\Field(type="date") * * @var DateTime|null */ #[ODM\Field(type: Type::DATE)] protected $deletedAt; /** * Set or clear the deleted at timestamp. * * @return self */ public function setDeletedAt(DateTime $deletedAt = null) { $this->deletedAt = $deletedAt; return $this; } /** * Get the deleted at timestamp value. Will return null if * the entity has not been soft deleted. * * @return DateTime|null */ public function getDeletedAt() { return $this->deletedAt; } /** * Check if the entity has been soft deleted. * * @return bool */ public function isDeleted() { return null !== $this->deletedAt; } } doctrine-extensions/src/SoftDeleteable/Traits/SoftDeleteableEntity.php 0000644 00000002700 15117737236 0022272 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable\Traits; use DateTime; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * A soft deletable trait you can apply to your Doctrine ORM entities. * Includes default annotation mapping. * * @author Wesley van Opdorp <wesley.van.opdorp@freshheads.com> */ trait SoftDeleteableEntity { /** * @ORM\Column(type="datetime", nullable=true) * * @var DateTime|null */ #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] protected $deletedAt; /** * Set or clear the deleted at timestamp. * * @return self */ public function setDeletedAt(DateTime $deletedAt = null) { $this->deletedAt = $deletedAt; return $this; } /** * Get the deleted at timestamp value. Will return null if * the entity has not been soft deleted. * * @return DateTime|null */ public function getDeletedAt() { return $this->deletedAt; } /** * Check if the entity has been soft deleted. * * @return bool */ public function isDeleted() { return null !== $this->deletedAt; } } doctrine-extensions/src/SoftDeleteable/SoftDeleteable.php 0000644 00000001662 15117737236 0017635 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable; /** * This interface is not necessary but can be implemented for * Domain Objects which in some cases needs to be identified as * SoftDeleteable * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface SoftDeleteable { // this interface is not necessary to implement /* * @gedmo:SoftDeleteable * to mark the class as SoftDeleteable use class annotation @gedmo:SoftDeleteable * this object will be able to be soft deleted * example: * * @gedmo:SoftDeleteable * class MyEntity */ } doctrine-extensions/src/SoftDeleteable/SoftDeleteableListener.php 0000644 00000007210 15117737236 0021336 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\SoftDeleteable; use Doctrine\Common\EventArgs; use Doctrine\ODM\MongoDB\UnitOfWork as MongoDBUnitOfWork; use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; use Gedmo\Mapping\MappedEventSubscriber; /** * SoftDeleteable listener * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class SoftDeleteableListener extends MappedEventSubscriber { /** * Pre soft-delete event * * @var string */ public const PRE_SOFT_DELETE = 'preSoftDelete'; /** * Post soft-delete event * * @var string */ public const POST_SOFT_DELETE = 'postSoftDelete'; /** * @return string[] */ public function getSubscribedEvents() { return [ 'loadClassMetadata', 'onFlush', ]; } /** * If it's a SoftDeleteable object, update the "deletedAt" field * and skip the removal of the object * * @return void */ public function onFlush(EventArgs $args) { $ea = $this->getEventAdapter($args); /** @var \Doctrine\ORM\EntityManagerInterface|\Doctrine\ODM\MongoDB\DocumentManager $om */ $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); $evm = $om->getEventManager(); // getScheduledDocumentDeletions foreach ($ea->getScheduledObjectDeletions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['softDeleteable']) && $config['softDeleteable']) { $reflProp = $meta->getReflectionProperty($config['fieldName']); $oldValue = $reflProp->getValue($object); $date = $ea->getDateValue($meta, $config['fieldName']); if (isset($config['hardDelete']) && $config['hardDelete'] && $oldValue instanceof \DateTimeInterface && $oldValue <= $date) { continue; // want to hard delete } $evm->dispatchEvent( self::PRE_SOFT_DELETE, $ea->createLifecycleEventArgsInstance($object, $om) ); $reflProp->setValue($object, $date); $om->persist($object); $uow->propertyChanged($object, $config['fieldName'], $oldValue, $date); if ($uow instanceof MongoDBUnitOfWork) { $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); } else { $uow->scheduleExtraUpdate($object, [ $config['fieldName'] => [$oldValue, $date], ]); } $evm->dispatchEvent( self::POST_SOFT_DELETE, $ea->createLifecycleEventArgsInstance($object, $om) ); } } } /** * Maps additional metadata * * @param LoadClassMetadataEventArgs $eventArgs * * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } protected function getNamespace() { return __NAMESPACE__; } } doctrine-extensions/src/Sortable/Entity/Repository/SortableRepository.php 0000644 00000006364 15117737236 0023156 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sortable\Entity\Repository; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; use Gedmo\Sortable\SortableListener; /** * Sortable Repository * * @author Lukas Botsch <lukas.botsch@gmail.com> */ class SortableRepository extends EntityRepository { /** * Sortable listener on event manager * * @var SortableListener */ protected $listener; /** * @var array */ protected $config; /** * @var ClassMetadata */ protected $meta; public function __construct(EntityManagerInterface $em, ClassMetadata $class) { parent::__construct($em, $class); $sortableListener = null; foreach ($em->getEventManager()->getAllListeners() as $event => $listeners) { foreach ($listeners as $hash => $listener) { if ($listener instanceof SortableListener) { $sortableListener = $listener; break 2; } } } if (null === $sortableListener) { throw new \Gedmo\Exception\InvalidMappingException('This repository can be attached only to ORM sortable listener'); } $this->listener = $sortableListener; $this->meta = $this->getClassMetadata(); $this->config = $this->listener->getConfiguration($this->_em, $this->meta->getName()); } /** * @return \Doctrine\ORM\Query */ public function getBySortableGroupsQuery(array $groupValues = []) { return $this->getBySortableGroupsQueryBuilder($groupValues)->getQuery(); } /** * @return \Doctrine\ORM\QueryBuilder */ public function getBySortableGroupsQueryBuilder(array $groupValues = []) { $groups = isset($this->config['groups']) ? array_combine(array_values($this->config['groups']), array_keys($this->config['groups'])) : []; foreach ($groupValues as $name => $value) { if (!in_array($name, $this->config['groups'], true)) { throw new \InvalidArgumentException('Sortable group "'.$name.'" is not defined in Entity '.$this->meta->getName()); } unset($groups[$name]); } if ([] !== $groups) { throw new \InvalidArgumentException('You need to specify values for the following groups to select by sortable groups: '.implode(', ', array_keys($groups))); } $qb = $this->createQueryBuilder('n'); $qb->orderBy('n.'.$this->config['position']); $i = 1; foreach ($groupValues as $group => $value) { $qb->andWhere('n.'.$group.' = :group'.$i) ->setParameter('group'.$i, $value); ++$i; } return $qb; } /** * @return array */ public function getBySortableGroups(array $groupValues = []) { $query = $this->getBySortableGroupsQuery($groupValues); return $query->getResult(); } } doctrine-extensions/src/Sortable/Mapping/Driver/Annotation.php 0000644 00000006311 15117737236 0020600 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sortable\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\SortableGroup; use Gedmo\Mapping\Annotation\SortablePosition; use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** * 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 <lukas.botsch@gmail.com> * * @internal */ class Annotation extends AbstractAnnotationDriver { /** * 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()}"); } } } } doctrine-extensions/src/Sortable/Mapping/Driver/Attribute.php 0000644 00000001363 15117737236 0020433 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sortable\Mapping\Driver; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for Sortable * behavioral extension. Used for extraction of extended * metadata from attributes specifically for Sortable * extension. * * @license MIT License (http://www.opensource.org/licenses/mit-license.php) * * @internal */ final class Attribute extends Annotation implements AttributeDriverInterface { } doctrine-extensions/src/Sortable/Mapping/Driver/Xml.php 0000644 00000006543 15117737236 0017235 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sortable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for Sortable * behavioral extension. Used for extraction of extended * metadata from xml specifically for Sortable * extension. * * @author Lukas Botsch <lukas.botsch@gmail.com> * * @internal */ class Xml extends BaseXml { /** * List of types which are valid for position field * * @var string[] */ private const VALID_TYPES = [ 'int', 'integer', 'smallint', 'bigint', ]; public function readExtendedMetadata($meta, array &$config) { /** * @var \SimpleXmlElement */ $xml = $this->_getMapping($meta->getName()); if (isset($xml->field)) { foreach ($xml->field as $mappingDoctrine) { $mapping = $mappingDoctrine->children(self::GEDMO_NAMESPACE_URI); $field = $this->_getAttribute($mappingDoctrine, 'name'); if (isset($mapping->{'sortable-position'})) { 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; } } $this->readSortableGroups($xml->field, $config, 'name'); } // Search for sortable-groups in association mappings if (isset($xml->{'many-to-one'})) { $this->readSortableGroups($xml->{'many-to-one'}, $config); } // Search for sortable-groups in association mappings if (isset($xml->{'many-to-many'})) { $this->readSortableGroups($xml->{'many-to-many'}, $config); } if (!$meta->isMappedSuperclass && $config) { if (!isset($config['position'])) { throw new InvalidMappingException("Missing property: 'position' in class - {$meta->getName()}"); } } } /** * Checks if $field type is valid as Sortable Position field * * @param ClassMetadata $meta * @param string $field * * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], self::VALID_TYPES, true); } private function readSortableGroups(\SimpleXMLElement $mapping, array &$config, string $fieldAttr = 'field'): void { foreach ($mapping as $mappingDoctrine) { $map = $mappingDoctrine->children(self::GEDMO_NAMESPACE_URI); $field = $this->_getAttribute($mappingDoctrine, $fieldAttr); if (isset($map->{'sortable-group'})) { if (!isset($config['groups'])) { $config['groups'] = []; } $config['groups'][] = $field; } } } } doctrine-extensions/src/Sortable/Mapping/Driver/Yaml.php 0000644 00000006702 15117737236 0017374 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sortable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for Sortable * behavioral extension. Used for extraction of extended * metadata from yaml specifically for Sortable * extension. * * @author Lukas Botsch <lukas.botsch@gmail.com> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * List of types which are valid for position fields * * @var string[] */ private const VALID_TYPES = [ 'int', 'integer', 'smallint', 'bigint', ]; /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { if (in_array('sortablePosition', $fieldMapping['gedmo'], true)) { 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; } } } $this->readSortableGroups($mapping['fields'], $config); } if (isset($mapping['manyToOne'])) { $this->readSortableGroups($mapping['manyToOne'], $config); } if (isset($mapping['manyToMany'])) { $this->readSortableGroups($mapping['manyToMany'], $config); } if (!$meta->isMappedSuperclass && $config) { if (!isset($config['position'])) { throw new InvalidMappingException("Missing property: 'position' in class - {$meta->getName()}"); } } } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } /** * Checks if $field type is valid as SortablePosition field * * @param ClassMetadata $meta * @param string $field * * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], self::VALID_TYPES, true); } private function readSortableGroups(iterable $mapping, array &$config): void { foreach ($mapping as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { if (in_array('sortableGroup', $fieldMapping['gedmo'], true)) { if (!isset($config['groups'])) { $config['groups'] = []; } $config['groups'][] = $field; } } } } } doctrine-extensions/src/Sortable/Mapping/Event/Adapter/ODM.php 0000644 00000005030 15117737236 0020310 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sortable\Mapping\Event\Adapter; use Doctrine\Common\Util\ClassUtils; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\Sortable\Mapping\Event\SortableAdapter; /** * Doctrine event adapter for ODM adapted * for sortable behavior * * @author Lukas Botsch <lukas.botsch@gmail.com> */ final class ODM extends BaseAdapterODM implements SortableAdapter { /** * @param ClassMetadata $meta * @param array $groups * * @return int */ public function getMaxPosition(array $config, $meta, $groups) { $dm = $this->getObjectManager(); $qb = $dm->createQueryBuilder($config['useObjectClass']); foreach ($groups as $group => $value) { if (is_object($value) && !$dm->getMetadataFactory()->isTransient(ClassUtils::getClass($value))) { $qb->field($group)->references($value); } else { $qb->field($group)->equals($value); } } $qb->sort($config['position'], 'desc'); $document = $qb->getQuery()->getSingleResult(); if ($document) { return $meta->getReflectionProperty($config['position'])->getValue($document); } return -1; } /** * @param array $relocation * @param array $delta * @param array $config * * @return void */ public function updatePositions($relocation, $delta, $config) { $dm = $this->getObjectManager(); $delta = array_map('intval', $delta); $qb = $dm->createQueryBuilder($config['useObjectClass']); $qb->updateMany(); $qb->field($config['position'])->inc($delta['delta']); $qb->field($config['position'])->gte($delta['start']); if ($delta['stop'] > 0) { $qb->field($config['position'])->lt($delta['stop']); } foreach ($relocation['groups'] as $group => $value) { if (is_object($value) && !$dm->getMetadataFactory()->isTransient(ClassUtils::getClass($value))) { $qb->field($group)->references($value); } else { $qb->field($group)->equals($value); } } $qb->getQuery()->execute(); } } doctrine-extensions/src/Sortable/Mapping/Event/Adapter/ORM.php 0000644 00000010551 15117737236 0020332 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sortable\Mapping\Event\Adapter; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\Sortable\Mapping\Event\SortableAdapter; use Gedmo\Sortable\SortableListener; /** * Doctrine event adapter for ORM adapted * for sortable behavior * * @author Lukas Botsch <lukas.botsch@gmail.com> * * @phpstan-import-type SortableRelocation from SortableListener */ final class ORM extends BaseAdapterORM implements SortableAdapter { /** * @param ClassMetadata $meta * @param array $groups * * @return int|null */ public function getMaxPosition(array $config, $meta, $groups) { $em = $this->getObjectManager(); $qb = $em->createQueryBuilder(); $qb->select('MAX(n.'.$config['position'].')') ->from($config['useObjectClass'], 'n'); $this->addGroupWhere($qb, $meta, $groups); $query = $qb->getQuery(); $query->useQueryCache(false); $query->disableResultCache(); $res = $query->getResult(); return $res[0][1]; } /** * @param array $relocation * @param array $delta * @param array $config * @phpstan-param SortableRelocation $relocation * * @return void */ public function updatePositions($relocation, $delta, $config) { $sign = $delta['delta'] < 0 ? '-' : '+'; $absDelta = abs($delta['delta']); $dql = "UPDATE {$relocation['name']} n"; $dql .= " SET n.{$config['position']} = n.{$config['position']} {$sign} {$absDelta}"; $dql .= " WHERE n.{$config['position']} >= {$delta['start']}"; // if not null, false or 0 if ($delta['stop'] > 0) { $dql .= " AND n.{$config['position']} < {$delta['stop']}"; } $i = -1; $params = []; foreach ($relocation['groups'] as $group => $value) { if (null === $value) { $dql .= " AND n.{$group} IS NULL"; } else { $dql .= " AND n.{$group} = :val___".(++$i); $params['val___'.$i] = $value; } } // add excludes if (!empty($delta['exclude'])) { $meta = $this->getObjectManager()->getClassMetadata($relocation['name']); if (1 === count($meta->getIdentifier())) { // if we only have one identifier, we can use IN syntax, for better performance $excludedIds = []; foreach ($delta['exclude'] as $entity) { if ($id = $meta->getFieldValue($entity, $meta->getIdentifier()[0])) { $excludedIds[] = $id; } } if (!empty($excludedIds)) { $params['excluded'] = $excludedIds; $dql .= " AND n.{$meta->getIdentifier()[0]} NOT IN (:excluded)"; } } elseif (count($meta->getIdentifier()) > 1) { foreach ($delta['exclude'] as $entity) { $j = 0; $dql .= ' AND NOT ('; foreach ($meta->getIdentifierValues($entity) as $id => $value) { $dql .= ($j > 0 ? ' AND ' : '')."n.{$id} = :val___".(++$i); $params['val___'.$i] = $value; ++$j; } $dql .= ')'; } } } $em = $this->getObjectManager(); $q = $em->createQuery($dql); $q->setParameters($params); $q->getSingleScalarResult(); } private function addGroupWhere(QueryBuilder $qb, ClassMetadata $metadata, iterable $groups): void { $i = 1; foreach ($groups as $group => $value) { if (null === $value) { $qb->andWhere($qb->expr()->isNull('n.'.$group)); } else { $qb->andWhere('n.'.$group.' = :group__'.$i); $qb->setParameter('group__'.$i, $value, $metadata->getTypeOfField($group)); } ++$i; } } } doctrine-extensions/src/Sortable/Mapping/Event/SortableAdapter.php 0000644 00000001050 15117737236 0021363 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sortable\Mapping\Event; use Gedmo\Mapping\Event\AdapterInterface; /** * Doctrine event adapter for the Sortable extension. * * @author Lukas Botsch <lukas.botsch@gmail.com> */ interface SortableAdapter extends AdapterInterface { } doctrine-extensions/src/Sortable/Sortable.php 0000644 00000002512 15117737236 0015412 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sortable; /** * This interface is not necessary but can be implemented for * Entities which in some cases needs to be identified as * Sortable * * @author Lukas Botsch <lukas.botsch@gmail.com> */ interface Sortable { // use now annotations instead of predefined methods, this interface is not necessary /* * @gedmo:SortablePosition - to mark property which will hold the item position use annotation @gedmo:SortablePosition * This property has to be numeric. The position index can be negative and will be counted from right to left. * * example: * * @gedmo:SortablePosition * @Column(type="int") * $position * * @gedmo:SortableGroup * @Column(type="string", length=64) * $category * */ /* * @gedmo:SortableGroup - to group node sorting by a property use annotation @gedmo:SortableGroup on this property * * example: * * @gedmo:SortableGroup * @Column(type="string", length=64) * $category */ } doctrine-extensions/src/Sortable/SortableListener.php 0000644 00000061215 15117737236 0017125 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Sortable; use Doctrine\Common\Comparable; use Doctrine\Common\EventArgs; use Doctrine\Common\Util\ClassUtils; use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\MappedEventSubscriber; use Gedmo\Sortable\Mapping\Event\SortableAdapter; use ProxyManager\Proxy\GhostObjectInterface; /** * The SortableListener maintains a sort index on your entities * to enable arbitrary sorting. * * This behavior can impact the performance of your application * since it does some additional calculations on persisted objects. * * @author Lukas Botsch <lukas.botsch@gmail.com> * * @phpstan-type SortableConfiguration = array{ * groups?: string[], * position?: string, * useObjectClass?: class-string, * } * * @phpstan-type SortableRelocation = array{ * name?: class-string, * groups?: mixed[], * deltas?: array<array{ * delta: int, * exclude: int[], * start: int, * stop: int, * }>, * } * * @phpstan-method SortableConfiguration getConfiguration(ObjectManager $objectManager, $class) * * @method SortableAdapter getEventAdapter(EventArgs $args) * * @final since gedmo/doctrine-extensions 3.11 */ class SortableListener extends MappedEventSubscriber { /** * @var array<string, array<string, mixed>> * @phpstan-var array<string, SortableRelocation> */ private $relocations = []; /** @var bool */ private $persistenceNeeded = false; /** @var array<string, int> */ private $maxPositions = []; /** * Specifies the list of events to listen * * @return string[] */ public function getSubscribedEvents() { return [ 'onFlush', 'loadClassMetadata', 'prePersist', 'postPersist', 'preUpdate', 'postRemove', 'postFlush', ]; } /** * Maps additional metadata * * @param LoadClassMetadataEventArgs $args * * @return void */ public function loadClassMetadata(EventArgs $args) { $ea = $this->getEventAdapter($args); $this->loadMetadataForObjectClass($ea->getObjectManager(), $args->getClassMetadata()); } /** * Collect position updates on objects being updated during flush * if they require changing. * * Persisting of positions is done later during prePersist, preUpdate and postRemove * events, otherwise the queries won't be executed within the transaction. * * The synchronization of the objects in memory is done in postFlush. This * ensures that the positions have been successfully persisted to database. * * @return void */ public function onFlush(EventArgs $args) { $this->persistenceNeeded = true; $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); // process all objects being deleted foreach ($ea->getScheduledObjectDeletions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); if ($config = $this->getConfiguration($om, $meta->getName())) { $this->processDeletion($ea, $config, $meta, $object); } } $updateValues = []; // process all objects being updated foreach ($ea->getScheduledObjectUpdates($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); if ($config = $this->getConfiguration($om, $meta->getName())) { $position = $meta->getReflectionProperty($config['position'])->getValue($object); $updateValues[$position] = [$ea, $config, $meta, $object]; } } krsort($updateValues); foreach ($updateValues as [$ea, $config, $meta, $object]) { $this->processUpdate($ea, $config, $meta, $object); } // process all objects being inserted foreach ($ea->getScheduledObjectInsertions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); if ($config = $this->getConfiguration($om, $meta->getName())) { $this->processInsert($ea, $config, $meta, $object); } } } /** * Update maxPositions as needed * * @return void */ public function prePersist(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); if ($config = $this->getConfiguration($om, $meta->getName())) { // Get groups $groups = $this->getGroups($meta, $config, $object); // Get hash $hash = $this->getHash($groups, $config); // Get max position if (!isset($this->maxPositions[$hash])) { $this->maxPositions[$hash] = $this->getMaxPosition($ea, $meta, $config, $object); } } } /** * @return void */ public function postPersist(EventArgs $args) { // persist position updates here, so that the update queries // are executed within transaction $this->persistRelocations($this->getEventAdapter($args)); } /** * @return void */ public function preUpdate(EventArgs $args) { // persist position updates here, so that the update queries // are executed within transaction $this->persistRelocations($this->getEventAdapter($args)); } /** * @return void */ public function postRemove(EventArgs $args) { // persist position updates here, so that the update queries // are executed within transaction $this->persistRelocations($this->getEventAdapter($args)); } /** * Sync objects in memory * * @return void */ public function postFlush(EventArgs $args) { $ea = $this->getEventAdapter($args); $em = $ea->getObjectManager(); $updatedObjects = []; foreach ($this->relocations as $hash => $relocation) { $config = $this->getConfiguration($em, $relocation['name']); foreach ($relocation['deltas'] as $delta) { if ($delta['start'] > $this->maxPositions[$hash] || 0 == $delta['delta']) { continue; } $meta = $em->getClassMetadata($relocation['name']); // now walk through the unit of work in memory objects and sync those $uow = $em->getUnitOfWork(); foreach ($uow->getIdentityMap() as $className => $objects) { // for inheritance mapped classes, only root is always in the identity map if ($className !== $ea->getRootObjectClass($meta) || !$this->getConfiguration($em, $className)) { continue; } foreach ($objects as $object) { if ($object instanceof GhostObjectInterface && !$object->isProxyInitialized()) { continue; } $changeSet = $ea->getObjectChangeSet($uow, $object); // if the entity's position is already changed, stop now if (array_key_exists($config['position'], $changeSet)) { continue; } // if the entity's group has changed, we stop now $groups = $this->getGroups($meta, $config, $object); foreach (array_keys($groups) as $group) { if (array_key_exists($group, $changeSet)) { continue 2; } } $oid = spl_object_id($object); $pos = $meta->getReflectionProperty($config['position'])->getValue($object); $matches = $pos >= $delta['start']; $matches = $matches && ($delta['stop'] <= 0 || $pos < $delta['stop']); $value = reset($relocation['groups']); while ($matches && ($group = key($relocation['groups']))) { $gr = $meta->getReflectionProperty($group)->getValue($object); if (null === $value) { $matches = null === $gr; } elseif (is_object($gr) && is_object($value) && $gr !== $value) { // Special case for equal objects but different instances. // If the object implements Comparable interface we can use its compareTo method // Otherwise we fallback to normal object comparison if ($gr instanceof Comparable) { $matches = $gr->compareTo($value); // @todo: Remove "is_int" check and only support integer as the interface expects. if (is_int($matches)) { $matches = 0 === $matches; } else { @trigger_error(sprintf( 'Support for "%s" as return type from "%s::compareTo()" is deprecated since' .' gedmo/doctrine-extensions 3.11 and will be removed in version 4.0. Return "integer" instead.', gettype($matches), Comparable::class ), E_USER_DEPRECATED); } } else { $matches = $gr == $value; } } else { $matches = $gr === $value; } $value = next($relocation['groups']); } if ($matches) { // We cannot use `$this->setFieldValue()` here, because it will create a change set, that will // prevent from other relocations being executed on this object. // We just update the object value and will create the change set later. if (!isset($updatedObjects[$oid])) { $updatedObjects[$oid] = [ 'object' => $object, 'field' => $config['position'], 'oldValue' => $pos, ]; } $updatedObjects[$oid]['newValue'] = $pos + $delta['delta']; $meta->getReflectionProperty($config['position'])->setValue($object, $updatedObjects[$oid]['newValue']); } } } } foreach ($updatedObjects as $updateData) { $this->setFieldValue($ea, $updateData['object'], $updateData['field'], $updateData['oldValue'], $updateData['newValue']); } // Clear relocations // unset only if relocations has been processed unset($this->relocations[$hash], $this->maxPositions[$hash]); } } /** * Computes node positions and updates the sort field in memory and in the db * * @param ClassMetadata $meta * @param object $object * * @return void */ protected function processInsert(SortableAdapter $ea, array $config, $meta, $object) { $em = $ea->getObjectManager(); $old = $meta->getReflectionProperty($config['position'])->getValue($object); $newPosition = $meta->getReflectionProperty($config['position'])->getValue($object); if (null === $newPosition) { $newPosition = -1; } // Get groups $groups = $this->getGroups($meta, $config, $object); // Get hash $hash = $this->getHash($groups, $config); // Get max position if (!isset($this->maxPositions[$hash])) { $this->maxPositions[$hash] = $this->getMaxPosition($ea, $meta, $config, $object); } // Compute position if it is negative if ($newPosition < 0) { $newPosition += $this->maxPositions[$hash] + 2; // position == -1 => append at end of list if ($newPosition < 0) { $newPosition = 0; } } // Set position to max position if it is too big $newPosition = min([$this->maxPositions[$hash] + 1, $newPosition]); // Compute relocations // New inserted entities should not be relocated by position update, so we exclude it. // Otherwise they could be relocated unintentionally. $relocation = [$hash, $config['useObjectClass'], $groups, $newPosition, -1, +1, [$object]]; // Apply existing relocations $applyDelta = 0; if (isset($this->relocations[$hash])) { foreach ($this->relocations[$hash]['deltas'] as $delta) { if ($delta['start'] <= $newPosition && ($delta['stop'] > $newPosition || $delta['stop'] < 0)) { $applyDelta += $delta['delta']; } } } $newPosition += $applyDelta; // Add relocations call_user_func_array([$this, 'addRelocation'], $relocation); // Set new position if ($old < 0 || null === $old) { $this->setFieldValue($ea, $object, $config['position'], $old, $newPosition); } } /** * Computes node positions and updates the sort field in memory and in the db * * @param ClassMetadata $meta * @param object $object * * @return void */ protected function processUpdate(SortableAdapter $ea, array $config, $meta, $object) { $em = $ea->getObjectManager(); $uow = $em->getUnitOfWork(); $changed = false; $groupHasChanged = false; $changeSet = $ea->getObjectChangeSet($uow, $object); // Get groups $groups = $this->getGroups($meta, $config, $object); // handle old groups $oldGroups = $groups; foreach (array_keys($groups) as $group) { if (array_key_exists($group, $changeSet)) { $changed = true; $oldGroups[$group] = $changeSet[$group][0]; } } $oldPosition = 0; $newPosition = 0; if ($changed) { $oldHash = $this->getHash($oldGroups, $config); $this->maxPositions[$oldHash] = $this->getMaxPosition($ea, $meta, $config, $object, $oldGroups); if (array_key_exists($config['position'], $changeSet)) { $oldPosition = $changeSet[$config['position']][0]; } else { $oldPosition = $meta->getReflectionProperty($config['position'])->getValue($object); } $this->addRelocation($oldHash, $config['useObjectClass'], $oldGroups, $oldPosition + 1, $this->maxPositions[$oldHash] + 1, -1); $groupHasChanged = true; } // Get hash $hash = $this->getHash($groups, $config); // Get max position if (!isset($this->maxPositions[$hash])) { $this->maxPositions[$hash] = $this->getMaxPosition($ea, $meta, $config, $object); } if (array_key_exists($config['position'], $changeSet)) { if ($changed && -1 === $this->maxPositions[$hash]) { // position has changed // the group of element has changed // and the target group has no children before $oldPosition = -1; $newPosition = -1; } else { // position was manually updated $oldPosition = $changeSet[$config['position']][0]; $newPosition = $changeSet[$config['position']][1]; $changed = $changed || $oldPosition != $newPosition; } } elseif ($changed) { $newPosition = $oldPosition; } if ($groupHasChanged) { $oldPosition = -1; } if (!$changed) { return; } // Compute position if it is negative if ($newPosition < 0) { if (-1 === $oldPosition) { $newPosition += $this->maxPositions[$hash] + 2; // position == -1 => append at end of list } else { $newPosition += $this->maxPositions[$hash] + 1; // position == -1 => append at end of list } if ($newPosition < 0) { $newPosition = 0; } } elseif ($newPosition > $this->maxPositions[$hash]) { if ($groupHasChanged) { $newPosition = $this->maxPositions[$hash] + 1; } else { $newPosition = $this->maxPositions[$hash]; } } else { $newPosition = min([$this->maxPositions[$hash], $newPosition]); } // Compute relocations /* CASE 1: shift backwards |----0----|----1----|----2----|----3----|----4----| |--node1--|--node2--|--node3--|--node4--|--node5--| Update node4: setPosition(1) --> Update position + 1 where position in [1,3) |--node1--|--node4--|--node2--|--node3--|--node5--| CASE 2: shift forward |----0----|----1----|----2----|----3----|----4----| |--node1--|--node2--|--node3--|--node4--|--node5--| Update node2: setPosition(3) --> Update position - 1 where position in (1,3] |--node1--|--node3--|--node4--|--node2--|--node5--| */ $relocation = null; if (-1 === $oldPosition) { // special case when group changes $relocation = [$hash, $config['useObjectClass'], $groups, $newPosition, -1, +1]; } elseif ($newPosition < $oldPosition) { $relocation = [$hash, $config['useObjectClass'], $groups, $newPosition, $oldPosition, +1]; } elseif ($newPosition > $oldPosition) { $relocation = [$hash, $config['useObjectClass'], $groups, $oldPosition + 1, $newPosition + 1, -1]; } if ($relocation) { // Add relocation call_user_func_array([$this, 'addRelocation'], $relocation); } // Set new position $this->setFieldValue($ea, $object, $config['position'], $oldPosition, $newPosition); } /** * Computes node positions and updates the sort field in memory and in the db * * @param ClassMetadata $meta * @param object $object * * @return void */ protected function processDeletion(SortableAdapter $ea, array $config, $meta, $object) { $position = $meta->getReflectionProperty($config['position'])->getValue($object); // Get groups $groups = $this->getGroups($meta, $config, $object); // Get hash $hash = $this->getHash($groups, $config); // Get max position if (!isset($this->maxPositions[$hash])) { $this->maxPositions[$hash] = $this->getMaxPosition($ea, $meta, $config, $object); } // Add relocation $this->addRelocation($hash, $config['useObjectClass'], $groups, $position, -1, -1); } /** * Persists relocations to database. * * @return void */ protected function persistRelocations(SortableAdapter $ea) { if (!$this->persistenceNeeded) { return; } $em = $ea->getObjectManager(); foreach ($this->relocations as $hash => $relocation) { $config = $this->getConfiguration($em, $relocation['name']); foreach ($relocation['deltas'] as $delta) { if ($delta['start'] > $this->maxPositions[$hash] || 0 == $delta['delta']) { continue; } $ea->updatePositions($relocation, $delta, $config); } } $this->persistenceNeeded = false; } /** * @param array $groups * * @return string */ protected function getHash($groups, array $config) { $data = $config['useObjectClass']; foreach ($groups as $group => $val) { if ($val instanceof \DateTime) { $val = $val->format('c'); } elseif (is_object($val)) { $val = spl_object_id($val); } $data .= $group.$val; } return md5($data); } /** * @param ClassMetadata $meta * @param array $config * @param object $object * * @return int */ protected function getMaxPosition(SortableAdapter $ea, $meta, $config, $object, array $groups = []) { $em = $ea->getObjectManager(); $uow = $em->getUnitOfWork(); $maxPos = null; // Get groups if ([] === $groups) { $groups = $this->getGroups($meta, $config, $object); } // Get hash $hash = $this->getHash($groups, $config); // Check for cached max position if (isset($this->maxPositions[$hash])) { return $this->maxPositions[$hash]; } // Check for groups that are associations. If the value is an object and is // scheduled for insert, it has no identifier yet and is obviously new // see issue #226 foreach ($groups as $val) { if (is_object($val) && ($uow->isScheduledForInsert($val) || !$em->getMetadataFactory()->isTransient(ClassUtils::getClass($val)) && $uow::STATE_MANAGED !== $ea->getObjectState($uow, $val))) { return -1; } } $maxPos = $ea->getMaxPosition($config, $meta, $groups); if (null === $maxPos) { $maxPos = -1; } return (int) $maxPos; } /** * Add a relocation rule * * @param string $hash The hash of the sorting group * @param string $class The object class * @param array $groups The sorting groups * @param int $start Inclusive index to start relocation from * @param int $stop Exclusive index to stop relocation at * @param int $delta The delta to add to relocated nodes * @param array $exclude Objects to be excluded from relocation * * @return void */ protected function addRelocation($hash, $class, $groups, $start, $stop, $delta, array $exclude = []) { if (!array_key_exists($hash, $this->relocations)) { $this->relocations[$hash] = ['name' => $class, 'groups' => $groups, 'deltas' => []]; } try { $newDelta = ['start' => $start, 'stop' => $stop, 'delta' => $delta, 'exclude' => $exclude]; array_walk($this->relocations[$hash]['deltas'], static function (&$val, $idx, $needle) { if ($val['start'] == $needle['start'] && $val['stop'] == $needle['stop']) { $val['delta'] += $needle['delta']; $val['exclude'] = array_merge($val['exclude'], $needle['exclude']); throw new \Exception('Found delta. No need to add it again.'); } // For every deletion relocation add newly created object to the list of excludes // otherwise position update queries will run for created objects as well. if (-1 == $val['delta'] && 1 == $needle['delta']) { $val['exclude'] = array_merge($val['exclude'], $needle['exclude']); } }, $newDelta); $this->relocations[$hash]['deltas'][] = $newDelta; } catch (\Exception $e) { } } /** * @param array $config * @param ClassMetadata $meta * @param object $object * * @return array */ protected function getGroups($meta, $config, $object) { $groups = []; if (isset($config['groups'])) { foreach ($config['groups'] as $group) { $groups[$group] = $meta->getReflectionProperty($group)->getValue($object); } } return $groups; } protected function getNamespace() { return __NAMESPACE__; } } doctrine-extensions/src/Timestampable/Mapping/Driver/Annotation.php 0000644 00000007075 15117737236 0021624 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Annotation\Timestampable; use Gedmo\Mapping\Driver\AbstractAnnotationDriver; /** * 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 <gediminas.morkevicius@gmail.com> * * @internal */ class Annotation extends AbstractAnnotationDriver { /** * 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; } } } } doctrine-extensions/src/Timestampable/Mapping/Driver/Attribute.php 0000644 00000001467 15117737236 0021454 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable\Mapping\Driver; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for Timestampable * behavioral extension. Used for extraction of extended * metadata from attributes specifically for Timestampable * extension. * * @author Kevin Mian Kraiker <kevin.mian@gmail.com> * @license MIT License (http://www.opensource.org/licenses/mit-license.php) * * @internal */ final class Attribute extends Annotation implements AttributeDriverInterface { } doctrine-extensions/src/Timestampable/Mapping/Driver/Xml.php 0000644 00000007460 15117737236 0020250 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for Timestampable * behavioral extension. Used for extraction of extended * metadata from xml specifically for Timestampable * extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Miha Vrhovnik <miha.vrhovnik@gmail.com> * * @internal */ class Xml extends BaseXml { /** * List of types which are valid for timestamp * * @var string[] */ private const VALID_TYPES = [ 'date', 'date_immutable', 'time', 'time_immutable', 'datetime', 'datetime_immutable', 'datetimetz', 'datetimetz_immutable', 'timestamp', 'vardatetime', 'integer', ]; public function readExtendedMetadata($meta, array &$config) { /** * @var \SimpleXmlElement */ $mapping = $this->_getMapping($meta->getName()); if (isset($mapping->field)) { /** * @var \SimpleXmlElement */ foreach ($mapping->field as $fieldMapping) { $fieldMappingDoctrine = $fieldMapping; $fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($fieldMapping->timestampable)) { /** * @var \SimpleXmlElement */ $data = $fieldMapping->timestampable; $field = $this->_getAttribute($fieldMappingDoctrine, 'name'); 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 (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, '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' === $this->_getAttribute($data, 'on')) { if (!$this->_isAttributeSet($data, 'field')) { throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $this->_getAttribute($data, 'field'); $valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value') : null; $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, ]; } $config[$this->_getAttribute($data, 'on')][] = $field; } } } } /** * Checks if $field type is valid * * @param ClassMetadata $meta * @param string $field * * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], self::VALID_TYPES, true); } } doctrine-extensions/src/Timestampable/Mapping/Driver/Yaml.php 0000644 00000007574 15117737236 0020420 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable\Mapping\Driver; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for Timestampable * behavioral extension. Used for extraction of extended * metadata from yaml specifically for Timestampable * extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * List of types which are valid for timestamp * * @var string[] */ private const VALID_TYPES = [ 'date', 'date_immutable', 'time', 'time_immutable', 'datetime', 'datetime_immutable', 'datetimetz', 'datetimetz_immutable', 'timestamp', 'vardatetime', 'integer', ]; /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo']['timestampable'])) { $mappingProperty = $fieldMapping['gedmo']['timestampable']; 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 (!isset($mappingProperty['on']) || !in_array($mappingProperty['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' === $mappingProperty['on']) { if (!isset($mappingProperty['field'])) { throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->getName()}"); } $trackedFieldAttribute = $mappingProperty['field']; $valueAttribute = $mappingProperty['value'] ?? null; if (is_array($trackedFieldAttribute) && null !== $valueAttribute) { throw new InvalidMappingException('Timestampable extension does not support multiple value changeset detection yet.'); } $field = [ 'field' => $field, 'trackedField' => $trackedFieldAttribute, 'value' => $valueAttribute, ]; } $config[$mappingProperty['on']][] = $field; } } } } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } /** * Checks if $field type is valid * * @param ClassMetadata $meta * @param string $field * * @return bool */ protected function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], self::VALID_TYPES, true); } } doctrine-extensions/src/Timestampable/Mapping/Event/Adapter/ODM.php 0000644 00000002567 15117737236 0021340 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable\Mapping\Event\Adapter; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\Timestampable\Mapping\Event\TimestampableAdapter; /** * Doctrine event adapter for ODM adapted * for Timestampable behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class ODM extends BaseAdapterODM implements TimestampableAdapter { /** * @param ClassMetadata $meta */ public function getDateValue($meta, $field) { $mapping = $meta->getFieldMapping($field); if (isset($mapping['type']) && 'timestamp' === $mapping['type']) { return time(); } if (isset($mapping['type']) && in_array($mapping['type'], ['date_immutable', 'time_immutable', 'datetime_immutable', 'datetimetz_immutable'], true)) { return new \DateTimeImmutable(); } return \DateTime::createFromFormat('U.u', number_format(microtime(true), 6, '.', '')) ->setTimeZone(new \DateTimeZone(date_default_timezone_get())); } } doctrine-extensions/src/Timestampable/Mapping/Event/Adapter/ORM.php 0000644 00000003653 15117737236 0021353 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable\Mapping\Event\Adapter; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\ClassMetadata; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\Timestampable\Mapping\Event\TimestampableAdapter; /** * Doctrine event adapter for ORM adapted * for Timestampable behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class ORM extends BaseAdapterORM implements TimestampableAdapter { /** * @param ClassMetadata $meta */ public function getDateValue($meta, $field) { $mapping = $meta->getFieldMapping($field); $converter = Type::getType($mapping['type'] ?? Types::DATETIME_MUTABLE); $platform = $this->getObjectManager()->getConnection()->getDriver()->getDatabasePlatform(); return $converter->convertToPHPValue($this->getRawDateValue($mapping), $platform); } /** * Generates current timestamp for the specified mapping * * @param array<string, mixed> $mapping * * @return \DateTimeInterface|int */ private function getRawDateValue(array $mapping) { if (isset($mapping['type']) && 'integer' === $mapping['type']) { return time(); } if (isset($mapping['type']) && in_array($mapping['type'], ['date_immutable', 'time_immutable', 'datetime_immutable', 'datetimetz_immutable'], true)) { return new \DateTimeImmutable(); } return \DateTime::createFromFormat('U.u', number_format(microtime(true), 6, '.', '')) ->setTimeZone(new \DateTimeZone(date_default_timezone_get())); } } doctrine-extensions/src/Timestampable/Mapping/Event/TimestampableAdapter.php 0000644 00000001516 15117737237 0023423 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable\Mapping\Event; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Mapping\Event\AdapterInterface; /** * Doctrine event adapter for the Timestampable extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface TimestampableAdapter extends AdapterInterface { /** * Get the date value. * * @param ClassMetadata $meta * @param string $field * * @return int|\DateTimeInterface */ public function getDateValue($meta, $field); } doctrine-extensions/src/Timestampable/Traits/Timestampable.php 0000644 00000002450 15117737237 0020712 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable\Traits; /** * Timestampable Trait, usable with PHP >= 5.4 * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ trait Timestampable { /** * @var \DateTime */ protected $createdAt; /** * @var \DateTime */ protected $updatedAt; /** * Sets createdAt. * * @return $this */ public function setCreatedAt(\DateTime $createdAt) { $this->createdAt = $createdAt; return $this; } /** * Returns createdAt. * * @return \DateTime */ public function getCreatedAt() { return $this->createdAt; } /** * Sets updatedAt. * * @return $this */ public function setUpdatedAt(\DateTime $updatedAt) { $this->updatedAt = $updatedAt; return $this; } /** * Returns updatedAt. * * @return \DateTime */ public function getUpdatedAt() { return $this->updatedAt; } } doctrine-extensions/src/Timestampable/Traits/TimestampableDocument.php 0000644 00000003332 15117737237 0022411 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable\Traits; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Annotation as Gedmo; /** * Timestampable Trait, usable with PHP >= 5.4 * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ trait TimestampableDocument { /** * @var \DateTime * @Gedmo\Timestampable(on="create") * @ODM\Field(type="date") */ #[Gedmo\Timestampable(on: 'create')] #[ODM\Field(type: Type::DATE)] protected $createdAt; /** * @var \DateTime * @Gedmo\Timestampable(on="update") * @ODM\Field(type="date") */ #[Gedmo\Timestampable(on: 'update')] #[ODM\Field(type: Type::DATE)] protected $updatedAt; /** * Sets createdAt. * * @return $this */ public function setCreatedAt(\DateTime $createdAt) { $this->createdAt = $createdAt; return $this; } /** * Returns createdAt. * * @return \DateTime */ public function getCreatedAt() { return $this->createdAt; } /** * Sets updatedAt. * * @return $this */ public function setUpdatedAt(\DateTime $updatedAt) { $this->updatedAt = $updatedAt; return $this; } /** * Returns updatedAt. * * @return \Datetime */ public function getUpdatedAt() { return $this->updatedAt; } } doctrine-extensions/src/Timestampable/Traits/TimestampableEntity.php 0000644 00000003344 15117737237 0022112 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable\Traits; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** * Timestampable Trait, usable with PHP >= 5.4 * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ trait TimestampableEntity { /** * @var \DateTime * @Gedmo\Timestampable(on="create") * @ORM\Column(type="datetime") */ #[Gedmo\Timestampable(on: 'create')] #[ORM\Column(type: Types::DATETIME_MUTABLE)] protected $createdAt; /** * @var \DateTime * @Gedmo\Timestampable(on="update") * @ORM\Column(type="datetime") */ #[Gedmo\Timestampable(on: 'update')] #[ORM\Column(type: Types::DATETIME_MUTABLE)] protected $updatedAt; /** * Sets createdAt. * * @return $this */ public function setCreatedAt(\DateTime $createdAt) { $this->createdAt = $createdAt; return $this; } /** * Returns createdAt. * * @return \DateTime */ public function getCreatedAt() { return $this->createdAt; } /** * Sets updatedAt. * * @return $this */ public function setUpdatedAt(\DateTime $updatedAt) { $this->updatedAt = $updatedAt; return $this; } /** * Returns updatedAt. * * @return \DateTime */ public function getUpdatedAt() { return $this->updatedAt; } } doctrine-extensions/src/Timestampable/Timestampable.php 0000644 00000002673 15117737237 0017453 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable; /** * This interface is not necessary but can be implemented for * Entities which in some cases needs to be identified as * Timestampable * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface Timestampable { // timestampable expects annotations on properties /* * @gedmo:Timestampable(on="create") * dates which should be updated on insert only */ /* * @gedmo:Timestampable(on="update") * dates which should be updated on update and insert */ /* * @gedmo:Timestampable(on="change", field="field", value="value") * dates which should be updated on changed "property" * value and become equal to given "value" */ /* * @gedmo:Timestampable(on="change", field="field") * dates which should be updated on changed "property" */ /* * @gedmo:Timestampable(on="change", fields={"field1", "field2"}) * dates which should be updated if at least one of the given fields changed */ /* * example * * @gedmo:Timestampable(on="create") * @Column(type="date") * $created */ } doctrine-extensions/src/Timestampable/TimestampableListener.php 0000644 00000002175 15117737237 0021156 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Timestampable; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\AbstractTrackingListener; use Gedmo\Timestampable\Mapping\Event\TimestampableAdapter; /** * The Timestampable listener handles the update of * dates on creation and update. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class TimestampableListener extends AbstractTrackingListener { /** * @param ClassMetadata $meta * @param string $field * @param TimestampableAdapter $eventAdapter * * @return mixed */ protected function getFieldValue($meta, $field, $eventAdapter) { return $eventAdapter->getDateValue($meta, $field); } protected function getNamespace() { return __NAMESPACE__; } } doctrine-extensions/src/Tool/Logging/DBAL/QueryAnalyzer.php 0000644 00000013617 15117737237 0017715 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tool\Logging\DBAL; use Doctrine\DBAL\Logging\SQLLogger; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; /** * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @deprecated since gedmo/doctrine-extensions 3.5. * * @final since gedmo/doctrine-extensions 3.11 */ class QueryAnalyzer implements SQLLogger { /** * Used database platform * * @var AbstractPlatform */ protected $platform; /** * Start time of currently executed query * * @var float */ private $queryStartTime; /** * Total execution time of all queries * * @var float */ private $totalExecutionTime = 0; /** * List of queries executed * * @var string[] */ private $queries = []; /** * Query execution times indexed * in same order as queries * * @var float[] */ private $queryExecutionTimes = []; /** * Initialize log listener with database * platform, which is needed for parameter * conversion */ public function __construct(AbstractPlatform $platform) { $this->platform = $platform; } /** * @return void */ public function startQuery($sql, array $params = null, array $types = null) { $this->queryStartTime = microtime(true); $this->queries[] = $this->generateSql($sql, $params, $types); } /** * @return void */ public function stopQuery() { $ms = round(microtime(true) - $this->queryStartTime, 4) * 1000; $this->queryExecutionTimes[] = $ms; $this->totalExecutionTime += $ms; } /** * Clean all collected data * * @return QueryAnalyzer */ public function cleanUp() { $this->queries = []; $this->queryExecutionTimes = []; $this->totalExecutionTime = 0; return $this; } /** * Dump the statistics of executed queries * * @param bool $dumpOnlySql * * @return string */ public function getOutput($dumpOnlySql = false) { $output = ''; if (!$dumpOnlySql) { $output .= 'Platform: '.$this->platform->getName().PHP_EOL; $output .= 'Executed queries: '.count($this->queries).', total time: '.$this->totalExecutionTime.' ms'.PHP_EOL; } foreach ($this->queries as $index => $sql) { if (!$dumpOnlySql) { $output .= 'Query('.($index + 1).') - '.$this->queryExecutionTimes[$index].' ms'.PHP_EOL; } $output .= $sql.';'.PHP_EOL; } $output .= PHP_EOL; return $output; } /** * Index of the slowest query executed * * @return int */ public function getSlowestQueryIndex() { $index = 0; $slowest = 0; foreach ($this->queryExecutionTimes as $i => $time) { if ($time > $slowest) { $slowest = $time; $index = $i; } } return $index; } /** * Get total execution time of queries * * @return float */ public function getTotalExecutionTime() { return $this->totalExecutionTime; } /** * Get all queries * * @return string[] */ public function getExecutedQueries() { return $this->queries; } /** * Get number of executed queries * * @return int */ public function getNumExecutedQueries() { return count($this->queries); } /** * Get all query execution times * * @return float[] */ public function getExecutionTimes() { return $this->queryExecutionTimes; } /** * Create the SQL with mapped parameters */ private function generateSql(string $sql, ?array $params, ?array $types): string { if (null === $params || [] === $params) { return $sql; } $converted = $this->getConvertedParams($params, $types); if (is_int(key($params))) { $index = key($converted); $sql = preg_replace_callback('@\?@sm', static function ($match) use (&$index, $converted) { return $converted[$index++]; }, $sql); } else { foreach ($converted as $key => $value) { $sql = str_replace(':'.$key, $value, $sql); } } return $sql; } /** * Get the converted parameter list */ private function getConvertedParams(array $params, array $types): array { $result = []; foreach ($params as $position => $value) { if (isset($types[$position])) { $type = $types[$position]; if (is_string($type)) { $type = Type::getType($type); } if ($type instanceof Type) { $value = $type->convertToDatabaseValue($value, $this->platform); } } else { if ($value instanceof \DateTimeInterface) { $value = $value->format($this->platform->getDateTimeFormatString()); } elseif (null !== $value) { $type = Type::getType(gettype($value)); $value = $type->convertToDatabaseValue($value, $this->platform); } } if (is_string($value)) { $value = "'{$value}'"; } elseif (null === $value) { $value = 'NULL'; } $result[$position] = $value; } return $result; } } doctrine-extensions/src/Tool/Wrapper/AbstractWrapper.php 0000644 00000005423 15117737237 0017532 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tool\Wrapper; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as OdmClassMetadata; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata as OrmClassMetadata; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Gedmo\Exception\UnsupportedObjectManagerException; use Gedmo\Tool\WrapperInterface; /** * Wraps entity or proxy for more convenient * manipulation * * @phpstan-template TClassMetadata of ClassMetadata * * @phpstan-implements WrapperInterface<TClassMetadata> * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ abstract class AbstractWrapper implements WrapperInterface { /** * Object metadata * * @var ClassMetadata&(OrmClassMetadata|OdmClassMetadata) * * @phpstan-var TClassMetadata */ protected $meta; /** * Wrapped object * * @var object */ protected $object; /** * Object manager instance * * @var ObjectManager */ protected $om; /** * Wrap object factory method * * @param object $object * * @throws \Gedmo\Exception\UnsupportedObjectManagerException * * @return WrapperInterface */ public static function wrap($object, ObjectManager $om) { if ($om instanceof EntityManagerInterface) { return new EntityWrapper($object, $om); } if ($om instanceof DocumentManager) { return new MongoDocumentWrapper($object, $om); } throw new UnsupportedObjectManagerException('Given object manager is not managed by wrapper'); } /** * @return void */ public static function clear() { @trigger_error(sprintf( 'Using "%s()" method is deprecated since gedmo/doctrine-extensions 3.5 and will be removed in version 4.0.', __METHOD__ ), E_USER_DEPRECATED); } public function getObject() { return $this->object; } public function getMetadata() { return $this->meta; } public function populate(array $data) { @trigger_error(sprintf( 'Using "%s()" method is deprecated since gedmo/doctrine-extensions 3.5 and will be removed in version 4.0.', __METHOD__ ), E_USER_DEPRECATED); foreach ($data as $field => $value) { $this->setPropertyValue($field, $value); } return $this; } } doctrine-extensions/src/Tool/Wrapper/EntityWrapper.php 0000644 00000006653 15117737237 0017251 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tool\Wrapper; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Proxy\Proxy; use Doctrine\Persistence\Proxy as PersistenceProxy; /** * Wraps entity or proxy for more convenient * manipulation * * @phpstan-extends AbstractWrapper<ClassMetadata> * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class EntityWrapper extends AbstractWrapper { /** * Entity identifier * * @var array|null */ private $identifier; /** * True if entity or proxy is loaded * * @var bool */ private $initialized = false; /** * Wrap entity * * @param object $entity */ public function __construct($entity, EntityManagerInterface $em) { $this->om = $em; $this->object = $entity; $this->meta = $em->getClassMetadata(get_class($this->object)); } public function getPropertyValue($property) { $this->initialize(); return $this->meta->getReflectionProperty($property)->getValue($this->object); } public function setPropertyValue($property, $value) { $this->initialize(); $this->meta->getReflectionProperty($property)->setValue($this->object, $value); return $this; } public function hasValidIdentifier() { return null !== $this->getIdentifier(); } public function getRootObjectName() { return $this->meta->rootEntityName; } public function getIdentifier($single = true) { if (null === $this->identifier) { if ($this->object instanceof Proxy) { $uow = $this->om->getUnitOfWork(); if ($uow->isInIdentityMap($this->object)) { $this->identifier = $uow->getEntityIdentifier($this->object); } else { $this->initialize(); } } if (null === $this->identifier) { $this->identifier = []; $incomplete = false; foreach ($this->meta->identifier as $name) { $this->identifier[$name] = $this->getPropertyValue($name); if (null === $this->identifier[$name]) { $incomplete = true; } } if ($incomplete) { $this->identifier = null; } } } if ($single && is_array($this->identifier)) { return reset($this->identifier); } return $this->identifier; } public function isEmbeddedAssociation($field) { return false; } /** * Initialize the entity if it is proxy * required when is detached or not initialized * * @return void */ protected function initialize() { if (!$this->initialized) { if ($this->object instanceof PersistenceProxy) { if (!$this->object->__isInitialized()) { $this->object->__load(); } } } } } doctrine-extensions/src/Tool/Wrapper/MongoDocumentWrapper.php 0000644 00000007335 15117737237 0020551 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tool\Wrapper; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use ProxyManager\Proxy\GhostObjectInterface; /** * Wraps document or proxy for more convenient * manipulation * * @phpstan-extends AbstractWrapper<ClassMetadata> * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class MongoDocumentWrapper extends AbstractWrapper { /** * Document identifier * * @var mixed */ private $identifier; /** * True if document or proxy is loaded * * @var bool */ private $initialized = false; /** * Wrap document * * @param object $document */ public function __construct($document, DocumentManager $dm) { $this->om = $dm; $this->object = $document; $this->meta = $dm->getClassMetadata(get_class($this->object)); } public function getPropertyValue($property) { $this->initialize(); return $this->meta->getReflectionProperty($property)->getValue($this->object); } public function getRootObjectName() { return $this->meta->rootDocumentName; } public function setPropertyValue($property, $value) { $this->initialize(); $this->meta->getReflectionProperty($property)->setValue($this->object, $value); return $this; } public function hasValidIdentifier() { return (bool) $this->getIdentifier(); } public function getIdentifier($single = true) { if (!$this->identifier) { if ($this->object instanceof GhostObjectInterface) { $uow = $this->om->getUnitOfWork(); if ($uow->isInIdentityMap($this->object)) { $this->identifier = (string) $uow->getDocumentIdentifier($this->object); } else { $this->initialize(); } } if (!$this->identifier) { $this->identifier = (string) $this->getPropertyValue($this->meta->identifier); } } return $this->identifier; } public function isEmbeddedAssociation($field) { return $this->getMetadata()->isSingleValuedEmbed($field); } /** * Initialize the document if it is proxy * required when is detached or not initialized * * @return void */ protected function initialize() { if (!$this->initialized) { if ($this->object instanceof GhostObjectInterface) { $uow = $this->om->getUnitOfWork(); if (!$this->object->isProxyInitialized()) { $persister = $uow->getDocumentPersister($this->meta->getName()); $identifier = null; if ($uow->isInIdentityMap($this->object)) { $identifier = $this->getIdentifier(); } else { // this may not happen but in case $getIdentifier = \Closure::bind(function () { return $this->identifier; }, $this->object, get_class($this->object)); $identifier = $getIdentifier(); } $this->object->initializeProxy(); $persister->load($identifier, $this->object); } } } } } doctrine-extensions/src/Tool/WrapperInterface.php 0000644 00000004305 15117737237 0016245 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tool; use Doctrine\Persistence\Mapping\ClassMetadata; /** * Interface for a wrapper of a managed object. * * @phpstan-template TClassMetadata of ClassMetadata * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface WrapperInterface { /** * Get the currently wrapped object. * * @return object */ public function getObject(); /** * Retrieves a property's value from the wrapped object. * * @param string $property * * @return mixed */ public function getPropertyValue($property); /** * Sets a property's value on the wrapped object. * * @param string $property * @param mixed $value * * @return $this */ public function setPropertyValue($property, $value); /** * @deprecated since gedmo/doctrine-extensions 3.5 and to be removed in version 4.0. * * Populates the wrapped object with the given property values. * * @return $this */ public function populate(array $data); /** * Checks if the identifier is valid. * * @return bool */ public function hasValidIdentifier(); /** * Get the object metadata. * * @return ClassMetadata * * @phpstan-return TClassMetadata */ public function getMetadata(); /** * Get the object identifier, single or composite. * * @param bool $single * * @return array|mixed Array if a composite value, otherwise a single scalar */ public function getIdentifier($single = true); /** * Get the root object class name. * * @return string * @phpstan-return class-string */ public function getRootObjectName(); /** * Checks if an association is embedded. * * @param string $field * * @return bool */ public function isEmbeddedAssociation($field); } doctrine-extensions/src/Translatable/Document/MappedSuperclass/AbstractPersonalTranslation.php 0000644 00000005462 15117737237 0027227 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Document\MappedSuperclass; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; use Doctrine\ODM\MongoDB\Types\Type; /** * Gedmo\Translatable\Document\AbstractPersonalTranslation * * @MongoODM\MappedSuperclass */ #[MongoODM\MappedSuperclass] abstract class AbstractPersonalTranslation { /** * @var string|null * * @MongoODM\Id */ #[MongoODM\Id] protected $id; /** * @var string|null * * @MongoODM\Field(type="string") */ #[MongoODM\Field(type: Type::STRING)] protected $locale; /** * Related document with ManyToOne relation * must be mapped by user * * @var object|null */ protected $object; /** * @var string|null * * @MongoODM\Field(type="string") */ #[MongoODM\Field(type: Type::STRING)] protected $field; /** * @var string|null * * @MongoODM\Field(type="string") */ #[MongoODM\Field(type: Type::STRING)] protected $content; /** * Get id * * @return string|null $id */ public function getId() { return $this->id; } /** * Set locale * * @param string $locale * * @return static */ public function setLocale($locale) { $this->locale = $locale; return $this; } /** * Get locale * * @return string */ public function getLocale() { return $this->locale; } /** * Set field * * @param string $field * * @return static */ public function setField($field) { $this->field = $field; return $this; } /** * Get field * * @return string */ public function getField() { return $this->field; } /** * Set object related * * @param object $object * * @return static */ public function setObject($object) { $this->object = $object; return $this; } /** * Get object related * * @return object */ public function getObject() { return $this->object; } /** * Set content * * @param string $content * * @return static */ public function setContent($content) { $this->content = $content; return $this; } /** * Get content * * @return string */ public function getContent() { return $this->content; } } doctrine-extensions/src/Translatable/Document/MappedSuperclass/AbstractTranslation.php 0000644 00000006560 15117737237 0025523 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Document\MappedSuperclass; use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM; use Doctrine\ODM\MongoDB\Types\Type; /** * Gedmo\Translatable\Document\MappedSuperclass\AbstractTranslation * * @MongoODM\MappedSuperclass */ #[MongoODM\MappedSuperclass] abstract class AbstractTranslation { /** * @var int * * @MongoODM\Id */ #[MongoODM\Id] protected $id; /** * @var string * * @MongoODM\Field(type="string") */ #[MongoODM\Field(type: Type::STRING)] protected $locale; /** * @var string * * @MongoODM\Field(type="string") */ #[MongoODM\Field(type: Type::STRING)] protected $objectClass; /** * @var string * * @MongoODM\Field(type="string") */ #[MongoODM\Field(type: Type::STRING)] protected $field; /** * @var string * * @MongoODM\Field(type="string", name="foreign_key") */ #[MongoODM\Field(name: 'foreign_key', type: Type::STRING)] protected $foreignKey; /** * @var string * * @MongoODM\Field(type="string") */ #[MongoODM\Field(type: Type::STRING)] protected $content; /** * Get id * * @return int $id */ public function getId() { return $this->id; } /** * Set locale * * @param string $locale * * @return static */ public function setLocale($locale) { $this->locale = $locale; return $this; } /** * Get locale * * @return string */ public function getLocale() { return $this->locale; } /** * Set field * * @param string $field * * @return static */ public function setField($field) { $this->field = $field; return $this; } /** * Get field * * @return string */ public function getField() { return $this->field; } /** * Set object class * * @param string $objectClass * * @return static */ public function setObjectClass($objectClass) { $this->objectClass = $objectClass; return $this; } /** * Get objectClass * * @return string */ public function getObjectClass() { return $this->objectClass; } /** * Set foreignKey * * @param string $foreignKey * * @return static */ public function setForeignKey($foreignKey) { $this->foreignKey = $foreignKey; return $this; } /** * Get foreignKey * * @return string */ public function getForeignKey() { return $this->foreignKey; } /** * Set content * * @param string $content * * @return static */ public function setContent($content) { $this->content = $content; return $this; } /** * Get content * * @return string */ public function getContent() { return $this->content; } } doctrine-extensions/src/Translatable/Document/Repository/TranslationRepository.php 0000644 00000021242 15117737237 0025035 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Document\Repository; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Iterator\Iterator; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; use Doctrine\ODM\MongoDB\Types\Type; use Doctrine\ODM\MongoDB\UnitOfWork; use Gedmo\Tool\Wrapper\MongoDocumentWrapper; use Gedmo\Translatable\Document\MappedSuperclass\AbstractPersonalTranslation; use Gedmo\Translatable\Mapping\Event\Adapter\ODM as TranslatableAdapterODM; use Gedmo\Translatable\TranslatableListener; /** * The TranslationRepository has some useful functions * to interact with translations. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class TranslationRepository extends DocumentRepository { /** * Current TranslatableListener instance used * in EntityManager * * @var TranslatableListener|null */ private $listener; public function __construct(DocumentManager $dm, UnitOfWork $uow, ClassMetadata $class) { if ($class->getReflectionClass()->isSubclassOf(AbstractPersonalTranslation::class)) { throw new \Gedmo\Exception\UnexpectedValueException('This repository is useless for personal translations'); } parent::__construct($dm, $uow, $class); } /** * Makes additional translation of $document $field into $locale * using $value * * @param object $document * @param string $field * @param string $locale * @param mixed $value * * @return static */ public function translate($document, $field, $locale, $value) { $meta = $this->dm->getClassMetadata(get_class($document)); $listener = $this->getTranslatableListener(); $config = $listener->getConfiguration($this->dm, $meta->getName()); if (!isset($config['fields']) || !in_array($field, $config['fields'], true)) { throw new \Gedmo\Exception\InvalidArgumentException("Document: {$meta->getName()} does not translate field - {$field}"); } $modRecordValue = (!$listener->getPersistDefaultLocaleTranslation() && $locale === $listener->getDefaultLocale()) || $listener->getTranslatableLocale($document, $meta, $this->getDocumentManager()) === $locale ; if ($modRecordValue) { $meta->getReflectionProperty($field)->setValue($document, $value); $this->dm->persist($document); } else { if (isset($config['translationClass'])) { $class = $config['translationClass']; } else { $ea = new TranslatableAdapterODM(); $class = $listener->getTranslationClass($ea, $config['useObjectClass']); } $foreignKey = $meta->getReflectionProperty($meta->getIdentifier()[0])->getValue($document); $objectClass = $config['useObjectClass']; $transMeta = $this->dm->getClassMetadata($class); $trans = $this->findOneBy(compact('locale', 'field', 'objectClass', 'foreignKey')); if (!$trans) { $trans = $transMeta->newInstance(); $transMeta->getReflectionProperty('foreignKey')->setValue($trans, $foreignKey); $transMeta->getReflectionProperty('objectClass')->setValue($trans, $objectClass); $transMeta->getReflectionProperty('field')->setValue($trans, $field); $transMeta->getReflectionProperty('locale')->setValue($trans, $locale); } $mapping = $meta->getFieldMapping($field); $type = $this->getType($mapping['type']); $transformed = $type->convertToDatabaseValue($value); $transMeta->getReflectionProperty('content')->setValue($trans, $transformed); if ($this->dm->getUnitOfWork()->isInIdentityMap($document)) { $this->dm->persist($trans); } else { $oid = spl_object_id($document); $listener->addPendingTranslationInsert($oid, $trans); } } return $this; } /** * Loads all translations with all translatable * fields from the given entity * * @param object $document * * @return array list of translations in locale groups */ public function findTranslations($document) { $result = []; $wrapped = new MongoDocumentWrapper($document, $this->dm); if ($wrapped->hasValidIdentifier()) { $documentId = $wrapped->getIdentifier(); $translationMeta = $this->getClassMetadata(); // table inheritance support $config = $this ->getTranslatableListener() ->getConfiguration($this->dm, $wrapped->getMetadata()->getName()); if (!$config) { return $result; } $documentClass = $config['useObjectClass']; $translationClass = $config['translationClass'] ?? $translationMeta->rootDocumentName; $qb = $this->dm->createQueryBuilder($translationClass); $q = $qb->field('foreignKey')->equals($documentId) ->field('objectClass')->equals($documentClass) ->field('content')->exists(true)->notEqual(null) ->sort('locale', 'asc') ->getQuery(); $q->setHydrate(false); $data = $q->execute(); if (is_iterable($data)) { foreach ($data as $row) { $result[$row['locale']][$row['field']] = $row['content']; } } } return $result; } /** * Find the object $class by the translated field. * Result is the first occurrence of translated field. * Query can be slow, since there are no indexes on such * columns * * @param string $field * @param string $value * @param string $class * @phpstan-param class-string $class * * @return object|null instance of $class or null if not found */ public function findObjectByTranslatedField($field, $value, $class) { $meta = $this->dm->getClassMetadata($class); if (!$meta->hasField($field)) { return null; } $qb = $this->createQueryBuilder(); $q = $qb->field('field')->equals($field) ->field('objectClass')->equals($meta->rootDocumentName) ->field('content')->equals($value) ->getQuery(); $q->setHydrate(false); $result = $q->execute(); if ($result instanceof Iterator) { $result = $result->toArray(); } $id = $result[0]['foreign_key'] ?? null; if (null === $id) { return null; } return $this->dm->find($class, $id); } /** * Loads all translations with all translatable * fields by a given document primary key * * @param mixed $id primary key value of document * * @return array */ public function findTranslationsByObjectId($id) { $result = []; if ($id) { $qb = $this->createQueryBuilder(); $q = $qb->field('foreignKey')->equals($id) ->field('content')->exists(true)->notEqual(null) ->sort('locale', 'asc') ->getQuery(); $q->setHydrate(false); $data = $q->execute(); if (is_iterable($data)) { foreach ($data as $row) { $result[$row['locale']][$row['field']] = $row['content']; } } } return $result; } /** * Get the currently used TranslatableListener * * @throws \Gedmo\Exception\RuntimeException if listener is not found */ private function getTranslatableListener(): TranslatableListener { if (null === $this->listener) { foreach ($this->dm->getEventManager()->getAllListeners() as $event => $listeners) { foreach ($listeners as $hash => $listener) { if ($listener instanceof TranslatableListener) { return $this->listener = $listener; } } } throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found'); } return $this->listener; } private function getType(string $type): Type { return Type::getType($type); } } doctrine-extensions/src/Translatable/Document/Translation.php 0000644 00000002556 15117737237 0020565 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Gedmo\Translatable\Document\Repository\TranslationRepository; /** * Gedmo\Translatable\Document\Translation * * @ODM\Document(repositoryClass="Gedmo\Translatable\Document\Repository\TranslationRepository") * @ODM\UniqueIndex(name="lookup_unique_idx", keys={ * "locale": "asc", * "object_class": "asc", * "foreign_key": "asc", * "field": "asc" * }) * @ODM\Index(name="translations_lookup_idx", keys={ * "locale": "asc", * "object_class": "asc", * "foreign_key": "asc" * }) */ #[ODM\Document(repositoryClass: TranslationRepository::class)] #[ODM\UniqueIndex(name: 'lookup_unique_idx', keys: ['locale' => 'asc', 'object_class' => 'asc', 'foreign_key' => 'asc', 'field' => 'asc'])] #[ODM\Index(name: 'translations_lookup_idx', keys: ['locale' => 'asc', 'object_class' => 'asc', 'foreign_key' => 'asc'])] class Translation extends MappedSuperclass\AbstractTranslation { /* * All required columns are mapped through inherited superclass */ } doctrine-extensions/src/Translatable/Entity/MappedSuperclass/AbstractPersonalTranslation.php 0000644 00000005724 15117737237 0026726 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Entity\MappedSuperclass; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation * * @ORM\MappedSuperclass */ #[ORM\MappedSuperclass] abstract class AbstractPersonalTranslation { /** * @var int|null * * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ #[ORM\Column(type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] protected $id; /** * @var string * * @ORM\Column(type="string", length=8) */ #[ORM\Column(type: Types::STRING, length: 8)] protected $locale; /** * @var string * * @ORM\Column(type="string", length=32) */ #[ORM\Column(type: Types::STRING, length: 32)] protected $field; /** * Related entity with ManyToOne relation * must be mapped by user * * @var object */ protected $object; /** * @var string * * @ORM\Column(type="text", nullable=true) */ #[ORM\Column(type: Types::TEXT, nullable: true)] protected $content; /** * Get id * * @return int|null $id */ public function getId() { return $this->id; } /** * Set locale * * @param string $locale * * @return static */ public function setLocale($locale) { $this->locale = $locale; return $this; } /** * Get locale * * @return string */ public function getLocale() { return $this->locale; } /** * Set field * * @param string $field * * @return static */ public function setField($field) { $this->field = $field; return $this; } /** * Get field * * @return string $field */ public function getField() { return $this->field; } /** * Set object related * * @param object $object * * @return static */ public function setObject($object) { $this->object = $object; return $this; } /** * Get related object * * @return object */ public function getObject() { return $this->object; } /** * Set content * * @param string $content * * @return static */ public function setContent($content) { $this->content = $content; return $this; } /** * Get content * * @return string */ public function getContent() { return $this->content; } } doctrine-extensions/src/Translatable/Entity/MappedSuperclass/AbstractTranslation.php 0000644 00000007143 15117737237 0025217 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Entity\MappedSuperclass; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation * * @ORM\MappedSuperclass */ #[ORM\MappedSuperclass] abstract class AbstractTranslation { /** * @var int * * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ #[ORM\Column(type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] protected $id; /** * @var string * * @ORM\Column(type="string", length=8) */ #[ORM\Column(type: Types::STRING, length: 8)] protected $locale; /** * @var string * * @ORM\Column(name="object_class", type="string", length=191) */ #[ORM\Column(name: 'object_class', type: Types::STRING, length: 191)] protected $objectClass; /** * @var string * * @ORM\Column(type="string", length=32) */ #[ORM\Column(type: Types::STRING, length: 32)] protected $field; /** * @var string * * @ORM\Column(name="foreign_key", type="string", length=64) */ #[ORM\Column(name: 'foreign_key', type: Types::STRING, length: 64)] protected $foreignKey; /** * @var string * * @ORM\Column(type="text", nullable=true) */ #[ORM\Column(type: Types::TEXT, nullable: true)] protected $content; /** * Get id * * @return int $id */ public function getId() { return $this->id; } /** * Set locale * * @param string $locale * * @return static */ public function setLocale($locale) { $this->locale = $locale; return $this; } /** * Get locale * * @return string */ public function getLocale() { return $this->locale; } /** * Set field * * @param string $field * * @return static */ public function setField($field) { $this->field = $field; return $this; } /** * Get field * * @return string */ public function getField() { return $this->field; } /** * Set object class * * @param string $objectClass * * @return static */ public function setObjectClass($objectClass) { $this->objectClass = $objectClass; return $this; } /** * Get objectClass * * @return string */ public function getObjectClass() { return $this->objectClass; } /** * Set foreignKey * * @param string $foreignKey * * @return static */ public function setForeignKey($foreignKey) { $this->foreignKey = $foreignKey; return $this; } /** * Get foreignKey * * @return string */ public function getForeignKey() { return $this->foreignKey; } /** * Set content * * @param string $content * * @return static */ public function setContent($content) { $this->content = $content; return $this; } /** * Get content * * @return string */ public function getContent() { return $this->content; } } doctrine-extensions/src/Translatable/Entity/Repository/TranslationRepository.php 0000644 00000022211 15117737237 0024530 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Entity\Repository; use Doctrine\DBAL\Types\Type; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; use Gedmo\Tool\Wrapper\EntityWrapper; use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation; use Gedmo\Translatable\Mapping\Event\Adapter\ORM as TranslatableAdapterORM; use Gedmo\Translatable\TranslatableListener; /** * The TranslationRepository has some useful functions * to interact with translations. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class TranslationRepository extends EntityRepository { /** * Current TranslatableListener instance used * in EntityManager * * @var TranslatableListener|null */ private $listener; public function __construct(EntityManagerInterface $em, ClassMetadata $class) { if ($class->getReflectionClass()->isSubclassOf(AbstractPersonalTranslation::class)) { throw new \Gedmo\Exception\UnexpectedValueException('This repository is useless for personal translations'); } parent::__construct($em, $class); } /** * Makes additional translation of $entity $field into $locale * using $value * * @param object $entity * @param string $field * @param string $locale * @param mixed $value * * @throws \Gedmo\Exception\InvalidArgumentException * * @return static */ public function translate($entity, $field, $locale, $value) { $meta = $this->_em->getClassMetadata(get_class($entity)); $listener = $this->getTranslatableListener(); $config = $listener->getConfiguration($this->_em, $meta->getName()); if (!isset($config['fields']) || !in_array($field, $config['fields'], true)) { throw new \Gedmo\Exception\InvalidArgumentException("Entity: {$meta->getName()} does not translate field - {$field}"); } $needsPersist = true; if ($locale === $listener->getTranslatableLocale($entity, $meta, $this->getEntityManager())) { $meta->getReflectionProperty($field)->setValue($entity, $value); $this->_em->persist($entity); } else { if (isset($config['translationClass'])) { $class = $config['translationClass']; } else { $ea = new TranslatableAdapterORM(); $class = $listener->getTranslationClass($ea, $config['useObjectClass']); } $foreignKey = $meta->getReflectionProperty($meta->getSingleIdentifierFieldName())->getValue($entity); $objectClass = $config['useObjectClass']; $transMeta = $this->_em->getClassMetadata($class); $trans = $this->findOneBy(compact('locale', 'objectClass', 'field', 'foreignKey')); if (!$trans) { $trans = $transMeta->newInstance(); $transMeta->getReflectionProperty('foreignKey')->setValue($trans, $foreignKey); $transMeta->getReflectionProperty('objectClass')->setValue($trans, $objectClass); $transMeta->getReflectionProperty('field')->setValue($trans, $field); $transMeta->getReflectionProperty('locale')->setValue($trans, $locale); } if ($listener->getDefaultLocale() != $listener->getTranslatableLocale($entity, $meta, $this->getEntityManager()) && $locale === $listener->getDefaultLocale()) { $listener->setTranslationInDefaultLocale(spl_object_id($entity), $field, $trans); $needsPersist = $listener->getPersistDefaultLocaleTranslation(); } $type = Type::getType($meta->getTypeOfField($field)); $transformed = $type->convertToDatabaseValue($value, $this->_em->getConnection()->getDatabasePlatform()); $transMeta->getReflectionProperty('content')->setValue($trans, $transformed); if ($needsPersist) { if ($this->_em->getUnitOfWork()->isInIdentityMap($entity)) { $this->_em->persist($trans); } else { $oid = spl_object_id($entity); $listener->addPendingTranslationInsert($oid, $trans); } } } return $this; } /** * Loads all translations with all translatable * fields from the given entity * * @param object $entity Must implement Translatable * * @return array list of translations in locale groups */ public function findTranslations($entity) { $result = []; $wrapped = new EntityWrapper($entity, $this->_em); if ($wrapped->hasValidIdentifier()) { $entityId = $wrapped->getIdentifier(); $config = $this ->getTranslatableListener() ->getConfiguration($this->_em, $wrapped->getMetadata()->getName()); if (!$config) { return $result; } $entityClass = $config['useObjectClass']; $translationMeta = $this->getClassMetadata(); // table inheritance support $translationClass = $config['translationClass'] ?? $translationMeta->rootEntityName; $qb = $this->_em->createQueryBuilder(); $qb->select('trans.content, trans.field, trans.locale') ->from($translationClass, 'trans') ->where('trans.foreignKey = :entityId', 'trans.objectClass = :entityClass') ->orderBy('trans.locale'); $q = $qb->getQuery(); $data = $q->execute( compact('entityId', 'entityClass'), Query::HYDRATE_ARRAY ); foreach ($data as $row) { $result[$row['locale']][$row['field']] = $row['content']; } } return $result; } /** * Find the entity $class by the translated field. * Result is the first occurrence of translated field. * Query can be slow, since there are no indexes on such * columns * * @param string $field * @param string $value * @param string $class * @phpstan-param class-string $class * * @return object instance of $class or null if not found */ public function findObjectByTranslatedField($field, $value, $class) { $entity = null; $meta = $this->_em->getClassMetadata($class); $translationMeta = $this->getClassMetadata(); // table inheritance support if ($meta->hasField($field)) { $dql = "SELECT trans.foreignKey FROM {$translationMeta->rootEntityName} trans"; $dql .= ' WHERE trans.objectClass = :class'; $dql .= ' AND trans.field = :field'; $dql .= ' AND trans.content = :value'; $q = $this->_em->createQuery($dql); $q->setParameters(compact('class', 'field', 'value')); $q->setMaxResults(1); $result = $q->getArrayResult(); $id = $result[0]['foreignKey'] ?? null; if (null !== $id) { $entity = $this->_em->find($class, $id); } } return $entity; } /** * Loads all translations with all translatable * fields by a given entity primary key * * @param mixed $id primary key value of an entity * * @return array */ public function findTranslationsByObjectId($id) { $result = []; if ($id) { $translationMeta = $this->getClassMetadata(); // table inheritance support $qb = $this->_em->createQueryBuilder(); $qb->select('trans.content, trans.field, trans.locale') ->from($translationMeta->rootEntityName, 'trans') ->where('trans.foreignKey = :entityId') ->orderBy('trans.locale'); $q = $qb->getQuery(); $data = $q->execute( ['entityId' => $id], Query::HYDRATE_ARRAY ); foreach ($data as $row) { $result[$row['locale']][$row['field']] = $row['content']; } } return $result; } /** * Get the currently used TranslatableListener * * @throws \Gedmo\Exception\RuntimeException if listener is not found */ private function getTranslatableListener(): TranslatableListener { if (null === $this->listener) { foreach ($this->_em->getEventManager()->getAllListeners() as $event => $listeners) { foreach ($listeners as $hash => $listener) { if ($listener instanceof TranslatableListener) { return $this->listener = $listener; } } } throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found'); } return $this->listener; } } doctrine-extensions/src/Translatable/Entity/Translation.php 0000644 00000003366 15117737237 0020263 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Entity; use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\Index; use Doctrine\ORM\Mapping\Table; use Doctrine\ORM\Mapping\UniqueConstraint; use Gedmo\Translatable\Entity\Repository\TranslationRepository; /** * Gedmo\Translatable\Entity\Translation * * @Table( * name="ext_translations", * options={"row_format": "DYNAMIC"}, * indexes={ * @Index(name="translations_lookup_idx", columns={ * "locale", "object_class", "foreign_key" * }), * @Index(name="general_translations_lookup_idx", columns={ * "object_class", "foreign_key" * }) * }, * uniqueConstraints={@UniqueConstraint(name="lookup_unique_idx", columns={ * "locale", "object_class", "field", "foreign_key" * })} * ) * @Entity(repositoryClass="Gedmo\Translatable\Entity\Repository\TranslationRepository") */ #[Entity(repositoryClass: TranslationRepository::class)] #[Table(name: 'ext_translations', options: ['row_format' => 'DYNAMIC'])] #[Index(name: 'translations_lookup_idx', columns: ['locale', 'object_class', 'foreign_key'])] #[Index(name: 'general_translations_lookup_idx', columns: ['object_class', 'foreign_key'])] #[UniqueConstraint(name: 'lookup_unique_idx', columns: ['locale', 'object_class', 'field', 'foreign_key'])] class Translation extends MappedSuperclass\AbstractTranslation { /* * All required columns are mapped through inherited superclass */ } doctrine-extensions/src/Translatable/Hydrator/ORM/ObjectHydrator.php 0000644 00000004515 15117737237 0021662 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Hydrator\ORM; use Doctrine\ORM\Internal\Hydration\ObjectHydrator as BaseObjectHydrator; use Gedmo\Translatable\TranslatableListener; /** * If query uses TranslationQueryWalker and is hydrating * objects - when it requires this custom object hydrator * in order to skip onLoad event from triggering retranslation * of the fields * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class ObjectHydrator extends BaseObjectHydrator { /** * State of skipOnLoad for listener between hydrations * * @see ObjectHydrator::prepare() * @see ObjectHydrator::cleanup() * * @var bool|null */ private $savedSkipOnLoad; /** * @return void */ protected function prepare() { $listener = $this->getTranslatableListener(); $this->savedSkipOnLoad = $listener->isSkipOnLoad(); $listener->setSkipOnLoad(true); parent::prepare(); } /** * @return void */ protected function cleanup() { parent::cleanup(); $listener = $this->getTranslatableListener(); $listener->setSkipOnLoad($this->savedSkipOnLoad ?? false); } /** * Get the currently used TranslatableListener * * @throws \Gedmo\Exception\RuntimeException if listener is not found * * @return TranslatableListener */ protected function getTranslatableListener() { $translatableListener = null; foreach ($this->_em->getEventManager()->getAllListeners() as $event => $listeners) { foreach ($listeners as $hash => $listener) { if ($listener instanceof TranslatableListener) { $translatableListener = $listener; break 2; } } } if (null === $translatableListener) { throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found'); } return $translatableListener; } } doctrine-extensions/src/Translatable/Hydrator/ORM/SimpleObjectHydrator.php 0000644 00000004561 15117737237 0023035 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Hydrator\ORM; use Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator as BaseSimpleObjectHydrator; use Gedmo\Translatable\TranslatableListener; /** * If query uses TranslationQueryWalker and is hydrating * objects - when it requires this custom object hydrator * in order to skip onLoad event from triggering retranslation * of the fields * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class SimpleObjectHydrator extends BaseSimpleObjectHydrator { /** * State of skipOnLoad for listener between hydrations * * @see SimpleObjectHydrator::prepare() * @see SimpleObjectHydrator::cleanup() * * @var bool|null */ private $savedSkipOnLoad; /** * @return void */ protected function prepare() { $listener = $this->getTranslatableListener(); $this->savedSkipOnLoad = $listener->isSkipOnLoad(); $listener->setSkipOnLoad(true); parent::prepare(); } /** * @return void */ protected function cleanup() { parent::cleanup(); $listener = $this->getTranslatableListener(); $listener->setSkipOnLoad($this->savedSkipOnLoad ?? false); } /** * Get the currently used TranslatableListener * * @throws \Gedmo\Exception\RuntimeException if listener is not found * * @return TranslatableListener */ protected function getTranslatableListener() { $translatableListener = null; foreach ($this->_em->getEventManager()->getAllListeners() as $event => $listeners) { foreach ($listeners as $hash => $listener) { if ($listener instanceof TranslatableListener) { $translatableListener = $listener; break 2; } } } if (null === $translatableListener) { throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found'); } return $translatableListener; } } doctrine-extensions/src/Translatable/Mapping/Driver/Annotation.php 0000644 00000012202 15117737237 0021436 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ 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; /** * 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 <gediminas.morkevicius@gmail.com> * * @internal */ class Annotation extends AbstractAnnotationDriver { /** * 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()}"); } } } } doctrine-extensions/src/Translatable/Mapping/Driver/Attribute.php 0000644 00000001440 15117737237 0021271 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Mapping\Driver; use Gedmo\Mapping\Annotation\Translatable; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for Translatable * behavioral extension. Used for extraction of extended * metadata from attributes specifically for Translatable * extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @internal */ final class Attribute extends Annotation implements AttributeDriverInterface { } doctrine-extensions/src/Translatable/Mapping/Driver/Xml.php 0000644 00000010533 15117737237 0020071 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver\Xml as BaseXml; /** * This is a xml mapping driver for Translatable * behavioral extension. Used for extraction of extended * metadata from xml specifically for Translatable * extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Miha Vrhovnik <miha.vrhovnik@gmail.com> * * @internal */ class Xml extends BaseXml { public function readExtendedMetadata($meta, array &$config) { /** * @var \SimpleXmlElement */ $xml = $this->_getMapping($meta->getName()); $xmlDoctrine = $xml; $xml = $xml->children(self::GEDMO_NAMESPACE_URI); if ('entity' === $xmlDoctrine->getName() || 'mapped-superclass' === $xmlDoctrine->getName()) { if ($xml->count() && isset($xml->translation)) { /** * @var \SimpleXmlElement */ $data = $xml->translation; if ($this->_isAttributeSet($data, 'locale')) { $config['locale'] = $this->_getAttribute($data, 'locale'); } elseif ($this->_isAttributeSet($data, 'language')) { $config['locale'] = $this->_getAttribute($data, 'language'); } if ($this->_isAttributeSet($data, 'entity')) { $entity = $this->_getAttribute($data, 'entity'); if (!$cl = $this->getRelatedClassName($meta, $entity)) { throw new InvalidMappingException("Translation entity class: {$entity} does not exist."); } $config['translationClass'] = $cl; } } } if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) { foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) { if ($meta->isInheritedEmbeddedClass($propertyName)) { continue; } $xmlEmbeddedClass = $this->_getMapping($embeddedClassInfo['class']); $this->inspectElementsForTranslatableFields($xmlEmbeddedClass, $config, $propertyName); } } if ($xmlDoctrine->{'attribute-overrides'}->count() > 0) { foreach ($xmlDoctrine->{'attribute-overrides'}->{'attribute-override'} as $overrideMapping) { $this->buildFieldConfiguration($this->_getAttribute($overrideMapping, 'name'), $overrideMapping->field, $config); } } $this->inspectElementsForTranslatableFields($xmlDoctrine, $config); 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()}"); } } } private function inspectElementsForTranslatableFields(\SimpleXMLElement $xml, array &$config, ?string $prefix = null): void { if (!isset($xml->field)) { return; } foreach ($xml->field as $mapping) { $mappingDoctrine = $mapping; $fieldName = $this->_getAttribute($mappingDoctrine, 'name'); if (null !== $prefix) { $fieldName = $prefix.'.'.$fieldName; } $this->buildFieldConfiguration($fieldName, $mapping, $config); } } private function buildFieldConfiguration(string $fieldName, \SimpleXMLElement $mapping, array &$config): void { $mapping = $mapping->children(self::GEDMO_NAMESPACE_URI); if ($mapping->count() > 0 && isset($mapping->translatable)) { $config['fields'][] = $fieldName; /** @var \SimpleXmlElement $data */ $data = $mapping->translatable; if ($this->_isAttributeSet($data, 'fallback')) { $config['fallback'][$fieldName] = $this->_getBooleanAttribute($data, 'fallback'); } } } } doctrine-extensions/src/Translatable/Mapping/Driver/Yaml.php 0000644 00000006573 15117737237 0020244 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; /** * This is a yaml mapping driver for Translatable * behavioral extension. Used for extraction of extended * metadata from yaml specifically for Translatable * extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['gedmo'])) { $classMapping = $mapping['gedmo']; if (isset($classMapping['translation']['entity'])) { $translationEntity = $classMapping['translation']['entity']; if (!$cl = $this->getRelatedClassName($meta, $translationEntity)) { throw new InvalidMappingException("Translation entity class: {$translationEntity} does not exist."); } $config['translationClass'] = $cl; } if (isset($classMapping['translation']['locale'])) { $config['locale'] = $classMapping['translation']['locale']; } elseif (isset($classMapping['translation']['language'])) { $config['locale'] = $classMapping['translation']['language']; } } if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { $this->buildFieldConfiguration($field, $fieldMapping, $config); } } if (isset($mapping['attributeOverride'])) { foreach ($mapping['attributeOverride'] as $field => $overrideMapping) { $this->buildFieldConfiguration($field, $overrideMapping, $config); } } 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()}"); } } } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } private function buildFieldConfiguration(string $field, array $fieldMapping, array &$config): void { if (isset($fieldMapping['gedmo'])) { if (in_array('translatable', $fieldMapping['gedmo'], true) || isset($fieldMapping['gedmo']['translatable'])) { // fields cannot be overrided and throws mapping exception $config['fields'][] = $field; if (isset($fieldMapping['gedmo']['translatable']['fallback'])) { $config['fallback'][$field] = $fieldMapping['gedmo']['translatable']['fallback']; } } } } } doctrine-extensions/src/Translatable/Mapping/Event/Adapter/ODM.php 0000644 00000015227 15117737237 0021163 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Mapping\Event\Adapter; use Doctrine\ODM\MongoDB\Iterator\Iterator; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\Tool\Wrapper\AbstractWrapper; use Gedmo\Tool\Wrapper\MongoDocumentWrapper; use Gedmo\Translatable\Document\MappedSuperclass\AbstractPersonalTranslation; use Gedmo\Translatable\Document\Translation; use Gedmo\Translatable\Mapping\Event\TranslatableAdapter; /** * Doctrine event adapter for ODM adapted * for Translatable behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class ODM extends BaseAdapterODM implements TranslatableAdapter { public function usesPersonalTranslation($translationClassName) { return $this ->getObjectManager() ->getClassMetadata($translationClassName) ->getReflectionClass() ->isSubclassOf(AbstractPersonalTranslation::class) ; } public function getDefaultTranslationClass() { return Translation::class; } public function loadTranslations($object, $translationClass, $locale, $objectClass) { $dm = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $dm); assert($wrapped instanceof MongoDocumentWrapper); $result = []; if ($this->usesPersonalTranslation($translationClass)) { // first try to load it using collection foreach ($wrapped->getMetadata()->fieldMappings as $mapping) { $isRightCollection = isset($mapping['association']) && ClassMetadata::REFERENCE_MANY === $mapping['association'] && $mapping['targetDocument'] === $translationClass && 'object' === $mapping['mappedBy'] ; if ($isRightCollection) { $collection = $wrapped->getPropertyValue($mapping['fieldName']); foreach ($collection as $trans) { if ($trans->getLocale() === $locale) { $result[] = [ 'field' => $trans->getField(), 'content' => $trans->getContent(), ]; } } return $result; } } $q = $dm ->createQueryBuilder($translationClass) ->field('object.$id')->equals($wrapped->getIdentifier()) ->field('locale')->equals($locale) ->getQuery() ; } else { // load translated content for all translatable fields // construct query $q = $dm ->createQueryBuilder($translationClass) ->field('foreignKey')->equals($wrapped->getIdentifier()) ->field('locale')->equals($locale) ->field('objectClass')->equals($objectClass) ->getQuery() ; } $q->setHydrate(false); $result = $q->execute(); if ($result instanceof Iterator) { $result = $result->toArray(); } return $result; } public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $translationClass, $objectClass) { $dm = $this->getObjectManager(); $qb = $dm ->createQueryBuilder($translationClass) ->field('locale')->equals($locale) ->field('field')->equals($field) ->limit(1) ; if ($this->usesPersonalTranslation($translationClass)) { $qb->field('object.$id')->equals($wrapped->getIdentifier()); } else { $qb->field('foreignKey')->equals($wrapped->getIdentifier()); $qb->field('objectClass')->equals($objectClass); } $q = $qb->getQuery(); return $q->getSingleResult(); } public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transClass, $objectClass) { $dm = $this->getObjectManager(); $qb = $dm ->createQueryBuilder($transClass) ->remove() ; if ($this->usesPersonalTranslation($transClass)) { $qb->field('object.$id')->equals($wrapped->getIdentifier()); } else { $qb->field('foreignKey')->equals($wrapped->getIdentifier()); $qb->field('objectClass')->equals($objectClass); } $q = $qb->getQuery(); return $q->execute(); } public function insertTranslationRecord($translation) { $dm = $this->getObjectManager(); $meta = $dm->getClassMetadata(get_class($translation)); $collection = $dm->getDocumentCollection($meta->getName()); $data = []; foreach ($meta->getReflectionProperties() as $fieldName => $reflProp) { if (!$meta->isIdentifier($fieldName)) { $data[$meta->getFieldMapping($fieldName)['name']] = $reflProp->getValue($translation); } } $insertResult = $collection->insertOne($data); if (false === $insertResult->isAcknowledged()) { throw new \Gedmo\Exception\RuntimeException('Failed to insert new Translation record'); } } public function getTranslationValue($object, $field, $value = false) { $dm = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $dm); assert($wrapped instanceof MongoDocumentWrapper); $meta = $wrapped->getMetadata(); $mapping = $meta->getFieldMapping($field); $type = $this->getType($mapping['type']); if (false === $value) { $value = $wrapped->getPropertyValue($field); } return $type->convertToDatabaseValue($value); } public function setTranslationValue($object, $field, $value) { $dm = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $dm); assert($wrapped instanceof MongoDocumentWrapper); $meta = $wrapped->getMetadata(); $mapping = $meta->getFieldMapping($field); $type = $this->getType($mapping['type']); $value = $type->convertToPHPValue($value); $wrapped->setPropertyValue($field, $value); } private function getType(string $type): Type { return Type::getType($type); } } doctrine-extensions/src/Translatable/Mapping/Event/Adapter/ORM.php 0000644 00000022601 15117737237 0021173 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Mapping\Event\Adapter; use Doctrine\Common\Proxy\Proxy; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\Tool\Wrapper\AbstractWrapper; use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation; use Gedmo\Translatable\Entity\Translation; use Gedmo\Translatable\Mapping\Event\TranslatableAdapter; /** * Doctrine event adapter for ORM adapted * for Translatable behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class ORM extends BaseAdapterORM implements TranslatableAdapter { public function usesPersonalTranslation($translationClassName) { return $this ->getObjectManager() ->getClassMetadata($translationClassName) ->getReflectionClass() ->isSubclassOf(AbstractPersonalTranslation::class) ; } public function getDefaultTranslationClass() { return Translation::class; } public function loadTranslations($object, $translationClass, $locale, $objectClass) { $em = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $em); $result = []; if ($this->usesPersonalTranslation($translationClass)) { // first try to load it using collection $found = false; $metadata = $wrapped->getMetadata(); assert($metadata instanceof ClassMetadataInfo); foreach ($metadata->getAssociationMappings() as $assoc) { $isRightCollection = $assoc['targetEntity'] === $translationClass && 'object' === $assoc['mappedBy'] && ClassMetadataInfo::ONE_TO_MANY === $assoc['type'] ; if ($isRightCollection) { $collection = $wrapped->getPropertyValue($assoc['fieldName']); foreach ($collection as $trans) { if ($trans->getLocale() === $locale) { $result[] = [ 'field' => $trans->getField(), 'content' => $trans->getContent(), ]; } } $found = true; break; } } // if collection is not set, fetch it through relation if (!$found) { $dql = 'SELECT t.content, t.field FROM '.$translationClass.' t'; $dql .= ' WHERE t.locale = :locale'; $dql .= ' AND t.object = :object'; $q = $em->createQuery($dql); $q->setParameters(compact('object', 'locale')); $result = $q->getArrayResult(); } } else { // load translated content for all translatable fields $objectId = $this->foreignKey($wrapped->getIdentifier(), $translationClass); // construct query $dql = 'SELECT t.content, t.field FROM '.$translationClass.' t'; $dql .= ' WHERE t.foreignKey = :objectId'; $dql .= ' AND t.locale = :locale'; $dql .= ' AND t.objectClass = :objectClass'; // fetch results $q = $em->createQuery($dql); $q->setParameters(compact('objectId', 'locale', 'objectClass')); $result = $q->getArrayResult(); } return $result; } public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $translationClass, $objectClass) { $em = $this->getObjectManager(); // first look in identityMap, will save one SELECT query foreach ($em->getUnitOfWork()->getIdentityMap() as $className => $objects) { if ($className === $translationClass) { foreach ($objects as $trans) { $isRequestedTranslation = !$trans instanceof Proxy && $trans->getLocale() === $locale && $trans->getField() === $field ; if ($isRequestedTranslation) { if ($this->usesPersonalTranslation($translationClass)) { $isRequestedTranslation = $trans->getObject() === $wrapped->getObject(); } else { $objectId = $this->foreignKey($wrapped->getIdentifier(), $translationClass); $isRequestedTranslation = $trans->getForeignKey() === $objectId && $trans->getObjectClass() === $wrapped->getMetadata()->getName() ; } } if ($isRequestedTranslation) { return $trans; } } } } $qb = $em->createQueryBuilder(); $qb->select('trans') ->from($translationClass, 'trans') ->where( 'trans.locale = :locale', 'trans.field = :field' ) ; $qb->setParameters(compact('locale', 'field')); if ($this->usesPersonalTranslation($translationClass)) { $qb->andWhere('trans.object = :object'); if ($wrapped->getIdentifier()) { $qb->setParameter('object', $wrapped->getObject()); } else { $qb->setParameter('object', null); } } else { $qb->andWhere('trans.foreignKey = :objectId'); $qb->andWhere('trans.objectClass = :objectClass'); $qb->setParameter('objectId', $this->foreignKey($wrapped->getIdentifier(), $translationClass)); $qb->setParameter('objectClass', $objectClass); } $q = $qb->getQuery(); $q->setMaxResults(1); $result = $q->getResult(); if ($result) { return array_shift($result); } return null; } public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transClass, $objectClass) { $qb = $this ->getObjectManager() ->createQueryBuilder() ->delete($transClass, 'trans') ; if ($this->usesPersonalTranslation($transClass)) { $qb->where('trans.object = :object'); $qb->setParameter('object', $wrapped->getObject()); } else { $qb->where( 'trans.foreignKey = :objectId', 'trans.objectClass = :class' ); $qb->setParameter('objectId', $this->foreignKey($wrapped->getIdentifier(), $transClass)); $qb->setParameter('class', $objectClass); } return $qb->getQuery()->getSingleScalarResult(); } public function insertTranslationRecord($translation) { $em = $this->getObjectManager(); $meta = $em->getClassMetadata(get_class($translation)); $data = []; foreach ($meta->getReflectionProperties() as $fieldName => $reflProp) { if (!$meta->isIdentifier($fieldName)) { $data[$meta->getColumnName($fieldName)] = $reflProp->getValue($translation); } } $table = $meta->getTableName(); if (!$em->getConnection()->insert($table, $data)) { throw new \Gedmo\Exception\RuntimeException('Failed to insert new Translation record'); } } public function getTranslationValue($object, $field, $value = false) { $em = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $em); $meta = $wrapped->getMetadata(); $type = Type::getType($meta->getTypeOfField($field)); if (false === $value) { $value = $wrapped->getPropertyValue($field); } return $type->convertToDatabaseValue($value, $em->getConnection()->getDatabasePlatform()); } public function setTranslationValue($object, $field, $value) { $em = $this->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $em); $meta = $wrapped->getMetadata(); $type = Type::getType($meta->getTypeOfField($field)); $value = $type->convertToPHPValue($value, $em->getConnection()->getDatabasePlatform()); $wrapped->setPropertyValue($field, $value); } /** * Transforms foreing key of translation to appropriate PHP value * to prevent database level cast * * @param mixed $key foreign key value * @param string $className translation class name * @phpstan-param class-string $className translation class name * * @return int|string transformed foreign key */ private function foreignKey($key, string $className) { $em = $this->getObjectManager(); $meta = $em->getClassMetadata($className); $type = Type::getType($meta->getTypeOfField('foreignKey')); switch ($type->getName()) { case Types::BIGINT: case Types::INTEGER: case Types::SMALLINT: return (int) $key; default: return (string) $key; } } } doctrine-extensions/src/Translatable/Mapping/Event/TranslatableAdapter.php 0000644 00000006155 15117737237 0023101 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Mapping\Event; use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Tool\Wrapper\AbstractWrapper; /** * Doctrine event adapter for the Translatable extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface TranslatableAdapter extends AdapterInterface { /** * Checks if the given translation class is a subclass of the personal translation class. * * @param string $translationClassName * * @phpstan-param class-string $translationClassName * * @return bool */ public function usesPersonalTranslation($translationClassName); /** * Get the default translation class used to store translations. * * @return string * @phpstan-return class-string */ public function getDefaultTranslationClass(); /** * Load the translations for a given object. * * @param object $object * @param string $translationClass * @param string $locale * @param string $objectClass * * @phpstan-param class-string $translationClass * @phpstan-param class-string $objectClass * * @return array */ public function loadTranslations($object, $translationClass, $locale, $objectClass); /** * Search for an existing translation record. * * @param string $locale * @param string $field * @param string $translationClass * @param string $objectClass * * @phpstan-param class-string $translationClass * @phpstan-param class-string $objectClass * * @return mixed null if nothing is found, translation object otherwise */ public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $translationClass, $objectClass); /** * Removes all associated translations for the given object. * * @param string $transClass * @param string $objectClass * * @phpstan-param class-string $transClass * @phpstan-param class-string $objectClass * * @return int */ public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transClass, $objectClass); /** * Inserts the translation record. * * @param object $translation * * @return void */ public function insertTranslationRecord($translation); /** * Get the transformed value for translation storage. * * @param object $object * @param string $field * @param mixed $value * * @return mixed */ public function getTranslationValue($object, $field, $value = false); /** * Transform the value from the database for translation * * @param object $object * @param string $field * @param mixed $value * * @return void */ public function setTranslationValue($object, $field, $value); } doctrine-extensions/src/Translatable/Query/TreeWalker/TranslationWalker.php 0000644 00000040426 15117737237 0023325 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable\Query\TreeWalker; use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Types\Type; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; use Doctrine\ORM\Query\AST\Join; use Doctrine\ORM\Query\AST\Node; use Doctrine\ORM\Query\AST\RangeVariableDeclaration; use Doctrine\ORM\Query\AST\SelectStatement; use Doctrine\ORM\Query\Exec\SingleSelectExecutor; use Doctrine\ORM\Query\SqlWalker; use Gedmo\Translatable\Hydrator\ORM\ObjectHydrator; use Gedmo\Translatable\Hydrator\ORM\SimpleObjectHydrator; use Gedmo\Translatable\Mapping\Event\Adapter\ORM as TranslatableEventAdapter; use Gedmo\Translatable\TranslatableListener; /** * The translation sql output walker makes it possible * to translate all query components during single query. * It works with any select query, any hydration method. * * Behind the scenes, during the object hydration it forces * custom hydrator in order to interact with TranslatableListener * and skip postLoad event which would cause automatic retranslation * of the fields. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class TranslationWalker extends SqlWalker { /** * Name for translation fallback hint * * @internal */ public const HINT_TRANSLATION_FALLBACKS = '__gedmo.translatable.stored.fallbacks'; /** * Customized object hydrator name * * @internal */ public const HYDRATE_OBJECT_TRANSLATION = '__gedmo.translatable.object.hydrator'; /** * Customized object hydrator name * * @internal */ public const HYDRATE_SIMPLE_OBJECT_TRANSLATION = '__gedmo.translatable.simple_object.hydrator'; /** * Stores all component references from select clause * * @var array */ private $translatedComponents = []; /** * DBAL database platform * * @var \Doctrine\DBAL\Platforms\AbstractPlatform */ private $platform; /** * DBAL database connection * * @var \Doctrine\DBAL\Connection */ private $conn; /** * List of aliases to replace with translation * content reference * * @var array */ private $replacements = []; /** * List of joins for translated components in query * * @var array */ private $components = []; /** * @var TranslatableListener */ private $listener; public function __construct($query, $parserResult, array $queryComponents) { parent::__construct($query, $parserResult, $queryComponents); $this->conn = $this->getConnection(); $this->platform = $this->getConnection()->getDatabasePlatform(); $this->listener = $this->getTranslatableListener(); $this->extractTranslatedComponents($queryComponents); } /** * @return Query\Exec\AbstractSqlExecutor */ public function getExecutor($AST) { // If it's not a Select, the TreeWalker ought to skip it, and just return the parent. // @see https://github.com/Atlantic18/DoctrineExtensions/issues/2013 if (!$AST instanceof SelectStatement) { return parent::getExecutor($AST); } $this->prepareTranslatedComponents(); return new SingleSelectExecutor($AST, $this); } /** * @return string */ public function walkSelectStatement(SelectStatement $AST) { $result = parent::walkSelectStatement($AST); if ([] === $this->translatedComponents) { return $result; } $hydrationMode = $this->getQuery()->getHydrationMode(); if (Query::HYDRATE_OBJECT === $hydrationMode) { $this->getQuery()->setHydrationMode(self::HYDRATE_OBJECT_TRANSLATION); $this->getEntityManager()->getConfiguration()->addCustomHydrationMode( self::HYDRATE_OBJECT_TRANSLATION, ObjectHydrator::class ); $this->getQuery()->setHint(Query::HINT_REFRESH, true); } elseif (Query::HYDRATE_SIMPLEOBJECT === $hydrationMode) { $this->getQuery()->setHydrationMode(self::HYDRATE_SIMPLE_OBJECT_TRANSLATION); $this->getEntityManager()->getConfiguration()->addCustomHydrationMode( self::HYDRATE_SIMPLE_OBJECT_TRANSLATION, SimpleObjectHydrator::class ); $this->getQuery()->setHint(Query::HINT_REFRESH, true); } return $result; } /** * @return string */ public function walkSelectClause($selectClause) { $result = parent::walkSelectClause($selectClause); $result = $this->replace($this->replacements, $result); return $result; } /** * @return string */ public function walkFromClause($fromClause) { $result = parent::walkFromClause($fromClause); $result .= $this->joinTranslations($fromClause); return $result; } /** * @return string */ public function walkWhereClause($whereClause) { $result = parent::walkWhereClause($whereClause); return $this->replace($this->replacements, $result); } /** * @return string */ public function walkHavingClause($havingClause) { $result = parent::walkHavingClause($havingClause); return $this->replace($this->replacements, $result); } /** * @return string */ public function walkOrderByClause($orderByClause) { $result = parent::walkOrderByClause($orderByClause); return $this->replace($this->replacements, $result); } /** * @return string */ public function walkSubselect($subselect) { $result = parent::walkSubselect($subselect); return $result; } /** * @return string */ public function walkSubselectFromClause($subselectFromClause) { $result = parent::walkSubselectFromClause($subselectFromClause); $result .= $this->joinTranslations($subselectFromClause); return $result; } /** * @return string */ public function walkSimpleSelectClause($simpleSelectClause) { $result = parent::walkSimpleSelectClause($simpleSelectClause); return $this->replace($this->replacements, $result); } /** * @return string */ public function walkGroupByClause($groupByClause) { $result = parent::walkGroupByClause($groupByClause); return $this->replace($this->replacements, $result); } /** * Walks from clause, and creates translation joins * for the translated components * * @param \Doctrine\ORM\Query\AST\FromClause|\Doctrine\ORM\Query\AST\SubselectFromClause $from */ private function joinTranslations(Node $from): string { $result = ''; foreach ($from->identificationVariableDeclarations as $decl) { if ($decl->rangeVariableDeclaration instanceof RangeVariableDeclaration) { if (isset($this->components[$decl->rangeVariableDeclaration->aliasIdentificationVariable])) { $result .= $this->components[$decl->rangeVariableDeclaration->aliasIdentificationVariable]; } } if (isset($decl->joinVariableDeclarations)) { foreach ($decl->joinVariableDeclarations as $joinDecl) { if ($joinDecl->join instanceof Join) { if (isset($this->components[$joinDecl->join->aliasIdentificationVariable])) { $result .= $this->components[$joinDecl->join->aliasIdentificationVariable]; } } } } else { // based on new changes foreach ($decl->joins as $join) { if ($join instanceof Join) { if (isset($this->components[$join->joinAssociationDeclaration->aliasIdentificationVariable])) { $result .= $this->components[$join->joinAssociationDeclaration->aliasIdentificationVariable]; } } } } } return $result; } /** * Creates a left join list for translations * on used query components * * @todo: make it cleaner */ private function prepareTranslatedComponents(): void { $q = $this->getQuery(); $locale = $q->getHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE); if (!$locale) { // use from listener $locale = $this->listener->getListenerLocale(); } $defaultLocale = $this->listener->getDefaultLocale(); if ($locale === $defaultLocale && !$this->listener->getPersistDefaultLocaleTranslation()) { // Skip preparation as there's no need to translate anything return; } $em = $this->getEntityManager(); $ea = new TranslatableEventAdapter(); $ea->setEntityManager($em); $quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); $joinStrategy = $q->getHint(TranslatableListener::HINT_INNER_JOIN) ? 'INNER' : 'LEFT'; foreach ($this->translatedComponents as $dqlAlias => $comp) { /** @var ClassMetadata $meta */ $meta = $comp['metadata']; $config = $this->listener->getConfiguration($em, $meta->getName()); $transClass = $this->listener->getTranslationClass($ea, $meta->getName()); $transMeta = $em->getClassMetadata($transClass); $transTable = $quoteStrategy->getTableName($transMeta, $this->platform); foreach ($config['fields'] as $field) { $compTblAlias = $this->walkIdentificationVariable($dqlAlias, $field); $tblAlias = $this->getSQLTableAlias('trans'.$compTblAlias.$field); $sql = " {$joinStrategy} JOIN ".$transTable.' '.$tblAlias; $sql .= ' ON '.$tblAlias.'.'.$quoteStrategy->getColumnName('locale', $transMeta, $this->platform) .' = '.$this->conn->quote($locale); $sql .= ' AND '.$tblAlias.'.'.$quoteStrategy->getColumnName('field', $transMeta, $this->platform) .' = '.$this->conn->quote($field); $identifier = $meta->getSingleIdentifierFieldName(); $idColName = $quoteStrategy->getColumnName($identifier, $meta, $this->platform); if ($ea->usesPersonalTranslation($transClass)) { $sql .= ' AND '.$tblAlias.'.'.$transMeta->getSingleAssociationJoinColumnName('object') .' = '.$compTblAlias.'.'.$idColName; } else { $sql .= ' AND '.$tblAlias.'.'.$quoteStrategy->getColumnName('objectClass', $transMeta, $this->platform) .' = '.$this->conn->quote($config['useObjectClass']); $mappingFK = $transMeta->getFieldMapping('foreignKey'); $mappingPK = $meta->getFieldMapping($identifier); $fkColName = $this->getCastedForeignKey($compTblAlias.'.'.$idColName, $mappingFK['type'], $mappingPK['type']); $sql .= ' AND '.$tblAlias.'.'.$quoteStrategy->getColumnName('foreignKey', $transMeta, $this->platform) .' = '.$fkColName; } isset($this->components[$dqlAlias]) ? $this->components[$dqlAlias] .= $sql : $this->components[$dqlAlias] = $sql; $originalField = $compTblAlias.'.'.$quoteStrategy->getColumnName($field, $meta, $this->platform); $substituteField = $tblAlias.'.'.$quoteStrategy->getColumnName('content', $transMeta, $this->platform); // Treat translation as original field type $fieldMapping = $meta->getFieldMapping($field); if ((($this->platform instanceof MySQLPlatform) && in_array($fieldMapping['type'], ['decimal'], true)) || (!($this->platform instanceof MySQLPlatform) && !in_array($fieldMapping['type'], ['datetime', 'datetimetz', 'date', 'time'], true))) { $type = Type::getType($fieldMapping['type']); $substituteField = 'CAST('.$substituteField.' AS '.$type->getSQLDeclaration($fieldMapping, $this->platform).')'; } // Fallback to original if was asked for if (($this->needsFallback() && (!isset($config['fallback'][$field]) || $config['fallback'][$field])) || (!$this->needsFallback() && isset($config['fallback'][$field]) && $config['fallback'][$field]) ) { $substituteField = 'COALESCE('.$substituteField.', '.$originalField.')'; } $this->replacements[$originalField] = $substituteField; } } } /** * Checks if translation fallbacks are needed */ private function needsFallback(): bool { $q = $this->getQuery(); $fallback = $q->getHint(TranslatableListener::HINT_FALLBACK); if (false === $fallback) { // non overrided $fallback = $this->listener->getTranslationFallback(); } // applies fallbacks to scalar hydration as well return (bool) $fallback; } /** * Search for translated components in the select clause */ private function extractTranslatedComponents(array $queryComponents): void { $em = $this->getEntityManager(); foreach ($queryComponents as $alias => $comp) { if (!isset($comp['metadata'])) { continue; } $meta = $comp['metadata']; $config = $this->listener->getConfiguration($em, $meta->getName()); if ($config && isset($config['fields'])) { $this->translatedComponents[$alias] = $comp; } } } /** * Get the currently used TranslatableListener * * @throws \Gedmo\Exception\RuntimeException if listener is not found */ private function getTranslatableListener(): TranslatableListener { $em = $this->getEntityManager(); foreach ($em->getEventManager()->getAllListeners() as $event => $listeners) { foreach ($listeners as $hash => $listener) { if ($listener instanceof TranslatableListener) { return $listener; } } } throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found'); } /** * Replaces given sql $str with required * results */ private function replace(array $repl, string $str): string { foreach ($repl as $target => $result) { $str = preg_replace_callback('/(\s|\()('.$target.')(,?)(\s|\)|$)/smi', static function (array $m) use ($result): string { return $m[1].$result.$m[3].$m[4]; }, $str); } return $str; } /** * Casts a foreign key if needed * * @NOTE: personal translations manages that for themselves. * * @param string $component a column with an alias to cast * @param string $typeFK translation table foreign key type * @param string $typePK primary key type which references translation table * * @return string modified $component if needed */ private function getCastedForeignKey(string $component, string $typeFK, string $typePK): string { // the keys are of same type if ($typeFK === $typePK) { return $component; } // try to look at postgres casting if ($this->platform instanceof PostgreSQLPlatform) { switch ($typeFK) { case 'string': case 'guid': // need to cast to VARCHAR $component = $component.'::VARCHAR'; break; } } // @TODO may add the same thing for MySQL for performance to match index return $component; } } doctrine-extensions/src/Translatable/Translatable.php 0000644 00000002131 15117737237 0017112 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable; /** * This interface is not necessary but can be implemented for * Entities which in some cases needs to be identified as * Translatable * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface Translatable { // use now annotations instead of predefined methods, this interface is not necessary /* * @gedmo:TranslationEntity * to specify custom translation class use * class annotation @gedmo:TranslationEntity(class="your\class") */ /* * @gedmo:Translatable * to mark the field as translatable, * these fields will be translated */ /* * @gedmo:Locale OR @gedmo:Language * to mark the field as locale used to override global * locale settings from TranslatableListener */ } doctrine-extensions/src/Translatable/TranslatableListener.php 0000644 00000070712 15117737237 0020632 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translatable; use Doctrine\Common\EventArgs; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ORM\ORMInvalidArgumentException; use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\MappedEventSubscriber; use Gedmo\Tool\Wrapper\AbstractWrapper; use Gedmo\Translatable\Mapping\Event\TranslatableAdapter; /** * The translation listener handles the generation and * loading of translations for entities which implements * the Translatable interface. * * This behavior can impact the performance of your application * since it does an additional query for each field to translate. * * Nevertheless the annotation metadata is properly cached and * it is not a big overhead to lookup all entity annotations since * the caching is activated for metadata * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @phpstan-type TranslatableConfiguration = array{ * fields?: string[], * fallback?: array<string, bool>, * locale?: string, * translationClass?: class-string, * useObjectClass?: class-string, * } * * @phpstan-method TranslatableConfiguration getConfiguration(ObjectManager $objectManager, $class) * * @method TranslatableAdapter getEventAdapter(EventArgs $args) * * @final since gedmo/doctrine-extensions 3.11 */ class TranslatableListener extends MappedEventSubscriber { /** * Query hint to override the fallback of translations * integer 1 for true, 0 false */ public const HINT_FALLBACK = 'gedmo.translatable.fallback'; /** * Query hint to override the fallback locale */ public const HINT_TRANSLATABLE_LOCALE = 'gedmo.translatable.locale'; /** * Query hint to use inner join strategy for translations */ public const HINT_INNER_JOIN = 'gedmo.translatable.inner_join.translations'; /** * Locale which is set on this listener. * If Entity being translated has locale defined it * will override this one * * @var string */ protected $locale = 'en_US'; /** * Default locale, this changes behavior * to not update the original record field if locale * which is used for updating is not default. This * will load the default translation in other locales * if record is not translated yet * * @var string */ private $defaultLocale = 'en_US'; /** * If this is set to false, when if entity does * not have a translation for requested locale * it will show a blank value * * @var bool */ private $translationFallback = false; /** * List of translations which do not have the foreign * key generated yet - MySQL case. These translations * will be updated with new keys on postPersist event * * @var array */ private $pendingTranslationInserts = []; /** * Currently in case if there is TranslationQueryWalker * in charge. We need to skip issuing additional queries * on load * * @var bool */ private $skipOnLoad = false; /** * Tracks locale the objects currently translated in * * @var array */ private $translatedInLocale = []; /** * Whether or not, to persist default locale * translation or keep it in original record * * @var bool */ private $persistDefaultLocaleTranslation = false; /** * Tracks translation object for default locale * * @var array */ private $translationInDefaultLocale = []; /** * Default translation value upon missing translation * * @var string|null */ private $defaultTranslationValue; /** * Specifies the list of events to listen * * @return string[] */ public function getSubscribedEvents() { return [ 'postLoad', 'postPersist', 'preFlush', 'onFlush', 'loadClassMetadata', ]; } /** * Set to skip or not onLoad event * * @param bool $bool * * @return static */ public function setSkipOnLoad($bool) { $this->skipOnLoad = (bool) $bool; return $this; } /** * Whether or not, to persist default locale * translation or keep it in original record * * @param bool $bool * * @return static */ public function setPersistDefaultLocaleTranslation($bool) { $this->persistDefaultLocaleTranslation = (bool) $bool; return $this; } /** * Check if should persist default locale * translation or keep it in original record * * @return bool */ public function getPersistDefaultLocaleTranslation() { return (bool) $this->persistDefaultLocaleTranslation; } /** * Add additional $translation for pending $oid object * which is being inserted * * @param int $oid * @param object $translation * * @return void */ public function addPendingTranslationInsert($oid, $translation) { $this->pendingTranslationInserts[$oid][] = $translation; } /** * Maps additional metadata * * @param LoadClassMetadataEventArgs $eventArgs * * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } /** * Get the translation class to be used * for the object $class * * @param string $class * @phpstan-param class-string $class * * @return string * @phpstan-return class-string */ public function getTranslationClass(TranslatableAdapter $ea, $class) { return self::$configurations[$this->name][$class]['translationClass'] ?? $ea->getDefaultTranslationClass() ; } /** * Enable or disable translation fallback * to original record value * * @param bool $bool * * @return static */ public function setTranslationFallback($bool) { $this->translationFallback = (bool) $bool; return $this; } /** * Weather or not is using the translation * fallback to original record * * @return bool */ public function getTranslationFallback() { return $this->translationFallback; } /** * Set the locale to use for translation listener * * @param string $locale * * @return static */ public function setTranslatableLocale($locale) { $this->validateLocale($locale); $this->locale = $locale; return $this; } /** * Set the default translation value on missing translation * * @deprecated usage of a non nullable value for defaultTranslationValue is deprecated * and will be removed on the next major release which will rely on the expected types */ public function setDefaultTranslationValue(?string $defaultTranslationValue): void { $this->defaultTranslationValue = $defaultTranslationValue; } /** * Sets the default locale, this changes behavior * to not update the original record field if locale * which is used for updating is not default * * @param string $locale * * @return static */ public function setDefaultLocale($locale) { $this->validateLocale($locale); $this->defaultLocale = $locale; return $this; } /** * Gets the default locale * * @return string */ public function getDefaultLocale() { return $this->defaultLocale; } /** * Get currently set global locale, used * extensively during query execution * * @return string */ public function getListenerLocale() { return $this->locale; } /** * Gets the locale to use for translation. Loads object * defined locale first.. * * @param object $object * @param ClassMetadata $meta * @param object $om * * @throws \Gedmo\Exception\RuntimeException if language or locale property is not * found in entity * * @return string */ public function getTranslatableLocale($object, $meta, $om = null) { $locale = $this->locale; $configurationLocale = self::$configurations[$this->name][$meta->getName()]['locale'] ?? null; if (null !== $configurationLocale) { $class = $meta->getReflectionClass(); if (!$class->hasProperty($configurationLocale)) { throw new \Gedmo\Exception\RuntimeException("There is no locale or language property ({$configurationLocale}) found on object: {$meta->getName()}"); } $reflectionProperty = $class->getProperty($configurationLocale); $reflectionProperty->setAccessible(true); $value = $reflectionProperty->getValue($object); if (is_object($value) && method_exists($value, '__toString')) { $value = $value->__toString(); } if ($this->isValidLocale($value)) { $locale = $value; } } elseif ($om instanceof DocumentManager) { [$mapping, $parentObject] = $om->getUnitOfWork()->getParentAssociation($object); if (null !== $parentObject) { $parentMeta = $om->getClassMetadata(get_class($parentObject)); $locale = $this->getTranslatableLocale($parentObject, $parentMeta, $om); } } return $locale; } /** * Handle translation changes in default locale * * This has to be done in the preFlush because, when an entity has been loaded * in a different locale, no changes will be detected. * * @return void */ public function preFlush(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); foreach ($this->translationInDefaultLocale as $oid => $fields) { $trans = reset($fields); if ($ea->usesPersonalTranslation(get_class($trans))) { $entity = $trans->getObject(); } else { $entity = $uow->tryGetById($trans->getForeignKey(), $trans->getObjectClass()); } if (!$entity) { continue; } try { $uow->scheduleForUpdate($entity); } catch (ORMInvalidArgumentException $e) { foreach ($fields as $field => $trans) { $this->removeTranslationInDefaultLocale($oid, $field); } } } } /** * Looks for translatable objects being inserted or updated * for further processing * * @return void */ public function onFlush(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); // check all scheduled inserts for Translatable objects foreach ($ea->getScheduledObjectInsertions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['fields'])) { $this->handleTranslatableObjectUpdate($ea, $object, true); } } // check all scheduled updates for Translatable entities foreach ($ea->getScheduledObjectUpdates($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['fields'])) { $this->handleTranslatableObjectUpdate($ea, $object, false); } } // check scheduled deletions for Translatable entities foreach ($ea->getScheduledObjectDeletions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); $config = $this->getConfiguration($om, $meta->getName()); if (isset($config['fields'])) { $wrapped = AbstractWrapper::wrap($object, $om); $transClass = $this->getTranslationClass($ea, $meta->getName()); \assert($wrapped instanceof AbstractWrapper); $ea->removeAssociatedTranslations($wrapped, $transClass, $config['useObjectClass']); } } } /** * Checks for inserted object to update their translation * foreign keys * * @return void */ public function postPersist(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); // check if entity is tracked by translatable and without foreign key if ($this->getConfiguration($om, $meta->getName()) && [] !== $this->pendingTranslationInserts) { $oid = spl_object_id($object); if (array_key_exists($oid, $this->pendingTranslationInserts)) { // load the pending translations without key $wrapped = AbstractWrapper::wrap($object, $om); $objectId = $wrapped->getIdentifier(); $translationClass = $this->getTranslationClass($ea, get_class($object)); foreach ($this->pendingTranslationInserts[$oid] as $translation) { if ($ea->usesPersonalTranslation($translationClass)) { $translation->setObject($objectId); } else { $translation->setForeignKey($objectId); } $ea->insertTranslationRecord($translation); } unset($this->pendingTranslationInserts[$oid]); } } } /** * After object is loaded, listener updates the translations * by currently used locale * * @return void */ public function postLoad(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); $config = $this->getConfiguration($om, $meta->getName()); $locale = $this->defaultLocale; $oid = null; if (isset($config['fields'])) { $locale = $this->getTranslatableLocale($object, $meta, $om); $oid = spl_object_id($object); $this->translatedInLocale[$oid] = $locale; } if ($this->skipOnLoad) { return; } if (isset($config['fields']) && ($locale !== $this->defaultLocale || $this->persistDefaultLocaleTranslation)) { // fetch translations $translationClass = $this->getTranslationClass($ea, $config['useObjectClass']); $result = $ea->loadTranslations( $object, $translationClass, $locale, $config['useObjectClass'] ); // translate object's translatable properties foreach ($config['fields'] as $field) { $translated = $this->defaultTranslationValue; foreach ($result as $entry) { if ($entry['field'] == $field) { $translated = $entry['content'] ?? null; break; } } // update translation if ($this->defaultTranslationValue !== $translated || (!$this->translationFallback && (!isset($config['fallback'][$field]) || !$config['fallback'][$field])) || ($this->translationFallback && isset($config['fallback'][$field]) && !$config['fallback'][$field]) ) { $ea->setTranslationValue($object, $field, $translated); // ensure clean changeset $ea->setOriginalObjectProperty( $om->getUnitOfWork(), $object, $field, $meta->getReflectionProperty($field)->getValue($object) ); } } } } /** * Sets translation object which represents translation in default language. * * @param int $oid hash of basic entity * @param string $field field of basic entity * @param mixed $trans Translation object * * @return void */ public function setTranslationInDefaultLocale($oid, $field, $trans) { if (!isset($this->translationInDefaultLocale[$oid])) { $this->translationInDefaultLocale[$oid] = []; } $this->translationInDefaultLocale[$oid][$field] = $trans; } /** * @return bool */ public function isSkipOnLoad() { return $this->skipOnLoad; } /** * Check if object has any translation object which represents translation in default language. * This is for internal use only. * * @param int $oid hash of the basic entity * * @return bool */ public function hasTranslationsInDefaultLocale($oid) { return array_key_exists($oid, $this->translationInDefaultLocale); } protected function getNamespace() { return __NAMESPACE__; } /** * Validates the given locale * * @param string $locale locale to validate * * @throws \Gedmo\Exception\InvalidArgumentException if locale is not valid * * @return void */ protected function validateLocale($locale) { if (!$this->isValidLocale($locale)) { throw new \Gedmo\Exception\InvalidArgumentException('Locale or language cannot be empty and must be set through Listener or Entity'); } } /** * Check if the given locale is valid */ private function isValidlocale(?string $locale): bool { return is_string($locale) && strlen($locale); } /** * Creates the translation for object being flushed * * @throws \UnexpectedValueException if locale is not valid, or * primary key is composite, missing or invalid */ private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, object $object, bool $isInsert): void { $om = $ea->getObjectManager(); $wrapped = AbstractWrapper::wrap($object, $om); $meta = $wrapped->getMetadata(); $config = $this->getConfiguration($om, $meta->getName()); // no need cache, metadata is loaded only once in MetadataFactoryClass $translationClass = $this->getTranslationClass($ea, $config['useObjectClass']); $translationMetadata = $om->getClassMetadata($translationClass); // check for the availability of the primary key $objectId = $wrapped->getIdentifier(); // load the currently used locale $locale = $this->getTranslatableLocale($object, $meta, $om); $uow = $om->getUnitOfWork(); $oid = spl_object_id($object); $changeSet = $ea->getObjectChangeSet($uow, $object); $translatableFields = $config['fields']; foreach ($translatableFields as $field) { $wasPersistedSeparetely = false; $skip = isset($this->translatedInLocale[$oid]) && $locale === $this->translatedInLocale[$oid]; $skip = $skip && !isset($changeSet[$field]) && !$this->getTranslationInDefaultLocale($oid, $field); if ($skip) { continue; // locale is same and nothing changed } $translation = null; foreach ($ea->getScheduledObjectInsertions($uow) as $trans) { if ($locale !== $this->defaultLocale && get_class($trans) === $translationClass && $trans->getLocale() === $this->defaultLocale && $trans->getField() === $field && $this->belongsToObject($ea, $trans, $object)) { $this->setTranslationInDefaultLocale($oid, $field, $trans); break; } } // lookup persisted translations foreach ($ea->getScheduledObjectInsertions($uow) as $trans) { if (get_class($trans) !== $translationClass || $trans->getLocale() !== $locale || $trans->getField() !== $field) { continue; } if ($ea->usesPersonalTranslation($translationClass)) { $wasPersistedSeparetely = $trans->getObject() === $object; } else { $wasPersistedSeparetely = $trans->getObjectClass() === $config['useObjectClass'] && $trans->getForeignKey() === $objectId; } if ($wasPersistedSeparetely) { $translation = $trans; break; } } // check if translation already is created if (!$isInsert && !$translation) { \assert($wrapped instanceof AbstractWrapper); $translation = $ea->findTranslation( $wrapped, $locale, $field, $translationClass, $config['useObjectClass'] ); } // create new translation if translation not already created and locale is different from default locale, otherwise, we have the date in the original record $persistNewTranslation = !$translation && ($locale !== $this->defaultLocale || $this->persistDefaultLocaleTranslation) ; if ($persistNewTranslation) { $translation = $translationMetadata->newInstance(); $translation->setLocale($locale); $translation->setField($field); if ($ea->usesPersonalTranslation($translationClass)) { $translation->setObject($object); } else { $translation->setObjectClass($config['useObjectClass']); $translation->setForeignKey($objectId); } } if ($translation) { // set the translated field, take value using reflection $content = $ea->getTranslationValue($object, $field); $translation->setContent($content); // check if need to update in database $transWrapper = AbstractWrapper::wrap($translation, $om); if (((null === $content && !$isInsert) || is_bool($content) || is_int($content) || is_string($content) || !empty($content)) && ($isInsert || !$transWrapper->getIdentifier() || isset($changeSet[$field]))) { if ($isInsert && !$objectId && !$ea->usesPersonalTranslation($translationClass)) { // if we do not have the primary key yet available // keep this translation in memory to insert it later with foreign key $this->pendingTranslationInserts[spl_object_id($object)][] = $translation; } else { // persist and compute change set for translation if ($wasPersistedSeparetely) { $ea->recomputeSingleObjectChangeset($uow, $translationMetadata, $translation); } else { $om->persist($translation); $uow->computeChangeSet($translationMetadata, $translation); } } } } if ($isInsert && null !== $this->getTranslationInDefaultLocale($oid, $field)) { // We can't rely on object field value which is created in non-default locale. // If we provide translation for default locale as well, the latter is considered to be trusted // and object content should be overridden. $wrapped->setPropertyValue($field, $this->getTranslationInDefaultLocale($oid, $field)->getContent()); $ea->recomputeSingleObjectChangeset($uow, $meta, $object); $this->removeTranslationInDefaultLocale($oid, $field); } } $this->translatedInLocale[$oid] = $locale; // check if we have default translation and need to reset the translation if (!$isInsert && strlen($this->defaultLocale)) { $this->validateLocale($this->defaultLocale); $modifiedChangeSet = $changeSet; foreach ($changeSet as $field => $changes) { if (in_array($field, $translatableFields, true)) { if ($locale !== $this->defaultLocale) { $ea->setOriginalObjectProperty($uow, $object, $field, $changes[0]); unset($modifiedChangeSet[$field]); } } } $ea->recomputeSingleObjectChangeset($uow, $meta, $object); // cleanup current changeset only if working in a another locale different than de default one, otherwise the changeset will always be reverted if ($locale !== $this->defaultLocale) { $ea->clearObjectChangeSet($uow, $object); // recompute changeset only if there are changes other than reverted translations if ($modifiedChangeSet || $this->hasTranslationsInDefaultLocale($oid)) { foreach ($modifiedChangeSet as $field => $changes) { $ea->setOriginalObjectProperty($uow, $object, $field, $changes[0]); } foreach ($translatableFields as $field) { if (null !== $this->getTranslationInDefaultLocale($oid, $field)) { $wrapped->setPropertyValue($field, $this->getTranslationInDefaultLocale($oid, $field)->getContent()); $this->removeTranslationInDefaultLocale($oid, $field); } } $ea->recomputeSingleObjectChangeset($uow, $meta, $object); } } } } /** * Removes translation object which represents translation in default language. * This is for internal use only. * * @param int $oid hash of the basic entity * @param string $field field of basic entity */ private function removeTranslationInDefaultLocale(int $oid, string $field): void { if (isset($this->translationInDefaultLocale[$oid])) { if (isset($this->translationInDefaultLocale[$oid][$field])) { unset($this->translationInDefaultLocale[$oid][$field]); } if (!$this->translationInDefaultLocale[$oid]) { // We removed the final remaining elements from the // translationInDefaultLocale[$oid] array, so we might as well // completely remove the entry at $oid. unset($this->translationInDefaultLocale[$oid]); } } } /** * Gets translation object which represents translation in default language. * This is for internal use only. * * @param int $oid hash of the basic entity * @param string $field field of basic entity * * @return mixed Returns translation object if it exists or NULL otherwise */ private function getTranslationInDefaultLocale(int $oid, string $field) { if (array_key_exists($oid, $this->translationInDefaultLocale)) { $ret = $this->translationInDefaultLocale[$oid][$field] ?? null; } else { $ret = null; } return $ret; } /** * Checks if the translation entity belongs to the object in question */ private function belongsToObject(TranslatableAdapter $ea, object $trans, object $object): bool { if ($ea->usesPersonalTranslation(get_class($trans))) { return $trans->getObject() === $object; } return $trans->getForeignKey() === $object->getId() && ($trans->getObjectClass() === get_class($object)); } } doctrine-extensions/src/Translator/Document/Translation.php 0000644 00000002445 15117737237 0020277 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translator\Document; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Doctrine\ODM\MongoDB\Types\Type; use Gedmo\Translator\Translation as BaseTranslation; /** * Document translation class. * * @author Konstantin Kudryashov <ever.zet@gmail.com> * * @ODM\MappedSuperclass */ #[ODM\MappedSuperclass] abstract class Translation extends BaseTranslation { /** * @var string|null * * @ODM\Id */ #[ODM\Id] protected $id; /** * @var string|null * * @ODM\Field(type="string") */ #[ODM\Field(type: Type::STRING)] protected $locale; /** * @var string|null * * @ODM\Field(type="string") */ #[ODM\Field(type: Type::STRING)] protected $property; /** * @var string|null * * @ODM\Field(type="string") */ #[ODM\Field(type: Type::STRING)] protected $value; /** * @return string|null $id */ public function getId() { return $this->id; } } doctrine-extensions/src/Translator/Entity/Translation.php 0000644 00000003006 15117737237 0017767 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translator\Entity; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\GeneratedValue; use Doctrine\ORM\Mapping\Id; use Doctrine\ORM\Mapping\MappedSuperclass; use Gedmo\Translator\Translation as BaseTranslation; /** * Entity translation class. * * @author Konstantin Kudryashov <ever.zet@gmail.com> * * @MappedSuperclass */ #[MappedSuperclass] abstract class Translation extends BaseTranslation { /** * @var int * * @Column(type="integer") * @Id * @GeneratedValue */ #[Column(type: Types::INTEGER)] #[Id] #[GeneratedValue] protected $id; /** * @var string * * @Column(type="string", length=8) */ #[Column(type: Types::STRING, length: 8)] protected $locale; /** * @var string * * @Column(type="string", length=32) */ #[Column(type: Types::STRING, length: 32)] protected $property; /** * @var string * * @Column(type="text", nullable=true) */ #[Column(type: Types::TEXT, nullable: true)] protected $value; /** * Get id * * @return int $id */ public function getId() { return $this->id; } } doctrine-extensions/src/Translator/Translation.php 0000644 00000004004 15117737237 0016512 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translator; /** * Base translation class. * * @author Konstantin Kudryashov <ever.zet@gmail.com> */ abstract class Translation implements TranslationInterface { /** * @var object|null */ protected $translatable; /** * @var string|null */ protected $locale; /** * @var string|null */ protected $property; /** * @var string|null */ protected $value; /** * Set translatable * * @param object $translatable */ public function setTranslatable($translatable) { $this->translatable = $translatable; } /** * Get translatable * * @return object|null */ public function getTranslatable() { return $this->translatable; } /** * Set locale * * @param string $locale */ public function setLocale($locale) { $this->locale = $locale; } /** * Get locale * * @return string|null */ public function getLocale() { return $this->locale; } /** * Set property * * @param string $property */ public function setProperty($property) { $this->property = $property; } /** * Get property * * @return string|null */ public function getProperty() { return $this->property; } /** * Set value * * @param string $value * * @return static */ public function setValue($value) { $this->value = $value; return $this; } /** * Get value * * @return string|null */ public function getValue() { return $this->value; } } doctrine-extensions/src/Translator/TranslationInterface.php 0000644 00000003040 15117737237 0020332 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translator; /** * Object for managing translations. * * @author Konstantin Kudryashov <ever.zet@gmail.com> */ interface TranslationInterface { /** * Set the translatable item. * * @param object $translatable * * @return void */ public function setTranslatable($translatable); /** * Get the translatable item. * * @return object */ public function getTranslatable(); /** * Set the translation locale. * * @param string $locale * * @return void */ public function setLocale($locale); /** * Get the translation locale. * * @return string */ public function getLocale(); /** * Set the translated property. * * @param string $property * * @return void */ public function setProperty($property); /** * Get the translated property. * * @return string */ public function getProperty(); /** * Set the translation value. * * @param string $value * * @return static */ public function setValue($value); /** * Get the translation value. * * @return string */ public function getValue(); } doctrine-extensions/src/Translator/TranslationProxy.php 0000644 00000013210 15117737237 0017553 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Translator; use Doctrine\Common\Collections\Collection; /** * Proxy class for Entity/Document translations. * * @author Konstantin Kudryashov <ever.zet@gmail.com> */ class TranslationProxy { /** * @var string */ protected $locale; /** * @var object */ protected $translatable; /** * @var string[] */ protected $properties = []; /** * @var string * * @phpstan-var class-string<TranslationInterface> */ protected $class; /** * @var Collection<int, TranslationInterface> */ protected $coll; /** * Initializes translations collection * * @param object $translatable object to translate * @param string $locale translation name * @param array $properties object properties to translate * @param string $class translation entity|document class * * @throws \InvalidArgumentException Translation class doesn't implement TranslationInterface * * @phpstan-param class-string<TranslationInterface> $class * @phpstan-param Collection<int, TranslationInterface> $coll */ public function __construct($translatable, $locale, array $properties, $class, Collection $coll) { $this->translatable = $translatable; $this->locale = $locale; $this->properties = $properties; $this->class = $class; $this->coll = $coll; if (!is_subclass_of($class, TranslationInterface::class)) { throw new \InvalidArgumentException(sprintf('Translation class should implement %s, "%s" given', TranslationInterface::class, $class)); } } /** * @param string $method * @param array $arguments * * @return mixed */ public function __call($method, $arguments) { $matches = []; if (preg_match('/^(set|get)(.*)$/', $method, $matches)) { $property = lcfirst($matches[2]); if (in_array($property, $this->properties, true)) { switch ($matches[1]) { case 'get': return $this->getTranslatedValue($property); case 'set': if (isset($arguments[0])) { $this->setTranslatedValue($property, $arguments[0]); return $this; } } } } $return = call_user_func_array([$this->translatable, $method], $arguments); if ($this->translatable === $return) { return $this; } return $return; } /** * @param string $property * * @return mixed */ public function __get($property) { if (in_array($property, $this->properties, true)) { if (method_exists($this, $getter = 'get'.ucfirst($property))) { return $this->$getter; } return $this->getTranslatedValue($property); } return $this->translatable->$property; } /** * @param string $property * @param mixed $value * * @return self */ public function __set($property, $value) { if (in_array($property, $this->properties, true)) { if (method_exists($this, $setter = 'set'.ucfirst($property))) { return $this->$setter($value); } $this->setTranslatedValue($property, $value); return $this; } $this->translatable->$property = $value; return $this; } /** * @param string $property * * @return bool */ public function __isset($property) { return in_array($property, $this->properties, true); } /** * Returns locale name for the current translation proxy instance. * * @return string */ public function getProxyLocale() { return $this->locale; } /** * Returns translated value for specific property. * * @param string $property property name * * @return mixed */ public function getTranslatedValue($property) { return $this ->findOrCreateTranslationForProperty($property, $this->getProxyLocale()) ->getValue(); } /** * Sets translated value for specific property. * * @param string $property property name * @param string $value value * * @return void */ public function setTranslatedValue($property, $value) { $this ->findOrCreateTranslationForProperty($property, $this->getProxyLocale()) ->setValue($value); } /** * Finds existing or creates new translation for specified property */ private function findOrCreateTranslationForProperty(string $property, string $locale): TranslationInterface { foreach ($this->coll as $translation) { if ($locale === $translation->getLocale() && $property === $translation->getProperty()) { return $translation; } } /** @var TranslationInterface $translation */ $translation = new $this->class(); $translation->setTranslatable($this->translatable); $translation->setProperty($property); $translation->setLocale($locale); $this->coll->add($translation); return $translation; } } doctrine-extensions/src/Tree/Document/MongoDB/Repository/AbstractTreeRepository.php 0000644 00000014421 15117737237 0024673 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Document\MongoDB\Repository; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Query\Builder; use Doctrine\ODM\MongoDB\Query\Query; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; use Doctrine\ODM\MongoDB\UnitOfWork; use Gedmo\Tree\RepositoryInterface; use Gedmo\Tree\RepositoryUtils; use Gedmo\Tree\RepositoryUtilsInterface; use Gedmo\Tree\TreeListener; abstract class AbstractTreeRepository extends DocumentRepository implements RepositoryInterface { /** * Tree listener on event manager * * @var TreeListener */ protected $listener; /** * Repository utils * * @var RepositoryUtilsInterface */ protected $repoUtils; public function __construct(DocumentManager $em, UnitOfWork $uow, ClassMetadata $class) { parent::__construct($em, $uow, $class); $treeListener = null; foreach ($em->getEventManager()->getAllListeners() as $listeners) { foreach ($listeners as $listener) { if ($listener instanceof \Gedmo\Tree\TreeListener) { $treeListener = $listener; break 2; } } } if (null === $treeListener) { throw new \Gedmo\Exception\InvalidMappingException('This repository can be attached only to ODM MongoDB tree listener'); } $this->listener = $treeListener; if (!$this->validate()) { throw new \Gedmo\Exception\InvalidMappingException('This repository cannot be used for tree type: '.$treeListener->getStrategy($em, $class->getName())->getName()); } $this->repoUtils = new RepositoryUtils($this->dm, $this->getClassMetadata(), $this->listener, $this); } /** * Sets the RepositoryUtilsInterface instance * * @return $this */ public function setRepoUtils(RepositoryUtilsInterface $repoUtils) { $this->repoUtils = $repoUtils; return $this; } /** * Returns the RepositoryUtilsInterface instance * * @return \Gedmo\Tree\RepositoryUtilsInterface|null */ public function getRepoUtils() { return $this->repoUtils; } public function childrenHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) { return $this->repoUtils->childrenHierarchy($node, $direct, $options, $includeNode); } public function buildTree(array $nodes, array $options = []) { return $this->repoUtils->buildTree($nodes, $options); } /** * @see \Gedmo\Tree\RepositoryUtilsInterface::setChildrenIndex */ public function setChildrenIndex($childrenIndex) { $this->repoUtils->setChildrenIndex($childrenIndex); } /** * @see \Gedmo\Tree\RepositoryUtilsInterface::getChildrenIndex */ public function getChildrenIndex() { return $this->repoUtils->getChildrenIndex(); } public function buildTreeArray(array $nodes) { return $this->repoUtils->buildTreeArray($nodes); } /** * Get all root nodes query builder * * @param string|null $sortByField Sort by field * @param string $direction Sort direction ("asc" or "desc") * * @return Builder */ abstract public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc'); /** * Get all root nodes query * * @param string|null $sortByField Sort by field * @param string $direction Sort direction ("asc" or "desc") * * @return Query */ abstract public function getRootNodesQuery($sortByField = null, $direction = 'asc'); /** * Returns a QueryBuilder configured to return an array of nodes suitable for buildTree method * * @param object $node Root node * @param bool $direct Obtain direct children? * @param array $options Options * @param bool $includeNode Include node in results? * * @return Builder */ abstract public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false); /** * Returns a Query configured to return an array of nodes suitable for buildTree method * * @param object $node Root node * @param bool $direct Obtain direct children? * @param array $options Options * @param bool $includeNode Include node in results? * * @return Query */ abstract public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false); /** * Get list of children followed by given $node. This returns a QueryBuilder object * * @param object $node if null, all tree nodes will be taken * @param bool $direct true to take only direct children * @param string $sortByField field name to sort by * @param string $direction sort direction : "ASC" or "DESC" * @param bool $includeNode Include the root node in results? * * @return Builder */ abstract public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); /** * Get list of children followed by given $node. This returns a Query * * @param object $node if null, all tree nodes will be taken * @param bool $direct true to take only direct children * @param string $sortByField field name to sort by * @param string $direction sort direction : "ASC" or "DESC" * @param bool $includeNode Include the root node in results? * * @return Query */ abstract public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); /** * Checks if current repository is right * for currently used tree strategy * * @return bool */ abstract protected function validate(); } doctrine-extensions/src/Tree/Document/MongoDB/Repository/MaterializedPathRepository.php 0000644 00000013640 15117737237 0025541 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Document\MongoDB\Repository; use Doctrine\ODM\MongoDB\Iterator\Iterator; use Gedmo\Exception\InvalidArgumentException; use Gedmo\Tool\Wrapper\MongoDocumentWrapper; use Gedmo\Tree\Strategy; use MongoDB\BSON\Regex; /** * The MaterializedPathRepository has some useful functions * to interact with MaterializedPath tree. Repository uses * the strategy used by listener * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class MaterializedPathRepository extends AbstractTreeRepository { /** * Get tree query builder * * @param object|null $rootNode * * @return \Doctrine\ODM\MongoDB\Query\Builder */ public function getTreeQueryBuilder($rootNode = null) { return $this->getChildrenQueryBuilder($rootNode, false, null, 'asc', true); } /** * Get tree query * * @param object|null $rootNode * * @return \Doctrine\ODM\MongoDB\Query\Query */ public function getTreeQuery($rootNode = null) { return $this->getTreeQueryBuilder($rootNode)->getQuery(); } /** * Get tree * * @param object|null $rootNode */ public function getTree($rootNode = null): Iterator { return $this->getTreeQuery($rootNode)->execute(); } public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc') { return $this->getChildrenQueryBuilder(null, true, $sortByField, $direction); } public function getRootNodesQuery($sortByField = null, $direction = 'asc') { return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery(); } public function getRootNodes($sortByField = null, $direction = 'asc') { return $this->getRootNodesQuery($sortByField, $direction)->execute(); } public function childCount($node = null, $direct = false) { $meta = $this->getClassMetadata(); if (is_object($node)) { if (!is_a($node, $meta->getName())) { throw new InvalidArgumentException('Node is not related to this repository'); } $wrapped = new MongoDocumentWrapper($node, $this->dm); if (!$wrapped->hasValidIdentifier()) { throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } } $qb = $this->getChildrenQueryBuilder($node, $direct); $qb->count(); return (int) $qb->getQuery()->execute(); } public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->dm, $meta->getName()); $separator = preg_quote($config['path_separator']); $qb = $this->dm->createQueryBuilder() ->find($meta->getName()); $regex = false; if (is_a($node, $meta->getName())) { $node = new MongoDocumentWrapper($node, $this->dm); $nodePath = preg_quote($node->getPropertyValue($config['path'])); if ($direct) { $regex = sprintf( '^%s([^%s]+%s)'.($includeNode ? '?' : '').'$', $nodePath, $separator, $separator ); } else { $regex = sprintf( '^%s(.+)'.($includeNode ? '?' : ''), $nodePath ); } } elseif ($direct) { $regex = sprintf( '^([^%s]+)'.($includeNode ? '?' : '').'%s$', $separator, $separator ); } if ($regex) { $qb->field($config['path'])->equals(new Regex($regex)); } $qb->sort(null === $sortByField ? $config['path'] : $sortByField, 'asc' === $direction ? 'asc' : 'desc'); return $qb; } /** * G{@inheritdoc} */ public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { return $this->getChildrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery(); } public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { return $this->getChildrenQuery($node, $direct, $sortByField, $direction, $includeNode)->execute(); } public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false) { $sortBy = [ 'field' => null, 'dir' => 'asc', ]; if (isset($options['childSort'])) { $sortBy = array_merge($sortBy, $options['childSort']); } return $this->getChildrenQueryBuilder($node, $direct, $sortBy['field'], $sortBy['dir'], $includeNode); } public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false) { return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery(); } public function getNodesHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) { $query = $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode); $query->setHydrate(false); return $query->toArray(); } protected function validate() { return Strategy::MATERIALIZED_PATH === $this->listener->getStrategy($this->dm, $this->getClassMetadata()->name)->getName(); } } doctrine-extensions/src/Tree/Entity/MappedSuperclass/AbstractClosure.php 0000644 00000004510 15117737237 0022613 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Entity\MappedSuperclass; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; /** * @ORM\MappedSuperclass */ #[ORM\MappedSuperclass] abstract class AbstractClosure { /** * @var int|null * * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") * @ORM\Column(type="integer") */ #[ORM\Column(type: Types::INTEGER)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] protected $id; /** * Mapped by listener * Visibility must be protected * * @var object|null */ protected $ancestor; /** * Mapped by listener * Visibility must be protected * * @var object|null */ protected $descendant; /** * @var int|null * * @ORM\Column(type="integer") */ #[ORM\Column(type: Types::INTEGER)] protected $depth; /** * @return int|null */ public function getId() { return $this->id; } /** * Set ancestor * * @param object $ancestor * * @return static */ public function setAncestor($ancestor) { $this->ancestor = $ancestor; return $this; } /** * Get ancestor * * @return object|null */ public function getAncestor() { return $this->ancestor; } /** * Set descendant * * @param object $descendant * * @return static */ public function setDescendant($descendant) { $this->descendant = $descendant; return $this; } /** * Get descendant * * @return object|null */ public function getDescendant() { return $this->descendant; } /** * Set depth * * @param int $depth * * @return static */ public function setDepth($depth) { $this->depth = $depth; return $this; } /** * Get depth * * @return int|null */ public function getDepth() { return $this->depth; } } doctrine-extensions/src/Tree/Entity/Repository/AbstractTreeRepository.php 0000644 00000020420 15117737237 0023100 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Entity\Repository; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Gedmo\Exception\InvalidArgumentException; use Gedmo\Tool\Wrapper\EntityWrapper; use Gedmo\Tree\RepositoryInterface; use Gedmo\Tree\RepositoryUtils; use Gedmo\Tree\RepositoryUtilsInterface; use Gedmo\Tree\TreeListener; abstract class AbstractTreeRepository extends EntityRepository implements RepositoryInterface { /** * Tree listener on event manager * * @var TreeListener */ protected $listener; /** * Repository utils * * @var RepositoryUtilsInterface */ protected $repoUtils; public function __construct(EntityManagerInterface $em, ClassMetadata $class) { parent::__construct($em, $class); $treeListener = null; foreach ($em->getEventManager()->getAllListeners() as $listeners) { foreach ($listeners as $listener) { if ($listener instanceof TreeListener) { $treeListener = $listener; break 2; } } } if (null === $treeListener) { throw new \Gedmo\Exception\InvalidMappingException('Tree listener was not found on your entity manager, it must be hooked into the event manager'); } $this->listener = $treeListener; if (!$this->validate()) { throw new \Gedmo\Exception\InvalidMappingException('This repository cannot be used for tree type: '.$treeListener->getStrategy($em, $class->getName())->getName()); } $this->repoUtils = new RepositoryUtils($this->_em, $this->getClassMetadata(), $this->listener, $this); } /** * Sets the RepositoryUtilsInterface instance * * @return static */ public function setRepoUtils(RepositoryUtilsInterface $repoUtils) { $this->repoUtils = $repoUtils; return $this; } /** * Returns the RepositoryUtilsInterface instance * * @return \Gedmo\Tree\RepositoryUtilsInterface|null */ public function getRepoUtils() { return $this->repoUtils; } public function childCount($node = null, $direct = false) { $meta = $this->getClassMetadata(); if (is_object($node)) { if (!is_a($node, $meta->getName())) { throw new InvalidArgumentException('Node is not related to this repository'); } $wrapped = new EntityWrapper($node, $this->_em); if (!$wrapped->hasValidIdentifier()) { throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } } $qb = $this->getChildrenQueryBuilder($node, $direct); // We need to remove the ORDER BY DQL part since some vendors could throw an error // in count queries $dqlParts = $qb->getDQLParts(); // We need to check first if there's an ORDER BY DQL part, because resetDQLPart doesn't // check if its internal array has an "orderby" index if (isset($dqlParts['orderBy'])) { $qb->resetDQLPart('orderBy'); } $aliases = $qb->getRootAliases(); $alias = $aliases[0]; $qb->select('COUNT('.$alias.')'); return (int) $qb->getQuery()->getSingleScalarResult(); } /** * @see \Gedmo\Tree\RepositoryUtilsInterface::childrenHierarchy */ public function childrenHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) { return $this->repoUtils->childrenHierarchy($node, $direct, $options, $includeNode); } /** * @see \Gedmo\Tree\RepositoryUtilsInterface::buildTree */ public function buildTree(array $nodes, array $options = []) { return $this->repoUtils->buildTree($nodes, $options); } /** * @see \Gedmo\Tree\RepositoryUtilsInterface::buildTreeArray */ public function buildTreeArray(array $nodes) { return $this->repoUtils->buildTreeArray($nodes); } /** * @see \Gedmo\Tree\RepositoryUtilsInterface::setChildrenIndex */ public function setChildrenIndex($childrenIndex) { $this->repoUtils->setChildrenIndex($childrenIndex); } /** * @see \Gedmo\Tree\RepositoryUtilsInterface::getChildrenIndex */ public function getChildrenIndex() { return $this->repoUtils->getChildrenIndex(); } /** * Get all root nodes query builder * * @param string|null $sortByField Sort by field * @param string $direction Sort direction ("asc" or "desc") * * @return QueryBuilder QueryBuilder object */ abstract public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc'); /** * Get all root nodes query * * @param string|null $sortByField Sort by field * @param string $direction Sort direction ("asc" or "desc") * * @return Query Query object */ abstract public function getRootNodesQuery($sortByField = null, $direction = 'asc'); /** * Returns a QueryBuilder configured to return an array of nodes suitable for buildTree method * * @param object $node Root node * @param bool $direct Obtain direct children? * @param array $options Options * @param bool $includeNode Include node in results? * * @return QueryBuilder QueryBuilder object */ abstract public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false); /** * Returns a Query configured to return an array of nodes suitable for buildTree method * * @param object $node Root node * @param bool $direct Obtain direct children? * @param array $options Options * @param bool $includeNode Include node in results? * * @return Query Query object */ abstract public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false); /** * Get list of children followed by given $node. This returns a QueryBuilder object * * @param object|null $node If null, all tree nodes will be taken * @param bool $direct True to take only direct children * @param string|string[]|null $sortByField Field name or array of fields names to sort by * @param string|string[] $direction Sort order ('ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements * @param bool $includeNode Include the root node in results? * * @return QueryBuilder QueryBuilder object */ abstract public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); /** * Get list of children followed by given $node. This returns a Query * * @param object|null $node If null, all tree nodes will be taken * @param bool $direct True to take only direct children * @param string|string[]|null $sortByField Field name or array of fields names to sort by * @param string|string[] $direction Sort order ('ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements * @param bool $includeNode Include the root node in results? * * @return Query Query object */ abstract public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); /** * @return QueryBuilder */ protected function getQueryBuilder() { return $this->getEntityManager()->createQueryBuilder(); } /** * Checks if current repository is right * for currently used tree strategy * * @return bool */ abstract protected function validate(); } doctrine-extensions/src/Tree/Entity/Repository/ClosureTreeRepository.php 0000644 00000061537 15117737237 0022767 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Entity\Repository; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Gedmo\Exception\InvalidArgumentException; use Gedmo\Tool\Wrapper\EntityWrapper; use Gedmo\Tree\Entity\MappedSuperclass\AbstractClosure; use Gedmo\Tree\Strategy; /** * The ClosureTreeRepository has some useful functions * to interact with Closure tree. Repository uses * the strategy used by listener * * @author Gustavo Adrian <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class ClosureTreeRepository extends AbstractTreeRepository { /** Alias for the level value used in the subquery of the getNodesHierarchy method */ public const SUBQUERY_LEVEL = 'level'; public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc') { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $qb = $this->getQueryBuilder(); $qb->select('node') ->from($config['useObjectClass'], 'node') ->where('node.'.$config['parent'].' IS NULL'); if ($sortByField) { $qb->orderBy('node.'.$sortByField, 'asc' === strtolower($direction) ? 'asc' : 'desc'); } return $qb; } public function getRootNodesQuery($sortByField = null, $direction = 'asc') { return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery(); } public function getRootNodes($sortByField = null, $direction = 'asc') { return $this->getRootNodesQuery($sortByField, $direction)->getResult(); } /** * Get the Tree path query by given $node * * @param object $node * * @throws InvalidArgumentException if input is not valid * * @return Query */ public function getPathQuery($node) { $meta = $this->getClassMetadata(); if (!is_a($node, $meta->getName())) { throw new InvalidArgumentException('Node is not related to this repository'); } if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) { throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $closureMeta = $this->_em->getClassMetadata($config['closure']); $dql = "SELECT c, node FROM {$closureMeta->getName()} c"; $dql .= ' INNER JOIN c.ancestor node'; $dql .= ' WHERE c.descendant = :node'; $dql .= ' ORDER BY c.depth DESC'; $q = $this->_em->createQuery($dql); $q->setParameters(compact('node')); return $q; } /** * Get the Tree path of Nodes by given $node * * @param object $node * * @return array list of Nodes in path */ public function getPath($node) { return array_map(static function (AbstractClosure $closure) { return $closure->getAncestor(); }, $this->getPathQuery($node)->getResult()); } /** * @param object|null $node If null, all tree nodes will be taken * @param bool $direct True to take only direct children * @param string|string[]|null $sortByField Field name or array of fields names to sort by * @param string|string[] $direction Sort order ('ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements * @param bool $includeNode Include the root node in results? * * @return QueryBuilder QueryBuilder object */ public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $qb = $this->getQueryBuilder(); if (null !== $node) { if (is_a($node, $meta->getName())) { if (!$this->_em->getUnitOfWork()->isInIdentityMap($node)) { throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } $where = 'c.ancestor = :node AND '; $qb->select('c, node') ->from($config['closure'], 'c') ->innerJoin('c.descendant', 'node'); if ($direct) { $where .= 'c.depth = 1'; } else { $where .= 'c.descendant <> :node'; } $qb->where($where); if ($includeNode) { $qb->orWhere('c.ancestor = :node AND c.descendant = :node'); } } else { throw new \InvalidArgumentException('Node is not related to this repository'); } } else { $qb->select('node') ->from($config['useObjectClass'], 'node'); if ($direct) { $qb->where('node.'.$config['parent'].' IS NULL'); } } if ($sortByField) { if (is_array($sortByField)) { foreach ($sortByField as $key => $field) { $fieldDirection = is_array($direction) ? ($direction[$key] ?? 'asc') : $direction; if (($meta->hasField($field) || $meta->isSingleValuedAssociation($field)) && in_array(strtolower($fieldDirection), ['asc', 'desc'], true)) { $qb->addOrderBy('node.'.$field, $fieldDirection); } else { throw new InvalidArgumentException(sprintf('Invalid sort options specified: field - %s, direction - %s', $field, $fieldDirection)); } } } else { if (($meta->hasField($sortByField) || $meta->isSingleValuedAssociation($sortByField)) && in_array(strtolower($direction), ['asc', 'desc'], true)) { $qb->orderBy('node.'.$sortByField, $direction); } else { throw new InvalidArgumentException(sprintf('Invalid sort options specified: field - %s, direction - %s', $sortByField, $direction)); } } } if ($node) { $qb->setParameter('node', $node); } return $qb; } /** * @param object|null $node If null, all tree nodes will be taken * @param bool $direct True to take only direct children * @param string|string[]|null $sortByField Field name or array of fields names to sort by * @param string|string[] $direction Sort order ('ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements * @param bool $includeNode Include the root node in results? * * @return Query Query object */ public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery(); } /** * @param object|null $node If null, all tree nodes will be taken * @param bool $direct True to take only direct children * @param string|string[]|null $sortByField Field name or array of fields names to sort by * @param string|string[] $direction Sort order ('ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements * @param bool $includeNode Include the root node in results? * * @return array|null List of children or null on failure */ public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { $result = $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode)->getResult(); if ($node) { $result = array_map(static function (AbstractClosure $closure) { return $closure->getDescendant(); }, $result); } return $result; } public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode); } public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode); } public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { return $this->children($node, $direct, $sortByField, $direction, $includeNode); } /** * Removes given $node from the tree and reparents its descendants * * @todo may be improved, to issue single query on reparenting * * @param object $node * * @throws \Gedmo\Exception\InvalidArgumentException * @throws \Gedmo\Exception\RuntimeException if something fails in transaction * * @return void */ public function removeFromTree($node) { $meta = $this->getClassMetadata(); if (!is_a($node, $meta->getName())) { throw new InvalidArgumentException('Node is not related to this repository'); } $wrapped = new EntityWrapper($node, $this->_em); if (!$wrapped->hasValidIdentifier()) { throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $pk = $meta->getSingleIdentifierFieldName(); $nodeId = $wrapped->getIdentifier(); $parent = $wrapped->getPropertyValue($config['parent']); $dql = "SELECT node FROM {$config['useObjectClass']} node"; $dql .= " WHERE node.{$config['parent']} = :node"; $q = $this->_em->createQuery($dql); $q->setParameters(compact('node')); $nodesToReparent = $q->getResult(); // process updates in transaction $this->_em->getConnection()->beginTransaction(); try { foreach ($nodesToReparent as $nodeToReparent) { $id = $meta->getReflectionProperty($pk)->getValue($nodeToReparent); $meta->getReflectionProperty($config['parent'])->setValue($nodeToReparent, $parent); $dql = "UPDATE {$config['useObjectClass']} node"; $dql .= " SET node.{$config['parent']} = :parent"; $dql .= " WHERE node.{$pk} = :id"; $q = $this->_em->createQuery($dql); $q->setParameters(compact('parent', 'id')); $q->getSingleScalarResult(); $this->listener ->getStrategy($this->_em, $meta->getName()) ->updateNode($this->_em, $nodeToReparent, $node); $oid = spl_object_id($nodeToReparent); $this->_em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['parent'], $parent); } $dql = "DELETE {$config['useObjectClass']} node"; $dql .= " WHERE node.{$pk} = :nodeId"; $q = $this->_em->createQuery($dql); $q->setParameters(compact('nodeId')); $q->getSingleScalarResult(); $this->_em->getConnection()->commit(); } catch (\Exception $e) { $this->_em->close(); $this->_em->getConnection()->rollback(); throw new \Gedmo\Exception\RuntimeException('Transaction failed: '.$e->getMessage(), $e->getCode(), $e); } // remove from identity map $this->_em->getUnitOfWork()->removeFromIdentityMap($node); $node = null; } /** * Process nodes and produce an array with the * structure of the tree * * @param array $nodes Array of nodes * * @return array Array with tree structure */ public function buildTreeArray(array $nodes) { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $nestedTree = []; $idField = $meta->getSingleIdentifierFieldName(); $hasLevelProp = !empty($config['level']); $levelProp = $hasLevelProp ? $config['level'] : self::SUBQUERY_LEVEL; $childrenIndex = $this->repoUtils->getChildrenIndex(); if ([] !== $nodes) { $firstLevel = $hasLevelProp ? $nodes[0][0]['descendant'][$levelProp] : $nodes[0][$levelProp]; $l = 1; // 1 is only an initial value. We could have a tree which has a root node with any level (subtrees) $refs = []; foreach ($nodes as $n) { $node = $n[0]['descendant']; $node[$childrenIndex] = []; $level = $hasLevelProp ? $node[$levelProp] : $n[$levelProp]; if ($l < $level) { $l = $level; } if ($l == $firstLevel) { $tmp = &$nestedTree; } else { $tmp = &$refs[$n['parent_id']][$childrenIndex]; } $key = count($tmp); $tmp[$key] = $node; $refs[$node[$idField]] = &$tmp[$key]; } unset($refs); } return $nestedTree; } public function getNodesHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) { return $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult(); } public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false) { return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery(); } public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false) { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $idField = $meta->getSingleIdentifierFieldName(); $subQuery = ''; $hasLevelProp = isset($config['level']) && $config['level']; if (!$hasLevelProp) { $subQuery = ', (SELECT MAX(c2.depth) + 1 FROM '.$config['closure']; $subQuery .= ' c2 WHERE c2.descendant = c.descendant GROUP BY c2.descendant) AS '.self::SUBQUERY_LEVEL; } $q = $this->_em->createQueryBuilder() ->select('c, node, p.'.$idField.' AS parent_id'.$subQuery) ->from($config['closure'], 'c') ->innerJoin('c.descendant', 'node') ->leftJoin('node.parent', 'p') ->addOrderBy($hasLevelProp ? 'node.'.$config['level'] : self::SUBQUERY_LEVEL, 'asc'); if (null !== $node) { $q->where('c.ancestor = :node'); $q->setParameters(compact('node')); } else { $q->groupBy('c.descendant'); } if (!$includeNode) { $q->andWhere('c.ancestor != c.descendant'); } $defaultOptions = []; $options = array_merge($defaultOptions, $options); if (isset($options['childSort']) && is_array($options['childSort']) && isset($options['childSort']['field'], $options['childSort']['dir'])) { $q->addOrderBy( 'node.'.$options['childSort']['field'], 'asc' === strtolower($options['childSort']['dir']) ? 'asc' : 'desc' ); } return $q; } /** * @return array|bool */ public function verify() { $nodeMeta = $this->getClassMetadata(); $nodeIdField = $nodeMeta->getSingleIdentifierFieldName(); $config = $this->listener->getConfiguration($this->_em, $nodeMeta->getName()); $closureMeta = $this->_em->getClassMetadata($config['closure']); $errors = []; $q = $this->_em->createQuery(" SELECT COUNT(node) FROM {$nodeMeta->getName()} AS node LEFT JOIN {$closureMeta->getName()} AS c WITH c.ancestor = node AND c.depth = 0 WHERE c.id IS NULL "); if ($missingSelfRefsCount = (int) $q->getSingleScalarResult()) { $errors[] = "Missing $missingSelfRefsCount self referencing closures"; } $q = $this->_em->createQuery(" SELECT COUNT(node) FROM {$nodeMeta->getName()} AS node INNER JOIN {$closureMeta->getName()} AS c1 WITH c1.descendant = node.{$config['parent']} LEFT JOIN {$closureMeta->getName()} AS c2 WITH c2.descendant = node.$nodeIdField AND c2.ancestor = c1.ancestor WHERE c2.id IS NULL AND node.$nodeIdField <> c1.ancestor "); if ($missingClosuresCount = (int) $q->getSingleScalarResult()) { $errors[] = "Missing $missingClosuresCount closures"; } $q = $this->_em->createQuery(" SELECT COUNT(c1.id) FROM {$closureMeta->getName()} AS c1 LEFT JOIN {$nodeMeta->getName()} AS node WITH c1.descendant = node.$nodeIdField LEFT JOIN {$closureMeta->getName()} AS c2 WITH c2.descendant = node.{$config['parent']} AND c2.ancestor = c1.ancestor WHERE c2.id IS NULL AND c1.descendant <> c1.ancestor "); if ($invalidClosuresCount = (int) $q->getSingleScalarResult()) { $errors[] = "Found $invalidClosuresCount invalid closures"; } if (!empty($config['level'])) { $levelField = $config['level']; $maxResults = 1000; $q = $this->_em->createQuery(" SELECT node.$nodeIdField AS id, node.$levelField AS node_level, MAX(c.depth) AS closure_level FROM {$nodeMeta->getName()} AS node INNER JOIN {$closureMeta->getName()} AS c WITH c.descendant = node.$nodeIdField GROUP BY node.$nodeIdField, node.$levelField HAVING node.$levelField IS NULL OR node.$levelField <> MAX(c.depth) + 1 ")->setMaxResults($maxResults); if ($invalidLevelsCount = count($q->getScalarResult())) { $errors[] = "Found $invalidLevelsCount invalid level values"; } } return $errors ?: true; } /** * @return void */ public function recover() { if (true === $this->verify()) { return; } $this->cleanUpClosure(); $this->rebuildClosure(); } /** * @return int */ public function rebuildClosure() { $nodeMeta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $nodeMeta->getName()); $closureMeta = $this->_em->getClassMetadata($config['closure']); $insertClosures = function ($entries) use ($closureMeta) { $closureTable = $closureMeta->getTableName(); $ancestorColumnName = $this->getJoinColumnFieldName($closureMeta->getAssociationMapping('ancestor')); $descendantColumnName = $this->getJoinColumnFieldName($closureMeta->getAssociationMapping('descendant')); $depthColumnName = $closureMeta->getColumnName('depth'); $conn = $this->_em->getConnection(); $conn->beginTransaction(); foreach ($entries as $entry) { $conn->insert($closureTable, array_combine( [$ancestorColumnName, $descendantColumnName, $depthColumnName], $entry )); } $conn->commit(); }; $buildClosures = function ($dql) use ($insertClosures) { $newClosuresCount = 0; $batchSize = 1000; $q = $this->_em->createQuery($dql)->setMaxResults($batchSize)->setCacheable(false); do { $entries = $q->getScalarResult(); $insertClosures($entries); $newClosuresCount += count($entries); } while ([] !== $entries); return $newClosuresCount; }; $nodeIdField = $nodeMeta->getSingleIdentifierFieldName(); $newClosuresCount = $buildClosures(" SELECT node.$nodeIdField AS ancestor, node.$nodeIdField AS descendant, 0 AS depth FROM {$nodeMeta->getName()} AS node LEFT JOIN {$closureMeta->getName()} AS c WITH c.ancestor = node AND c.depth = 0 WHERE c.id IS NULL "); $newClosuresCount += $buildClosures(" SELECT IDENTITY(c1.ancestor) AS ancestor, node.$nodeIdField AS descendant, c1.depth + 1 AS depth FROM {$nodeMeta->getName()} AS node INNER JOIN {$closureMeta->getName()} AS c1 WITH c1.descendant = node.{$config['parent']} LEFT JOIN {$closureMeta->getName()} AS c2 WITH c2.descendant = node.$nodeIdField AND c2.ancestor = c1.ancestor WHERE c2.id IS NULL AND node.$nodeIdField <> c1.ancestor "); return $newClosuresCount; } /** * @return int */ public function cleanUpClosure() { $conn = $this->_em->getConnection(); $nodeMeta = $this->getClassMetadata(); $nodeIdField = $nodeMeta->getSingleIdentifierFieldName(); $config = $this->listener->getConfiguration($this->_em, $nodeMeta->getName()); $closureMeta = $this->_em->getClassMetadata($config['closure']); $closureTableName = $closureMeta->getTableName(); $dql = " SELECT c1.id AS id FROM {$closureMeta->getName()} AS c1 LEFT JOIN {$nodeMeta->getName()} AS node WITH c1.descendant = node.$nodeIdField LEFT JOIN {$closureMeta->getName()} AS c2 WITH c2.descendant = node.{$config['parent']} AND c2.ancestor = c1.ancestor WHERE c2.id IS NULL AND c1.descendant <> c1.ancestor "; $deletedClosuresCount = 0; $batchSize = 1000; $q = $this->_em->createQuery($dql)->setMaxResults($batchSize)->setCacheable(false); while (($ids = $q->getScalarResult()) && [] !== $ids) { $ids = array_map(static function (array $el) { return $el['id']; }, $ids); $query = "DELETE FROM {$closureTableName} WHERE id IN (".implode(', ', $ids).')'; if (0 === $conn->executeStatement($query)) { throw new \RuntimeException('Failed to remove incorrect closures'); } $deletedClosuresCount += count($ids); } return $deletedClosuresCount; } /** * @return int */ public function updateLevelValues() { $nodeMeta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $nodeMeta->getName()); $levelUpdatesCount = 0; if (!empty($config['level'])) { $levelField = $config['level']; $nodeIdField = $nodeMeta->getSingleIdentifierFieldName(); $closureMeta = $this->_em->getClassMetadata($config['closure']); $batchSize = 1000; $q = $this->_em->createQuery(" SELECT node.$nodeIdField AS id, node.$levelField AS node_level, MAX(c.depth) AS closure_level FROM {$nodeMeta->getName()} AS node INNER JOIN {$closureMeta->getName()} AS c WITH c.descendant = node.$nodeIdField GROUP BY node.$nodeIdField, node.$levelField HAVING node.$levelField IS NULL OR node.$levelField <> MAX(c.depth) + 1 ")->setMaxResults($batchSize)->setCacheable(false); do { $entries = $q->getScalarResult(); $this->_em->getConnection()->beginTransaction(); foreach ($entries as $entry) { unset($entry['node_level']); $this->_em->createQuery(" UPDATE {$nodeMeta->getName()} AS node SET node.$levelField = (:closure_level + 1) WHERE node.$nodeIdField = :id ")->execute($entry); } $this->_em->getConnection()->commit(); $levelUpdatesCount += count($entries); } while ([] !== $entries); } return $levelUpdatesCount; } protected function validate() { return Strategy::CLOSURE === $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName(); } /** * @param array $association * * @return string|null */ protected function getJoinColumnFieldName($association) { if (count($association['joinColumnFieldNames']) > 1) { throw new \RuntimeException('More association on field '.$association['fieldName']); } return array_shift($association['joinColumnFieldNames']); } } doctrine-extensions/src/Tree/Entity/Repository/MaterializedPathRepository.php 0000644 00000021127 15117737237 0023751 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Entity\Repository; use Gedmo\Tool\Wrapper\EntityWrapper; use Gedmo\Tree\Strategy; /** * The MaterializedPathRepository has some useful functions * to interact with MaterializedPath tree. Repository uses * the strategy used by listener * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class MaterializedPathRepository extends AbstractTreeRepository { /** * Get tree query builder * * @param object $rootNode * * @return \Doctrine\ORM\QueryBuilder */ public function getTreeQueryBuilder($rootNode = null) { return $this->getChildrenQueryBuilder($rootNode, false, null, 'asc', true); } /** * Get tree query * * @param object $rootNode * * @return \Doctrine\ORM\Query */ public function getTreeQuery($rootNode = null) { return $this->getTreeQueryBuilder($rootNode)->getQuery(); } /** * Get tree * * @param object $rootNode * * @return array */ public function getTree($rootNode = null) { return $this->getTreeQuery($rootNode)->execute(); } public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc') { return $this->getChildrenQueryBuilder(null, true, $sortByField, $direction); } public function getRootNodesQuery($sortByField = null, $direction = 'asc') { return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery(); } public function getRootNodes($sortByField = null, $direction = 'asc') { return $this->getRootNodesQuery($sortByField, $direction)->execute(); } /** * Get the Tree path query builder by given $node * * @param object $node * * @return \Doctrine\ORM\QueryBuilder */ public function getPathQueryBuilder($node) { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $alias = 'materialized_path_entity'; $qb = $this->getQueryBuilder() ->select($alias) ->from($config['useObjectClass'], $alias); $node = new EntityWrapper($node, $this->_em); $nodePath = $node->getPropertyValue($config['path']); $paths = []; $nodePathLength = strlen($nodePath); $separatorMatchOffset = 0; while ($separatorMatchOffset < $nodePathLength) { $separatorPos = strpos($nodePath, $config['path_separator'], $separatorMatchOffset); if (false === $separatorPos || $separatorPos === $nodePathLength - 1) { // last node, done $paths[] = $nodePath; $separatorMatchOffset = $nodePathLength; } elseif (0 === $separatorPos) { // path starts with separator, continue $separatorMatchOffset = 1; } else { // add node $paths[] = substr($nodePath, 0, $config['path_ends_with_separator'] ? $separatorPos + 1 : $separatorPos); $separatorMatchOffset = $separatorPos + 1; } } $qb->where($qb->expr()->in( $alias.'.'.$config['path'], $paths )); $qb->orderBy($alias.'.'.$config['level'], 'ASC'); return $qb; } /** * Get the Tree path query by given $node * * @param object $node * * @return \Doctrine\ORM\Query */ public function getPathQuery($node) { return $this->getPathQueryBuilder($node)->getQuery(); } /** * Get the Tree path of Nodes by given $node * * @param object $node * * @return array list of Nodes in path */ public function getPath($node) { return $this->getPathQuery($node)->getResult(); } public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $separator = addcslashes($config['path_separator'], '%'); $alias = 'materialized_path_entity'; $path = $config['path']; $qb = $this->getQueryBuilder() ->select($alias) ->from($config['useObjectClass'], $alias); $expr = ''; $includeNodeExpr = ''; if (is_a($node, $meta->getName())) { $node = new EntityWrapper($node, $this->_em); $nodePath = $node->getPropertyValue($path); $expr = $qb->expr()->andx()->add( $qb->expr()->like( $alias.'.'.$path, $qb->expr()->literal( $nodePath .($config['path_ends_with_separator'] ? '' : $separator).'%' ) ) ); if ($includeNode) { $includeNodeExpr = $qb->expr()->eq($alias.'.'.$path, $qb->expr()->literal($nodePath)); } else { $expr->add($qb->expr()->neq($alias.'.'.$path, $qb->expr()->literal($nodePath))); } if ($direct) { $expr->add( $qb->expr()->orx( $qb->expr()->eq($alias.'.'.$config['level'], $qb->expr()->literal($node->getPropertyValue($config['level']))), $qb->expr()->eq($alias.'.'.$config['level'], $qb->expr()->literal($node->getPropertyValue($config['level']) + 1)) ) ); } } elseif ($direct) { $expr = $qb->expr()->not( $qb->expr()->like($alias.'.'.$path, $qb->expr()->literal( ($config['path_starts_with_separator'] ? $separator : '') .'%'.$separator.'%' .($config['path_ends_with_separator'] ? $separator : '') ) ) ); } if ($expr) { $qb->where('('.$expr.')'); } if ($includeNodeExpr) { $qb->orWhere('('.$includeNodeExpr.')'); } $orderByField = null === $sortByField ? $alias.'.'.$config['path'] : $alias.'.'.$sortByField; $orderByDir = 'asc' === $direction ? 'asc' : 'desc'; $qb->orderBy($orderByField, $orderByDir); return $qb; } public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { return $this->getChildrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery(); } public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'asc', $includeNode = false) { return $this->getChildrenQuery($node, $direct, $sortByField, $direction, $includeNode)->execute(); } public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false) { $sortBy = [ 'field' => null, 'dir' => 'asc', ]; if (isset($options['childSort'])) { $sortBy = array_merge($sortBy, $options['childSort']); } return $this->getChildrenQueryBuilder($node, $direct, $sortBy['field'], $sortBy['dir'], $includeNode); } public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false) { return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery(); } public function getNodesHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $path = $config['path']; $nodes = $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult(); usort( $nodes, static function ($a, $b) use ($path) { return strcmp($a[$path], $b[$path]); } ); return $nodes; } protected function validate() { return Strategy::MATERIALIZED_PATH === $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName(); } } doctrine-extensions/src/Tree/Entity/Repository/NestedTreeRepository.php 0000644 00000130131 15117737237 0022560 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Entity\Repository; use Doctrine\ORM\Proxy\Proxy; use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; use Gedmo\Exception\InvalidArgumentException; use Gedmo\Exception\UnexpectedValueException; use Gedmo\Tool\Wrapper\EntityWrapper; use Gedmo\Tree\Strategy; use Gedmo\Tree\Strategy\ORM\Nested; /** * The NestedTreeRepository has some useful functions * to interact with NestedSet tree. Repository uses * the strategy used by listener * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @method persistAsFirstChild($node) * @method persistAsFirstChildOf($node, $parent) * @method persistAsLastChild($node) * @method persistAsLastChildOf($node, $parent) * @method persistAsNextSibling($node) * @method persistAsNextSiblingOf($node, $sibling) * @method persistAsPrevSibling($node) * @method persistAsPrevSiblingOf($node, $sibling) */ class NestedTreeRepository extends AbstractTreeRepository { /** * Allows the following 'virtual' methods: * - persistAsFirstChild($node) * - persistAsFirstChildOf($node, $parent) * - persistAsLastChild($node) * - persistAsLastChildOf($node, $parent) * - persistAsNextSibling($node) * - persistAsNextSiblingOf($node, $sibling) * - persistAsPrevSibling($node) * - persistAsPrevSiblingOf($node, $sibling) * Inherited virtual methods: * - find* * * @see \Doctrine\ORM\EntityRepository * * @throws InvalidArgumentException If arguments are invalid * @throws \BadMethodCallException If the method called is an invalid find* or persistAs* method * or no find* either persistAs* method at all and therefore an invalid method call * * @return mixed TreeNestedRepository if persistAs* is called */ public function __call($method, $args) { if ('persistAs' === substr($method, 0, 9)) { if (!isset($args[0])) { throw new \Gedmo\Exception\InvalidArgumentException('Node to persist must be available as first argument'); } $node = $args[0]; $wrapped = new EntityWrapper($node, $this->_em); $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $position = substr($method, 9); if ('Of' === substr($method, -2)) { if (!isset($args[1])) { throw new \Gedmo\Exception\InvalidArgumentException('If "Of" is specified you must provide parent or sibling as the second argument'); } $parentOrSibling = $args[1]; if (strstr($method, 'Sibling')) { $wrappedParentOrSibling = new EntityWrapper($parentOrSibling, $this->_em); $newParent = $wrappedParentOrSibling->getPropertyValue($config['parent']); if (null === $newParent && isset($config['root'])) { throw new UnexpectedValueException('Cannot persist sibling for a root node, tree operation is not possible'); } $node->sibling = $parentOrSibling; $parentOrSibling = $newParent; } $wrapped->setPropertyValue($config['parent'], $parentOrSibling); $position = substr($position, 0, -2); } $wrapped->setPropertyValue($config['left'], 0); // simulate changeset $oid = spl_object_id($node); $this->listener ->getStrategy($this->_em, $meta->getName()) ->setNodePosition($oid, $position) ; $this->_em->persist($node); return $this; } return parent::__call($method, $args); } public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc') { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $qb = $this->getQueryBuilder(); $qb ->select('node') ->from($config['useObjectClass'], 'node') ->where($qb->expr()->isNull('node.'.$config['parent'])) ; if (null !== $sortByField) { $qb->orderBy('node.'.$sortByField, 'asc' === strtolower($direction) ? 'asc' : 'desc'); } else { $qb->orderBy('node.'.$config['left'], 'ASC'); } return $qb; } public function getRootNodesQuery($sortByField = null, $direction = 'asc') { return $this->getRootNodesQueryBuilder($sortByField, $direction)->getQuery(); } public function getRootNodes($sortByField = null, $direction = 'asc') { return $this->getRootNodesQuery($sortByField, $direction)->getResult(); } /** * Get the Tree path query builder by given $node * * @param object $node * * @throws InvalidArgumentException if input is not valid * * @return QueryBuilder */ public function getPathQueryBuilder($node) { $meta = $this->getClassMetadata(); if (!is_a($node, $meta->getName())) { throw new InvalidArgumentException('Node is not related to this repository'); } $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $wrapped = new EntityWrapper($node, $this->_em); if (!$wrapped->hasValidIdentifier()) { throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } $left = $wrapped->getPropertyValue($config['left']); $right = $wrapped->getPropertyValue($config['right']); $qb = $this->getQueryBuilder(); $qb->select('node') ->from($config['useObjectClass'], 'node') ->where($qb->expr()->lte('node.'.$config['left'], $left)) ->andWhere($qb->expr()->gte('node.'.$config['right'], $right)) ->orderBy('node.'.$config['left'], 'ASC') ; if (isset($config['root'])) { $rootId = $wrapped->getPropertyValue($config['root']); $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); } return $qb; } /** * Get the Tree path query by given $node * * @param object $node * * @return \Doctrine\ORM\Query */ public function getPathQuery($node) { return $this->getPathQueryBuilder($node)->getQuery(); } /** * Get the Tree path of Nodes by given $node * * @param object $node * * @return array list of Nodes in path */ public function getPath($node) { return $this->getPathQuery($node)->getResult(); } /** * @param object|null $node If null, all tree nodes will be taken * @param bool $direct True to take only direct children * @param string|string[]|null $sortByField Field name or array of fields names to sort by * @param string|string[] $direction Sort order ('ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements * @param bool $includeNode Include the root node in results? * * @return QueryBuilder QueryBuilder object */ public function childrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $qb = $this->getQueryBuilder(); $qb->select('node') ->from($config['useObjectClass'], 'node') ; if (null !== $node) { if (is_a($node, $meta->getName())) { $wrapped = new EntityWrapper($node, $this->_em); if (!$wrapped->hasValidIdentifier()) { throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } if ($direct) { $qb->where($qb->expr()->eq('node.'.$config['parent'], ':pid')); $qb->setParameter('pid', $wrapped->getIdentifier()); } else { $left = $wrapped->getPropertyValue($config['left']); $right = $wrapped->getPropertyValue($config['right']); if ($left && $right) { $qb->where($qb->expr()->lt('node.'.$config['right'], $right)); $qb->andWhere($qb->expr()->gt('node.'.$config['left'], $left)); } } if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $wrapped->getPropertyValue($config['root'])); } if ($includeNode) { $idField = $meta->getSingleIdentifierFieldName(); $qb->where('('.$qb->getDqlPart('where').') OR node.'.$idField.' = :rootNode'); $qb->setParameter('rootNode', $node); } } else { throw new \InvalidArgumentException('Node is not related to this repository'); } } else { if ($direct) { $qb->where($qb->expr()->isNull('node.'.$config['parent'])); } } if (!$sortByField) { $qb->orderBy('node.'.$config['left'], 'ASC'); } elseif (is_array($sortByField)) { foreach ($sortByField as $key => $field) { $fieldDirection = is_array($direction) ? ($direction[$key] ?? 'asc') : $direction; if (($meta->hasField($field) || $meta->isSingleValuedAssociation($field)) && in_array(strtolower($fieldDirection), ['asc', 'desc'], true)) { $qb->addOrderBy('node.'.$field, $fieldDirection); } else { throw new InvalidArgumentException(sprintf('Invalid sort options specified: field - %s, direction - %s', $field, $fieldDirection)); } } } else { if (($meta->hasField($sortByField) || $meta->isSingleValuedAssociation($sortByField)) && in_array(strtolower($direction), ['asc', 'desc'], true)) { $qb->orderBy('node.'.$sortByField, $direction); } else { throw new InvalidArgumentException(sprintf('Invalid sort options specified: field - %s, direction - %s', $sortByField, $direction)); } } return $qb; } /** * @param object|null $node if null, all tree nodes will be taken * @param bool $direct true to take only direct children * @param string|string[]|null $sortByField Field name or array of fields names to sort by * @param string|string[] $direction Sort order ('ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements * @param bool $includeNode Include the root node in results? * * @return Query Query object */ public function childrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode)->getQuery(); } /** * @param object|null $node The object to fetch children for; if null, all nodes will be retrieved * @param bool $direct Flag indicating whether only direct children should be retrieved * @param string|string[]|null $sortByField Field name or array of fields names to sort by * @param string|string[] $direction Sort order ('ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements * @param bool $includeNode Flag indicating whether the given node should be included in the results * * @return array|null List of children or null on failure */ public function children($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { $q = $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode); return $q->getResult(); } /** * @param object|null $node if null, all tree nodes will be taken * @param bool $direct true to take only direct children * @param string|string[]|null $sortByField Field name or array of fields names to sort by * @param string|string[] $direction Sort order ('ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements * @param bool $includeNode Include the root node in results? * * @return QueryBuilder Query object */ public function getChildrenQueryBuilder($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { return $this->childrenQueryBuilder($node, $direct, $sortByField, $direction, $includeNode); } public function getChildrenQuery($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { return $this->childrenQuery($node, $direct, $sortByField, $direction, $includeNode); } public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false) { return $this->children($node, $direct, $sortByField, $direction, $includeNode); } /** * Get tree leafs query builder * * @param object $root root node in case of root tree is required * @param string $sortByField field name to sort by * @param string $direction sort direction : "ASC" or "DESC" * * @throws InvalidArgumentException if input is not valid * * @return QueryBuilder */ public function getLeafsQueryBuilder($root = null, $sortByField = null, $direction = 'ASC') { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); if (isset($config['root']) && null === $root) { throw new InvalidArgumentException('If tree has root, getLeafs method requires any node of this tree'); } $qb = $this->getQueryBuilder(); $qb->select('node') ->from($config['useObjectClass'], 'node') ->where($qb->expr()->eq('node.'.$config['right'], '1 + node.'.$config['left'])) ; if (isset($config['root'])) { if (is_a($root, $meta->getName())) { $wrapped = new EntityWrapper($root, $this->_em); $rootId = $wrapped->getPropertyValue($config['root']); if (!$rootId) { throw new InvalidArgumentException('Root node must be managed'); } $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); } else { throw new InvalidArgumentException('Node is not related to this repository'); } } if (!$sortByField) { if (isset($config['root'])) { $qb->addOrderBy('node.'.$config['root'], 'ASC'); } $qb->addOrderBy('node.'.$config['left'], 'ASC'); } else { if ($meta->hasField($sortByField) && in_array(strtolower($direction), ['asc', 'desc'], true)) { $qb->orderBy('node.'.$sortByField, $direction); } else { throw new InvalidArgumentException("Invalid sort options specified: field - {$sortByField}, direction - {$direction}"); } } return $qb; } /** * Get tree leafs query * * @param object $root root node in case of root tree is required * @param string $sortByField field name to sort by * @param string $direction sort direction : "ASC" or "DESC" * * @return \Doctrine\ORM\Query */ public function getLeafsQuery($root = null, $sortByField = null, $direction = 'ASC') { return $this->getLeafsQueryBuilder($root, $sortByField, $direction)->getQuery(); } /** * Get list of leaf nodes of the tree * * @param object $root root node in case of root tree is required * @param string $sortByField field name to sort by * @param string $direction sort direction : "ASC" or "DESC" * * @return array */ public function getLeafs($root = null, $sortByField = null, $direction = 'ASC') { return $this->getLeafsQuery($root, $sortByField, $direction)->getResult(); } /** * Get the query builder for next siblings of the given $node * * @param object $node * @param bool $includeSelf include the node itself * * @throws \Gedmo\Exception\InvalidArgumentException if input is invalid * * @return QueryBuilder */ public function getNextSiblingsQueryBuilder($node, $includeSelf = false) { $meta = $this->getClassMetadata(); if (!is_a($node, $meta->getName())) { throw new InvalidArgumentException('Node is not related to this repository'); } $wrapped = new EntityWrapper($node, $this->_em); if (!$wrapped->hasValidIdentifier()) { throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $parent = $wrapped->getPropertyValue($config['parent']); $left = $wrapped->getPropertyValue($config['left']); $qb = $this->getQueryBuilder(); $qb->select('node') ->from($config['useObjectClass'], 'node') ->where($includeSelf ? $qb->expr()->gte('node.'.$config['left'], $left) : $qb->expr()->gt('node.'.$config['left'], $left) ) ->orderBy("node.{$config['left']}", 'ASC') ; if ($parent) { $wrappedParent = new EntityWrapper($parent, $this->_em); $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); $qb->setParameter('pid', $wrappedParent->getIdentifier()); } elseif (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':root')); $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); $root = isset($config['rootIdentifierMethod']) ? $node->{$config['rootIdentifierMethod']}() : $wrapped->getPropertyValue($config['root']) ; $qb->setParameter('root', $root); } else { $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); } return $qb; } /** * Get the query for next siblings of the given $node * * @param object $node * @param bool $includeSelf include the node itself * * @return \Doctrine\ORM\Query */ public function getNextSiblingsQuery($node, $includeSelf = false) { return $this->getNextSiblingsQueryBuilder($node, $includeSelf)->getQuery(); } /** * Find the next siblings of the given $node * * @param object $node * @param bool $includeSelf include the node itself * * @return array */ public function getNextSiblings($node, $includeSelf = false) { return $this->getNextSiblingsQuery($node, $includeSelf)->getResult(); } /** * Get query builder for previous siblings of the given $node * * @param object $node * @param bool $includeSelf include the node itself * * @throws \Gedmo\Exception\InvalidArgumentException if input is invalid * * @return QueryBuilder */ public function getPrevSiblingsQueryBuilder($node, $includeSelf = false) { $meta = $this->getClassMetadata(); if (!is_a($node, $meta->getName())) { throw new InvalidArgumentException('Node is not related to this repository'); } $wrapped = new EntityWrapper($node, $this->_em); if (!$wrapped->hasValidIdentifier()) { throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $parent = $wrapped->getPropertyValue($config['parent']); $left = $wrapped->getPropertyValue($config['left']); $qb = $this->getQueryBuilder(); $qb->select('node') ->from($config['useObjectClass'], 'node') ->where($includeSelf ? $qb->expr()->lte('node.'.$config['left'], $left) : $qb->expr()->lt('node.'.$config['left'], $left) ) ->orderBy("node.{$config['left']}", 'ASC') ; if ($parent) { $wrappedParent = new EntityWrapper($parent, $this->_em); $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); $qb->setParameter('pid', $wrappedParent->getIdentifier()); } elseif (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':root')); $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); $method = $config['rootIdentifierMethod']; $qb->setParameter('root', $node->$method()); } else { $qb->andWhere($qb->expr()->isNull('node.'.$config['parent'])); } return $qb; } /** * Get query for previous siblings of the given $node * * @param object $node * @param bool $includeSelf include the node itself * * @throws \Gedmo\Exception\InvalidArgumentException if input is invalid * * @return \Doctrine\ORM\Query */ public function getPrevSiblingsQuery($node, $includeSelf = false) { return $this->getPrevSiblingsQueryBuilder($node, $includeSelf)->getQuery(); } /** * Find the previous siblings of the given $node * * @param object $node * @param bool $includeSelf include the node itself * * @return array */ public function getPrevSiblings($node, $includeSelf = false) { return $this->getPrevSiblingsQuery($node, $includeSelf)->getResult(); } /** * Move the node down in the same level * * @param object $node * @param int|bool $number integer - number of positions to shift * boolean - if "true" - shift till last position * * @throws \RuntimeException if something fails in transaction * * @return bool true if shifted */ public function moveDown($node, $number = 1) { $result = false; $meta = $this->getClassMetadata(); if (is_a($node, $meta->getName())) { $nextSiblings = $this->getNextSiblings($node); if ($numSiblings = count($nextSiblings)) { $result = true; if (true === $number) { $number = $numSiblings; } elseif ($number > $numSiblings) { $number = $numSiblings; } $this->listener ->getStrategy($this->_em, $meta->getName()) ->updateNode($this->_em, $node, $nextSiblings[$number - 1], Nested::NEXT_SIBLING); } } else { throw new InvalidArgumentException('Node is not related to this repository'); } return $result; } /** * Move the node up in the same level * * @param object $node * @param int|bool $number integer - number of positions to shift * boolean - true shift till first position * * @throws \RuntimeException if something fails in transaction * * @return bool true if shifted */ public function moveUp($node, $number = 1) { $result = false; $meta = $this->getClassMetadata(); if (is_a($node, $meta->getName())) { $prevSiblings = array_reverse($this->getPrevSiblings($node)); if ($numSiblings = count($prevSiblings)) { $result = true; if (true === $number) { $number = $numSiblings; } elseif ($number > $numSiblings) { $number = $numSiblings; } $this->listener ->getStrategy($this->_em, $meta->getName()) ->updateNode($this->_em, $node, $prevSiblings[$number - 1], Nested::PREV_SIBLING); } } else { throw new InvalidArgumentException('Node is not related to this repository'); } return $result; } /** * UNSAFE: be sure to backup before running this method when necessary * * Removes given $node from the tree and reparents its descendants * * @param object $node * * @throws \RuntimeException if something fails in transaction * * @return void */ public function removeFromTree($node) { $meta = $this->getClassMetadata(); if (is_a($node, $meta->getName())) { $wrapped = new EntityWrapper($node, $this->_em); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $right = $wrapped->getPropertyValue($config['right']); $left = $wrapped->getPropertyValue($config['left']); $rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null; // if node has no children if ($right == $left + 1) { $this->removeSingle($wrapped); $this->listener ->getStrategy($this->_em, $meta->getName()) ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId); return; // node was a leaf } // process updates in transaction $this->_em->getConnection()->beginTransaction(); try { $parent = $wrapped->getPropertyValue($config['parent']); $parentId = null; if ($parent) { $wrappedParent = new EntityWrapper($parent, $this->_em); $parentId = $wrappedParent->getIdentifier(); } $pk = $meta->getSingleIdentifierFieldName(); $nodeId = $wrapped->getIdentifier(); $shift = -1; // in case if root node is removed, children become roots if (isset($config['root']) && !$parent) { // get node's children $qb = $this->getQueryBuilder(); $qb->select('node.'.$pk, 'node.'.$config['left'], 'node.'.$config['right']) ->from($config['useObjectClass'], 'node'); $qb->andWhere($qb->expr()->eq('node.'.$config['parent'], ':pid')); $qb->setParameter('pid', $nodeId); $nodes = $qb->getQuery()->getArrayResult(); // go through each of the node's children foreach ($nodes as $newRoot) { $left = $newRoot[$config['left']]; $right = $newRoot[$config['right']]; $rootId = $newRoot[$pk]; $shift = -($left - 1); // set the root of this child node and its children to the newly formed tree $qb = $this->getQueryBuilder(); $qb->update($config['useObjectClass'], 'node'); $qb->set('node.'.$config['root'], ':rid'); $qb->setParameter('rid', $rootId); $qb->where($qb->expr()->eq('node.'.$config['root'], ':rpid')); $qb->setParameter('rpid', $nodeId); $qb->andWhere($qb->expr()->gte('node.'.$config['left'], $left)); $qb->andWhere($qb->expr()->lte('node.'.$config['right'], $right)); $qb->getQuery()->getSingleScalarResult(); // Set the parent to NULL for this child node, i.e. make it root $qb = $this->getQueryBuilder(); $qb->update($config['useObjectClass'], 'node'); $qb->set('node.'.$config['parent'], ':pid'); $qb->setParameter('pid', $parentId); $qb->where($qb->expr()->eq('node.'.$config['parent'], ':rpid')); $qb->setParameter('rpid', $nodeId); $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); $qb->getQuery()->getSingleScalarResult(); // fix left, right and level values for the newly formed tree $this->listener ->getStrategy($this->_em, $meta->getName()) ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, -1); $this->listener ->getStrategy($this->_em, $meta->getName()) ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId); } } else { // set parent of all direct children to be the parent of the node being deleted $qb = $this->getQueryBuilder(); $qb->update($config['useObjectClass'], 'node'); $qb->set('node.'.$config['parent'], ':pid'); $qb->setParameter('pid', $parentId); $qb->where($qb->expr()->eq('node.'.$config['parent'], ':rpid')); $qb->setParameter('rpid', $nodeId); if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); } $qb->getQuery()->getSingleScalarResult(); // fix left, right and level values for the node's children $this->listener ->getStrategy($this->_em, $meta->getName()) ->shiftRangeRL($this->_em, $config['useObjectClass'], $left, $right, $shift, $rootId, $rootId, -1); $this->listener ->getStrategy($this->_em, $meta->getName()) ->shiftRL($this->_em, $config['useObjectClass'], $right, -2, $rootId); } $this->removeSingle($wrapped); $this->_em->getConnection()->commit(); } catch (\Exception $e) { $this->_em->close(); $this->_em->getConnection()->rollback(); throw new \Gedmo\Exception\RuntimeException('Transaction failed', $e->getCode(), $e); } } else { throw new InvalidArgumentException('Node is not related to this repository'); } } /** * Reorders $node's child nodes, * according to the $sortByField and $direction specified * * @param object|null $node node from which to start reordering the tree; null will reorder everything * @param string $sortByField field name to sort by * @param string $direction sort direction : "ASC" or "DESC" * @param bool $verify true to verify tree first * @param bool $recursive true to also reorder further descendants, not just the direct children * * @return void */ public function reorder($node, $sortByField = null, $direction = 'ASC', $verify = true, $recursive = true) { $meta = $this->getClassMetadata(); if (null === $node || is_a($node, $meta->getName())) { $config = $this->listener->getConfiguration($this->_em, $meta->getName()); if ($verify && is_array($this->verify())) { return; } $nodes = $this->children($node, true, $sortByField, $direction); foreach ($nodes as $node) { $wrapped = new EntityWrapper($node, $this->_em); $right = $wrapped->getPropertyValue($config['right']); $left = $wrapped->getPropertyValue($config['left']); $this->moveDown($node, true); if ($recursive && $left != ($right - 1)) { $this->reorder($node, $sortByField, $direction, false); } } } else { throw new InvalidArgumentException('Node is not related to this repository'); } } /** * Reorders all nodes in the tree according to the $sortByField and $direction specified. * * @param string $sortByField field name to sort by * @param string $direction sort direction : "ASC" or "DESC" * @param bool $verify true to verify tree first * * @return void */ public function reorderAll($sortByField = null, $direction = 'ASC', $verify = true) { $this->reorder(null, $sortByField, $direction, $verify); } /** * Verifies that current tree is valid. * If any error is detected it will return an array * with a list of errors found on tree * * @return array|bool true on success,error list on failure */ public function verify() { if (!$this->childCount()) { return true; // tree is empty } $errors = []; $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); if (isset($config['root'])) { $trees = $this->getRootNodes(); foreach ($trees as $tree) { $this->verifyTree($errors, $tree); } } else { $this->verifyTree($errors); } return $errors ?: true; } /** * NOTE: flush your entity manager after * * Tries to recover the tree * * @return void */ public function recover() { if (true === $this->verify()) { return; } $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $self = $this; $em = $this->_em; $doRecover = static function ($root, &$count, &$lvl) use ($meta, $config, $self, $em, &$doRecover) { $lft = $count++; foreach ($self->getChildren($root, true) as $child) { $depth = ($lvl + 1); $doRecover($child, $count, $depth); } $rgt = $count++; $meta->getReflectionProperty($config['left'])->setValue($root, $lft); $meta->getReflectionProperty($config['right'])->setValue($root, $rgt); if (isset($config['level'])) { $meta->getReflectionProperty($config['level'])->setValue($root, $lvl); } $em->persist($root); }; if (isset($config['root'])) { foreach ($this->getRootNodes() as $root) { $count = 1; // reset on every root node $lvl = 0; $doRecover($root, $count, $lvl); } } else { $count = 1; $lvl = 0; foreach ($this->getChildren(null, true) as $root) { $doRecover($root, $count, $lvl); } } } public function getNodesHierarchyQueryBuilder($node = null, $direct = false, array $options = [], $includeNode = false) { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); return $this->childrenQueryBuilder( $node, $direct, isset($config['root']) ? [$config['root'], $config['left']] : $config['left'], 'ASC', $includeNode ); } public function getNodesHierarchyQuery($node = null, $direct = false, array $options = [], $includeNode = false) { return $this->getNodesHierarchyQueryBuilder($node, $direct, $options, $includeNode)->getQuery(); } public function getNodesHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) { return $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult(); } protected function validate() { return Strategy::NESTED === $this->listener->getStrategy($this->_em, $this->getClassMetadata()->name)->getName(); } /** * Collect errors on given tree if * where are any */ private function verifyTree(array &$errors, ?object $root = null): void { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $identifier = $meta->getSingleIdentifierFieldName(); if (isset($config['root'])) { $rootId = $meta->getReflectionProperty($config['root'])->getValue($root); if (is_object($rootId)) { $rootId = $meta->getReflectionProperty($identifier)->getValue($rootId); } } else { $rootId = null; } $qb = $this->getQueryBuilder(); $qb->select($qb->expr()->min('node.'.$config['left'])) ->from($config['useObjectClass'], 'node') ; if (isset($config['root'])) { $qb->where($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); } $min = (int) $qb->getQuery()->getSingleScalarResult(); $edge = $this->listener->getStrategy($this->_em, $meta->getName())->max($this->_em, $config['useObjectClass'], $rootId); // check duplicate right and left values for ($i = $min; $i <= $edge; ++$i) { $qb = $this->getQueryBuilder(); $qb->select($qb->expr()->count('node.'.$identifier)) ->from($config['useObjectClass'], 'node') ->where($qb->expr()->orX( $qb->expr()->eq('node.'.$config['left'], $i), $qb->expr()->eq('node.'.$config['right'], $i) )) ; if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); } $count = (int) $qb->getQuery()->getSingleScalarResult(); if (1 !== $count) { if (0 === $count) { $errors[] = "index [{$i}], missing".($root ? ' on tree root: '.$rootId : ''); } else { $errors[] = "index [{$i}], duplicate".($root ? ' on tree root: '.$rootId : ''); } } } // check for missing parents $qb = $this->getQueryBuilder(); $qb->select('node') ->from($config['useObjectClass'], 'node') ->leftJoin('node.'.$config['parent'], 'parent') ->where($qb->expr()->isNotNull('node.'.$config['parent'])) ->andWhere($qb->expr()->isNull('parent.'.$identifier)) ; if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); } $nodes = $qb->getQuery()->getArrayResult(); if ([] !== $nodes) { foreach ($nodes as $node) { $errors[] = "node [{$node[$identifier]}] has missing parent".($root ? ' on tree root: '.$rootId : ''); } return; // loading broken relation can cause infinite loop } $qb = $this->getQueryBuilder(); $qb->select('node') ->from($config['useObjectClass'], 'node') ->where($qb->expr()->lt('node.'.$config['right'], 'node.'.$config['left'])) ; if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); } $result = $qb->getQuery() ->setMaxResults(1) ->getResult(Query::HYDRATE_ARRAY); $node = [] !== $result ? array_shift($result) : []; if ([] !== $node) { $id = $node[$identifier]; $errors[] = "node [{$id}], left is greater than right".($root ? ' on tree root: '.$rootId : ''); } $qb = $this->getQueryBuilder(); $qb->select('node') ->from($config['useObjectClass'], 'node') ; if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); } $nodes = $qb->getQuery()->getResult(Query::HYDRATE_OBJECT); foreach ($nodes as $node) { $right = $meta->getReflectionProperty($config['right'])->getValue($node); $left = $meta->getReflectionProperty($config['left'])->getValue($node); $id = $meta->getReflectionProperty($identifier)->getValue($node); $parent = $meta->getReflectionProperty($config['parent'])->getValue($node); if (!$right || !$left) { $errors[] = "node [{$id}] has invalid left or right values"; } elseif ($right == $left) { $errors[] = "node [{$id}] has identical left and right values"; } elseif ($parent) { if ($parent instanceof Proxy && !$parent->__isInitialized()) { $this->_em->refresh($parent); } $parentRight = $meta->getReflectionProperty($config['right'])->getValue($parent); $parentLeft = $meta->getReflectionProperty($config['left'])->getValue($parent); $parentId = $meta->getReflectionProperty($identifier)->getValue($parent); if ($left < $parentLeft) { $errors[] = "node [{$id}] left is less than parent`s [{$parentId}] left value"; } elseif ($right > $parentRight) { $errors[] = "node [{$id}] right is greater than parent`s [{$parentId}] right value"; } } else { $qb = $this->getQueryBuilder(); $qb->select($qb->expr()->count('node.'.$identifier)) ->from($config['useObjectClass'], 'node') ->where($qb->expr()->lt('node.'.$config['left'], $left)) ->andWhere($qb->expr()->gt('node.'.$config['right'], $right)) ; if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); } if ($count = (int) $qb->getQuery()->getSingleScalarResult()) { $errors[] = "node [{$id}] parent field is blank, but it has a parent"; } } } } /** * Removes single node without touching children * * @internal */ private function removeSingle(EntityWrapper $wrapped): void { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->_em, $meta->getName()); $pk = $meta->getSingleIdentifierFieldName(); $nodeId = $wrapped->getIdentifier(); // prevent from deleting whole branch $qb = $this->getQueryBuilder(); $qb->update($config['useObjectClass'], 'node') ->set('node.'.$config['left'], 0) ->set('node.'.$config['right'], 0); $qb->andWhere($qb->expr()->eq('node.'.$pk, ':id')); $qb->setParameter('id', $nodeId); $qb->getQuery()->getSingleScalarResult(); // remove the node from database $qb = $this->getQueryBuilder(); $qb->delete($config['useObjectClass'], 'node'); $qb->andWhere($qb->expr()->eq('node.'.$pk, ':id')); $qb->setParameter('id', $nodeId); $qb->getQuery()->getSingleScalarResult(); // remove from identity map $this->_em->getUnitOfWork()->removeFromIdentityMap($wrapped->getObject()); } } doctrine-extensions/src/Tree/Hydrator/ORM/TreeObjectHydrator.php 0000644 00000017634 15117737237 0020773 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Hydrator\ORM; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Internal\Hydration\ObjectHydrator; use Doctrine\ORM\PersistentCollection; use Gedmo\Tree\TreeListener; /** * Automatically maps the parent and children properties of Tree nodes * * @author Ilija Tovilo <ilija.tovilo@me.com> * * @final since gedmo/doctrine-extensions 3.11 */ class TreeObjectHydrator extends ObjectHydrator { /** * @var array */ private $config; /** * @var string */ private $idField; /** * @var string */ private $parentField; /** * @var string */ private $childrenField; /** * @param object $object * @param string $property * @param mixed $value * * @return void */ public function setPropertyValue($object, $property, $value) { $meta = $this->_em->getClassMetadata(get_class($object)); $meta->getReflectionProperty($property)->setValue($object, $value); } /** * We hook into the `hydrateAllData` to map the children collection of the entity * * @return mixed[] */ protected function hydrateAllData() { $data = parent::hydrateAllData(); if ([] === $data) { return $data; } $listener = $this->getTreeListener($this->_em); $entityClass = $this->getEntityClassFromHydratedData($data); $this->config = $listener->getConfiguration($this->_em, $entityClass); $this->idField = $this->getIdField($entityClass); $this->parentField = $this->getParentField(); $this->childrenField = $this->getChildrenField($entityClass); $childrenHashmap = $this->buildChildrenHashmap($data); $this->populateChildrenArray($data, $childrenHashmap); // Only return root elements or elements who's parents haven't been fetched // The sub-nodes will be accessible via the `children` property return $this->getRootNodes($data); } /** * Creates a hashmap to quickly find the children of a node * * ``` * [parentId => [child1, child2, ...], ...] * ``` * * @param array $nodes * * @return array */ protected function buildChildrenHashmap($nodes) { $r = []; foreach ($nodes as $node) { $parentProxy = $this->getPropertyValue($node, $this->config['parent']); $parentId = null; if (null !== $parentProxy) { $parentId = $this->getPropertyValue($parentProxy, $this->idField); } $r[$parentId][] = $node; } return $r; } /** * @param array $nodes * @param array $childrenHashmap * * @return void */ protected function populateChildrenArray($nodes, $childrenHashmap) { foreach ($nodes as $node) { $nodeId = $this->getPropertyValue($node, $this->idField); $childrenCollection = $this->getPropertyValue($node, $this->childrenField); if (null === $childrenCollection) { $childrenCollection = new ArrayCollection(); $this->setPropertyValue($node, $this->childrenField, $childrenCollection); } // Initialize all the children collections in order to avoid "SELECT" queries. if ($childrenCollection instanceof PersistentCollection && !$childrenCollection->isInitialized()) { $childrenCollection->setInitialized(true); } if (!isset($childrenHashmap[$nodeId])) { continue; } $childrenCollection->clear(); foreach ($childrenHashmap[$nodeId] as $child) { $childrenCollection->add($child); } } } /** * @param array $nodes * * @return array */ protected function getRootNodes($nodes) { $idHashmap = $this->buildIdHashmap($nodes); $rootNodes = []; foreach ($nodes as $node) { $parentProxy = $this->getPropertyValue($node, $this->config['parent']); $parentId = null; if (null !== $parentProxy) { $parentId = $this->getPropertyValue($parentProxy, $this->idField); } if (null === $parentId || !array_key_exists($parentId, $idHashmap)) { $rootNodes[] = $node; } } return $rootNodes; } /** * Creates a hashmap of all nodes returned in the query * * ``` * [node1.id => true, node2.id => true, ...] * ``` * * @return array */ protected function buildIdHashmap(array $nodes) { $ids = []; foreach ($nodes as $node) { $id = $this->getPropertyValue($node, $this->idField); $ids[$id] = true; } return $ids; } /** * @param string $entityClass * @phpstan-param class-string $entityClass * * @return string */ protected function getIdField($entityClass) { $meta = $this->getClassMetadata($entityClass); return $meta->getSingleIdentifierFieldName(); } /** * @return string */ protected function getParentField() { if (!isset($this->config['parent'])) { throw new \Gedmo\Exception\InvalidMappingException('The `parent` property is required for the TreeHydrator to work'); } return $this->config['parent']; } /** * @param string $entityClass * @phpstan-param class-string $entityClass * * @return string */ protected function getChildrenField($entityClass) { $meta = $this->getClassMetadata($entityClass); foreach ($meta->getReflectionProperties() as $property) { // Skip properties that have no association if (!$meta->hasAssociation($property->getName())) { continue; } $associationMapping = $meta->getAssociationMapping($property->getName()); // Make sure the association is mapped by the parent property if ($associationMapping['mappedBy'] !== $this->parentField) { continue; } return $associationMapping['fieldName']; } throw new \Gedmo\Exception\InvalidMappingException('The children property could not found. It is identified through the `mappedBy` annotation to your parent property.'); } /** * @return TreeListener */ protected function getTreeListener(EntityManagerInterface $em) { foreach ($em->getEventManager()->getAllListeners() as $listeners) { foreach ($listeners as $listener) { if ($listener instanceof TreeListener) { return $listener; } } } throw new \Gedmo\Exception\InvalidMappingException('Tree listener was not found on your entity manager, it must be hooked into the event manager'); } /** * @param array $data * * @return string */ protected function getEntityClassFromHydratedData($data) { $firstMappedEntity = array_values($data); $firstMappedEntity = $firstMappedEntity[0]; return $this->_em->getClassMetadata(get_class($firstMappedEntity))->rootEntityName; } /** * @param object $object * @param string $property * * @return mixed */ protected function getPropertyValue($object, $property) { $meta = $this->_em->getClassMetadata(get_class($object)); return $meta->getReflectionProperty($property)->getValue($object); } } doctrine-extensions/src/Tree/Mapping/Driver/Annotation.php 0000644 00000027254 15117737237 0017736 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ 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; /** * 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 <gediminas.morkevicius@gmail.com> * @author <rocco@roccosportal.com> * * @internal */ class Annotation extends AbstractAnnotationDriver { /** * 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 array */ 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 if ($this->reader->getPropertyAnnotation($property, self::LEVEL)) { $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; } // path if ($pathAnnotation = $this->reader->getPropertyAnnotation($property, self::PATH)) { $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 ($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 ($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 ($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()}"); } } } } doctrine-extensions/src/Tree/Mapping/Driver/Attribute.php 0000644 00000001434 15117737237 0017557 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Mapping\Driver; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for Tree * behavioral extension. Used for extraction of extended * metadata from attributes specifically for Tree * extension. * * @author Kevin Mian Kraiker <kevin.mian@gmail.com> * @license MIT License (http://www.opensource.org/licenses/mit-license.php) * * @internal */ final class Attribute extends Annotation implements AttributeDriverInterface { } doctrine-extensions/src/Tree/Mapping/Driver/Xml.php 0000644 00000034653 15117737237 0016365 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver\Xml as BaseXml; use Gedmo\Tree\Mapping\Validator; /** * This is a xml mapping driver for Tree * behavioral extension. Used for extraction of extended * metadata from xml specifically for Tree * extension. * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Miha Vrhovnik <miha.vrhovnik@gmail.com> * * @internal */ class Xml extends BaseXml { /** * List of tree strategies available * * @var array */ private $strategies = [ 'nested', 'closure', 'materializedPath', ]; public function readExtendedMetadata($meta, array &$config) { /** * @var \SimpleXmlElement */ $xml = $this->_getMapping($meta->getName()); $xmlDoctrine = $xml; $xml = $xml->children(self::GEDMO_NAMESPACE_URI); $validator = new Validator(); if (isset($xml->tree) && $this->_isAttributeSet($xml->tree, 'type')) { $strategy = $this->_getAttribute($xml->tree, 'type'); if (!in_array($strategy, $this->strategies, true)) { throw new InvalidMappingException("Tree type: $strategy is not available."); } $config['strategy'] = $strategy; $config['activate_locking'] = $this->_isAttributeSet($xml->tree, 'activate-locking') && $this->_getBooleanAttribute($xml->tree, 'activate-locking'); if ($lockingTimeout = $this->_getAttribute($xml->tree, 'locking-timeout')) { $config['locking_timeout'] = (int) $lockingTimeout; if ($config['locking_timeout'] < 1) { throw new InvalidMappingException('Tree Locking Timeout must be at least of 1 second.'); } } else { $config['locking_timeout'] = 3; } } if (isset($xml->{'tree-closure'}) && $this->_isAttributeSet($xml->{'tree-closure'}, 'class')) { $class = $this->_getAttribute($xml->{'tree-closure'}, 'class'); if (!$cl = $this->getRelatedClassName($meta, $class)) { throw new InvalidMappingException("Tree closure class: {$class} does not exist."); } $config['closure'] = $cl; } if (isset($xmlDoctrine->field)) { foreach ($xmlDoctrine->field as $mapping) { $mappingDoctrine = $mapping; $mapping = $mapping->children(self::GEDMO_NAMESPACE_URI); $field = $this->_getAttribute($mappingDoctrine, 'name'); if (isset($mapping->{'tree-left'})) { 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; } elseif (isset($mapping->{'tree-right'})) { 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; } elseif (isset($mapping->{'tree-root'})) { if (!$validator->isValidFieldForRoot($meta, $field)) { throw new InvalidMappingException("Tree root field - [{$field}] type is not valid and must be any of the 'integer' types or 'string' in class - {$meta->getName()}"); } $config['root'] = $field; } elseif (isset($mapping->{'tree-level'})) { 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; } elseif (isset($mapping->{'tree-path'})) { 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()}"); } $separator = $this->_getAttribute($mapping->{'tree-path'}, 'separator'); if (strlen($separator) > 1) { throw new InvalidMappingException("Tree Path field - [{$field}] Separator {$separator} is invalid. It must be only one character long."); } $appendId = $this->_isAttributeSet($mapping->{'tree-path'}, 'append_id') ? $this->_getBooleanAttribute($mapping->{'tree-path'}, 'append_id') : null; $startsWithSeparator = $this->_isAttributeSet($mapping->{'tree-path'}, 'starts_with_separator') && $this->_getBooleanAttribute($mapping->{'tree-path'}, 'starts_with_separator'); $endsWithSeparator = !$this->_isAttributeSet($mapping->{'tree-path'}, 'ends_with_separator') || $this->_getBooleanAttribute($mapping->{'tree-path'}, 'ends_with_separator'); $config['path'] = $field; $config['path_separator'] = $separator; $config['path_append_id'] = $appendId; $config['path_starts_with_separator'] = $startsWithSeparator; $config['path_ends_with_separator'] = $endsWithSeparator; } elseif (isset($mapping->{'tree-path-source'})) { 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; } elseif (isset($mapping->{'tree-path-hash'})) { if (!$validator->isValidFieldForPathSource($meta, $field)) { throw new InvalidMappingException("Tree PathHash field - [{$field}] type is not valid and must be 'string' in class - {$meta->getName()}"); } $config['path_hash'] = $field; } elseif (isset($mapping->{'tree-lock-time'})) { if (!$validator->isValidFieldForLockTime($meta, $field)) { throw new InvalidMappingException("Tree LockTime 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 ('mapped-superclass' === $xmlDoctrine->getName()) { if (isset($xmlDoctrine->{'many-to-one'})) { foreach ($xmlDoctrine->{'many-to-one'} as $manyToOneMapping) { /** * @var \SimpleXMLElement */ $manyToOneMappingDoctrine = $manyToOneMapping; $manyToOneMapping = $manyToOneMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($manyToOneMapping->{'tree-parent'})) { $field = $this->_getAttribute($manyToOneMappingDoctrine, 'field'); $targetEntity = $meta->getAssociationTargetClass($field); if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) { throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); } $config['parent'] = $field; } if (isset($manyToOneMapping->{'tree-root'})) { $field = $this->_getAttribute($manyToOneMappingDoctrine, 'field'); $targetEntity = $meta->getAssociationTargetClass($field); if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) { throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->getName()}"); } $config['root'] = $field; } } } elseif (isset($xmlDoctrine->{'reference-one'})) { foreach ($xmlDoctrine->{'reference-one'} as $referenceOneMapping) { /** * @var \SimpleXMLElement */ $referenceOneMappingDoctrine = $referenceOneMapping; $referenceOneMapping = $referenceOneMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($referenceOneMapping->{'tree-parent'})) { $field = $this->_getAttribute($referenceOneMappingDoctrine, 'field'); if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) { throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); } $config['parent'] = $field; } if (isset($referenceOneMapping->{'tree-root'})) { $field = $this->_getAttribute($referenceOneMappingDoctrine, 'field'); if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) { throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->getName()}"); } $config['root'] = $field; } } } } elseif ('entity' === $xmlDoctrine->getName()) { if (isset($xmlDoctrine->{'many-to-one'})) { foreach ($xmlDoctrine->{'many-to-one'} as $manyToOneMapping) { /** * @var \SimpleXMLElement */ $manyToOneMappingDoctrine = $manyToOneMapping; $manyToOneMapping = $manyToOneMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($manyToOneMapping->{'tree-parent'})) { $field = $this->_getAttribute($manyToOneMappingDoctrine, 'field'); $targetEntity = $meta->getAssociationTargetClass($field); if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) { throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); } $config['parent'] = $field; } if (isset($manyToOneMapping->{'tree-root'})) { $field = $this->_getAttribute($manyToOneMappingDoctrine, 'field'); $targetEntity = $meta->getAssociationTargetClass($field); if (!$cl = $this->getRelatedClassName($meta, $targetEntity)) { throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->getName()}"); } $config['root'] = $field; } } } } elseif ('document' === $xmlDoctrine->getName()) { if (isset($xmlDoctrine->{'reference-one'})) { foreach ($xmlDoctrine->{'reference-one'} as $referenceOneMapping) { /** * @var \SimpleXMLElement */ $referenceOneMappingDoctrine = $referenceOneMapping; $referenceOneMapping = $referenceOneMapping->children(self::GEDMO_NAMESPACE_URI); if (isset($referenceOneMapping->{'tree-parent'})) { $field = $this->_getAttribute($referenceOneMappingDoctrine, 'field'); if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) { throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); } $config['parent'] = $field; } if (isset($referenceOneMapping->{'tree-root'})) { $field = $this->_getAttribute($referenceOneMappingDoctrine, 'field'); if (!$cl = $this->getRelatedClassName($meta, $this->_getAttribute($referenceOneMappingDoctrine, 'target-document'))) { throw new InvalidMappingException("Unable to find root descendant relation through root field - [{$field}] in class - {$meta->getName()}"); } $config['root'] = $field; } } } } 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()}"); } } } } doctrine-extensions/src/Tree/Mapping/Driver/Yaml.php 0000644 00000025642 15117737237 0016525 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Mapping\Driver; use Gedmo\Exception\InvalidMappingException; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; use Gedmo\Tree\Mapping\Validator; /** * This is a yaml mapping driver for Tree * behavioral extension. Used for extraction of extended * metadata from yaml specifically for Tree * extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; /** * List of tree strategies available * * @var array */ private $strategies = [ 'nested', 'closure', 'materializedPath', ]; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); $validator = new Validator(); if (isset($mapping['gedmo'])) { $classMapping = $mapping['gedmo']; if (isset($classMapping['tree']['type'])) { $strategy = $classMapping['tree']['type']; if (!in_array($strategy, $this->strategies, true)) { throw new InvalidMappingException("Tree type: $strategy is not available."); } $config['strategy'] = $strategy; $config['activate_locking'] = $classMapping['tree']['activateLocking'] ?? false; $config['locking_timeout'] = isset($classMapping['tree']['lockingTimeout']) ? (int) $classMapping['tree']['lockingTimeout'] : 3; if ($config['locking_timeout'] < 1) { throw new InvalidMappingException('Tree Locking Timeout must be at least of 1 second.'); } } if (isset($classMapping['tree']['closure'])) { if (!$class = $this->getRelatedClassName($meta, $classMapping['tree']['closure'])) { throw new InvalidMappingException("Tree closure class: {$classMapping['tree']['closure']} does not exist."); } $config['closure'] = $class; } } if (isset($mapping['id'])) { foreach ($mapping['id'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { if (in_array('treePathSource', $fieldMapping['gedmo'], true)) { 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; } } } } if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $fieldMapping) { if (isset($fieldMapping['gedmo'])) { if (in_array('treeLeft', $fieldMapping['gedmo'], true)) { 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; } elseif (in_array('treeRight', $fieldMapping['gedmo'], true)) { 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; } elseif (in_array('treeLevel', $fieldMapping['gedmo'], true)) { 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; } elseif (in_array('treeRoot', $fieldMapping['gedmo'], true)) { if (!$validator->isValidFieldForRoot($meta, $field)) { throw new InvalidMappingException("Tree root field - [{$field}] type is not valid and must be any of the 'integer' types or 'string' in class - {$meta->getName()}"); } $config['root'] = $field; } elseif (in_array('treePath', $fieldMapping['gedmo'], true) || isset($fieldMapping['gedmo']['treePath'])) { 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()}"); } $treePathInfo = $fieldMapping['gedmo']['treePath'] ?? $fieldMapping['gedmo'][array_search( 'treePath', $fieldMapping['gedmo'], true )]; if (is_array($treePathInfo) && isset($treePathInfo['separator'])) { $separator = $treePathInfo['separator']; } else { $separator = '|'; } if (strlen($separator) > 1) { throw new InvalidMappingException("Tree Path field - [{$field}] Separator {$separator} is invalid. It must be only one character long."); } if (is_array($treePathInfo) && isset($treePathInfo['appendId'])) { $appendId = $treePathInfo['appendId']; } else { $appendId = null; } if (is_array($treePathInfo) && isset($treePathInfo['startsWithSeparator'])) { $startsWithSeparator = $treePathInfo['startsWithSeparator']; } else { $startsWithSeparator = false; } if (is_array($treePathInfo) && isset($treePathInfo['endsWithSeparator'])) { $endsWithSeparator = $treePathInfo['endsWithSeparator']; } else { $endsWithSeparator = true; } $config['path'] = $field; $config['path_separator'] = $separator; $config['path_append_id'] = $appendId; $config['path_starts_with_separator'] = $startsWithSeparator; $config['path_ends_with_separator'] = $endsWithSeparator; } elseif (in_array('treePathSource', $fieldMapping['gedmo'], true)) { 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; } elseif (in_array('treePathHash', $fieldMapping['gedmo'], true)) { if (!$validator->isValidFieldForPathSource($meta, $field)) { throw new InvalidMappingException("Tree PathHash field - [{$field}] type is not valid and must be 'string' in class - {$meta->getName()}"); } $config['path_hash'] = $field; } elseif (in_array('treeLockTime', $fieldMapping['gedmo'], true)) { if (!$validator->isValidFieldForLocktime($meta, $field)) { throw new InvalidMappingException("Tree LockTime field - [{$field}] type is not valid. It must be \"date\" in class - {$meta->getName()}"); } $config['lock_time'] = $field; } elseif (in_array('treeParent', $fieldMapping['gedmo'], true)) { $config['parent'] = $field; } } } } if (isset($config['activate_locking']) && $config['activate_locking'] && !isset($config['lock_time'])) { throw new InvalidMappingException('You need to map a date|datetime|timestamp field as the tree lock time field to activate locking support.'); } if (isset($mapping['manyToOne'])) { foreach ($mapping['manyToOne'] as $field => $relationMapping) { if (isset($relationMapping['gedmo'])) { if (in_array('treeParent', $relationMapping['gedmo'], true)) { if (!$rel = $this->getRelatedClassName($meta, $relationMapping['targetEntity'])) { throw new InvalidMappingException("Unable to find ancestor/parent child relation through ancestor field - [{$field}] in class - {$meta->getName()}"); } $config['parent'] = $field; } if (in_array('treeRoot', $relationMapping['gedmo'], true)) { if (!$rel = $this->getRelatedClassName($meta, $relationMapping['targetEntity'])) { throw new InvalidMappingException("Unable to find root-descendant relation through root field - [{$field}] in class - {$meta->getName()}"); } $config['root'] = $field; } } } } 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()}"); } } } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } } doctrine-extensions/src/Tree/Mapping/Event/Adapter/ODM.php 0000644 00000001245 15117737237 0017441 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Mapping\Event\Adapter; use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM; use Gedmo\Tree\Mapping\Event\TreeAdapter; /** * Doctrine event adapter for ODM adapted * for Tree behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class ODM extends BaseAdapterODM implements TreeAdapter { // Nothing specific yet } doctrine-extensions/src/Tree/Mapping/Event/Adapter/ORM.php 0000644 00000001245 15117737237 0017457 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Mapping\Event\Adapter; use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM; use Gedmo\Tree\Mapping\Event\TreeAdapter; /** * Doctrine event adapter for ORM adapted * for Tree behavior * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class ORM extends BaseAdapterORM implements TreeAdapter { // Nothing specific yet } doctrine-extensions/src/Tree/Mapping/Event/TreeAdapter.php 0000644 00000001056 15117737237 0017642 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Mapping\Event; use Gedmo\Mapping\Event\AdapterInterface; /** * Doctrine event adapter for the Tree extension. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface TreeAdapter extends AdapterInterface { } doctrine-extensions/src/Tree/Mapping/Validator.php 0000644 00000014450 15117737237 0016310 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Mapping; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; /** * This is a validator for all mapping drivers for Tree * behavioral extension, containing methods to validate * mapping information * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author <rocco@roccosportal.com> * * @final since gedmo/doctrine-extensions 3.11 */ class Validator { /** * List of types which are valid for tree fields * * @var string[] */ private const VALID_TYPES = [ 'integer', 'smallint', 'bigint', 'int', ]; /** * List of types which are valid for the path (materialized path strategy) * * @var string[] */ private $validPathTypes = [ 'string', 'text', ]; /** * List of types which are valid for the path source (materialized path strategy) * * @var string[] */ private $validPathSourceTypes = [ 'id', 'integer', 'smallint', 'bigint', 'string', 'int', 'float', ]; /** * List of types which are valid for the path hash (materialized path strategy) * * @var string[] */ private $validPathHashTypes = [ 'string', ]; /** * List of types which are valid for the path source (materialized path strategy) * * @var string[] */ private $validRootTypes = [ 'integer', 'smallint', 'bigint', 'int', 'string', 'guid', ]; /** * Checks if $field type is valid * * @param ClassMetadata $meta * @param string $field * * @return bool */ public function isValidField($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], self::VALID_TYPES, true); } /** * Checks if $field type is valid for Path field * * @param ClassMetadata $meta * @param string $field * * @return bool */ public function isValidFieldForPath($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], $this->validPathTypes, true); } /** * Checks if $field type is valid for PathSource field * * @param ClassMetadata $meta * @param string $field * * @return bool */ public function isValidFieldForPathSource($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], $this->validPathSourceTypes, true); } /** * Checks if $field type is valid for PathHash field * * @param ClassMetadata $meta * @param string $field * * @return bool */ public function isValidFieldForPathHash($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], $this->validPathHashTypes, true); } /** * Checks if $field type is valid for LockTime field * * @param ClassMetadata $meta * @param string $field * * @return bool */ public function isValidFieldForLockTime($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && ('date' === $mapping['type'] || 'datetime' === $mapping['type'] || 'timestamp' === $mapping['type']); } /** * Checks if $field type is valid for Root field * * @param ClassMetadata $meta * @param string $field * * @return bool */ public function isValidFieldForRoot($meta, $field) { $mapping = $meta->getFieldMapping($field); return $mapping && in_array($mapping['type'], $this->validRootTypes, true); } /** * Validates metadata for nested type tree * * @param ClassMetadata $meta * * @throws InvalidMappingException * * @return void */ public function validateNestedTreeMetadata($meta, array $config) { $missingFields = []; if (!isset($config['parent'])) { $missingFields[] = 'ancestor'; } if (!isset($config['left'])) { $missingFields[] = 'left'; } if (!isset($config['right'])) { $missingFields[] = 'right'; } if ($missingFields) { throw new InvalidMappingException('Missing properties: '.implode(', ', $missingFields)." in class - {$meta->getName()}"); } } /** * Validates metadata for closure type tree * * @param ClassMetadata $meta * * @throws InvalidMappingException * * @return void */ public function validateClosureTreeMetadata($meta, array $config) { $missingFields = []; if (!isset($config['parent'])) { $missingFields[] = 'ancestor'; } if (!isset($config['closure'])) { $missingFields[] = 'closure class'; } if ($missingFields) { throw new InvalidMappingException('Missing properties: '.implode(', ', $missingFields)." in class - {$meta->getName()}"); } } /** * Validates metadata for materialized path type tree * * @param ClassMetadata $meta * * @throws InvalidMappingException * * @return void */ public function validateMaterializedPathTreeMetadata($meta, array $config) { $missingFields = []; if (!isset($config['parent'])) { $missingFields[] = 'ancestor'; } if (!isset($config['path'])) { $missingFields[] = 'path'; } if (!isset($config['path_source'])) { $missingFields[] = 'path_source'; } if ($missingFields) { throw new InvalidMappingException('Missing properties: '.implode(', ', $missingFields)." in class - {$meta->getName()}"); } } } doctrine-extensions/src/Tree/Strategy/ODM/MongoDB/MaterializedPath.php 0000644 00000006762 15117737237 0021734 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Strategy\ODM\MongoDB; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Tool\Wrapper\AbstractWrapper; use Gedmo\Tree\Strategy\AbstractMaterializedPath; use MongoDB\BSON\Regex; use MongoDB\BSON\UTCDateTime; /** * This strategy makes tree using materialized path strategy * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class MaterializedPath extends AbstractMaterializedPath { /** * @param DocumentManager $om */ public function removeNode($om, $meta, $config, $node) { $uow = $om->getUnitOfWork(); $wrapped = AbstractWrapper::wrap($node, $om); // Remove node's children $results = $om->createQueryBuilder() ->find($meta->getName()) ->field($config['path'])->equals(new Regex('^'.preg_quote($wrapped->getPropertyValue($config['path'])).'.?+')) ->getQuery() ->execute(); foreach ($results as $node) { $uow->scheduleForDelete($node); } } /** * @param DocumentManager $om */ public function getChildren($om, $meta, $config, $originalPath) { return $om->createQueryBuilder() ->find($meta->getName()) ->field($config['path'])->equals(new Regex('^'.preg_quote($originalPath).'.+')) ->sort($config['path'], 'asc') // This may save some calls to updateNode ->getQuery() ->execute(); } /** * @param DocumentManager $om */ protected function lockTrees(ObjectManager $om, AdapterInterface $ea) { $uow = $om->getUnitOfWork(); foreach ($this->rootsOfTreesWhichNeedsLocking as $oid => $root) { $meta = $om->getClassMetadata(get_class($root)); $config = $this->listener->getConfiguration($om, $meta->getName()); $lockTimeProp = $meta->getReflectionProperty($config['lock_time']); $lockTimeProp->setAccessible(true); $lockTimeValue = new UTCDateTime(); $lockTimeProp->setValue($root, $lockTimeValue); $changes = [ $config['lock_time'] => [null, $lockTimeValue], ]; $ea->recomputeSingleObjectChangeSet($uow, $meta, $root); } } /** * @param DocumentManager $om */ protected function releaseTreeLocks(ObjectManager $om, AdapterInterface $ea) { $uow = $om->getUnitOfWork(); foreach ($this->rootsOfTreesWhichNeedsLocking as $oid => $root) { $meta = $om->getClassMetadata(get_class($root)); $config = $this->listener->getConfiguration($om, $meta->getName()); $lockTimeProp = $meta->getReflectionProperty($config['lock_time']); $lockTimeProp->setAccessible(true); $lockTimeValue = null; $lockTimeProp->setValue($root, $lockTimeValue); $changes = [ $config['lock_time'] => [null, null], ]; $ea->recomputeSingleObjectChangeSet($uow, $meta, $root); unset($this->rootsOfTreesWhichNeedsLocking[$oid]); } } } doctrine-extensions/src/Tree/Strategy/ORM/Closure.php 0000644 00000050747 15117737237 0016654 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Strategy\ORM; use Doctrine\DBAL\Connection; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata as ORMClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Gedmo\Exception\RuntimeException; use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Tool\Wrapper\AbstractWrapper; use Gedmo\Tree\Strategy; use Gedmo\Tree\TreeListener; use Psr\Cache\CacheItemPoolInterface; /** * This strategy makes tree act like * a closure table. * * @author Gustavo Adrian <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class Closure implements Strategy { /** * TreeListener * * @var TreeListener */ protected $listener; /** * List of pending Nodes, which needs to * be post processed because of having a parent Node * which requires some additional calculations * * @var array */ private $pendingChildNodeInserts = []; /** * List of nodes which has their parents updated, but using * new nodes. They have to wait until their parents are inserted * on DB to make the update * * @var array */ private $pendingNodeUpdates = []; /** * List of pending Nodes, which needs their "level" * field value set * * @var array */ private $pendingNodesLevelProcess = []; public function __construct(TreeListener $listener) { $this->listener = $listener; } public function getName() { return Strategy::CLOSURE; } /** * @param EntityManagerInterface $em */ public function processMetadataLoad($em, $meta) { // TODO: Remove the body of this method in the next major version. $config = $this->listener->getConfiguration($em, $meta->getName()); $closureMetadata = $em->getClassMetadata($config['closure']); $cmf = $em->getMetadataFactory(); $hasTheUserExplicitlyDefinedMapping = true; if (!$closureMetadata->hasAssociation('ancestor')) { @trigger_error(sprintf( 'Not adding mapping explicitly to "ancestor" property in "%s" is deprecated and will not work in' .' version 4.0. You MUST explicitly set the mapping as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table', $closureMetadata->getName() ), E_USER_DEPRECATED); $hasTheUserExplicitlyDefinedMapping = false; // create ancestor mapping $ancestorMapping = [ 'fieldName' => 'ancestor', 'id' => false, 'joinColumns' => [ [ 'name' => 'ancestor', 'referencedColumnName' => 'id', 'unique' => false, 'nullable' => false, 'onDelete' => 'CASCADE', 'onUpdate' => null, 'columnDefinition' => null, ], ], 'inversedBy' => null, 'targetEntity' => $meta->getName(), 'cascade' => null, 'fetch' => ClassMetadataInfo::FETCH_LAZY, ]; $closureMetadata->mapManyToOne($ancestorMapping); $closureMetadata->reflFields['ancestor'] = $cmf ->getReflectionService() ->getAccessibleProperty($closureMetadata->getName(), 'ancestor') ; } if (!$closureMetadata->hasAssociation('descendant')) { @trigger_error(sprintf( 'Not adding mapping explicitly to "descendant" property in "%s" is deprecated and will not work in' .' version 4.0. You MUST explicitly set the mapping as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table', $closureMetadata->getName() ), E_USER_DEPRECATED); $hasTheUserExplicitlyDefinedMapping = false; // create descendant mapping $descendantMapping = [ 'fieldName' => 'descendant', 'id' => false, 'joinColumns' => [ [ 'name' => 'descendant', 'referencedColumnName' => 'id', 'unique' => false, 'nullable' => false, 'onDelete' => 'CASCADE', 'onUpdate' => null, 'columnDefinition' => null, ], ], 'inversedBy' => null, 'targetEntity' => $meta->getName(), 'cascade' => null, 'fetch' => ClassMetadataInfo::FETCH_LAZY, ]; $closureMetadata->mapManyToOne($descendantMapping); $closureMetadata->reflFields['descendant'] = $cmf ->getReflectionService() ->getAccessibleProperty($closureMetadata->getName(), 'descendant') ; } if (!$this->hasClosureTableUniqueConstraint($closureMetadata)) { @trigger_error(sprintf( 'Not adding a unique constraint explicitly to "%s" is deprecated and will not be automatically' .' added in version 4.0. You SHOULD explicitly add the unique constraint as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table', $closureMetadata->getName() ), E_USER_DEPRECATED); $hasTheUserExplicitlyDefinedMapping = false; // create unique index on ancestor and descendant $indexName = substr(strtoupper('IDX_'.md5($closureMetadata->getName())), 0, 20); $closureMetadata->table['uniqueConstraints'][$indexName] = [ 'columns' => [ $this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('ancestor')), $this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('descendant')), ], ]; } if (!$this->hasClosureTableDepthIndex($closureMetadata)) { @trigger_error(sprintf( 'Not adding an index with "depth" column explicitly to "%s" is deprecated and will not be automatically' .' added in version 4.0. You SHOULD explicitly add the index as in our docs: https://github.com/doctrine-extensions/DoctrineExtensions/blob/main/doc/tree.md#closure-table', $closureMetadata->getName() ), E_USER_DEPRECATED); $hasTheUserExplicitlyDefinedMapping = false; // this one may not be very useful $indexName = substr(strtoupper('IDX_'.md5($meta->getName().'depth')), 0, 20); $closureMetadata->table['indexes'][$indexName] = [ 'columns' => ['depth'], ]; } if (!$hasTheUserExplicitlyDefinedMapping) { $metadataFactory = $em->getMetadataFactory(); $getCache = \Closure::bind(static function (AbstractClassMetadataFactory $metadataFactory): ?CacheItemPoolInterface { return $metadataFactory->getCache(); }, null, \get_class($metadataFactory)); $metadataCache = $getCache($metadataFactory); if (null !== $metadataCache) { // @see https://github.com/doctrine/persistence/pull/144 // @see \Doctrine\Persistence\Mapping\AbstractClassMetadataFactory::getCacheKey() $cacheKey = str_replace('\\', '__', $closureMetadata->getName()).'__CLASSMETADATA__'; $item = $metadataCache->getItem($cacheKey); $metadataCache->save($item->set($closureMetadata)); } } } public function onFlushEnd($em, AdapterInterface $ea) { } public function processPrePersist($em, $node) { $this->pendingChildNodeInserts[spl_object_id($em)][spl_object_id($node)] = $node; } public function processPreUpdate($em, $node) { } public function processPreRemove($em, $node) { } public function processScheduledInsertion($em, $node, AdapterInterface $ea) { } public function processScheduledDelete($em, $entity) { } public function processPostUpdate($em, $entity, AdapterInterface $ea) { \assert($em instanceof EntityManagerInterface); $meta = $em->getClassMetadata(get_class($entity)); $config = $this->listener->getConfiguration($em, $meta->getName()); // Process TreeLevel field value if (!empty($config)) { $this->setLevelFieldOnPendingNodes($em); } } public function processPostRemove($em, $entity, AdapterInterface $ea) { } /** * @param EntityManagerInterface $em */ public function processPostPersist($em, $entity, AdapterInterface $ea) { $uow = $em->getUnitOfWork(); $emHash = spl_object_id($em); while ($node = array_shift($this->pendingChildNodeInserts[$emHash])) { $meta = $em->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($em, $meta->getName()); $identifier = $meta->getSingleIdentifierFieldName(); $nodeId = $meta->getReflectionProperty($identifier)->getValue($node); $parent = $meta->getReflectionProperty($config['parent'])->getValue($node); $closureClass = $config['closure']; $closureMeta = $em->getClassMetadata($closureClass); $closureTable = $closureMeta->getTableName(); $ancestorColumnName = $this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('ancestor')); $descendantColumnName = $this->getJoinColumnFieldName($em->getClassMetadata($config['closure'])->getAssociationMapping('descendant')); $depthColumnName = $em->getClassMetadata($config['closure'])->getColumnName('depth'); $entries = [ [ $ancestorColumnName => $nodeId, $descendantColumnName => $nodeId, $depthColumnName => 0, ], ]; if ($parent) { $dql = "SELECT c, a FROM {$closureMeta->getName()} c"; $dql .= ' JOIN c.ancestor a'; $dql .= ' WHERE c.descendant = :parent'; $q = $em->createQuery($dql); $q->setParameters(compact('parent')); $ancestors = $q->getArrayResult(); if ([] === $ancestors) { // The parent has been persisted after the child, postpone the evaluation $this->pendingChildNodeInserts[$emHash][] = $node; continue; } foreach ($ancestors as $ancestor) { $entries[] = [ $ancestorColumnName => $ancestor['ancestor'][$identifier], $descendantColumnName => $nodeId, $depthColumnName => $ancestor['depth'] + 1, ]; } if (isset($config['level'])) { $this->pendingNodesLevelProcess[$nodeId] = $node; } } elseif (isset($config['level'])) { $uow->scheduleExtraUpdate($node, [$config['level'] => [null, 1]]); $ea->setOriginalObjectProperty($uow, $node, $config['level'], 1); $levelProp = $meta->getReflectionProperty($config['level']); $levelProp->setValue($node, 1); } foreach ($entries as $closure) { if (!$em->getConnection()->insert($closureTable, $closure)) { throw new RuntimeException('Failed to insert new Closure record'); } } } // Process pending node updates if (!empty($this->pendingNodeUpdates)) { foreach ($this->pendingNodeUpdates as $info) { $this->updateNode($em, $info['node'], $info['oldParent']); } $this->pendingNodeUpdates = []; } // Process TreeLevel field value $this->setLevelFieldOnPendingNodes($em); } /** * @param EntityManagerInterface $em */ public function processScheduledUpdate($em, $node, AdapterInterface $ea) { $meta = $em->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($em, $meta->getName()); $uow = $em->getUnitOfWork(); $changeSet = $uow->getEntityChangeSet($node); if (array_key_exists($config['parent'], $changeSet)) { // If new parent is new, we need to delay the update of the node // until it is inserted on DB $parent = $changeSet[$config['parent']][1] ? AbstractWrapper::wrap($changeSet[$config['parent']][1], $em) : null; if ($parent && !$parent->getIdentifier()) { $this->pendingNodeUpdates[spl_object_id($node)] = [ 'node' => $node, 'oldParent' => $changeSet[$config['parent']][0], ]; } else { $this->updateNode($em, $node, $changeSet[$config['parent']][0]); } } } /** * Update node and closures * * @param object $node * @param object $oldParent * * @return void */ public function updateNode(EntityManagerInterface $em, $node, $oldParent) { $wrapped = AbstractWrapper::wrap($node, $em); $meta = $wrapped->getMetadata(); $config = $this->listener->getConfiguration($em, $meta->getName()); $closureMeta = $em->getClassMetadata($config['closure']); $nodeId = $wrapped->getIdentifier(); $parent = $wrapped->getPropertyValue($config['parent']); $table = $closureMeta->getTableName(); $conn = $em->getConnection(); // ensure integrity if ($parent) { $dql = "SELECT COUNT(c) FROM {$closureMeta->getName()} c"; $dql .= ' WHERE c.ancestor = :node'; $dql .= ' AND c.descendant = :parent'; $q = $em->createQuery($dql); $q->setParameters(compact('node', 'parent')); if ($q->getSingleScalarResult()) { throw new \Gedmo\Exception\UnexpectedValueException("Cannot set child as parent to node: {$nodeId}"); } } if ($oldParent) { $subQuery = "SELECT c2.id FROM {$table} c1"; $subQuery .= " JOIN {$table} c2 ON c1.descendant = c2.descendant"; $subQuery .= ' WHERE c1.ancestor = :nodeId AND c2.depth > c1.depth'; $ids = $conn->executeQuery($subQuery, compact('nodeId'))->fetchFirstColumn(); if ([] !== $ids) { // using subquery directly, sqlite acts unfriendly $query = "DELETE FROM {$table} WHERE id IN (".implode(', ', $ids).')'; if (0 === $conn->executeStatement($query)) { throw new RuntimeException('Failed to remove old closures'); } } } if ($parent) { $wrappedParent = AbstractWrapper::wrap($parent, $em); $parentId = $wrappedParent->getIdentifier(); $query = 'SELECT c1.ancestor, c2.descendant, (c1.depth + c2.depth + 1) AS depth'; $query .= " FROM {$table} c1, {$table} c2"; $query .= ' WHERE c1.descendant = :parentId'; $query .= ' AND c2.ancestor = :nodeId'; $closures = $conn->executeQuery($query, compact('nodeId', 'parentId'))->fetchAllAssociative(); foreach ($closures as $closure) { if (!$conn->insert($table, $closure)) { throw new RuntimeException('Failed to insert new Closure record'); } } } if (isset($config['level'])) { $this->pendingNodesLevelProcess[$nodeId] = $node; } } /** * @param array $association * * @return string|null */ protected function getJoinColumnFieldName($association) { if (count($association['joinColumnFieldNames']) > 1) { throw new RuntimeException('More association on field '.$association['fieldName']); } return array_shift($association['joinColumnFieldNames']); } /** * Process pending entities to set their "level" value * * @param EntityManagerInterface $em * * @return void */ protected function setLevelFieldOnPendingNodes(ObjectManager $em) { if (!empty($this->pendingNodesLevelProcess)) { $first = array_slice($this->pendingNodesLevelProcess, 0, 1); $first = array_shift($first); $meta = $em->getClassMetadata(get_class($first)); unset($first); $identifier = $meta->getIdentifier(); $mapping = $meta->getFieldMapping($identifier[0]); $config = $this->listener->getConfiguration($em, $meta->getName()); $closureClass = $config['closure']; $closureMeta = $em->getClassMetadata($closureClass); $uow = $em->getUnitOfWork(); foreach ($this->pendingNodesLevelProcess as $node) { $children = $em->getRepository($meta->getName())->children($node); foreach ($children as $child) { $this->pendingNodesLevelProcess[AbstractWrapper::wrap($child, $em)->getIdentifier()] = $child; } } // Avoid type conversion performance penalty $type = 'integer' === $mapping['type'] ? Connection::PARAM_INT_ARRAY : Connection::PARAM_STR_ARRAY; // We calculate levels for all nodes $sql = 'SELECT c.descendant, MAX(c.depth) + 1 AS levelNum '; $sql .= 'FROM '.$closureMeta->getTableName().' c '; $sql .= 'WHERE c.descendant IN (?) '; $sql .= 'GROUP BY c.descendant'; $levelsAssoc = $em->getConnection()->executeQuery($sql, [array_keys($this->pendingNodesLevelProcess)], [$type])->fetchAllNumeric(); // create key pair array with resultset $levels = []; foreach ($levelsAssoc as $level) { $levels[$level[0]] = $level[1]; } $levelsAssoc = null; // Now we update levels foreach ($this->pendingNodesLevelProcess as $nodeId => $node) { // Update new level $level = $levels[$nodeId]; $levelProp = $meta->getReflectionProperty($config['level']); $uow->scheduleExtraUpdate( $node, [$config['level'] => [ $levelProp->getValue($node), $level, ]] ); $levelProp->setValue($node, $level); $uow->setOriginalEntityProperty(spl_object_id($node), $config['level'], $level); } $this->pendingNodesLevelProcess = []; } } /** * @param ORMClassMetadata $closureMetadata */ private function hasClosureTableUniqueConstraint(ClassMetadata $closureMetadata): bool { if (!isset($closureMetadata->table['uniqueConstraints'])) { return false; } foreach ($closureMetadata->table['uniqueConstraints'] as $uniqueConstraint) { if ([] === array_diff(['ancestor', 'descendant'], $uniqueConstraint['columns'])) { return true; } } return false; } /** * @param ORMClassMetadata $closureMetadata */ private function hasClosureTableDepthIndex(ClassMetadata $closureMetadata): bool { if (!isset($closureMetadata->table['indexes'])) { return false; } foreach ($closureMetadata->table['indexes'] as $uniqueConstraint) { if ([] === array_diff(['depth'], $uniqueConstraint['columns'])) { return true; } } return false; } } doctrine-extensions/src/Tree/Strategy/ORM/MaterializedPath.php 0000644 00000004705 15117737237 0020460 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Strategy\ORM; use Doctrine\ORM\EntityManagerInterface; use Gedmo\Tool\Wrapper\AbstractWrapper; use Gedmo\Tree\Strategy\AbstractMaterializedPath; /** * This strategy makes tree using materialized path strategy * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class MaterializedPath extends AbstractMaterializedPath { /** * @param EntityManagerInterface $om */ public function removeNode($om, $meta, $config, $node) { $uow = $om->getUnitOfWork(); $wrapped = AbstractWrapper::wrap($node, $om); $path = addcslashes($wrapped->getPropertyValue($config['path']), '%'); $separator = $config['path_ends_with_separator'] ? null : $config['path_separator']; // Remove node's children $qb = $om->createQueryBuilder(); $qb->select('e') ->from($config['useObjectClass'], 'e') ->where($qb->expr()->like('e.'.$config['path'], $qb->expr()->literal($path.$separator.'%'))); if (isset($config['level'])) { $lvlField = $config['level']; $lvl = $wrapped->getPropertyValue($lvlField); if (!empty($lvl)) { $qb->andWhere($qb->expr()->gt('e.'.$lvlField, $qb->expr()->literal($lvl))); } } $results = $qb->getQuery() ->execute(); foreach ($results as $node) { $uow->scheduleForDelete($node); } } /** * @param EntityManagerInterface $om */ public function getChildren($om, $meta, $config, $path) { $path = addcslashes($path, '%'); $qb = $om->createQueryBuilder(); $qb->select('e') ->from($config['useObjectClass'], 'e') ->where($qb->expr()->like('e.'.$config['path'], $qb->expr()->literal($path.'%'))) ->andWhere('e.'.$config['path'].' != :path') ->orderBy('e.'.$config['path'], 'asc'); // This may save some calls to updateNode $qb->setParameter('path', $path); return $qb->getQuery() ->execute(); } } doctrine-extensions/src/Tree/Strategy/ORM/Nested.php 0000644 00000072073 15117737237 0016456 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Strategy\ORM; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Proxy\Proxy; use Gedmo\Exception\UnexpectedValueException; use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Tool\Wrapper\AbstractWrapper; use Gedmo\Tree\Node; use Gedmo\Tree\Strategy; use Gedmo\Tree\TreeListener; /** * This strategy makes the tree act like a nested set. * * This behavior can impact the performance of your application * since nested set trees are slow on inserts and updates. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class Nested implements Strategy { /** * Previous sibling position */ public const PREV_SIBLING = 'PrevSibling'; /** * Next sibling position */ public const NEXT_SIBLING = 'NextSibling'; /** * First child position */ public const FIRST_CHILD = 'FirstChild'; /** * Last child position */ public const LAST_CHILD = 'LastChild'; public const ALLOWED_NODE_POSITIONS = [ self::PREV_SIBLING, self::NEXT_SIBLING, self::FIRST_CHILD, self::LAST_CHILD, ]; /** * TreeListener * * @var TreeListener */ protected $listener; /** * The max number of "right" field of the * tree in case few root nodes will be persisted * on one flush for node classes * * @var array<string, int> */ private $treeEdges = []; /** * Stores a list of node position strategies * for each node by object id * * @var array<int, string> * * @phpstan-var array<int, value-of<self::ALLOWED_NODE_POSITIONS>> */ private $nodePositions = []; /** * Stores a list of delayed nodes for correct order of updates * * @var array<int, array<int, array<string, Node|object|string>>> * * @phpstan-var array<int, array<int, array{node: Node|object, position: value-of<self::ALLOWED_NODE_POSITIONS>}>> */ private $delayedNodes = []; public function __construct(TreeListener $listener) { $this->listener = $listener; } public function getName() { return Strategy::NESTED; } /** * Set node position strategy * * @param int $oid * @param string $position * * @return void */ public function setNodePosition($oid, $position) { if (!in_array($position, self::ALLOWED_NODE_POSITIONS, true)) { throw new \Gedmo\Exception\InvalidArgumentException("Position: {$position} is not valid in nested set tree"); } $this->nodePositions[$oid] = $position; } public function processScheduledInsertion($em, $node, AdapterInterface $ea) { /** @var ClassMetadata $meta */ $meta = $em->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($em, $meta->getName()); $meta->getReflectionProperty($config['left'])->setValue($node, 0); $meta->getReflectionProperty($config['right'])->setValue($node, 0); if (isset($config['level'])) { $meta->getReflectionProperty($config['level'])->setValue($node, 0); } if (isset($config['root']) && !$meta->hasAssociation($config['root']) && !isset($config['rootIdentifierMethod'])) { $meta->getReflectionProperty($config['root'])->setValue($node, 0); } elseif (isset($config['rootIdentifierMethod']) && null === $meta->getReflectionProperty($config['root'])->getValue($node)) { $meta->getReflectionProperty($config['root'])->setValue($node, 0); } } /** * @param EntityManagerInterface $em */ public function processScheduledUpdate($em, $node, AdapterInterface $ea) { $meta = $em->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($em, $meta->getName()); $uow = $em->getUnitOfWork(); $changeSet = $uow->getEntityChangeSet($node); if (isset($config['root'], $changeSet[$config['root']])) { throw new \Gedmo\Exception\UnexpectedValueException('Root cannot be changed manually, change parent instead'); } $oid = spl_object_id($node); if (isset($changeSet[$config['left']], $this->nodePositions[$oid])) { $wrapped = AbstractWrapper::wrap($node, $em); $parent = $wrapped->getPropertyValue($config['parent']); // revert simulated changeset $uow->clearEntityChangeSet($oid); $wrapped->setPropertyValue($config['left'], $changeSet[$config['left']][0]); $uow->setOriginalEntityProperty($oid, $config['left'], $changeSet[$config['left']][0]); // set back all other changes foreach ($changeSet as $field => $set) { if ($field !== $config['left']) { if (is_array($set) && array_key_exists(0, $set) && array_key_exists(1, $set)) { $uow->setOriginalEntityProperty($oid, $field, $set[0]); $wrapped->setPropertyValue($field, $set[1]); } else { $uow->setOriginalEntityProperty($oid, $field, $set); $wrapped->setPropertyValue($field, $set); } } } $uow->recomputeSingleEntityChangeSet($meta, $node); $this->updateNode($em, $node, $parent); } elseif (isset($changeSet[$config['parent']])) { $this->updateNode($em, $node, $changeSet[$config['parent']][1]); } } /** * @param EntityManagerInterface $em */ public function processPostPersist($em, $node, AdapterInterface $ea) { $meta = $em->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($em, $meta->getName()); $parent = $meta->getReflectionProperty($config['parent'])->getValue($node); $this->updateNode($em, $node, $parent, self::LAST_CHILD); } /** * @param EntityManagerInterface $em */ public function processScheduledDelete($em, $node) { $meta = $em->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($em, $meta->getName()); $uow = $em->getUnitOfWork(); $wrapped = AbstractWrapper::wrap($node, $em); $leftValue = $wrapped->getPropertyValue($config['left']); $rightValue = $wrapped->getPropertyValue($config['right']); if (!$leftValue || !$rightValue) { return; } $rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null; $diff = $rightValue - $leftValue + 1; if ($diff > 2) { $qb = $em->createQueryBuilder(); $qb->select('node') ->from($config['useObjectClass'], 'node') ->where($qb->expr()->between('node.'.$config['left'], '?1', '?2')) ->setParameters([1 => $leftValue, 2 => $rightValue]); if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); } $q = $qb->getQuery(); // get nodes for deletion $nodes = $q->getResult(); foreach ((array) $nodes as $removalNode) { $uow->scheduleForDelete($removalNode); } } $this->shiftRL($em, $config['useObjectClass'], $rightValue + 1, -$diff, $rootId); } public function onFlushEnd($em, AdapterInterface $ea) { // reset values $this->treeEdges = []; } public function processPreRemove($em, $node) { } public function processPrePersist($em, $node) { } public function processPreUpdate($em, $node) { } public function processMetadataLoad($em, $meta) { } public function processPostUpdate($em, $entity, AdapterInterface $ea) { } public function processPostRemove($em, $entity, AdapterInterface $ea) { } /** * Update the $node with a different $parent destination * * @param Node|object $node target node * @param Node|object $parent destination node * @param string $position * * @phpstan-param value-of<self::ALLOWED_NODE_POSITIONS> $position * * @throws \Gedmo\Exception\UnexpectedValueException * * @return void */ public function updateNode(EntityManagerInterface $em, $node, $parent, $position = self::FIRST_CHILD) { $wrapped = AbstractWrapper::wrap($node, $em); /** @var ClassMetadata $meta */ $meta = $wrapped->getMetadata(); $config = $this->listener->getConfiguration($em, $meta->getName()); $root = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null; $identifierField = $meta->getSingleIdentifierFieldName(); $nodeId = $wrapped->getIdentifier(); $left = $wrapped->getPropertyValue($config['left']); $right = $wrapped->getPropertyValue($config['right']); $isNewNode = empty($left) && empty($right); if ($isNewNode) { $left = 1; $right = 2; } $oid = spl_object_id($node); if (isset($this->nodePositions[$oid])) { $position = $this->nodePositions[$oid]; } $level = 0; $treeSize = $right - $left + 1; $newRoot = null; if ($parent) { // || (!$parent && isset($config['rootIdentifierMethod'])) $wrappedParent = AbstractWrapper::wrap($parent, $em); $parentRoot = isset($config['root']) ? $wrappedParent->getPropertyValue($config['root']) : null; $parentOid = spl_object_id($parent); $parentLeft = $wrappedParent->getPropertyValue($config['left']); $parentRight = $wrappedParent->getPropertyValue($config['right']); if (empty($parentLeft) && empty($parentRight)) { // parent node is a new node, but wasn't processed yet (due to Doctrine commit order calculator redordering) // We delay processing of node to the moment parent node will be processed if (!isset($this->delayedNodes[$parentOid])) { $this->delayedNodes[$parentOid] = []; } $this->delayedNodes[$parentOid][] = ['node' => $node, 'position' => $position]; return; } if (!$isNewNode && $root === $parentRoot && $parentLeft >= $left && $parentRight <= $right) { throw new UnexpectedValueException("Cannot set child as parent to node: {$nodeId}"); } if (isset($config['level'])) { $level = $wrappedParent->getPropertyValue($config['level']); } switch ($position) { case self::PREV_SIBLING: if (property_exists($node, 'sibling')) { $wrappedSibling = AbstractWrapper::wrap($node->sibling, $em); $start = $wrappedSibling->getPropertyValue($config['left']); ++$level; } else { $newParent = $wrappedParent->getPropertyValue($config['parent']); if (null === $newParent && ((isset($config['root']) && $config['root'] == $config['parent']) || $isNewNode)) { throw new UnexpectedValueException('Cannot persist sibling for a root node, tree operation is not possible'); } if (null === $newParent && (isset($config['root']) || $isNewNode)) { // root is a different column from parent (pointing to another table?), do nothing } else { $wrapped->setPropertyValue($config['parent'], $newParent); } $em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node); $start = $parentLeft; } break; case self::NEXT_SIBLING: if (property_exists($node, 'sibling')) { $wrappedSibling = AbstractWrapper::wrap($node->sibling, $em); $start = $wrappedSibling->getPropertyValue($config['right']) + 1; ++$level; } else { $newParent = $wrappedParent->getPropertyValue($config['parent']); if (null === $newParent && ((isset($config['root']) && $config['root'] == $config['parent']) || $isNewNode)) { throw new UnexpectedValueException('Cannot persist sibling for a root node, tree operation is not possible'); } if (null === $newParent && (isset($config['root']) || $isNewNode)) { // root is a different column from parent (pointing to another table?), do nothing } else { $wrapped->setPropertyValue($config['parent'], $newParent); } $em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node); $start = $parentRight + 1; } break; case self::LAST_CHILD: $start = $parentRight; ++$level; break; case self::FIRST_CHILD: default: $start = $parentLeft + 1; ++$level; break; } $this->shiftRL($em, $config['useObjectClass'], $start, $treeSize, $parentRoot); if (!$isNewNode && $root === $parentRoot && $left >= $start) { $left += $treeSize; $wrapped->setPropertyValue($config['left'], $left); } if (!$isNewNode && $root === $parentRoot && $right >= $start) { $right += $treeSize; $wrapped->setPropertyValue($config['right'], $right); } $newRoot = $parentRoot; } elseif (!isset($config['root']) || ($meta->isSingleValuedAssociation($config['root']) && null !== $parent && ($newRoot = $meta->getFieldValue($node, $config['root'])))) { if (!isset($this->treeEdges[$meta->getName()])) { $this->treeEdges[$meta->getName()] = $this->max($em, $config['useObjectClass'], $newRoot) + 1; } $level = 0; $parentLeft = 0; $parentRight = $this->treeEdges[$meta->getName()]; $this->treeEdges[$meta->getName()] += 2; switch ($position) { case self::PREV_SIBLING: if (property_exists($node, 'sibling')) { $wrappedSibling = AbstractWrapper::wrap($node->sibling, $em); $start = $wrappedSibling->getPropertyValue($config['left']); } else { $wrapped->setPropertyValue($config['parent'], null); $em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node); $start = $parentLeft + 1; } break; case self::NEXT_SIBLING: if (property_exists($node, 'sibling')) { $wrappedSibling = AbstractWrapper::wrap($node->sibling, $em); $start = $wrappedSibling->getPropertyValue($config['right']) + 1; } else { $wrapped->setPropertyValue($config['parent'], null); $em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $node); $start = $parentRight; } break; case self::LAST_CHILD: $start = $parentRight; break; case self::FIRST_CHILD: default: $start = $parentLeft + 1; break; } $this->shiftRL($em, $config['useObjectClass'], $start, $treeSize, null); if (!$isNewNode && $left >= $start) { $left += $treeSize; $wrapped->setPropertyValue($config['left'], $left); } if (!$isNewNode && $right >= $start) { $right += $treeSize; $wrapped->setPropertyValue($config['right'], $right); } } else { $start = 1; if (isset($config['rootIdentifierMethod'])) { $method = $config['rootIdentifierMethod']; $newRoot = $node->$method(); $repo = $em->getRepository($config['useObjectClass']); $criteria = new Criteria(); $criteria->andWhere(Criteria::expr()->notIn($wrapped->getMetadata()->getIdentifier()[0], [$wrapped->getIdentifier()])); $criteria->andWhere(Criteria::expr()->eq($config['root'], $node->$method())); $criteria->andWhere(Criteria::expr()->isNull($config['parent'])); $criteria->andWhere(Criteria::expr()->eq($config['level'], 0)); $criteria->orderBy([$config['right'] => Criteria::ASC]); $roots = $repo->matching($criteria)->toArray(); $last = array_pop($roots); $start = ($last) ? $meta->getFieldValue($last, $config['right']) + 1 : 1; } elseif ($meta->isSingleValuedAssociation($config['root'])) { $newRoot = $node; } else { $newRoot = $wrapped->getIdentifier(); } } $diff = $start - $left; if (!$isNewNode) { $levelDiff = isset($config['level']) ? $level - $wrapped->getPropertyValue($config['level']) : null; $this->shiftRangeRL( $em, $config['useObjectClass'], $left, $right, $diff, $root, $newRoot, $levelDiff ); $this->shiftRL($em, $config['useObjectClass'], $left, -$treeSize, $root); } else { $qb = $em->createQueryBuilder(); $qb->update($config['useObjectClass'], 'node'); if (isset($config['root'])) { $qb->set('node.'.$config['root'], ':rid'); $qb->setParameter('rid', $newRoot); $wrapped->setPropertyValue($config['root'], $newRoot); $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['root'], $newRoot); } if (isset($config['level'])) { $qb->set('node.'.$config['level'], $level); $wrapped->setPropertyValue($config['level'], $level); $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['level'], $level); } if (isset($newParent)) { $wrappedNewParent = AbstractWrapper::wrap($newParent, $em); $newParentId = $wrappedNewParent->getIdentifier(); $qb->set('node.'.$config['parent'], ':pid'); $qb->setParameter('pid', $newParentId); $wrapped->setPropertyValue($config['parent'], $newParent); $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['parent'], $newParent); } $qb->set('node.'.$config['left'], $left + $diff); $qb->set('node.'.$config['right'], $right + $diff); // node id cannot be null $qb->where($qb->expr()->eq('node.'.$identifierField, ':id')); $qb->setParameter('id', $nodeId); $qb->getQuery()->getSingleScalarResult(); $wrapped->setPropertyValue($config['left'], $left + $diff); $wrapped->setPropertyValue($config['right'], $right + $diff); $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['left'], $left + $diff); $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['right'], $right + $diff); } if (isset($this->delayedNodes[$oid])) { foreach ($this->delayedNodes[$oid] as $nodeData) { $this->updateNode($em, $nodeData['node'], $node, $nodeData['position']); } } } /** * Get the edge of tree * * @param string $class * @param int $rootId * * @phpstan-param class-string $class * * @return int */ public function max(EntityManagerInterface $em, $class, $rootId = 0) { $meta = $em->getClassMetadata($class); $config = $this->listener->getConfiguration($em, $meta->getName()); $qb = $em->createQueryBuilder(); $qb->select($qb->expr()->max('node.'.$config['right'])) ->from($config['useObjectClass'], 'node'); if (isset($config['root']) && $rootId) { $qb->where($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $rootId); } $query = $qb->getQuery(); $right = $query->getSingleScalarResult(); return (int) $right; } /** * Shift tree left and right values by delta * * @param string $class * @param int $first * @param int $delta * @param int|string $root * * @phpstan-param class-string $class * * @return void */ public function shiftRL(EntityManagerInterface $em, $class, $first, $delta, $root = null) { $meta = $em->getClassMetadata($class); $config = $this->listener->getConfiguration($em, $class); $sign = ($delta >= 0) ? ' + ' : ' - '; $absDelta = abs($delta); $qb = $em->createQueryBuilder(); $qb->update($config['useObjectClass'], 'node') ->set('node.'.$config['left'], "node.{$config['left']} {$sign} {$absDelta}") ->where($qb->expr()->gte('node.'.$config['left'], $first)); if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $root); } $qb->getQuery()->getSingleScalarResult(); $qb = $em->createQueryBuilder(); $qb->update($config['useObjectClass'], 'node') ->set('node.'.$config['right'], "node.{$config['right']} {$sign} {$absDelta}") ->where($qb->expr()->gte('node.'.$config['right'], $first)); if (isset($config['root'])) { $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $root); } $qb->getQuery()->getSingleScalarResult(); // update in memory nodes increases performance, saves some IO foreach ($em->getUnitOfWork()->getIdentityMap() as $className => $nodes) { // for inheritance mapped classes, only root is always in the identity map if ($className !== $meta->rootEntityName) { continue; } foreach ($nodes as $node) { if ($node instanceof Proxy && !$node->__isInitialized()) { continue; } $nodeMeta = $em->getClassMetadata(get_class($node)); if (!array_key_exists($config['left'], $nodeMeta->getReflectionProperties())) { continue; } $oid = spl_object_id($node); $left = $meta->getReflectionProperty($config['left'])->getValue($node); $currentRoot = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($node) : null; if ($currentRoot === $root && $left >= $first) { $meta->getReflectionProperty($config['left'])->setValue($node, $left + $delta); $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['left'], $left + $delta); } $right = $meta->getReflectionProperty($config['right'])->getValue($node); if ($currentRoot === $root && $right >= $first) { $meta->getReflectionProperty($config['right'])->setValue($node, $right + $delta); $em->getUnitOfWork()->setOriginalEntityProperty($oid, $config['right'], $right + $delta); } } } } /** * Shift range of right and left values on tree * depending on tree level difference also * * @param string $class * @param int $first * @param int $last * @param int $delta * @param int|string $root * @param int|string $destRoot * @param int $levelDelta * * @phpstan-param class-string $class * * @return void */ public function shiftRangeRL(EntityManagerInterface $em, $class, $first, $last, $delta, $root = null, $destRoot = null, $levelDelta = null) { // @todo: Remove the following condition and assignment in the next major release and use 0 as default value for // the `$levelDelta` parameter. if (null === $levelDelta && func_num_args() >= 8) { @trigger_error(sprintf( 'Passing a type different than "int" as argument 8 to "%s()" is deprecated since gedmo/doctrine-extensions'. ' 3.9 and will throw a "%s" error in version 4.0.', __METHOD__, \TypeError::class ), E_USER_DEPRECATED); } $levelDelta = $levelDelta ?? 0; $meta = $em->getClassMetadata($class); $config = $this->listener->getConfiguration($em, $class); $sign = ($delta >= 0) ? ' + ' : ' - '; $absDelta = abs($delta); $levelSign = ($levelDelta >= 0) ? ' + ' : ' - '; $absLevelDelta = abs($levelDelta); $qb = $em->createQueryBuilder(); $qb->update($config['useObjectClass'], 'node') ->set('node.'.$config['left'], "node.{$config['left']} {$sign} {$absDelta}") ->set('node.'.$config['right'], "node.{$config['right']} {$sign} {$absDelta}") ->where($qb->expr()->gte('node.'.$config['left'], $first)) ->andWhere($qb->expr()->lte('node.'.$config['right'], $last)); if (isset($config['root'])) { $qb->set('node.'.$config['root'], ':drid'); $qb->setParameter('drid', $destRoot); $qb->andWhere($qb->expr()->eq('node.'.$config['root'], ':rid')); $qb->setParameter('rid', $root); } if (isset($config['level'])) { $qb->set('node.'.$config['level'], "node.{$config['level']} {$levelSign} {$absLevelDelta}"); } $qb->getQuery()->getSingleScalarResult(); // update in memory nodes increases performance, saves some IO foreach ($em->getUnitOfWork()->getIdentityMap() as $className => $nodes) { // for inheritance mapped classes, only root is always in the identity map if ($className !== $meta->rootEntityName) { continue; } foreach ($nodes as $node) { if ($node instanceof Proxy && !$node->__isInitialized()) { continue; } $nodeMeta = $em->getClassMetadata(get_class($node)); if (!array_key_exists($config['left'], $nodeMeta->getReflectionProperties())) { continue; } $left = $meta->getReflectionProperty($config['left'])->getValue($node); $right = $meta->getReflectionProperty($config['right'])->getValue($node); $currentRoot = isset($config['root']) ? $meta->getReflectionProperty($config['root'])->getValue($node) : null; if ($currentRoot === $root && $left >= $first && $right <= $last) { $oid = spl_object_id($node); $uow = $em->getUnitOfWork(); $meta->getReflectionProperty($config['left'])->setValue($node, $left + $delta); $uow->setOriginalEntityProperty($oid, $config['left'], $left + $delta); $meta->getReflectionProperty($config['right'])->setValue($node, $right + $delta); $uow->setOriginalEntityProperty($oid, $config['right'], $right + $delta); if (isset($config['root'])) { $meta->getReflectionProperty($config['root'])->setValue($node, $destRoot); $uow->setOriginalEntityProperty($oid, $config['root'], $destRoot); } if (isset($config['level'])) { $level = $meta->getReflectionProperty($config['level'])->getValue($node); $meta->getReflectionProperty($config['level'])->setValue($node, $level + $levelDelta); $uow->setOriginalEntityProperty($oid, $config['level'], $level + $levelDelta); } } } } } } doctrine-extensions/src/Tree/Strategy/AbstractMaterializedPath.php 0000644 00000041126 15117737237 0021505 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Strategy; use Doctrine\ODM\MongoDB\UnitOfWork as MongoDBUnitOfWork; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Gedmo\Exception\RuntimeException; use Gedmo\Exception\TreeLockingException; use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Tree\Strategy; use Gedmo\Tree\TreeListener; use MongoDB\BSON\UTCDateTime; use ProxyManager\Proxy\GhostObjectInterface; /** * This strategy makes tree using materialized path strategy * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author <rocco@roccosportal.com> */ abstract class AbstractMaterializedPath implements Strategy { public const ACTION_INSERT = 'insert'; public const ACTION_UPDATE = 'update'; public const ACTION_REMOVE = 'remove'; /** * @var TreeListener */ protected $listener; /** * Array of objects which were scheduled for path processes * * @var array */ protected $scheduledForPathProcess = []; /** * Array of objects which were scheduled for path process. * This time, this array contains the objects with their ID * already set * * @var array */ protected $scheduledForPathProcessWithIdSet = []; /** * Roots of trees which needs to be locked * * @var array */ protected $rootsOfTreesWhichNeedsLocking = []; /** * Objects which are going to be inserted (set only if tree locking is used) * * @var array */ protected $pendingObjectsToInsert = []; /** * Objects which are going to be updated (set only if tree locking is used) * * @var array */ protected $pendingObjectsToUpdate = []; /** * Objects which are going to be removed (set only if tree locking is used) * * @var array */ protected $pendingObjectsToRemove = []; public function __construct(TreeListener $listener) { $this->listener = $listener; } public function getName() { return Strategy::MATERIALIZED_PATH; } public function processScheduledInsertion($om, $node, AdapterInterface $ea) { $meta = $om->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($om, $meta->getName()); $fieldMapping = $meta->getFieldMapping($config['path_source']); if ($meta->isIdentifier($config['path_source']) || 'string' === $fieldMapping['type']) { $this->scheduledForPathProcess[spl_object_id($node)] = $node; } else { $this->updateNode($om, $node, $ea); } } public function processScheduledUpdate($om, $node, AdapterInterface $ea) { $meta = $om->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($om, $meta->getName()); $uow = $om->getUnitOfWork(); $changeSet = $ea->getObjectChangeSet($uow, $node); if (isset($changeSet[$config['parent']]) || isset($changeSet[$config['path_source']])) { if (isset($changeSet[$config['path']])) { $originalPath = $changeSet[$config['path']][0]; } else { $pathProp = $meta->getReflectionProperty($config['path']); $pathProp->setAccessible(true); $originalPath = $pathProp->getValue($node); } $this->updateNode($om, $node, $ea); $this->updateChildren($om, $node, $ea, $originalPath); } } public function processPostPersist($om, $node, AdapterInterface $ea) { $oid = spl_object_id($node); if ($this->scheduledForPathProcess && array_key_exists($oid, $this->scheduledForPathProcess)) { $this->scheduledForPathProcessWithIdSet[$oid] = $node; unset($this->scheduledForPathProcess[$oid]); if (empty($this->scheduledForPathProcess)) { foreach ($this->scheduledForPathProcessWithIdSet as $oid => $node) { $this->updateNode($om, $node, $ea); unset($this->scheduledForPathProcessWithIdSet[$oid]); } } } $this->processPostEventsActions($om, $ea, $node, self::ACTION_INSERT); } public function processPostUpdate($om, $node, AdapterInterface $ea) { $this->processPostEventsActions($om, $ea, $node, self::ACTION_UPDATE); } public function processPostRemove($om, $node, AdapterInterface $ea) { $this->processPostEventsActions($om, $ea, $node, self::ACTION_REMOVE); } public function onFlushEnd($om, AdapterInterface $ea) { $this->lockTrees($om, $ea); } public function processPreRemove($om, $node) { $this->processPreLockingActions($om, $node, self::ACTION_REMOVE); } public function processPrePersist($om, $node) { $this->processPreLockingActions($om, $node, self::ACTION_INSERT); } public function processPreUpdate($om, $node) { $this->processPreLockingActions($om, $node, self::ACTION_UPDATE); } public function processMetadataLoad($om, $meta) { } public function processScheduledDelete($om, $node) { $meta = $om->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($om, $meta->getName()); $this->removeNode($om, $meta, $config, $node); } /** * Update the $node * * @param object $node target node * @param AdapterInterface $ea event adapter * * @return void */ public function updateNode(ObjectManager $om, $node, AdapterInterface $ea) { $meta = $om->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($om, $meta->getName()); $uow = $om->getUnitOfWork(); $parentProp = $meta->getReflectionProperty($config['parent']); $parentProp->setAccessible(true); $parent = $parentProp->getValue($node); $pathProp = $meta->getReflectionProperty($config['path']); $pathProp->setAccessible(true); $pathSourceProp = $meta->getReflectionProperty($config['path_source']); $pathSourceProp->setAccessible(true); $path = (string) $pathSourceProp->getValue($node); // We need to avoid the presence of the path separator in the path source if (false !== strpos($path, $config['path_separator'])) { $msg = 'You can\'t use the Path separator ("%s") as a character for your PathSource field value.'; throw new RuntimeException(sprintf($msg, $config['path_separator'])); } $fieldMapping = $meta->getFieldMapping($config['path_source']); // default behavior: if PathSource field is a string, we append the ID to the path // path_append_id is true: always append id // path_append_id is false: never append id if (true === $config['path_append_id'] || ('string' === $fieldMapping['type'] && false !== $config['path_append_id'])) { if (method_exists($meta, 'getIdentifierValue')) { $identifier = $meta->getIdentifierValue($node); } else { $identifierProp = $meta->getReflectionProperty($meta->getSingleIdentifierFieldName()); $identifierProp->setAccessible(true); $identifier = $identifierProp->getValue($node); } $path .= '-'.$identifier; } if ($parent) { // Ensure parent has been initialized in the case where it's a proxy $om->initializeObject($parent); $changeSet = $uow->isScheduledForUpdate($parent) ? $ea->getObjectChangeSet($uow, $parent) : false; $pathOrPathSourceHasChanged = $changeSet && (isset($changeSet[$config['path_source']]) || isset($changeSet[$config['path']])); if ($pathOrPathSourceHasChanged || !$pathProp->getValue($parent)) { $this->updateNode($om, $parent, $ea); } $parentPath = $pathProp->getValue($parent); // if parent path not ends with separator if ($parentPath[strlen($parentPath) - 1] !== $config['path_separator']) { // add separator $path = $pathProp->getValue($parent).$config['path_separator'].$path; } else { // don't add separator $path = $pathProp->getValue($parent).$path; } } if ($config['path_starts_with_separator'] && (strlen($path) > 0 && $path[0] !== $config['path_separator'])) { $path = $config['path_separator'].$path; } if ($config['path_ends_with_separator'] && ($path[strlen($path) - 1] !== $config['path_separator'])) { $path .= $config['path_separator']; } $pathProp->setValue($node, $path); $changes = [ $config['path'] => [null, $path], ]; $pathHash = null; if (isset($config['path_hash'])) { $pathHash = md5($path); $pathHashProp = $meta->getReflectionProperty($config['path_hash']); $pathHashProp->setAccessible(true); $pathHashProp->setValue($node, $pathHash); $changes[$config['path_hash']] = [null, $pathHash]; } if (isset($config['root'])) { $root = null; // Define the root value by grabbing the top of the current path $rootFinderPath = explode($config['path_separator'], $path); $rootIndex = $config['path_starts_with_separator'] ? 1 : 0; $root = $rootFinderPath[$rootIndex]; // If it is an association, then make it an reference // to the entity if ($meta->hasAssociation($config['root'])) { $rootClass = $meta->getAssociationTargetClass($config['root']); $root = $om->getReference($rootClass, $root); } $rootProp = $meta->getReflectionProperty($config['root']); $rootProp->setAccessible(true); $rootProp->setValue($node, $root); $changes[$config['root']] = [null, $root]; } if (isset($config['level'])) { $level = substr_count($path, $config['path_separator']); $levelProp = $meta->getReflectionProperty($config['level']); $levelProp->setAccessible(true); $levelProp->setValue($node, $level); $changes[$config['level']] = [null, $level]; } if (!$uow instanceof MongoDBUnitOfWork) { $ea->setOriginalObjectProperty($uow, $node, $config['path'], $path); $uow->scheduleExtraUpdate($node, $changes); } else { $ea->recomputeSingleObjectChangeSet($uow, $meta, $node); } if (isset($config['path_hash'])) { $ea->setOriginalObjectProperty($uow, $node, $config['path_hash'], $pathHash); } } /** * Update node's children * * @param object $node * @param string $originalPath * * @return void */ public function updateChildren(ObjectManager $om, $node, AdapterInterface $ea, $originalPath) { $meta = $om->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($om, $meta->getName()); $children = $this->getChildren($om, $meta, $config, $originalPath); foreach ($children as $child) { $this->updateNode($om, $child, $ea); } } /** * Process pre-locking actions * * @param ObjectManager $om * @param object $node * @param string $action * * @return void */ public function processPreLockingActions($om, $node, $action) { $meta = $om->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($om, $meta->getName()); if ($config['activate_locking']) { $parentProp = $meta->getReflectionProperty($config['parent']); $parentProp->setAccessible(true); $parentNode = $node; while (($parent = $parentProp->getValue($parentNode)) !== null) { $parentNode = $parent; } // In some cases, the parent could be a not initialized proxy. In this case, the // "lockTime" field may NOT be loaded yet and have null instead of the date. // We need to be sure that this field has its real value if ($parentNode !== $node && $parentNode instanceof GhostObjectInterface) { $parentNode->initializeProxy(); } // If tree is already locked, we throw an exception $lockTimeProp = $meta->getReflectionProperty($config['lock_time']); $lockTimeProp->setAccessible(true); $lockTime = $lockTimeProp->getValue($parentNode); if (null !== $lockTime) { $lockTime = $lockTime instanceof UTCDateTime ? $lockTime->toDateTime()->getTimestamp() : $lockTime->getTimestamp(); } if (null !== $lockTime && ($lockTime >= (time() - $config['locking_timeout']))) { $msg = 'Tree with root id "%s" is locked.'; $id = $meta->getIdentifierValue($parentNode); throw new TreeLockingException(sprintf($msg, $id)); } $this->rootsOfTreesWhichNeedsLocking[spl_object_id($parentNode)] = $parentNode; $oid = spl_object_id($node); switch ($action) { case self::ACTION_INSERT: $this->pendingObjectsToInsert[$oid] = $node; break; case self::ACTION_UPDATE: $this->pendingObjectsToUpdate[$oid] = $node; break; case self::ACTION_REMOVE: $this->pendingObjectsToRemove[$oid] = $node; break; default: throw new \InvalidArgumentException(sprintf('"%s" is not a valid action.', $action)); } } } /** * Process pre-locking actions * * @param object $node * @param string $action * * @return void */ public function processPostEventsActions(ObjectManager $om, AdapterInterface $ea, $node, $action) { $meta = $om->getClassMetadata(get_class($node)); $config = $this->listener->getConfiguration($om, $meta->getName()); if ($config['activate_locking']) { switch ($action) { case self::ACTION_INSERT: unset($this->pendingObjectsToInsert[spl_object_id($node)]); break; case self::ACTION_UPDATE: unset($this->pendingObjectsToUpdate[spl_object_id($node)]); break; case self::ACTION_REMOVE: unset($this->pendingObjectsToRemove[spl_object_id($node)]); break; default: throw new \InvalidArgumentException(sprintf('"%s" is not a valid action.', $action)); } if (empty($this->pendingObjectsToInsert) && empty($this->pendingObjectsToUpdate) && empty($this->pendingObjectsToRemove)) { $this->releaseTreeLocks($om, $ea); } } } /** * Remove node and its children * * @param ObjectManager $om * @param ClassMetadata $meta Metadata * @param array<string, mixed> $config config * @param object $node node to remove * * @return void */ abstract public function removeNode($om, $meta, $config, $node); /** * Returns children of the node with its original path * * @param ObjectManager $om * @param ClassMetadata $meta Metadata * @param array<string, mixed> $config config * @param string $originalPath original path of object * * @return array|\Traversable */ abstract public function getChildren($om, $meta, $config, $originalPath); /** * Locks all needed trees * * @return void */ protected function lockTrees(ObjectManager $om, AdapterInterface $ea) { // Do nothing by default } /** * Releases all trees which are locked * * @return void */ protected function releaseTreeLocks(ObjectManager $om, AdapterInterface $ea) { // Do nothing by default } } doctrine-extensions/src/Tree/Traits/MaterializedPath.php 0000644 00000004206 15117737237 0017463 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Traits; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; /** * MaterializedPath Trait * * @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de> */ trait MaterializedPath { /** * @var string */ protected $path; /** * @var self */ protected $parent; /** * @var int */ protected $level; /** * @var Collection|self[] */ protected $children; /** * @var string */ protected $hash; /** * @param self $parent * * @return self */ public function setParent(self $parent = null) { $this->parent = $parent; return $this; } /** * @return self */ public function getParent() { return $this->parent; } /** * @param string $path * * @return self */ public function setPath($path) { $this->path = $path; return $this; } /** * @return string */ public function getPath() { return $this->path; } /** * @return int */ public function getLevel() { return $this->level; } /** * @param string $hash * * @return self */ public function setHash($hash) { $this->hash = $hash; return $this; } /** * @return string */ public function getHash() { return $this->hash; } /** * @param Collection|self[] $children * * @return self */ public function setChildren($children) { $this->children = $children; return $this; } /** * @return Collection|self[] */ public function getChildren() { return $this->children = $this->children ?: new ArrayCollection(); } } doctrine-extensions/src/Tree/Traits/NestedSet.php 0000644 00000001234 15117737237 0016130 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Traits; /** * NestedSet Trait, usable with PHP >= 5.4 * * @author Renaat De Muynck <renaat.demuynck@gmail.com> */ trait NestedSet { /** * @var int */ private $root; /** * @var int */ private $level; /** * @var int */ private $left; /** * @var int */ private $right; } doctrine-extensions/src/Tree/Traits/NestedSetEntity.php 0000644 00000002563 15117737237 0017333 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Traits; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** * NestedSet Trait, usable with PHP >= 5.4 * * @author Renaat De Muynck <renaat.demuynck@gmail.com> */ trait NestedSetEntity { /** * @var int * @Gedmo\TreeRoot * @ORM\Column(name="root", type="integer", nullable=true) */ #[ORM\Column(name: 'root', type: Types::INTEGER, nullable: true)] #[Gedmo\TreeRoot] private $root; /** * @var int * @Gedmo\TreeLevel * @ORM\Column(name="lvl", type="integer") */ #[ORM\Column(name: 'lvl', type: Types::INTEGER)] #[Gedmo\TreeLevel] private $level; /** * @var int * @Gedmo\TreeLeft * @ORM\Column(name="lft", type="integer") */ #[ORM\Column(name: 'lft', type: Types::INTEGER)] #[Gedmo\TreeLeft] private $left; /** * @var int * @Gedmo\TreeRight * @ORM\Column(name="rgt", type="integer") */ #[ORM\Column(name: 'rgt', type: Types::INTEGER)] #[Gedmo\TreeRight] private $right; } doctrine-extensions/src/Tree/Traits/NestedSetEntityUuid.php 0000644 00000001521 15117737237 0020153 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree\Traits; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; /** * NestedSet Trait with UUid, usable with PHP >= 5.4 * * @author Benjamin Lazarecki <benjamin.lazarecki@sensiolabs.com> */ trait NestedSetEntityUuid { use NestedSetEntity; /** * @var string * @Gedmo\TreeRoot * @ORM\Column(name="root", type="string", nullable=true) */ #[ORM\Column(name: 'root', type: Types::STRING, nullable: true)] #[Gedmo\TreeRoot] private $root; } doctrine-extensions/src/Tree/Node.php 0000644 00000002356 15117737237 0013657 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree; /** * This interface is not necessary but can be implemented for * Entities which in some cases needs to be identified as * Tree Node * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface Node { // use now annotations instead of predefined methods, this interface is not necessary /* * @gedmo:TreeLeft * to mark the field as "tree left" use property annotation @gedmo:TreeLeft * it will use this field to store tree left value */ /* * @gedmo:TreeRight * to mark the field as "tree right" use property annotation @gedmo:TreeRight * it will use this field to store tree right value */ /* * @gedmo:TreeParent * in every tree there should be link to parent. To identify a relation * as parent relation to child use @Tree:Ancestor annotation on the related property */ /* * @gedmo:TreeLevel * level of node. */ } doctrine-extensions/src/Tree/RepositoryInterface.php 0000644 00000005164 15117737237 0016772 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree; use Gedmo\Exception\InvalidArgumentException; /** * This interface ensures a consistent API between repositories for the ORM and the ODM. * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface RepositoryInterface extends RepositoryUtilsInterface { /** * Get all root nodes. * * @param string $sortByField * @param string $direction * * @return array */ public function getRootNodes($sortByField = null, $direction = 'asc'); /** * Returns an array of nodes optimized for building a tree. * * @param object $node Root node * @param bool $direct Flag indicating whether only direct children should be retrieved * @param array $options Options, see {@see RepositoryUtilsInterface::buildTree()} for supported keys * @param bool $includeNode Flag indicating whether the given node should be included in the results * * @return array */ public function getNodesHierarchy($node = null, $direct = false, array $options = [], $includeNode = false); /** * Get the list of children for the given node. * * @param object|null $node If null, all tree nodes will be taken * @param bool $direct True to take only direct children * @param string|string[]|null $sortByField Field name or array of fields names to sort by * @param string|string[] $direction Sort order ('ASC'|'DESC'). If $sortByField is an array, this may also be an array with matching number of elements * @param bool $includeNode Include the root node in results? * * @return array|null List of children or null on failure */ public function getChildren($node = null, $direct = false, $sortByField = null, $direction = 'ASC', $includeNode = false); /** * Counts the children of the given node * * @param object|null $node The object to count children for; if null, all nodes will be counted * @param bool $direct Flag indicating whether only direct children should be counted * * @return int * * @throws InvalidArgumentException if the input is invalid */ public function childCount($node = null, $direct = false); } doctrine-extensions/src/Tree/RepositoryUtils.php 0000644 00000014331 15117737237 0016166 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Gedmo\Exception\InvalidArgumentException; use Gedmo\Tool\Wrapper\EntityWrapper; use Gedmo\Tool\Wrapper\MongoDocumentWrapper; /** * @final since gedmo/doctrine-extensions 3.11 */ class RepositoryUtils implements RepositoryUtilsInterface { /** @var ClassMetadata */ protected $meta; /** @var TreeListener */ protected $listener; /** @var ObjectManager&(DocumentManager|EntityManagerInterface) */ protected $om; /** @var RepositoryInterface */ protected $repo; /** * This index is used to hold the children of a node * when using any of the buildTree related methods. * * @var string */ protected $childrenIndex = '__children'; /** * @param ObjectManager&(DocumentManager|EntityManagerInterface) $om * @param TreeListener $listener * @param RepositoryInterface $repo */ public function __construct(ObjectManager $om, ClassMetadata $meta, $listener, $repo) { $this->om = $om; $this->meta = $meta; $this->listener = $listener; $this->repo = $repo; } /** * @return ClassMetadata */ public function getClassMetadata() { return $this->meta; } public function childrenHierarchy($node = null, $direct = false, array $options = [], $includeNode = false) { $meta = $this->getClassMetadata(); if (null !== $node) { if (is_a($node, $meta->getName())) { $wrapperClass = $this->om instanceof \Doctrine\ORM\EntityManagerInterface ? EntityWrapper::class : MongoDocumentWrapper::class; $wrapped = new $wrapperClass($node, $this->om); if (!$wrapped->hasValidIdentifier()) { throw new InvalidArgumentException('Node is not managed by UnitOfWork'); } } } else { $includeNode = true; } // Gets the array of $node results. It must be ordered by depth $nodes = $this->repo->getNodesHierarchy($node, $direct, $options, $includeNode); return $this->repo->buildTree($nodes, $options); } public function buildTree(array $nodes, array $options = []) { $meta = $this->getClassMetadata(); $nestedTree = $this->repo->buildTreeArray($nodes); $default = [ 'decorate' => false, 'rootOpen' => '<ul>', 'rootClose' => '</ul>', 'childOpen' => '<li>', 'childClose' => '</li>', 'nodeDecorator' => static function ($node) use ($meta) { // override and change it, guessing which field to use if ($meta->hasField('title')) { $field = 'title'; } elseif ($meta->hasField('name')) { $field = 'name'; } else { throw new InvalidArgumentException('Cannot find any representation field'); } return $node[$field]; }, ]; $options = array_merge($default, $options); // If you don't want any html output it will return the nested array if (!$options['decorate']) { return $nestedTree; } if ([] === $nestedTree) { return ''; } $childrenIndex = $this->childrenIndex; $build = static function ($tree) use (&$build, &$options, $childrenIndex) { $output = is_string($options['rootOpen']) ? $options['rootOpen'] : $options['rootOpen']($tree); foreach ($tree as $node) { $output .= is_string($options['childOpen']) ? $options['childOpen'] : $options['childOpen']($node); $output .= $options['nodeDecorator']($node); if ([] !== $node[$childrenIndex]) { $output .= $build($node[$childrenIndex]); } $output .= is_string($options['childClose']) ? $options['childClose'] : $options['childClose']($node); } return $output.(is_string($options['rootClose']) ? $options['rootClose'] : $options['rootClose']($tree)); }; return $build($nestedTree); } public function buildTreeArray(array $nodes) { $meta = $this->getClassMetadata(); $config = $this->listener->getConfiguration($this->om, $meta->getName()); $nestedTree = []; $l = 0; if ([] !== $nodes) { // Node Stack. Used to help building the hierarchy $stack = []; foreach ($nodes as $child) { $item = $child; $item[$this->childrenIndex] = []; // Number of stack items $l = count($stack); // Check if we're dealing with different levels while ($l > 0 && $stack[$l - 1][$config['level']] >= $item[$config['level']]) { array_pop($stack); --$l; } // Stack is empty (we are inspecting the root) if (0 == $l) { // Assigning the root child $i = count($nestedTree); $nestedTree[$i] = $item; $stack[] = &$nestedTree[$i]; } else { // Add child to parent $i = count($stack[$l - 1][$this->childrenIndex]); $stack[$l - 1][$this->childrenIndex][$i] = $item; $stack[] = &$stack[$l - 1][$this->childrenIndex][$i]; } } } return $nestedTree; } public function setChildrenIndex($childrenIndex) { $this->childrenIndex = $childrenIndex; } public function getChildrenIndex() { return $this->childrenIndex; } } doctrine-extensions/src/Tree/RepositoryUtilsInterface.php 0000644 00000007166 15117737237 0020017 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree; use Gedmo\Exception\InvalidArgumentException; interface RepositoryUtilsInterface { /** * Retrieves the nested array or decorated output. * * Uses options to handle decorations * * @param object|null $node The object to fetch children for; if null, all nodes will be retrieved * @param bool $direct Flag indicating whether only direct children should be retrieved * @param array $options Options configuring the output, supported keys include: * - decorate: boolean (false) - retrieves the tree as an HTML `<ul>` element * - nodeDecorator: Closure (null) - uses $node as argument and returns the decorated item as a string * - rootOpen: string || Closure ('<ul>') - branch start, Closure will be given $children as a parameter * - rootClose: string ('</ul>') - branch close * - childOpen: string || Closure ('<li>') - start of node, Closure will be given $node as a parameter * - childClose: string ('</li>') - close of node * - childSort: array || keys allowed: field: field to sort on, dir: direction. 'asc' or 'desc' * @param bool $includeNode Flag indicating whether the given node should be included in the results * * @return array|string * * @throws InvalidArgumentException */ public function childrenHierarchy($node = null, $direct = false, array $options = [], $includeNode = false); /** * Retrieves the nested array or the decorated output. * * Uses options to handle decorations * * NOTE: nodes should be fetched and hydrated as array * * @param object[] $nodes The nodes to build the tree from * @param array $options Options configuring the output, supported keys include: * - decorate: boolean (false) - retrieves the tree as an HTML `<ul>` element * - nodeDecorator: Closure (null) - uses $node as argument and returns the decorated item as a string * - rootOpen: string || Closure ('<ul>') - branch start, Closure will be given $children as a parameter * - rootClose: string ('</ul>') - branch close * - childOpen: string || Closure ('<li>') - start of node, Closure will be given $node as a parameter * - childClose: string ('</li>') - close of node * * @return array|string * * @throws InvalidArgumentException */ public function buildTree(array $nodes, array $options = []); /** * Process a list of nodes and produce an array with the structure of the tree. * * @param object[] $nodes The nodes to build the tree from * * @return array */ public function buildTreeArray(array $nodes); /** * Sets the current children index. * * @param string $childrenIndex * * @return void */ public function setChildrenIndex($childrenIndex); /** * Gets the current children index. * * @return string */ public function getChildrenIndex(); } doctrine-extensions/src/Tree/Strategy.php 0000644 00000006505 15117737237 0014574 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\Event\AdapterInterface; interface Strategy { /** * NestedSet strategy */ public const NESTED = 'nested'; /** * Closure strategy */ public const CLOSURE = 'closure'; /** * Materialized Path strategy */ public const MATERIALIZED_PATH = 'materializedPath'; /** * Create a new strategy instance */ public function __construct(TreeListener $listener); /** * Get the name of the strategy * * @return string */ public function getName(); /** * Operations after metadata is loaded * * @param ObjectManager $om * @param ClassMetadata $meta * * @return void */ public function processMetadataLoad($om, $meta); /** * Operations on tree node insertion * * @param ObjectManager $om * @param object $object * * @return void */ public function processScheduledInsertion($om, $object, AdapterInterface $ea); /** * Operations on tree node updates * * @param ObjectManager $om * @param object $object * * @return void */ public function processScheduledUpdate($om, $object, AdapterInterface $ea); /** * Operations on tree node delete * * @param ObjectManager $om * @param object $object * * @return void */ public function processScheduledDelete($om, $object); /** * Operations on tree node removal * * @param ObjectManager $om * @param object $object * * @return void */ public function processPreRemove($om, $object); /** * Operations on tree node persist * * @param ObjectManager $om * @param object $object * * @return void */ public function processPrePersist($om, $object); /** * Operations on tree node update * * @param ObjectManager $om * @param object $object * * @return void */ public function processPreUpdate($om, $object); /** * Operations on tree node insertions * * @param ObjectManager $om * @param object $object * * @return void */ public function processPostPersist($om, $object, AdapterInterface $ea); /** * Operations on tree node updates * * @param ObjectManager $om * @param object $object * * @return void */ public function processPostUpdate($om, $object, AdapterInterface $ea); /** * Operations on tree node removals * * @param ObjectManager $om * @param object $object * * @return void */ public function processPostRemove($om, $object, AdapterInterface $ea); /** * Operations on the end of flush process * * @param ObjectManager $om * * @return void */ public function onFlushEnd($om, AdapterInterface $ea); } doctrine-extensions/src/Tree/TreeListener.php 0000644 00000022674 15117737237 0015404 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Tree; use Doctrine\Common\EventArgs; use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; use Doctrine\Persistence\ObjectManager; use Gedmo\Mapping\MappedEventSubscriber; use Gedmo\Tree\Mapping\Event\TreeAdapter; /** * The tree listener handles the synchronization of * tree nodes. Can implement different * strategies on handling the tree. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @phpstan-type TreeConfiguration = array{ * activate_locking?: bool, * closure?: class-string, * left?: string, * level?: string, * lock_time?: string, * locking_timeout?: int, * parent?: string, * path?: string, * path_source?: string, * path_separator?: string, * path_append_id?: ?bool, * path_starts_with_separator?: bool, * path_ends_with_separator?: bool, * path_hash?: string, * right?: string, * root?: string, * rootIdentifierMethod?: string, * strategy?: string, * useObjectClass?: class-string, * } * * @phpstan-method TreeConfiguration getConfiguration(ObjectManager $objectManager, $class) * * @method TreeAdapter getEventAdapter(EventArgs $args) */ class TreeListener extends MappedEventSubscriber { /** * Tree processing strategies for object classes * * @var array */ private $strategies = []; /** * List of strategy instances * * @var array */ private $strategyInstances = []; /** * List of used classes on flush * * @var array */ private $usedClassesOnFlush = []; /** * Specifies the list of events to listen * * @return string[] */ public function getSubscribedEvents() { return [ 'prePersist', 'preRemove', 'preUpdate', 'onFlush', 'loadClassMetadata', 'postPersist', 'postUpdate', 'postRemove', ]; } /** * Get the used strategy for tree processing * * @param string $class * * @return Strategy */ public function getStrategy(ObjectManager $om, $class) { if (!isset($this->strategies[$class])) { $config = $this->getConfiguration($om, $class); if (!$config) { throw new \Gedmo\Exception\UnexpectedValueException("Tree object class: {$class} must have tree metadata at this point"); } $managerName = 'UnsupportedManager'; if ($om instanceof \Doctrine\ORM\EntityManagerInterface) { $managerName = 'ORM'; } elseif ($om instanceof \Doctrine\ODM\MongoDB\DocumentManager) { $managerName = 'ODM\\MongoDB'; } if (!isset($this->strategyInstances[$config['strategy']])) { $strategyClass = $this->getNamespace().'\\Strategy\\'.$managerName.'\\'.ucfirst($config['strategy']); if (!class_exists($strategyClass)) { throw new \Gedmo\Exception\InvalidArgumentException($managerName." TreeListener does not support tree type: {$config['strategy']}"); } $this->strategyInstances[$config['strategy']] = new $strategyClass($this); } $this->strategies[$class] = $config['strategy']; } return $this->strategyInstances[$this->strategies[$class]]; } /** * Looks for Tree objects being updated * for further processing * * @return void */ public function onFlush(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); // check all scheduled updates for TreeNodes foreach ($ea->getScheduledObjectInsertions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); if ($this->getConfiguration($om, $meta->getName())) { $this->usedClassesOnFlush[$meta->getName()] = null; $this->getStrategy($om, $meta->getName())->processScheduledInsertion($om, $object, $ea); $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); } } foreach ($ea->getScheduledObjectUpdates($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); if ($this->getConfiguration($om, $meta->getName())) { $this->usedClassesOnFlush[$meta->getName()] = null; $this->getStrategy($om, $meta->getName())->processScheduledUpdate($om, $object, $ea); } } foreach ($ea->getScheduledObjectDeletions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); if ($this->getConfiguration($om, $meta->getName())) { $this->usedClassesOnFlush[$meta->getName()] = null; $this->getStrategy($om, $meta->getName())->processScheduledDelete($om, $object); } } foreach ($this->getStrategiesUsedForObjects($this->usedClassesOnFlush) as $strategy) { $strategy->onFlushEnd($om, $ea); } } /** * Updates tree on Node removal * * @return void */ public function preRemove(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); if ($this->getConfiguration($om, $meta->getName())) { $this->getStrategy($om, $meta->getName())->processPreRemove($om, $object); } } /** * Checks for persisted Nodes * * @return void */ public function prePersist(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); if ($this->getConfiguration($om, $meta->getName())) { $this->getStrategy($om, $meta->getName())->processPrePersist($om, $object); } } /** * Checks for updated Nodes * * @return void */ public function preUpdate(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); if ($this->getConfiguration($om, $meta->getName())) { $this->getStrategy($om, $meta->getName())->processPreUpdate($om, $object); } } /** * Checks for pending Nodes to fully synchronize * the tree * * @return void */ public function postPersist(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); if ($this->getConfiguration($om, $meta->getName())) { $this->getStrategy($om, $meta->getName())->processPostPersist($om, $object, $ea); } } /** * Checks for pending Nodes to fully synchronize * the tree * * @return void */ public function postUpdate(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); if ($this->getConfiguration($om, $meta->getName())) { $this->getStrategy($om, $meta->getName())->processPostUpdate($om, $object, $ea); } } /** * Checks for pending Nodes to fully synchronize * the tree * * @return void */ public function postRemove(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); if ($this->getConfiguration($om, $meta->getName())) { $this->getStrategy($om, $meta->getName())->processPostRemove($om, $object, $ea); } } /** * Mapps additional metadata * * @param LoadClassMetadataEventArgs $eventArgs * * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { $om = $eventArgs->getObjectManager(); $meta = $eventArgs->getClassMetadata(); $this->loadMetadataForObjectClass($om, $meta); if (isset(self::$configurations[$this->name][$meta->getName()]) && self::$configurations[$this->name][$meta->getName()]) { $this->getStrategy($om, $meta->getName())->processMetadataLoad($om, $meta); } } protected function getNamespace() { return __NAMESPACE__; } /** * Get the list of strategy instances used for * given object classes * * @return Strategy[] */ protected function getStrategiesUsedForObjects(array $classes) { $strategies = []; foreach ($classes as $name => $opt) { if (isset($this->strategies[$name]) && !isset($strategies[$this->strategies[$name]])) { $strategies[$this->strategies[$name]] = $this->strategyInstances[$this->strategies[$name]]; } } return $strategies; } } doctrine-extensions/src/Uploadable/Event/UploadableBaseEventArgs.php 0000644 00000006364 15117737237 0021711 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\Event; use Doctrine\Common\EventArgs; use Doctrine\ORM\EntityManagerInterface; use Gedmo\Uploadable\FileInfo\FileInfoInterface; use Gedmo\Uploadable\UploadableListener; /** * Abstract Base Event to be extended by Uploadable events * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ abstract class UploadableBaseEventArgs extends EventArgs { /** * The instance of the Uploadable listener that fired this event * * @var UploadableListener */ private $uploadableListener; /** * @var EntityManagerInterface */ private $em; /** * @todo Check if this property must be removed, as it is not used. * * @var array */ private $config = []; /** * The Uploadable entity * * @var object */ private $entity; /** * The configuration of the Uploadable extension for this entity class * * @todo Check if this property must be removed, as it is never set. * * @var array */ private $extensionConfiguration; /** * @var FileInfoInterface */ private $fileInfo; /** * Is the file being created, updated or removed? * This value can be: CREATE, UPDATE or DELETE * * @var string */ private $action; /** * @param object $entity * @param string $action */ public function __construct(UploadableListener $listener, EntityManagerInterface $em, array $config, FileInfoInterface $fileInfo, $entity, $action) { $this->uploadableListener = $listener; $this->em = $em; $this->config = $config; $this->fileInfo = $fileInfo; $this->entity = $entity; $this->action = $action; } /** * Retrieve the associated listener * * @return \Gedmo\Uploadable\UploadableListener */ public function getListener() { return $this->uploadableListener; } /** * Retrieve associated EntityManager * * @return \Doctrine\ORM\EntityManagerInterface */ public function getEntityManager() { return $this->em; } /** * Retrieve associated Entity * * @return object */ public function getEntity() { return $this->entity; } /** * Retrieve associated Uploadable extension configuration * * @return array */ public function getExtensionConfiguration() { return $this->extensionConfiguration; } /** * Retrieve the FileInfo associated with this entity. * * @return \Gedmo\Uploadable\FileInfo\FileInfoInterface */ public function getFileInfo() { return $this->fileInfo; } /** * Retrieve the action being performed to the entity: CREATE, UPDATE or DELETE * * @return string */ public function getAction() { return $this->action; } } doctrine-extensions/src/Uploadable/Event/UploadablePostFileProcessEventArgs.php 0000644 00000001212 15117737237 0024106 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\Event; /** * Post File Process Event for the Uploadable extension * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadablePostFileProcessEventArgs extends UploadableBaseEventArgs { } doctrine-extensions/src/Uploadable/Event/UploadablePreFileProcessEventArgs.php 0000644 00000001210 15117737237 0023705 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\Event; /** * Pre File Process Event for the Uploadable extension * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class UploadablePreFileProcessEventArgs extends UploadableBaseEventArgs { } doctrine-extensions/src/Uploadable/FileInfo/FileInfoArray.php 0000644 00000003061 15117737237 0020322 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\FileInfo; /** * FileInfoArray * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class FileInfoArray implements FileInfoInterface { /** * @var array */ protected $fileInfo; public function __construct(array $fileInfo) { $keys = ['error', 'size', 'type', 'tmp_name', 'name']; foreach ($keys as $k) { if (!isset($fileInfo[$k])) { $msg = 'There are missing keys in the fileInfo. '; $msg .= 'Keys needed: '.implode(',', $keys); throw new \RuntimeException($msg); } } $this->fileInfo = $fileInfo; } public function getTmpName() { return $this->fileInfo['tmp_name']; } public function getName() { return $this->fileInfo['name']; } public function getSize() { return $this->fileInfo['size']; } public function getType() { return $this->fileInfo['type']; } public function getError() { return $this->fileInfo['error']; } public function isUploadedFile() { return true; } } doctrine-extensions/src/Uploadable/FileInfo/FileInfoInterface.php 0000644 00000002046 15117737237 0021146 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\FileInfo; /** * FileInfoInterface * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface FileInfoInterface { /** * @return string|null */ public function getTmpName(); /** * @return string|null */ public function getName(); /** * @return int|null */ public function getSize(); /** * @return string|null */ public function getType(); /** * @return int */ public function getError(); /** * This method must return true if the file is coming from $_FILES, or false instead. * * @return bool */ public function isUploadedFile(); } doctrine-extensions/src/Uploadable/FilenameGenerator/FilenameGeneratorAlphanumeric.php 0000644 00000001604 15117737237 0025445 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\FilenameGenerator; /** * FilenameGeneratorAlphanumeric * * This class generates a filename, leaving only lowercase * alphanumeric characters * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class FilenameGeneratorAlphanumeric implements FilenameGeneratorInterface { public static function generate($filename, $extension, $object = null) { return preg_replace('/[^a-z0-9]+/', '-', strtolower($filename)).$extension; } } doctrine-extensions/src/Uploadable/FilenameGenerator/FilenameGeneratorInterface.php 0000644 00000001561 15117737237 0024737 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\FilenameGenerator; /** * FilenameGeneratorInterface * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface FilenameGeneratorInterface { /** * Generates a new filename * * @param string $filename Filename without extension * @param string $extension Extension with dot: .jpg, .gif, etc * @param object|null $object * * @return string */ public static function generate($filename, $extension, $object = null); } doctrine-extensions/src/Uploadable/FilenameGenerator/FilenameGeneratorSha1.php 0000644 00000001413 15117737237 0023627 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\FilenameGenerator; /** * FilenameGeneratorSha1 * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class FilenameGeneratorSha1 implements FilenameGeneratorInterface { public static function generate($filename, $extension, $object = null) { return sha1(uniqid($filename.$extension, true)).$extension; } } doctrine-extensions/src/Uploadable/Mapping/Driver/Annotation.php 0000644 00000010461 15117737237 0021077 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ 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; /** * This is an annotation mapping driver for Uploadable * behavioral extension. Used for extraction of extended * metadata from Annotations specifically for Uploadable * extension. * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @internal */ class Annotation extends AbstractAnnotationDriver { /** * 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(); } } 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); } } doctrine-extensions/src/Uploadable/Mapping/Driver/Attribute.php 0000644 00000001313 15117737237 0020724 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\Mapping\Driver; use Gedmo\Mapping\Annotation\Uploadable; use Gedmo\Mapping\Driver\AttributeDriverInterface; /** * This is an attribute mapping driver for Uploadable * behavioral extension. Used for extraction of extended * metadata from attribute specifically for Uploadable * extension. * * @internal */ class Attribute extends Annotation implements AttributeDriverInterface { } doctrine-extensions/src/Uploadable/Mapping/Driver/Xml.php 0000644 00000010437 15117737237 0017530 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\Mapping\Driver; use Gedmo\Mapping\Driver\Xml as BaseXml; use Gedmo\Uploadable\Mapping\Validator; /** * This is a xml mapping driver for Uploadable * behavioral extension. Used for extraction of extended * metadata from xml specifically for Uploadable * extension. * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * @author Miha Vrhovnik <miha.vrhovnik@gmail.com> * * @internal */ class Xml extends BaseXml { public function readExtendedMetadata($meta, array &$config) { /** * @var \SimpleXmlElement */ $xml = $this->_getMapping($meta->getName()); $xmlDoctrine = $xml; $xml = $xml->children(self::GEDMO_NAMESPACE_URI); if ('entity' === $xmlDoctrine->getName() || 'mapped-superclass' === $xmlDoctrine->getName()) { if (isset($xml->uploadable)) { $xmlUploadable = $xml->uploadable; $config['uploadable'] = true; $config['allowOverwrite'] = $this->_isAttributeSet($xmlUploadable, 'allow-overwrite') ? (bool) $this->_getAttribute($xmlUploadable, 'allow-overwrite') : false; $config['appendNumber'] = $this->_isAttributeSet($xmlUploadable, 'append-number') ? (bool) $this->_getAttribute($xmlUploadable, 'append-number') : false; $config['path'] = $this->_isAttributeSet($xmlUploadable, 'path') ? $this->_getAttribute($xml->{'uploadable'}, 'path') : ''; $config['pathMethod'] = $this->_isAttributeSet($xmlUploadable, 'path-method') ? $this->_getAttribute($xml->{'uploadable'}, 'path-method') : ''; $config['callback'] = $this->_isAttributeSet($xmlUploadable, 'callback') ? $this->_getAttribute($xml->{'uploadable'}, 'callback') : ''; $config['fileMimeTypeField'] = false; $config['fileNameField'] = false; $config['filePathField'] = false; $config['fileSizeField'] = false; $config['filenameGenerator'] = $this->_isAttributeSet($xmlUploadable, 'filename-generator') ? $this->_getAttribute($xml->{'uploadable'}, 'filename-generator') : Validator::FILENAME_GENERATOR_NONE; $config['maxSize'] = $this->_isAttributeSet($xmlUploadable, 'max-size') ? (float) $this->_getAttribute($xml->{'uploadable'}, 'max-size') : (float) 0; $config['allowedTypes'] = $this->_isAttributeSet($xmlUploadable, 'allowed-types') ? $this->_getAttribute($xml->{'uploadable'}, 'allowed-types') : ''; $config['disallowedTypes'] = $this->_isAttributeSet($xmlUploadable, 'disallowed-types') ? $this->_getAttribute($xml->{'uploadable'}, 'disallowed-types') : ''; if (isset($xmlDoctrine->field)) { foreach ($xmlDoctrine->field as $mapping) { $mappingDoctrine = $mapping; $mapping = $mapping->children(self::GEDMO_NAMESPACE_URI); $field = $this->_getAttribute($mappingDoctrine, 'name'); if (isset($mapping->{'uploadable-file-mime-type'})) { $config['fileMimeTypeField'] = $field; } elseif (isset($mapping->{'uploadable-file-size'})) { $config['fileSizeField'] = $field; } elseif (isset($mapping->{'uploadable-file-name'})) { $config['fileNameField'] = $field; } elseif (isset($mapping->{'uploadable-file-path'})) { $config['filePathField'] = $field; } } } Validator::validateConfiguration($meta, $config); } } } } doctrine-extensions/src/Uploadable/Mapping/Driver/Yaml.php 0000644 00000007105 15117737237 0017670 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\Mapping\Driver; use Gedmo\Mapping\Driver; use Gedmo\Mapping\Driver\File; use Gedmo\Uploadable\Mapping\Validator; /** * This is a yaml mapping driver for Uploadable * behavioral extension. Used for extraction of extended * metadata from yaml specifically for Uploadable * extension. * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @deprecated since gedmo/doctrine-extensions 3.5, will be removed in version 4.0. * * @internal */ class Yaml extends File implements Driver { /** * File extension * * @var string */ protected $_extension = '.dcm.yml'; public function readExtendedMetadata($meta, array &$config) { $mapping = $this->_getMapping($meta->getName()); if (isset($mapping['gedmo'])) { $classMapping = $mapping['gedmo']; if (isset($classMapping['uploadable'])) { $uploadable = $classMapping['uploadable']; $config['uploadable'] = true; $config['allowOverwrite'] = isset($uploadable['allowOverwrite']) ? (bool) $uploadable['allowOverwrite'] : false; $config['appendNumber'] = isset($uploadable['appendNumber']) ? (bool) $uploadable['appendNumber'] : false; $config['path'] = $uploadable['path'] ?? ''; $config['pathMethod'] = $uploadable['pathMethod'] ?? ''; $config['callback'] = $uploadable['callback'] ?? ''; $config['fileMimeTypeField'] = false; $config['fileNameField'] = false; $config['filePathField'] = false; $config['fileSizeField'] = false; $config['filenameGenerator'] = $uploadable['filenameGenerator'] ?? Validator::FILENAME_GENERATOR_NONE; $config['maxSize'] = isset($uploadable['maxSize']) ? (float) $uploadable['maxSize'] : (float) 0; $config['allowedTypes'] = $uploadable['allowedTypes'] ?? ''; $config['disallowedTypes'] = $uploadable['disallowedTypes'] ?? ''; if (isset($mapping['fields'])) { foreach ($mapping['fields'] as $field => $info) { if (isset($info['gedmo']) && array_key_exists(0, $info['gedmo'])) { if ('uploadableFileMimeType' === $info['gedmo'][0]) { $config['fileMimeTypeField'] = $field; } elseif ('uploadableFileSize' === $info['gedmo'][0]) { $config['fileSizeField'] = $field; } elseif ('uploadableFileName' === $info['gedmo'][0]) { $config['fileNameField'] = $field; } elseif ('uploadableFilePath' === $info['gedmo'][0]) { $config['filePathField'] = $field; } } } } Validator::validateConfiguration($meta, $config); } } } protected function _loadMappingFile($file) { return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)); } } doctrine-extensions/src/Uploadable/Mapping/Validator.php 0000644 00000020125 15117737237 0017455 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\Mapping; use Doctrine\Persistence\Mapping\ClassMetadata; use Gedmo\Exception\InvalidMappingException; use Gedmo\Exception\UploadableCantWriteException; use Gedmo\Exception\UploadableInvalidPathException; use Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorInterface; /** * This class is used to validate mapping information * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class Validator { public const UPLOADABLE_FILE_MIME_TYPE = 'UploadableFileMimeType'; public const UPLOADABLE_FILE_NAME = 'UploadableFileName'; public const UPLOADABLE_FILE_PATH = 'UploadableFilePath'; public const UPLOADABLE_FILE_SIZE = 'UploadableFileSize'; public const FILENAME_GENERATOR_SHA1 = 'SHA1'; public const FILENAME_GENERATOR_ALPHANUMERIC = 'ALPHANUMERIC'; public const FILENAME_GENERATOR_NONE = 'NONE'; /** * Determines if we should throw an exception in the case the "allowedTypes" and * "disallowedTypes" options are BOTH set. Useful for testing purposes * * @var bool */ public static $enableMimeTypesConfigException = true; /** * List of types which are valid for UploadableFileMimeType field * * @var array */ public static $validFileMimeTypeTypes = [ 'string', ]; /** * List of types which are valid for UploadableFileName field * * @var array */ public static $validFileNameTypes = [ 'string', ]; /** * List of types which are valid for UploadableFilePath field * * @var array */ public static $validFilePathTypes = [ 'string', ]; /** * List of types which are valid for UploadableFileSize field for ORM * * @var array */ public static $validFileSizeTypes = [ 'decimal', ]; /** * List of types which are valid for UploadableFileSize field for ODM * * @var array */ public static $validFileSizeTypesODM = [ 'float', ]; /** * Whether to validate if the directory of the file exists and is writable, useful to disable it when using * stream wrappers which don't support is_dir (like Gaufrette) * * @var bool */ public static $validateWritableDirectory = true; /** * @param string $field * * @return void */ public static function validateFileNameField(ClassMetadata $meta, $field) { self::validateField($meta, $field, self::UPLOADABLE_FILE_NAME, self::$validFileNameTypes); } /** * @param string $field * * @return void */ public static function validateFileMimeTypeField(ClassMetadata $meta, $field) { self::validateField($meta, $field, self::UPLOADABLE_FILE_MIME_TYPE, self::$validFileMimeTypeTypes); } /** * @param string $field * * @return void */ public static function validateFilePathField(ClassMetadata $meta, $field) { self::validateField($meta, $field, self::UPLOADABLE_FILE_PATH, self::$validFilePathTypes); } /** * @param string $field * * @return void */ public static function validateFileSizeField(ClassMetadata $meta, $field) { self::validateField($meta, $field, self::UPLOADABLE_FILE_SIZE, self::$validFileSizeTypes); } /** * @param ClassMetadata $meta * @param string $field * @param string $uploadableField * @param string[] $validFieldTypes * * @return void */ public static function validateField($meta, $field, $uploadableField, $validFieldTypes) { if ($meta->isMappedSuperclass) { return; } $fieldMapping = $meta->getFieldMapping($field); if (!in_array($fieldMapping['type'], $validFieldTypes, true)) { $msg = 'Field "%s" to work as an "%s" field must be of one of the following types: "%s".'; throw new InvalidMappingException(sprintf($msg, $field, $uploadableField, implode(', ', $validFieldTypes))); } } /** * @param string $path * * @return void */ public static function validatePath($path) { if (!is_string($path) || '' === $path) { throw new UploadableInvalidPathException('Path must be a string containing the path to a valid directory.'); } if (!self::$validateWritableDirectory) { return; } if (!is_dir($path) && !@mkdir($path, 0777, true)) { throw new UploadableInvalidPathException(sprintf('Unable to create "%s" directory.', $path)); } if (!is_writable($path)) { throw new UploadableCantWriteException(sprintf('Directory "%s" is not writable.', $path)); } } /** * @return void */ public static function validateConfiguration(ClassMetadata $meta, array &$config) { if (!$config['filePathField'] && !$config['fileNameField']) { throw new InvalidMappingException(sprintf('Class "%s" must have an UploadableFilePath or UploadableFileName field.', $meta->getName())); } $refl = $meta->getReflectionClass(); if ('' !== $config['pathMethod'] && !$refl->hasMethod($config['pathMethod'])) { throw new InvalidMappingException(sprintf('Class "%s" doesn\'t have method "%s"!', $meta->getName(), $config['pathMethod'])); } if ('' !== $config['callback'] && !$refl->hasMethod($config['callback'])) { throw new InvalidMappingException(sprintf('Class "%s" doesn\'t have method "%s"!', $meta->getName(), $config['callback'])); } $config['maxSize'] = (float) $config['maxSize']; if ($config['maxSize'] < 0) { throw new InvalidMappingException(sprintf('Option "maxSize" must be a number >= 0 for class "%s".', $meta->getName())); } if (self::$enableMimeTypesConfigException && '' !== $config['allowedTypes'] && '' !== $config['disallowedTypes']) { $msg = 'You\'ve set "allowedTypes" and "disallowedTypes" options. You must set only one in class "%s".'; throw new InvalidMappingException(sprintf($msg, $meta->getName())); } $config['allowedTypes'] = $config['allowedTypes'] ? (false !== strpos($config['allowedTypes'], ',') ? explode(',', $config['allowedTypes']) : [$config['allowedTypes']]) : false; $config['disallowedTypes'] = $config['disallowedTypes'] ? (false !== strpos($config['disallowedTypes'], ',') ? explode(',', $config['disallowedTypes']) : [$config['disallowedTypes']]) : false; if ($config['fileNameField']) { self::validateFileNameField($meta, $config['fileNameField']); } if ($config['filePathField']) { self::validateFilePathField($meta, $config['filePathField']); } if ($config['fileMimeTypeField']) { self::validateFileMimeTypeField($meta, $config['fileMimeTypeField']); } if ($config['fileSizeField']) { self::validateFileSizeField($meta, $config['fileSizeField']); } switch ((string) $config['filenameGenerator']) { case self::FILENAME_GENERATOR_ALPHANUMERIC: case self::FILENAME_GENERATOR_SHA1: case self::FILENAME_GENERATOR_NONE: break; default: if (!class_exists($config['filenameGenerator']) || !is_subclass_of($config['filenameGenerator'], FilenameGeneratorInterface::class)) { throw new InvalidMappingException(sprintf('Class "%s" needs a valid value for filenameGenerator. It can be: SHA1, ALPHANUMERIC, NONE or a class implementing %s.', $meta->getName(), FilenameGeneratorInterface::class)); } } } } doctrine-extensions/src/Uploadable/MimeType/MimeTypeGuesser.php 0000644 00000002415 15117737237 0020757 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\MimeType; use Gedmo\Exception\UploadableFileNotReadableException; use Gedmo\Exception\UploadableInvalidFileException; /** * Mime type guesser * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> * * @final since gedmo/doctrine-extensions 3.11 */ class MimeTypeGuesser implements MimeTypeGuesserInterface { public function guess($filePath) { if (!is_file($filePath)) { throw new UploadableInvalidFileException(sprintf('File "%s" does not exist.', $filePath)); } if (!is_readable($filePath)) { throw new UploadableFileNotReadableException(sprintf('File "%s" is not readable.', $filePath)); } if (function_exists('finfo_open')) { if (!$finfo = new \finfo(FILEINFO_MIME_TYPE)) { return null; } return $finfo->file($filePath); } return null; } } doctrine-extensions/src/Uploadable/MimeType/MimeTypeGuesserInterface.php 0000644 00000001230 15117737237 0022572 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\MimeType; /** * Interface for mime type guessers * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface MimeTypeGuesserInterface { /** * @param string $filePath * * @return string|null */ public function guess($filePath); } doctrine-extensions/src/Uploadable/MimeType/MimeTypesExtensionsMap.php 0000644 00000101276 15117737237 0022327 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable\MimeType; /** * Class that holds a map of mime types and their default extensions * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ abstract class MimeTypesExtensionsMap { /** * Map of mime types and their default extensions. * * @var array */ public static $map = [ 'application/andrew-inset' => 'ez', 'application/applixware' => 'aw', 'application/atom+xml' => 'atom', 'application/atomcat+xml' => 'atomcat', 'application/atomsvc+xml' => 'atomsvc', 'application/ccxml+xml' => 'ccxml', 'application/cdmi-capability' => 'cdmia', 'application/cdmi-container' => 'cdmic', 'application/cdmi-domain' => 'cdmid', 'application/cdmi-object' => 'cdmio', 'application/cdmi-queue' => 'cdmiq', 'application/cu-seeme' => 'cu', 'application/davmount+xml' => 'davmount', 'application/dssc+der' => 'dssc', 'application/dssc+xml' => 'xdssc', 'application/ecmascript' => 'ecma', 'application/emma+xml' => 'emma', 'application/epub+zip' => 'epub', 'application/exi' => 'exi', 'application/font-tdpfr' => 'pfr', 'application/hyperstudio' => 'stk', 'application/inkml+xml' => 'ink', 'application/ipfix' => 'ipfix', 'application/java-archive' => 'jar', 'application/java-serialized-object' => 'ser', 'application/java-vm' => 'class', 'application/javascript' => 'js', 'application/json' => 'json', 'application/lost+xml' => 'lostxml', 'application/mac-binhex40' => 'hqx', 'application/mac-compactpro' => 'cpt', 'application/mads+xml' => 'mads', 'application/marc' => 'mrc', 'application/marcxml+xml' => 'mrcx', 'application/mathematica' => 'ma', 'application/mathml+xml' => 'mathml', 'application/mbox' => 'mbox', 'application/mediaservercontrol+xml' => 'mscml', 'application/metalink4+xml' => 'meta4', 'application/mets+xml' => 'mets', 'application/mods+xml' => 'mods', 'application/mp21' => 'm21', 'application/mp4' => 'mp4s', 'application/msword' => 'doc', 'application/mxf' => 'mxf', 'application/octet-stream' => 'bin', 'application/oda' => 'oda', 'application/oebps-package+xml' => 'opf', 'application/ogg' => 'ogx', 'application/onenote' => 'onetoc', 'application/oxps' => 'oxps', 'application/patch-ops-error+xml' => 'xer', 'application/pdf' => 'pdf', 'application/pgp-encrypted' => 'pgp', 'application/pgp-signature' => 'asc', 'application/pics-rules' => 'prf', 'application/pkcs10' => 'p10', 'application/pkcs7-mime' => 'p7m', 'application/pkcs7-signature' => 'p7s', 'application/pkcs8' => 'p8', 'application/pkix-attr-cert' => 'ac', 'application/pkix-cert' => 'cer', 'application/pkix-crl' => 'crl', 'application/pkix-pkipath' => 'pkipath', 'application/pkixcmp' => 'pki', 'application/pls+xml' => 'pls', 'application/postscript' => 'ai', 'application/prs.cww' => 'cww', 'application/pskc+xml' => 'pskcxml', 'application/rdf+xml' => 'rdf', 'application/reginfo+xml' => 'rif', 'application/relax-ng-compact-syntax' => 'rnc', 'application/resource-lists+xml' => 'rl', 'application/resource-lists-diff+xml' => 'rld', 'application/rls-services+xml' => 'rs', 'application/rpki-ghostbusters' => 'gbr', 'application/rpki-manifest' => 'mft', 'application/rpki-roa' => 'roa', 'application/rsd+xml' => 'rsd', 'application/rss+xml' => 'rss', 'application/rtf' => 'rtf', 'application/sbml+xml' => 'sbml', 'application/scvp-cv-request' => 'scq', 'application/scvp-cv-response' => 'scs', 'application/scvp-vp-request' => 'spq', 'application/scvp-vp-response' => 'spp', 'application/sdp' => 'sdp', 'application/set-payment-initiation' => 'setpay', 'application/set-registration-initiation' => 'setreg', 'application/shf+xml' => 'shf', 'application/smil+xml' => 'smi', 'application/sparql-query' => 'rq', 'application/sparql-results+xml' => 'srx', 'application/srgs' => 'gram', 'application/srgs+xml' => 'grxml', 'application/sru+xml' => 'sru', 'application/ssml+xml' => 'ssml', 'application/tei+xml' => 'tei', 'application/thraud+xml' => 'tfi', 'application/timestamped-data' => 'tsd', 'application/vnd.3gpp.pic-bw-large' => 'plb', 'application/vnd.3gpp.pic-bw-small' => 'psb', 'application/vnd.3gpp.pic-bw-var' => 'pvb', 'application/vnd.3gpp2.tcap' => 'tcap', 'application/vnd.3m.post-it-notes' => 'pwn', 'application/vnd.accpac.simply.aso' => 'aso', 'application/vnd.accpac.simply.imp' => 'imp', 'application/vnd.acucobol' => 'acu', 'application/vnd.acucorp' => 'atc', 'application/vnd.adobe.air-application-installer-package+zip' => 'air', 'application/vnd.adobe.fxp' => 'fxp', 'application/vnd.adobe.xdp+xml' => 'xdp', 'application/vnd.adobe.xfdf' => 'xfdf', 'application/vnd.ahead.space' => 'ahead', 'application/vnd.airzip.filesecure.azf' => 'azf', 'application/vnd.airzip.filesecure.azs' => 'azs', 'application/vnd.amazon.ebook' => 'azw', 'application/vnd.americandynamics.acc' => 'acc', 'application/vnd.amiga.ami' => 'ami', 'application/vnd.android.package-archive' => 'apk', 'application/vnd.anser-web-certificate-issue-initiation' => 'cii', 'application/vnd.anser-web-funds-transfer-initiation' => 'fti', 'application/vnd.antix.game-component' => 'atx', 'application/vnd.apple.installer+xml' => 'mpkg', 'application/vnd.apple.mpegurl' => 'm3u8', 'application/vnd.aristanetworks.swi' => 'swi', 'application/vnd.astraea-software.iota' => 'iota', 'application/vnd.audiograph' => 'aep', 'application/vnd.blueice.multipass' => 'mpm', 'application/vnd.bmi' => 'bmi', 'application/vnd.businessobjects' => 'rep', 'application/vnd.chemdraw+xml' => 'cdxml', 'application/vnd.chipnuts.karaoke-mmd' => 'mmd', 'application/vnd.cinderella' => 'cdy', 'application/vnd.claymore' => 'cla', 'application/vnd.cloanto.rp9' => 'rp9', 'application/vnd.clonk.c4group' => 'c4g', 'application/vnd.cluetrust.cartomobile-config' => 'c11amc', 'application/vnd.cluetrust.cartomobile-config-pkg' => 'c11amz', 'application/vnd.commonspace' => 'csp', 'application/vnd.contact.cmsg' => 'cdbcmsg', 'application/vnd.cosmocaller' => 'cmc', 'application/vnd.crick.clicker' => 'clkx', 'application/vnd.crick.clicker.keyboard' => 'clkk', 'application/vnd.crick.clicker.palette' => 'clkp', 'application/vnd.crick.clicker.template' => 'clkt', 'application/vnd.crick.clicker.wordbank' => 'clkw', 'application/vnd.criticaltools.wbs+xml' => 'wbs', 'application/vnd.ctc-posml' => 'pml', 'application/vnd.cups-ppd' => 'ppd', 'application/vnd.curl.car' => 'car', 'application/vnd.curl.pcurl' => 'pcurl', 'application/vnd.data-vision.rdz' => 'rdz', 'application/vnd.dece.data' => 'uvf', 'application/vnd.dece.ttml+xml' => 'uvt', 'application/vnd.dece.unspecified' => 'uvx', 'application/vnd.dece.zip' => 'uvz', 'application/vnd.denovo.fcselayout-link' => 'fe_launch', 'application/vnd.dna' => 'dna', 'application/vnd.dolby.mlp' => 'mlp', 'application/vnd.dpgraph' => 'dpg', 'application/vnd.dreamfactory' => 'dfac', 'application/vnd.dvb.ait' => 'ait', 'application/vnd.dvb.service' => 'svc', 'application/vnd.dynageo' => 'geo', 'application/vnd.ecowin.chart' => 'mag', 'application/vnd.enliven' => 'nml', 'application/vnd.epson.esf' => 'esf', 'application/vnd.epson.msf' => 'msf', 'application/vnd.epson.quickanime' => 'qam', 'application/vnd.epson.salt' => 'slt', 'application/vnd.epson.ssf' => 'ssf', 'application/vnd.eszigno3+xml' => 'es3', 'application/vnd.ezpix-album' => 'ez2', 'application/vnd.ezpix-package' => 'ez3', 'application/vnd.fdf' => 'fdf', 'application/vnd.fdsn.mseed' => 'mseed', 'application/vnd.fdsn.seed' => 'seed', 'application/vnd.flographit' => 'gph', 'application/vnd.fluxtime.clip' => 'ftc', 'application/vnd.framemaker' => 'fm', 'application/vnd.frogans.fnc' => 'fnc', 'application/vnd.frogans.ltf' => 'ltf', 'application/vnd.fsc.weblaunch' => 'fsc', 'application/vnd.fujitsu.oasys' => 'oas', 'application/vnd.fujitsu.oasys2' => 'oa2', 'application/vnd.fujitsu.oasys3' => 'oa3', 'application/vnd.fujitsu.oasysgp' => 'fg5', 'application/vnd.fujitsu.oasysprs' => 'bh2', 'application/vnd.fujixerox.ddd' => 'ddd', 'application/vnd.fujixerox.docuworks' => 'xdw', 'application/vnd.fujixerox.docuworks.binder' => 'xbd', 'application/vnd.fuzzysheet' => 'fzs', 'application/vnd.genomatix.tuxedo' => 'txd', 'application/vnd.geogebra.file' => 'ggb', 'application/vnd.geogebra.tool' => 'ggt', 'application/vnd.geometry-explorer' => 'gex', 'application/vnd.geonext' => 'gxt', 'application/vnd.geoplan' => 'g2w', 'application/vnd.geospace' => 'g3w', 'application/vnd.gmx' => 'gmx', 'application/vnd.google-earth.kml+xml' => 'kml', 'application/vnd.google-earth.kmz' => 'kmz', 'application/vnd.grafeq' => 'gqf', 'application/vnd.groove-account' => 'gac', 'application/vnd.groove-help' => 'ghf', 'application/vnd.groove-identity-message' => 'gim', 'application/vnd.groove-injector' => 'grv', 'application/vnd.groove-tool-message' => 'gtm', 'application/vnd.groove-tool-template' => 'tpl', 'application/vnd.groove-vcard' => 'vcg', 'application/vnd.hal+xml' => 'hal', 'application/vnd.handheld-entertainment+xml' => 'zmm', 'application/vnd.hbci' => 'hbci', 'application/vnd.hhe.lesson-player' => 'les', 'application/vnd.hp-hpgl' => 'hpgl', 'application/vnd.hp-hpid' => 'hpid', 'application/vnd.hp-hps' => 'hps', 'application/vnd.hp-jlyt' => 'jlt', 'application/vnd.hp-pcl' => 'pcl', 'application/vnd.hp-pclxl' => 'pclxl', 'application/vnd.hydrostatix.sof-data' => 'sfd-hdstx', 'application/vnd.hzn-3d-crossword' => 'x3d', 'application/vnd.ibm.minipay' => 'mpy', 'application/vnd.ibm.modcap' => 'afp', 'application/vnd.ibm.rights-management' => 'irm', 'application/vnd.ibm.secure-container' => 'sc', 'application/vnd.iccprofile' => 'icc', 'application/vnd.igloader' => 'igl', 'application/vnd.immervision-ivp' => 'ivp', 'application/vnd.immervision-ivu' => 'ivu', 'application/vnd.insors.igm' => 'igm', 'application/vnd.intercon.formnet' => 'xpw', 'application/vnd.intergeo' => 'i2g', 'application/vnd.intu.qbo' => 'qbo', 'application/vnd.intu.qfx' => 'qfx', 'application/vnd.ipunplugged.rcprofile' => 'rcprofile', 'application/vnd.irepository.package+xml' => 'irp', 'application/vnd.is-xpr' => 'xpr', 'application/vnd.isac.fcs' => 'fcs', 'application/vnd.jam' => 'jam', 'application/vnd.jcp.javame.midlet-rms' => 'rms', 'application/vnd.jisp' => 'jisp', 'application/vnd.joost.joda-archive' => 'joda', 'application/vnd.kahootz' => 'ktz', 'application/vnd.kde.karbon' => 'karbon', 'application/vnd.kde.kchart' => 'chrt', 'application/vnd.kde.kformula' => 'kfo', 'application/vnd.kde.kivio' => 'flw', 'application/vnd.kde.kontour' => 'kon', 'application/vnd.kde.kpresenter' => 'kpr', 'application/vnd.kde.kspread' => 'ksp', 'application/vnd.kde.kword' => 'kwd', 'application/vnd.kenameaapp' => 'htke', 'application/vnd.kidspiration' => 'kia', 'application/vnd.kinar' => 'kne', 'application/vnd.koan' => 'skp', 'application/vnd.kodak-descriptor' => 'sse', 'application/vnd.las.las+xml' => 'lasxml', 'application/vnd.llamagraphics.life-balance.desktop' => 'lbd', 'application/vnd.llamagraphics.life-balance.exchange+xml' => 'lbe', 'application/vnd.lotus-1-2-3' => '123', 'application/vnd.lotus-approach' => 'apr', 'application/vnd.lotus-freelance' => 'pre', 'application/vnd.lotus-notes' => 'nsf', 'application/vnd.lotus-organizer' => 'org', 'application/vnd.lotus-screencam' => 'scm', 'application/vnd.lotus-wordpro' => 'lwp', 'application/vnd.macports.portpkg' => 'portpkg', 'application/vnd.mcd' => 'mcd', 'application/vnd.medcalcdata' => 'mc1', 'application/vnd.mediastation.cdkey' => 'cdkey', 'application/vnd.mfer' => 'mwf', 'application/vnd.mfmp' => 'mfm', 'application/vnd.micrografx.flo' => 'flo', 'application/vnd.micrografx.igx' => 'igx', 'application/vnd.mif' => 'mif', 'application/vnd.mobius.daf' => 'daf', 'application/vnd.mobius.dis' => 'dis', 'application/vnd.mobius.mbk' => 'mbk', 'application/vnd.mobius.mqy' => 'mqy', 'application/vnd.mobius.msl' => 'msl', 'application/vnd.mobius.plc' => 'plc', 'application/vnd.mobius.txf' => 'txf', 'application/vnd.mophun.application' => 'mpn', 'application/vnd.mophun.certificate' => 'mpc', 'application/vnd.mozilla.xul+xml' => 'xul', 'application/vnd.ms-artgalry' => 'cil', 'application/vnd.ms-cab-compressed' => 'cab', 'application/vnd.ms-excel' => 'xls', 'application/vnd.ms-excel.addin.macroenabled.12' => 'xlam', 'application/vnd.ms-excel.sheet.binary.macroenabled.12' => 'xlsb', 'application/vnd.ms-excel.sheet.macroenabled.12' => 'xlsm', 'application/vnd.ms-excel.template.macroenabled.12' => 'xltm', 'application/vnd.ms-fontobject' => 'eot', 'application/vnd.ms-htmlhelp' => 'chm', 'application/vnd.ms-ims' => 'ims', 'application/vnd.ms-lrm' => 'lrm', 'application/vnd.ms-officetheme' => 'thmx', 'application/vnd.ms-pki.seccat' => 'cat', 'application/vnd.ms-pki.stl' => 'stl', 'application/vnd.ms-powerpoint' => 'ppt', 'application/vnd.ms-powerpoint.addin.macroenabled.12' => 'ppam', 'application/vnd.ms-powerpoint.presentation.macroenabled.12' => 'pptm', 'application/vnd.ms-powerpoint.slide.macroenabled.12' => 'sldm', 'application/vnd.ms-powerpoint.slideshow.macroenabled.12' => 'ppsm', 'application/vnd.ms-powerpoint.template.macroenabled.12' => 'potm', 'application/vnd.ms-project' => 'mpp', 'application/vnd.ms-word.document.macroenabled.12' => 'docm', 'application/vnd.ms-word.template.macroenabled.12' => 'dotm', 'application/vnd.ms-works' => 'wps', 'application/vnd.ms-wpl' => 'wpl', 'application/vnd.ms-xpsdocument' => 'xps', 'application/vnd.mseq' => 'mseq', 'application/vnd.musician' => 'mus', 'application/vnd.muvee.style' => 'msty', 'application/vnd.mynfc' => 'taglet', 'application/vnd.neurolanguage.nlu' => 'nlu', 'application/vnd.noblenet-directory' => 'nnd', 'application/vnd.noblenet-sealer' => 'nns', 'application/vnd.noblenet-web' => 'nnw', 'application/vnd.nokia.n-gage.data' => 'ngdat', 'application/vnd.nokia.n-gage.symbian.install' => 'n-gage', 'application/vnd.nokia.radio-preset' => 'rpst', 'application/vnd.nokia.radio-presets' => 'rpss', 'application/vnd.novadigm.edm' => 'edm', 'application/vnd.novadigm.edx' => 'edx', 'application/vnd.novadigm.ext' => 'ext', 'application/vnd.oasis.opendocument.chart' => 'odc', 'application/vnd.oasis.opendocument.chart-template' => 'otc', 'application/vnd.oasis.opendocument.database' => 'odb', 'application/vnd.oasis.opendocument.formula' => 'odf', 'application/vnd.oasis.opendocument.formula-template' => 'odft', 'application/vnd.oasis.opendocument.graphics' => 'odg', 'application/vnd.oasis.opendocument.graphics-template' => 'otg', 'application/vnd.oasis.opendocument.image' => 'odi', 'application/vnd.oasis.opendocument.image-template' => 'oti', 'application/vnd.oasis.opendocument.presentation' => 'odp', 'application/vnd.oasis.opendocument.presentation-template' => 'otp', 'application/vnd.oasis.opendocument.spreadsheet' => 'ods', 'application/vnd.oasis.opendocument.spreadsheet-template' => 'ots', 'application/vnd.oasis.opendocument.text' => 'odt', 'application/vnd.oasis.opendocument.text-master' => 'odm', 'application/vnd.oasis.opendocument.text-template' => 'ott', 'application/vnd.oasis.opendocument.text-web' => 'oth', 'application/vnd.olpc-sugar' => 'xo', 'application/vnd.oma.dd2+xml' => 'dd2', 'application/vnd.openofficeorg.extension' => 'oxt', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', 'application/vnd.openxmlformats-officedocument.presentationml.slide' => 'sldx', 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx', 'application/vnd.openxmlformats-officedocument.presentationml.template' => 'potx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => 'xltx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => 'dotx', 'application/vnd.osgeo.mapguide.package' => 'mgp', 'application/vnd.osgi.dp' => 'dp', 'application/vnd.palm' => 'pdb', 'application/vnd.pawaafile' => 'paw', 'application/vnd.pg.format' => 'str', 'application/vnd.pg.osasli' => 'ei6', 'application/vnd.picsel' => 'efif', 'application/vnd.pmi.widget' => 'wg', 'application/vnd.pocketlearn' => 'plf', 'application/vnd.powerbuilder6' => 'pbd', 'application/vnd.previewsystems.box' => 'box', 'application/vnd.proteus.magazine' => 'mgz', 'application/vnd.publishare-delta-tree' => 'qps', 'application/vnd.pvi.ptid1' => 'ptid', 'application/vnd.quark.quarkxpress' => 'qxd', 'application/vnd.realvnc.bed' => 'bed', 'application/vnd.recordare.musicxml' => 'mxl', 'application/vnd.recordare.musicxml+xml' => 'musicxml', 'application/vnd.rig.cryptonote' => 'cryptonote', 'application/vnd.rim.cod' => 'cod', 'application/vnd.rn-realmedia' => 'rm', 'application/vnd.route66.link66+xml' => 'link66', 'application/vnd.sailingtracker.track' => 'st', 'application/vnd.seemail' => 'see', 'application/vnd.sema' => 'sema', 'application/vnd.semd' => 'semd', 'application/vnd.semf' => 'semf', 'application/vnd.shana.informed.formdata' => 'ifm', 'application/vnd.shana.informed.formtemplate' => 'itp', 'application/vnd.shana.informed.interchange' => 'iif', 'application/vnd.shana.informed.package' => 'ipk', 'application/vnd.simtech-mindmapper' => 'twd', 'application/vnd.smaf' => 'mmf', 'application/vnd.smart.teacher' => 'teacher', 'application/vnd.solent.sdkm+xml' => 'sdkm', 'application/vnd.spotfire.dxp' => 'dxp', 'application/vnd.spotfire.sfs' => 'sfs', 'application/vnd.stardivision.calc' => 'sdc', 'application/vnd.stardivision.draw' => 'sda', 'application/vnd.stardivision.impress' => 'sdd', 'application/vnd.stardivision.math' => 'smf', 'application/vnd.stardivision.writer' => 'sdw', 'application/vnd.stardivision.writer-global' => 'sgl', 'application/vnd.stepmania.package' => 'smzip', 'application/vnd.stepmania.stepchart' => 'sm', 'application/vnd.sun.xml.calc' => 'sxc', 'application/vnd.sun.xml.calc.template' => 'stc', 'application/vnd.sun.xml.draw' => 'sxd', 'application/vnd.sun.xml.draw.template' => 'std', 'application/vnd.sun.xml.impress' => 'sxi', 'application/vnd.sun.xml.impress.template' => 'sti', 'application/vnd.sun.xml.math' => 'sxm', 'application/vnd.sun.xml.writer' => 'sxw', 'application/vnd.sun.xml.writer.global' => 'sxg', 'application/vnd.sun.xml.writer.template' => 'stw', 'application/vnd.sus-calendar' => 'sus', 'application/vnd.svd' => 'svd', 'application/vnd.symbian.install' => 'sis', 'application/vnd.syncml+xml' => 'xsm', 'application/vnd.syncml.dm+wbxml' => 'bdm', 'application/vnd.syncml.dm+xml' => 'xdm', 'application/vnd.tao.intent-module-archive' => 'tao', 'application/vnd.tcpdump.pcap' => 'pcap', 'application/vnd.tmobile-livetv' => 'tmo', 'application/vnd.trid.tpt' => 'tpt', 'application/vnd.triscape.mxs' => 'mxs', 'application/vnd.trueapp' => 'tra', 'application/vnd.ufdl' => 'ufd', 'application/vnd.uiq.theme' => 'utz', 'application/vnd.umajin' => 'umj', 'application/vnd.unity' => 'unityweb', 'application/vnd.uoml+xml' => 'uoml', 'application/vnd.vcx' => 'vcx', 'application/vnd.visio' => 'vsd', 'application/vnd.visionary' => 'vis', 'application/vnd.vsf' => 'vsf', 'application/vnd.wap.wbxml' => 'wbxml', 'application/vnd.wap.wmlc' => 'wmlc', 'application/vnd.wap.wmlscriptc' => 'wmlsc', 'application/vnd.webturbo' => 'wtb', 'application/vnd.wolfram.player' => 'nbp', 'application/vnd.wordperfect' => 'wpd', 'application/vnd.wqd' => 'wqd', 'application/vnd.wt.stf' => 'stf', 'application/vnd.xara' => 'xar', 'application/vnd.xfdl' => 'xfdl', 'application/vnd.yamaha.hv-dic' => 'hvd', 'application/vnd.yamaha.hv-script' => 'hvs', 'application/vnd.yamaha.hv-voice' => 'hvp', 'application/vnd.yamaha.openscoreformat' => 'osf', 'application/vnd.yamaha.openscoreformat.osfpvg+xml' => 'osfpvg', 'application/vnd.yamaha.smaf-audio' => 'saf', 'application/vnd.yamaha.smaf-phrase' => 'spf', 'application/vnd.yellowriver-custom-menu' => 'cmp', 'application/vnd.zul' => 'zir', 'application/vnd.zzazz.deck+xml' => 'zaz', 'application/voicexml+xml' => 'vxml', 'application/widget' => 'wgt', 'application/winhlp' => 'hlp', 'application/wsdl+xml' => 'wsdl', 'application/wspolicy+xml' => 'wspolicy', 'application/x-7z-compressed' => '7z', 'application/x-abiword' => 'abw', 'application/x-ace-compressed' => 'ace', 'application/x-authorware-bin' => 'aab', 'application/x-authorware-map' => 'aam', 'application/x-authorware-seg' => 'aas', 'application/x-bcpio' => 'bcpio', 'application/x-bittorrent' => 'torrent', 'application/x-bzip' => 'bz', 'application/x-bzip2' => 'bz2', 'application/x-cdlink' => 'vcd', 'application/x-chat' => 'chat', 'application/x-chess-pgn' => 'pgn', 'application/x-cpio' => 'cpio', 'application/x-csh' => 'csh', 'application/x-debian-package' => 'deb', 'application/x-director' => 'dir', 'application/x-doom' => 'wad', 'application/x-dtbncx+xml' => 'ncx', 'application/x-dtbook+xml' => 'dtb', 'application/x-dtbresource+xml' => 'res', 'application/x-dvi' => 'dvi', 'application/x-font-bdf' => 'bdf', 'application/x-font-ghostscript' => 'gsf', 'application/x-font-linux-psf' => 'psf', 'application/x-font-otf' => 'otf', 'application/x-font-pcf' => 'pcf', 'application/x-font-snf' => 'snf', 'application/x-font-ttf' => 'ttf', 'application/x-font-type1' => 'pfa', 'application/x-font-woff' => 'woff', 'application/x-futuresplash' => 'spl', 'application/x-gnumeric' => 'gnumeric', 'application/x-gtar' => 'gtar', 'application/x-hdf' => 'hdf', 'application/x-java-jnlp-file' => 'jnlp', 'application/x-latex' => 'latex', 'application/x-mobipocket-ebook' => 'prc', 'application/x-ms-application' => 'application', 'application/x-ms-wmd' => 'wmd', 'application/x-ms-wmz' => 'wmz', 'application/x-ms-xbap' => 'xbap', 'application/x-msaccess' => 'mdb', 'application/x-msbinder' => 'obd', 'application/x-mscardfile' => 'crd', 'application/x-msclip' => 'clp', 'application/x-msdownload' => 'exe', 'application/x-msmediaview' => 'mvb', 'application/x-msmetafile' => 'wmf', 'application/x-msmoney' => 'mny', 'application/x-mspublisher' => 'pub', 'application/x-msschedule' => 'scd', 'application/x-msterminal' => 'trm', 'application/x-mswrite' => 'wri', 'application/x-netcdf' => 'nc', 'application/x-pkcs12' => 'p12', 'application/x-pkcs7-certificates' => 'p7b', 'application/x-pkcs7-certreqresp' => 'p7r', 'application/x-rar-compressed' => 'rar', 'application/x-sh' => 'sh', 'application/x-shar' => 'shar', 'application/x-shockwave-flash' => 'swf', 'application/x-silverlight-app' => 'xap', 'application/x-stuffit' => 'sit', 'application/x-stuffitx' => 'sitx', 'application/x-sv4cpio' => 'sv4cpio', 'application/x-sv4crc' => 'sv4crc', 'application/x-tar' => 'tar', 'application/x-tcl' => 'tcl', 'application/x-tex' => 'tex', 'application/x-tex-tfm' => 'tfm', 'application/x-texinfo' => 'texinfo', 'application/x-ustar' => 'ustar', 'application/x-wais-source' => 'src', 'application/x-x509-ca-cert' => 'der', 'application/x-xfig' => 'fig', 'application/x-xpinstall' => 'xpi', 'application/xcap-diff+xml' => 'xdf', 'application/xenc+xml' => 'xenc', 'application/xhtml+xml' => 'xhtml', 'application/xml' => 'xml', 'application/xml-dtd' => 'dtd', 'application/xop+xml' => 'xop', 'application/xslt+xml' => 'xslt', 'application/xspf+xml' => 'xspf', 'application/xv+xml' => 'mxml', 'application/yang' => 'yang', 'application/yin+xml' => 'yin', 'application/zip' => 'zip', 'audio/adpcm' => 'adp', 'audio/basic' => 'au', 'audio/midi' => 'mid', 'audio/mp4' => 'mp4a', 'audio/mpeg' => 'mpga', 'audio/ogg' => 'oga', 'audio/vnd.dece.audio' => 'uva', 'audio/vnd.digital-winds' => 'eol', 'audio/vnd.dra' => 'dra', 'audio/vnd.dts' => 'dts', 'audio/vnd.dts.hd' => 'dtshd', 'audio/vnd.lucent.voice' => 'lvp', 'audio/vnd.ms-playready.media.pya' => 'pya', 'audio/vnd.nuera.ecelp4800' => 'ecelp4800', 'audio/vnd.nuera.ecelp7470' => 'ecelp7470', 'audio/vnd.nuera.ecelp9600' => 'ecelp9600', 'audio/vnd.rip' => 'rip', 'audio/webm' => 'weba', 'audio/x-aac' => 'aac', 'audio/x-aiff' => 'aif', 'audio/x-mpegurl' => 'm3u', 'audio/x-ms-wax' => 'wax', 'audio/x-ms-wma' => 'wma', 'audio/x-pn-realaudio' => 'ram', 'audio/x-pn-realaudio-plugin' => 'rmp', 'audio/x-wav' => 'wav', 'chemical/x-cdx' => 'cdx', 'chemical/x-cif' => 'cif', 'chemical/x-cmdf' => 'cmdf', 'chemical/x-cml' => 'cml', 'chemical/x-csml' => 'csml', 'chemical/x-xyz' => 'xyz', 'image/bmp' => 'bmp', 'image/cgm' => 'cgm', 'image/g3fax' => 'g3', 'image/gif' => 'gif', 'image/ief' => 'ief', 'image/jpeg' => 'jpeg', 'image/ktx' => 'ktx', 'image/png' => 'png', 'image/prs.btif' => 'btif', 'image/svg+xml' => 'svg', 'image/tiff' => 'tiff', 'image/vnd.adobe.photoshop' => 'psd', 'image/vnd.dece.graphic' => 'uvi', 'image/vnd.dvb.subtitle' => 'sub', 'image/vnd.djvu' => 'djvu', 'image/vnd.dwg' => 'dwg', 'image/vnd.dxf' => 'dxf', 'image/vnd.fastbidsheet' => 'fbs', 'image/vnd.fpx' => 'fpx', 'image/vnd.fst' => 'fst', 'image/vnd.fujixerox.edmics-mmr' => 'mmr', 'image/vnd.fujixerox.edmics-rlc' => 'rlc', 'image/vnd.ms-modi' => 'mdi', 'image/vnd.net-fpx' => 'npx', 'image/vnd.wap.wbmp' => 'wbmp', 'image/vnd.xiff' => 'xif', 'image/webp' => 'webp', 'image/x-cmu-raster' => 'ras', 'image/x-cmx' => 'cmx', 'image/x-freehand' => 'fh', 'image/x-icon' => 'ico', 'image/x-pcx' => 'pcx', 'image/x-pict' => 'pic', 'image/x-portable-anymap' => 'pnm', 'image/x-portable-bitmap' => 'pbm', 'image/x-portable-graymap' => 'pgm', 'image/x-portable-pixmap' => 'ppm', 'image/x-rgb' => 'rgb', 'image/x-xbitmap' => 'xbm', 'image/x-xpixmap' => 'xpm', 'image/x-xwindowdump' => 'xwd', 'message/rfc822' => 'eml', 'model/iges' => 'igs', 'model/mesh' => 'msh', 'model/vnd.collada+xml' => 'dae', 'model/vnd.dwf' => 'dwf', 'model/vnd.gdl' => 'gdl', 'model/vnd.gtw' => 'gtw', 'model/vnd.mts' => 'mts', 'model/vnd.vtu' => 'vtu', 'model/vrml' => 'wrl', 'text/calendar' => 'ics', 'text/css' => 'css', 'text/csv' => 'csv', 'text/html' => 'html', 'text/n3' => 'n3', 'text/plain' => 'txt', 'text/prs.lines.tag' => 'dsc', 'text/richtext' => 'rtx', 'text/sgml' => 'sgml', 'text/tab-separated-values' => 'tsv', 'text/troff' => 't', 'text/turtle' => 'ttl', 'text/uri-list' => 'uri', 'text/vcard' => 'vcard', 'text/vnd.curl' => 'curl', 'text/vnd.curl.dcurl' => 'dcurl', 'text/vnd.curl.scurl' => 'scurl', 'text/vnd.curl.mcurl' => 'mcurl', 'text/vnd.dvb.subtitle' => 'sub', 'text/vnd.fly' => 'fly', 'text/vnd.fmi.flexstor' => 'flx', 'text/vnd.graphviz' => 'gv', 'text/vnd.in3d.3dml' => '3dml', 'text/vnd.in3d.spot' => 'spot', 'text/vnd.sun.j2me.app-descriptor' => 'jad', 'text/vnd.wap.wml' => 'wml', 'text/vnd.wap.wmlscript' => 'wmls', 'text/x-asm' => 's', 'text/x-c' => 'c', 'text/x-fortran' => 'f', 'text/x-pascal' => 'p', 'text/x-java-source' => 'java', 'text/x-setext' => 'etx', 'text/x-uuencode' => 'uu', 'text/x-vcalendar' => 'vcs', 'text/x-vcard' => 'vcf', 'video/3gpp' => '3gp', 'video/3gpp2' => '3g2', 'video/h261' => 'h261', 'video/h263' => 'h263', 'video/h264' => 'h264', 'video/jpeg' => 'jpgv', 'video/jpm' => 'jpm', 'video/mj2' => 'mj2', 'video/mp4' => 'mp4', 'video/mpeg' => 'mpeg', 'video/ogg' => 'ogv', 'video/quicktime' => 'qt', 'video/vnd.dece.hd' => 'uvh', 'video/vnd.dece.mobile' => 'uvm', 'video/vnd.dece.pd' => 'uvp', 'video/vnd.dece.sd' => 'uvs', 'video/vnd.dece.video' => 'uvv', 'video/vnd.dvb.file' => 'dvb', 'video/vnd.fvt' => 'fvt', 'video/vnd.mpegurl' => 'mxu', 'video/vnd.ms-playready.media.pyv' => 'pyv', 'video/vnd.uvvu.mp4' => 'uvu', 'video/vnd.vivo' => 'viv', 'video/webm' => 'webm', 'video/x-f4v' => 'f4v', 'video/x-fli' => 'fli', 'video/x-flv' => 'flv', 'video/x-m4v' => 'm4v', 'video/x-ms-asf' => 'asf', 'video/x-ms-wm' => 'wm', 'video/x-ms-wmv' => 'wmv', 'video/x-ms-wmx' => 'wmx', 'video/x-ms-wvx' => 'wvx', 'video/x-msvideo' => 'avi', 'video/x-sgi-movie' => 'movie', 'x-conference/x-cooltalk' => 'ice', ]; } doctrine-extensions/src/Uploadable/Events.php 0000644 00000002304 15117737237 0015400 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable; /** * Container for all Gedmo Uploadable events * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class Events { /** * The uploadablePreFileProcess event occurs before a file is processed inside * the Uploadable listener. This means it happens before the file is validated and moved * to the configured path. * * @var string */ public const uploadablePreFileProcess = 'uploadablePreFileProcess'; /** * The uploadablePostFileProcess event occurs after a file is processed inside * the Uploadable listener. This means it happens after the file is validated and moved * to the configured path. * * @var string */ public const uploadablePostFileProcess = 'uploadablePostFileProcess'; private function __construct() { } } doctrine-extensions/src/Uploadable/Uploadable.php 0000644 00000001616 15117737237 0016211 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable; /** * This interface is not necessary but can be implemented for * Domain Objects which in some cases needs to be identified as * Uploadable * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface Uploadable { // this interface is not necessary to implement /* * @gedmo:Uploadable * to mark the class as Uploadable use class annotation @gedmo:Uploadable * this object will be able Uploadable * example: * * @gedmo:Uploadable * class MyEntity */ } doctrine-extensions/src/Uploadable/UploadableListener.php 0000644 00000062365 15117737237 0017727 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo\Uploadable; use Doctrine\Common\EventArgs; use Doctrine\DBAL\Types\Type; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\NotifyPropertyChanged; use Gedmo\Exception\UploadableCantWriteException; use Gedmo\Exception\UploadableCouldntGuessMimeTypeException; use Gedmo\Exception\UploadableExtensionException; use Gedmo\Exception\UploadableFileAlreadyExistsException; use Gedmo\Exception\UploadableFormSizeException; use Gedmo\Exception\UploadableIniSizeException; use Gedmo\Exception\UploadableInvalidMimeTypeException; use Gedmo\Exception\UploadableMaxSizeException; use Gedmo\Exception\UploadableNoFileException; use Gedmo\Exception\UploadableNoPathDefinedException; use Gedmo\Exception\UploadableNoTmpDirException; use Gedmo\Exception\UploadablePartialException; use Gedmo\Exception\UploadableUploadException; use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Mapping\MappedEventSubscriber; use Gedmo\Uploadable\Event\UploadablePostFileProcessEventArgs; use Gedmo\Uploadable\Event\UploadablePreFileProcessEventArgs; use Gedmo\Uploadable\FileInfo\FileInfoArray; use Gedmo\Uploadable\FileInfo\FileInfoInterface; use Gedmo\Uploadable\Mapping\Validator; use Gedmo\Uploadable\MimeType\MimeTypeGuesser; use Gedmo\Uploadable\MimeType\MimeTypeGuesserInterface; /** * Uploadable listener * * @author Gustavo Falco <comfortablynumb84@gmail.com> * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ class UploadableListener extends MappedEventSubscriber { public const ACTION_INSERT = 'INSERT'; public const ACTION_UPDATE = 'UPDATE'; /** * Default path to move files in * * @var string */ private $defaultPath; /** * Mime type guesser * * @var MimeTypeGuesserInterface */ private $mimeTypeGuesser; /** * Default FileInfoInterface class * * @var string */ private $defaultFileInfoClass = FileInfoArray::class; /** * Array of files to remove on postFlush * * @var array */ private $pendingFileRemovals = []; /** * Array of FileInfoInterface objects. The index is the hash of the entity owner * of the FileInfoInterface object. * * @var array */ private $fileInfoObjects = []; public function __construct(MimeTypeGuesserInterface $mimeTypeGuesser = null) { parent::__construct(); $this->mimeTypeGuesser = $mimeTypeGuesser ? $mimeTypeGuesser : new MimeTypeGuesser(); } /** * @return string[] */ public function getSubscribedEvents() { return [ 'loadClassMetadata', 'preFlush', 'onFlush', 'postFlush', ]; } /** * This event is needed in special cases where the entity needs to be updated, but it only has the * file field modified. Since we can't mark an entity as "dirty" in the "addEntityFileInfo" method, * doctrine thinks the entity has no changes, which produces that the "onFlush" event gets never called. * Here we mark the entity as dirty, so the "onFlush" event gets called, and the file is processed. * * @return void */ public function preFlush(EventArgs $args) { if (empty($this->fileInfoObjects)) { // Nothing to do return; } $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); foreach ($this->fileInfoObjects as $info) { $entity = $info['entity']; $meta = $om->getClassMetadata(get_class($entity)); $config = $this->getConfiguration($om, $meta->getName()); // If the entity is in the identity map, it means it will be updated. We need to force the // "dirty check" here by "modifying" the path. We are actually setting the same value, but // this will mark the entity as dirty, and the "onFlush" event will be fired, even if there's // no other change in the entity's fields apart from the file itself. if ($uow->isInIdentityMap($entity)) { if ($config['filePathField']) { $path = $this->getFilePathFieldValue($meta, $config, $entity); $uow->propertyChanged($entity, $config['filePathField'], $path, $path); } else { $fileName = $this->getFileNameFieldValue($meta, $config, $entity); $uow->propertyChanged($entity, $config['fileNameField'], $fileName, $fileName); } $uow->scheduleForUpdate($entity); } } } /** * Handle file-uploading depending on the action * being done with objects * * @return void */ public function onFlush(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); // Do we need to upload files? foreach ($this->fileInfoObjects as $info) { $entity = $info['entity']; $scheduledForInsert = $uow->isScheduledForInsert($entity); $scheduledForUpdate = $uow->isScheduledForUpdate($entity); $action = ($scheduledForInsert || $scheduledForUpdate) ? ($scheduledForInsert ? self::ACTION_INSERT : self::ACTION_UPDATE) : false; if ($action) { $this->processFile($ea, $entity, $action); } } // Do we need to remove any files? foreach ($ea->getScheduledObjectDeletions($uow) as $object) { $meta = $om->getClassMetadata(get_class($object)); if ($config = $this->getConfiguration($om, $meta->getName())) { if (isset($config['uploadable']) && $config['uploadable']) { $this->addFileRemoval($meta, $config, $object); } } } } /** * Handle removal of files * * @return void */ public function postFlush(EventArgs $args) { if (!empty($this->pendingFileRemovals)) { foreach ($this->pendingFileRemovals as $file) { $this->removeFile($file); } $this->pendingFileRemovals = []; } $this->fileInfoObjects = []; } /** * If it's a Uploadable object, verify if the file was uploaded. * If that's the case, process it. * * @param object $object * @param string $action * * @throws \Gedmo\Exception\UploadableNoPathDefinedException * @throws \Gedmo\Exception\UploadableCouldntGuessMimeTypeException * @throws \Gedmo\Exception\UploadableMaxSizeException * @throws \Gedmo\Exception\UploadableInvalidMimeTypeException * * @return void */ public function processFile(AdapterInterface $ea, $object, $action) { $oid = spl_object_id($object); $om = $ea->getObjectManager(); \assert($om instanceof EntityManagerInterface); $uow = $om->getUnitOfWork(); $meta = $om->getClassMetadata(get_class($object)); $config = $this->getConfiguration($om, $meta->getName()); if (!$config || !isset($config['uploadable']) || !$config['uploadable']) { // Nothing to do return; } $refl = $meta->getReflectionClass(); $fileInfo = $this->fileInfoObjects[$oid]['fileInfo']; $evm = $om->getEventManager(); if ($evm->hasListeners(Events::uploadablePreFileProcess)) { $evm->dispatchEvent(Events::uploadablePreFileProcess, new UploadablePreFileProcessEventArgs( $this, $om, $config, $fileInfo, $object, $action )); } // Validations if ($config['maxSize'] > 0 && $fileInfo->getSize() > $config['maxSize']) { $msg = 'File "%s" exceeds the maximum allowed size of %d bytes. File size: %d bytes'; throw new UploadableMaxSizeException(sprintf($msg, $fileInfo->getName(), $config['maxSize'], $fileInfo->getSize())); } $mime = $this->mimeTypeGuesser->guess($fileInfo->getTmpName()); if (!$mime) { throw new UploadableCouldntGuessMimeTypeException(sprintf('Couldn\'t guess mime type for file "%s".', $fileInfo->getName())); } if ($config['allowedTypes'] || $config['disallowedTypes']) { $ok = $config['allowedTypes'] ? false : true; $mimes = $config['allowedTypes'] ? $config['allowedTypes'] : $config['disallowedTypes']; foreach ($mimes as $m) { if ($mime === $m) { $ok = $config['allowedTypes'] ? true : false; break; } } if (!$ok) { throw new UploadableInvalidMimeTypeException(sprintf('Invalid mime type "%s" for file "%s".', $mime, $fileInfo->getName())); } } $path = $this->getPath($meta, $config, $object); if (self::ACTION_UPDATE === $action) { // First we add the original file to the pendingFileRemovals array $this->addFileRemoval($meta, $config, $object); } // We generate the filename based on configuration $generatorNamespace = 'Gedmo\Uploadable\FilenameGenerator'; switch ($config['filenameGenerator']) { case Validator::FILENAME_GENERATOR_ALPHANUMERIC: $generatorClass = $generatorNamespace.'\FilenameGeneratorAlphanumeric'; break; case Validator::FILENAME_GENERATOR_SHA1: $generatorClass = $generatorNamespace.'\FilenameGeneratorSha1'; break; case Validator::FILENAME_GENERATOR_NONE: $generatorClass = false; break; default: $generatorClass = $config['filenameGenerator']; } $info = $this->moveFile($fileInfo, $path, $generatorClass, $config['allowOverwrite'], $config['appendNumber'], $object); // We override the mime type with the guessed one $info['fileMimeType'] = $mime; if ('' !== $config['callback']) { $callbackMethod = $refl->getMethod($config['callback']); $callbackMethod->setAccessible(true); $callbackMethod->invokeArgs($object, [$info]); } if ($config['filePathField']) { $this->updateField($object, $uow, $ea, $meta, $config['filePathField'], $info['filePath']); } if ($config['fileNameField']) { $this->updateField($object, $uow, $ea, $meta, $config['fileNameField'], $info['fileName']); } if ($config['fileMimeTypeField']) { $this->updateField($object, $uow, $ea, $meta, $config['fileMimeTypeField'], $info['fileMimeType']); } if ($config['fileSizeField']) { $typeOfSizeField = Type::getType($meta->getTypeOfField($config['fileSizeField'])); $value = $typeOfSizeField->convertToPHPValue( $info['fileSize'], $om->getConnection()->getDatabasePlatform() ); $this->updateField($object, $uow, $ea, $meta, $config['fileSizeField'], $value); } $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); if ($evm->hasListeners(Events::uploadablePostFileProcess)) { $evm->dispatchEvent(Events::uploadablePostFileProcess, new UploadablePostFileProcessEventArgs( $this, $om, $config, $fileInfo, $object, $action )); } unset($this->fileInfoObjects[$oid]); } /** * Simple wrapper for the function "unlink" to ease testing * * @param string $filePath * * @return bool */ public function removeFile($filePath) { if (is_file($filePath)) { return @unlink($filePath); } return false; } /** * Moves the file to the specified path * * @param string $path * @param string|bool $filenameGeneratorClass * @param bool $overwrite * @param bool $appendNumber * @param object $object * * @return array * * @throws \Gedmo\Exception\UploadableUploadException * @throws \Gedmo\Exception\UploadableNoFileException * @throws \Gedmo\Exception\UploadableExtensionException * @throws \Gedmo\Exception\UploadableIniSizeException * @throws \Gedmo\Exception\UploadableFormSizeException * @throws \Gedmo\Exception\UploadableFileAlreadyExistsException * @throws \Gedmo\Exception\UploadablePartialException * @throws \Gedmo\Exception\UploadableNoTmpDirException * @throws \Gedmo\Exception\UploadableCantWriteException * * @phpstan-param class-string|false $filenameGeneratorClass */ public function moveFile(FileInfoInterface $fileInfo, $path, $filenameGeneratorClass = false, $overwrite = false, $appendNumber = false, $object = null) { if ($fileInfo->getError() > 0) { switch ($fileInfo->getError()) { case 1: $msg = 'Size of uploaded file "%s" exceeds limit imposed by directive "upload_max_filesize" in php.ini'; throw new UploadableIniSizeException(sprintf($msg, $fileInfo->getName())); case 2: $msg = 'Size of uploaded file "%s" exceeds limit imposed by option MAX_FILE_SIZE in your form.'; throw new UploadableFormSizeException(sprintf($msg, $fileInfo->getName())); case 3: $msg = 'File "%s" was partially uploaded.'; throw new UploadablePartialException(sprintf($msg, $fileInfo->getName())); case 4: $msg = 'No file was uploaded!'; throw new UploadableNoFileException($msg); case 6: $msg = 'Upload failed. Temp dir is missing.'; throw new UploadableNoTmpDirException($msg); case 7: $msg = 'File "%s" couldn\'t be uploaded because directory is not writable.'; throw new UploadableCantWriteException(sprintf($msg, $fileInfo->getName())); case 8: $msg = 'A PHP Extension stopped the uploaded for some reason.'; throw new UploadableExtensionException($msg); default: throw new UploadableUploadException(sprintf('There was an unknown problem while uploading file "%s"', $fileInfo->getName())); } } $info = [ 'fileName' => '', 'fileExtension' => '', 'fileWithoutExt' => '', 'origFileName' => '', 'filePath' => '', 'fileMimeType' => $fileInfo->getType(), 'fileSize' => $fileInfo->getSize(), ]; $info['fileName'] = basename($fileInfo->getName()); $info['filePath'] = $path.'/'.$info['fileName']; $hasExtension = strrpos($info['fileName'], '.'); if ($hasExtension) { $info['fileExtension'] = substr($info['filePath'], strrpos($info['filePath'], '.')); $info['fileWithoutExt'] = substr($info['filePath'], 0, strrpos($info['filePath'], '.')); } else { $info['fileWithoutExt'] = $info['filePath']; } // Save the original filename for later use $info['origFileName'] = $info['fileName']; // Now we generate the filename using the configured class if (false !== $filenameGeneratorClass) { $filename = $filenameGeneratorClass::generate( str_replace($path.'/', '', $info['fileWithoutExt']), $info['fileExtension'], $object ); $info['filePath'] = str_replace( '/'.$info['fileName'], '/'.$filename, $info['filePath'] ); $info['fileName'] = $filename; if ($pos = strrpos($info['filePath'], '.')) { // ignores positions like "./file" at 0 see #915 $info['fileWithoutExt'] = substr($info['filePath'], 0, $pos); } else { $info['fileWithoutExt'] = $info['filePath']; } } if (is_file($info['filePath'])) { if ($overwrite) { $this->cancelFileRemoval($info['filePath']); $this->removeFile($info['filePath']); } elseif ($appendNumber) { $counter = 1; $info['filePath'] = $info['fileWithoutExt'].'-'.$counter.$info['fileExtension']; do { $info['filePath'] = $info['fileWithoutExt'].'-'.(++$counter).$info['fileExtension']; } while (is_file($info['filePath'])); } else { throw new UploadableFileAlreadyExistsException(sprintf('File "%s" already exists!', $info['filePath'])); } } if (!$this->doMoveFile($fileInfo->getTmpName(), $info['filePath'], $fileInfo->isUploadedFile())) { throw new UploadableUploadException(sprintf('File "%s" was not uploaded, or there was a problem moving it to the location "%s".', $fileInfo->getName(), $path)); } return $info; } /** * Simple wrapper method used to move the file. If it's an uploaded file * it will use the "move_uploaded_file method. If it's not, it will * simple move it * * @param string $source Source file * @param string $dest Destination file * @param bool $isUploadedFile Whether this is an uploaded file? * * @return bool */ public function doMoveFile($source, $dest, $isUploadedFile = true) { return $isUploadedFile ? @move_uploaded_file($source, $dest) : @copy($source, $dest); } /** * Maps additional metadata * * @param LoadClassMetadataEventArgs $eventArgs * * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } /** * Sets the default path * * @param string $path * * @return void */ public function setDefaultPath($path) { $this->defaultPath = $path; } /** * Returns default path * * @return string|null */ public function getDefaultPath() { return $this->defaultPath; } /** * Sets file info default class * * @param string $defaultFileInfoClass * * @return void */ public function setDefaultFileInfoClass($defaultFileInfoClass) { if (!is_string($defaultFileInfoClass) || !class_exists($defaultFileInfoClass) || !is_subclass_of($defaultFileInfoClass, FileInfoInterface::class) ) { throw new \Gedmo\Exception\InvalidArgumentException(sprintf('Default FileInfo class must be a valid class, and it must implement "%s".', FileInfoInterface::class)); } $this->defaultFileInfoClass = $defaultFileInfoClass; } /** * Returns file info default class * * @return string */ public function getDefaultFileInfoClass() { return $this->defaultFileInfoClass; } /** * Adds a FileInfoInterface object for the given entity * * @param object $entity * @param array|FileInfoInterface|mixed $fileInfo * * @phpstan-assert FileInfoInterface|array $fileInfo * * @throws \RuntimeException * * @return void */ public function addEntityFileInfo($entity, $fileInfo) { $fileInfoClass = $this->getDefaultFileInfoClass(); $fileInfo = is_array($fileInfo) ? new $fileInfoClass($fileInfo) : $fileInfo; if (!$fileInfo instanceof FileInfoInterface) { $msg = 'You must pass an instance of FileInfoInterface or a valid array for entity of class "%s".'; throw new \RuntimeException(sprintf($msg, get_class($entity))); } $this->fileInfoObjects[spl_object_id($entity)] = [ 'entity' => $entity, 'fileInfo' => $fileInfo, ]; } /** * @param object $entity * * @return FileInfoInterface */ public function getEntityFileInfo($entity) { $oid = spl_object_id($entity); if (!isset($this->fileInfoObjects[$oid])) { throw new \RuntimeException(sprintf('There\'s no FileInfoInterface object for entity of class "%s".', get_class($entity))); } return $this->fileInfoObjects[$oid]['fileInfo']; } /** * @return void */ public function setMimeTypeGuesser(MimeTypeGuesserInterface $mimeTypeGuesser) { $this->mimeTypeGuesser = $mimeTypeGuesser; } /** * @return \Gedmo\Uploadable\MimeType\MimeTypeGuesserInterface */ public function getMimeTypeGuesser() { return $this->mimeTypeGuesser; } /** * @param object $object Entity * * @return string * * @throws UploadableNoPathDefinedException */ protected function getPath(ClassMetadata $meta, array $config, $object) { $path = $config['path']; if ('' === $path) { $defaultPath = $this->getDefaultPath(); if ('' !== $config['pathMethod']) { $getPathMethod = \Closure::bind(function (string $pathMethod, ?string $defaultPath): string { return $this->{$pathMethod}($defaultPath); }, $object, $meta->getReflectionClass()->getName()); $path = $getPathMethod($config['pathMethod'], $defaultPath); } elseif (null !== $defaultPath) { $path = $defaultPath; } else { $msg = 'You have to define the path to save files either in the listener, or in the class "%s"'; throw new UploadableNoPathDefinedException(sprintf($msg, $meta->getName())); } } Validator::validatePath($path); $path = rtrim($path, '\/'); return $path; } /** * @param ClassMetadata $meta * @param array $config * @param object $object Entity * * @return void */ protected function addFileRemoval($meta, $config, $object) { if ($config['filePathField']) { $this->pendingFileRemovals[] = $this->getFilePathFieldValue($meta, $config, $object); } else { $path = $this->getPath($meta, $config, $object); $fileName = $this->getFileNameFieldValue($meta, $config, $object); $this->pendingFileRemovals[] = $path.DIRECTORY_SEPARATOR.$fileName; } } /** * @param string $filePath * * @return void */ protected function cancelFileRemoval($filePath) { $k = array_search($filePath, $this->pendingFileRemovals, true); if (false !== $k) { unset($this->pendingFileRemovals[$k]); } } /** * Returns value of the entity's property * * @param string $propertyName * @param object $object * * @return mixed */ protected function getPropertyValueFromObject(ClassMetadata $meta, $propertyName, $object) { $getFilePath = \Closure::bind(function (string $propertyName) { return $this->{$propertyName}; }, $object, $meta->getReflectionClass()->getName()); return $getFilePath($propertyName); } /** * Returns the path of the entity's file * * @param object $object * * @return string */ protected function getFilePathFieldValue(ClassMetadata $meta, array $config, $object) { return $this->getPropertyValueFromObject($meta, $config['filePathField'], $object); } /** * Returns the name of the entity's file * * @param object $object * * @return string */ protected function getFileNameFieldValue(ClassMetadata $meta, array $config, $object) { return $this->getPropertyValueFromObject($meta, $config['fileNameField'], $object); } protected function getNamespace() { return __NAMESPACE__; } /** * @param object $object * @param object $uow * @param string $field * @param mixed $value * @param bool $notifyPropertyChanged * * @return void */ protected function updateField($object, $uow, AdapterInterface $ea, ClassMetadata $meta, $field, $value, $notifyPropertyChanged = true) { $property = $meta->getReflectionProperty($field); $oldValue = $property->getValue($object); $property->setValue($object, $value); if ($notifyPropertyChanged && $object instanceof NotifyPropertyChanged) { $uow = $ea->getObjectManager()->getUnitOfWork(); $uow->propertyChanged($object, $field, $oldValue, $value); } } } doctrine-extensions/src/AbstractTrackingListener.php 0000644 00000023513 15117737237 0017025 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo; use Doctrine\Common\EventArgs; use Doctrine\DBAL\Types\Type; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Types\Type as TypeODM; use Doctrine\ORM\UnitOfWork; use Doctrine\Persistence\Event\LoadClassMetadataEventArgs; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\NotifyPropertyChanged; use Doctrine\Persistence\ObjectManager; use Gedmo\Exception\UnexpectedValueException; use Gedmo\Mapping\Event\AdapterInterface; use Gedmo\Mapping\MappedEventSubscriber; /** * The AbstractTrackingListener provides generic functions for all listeners. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ abstract class AbstractTrackingListener extends MappedEventSubscriber { /** * Specifies the list of events to listen on. * * @return string[] */ public function getSubscribedEvents() { return [ 'prePersist', 'onFlush', 'loadClassMetadata', ]; } /** * Maps additional metadata for the object. * * @param LoadClassMetadataEventArgs $eventArgs * * @return void */ public function loadClassMetadata(EventArgs $eventArgs) { $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } /** * Processes object updates when the manager is flushed. * * @return void */ public function onFlush(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); // check all scheduled updates $all = array_merge($ea->getScheduledObjectInsertions($uow), $ea->getScheduledObjectUpdates($uow)); foreach ($all as $object) { $meta = $om->getClassMetadata(get_class($object)); if (!$config = $this->getConfiguration($om, $meta->getName())) { continue; } $changeSet = $ea->getObjectChangeSet($uow, $object); $needChanges = false; if ($uow->isScheduledForInsert($object) && isset($config['create'])) { foreach ($config['create'] as $field) { // Field can not exist in change set, i.e. when persisting an embedded object without a parent $new = array_key_exists($field, $changeSet) ? $changeSet[$field][1] : false; if (null === $new) { // let manual values $needChanges = true; $this->updateField($object, $ea, $meta, $field); } } } if (isset($config['update'])) { foreach ($config['update'] as $field) { $isInsertAndNull = $uow->isScheduledForInsert($object) && array_key_exists($field, $changeSet) && null === $changeSet[$field][1]; if (!isset($changeSet[$field]) || $isInsertAndNull) { // let manual values $needChanges = true; $this->updateField($object, $ea, $meta, $field); } } } if (!$uow->isScheduledForInsert($object) && isset($config['change'])) { foreach ($config['change'] as $options) { if (isset($changeSet[$options['field']])) { continue; // value was set manually } if (!is_array($options['trackedField'])) { $singleField = true; $trackedFields = [$options['trackedField']]; } else { $singleField = false; $trackedFields = $options['trackedField']; } foreach ($trackedFields as $trackedField) { $trackedChild = null; $tracked = null; $parts = explode('.', $trackedField); if (isset($parts[1])) { $tracked = $parts[0]; $trackedChild = $parts[1]; } if (!isset($tracked) || array_key_exists($trackedField, $changeSet)) { $tracked = $trackedField; $trackedChild = null; } if (isset($changeSet[$tracked])) { $changes = $changeSet[$tracked]; if (isset($trackedChild)) { $changingObject = $changes[1]; if (!is_object($changingObject)) { throw new UnexpectedValueException("Field - [{$tracked}] is expected to be object in class - {$meta->getName()}"); } $objectMeta = $om->getClassMetadata(get_class($changingObject)); $om->initializeObject($changingObject); $value = $objectMeta->getReflectionProperty($trackedChild)->getValue($changingObject); } else { $value = $changes[1]; } $configuredValues = $this->getPhpValues($options['value'], $meta->getTypeOfField($tracked), $om); if (null === $configuredValues || ($singleField && in_array($value, $configuredValues, true))) { $needChanges = true; $this->updateField($object, $ea, $meta, $options['field']); } } } } } if ($needChanges) { $ea->recomputeSingleObjectChangeSet($uow, $meta, $object); } } } /** * Processes updates when an object is persisted in the manager. * * @return void */ public function prePersist(EventArgs $args) { $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); $object = $ea->getObject(); $meta = $om->getClassMetadata(get_class($object)); if ($config = $this->getConfiguration($om, $meta->getName())) { if (isset($config['update'])) { foreach ($config['update'] as $field) { if (null === $meta->getReflectionProperty($field)->getValue($object)) { // let manual values $this->updateField($object, $ea, $meta, $field); } } } if (isset($config['create'])) { foreach ($config['create'] as $field) { if (null === $meta->getReflectionProperty($field)->getValue($object)) { // let manual values $this->updateField($object, $ea, $meta, $field); } } } } } /** * Get the value for an updated field. * * @param ClassMetadata $meta * @param string $field * @param AdapterInterface $eventAdapter * * @return mixed */ abstract protected function getFieldValue($meta, $field, $eventAdapter); /** * Updates a field. * * @param object $object * @param AdapterInterface $eventAdapter * @param ClassMetadata $meta * @param string $field * * @return void */ protected function updateField($object, $eventAdapter, $meta, $field) { $property = $meta->getReflectionProperty($field); $oldValue = $property->getValue($object); $newValue = $this->getFieldValue($meta, $field, $eventAdapter); // if field value is reference, persist object if ($meta->hasAssociation($field) && is_object($newValue) && !$eventAdapter->getObjectManager()->contains($newValue)) { $uow = $eventAdapter->getObjectManager()->getUnitOfWork(); // Check to persist only when the object isn't already managed, always persists for MongoDB if (!($uow instanceof UnitOfWork) || UnitOfWork::STATE_MANAGED !== $uow->getEntityState($newValue)) { $eventAdapter->getObjectManager()->persist($newValue); } } $property->setValue($object, $newValue); if ($object instanceof NotifyPropertyChanged) { $uow = $eventAdapter->getObjectManager()->getUnitOfWork(); $uow->propertyChanged($object, $field, $oldValue, $newValue); } } /** * @param mixed $values * * @return mixed[]|null */ private function getPhpValues($values, ?string $type, ObjectManager $om): ?array { if (null === $values) { return null; } if (!is_array($values)) { $values = [$values]; } if (null !== $type) { foreach ($values as $i => $value) { if ($om instanceof DocumentManager) { if (TypeODM::hasType($type)) { $values[$i] = TypeODM::getType($type) ->convertToPHPValue($value); } else { $values[$i] = $value; } } elseif (Type::hasType($type)) { $values[$i] = Type::getType($type) ->convertToPHPValue($value, $om->getConnection()->getDatabasePlatform()); } } } return $values; } } doctrine-extensions/src/DoctrineExtensions.php 0000644 00000011005 15117737237 0015711 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo; use function class_exists; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Cache\ArrayCache; use Doctrine\Common\Cache\Psr6\CacheAdapter; use Doctrine\ODM\MongoDB\Mapping\Driver as DriverMongodbODM; use Doctrine\ORM\Mapping\Driver as DriverORM; use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; use Symfony\Component\Cache\Adapter\ArrayAdapter; /** * Version class allows checking the required dependencies * and the current version of the Doctrine Extensions library. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ final class DoctrineExtensions { /** * Current version of extensions */ public const VERSION = '3.11.0'; /** * Hooks all extension metadata mapping drivers into * the given driver chain of drivers for the ORM. */ public static function registerMappingIntoDriverChainORM(MappingDriverChain $driverChain, Reader $reader = null): void { if (!$reader) { $reader = self::createAnnotationReader(); } $annotationDriver = new DriverORM\AnnotationDriver($reader, [ __DIR__.'/Translatable/Entity', __DIR__.'/Loggable/Entity', __DIR__.'/Tree/Entity', ]); $driverChain->addDriver($annotationDriver, 'Gedmo'); } /** * Hooks only superclass extension metadata mapping drivers into * the given driver chain of drivers for the ORM. */ public static function registerAbstractMappingIntoDriverChainORM(MappingDriverChain $driverChain, Reader $reader = null): void { if (!$reader) { $reader = self::createAnnotationReader(); } $annotationDriver = new DriverORM\AnnotationDriver($reader, [ __DIR__.'/Translatable/Entity/MappedSuperclass', __DIR__.'/Loggable/Entity/MappedSuperclass', __DIR__.'/Tree/Entity/MappedSuperclass', ]); $driverChain->addDriver($annotationDriver, 'Gedmo'); } /** * Hooks all extension metadata mapping drivers into * the given driver chain of drivers for the MongoDB ODM. */ public static function registerMappingIntoDriverChainMongodbODM(MappingDriverChain $driverChain, Reader $reader = null): void { if (!$reader) { $reader = self::createAnnotationReader(); } $annotationDriver = new DriverMongodbODM\AnnotationDriver($reader, [ __DIR__.'/Translatable/Document', __DIR__.'/Loggable/Document', ]); $driverChain->addDriver($annotationDriver, 'Gedmo'); } /** * Hooks only superclass extension metadata mapping drivers into * the given driver chain of drivers for the MongoDB ODM. */ public static function registerAbstractMappingIntoDriverChainMongodbODM(MappingDriverChain $driverChain, Reader $reader = null): void { if (!$reader) { $reader = self::createAnnotationReader(); } $annotationDriver = new DriverMongodbODM\AnnotationDriver($reader, [ __DIR__.'/Translatable/Document/MappedSuperclass', __DIR__.'/Loggable/Document/MappedSuperclass', ]); $driverChain->addDriver($annotationDriver, 'Gedmo'); } /** * Registers all extension annotations. * * @deprecated to be removed in 4.0, annotation classes are autoloaded instead */ public static function registerAnnotations(): void { @trigger_error(sprintf( '"%s()" is deprecated since gedmo/doctrine-extensions 3.11 and will be removed in version 4.0.', __METHOD__ ), E_USER_DEPRECATED); // Purposefully no-op'd, all supported versions of `doctrine/annotations` support autoloading } private static function createAnnotationReader(): AnnotationReader { $reader = new AnnotationReader(); if (class_exists(ArrayAdapter::class)) { $reader = new PsrCachedReader($reader, new ArrayAdapter()); } elseif (class_exists(ArrayCache::class)) { $reader = new PsrCachedReader($reader, CacheAdapter::wrap(new ArrayCache())); } return $reader; } } doctrine-extensions/src/Exception.php 0000644 00000001217 15117737237 0014024 0 ustar 00 <?php /* * This file is part of the Doctrine Behavioral Extensions package. * (c) Gediminas Morkevicius <gediminas.morkevicius@gmail.com> http://www.gediminasm.org * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Gedmo; /** * Marker interface for all exceptions in the Doctrine Extensions package. * * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com> */ interface Exception { /* * Following best practices for PHP5.3 package exceptions. * All exceptions thrown in this package will have to implement this interface */ } doctrine-extensions/.yamllint 0000644 00000000417 15117737237 0012421 0 ustar 00 ignore: vendor/ extends: default rules: comments: disable comments-indentation: disable document-start: disable empty-lines: max: 1 max-start: 0 max-end: 0 line-length: disable truthy: allowed-values: ['true', 'false'] check-keys: false doctrine-extensions/CHANGELOG-v2.4.x.md 0000644 00000003245 15117737237 0013337 0 ustar 00 # Doctrine Extensions Changelog - v2.4.x :warning: This is an archived changelog from the v2.4.x history of Doctrine Extensions. View the main [CHANGELOG.md](CHANGELOG.md) file for the most recent version history. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). --- ## [2.4.42] - 2020-08-20 ### Translatable #### Fixed - Allow for both falsy and null-fallback translatable values (#2152) ## [2.4.41] - 2020-05-10 ### Sluggable #### Fixed - Remove PHPDoc samples as they are interpreted by Annotation Reader (#2120) ## [2.4.40] - 2020-04-27 ### SoftDeleteable #### Fixed - Invalidate query cache when toggling filter on/off for an entity (#2112) ## [2.4.39] - 2020-01-18 ### Tree #### Fixed - The value of path source property is cast to string type for Materialized Path Tree strategy (#2061) ## [2.4.38] - 2019-11-08 ### Global / Shared #### Fixed - Add `parent::__construct()` calls to Listeners w/ custom constructors (#2012) - Add upcoming Doctrine ODM 2.0 to `composer.json` conflicts (#2027) ### Loggable #### Fixed - Added missing string casting of `objectId` in `LogEntryRepository::revert()` method (#2009) ### ReferenceIntegrity #### Fixed - Get class from meta in ReferenceIntegrityListener (#2021) ### Translatable #### Fixed - Return default AST executor instead of throwing Exception in Walker (#2018) - Fix duplicate inherited properties (#2029) ### Tree #### Fixed - Remove hard-coded parent column name in repository prev/next sibling queries (#2020) ## [2.4.37] - 2019-03-17 ### Translatable #### Fixed - Bugfix to load null value translations (#1990) doctrine-extensions/CHANGELOG.md 0000644 00000027763 15117737237 0012415 0 ustar 00 # Doctrine Extensions Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). Each release should include sub-headers for the Extension above the types of changes, in order to more easily recognize how an Extension has changed in a release. ``` ## [3.6.1] - 2022-07-26 ### Fixed - Sortable: Fix issue with add+delete position synchronization (#1932) ``` --- ## [Unreleased] ## [3.11.1] - 2023-02-20 ### Fixed - Loggable: Remove unfixable deprecation when extending `LoggableListener` - Remove unfixable deprecations when extending repository classes - Fix error caused by the attempt of "doctrine/annotations" parsing a `@note` annotation ## [3.11.0] - 2023-01-26 ### Added - Tree: Add `Nested::ALLOWED_NODE_POSITIONS` constant in order to expose the available node positions - Support for `doctrine/collections` 2.0 - Support for `doctrine/event-manager` 2.0 - Loggable: Add `LogEntryInterface` interface in order to be implemented by log entry models ### Fixed - Sortable: Fix return value check of Comparable interface (#2541) - Uploadable: Retrieve the correct metadata when uploading entities of different classes (#2071) - Translatable: Fix property existence check at `TranslatableListener::getTranslatableLocale()` ### Deprecated - In order to close the API, `@final` and `@internal` annotations were added to all non base classes, which means that extending these classes is deprecated and can not be inherited in version 4.0. - Sortable: Accepting a return type other than "integer" from `Comparable::compareTo()` is deprecated in `SortableListener::postFlush()`. This will not be accepted in version 4.0. - Deprecate the annotation reader being allowed to be any object. In 4.0, a `Doctrine\Common\Annotations\Reader` or `Gedmo\Mapping\Driver\AttributeReader` instance will be required. - `Gedmo\DoctrineExtensions::registerAnnotations()` is deprecated and will be removed in 4.0, the method has been no-op'd as all supported `doctrine/annotations` versions support autoloading - Loggable: Constants `LoggableListener::ACTION_CREATE`, `LoggableListener::ACTION_UPDATE` and `LoggableListener::ACTION_REMOVE` are deprecated. Use `LogEntryInterface::ACTION_CREATE`, `LogEntryInterface::ACTION_UPDATE` and `LogEntryInterface::ACTION_REMOVE` instead. ## [3.10.0] - 2022-11-14 ### Changed - Bump "doctrine/event-manager" dependency from ^1.0 to ^1.2. ### Fixed - Tree: TreeRoot without rootIdentifierMethod when calling getNextSiblings (#2518) - Sortable: Fix duplicated positions when manually updating position on more than one object (#2439) ## [3.9.0] - 2022-09-22 ### Fixed - Tree: Allow sorting children by a ManyToOne relation (#2492) - Tree: Fix passing `null` to `abs()` function - Timestampable: Use an attribute in Timestampable attribute docs ### Deprecated - Tree: Passing `null` as argument 8 to `Nested::shiftRangeRL()` ## [3.8.0] - 2022-07-17 ### Added - Sluggable: Add support for `DateTimeImmutable` fields - Tree: [NestedSet] `childrenQueryBuilder()` to allow specifying sort order separately for each field - Tree: [NestedSet] Added option to reorder only direct children in `reorder()` method ### Changed - Tree: In `ClosureTreeRepository::removeFromTree()` and `NestedTreeRepository::removeFromTree()` when something fails in the transaction, it uses the `code` from the original exception to construct the `\Gedmo\Exception\RuntimeException` instance instead of `null`. ### Fixed - Sluggable: Cast slug to string before passing it as argument 2 to `preg_match()` (#2473) - Sortable: [SortableGroup] Fix sorting date columns in SQLite (#2462). - PHPDoc of `AbstractMaterializedPath::removeNode()` and `AbstractMaterializedPath::getChildren()` - Retrieving the proper metadata cache from Doctrine when using a CacheWarmer. ## [3.7.0] - 2022-05-17 ### Added - Add support for doctrine/persistence 3 ### Changed - Removed call to deprecated `ClassMetadataFactory::getCacheDriver()` method. - Dropped support for doctrine/mongodb-odm < 2.3. - Make doctrine/cache an optional dependency. ### Fixed - Loggable: Fix `appendNumber` renaming for files without extension (#2228) ## [3.6.0] - 2022-03-19 ### Added - Translatable: Add defaultTranslationValue option to allow null or string value (#2167). TranslatableListener can hydrate object properties with null value, but it may cause a Type error for non-nullable getter upon a missing translation. ### Fixed - Uploadable: `FileInfoInterface::getSize()` return type declaration (#2413). - Tree: Setting a new Tree Root when Tree Parent is `null`. - Tree: update cache key used by Closure to match Doctrine's one (#2416). - Tree: persist order does not affect entities on Closure (#2432) ## [3.5.0] - 2022-01-10 ### Added - SoftDeleteable: Support to use annotations as attributes on PHP >= 8.0. - Blameable: Support to use annotations as attributes on PHP >= 8.0. - IpTraceable: Support to use annotations as attributes on PHP >= 8.0. - Sortable: Support to use annotations as attributes on PHP >= 8.0. - Sluggable: Support to use annotations as attributes on PHP >= 8.0. - Uploadable: Support to use annotations as attributes on PHP >= 8.0. - Tree: Support to use annotations as attributes on PHP >= 8.0. - References: Support to use annotations as attributes on PHP >= 8.0. - ReferenceIntegrity: Support to use annotations as attributes on PHP >= 8.0. - SoftDeleteable: Support for custom column types (like Carbon). - Timestampable: Support for custom column types (like Carbon). - Translatable: Added an index to `Translation` entity to speed up searches using `Gedmo\Translatable\Entity\Repository\TranslationRepository::findTranslations()` method. - `Gedmo\Mapping\Event\AdapterInterface::getObject()` method. ### Fixed - Blameable, IpTraceable, Timestampable: Type handling for the tracked field values configured in the origin field. - Loggable: Using only PHP 8 attributes. - References: Avoid deprecations using LazyCollection with PHP 8.1 - Tree: Association mapping problems using Closure tree strategy (by manually defining mapping on the closure entity). - Wrong PHPDoc type declarations. - Avoid calling deprecated `AbstractClassMetadataFactory::getCacheDriver()` method. - Avoid deprecations using `doctrine/mongodb-odm` >= 2.2 - Translatable: `Gedmo\Translatable\Document\Repository\TranslationRepository::findObjectByTranslatedField()` method accessing a non-existing key. ### Deprecated - Tree: When using Closure tree strategy, it is deprecated not defining the mapping associations of the closure entity. - `Gedmo\Tool\Logging\DBAL\QueryAnalizer` class without replacement. - Using YAML mapping is deprecated, you SHOULD migrate to attributes, annotations or XML. - `Gedmo\Mapping\Event\AdapterInterface::__call()` method. - `Gedmo\Tool\Wrapper\AbstractWrapper::clear()` method. - `Gedmo\Tool\Wrapper\WrapperInterface::populate()` method. ### Changed - In order to use a custom cache for storing configuration of an extension, the user has to call `setCacheItemPool()` on the extension listener passing an instance of `Psr\Cache\CacheItemPoolInterface`. ## [3.4.0] - 2021-12-05 ### Added - PHP 8 Attributes support for Doctrine MongoDB to document & traits. - Support for doctrine/dbal >=3.2. - Timestampable: Support to use annotations as attributes on PHP >= 8.0. - Loggable: Support to use annotations as attributes on PHP >= 8.0. ### Changed - Translatable: Dropped support for other values than "true", "false", "1" and "0" in the `fallback` attribute of the `translatable` element in the XML mapping. - Tree: Dropped support for other values than "true", "false", "1" and "0" in the `activate-locking` attribute of the `tree` element in the XML mapping. - Tree: Dropped support for other values than "true", "false", "1" and "0" in the `append_id`, `starts_with_separator` and `ends_with_separator` attributes of the `tree-path` element in the XML mapping. - Dropped support for doctrine/dbal < 2.13.1. - The third argument of `Gedmo\SoftDeleteable\Query\TreeWalker\Exec\MultiTableDeleteExecutor::__construct()` requires a `Doctrine\ORM\Mapping\ClassMetadata` instance. ## [3.3.1] - 2021-11-18 ### Fixed - Translatable: Using ORM/ODM attribute mapping and translatable annotations. - Tree: Missing support for `tree-path-hash` fields in XML mapping. - Tree: Check for affected rows at `ClosureTreeRepository::cleanUpClosure()` and `Closure::updateNode()`. - `Gedmo\Mapping\Driver\Xml::_loadMappingFile()` behavior in scenarios where `libxml_disable_entity_loader(true)` was previously called. - Loggable: Missing support for `versioned` fields at `attribute-override` in XML mapping. ## [3.3.0] - 2021-11-15 ### Added - Support to use Translatable annotations as attributes on PHP >= 8.0. ### Deprecated - `Gedmo\Mapping\Driver\File::$_paths` property and `Gedmo\Mapping\Driver\File::setPaths()` method are deprecated and will be removed in version 4.0, as they are not used. ### Fixed - Value passed in the `--config` option to `fix-cs` Composer script. - Return value for `replaceRelative()` and `replaceInverseRelative()` at `Gedmo\Sluggable\Mapping\Event\Adapter\ODM` if the query result does not implement `Doctrine\ODM\MongoDB\Iterator\Iterator`. - Restored compatibility with doctrine/orm >= 2.10.2 (#2272). Since doctrine/orm 2.10, `Doctrine\ORM\UnitOfWork` relies on SPL object IDs instead of hashes, thus we need to adapt our codebase in order to be compatible with this change. As `Doctrine\ODM\MongoDB\UnitOfWork` from doctrine/mongodb-odm still uses `spl_object_hash()`, all `spl_object_hash()` calls were replaced by `spl_object_id()` to make it work with both ORM and ODM managers. ## [3.2.0] - 2021-10-05 ### Added - PHP 8 Attributes for Doctrine ORM to entities & traits (#2251) ### Fixed - Removed legacy checks targeting older versions of PHP (#2201) - Added missing XSD definitions (#2244) - Replaced undefined constants from `Doctrine\DBAL\Types\Type` at `Gedmo\Translatable\Mapping\Event\Adapter\ORM::foreignKey()` (#2250) - Add conflict against "doctrine/orm" >=2.10 in order to guarantee the schema extension (see https://github.com/doctrine/orm/pull/8852) (#2255) ## [3.1.0] - 2021-06-22 ### Fixed - Allow installing doctrine/cache 2.0 (thanks @alcaeus!) - Make doctrine/cache a dev dependency ## [3.0.5] - 2021-04-23 ### Fixed - Use path_separator when removing children (#2217) ## [3.0.4] - 2021-03-27 ### Fixed - Add hacky measure to resolve incompatibility with DoctrineBundle 2.3 [#2211](https://github.com/doctrine-extensions/DoctrineExtensions/pull/2211) ## [3.0.3] - 2021-01-23 ### Fixed - Add PHP 8 compatibility to `composer.json`, resolving minor function parameter deprecations [#2194](https://github.com/Atlantic18/DoctrineExtensions/pull/2194) ## [3.0.2] - 2021-01-23 - Ignore; tag & version mismatch ## [3.0.1] - 2021-01-23 - Ignore; wrong branch published ## [3.0.0] - 2020-09-23 ### Notable & Breaking Changes - Minimum PHP version requirement of 7.2 - Source files moved from `/lib/Gedmo` to `/src` - Added compatibility for `doctrine/common` 3.0 and `doctrine/persistence` 2.0 - All string column type annotations changed to 191 character length (#1941) - Removed support for `\Zend_date` date format [#2163](https://github.com/Atlantic18/DoctrineExtensions/pull/2163) - Renamed `Zend Framework` to `Laminas` [#2163](https://github.com/Atlantic18/DoctrineExtensions/pull/2163) Changes below marked ⚠️ may also be breaking, if you have extended DoctrineExtensions. ### MongoDB - Requires the `ext-mongodb` PHP extension. Usage of `ext-mongo` is deprecated and will be removed in the next major version. - Minimum Doctrine MongoDB ODM requirement of 2.0 - Usages of `\MongoDate` replaced with `MongoDB\BSON\UTCDateTime` ### Global / Shared #### Fixed - Removed `null` parameter from `Doctrine\Common\Cache\Cache::save()` calls (#1996) ### Tree #### Fixed - The value of path source property is cast to string type for Materialized Path Tree strategy (#2061) ### SoftDeleteable #### Changed - ⚠️ Generate different Date values based on column type (#2115) doctrine-extensions/LICENSE 0000644 00000002176 15117737237 0011600 0 ustar 00 Copyright (c) 2011-2015 Gediminas Morkevičius The MIT license, reference http://www.opensource.org/licenses/mit-license.php Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. doctrine-extensions/Makefile 0000644 00000002016 15117737237 0012224 0 ustar 00 all: @echo "Please choose a task." .PHONY: all lint: lint-composer lint-yaml lint-xml .PHONY: lint lint-composer: composer-normalize --dry-run composer validate .PHONY: lint-composer lint-xml: find './tests/.' \( -name '*.xml' \) \ | while read xmlFile; \ do \ XMLLINT_INDENT=' ' xmllint --encode UTF-8 --format "$$xmlFile"|diff - "$$xmlFile"; \ if [ $$? -ne 0 ]; then echo "$$xmlFile" && exit 1; fi; \ done .PHONY: lint-xml lint-doctrine-xml-schema: find './tests/Gedmo/Mapping/Driver/Xml/.' \( -name '*.xml' \) \ | while read xmlFile; \ do \ xmllint --encode UTF-8 --format "$$xmlFile" --schema "./doctrine-mapping.xsd"; \ if [ $$? -ne 0 ]; then echo "$$xmlFile" && exit 1; fi; \ done .PHONY: lint-doctrine-xml-schema cs-fix-doctrine-xml: find './tests/Gedmo/Mapping/Driver/Xml/.' \( -name '*.xml' \) \ | while read xmlFile; \ do \ XMLLINT_INDENT=' ' xmllint --encode UTF-8 --format "$$xmlFile" --output "$$xmlFile"; \ done .PHONY: cs-fix-doctrine-xml lint-yaml: yamllint . .PHONY: lint-yaml doctrine-extensions/README.md 0000644 00000014054 15117737237 0012050 0 ustar 00 # Doctrine Behavioral Extensions [](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/continuous-integration.yml) [](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/qa.yml) [](https://github.com/doctrine-extensions/DoctrineExtensions/actions/workflows/coding-standards.yml) [](https://packagist.org/packages/gedmo/doctrine-extensions) This package contains extensions for Doctrine ORM and MongoDB ODM that offer new functionality or tools to use Doctrine more efficiently. These behaviors can be easily attached to the event system of Doctrine and handle the records being flushed in a behavioral way. --- ## Doctrine Extensions 3.0 Released :tada: 3.0 focuses on refreshing this package for today's PHP. This includes: - Bumping minimum version requirements of PHP, Doctrine, and other dependencies - Implementing support for the latest Doctrine MongoDB & Common packages - Updating the test suite, add code and style standards, and other needed build tools - Cleaning up documentation, code, comments, etc. [Read the Upgrade Doc for more info.](/doc/upgrading/upgrade-v2.4-to-v3.0.md) --- ## Installation composer require gedmo/doctrine-extensions * [Symfony 4](/doc/symfony4.md) * [Laravel 5](https://www.laraveldoctrine.org/docs/1.3/extensions) * [Laminas](/doc/laminas.md) ### Upgrading * [From 2.4.x to 3.0](/doc/upgrading/upgrade-v2.4-to-v3.0.md) ## Extensions #### ORM & MongoDB ODM - [**Blameable**](/doc/blameable.md) - updates string or reference fields on create, update and even property change with a string or object (e.g. user). - [**Loggable**](/doc/loggable.md) - helps tracking changes and history of objects, also supports version management. - [**Sluggable**](/doc/sluggable.md) - urlizes your specified fields into single unique slug - [**Timestampable**](/doc/timestampable.md) - updates date fields on create, update and even property change. - [**Translatable**](/doc/translatable.md) - gives you a very handy solution for translating records into different languages. Easy to setup, easier to use. - [**Tree**](/doc/tree.md) - automates the tree handling process and adds some tree-specific functions on repository. (**closure**, **nested set** or **materialized path**) _(MongoDB ODM only supports materialized path)_ #### ORM Only - [**IpTraceable**](/doc/ip_traceable.md) - inherited from Timestampable, sets IP address instead of timestamp - [**SoftDeleteable**](/doc/softdeleteable.md) - allows to implicitly remove records - [**Sortable**](/doc/sortable.md) - makes any document or entity sortable - [**Uploadable**](/doc/uploadable.md) - provides file upload handling in entity fields #### MongoDB ODM Only - [**References**](/doc/references.md) - supports linking Entities in Documents and vice versa - [**ReferenceIntegrity**](/doc/reference_integrity.md) - constrains ODM MongoDB Document references All extensions support **Attribute**, **Annotation** and **XML** mapping. Additional mapping drivers can be easily implemented using Mapping extension to handle the additional metadata mapping. ### Version Compatibility | Extensions Version | Compatible Doctrine ORM & Common Library | | --- | --- | | 2.4 | 2.5+ | | 2.3 | 2.2 - 2.4 | If you are setting up the Entity Manager without a framework, see the [the example](/example/em.php) to prevent issues like #1310 ### XML Mapping XML mapping needs to be in a different namespace, the declared namespace for Doctrine extensions is http://gediminasm.org/schemas/orm/doctrine-extensions-mapping So root node now looks like this: ```xml <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping"> ... </doctrine-mapping> ``` XML mapping xsd schemas are also versioned and can be used by version suffix: - Latest version - **http://gediminasm.org/schemas/orm/doctrine-extensions-mapping** - 2.2.x version - **http://gediminasm.org/schemas/orm/doctrine-extensions-mapping-2-2** - 2.1.x version - **http://gediminasm.org/schemas/orm/doctrine-extensions-mapping-2-1** ### Running Tests To set up and run the tests, follow these steps: - Install [Docker](https://www.docker.com/) and ensure you have `docker compose` - From the project root, run `docker compose up -d` to start containers in daemon mode - Enter the container via `docker compose exec php bash` and navigate to the root directory: `cd /var/www` - Install Composer dependencies via `composer install` - Run the tests: `bin/phpunit -c tests/` ### Running the Example To set up and run example, follow these steps: - go to the root directory of extensions - download composer: `wget https://getcomposer.org/composer.phar` - install dev libraries: `php composer.phar install` - edit `example/em.php` and configure your database on top of the file - run: `php example/bin/console` or `php example/bin/console` for console commands - run: `php example/bin/console orm:schema-tool:create` to create the schema - run: `php example/bin/console app:print-category-translation-tree` to run the example to print the category translation tree ### Contributors Thanks to [everyone participating](http://github.com/doctrine-extensions/DoctrineExtensions/contributors) in the development of these great Doctrine extensions! And especially ones who create and maintain new extensions: - Lukas Botsch [lbotsch](http://github.com/lbotsch) - Gustavo Adrian [comfortablynumb](http://github.com/comfortablynumb) - Boussekeyt Jules [gordonslondon](http://github.com/gordonslondon) - Kudryashov Konstantin [everzet](http://github.com/everzet) - David Buchmann [dbu](https://github.com/dbu) doctrine-extensions/compose.yaml 0000644 00000000766 15117737237 0013127 0 ustar 00 services: php: build: context: .docker/php target: php args: PHP_VERSION: ${PHP_VERSION:-8.1-cli} volumes: - .:/var/www working_dir: /var/www environment: MONGODB_SERVER: 'mongodb://mongodb:27017' tty: true stdin_open: true mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: de_root_password MYSQL_DATABASE: de_testing MYSQL_USER: de_user MYSQL_PASSWORD: de_password mongodb: image: mongo doctrine-extensions/composer.json 0000644 00000005704 15117737237 0013315 0 ustar 00 { "name": "gedmo/doctrine-extensions", "description": "Doctrine behavioral extensions", "license": "MIT", "type": "library", "keywords": [ "behaviors", "doctrine", "extensions", "gedmo", "sluggable", "loggable", "odm", "orm", "translatable", "tree", "nestedset", "sortable", "timestampable", "blameable", "uploadable" ], "authors": [ { "name": "Gediminas Morkevicius", "email": "gediminas.morkevicius@gmail.com" }, { "name": "Gustavo Falco", "email": "comfortablynumb84@gmail.com" }, { "name": "David Buchmann", "email": "david@liip.ch" } ], "homepage": "http://gediminasm.org/", "support": { "email": "gediminas.morkevicius@gmail.com", "wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/main/doc" }, "require": { "php": "^7.2 || ^8.0", "behat/transliterator": "~1.2", "doctrine/annotations": "^1.13 || ^2.0", "doctrine/collections": "^1.2 || ^2.0", "doctrine/common": "^2.13 || ^3.0", "doctrine/event-manager": "^1.2 || ^2.0", "doctrine/persistence": "^2.2 || ^3.0", "psr/cache": "^1 || ^2 || ^3", "symfony/cache": "^4.4 || ^5.3 || ^6.0", "symfony/deprecation-contracts": "^2.1 || ^3.0" }, "require-dev": { "doctrine/cache": "^1.11 || ^2.0", "doctrine/dbal": "^2.13.1 || ^3.2", "doctrine/doctrine-bundle": "^2.3", "doctrine/mongodb-odm": "^2.3", "doctrine/orm": "^2.10.2", "friendsofphp/php-cs-fixer": "^3.4.0,<3.10", "nesbot/carbon": "^2.55", "phpstan/phpstan": "^1.9", "phpstan/phpstan-doctrine": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5 || ^9.5", "symfony/console": "^4.4 || ^5.3 || ^6.0", "symfony/phpunit-bridge": "^6.0", "symfony/yaml": "^4.4 || ^5.3 || ^6.0" }, "conflict": { "doctrine/cache": "<1.11", "doctrine/dbal": "<2.13.1 || ^3.0 <3.2", "doctrine/mongodb-odm": "<2.3", "doctrine/orm": "<2.10.2", "sebastian/comparator": "<2.0" }, "suggest": { "doctrine/mongodb-odm": "to use the extensions with the MongoDB ODM", "doctrine/orm": "to use the extensions with the ORM", "symfony/cache": "to cache parsed annotations" }, "autoload": { "psr-4": { "Gedmo\\": "src/" } }, "autoload-dev": { "psr-4": { "Gedmo\\Tests\\": "tests/Gedmo/" } }, "config": { "bin-dir": "bin", "sort-packages": true }, "extra": { "branch-alias": { "dev-main": "3.12-dev" } }, "scripts": { "fix-cs": "php-cs-fixer fix --config=.php-cs-fixer.dist.php" } } doctrine-extensions/doctrine-mapping.xsd 0000644 00000000654 15117737237 0014552 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <schema elementFormDefault="qualified" xmlns="http://www.w3.org/2001/XMLSchema"> <import namespace="http://doctrine-project.org/schemas/orm/doctrine-mapping" schemaLocation="./vendor/doctrine/orm/doctrine-mapping.xsd"/> <import namespace="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping" schemaLocation="./schemas/orm/doctrine-extensions-mapping-2-2.xsd"/> </schema>
Coded With 💗 by
0x6ick