2020-01-02 18:45:41 +01:00
< ? php
2020-02-22 18:14:36 +01:00
/**
* This file is part of Part - DB ( https :// github . com / Part - DB / Part - DB - symfony ) .
*
2022-11-29 22:28:53 +01:00
* Copyright ( C ) 2019 - 2022 Jan Böhmer ( https :// github . com / jbtronics )
2020-02-22 18:14:36 +01:00
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*/
2020-01-05 15:46:58 +01:00
declare ( strict_types = 1 );
2020-01-02 18:45:41 +01:00
namespace App\Services\Trees ;
2020-02-01 19:48:07 +01:00
use App\Entity\Base\AbstractDBElement ;
use App\Entity\Base\AbstractNamedDBElement ;
use App\Entity\Base\AbstractStructuralDBElement ;
2022-08-13 01:53:43 +02:00
use App\Entity\Parts\Category ;
use App\Entity\Parts\Footprint ;
use App\Entity\Parts\Manufacturer ;
2023-09-04 22:57:40 +02:00
use App\Entity\Parts\StorageLocation ;
2022-08-13 01:53:43 +02:00
use App\Entity\Parts\Supplier ;
2023-11-29 20:49:16 +01:00
use App\Entity\ProjectSystem\Project ;
2020-01-02 22:55:28 +01:00
use App\Helpers\Trees\TreeViewNode ;
2020-01-04 20:24:09 +01:00
use App\Helpers\Trees\TreeViewNodeIterator ;
2024-12-28 22:31:04 +01:00
use App\Repository\NamedDBElementRepository ;
2020-01-02 18:45:41 +01:00
use App\Repository\StructuralDBElementRepository ;
2023-11-29 20:49:16 +01:00
use App\Services\Cache\ElementCacheTagGenerator ;
use App\Services\Cache\UserCacheKeyGenerator ;
2020-01-02 18:45:41 +01:00
use App\Services\EntityURLGenerator ;
use Doctrine\ORM\EntityManagerInterface ;
2022-08-14 19:32:53 +02:00
use InvalidArgumentException ;
use RecursiveIteratorIterator ;
2023-07-03 00:34:37 +02:00
use Symfony\Component\Routing\Generator\UrlGeneratorInterface ;
2020-01-02 18:45:41 +01:00
use Symfony\Contracts\Cache\ItemInterface ;
use Symfony\Contracts\Cache\TagAwareCacheInterface ;
2020-01-02 22:55:28 +01:00
use Symfony\Contracts\Translation\TranslatorInterface ;
2020-01-02 18:45:41 +01:00
2022-08-14 19:32:53 +02:00
use function count ;
2023-06-11 15:02:59 +02:00
/**
* @ see \App\Tests\Services\Trees\TreeViewGeneratorTest
*/
2020-01-02 18:45:41 +01:00
class TreeViewGenerator
{
2023-11-29 20:49:16 +01:00
public function __construct (
protected EntityURLGenerator $urlGenerator ,
protected EntityManagerInterface $em ,
protected TagAwareCacheInterface $cache ,
protected ElementCacheTagGenerator $tagGenerator ,
protected UserCacheKeyGenerator $keyGenerator ,
protected TranslatorInterface $translator ,
2024-03-03 19:57:31 +01:00
private readonly UrlGeneratorInterface $router ,
2023-11-29 20:49:16 +01:00
protected bool $rootNodeExpandedByDefault ,
protected bool $rootNodeEnabled ,
) {
2020-01-02 18:45:41 +01:00
}
2020-01-02 22:55:28 +01:00
/**
* Gets a TreeView list for the entities of the given class .
2024-02-25 00:48:15 +01:00
* The result is cached , if the full tree should be shown and no element should be selected .
2020-01-04 20:24:09 +01:00
*
2023-11-29 20:49:16 +01:00
* @ param string $class The class for which the treeView should be generated
* @ param AbstractStructuralDBElement | null $parent The root nodes in the tree should have this element as parent ( use null , if you want to get all entities )
* @ param string $mode The link type that will be generated for the hyperlink section of each node ( see EntityURLGenerator for possible values ) .
2020-03-15 13:56:31 +01:00
* Set to empty string , to disable href field .
2023-11-29 20:49:16 +01:00
* @ param AbstractDBElement | null $selectedElement The element that should be selected . If set to null , no element will be selected .
2020-01-04 20:24:09 +01:00
*
2020-08-21 21:36:22 +02:00
* @ return TreeViewNode [] an array of TreeViewNode [] elements of the root elements
2020-01-02 22:55:28 +01:00
*/
2023-11-29 20:49:16 +01:00
public function getTreeView (
string $class ,
? AbstractStructuralDBElement $parent = null ,
string $mode = 'list_parts' ,
? AbstractDBElement $selectedElement = null
2024-02-25 00:48:15 +01:00
) : array
{
//If we just want a part of a tree, don't cache it or select a specific element, don't cache it
if ( $parent instanceof AbstractStructuralDBElement || $selectedElement instanceof AbstractDBElement ) {
return $this -> getTreeViewUncached ( $class , $parent , $mode , $selectedElement );
}
$secure_class_name = $this -> tagGenerator -> getElementTypeCacheTag ( $class );
$key = 'sidebar_treeview_' . $this -> keyGenerator -> generateKey () . '_' . $secure_class_name ;
$key .= $mode ;
return $this -> cache -> get ( $key , function ( ItemInterface $item ) use ( $class , $parent , $mode , $selectedElement , $secure_class_name ) {
// Invalidate when groups, an element with the class or the user changes
$item -> tag ([ 'groups' , 'tree_treeview' , $this -> keyGenerator -> generateKey (), $secure_class_name ]);
return $this -> getTreeViewUncached ( $class , $parent , $mode , $selectedElement );
});
}
/**
* Gets a TreeView list for the entities of the given class .
*
* @ param string $class The class for which the treeView should be generated
* @ param AbstractStructuralDBElement | null $parent The root nodes in the tree should have this element as parent ( use null , if you want to get all entities )
* @ param string $mode The link type that will be generated for the hyperlink section of each node ( see EntityURLGenerator for possible values ) .
* Set to empty string , to disable href field .
* @ param AbstractDBElement | null $selectedElement The element that should be selected . If set to null , no element will be selected .
*
* @ return TreeViewNode [] an array of TreeViewNode [] elements of the root elements
*/
private function getTreeViewUncached (
string $class ,
? AbstractStructuralDBElement $parent = null ,
string $mode = 'list_parts' ,
? AbstractDBElement $selectedElement = null
2023-11-29 20:49:16 +01:00
) : array {
2020-01-02 22:55:28 +01:00
$head = [];
2020-10-03 13:56:30 +02:00
$href_type = $mode ;
2020-01-02 22:55:28 +01:00
//When we use the newEdit type, add the New Element node.
2020-10-03 13:56:30 +02:00
if ( 'newEdit' === $mode ) {
2020-01-02 22:55:28 +01:00
//Generate the url for the new node
2023-08-01 15:06:44 +02:00
//DO NOT try to create an object from the class, as this might be an proxy, which can not be easily initialized, so just pass the class_name directly
$href = $this -> urlGenerator -> createURL ( $class );
2020-01-02 22:55:28 +01:00
$new_node = new TreeViewNode ( $this -> translator -> trans ( 'entity.tree.new' ), $href );
//When the id of the selected element is null, then we have a new element, and we need to select "new" node
2023-06-11 14:55:06 +02:00
if ( ! $selectedElement instanceof AbstractDBElement || null === $selectedElement -> getID ()) {
2020-01-02 22:55:28 +01:00
$new_node -> setSelected ( true );
}
$head [] = $new_node ;
//Add spacing
$head [] = ( new TreeViewNode ( '' )) -> setDisabled ( true );
//Every other treeNode will be used for edit
$href_type = 'edit' ;
}
2020-10-03 13:56:30 +02:00
if ( $mode === 'list_parts_root' ) {
$href_type = 'list_parts' ;
}
2020-10-03 14:04:43 +02:00
if ( $mode === 'devices' ) {
2022-12-18 21:58:21 +01:00
$href_type = 'list_parts' ;
2020-10-03 14:04:43 +02:00
}
2020-01-02 18:45:41 +01:00
$generic = $this -> getGenericTree ( $class , $parent );
$treeIterator = new TreeViewNodeIterator ( $generic );
2022-08-14 19:32:53 +02:00
$recursiveIterator = new RecursiveIteratorIterator ( $treeIterator , RecursiveIteratorIterator :: SELF_FIRST );
2020-01-02 18:45:41 +01:00
foreach ( $recursiveIterator as $item ) {
2020-01-05 15:46:58 +01:00
/** @var TreeViewNode $item */
2023-06-11 14:55:06 +02:00
if ( $selectedElement instanceof AbstractDBElement && $item -> getId () === $selectedElement -> getID ()) {
2020-01-04 20:24:09 +01:00
$item -> setSelected ( true );
2020-01-02 18:45:41 +01:00
}
2023-06-11 18:59:07 +02:00
if ( $item -> getNodes () !== null && $item -> getNodes () !== []) {
2023-11-29 20:49:16 +01:00
$item -> addTag (( string ) count ( $item -> getNodes ()));
2020-01-02 18:45:41 +01:00
}
2023-06-11 18:59:07 +02:00
if ( $href_type !== '' && null !== $item -> getId ()) {
2023-12-05 21:55:20 +01:00
$entity = $this -> em -> find ( $class , $item -> getId ());
2020-01-02 18:45:41 +01:00
$item -> setHref ( $this -> urlGenerator -> getURL ( $entity , $href_type ));
}
2020-04-29 22:59:14 +02:00
//Translate text if text starts with $$
2023-05-27 23:58:28 +02:00
if ( str_starts_with ( $item -> getText (), '$$' )) {
2020-04-29 22:59:14 +02:00
$item -> setText ( $this -> translator -> trans ( substr ( $item -> getText (), 2 )));
}
2020-01-02 18:45:41 +01:00
}
2022-08-13 01:46:53 +02:00
if (( $mode === 'list_parts_root' || $mode === 'devices' ) && $this -> rootNodeEnabled ) {
2023-07-03 00:34:37 +02:00
//We show the root node as a link to the list of all parts
$show_all_parts_url = $this -> router -> generate ( 'parts_show_all' );
$root_node = new TreeViewNode ( $this -> entityClassToRootNodeString ( $class ), $show_all_parts_url , $generic );
2022-08-05 00:24:28 +02:00
$root_node -> setExpanded ( $this -> rootNodeExpandedByDefault );
2022-08-13 02:18:32 +02:00
$root_node -> setIcon ( $this -> entityClassToRootNodeIcon ( $class ));
2020-10-03 13:56:30 +02:00
$generic = [ $root_node ];
}
2020-01-02 22:55:28 +01:00
return array_merge ( $head , $generic );
2020-01-02 18:45:41 +01:00
}
2022-08-13 01:53:43 +02:00
protected function entityClassToRootNodeString ( string $class ) : string
{
2023-06-11 15:02:59 +02:00
return match ( $class ) {
Category :: class => $this -> translator -> trans ( 'category.labelp' ),
2023-09-04 22:57:40 +02:00
StorageLocation :: class => $this -> translator -> trans ( 'storelocation.labelp' ),
2023-06-11 15:02:59 +02:00
Footprint :: class => $this -> translator -> trans ( 'footprint.labelp' ),
Manufacturer :: class => $this -> translator -> trans ( 'manufacturer.labelp' ),
Supplier :: class => $this -> translator -> trans ( 'supplier.labelp' ),
Project :: class => $this -> translator -> trans ( 'project.labelp' ),
default => $this -> translator -> trans ( 'tree.root_node.text' ),
};
2022-08-13 01:53:43 +02:00
}
2022-08-13 02:18:32 +02:00
protected function entityClassToRootNodeIcon ( string $class ) : ? string
{
$icon = " fa-fw fa-treeview fa-solid " ;
2023-06-11 15:02:59 +02:00
return match ( $class ) {
2023-11-29 20:49:16 +01:00
Category :: class => $icon . 'fa-tags' ,
StorageLocation :: class => $icon . 'fa-cube' ,
Footprint :: class => $icon . 'fa-microchip' ,
Manufacturer :: class => $icon . 'fa-industry' ,
Supplier :: class => $icon . 'fa-truck' ,
Project :: class => $icon . 'fa-archive' ,
2023-06-11 15:02:59 +02:00
default => null ,
};
2022-08-13 02:18:32 +02:00
}
2020-01-02 18:45:41 +01:00
/**
* /**
* 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 .
*
2023-11-29 20:49:16 +01:00
* @ param string $class The class for which the tree should be generated
2024-12-28 22:31:04 +01:00
* @ phpstan - param class - string < AbstractNamedDBElement > $class
2023-11-29 20:49:16 +01:00
* @ param AbstractStructuralDBElement | null $parent the parent the root elements should have
2020-01-04 20:24:09 +01:00
*
2020-01-02 18:45:41 +01:00
* @ return TreeViewNode []
*/
2020-02-01 19:48:07 +01:00
public function getGenericTree ( string $class , ? AbstractStructuralDBElement $parent = null ) : array
2020-01-02 18:45:41 +01:00
{
2020-08-21 21:36:22 +02:00
if ( ! is_a ( $class , AbstractNamedDBElement :: class , true )) {
2022-08-14 19:32:53 +02:00
throw new InvalidArgumentException ( '$class must be a class string that implements StructuralDBElement or NamedDBElement!' );
2020-01-02 18:45:41 +01:00
}
2023-06-11 14:55:06 +02:00
if ( $parent instanceof AbstractStructuralDBElement && ! $parent instanceof $class ) {
2022-08-14 19:32:53 +02:00
throw new InvalidArgumentException ( '$parent must be of the type $class!' );
2020-01-02 18:45:41 +01:00
}
2024-12-28 22:31:04 +01:00
/** @var NamedDBElementRepository<AbstractNamedDBElement> $repo */
2020-01-02 18:45:41 +01:00
$repo = $this -> em -> getRepository ( $class );
2023-04-15 23:14:53 +02:00
//If we just want a part of a tree, don't cache it
2023-06-11 14:55:06 +02:00
if ( $parent instanceof AbstractStructuralDBElement ) {
2024-12-28 22:31:04 +01:00
return $repo -> getGenericNodeTree ( $parent ); //@phpstan-ignore-line PHPstan does not seem to recognize, that we have a StructuralDBElementRepository here, which have 1 argument
2020-01-02 18:45:41 +01:00
}
2023-11-29 20:49:16 +01:00
$secure_class_name = $this -> tagGenerator -> getElementTypeCacheTag ( $class );
2020-01-02 18:45:41 +01:00
$key = 'treeview_' . $this -> keyGenerator -> generateKey () . '_' . $secure_class_name ;
2020-01-05 15:46:58 +01:00
return $this -> cache -> get ( $key , function ( ItemInterface $item ) use ( $repo , $parent , $secure_class_name ) {
2023-04-15 23:14:53 +02:00
// Invalidate when groups, an element with the class or the user changes
2020-01-02 18:45:41 +01:00
$item -> tag ([ 'groups' , 'tree_treeview' , $this -> keyGenerator -> generateKey (), $secure_class_name ]);
2024-12-28 22:31:04 +01:00
return $repo -> getGenericNodeTree ( $parent ); //@phpstan-ignore-line
2020-01-02 18:45:41 +01:00
});
}
2020-01-04 20:24:09 +01:00
}