From f75f17c92b918d4b9f13583b1913465dbd454a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 20 Aug 2019 12:34:43 +0200 Subject: [PATCH] Cache the tree nodes list generated for StructuralEntityType. --- config/packages/cache.yaml | 9 +- config/services.yaml | 6 ++ src/Entity/Base/StructuralDBElement.php | 2 + .../TreeCacheInvalidationListener.php | 84 +++++++++++++++++++ src/Form/Type/StructuralEntityType.php | 13 ++- src/Services/TreeBuilder.php | 46 +++++++++- 6 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 src/EntityListeners/TreeCacheInvalidationListener.php diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml index 93e620ef..a3e47f71 100644 --- a/config/packages/cache.yaml +++ b/config/packages/cache.yaml @@ -2,7 +2,7 @@ framework: cache: # Put the unique name of your app here: the prefix seed # is used to compute stable namespaces for cache keys. - #prefix_seed: your_vendor_name/app_name + prefix_seed: Part-DB/Part-DB # The app cache caches to the filesystem by default. # Other options include: @@ -15,5 +15,8 @@ framework: #app: cache.adapter.apcu # Namespaced pools use the above "app" backend by default - #pools: - #my.dedicated.cache: ~ + pools: + # Here all things related to cache the tree structures + tree.cache: + adapter: cache.app + tags: true diff --git a/config/services.yaml b/config/services.yaml index 47890851..466d1f45 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -37,6 +37,12 @@ services: tags: - { name: "doctrine.orm.entity_listener" } + tree_invalidation_listener: + class: App\EntityListeners\TreeCacheInvalidationListener + tags: + - { name: doctrine.orm.entity_listener } + + App\Command\UpdateExchangeRatesCommand: arguments: $base_current: '%default_currency%' diff --git a/src/Entity/Base/StructuralDBElement.php b/src/Entity/Base/StructuralDBElement.php index db749f7e..a8c87b12 100644 --- a/src/Entity/Base/StructuralDBElement.php +++ b/src/Entity/Base/StructuralDBElement.php @@ -72,6 +72,8 @@ use Symfony\Component\Serializer\Annotation\Groups; * * @ORM\MappedSuperclass(repositoryClass="App\Repository\StructuralDBElementRepository") * + * @ORM\EntityListeners({"App\Security\EntityListeners\ElementPermissionListener", "App\EntityListeners\TreeCacheInvalidationListener"}) + * * @UniqueEntity(fields={"name", "parent"}, ignoreNull=false, message="structural.entity.unique_name") */ abstract class StructuralDBElement extends AttachmentContainingDBElement diff --git a/src/EntityListeners/TreeCacheInvalidationListener.php b/src/EntityListeners/TreeCacheInvalidationListener.php new file mode 100644 index 00000000..e18e936d --- /dev/null +++ b/src/EntityListeners/TreeCacheInvalidationListener.php @@ -0,0 +1,84 @@ +cache = $treeCache; + } + + /** + * @ORM\PostUpdate() + * @ORM\PostPersist() + * @ORM\PostRemove() + * + * @param DBElement $element + * @param LifecycleEventArgs $event + */ + public function invalidate(DBElement $element, LifecycleEventArgs $event) + { + //If an element was changed, then invalidate all cached trees with this element class + if ($element instanceof StructuralDBElement) { + $secure_class_name = str_replace("\\", '_', get_class($element)); + $this->cache->invalidateTags([$secure_class_name]); + } + + //If a user change, then invalidate all cached trees for him + if ($element instanceof User) { + $tag = "user_" . $element->getUsername(); + $this->cache->invalidateTags([$tag]); + } + + /* 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]); + } + + } +} \ No newline at end of file diff --git a/src/Form/Type/StructuralEntityType.php b/src/Form/Type/StructuralEntityType.php index a91d8170..c2b26517 100644 --- a/src/Form/Type/StructuralEntityType.php +++ b/src/Form/Type/StructuralEntityType.php @@ -35,6 +35,7 @@ namespace App\Form\Type; use App\Entity\Base\StructuralDBElement; use App\Entity\Parts\Storelocation; use App\Repository\StructuralDBElementRepository; +use App\Services\TreeBuilder; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\ChoiceList\Loader\CallbackChoiceLoader; @@ -54,10 +55,13 @@ class StructuralEntityType extends AbstractType { protected $em; protected $options; + /** @var TreeBuilder */ + protected $builder; - public function __construct(EntityManagerInterface $em) + public function __construct(EntityManagerInterface $em, TreeBuilder $builder) { $this->em = $em; + $this->builder = $builder; } public function configureOptions(OptionsResolver $resolver) @@ -122,9 +126,12 @@ class StructuralEntityType extends AbstractType { $this->options = $options; + $choices = $this->builder->typeToNodesList($options['class'], null); + + /** @var StructuralDBElementRepository $repo */ - $repo = $this->em->getRepository($options['class']); - $choices = $repo->toNodesList(null); + /*$repo = $this->em->getRepository($options['class']); + $choices = $repo->toNodesList(null); */ return $choices; } diff --git a/src/Services/TreeBuilder.php b/src/Services/TreeBuilder.php index fb3d51aa..dadf2c24 100644 --- a/src/Services/TreeBuilder.php +++ b/src/Services/TreeBuilder.php @@ -37,7 +37,16 @@ use App\Entity\Base\StructuralDBElement; use App\Helpers\TreeViewNode; use App\Repository\StructuralDBElementRepository; use Doctrine\ORM\EntityManagerInterface; +use Psr\Cache\CacheItemPoolInterface; use Symfony\Bundle\MakerBundle\Str; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; +use Symfony\Component\Security\Core\Security; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -50,12 +59,17 @@ class TreeBuilder protected $url_generator; protected $em; protected $translator; + protected $cache; + protected $security; - public function __construct(EntityURLGenerator $URLGenerator, EntityManagerInterface $em, TranslatorInterface $translator) + public function __construct(EntityURLGenerator $URLGenerator, EntityManagerInterface $em, + TranslatorInterface $translator, TagAwareCacheInterface $treeCache, Security $security) { $this->url_generator = $URLGenerator; $this->em = $em; $this->translator = $translator; + $this->security = $security; + $this->cache = $treeCache; } /** @@ -149,4 +163,34 @@ class TreeBuilder return $array; } + + /** + * Gets a flattened hierachical tree. Useful for generating option lists. + * In difference to the Repository Function, the results here are cached. + * @param string $class_name The class name of the entity you want to retrieve. + * @param StructuralDBElement|null $parent This entity will be used as root element. Set to null, to use global root + * @return StructuralDBElement[] A flattened list containing the tree elements. + * @throws \Psr\Cache\InvalidArgumentException + */ + public function typeToNodesList(string $class_name, ?StructuralDBElement $parent = null): array + { + $username = $this->security->getUser()->getUsername(); + $parent_id = $parent != null ? $parent->getID() : "0"; + // Backslashes are not allowed in cache keys + $secure_class_name = str_replace("\\", '_', $class_name); + $key = "list_" . $username . "_" . $secure_class_name . $parent_id; + + $ret = $this->cache->get($key, function (ItemInterface $item) use ($class_name, $parent, $secure_class_name, $username) { + // Invalidate when groups, a element with the class or the user changes + $item->tag(['groups', 'tree_list', 'user_' . $username, $secure_class_name]); + /** + * @var $repo StructuralDBElementRepository + */ + $repo = $this->em->getRepository($class_name); + + return $repo->toNodesList($parent); + }); + + return $ret; + } }