diff --git a/docs/TODO.md b/docs/TODO.md index f980ae5..14544b3 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -21,7 +21,7 @@ [.] → Models - [ ] → Views (Twig/Liquid/PHP) + [x] → Views (Twig/Liquid/PHP) [x] → JavaScript/CSS Asset loading diff --git a/src/bootstrap/requires.php b/src/bootstrap/requires.php index 2e5cdc7..b0a0620 100644 --- a/src/bootstrap/requires.php +++ b/src/bootstrap/requires.php @@ -117,6 +117,7 @@ final class requires { $file_name = common::get_string_left($file, $pos); $file_kind = common::get_string_right($file, common::string_length($file) - $pos); $file_type = match ($file_kind) { + ".twig" => ".twig", ".tpl" => ".tpl", default => ".php", }; diff --git a/src/classes/enums/view_type.php b/src/classes/enums/view_type.php index 2119660..b4b7b1f 100644 --- a/src/classes/enums/view_type.php +++ b/src/classes/enums/view_type.php @@ -13,4 +13,8 @@ enum view_type: string { case LIQUID = ".tpl"; case TWIG = ".twig"; case PHP = ".php"; + + public static function get_file_extension_for(view_type $type): string { + return $type->value; + } } \ No newline at end of file diff --git a/src/classes/php_file_cache.php b/src/classes/php_file_cache.php new file mode 100644 index 0000000..91df421 --- /dev/null +++ b/src/classes/php_file_cache.php @@ -0,0 +1,35 @@ + + * @copyright (c) 2025, Robert Strutts + * @license MIT + */ + +class php_file_cache { + protected $cache_path; + + public function __construct($path) { + $this->cache_path = rtrim($path, '/') . '/'; + if (!is_dir($this->cache_path)) { + mkdir($this->cache_path, 0775, true); + } + } + + public function get($key) { + $file = $this->cache_path . md5($key) . '.cache.php'; + if (file_exists($file)) { + return unserialize(file_get_contents($file)); + } + return null; + } + + public function set($key, $value) { + $file = $this->cache_path . md5($key) . '.cache.php'; + file_put_contents($file, serialize($value)); + } +} diff --git a/src/classes/services/liquid_templates.php b/src/classes/services/liquid_templates.php new file mode 100644 index 0000000..7b40da1 --- /dev/null +++ b/src/classes/services/liquid_templates.php @@ -0,0 +1,78 @@ + + * @copyright (c) 2025, Robert Strutts + * @license MIT + */ + +namespace CodeHydrater\services; + +use CodeHydrater\bootstrap\registry as Reg; +use CodeHydrater\php_file_cache as FileCache; +use Liquid\{Liquid, Template, Context}; +use Liquid\Cache\Local; + +final class liquid_templates { + private $use_local_cache = false; + private $liquid; + private $dir; + private $extension = 'tpl'; + + public function __construct(string $template_extension = 'tpl') { + $this->extension = $template_extension; + if (! Reg::get('loader')->is_loaded('Liquid')) { + Reg::get('loader')->add_namespace('Liquid', CodeHydrater_PROJECT . 'vendor/liquid/liquid/src/Liquid'); + } + + Liquid::set('INCLUDE_SUFFIX', $template_extension); + Liquid::set('INCLUDE_PREFIX', ''); + + $this->dir = CodeHydrater_PROJECT . 'views/liquid'; + $this->liquid = new Template($this->dir); + } + + public function whitespace_control() { + /* Force whitespace control to be used by switching tags from {%- to {% + * Also, will error out if you try {%- + * Need to figure out how to turn back on whitespaces with {%@ errors! not important... + */ + Liquid::set('TAG_START', '(?:{%@)|\s*{%'); + Liquid::set('TAG_END', '(?:@%}|%}\s*)'); + } + + public function parse(string $source) { + return $this->liquid->parse($source); + } + + public function parse_file(string $file) { + $safe_file = \CodeHydrater\security::filter_uri($file); + if ($this->use_local_cache) { + $cache = new FileCache(BaseDir . "/protected/runtime/liquid_cache"); + + $templateName = $safe_file . "." . $this->extension; + $templatePath = $this->dir . "/". $safe_file . "." . $this->extension; + + $templateSource = file_get_contents($templatePath); + $cached = $cache->get($templateName); + if (!$cached) { + $this->liquid->parseFile($safe_file); + $cache->set($templateName, $this->liquid); + } else { + $this->liquid = $cached; + } + } else { + $this->liquid->parseFile($safe_file); + } + } + + public function render(array $assigns = array(), $filters = null, array $registers = array()): string { + return $this->liquid->render($assigns, $filters, $registers); + } + + public function get_engine() { + return $this->liquid; + } +} \ No newline at end of file diff --git a/src/classes/services/twig.php b/src/classes/services/twig.php new file mode 100644 index 0000000..2f2965f --- /dev/null +++ b/src/classes/services/twig.php @@ -0,0 +1,23 @@ + + * @copyright (c) 2025, Robert Strutts + * @license MIT + */ +namespace CodeHydrater\services; + +class twig { + public static function init() { + \CodeHydrater\bootstrap\registry::get('loader')->add_namespace("Twig", CodeHydrater_PROJECT. "/vendor/twig/twig/src"); + \CodeHydrater\bootstrap\registry::get('loader')->add_namespace("Symfony\Polyfill\Mbstring", CodeHydrater_PROJECT. "/vendor/symfony/polyfill-mbstring"); + \CodeHydrater\bootstrap\registry::get('loader')->add_namespace("Symfony\Polyfill\Ctype", CodeHydrater_PROJECT. "/vendor/symfony/polyfill-ctype"); + $loader = new \Twig\Loader\FilesystemLoader(CodeHydrater_PROJECT. "views/twig"); + $twig = new \Twig\Environment($loader, [ + 'cache' => BaseDir . "/protected/runtime/compilation_cache", + ]); + return $twig; + } +} diff --git a/src/classes/view.php b/src/classes/view.php index b72437b..51ba69f 100644 --- a/src/classes/view.php +++ b/src/classes/view.php @@ -10,23 +10,23 @@ declare(strict_types=1); namespace CodeHydrater; +use \CodeHydrater\enums\view_type as ViewType; + final class view { public $white_space_control = false; public $page_output; private $vars = []; - private $project_dir = ""; // Not used anymore private $files = []; private $template = false; private $use_template_engine = false; + private $use_template_engine_twig = false; + private $use_template_engine_liquid = false; private $template_type = 'tpl'; - protected $tempalte_engine = null; + protected $tempalte_engine_twig = null; + protected $tempalte_engine_liquid = null; private function get_file(string $view_file, string $default): string { - $file_ext = bootstrap\common::get_string_right($view_file, 4); - if (! bootstrap\common::is_string_found($file_ext, '.')) { - $view_file .= '.php'; - } - $file = (empty($default)) ? "{$this->project_dir}/views/{$view_file}" : "{$this->project_dir}/views/{$default}/{$view_file}"; + $file = (empty($default)) ? "views/{$view_file}" : "views/{$default}/{$view_file}"; $path = bootstrap\site_helper::get_root(); $vf = $path . $file; if ( bootstrap\requires::safer_file_exists($vf) !== false) { @@ -38,18 +38,23 @@ final class view { /** * Alias to set_view */ - public function include(string $file): void { + public function include(string $file, ViewType $type = ViewType::PHP): void { $this->set_view($file); } /** * Alias to set_view */ - public function add_view(string $file): void { + public function add_view(string $file, ViewType $type = ViewType::PHP): void { $this->set_view($file); } - private function find_view_path(string $view_file) { + /** + * Check the $_GET['render'] for which folder to use to render the view + * @param string $view_file + * @return file | false + */ + private function find_view_path(string $view_file): string|false { $found = false; $default_paths = bootstrap\configure::get('view_mode', 'default_paths'); @@ -70,44 +75,41 @@ final class view { } return ($found) ? $file : false; } - - private function find_template_path($tpl_file) { - $file = "{$this->project_dir}/views/includes/{$tpl_file}"; - $path = bootstrap\site_helper::get_root(); - $vf = $path . $file; - return bootstrap\requires::safer_file_exists($vf); - } - + /** * Use View File * @param string $view_file * @param string $render_path * @throws Exception */ - public function set_view(string $view_file = null): void { + public function set_view(string $view_file = null, ViewType $type = ViewType::PHP): void { if ($view_file === null) { return; } - $file_ext = bootstrap\common::get_string_right($view_file, 4); - if (! bootstrap\common::is_string_found($file_ext, '.')) { - $file_ext = '.php'; - } else if ($file_ext !== '.php') { - $this->use_template_engine = true; + $file_ext = ViewType::get_file_extension_for($type); + $file = $this->find_view_path($view_file . $file_ext); + if ($type == ViewType::TWIG) { + $this->use_template_engine_twig = true; $this->template_type = str_replace('.', '', $file_ext); + $path = bootstrap\site_helper::get_root(); + if ($file !== false) { + $file = str_replace("views/twig", "", $file); // Remove Path, as it is defined in the service file + } } - - if ($file_ext === '.php') { - $file = $this->find_view_path($view_file); - } else { - $file = $this->find_template_path($view_file); + if ($type == ViewType::LIQUID) { + $this->use_template_engine_liquid = true; + $this->template_type = str_replace('.', '', $file_ext); + if ($file !== false) { + $file = ltrim(str_replace("views/liquid", "", $file), "/"); // Remove Path, as it is defined in the service file + } } - + if ($file === false) { echo "No view file exists for: {$view_file}!"; throw new \Exception("View File does not exist: " . $view_file); } else { - $this->files[] = array('file'=>$file, 'path'=>"project", 'file_type'=>$file_ext); + $this->files[] = array('file'=>$file, 'path'=> bootstrap\UseDir::PROJECT, 'file_type'=>$file_ext); } } @@ -116,17 +118,16 @@ final class view { } /** - * Use Template with view + * Use PHP Template with view * @param string $render_page * @return bool was found */ public function set_template(string $render_page): bool { if (! empty($render_page)) { $render_page = str_replace('.php', '', $render_page); - $templ = "{$this->project_dir}/templates/{$render_page}"; + $templ = "/templates/{$render_page}"; if (bootstrap\requires::safer_file_exists(bootstrap\site_helper::get_root() . $templ. '.php') !== false) { $this->template = $templ; -// echo $this->template; return true; } } @@ -161,8 +162,8 @@ final class view { * @param type $local * @param string $file */ - public function render($local, string $file = null) { - echo $this->fetch($local, $file); + public function render($local, string $file = null, ViewType $type = ViewType::PHP) { + echo $this->fetch($local, $file, $type); } /** @@ -170,11 +171,11 @@ final class view { * @param type $local = $this * @param $file (optional view file) */ - public function fetch($local, string $file = null): string { + public function fetch($local, string $file = null, ViewType $type = ViewType::PHP): string { $page_output = ob_get_clean(); // Get echos before View bootstrap\views::ob_start(); $saved_ob_level = ob_get_level(); - $this->set_view($file); + $this->set_view($file, $type); unset($file); @@ -182,25 +183,38 @@ final class view { $local = $this; // FALL Back, please use fetch($this); } - if ($this->use_template_engine) { - $this->tempalte_engine = bootstrap\registry::get('di')->get_service('templates', [$this->template_type]); + if ($this->use_template_engine_liquid) { + $this->tempalte_engine_liquid = bootstrap\registry::get('di')->get_service('liquid', [$this->template_type]); if ($this->white_space_control) { $this->tempalte_engine->whitespace_control(); } } + if ($this->use_template_engine_twig) { + $this->tempalte_engine_twig = bootstrap\registry::get('di')->get_service('twig', [$this->template_type]); + } + if (count($this->files) > 0) { foreach ($this->files as $view_file) { if ($view_file['file_type'] == '.php') { - bootstrap\requires::secure_include($view_file['file'], bootstrap\UseDir::PROJECT, $local, $this->vars); // Include the file - } else { + bootstrap\requires::secure_include($view_file['file'], bootstrap\UseDir::PROJECT, $local, $this->vars); // Include the PHP file + + } else if ($view_file['file_type'] == '.tpl') { + // Liquid uses .tpl by default, so remove that File Ext $template_file = str_replace('.tpl', '', $view_file['file']); - $this->tempalte_engine->parse_file($template_file); + $this->tempalte_engine_liquid->parse_file($template_file); + $assigns = $this->vars['template_assigns'] ?? []; $filters = $this->vars['template_filters'] ?? null; $registers = $this->vars['template_registers'] ?? []; - $assigns['production'] =(\main_tts\is_live()); - echo $this->tempalte_engine->render($assigns, $filters, $registers); + $assigns['production'] =(bootstrap\is_live()); + echo $this->tempalte_engine_liquid->render($assigns, $filters, $registers); + + } else if ($view_file['file_type'] == '.twig') { + $twig_data = $this->vars['twig_data'] ?? []; + echo $this->tempalte_engine_twig->render($view_file['file'], $twig_data); + } else { + throw new \Exception("Unable View File Type"); } } } @@ -208,19 +222,19 @@ final class view { $page_output .= ob_get_clean(); try { - if (bootstrap\common::get_bool(bootstrap\configure::get('CodeHydrater', 'check_HTML_tags')) === true) { - $tags = \CodeHydrater\tag_matches::check_tags($page_output); - if (! empty($tags['output'])) { - $page_output .= $tags['output']; - $page_output .= ''; - foreach($this->files as $bad) { - $page_output .= ""; + if (bootstrap\common::get_bool(bootstrap\configure::get('CodeHydrater', 'check_HTML_tags')) === true) { + $tags = \CodeHydrater\tag_matches::check_tags($page_output); + if (! empty($tags['output'])) { + $page_output .= $tags['output']; + $page_output .= ''; + foreach($this->files as $bad) { + $page_output .= ""; + } + } } - } - } } catch (exceptions\Bool_Exception $e) { if (bootstrap\configure::get('CodeHydrater', 'live') === false) { - $page_output .= assets::alert('SET Config for tts: check_HTML_tags'); + $page_output .= assets::alert('SET Config for: check_HTML_tags'); } }