Skip to content

Commit

Permalink
[DoctrineExtra] Customize generate graph schema
Browse files Browse the repository at this point in the history
  • Loading branch information
mpoiriert committed Oct 19, 2024
1 parent a8d0284 commit c40b1cc
Show file tree
Hide file tree
Showing 12 changed files with 766 additions and 49 deletions.
25 changes: 25 additions & 0 deletions app/src/GraphGenerator/ContextPreparator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\GraphGenerator;

use App\Entity\User;
use Draw\DoctrineExtra\ORM\GraphSchema\Event\PrepareContextEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

class ContextPreparator
{
#[AsEventListener]
public function prepareImport(PrepareContextEvent $event): void
{
$context = $event->getContext();

if ('user' !== $context->getName()) {
return;
}

$event->getContext()
->setIgnoreAll(true)
->forEntityCluster(User::class)
;
}
}
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
File renamed without changes
482 changes: 482 additions & 0 deletions doc/user.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 13 additions & 43 deletions packages/doctrine-extra/ORM/Command/GenerateGraphSchemaCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,29 @@

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;

class GenerateGraphSchemaCommand extends Command
{
public function __construct(
private EntityManagerInterface $entityManager)
{
private EntityManagerInterface $entityManager,
private GraphGenerator $graphGenerator,
) {
parent::__construct();
}

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()))
;
Expand All @@ -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<int, ClassMetadata<object>> $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();
}
}
93 changes: 93 additions & 0 deletions packages/doctrine-extra/ORM/GraphSchema/Context.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace Draw\DoctrineExtra\ORM\GraphSchema;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Exclude;

#[Exclude]
class Context
{
private array $ignoreEntities = [];

private array $forEntities = [];

private bool $ignoreAll = false;

public function __construct(
private EntityManagerInterface $entityManager,
private string $name = 'default',
) {
}

public function getEntityManager(): EntityManagerInterface
{
return $this->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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Draw\DoctrineExtra\ORM\GraphSchema\Event;

use Draw\DoctrineExtra\ORM\GraphSchema\Context;
use Symfony\Component\DependencyInjection\Attribute\Exclude;

#[Exclude]
class PrepareContextEvent
{
public function __construct(
private Context $context,
) {
}

public function getContext(): Context
{
return $this->context;
}
}
105 changes: 105 additions & 0 deletions packages/doctrine-extra/ORM/GraphSchema/GraphGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace Draw\DoctrineExtra\ORM\GraphSchema;

use Doctrine\DBAL\Schema\Visitor\Graphviz;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Tools\SchemaTool;
use Psr\EventDispatcher\EventDispatcherInterface;

class GraphGenerator
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
) {
}

public function generate(Context $context): string
{
$this->eventDispatcher->dispatch(new Event\PrepareContextEvent($context));

$entityManager = $context->getEntityManager();

/** @var array<int, ClassMetadata<object>> $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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -83,6 +84,12 @@ public static function provideTestLoad(): iterable
GenerateGraphSchemaCommand::class,
]
),
new ServiceConfiguration(
'draw.doctrine_extra.orm.graph_schema.graph_generator',
[
GraphGenerator::class,
]
),
],
[
'doctrine' => [
Expand Down
12 changes: 9 additions & 3 deletions tests/AppKernelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading

0 comments on commit c40b1cc

Please sign in to comment.