. * */ namespace Vvveb\System\Component; use function Vvveb\dashesToCamelCase; use function Vvveb\removeJsonComments; use Vvveb\System\Cache; use Vvveb\System\Core\View; if (! defined('COMPONENT_CACHE_FLAG_LOCK')) { define('COMPONENT_CACHE_FLAG_LOCK', PHP_INT_MIN + 1); define('COMPONENT_CACHE_FLAG_REGENERATE', PHP_INT_MIN + 2); //time define('COMPONENT_CACHE_EXPIRE_DELAY', 5); //real expiration time +5 seconds define('COMPONENT_CACHE_WAIT', 1); //wait for cache generation define('COMPONENT_CACHE_MAX_WAIT_RETRY', 3); //wait for cache generation define('COMPONENT_CACHE_LOCK_EXPIRE', 20); //lock can not be set more than COMPONENT_CACHE_LOCK_EXPIRE seconds define('COMPONENT_CACHE_EXPIRE', 20); } class Component { static private $instance; private $queue; private $components = []; private $componentsFile; private $loaded; private $content; private $view; private $app; private $documentType = 'html'; private $cache = true; static function getInstance($view = false, $regenerate = false, $content = false, $app = null) { if (self :: $instance === NULL) { if (! $view) { $view = View::getInstance(); } self :: $instance = new self($view, $regenerate, $content); } return self :: $instance; } function __construct($view, $regenerate = false, $content = false, $app = null) { if ($this->loaded) { return true; } $this->app = $app ?? APP; if (! $view) { $view = View::getInstance(); } $this->view = $view; $this->componentsFile = $view->serviceTemplate() . '.component'; $this->content = $content; if ((! file_exists($this->componentsFile)) || $regenerate) { $this->generateRequiredComponents(); $this->saveTemplateComponents(); $this->loadComponents(); $this->loaded = true; } else { } if ($this->loaded) { return true; } $this->loadTemplateComponents(); $this->loadComponents(); $this->loaded = true; } function isComponent($name) { return isset($this->components[$name]); } function getComponent($name) { return $this->components[$name] ?? []; } function loadComponents() { $view = $this->view; $cache = []; $objs = []; $namespace = 'component'; $notFound404 = false; if (is_array($this->components)) { foreach ($this->components as $component => $instances) { if (strpos($component, 'plugin') === 0) { $template = str_replace('plugin', '', $component); $p = strrpos($template, '-'); $pluginName = substr($template, 1, $p - 1); $nameSpace = substr($template, $p + 1); $app = ''; $pluginClassName = dashesToCamelCase($pluginName); $class = "Vvveb\Plugins\\$pluginClassName\Component\\$nameSpace"; $file = DIR_PLUGINS . "$pluginName/component/" . str_replace('-', '/', $nameSpace) . '.php'; } else { $class = '\Vvveb\Component\\' . str_replace('-', '\\', $component); $file = DIR_ROOT . $this->app . DS . 'component' . DS . str_replace('-', '/', $component) . '.php'; } $component = str_replace('-', '_', $component); if (file_exists($file)) { include_once $file; foreach ($instances as $instance => $options) { $obj = new $class($options); if ($obj->cacheExpire && $this->cache && ($cacheKey = $obj->cacheKey())) { $cacheExpireKey = $cacheKey . '_expire'; $cache[] = $cacheKey; $cache[] = $cacheExpireKey; $obj->component = $component; $objs[$obj->cacheKey][$instance] = $obj; } else { $results = $obj->results(); if ($results !== false && $results !== null) { $results['_instance'] = $obj; } $comp = &$view->_component[$component]; $comp[$instance] = $results; if (isset($results['404']) && $results['404'] == true) { $notFound404 = true; } } } } } if ($this->cache) { //get cached data $cacheDriver = Cache::getInstance(); //$cacheDriver = new Memcached(); $null = []; $data = $cacheDriver->getMulti($namespace, $cache, SITE_ID) ?? []; foreach ($objs as $cacheKey => $instances) { foreach ($instances as $index => $instance) { if (! isset($data[$cacheKey]) || ! is_array($data[$cacheKey])) { $data[$cacheKey] = []; } $component = $instance->component; $component = str_replace('-', '_', $component); $data[$cacheKey]['_instance'] = $instance; $comp = &$view->_component[$component]; $comp[$index] = $data[$cacheKey]; if (isset($comp[$index]['404']) && $comp[$index]['404'] == true) { $notFound404 = true; } } //cache hit, remove from sql regeneration queue $cacheExpireKey = $cacheKey . '_expire'; //if no lock set (! -1) and cache is expiring then set lock and set for regeneration if (! isset($data[$cacheExpireKey]) || ! isset($data[$cacheKey]) || ($data[$cacheExpireKey] && ($data[$cacheExpireKey] > 0) && ($data[$cacheExpireKey] + COMPONENT_CACHE_EXPIRE_DELAY) < $_SERVER['REQUEST_TIME'])) { $cacheDriver->set($namespace, $cacheExpireKey, COMPONENT_CACHE_FLAG_LOCK, COMPONENT_CACHE_LOCK_EXPIRE); //set lock $data[$cacheExpireKey] = COMPONENT_CACHE_FLAG_REGENERATE; //set regeneration flag } if ($data[$cacheExpireKey] > 0) { unset($objs[$cacheKey], $cache[$cacheKey], $cache[$cacheExpireKey], $data[$cacheKey], $data[$cacheExpireKey]); } } $wait = true; $retry = 0; //run sql queries for uncached components $saveCache = []; while ($wait && $retry < COMPONENT_CACHE_MAX_WAIT_RETRY) { $wait = false; $retry++; foreach ($objs as $key => $objects) { $cacheExpireKey = $key . '_expire'; //check lock if ((! isset($data[$cacheExpireKey]) || ! ($data[$cacheExpireKey])) || ! isset($data[$key]) || $data[$cacheExpireKey] == COMPONENT_CACHE_FLAG_REGENERATE) { //lock set by this script, regenerate content $id = key($objects); $results = $objects[$id]->results(); if ($results !== null) { $saveCache[$key] = $results; $saveCache[$cacheExpireKey] = $_SERVER['REQUEST_TIME'] + $objects[$id]->cacheExpire; } if (isset($results['404']) && $results['404'] == true) { $notFound404 = true; } $data[$cacheExpireKey] = 0; foreach ($objects as $index => $instance) { $results['_instance'] = $instance; $component = $instance->component; $component = str_replace('-', '_', $component); $comp = &$view->_component[$component]; $comp[$index] = $results; } } else { if ($data[$cacheExpireKey] == COMPONENT_CACHE_FLAG_LOCK) { //error_log("wait for $cacheExpireKey"); //item is locked, some other script is generating content $wait = true; } } } if ($wait) { error_log('wait cache ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] . print_r($cache,1)); //get @sleep(COMPONENT_CACHE_WAIT); $data = $cacheDriver->getMulti($namespace, $cache); } } if ($retry >= COMPONENT_CACHE_MAX_WAIT_RETRY) { error_log('error:CACHE max retry reached for ' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']); } } if (! empty($saveCache)) { $cacheDriver->setMulti($namespace, $saveCache, /*$_SERVER['REQUEST_TIME'] + */COMPONENT_CACHE_EXPIRE * 3600, SITE_ID); } } //call request for each component foreach ($this->components as $component => $instances) { foreach ($instances as $index => $options) { $component = str_replace('-', '_', $component); if (isset($view->_component[$component])) { $comp = &$view->_component[$component]; $results = &$comp[$index]; $object = $results['_instance'] ?? false; if ($object && method_exists($object, 'request')) { $object->request($results, $index); } } } } unset($data, $cache); $this->components = NULL; if ($notFound404) { \Vvveb\System\Core\FrontController::notFound(false); } } /* static function isLoaded($component) { return isset($this->queue[$component]); } */ function results() { } function generateRequiredComponents() { //get components from html page $document = new \DomDocument(); $document->preserveWhiteSpace = false; $document->recover = true; $document->strictErrorChecking = false; $document->formatOutput = false; $document->resolveExternals = false; $document->validateOnParse = false; $document->xmlStandalone = true; $view = view::getInstance(); libxml_use_internal_errors(true); $this->documentType = $view->getDocumentType(); if ($this->content) { if ($this->documentType == 'html') { @$document->loadHTML($this->content); } else { @$document->loadXML($this->content); } } else { $view = $this->view; $template = $view->template(); $extension = strtolower(trim(substr($template, -4), '.')); $this->documentType = $extension; if ($template[0] == '/') { } else { if (strpos($template, 'plugins/') === 0) { $template = str_replace('plugins/', '', $template); $p = strpos($template, '/'); $pluginName = substr($template, 0, $p); $nameSpace = substr($template, $p + 1); $app = ''; if ($this->app != 'app') { $app = $this->app . DS; } $template = DIR_PLUGINS . $pluginName . DS . 'public' . DS . $app . $nameSpace; } else { $template = $view->getTemplatePath() . $template; } } if ($this->documentType == 'html') { @$document->loadHTMLFile($template, LIBXML_NOWARNING | LIBXML_NOERROR); } else { $content = file_get_contents($template); if ($this->documentType == 'json') { //remove json comments from line start $content = removeJsonComments($content); $json = json_decode($content, true); $json = \Vvveb\prepareJson($json); $xml = \Vvveb\array2xml($json); @$document->loadXML($xml); } else { @$document->loadXML($content, LIBXML_NOWARNING | LIBXML_NOERROR); } } } $xpath = new \DOMXpath($document); $i = 0; //include froms in case any component_ is included while (($elements = $xpath->query('//*[ @data-v-copy-from or @data-v-save-global ]')) && $elements->length && $i++ < 2) { $fromDocument = new \DomDocument(); $fromDocument->preserveWhiteSpace = false; $fromDocument->recover = true; $fromDocument->strictErrorChecking = false; $fromDocument->formatOutput = false; $fromDocument->resolveExternals = false; $fromDocument->validateOnParse = false; $fromDocument->xmlStandalone = true; foreach ($elements as $element) { $attribute = $element->getAttribute('data-v-copy-from') ?: $element->getAttribute('data-v-save-global'); $element->removeAttribute('data-v-copy-from'); $element->removeAttribute('data-v-save-global'); if (preg_match('/([^\,]+)\,([^$,]+)/', $attribute , $from)) { $file = html_entity_decode(trim($from[1])); $selector = html_entity_decode(trim($from[2])); if ($this->documentType == 'html') { $fromDocument->loadHTMLFile($view->getTemplatePath() . $file); } else { $fromDocument->loadXML($view->getTemplatePath() . $file); } $fromXpath = new \DOMXpath($fromDocument); $fromElements = $fromXpath->query(\Vvveb\cssToXpath($selector)); $count = 0; $parent = $element->parentNode; foreach ($fromElements as $externalNode) { $importedNode = $document->importNode($externalNode, true); if ($parent) { if ($count) { $parent->appendChild($importedNode); } else { $parent->replaceChild($importedNode, $element); } $element = $importedNode; //$parent = $element->parentNode; $count++; } } } } } //search for elements that have an attribute starting with data-v-component- $elements = $xpath->query('//*[@*[starts-with(name(), "data-v-component-")]]'); foreach ($elements as $element) { $component = ''; $opts = []; foreach ($element->attributes as $attr) { $nodeName = $attr->nodeName; if (strpos($nodeName, 'data-v-component-') === 0) { $component = str_replace('data-v-component-', '', $nodeName); //$classes = explode(' ', trim($attr->nodeValue)); } else { if (strpos($nodeName, 'data-v-') === 0) { $option = str_replace('data-v-', '', $nodeName); $opts[$option] = $attr->nodeValue; } } } //get all classes //search for options $options = null; //validate options $validOptions = []; if (strpos($component, 'plugin') === 0) { $template = str_replace('plugin', '', $component); $p = strrpos($template, '-'); $pluginName = substr($template, 1, $p - 1); $nameSpace = substr($template, $p + 1); $app = ''; if ($this->app != 'app') { $app = $this->app . DS; } $pluginClassName = dashesToCamelCase($pluginName); $componentClass = "Vvveb\Plugins\\$pluginClassName\Component\\$nameSpace"; $file = DIR_PLUGINS . "$pluginName/component/" . str_replace('-', '/', $nameSpace) . '.php'; } else { $componentClass = '\Vvveb\Component\\' . ucfirst(str_replace('-', '\\', $component)); $file = DIR_ROOT . $this->app . DS . 'component/' . str_replace('-', '/', $component) . '.php'; } if (file_exists($file)) { include_once $file; //$componentClass = new $componentClass; //do not add design only components if (isset($componentClass::$designOnly) && $componentClass::$designOnly == true) { continue; } $validOptions = array_keys($componentClass::$defaultOptions); } else { if (defined('DEBUG') && \DEBUG) { error_log("Component does not exist $componentClass => $file"); //die("Component does not exist $componentClass => $file"); } continue; } //save options foreach ($opts as $name => $option) { if (in_array($name, $validOptions) && isset($option) !== false) { if ((isset($option[0]) && ($option[0] == '{' || $option[0] == '[')) || (strpos($option, ',') !== false)) { $options[$name] = json_decode($option, 1); } else { $options[$name] = $option; } } } $options['_hash'] = md5($component . serialize($options)); $components[$component][] = $options; } if (isset($components)) { $this->components = $components; } //get fields for component //load components and feed fields } function saveTemplateComponents() { $php = var_export($this->components, true); $php = preg_replace('/\s+/', ' ', $php); //repeating end lines $php = preg_replace('/\n+/', '', $php); return file_put_contents($this->componentsFile, 'componentsFile; if (isset($components)) { $this->components = $components; } //keep only requested service if (isset($_GET['component_ajax'])) { } return true; } }