silverstripe-framework/src/View/ViewableData.php

665 lines
20 KiB
PHP
Raw Normal View History

<?php
namespace SilverStripe\View;
use Exception;
2017-11-22 02:16:46 +01:00
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injectable;
2017-11-22 02:16:46 +01:00
use SilverStripe\Core\Manifest\ModuleResourceLoader;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\ArrayLib;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Debug;
use IteratorAggregate;
use LogicException;
use InvalidArgumentException;
use UnexpectedValueException;
use ArrayIterator;
/**
* A ViewableData object is any object that can be rendered into a template/view.
*
* A view interrogates the object being currently rendered in order to get data to render into the template. This data
* is provided and automatically escaped by ViewableData. Any class that needs to be available to a view (controllers,
* {@link DataObject}s, page controls) should inherit from this class.
*/
class ViewableData implements IteratorAggregate
2016-11-29 00:31:16 +01:00
{
use Extensible {
defineMethods as extensibleDefineMethods;
}
use Injectable;
use Configurable;
2016-11-29 00:31:16 +01:00
/**
* An array of objects to cast certain fields to. This is set up as an array in the format:
*
* <code>
* public static $casting = array (
* 'FieldName' => 'ClassToCastTo(Arguments)'
* );
* </code>
*
* @var array
* @config
*/
private static $casting = array(
'CSSClasses' => 'Varchar'
);
/**
* The default object to cast scalar fields to if casting information is not specified, and casting to an object
* is required.
*
* @var string
* @config
*/
private static $default_cast = 'Text';
/**
* @var array
*/
private static $casting_cache = array();
// -----------------------------------------------------------------------------------------------------------------
/**
* A failover object to attempt to get data from if it is not present on this object.
*
* @var ViewableData
*/
protected $failover;
/**
* @var ViewableData
*/
protected $customisedObject;
/**
* @var array
*/
private $objCache = array();
public function __construct()
{
}
2016-11-29 00:31:16 +01:00
// -----------------------------------------------------------------------------------------------------------------
// FIELD GETTERS & SETTERS -----------------------------------------------------------------------------------------
/**
* Check if a field exists on this object or its failover.
* Note that, unlike the core isset() implementation, this will return true if the property is defined
* and set to null.
*
* @param string $property
* @return bool
*/
public function __isset($property)
{
// getField() isn't a field-specific getter and shouldn't be treated as such
if (strtolower($property) !== 'field' && $this->hasMethod($method = "get$property")) {
return true;
} elseif ($this->hasField($property)) {
return true;
} elseif ($this->failover) {
return isset($this->failover->$property);
}
return false;
}
/**
* Get the value of a property/field on this object. This will check if a method called get{$property} exists, then
* check if a field is available using {@link ViewableData::getField()}, then fall back on a failover object.
*
* @param string $property
* @return mixed
*/
public function __get($property)
{
// getField() isn't a field-specific getter and shouldn't be treated as such
if (strtolower($property) !== 'field' && $this->hasMethod($method = "get$property")) {
return $this->$method();
} elseif ($this->hasField($property)) {
return $this->getField($property);
} elseif ($this->failover) {
return $this->failover->$property;
}
return null;
}
/**
* Set a property/field on this object. This will check for the existence of a method called set{$property}, then
* use the {@link ViewableData::setField()} method.
*
* @param string $property
* @param mixed $value
*/
public function __set($property, $value)
{
$this->objCacheClear();
if ($this->hasMethod($method = "set$property")) {
$this->$method($value);
} else {
$this->setField($property, $value);
}
}
/**
* Set a failover object to attempt to get data from if it is not present on this object.
*
* @param ViewableData $failover
*/
public function setFailover(ViewableData $failover)
{
// Ensure cached methods from previous failover are removed
if ($this->failover) {
$this->removeMethodsFrom('failover');
}
$this->failover = $failover;
$this->defineMethods();
}
/**
* Get the current failover object if set
*
* @return ViewableData|null
*/
public function getFailover()
{
return $this->failover;
}
/**
* Check if a field exists on this object. This should be overloaded in child classes.
*
* @param string $field
* @return bool
*/
public function hasField($field)
{
return property_exists($this, $field);
}
/**
* Get the value of a field on this object. This should be overloaded in child classes.
*
* @param string $field
* @return mixed
*/
public function getField($field)
{
return $this->$field;
}
/**
* Set a field on this object. This should be overloaded in child classes.
*
* @param string $field
* @param mixed $value
* @return $this
*/
public function setField($field, $value)
{
$this->objCacheClear();
$this->$field = $value;
return $this;
}
// -----------------------------------------------------------------------------------------------------------------
/**
* Add methods from the {@link ViewableData::$failover} object, as well as wrapping any methods prefixed with an
* underscore into a {@link ViewableData::cachedCall()}.
*
* @throws LogicException
*/
public function defineMethods()
{
if ($this->failover && !is_object($this->failover)) {
throw new LogicException("ViewableData::\$failover set to a non-object");
}
if ($this->failover) {
$this->addMethodsFrom('failover');
if (isset($_REQUEST['debugfailover'])) {
$class = static::class;
$failoverClass = get_class($this->failover);
Debug::message("$class created with a failover class of {$failoverClass}");
2016-11-29 00:31:16 +01:00
}
}
$this->extensibleDefineMethods();
2016-11-29 00:31:16 +01:00
}
/**
* Merge some arbitrary data in with this object. This method returns a {@link ViewableData_Customised} instance
* with references to both this and the new custom data.
*
* Note that any fields you specify will take precedence over the fields on this object.
*
* @param array|ViewableData $data
* @return ViewableData_Customised
*/
public function customise($data)
{
if (is_array($data) && (empty($data) || ArrayLib::is_associative($data))) {
$data = new ArrayData($data);
}
if ($data instanceof ViewableData) {
return new ViewableData_Customised($this, $data);
}
throw new InvalidArgumentException(
'ViewableData->customise(): $data must be an associative array or a ViewableData instance'
);
}
/**
* Return true if this object "exists" i.e. has a sensible value
*
* This method should be overriden in subclasses to provide more context about the classes state. For example, a
* {@link DataObject} class could return false when it is deleted from the database
*
* @return bool
*/
public function exists()
{
return true;
}
/**
* @return string the class name
*/
public function __toString()
{
return static::class;
}
2016-11-29 00:31:16 +01:00
/**
* @return ViewableData
*/
public function getCustomisedObj()
{
return $this->customisedObject;
}
/**
* @param ViewableData $object
*/
public function setCustomisedObj(ViewableData $object)
{
$this->customisedObject = $object;
}
// CASTING ---------------------------------------------------------------------------------------------------------
/**
* Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object)
* for a field on this object. This helper will be a subclass of DBField.
*
* @param string $field
* @return string Casting helper As a constructor pattern, and may include arguments.
* @throws Exception
2016-11-29 00:31:16 +01:00
*/
public function castingHelper($field)
{
$specs = static::config()->get('casting');
2016-11-29 00:31:16 +01:00
if (isset($specs[$field])) {
return $specs[$field];
}
// If no specific cast is declared, fall back to failover.
// Note that if there is a failover, the default_cast will always
// be drawn from this object instead of the top level object.
$failover = $this->getFailover();
if ($failover) {
$cast = $failover->castingHelper($field);
if ($cast) {
return $cast;
}
}
// Fall back to default_cast
$default = $this->config()->get('default_cast');
2017-02-22 04:14:53 +01:00
if (empty($default)) {
throw new Exception("No default_cast");
2017-02-22 04:14:53 +01:00
}
return $default;
2016-11-29 00:31:16 +01:00
}
/**
* Get the class name a field on this object will be casted to.
*
* @param string $field
* @return string
*/
public function castingClass($field)
{
// Strip arguments
$spec = $this->castingHelper($field);
return trim(strtok($spec, '('));
}
/**
* Return the string-format type for the given field.
*
* @param string $field
* @return string 'xml'|'raw'
*/
public function escapeTypeForField($field)
{
2017-02-22 04:14:53 +01:00
$class = $this->castingClass($field) ?: $this->config()->get('default_cast');
2016-11-29 00:31:16 +01:00
// TODO: It would be quicker not to instantiate the object, but to merely
// get its class from the Injector
2017-02-22 04:14:53 +01:00
/** @var DBField $type */
$type = Injector::inst()->get($class, true);
return $type->config()->get('escape_type');
2016-11-29 00:31:16 +01:00
}
// TEMPLATE ACCESS LAYER -------------------------------------------------------------------------------------------
/**
* Render this object into the template, and get the result as a string. You can pass one of the following as the
* $template parameter:
* - a template name (e.g. Page)
* - an array of possible template names - the first valid one will be used
* - an SSViewer instance
*
* @param string|array|SSViewer $template the template to render into
* @param array $customFields fields to customise() the object with before rendering
* @return DBHTMLText
*/
public function renderWith($template, $customFields = null)
{
if (!is_object($template)) {
$template = SSViewer::create($template);
2016-11-29 00:31:16 +01:00
}
API Refactor bootstrap, request handling See https://github.com/silverstripe/silverstripe-framework/pull/7037 and https://github.com/silverstripe/silverstripe-framework/issues/6681 Squashed commit of the following: commit 8f65e5653211240650eaa4fa65bb83b45aae6d58 Author: Ingo Schommer <me@chillu.com> Date: Thu Jun 22 22:25:50 2017 +1200 Fixed upgrade guide spelling commit 76f95944fa89b0b540704b8d744329f690f9698c Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 16:38:34 2017 +1200 BUG Fix non-test class manifest including sapphiretest / functionaltest commit 9379834cb4b2e5177a2600049feec05bf111c16b Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 15:50:47 2017 +1200 BUG Fix nesting bug in Kernel commit 188ce35d82599360c40f0f2de29579c56fb90761 Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 15:14:51 2017 +1200 BUG fix db bootstrapping issues commit 7ed4660e7a63915e8e974deeaba9807bc4d38b0d Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 14:49:07 2017 +1200 BUG Fix issue in DetailedErrorFormatter commit 738f50c497166f81ccbe3f40fbcff895ce71f82f Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 11:49:19 2017 +1200 Upgrading notes on mysite/_config.php commit 6279d28e5e455916f902a2f963c014d8899f7fc7 Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 11:43:28 2017 +1200 Update developer documentation commit 5c90d53a84ef0139c729396949a7857fae60436f Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 22 10:48:44 2017 +1200 Update installer to not use global databaseConfig commit f9b2ba4755371f08bd95f6908ac612fcbb7ca205 Author: Damian Mooyman <damian@silverstripe.com> Date: Wed Jun 21 21:04:39 2017 +1200 Fix behat issues commit 5b59a912b60282b4dad4ef10ed3b97c5d0a761ac Author: Damian Mooyman <damian@silverstripe.com> Date: Wed Jun 21 17:07:11 2017 +1200 Move HTTPApplication to SilverStripe\Control namespace commit e2c4a18f637bdd3d276619554de60ee8b4d95ced Author: Damian Mooyman <damian@silverstripe.com> Date: Wed Jun 21 16:29:03 2017 +1200 More documentation Fix up remaining tests Refactor temp DB into TempDatabase class so it’s available outside of unit tests. commit 5d235e64f341d6251bfe9f4833f15cc8593c5034 Author: Damian Mooyman <damian@silverstripe.com> Date: Wed Jun 21 12:13:15 2017 +1200 API HTTPRequestBuilder::createFromEnvironment() now cleans up live globals BUG Fix issue with SSViewer Fix Security / View tests commit d88d4ed4e48291cb65407f222f190064b1f1deeb Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 20 16:39:43 2017 +1200 API Refactor AppKernel into CoreKernel commit f7946aec3391139ae1b4029c353c327a36552b36 Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 20 16:00:40 2017 +1200 Docs and minor cleanup commit 12bd31f9366327650b5c0c0f96cd0327d44faf0a Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 20 15:34:34 2017 +1200 API Remove OutputMiddleware API Move environment / global / ini management into Environment class API Move getTempFolder into TempFolder class API Implement HTTPRequestBuilder / CLIRequestBuilder BUG Restore SS_ALLOWED_HOSTS check in original location API CoreKernel now requires $basePath to be passed in API Refactor installer.php to use application to bootstrap API move memstring conversion globals to Convert BUG Fix error in CoreKernel nesting not un-nesting itself properly. commit bba979114624247cf463cf2a8c9e4be9a7c3a772 Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 19 18:07:53 2017 +1200 API Create HTTPMiddleware and standardise middleware for request handling commit 2a10c2397bdc53001013f607b5d38087ce6c0730 Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 19 17:42:42 2017 +1200 Fixed ORM tests commit d75a8d1d93398af4bd0432df9e4bc6295c15a3fe Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 19 17:15:07 2017 +1200 FIx i18n tests commit 06364af3c379c931889c4cc34dd920fee3db204a Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 19 16:59:34 2017 +1200 Fix controller namespace Move states to sub namespace commit 2a278e2953d2dbb19f78d91c919048e1fc935436 Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 19 12:49:45 2017 +1200 Fix forms namespace commit b65c21241bee019730027071d815dbf7571197a4 Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 15 18:56:48 2017 +1200 Update API usages commit d1d4375c95a264a6b63cbaefc2c1d12f808bfd82 Author: Damian Mooyman <damian@silverstripe.com> Date: Thu Jun 15 18:41:44 2017 +1200 API Refactor $flush into HTPPApplication API Enforce health check in Controller::pushCurrent() API Better global backup / restore Updated Director::test() to use new API commit b220534f06732db4fa940d8724c2a85c0ba2495a Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 22:05:57 2017 +1200 Move app nesting to a test state helper commit 603704165c08d0c1c81fd5e6bb9506326eeee17b Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 21:46:04 2017 +1200 Restore kernel stack to fix multi-level nesting commit 2f6336a15bf79dc8c2edd44cec1931da2dd51c28 Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 17:23:21 2017 +1200 API Implement kernel nesting commit fc7188da7d6ad6785354bab61f08700454c81d91 Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 15:43:13 2017 +1200 Fix core tests commit a0ae7235148fffd71f2f02d1fe7fe45bf3aa39eb Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 15:23:52 2017 +1200 Fix manifest tests commit ca033952513633e182040d3d13e1caa9000ca184 Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 15:00:00 2017 +1200 API Move extension management into test state commit c66d4339777663a8a04661fea32a0cf35b95d20f Author: Damian Mooyman <damian@silverstripe.com> Date: Tue Jun 13 14:10:59 2017 +1200 API Refactor SapphireTest state management into SapphireTestState API Remove Injector::unregisterAllObjects() API Remove FakeController commit f26ae75c6ecaafa0dec1093264e0187191e6764d Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 12 18:04:34 2017 +1200 Implement basic CLI application object commit 001d5596621404892de0a5413392379eff990641 Author: Damian Mooyman <damian@silverstripe.com> Date: Mon Jun 12 17:39:38 2017 +1200 Remove references to SapphireTest::is_running_test() Upgrade various code commit de079c041dacd96bc4f4b66421fa2b2cc4c320f8 Author: Damian Mooyman <damian@silverstripe.com> Date: Wed Jun 7 18:07:33 2017 +1200 API Implement APP object API Refactor of Session
2017-06-22 12:50:45 +02:00
$data = $this->getCustomisedObj() ?: $this;
2016-11-29 00:31:16 +01:00
if ($customFields instanceof ViewableData) {
$data = $data->customise($customFields);
}
if ($template instanceof SSViewer) {
return $template->process($data, is_array($customFields) ? $customFields : null);
}
throw new UnexpectedValueException(
"ViewableData::renderWith(): unexpected ".get_class($template)." object, expected an SSViewer instance"
);
}
/**
* Generate the cache name for a field
*
* @param string $fieldName Name of field
* @param array $arguments List of optional arguments given
* @return string
*/
protected function objCacheName($fieldName, $arguments)
{
return $arguments
? $fieldName . ":" . implode(',', $arguments)
: $fieldName;
}
/**
* Get a cached value from the field cache
*
* @param string $key Cache key
* @return mixed
*/
protected function objCacheGet($key)
{
if (isset($this->objCache[$key])) {
return $this->objCache[$key];
}
return null;
}
/**
* Store a value in the field cache
*
* @param string $key Cache key
* @param mixed $value
* @return $this
*/
protected function objCacheSet($key, $value)
{
$this->objCache[$key] = $value;
return $this;
}
/**
* Clear object cache
*
* @return $this
*/
protected function objCacheClear()
{
$this->objCache = [];
return $this;
}
/**
* Get the value of a field on this object, automatically inserting the value into any available casting objects
* that have been specified.
*
* @param string $fieldName
* @param array $arguments
* @param bool $cache Cache this object
* @param string $cacheName a custom cache name
* @return Object|DBField
*/
public function obj($fieldName, $arguments = [], $cache = false, $cacheName = null)
{
if (!$cacheName && $cache) {
$cacheName = $this->objCacheName($fieldName, $arguments);
}
// Check pre-cached value
$value = $cache ? $this->objCacheGet($cacheName) : null;
if ($value !== null) {
return $value;
}
// Load value from record
if ($this->hasMethod($fieldName)) {
$value = call_user_func_array(array($this, $fieldName), $arguments ?: []);
} else {
$value = $this->$fieldName;
}
// Cast object
if (!is_object($value)) {
// Force cast
$castingHelper = $this->castingHelper($fieldName);
$valueObject = Injector::inst()->create($castingHelper, $fieldName);
2016-11-29 00:31:16 +01:00
$valueObject->setValue($value, $this);
$value = $valueObject;
}
// Record in cache
if ($cache) {
$this->objCacheSet($cacheName, $value);
}
return $value;
}
/**
* A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again
* without re-running the method.
*
* @param string $field
* @param array $arguments
* @param string $identifier an optional custom cache identifier
* @return Object|DBField
*/
public function cachedCall($field, $arguments = [], $identifier = null)
{
return $this->obj($field, $arguments, true, $identifier);
}
/**
* Checks if a given method/field has a valid value. If the result is an object, this will return the result of the
* exists method, otherwise will check if the result is not just an empty paragraph tag.
*
* @param string $field
* @param array $arguments
* @param bool $cache
* @return bool
*/
public function hasValue($field, $arguments = [], $cache = true)
{
$result = $this->obj($field, $arguments, $cache);
return $result->exists();
}
/**
* Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
* template.
*
* @param string $field
* @param array $arguments
* @param bool $cache
* @return string
*/
public function XML_val($field, $arguments = [], $cache = false)
{
$result = $this->obj($field, $arguments, $cache);
// Might contain additional formatting over ->XML(). E.g. parse shortcodes, nl2br()
return $result->forTemplate();
}
/**
* Get an array of XML-escaped values by field name
*
* @param array $fields an array of field names
* @return array
*/
public function getXMLValues($fields)
{
$result = array();
foreach ($fields as $field) {
$result[$field] = $this->XML_val($field);
}
return $result;
}
// ITERATOR SUPPORT ------------------------------------------------------------------------------------------------
/**
* Return a single-item iterator so you can iterate over the fields of a single record.
*
* This is useful so you can use a single record inside a <% control %> block in a template - and then use
* to access individual fields on this object.
*
* @return ArrayIterator
*/
public function getIterator()
{
return new ArrayIterator(array($this));
}
// UTILITY METHODS -------------------------------------------------------------------------------------------------
/**
* When rendering some objects it is necessary to iterate over the object being rendered, to do this, you need
* access to itself.
*
* @return ViewableData
*/
public function Me()
{
return $this;
}
2017-11-22 02:16:46 +01:00
/**
* Return the directory if the current active theme (relative to the site root).
*
* This method is useful for things such as accessing theme images from your template without hardcoding the theme
* page - e.g. <img src="$ThemeDir/images/something.gif">.
*
* This method should only be used when a theme is currently active. However, it will fall over to the current
* project directory.
*
* @return string URL to the current theme
* @deprecated 4.0.0..5.0.0 Use $resourcePath or $resourceURL template helpers instead
*/
public function ThemeDir()
{
Deprecation::notice('5.0', 'Use $resourcePath or $resourceURL template helpers instead');
$themes = SSViewer::get_themes();
foreach ($themes as $theme) {
// Skip theme sets
if (strpos($theme, '$') === 0) {
continue;
}
// Map theme path to url
$themePath = ThemeResourceLoader::inst()->getPath($theme);
return ModuleResourceLoader::resourceURL($themePath);
}
return project();
}
2016-11-29 00:31:16 +01:00
/**
* Get part of the current classes ancestry to be used as a CSS class.
*
* This method returns an escaped string of CSS classes representing the current classes ancestry until it hits a
* stop point - e.g. "Page DataObject ViewableData".
*
* @param string $stopAtClass the class to stop at (default: ViewableData)
* @return string
* @uses ClassInfo
*/
public function CSSClasses($stopAtClass = self::class)
2016-11-29 00:31:16 +01:00
{
$classes = array();
$classAncestry = array_reverse(ClassInfo::ancestry(static::class));
2016-11-29 00:31:16 +01:00
$stopClasses = ClassInfo::ancestry($stopAtClass);
foreach ($classAncestry as $class) {
if (in_array($class, $stopClasses)) {
break;
}
$classes[] = $class;
}
// optionally add template identifier
if (isset($this->template) && !in_array($this->template, $classes)) {
$classes[] = $this->template;
}
// Strip out namespaces
$classes = preg_replace('#.*\\\\#', '', $classes);
return Convert::raw2att(implode(' ', $classes));
}
/**
* Return debug information about this object that can be rendered into a template
*
* @return ViewableData_Debugger
*/
public function Debug()
{
return new ViewableData_Debugger($this);
}
}