diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd
index e7a94b702e2..80c65365ac8 100644
--- a/doctrine-mapping.xsd
+++ b/doctrine-mapping.xsd
@@ -274,6 +274,8 @@
+
+
@@ -542,6 +544,8 @@
+
+
diff --git a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php
index 8b9e62d548a..743e63a9748 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.
+ *
+ * @param bool $flag
+ *
+ * @return $this
+ */
+ public function insertable($flag = true)
+ {
+ $this->mapping['insertable'] = (bool) $flag;
+
+ return $this;
+ }
+
+ /**
+ * Sets updateable.
+ *
+ * @param bool $flag
+ *
+ * @return $this
+ */
+ public function updateable($flag = true)
+ {
+ $this->mapping['updateable'] = (bool) $flag;
+
+ return $this;
+ }
+
/**
* Sets scale.
*
diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
index d736886563c..f7819a3a4c8 100644
--- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
+++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -413,6 +413,12 @@ class ClassMetadataInfo implements ClassMetadata
* - nullable (boolean, optional)
* Whether the column is nullable. Defaults to FALSE.
*
+ * - 'insertable' (boolean, optional)
+ * Whether the column is insertable. Defaults to TRUE.
+ *
+ * - 'updateable' (boolean, optional)
+ * Whether the column is updateable. Defaults to TRUE.
+ *
* - columnDefinition (string, optional, schema-only)
* The SQL fragment that is used when generating the DDL for the column.
*
@@ -433,6 +439,8 @@ class ClassMetadataInfo implements ClassMetadata
* length?: int,
* id?: bool,
* nullable?: bool,
+ * insertable?: bool,
+ * updateable?: bool,
* columnDefinition?: string,
* precision?: int,
* scale?: int,
@@ -1255,6 +1263,34 @@ public function isNullable($fieldName)
return $mapping !== false && isset($mapping['nullable']) && $mapping['nullable'];
}
+ /**
+ * Checks if the field is insertable.
+ *
+ * @param string $fieldName The field name.
+ *
+ * @return bool TRUE if the field is not null, FALSE otherwise.
+ */
+ public function isInsertable($fieldName)
+ {
+ $mapping = $this->getFieldMapping($fieldName);
+
+ return $mapping !== false && isset($mapping['insertable']) && $mapping['insertable'];
+ }
+
+ /**
+ * Checks if the field is updateable.
+ *
+ * @param string $fieldName The field name.
+ *
+ * @return bool TRUE if the field is not null, FALSE otherwise.
+ */
+ public function isUpdateable($fieldName)
+ {
+ $mapping = $this->getFieldMapping($fieldName);
+
+ return $mapping !== false && isset($mapping['updateable']) && $mapping['updateable'];
+ }
+
/**
* Gets a column name for a field name.
* If the column name for the field cannot be found, the given field name
@@ -1282,6 +1318,8 @@ public function getColumnName($fieldName)
* columnName?: string,
* inherited?: class-string,
* nullable?: bool,
+ * insertable?: bool,
+ * updateable?: bool,
* originalClass?: class-string,
* originalField?: string,
* scale?: int,
diff --git a/lib/Doctrine/ORM/Mapping/Column.php b/lib/Doctrine/ORM/Mapping/Column.php
index 622b507a0e2..172bc0d2c6b 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 $updateable = true;
+
/** @var array */
public $options = [];
@@ -61,6 +67,8 @@ public function __construct(
?int $scale = null,
bool $unique = false,
bool $nullable = false,
+ ?bool $insertable = true,
+ ?bool $updateable = true,
array $options = [],
?string $columnDefinition = null
) {
@@ -71,6 +79,8 @@ public function __construct(
$this->scale = $scale;
$this->unique = $unique;
$this->nullable = $nullable;
+ $this->insertable = $insertable;
+ $this->updateable = $updateable;
$this->options = $options;
$this->columnDefinition = $columnDefinition;
}
diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
index 086a3baf879..8ef72f92fff 100644
--- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
+++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
@@ -720,6 +720,8 @@ private function joinColumnToArray(Mapping\JoinColumn $joinColumn): array
* unique: bool,
* nullable: bool,
* precision: int,
+ * insertable?: bool,
+ * updateble?: bool,
* options?: mixed[],
* columnName?: string,
* columnDefinition?: string
@@ -728,15 +730,23 @@ 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['insertable'] = $column->insertable;
+ }
+
+ if ($column->updateable) {
+ $mapping['updateable'] = $column->updateable;
+ }
+
if ($column->options) {
$mapping['options'] = $column->options;
}
diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php
index d6fc9ca8b47..262e88eaca8 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,
+ * insertable?: bool,
+ * updateable?: bool,
* version?: bool,
* columnDefinition?: string,
* options?: array
@@ -839,6 +841,14 @@ private function columnToArray(SimpleXMLElement $fieldMapping): array
$mapping['nullable'] = $this->evaluateBoolean($fieldMapping['nullable']);
}
+ if (isset($fieldMapping['insertable'])) {
+ $mapping['insertable'] = $this->evaluateBoolean($fieldMapping['insertable']);
+ }
+
+ if (isset($fieldMapping['updateable'])) {
+ $mapping['updateable'] = $this->evaluateBoolean($fieldMapping['updateable']);
+ }
+
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 97c73eae9a6..166e19816aa 100644
--- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php
+++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php
@@ -786,6 +786,8 @@ private function joinColumnToArray(array $joinColumnElement): array
* unique?: mixed,
* options?: mixed,
* nullable?: mixed,
+ * insertable?: mixed,
+ * updateable?: mixed,
* version?: mixed,
* columnDefinition?: mixed
* }|null $column
@@ -801,6 +803,8 @@ private function joinColumnToArray(array $joinColumnElement): array
* unique?: bool,
* options?: mixed,
* nullable?: mixed,
+ * insertable?: mixed,
+ * updateable?: mixed,
* version?: mixed,
* columnDefinition?: mixed
* }
@@ -848,6 +852,14 @@ private function columnToArray(string $fieldName, ?array $column): array
$mapping['nullable'] = $column['nullable'];
}
+ if (isset($column['insertable'])) {
+ $mapping['insertable'] = $column['insertable'];
+ }
+
+ if (isset($column['updateable'])) {
+ $mapping['updateable'] = $column['updateable'];
+ }
+
if (isset($column['version']) && $column['version']) {
$mapping['version'] = $column['version'];
}
diff --git a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php
index 8ce4c03ce11..a46c97a439a 100644
--- a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php
+++ b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php
@@ -22,6 +22,7 @@
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys;
use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
+use Doctrine\ORM\Persisters\Exception\NonUpdateableField;
use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
use Doctrine\ORM\Persisters\SqlExpressionVisitor;
use Doctrine\ORM\Persisters\SqlValueVisitor;
@@ -627,6 +628,19 @@ protected function prepareUpdateData($entity)
$fieldMapping = $this->class->fieldMappings[$field];
$columnName = $fieldMapping['columnName'];
+ $isInsert = ! empty($this->queuedInserts[spl_object_id($entity)]);
+ if (! $isInsert && ! $this->class->isUpdateable($field)) {
+ if ($change[0] !== $change[1]) {
+ throw NonUpdateableField::byName($field);
+ }
+
+ continue;
+ }
+
+ if ($isInsert && ! $this->class->isInsertable($field)) {
+ continue;
+ }
+
$this->columnTypes[$columnName] = $fieldMapping['type'];
$result[$this->getOwningTable($field)][$columnName] = $newVal;
@@ -1442,6 +1456,10 @@ protected function getInsertColumnList()
}
if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name) {
+ if (! $this->class->isInsertable($name)) {
+ 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/Exception/NonUpdateableField.php b/lib/Doctrine/ORM/Persisters/Exception/NonUpdateableField.php
new file mode 100644
index 00000000000..c4208fd3849
--- /dev/null
+++ b/lib/Doctrine/ORM/Persisters/Exception/NonUpdateableField.php
@@ -0,0 +1,17 @@
+addAttribute('nullable', $field['nullable'] ? 'true' : 'false');
}
+
+ if (isset($field['insertable'])) {
+ $fieldXml->addAttribute('insertable', $field['insertable'] ? 'true' : 'false');
+ }
+
+ if (isset($field['updateable'])) {
+ $fieldXml->addAttribute('updateable', $field['updateable'] ? 'true' : 'false');
+ }
}
}
diff --git a/tests/Doctrine/Tests/Models/Upsertable/Insertable.php b/tests/Doctrine/Tests/Models/Upsertable/Insertable.php
new file mode 100644
index 00000000000..18f9138c642
--- /dev/null
+++ b/tests/Doctrine/Tests/Models/Upsertable/Insertable.php
@@ -0,0 +1,44 @@
+entityManager = $this->getTestEntityManager();
+ }
+
+ public function testInsertSQLUsesInsertableColumns(): void
+ {
+ $persister = new BasicEntityPersister($this->entityManager, $this->entityManager->getClassMetadata(Insertable::class));
+ $method = new ReflectionMethod($persister, 'getInsertSQL');
+ $method->setAccessible(true);
+
+ self::assertEquals('INSERT INTO insertable_column (insertableContent, insertableContentDefault) VALUES (?, ?)', $method->invoke($persister));
+ }
+
+ public function testUpdateSQLUsesUpdateableColumns(): void
+ {
+ $persister = new BasicEntityPersister($this->entityManager, $this->entityManager->getClassMetadata(Updateable::class));
+
+ $entity = new Updateable();
+ $entity->id = 1;
+ $entity->nonUpdateableContent = 'non-persistable';
+ $entity->updateableContent = 'persistable';
+ $entity->updateableContentDefault = 'persistable';
+
+ $this->entityManager->getUnitOfWork()->registerManaged($entity, ['id' => 1], ['nonUpdateableContent' => 'default', 'updateableContent' => 'default', 'updateableContentDefault' => 'default']);
+
+ $this->entityManager->getUnitOfWork()->propertyChanged($entity, 'nonUpdateableContent', 'non-persistable', 'non-persistable');
+ $this->entityManager->getUnitOfWork()->propertyChanged($entity, 'updateableContent', 'default', 'persistable');
+ $this->entityManager->getUnitOfWork()->propertyChanged($entity, 'updateableContentDefault', 'default', 'persistable');
+
+ $persister->update($entity);
+
+ $executeStatements = $this->entityManager->getConnection()->getExecuteStatements();
+
+ self::assertEquals('UPDATE updateable_column SET updateableContent = ?, updateableContentDefault = ? WHERE id = ?', $executeStatements[0]['sql']);
+ }
+
+ public function testExceptionIsThrownWhenTryingToChangeNonUpdateableColumn(): void
+ {
+ $this->expectException(NonUpdateableField::class);
+
+ $persister = new BasicEntityPersister($this->entityManager, $this->entityManager->getClassMetadata(Updateable::class));
+
+ $entity = new Updateable();
+ $entity->nonUpdateableContent = 'non-persistable';
+ $entity->updateableContent = 'persistable';
+ $entity->updateableContentDefault = 'persistable';
+
+ $this->entityManager->getUnitOfWork()->registerManaged($entity, ['id' => 1], ['nonUpdateableContent' => 'default', 'updateableContent' => 'default', 'updateableContentDefault' => 'default']);
+
+ $this->entityManager->getUnitOfWork()->propertyChanged($entity, 'nonUpdateableContent', 'default', 'non-persistable');
+ $this->entityManager->getUnitOfWork()->propertyChanged($entity, 'updateableContent', 'default', 'persistable');
+ $this->entityManager->getUnitOfWork()->propertyChanged($entity, 'updateableContentDefault', 'default', 'persistable');
+
+ $persister->update($entity);
+ }
+}