diff --git a/app/src/GraphGenerator/ContextPreparator.php b/app/src/GraphGenerator/ContextPreparator.php new file mode 100644 index 00000000..67e88e04 --- /dev/null +++ b/app/src/GraphGenerator/ContextPreparator.php @@ -0,0 +1,25 @@ +getContext(); + + if ('user' !== $context->getName()) { + return; + } + + $event->getContext() + ->setIgnoreAll(true) + ->forEntityCluster(User::class) + ; + } +} diff --git a/composer.json b/composer.json index b099e445..68a37087 100644 --- a/composer.json +++ b/composer.json @@ -296,7 +296,8 @@ ], "generate:artifact": [ "Composer\\Config::disableProcessTimeout", - "bin/console draw:doctrine:generate-graph-schema | dot -Tsvg -o doc/databse.svg" + "bin/console draw:doctrine:generate-graph-schema | dot -Tsvg -o doc/database.svg", + "bin/console draw:doctrine:generate-graph-schema user | dot -Tsvg -o doc/user.svg" ] }, "minimum-stability": "dev", diff --git a/doc/databse.svg b/doc/database.svg similarity index 100% rename from doc/databse.svg rename to doc/database.svg diff --git a/doc/user.svg b/doc/user.svg new file mode 100644 index 00000000..ab83d643 --- /dev/null +++ b/doc/user.svg @@ -0,0 +1,482 @@ + + + + + + +draw + + + +acme__user_address:se->draw_acme__user:se + + + + + +draw_acme__user:se->draw_acme__base_object:se + + + + + +draw_acme__user:se->draw_acme__base_object:se + + + + + +draw_acme__user:se->draw_acme__base_object:se + + + + + +draw_acme__user:se->draw_acme__base_object:se + + + + + +draw_acme__user:se->draw_acme__base_object:se + + + + + +draw_acme__user:se->draw_acme__base_object:se + + + + + +draw_acme__user:se->draw_acme__base_object:se + + + + + +acme__user_tag:se->draw_acme__user:se + + + + + +acme__user_tag:se->draw_acme__tag:se + + + + + +user_tag:se->draw_acme__user:se + + + + + +user_tag:se->draw_acme__tag:se + + + + + +draw_user__user_lock:se->draw_acme__user:se + + + + + +user_migration:se->draw_acme__user:se + + + + + +user_migration:se->draw_entity_migrator__migration:se + + + + + +acme__user_address + + +acme__user_address + +id + +integer + + + +user_id + +guid + + +position + +integer + + +address_street + +string + + +address_postal_code + +string + + +address_city + +string + + +address_country + +string + + + + + +draw_acme__user + + +draw_acme__user + +id + +guid + + + +child_object1_id + +integer + + +child_object2_id + +integer + + +on_delete_restrict_id + +integer + + +on_delete_cascade_id + +integer + + +on_delete_set_null_id + +integer + + +on_delete_cascade_config_overridden_id + +integer + + +on_delete_cascade_attribute_overridden_id + +integer + + +roles + +json + + +level + +string + + +date_of_birth + +datetime_immutable + + +comment + +text + + +preferred_locale + +string + + +email_auth_code + +string + + +email_auth_code_generated_at + +datetime_immutable + + +two_factor_authentication_enabled_providers + +json + + +force_enabling_two_factor_authentication + +boolean + + +totp_secret + +string + + +manual_lock + +boolean + + +need_change_password + +boolean + + +email + +string + + +password + +string + + +last_password_updated_at + +datetime_immutable + + +address_street + +string + + +address_postal_code + +string + + +address_city + +string + + +address_country + +string + + + + + +acme__user_tag + + +acme__user_tag + +id + +integer + + + +user_id + +guid + + +tag_id + +bigint + + + + + +draw_acme__tag + + +draw_acme__tag + +id + +bigint + + + +name + +string + + +active + +boolean + + + + + +draw_acme__base_object + + +draw_acme__base_object + +id + +integer + + + +discriminator_type + +string + + +attribute_1 + +string + + +date_time_immutable + +datetime_immutable + + +attribute_2 + +string + + + + + +user_tag + + +user_tag + +user_id + +guid + + + +tag_id + +bigint + + + + + + +draw_user__user_lock + + +draw_user__user_lock + +id + +guid + + + +user_id + +guid + + +reason + +string + + +created_at + +datetime_immutable + + +lock_on + +datetime_immutable + + +expires_at + +datetime_immutable + + +unlock_until + +datetime_immutable + + + + + +user_migration + + +user_migration + +id + +bigint + + + +entity_id + +guid + + +migration_id + +integer + + +state + +string + + +transition_logs + +json + + +created_at + +datetime_immutable + + + + + +draw_entity_migrator__migration + +draw_entity_migrator__migration + + + diff --git a/packages/doctrine-extra/ORM/Command/GenerateGraphSchemaCommand.php b/packages/doctrine-extra/ORM/Command/GenerateGraphSchemaCommand.php index 9c4ac595..6a89cd41 100644 --- a/packages/doctrine-extra/ORM/Command/GenerateGraphSchemaCommand.php +++ b/packages/doctrine-extra/ORM/Command/GenerateGraphSchemaCommand.php @@ -2,11 +2,11 @@ namespace Draw\DoctrineExtra\ORM\Command; -use Doctrine\DBAL\Schema\Visitor\Graphviz; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Tools\SchemaTool; +use Draw\DoctrineExtra\ORM\GraphSchema\Context; +use Draw\DoctrineExtra\ORM\GraphSchema\GraphGenerator; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -14,8 +14,9 @@ class GenerateGraphSchemaCommand extends Command { public function __construct( - private EntityManagerInterface $entityManager) - { + private EntityManagerInterface $entityManager, + private GraphGenerator $graphGenerator, + ) { parent::__construct(); } @@ -23,6 +24,7 @@ protected function configure(): void { $this ->setName('draw:doctrine:generate-graph-schema') + ->addArgument('context-name', InputArgument::OPTIONAL, 'The context name to use.', 'default') ->setDescription('Get dot from database schema.') ->setHelp(\sprintf('Usage: bin/console %s | dot -Tsvg -o /tmp/databse.svg', $this->getName())) ; @@ -31,45 +33,13 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $io->writeln($this->getDot()); + $io->writeln( + $this->graphGenerator + ->generate( + new Context($this->entityManager, $input->getArgument('context-name')) + ) + ); return Command::SUCCESS; } - - /** - * Get dot from database schema. - */ - protected function getDot(): string - { - /** @var array> $metadata */ - $metadata = $this->entityManager->getMetadataFactory()->getAllMetadata(); - - usort($metadata, static fn (ClassMetadata $a, ClassMetadata $b): int => $a->getTableName() <=> $b->getTableName()); - - $tool = new SchemaTool($this->entityManager); - $schema = $tool->getSchemaFromMetadata($metadata); - - $visitor = new Graphviz(); - - $visitor->acceptSchema($schema); - - foreach ($schema->getTables() as $table) { - $visitor->acceptTable($table); - foreach ($table->getColumns() as $column) { - $visitor->acceptColumn($table, $column); - } - foreach ($table->getIndexes() as $index) { - $visitor->acceptIndex($table, $index); - } - foreach ($table->getForeignKeys() as $foreignKey) { - $visitor->acceptForeignKey($table, $foreignKey); - } - } - - foreach ($schema->getSequences() as $sequence) { - $visitor->acceptSequence($sequence); - } - - return $visitor->getOutput(); - } } diff --git a/packages/doctrine-extra/ORM/GraphSchema/Context.php b/packages/doctrine-extra/ORM/GraphSchema/Context.php new file mode 100644 index 00000000..65671e91 --- /dev/null +++ b/packages/doctrine-extra/ORM/GraphSchema/Context.php @@ -0,0 +1,93 @@ +entityManager; + } + + public function getName(): string + { + return $this->name; + } + + public function setIgnoreAll(bool $ignoreAll): self + { + $this->ignoreAll = $ignoreAll; + + return $this; + } + + public function getIgnoreAll(): bool + { + return $this->ignoreAll; + } + + public function forEntity(string $entity): self + { + if (!\in_array($entity, $this->ignoreEntities, true)) { + $this->forEntities[] = $entity; + } + + return $this; + } + + public function getForEntities(): array + { + return $this->forEntities; + } + + public function forEntityCluster(string $entity, bool $includeReverseRelation = true): self + { + $this->forEntities[] = $entity; + + foreach ($this->entityManager->getClassMetadata($entity)->getAssociationMappings() as $associationMapping) { + $this->forEntity($associationMapping['targetEntity']); + } + + if ($includeReverseRelation) { + foreach ($this->entityManager->getMetadataFactory()->getAllMetadata() as $metadata) { + foreach ($metadata->getAssociationMappings() as $associationMapping) { + if ($associationMapping['targetEntity'] !== $entity) { + continue; + } + + $this->forEntity($associationMapping['sourceEntity']); + } + } + } + + return $this; + } + + public function ignoreEntity(string $entity): self + { + $this->ignoreEntities[] = $entity; + + return $this; + } + + public function getIgnoreEntities(): array + { + return $this->ignoreEntities; + } +} diff --git a/packages/doctrine-extra/ORM/GraphSchema/Event/PrepareContextEvent.php b/packages/doctrine-extra/ORM/GraphSchema/Event/PrepareContextEvent.php new file mode 100644 index 00000000..0a5c7eb3 --- /dev/null +++ b/packages/doctrine-extra/ORM/GraphSchema/Event/PrepareContextEvent.php @@ -0,0 +1,20 @@ +context; + } +} diff --git a/packages/doctrine-extra/ORM/GraphSchema/GraphGenerator.php b/packages/doctrine-extra/ORM/GraphSchema/GraphGenerator.php new file mode 100644 index 00000000..f871e98e --- /dev/null +++ b/packages/doctrine-extra/ORM/GraphSchema/GraphGenerator.php @@ -0,0 +1,105 @@ +eventDispatcher->dispatch(new Event\PrepareContextEvent($context)); + + $entityManager = $context->getEntityManager(); + + /** @var array> $metadata */ + $metadata = $entityManager->getMetadataFactory()->getAllMetadata(); + + usort($metadata, static fn (ClassMetadata $a, ClassMetadata $b): int => $a->getTableName() <=> $b->getTableName()); + + $tool = new SchemaTool($entityManager); + $schema = $tool->getSchemaFromMetadata($metadata); + + $visitor = new Graphviz(); + + $visitor->acceptSchema($schema); + + $ignoreTables = $this->buildIgnoreTables($context); + + foreach ($schema->getTables() as $table) { + if (\in_array($table->getName(), $ignoreTables, true)) { + continue; + } + + $visitor->acceptTable($table); + foreach ($table->getColumns() as $column) { + $visitor->acceptColumn($table, $column); + } + foreach ($table->getIndexes() as $index) { + $visitor->acceptIndex($table, $index); + } + foreach ($table->getForeignKeys() as $foreignKey) { + $visitor->acceptForeignKey($table, $foreignKey); + } + } + + foreach ($schema->getSequences() as $sequence) { + $visitor->acceptSequence($sequence); + } + + return $visitor->getOutput(); + } + + private function buildIgnoreTables(Context $context): array + { + $entityManager = $context->getEntityManager(); + $ignoreTables = []; + + if ($context->getIgnoreAll()) { + foreach ($entityManager->getMetadataFactory()->getAllMetadata() as $metadata) { + $ignoreTables[] = $metadata->getTableName(); + foreach ($metadata->getAssociationMappings() as $associationMapping) { + if (!isset($associationMapping['joinTable'])) { + continue; + } + + $ignoreTables[] = $associationMapping['joinTable']['name']; + } + } + } + + $forEntities = $context->getForEntities(); + foreach ($forEntities as $entity) { + $metadata = $entityManager->getClassMetadata($entity); + $ignoreTables = array_diff( + $ignoreTables, + [$metadata->getTableName()], + ); + + foreach ($metadata->getAssociationMappings() as $associationMapping) { + if (!isset($associationMapping['joinTable'])) { + continue; + } + + if (!\in_array($associationMapping['targetEntity'], $forEntities, true)) { + continue; + } + + $ignoreTables = array_diff( + $ignoreTables, + [$associationMapping['joinTable']['name']], + ); + } + } + + return array_values($ignoreTables); + } +} diff --git a/packages/doctrine-extra/Tests/DependencyInjection/DoctrineExtraIntegrationTest.php b/packages/doctrine-extra/Tests/DependencyInjection/DoctrineExtraIntegrationTest.php index 10b271ff..222c7eea 100644 --- a/packages/doctrine-extra/Tests/DependencyInjection/DoctrineExtraIntegrationTest.php +++ b/packages/doctrine-extra/Tests/DependencyInjection/DoctrineExtraIntegrationTest.php @@ -12,6 +12,7 @@ use Draw\DoctrineExtra\ORM\Command\MysqlDumpCommand; use Draw\DoctrineExtra\ORM\Command\MysqlImportFileCommand; use Draw\DoctrineExtra\ORM\EntityHandler; +use Draw\DoctrineExtra\ORM\GraphSchema\GraphGenerator; use PHPUnit\Framework\Attributes\CoversClass; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -83,6 +84,12 @@ public static function provideTestLoad(): iterable GenerateGraphSchemaCommand::class, ] ), + new ServiceConfiguration( + 'draw.doctrine_extra.orm.graph_schema.graph_generator', + [ + GraphGenerator::class, + ] + ), ], [ 'doctrine' => [ diff --git a/tests/AppKernelTest.php b/tests/AppKernelTest.php index f397366d..856c6fec 100644 --- a/tests/AppKernelTest.php +++ b/tests/AppKernelTest.php @@ -12,10 +12,16 @@ class AppKernelTest extends KernelTestCase { use EventDispatcherTesterTrait; + private bool $resetFile = false; + public function testEventDispatcherConfiguration(): void { - $this->assertEventDispatcherConfiguration( - __DIR__.'/fixtures/AppKernelTest/testEventDispatcherConfiguration/event_dispatcher.xml' - ); + $path = __DIR__.'/fixtures/AppKernelTest/testEventDispatcherConfiguration/event_dispatcher.xml'; + + if ($this->resetFile && file_exists($path)) { + unlink($path); + } + + $this->assertEventDispatcherConfiguration($path); } } diff --git a/tests/DoctrineExtra/ORM/Command/GenerateGraphSchemaCommandTest.php b/tests/DoctrineExtra/ORM/Command/GenerateGraphSchemaCommandTest.php index 20b9befe..7437adee 100644 --- a/tests/DoctrineExtra/ORM/Command/GenerateGraphSchemaCommandTest.php +++ b/tests/DoctrineExtra/ORM/Command/GenerateGraphSchemaCommandTest.php @@ -9,6 +9,7 @@ use Draw\DoctrineExtra\ORM\Command\GenerateGraphSchemaCommand; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; /** * @internal @@ -27,7 +28,11 @@ public function getCommandName(): string public static function provideTestArgument(): iterable { - return []; + yield [ + 'context-name', + InputArgument::OPTIONAL, + 'default', + ]; } public static function provideTestOption(): iterable @@ -42,7 +47,7 @@ public static function provideTestOption(): iterable */ public function testExecute(): void { - $this->execute([]) + $this->execute(['context-name' => 'user']) ->test( CommandDataTester::create() ->setExpectedDisplay(null) diff --git a/tests/fixtures/AppKernelTest/testEventDispatcherConfiguration/event_dispatcher.xml b/tests/fixtures/AppKernelTest/testEventDispatcherConfiguration/event_dispatcher.xml index 3f94383e..bb8dc50e 100644 --- a/tests/fixtures/AppKernelTest/testEventDispatcherConfiguration/event_dispatcher.xml +++ b/tests/fixtures/AppKernelTest/testEventDispatcherConfiguration/event_dispatcher.xml @@ -89,6 +89,9 @@ + + +