* $template = new Qs_Template(); * $template->addArrayDataSource($bodyVars); * $body = $template->render('contactFormEmailBody', Qs_Template::SETTINGS); * * * @method Qs_Template setText() setText(string $template) * @method Qs_Template setFile() setFile(string $fileName) * @method Qs_Template setSettings() setSettings(string $fieldName) * @method Qs_Template setCallback() setCallback(callback $callback) */ class Qs_Template { const SOURCE_ARRAY = 'array'; const SOURCE_GLOBALS = 'globals'; const SOURCE_CALLBACK = 'callback'; const SOURCE_CONSTANT = 'constant'; const TEXT = 'text'; const FILE = 'file'; const SETTINGS = 'settings'; const CALLBACK = 'callback'; protected static $_templateTypes = [self::TEXT, self::FILE, self::SETTINGS, self::CALLBACK]; protected static $_placeholderPattern = '/\{[A-Za-z0-9_\[\]]+\}/'; /** * Handle conditional attributes: data-st-if="condition" * @var bool */ protected $_dynamic = false; protected $_options = []; protected $_dataSources = []; protected $_templateSpec; protected $_templateType; public function __construct($options = null) { if (is_array($options)) { $this->setOptions($options); } elseif ($options instanceof Zend_Config) { $this->setConfig($options); } return $this; } public function __call($name, $args) { if (preg_match('/^set(.*)$/', $name, $matches) && in_array(strtolower($matches), self::$_templateTypes) ) { return $this->setContent(reset($args), strtolower($matches[1])); } throw new Exception('Call to undefined function ' . __CLASS__ . '::' . $name); } public function setOptions($options) { if (isset($options['options'])) { unset($options['options']); } foreach ($options as $name => $value) { $method = 'set' . ucfirst($name); if (method_exists($this, $method)) { $this->{$method}($value); } else { $this->_options[$name] = $value; } } return $this; } public function setConfig(Zend_Config $config) { return $this->setOptions($config->toArray()); } /** * @return boolean */ public function isDynamic() { return $this->_dynamic; } /** * @param boolean $dynamic */ public function setDynamic($dynamic = true) { $this->_dynamic = $dynamic; return $this; } public function cleanDataSources() { $this->_dataSources = []; return $this; } /** * Set multiple data sources overwriting previous data sources * @param array $dataSources * @return Qs_Template */ public function setDataSources(array $dataSources) { $this->cleanDataSources(); $this->addDataSources($dataSources); return $this; } /** * Add multiple data sources * @param string|array $dataSources array(string $type, mixed $dataSource, [array $placeholders]) * @throws Exception * @return Qs_Template */ public function addDataSources(array $dataSources) { foreach ($dataSources as $spec) { $type = $dataSource = $placeholders = null; if (is_string($spec)) { $type = $spec; } elseif (is_array($spec)) { if (isset($spec['type'])) { $type = $spec['type']; $dataSource = (isset($spec['dataSource'])) ? $spec['dataSource'] : null; $placeholders = isset($spec['placeholders']) ? $spec['placeholders'] : null; } else { $type = array_shift($spec); $dataSource = array_shift($spec); $placeholders = array_shift($spec); } } if ($type) { $this->addDataSource($type, $dataSource, $placeholders); } else { throw new Exception('Invalid DataSource passed to setDataSources()'); } } return $this; } /** * @param string $type * @param mixed $dataSource Note that later added a data source is processed first * @param null|array $placeholders If parameter $placeholders is passed then this placeholders will be retrieved * only from this data source. Note that later added placeholders overwrites the previous. * @throws Exception * @return Qs_Template */ public function addDataSource($type, $dataSource, $placeholders = null) { $method = 'add' . ucfirst($type) . 'DataSource'; if (method_exists($this, $method)) { $placeholders = (null !== $placeholders) ? (array) $placeholders : null; $this->{$method}($dataSource, $placeholders); } else { throw new Exception('Unknown DataSource of type "' . $type . '" passed to addDataSource()'); } return $this; } /** * @param array $dataSource Array containing pairs $placeholder => $value * @param null|array $placeholders * @return Qs_Template */ public function addArrayDataSource($dataSource, $placeholders = null) { $dataSource = (array) $dataSource; $placeholders = (isset($placeholders) && is_array($placeholders)) ? $placeholders : null; $this->_dataSources[] = [ 'type' => self::SOURCE_ARRAY, 'placeholders' => $placeholders, 'source' => $dataSource, ]; return $this; } /** * @param null $dataSource Name of global variable or null to use $GLOBALS as source * @param null|array $placeholders * @throws Exception * @return Qs_Template */ public function addGlobalsDataSource($dataSource = null, $placeholders = null) { if (isset($dataSource) && !isset($GLOBALS[$dataSource])) { throw new Exception('Invalid variable name passed to addGlobalsDataSource()'); } $placeholders = (isset($placeholders) && is_array($placeholders)) ? $placeholders : null; $this->_dataSources[] = [ 'type' => self::SOURCE_GLOBALS, 'placeholders' => $placeholders, 'source' => $dataSource, ]; return $this; } /** * @param callback $dataSource Function that accepts one parameter $placeholder and returns its value or null * @param null|array $placeholders * @throws Exception * @return Qs_Template */ public function addCallbackDataSource($dataSource, $placeholders = null) { if (is_callable($dataSource)) { $placeholders = (isset($placeholders) && is_array($placeholders)) ? $placeholders : null; $this->_dataSources[] = [ 'type' => self::SOURCE_CALLBACK, 'placeholders' => $placeholders, 'source' => $dataSource, ]; } else { throw new Exception('Invalid callback passed to addCallbackDataSource()'); } return $this; } /** * @param null|string $dataSource Name of constant or null to use Qs_Constant as source * @param null|array $placeholders * @throws Exception * @return Qs_Template */ public function addConstantDataSource($dataSource = null, $placeholders = null) { if (isset($dataSource) && !defined($dataSource)) { throw new Exception('Invalid constant name passed to addConstantDataSource()'); } $placeholders = (isset($placeholders) && is_array($placeholders)) ? $placeholders : null; $this->_dataSources[] = [ 'type' => self::SOURCE_CONSTANT, 'placeholders' => $placeholders, 'source' => $dataSource, ]; return $this; } /** * @param string|callback $spec Depends on $type: text, filename, field in settings or callback * @param string $type Template type (@see Qs_Template::$_templateTypes) * @throws Exception * @return Qs_Template */ public function setContent($spec, $type = self::TEXT) { if (in_array($type, self::$_templateTypes)) { $this->_templateSpec = $spec; $this->_templateType = $type; } else { throw new Exception('Invalid template type passed to setContent()'); } return $this; } /** * Returns raw template * @throws Exception * @return string */ public function getContent() { $method = '_getContentFrom' . ucfirst($this->_templateType); if (method_exists($this, $method)) { return $this->{$method}(); } throw new Exception('Unknown template type'); } protected function _getContentFromText() { return $this->_templateSpec; } protected function _getContentFromFile() { if (file_exists($this->_templateSpec) && false !== ($template = file_get_contents($this->_templateSpec))) { return $template; } throw new Exception('Template file not found'); } protected function _getContentFromSettings() { if (false !== ($template = App_Settings_Obj::get($this->_templateSpec))) { return $template; } throw new Exception('Template not found'); } protected function _getContentFromCallback() { if (is_callable($this->_templateSpec)) { return call_user_func($this->_templateSpec); } throw new Exception('Invalid template callback'); } protected function _getContentPlaceholders($template = null) { $template = (null === $template) ? $this->getContent() : $template; $matches = []; if (false !== preg_match_all(self::$_placeholderPattern, $template, $matches)) { $matches = $matches[0]; } foreach ($matches as &$placeholder) { $placeholder = trim($placeholder, '{}'); } return $matches; } /** * @param string $placeholder * @param int $sourceId * @return mixed|null */ protected function _getPlaceholderValueFromSource($placeholder, $sourceId) { if (empty($this->_dataSources[$sourceId])) { return null; } $dataSource = $this->_dataSources[$sourceId]; switch ($dataSource['type']) { case self::SOURCE_ARRAY: $value = Qs_Array::get($dataSource['source'], $placeholder); break; case self::SOURCE_CONSTANT: $value = isset($dataSource['source']) ? constant($dataSource['source']) : Qs_Constant::get($placeholder); break; case self::SOURCE_GLOBALS: $value = isset($dataSource['source']) ? Qs_Array::get($GLOBALS, $dataSource['source']) : Qs_Array::get($GLOBALS, $placeholder); break; case self::SOURCE_CALLBACK: $value = call_user_func($dataSource['source'], $placeholder); break; default: $value = null; } return $value; } protected function _getPlaceholderValue($placeholder, $sourceId = null) { if (null !== $sourceId && isset($this->_dataSources[$sourceId])) { return $this->_getPlaceholderValueFromSource($placeholder, $sourceId); } $value = ''; foreach (array_keys($this->_dataSources) as $sourceId) { if (null !== ($value = $this->_getPlaceholderValueFromSource($placeholder, $sourceId))) { break; } } return $value; } /** * Returns an array of placeholders with values * @param string $template * @return array Array of pairs '{$placeholder}' => '$value' */ protected function _getContentPlaceholderValues($template = null) { $template = (null === $template) ? $this->getContent() : $template; $placeholderSources = []; // placeholder => sourceId foreach ($this->_dataSources as $dataSourceId => $dataSourceSpec) { if (isset($dataSourceSpec['placeholders']) && !empty($dataSourceSpec['placeholders'])) { $placeholderSources = array_merge( $placeholderSources, array_fill_keys($dataSourceSpec['placeholders'], $dataSourceId) ); } } $values = []; // placeholder => value $templatePlaceholders = $this->_getContentPlaceholders($template); $templatePlaceholders = array_unique($templatePlaceholders); foreach ($templatePlaceholders as $name) { $sourceId = (isset($placeholderSources[$name])) ? $placeholderSources[$name] : null; $values['{' . $name . '}'] = $this->_getPlaceholderValue($name, $sourceId); } return $values; } /** * @param string $template OPTIONAL * @param string $templateType OPTIONAL * @return string */ public function render($template = null, $templateType = self::TEXT) { if (null !== $template) { $this->setContent($template, $templateType); } $template = $this->getContent(); if ($template) { $map = $this->_getContentPlaceholderValues($template); if ($this->isDynamic()) { $template = $this->_processDynamic($template, $map); } $template = str_replace(array_keys($map), array_values($map), $template); } return $template; } protected function _processDynamic($html, $map) { $dom = new DOMDocument(); if (false === $dom->loadHTML($html)) { return $html; } $xpath = new DomXpath($dom); $list = $xpath->query('//*[@data-st-if]'); /** @var \DOMElement $node */ foreach ($list as $node) { $value = $node->attributes->getNamedItem('data-st-if')->value; $node->removeAttribute('data-st-if'); $node->removeAttribute('data-st-comment'); if ($value) { $value = htmlspecialchars_decode($value); $revert = false; if ('!' === $value[0]) { $revert = true; $value = substr($value, 1); } if ($revert !== empty($map['{' . $value . '}'])) { $node->parentNode->removeChild($node); } } } // remove automatically added tags: "doctype", "html", "body" return preg_replace('~<(?:!DOCTYPE|/?(?:html|body))[^>]*>\s*~i', '', $dom->saveHTML()); } }