From a3ebd6c9a4e919b8bed1cecb8d5aff3a0bf233eb Mon Sep 17 00:00:00 2001
From: Yassine Doghri <yassine@doghri.fr>
Date: Fri, 14 Oct 2022 15:29:01 +0000
Subject: [PATCH] refactor(view-components): use CI4's View Decorators to
 render components

---
 app/Common.php                                |   2 +-
 app/Config/Services.php                       |  21 ---
 app/Config/View.php                           |   3 +-
 app/Libraries/View.php                        | 142 ------------------
 .../ViewComponents/ComponentRenderer.php      |  17 +--
 app/Libraries/ViewComponents/Decorator.php    |  38 +++++
 6 files changed, 42 insertions(+), 181 deletions(-)
 delete mode 100644 app/Libraries/View.php
 create mode 100644 app/Libraries/ViewComponents/Decorator.php

diff --git a/app/Common.php b/app/Common.php
index 6ae40b6565..1f3b17c2ab 100644
--- a/app/Common.php
+++ b/app/Common.php
@@ -2,8 +2,8 @@
 
 declare(strict_types=1);
 
-use App\Libraries\View;
 use Config\Services;
+use Config\View;
 use ViewThemes\Theme;
 
 /**
diff --git a/app/Config/Services.php b/app/Config/Services.php
index 88da0c416a..3fa2e09245 100644
--- a/app/Config/Services.php
+++ b/app/Config/Services.php
@@ -7,13 +7,10 @@ namespace Config;
 use App\Libraries\Breadcrumb;
 use App\Libraries\Negotiate;
 use App\Libraries\Router;
-use App\Libraries\View;
 use CodeIgniter\Config\BaseService;
 use CodeIgniter\HTTP\Request;
 use CodeIgniter\HTTP\RequestInterface;
 use CodeIgniter\Router\RouteCollectionInterface;
-use Config\Services as AppServices;
-use Config\View as ViewConfig;
 
 /**
  * Services Configuration file.
@@ -48,24 +45,6 @@ class Services extends BaseService
         return new Router($routes, $request);
     }
 
-    /**
-     * The Renderer class is the class that actually displays a file to the user. The default View class within
-     * CodeIgniter is intentionally simple, but this service could easily be replaced by a template engine if the user
-     * needed to.
-     */
-    public static function renderer(?string $viewPath = null, ?ViewConfig $config = null, bool $getShared = true): View
-    {
-        if ($getShared) {
-            return static::getSharedInstance('renderer', $viewPath, $config);
-        }
-
-        $viewPath = $viewPath ?: config('Paths')
-            ->viewDirectory;
-        $config = $config ?? config('View');
-
-        return new View($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger());
-    }
-
     /**
      * The Negotiate class provides the content negotiation features for working the request to determine correct
      * language, encoding, charset, and more.
diff --git a/app/Config/View.php b/app/Config/View.php
index 212f28c917..104cdd8d86 100644
--- a/app/Config/View.php
+++ b/app/Config/View.php
@@ -6,6 +6,7 @@ namespace Config;
 
 use CodeIgniter\Config\View as BaseView;
 use CodeIgniter\View\ViewDecoratorInterface;
+use ViewComponents\Decorator;
 
 class View extends BaseView
 {
@@ -46,5 +47,5 @@ class View extends BaseView
      *
      * @var class-string<ViewDecoratorInterface>[]
      */
-    public array $decorators = [];
+    public array $decorators = [Decorator::class];
 }
diff --git a/app/Libraries/View.php b/app/Libraries/View.php
deleted file mode 100644
index 1fbb09ecec..0000000000
--- a/app/Libraries/View.php
+++ /dev/null
@@ -1,142 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace App\Libraries;
-
-use CodeIgniter\Debug\Toolbar\Collectors\Views;
-use CodeIgniter\Filters\DebugToolbar;
-use CodeIgniter\View\Exceptions\ViewException;
-use CodeIgniter\View\View as CodeIgniterView;
-use Config\Toolbar;
-
-class View extends CodeIgniterView
-{
-    /**
-     * Builds the output based upon a file name and any
-     * data that has already been set.
-     *
-     * Valid $options:
-     *  - cache      Number of seconds to cache for
-     *  - cache_name Name to use for cache
-     *
-     * @param string     $view     File name of the view source
-     * @param array<string, mixed>|null $options  Reserved for 3rd-party uses since
-     *                             it might be needed to pass additional info
-     *                             to other template engines.
-     * @param bool|null  $saveData If true, saves data for subsequent calls,
-     *                             if false, cleans the data after displaying,
-     *                             if null, uses the config setting.
-     */
-    public function render(string $view, ?array $options = null, ?bool $saveData = null): string
-    {
-        $this->renderVars['start'] = microtime(true);
-
-        // Store the results here so even if
-        // multiple views are called in a view, it won't
-        // clean it unless we mean it to.
-        $saveData = $saveData ?? $this->saveData;
-        $fileExt = pathinfo($view, PATHINFO_EXTENSION);
-        $realPath = $fileExt === '' ? $view . '.php' : $view; // allow Views as .html, .tpl, etc (from CI3)
-        $this->renderVars['view'] = $realPath;
-        $this->renderVars['options'] = $options ?? [];
-
-        // Was it cached?
-        if (isset($this->renderVars['options']['cache'])) {
-            $cacheName = $this->renderVars['options']['cache_name'] ?? str_replace(
-                '.php',
-                '',
-                $this->renderVars['view']
-            );
-            $cacheName = str_replace(['\\', '/'], '', $cacheName);
-
-            $this->renderVars['cacheName'] = $cacheName;
-
-            if ($output = cache($this->renderVars['cacheName'])) {
-                $this->logPerformance($this->renderVars['start'], microtime(true), $this->renderVars['view']);
-
-                return $output;
-            }
-        }
-
-        $this->renderVars['file'] = $this->viewPath . $this->renderVars['view'];
-
-        if (! is_file($this->renderVars['file'])) {
-            $this->renderVars['file'] = $this->loader->locateFile(
-                $this->renderVars['view'],
-                'Views',
-                $fileExt === '' ? 'php' : $fileExt
-            );
-        }
-
-        // locateFile will return an empty string if the file cannot be found.
-        if ($this->renderVars['file'] === '') {
-            throw ViewException::forInvalidFile($this->renderVars['view']);
-        }
-
-        // Make our view data available to the view.
-        $this->tempData = $this->tempData ?? $this->data;
-
-        if ($saveData) {
-            $this->data = $this->tempData;
-        }
-
-        // Save current vars
-        $renderVars = $this->renderVars;
-
-        $output = (function (): string {
-            extract($this->tempData);
-            ob_start();
-            include $this->renderVars['file'];
-
-            return ob_get_clean() ?: '';
-        })();
-
-        // Get back current vars
-        $this->renderVars = $renderVars;
-
-        // When using layouts, the data has already been stored
-        // in $this->sections, and no other valid output
-        // is allowed in $output so we'll overwrite it.
-        if ($this->layout !== null && $this->sectionStack === []) {
-            $layoutView = $this->layout;
-            $this->layout = null;
-            // Save current vars
-            $renderVars = $this->renderVars;
-            $output = $this->render($layoutView, $options, $saveData);
-            // Get back current vars
-            $this->renderVars = $renderVars;
-        }
-
-        $output = service('components')
-            ->setCurrentView($this->renderVars['file'])
-            ->render($output);
-
-        $this->logPerformance($this->renderVars['start'], microtime(true), $this->renderVars['view']);
-
-        if (($this->debug && (! isset($options['debug']) || $options['debug'] === true))
-            && in_array(DebugToolbar::class, service('filters')->getFiltersClass()['after'], true)
-        ) {
-            $toolbarCollectors = config(Toolbar::class)->collectors;
-
-            if (in_array(Views::class, $toolbarCollectors, true)) {
-                // Clean up our path names to make them a little cleaner
-                $this->renderVars['file'] = clean_path($this->renderVars['file']);
-                $this->renderVars['file'] = ++$this->viewsCount . ' ' . $this->renderVars['file'];
-
-                $output = '<!-- DEBUG-VIEW START ' . $this->renderVars['file'] . ' -->' . PHP_EOL
-                    . $output . PHP_EOL
-                    . '<!-- DEBUG-VIEW ENDED ' . $this->renderVars['file'] . ' -->' . PHP_EOL;
-            }
-        }
-
-        // Should we cache?
-        if (isset($this->renderVars['options']['cache'])) {
-            cache()->save($this->renderVars['cacheName'], $output, (int) $this->renderVars['options']['cache']);
-        }
-
-        $this->tempData = null;
-
-        return $output;
-    }
-}
diff --git a/app/Libraries/ViewComponents/ComponentRenderer.php b/app/Libraries/ViewComponents/ComponentRenderer.php
index 70a42a006e..097126154f 100644
--- a/app/Libraries/ViewComponents/ComponentRenderer.php
+++ b/app/Libraries/ViewComponents/ComponentRenderer.php
@@ -14,23 +14,11 @@ class ComponentRenderer
 {
     protected ViewComponents $config;
 
-    /**
-     * File name of the view source
-     */
-    protected string $currentView;
-
     public function __construct()
     {
         $this->config = config('ViewComponents');
     }
 
-    public function setCurrentView(string $view): self
-    {
-        $this->currentView = $view;
-
-        return $this;
-    }
-
     public function render(string $output): string
     {
         // Try to locate any custom tags, with PascalCase names like: Button, Label, etc.
@@ -139,10 +127,7 @@ class ComponentRenderer
         // TODO: Is there a better way to locate components local to current module?
         $pathsToDiscover = [];
         $lookupPaths = $this->config->lookupPaths;
-        $pathsToDiscover = array_filter($lookupPaths, function ($path): bool {
-            return str_starts_with($this->currentView, $path);
-        });
-        $pathsToDiscover = array_values($pathsToDiscover);
+        $pathsToDiscover = array_values($lookupPaths);
         $pathsToDiscover[] = $this->config->defaultLookupPath;
 
         $namePath = str_replace('.', '/', $name);
diff --git a/app/Libraries/ViewComponents/Decorator.php b/app/Libraries/ViewComponents/Decorator.php
new file mode 100644
index 0000000000..9ca40085f9
--- /dev/null
+++ b/app/Libraries/ViewComponents/Decorator.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace ViewComponents;
+
+use CodeIgniter\View\ViewDecoratorInterface;
+
+/**
+ * Class Decorator
+ *
+ * Enables rendering of View Components into the views.
+ *
+ * Borrowed and adapted from https://github.com/lonnieezell/Bonfire2/
+ */
+class Decorator implements ViewDecoratorInterface
+{
+    private static ?ComponentRenderer $components = null;
+
+    public static function decorate(string $html): string
+    {
+        $components = self::factory();
+
+        return $components->render($html);
+    }
+
+    /**
+     * Factory method to create a new instance of ComponentRenderer
+     */
+    private static function factory(): ComponentRenderer
+    {
+        if (self::$components === null) {
+            self::$components = new ComponentRenderer();
+        }
+
+        return self::$components;
+    }
+}
-- 
GitLab