Skip to content

Commit

Permalink
feat(make:factory): use factories to default non-nullable relationships
Browse files Browse the repository at this point in the history
Co-authored-by: Benjamin Knecht <benblub@users.noreply.github.com>
  • Loading branch information
nikophil and benblub committed Nov 22, 2022
1 parent 8332956 commit 059122c
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 6 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ sca: docker-start bin/tools/phpstan/vendor ### Run static analysis
bin/tools/phpstan/vendor: vendor bin/tools/phpstan/composer.json $(wildcard bin/tools/phpstan/composer.lock)
@${DOCKER_PHP} composer bin phpstan install

database-generate-migration: docker-start vendor ### Generate new migration based on mapping in Zenstruck\Foundry\Tests\Fixtures\Entity
database-generate-migration: docker-start vendor database-drop-schema ### Generate new migration based on mapping in Zenstruck\Foundry\Tests\Fixtures\Entity
@${DOCKER_PHP} vendor/bin/doctrine-migrations migrations:migrate --no-interaction --allow-no-migration # first, let's load into db existing migrations
@${DOCKER_PHP} vendor/bin/doctrine-migrations migrations:diff --no-interaction
@${DOCKER_PHP} vendor/bin/doctrine-migrations migrations:migrate --no-interaction # load the new migration
Expand Down
48 changes: 44 additions & 4 deletions src/Bundle/Maker/MakeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,21 @@ final class MakeFactory extends AbstractMaker
\DateTimeImmutable::class => '\DateTimeImmutable::createFromMutable(self::faker()->dateTime()),',
];

/** @var string[] */
/** @var array<string, string> */
private array $entitiesWithFactories;

public function __construct(private ManagerRegistry $managerRegistry, \Traversable $factories, private string $projectDir, private KernelInterface $kernel)
{
$this->entitiesWithFactories = \array_map(
static fn(ModelFactory $factory): string => $factory::getEntityClass(),
\iterator_to_array($factories)
$this->entitiesWithFactories = \array_unique(
\array_reduce(
\iterator_to_array($factories),
static function(array $carry, ModelFactory $factory): array {
$carry[\get_class($factory)] = $factory::getEntityClass();

return $carry;
},
[]
)
);
}

Expand Down Expand Up @@ -268,6 +275,32 @@ private function defaultPropertiesForPersistedObject(string $class, bool $allFie

$dbType = $em instanceof EntityManagerInterface ? 'ORM' : 'ODM';

// If Factory exist for related entities populate too with auto defaults
foreach ($metadata->associationMappings as $item) {
// if joinColumns is not written entity is default nullable ($nullable = true;)
if (!\array_key_exists('joinColumns', $item)) {
continue;
}

if (!\array_key_exists('nullable', $item['joinColumns'][0] ?? [])) {
continue;
}

if (true === $item['joinColumns'][0]['nullable']) {
continue;
}

$fieldName = $item['fieldName'];

if (!$factoryClass = $this->getFactoryForClass($item['targetEntity'])) {
yield \lcfirst($fieldName) => "null, // TODO add {$item['targetEntity']} {$dbType} type manually";

continue;
}

yield \lcfirst($fieldName) => "\\{$factoryClass}::new(),";
}

foreach ($metadata->fieldMappings as $property) {
// ignore identifiers and nullable fields
if ((!$allFields && ($property['nullable'] ?? false)) || \in_array($property['fieldName'], $ids, true)) {
Expand Down Expand Up @@ -342,4 +375,11 @@ private function doctrineEnabled(): bool

return $ormEnabled || $odmEnabled;
}

private function getFactoryForClass(string $class): ?string
{
$factories = \array_flip($this->entitiesWithFactories);

return $factories[$class] ?? null;
}
}
60 changes: 60 additions & 0 deletions tests/Fixtures/Entity/EntityWithRelations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Zenstruck\Foundry\Tests\Fixtures\Entity;

use Doctrine\ORM\Mapping as ORM;
use Zenstruck\Foundry\Tests\Fixtures\Entity\Cascade\Brand;

/**
* @ORM\Entity
* @ORM\Table(name="entity_with_relations")
*/
class EntityWithRelations
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;

/**
* @ORM\OneToOne(targetEntity=Category::class, cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=false)
*/
private $oneToOne;

/**
* @ORM\OneToOne(targetEntity=Category::class, cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=true)
*/
private $oneToOneNullable;

/**
* @ORM\ManyToOne(targetEntity=Category::class)
* @ORM\JoinColumn(nullable=false)
*/
private $manyToOne;

/**
* @ORM\ManyToOne(targetEntity=Category::class)
* @ORM\JoinColumn(nullable=true)
*/
private $manyToOneNullable;

/**
* @ORM\ManyToOne(targetEntity=Category::class)
*/
private $manyToOneNullableDefault;

/**
* @ORM\ManyToMany(targetEntity=Category::class)
*/
private $manyToMany;

/**
* @ORM\ManyToOne(targetEntity=Brand::class)
* @ORM\JoinColumn(nullable=false)
*/
private $manyToOneWithNotExistingFactory;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace App\Factory;

use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
use Zenstruck\Foundry\Tests\Fixtures\Entity\EntityWithRelations;

/**
* @extends ModelFactory<EntityWithRelations>
*
* @method EntityWithRelations|Proxy create(array|callable $attributes = [])
* @method static EntityWithRelations|Proxy createOne(array $attributes = [])
* @method static EntityWithRelations|Proxy find(object|array|mixed $criteria)
* @method static EntityWithRelations|Proxy findOrCreate(array $attributes)
* @method static EntityWithRelations|Proxy first(string $sortedField = 'id')
* @method static EntityWithRelations|Proxy last(string $sortedField = 'id')
* @method static EntityWithRelations|Proxy random(array $attributes = [])
* @method static EntityWithRelations|Proxy randomOrCreate(array $attributes = [])
* @method static EntityWithRelations[]|Proxy[] all()
* @method static EntityWithRelations[]|Proxy[] createMany(int $number, array|callable $attributes = [])
* @method static EntityWithRelations[]|Proxy[] createSequence(array|callable $sequence)
* @method static EntityWithRelations[]|Proxy[] findBy(array $attributes)
* @method static EntityWithRelations[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])
* @method static EntityWithRelations[]|Proxy[] randomSet(int $number, array $attributes = [])
*/
final class EntityWithRelationsFactory extends ModelFactory
{
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
*
* @todo inject services if required
*/
public function __construct()
{
parent::__construct();
}

/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
*
* @todo add your default values here
*/
protected function getDefaults(): array
{
return [
'manyToOne' => \Zenstruck\Foundry\Tests\Fixtures\Factories\CategoryFactory::new(),
'manyToOneWithNotExistingFactory' => null, // TODO add Zenstruck\Foundry\Tests\Fixtures\Entity\Cascade\Brand ORM type manually
'oneToOne' => \Zenstruck\Foundry\Tests\Fixtures\Factories\CategoryFactory::new(),
];
}

/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
*/
protected function initialize(): self
{
return $this
// ->afterInstantiate(function(EntityWithRelations $entityWithRelations): void {})
;
}

protected static function getClass(): string
{
return EntityWithRelations::class;
}
}
54 changes: 54 additions & 0 deletions tests/Fixtures/Migrations/Version20221117081744.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Zenstruck\Foundry\Tests\Fixtures\Migrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20221117081744 extends AbstractMigration
{
public function getDescription(): string
{
return 'Seventh migration.';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE entity_with_relations (id INT AUTO_INCREMENT NOT NULL, oneToOne_id INT NOT NULL, oneToOneNullable_id INT DEFAULT NULL, manyToOne_id INT NOT NULL, manyToOneNullable_id INT DEFAULT NULL, manyToOneNullableDefault_id INT DEFAULT NULL, manyToOneWithNotExistingFactory_id INT NOT NULL, UNIQUE INDEX UNIQ_A9C9EC969017888C (oneToOne_id), UNIQUE INDEX UNIQ_A9C9EC96DA2BFB84 (oneToOneNullable_id), INDEX IDX_A9C9EC962E3A088A (manyToOne_id), INDEX IDX_A9C9EC968097B86C (manyToOneNullable_id), INDEX IDX_A9C9EC968572C13C (manyToOneNullableDefault_id), INDEX IDX_A9C9EC96FF92FDCA (manyToOneWithNotExistingFactory_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB');
$this->addSql('CREATE TABLE entitywithrelations_category (entitywithrelations_id INT NOT NULL, category_id INT NOT NULL, INDEX IDX_CD6EBFAB337AA4F7 (entitywithrelations_id), INDEX IDX_CD6EBFAB12469DE2 (category_id), PRIMARY KEY(entitywithrelations_id, category_id)) DEFAULT CHARACTER SET utf8 COLLATE `utf8_unicode_ci` ENGINE = InnoDB');
$this->addSql('ALTER TABLE entity_with_relations ADD CONSTRAINT FK_A9C9EC969017888C FOREIGN KEY (oneToOne_id) REFERENCES categories (id)');
$this->addSql('ALTER TABLE entity_with_relations ADD CONSTRAINT FK_A9C9EC96DA2BFB84 FOREIGN KEY (oneToOneNullable_id) REFERENCES categories (id)');
$this->addSql('ALTER TABLE entity_with_relations ADD CONSTRAINT FK_A9C9EC962E3A088A FOREIGN KEY (manyToOne_id) REFERENCES categories (id)');
$this->addSql('ALTER TABLE entity_with_relations ADD CONSTRAINT FK_A9C9EC968097B86C FOREIGN KEY (manyToOneNullable_id) REFERENCES categories (id)');
$this->addSql('ALTER TABLE entity_with_relations ADD CONSTRAINT FK_A9C9EC968572C13C FOREIGN KEY (manyToOneNullableDefault_id) REFERENCES categories (id)');
$this->addSql('ALTER TABLE entity_with_relations ADD CONSTRAINT FK_A9C9EC96FF92FDCA FOREIGN KEY (manyToOneWithNotExistingFactory_id) REFERENCES brand_cascade (id)');
$this->addSql('ALTER TABLE entitywithrelations_category ADD CONSTRAINT FK_CD6EBFAB337AA4F7 FOREIGN KEY (entitywithrelations_id) REFERENCES entity_with_relations (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE entitywithrelations_category ADD CONSTRAINT FK_CD6EBFAB12469DE2 FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE entity_with_relations DROP FOREIGN KEY FK_A9C9EC969017888C');
$this->addSql('ALTER TABLE entity_with_relations DROP FOREIGN KEY FK_A9C9EC96DA2BFB84');
$this->addSql('ALTER TABLE entity_with_relations DROP FOREIGN KEY FK_A9C9EC962E3A088A');
$this->addSql('ALTER TABLE entity_with_relations DROP FOREIGN KEY FK_A9C9EC968097B86C');
$this->addSql('ALTER TABLE entity_with_relations DROP FOREIGN KEY FK_A9C9EC968572C13C');
$this->addSql('ALTER TABLE entity_with_relations DROP FOREIGN KEY FK_A9C9EC96FF92FDCA');
$this->addSql('ALTER TABLE entitywithrelations_category DROP FOREIGN KEY FK_CD6EBFAB337AA4F7');
$this->addSql('ALTER TABLE entitywithrelations_category DROP FOREIGN KEY FK_CD6EBFAB12469DE2');
$this->addSql('DROP TABLE entity_with_relations');
$this->addSql('DROP TABLE entitywithrelations_category');
}

public function isTransactional(): bool
{
return false;
}
}
19 changes: 19 additions & 0 deletions tests/Functional/Bundle/Maker/MakeFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Zenstruck\Foundry\Tests\Fixtures\Document\Comment;
use Zenstruck\Foundry\Tests\Fixtures\Document\Post;
use Zenstruck\Foundry\Tests\Fixtures\Entity\Category;
use Zenstruck\Foundry\Tests\Fixtures\Entity\EntityWithRelations;
use Zenstruck\Foundry\Tests\Fixtures\Entity\Tag;
use Zenstruck\Foundry\Tests\Fixtures\Kernel;
use Zenstruck\Foundry\Tests\Fixtures\Object\SomeObject;
Expand Down Expand Up @@ -345,4 +346,22 @@ public function can_create_factory_with_auto_activated_not_persisted_option(): v

$this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/CategoryFactory.php'));
}

/**
* @test
*/
public function can_create_factory_with_relation_defaults(): void
{
if (!\getenv('USE_ORM')) {
self::markTestSkipped('doctrine/orm not enabled.');
}

$tester = new CommandTester((new Application(self::bootKernel()))->find('make:factory'));

$this->assertFileDoesNotExist(self::tempFile('src/Factory/EntityWithRelationsFactory.php'));

$tester->execute(['class' => EntityWithRelations::class]);

$this->assertFileFromMakerSameAsExpectedFile(self::tempFile('src/Factory/EntityWithRelationsFactory.php'));
}
}
5 changes: 4 additions & 1 deletion tests/Functional/WithMigrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ public function it_generates_a_valid_schema(): void
{
$kernel = static::bootKernel();
$validator = new SchemaValidator($kernel->getContainer()->get('doctrine')->getManager());
self::assertEmpty($validator->validateMapping());
self::assertEmpty(
$validator->validateMapping(),
\implode("\n", \array_map(static fn($s): string => \implode("\n", $s), $validator->validateMapping()))
);
self::assertTrue($validator->schemaInSyncWithMetadata());
}

Expand Down

0 comments on commit 059122c

Please sign in to comment.