From 0af648c3472d6abc75b8a2edc32538d973caca4a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Guillaume=20Perr=C3=A9al?= <guillaume.perreal@irstea.fr>
Date: Tue, 8 Mar 2016 15:46:49 +0100
Subject: [PATCH] =?UTF-8?q?Premi=C3=A8re=20version=20fonctionnelle=20pour?=
 =?UTF-8?q?=20les=20entit=C3=A9s.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Command/EntitySchemaCommand.php     |  60 +++++++++
 Commmand/OrmSchemaCommand.php       | 201 ----------------------------
 Model/Arrow/BaseArrow.php           |  64 +++++++++
 Model/Arrow/ExtendsClass.php        |  22 +++
 Model/Arrow/ImplementsInterface.php |  25 ++++
 Model/Arrow/UsesTrait.php           |  25 ++++
 Model/ClassVisitor.php              | 113 ++++++++++++++++
 Model/ClassVisitorInterface.php     |  21 +++
 Model/Node/BaseNode.php             |  99 ++++++++++++++
 Model/Node/Class_.php               |  28 ++++
 Model/Node/Interface_.php           |  22 +++
 Model/Node/Trait_.php               |  22 +++
 Model/Orm/EntityVisitor.php         |  51 +++++++
 Model/UmlComponentInterface.php     |  18 +++
 Model/UmlNodeInterface.php          |  18 +++
 15 files changed, 588 insertions(+), 201 deletions(-)
 create mode 100644 Command/EntitySchemaCommand.php
 delete mode 100644 Commmand/OrmSchemaCommand.php
 create mode 100644 Model/Arrow/BaseArrow.php
 create mode 100644 Model/Arrow/ExtendsClass.php
 create mode 100644 Model/Arrow/ImplementsInterface.php
 create mode 100644 Model/Arrow/UsesTrait.php
 create mode 100644 Model/ClassVisitor.php
 create mode 100644 Model/ClassVisitorInterface.php
 create mode 100644 Model/Node/BaseNode.php
 create mode 100644 Model/Node/Class_.php
 create mode 100644 Model/Node/Interface_.php
 create mode 100644 Model/Node/Trait_.php
 create mode 100644 Model/Orm/EntityVisitor.php
 create mode 100644 Model/UmlComponentInterface.php
 create mode 100644 Model/UmlNodeInterface.php

diff --git a/Command/EntitySchemaCommand.php b/Command/EntitySchemaCommand.php
new file mode 100644
index 0000000..d733514
--- /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 0c4e057..0000000
--- 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 0000000..432ffe6
--- /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 0000000..384541d
--- /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 0000000..dbdede8
--- /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 0000000..925284d
--- /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 0000000..10d61d5
--- /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 0000000..6afc61e
--- /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 0000000..a0d74c4
--- /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 0000000..ef3f838
--- /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 0000000..4f53b3f
--- /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 0000000..2b7001f
--- /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 0000000..7478637
--- /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 0000000..65b13c7
--- /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 0000000..2594948
--- /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);
+}
-- 
GitLab