Skip to content

Commit

Permalink
Extract FieldMapping in its own DTO
Browse files Browse the repository at this point in the history
  • Loading branch information
greg0ire committed Jan 21, 2023
1 parent 7862b99 commit 8c4ffc1
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 113 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?: string,
* 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 @@ -412,65 +387,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> (string, 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 @@ -1252,12 +1171,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 @@ -1372,7 +1288,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 @@ -1393,6 +1309,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 @@ -1522,6 +1440,7 @@ protected function _validateAndCompleteAssociationMapping(array $mapping)
);
}

assert(is_string($mapping['fieldName']));
$this->identifier[] = $mapping['fieldName'];
$this->containsForeignIdentifier = true;
}
Expand Down Expand Up @@ -2388,10 +2307,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 array<string, mixed> $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 @@ -2968,7 +2885,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
9 changes: 5 additions & 4 deletions lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,9 @@ private function getShortName(string $className): string
* Puts the `inherited` and `declared` values into mapping information for fields, associations
* and embedded classes.
*
* @param mixed[] $mapping
* @param mixed[]|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 @@ -345,8 +345,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
132 changes: 132 additions & 0 deletions lib/Doctrine/ORM/Mapping/FieldMapping.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Mapping;

use ArrayAccess;

use function in_array;
use function property_exists;

/** @template-implements ArrayAccess<string, mixed> */
final class FieldMapping implements ArrayAccess
{
/** @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|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;
}

/**
* {@inheritDoc}
*/
public function offsetExists($offset): bool
{
return isset($this->$offset);
}

public function offsetGet($offset): mixed
{
return $this->$offset;
}

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

/**
* {@inheritDoc}
*/
public function offsetUnset($offset): void
{
$this->$offset = null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1845,7 +1845,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 @@ -242,7 +243,7 @@ private function formatField(string $label, mixed $value): array
/**
* Format the association mappings
*
* @psalm-param array<string, array<string, mixed>> $propertyMappings
* @psalm-param array<string, FieldMapping|array<string, mixed>> $propertyMappings
*
* @return string[][]
* @psalm-return list<array{0: string, 1: string}>
Expand All @@ -254,7 +255,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
9 changes: 5 additions & 4 deletions lib/Doctrine/ORM/Tools/SchemaTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\FieldMapping;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Mapping\QuoteStrategy;
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
Expand Down Expand Up @@ -447,11 +448,11 @@ private function gatherColumns(ClassMetadata $class, Table $table): void
* Creates a column definition as required by the DBAL from an ORM field mapping definition.
*
* @param ClassMetadata $class The class that owns the field mapping.
* @psalm-param array<string, mixed> $mapping The field mapping.
* @psalm-param FieldMapping $mapping The field mapping.
*/
private function gatherColumn(
ClassMetadata $class,
array $mapping,
FieldMapping $mapping,
Table $table,
): void {
$columnName = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
Expand Down Expand Up @@ -766,11 +767,11 @@ private function gatherRelationJoinColumns(
}

/**
* @param mixed[] $mapping
* @param FieldMapping|mixed[] $mapping
*
* @return mixed[]
*/
private function gatherColumnOptions(array $mapping): array
private function gatherColumnOptions(FieldMapping|array $mapping): array
{
$mappingOptions = $mapping['options'] ?? [];

Expand Down
Loading

0 comments on commit 8c4ffc1

Please sign in to comment.