From 8b666a397c3ee0674428f78eb3c057e3c484a792 Mon Sep 17 00:00:00 2001 From: Christian Mehldau Date: Sun, 9 Jan 2022 16:48:24 +0100 Subject: [PATCH 1/3] Generated/Virtual Columns: Insertable / Updateable Defines whether a column is included in an SQL INSERT and/or UPDATE statement. Throws an exception for UPDATE statements attempting to update this field/column. Closes #5728 --- docs/en/reference/annotations-reference.rst | 18 ++++ docs/en/reference/attributes-reference.rst | 12 +++ docs/en/reference/basic-mapping.rst | 4 + docs/en/reference/xml-mapping.rst | 5 ++ doctrine-mapping.xsd | 13 +++ .../ORM/Cache/DefaultEntityHydrator.php | 12 ++- .../ORM/Mapping/Builder/FieldBuilder.php | 28 +++++++ .../ORM/Mapping/ClassMetadataInfo.php | 61 +++++++++++++- lib/Doctrine/ORM/Mapping/Column.php | 22 ++++- .../ORM/Mapping/Driver/AnnotationDriver.php | 45 ++++++++-- .../ORM/Mapping/Driver/AttributeDriver.php | 26 ++++++ lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 14 ++++ .../ORM/Mapping/Driver/YamlDriver.php | 18 ++++ lib/Doctrine/ORM/Mapping/MappingException.php | 5 ++ .../Entity/BasicEntityPersister.php | 82 +++++++++++++------ .../Entity/JoinedSubclassPersister.php | 29 ++++--- .../ORM/Tools/Export/Driver/XmlExporter.php | 8 ++ phpstan-baseline.neon | 12 +-- .../Tests/Models/Upsertable/Insertable.php | 74 +++++++++++++++++ .../Tests/Models/Upsertable/Updatable.php | 72 ++++++++++++++++ .../Functional/InsertableUpdateableTest.php | 74 +++++++++++++++++ .../ORM/Mapping/AbstractMappingDriverTest.php | 26 ++++++ .../Tests/ORM/Mapping/ClassMetadataTest.php | 4 + ...ine.Tests.Models.Upsertable.Insertable.php | 29 +++++++ ...rine.Tests.Models.Upsertable.Updatable.php | 28 +++++++ ...Tests.Models.Upsertable.Insertable.dcm.xml | 16 ++++ ....Tests.Models.Upsertable.Updatable.dcm.xml | 16 ++++ ...Tests.Models.Upsertable.Insertable.dcm.yml | 17 ++++ ....Tests.Models.Upsertable.Updatable.dcm.yml | 17 ++++ 29 files changed, 727 insertions(+), 60 deletions(-) create mode 100644 tests/Doctrine/Tests/Models/Upsertable/Insertable.php create mode 100644 tests/Doctrine/Tests/Models/Upsertable/Updatable.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/InsertableUpdateableTest.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Upsertable.Insertable.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Upsertable.Updatable.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Upsertable.Insertable.dcm.xml create mode 100644 tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Upsertable.Updatable.dcm.xml create mode 100644 tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Upsertable.Insertable.dcm.yml create mode 100644 tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Upsertable.Updatable.dcm.yml diff --git a/docs/en/reference/annotations-reference.rst b/docs/en/reference/annotations-reference.rst index 226842d71a4..de3f370bc14 100644 --- a/docs/en/reference/annotations-reference.rst +++ b/docs/en/reference/annotations-reference.rst @@ -123,6 +123,18 @@ Optional attributes: - **nullable**: Determines if NULL values allowed for this column. If not specified, default value is false. +- **insertable**: Boolean value to determine if the column should be + included when inserting a new row into the underlying entities table. + If not specified, default value is true. + +- **updatable**: Boolean value to determine if the column should be + included when updating the row of the underlying entities table. + If not specified, default value is true. + +- **generated**: An enum with the possible values ALWAYS, INSERT, NEVER. Is + used after an INSERT or UPDATE statement to determine if the database + generated this value and it needs to be fetched using a SELECT statement. + - **options**: Array of additional options: - ``default``: The default value to set for the column if no value @@ -193,6 +205,12 @@ Examples: */ protected $loginCount; + /** + * Generated column + * @Column(type="string", name="user_fullname", insertable=false, updatable=false) + */ + protected $fullname; + .. _annref_column_result: @ColumnResult diff --git a/docs/en/reference/attributes-reference.rst b/docs/en/reference/attributes-reference.rst index 89b1f8cad33..d811e26b316 100644 --- a/docs/en/reference/attributes-reference.rst +++ b/docs/en/reference/attributes-reference.rst @@ -178,6 +178,18 @@ Optional parameters: - **nullable**: Determines if NULL values allowed for this column. If not specified, default value is ``false``. +- **insertable**: Boolean value to determine if the column should be + included when inserting a new row into the underlying entities table. + If not specified, default value is true. + +- **updatable**: Boolean value to determine if the column should be + included when updating the row of the underlying entities table. + If not specified, default value is true. + +- **generated**: An enum with the possible values ALWAYS, INSERT, NEVER. Is + used after an INSERT or UPDATE statement to determine if the database + generated this value and it needs to be fetched using a SELECT statement. + - **options**: Array of additional options: - ``default``: The default value to set for the column if no value diff --git a/docs/en/reference/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index b613f9d6386..b17a968e3d3 100644 --- a/docs/en/reference/basic-mapping.rst +++ b/docs/en/reference/basic-mapping.rst @@ -199,6 +199,10 @@ list: unique key. - ``nullable``: (optional, default FALSE) Whether the database column is nullable. +- ``insertable``: (optional, default TRUE) Whether the database + column should be inserted. +- ``updatable``: (optional, default TRUE) Whether the database + column should be updated. - ``enumType``: (optional, requires PHP 8.1 and ORM 2.11) The PHP enum type name to convert the database value into. - ``precision``: (optional, default 0) The precision for a decimal diff --git a/docs/en/reference/xml-mapping.rst b/docs/en/reference/xml-mapping.rst index 35856e6fd56..39e637a945c 100644 --- a/docs/en/reference/xml-mapping.rst +++ b/docs/en/reference/xml-mapping.rst @@ -256,6 +256,11 @@ Optional attributes: table? Defaults to false. - nullable - Should this field allow NULL as a value? Defaults to false. +- insertable - Should this field be inserted? Defaults to true. +- updatable - Should this field be updated? Defaults to true. +- generated - Enum of the values ALWAYS, INSERT, NEVER that determines if + generated value must be fetched from database after INSERT or UPDATE. + Defaults to "NEVER". - version - Should this field be used for optimistic locking? Only works on fields with type integer or datetime. - scale - Scale of a decimal type. diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index 142f204ae35..db7f4311134 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -288,6 +288,14 @@ + + + + + + + + @@ -299,6 +307,9 @@ + + + @@ -623,6 +634,8 @@ + + diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index aaba02d067f..384664fa5e6 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -55,8 +55,16 @@ public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $e $data = $this->uow->getOriginalEntityData($entity); $data = array_merge($data, $metadata->getIdentifierValues($entity)); // why update has no identifier values ? - if ($metadata->isVersioned) { - $data[$metadata->versionField] = $metadata->getFieldValue($entity, $metadata->versionField); + if ($metadata->requiresFetchAfterChange) { + if ($metadata->isVersioned) { + $data[$metadata->versionField] = $metadata->getFieldValue($entity, $metadata->versionField); + } + + foreach ($metadata->fieldMappings as $name => $fieldMapping) { + if (isset($fieldMapping['generated'])) { + $data[$name] = $metadata->getFieldValue($entity, $name); + } + } } foreach ($metadata->associationMappings as $name => $assoc) { diff --git a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php index 8b9e62d548a..51b366164a6 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php @@ -110,6 +110,34 @@ public function precision($p) return $this; } + /** + * Sets insertable. + * + * @return $this + */ + public function insertable(bool $flag = true) + { + if (! $flag) { + $this->mapping['notInsertable'] = true; + } + + return $this; + } + + /** + * Sets updatable. + * + * @return $this + */ + public function updatable(bool $flag = true) + { + if (! $flag) { + $this->mapping['notUpdatable'] = true; + } + + return $this; + } + /** * Sets scale. * diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 0359c2731da..113738b3983 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -80,6 +80,9 @@ * length?: int, * id?: bool, * nullable?: bool, + * notInsertable?: bool, + * notUpdatable?: bool, + * generated?: string, * enumType?: class-string, * columnDefinition?: string, * precision?: int, @@ -258,6 +261,21 @@ class ClassMetadataInfo implements ClassMetadata */ public const CACHE_USAGE_READ_WRITE = 3; + /** + * The value of this column is never generated by the database. + */ + public const GENERATED_NEVER = 0; + + /** + * The value of this column is generated by the database on INSERT, but not on UPDATE. + */ + public const GENERATED_INSERT = 1; + + /** + * The value of this column is generated by the database on both INSERT and UDPATE statements. + */ + public const GENERATED_ALWAYS = 2; + /** * READ-ONLY: The name of the entity class. * @@ -439,6 +457,12 @@ class ClassMetadataInfo implements ClassMetadata * - nullable (boolean, optional) * Whether the column is nullable. Defaults to FALSE. * + * - 'notInsertable' (boolean, optional) + * Whether the column is not insertable. Optional. Is only set if value is TRUE. + * + * - 'notUpdatable' (boolean, optional) + * Whether the column is updatable. Optional. Is only set if value is TRUE. + * * - columnDefinition (string, optional, schema-only) * The SQL fragment that is used when generating the DDL for the column. * @@ -659,13 +683,21 @@ class ClassMetadataInfo implements ClassMetadata */ public $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT; + /** + * READ-ONLY: A Flag indicating whether one or more columns of this class + * have to be reloaded after insert / update operations. + * + * @var bool + */ + public $requiresFetchAfterChange = false; + /** * READ-ONLY: A flag for whether or not instances of this class are to be versioned * with optimistic locking. * * @var bool */ - public $isVersioned; + public $isVersioned = false; /** * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any). @@ -963,6 +995,10 @@ public function __sleep() $serialized[] = 'cache'; } + if ($this->requiresFetchAfterChange) { + $serialized[] = 'requiresFetchAfterChange'; + } + return $serialized; } @@ -1609,6 +1645,16 @@ protected function validateAndCompleteFieldMapping(array $mapping): array $mapping['requireSQLConversion'] = true; } + if (isset($mapping['generated'])) { + if (! in_array($mapping['generated'], [self::GENERATED_NEVER, self::GENERATED_INSERT, self::GENERATED_ALWAYS])) { + throw MappingException::invalidGeneratedMode($mapping['generated']); + } + + if ($mapping['generated'] === self::GENERATED_NEVER) { + unset($mapping['generated']); + } + } + if (isset($mapping['enumType'])) { if (PHP_VERSION_ID < 80100) { throw MappingException::enumsRequirePhp81($this->name, $mapping['fieldName']); @@ -2673,6 +2719,10 @@ public function mapField(array $mapping) $mapping = $this->validateAndCompleteFieldMapping($mapping); $this->assertFieldNotMapped($mapping['fieldName']); + if (isset($mapping['generated'])) { + $this->requiresFetchAfterChange = true; + } + $this->fieldMappings[$mapping['fieldName']] = $mapping; } @@ -3403,8 +3453,9 @@ public function setSequenceGeneratorDefinition(array $definition) */ public function setVersionMapping(array &$mapping) { - $this->isVersioned = true; - $this->versionField = $mapping['fieldName']; + $this->isVersioned = true; + $this->versionField = $mapping['fieldName']; + $this->requiresFetchAfterChange = true; if (! isset($mapping['default'])) { if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) { @@ -3427,6 +3478,10 @@ public function setVersionMapping(array &$mapping) public function setVersioned($bool) { $this->isVersioned = $bool; + + if ($bool) { + $this->requiresFetchAfterChange = true; + } } /** diff --git a/lib/Doctrine/ORM/Mapping/Column.php b/lib/Doctrine/ORM/Mapping/Column.php index b90a80ff941..f799bd9bccd 100644 --- a/lib/Doctrine/ORM/Mapping/Column.php +++ b/lib/Doctrine/ORM/Mapping/Column.php @@ -44,6 +44,12 @@ final class Column implements Annotation /** @var bool */ public $nullable = false; + /** @var bool */ + public $insertable = true; + + /** @var bool */ + public $updatable = true; + /** @var class-string<\BackedEnum>|null */ public $enumType = null; @@ -53,9 +59,17 @@ final class Column implements Annotation /** @var string|null */ public $columnDefinition; + /** + * @var string|null + * @psalm-var 'NEVER'|'INSERT'|'ALWAYS'|null + * @Enum({"NEVER", "INSERT", "ALWAYS"}) + */ + public $generated; + /** * @param class-string<\BackedEnum>|null $enumType * @param array $options + * @psalm-param 'NEVER'|'INSERT'|'ALWAYS'|null $generated */ public function __construct( ?string $name = null, @@ -65,9 +79,12 @@ public function __construct( ?int $scale = null, bool $unique = false, bool $nullable = false, + bool $insertable = true, + bool $updatable = true, ?string $enumType = null, array $options = [], - ?string $columnDefinition = null + ?string $columnDefinition = null, + ?string $generated = null ) { $this->name = $name; $this->type = $type; @@ -76,8 +93,11 @@ public function __construct( $this->scale = $scale; $this->unique = $unique; $this->nullable = $nullable; + $this->insertable = $insertable; + $this->updatable = $updatable; $this->enumType = $enumType; $this->options = $options; $this->columnDefinition = $columnDefinition; + $this->generated = $generated; } } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 5e6e169051c..bceef45da17 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -633,6 +633,22 @@ private function getFetchMode(string $className, string $fetchMode): int return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode); } + /** + * Attempts to resolve the generated mode. + * + * @psalm-return ClassMetadataInfo::GENERATED_* + * + * @throws MappingException If the fetch mode is not valid. + */ + private function getGeneratedMode(string $generatedMode): int + { + if (! defined('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode)) { + throw MappingException::invalidGeneratedMode($generatedMode); + } + + return constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode); + } + /** * Parses the given method. * @@ -718,6 +734,9 @@ private function joinColumnToArray(Mapping\JoinColumn $joinColumn): array * unique: bool, * nullable: bool, * precision: int, + * notInsertable?: bool, + * notUpdateble?: bool, + * generated?: ClassMetadataInfo::GENERATED_*, * enumType?: class-string, * options?: mixed[], * columnName?: string, @@ -727,15 +746,27 @@ private function joinColumnToArray(Mapping\JoinColumn $joinColumn): array private function columnToArray(string $fieldName, Mapping\Column $column): array { $mapping = [ - 'fieldName' => $fieldName, - 'type' => $column->type, - 'scale' => $column->scale, - 'length' => $column->length, - 'unique' => $column->unique, - 'nullable' => $column->nullable, - 'precision' => $column->precision, + 'fieldName' => $fieldName, + 'type' => $column->type, + 'scale' => $column->scale, + 'length' => $column->length, + 'unique' => $column->unique, + 'nullable' => $column->nullable, + 'precision' => $column->precision, ]; + if (! $column->insertable) { + $mapping['notInsertable'] = true; + } + + if (! $column->updatable) { + $mapping['notUpdatable'] = true; + } + + if ($column->generated) { + $mapping['generated'] = $this->getGeneratedMode($column->generated); + } + if ($column->options) { $mapping['options'] = $column->options; } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php index 6e0f3182ac1..3155fa753c9 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php @@ -528,6 +528,20 @@ private function getFetchMode(string $className, string $fetchMode): int return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode); } + /** + * Attempts to resolve the generated mode. + * + * @throws MappingException If the fetch mode is not valid. + */ + private function getGeneratedMode(string $generatedMode): int + { + if (! defined('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode)) { + throw MappingException::invalidGeneratedMode($generatedMode); + } + + return constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode); + } + /** * Parses the given method. * @@ -644,6 +658,18 @@ private function columnToArray(string $fieldName, Mapping\Column $column): array $mapping['columnDefinition'] = $column->columnDefinition; } + if ($column->updatable === false) { + $mapping['notUpdatable'] = true; + } + + if ($column->insertable === false) { + $mapping['notInsertable'] = true; + } + + if ($column->generated !== null) { + $mapping['generated'] = $this->getGeneratedMode($column->generated); + } + if ($column->enumType) { $mapping['enumType'] = $column->enumType; } diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index f6eba16a7f9..2d0cccd2efa 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -800,6 +800,8 @@ private function joinColumnToArray(SimpleXMLElement $joinColumnElement): array * scale?: int, * unique?: bool, * nullable?: bool, + * notInsertable?: bool, + * notUpdatable?: bool, * enumType?: string, * version?: bool, * columnDefinition?: string, @@ -840,6 +842,18 @@ private function columnToArray(SimpleXMLElement $fieldMapping): array $mapping['nullable'] = $this->evaluateBoolean($fieldMapping['nullable']); } + if (isset($fieldMapping['insertable']) && ! $this->evaluateBoolean($fieldMapping['insertable'])) { + $mapping['notInsertable'] = true; + } + + if (isset($fieldMapping['updatable']) && ! $this->evaluateBoolean($fieldMapping['updatable'])) { + $mapping['notUpdatable'] = true; + } + + if (isset($fieldMapping['generated'])) { + $mapping['generated'] = constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . (string) $fieldMapping['generated']); + } + if (isset($fieldMapping['version']) && $fieldMapping['version']) { $mapping['version'] = $this->evaluateBoolean($fieldMapping['version']); } diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 1eab5e87e4c..ff11dfbb7ca 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -786,6 +786,9 @@ private function joinColumnToArray(array $joinColumnElement): array * unique?: mixed, * options?: mixed, * nullable?: mixed, + * insertable?: mixed, + * updatable?: mixed, + * generated?: mixed, * enumType?: class-string, * version?: mixed, * columnDefinition?: mixed @@ -802,6 +805,9 @@ private function joinColumnToArray(array $joinColumnElement): array * unique?: bool, * options?: mixed, * nullable?: mixed, + * notInsertable?: mixed, + * notUpdatable?: mixed, + * generated?: mixed, * enumType?: class-string, * version?: mixed, * columnDefinition?: mixed @@ -850,6 +856,18 @@ private function columnToArray(string $fieldName, ?array $column): array $mapping['nullable'] = $column['nullable']; } + if (isset($column['insertable']) && ! (bool) $column['insertable']) { + $mapping['notInsertable'] = true; + } + + if (isset($column['updatable']) && ! (bool) $column['updatable']) { + $mapping['notUpdatable'] = true; + } + + if (isset($column['generated'])) { + $mapping['generated'] = constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $column['generated']); + } + if (isset($column['version']) && $column['version']) { $mapping['version'] = $column['version']; } diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index 3fdc026a41e..0ba7506dfd1 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -825,6 +825,11 @@ public static function invalidFetchMode($className, $annotation) return new self("Entity '" . $className . "' has a mapping with invalid fetch mode '" . $annotation . "'"); } + public static function invalidGeneratedMode(string $annotation): MappingException + { + return new self("Invalid generated mode '" . $annotation . "'"); + } + /** * @param string $className * diff --git a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php index 4a32448fbc8..e3762dd3226 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php @@ -30,8 +30,10 @@ use Doctrine\ORM\UnitOfWork; use Doctrine\ORM\Utility\IdentifierFlattener; use Doctrine\ORM\Utility\PersisterHelper; +use LengthException; use function array_combine; +use function array_keys; use function array_map; use function array_merge; use function array_search; @@ -284,8 +286,8 @@ public function executeInserts() $id = $this->class->getIdentifierValues($entity); } - if ($this->class->isVersioned) { - $this->assignDefaultVersionValue($entity, $id); + if ($this->class->requiresFetchAfterChange) { + $this->assignDefaultVersionAndUpsertableValues($entity, $id); } } @@ -297,50 +299,71 @@ public function executeInserts() /** * Retrieves the default version value which was created * by the preceding INSERT statement and assigns it back in to the - * entities version field. + * entities version field if the given entity is versioned. + * Also retrieves values of columns marked as 'non insertable' and / or + * 'not updatetable' and assigns them back to the entities corresponding fields. * * @param object $entity * @param mixed[] $id * * @return void */ - protected function assignDefaultVersionValue($entity, array $id) + protected function assignDefaultVersionAndUpsertableValues($entity, array $id) { - $value = $this->fetchVersionValue($this->class, $id); + $values = $this->fetchVersionAndNotUpsertableValues($this->class, $id); - $this->class->setFieldValue($entity, $this->class->versionField, $value); + foreach ($values as $field => $value) { + $value = Type::getType($this->class->fieldMappings[$field]['type'])->convertToPHPValue($value, $this->platform); + + $this->class->setFieldValue($entity, $field, $value); + } } /** - * Fetches the current version value of a versioned entity. + * Fetches the current version value of a versioned entity and / or the values of fields + * marked as 'not insertable' and / or 'not updatable'. * * @param ClassMetadata $versionedClass * @param mixed[] $id * * @return mixed */ - protected function fetchVersionValue($versionedClass, array $id) + protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id) { - $versionField = $versionedClass->versionField; - $fieldMapping = $versionedClass->fieldMappings[$versionField]; - $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); - $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform); - $columnName = $this->quoteStrategy->getColumnName($versionField, $versionedClass, $this->platform); + $columnNames = []; + foreach ($this->class->fieldMappings as $key => $column) { + if (isset($column['generated']) || ($this->class->isVersioned && $key === $versionedClass->versionField)) { + $columnNames[$key] = $this->quoteStrategy->getColumnName($key, $versionedClass, $this->platform); + } + } + + $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); + $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform); // FIXME: Order with composite keys might not be correct - $sql = 'SELECT ' . $columnName - . ' FROM ' . $tableName - . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?'; + $sql = 'SELECT ' . implode(', ', $columnNames) + . ' FROM ' . $tableName + . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?'; $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id); - $value = $this->conn->fetchOne( + $values = $this->conn->fetchNumeric( $sql, array_values($flatId), $this->extractIdentifierTypes($id, $versionedClass) ); - return Type::getType($fieldMapping['type'])->convertToPHPValue($value, $this->platform); + if ($values === false) { + throw new LengthException('Unexpected empty result for database query.'); + } + + $values = array_combine(array_keys($columnNames), $values); + + if (! $values) { + throw new LengthException('Unexpected number of database columns.'); + } + + return $values; } /** @@ -383,10 +406,10 @@ public function update($entity) $this->updateTable($entity, $quotedTableName, $data, $isVersioned); - if ($isVersioned) { + if ($this->class->requiresFetchAfterChange) { $id = $this->class->getIdentifierValues($entity); - $this->assignDefaultVersionValue($entity, $id); + $this->assignDefaultVersionAndUpsertableValues($entity, $id); } } @@ -594,12 +617,13 @@ public function delete($entity) * ) * * - * @param object $entity The entity for which to prepare the data. + * @param object $entity The entity for which to prepare the data. + * @param bool $isInsert Whether the data to be prepared refers to an insert statement. * * @return mixed[][] The prepared data. * @psalm-return array> */ - protected function prepareUpdateData($entity) + protected function prepareUpdateData($entity, $isInsert = false) { $versionField = null; $result = []; @@ -625,6 +649,14 @@ protected function prepareUpdateData($entity) $fieldMapping = $this->class->fieldMappings[$field]; $columnName = $fieldMapping['columnName']; + if (! $isInsert && isset($fieldMapping['notUpdatable'])) { + continue; + } + + if ($isInsert && isset($fieldMapping['notInsertable'])) { + continue; + } + $this->columnTypes[$columnName] = $fieldMapping['type']; $result[$this->getOwningTable($field)][$columnName] = $newVal; @@ -692,7 +724,7 @@ protected function prepareUpdateData($entity) */ protected function prepareInsertData($entity) { - return $this->prepareUpdateData($entity); + return $this->prepareUpdateData($entity, true); } /** @@ -1440,6 +1472,10 @@ protected function getInsertColumnList() } if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name) { + if (isset($this->class->fieldMappings[$name]['notInsertable'])) { + continue; + } + $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform); $this->columnTypes[$name] = $this->class->fieldMappings[$name]['type']; } diff --git a/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php index 5c59168eb75..aa195d04aa9 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php @@ -6,6 +6,7 @@ use Doctrine\Common\Collections\Criteria; use Doctrine\DBAL\LockMode; +use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Internal\SQLResultCasing; use Doctrine\ORM\Mapping\ClassMetadata; @@ -168,8 +169,8 @@ public function executeInserts() $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity); } - if ($this->class->isVersioned) { - $this->assignDefaultVersionValue($entity, $id); + if ($this->class->requiresFetchAfterChange) { + $this->assignDefaultVersionAndUpsertableValues($entity, $id); } // Execute inserts on subtables. @@ -211,9 +212,6 @@ public function update($entity) } $isVersioned = $this->class->isVersioned; - if ($isVersioned === false) { - return; - } $versionedClass = $this->getVersionedClassMetadata(); $versionedTable = $versionedClass->getTableName(); @@ -225,10 +223,10 @@ public function update($entity) $this->updateTable($entity, $tableName, $data, $versioned); } - // Make sure the table with the version column is updated even if no columns on that - // table were affected. - if ($isVersioned) { - if (! isset($updateData[$versionedTable])) { + if ($this->class->requiresFetchAfterChange) { + // Make sure the table with the version column is updated even if no columns on that + // table were affected. + if ($isVersioned && ! isset($updateData[$versionedTable])) { $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); $this->updateTable($entity, $tableName, [], true); @@ -236,7 +234,7 @@ public function update($entity) $identifiers = $this->em->getUnitOfWork()->getEntityIdentifier($entity); - $this->assignDefaultVersionValue($entity, $identifiers); + $this->assignDefaultVersionAndUpsertableValues($entity, $identifiers); } } @@ -549,10 +547,15 @@ protected function getInsertColumnList() /** * {@inheritdoc} */ - protected function assignDefaultVersionValue($entity, array $id) + protected function assignDefaultVersionAndUpsertableValues($entity, array $id) { - $value = $this->fetchVersionValue($this->getVersionedClassMetadata(), $id); - $this->class->setFieldValue($entity, $this->class->versionField, $value); + $values = $this->fetchVersionAndNotUpsertableValues($this->getVersionedClassMetadata(), $id); + + foreach ($values as $field => $value) { + $value = Type::getType($this->class->fieldMappings[$field]['type'])->convertToPHPValue($value, $this->platform); + + $this->class->setFieldValue($entity, $field, $value); + } } private function getJoinSql(string $baseTableAlias): string diff --git a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php index 8c9ce6d7b92..11ef880ff7b 100644 --- a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php +++ b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php @@ -215,6 +215,14 @@ public function exportClassMetadata(ClassMetadataInfo $metadata) if (isset($field['nullable'])) { $fieldXml->addAttribute('nullable', $field['nullable'] ? 'true' : 'false'); } + + if (isset($field['notInsertable'])) { + $fieldXml->addAttribute('insertable', 'false'); + } + + if (isset($field['notUpdatable'])) { + $fieldXml->addAttribute('updatable', 'false'); + } } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c4f54cca953..17ecb7083a1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -760,16 +760,6 @@ parameters: count: 1 path: lib/Doctrine/ORM/Persisters/Entity/CachedPersisterContext.php - - - message: "#^If condition is always true\\.$#" - count: 1 - path: lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php - - - - message: "#^Left side of && is always true\\.$#" - count: 1 - path: lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php - - message: "#^Parameter \\#1 \\$em of method Doctrine\\\\ORM\\\\Id\\\\AbstractIdGenerator\\:\\:generate\\(\\) expects Doctrine\\\\ORM\\\\EntityManager, Doctrine\\\\ORM\\\\EntityManagerInterface given\\.$#" count: 1 @@ -1741,7 +1731,7 @@ parameters: path: lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php - - message: "#^Offset 'version' on array\\{type\\: string, fieldName\\: string, columnName\\: string, length\\?\\: int, id\\?\\: bool, nullable\\?\\: bool, enumType\\?\\: class\\-string\\, columnDefinition\\?\\: string, \\.\\.\\.\\} in isset\\(\\) does not exist\\.$#" + message: "#^Offset 'version' on array\\{type\\: string, fieldName\\: string, columnName\\: string, length\\?\\: int, id\\?\\: bool, nullable\\?\\: bool, notInsertable\\?\\: bool, notUpdatable\\?\\: bool, \\.\\.\\.\\} in isset\\(\\) does not exist\\.$#" count: 1 path: lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php diff --git a/tests/Doctrine/Tests/Models/Upsertable/Insertable.php b/tests/Doctrine/Tests/Models/Upsertable/Insertable.php new file mode 100644 index 00000000000..3ebfc1a76ff --- /dev/null +++ b/tests/Doctrine/Tests/Models/Upsertable/Insertable.php @@ -0,0 +1,74 @@ + '1234'], generated: 'INSERT')] + public $nonInsertableContent; + + /** + * @var string + * @Column(type="string", insertable=true) + */ + #[Column(type: 'string', insertable: true)] + public $insertableContent; + + public static function loadMetadata(ClassMetadata $metadata): ClassMetadata + { + $metadata->setPrimaryTable( + ['name' => 'insertable_column'] + ); + + $metadata->mapField( + [ + 'id' => true, + 'fieldName' => 'id', + ] + ); + $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO); + + $metadata->mapField( + [ + 'fieldName' => 'nonInsertableContent', + 'notInsertable' => true, + 'options' => ['default' => '1234'], + 'generated' => ClassMetadataInfo::GENERATED_INSERT, + ] + ); + $metadata->mapField( + ['fieldName' => 'insertableContent'] + ); + + return $metadata; + } +} diff --git a/tests/Doctrine/Tests/Models/Upsertable/Updatable.php b/tests/Doctrine/Tests/Models/Upsertable/Updatable.php new file mode 100644 index 00000000000..bd793773512 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Upsertable/Updatable.php @@ -0,0 +1,72 @@ +setPrimaryTable( + ['name' => 'updatable_column'] + ); + + $metadata->mapField( + [ + 'id' => true, + 'fieldName' => 'id', + ] + ); + $metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO); + + $metadata->mapField( + [ + 'fieldName' => 'nonUpdatableContent', + 'notUpdatable' => true, + 'generated' => ClassMetadataInfo::GENERATED_ALWAYS, + ] + ); + $metadata->mapField( + ['fieldName' => 'updatableContent'] + ); + + return $metadata; + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/InsertableUpdateableTest.php b/tests/Doctrine/Tests/ORM/Functional/InsertableUpdateableTest.php new file mode 100644 index 00000000000..3c9d20d40b1 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/InsertableUpdateableTest.php @@ -0,0 +1,74 @@ +_schemaTool->createSchema( + [ + $this->_em->getClassMetadata(Updatable::class), + $this->_em->getClassMetadata(Insertable::class), + ] + ); + } catch (ToolsException $e) { + } + } + + public function testNotInsertableIsFetchedFromDatabase(): void + { + $insertable = new Insertable(); + $insertable->insertableContent = 'abcdefg'; + + $this->_em->persist($insertable); + $this->_em->flush(); + + // gets inserted from default value and fetches value from database + self::assertEquals('1234', $insertable->nonInsertableContent); + + $insertable->nonInsertableContent = '5678'; + + $this->_em->flush(); + $this->_em->clear(); + + $insertable = $this->_em->find(Insertable::class, $insertable->id); + + // during UPDATE statement it is not ignored + self::assertEquals('5678', $insertable->nonInsertableContent); + } + + public function testNotUpdatableIsFetched(): void + { + $updatable = new Updatable(); + $updatable->updatableContent = 'foo'; + $updatable->nonUpdatableContent = 'foo'; + + $this->_em->persist($updatable); + $this->_em->flush(); + + $updatable->updatableContent = 'bar'; + $updatable->nonUpdatableContent = 'baz'; + + $this->_em->flush(); + + self::assertEquals('foo', $updatable->nonUpdatableContent); + + $this->_em->clear(); + + $cleanUpdatable = $this->_em->find(Updatable::class, $updatable->id); + + self::assertEquals('bar', $cleanUpdatable->updatableContent); + self::assertEquals('foo', $cleanUpdatable->nonUpdatableContent); + } +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php index e3b05196cbf..9b17ed30270 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php @@ -68,6 +68,8 @@ use Doctrine\Tests\Models\Enums\Suit; use Doctrine\Tests\Models\TypedProperties\Contact; use Doctrine\Tests\Models\TypedProperties\UserTyped; +use Doctrine\Tests\Models\Upsertable\Insertable; +use Doctrine\Tests\Models\Upsertable\Updatable; use Doctrine\Tests\OrmTestCase; use function assert; @@ -1145,6 +1147,30 @@ public function testReservedWordInTableColumn(): void self::assertSame('count', $metadata->getFieldMapping('count')['columnName']); } + public function testInsertableColumn(): void + { + $metadata = $this->createClassMetadata(Insertable::class); + + $mapping = $metadata->getFieldMapping('nonInsertableContent'); + + self::assertArrayHasKey('notInsertable', $mapping); + self::assertArrayHasKey('generated', $mapping); + self::assertSame(ClassMetadataInfo::GENERATED_INSERT, $mapping['generated']); + self::assertArrayNotHasKey('notInsertable', $metadata->getFieldMapping('insertableContent')); + } + + public function testUpdatableColumn(): void + { + $metadata = $this->createClassMetadata(Updatable::class); + + $mapping = $metadata->getFieldMapping('nonUpdatableContent'); + + self::assertArrayHasKey('notUpdatable', $mapping); + self::assertArrayHasKey('generated', $mapping); + self::assertSame(ClassMetadataInfo::GENERATED_ALWAYS, $mapping['generated']); + self::assertArrayNotHasKey('notUpdatable', $metadata->getFieldMapping('updatableContent')); + } + /** * @requires PHP 8.1 */ diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php index 5010b6c9349..65d3343f873 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php @@ -67,7 +67,10 @@ public function testClassMetadataInstanceSerialization(): void $cm->setDiscriminatorColumn(['name' => 'disc', 'type' => 'integer']); $cm->mapOneToOne(['fieldName' => 'phonenumbers', 'targetEntity' => 'CmsAddress', 'mappedBy' => 'foo']); $cm->markReadOnly(); + $cm->mapField(['fieldName' => 'status', 'notInsertable' => true, 'generated' => ClassMetadata::GENERATED_ALWAYS]); $cm->addNamedQuery(['name' => 'dql', 'query' => 'foo']); + + self::assertTrue($cm->requiresFetchAfterChange); self::assertEquals(1, count($cm->associationMappings)); $serialized = serialize($cm); @@ -92,6 +95,7 @@ public function testClassMetadataInstanceSerialization(): void self::assertEquals(CMS\CmsAddress::class, $oneOneMapping['targetEntity']); self::assertTrue($cm->isReadOnly); self::assertEquals(['dql' => ['name' => 'dql', 'query' => 'foo', 'dql' => 'foo']], $cm->namedQueries); + self::assertTrue($cm->requiresFetchAfterChange); } public function testFieldIsNullable(): void diff --git a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Upsertable.Insertable.php b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Upsertable.Insertable.php new file mode 100644 index 00000000000..403cdb2ef2e --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Upsertable.Insertable.php @@ -0,0 +1,29 @@ +setPrimaryTable( + ['name' => 'insertable_column'] +); + +$metadata->mapField( + [ + 'id' => true, + 'fieldName' => 'id', + ] +); +$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO); + +$metadata->mapField( + [ + 'fieldName' => 'nonInsertableContent', + 'notInsertable' => true, + 'options' => ['default' => '1234'], + 'generated' => ClassMetadataInfo::GENERATED_INSERT, + ] +); +$metadata->mapField( + ['fieldName' => 'insertableContent'] +); diff --git a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Upsertable.Updatable.php b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Upsertable.Updatable.php new file mode 100644 index 00000000000..9fe2627c0e6 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Upsertable.Updatable.php @@ -0,0 +1,28 @@ +setPrimaryTable( + ['name' => 'updatable_column'] +); + +$metadata->mapField( + [ + 'id' => true, + 'fieldName' => 'id', + ] +); +$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO); + +$metadata->mapField( + [ + 'fieldName' => 'nonUpdatableContent', + 'notUpdatable' => true, + 'generated' => ClassMetadataInfo::GENERATED_ALWAYS, + ] +); +$metadata->mapField( + ['fieldName' => 'updatableContent'] +); diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Upsertable.Insertable.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Upsertable.Insertable.dcm.xml new file mode 100644 index 00000000000..6a216375822 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Upsertable.Insertable.dcm.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Upsertable.Updatable.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Upsertable.Updatable.dcm.xml new file mode 100644 index 00000000000..acaf07a8cd7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Upsertable.Updatable.dcm.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Upsertable.Insertable.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Upsertable.Insertable.dcm.yml new file mode 100644 index 00000000000..4e5e8531841 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Upsertable.Insertable.dcm.yml @@ -0,0 +1,17 @@ +Doctrine\Tests\Models\Upsertable\Insertable: + type: entity + table: insertable_column + id: + id: + generator: + strategy: AUTO + fields: + nonInsertableContent: + type: string + insertable: false + generated: INSERT + options: + default: 1234 + insertableContent: + type: string + insertable: true \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Upsertable.Updatable.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Upsertable.Updatable.dcm.yml new file mode 100644 index 00000000000..386ae77209b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Upsertable.Updatable.dcm.yml @@ -0,0 +1,17 @@ +Doctrine\Tests\Models\Upsertable\Updatable: + type: entity + table: updatable_column + id: + id: + generator: + strategy: AUTO + fields: + nonUpdatableContent: + type: string + updatable: false + generated: ALWAYS + options: + default: 1234 + updatableContent: + type: string + updatable: true From 128c54d57c239189c4c9e754d75d17031e89ca6e Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 10 Jan 2022 19:53:58 +0100 Subject: [PATCH 2/3] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Grégoire Paris --- lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php | 4 ++-- lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php index 51b366164a6..f6d2db6c70e 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php @@ -115,7 +115,7 @@ public function precision($p) * * @return $this */ - public function insertable(bool $flag = true) + public function insertable(bool $flag = true): self { if (! $flag) { $this->mapping['notInsertable'] = true; @@ -129,7 +129,7 @@ public function insertable(bool $flag = true) * * @return $this */ - public function updatable(bool $flag = true) + public function updatable(bool $flag = true): self { if (! $flag) { $this->mapping['notUpdatable'] = true; diff --git a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php index e3762dd3226..d9ac746adc1 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php @@ -301,7 +301,7 @@ public function executeInserts() * by the preceding INSERT statement and assigns it back in to the * entities version field if the given entity is versioned. * Also retrieves values of columns marked as 'non insertable' and / or - * 'not updatetable' and assigns them back to the entities corresponding fields. + * 'not updatable' and assigns them back to the entities corresponding fields. * * @param object $entity * @param mixed[] $id @@ -623,7 +623,7 @@ public function delete($entity) * @return mixed[][] The prepared data. * @psalm-return array> */ - protected function prepareUpdateData($entity, $isInsert = false) + protected function prepareUpdateData($entity, bool $isInsert = false) { $versionField = null; $result = []; From 994718920769fafa25fc14bf0bfe489c5e9cf696 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 10 Jan 2022 20:06:54 +0100 Subject: [PATCH 3/3] Add example for virtual column usage in attributes to docs. --- docs/en/reference/annotations-reference.rst | 1 + docs/en/reference/attributes-reference.rst | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/en/reference/annotations-reference.rst b/docs/en/reference/annotations-reference.rst index de3f370bc14..24ce6a5e87c 100644 --- a/docs/en/reference/annotations-reference.rst +++ b/docs/en/reference/annotations-reference.rst @@ -208,6 +208,7 @@ Examples: /** * Generated column * @Column(type="string", name="user_fullname", insertable=false, updatable=false) + * MySQL example: full_name char(41) GENERATED ALWAYS AS (concat(firstname,' ',lastname)), */ protected $fullname; diff --git a/docs/en/reference/attributes-reference.rst b/docs/en/reference/attributes-reference.rst index d811e26b316..ce781220aff 100644 --- a/docs/en/reference/attributes-reference.rst +++ b/docs/en/reference/attributes-reference.rst @@ -260,6 +260,15 @@ Examples: )] protected $loginCount; + // MySQL example: full_name char(41) GENERATED ALWAYS AS (concat(firstname,' ',lastname)), + #[Column( + type: "string", + name: "user_fullname", + insertable: false, + updatable: false + )] + protected $fullname; + .. _attrref_cache: #[Cache]