vendor/heimrichhannot/contao-utils-bundle/src/Cache/DatabaseTreeCache.php line 77

Open in your IDE?
  1. <?php
  2. /*
  3.  * Copyright (c) 2021 Heimrich & Hannot GmbH
  4.  *
  5.  * @license LGPL-3.0-or-later
  6.  */
  7. namespace HeimrichHannot\UtilsBundle\Cache;
  8. use Contao\CoreBundle\Framework\ContaoFrameworkInterface;
  9. use Contao\Database;
  10. use Contao\System;
  11. use HeimrichHannot\UtilsBundle\Container\ContainerUtil;
  12. use HeimrichHannot\UtilsBundle\Model\ModelUtil;
  13. use Symfony\Component\Filesystem\Filesystem;
  14. use Symfony\Component\HttpFoundation\RequestStack;
  15. class DatabaseTreeCache
  16. {
  17.     /**
  18.      * Cache tree.
  19.      *
  20.      * @var array
  21.      */
  22.     protected static $cache = [];
  23.     /**
  24.      * @var ContaoFrameworkInterface
  25.      */
  26.     protected $framework;
  27.     /**
  28.      * @var Filesystem
  29.      */
  30.     protected $filesystem;
  31.     /**
  32.      * @var ModelUtil
  33.      */
  34.     protected $modelUtil;
  35.     /**
  36.      * @var Database
  37.      */
  38.     protected $database;
  39.     /**
  40.      * Tree cache directory.
  41.      *
  42.      * @var string
  43.      */
  44.     protected $cacheDir;
  45.     /**
  46.      * @var ContainerUtil
  47.      */
  48.     protected $containerUtil;
  49.     /**
  50.      * @var RequestStack
  51.      */
  52.     protected $requestStack;
  53.     public function __construct(ContaoFrameworkInterface $frameworkFilesystem $filesystemModelUtil $modelUtilContainerUtil $containerUtilRequestStack $requestStack)
  54.     {
  55.         $this->framework $framework;
  56.         $this->filesystem $filesystem;
  57.         $this->modelUtil $modelUtil;
  58.         $this->database $this->framework->createInstance(Database::class);
  59.         $this->cacheDir \Contao\System::getContainer()->getParameter('kernel.cache_dir').'/tree_cache';
  60.         $this->containerUtil $containerUtil;
  61.         $this->requestStack $requestStack;
  62.     }
  63.     /**
  64.      * Generate tree cache.
  65.      */
  66.     public function loadDataContainer($table)
  67.     {
  68.         if (!$this->database->tableExists($table)) {
  69.             return;
  70.         }
  71.         if (!isset($GLOBALS['TL_DCA'][$table]) || !isset($GLOBALS['TL_DCA'][$table]['config']['treeCache']) || !\is_array($GLOBALS['TL_DCA'][$table]['config']['treeCache'])) {
  72.             return;
  73.         }
  74.         if ($this->containerUtil->isInstall() || !$this->requestStack->getCurrentRequest()) {
  75.             return;
  76.         }
  77.         if (!$this->isCompleteInstallation($table)) {
  78.             return;
  79.         }
  80.         $configurations $GLOBALS['TL_DCA'][$table]['config']['treeCache'];
  81.         foreach ($configurations as $key => $config) {
  82.             $this->addConfigToTreeCache($table$key$config);
  83.         }
  84.     }
  85.     public function addConfigToTreeCache(string $tablestring $key, array $config = [])
  86.     {
  87.         $filename $table.'_'.$key.'.php';
  88.         if (file_exists($this->cacheDir.'/'.$filename)) {
  89.             return;
  90.         }
  91.         if (null === ($roots $this->modelUtil->findModelInstancesBy($table$config['columns'] ?? [], $config['values'] ?? [], $config['options']))) {
  92.             return;
  93.         }
  94.         $tree $this->generateCacheTree($table$roots->fetchEach($key), $key$config);
  95.         $this->filesystem->dumpFile(
  96.             $this->cacheDir.'/'.$filename,
  97.             sprintf("<?php\n\nreturn %s;\n"var_export($treetrue))
  98.         );
  99.     }
  100.     /**
  101.      * Get all child records for given parent entities.
  102.      *
  103.      * @param string $table     The database table
  104.      * @param array  $ids       The parent entity ids
  105.      * @param int    $maxLevels The max stop level
  106.      * @param string Custom index key (default: primary key from model)
  107.      * @param array $children Internal children return array
  108.      * @param int   $level    Internal depth attribute
  109.      *
  110.      * @return array An array containing all children for given parent entities
  111.      */
  112.     public function getChildRecords(string $table, array $ids = [], $maxLevels nullstring $key 'id', array $children = [], int $level 0): array
  113.     {
  114.         if (null === ($tree $this->getTreeCache($table$key))) {
  115.             return $this->database->getChildRecords($ids$table);
  116.         }
  117.         foreach ($ids as $i => $id) {
  118.             if (!isset($tree[$id]) || !\is_array($tree[$id])) {
  119.                 continue;
  120.             }
  121.             $children array_merge($children$tree[$id]);
  122.             if (=== $maxLevels) {
  123.                 continue;
  124.             }
  125.             if ($maxLevels && $level $maxLevels) {
  126.                 return [];
  127.             }
  128.             if (!empty($nested self::getChildRecords($table$tree[$id], $maxLevels$key$children, ++$level))) {
  129.                 $children $nested;
  130.             } else {
  131.                 $depth 0;
  132.             }
  133.         }
  134.         return $children;
  135.     }
  136.     /**
  137.      * Get all parent records for given child entity.
  138.      *
  139.      * @param string $table     The database table
  140.      * @param int    $id        The current entity id
  141.      * @param int    $maxLevels The max stop level
  142.      * @param string Custom index key (default: primary key from model)
  143.      * @param array $parents Internal children return array
  144.      * @param int   $level   Internal depth attribute
  145.      *
  146.      * @return array An array containing all children for given parent entities
  147.      */
  148.     public function getParentRecords(string $tableint $id$maxLevels nullstring $key 'id', array $parents = [], int $level 0): array
  149.     {
  150.         if (null === ($tree $this->getTreeCache($table$key))) {
  151.             return $this->database->getParentRecords($id$table);
  152.         }
  153.         if (isset($tree[$id]) && === $level) {
  154.             $parents[] = $id;
  155.         }
  156.         foreach ($tree as $pid => $ids) {
  157.             if (!\in_array($id$ids)) {
  158.                 continue;
  159.             }
  160.             $parents[] = $pid;
  161.             if (=== $maxLevels) {
  162.                 continue;
  163.             }
  164.             if ($maxLevels && $level $maxLevels) {
  165.                 return [];
  166.             }
  167.             if (!empty($nested self::getParentRecords($table$pid$maxLevels$key$parents, ++$level))) {
  168.                 $parents $nested;
  169.             } else {
  170.                 $level 0;
  171.             }
  172.         }
  173.         return $parents;
  174.     }
  175.     /**
  176.      * Get the tree cache for a given table and key.
  177.      *
  178.      * @param string $table The database table
  179.      * @param string Custom index key (default: primary key from model)
  180.      */
  181.     public function getTreeCache($table$key): ?array
  182.     {
  183.         $filename $table.'_'.$key.'.php';
  184.         if (file_exists($this->cacheDir.'/'.$filename)) {
  185.             self::$cache[$table.'_'.$key] = (include $this->cacheDir.'/'.$filename);
  186.         }
  187.         return self::$cache[$table.'_'.$key] ?? null;
  188.     }
  189.     /**
  190.      * Generate the flat cache tree.
  191.      *
  192.      * @param string $table  The database table
  193.      * @param string $key    Custom index key (default: primary key from model)
  194.      * @param array  $ids    Root identifiers (parent ids)
  195.      * @param array  $config Tree config
  196.      * @param array  $return Internal return array
  197.      *
  198.      * @return array The flat cache tree
  199.      */
  200.     public function generateCacheTree(string $table, array $ids = [], string $key 'id', array $config = [], $return = []): array
  201.     {
  202.         foreach ($ids as $id) {
  203.             if (null === ($children $this->modelUtil->findModelInstancesBy($table, [$table.'.pid = ?'], $id$config['options']))) {
  204.                 $return[$id] = [];
  205.                 continue;
  206.             }
  207.             while ($children->next()) {
  208.                 $return[$children->pid][$children->{$key}] = $children->{$key};
  209.                 $return $this->generateCacheTree($table, [$children->{$key}], $key$config$return);
  210.             }
  211.         }
  212.         return $return;
  213.     }
  214.     /**
  215.      * Generate all cache trees.
  216.      *
  217.      * @param $cacheDir
  218.      */
  219.     public function generateAllCacheTree($cacheDir)
  220.     {
  221.         $this->cacheDir $cacheDir;
  222.         $tables $this->database->listTables();
  223.         foreach ($tables as $table) {
  224.             // trigger loadDataContainer TL_HOOK
  225.             System::getContainer()->get('huh.utils.dca')->loadDc($table);
  226.         }
  227.     }
  228.     /**
  229.      * Register a dca to the tree cache.
  230.      *
  231.      * @param string $table   (The dca table)
  232.      * @param array  $columns Parent sql filter columns (e.g. `tl_page.type`)
  233.      * @param array  $values  Parent sql filter values (e.g. `root` for `tl_page.type`)
  234.      * @param array  $options SQL Options for sorting
  235.      * @param string Custom index key (default: primary key from model)
  236.      *
  237.      * @return bool Acknowledge state if register succeeded
  238.      */
  239.     public function registerDcaToCacheTree(string $table, array $columns = [], array $values = [], array $options = [], string $key 'id')
  240.     {
  241.         System::getContainer()->get('huh.utils.dca')->loadDc($table);
  242.         if (!isset($GLOBALS['TL_DCA'][$table])) {
  243.             return false;
  244.         }
  245.         $GLOBALS['TL_DCA'][$table]['config']['treeCache'][$key] = [
  246.             'columns' => $columns,
  247.             'values' => $values,
  248.             'options' => $options,
  249.             'key' => $key,
  250.         ];
  251.         $GLOBALS['TL_DCA'][$table]['config']['ondelete_callback']['huh.utils.cache.database_tree'] = ['huh.utils.cache.database_tree''purgeCacheTree'];
  252.         $GLOBALS['TL_DCA'][$table]['config']['oncut_callback']['huh.utils.cache.database_tree'] = ['huh.utils.cache.database_tree''purgeCacheTree'];
  253.         $GLOBALS['TL_DCA'][$table]['config']['onsubmit_callback']['huh.utils.cache.database_tree'] = ['huh.utils.cache.database_tree''purgeCacheTree'];
  254.         $GLOBALS['TL_DCA'][$table]['config']['onrestore_callback']['huh.utils.cache.database_tree'] = ['huh.utils.cache.database_tree''purgeCacheTree'];
  255.         return true;
  256.     }
  257.     /**
  258.      * Purge the tree cache completely in order to take table relations into consideration.
  259.      */
  260.     public function purgeCacheTree()
  261.     {
  262.         $this->filesystem->remove($this->cacheDir);
  263.     }
  264.     private function isCompleteInstallation($table)
  265.     {
  266.         try {
  267.             $this->modelUtil->findOneModelInstanceBy($table, [], []);
  268.         } catch (\Exception $e) {
  269.             return false;
  270.         }
  271.         return true;
  272.     }
  273. }