diff --git a/src/Controller/TreeController.php b/src/Controller/TreeController.php index 9d5d09c0..06a41b58 100644 --- a/src/Controller/TreeController.php +++ b/src/Controller/TreeController.php @@ -27,114 +27,94 @@ use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Storelocation; use App\Entity\Parts\Supplier; +use App\Entity\UserSystem\U2FKey; +use App\Entity\UserSystem\User; use App\Services\ToolsTreeBuilder; use App\Services\TreeBuilder; +use App\Services\Trees\TreeViewGenerator; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; /** * This controller has the purpose to provide the data for all treeviews. + * @Route("/tree") */ class TreeController extends AbstractController { + protected $treeGenerator; + + public function __construct(TreeViewGenerator $treeGenerator) + { + $this->treeGenerator = $treeGenerator; + } + /** - * @Route("/tree/tools", name="tree_tools") + * @Route("/tools", name="tree_tools") */ public function tools(ToolsTreeBuilder $builder) { $tree = $builder->getTree(); - //Ignore null values, to save data return $this->json($tree, 200, [], ['skip_null_values' => true]); } /** - * @Route("/tree/category/{id}", name="tree_category") - * @Route("/tree/categories") + * @Route("/category/{id}", name="tree_category") + * @Route("/categories") */ - public function categoryTree(TreeBuilder $builder, Category $category = null) + public function categoryTree(Category $category = null) { - if (null !== $category) { - $tree[] = $builder->elementToTreeNode($category); - } else { - $tree = $builder->typeToTree(Category::class); - } - + $tree = $this->treeGenerator->getTreeView(Category::class, $category); return $this->json($tree, 200, [], ['skip_null_values' => true]); } /** - * @Route("/tree/footprint/{id}", name="tree_footprint") - * @Route("/tree/footprints") + * @Route("/footprint/{id}", name="tree_footprint") + * @Route("/footprints") */ - public function footprintTree(TreeBuilder $builder, Footprint $footprint = null) + public function footprintTree(Footprint $footprint = null) { - if (null !== $footprint) { - $tree[] = $builder->elementToTreeNode($footprint); - } else { - $tree = $builder->typeToTree(Footprint::class); - } - + $tree = $this->treeGenerator->getTreeView(Footprint::class, $footprint); return $this->json($tree, 200, [], ['skip_null_values' => true]); } /** - * @Route("/tree/location/{id}", name="tree_location") - * @Route("/tree/locations") + * @Route("/location/{id}", name="tree_location") + * @Route("/locations") */ - public function locationTree(TreeBuilder $builder, Storelocation $location = null) + public function locationTree(Storelocation $location = null) { - if (null !== $location) { - $tree[] = $builder->elementToTreeNode($location); - } else { - $tree = $builder->typeToTree(Storelocation::class); - } - + $tree = $this->treeGenerator->getTreeView(Storelocation::class, $location); return $this->json($tree, 200, [], ['skip_null_values' => true]); } /** - * @Route("/tree/manufacturer/{id}", name="tree_manufacturer") - * @Route("/tree/manufacturers") + * @Route("/manufacturer/{id}", name="tree_manufacturer") + * @Route("/manufacturers") */ - public function manufacturerTree(TreeBuilder $builder, Manufacturer $manufacturer = null) + public function manufacturerTree(Manufacturer $manufacturer = null) { - if (null !== $manufacturer) { - $tree[] = $builder->elementToTreeNode($manufacturer); - } else { - $tree = $builder->typeToTree(Manufacturer::class); - } - + $tree = $this->treeGenerator->getTreeView(Manufacturer::class, $manufacturer); return $this->json($tree, 200, [], ['skip_null_values' => true]); } /** - * @Route("/tree/supplier/{id}", name="tree_supplier") - * @Route("/tree/suppliers") + * @Route("/supplier/{id}", name="tree_supplier") + * @Route("/suppliers") */ - public function supplierTree(TreeBuilder $builder, Supplier $supplier = null) + public function supplierTree(Supplier $supplier = null) { - if (null !== $supplier) { - $tree[] = $builder->elementToTreeNode($supplier); - } else { - $tree = $builder->typeToTree(Supplier::class); - } - + $tree = $this->treeGenerator->getTreeView(Supplier::class, $supplier); return $this->json($tree, 200, [], ['skip_null_values' => true]); } /** - * @Route("/tree/device/{id}", name="tree_device") - * @Route("/tree/devices") + * @Route("/device/{id}", name="tree_device") + * @Route("/devices") */ - public function deviceTree(TreeBuilder $builder, Device $device = null) + public function deviceTree(Device $device = null) { - if (null !== $device) { - $tree[] = $builder->elementToTreeNode($device); - } else { - $tree = $builder->typeToTree(Device::class, null); - } - + $tree = $this->treeGenerator->getTreeView(Device::class, $device, ''); return $this->json($tree, 200, [], ['skip_null_values' => true]); } } diff --git a/src/Entity/Base/NamedDBElement.php b/src/Entity/Base/NamedDBElement.php index 61cac441..d45885ff 100644 --- a/src/Entity/Base/NamedDBElement.php +++ b/src/Entity/Base/NamedDBElement.php @@ -30,7 +30,7 @@ use Symfony\Component\Validator\Constraints as Assert; /** * All subclasses of this class have an attribute "name". * - * @ORM\MappedSuperclass() + * @ORM\MappedSuperclass(repositoryClass="App\Repository\UserRepository") * @ORM\HasLifecycleCallbacks() */ abstract class NamedDBElement extends DBElement diff --git a/src/Entity/Base/StructuralDBElement.php b/src/Entity/Base/StructuralDBElement.php index ebf55b86..d99a17db 100644 --- a/src/Entity/Base/StructuralDBElement.php +++ b/src/Entity/Base/StructuralDBElement.php @@ -26,6 +26,7 @@ namespace App\Entity\Base; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Validator\Constraints\NoneOfItsChildren; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Serializer\Annotation\Groups; @@ -244,13 +245,16 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement * * @param bool $recursive if true, the search is recursive * - * @return static[] all subelements as an array of objects (sorted by their full path) + * @return Collection all subelements as an array of objects (sorted by their full path) */ public function getSubelements(): iterable { return $this->children; } + /** + * @return Collection + */ public function getChildren(): iterable { return $this->children; diff --git a/src/Helpers/TreeViewNode.php b/src/Helpers/TreeViewNode.php index 32722bab..c1966049 100644 --- a/src/Helpers/TreeViewNode.php +++ b/src/Helpers/TreeViewNode.php @@ -21,6 +21,10 @@ namespace App\Helpers; +use App\Entity\Base\DBElement; +use App\Entity\Base\NamedDBElement; +use App\Entity\Base\StructuralDBElement; + /** * This class represents a node for the bootstrap treeview node. * When you serialize an array of these objects to JSON, you can use the serialized data in data for the treeview. @@ -35,6 +39,8 @@ class TreeViewNode protected $tags; + protected $id; + /** * Creates a new TreeView node with the given parameters. * @@ -53,6 +59,28 @@ class TreeViewNode //$this->state = new TreeViewNodeState(); } + /** + * Return the ID of the entity associated with this node. + * Null if this node is not connected with an entity. + * @return int|null + */ + public function getId() : ?int + { + return $this->id; + } + + /** + * Sets the ID of the entity associated with this node. + * Null if this node is not connected with an entity. + * @param int|null $id + * @return $this + */ + public function setId(?int $id): self + { + $this->id = $id; + return $this; + } + /** * Returns the node text. * @@ -104,7 +132,7 @@ class TreeViewNode /** * Returns the children nodes of this node. * - * @return array|null + * @return TreeViewNode[]|null */ public function getNodes(): ?array { diff --git a/src/Helpers/Trees/StructuralDBElementIterator.php b/src/Helpers/Trees/StructuralDBElementIterator.php new file mode 100644 index 00000000..eeae0b36 --- /dev/null +++ b/src/Helpers/Trees/StructuralDBElementIterator.php @@ -0,0 +1,58 @@ +|StructuralDBElement[] + */ + public function __construct($nodes) + { + parent::__construct($nodes); + } + + /** + * @inheritDoc + */ + public function hasChildren() + { + /** @var StructuralDBElement $element */ + $element = $this->current(); + return !empty($element->getSubelements()); + } + + /** + * @inheritDoc + */ + public function getChildren() + { + /** @var StructuralDBElement $element */ + $element = $this->current(); + return new StructuralDBElementIterator($element->getSubelements()->toArray()); + } +} \ No newline at end of file diff --git a/src/Helpers/Trees/TreeViewNodeIterator.php b/src/Helpers/Trees/TreeViewNodeIterator.php new file mode 100644 index 00000000..3722ec6d --- /dev/null +++ b/src/Helpers/Trees/TreeViewNodeIterator.php @@ -0,0 +1,56 @@ +current(); + return !empty($element->getNodes()); + } + + /** + * @inheritDoc + */ + public function getChildren() + { + /** @var TreeViewNode $element */ + $element = $this->current(); + return new TreeViewNodeIterator($element->getNodes()); + } +} \ No newline at end of file diff --git a/src/Repository/NamedDBElementRepository.php b/src/Repository/NamedDBElementRepository.php new file mode 100644 index 00000000..167d1f88 --- /dev/null +++ b/src/Repository/NamedDBElementRepository.php @@ -0,0 +1,53 @@ +findAll(); + foreach ($entities as $entity) { + /** @var $entity NamedDBElement */ + $node = new TreeViewNode($entity->getName(), null, null); + $node->setId($entity->getID()); + $result[] = $node; + } + + return $result; + } +} \ No newline at end of file diff --git a/src/Repository/StructuralDBElementRepository.php b/src/Repository/StructuralDBElementRepository.php index b084e679..275212b1 100644 --- a/src/Repository/StructuralDBElementRepository.php +++ b/src/Repository/StructuralDBElementRepository.php @@ -22,9 +22,12 @@ namespace App\Repository; use App\Entity\Base\StructuralDBElement; +use App\Helpers\Trees\StructuralDBElementIterator; +use App\Helpers\TreeViewNode; use Doctrine\ORM\EntityRepository; +use Symfony\Component\Stopwatch\Stopwatch; -class StructuralDBElementRepository extends EntityRepository +class StructuralDBElementRepository extends NamedDBElementRepository { /** * Finds all nodes without a parent node. They are our root nodes. @@ -36,6 +39,31 @@ class StructuralDBElementRepository extends EntityRepository return $this->findBy(['parent' => null], ['name' => 'ASC']); } + + /** + * Gets a tree of TreeViewNode elements. The root elements has $parent as parent. + * The treeview is generic, that means the href are null and ID values are set. + * @param StructuralDBElement|null $parent The parent the root elements should have. + * @return TreeViewNode[] + */ + public function getGenericNodeTree(?StructuralDBElement $parent = null) : array + { + $result = []; + + $entities = $this->findBy(['parent' => $parent], ['name' => 'ASC']); + foreach ($entities as $entity) { + /** @var StructuralDBElement $entity */ + //Make a recursive call to find all children nodes + $children = $this->getGenericNodeTree($entity); + $node = new TreeViewNode($entity->getName(), null, $children); + //Set the ID of this entity to later be able to reconstruct the URL + $node->setId($entity->getID()); + $result[] = $node; + } + + return $result; + } + /** * Gets a flattened hierarchical tree. Useful for generating option lists. * @@ -49,16 +77,13 @@ class StructuralDBElementRepository extends EntityRepository $entities = $this->findBy(['parent' => $parent], ['name' => 'ASC']); - /* - * I think it is very difficult to replace this recursive array_merge, - * so if you want to change it you should have a better idea than adding each list to $result array - * and do an array_merge(...$result) at the end. - */ + $elementIterator = new StructuralDBElementIterator($entities); + $recursiveIterator = new \RecursiveIteratorIterator($elementIterator, \RecursiveIteratorIterator::SELF_FIRST); + //$result = iterator_to_array($recursiveIterator); - foreach ($entities as $entity) { - /* @var StructuralDBElement $entity */ - $result[] = $entity; - $result = array_merge($result, $this->toNodesList($entity)); + //We can not use iterator_to_array here or we get only the parent elements + foreach($recursiveIterator as $item) { + $result[] = $item; } return $result; diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index ea3e4345..6b95341a 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -37,7 +37,7 @@ use Symfony\Component\Security\Core\User\UserInterface; * @method User[] findAll() * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class UserRepository extends EntityRepository implements PasswordUpgraderInterface +class UserRepository extends NamedDBElementRepository implements PasswordUpgraderInterface { protected $anonymous_user; diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php new file mode 100644 index 00000000..6ac8101e --- /dev/null +++ b/src/Services/Trees/TreeViewGenerator.php @@ -0,0 +1,114 @@ +urlGenerator = $URLGenerator; + $this->em = $em; + $this->cache = $treeCache; + $this->keyGenerator = $keyGenerator; + } + + public function getTreeView(string $class, ?StructuralDBElement $parent = null, string $href_type = 'list_parts', DBElement $selectedElement = null) + { + $generic = $this->getGenericTree($class, $parent); + $treeIterator = new TreeViewNodeIterator($generic); + $recursiveIterator = new \RecursiveIteratorIterator($treeIterator); + foreach ($recursiveIterator as $item) { + /** @var $item TreeViewNode */ + if ($selectedElement !== null && $item->getId() === $selectedElement->getID()) { + $item->setSelected(true); + } + + if (!empty($item->getNodes())) { + $item->addTag((string) \count($item->getNodes())); + } + + if (!empty($href_type)) { + $entity = $this->em->getPartialReference($class, $item->getId()); + $item->setHref($this->urlGenerator->getURL($entity, $href_type)); + } + } + + return $generic; + } + + /** + * /** + * Gets a tree of TreeViewNode elements. The root elements has $parent as parent. + * The treeview is generic, that means the href are null and ID values are set. + * + * @param string $class The class for which the tree should be generated + * @param StructuralDBElement|null $parent The parent the root elements should have. + * @return TreeViewNode[] + */ + public function getGenericTree(string $class, ?StructuralDBElement $parent = null) : array + { + if(!is_a($class, StructuralDBElement::class, true)) { + throw new \InvalidArgumentException('$class must be a class string that implements StructuralDBElement!'); + } + if($parent !== null && !is_a($parent, $class)) { + throw new \InvalidArgumentException('$parent must be of the type class!'); + } + + /** @var StructuralDBElementRepository $repo */ + $repo = $this->em->getRepository($class); + + //If we just want a part of a tree, dont cache it + if ($parent !== null) { + return $repo->getGenericNodeTree($parent); + } + + $secure_class_name = str_replace('\\', '_', $class); + $key = 'treeview_'.$this->keyGenerator->generateKey().'_'.$secure_class_name; + + $ret = $this->cache->get($key, function (ItemInterface $item) use ($repo, $parent, $secure_class_name) { + // Invalidate when groups, a element with the class or the user changes + $item->tag(['groups', 'tree_treeview', $this->keyGenerator->generateKey(), $secure_class_name]); + return $repo->getGenericNodeTree($parent); + }); + + return $ret; + } +} \ No newline at end of file diff --git a/tests/Repository/NamedDBElementRepositoryTest.php b/tests/Repository/NamedDBElementRepositoryTest.php new file mode 100644 index 00000000..df2e4e1e --- /dev/null +++ b/tests/Repository/NamedDBElementRepositoryTest.php @@ -0,0 +1,66 @@ +entityManager = $kernel->getContainer() + ->get('doctrine') + ->getManager(); + + $this->repo = $this->entityManager->getRepository(User::class); + } + + public function testGetGenericNodeTree() + { + $tree = $this->repo->getGenericNodeTree(); + + $this->assertIsArray($tree); + $this->assertContainsOnlyInstancesOf(TreeViewNode::class, $tree); + $this->assertCount(4, $tree); + $this->assertEquals('anonymous', $tree[0]->getText()); + $this->assertEmpty($tree[0]->getNodes()); + } +} diff --git a/tests/Repository/StructuralDBElementRepositoryTest.php b/tests/Repository/StructuralDBElementRepositoryTest.php new file mode 100644 index 00000000..282d2a89 --- /dev/null +++ b/tests/Repository/StructuralDBElementRepositoryTest.php @@ -0,0 +1,123 @@ +entityManager = $kernel->getContainer() + ->get('doctrine') + ->getManager(); + + $this->repo = $this->entityManager->getRepository(AttachmentType::class); + } + + public function testFindRootNodes() : void + { + $root_nodes = $this->repo->findRootNodes(); + $this->assertCount(3, $root_nodes); + $this->assertContainsOnlyInstancesOf(AttachmentType::class, $root_nodes); + + //Asc sorting + $this->assertEquals('Node 1', $root_nodes[0]->getName()); + $this->assertEquals('Node 2', $root_nodes[1]->getName()); + $this->assertEquals('Node 3', $root_nodes[2]->getName()); + } + + public function testGetGenericTree() : void + { + $tree = $this->repo->getGenericNodeTree(); + $this->assertIsArray($tree); + $this->assertContainsOnlyInstancesOf(TreeViewNode::class, $tree); + + $this->assertCount(3, $tree); + $this->assertCount(2, $tree[0]->getNodes()); + $this->assertCount(1, $tree[0]->getNodes()[0]->getNodes()); + $this->assertEmpty($tree[2]->getNodes()); + $this->assertEmpty($tree[1]->getNodes()[0]->getNodes()); + + //Check text + $this->assertEquals('Node 1', $tree[0]->getText()); + $this->assertEquals('Node 2', $tree[1]->getText()); + $this->assertEquals('Node 3', $tree[2]->getText()); + $this->assertEquals('Node 1.1', $tree[0]->getNodes()[0]->getText()); + $this->assertEquals('Node 1.1.1', $tree[0]->getNodes()[0]->getNodes()[0]->getText()); + + //Check that IDs were set correctly + $this->assertEquals(1, $tree[0]->getId()); + $this->assertEquals(2, $tree[1]->getId()); + $this->assertEquals(7, $tree[0]->getNodes()[0]->getNodes()[0]->getId()); + + } + + /** + * Test $repo->toNodesList() for null as parameter + */ + public function testToNodesListRoot() : void + { + //List all root nodes and their children + $nodes = $this->repo->toNodesList(); + + $this->assertCount(7, $nodes); + $this->assertContainsOnlyInstancesOf(AttachmentType::class, $nodes); + $this->assertEquals('Node 1', $nodes[0]->getName()); + $this->assertEquals('Node 1.1', $nodes[1]->getName()); + $this->assertEquals('Node 1.1.1', $nodes[2]->getName()); + $this->assertEquals('Node 1.2', $nodes[3]->getName()); + $this->assertEquals('Node 2', $nodes[4]->getName()); + $this->assertEquals('Node 2.1', $nodes[5]->getName()); + $this->assertEquals('Node 3', $nodes[6]->getName()); + } + + public function testToNodesListElement() : void + { + //List all nodes that are children to Node 1 + $node1 = $this->repo->find(1); + $nodes = $this->repo->toNodesList($node1); + + $this->assertCount(3, $nodes); + $this->assertContainsOnlyInstancesOf(AttachmentType::class, $nodes); + $this->assertEquals('Node 1.1', $nodes[0]->getName()); + $this->assertEquals('Node 1.1.1', $nodes[1]->getName()); + $this->assertEquals('Node 1.2', $nodes[2]->getName()); + } +} diff --git a/tests/Services/Trees/TreeGeneratorTest.php b/tests/Services/Trees/TreeGeneratorTest.php new file mode 100644 index 00000000..388a259c --- /dev/null +++ b/tests/Services/Trees/TreeGeneratorTest.php @@ -0,0 +1,76 @@ +service = self::$container->get(TreeViewGenerator::class); + } + + public function testGetGenericTree() + { + $tree = $this->service->getGenericTree(AttachmentType::class, null); + + $this->assertIsArray($tree); + $this->assertContainsOnlyInstancesOf(TreeViewNode::class, $tree); + + $this->assertCount(3, $tree); + $this->assertCount(2, $tree[0]->getNodes()); + $this->assertCount(1, $tree[0]->getNodes()[0]->getNodes()); + $this->assertEmpty($tree[2]->getNodes()); + $this->assertEmpty($tree[1]->getNodes()[0]->getNodes()); + + //Check text + $this->assertEquals('Node 1', $tree[0]->getText()); + $this->assertEquals('Node 2', $tree[1]->getText()); + $this->assertEquals('Node 3', $tree[2]->getText()); + $this->assertEquals('Node 1.1', $tree[0]->getNodes()[0]->getText()); + $this->assertEquals('Node 1.1.1', $tree[0]->getNodes()[0]->getNodes()[0]->getText()); + + //Check that IDs were set correctly + $this->assertEquals(1, $tree[0]->getId()); + $this->assertEquals(2, $tree[1]->getId()); + $this->assertEquals(7, $tree[0]->getNodes()[0]->getNodes()[0]->getId()); + + } +}