Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Field mapping DTO #10607

Merged
merged 2 commits into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions lib/Doctrine/ORM/Mapping/ArrayAccessImplementation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Mapping;

use InvalidArgumentException;

use function property_exists;

/** @internal */
trait ArrayAccessImplementation
{
/** @param string $offset */
public function offsetExists(mixed $offset): bool
{
return isset($this->$offset);
}

/** @param string $offset */
public function offsetGet(mixed $offset): mixed
{
if (! property_exists($this, $offset)) {
throw new InvalidArgumentException('Undefined property: ' . $offset);
}

return $this->$offset;
}

/** @param string $offset */
public function offsetSet(mixed $offset, mixed $value): void
{
$this->$offset = $value;
}

/** @param string $offset */
public function offsetUnset(mixed $offset): void
{
$this->$offset = null;
}
}
104 changes: 11 additions & 93 deletions lib/Doctrine/ORM/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
use function in_array;
use function interface_exists;
use function is_array;
use function is_string;
use function is_subclass_of;
use function ltrim;
use function method_exists;
Expand All @@ -66,32 +67,6 @@
*
* @template-covariant T of object
* @template-implements PersistenceClassMetadata<T>
* @psalm-type FieldMapping = array{
* type: string,
* fieldName: string,
* columnName: string,
* length?: int,
* id?: bool,
* nullable?: bool,
* notInsertable?: bool,
* notUpdatable?: bool,
* generated?: int,
* enumType?: class-string<BackedEnum>,
* columnDefinition?: string,
* precision?: int,
* scale?: int,
* unique?: bool,
* inherited?: class-string,
* originalClass?: class-string,
* originalField?: string,
* quoted?: bool,
* requireSQLConversion?: bool,
* declared?: class-string,
* declaredField?: string,
* options?: array<string, mixed>,
* version?: string,
* default?: string|int,
* }
* @psalm-type JoinColumnData = array{
* name: string,
* referencedColumnName: string,
Expand Down Expand Up @@ -442,65 +417,9 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable

/**
* READ-ONLY: The field mappings of the class.
* Keys are field names and values are mapping definitions.
*
* The mapping definition array has the following values:
*
* - <b>fieldName</b> (string)
* The name of the field in the Entity.
*
* - <b>type</b> (string)
* The type name of the mapped field. Can be one of Doctrine's mapping types
* or a custom mapping type.
*
* - <b>columnName</b> (string, optional)
* The column name. Optional. Defaults to the field name.
*
* - <b>length</b> (integer, optional)
* The database length of the column. Optional. Default value taken from
* the type.
*
* - <b>id</b> (boolean, optional)
* Marks the field as the primary key of the entity. Multiple fields of an
* entity can have the id attribute, forming a composite key.
* Keys are field names and values are FieldMapping instances
*
* - <b>nullable</b> (boolean, optional)
* Whether the column is nullable. Defaults to FALSE.
*
* - <b>'notInsertable'</b> (boolean, optional)
* Whether the column is not insertable. Optional. Is only set if value is TRUE.
*
* - <b>'notUpdatable'</b> (boolean, optional)
* Whether the column is updatable. Optional. Is only set if value is TRUE.
*
* - <b>columnDefinition</b> (string, optional, schema-only)
* The SQL fragment that is used when generating the DDL for the column.
*
* - <b>precision</b> (integer, optional, schema-only)
* The precision of a decimal column. Only valid if the column type is decimal.
*
* - <b>scale</b> (integer, optional, schema-only)
* The scale of a decimal column. Only valid if the column type is decimal.
*
* - <b>'unique'</b> (boolean, optional, schema-only)
* Whether a unique constraint should be generated for the column.
*
* - <b>'inherited'</b> (string, optional)
* This is set when the field is inherited by this class from another (inheritance) parent
* <em>entity</em> class. The value is the FQCN of the topmost entity class that contains
* mapping information for this field. (If there are transient classes in the
* class hierarchy, these are ignored, so the class property may in fact come
* from a class further up in the PHP class hierarchy.)
* Fields initially declared in mapped superclasses are
* <em>not</em> considered 'inherited' in the nearest entity subclasses.
*
* - <b>'declared'</b> (string, optional)
* This is set when the field does not appear for the first time in this class, but is originally
* declared in another parent <em>entity or mapped superclass</em>. The value is the FQCN
* of the topmost non-transient class that contains mapping information for this field.
*
* @var mixed[]
* @psalm-var array<string, FieldMapping>
* @var array<string, FieldMapping>
*/
public array $fieldMappings = [];

Expand Down Expand Up @@ -1283,12 +1202,9 @@ public function getColumnName(string $fieldName): string
* Gets the mapping of a (regular) field that holds some data but not a
* reference to another object.
*
* @return mixed[] The field mapping.
* @psalm-return FieldMapping
*
* @throws MappingException
*/
public function getFieldMapping(string $fieldName): array
public function getFieldMapping(string $fieldName): FieldMapping
{
if (! isset($this->fieldMappings[$fieldName])) {
throw MappingException::mappingNotFound($this->name, $fieldName);
Expand Down Expand Up @@ -1403,7 +1319,7 @@ private function validateAndCompleteTypedAssociationMapping(array $mapping): arr
*
* @throws MappingException
*/
protected function validateAndCompleteFieldMapping(array $mapping): array
protected function validateAndCompleteFieldMapping(array $mapping): FieldMapping
{
// Check mandatory fields
if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) {
Expand All @@ -1424,6 +1340,8 @@ protected function validateAndCompleteFieldMapping(array $mapping): array
$mapping['columnName'] = $this->namingStrategy->propertyToColumnName($mapping['fieldName'], $this->name);
}

$mapping = FieldMapping::fromMappingArray($mapping);

if ($mapping['columnName'][0] === '`') {
$mapping['columnName'] = trim($mapping['columnName'], '`');
$mapping['quoted'] = true;
Expand Down Expand Up @@ -1535,6 +1453,7 @@ protected function _validateAndCompleteAssociationMapping(array $mapping): array
);
}

assert(is_string($mapping['fieldName']));
$this->identifier[] = $mapping['fieldName'];
$this->containsForeignIdentifier = true;
}
Expand Down Expand Up @@ -2338,10 +2257,8 @@ public function addInheritedAssociationMapping(array $mapping/*, $owningClassNam
* INTERNAL:
* Adds a field mapping without completing/validating it.
* This is mainly used to add inherited field mappings to derived classes.
*
* @psalm-param FieldMapping $fieldMapping
*/
public function addInheritedFieldMapping(array $fieldMapping): void
public function addInheritedFieldMapping(FieldMapping $fieldMapping): void
{
$this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping;
$this->columnNames[$fieldMapping['fieldName']] = $fieldMapping['columnName'];
Expand Down Expand Up @@ -2942,7 +2859,8 @@ public function mapEmbedded(array $mapping): void
*/
public function inlineEmbeddable(string $property, ClassMetadata $embeddable): void
{
foreach ($embeddable->fieldMappings as $fieldMapping) {
foreach ($embeddable->fieldMappings as $originalFieldMapping) {
$fieldMapping = (array) $originalFieldMapping;
$fieldMapping['originalClass'] ??= $embeddable->name;
$fieldMapping['declaredField'] = isset($fieldMapping['declaredField'])
? $property . '.' . $fieldMapping['declaredField']
Expand Down
8 changes: 4 additions & 4 deletions lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
* @extends AbstractClassMetadataFactory<ClassMetadata>
* @psalm-import-type AssociationMapping from ClassMetadata
* @psalm-import-type EmbeddedClassMapping from ClassMetadata
* @psalm-import-type FieldMapping from ClassMetadata
*/
class ClassMetadataFactory extends AbstractClassMetadataFactory
{
Expand Down Expand Up @@ -388,7 +387,7 @@ private function getShortName(string $className): string
*
* @param AssociationMapping|EmbeddedClassMapping|FieldMapping $mapping
*/
private function addMappingInheritanceInformation(array &$mapping, ClassMetadata $parentClass): void
private function addMappingInheritanceInformation(array|FieldMapping &$mapping, ClassMetadata $parentClass): void
{
if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) {
$mapping['inherited'] = $parentClass->name;
Expand All @@ -405,8 +404,9 @@ private function addMappingInheritanceInformation(array &$mapping, ClassMetadata
private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass): void
{
foreach ($parentClass->fieldMappings as $mapping) {
$this->addMappingInheritanceInformation($mapping, $parentClass);
$subClass->addInheritedFieldMapping($mapping);
$subClassMapping = clone $mapping;
$this->addMappingInheritanceInformation($subClassMapping, $parentClass);
$subClass->addInheritedFieldMapping($subClassMapping);
}

foreach ($parentClass->reflFields as $name => $field) {
Expand Down
142 changes: 142 additions & 0 deletions lib/Doctrine/ORM/Mapping/FieldMapping.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Mapping;

use ArrayAccess;
use BackedEnum;

use function in_array;
use function property_exists;

/** @template-implements ArrayAccess<string, mixed> */
final class FieldMapping implements ArrayAccess
{
use ArrayAccessImplementation;

/** The database length of the column. Optional. Default value taken from the type. */
public int|null $length = null;
/**
* Marks the field as the primary key of the entity. Multiple
* fields of an entity can have the id attribute, forming a composite key.
*/
public bool|null $id = null;
public bool|null $nullable = null;
public bool|null $notInsertable = null;
public bool|null $notUpdatable = null;
public string|null $columnDefinition = null;
/** @psalm-var ClassMetadata::GENERATED_* */
public int|null $generated = null;
/** @var class-string<BackedEnum>|null */
public string|null $enumType = null;
/**
* The precision of a decimal column.
* Only valid if the column type is decimal
*/
public int|null $precision = null;
/**
* The scale of a decimal column.
* Only valid if the column type is decimal
*/
public int|null $scale = null;
/** Whether a unique constraint should be generated for the column. */
public bool|null $unique = null;
/**
* @var class-string|null This is set when the field is inherited by this
* class from another (inheritance) parent <em>entity</em> class. The value
* is the FQCN of the topmost entity class that contains mapping information
* for this field. (If there are transient classes in the class hierarchy,
* these are ignored, so the class property may in fact come from a class
* further up in the PHP class hierarchy.)
* Fields initially declared in mapped superclasses are
* <em>not</em> considered 'inherited' in the nearest entity subclasses.
*/
public string|null $inherited = null;

public string|null $originalClass = null;
public string|null $originalField = null;
public bool|null $quoted = null;
/**
* @var class-string|null This is set when the field does not appear for
* the first time in this class, but is originally declared in another
* parent <em>entity or mapped superclass</em>. The value is the FQCN of
* the topmost non-transient class that contains mapping information for
* this field.
*/
public string|null $declared = null;
public string|null $declaredField = null;
public array|null $options = null;
public bool|null $version = null;
public string|int|null $default = null;

/**
* @param string $type The type name of the mapped field. Can be one of
* Doctrine's mapping types or a custom mapping type.
* @param string $fieldName The name of the field in the Entity.
* @param string $columnName The column name. Optional. Defaults to the field name.
*/
public function __construct(
public string $type,
public string $fieldName,
public string $columnName,
) {
}

/** @param array{type: string, fieldName: string, columnName: string} $mappingArray */
public static function fromMappingArray(array $mappingArray): self
{
$mapping = new self(
$mappingArray['type'],
$mappingArray['fieldName'],
$mappingArray['columnName'],
);
foreach ($mappingArray as $key => $value) {
if (in_array($key, ['type', 'fieldName', 'columnName'])) {
continue;
}

if (property_exists($mapping, $key)) {
$mapping->$key = $value;
}
}

return $mapping;
}

/** @return list<string> */
public function __sleep(): array
{
$serialized = ['type', 'fieldName', 'columnName'];

foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted'] as $boolKey) {
if ($this->$boolKey) {
$serialized[] = $boolKey;
}
}

foreach (
[
'length',
'columnDefinition',
'generated',
'enumType',
'precision',
'scale',
'inherited',
'originalClass',
'originalField',
'declared',
'declaredField',
'options',
'default',
] as $key
) {
if ($this->$key !== null) {
$serialized[] = $key;
}
}

return $serialized;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1847,7 +1847,7 @@ private function getTypes(string $field, mixed $value, ClassMetadata $class): ar

switch (true) {
case isset($class->fieldMappings[$field]):
$types = array_merge($types, [$class->fieldMappings[$field]['type']]);
$types = array_merge($types, [$class->fieldMappings[$field]->type]);
break;

case isset($class->associationMappings[$field]):
Expand Down
Loading