diff --git a/.gitignore b/.gitignore index 6f6dd37ff..39f63f4a3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /tests/Fixtures/tmp /var/ /docs/output/ +/credentials.php diff --git a/credentials.dist.php b/credentials.dist.php new file mode 100644 index 000000000..5225a2645 --- /dev/null +++ b/credentials.dist.php @@ -0,0 +1,14 @@ + '', + 'user' => '', + 'password' => '', + 'host' => '', + ]; +} diff --git a/migrations-db.php b/migrations-db.php new file mode 100644 index 000000000..546e910f7 --- /dev/null +++ b/migrations-db.php @@ -0,0 +1,12 @@ + $credentials['dbname'], + 'user' => $credentials['user'], + 'password' => $credentials['password'], + 'host' => $credentials['host'], + 'driver' => 'pdo_mysql', +]; diff --git a/migrations.php b/migrations.php new file mode 100644 index 000000000..0ea8078aa --- /dev/null +++ b/migrations.php @@ -0,0 +1,21 @@ + [ + 'table_name' => 'doctrine_migration_versions', + 'version_column_name' => 'version', + 'version_column_length' => 1024, + 'executed_at_column_name' => 'executed_at', + 'execution_time_column_name' => 'execution_time', + ], + + 'migrations_paths' => [ + 'Zenstruck\Foundry\Tests\Fixtures\Migrations' => 'tests/Fixtures/Migrations', + ], + + 'all_or_nothing' => false, + 'check_database_platform' => true, + 'organize_migrations' => 'none', + 'connection' => null, + 'em' => null, +]; diff --git a/src/Bundle/Maker/MakeFactory.php b/src/Bundle/Maker/MakeFactory.php index 04cd01408..de1fcd4c5 100644 --- a/src/Bundle/Maker/MakeFactory.php +++ b/src/Bundle/Maker/MakeFactory.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Finder\Finder; use Zenstruck\Foundry\ModelFactory; /** @@ -202,6 +203,40 @@ private function defaultPropertiesFor(string $class, bool $allFields): iterable $metadata = $em->getClassMetadata($class); $ids = $metadata->getIdentifierFieldNames(); + // TODO cleanup the code + // TODO class exist dont relay on fix namespaces + // TODO write some tests + // TODO test with kind of possible relations + // If Factory exist for related entities populate too with auto defaults + $relatedEntities = $metadata->associationMappings; + foreach ($relatedEntities as $item) { + // if joinColumns is not written entity is default nullable ($nullable = true;) + // @see vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/JoinColumn.php LINE 28 + if (!\array_key_exists('joinColumns', $item)) { + continue; + } + + // if this key is available its a ManyToMany relation but its optional, Key must not be present to be ManyToMany + if (\array_key_exists('joinTable', $item)) { + continue; + } + + if (true === $item['joinColumns'][0]['nullable']) { + continue; + } + + $joinedColumns = $item['joinColumns']; + $fieldName = $item['fieldName']; + + $targetEntityArray = \explode('\\', $item['targetEntity']); + $targetEntity = \end($targetEntityArray); + $factory = \ucfirst($targetEntity).'Factory'; + + if ($this->isFactory($factory)) { + yield \lcfirst($fieldName) => \ucfirst($targetEntity).'Factory::new(),'; + } + } + foreach ($metadata->fieldMappings as $property) { // ignore identifiers and nullable fields if ((!$allFields && ($property['nullable'] ?? false)) || \in_array($property['fieldName'], $ids, true)) { @@ -218,4 +253,31 @@ private function defaultPropertiesFor(string $class, bool $allFields): iterable yield $property['fieldName'] => $value; } } + + private function isFactory($factory) + { + // Quickfix on Github CI - TODO + if (\class_exists('Zenstruck\Foundry\Tests\Fixtures\Factories\\'.$factory)) { + return true; + } + + $dirs = []; + + if (\is_dir('src')) { + $dirs[] = 'src/'; + } + + if (\is_dir('Tests')) { + $dirs[] = 'Tests/'; + } + + $finder = new Finder(); + $finder->in($dirs)->files()->name($factory.'.php'); + + if (\count(\iterator_to_array($finder)) > 0) { + return true; + } + + return false; + } } diff --git a/tests/Fixtures/Entity/Bar.php b/tests/Fixtures/Entity/Bar.php new file mode 100644 index 000000000..565045394 --- /dev/null +++ b/tests/Fixtures/Entity/Bar.php @@ -0,0 +1,36 @@ +foo; + } + + public function setFoo(?Foo $foo): self + { + $this->foo = $foo; + + return $this; + } +} diff --git a/tests/Fixtures/Entity/Foo.php b/tests/Fixtures/Entity/Foo.php new file mode 100644 index 000000000..e1f1feb4b --- /dev/null +++ b/tests/Fixtures/Entity/Foo.php @@ -0,0 +1,132 @@ +oneToMany = new ArrayCollection(); + $this->manyToMany = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getOneToOne(): ?Bar + { + return $this->oneToOne; + } + + public function setOneToOne(Bar $oneToOne): self + { + $this->oneToOne = $oneToOne; + + return $this; + } + + /** + * @return Collection|Bar[] + */ + public function getOneToMany(): Collection + { + return $this->oneToMany; + } + + public function addOneToMany(Bar $oneToMany): self + { + if (!$this->oneToMany->contains($oneToMany)) { + $this->oneToMany[] = $oneToMany; + $oneToMany->setFoo($this); + } + + return $this; + } + + public function removeOneToMany(Bar $oneToMany): self + { + if ($this->oneToMany->removeElement($oneToMany)) { + // set the owning side to null (unless already changed) + if ($oneToMany->getFoo() === $this) { + $oneToMany->setFoo(null); + } + } + + return $this; + } + + public function getManyToOne(): ?Bar + { + return $this->manyToOne; + } + + public function setManyToOne(?Bar $manyToOne): self + { + $this->manyToOne = $manyToOne; + + return $this; + } + + /** + * @return Collection|Bar[] + */ + public function getManyToMany(): Collection + { + return $this->manyToMany; + } + + public function addManyToMany(Bar $manyToMany): self + { + if (!$this->manyToMany->contains($manyToMany)) { + $this->manyToMany[] = $manyToMany; + } + + return $this; + } + + public function removeManyToMany(Bar $manyToMany): self + { + $this->manyToMany->removeElement($manyToMany); + + return $this; + } +} diff --git a/tests/Fixtures/Factories/BarFactory.php b/tests/Fixtures/Factories/BarFactory.php new file mode 100644 index 000000000..c99eac15d --- /dev/null +++ b/tests/Fixtures/Factories/BarFactory.php @@ -0,0 +1,22 @@ + + */ +final class BarFactory extends ModelFactory +{ + protected static function getClass(): string + { + return Bar::class; + } + + protected function getDefaults(): array + { + return ['value' => 'Some Bar']; + } +} diff --git a/tests/Functional/Bundle/Maker/MakeFactoryTest.php b/tests/Functional/Bundle/Maker/MakeFactoryTest.php index c8a71014b..27618481f 100644 --- a/tests/Functional/Bundle/Maker/MakeFactoryTest.php +++ b/tests/Functional/Bundle/Maker/MakeFactoryTest.php @@ -6,6 +6,7 @@ use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; use Symfony\Component\Console\Tester\CommandTester; use Zenstruck\Foundry\Tests\Fixtures\Entity\Category; +use Zenstruck\Foundry\Tests\Fixtures\Entity\Foo; use Zenstruck\Foundry\Tests\Fixtures\Entity\Tag; /** @@ -13,6 +14,81 @@ */ final class MakeFactoryTest extends MakerTestCase { + /** + * @test + */ + public function can_create_factory_with_relation_defaults(): void + { + $tester = new CommandTester((new Application(self::bootKernel()))->find('make:factory')); + + $this->assertFileDoesNotExist(self::tempFile('src/Factory/FooFactory.php')); + + $tester->execute(['entity' => Foo::class]); + + $this->assertFileExists(self::tempFile('src/Factory/FooFactory.php')); + $this->assertSame(<< + * + * @method static Foo|Proxy createOne(array \$attributes = []) + * @method static Foo[]|Proxy[] createMany(int \$number, array|callable \$attributes = []) + * @method static Foo|Proxy find(object|array|mixed \$criteria) + * @method static Foo|Proxy findOrCreate(array \$attributes) + * @method static Foo|Proxy first(string \$sortedField = 'id') + * @method static Foo|Proxy last(string \$sortedField = 'id') + * @method static Foo|Proxy random(array \$attributes = []) + * @method static Foo|Proxy randomOrCreate(array \$attributes = []) + * @method static Foo[]|Proxy[] all() + * @method static Foo[]|Proxy[] findBy(array \$attributes) + * @method static Foo[]|Proxy[] randomSet(int \$number, array \$attributes = []) + * @method static Foo[]|Proxy[] randomRange(int \$min, int \$max, array \$attributes = []) + * @method Foo|Proxy create(array|callable \$attributes = []) + */ +final class FooFactory extends ModelFactory +{ + public function __construct() + { + parent::__construct(); + + // TODO inject services if required (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services) + } + + protected function getDefaults(): array + { + return [ + // TODO add your default values here (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories) + 'oneToOne' => BarFactory::new(), + 'manyToOne' => BarFactory::new(), + ]; + } + + protected function initialize(): self + { + // see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization + return \$this + // ->afterInstantiate(function(Foo \$foo): void {}) + ; + } + + protected static function getClass(): string + { + return Foo::class; + } +} + +EOF + , \file_get_contents(self::tempFile('src/Factory/FooFactory.php')) + ); + } + /** * @test */