overlay = $overlay ?: []; $this->underlay = $underlay ?: []; $this->cacheGlobalProperties(); $this->cacheIteratorProperties(); } /** * Build cache of global properties */ protected function cacheGlobalProperties() { if (self::$globalProperties !== null) { return; } self::$globalProperties = $this->getPropertiesFromProvider( TemplateGlobalProvider::class, 'get_template_global_variables' ); } /** * Build cache of global iterator properties */ protected function cacheIteratorProperties() { if (self::$iteratorProperties !== null) { return; } self::$iteratorProperties = $this->getPropertiesFromProvider( TemplateIteratorProvider::class, 'get_template_iterator_variables', true // Call non-statically ); } /** * @var string $interfaceToQuery * @var string $variableMethod * @var boolean $createObject * @return array */ protected function getPropertiesFromProvider($interfaceToQuery, $variableMethod, $createObject = false) { $methods = []; $implementors = ClassInfo::implementorsOf($interfaceToQuery); if ($implementors) { foreach ($implementors as $implementor) { // Create a new instance of the object for method calls if ($createObject) { $implementor = new $implementor(); $exposedVariables = $implementor->$variableMethod(); } else { $exposedVariables = $implementor::$variableMethod(); } foreach ($exposedVariables as $varName => $details) { if (!is_array($details)) { $details = [ 'method' => $details, 'casting' => ViewableData::config()->uninherited('default_cast') ]; } // If just a value (and not a key => value pair), use method name for both key and value if (is_numeric($varName)) { $varName = $details['method']; } // Add in a reference to the implementing class (might be a string class name or an instance) $details['implementor'] = $implementor; // And a callable array if (isset($details['method'])) { $details['callable'] = [$implementor, $details['method']]; } // Save with both uppercase & lowercase first letter, so either works $lcFirst = strtolower($varName[0] ?? '') . substr($varName ?? '', 1); $result[$lcFirst] = $details; $result[ucfirst($varName)] = $details; } } } return $result; } /** * Look up injected value - it may be part of an "overlay" (arguments passed to <% include %>), * set on the current item, part of an "underlay" ($Layout or $Content), or an iterator/global property * * @param string $property Name of property * @param array $params * @param bool $cast If true, an object is always returned even if not an object. * @return array|null */ public function getInjectedValue($property, array $params, $cast = true) { // Get source for this value $source = $this->getValueSource($property); if (!$source) { return null; } // Look up the value - either from a callable, or from a directly provided value $res = []; if (isset($source['callable'])) { $res['value'] = $source['callable'](...$params); } elseif (isset($source['value'])) { $res['value'] = $source['value']; } else { throw new InvalidArgumentException( "Injected property $property doesn't have a value or callable value source provided" ); } // If we want to provide a casted object, look up what type object to use if ($cast) { $res['obj'] = $this->castValue($res['value'], $source); } return $res; } /** * Store the current overlay (as it doesn't directly apply to the new scope * that's being pushed). We want to store the overlay against the next item * "up" in the stack (hence upIndex), rather than the current item, because * SSViewer_Scope::obj() has already been called and pushed the new item to * the stack by this point * * @return SSViewer_Scope */ public function pushScope() { $scope = parent::pushScope(); $upIndex = $this->getUpIndex(); if ($upIndex !== null) { $itemStack = $this->getItemStack(); $itemStack[$upIndex][SSViewer_Scope::ITEM_OVERLAY] = $this->overlay; $this->setItemStack($itemStack); $this->overlay = []; } return $scope; } /** * Now that we're going to jump up an item in the item stack, we need to * restore the overlay that was previously stored against the next item "up" * in the stack from the current one * * @return SSViewer_Scope */ public function popScope() { $upIndex = $this->getUpIndex(); if ($upIndex !== null) { $itemStack = $this->getItemStack(); $this->overlay = $itemStack[$this->getUpIndex()][SSViewer_Scope::ITEM_OVERLAY]; } return parent::popScope(); } /** * $Up and $Top need to restore the overlay from the parent and top-level * scope respectively. * * @param string $name * @param array $arguments * @param bool $cache * @param string $cacheName * @return $this */ public function obj($name, $arguments = [], $cache = false, $cacheName = null) { $overlayIndex = false; switch ($name) { case 'Up': $upIndex = $this->getUpIndex(); if ($upIndex === null) { throw new \LogicException('Up called when we\'re already at the top of the scope'); } $overlayIndex = $upIndex; // Parent scope break; case 'Top': $overlayIndex = 0; // Top-level scope break; } if ($overlayIndex !== false) { $itemStack = $this->getItemStack(); if (!$this->overlay && isset($itemStack[$overlayIndex][SSViewer_Scope::ITEM_OVERLAY])) { $this->overlay = $itemStack[$overlayIndex][SSViewer_Scope::ITEM_OVERLAY]; } } parent::obj($name, $arguments, $cache, $cacheName); return $this; } /** * {@inheritdoc} */ public function getObj($name, $arguments = [], $cache = false, $cacheName = null) { $result = $this->getInjectedValue($name, (array)$arguments); if ($result) { return $result['obj']; } return parent::getObj($name, $arguments, $cache, $cacheName); } /** * {@inheritdoc} */ public function __call($name, $arguments) { // Extract the method name and parameters $property = $arguments[0]; // The name of the public function being called // The public function parameters in an array $params = (isset($arguments[1])) ? (array)$arguments[1] : []; $val = $this->getInjectedValue($property, $params); if ($val) { $obj = $val['obj']; if ($name === 'hasValue') { $result = ($obj instanceof ViewableData) ? $obj->exists() : (bool)$obj; } else { $result = $obj->forTemplate(); // XML_val } $this->resetLocalScope(); return $result; } return parent::__call($name, $arguments); } /** * Evaluate a template override * * @param string $property Name of override requested * @param array $overrides List of overrides available * @return null|array Null if not provided, or array with 'value' or 'callable' key */ protected function processTemplateOverride($property, $overrides) { if (!isset($overrides[$property])) { return null; } // Detect override type $override = $overrides[$property]; // Late-evaluate this value if (!is_string($override) && is_callable($override)) { $override = $override(); // Late override may yet return null if (!isset($override)) { return null; } } return [ 'value' => $override ]; } /** * Determine source to use for getInjectedValue * * @param string $property * @return array|null */ protected function getValueSource($property) { // Check for a presenter-specific override $overlay = $this->processTemplateOverride($property, $this->overlay); if (isset($overlay)) { return $overlay; } // Check if the method to-be-called exists on the target object - if so, don't check any further // injection locations $on = $this->itemIterator ? $this->itemIterator->current() : $this->item; if ($on && (isset($on->$property) || method_exists($on, $property ?? ''))) { return null; } // Check for a presenter-specific override $underlay = $this->processTemplateOverride($property, $this->underlay); if (isset($underlay)) { return $underlay; } // Then for iterator-specific overrides if (array_key_exists($property, self::$iteratorProperties)) { $source = self::$iteratorProperties[$property]; /** @var TemplateIteratorProvider $implementor */ $implementor = $source['implementor']; if ($this->itemIterator) { // Set the current iterator position and total (the object instance is the first item in // the callable array) $implementor->iteratorProperties( $this->itemIterator->key(), $this->itemIteratorTotal ); } else { // If we don't actually have an iterator at the moment, act like a list of length 1 $implementor->iteratorProperties(0, 1); } return $source; } // And finally for global overrides if (array_key_exists($property, self::$globalProperties)) { return self::$globalProperties[$property]; //get the method call } // No value return null; } /** * Ensure the value is cast safely * * @param mixed $value * @param array $source * @return DBField */ protected function castValue($value, $source) { // Already cast if (is_object($value)) { return $value; } // Get provided or default cast $casting = empty($source['casting']) ? ViewableData::config()->uninherited('default_cast') : $source['casting']; return DBField::create_field($casting, $value); } }