createCallableArray( self::$globalProperties, TemplateGlobalProvider::class, "get_template_global_variables" ); } // Build up iterator property providers array only once per request if (self::$iteratorProperties === null) { self::$iteratorProperties = array(); // Get all the exposed variables from all classes that implement the TemplateIteratorProvider interface // //call non-statically $this->createCallableArray( self::$iteratorProperties, TemplateIteratorProvider::class, "get_template_iterator_variables", true ); } $this->overlay = $overlay ? $overlay : array(); $this->underlay = $underlay ? $underlay : array(); } protected function createCallableArray(&$extraArray, $interfaceToQuery, $variableMethod, $createObject = false) { $implementers = ClassInfo::implementorsOf($interfaceToQuery); if ($implementers) { foreach ($implementers as $implementer) { // Create a new instance of the object for method calls if ($createObject) { $implementer = new $implementer(); } // Get the exposed variables $exposedVariables = call_user_func(array($implementer, $variableMethod)); foreach ($exposedVariables as $varName => $details) { if (!is_array($details)) { $details = array( 'method' => $details, 'casting' => ViewableData::config()->uninherited('default_cast') ); } // If just a value (and not a key => value pair), use it 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['implementer'] = $implementer; // And a callable array if (isset($details['method'])) { $details['callable'] = array($implementer, $details['method']); } // Save with both uppercase & lowercase first letter, so either works $lcFirst = strtolower($varName[0]) . substr($varName, 1); $extraArray[$lcFirst] = $details; $extraArray[ucfirst($varName)] = $details; } } } } /** * Get the injected value * * @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 Result array with the keys 'value' for raw value, or 'obj' if contained in an object * @throws InvalidArgumentException */ public function getInjectedValue($property, $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'] = call_user_func_array($source['callable'], $params); } elseif (isset($source['value'])) { $res['value'] = $source['value']; } else { throw new InvalidArgumentException( "Injected property $property does'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 = array(); } 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) { user_error('Up called when we\'re already at the top of the scope', E_USER_ERROR); } $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; } 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); } 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 if (isset($arguments[1]) && $arguments[1] != null) { $params = $arguments[1]; } else { $params = array(); } $val = $this->getInjectedValue($property, $params); if ($val) { $obj = $val['obj']; if ($name === 'hasValue') { $res = $obj instanceof ViewableData ? $obj->exists() : (bool)$obj; } else { // XML_val $res = $obj->forTemplate(); } $this->resetLocalScope(); return $res; } else { 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_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 (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 $implementer */ $implementer = $source['implementer']; if ($this->itemIterator) { // Set the current iterator position and total (the object instance is the first item in // the callable array) $implementer->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 $implementer->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); } }