Skip to content

Commit 892ed14

Browse files
authored
feat(make:factory): improve Doctrine default fields guesser (zenstruck#364)
1 parent 7b08360 commit 892ed14

29 files changed

+684
-171
lines changed

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ database-generate-migration: docker-start vendor database-drop-schema ### Genera
7373
@${DOCKER_PHP} vendor/bin/doctrine-migrations migrations:migrate --no-interaction --allow-no-migration # first, let's load into db existing migrations
7474
@${DOCKER_PHP} vendor/bin/doctrine-migrations migrations:diff --no-interaction
7575
@${DOCKER_PHP} vendor/bin/doctrine-migrations migrations:migrate --no-interaction # load the new migration
76+
@${DOCKER_PHP} bin/doctrine orm:validate-schema
7677

7778
database-validate-mapping: docker-start vendor database-drop-schema ### Validate mapping in Zenstruck\Foundry\Tests\Fixtures\Entity
7879
@${DOCKER_PHP} vendor/bin/doctrine-migrations migrations:migrate --no-interaction --allow-no-migration

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"doctrine/persistence": "^1.3.3|^2.0|^3.0",
1717
"fakerphp/faker": "^1.5",
1818
"symfony/deprecation-contracts": "^2.2|^3.0",
19-
"symfony/property-access": "^3.4|^4.4|^5.0|^6.0",
19+
"symfony/property-access": "^4.4|^5.0|^6.0",
2020
"symfony/string": "^5.4|^6.0",
2121
"zenstruck/assert": "^1.0",
2222
"zenstruck/callback": "^1.1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\Bundle\Maker\Factory;
6+
7+
use Doctrine\Persistence\ManagerRegistry;
8+
use Doctrine\Persistence\Mapping\ClassMetadata;
9+
use Doctrine\Persistence\ObjectManager;
10+
11+
/** @internal */
12+
abstract class AbstractDoctrineDefaultPropertiesGuesser implements DefaultPropertiesGuesser
13+
{
14+
public function __construct(protected ManagerRegistry $managerRegistry, private FactoryFinder $factoryFinder)
15+
{
16+
}
17+
18+
/** @param class-string $fieldClass */
19+
protected function addDefaultValueUsingFactory(MakeFactoryData $makeFactoryData, string $fieldName, string $fieldClass, bool $isMultiple = false): void
20+
{
21+
if (!$factoryClass = $this->factoryFinder->getFactoryForClass($fieldClass)) {
22+
$makeFactoryData->addDefaultProperty(\lcfirst($fieldName), "null, // TODO add {$fieldClass} type manually");
23+
24+
return;
25+
}
26+
27+
$factoryMethod = $isMultiple ? 'new()->many(5)' : 'new()';
28+
29+
$factory = new \ReflectionClass($factoryClass);
30+
$makeFactoryData->addUse($factory->getName());
31+
$makeFactoryData->addDefaultProperty(\lcfirst($fieldName), "{$factory->getShortName()}::{$factoryMethod},");
32+
}
33+
34+
protected function getClassMetadata(MakeFactoryData $makeFactoryData): ClassMetadata
35+
{
36+
$class = $makeFactoryData->getObjectFullyQualifiedClassName();
37+
38+
$em = $this->managerRegistry->getManagerForClass($class);
39+
40+
if (!$em instanceof ObjectManager) {
41+
throw new \InvalidArgumentException("\"{$class}\" is not a valid Doctrine class name.");
42+
}
43+
44+
return $em->getClassMetadata($class);
45+
}
46+
}

src/Bundle/Maker/Factory/DefaultPropertiesGuesser.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ interface DefaultPropertiesGuesser
99
{
1010
public function __invoke(MakeFactoryData $makeFactoryData, bool $allFields): void;
1111

12-
public function supports(bool $persisted): bool;
12+
public function supports(MakeFactoryData $makeFactoryData): bool;
1313
}

src/Bundle/Maker/Factory/DoctrineObjectDefaultPropertiesGuesser.php

-121
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Zenstruck\Foundry\Bundle\Maker\Factory;
6+
7+
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ODMClassMetadata;
8+
use Doctrine\ORM\Mapping\ClassMetadataInfo as ORMClassMetadata;
9+
10+
/**
11+
* @internal
12+
*/
13+
final class DoctrineScalarFieldsDefaultPropertiesGuesser extends AbstractDoctrineDefaultPropertiesGuesser
14+
{
15+
private const DEFAULTS = [
16+
'ARRAY' => '[],',
17+
'ASCII_STRING' => 'self::faker()->text({length}),',
18+
'BIGINT' => 'self::faker()->randomNumber(),',
19+
'BLOB' => 'self::faker()->text(),',
20+
'BOOLEAN' => 'self::faker()->boolean(),',
21+
'DATE' => 'self::faker()->dateTime(),',
22+
'DATE_MUTABLE' => 'self::faker()->dateTime(),',
23+
'DATE_IMMUTABLE' => '\DateTimeImmutable::createFromMutable(self::faker()->dateTime()),',
24+
'DATETIME_MUTABLE' => 'self::faker()->dateTime(),',
25+
'DATETIME_IMMUTABLE' => '\DateTimeImmutable::createFromMutable(self::faker()->dateTime()),',
26+
'DATETIMETZ_MUTABLE' => 'self::faker()->dateTime(),',
27+
'DATETIMETZ_IMMUTABLE' => '\DateTimeImmutable::createFromMutable(self::faker()->dateTime()),',
28+
'DECIMAL' => 'self::faker()->randomFloat(),',
29+
'FLOAT' => 'self::faker()->randomFloat(),',
30+
'INTEGER' => 'self::faker()->randomNumber(),',
31+
'INT' => 'self::faker()->randomNumber(),',
32+
'JSON' => '[],',
33+
'JSON_ARRAY' => '[],',
34+
'SIMPLE_ARRAY' => '[],',
35+
'SMALLINT' => 'self::faker()->numberBetween(1, 32767),',
36+
'STRING' => 'self::faker()->text({length}),',
37+
'TEXT' => 'self::faker()->text({length}),',
38+
'TIME_MUTABLE' => 'self::faker()->datetime(),',
39+
'TIME_IMMUTABLE' => '\DateTimeImmutable::createFromMutable(self::faker()->datetime()),',
40+
];
41+
42+
public function __invoke(MakeFactoryData $makeFactoryData, bool $allFields): void
43+
{
44+
/** @var ODMClassMetadata|ORMClassMetadata $metadata */
45+
$metadata = $this->getClassMetadata($makeFactoryData);
46+
47+
$ids = $metadata->getIdentifierFieldNames();
48+
49+
foreach ($metadata->fieldMappings as $property) {
50+
if ($property['embedded'] ?? false) {
51+
// skip ODM embedded
52+
continue;
53+
}
54+
55+
$fieldName = $property['fieldName'];
56+
57+
if (\str_contains($fieldName, '.')) {
58+
// this is a "subfield" of an ORM embeddable field.
59+
continue;
60+
}
61+
62+
// ignore identifiers and nullable fields
63+
if ((!$allFields && ($property['nullable'] ?? false)) || \in_array($fieldName, $ids, true)) {
64+
continue;
65+
}
66+
67+
$type = \mb_strtoupper($property['type']);
68+
$value = "null, // TODO add {$type} type manually";
69+
$length = $property['length'] ?? '';
70+
71+
if (\array_key_exists($type, self::DEFAULTS)) {
72+
$value = self::DEFAULTS[$type];
73+
}
74+
75+
$makeFactoryData->addDefaultProperty($fieldName, \str_replace('{length}', (string) $length, $value));
76+
}
77+
}
78+
79+
public function supports(MakeFactoryData $makeFactoryData): bool
80+
{
81+
return $makeFactoryData->isPersisted();
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Zenstruck\Foundry\Bundle\Maker\Factory;
4+
5+
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as ODMClassMetadata;
6+
7+
/**
8+
* @internal
9+
*/
10+
class ODMDefaultPropertiesGuesser extends AbstractDoctrineDefaultPropertiesGuesser
11+
{
12+
public function __invoke(MakeFactoryData $makeFactoryData, bool $allFields): void
13+
{
14+
$metadata = $this->getClassMetadata($makeFactoryData);
15+
16+
if (!$metadata instanceof ODMClassMetadata) {
17+
throw new \InvalidArgumentException("\"{$makeFactoryData->getObjectFullyQualifiedClassName()}\" is not a valid ODM class.");
18+
}
19+
20+
foreach ($metadata->associationMappings as $item) {
21+
/** @phpstan-ignore-next-line */
22+
if (!$item['embedded'] || !$item['targetDocument']) {
23+
// foundry does not support ODM references
24+
continue;
25+
}
26+
27+
$fieldName = $item['fieldName'];
28+
/** @phpstan-ignore-next-line */
29+
$isMultiple = ODMClassMetadata::MANY === $item['type'];
30+
31+
$isNullable = $makeFactoryData->getObject()->getProperty($fieldName)->getType()?->allowsNull() ?? true;
32+
33+
if (!$allFields && ($isMultiple || $isNullable)) {
34+
continue;
35+
}
36+
37+
$this->addDefaultValueUsingFactory($makeFactoryData, $fieldName, $item['targetDocument'], $isMultiple);
38+
}
39+
}
40+
41+
public function supports(MakeFactoryData $makeFactoryData): bool
42+
{
43+
try {
44+
$metadata = $this->getClassMetadata($makeFactoryData);
45+
46+
return $metadata instanceof ODMClassMetadata;
47+
} catch (\InvalidArgumentException) {
48+
return false;
49+
}
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace Zenstruck\Foundry\Bundle\Maker\Factory;
4+
5+
use Doctrine\ORM\Mapping\ClassMetadataInfo as ORMClassMetadata;
6+
7+
/**
8+
* @internal
9+
*/
10+
class ORMDefaultPropertiesGuesser extends AbstractDoctrineDefaultPropertiesGuesser
11+
{
12+
public function __invoke(MakeFactoryData $makeFactoryData, bool $allFields): void
13+
{
14+
$metadata = $this->getClassMetadata($makeFactoryData);
15+
16+
if (!$metadata instanceof ORMClassMetadata) {
17+
throw new \InvalidArgumentException("\"{$makeFactoryData->getObjectFullyQualifiedClassName()}\" is not a valid ORM class.");
18+
}
19+
20+
$this->guessDefaultValueForORMAssociativeFields($makeFactoryData, $metadata);
21+
$this->guessDefaultValueForEmbedded($makeFactoryData, $metadata, $allFields);
22+
}
23+
24+
public function supports(MakeFactoryData $makeFactoryData): bool
25+
{
26+
try {
27+
$metadata = $this->getClassMetadata($makeFactoryData);
28+
29+
return $metadata instanceof ORMClassMetadata;
30+
} catch (\InvalidArgumentException) {
31+
return false;
32+
}
33+
}
34+
35+
private function guessDefaultValueForORMAssociativeFields(MakeFactoryData $makeFactoryData, ORMClassMetadata $metadata): void
36+
{
37+
foreach ($metadata->associationMappings as $item) {
38+
// if joinColumns is not written entity is default nullable ($nullable = true;)
39+
if (true === ($item['joinColumns'][0]['nullable'] ?? true)) {
40+
continue;
41+
}
42+
43+
if (isset($item['mappedBy']) || isset($item['joinTable'])) {
44+
// we don't want to add defaults for X-To-Many relationships
45+
continue;
46+
}
47+
48+
$this->addDefaultValueUsingFactory($makeFactoryData, $item['fieldName'], $item['targetEntity']);
49+
}
50+
}
51+
52+
private function guessDefaultValueForEmbedded(MakeFactoryData $makeFactoryData, ORMClassMetadata $metadata, bool $allFields): void
53+
{
54+
foreach ($metadata->embeddedClasses as $fieldName => $item) {
55+
$isNullable = $makeFactoryData->getObject()->getProperty($fieldName)->getType()?->allowsNull() ?? true;
56+
57+
if (!$allFields && $isNullable) {
58+
continue;
59+
}
60+
61+
$this->addDefaultValueUsingFactory($makeFactoryData, $fieldName, $item['class']);
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)