Skip to content

Commit e5caafe

Browse files
authored
feat: manage association overrides for Doctrine (#385)
1 parent 5c7bb2f commit e5caafe

29 files changed

+332
-96
lines changed

phpstan.neon

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ parameters:
77
array{
88
exclude: boolean,
99
range: ?string,
10-
relationTableName: ?string,
1110
cardinality: string,
12-
ormColumn: array<string, string|string[]>,
1311
groups: string[],
1412
mappedBy: ?string,
1513
inversedBy: ?string,
@@ -51,7 +49,14 @@ parameters:
5149
header: ?string,
5250
namespaces: array{prefix: ?string, entity: string, enum: string, interface: string},
5351
uses: array<string, array{alias: ?string}>,
54-
doctrine: array{useCollection: boolean, resolveTargetEntityConfigPath: ?string, resolveTargetEntityConfigType: 'XML'|'yaml', inheritanceAttributes: array<string, (int|bool|null|string|string[]|string[][]|\Nette\PhpGenerator\Literal)[]>, inheritanceType: 'JOINED'|'SINGLE_TABLE'|'SINGLE_COLLECTION'|'TABLE_PER_CLASS'|'COLLECTION_PER_CLASS'|'NONE'},
52+
doctrine: array{
53+
useCollection: boolean,
54+
resolveTargetEntityConfigPath: ?string,
55+
resolveTargetEntityConfigType: 'XML'|'yaml',
56+
inheritanceAttributes: array<string, (int|bool|null|string|string[]|string[][]|\Nette\PhpGenerator\Literal)[]>,
57+
inheritanceType: 'JOINED'|'SINGLE_TABLE'|'SINGLE_COLLECTION'|'TABLE_PER_CLASS'|'COLLECTION_PER_CLASS'|'NONE',
58+
maxIdentifierLength: integer
59+
},
5560
validator: array{assertType: boolean},
5661
author: false|string,
5762
fieldVisibility: string,

src/AttributeGenerator/AbstractAttributeGenerator.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ public function generatePropertyAttributes(Property $property, string $className
6363
return [];
6464
}
6565

66+
/**
67+
* {@inheritdoc}
68+
*/
69+
public function generateLateClassAttributes(Class_ $class): array
70+
{
71+
return [];
72+
}
73+
6674
/**
6775
* {@inheritdoc}
6876
*/

src/AttributeGenerator/ApiPlatformCoreAttributeGenerator.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,9 @@ public function generateClassAttributes(Class_ $class): array
8181
} else {
8282
$arguments['operations'] = [];
8383
foreach ($class->operations as $operationMetadataClass => $methodConfig) {
84-
$arguments['operations'][] = new Literal(sprintf('new %s(%s)',
84+
$arguments['operations'][] = new Literal(sprintf('new %s(...?:)',
8585
$operationMetadataClass,
86-
implode(', ', array_map(
87-
fn ($k, $v) => sprintf('%s: %s', $k, (\is_string($v) ? sprintf("'%s'", addslashes($v)) : (\is_scalar($v) ? $v : ''))),
88-
array_keys($methodConfig ?? []), array_values($methodConfig ?? [])
89-
))
90-
));
86+
), [$methodConfig ?? []]);
9187
}
9288
}
9389
}

src/AttributeGenerator/AttributeGeneratorInterface.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
use ApiPlatform\SchemaGenerator\Model\Use_;
2020

2121
/**
22-
* Attribute Generator Interface.
23-
*
2422
* @author Kévin Dunglas <[email protected]>
2523
*/
2624
interface AttributeGeneratorInterface
@@ -39,6 +37,13 @@ public function generateClassAttributes(Class_ $class): array;
3937
*/
4038
public function generatePropertyAttributes(Property $property, string $className): array;
4139

40+
/**
41+
* Generates class attributes once class and properties attributes for all classes have been generated.
42+
*
43+
* @return Attribute[]
44+
*/
45+
public function generateLateClassAttributes(Class_ $class): array;
46+
4247
/**
4348
* Generates uses.
4449
*
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\SchemaGenerator\AttributeGenerator;
15+
16+
use ApiPlatform\SchemaGenerator\Model\Attribute;
17+
use ApiPlatform\SchemaGenerator\Model\Class_;
18+
use Nette\PhpGenerator\Literal;
19+
20+
final class DoctrineOrmAssociationOverrideAttributeGenerator extends AbstractAttributeGenerator
21+
{
22+
use GenerateIdentifierNameTrait;
23+
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
public function generateLateClassAttributes(Class_ $class): array
28+
{
29+
if ($class->isAbstract || !($parentName = $class->parent())) {
30+
return [];
31+
}
32+
33+
$attributes = [];
34+
$associationOverrides = [];
35+
36+
while ($parentName) {
37+
$parent = $this->classes[$parentName] ?? null;
38+
if (!$parent || !$parent->isAbstract) {
39+
$parentName = null;
40+
41+
continue;
42+
}
43+
44+
foreach ($parent->properties() as $property) {
45+
if ((
46+
$joinTableAttribute = $property->getAttributeWithName('ORM\JoinTable'))
47+
&& \is_string($joinTableName = $joinTableAttribute->args()['name'])) {
48+
$overrideJoinTableName = $this->generateIdentifierName($joinTableName.$class->name(), 'join_table', $this->config);
49+
$overrideArgs = [
50+
'name' => $property->name(),
51+
'joinTable' => new Literal("new ORM\JoinTable(...?:)", [['name' => $overrideJoinTableName]]),
52+
];
53+
54+
$joinColumnAttribute = $property->getAttributeWithName('ORM\JoinColumn');
55+
$overrideArgs['joinColumns'] = [new Literal("new ORM\JoinColumn(...?:)", [$joinColumnAttribute ? $joinColumnAttribute->args() : []])];
56+
57+
$inverseJoinColumnAttribute = $property->getAttributeWithName('ORM\InverseJoinColumn');
58+
$overrideArgs['inverseJoinColumns'] = [new Literal("new ORM\InverseJoinColumn(...?:)", [$inverseJoinColumnAttribute ? $inverseJoinColumnAttribute->args() : []])];
59+
60+
$associationOverrides[] = new Literal(
61+
"new ORM\AssociationOverride(...?:)",
62+
[$overrideArgs]
63+
);
64+
}
65+
}
66+
67+
$parentName = $parent->parent();
68+
}
69+
70+
if ($associationOverrides) {
71+
$attributes[] = new Attribute('ORM\AssociationOverrides', [$associationOverrides]);
72+
}
73+
74+
return $attributes;
75+
}
76+
}

src/AttributeGenerator/DoctrineOrmAttributeGenerator.php

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
*/
2929
final class DoctrineOrmAttributeGenerator extends AbstractAttributeGenerator
3030
{
31+
use GenerateIdentifierNameTrait;
32+
3133
private const RESERVED_KEYWORDS = [
3234
'add',
3335
'create',
@@ -85,7 +87,7 @@ public function generateClassAttributes(Class_ $class): array
8587
continue;
8688
}
8789

88-
$attributes[] = new Attribute('ORM\Table', ['name' => strtolower($class->name())]);
90+
$attributes[] = new Attribute('ORM\Table', ['name' => $this->generateIdentifierName($class->name(), 'table', $this->config)]);
8991
}
9092

9193
return $attributes;
@@ -100,18 +102,10 @@ public function generatePropertyAttributes(Property $property, string $className
100102
return [];
101103
}
102104

103-
if ($property->ormColumn) {
104-
return [new Attribute('ORM\Column', $property->ormColumn)];
105-
}
106-
107105
if ($property->isId) {
108106
return $this->generateIdAttributes();
109107
}
110108

111-
if (isset($this->config['types'][$className]['properties'][$property->name()])) {
112-
$property->relationTableName = $this->config['types'][$className]['properties'][$property->name()]['relationTableName'];
113-
}
114-
115109
$type = null;
116110
if ($property->isEnum) {
117111
$type = $property->isArray ? 'simple_array' : 'string';
@@ -173,14 +167,22 @@ public function generatePropertyAttributes(Property $property, string $className
173167
return [new Attribute('ORM\Column', $args)];
174168
}
175169

176-
if (null === $relationName = $this->getRelationName($property, $className)) {
170+
if (!$property->reference) {
171+
$this->logger ? $this->logger->error('There is no reference for the property "{property}" from the class "{class}"', ['property' => $property->name(), 'class' => $className]) : null;
172+
173+
return [];
174+
}
175+
176+
if (null === $relationName = $this->getRelationName($property)) {
177177
return [];
178178
}
179179

180180
if ($property->isEmbedded) {
181181
return [new Attribute('ORM\Embedded', ['class' => $relationName, 'columnPrefix' => $property->columnPrefix])];
182182
}
183183

184+
$relationTableName = $this->generateIdentifierName($className.ucfirst($property->reference->name()).ucfirst($property->name()), 'join_table', $this->config);
185+
184186
$attributes = [];
185187
switch ($property->cardinality) {
186188
case CardinalitiesExtractor::CARDINALITY_0_1:
@@ -212,9 +214,7 @@ public function generatePropertyAttributes(Property $property, string $className
212214
} else {
213215
$attributes[] = new Attribute('ORM\ManyToMany', ['targetEntity' => $relationName]);
214216
}
215-
if ($property->relationTableName) {
216-
$attributes[] = new Attribute('ORM\JoinTable', ['name' => $property->relationTableName]);
217-
}
217+
$attributes[] = new Attribute('ORM\JoinTable', ['name' => $relationTableName]);
218218
$attributes[] = new Attribute('ORM\InverseJoinColumn', ['unique' => true]);
219219
break;
220220
case CardinalitiesExtractor::CARDINALITY_1_N:
@@ -223,16 +223,12 @@ public function generatePropertyAttributes(Property $property, string $className
223223
} else {
224224
$attributes[] = new Attribute('ORM\ManyToMany', ['targetEntity' => $relationName]);
225225
}
226-
if ($property->relationTableName) {
227-
$attributes[] = new Attribute('ORM\JoinTable', ['name' => $property->relationTableName]);
228-
}
226+
$attributes[] = new Attribute('ORM\JoinTable', ['name' => $relationTableName]);
229227
$attributes[] = new Attribute('ORM\InverseJoinColumn', ['nullable' => false, 'unique' => true]);
230228
break;
231229
case CardinalitiesExtractor::CARDINALITY_N_N:
232230
$attributes[] = new Attribute('ORM\ManyToMany', ['targetEntity' => $relationName]);
233-
if ($property->relationTableName) {
234-
$attributes[] = new Attribute('ORM\JoinTable', ['name' => $property->relationTableName]);
235-
}
231+
$attributes[] = new Attribute('ORM\JoinTable', ['name' => $relationTableName]);
236232
break;
237233
}
238234

@@ -277,13 +273,11 @@ private function generateIdAttributes(): array
277273
/**
278274
* Gets class or interface name to use in relations.
279275
*/
280-
private function getRelationName(Property $property, string $className): ?string
276+
private function getRelationName(Property $property): ?string
281277
{
282278
$reference = $property->reference;
283279

284280
if (!$reference) {
285-
$this->logger ? $this->logger->error('There is no reference for the property "{property}" from the class "{class}"', ['property' => $property->name(), 'class' => $className]) : null;
286-
287281
return null;
288282
}
289283

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\SchemaGenerator\AttributeGenerator;
15+
16+
use function Symfony\Component\String\u;
17+
18+
trait GenerateIdentifierNameTrait
19+
{
20+
/**
21+
* @param Configuration $config
22+
*/
23+
public function generateIdentifierName(string $name, string $defaultName, array $config): string
24+
{
25+
$maxIdentifierLength = $config['doctrine']['maxIdentifierLength'];
26+
27+
$identifierName = u($name)->snake()->toString();
28+
29+
if (\strlen($identifierName) > $maxIdentifierLength) {
30+
$identifierName = $defaultName.'_'.hash('adler32', $name);
31+
if (\strlen($identifierName) > $maxIdentifierLength) {
32+
throw new \RuntimeException('Identifier name with default name exceeds maximum identifier length.');
33+
}
34+
}
35+
36+
return $identifierName;
37+
}
38+
}

src/ClassMutator/AttributeAppender.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ public function __invoke(Class_ $class, array $context): void
4444
$this->generatePropertiesAttributes($class);
4545
}
4646

47+
public function appendLate(Class_ $class): void
48+
{
49+
$this->generateLateClassAttributes($class);
50+
}
51+
4752
private function generateClassUses(Class_ $class): void
4853
{
4954
$interfaceNamespace = isset($this->classes[$class->name()]) ? $this->classes[$class->name()]->interfaceNamespace() : null;
@@ -87,4 +92,13 @@ private function generatePropertiesAttributes(Class_ $class): void
8792
}
8893
}
8994
}
95+
96+
private function generateLateClassAttributes(Class_ $class): void
97+
{
98+
foreach ($this->attributeGenerators as $generator) {
99+
foreach ($generator->generateLateClassAttributes($class) as $attribute) {
100+
$class->addAttribute($attribute);
101+
}
102+
}
103+
}
90104
}

src/Model/AddAttributeTrait.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ trait AddAttributeTrait
1818
public function addAttribute(Attribute $attribute): self
1919
{
2020
if (!\in_array($attribute, $this->attributes, true)) {
21-
if (empty(array_filter(
22-
$this->attributes,
23-
fn (Attribute $attr) => $attr->name() === $attribute->name()
24-
))) {
21+
if (!$this->getAttributeWithName($attribute->name())) {
2522
if ($attribute->append) {
2623
$this->attributes[] = $attribute;
2724
}
@@ -37,4 +34,12 @@ public function addAttribute(Attribute $attribute): self
3734

3835
return $this;
3936
}
37+
38+
public function getAttributeWithName(string $name): ?Attribute
39+
{
40+
return array_values(array_filter(
41+
$this->attributes,
42+
fn (Attribute $attr) => $attr->name() === $name
43+
))[0] ?? null;
44+
}
4045
}

src/Model/Property.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ abstract class Property
3232
/** @var bool can be true and array type false if the property is an array of references */
3333
public bool $isArray = false;
3434
public ?Class_ $reference = null;
35-
/** @var array<string, string|string[]> */
36-
public ?array $ormColumn = null;
3735
public bool $isReadable = true;
3836
public bool $isWritable = true;
3937
public bool $isRequired = false;
@@ -47,7 +45,6 @@ abstract class Property
4745
public $columnPrefix = false;
4846
public bool $isId = false;
4947
public ?string $typeHint = null;
50-
public ?string $relationTableName = null;
5148
public bool $isEnum = false;
5249
public ?string $adderRemoverTypeHint = null;
5350
/** @var string[] */

0 commit comments

Comments
 (0)