Commit 0af648c3 authored by Guillaume Perréal's avatar Guillaume Perréal
Browse files

Première version fonctionnelle pour les entités.

parent da07abb4
<?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);
}
}
<?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";
}
}
}
<?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." ");
}
}
<?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);
}
}
<?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, "..|>");
}
}
<?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, "--|>");
}
}
<?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);
}
}
}
<?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);
}
<?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." ");
}