diff --git a/Command/EntitySchemaCommand.php b/Command/EntitySchemaCommand.php new file mode 100644 index 0000000000000000000000000000000000000000..d7335149b6d43511b4e0d8695b652b6f49959f2c --- /dev/null +++ b/Command/EntitySchemaCommand.php @@ -0,0 +1,60 @@ +<?php + +/* + * Copyright (C) 2015 IRSTEA + * All rights reserved. + */ +namespace Irstea\PlantUmlBundle\Command; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\ClassMetadata; +use Irstea\PlantUmlBundle\Model\Orm\EntityVisitor; +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Description of ImportAffiliationCommand + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class EntitySchemaCommand extends ContainerAwareCommand +{ + protected function configure() + { + $this + ->setName('irstea:plantuml:entities') + ->setDescription("Génère un schéma PlantUML à partir des métadonnées de Doctrine."); + } + + /** + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @SuppressWarnings(UnusedFormalParameter) + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + /* @var $manager EntityManagerInterface */ + $manager = $this->getContainer()->get('doctrine.orm.entity_manager'); + $factory = $manager->getMetadataFactory(); + + $allMetadata = $factory->getAllMetadata(); + + $visitor = new EntityVisitor($allMetadata); + foreach($allMetadata as $metadata) { + /* @var $metadata ClassMetadata */ + $visitor->visitClass($metadata->getName()); + } + + $stream = fopen("php://output", "wt"); + fputs($stream, "@startuml\n"); + fputs($stream, 'set namespaceSeparator \\\\'."\n"); + $visitor->outputTo($stream); + fputs($stream, "@enduml\n"); + fclose($stream); + } +} diff --git a/Commmand/OrmSchemaCommand.php b/Commmand/OrmSchemaCommand.php deleted file mode 100644 index 0c4e057aabc3735e59ed4f238bec016f53cafb04..0000000000000000000000000000000000000000 --- a/Commmand/OrmSchemaCommand.php +++ /dev/null @@ -1,201 +0,0 @@ -<?php - -/* - * Copyright (C) 2015 IRSTEA - * All rights reserved. - */ -namespace Irstea\PlantUmlBundle\Command; - -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadata; -use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Description of ImportAffiliationCommand - * - * @author Guillaume Perréal <guillaume.perreal@irstea.fr> - */ -class UmlSchemaCommand extends ContainerAwareCommand -{ - /** - * @var array[] - */ - private $namespaces = []; - - /** - * @var string[] - */ - private $nodes = []; - - /** - * @var string[] - */ - private $arrows = []; - - protected function configure() - { - $this - ->setName('irstea:plantuml:doctrine') - ->setDescription("Génère un schéma PlantUML à partir des métadonnées Doctrine."); - } - - /** - * - * @param InputInterface $input - * @param OutputInterface $output - * - * @SuppressWarnings(UnusedFormalParameter) - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - /* @var $manager EntityManagerInterface */ - $manager = $this->getContainer()->get('doctrine.orm.entity_manager'); - $factory = $manager->getMetadataFactory(); - $allMetadata = $factory->getAllMetadata(); - - array_walk($allMetadata, [$this, 'processClass']); - - echo "@startuml\n"; - $this->writeNamespaces($this->namespaces); - echo "\n\n"; - echo implode("\n", $this->nodes); - echo "\n\n"; - echo implode("\n", $this->arrows); - echo "\n\n@enduml\n"; - } - - protected function processClass(ClassMetadata $metadata) - { - $node = []; - - $name = $this->formatName($metadata->getName()); - $class = $metadata->getReflectionClass(); - - $node[] = sprintf( - '%s%s %s%s {', - $class->isAbstract() ? 'abstract ' : '', - $class->isInterface() ? 'interface' : 'class', - $name, - $metadata->isMappedSuperclass ? ' <<mappedSuperClass>>' : '' - ); - - foreach($metadata->fieldMappings as $field) { - $node[] = sprintf("\t%s: %s", $field['fieldName'], $field['type']); - } - - $node[] = '}'; - - $this->addNode($name, implode("\n", $node), true); - - foreach($metadata->getAssociationMappings() as $association) { - if (!$association['isOwningSide']) { - continue; - } - - $sourceCard = ($association['type'] & (ClassMetadata::MANY_TO_MANY | ClassMetadata::MANY_TO_ONE)) ? '*' : '1'; - $sourceArrow = $association['inversedBy'] ? '<' : ''; - $targetCard = ($association['type'] & ClassMetadata::TO_MANY) ? '*' : '1'; - $targetArrow = '>'; - - $this->addArrow( - $association['sourceEntity'], - $association['targetEntity'], - sprintf('"%s" %s--%s "%s"', $sourceCard, $sourceArrow, $targetArrow, $targetCard) - ); - } - - $parentClass = $class->getParentClass(); - if ($parentClass) { - $this->addArrow($parentClass->getName(), $metadata->getName(), '<|--'); - } - foreach($class->getInterfaceNames() as $interfaceName) { - $this->addNode($interfaceName, sprintf("interface %s {\n}", $this->formatName($interfaceName))); - $this->addArrow($interfaceName, $metadata->getName(), '<|..'); - } - foreach($class->getTraitNames() as $traitName) { - $this->addNode($traitName, sprintf("class %s <<trait>> {\n}", $this->formatName($traitName))); - $this->addArrow($traitName, $metadata->getName(), '<|--'); - } - } - - /** - * Enregistre un namespace. - * - * @param string[] $components - */ - protected function addNamespace(array $components) - { - $current = &$this->namespaces; - foreach($components as $next) { - if (!isset($current[$next])) { - $current[$next] = []; - } - $current =& $current[$next]; - } - } - - /** - * - * @param string $name - * @return string - */ - protected function formatName($name) - { - return strtr($name, '\\', '.'); - } - - /** - * Ajoute un noeud. - * - * @param string $name Nom du noeud. - * @param string $body Déclaration du noeud. - * @param bool $overwrite Doit-on remplacer la déclaration s'il existe déjà ? - * - * @return string Le nom de noeud "PlantUml-compatible". - */ - protected function addNode($name, $body = '', $overwrite = false) - { - $fullName = $this->formatName($name); - $components = explode('\\', $name); - array_pop($components); - $this->addNamespace($components); - - if (!isset($this->nodes[$fullName]) || $overwrite) { - $this->nodes[$fullName] = $body; - } - return $fullName; - } - - /** - * Ajoute une flèche sur le schéma. - * - * @param string $source Nom de l'origine. - * @param string $target Nom de la destination. - * @param string $type Description du lien. - */ - protected function addArrow($source, $target, $type = '-->') - { - $sourceName = $this->addNode($source); - $targetName = $this->addNode($target); - - $this->arrows[] = sprintf('%s %s %s', $sourceName, $type, $targetName); - } - - /** - * Génère les déclarations de namespaces. - * - * @param array $current - */ - protected function writeNamespaces(array $current) - { - foreach($current as $name => $children) { - echo sprintf("namespace %s {\n", $name); - $this->writeNamespaces($children); - echo "}\n"; - } - } -} diff --git a/Model/Arrow/BaseArrow.php b/Model/Arrow/BaseArrow.php new file mode 100644 index 0000000000000000000000000000000000000000..432ffe631f6694b254adcde6ebb965b7935970a9 --- /dev/null +++ b/Model/Arrow/BaseArrow.php @@ -0,0 +1,64 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model\Arrow; + +use Irstea\PlantUmlBundle\Model\UmlComponentInterface; +use Irstea\PlantUmlBundle\Model\UmlNodeInterface; + +/** + * Description of Arrow + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class BaseArrow implements UmlComponentInterface +{ + /** + * @var UmlNodeInterface + */ + private $source; + + /** + * @var UmlNodeInterface + */ + private $target; + + /** + * @var string + */ + private $link; + + public function __construct(UmlNodeInterface $source, UmlNodeInterface $target, $link = "--", $label = null) + { + $this->source = $source; + $this->target = $target; + $this->link = $link; + $this->label = $label; + } + + public function outputTo($stream) + { + $this->source->outputAliasTo($stream); + $this->outputLinkTo($stream); + $this->target->outputAliasTo($stream); + $this->outputLabelTo($stream); + fputs($stream, "\n"); + } + + protected function outputLabelTo($stream) + { + if ($this->label) { + fputs($stream, " : {$this->label}"); + } + } + + protected function outputLinkTo($stream) + { + fputs($stream, " ".$this->link." "); + } +} diff --git a/Model/Arrow/ExtendsClass.php b/Model/Arrow/ExtendsClass.php new file mode 100644 index 0000000000000000000000000000000000000000..384541da2bef1d5d2985975e3a27d078acbf0cf1 --- /dev/null +++ b/Model/Arrow/ExtendsClass.php @@ -0,0 +1,22 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model\Arrow; + +/** + * Description of ExtendsClass + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class ExtendsClass extends BaseArrow +{ + public function __construct(\Irstea\PlantUmlBundle\Model\UmlNodeInterface $source, \Irstea\PlantUmlBundle\Model\UmlNodeInterface $target, $link = "--", $label = null) + { + parent::__construct($source, $target, $link, $label); + } +} diff --git a/Model/Arrow/ImplementsInterface.php b/Model/Arrow/ImplementsInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..dbdede845ccdaca7a8f3c025d3c63dea2fc1dbab --- /dev/null +++ b/Model/Arrow/ImplementsInterface.php @@ -0,0 +1,25 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model\Arrow; + +use Irstea\PlantUmlBundle\Model\Node\Interface_; +use Irstea\PlantUmlBundle\Model\UmlNodeInterface; + +/** + * Description of ImplementsInterface + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class ImplementsInterface extends BaseArrow +{ + public function __construct(UmlNodeInterface $source, Interface_ $target) + { + parent::__construct($source, $target, "..|>"); + } +} diff --git a/Model/Arrow/UsesTrait.php b/Model/Arrow/UsesTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..925284d35f90091c736ab7d12613392711c31d54 --- /dev/null +++ b/Model/Arrow/UsesTrait.php @@ -0,0 +1,25 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model\Arrow; + +use Irstea\PlantUmlBundle\Model\Node\Trait_; +use Irstea\PlantUmlBundle\Model\UmlNodeInterface; + +/** + * Description of UseTrait + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class UsesTrait extends BaseArrow +{ + public function __construct(UmlNodeInterface $source, Trait_ $trait) + { + parent::__construct($source, $trait, "--|>"); + } +} diff --git a/Model/ClassVisitor.php b/Model/ClassVisitor.php new file mode 100644 index 0000000000000000000000000000000000000000..10d61d51f4f1d521781b6cbdd5d6dde78471a960 --- /dev/null +++ b/Model/ClassVisitor.php @@ -0,0 +1,113 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model; + +use Irstea\PlantUmlBundle\Model\Node\Class_; +use Irstea\PlantUmlBundle\Model\Node\Interface_; +use Irstea\PlantUmlBundle\Model\Node\Trait_; +use ReflectionClass; + +/** + * Description of Visitor + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class ClassVisitor implements ClassVisitorInterface, UmlComponentInterface +{ + /** + * @var UmlNodeInterface[] + */ + protected $nodes = []; + + /** + * @var UmlComponentInterface[] + */ + protected $arrows = []; + + public function visitClass($className) + { + assert('is_string($className)', $className); + if (isset($this->nodes[$className])) { + return $this->nodes[$className]; + } + + return $this->doVisitClass($className); + } + + /** + * + * @param string $className + * @return UmlNodeInterface + */ + protected function doVisitClass($className) + { + $reflection = new ReflectionClass($className); + return $this->visitClassReflection($reflection); + } + + /** + * @param ReflectionClass $class + * @return UmlNodeInterface + */ + protected function visitClassReflection(ReflectionClass $class) + { + if ($class->isTrait()) { + $node = new Trait_($class->getName()); + } elseif ($class->isInterface()) { + $node = new Interface_($class->getName()); + } else { + $node = new Class_($class->getName(), $class->isAbstract(), $class->isFinal()); + } + $this->nodes[$class->getName()] = $node; + + $parentClass = $class->getParentClass(); + $traitNames = $class->getTraitNames(); + + $indirectInterfaces = array_filter( + array_map( + function ($i) { return $i->getInterfaceNames(); }, + $class->getInterfaces() + ) + ); + $interfaceNames = $class->getInterfaceNames(); + if (!empty($indirectInterfaces)) { + $indirectInterfaces = call_user_func_array('array_merge', $indirectInterfaces); + $interfaceNames = array_diff($interfaceNames, $indirectInterfaces); + } + + if ($parentClass) { + $traitNames = array_diff($traitNames, $parentClass->getTraitNames()); + $interfaceNames = array_diff($interfaceNames, $parentClass->getInterfaceNames()); + $this->visitRelations($node, [$parentClass->getName()], 'Irstea\PlantUmlBundle\Model\Arrow\ExtendsClass'); + } + + $this->visitRelations($node, $interfaceNames, 'Irstea\PlantUmlBundle\Model\Arrow\ImplementsInterface'); + $this->visitRelations($node, $traitNames, 'Irstea\PlantUmlBundle\Model\Arrow\UsesTrait'); + + return $node; + } + + protected function visitRelations(UmlNodeInterface $source, array $classNames, $relationClass) + { + foreach ($classNames as $className) { + $target = $this->visitClass($className); + $this->arrows[] = new $relationClass($source, $target); + } + } + + public function outputTo($stream) + { + foreach ($this->nodes as $node) { + $node->outputTo($stream); + } + foreach ($this->arrows as $arrow) { + $arrow->outputTo($stream); + } + } +} diff --git a/Model/ClassVisitorInterface.php b/Model/ClassVisitorInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..6afc61e98b5513d2d835fabe2e3feb416f78af5c --- /dev/null +++ b/Model/ClassVisitorInterface.php @@ -0,0 +1,21 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model; + +/** + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +interface ClassVisitorInterface +{ + /** + * @return UmlNodeInterface + */ + public function visitClass($className); +} diff --git a/Model/Node/BaseNode.php b/Model/Node/BaseNode.php new file mode 100644 index 0000000000000000000000000000000000000000..a0d74c44709d22d6e528e1bb84cfe900b5c9dd9a --- /dev/null +++ b/Model/Node/BaseNode.php @@ -0,0 +1,99 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model\Node; + +use Irstea\PlantUmlBundle\Model\UmlNodeInterface; + +/** + * Description of Class + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class BaseNode implements UmlNodeInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var string + */ + private $nodeType; + + /** + * @var string[] + */ + private $classifiers; + + /** + * @var string[] + */ + private $stereotypes; + + /** + * @param string $name + * @param string $nodeType + * @param array $classifiers + * @param array $stereotypes + */ + function __construct($name, $nodeType, array $classifiers = [], array $stereotypes = []) + { + $this->name = $name; + $this->nodeType = $nodeType; + $this->classifiers = $classifiers; + $this->stereotypes = $stereotypes; + } + + public function outputTo($stream) + { + $this->outputClassifiersTo($stream); + $this->outputNodeTypeTo($stream); + $this->outputAliasTo($stream); + $this->outputStereotypesTo($stream); + fputs($stream, " {\n"); + $this->outputAttributesTo($stream); + $this->outputMethodsTo($stream); + fputs($stream, "}\n"); + } + + public function outputAliasTo($stream) + { + fputs($stream, '"'.str_replace('\\', '\\\\', $this->name).'"'); + } + + protected function outputClassifiersTo($stream) + { + foreach($this->classifiers as $classifier) { + fputs($stream, "$classifier "); + } + } + + protected function outputNodeTypeTo($stream) + { + fputs($stream, $this->nodeType." "); + } + + protected function outputStereotypesTo($stream) + { + foreach($this->stereotypes as $stereotypes) { + fputs($stream, " <<$stereotypes>>"); + } + } + + protected function outputAttributesTo($stream) + { + // NOP + } + + protected function outputMethodsTo($stream) + { + // NOP + } +} diff --git a/Model/Node/Class_.php b/Model/Node/Class_.php new file mode 100644 index 0000000000000000000000000000000000000000..ef3f838674a315f3b47da6373f22713c813393c4 --- /dev/null +++ b/Model/Node/Class_.php @@ -0,0 +1,28 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model\Node; + +/** + * Description of Class_ + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class Class_ extends BaseNode +{ + public function __construct($name, $isAbstract, $isFinal) + { + $classifiers = []; + if ($isAbstract) { + $classifiers[] = 'abstract'; + } elseif ($isFinal) { + $classifiers[] = 'final'; + } + parent::__construct($name, "class", $classifiers); + } +} diff --git a/Model/Node/Interface_.php b/Model/Node/Interface_.php new file mode 100644 index 0000000000000000000000000000000000000000..4f53b3fbdfc23cd3e725a9abbf2dc6c7ef236b13 --- /dev/null +++ b/Model/Node/Interface_.php @@ -0,0 +1,22 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model\Node; + +/** + * Description of Interface_ + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class Interface_ extends BaseNode +{ + public function __construct($name) + { + parent::__construct($name, 'interface', [], ['interface']); + } +} diff --git a/Model/Node/Trait_.php b/Model/Node/Trait_.php new file mode 100644 index 0000000000000000000000000000000000000000..2b7001f74bff238afefe7c8b4c4480bdefc52a94 --- /dev/null +++ b/Model/Node/Trait_.php @@ -0,0 +1,22 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model\Node; + +/** + * Description of Trait_ + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class Trait_ extends BaseNode +{ + public function __construct($name) + { + parent::__construct($name, 'class', [], ['trait']); + } +} diff --git a/Model/Orm/EntityVisitor.php b/Model/Orm/EntityVisitor.php new file mode 100644 index 0000000000000000000000000000000000000000..74786379d83ad55bdf43bc254b6fdb184ba29d64 --- /dev/null +++ b/Model/Orm/EntityVisitor.php @@ -0,0 +1,51 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model\Orm; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Irstea\PlantUmlBundle\Model\ClassVisitor; + +/** + * Description of Visitor + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +class EntityVisitor extends ClassVisitor +{ + /** + * @var ClassMetadata[] + */ + protected $metadata = []; + + public function __construct(array $metadata) + { + foreach($metadata as $md) { + /* @var $md ClassMetadata */ + $this->metadata[$md->getReflectionClass()->getName()] = $md; + } + } + + public function doVisitClass($className) + { + if (isset($this->metadata[$className])) { + return $this->visitClassMetadata($this->metadata[$className]); + } + + return parent::doVisitClass($className); + } + + /** + * @param ClassMetadata $metadata + * @return AbstractNode + */ + protected function visitClassMetadata(ClassMetadata $metadata) + { + return $this->visitClassReflection($metadata->getReflectionClass()); + } +} diff --git a/Model/UmlComponentInterface.php b/Model/UmlComponentInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..65b13c7c9a726f136343582ff99f523a4fd4d4a2 --- /dev/null +++ b/Model/UmlComponentInterface.php @@ -0,0 +1,18 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model; + +/** + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +interface UmlComponentInterface +{ + public function outputTo($stream); +} diff --git a/Model/UmlNodeInterface.php b/Model/UmlNodeInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..2594948c2e1b514c08c20a6cc57f7c31ea0baceb --- /dev/null +++ b/Model/UmlNodeInterface.php @@ -0,0 +1,18 @@ +<?php + +/* + * © 2016 IRSTEA + * Guillaume Perréal <guillaume.perreal@irstea.fr> + * Tous droits réservés. + */ + +namespace Irstea\PlantUmlBundle\Model; + +/** + * + * @author Guillaume Perréal <guillaume.perreal@irstea.fr> + */ +interface UmlNodeInterface extends \Irstea\PlantUmlBundle\Model\UmlComponentInterface +{ + public function outputAliasTo($stream); +}