Commit 93e12c5d authored by Guillaume Perréal's avatar Guillaume Perréal
Browse files

Ajout d'un système de configuration.

No related merge requests found
Showing with 566 additions and 54 deletions
+566 -54
......@@ -7,24 +7,36 @@
namespace Irstea\PlantUmlBundle\Command;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Irstea\PlantUmlBundle\Doctrine\AssociationDecorator;
use Irstea\PlantUmlBundle\Doctrine\DoctrineNamespace;
use Irstea\PlantUmlBundle\Doctrine\EntityDecorator;
use Irstea\PlantUmlBundle\Model\ClassFilterInterface;
use Irstea\PlantUmlBundle\Model\ClassVisitor;
use Irstea\PlantUmlBundle\Model\Decorator\CompositeDecorator;
use Irstea\PlantUmlBundle\Model\Decorator\FilteringDecorator;
use Irstea\PlantUmlBundle\Model\Decorator\InheritanceDecorator;
use Irstea\PlantUmlBundle\Model\Decorator\NullDecorator;
use Irstea\PlantUmlBundle\Model\DecoratorInterface;
use Irstea\PlantUmlBundle\Model\Filter\AcceptAllFilter;
use Irstea\PlantUmlBundle\Model\Filter\Composite\AllFilter;
use Irstea\PlantUmlBundle\Model\Filter\DirectoryFilter;
use Irstea\PlantUmlBundle\Model\Filter\NamespaceFilter;
use Irstea\PlantUmlBundle\Model\Filter\Whitelist;
use Irstea\PlantUmlBundle\Model\Namespace_\BundleNamespace;
use Irstea\PlantUmlBundle\Model\Namespace_\FlatNamespace;
use Irstea\PlantUmlBundle\Model\Namespace_\MappedNamespace;
use Irstea\PlantUmlBundle\Model\Namespace_\Php\RootNamespace;
use Irstea\PlantUmlBundle\Model\NamespaceInterface;
use Irstea\PlantUmlBundle\Writer\OutputWriter;
use ReflectionClass;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\Exception\RuntimeException;
/**
......@@ -32,13 +44,39 @@ use Symfony\Component\Console\Output\OutputInterface;
*
* @author Guillaume Perréal <guillaume.perreal@irstea.fr>
*/
class EntitySchemaCommand extends ContainerAwareCommand
class GenerateCommand extends ContainerAwareCommand
{
/**
* @var string[]
*/
private $bundles;
/**
* @var KernelInterface
*/
private $kernel;
/**
* @var EntityManagerInterface
*/
private $entityManager;
protected function configure()
{
$this
->setName('irstea:plantuml:entities')
->setDescription("Génère un schéma PlantUML à partir des métadonnées de Doctrine.");
->setName('irstea:plantuml:generate')
->setDescription("Génère un graphe en PlantUML.")
->addArgument('graph', InputArgument::REQUIRED, 'Nom du graphe à générer');
}
protected function initialize(InputInterface $input, OutputInterface $output)
{
parent::initialize($input, $output);
// @todo: DI
$this->bundles = $this->getContainer()->getParameter('kernel.bundles');
$this->kernel = $this->getContainer()->get('kernel');
$this->entityManager = $this->getContainer()->get('doctrine.orm.entity_manager');
}
/**
......@@ -50,41 +88,29 @@ class EntitySchemaCommand extends ContainerAwareCommand
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
/* @var $manager EntityManagerInterface */
$manager = $this->getContainer()->get('doctrine.orm.entity_manager');
$factory = $manager->getMetadataFactory();
$allMetadata = $factory->getAllMetadata();
$name = $input->getArgument('graph');
$graphs = $this->getContainer()->getParameter('irstea_plant_uml.graphs');
if (!isset($graphs[$name])) {
throw new InvalidArgumentException("Le graphe '$name' n'est pas défini.");
}
$decorationFilter = new DirectoryFilter(
[
realpath($this->getContainer()->getParameter('kernel.root_dir').'/../src')
]
);
$config = $graphs[$name];
$bundleNamespace = 'Irstea\\SygadeBundle\\Entity\\';
$classes = $this->findClasses($config['sources']);
if (empty($classes)) {
$output->writeln("Nothing to analyze.");
return 0;
}
$entityFilter = new NamespaceFilter([$bundleNamespace]);
$namespace = $this->buildNamespace($config['layout']['namespaces']);
$displayFilter = $this->buildFilter($config['layout']);
//$namespace = new BundleNamespace($this->getContainer()->getParameter('kernel.bundles'));
$namespace = new MappedNamespace([$bundleNamespace => '']);
$decorator = $this->buildDecorator($config['decoration']);
$decorator = new FilteringDecorator(
new CompositeDecorator([
new InheritanceDecorator(),
new EntityDecorator($factory),
new AssociationDecorator($factory),
]),
$decorationFilter
);
$visitor = new ClassVisitor($decorator, $displayFilter, $namespace);
$visitor = new ClassVisitor($decorator, null, $namespace);
foreach($allMetadata as $metadata) {
/* @var $metadata ClassMetadata */
if ($entityFilter->accept($metadata->getReflectionClass())) {
$visitor->visitClass($metadata->getName());
}
foreach($classes as $class) {
$visitor->visitClass($class);
}
$writer = new OutputWriter($output);
......@@ -92,4 +118,223 @@ class EntitySchemaCommand extends ContainerAwareCommand
$visitor->outputTo($writer);
$writer->write("@enduml\n");
}
/**
* @param ReflectionClass[]
*/
protected function findClasses(array $config)
{
switch($config['type']) {
case 'entities':
$classes = $this->findEntities($config['entity_manager']);
break;
case 'classes':
$classes = $this->findClassesInDirectories($config['directories']);
break;
}
$filter = $this->buildFilter($config);
if ($filter) {
return array_filter($classes, [$filter, 'accept']);
}
return $classes;
}
/**
* @return ReflectionClass[]
*/
protected function findEntities($managerName)
{
$doctrine = $this->getContainer()->get('doctrine');
$this->entityManager = $doctrine->getManager($managerName);
$classes = [];
foreach($this->entityManager->getMetadataFactory()->getAllMetadata() as $metadata) {
$classes[$metadata->getName()] = $metadata->getReflectionClass();
}
return $classes;
}
/**
*
* @return ReflectionClass[]
*/
protected function findClassesInDirectories($directories)
{
$paths = $this->parseDirectories($directories);
$files = Finder::create()
->in($paths)
->files()
->name('*.php')
->getIterator();
foreach($files as $file)
{
/* @var $file SplFileInfo */
$path = $file->getPathname();
try {
irstea_plantmul_include($path);
} catch(Exception $ex) {
printf("%s: %s (%s)\n", $path, get_class($ex), $ex->getMessage());
}
}
$filter = new DirectoryFilter($paths);
$classes = [];
foreach(get_declared_classes() as $className) {
$class = new ReflectionClass($className);
if ($filter->accept($class)) {
$classes[$className] = $class;
}
}
return $classes;
}
/**
* @param array $config
* @return DecoratorInterface
* @throws RuntimeException
*/
protected function buildDecorator(array $config)
{
$decorators = [];
foreach($config['decorators'] as $decorator) {
switch($decorator) {
case 'inheritance':
$decorators[] = new InheritanceDecorator();
break;
case 'entity':
$decorators[] = new EntityDecorator($this->entityManager->getMetadataFactory());
break;
case 'associations':
$decorators[] = new AssociationDecorator($this->entityManager->getMetadataFactory());
break;
/*default:
throw new RuntimeException("Decorator '$decorator' not yet implemented");*/
}
}
if (empty($decorators)) {
return NullDecorator::instance();
}
if (count($decorators) === 1) {
$decorator = $decorators[0];
} else {
$decorator = new CompositeDecorator($decorators);
}
$filter = $this->buildFilter($config);
if ($filter) {
$decorator = new FilteringDecorator($decorator, $filter);
}
return $decorator;
}
/**
* @param string $config
* @return NamespaceInterface
*/
protected function buildNamespace($config)
{
switch($config) {
case 'php':
return new RootNamespace();
case 'flat':
return new FlatNamespace();
case 'entities':
return new DoctrineNamespace($this->entityManager->getConfiguration()->getEntityNamespaces());
case 'bundles':
return new BundleNamespace($this->bundles);
}
}
/**
* @param array $config
* @return ClassFilterInterface|null
*/
protected function buildFilter(array $config)
{
$filters = array_merge(
isset($config['include']) ? $this->buildSubFilters($config['include'], false) : [],
isset($config['exclude']) ? $this->buildSubFilters($config['exclude'], true) : []
);
switch(count($filters)) {
case 0:
return null;
case 1:
return $filters[0];
default:
return new AllFilter($filters);
}
}
/**
* @param array $config
* @param boolean $notFound
* @return ClassFilterInterface|null
*/
protected function buildSubFilters(array $config, $notFound)
{
$filters = [];
if (!empty($config['directories'])) {
$paths = $this->parseDirectories($config['directories']);
$filters[] = new DirectoryFilter($paths, $notFound);
}
if (!empty($config['namespaces'])) {
$namespaces = $this->parseNamespaces($config['namespaces']);
$filters[] = new NamespaceFilter($namespaces, $notFound);
}
return $filters;
}
/**
* @param array $paths
* @return array
*/
protected function parseDirectories(array $paths)
{
$actualPaths = [];
foreach($paths as $path) {
if (preg_match('/^@(\w+)(.*)$/', $path, $groups)) {
$bundle = $this->kernel->getBundle($groups[1]);
$path = $bundle->getPath() . $groups[2];
}
$actualPaths[] = realpath($path);
}
return $actualPaths;
}
/**
* @param array $paths
* @return array
*/
protected function parseNamespaces(array $namespaces)
{
$actualNamespaces = [];
foreach($namespaces as $namespace) {
if (preg_match('/^@(\w+)(.*)$/', $namespace, $groups)) {
$bundle = $this->kernel->getBundle($groups[1]);
$namespace = $bundle->getNamespace() . $groups[2];
}
$actualNamespaces[] = $namespace;
}
return $actualNamespaces;
}
}
function irstea_plantmul_include($filepath)
{
@include_once($filepath);
}
\ No newline at end of file
<?php
/*
* © 2016 IRSTEA
* Guillaume Perréal <guillaume.perreal@irstea.fr>
* Tous droits réservés.
*/
namespace Irstea\PlantUmlBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* Description of Configuration
*
* @author Guillaume Perréal <guillaume.perreal@irstea.fr>
*/
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$treeBuilder->root('irstea_plant_uml')
->children()
->arrayNode('binaries')
->addDefaultsIfNotSet()
->children()
->scalarNode('java')
->defaultValue("java")
->end()
->scalarNode('plamtuml_jar')
->defaultValue("plantuml.jar")
->end()
->scalarNode('dot')
->defaultValue("dot")
->end()
->end()
->end()
->append($this->buildGraphNode())
->end();
return $treeBuilder;
}
protected function buildGraphNode()
{
$node = (new TreeBuilder())->root('graphs');
$node
->useAttributeAsKey('name')
->prototype('array')
->children()
->arrayNode('sources')
->addDefaultsIfNotSet()
->children()
->enumNode('type')
->defaultValue('classes')
->values(['classes', 'entities'])
->end()
->scalarNode('entity_manager')
->defaultValue('default')
->end()
->arrayNode('directories')
->defaultValue(['%kernel.root_dir%/../src'])
->prototype('scalar')->end()
->end()
->append($this->buildFilterNode('include'))
->append($this->buildFilterNode('exclude'))
->end()
->end()
->arrayNode('layout')
->addDefaultsIfNotSet()
->children()
->enumNode('namespaces')
->defaultValue('php')
->values(['bundles', 'php', 'flat', 'entities'])
->end()
->append($this->buildFilterNode('include'))
->append($this->buildFilterNode('exclude'))
->end()
->end()
->arrayNode('decoration')
->addDefaultsIfNotSet()
->children()
->arrayNode('decorators')
->defaultValue(['inheritance', 'entity', 'associations', 'properties', 'methods'])
->prototype('enum')
->values(['inheritance', 'entity', 'associations', 'properties', 'methods'])
->end()
->end()
->append($this->buildFilterNode('include'))
->append($this->buildFilterNode('exclude'))
->end()
->end()
->end()
->end();
return $node;
}
protected function buildFilterNode($nodeName)
{
$node = (new TreeBuilder())->root($nodeName);
$node
->children()
->arrayNode('directories')
->prototype('scalar')->end()
->end()
->arrayNode('namespaces')
->prototype('scalar')->end()
->end()
->end();
return $node;
}
}
<?php
/*
* © 2016 IRSTEA
* Guillaume Perréal <guillaume.perreal@irstea.fr>
* Tous droits réservés.
*/
namespace Irstea\PlantUmlBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
* Description of IrsteaPlantUmlExtension
*
* @author Guillaume Perréal <guillaume.perreal@irstea.fr>
*/
class IrsteaPlantUmlExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('irstea_plant_uml.binaries', $config['binaries']);
$container->setParameter('irstea_plant_uml.graphs', $config['graphs']);
}
}
......@@ -70,15 +70,21 @@ class ClassVisitor implements ClassVisitorInterface, WritableInterface
return $this;
}
public function visitClass($className)
public function visitClass($classOrName)
{
assert('is_string($className)', $className);
if ($classOrName instanceof \ReflectionClass) {
$class = $classOrName;
} elseif (is_string($classOrName)) {
$class = new \ReflectionClass($classOrName);
} else {
throw new \InvalidArgumentException("Invalid argument, expected ReflectionClass or string");
}
$className = $class->getName();
if (isset($this->nodes[$className])) {
return $this->nodes[$className];
}
$class = new ReflectionClass($className);
if (!$this->filter->accept($class)) {
return $this->nodes[$className] = false;
}
......
......@@ -8,6 +8,8 @@
namespace Irstea\PlantUmlBundle\Model;
use ReflectionClass;
/**
*
* @author Guillaume Perréal <guillaume.perreal@irstea.fr>
......@@ -15,7 +17,8 @@ namespace Irstea\PlantUmlBundle\Model;
interface ClassVisitorInterface
{
/**
* @param ReflectionClass|string
* @return NodeInterface
*/
public function visitClass($className);
public function visitClass($classOrName);
}
<?php
/*
* © 2016 IRSTEA
* Guillaume Perréal <guillaume.perreal@irstea.fr>
* Tous droits réservés.
*/
namespace Irstea\PlantUmlBundle\Model\Filter\Composite;
use Irstea\PlantUmlBundle\Model\ClassFilterInterface;
use ReflectionClass;
/**
* Description of CompositeFilter
*
* @author Guillaume Perréal <guillaume.perreal@irstea.fr>
*/
abstract class AbstractCompositeFilter implements ClassFilterInterface
{
/**
* @var ClassFilterInterface
*/
protected $filters;
public function __construct(array $filters)
{
$this->filters = $filters;
}
}
<?php
/*
* © 2016 IRSTEA
* Guillaume Perréal <guillaume.perreal@irstea.fr>
* Tous droits réservés.
*/
namespace Irstea\PlantUmlBundle\Model\Filter\Composite;
/**
* Description of AnyFilter
*
* @author Guillaume Perréal <guillaume.perreal@irstea.fr>
*/
class AllFilter extends AbstractCompositeFilter
{
public function accept(\ReflectionClass $class)
{
foreach($this->filters as $filter) {
if (!$filter->accept($class)) {
return false;
}
}
return true;
}
}
<?php
/*
* © 2016 IRSTEA
* Guillaume Perréal <guillaume.perreal@irstea.fr>
* Tous droits réservés.
*/
namespace Irstea\PlantUmlBundle\Model\Filter\Composite;
/**
* Description of AnyFilter
*
* @author Guillaume Perréal <guillaume.perreal@irstea.fr>
*/
class AnyFilter extends AbstractCompositeFilter
{
public function accept(\ReflectionClass $class)
{
foreach($this->filters as $filter) {
if ($filter->accept($class)) {
return true;
}
}
return false;
}
}
......@@ -21,20 +21,32 @@ class DirectoryFilter implements ClassFilterInterface
/**
* @var string[]
*/
private $accepted = [];
private $paths = [];
public function __construct(array $accepted)
/**
* @var boolean
*/
private $notFound;
public function __construct(array $paths, $notFound = false)
{
$this->accepted = $accepted;
$this->paths = array_map(
function ($path) {
return rtrim(strtr($path, '/\\', DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
},
$paths
);
$this->notFound = $notFound;
}
public function accept(ReflectionClass $class)
{
foreach($this->accepted as $path) {
if (strpos($class->getFileName(), $path) === 0) {
return true;
$filepath = dirname($class->getFileName());
foreach($this->paths as $path) {
if (strpos($filepath, $path) === 0) {
return !$this->notFound;
}
}
return false;
return $this->notFound;
}
}
......@@ -21,20 +21,32 @@ class NamespaceFilter implements ClassFilterInterface
/**
* @var string[]
*/
private $accepted = [];
private $namespaces = [];
public function __construct(array $accepted)
/**
* @var boolena
*/
private $notFound;
public function __construct(array $namespaces, $notFound = false)
{
$this->accepted = $accepted;
$this->namespaces = array_map(
function ($namespace) {
return trim($namespace, '\\').'\\';
},
$namespaces
);
$this->notFound = $notFound;
}
public function accept(ReflectionClass $class)
{
foreach($this->accepted as $namespace) {
if (strpos($class->getName(), $namespace) === 0) {
return true;
$className = $class->getNamespaceName().'\\';
foreach($this->namespaces as $namespace) {
if (strpos($className, $namespace) === 0) {
return !$this->notFound;
}
}
return false;
return $this->notFound;
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment