*/ class Hook extends AbstractSmartyPlugin { private $dispatcher; /** @var Translator */ protected $translator; /** @var Module */ protected $smartyPluginModule = null; /** @var array */ protected $hookResults = array(); /** @var array */ protected $varstack = array(); /** @var bool debug */ protected $debug = false; public function __construct($debug, ContainerAwareEventDispatcher $dispatcher) { $this->debug = $debug; $this->dispatcher = $dispatcher; $this->translator = $dispatcher->getContainer()->get("thelia.translator"); $this->hookResults = array(); } /** * Generates the content of the hook * * {hook name="hook_code" var1="value1" var2="value2" ... } * * This function create an event, feed it with the custom variables passed to the function (var1, var2, ...) and * dispatch it to the hooks that respond to it. * * The name of the event is `hook.{context}.{hook_code}` where : * * context : the id of the context of the smarty render : 1: frontoffice, 2: backoffice, 3: email, 4: pdf * * hook_code : the code of the hook * * The event collects all the fragments of text rendered in each modules functions that listen to this event. * Finally, this fragments are concatenated and injected in the template * * @param array $params the params passed in the smarty function * @param \TheliaSmarty\Template\SmartyParser $smarty the smarty parser * * @return string the contents generated by modules */ public function processHookFunction($params, &$smarty) { $hookName = $this->getParam($params, 'name'); $module = intval($this->getParam($params, 'module', 0)); $moduleCode = $this->getParam($params, 'modulecode', ""); $type = $smarty->getTemplateDefinition()->getType(); $event = new HookRenderEvent($hookName, $params); $event->setArguments($this->getArgumentsFromParams($params)); $eventName = sprintf('hook.%s.%s', $type, $hookName); // this is a hook specific to a module if (0 === $module && "" !== $moduleCode) { if (null !== $mod = ModuleQuery::create()->findOneByCode($moduleCode)) { $module = $mod->getId(); } } if (0 !== $module) { $eventName .= '.' . $module; } $this->getDispatcher()->dispatch($eventName, $event); $content = trim($event->dump()); if ($this->debug && $smarty->getRequest()->get('SHOW_HOOK')) { $content = self::showHook($hookName, $params) . $content; } $this->hookResults[$hookName] = $content; // support for compatibility with module_include if ($type === TemplateDefinition::BACK_OFFICE) { $content .= $this->moduleIncludeCompat($params, $smarty); } return $content; } /** * Call the plugin function module_include for backward compatibility. * * @param array $params the params passed in the smarty function * @param \TheliaSmarty\Template\SmartyParser $smarty the smarty parser * * @return string the contents generated by module_include function */ protected function moduleIncludeCompat($params, &$smarty) { $plugin = $this->getSmartyPluginModule(); $params = array( "location" => $this->getParam($params, 'location', null), "module" => $this->getParam($params, 'modulecode', null), "countvar" => $this->getParam($params, 'countvar', null) ); return $plugin->theliaModule($params, $smarty); } /** * get the smarty plugin Module * * @return Module the smarty plugin Module */ protected function getSmartyPluginModule() { if (null === $this->smartyPluginModule) { $this->smartyPluginModule = $this->dispatcher->getContainer()->get("smarty.plugin.module"); } return $this->smartyPluginModule; } protected function showHook($hookName, $params) { $content = '
' . $hookName; foreach ($params as $name => $value) { if ($name !== 'location' && $name !== "name") { $type = ''; if (is_object($value)) { $value = get_class($value); $type = 'object'; } elseif (is_array($value)) { $value = implode(',', $value); $type = 'array'; } elseif (is_int($value)) { $type = 'float'; } elseif (is_int($value)) { $type = 'int'; } elseif (is_string($value)) { $value = (strlen($value) > 30) ? substr($value, 0, 30) . '...' : $value; $type = 'string'; } if ($type !== '') { $type = '' . $type . ' '; } $content .= '' . $name . ' = ' . $type . $value . ''; } } return $content . '
'; } /** * Process the content of the hook block. * * {hookblock name="hook_code" var1="value1" var2="value2" ... } * * This function create an event, feed it with the custom variables passed to the function (var1, var2, ...) and * dispatch it to the hooks that respond to it. * * The name of the event is `hook.{context}.{hook_code}` where : * * context : the id of the context of the smarty render : 1: frontoffice, 2: backoffice, 3: email, 4: pdf * * hook_code : the code of the hook * * The event collects all the fragments generated by modules that listen to this event and add it to a fragmentBag. * This fragmentBag is not used directly. This is the forhook block that iterates over the fragmentBag to inject * data in the template. * * @param array $params * @param string $content * @param \TheliaSmarty\Template\SmartyParser $smarty * @param bool $repeat * * @return string the generated content */ public function processHookBlock($params, $content, $smarty, &$repeat) { $hookName = $this->getParam($params, 'name'); $module = intval($this->getParam($params, 'module', 0)); // explicit definition of variable that can be returned $fields = preg_replace( '|[^a-zA-Z0-9,\-_]|', '', $this->getParam($params, 'fields', '') ); $fields = ('' !== $fields) ? explode(",", $fields) : []; if (!$repeat) { if ($this->debug && $smarty->getRequest()->get('SHOW_HOOK')) { $content = self::showHook($hookName, $params) . $content; } return $content; } $type = $smarty->getTemplateDefinition()->getType(); $event = new HookRenderBlockEvent($hookName, $params, $fields); $event->setArguments($this->getArgumentsFromParams($params)); $eventName = sprintf('hook.%s.%s', $type, $hookName); // this is a hook specific to a module if (0 !== $module) { $eventName .= '.' . $module; } $this->getDispatcher()->dispatch($eventName, $event); // save results so we can use it in forHook block $this->hookResults[$hookName] = $event->get(); } /** * Process a {forhook rel="hookname"} ... {/forhook} * * The forhook iterates over the results return by a hookblock : * * {hookblock name="product.additional"} * {forhook rel="product.additional"} *
*

{$title}

*

{$content}

*
* {/forhook} * {/hookblock} * * @param array $params * @param string $content * @param \TheliaSmarty\Template\SmartyParser $smarty * @param bool $repeat * * @throws \InvalidArgumentException * @return string the generated content */ public function processForHookBlock($params, $content, $smarty, &$repeat) { $rel = $this->getParam($params, 'rel'); if (null == $rel) { throw new \InvalidArgumentException( $this->translator->trans("Missing 'rel' parameter in forHook arguments") ); } /** @var FragmentBag $fragments */ $fragments = null; // first call if ($content === null) { if (!array_key_exists($rel, $this->hookResults)) { throw new \InvalidArgumentException( $this->translator->trans("Related hook name '%name' is not defined.", ['%name' => $rel]) ); } $fragments = $this->hookResults[$rel]; $fragments->rewind(); if ($fragments->isEmpty()) { $repeat = false; } } else { $fragments = $this->hookResults[$rel]; $fragments->next(); } if ($fragments->valid()) { /** @var Fragment $fragment */ $fragment = $fragments->current(); // On first iteration, save variables that may be overwritten by this hook if (!isset($this->varstack[$rel])) { $saved_vars = array(); $varlist = $fragment->getVars(); foreach ($varlist as $var) { $saved_vars[$var] = $smarty->getTemplateVars($var); } $this->varstack[$rel] = $saved_vars; } foreach ($fragment->getVarVal() as $var => $val) { $smarty->assign($var, $val); } // continue iteration $repeat = true; } // end if (!$repeat) { // Restore previous variables values before terminating if (isset($this->varstack[$rel])) { foreach ($this->varstack[$rel] as $var => $value) { $smarty->assign($var, $value); } unset($this->varstack[$rel]); } } if ($content !== null) { if ($fragments->isEmpty()) { $content = ""; } return $content; } return ''; } /** * Process {elsehook rel="hookname"} ... {/elsehook} block * * @param array $params hook parameters * @param string $content hook text content * @param \Smarty_Internal_Template $template the Smarty object * @param boolean $repeat repeat indicator (see Smarty doc.) * * @return string the hook output */ public function elseHook( $params, $content, /** @noinspection PhpUnusedParameterInspection */ $template, &$repeat ) { // When encountering close tag, check if hook has results. if ($repeat === false) { return $this->checkEmptyHook($params) ? $content : ''; } return ''; } /** * Process {ifhook rel="hookname"} ... {/ifhook} block * * @param array $params hook parameters * @param string $content hook text content * @param \Smarty_Internal_Template $template the Smarty object * @param boolean $repeat repeat indicator (see Smarty doc.) * * @return string the hook output */ public function ifHook($params, $content, /** @noinspection PhpUnusedParameterInspection */ $template, &$repeat) { // When encountering close tag, check if hook has results. if ($repeat === false) { return $this->checkEmptyHook($params) ? '' : $content; } return ''; } /** * Check if a hook has returned results. The hook should have been executed before, or an * InvalidArgumentException is thrown * * @param array $params * * @return boolean true if the hook is empty * @throws \InvalidArgumentException */ protected function checkEmptyHook($params) { $hookName = $this->getParam($params, 'rel'); if (null == $hookName) { throw new \InvalidArgumentException( $this->translator->trans("Missing 'rel' parameter in ifhook/elsehook arguments") ); } if (!isset($this->hookResults[$hookName])) { throw new \InvalidArgumentException( $this->translator->trans("Related hook name '%name' is not defined.", ['%name' => $hookName]) ); } return (is_string($this->hookResults[$hookName]) && '' === $this->hookResults[$hookName] || !is_string($this->hookResults[$hookName]) && $this->hookResults[$hookName]->isEmpty() ); } /** * Clean the params of the params passed to the hook function or block to feed the arguments of the event * with relevant arguments. * * @param $params * * @return array */ protected function getArgumentsFromParams($params) { $args = array(); $excludes = array("name", "before", "separator", "after", "fields"); if (is_array($params)) { foreach ($params as $key => $value) { if (!in_array($key, $excludes)) { $args[$key] = $value; } } } return $args; } /** * Define the various smarty plugins handled by this class * * @return array an array of smarty plugin descriptors */ public function getPluginDescriptors() { return array( new SmartyPluginDescriptor('function', 'hook', $this, 'processHookFunction'), new SmartyPluginDescriptor('block', 'hookblock', $this, 'processHookBlock'), new SmartyPluginDescriptor('block', 'forhook', $this, 'processForHookBlock'), new SmartyPluginDescriptor('block', 'elsehook', $this, 'elseHook'), new SmartyPluginDescriptor('block', 'ifhook', $this, 'ifHook'), ); } /** * Return the event dispatcher, * * @return \Symfony\Component\EventDispatcher\EventDispatcher */ public function getDispatcher() { return $this->dispatcher; } }