diff --git a/Command/GenerateCommand.php b/Command/GenerateCommand.php index 0a2a7374c6dcac3873b3688ec1dfccc7d8123ecf..b17b5f0c6ea5cda817aed75237e4f4734e5bfc96 100644 --- a/Command/GenerateCommand.php +++ b/Command/GenerateCommand.php @@ -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 diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php new file mode 100644 index 0000000000000000000000000000000000000000..e112708c847a7dc6161ff129832764fcbe9d9537 --- /dev/null +++ b/DependencyInjection/Configuration.php @@ -0,0 +1,121 @@ +<?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; + } + +} + diff --git a/DependencyInjection/IrsteaPlantUmlExtension.php b/DependencyInjection/IrsteaPlantUmlExtension.php new file mode 100644 index 0000000000000000000000000000000000000000..ae45c05e1258cb4dbe8928230f980aa6437bdb25 --- /dev/null +++ b/DependencyInjection/IrsteaPlantUmlExtension.php @@ -0,0 +1,29 @@ +<?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']); + } +} diff --git a/Model/ClassVisitor.php b/Model/ClassVisitor.php index 75730ae8dcb501e520e6601466af9dd00541e8e9..1f557bc08da5c95421faa459e3cd8be9f0b48216 100644 --- a/Model/ClassVisitor.php +++ b/Model/ClassVisitor.php @@ -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; } diff --git a/Model/ClassVisitorInterface.php b/Model/ClassVisitorInterface.php index 91071200e816a922dbab8f2c86aaa80741ec01ca..dbc6608022934facf36f3e89e092f958d59746ac 100644 --- a/Model/ClassVisitorInterface.php +++ b/Model/ClassVisitorInterface.php @@ -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); } diff --git a/Model/Filter/Composite/AbstractCompositeFilter.php b/Model/Filter/Composite/AbstractCompositeFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..7d2971574ab8fb708ab3b90fbaafa41ad2d6bb49 --- /dev/null +++ b/Model/Filter/Composite/AbstractCompositeFilter.php @@ -0,0 +1,30 @@ +<?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; + } +} diff --git a/Model/Filter/Composite/AllFilter.php b/Model/Filter/Composite/AllFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..b3033280ae52671ef8c5a3d87b49073d5f8f75b3 --- /dev/null +++ b/Model/Filter/Composite/AllFilter.php @@ -0,0 +1,27 @@ +<?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; + } +} diff --git a/Model/Filter/Composite/AnyFilter.php b/Model/Filter/Composite/AnyFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..e671b857e4af9fffd49cedc61f57ba4e35aa126d --- /dev/null +++ b/Model/Filter/Composite/AnyFilter.php @@ -0,0 +1,27 @@ +<?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; + } +} diff --git a/Model/Filter/DirectoryFilter.php b/Model/Filter/DirectoryFilter.php index 02a8f81dd78da225c8644ae8d8c31d403af418ad..85fa8cf56db664875228e7a33472897fcb5e6263 100644 --- a/Model/Filter/DirectoryFilter.php +++ b/Model/Filter/DirectoryFilter.php @@ -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; } } diff --git a/Model/Filter/NamespaceFilter.php b/Model/Filter/NamespaceFilter.php index dcafefc871fb3095526a91eef3cd80e9e9023e16..4b01aa2f1eced69ea3717851e52ef3a4db4faa19 100644 --- a/Model/Filter/NamespaceFilter.php +++ b/Model/Filter/NamespaceFilter.php @@ -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; } }