diff --git a/src/EntityListeners/TreeCacheInvalidationListener.php b/src/EntityListeners/TreeCacheInvalidationListener.php index 4cbcf8f8..eae7ce35 100644 --- a/src/EntityListeners/TreeCacheInvalidationListener.php +++ b/src/EntityListeners/TreeCacheInvalidationListener.php @@ -24,49 +24,52 @@ namespace App\EntityListeners; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractStructuralDBElement; -use App\Entity\LabelSystem\LabelProfile; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; -use App\Services\UserSystem\UserCacheKeyGenerator; -use Doctrine\ORM\Event\LifecycleEventArgs; +use App\Services\Cache\ElementCacheTagGenerator; +use App\Services\Cache\UserCacheKeyGenerator; +use Doctrine\ORM\Event\PostPersistEventArgs; +use Doctrine\ORM\Event\PostRemoveEventArgs; +use Doctrine\ORM\Event\PostUpdateEventArgs; use Doctrine\ORM\Mapping as ORM; -use function get_class; use Symfony\Contracts\Cache\TagAwareCacheInterface; class TreeCacheInvalidationListener { - public function __construct(protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator) + public function __construct( + protected TagAwareCacheInterface $cache, + protected UserCacheKeyGenerator $keyGenerator, + protected ElementCacheTagGenerator $tagGenerator + ) { } #[ORM\PostUpdate] #[ORM\PostPersist] #[ORM\PostRemove] - public function invalidate(AbstractDBElement $element, LifecycleEventArgs $event): void + public function invalidate(AbstractDBElement $element, PostUpdateEventArgs|PostPersistEventArgs|PostRemoveEventArgs $event): void { - //If an element was changed, then invalidate all cached trees with this element class - if ($element instanceof AbstractStructuralDBElement || $element instanceof LabelProfile) { - $secure_class_name = str_replace('\\', '_', $element::class); - $this->cache->invalidateTags([$secure_class_name]); + //For all changes, we invalidate the cache for all elements of this class + $tags = [$this->tagGenerator->getElementTypeCacheTag($element)]; - //Trigger a sidebar reload for all users (see SidebarTreeUpdater service) - if(!$element instanceof LabelProfile) { - $this->cache->invalidateTags(['sidebar_tree_update']); - } + + //For changes on structural elements, we also invalidate the sidebar tree + if ($element instanceof AbstractStructuralDBElement) { + $tags[] = 'sidebar_tree_update'; } - //If a user change, then invalidate all cached trees for him + //For user changes, we invalidate the cache for this user if ($element instanceof User) { - $secure_class_name = str_replace('\\', '_', $element::class); - $tag = $this->keyGenerator->generateKey($element); - $this->cache->invalidateTags([$tag, $secure_class_name]); + $tags[] = $this->keyGenerator->generateKey($element); } /* If any group change, then invalidate all cached trees. Users Permissions can be inherited from groups, so a change in any group can cause big permisssion changes for users. So to be sure, invalidate all trees */ if ($element instanceof Group) { - $tag = 'groups'; - $this->cache->invalidateTags([$tag]); + $tags[] = 'groups'; } + + //Invalidate the cache for the given tags + $this->cache->invalidateTags($tags); } } diff --git a/src/Services/Cache/ElementCacheTagGenerator.php b/src/Services/Cache/ElementCacheTagGenerator.php new file mode 100644 index 00000000..7873ba1b --- /dev/null +++ b/src/Services/Cache/ElementCacheTagGenerator.php @@ -0,0 +1,70 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\Cache; + +use Doctrine\Persistence\Proxy; + +/** + * The purpose of this class is to generate cache tags for elements. + * E.g. to easily invalidate all caches for a given element type. + */ +class ElementCacheTagGenerator +{ + private array $cache = []; + + public function __construct() + { + } + + /** + * Returns a cache tag for the given element type, which can be used to invalidate all caches for this element type. + * @param string|object $element + * @return string + */ + public function getElementTypeCacheTag(string|object $element): string + { + //Ensure that the given element is a class name + if (is_object($element)) { + $element = get_class($element); + } else { //And that the class exists + if (!class_exists($element)) { + throw new \InvalidArgumentException("The given class '$element' does not exist!"); + } + } + + //Check if the tag is already cached + if (isset($this->cache[$element])) { + return $this->cache[$element]; + } + + //If the element is a proxy, then get the real class name of the underlying object + if ($element instanceof Proxy || str_starts_with($element, 'Proxies\\')) { + $element = get_parent_class($element); + } + + //Replace all backslashes with underscores to prevent problems with the cache and save the result + $this->cache[$element] = str_replace('\\', '_', $element); + return $this->cache[$element]; + } +} \ No newline at end of file diff --git a/src/Services/UserSystem/UserCacheKeyGenerator.php b/src/Services/Cache/UserCacheKeyGenerator.php similarity index 98% rename from src/Services/UserSystem/UserCacheKeyGenerator.php rename to src/Services/Cache/UserCacheKeyGenerator.php index f8aec6a1..be1b7ad6 100644 --- a/src/Services/UserSystem/UserCacheKeyGenerator.php +++ b/src/Services/Cache/UserCacheKeyGenerator.php @@ -20,12 +20,12 @@ declare(strict_types=1); -namespace App\Services\UserSystem; +namespace App\Services\Cache; -use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Component\HttpFoundation\Request; use App\Entity\UserSystem\User; use Locale; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; /** diff --git a/src/Services/EDAIntegration/KiCADHelper.php b/src/Services/EDAIntegration/KiCADHelper.php index 59cbc07c..e5b2268a 100644 --- a/src/Services/EDAIntegration/KiCADHelper.php +++ b/src/Services/EDAIntegration/KiCADHelper.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace App\Services\EDAIntegration; use App\Entity\Parts\Category; +use App\EntityListeners\TreeCacheInvalidationListener; +use App\Services\Cache\ElementCacheTagGenerator; use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\EntityManagerInterface; use Symfony\Contracts\Cache\ItemInterface; @@ -36,6 +38,7 @@ class KiCADHelper private readonly NodesListBuilder $nodesListBuilder, private readonly TagAwareCacheInterface $kicadCache, private readonly EntityManagerInterface $em, + private readonly ElementCacheTagGenerator $tagGenerator, ) { @@ -52,7 +55,7 @@ class KiCADHelper { return $this->kicadCache->get('kicad_categories', function (ItemInterface $item) { //Invalidate the cache on category changes - $secure_class_name = str_replace('\\', '_', Category::class); + $secure_class_name = $this->tagGenerator->getElementTypeCacheTag(Category::class); $item->tag($secure_class_name); $categories = $this->nodesListBuilder->typeToNodesList(Category::class); diff --git a/src/Services/LabelSystem/LabelProfileDropdownHelper.php b/src/Services/LabelSystem/LabelProfileDropdownHelper.php index d7f1120d..ac5020a2 100644 --- a/src/Services/LabelSystem/LabelProfileDropdownHelper.php +++ b/src/Services/LabelSystem/LabelProfileDropdownHelper.php @@ -44,15 +44,20 @@ namespace App\Services\LabelSystem; use App\Entity\LabelSystem\LabelProfile; use App\Entity\LabelSystem\LabelSupportedElement; use App\Repository\LabelProfileRepository; -use App\Services\UserSystem\UserCacheKeyGenerator; +use App\Services\Cache\ElementCacheTagGenerator; +use App\Services\Cache\UserCacheKeyGenerator; use Doctrine\ORM\EntityManagerInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; final class LabelProfileDropdownHelper { - public function __construct(private readonly TagAwareCacheInterface $cache, private readonly EntityManagerInterface $entityManager, private readonly UserCacheKeyGenerator $keyGenerator) - { + public function __construct( + private readonly TagAwareCacheInterface $cache, + private readonly EntityManagerInterface $entityManager, + private readonly UserCacheKeyGenerator $keyGenerator, + private readonly ElementCacheTagGenerator $tagGenerator, + ) { } /** @@ -67,7 +72,7 @@ final class LabelProfileDropdownHelper $type = LabelSupportedElement::from($type); } - $secure_class_name = str_replace('\\', '_', LabelProfile::class); + $secure_class_name = $this->tagGenerator->getElementTypeCacheTag(LabelProfile::class); $key = 'profile_dropdown_'.$this->keyGenerator->generateKey().'_'.$secure_class_name.'_'.$type->value; /** @var LabelProfileRepository $repo */ diff --git a/src/Services/Trees/NodesListBuilder.php b/src/Services/Trees/NodesListBuilder.php index 66299185..ea0ac4e1 100644 --- a/src/Services/Trees/NodesListBuilder.php +++ b/src/Services/Trees/NodesListBuilder.php @@ -22,14 +22,13 @@ declare(strict_types=1); namespace App\Services\Trees; -use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Base\AbstractDBElement; -use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; use App\Repository\AttachmentContainingDBElementRepository; use App\Repository\DBElementRepository; use App\Repository\StructuralDBElementRepository; -use App\Services\UserSystem\UserCacheKeyGenerator; +use App\Services\Cache\ElementCacheTagGenerator; +use App\Services\Cache\UserCacheKeyGenerator; use Doctrine\ORM\EntityManagerInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; @@ -40,8 +39,12 @@ use Symfony\Contracts\Cache\TagAwareCacheInterface; */ class NodesListBuilder { - public function __construct(protected EntityManagerInterface $em, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator) - { + public function __construct( + protected EntityManagerInterface $em, + protected TagAwareCacheInterface $cache, + protected UserCacheKeyGenerator $keyGenerator, + protected ElementCacheTagGenerator $tagGenerator, + ) { } /** @@ -50,9 +53,9 @@ class NodesListBuilder * * @template T of AbstractDBElement * - * @param string $class_name the class name of the entity you want to retrieve + * @param string $class_name the class name of the entity you want to retrieve * @phpstan-param class-string $class_name - * @param AbstractStructuralDBElement|null $parent This entity will be used as root element. Set to null, to use global root + * @param AbstractStructuralDBElement|null $parent This entity will be used as root element. Set to null, to use global root * * @return AbstractDBElement[] a flattened list containing the tree elements * @phpstan-return list @@ -86,7 +89,7 @@ class NodesListBuilder { $parent_id = $parent instanceof AbstractStructuralDBElement ? $parent->getID() : '0'; // Backslashes are not allowed in cache keys - $secure_class_name = str_replace('\\', '_', $class_name); + $secure_class_name = $this->tagGenerator->getElementTypeCacheTag($class_name); $key = 'list_'.$this->keyGenerator->generateKey().'_'.$secure_class_name.$parent_id; return $this->cache->get($key, function (ItemInterface $item) use ($class_name, $parent, $secure_class_name) { @@ -105,7 +108,7 @@ class NodesListBuilder * The value is cached for performance reasons. * * @template T of AbstractStructuralDBElement - * @param T $element + * @param T $element * @return AbstractStructuralDBElement[] * * @phpstan-return list diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index 3e5763ca..18571306 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -22,9 +22,7 @@ declare(strict_types=1); namespace App\Services\Trees; -use Symfony\Bundle\SecurityBundle\Security; use App\Entity\Attachments\AttachmentType; -use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -34,10 +32,12 @@ use App\Entity\Parts\Part; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; use App\Entity\PriceInformations\Currency; +use App\Entity\ProjectSystem\Project; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\User; use App\Helpers\Trees\TreeViewNode; -use App\Services\UserSystem\UserCacheKeyGenerator; +use App\Services\Cache\UserCacheKeyGenerator; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\TagAwareCacheInterface; diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 056572a5..e158ca69 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -25,17 +25,18 @@ namespace App\Services\Trees; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; -use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\StorageLocation; use App\Entity\Parts\Supplier; +use App\Entity\ProjectSystem\Project; use App\Helpers\Trees\TreeViewNode; use App\Helpers\Trees\TreeViewNodeIterator; use App\Repository\StructuralDBElementRepository; +use App\Services\Cache\ElementCacheTagGenerator; +use App\Services\Cache\UserCacheKeyGenerator; use App\Services\EntityURLGenerator; -use App\Services\UserSystem\UserCacheKeyGenerator; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use RecursiveIteratorIterator; @@ -51,25 +52,37 @@ use function count; */ class TreeViewGenerator { - public function __construct(protected EntityURLGenerator $urlGenerator, protected EntityManagerInterface $em, protected TagAwareCacheInterface $cache, - protected UserCacheKeyGenerator $keyGenerator, protected TranslatorInterface $translator, private UrlGeneratorInterface $router, - protected bool $rootNodeExpandedByDefault, protected bool $rootNodeEnabled) - { + public function __construct( + protected EntityURLGenerator $urlGenerator, + protected EntityManagerInterface $em, + protected TagAwareCacheInterface $cache, + protected ElementCacheTagGenerator $tagGenerator, + protected UserCacheKeyGenerator $keyGenerator, + protected TranslatorInterface $translator, + private UrlGeneratorInterface $router, + protected bool $rootNodeExpandedByDefault, + protected bool $rootNodeEnabled, + + ) { } /** * 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). + * @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. + * @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 */ - public function getTreeView(string $class, ?AbstractStructuralDBElement $parent = null, string $mode = 'list_parts', ?AbstractDBElement $selectedElement = null): array - { + public function getTreeView( + string $class, + ?AbstractStructuralDBElement $parent = null, + string $mode = 'list_parts', + ?AbstractDBElement $selectedElement = null + ): array { $head = []; $href_type = $mode; @@ -110,7 +123,7 @@ class TreeViewGenerator } if ($item->getNodes() !== null && $item->getNodes() !== []) { - $item->addTag((string) count($item->getNodes())); + $item->addTag((string)count($item->getNodes())); } if ($href_type !== '' && null !== $item->getId()) { @@ -155,12 +168,12 @@ class TreeViewGenerator { $icon = "fa-fw fa-treeview fa-solid "; return match ($class) { - 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', + 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', default => null, }; } @@ -170,8 +183,8 @@ class TreeViewGenerator * 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 AbstractStructuralDBElement|null $parent the parent the root elements should have + * @param string $class The class for which the tree should be generated + * @param AbstractStructuralDBElement|null $parent the parent the root elements should have * * @return TreeViewNode[] */ @@ -192,13 +205,12 @@ class TreeViewGenerator return $repo->getGenericNodeTree($parent); } - $secure_class_name = str_replace('\\', '_', $class); + $secure_class_name = $this->tagGenerator->getElementTypeCacheTag($class); $key = 'treeview_'.$this->keyGenerator->generateKey().'_'.$secure_class_name; return $this->cache->get($key, function (ItemInterface $item) use ($repo, $parent, $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 $repo->getGenericNodeTree($parent); }); }