Skip to content

Commit

Permalink
Extract FieldMapping in its own DTO
Browse files Browse the repository at this point in the history
In the past, it has been decided to use arrays for this out of
legitimate performance concerns. But PHP has evolved, and now, it is
more performant and memory efficient to use objects.
  • Loading branch information
greg0ire committed Mar 31, 2023
1 parent e8376b3 commit fcda732
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 119 deletions.
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;

/** @var int|null The database length of the column. Optional. Default value taken from the type. */
public int|null $length = null;
/**
* @var bool|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;
/** ClassMetadata::GENERATED_* */
public int|null $generated = null;
/** @var class-string<BackedEnum>|null */
public string|null $enumType = null;
/**
* @var int|null The precision of a decimal column.
* Only valid if the column type is decimal
*/
public int|null $precision = null;
/**
* @var int|null The scale of a decimal column.
* Only valid if the column type is decimal
*/
public int|null $scale = null;
/** @var bool|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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\FieldMapping;
use Doctrine\Persistence\Mapping\MappingException;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
Expand Down Expand Up @@ -42,7 +43,6 @@
* @link www.doctrine-project.org
*
* @psalm-import-type AssociationMapping from ClassMetadata
* @psalm-import-type FieldMapping from ClassMetadata
*/
final class MappingDescribeCommand extends AbstractEntityManagerCommand
{
Expand Down Expand Up @@ -257,7 +257,7 @@ private function formatMappings(array $propertyMappings): array
foreach ($propertyMappings as $propertyName => $mapping) {
$output[] = $this->formatField(sprintf(' %s', $propertyName), '');

foreach ($mapping as $field => $value) {
foreach ((array) $mapping as $field => $value) {
$output[] = $this->formatField(sprintf(' %s', $field), $this->formatValue($value));
}
}
Expand Down
Loading

0 comments on commit fcda732

Please sign in to comment.