Version: current

       1 <?php
       2 /**
       3  * A ViewableData object is any object that can be rendered into a template/view.
       4  *
       5  * A view interrogates the object being currently rendered in order to get data to render into the template. This data
       6  * is provided and automatically escaped by ViewableData. Any class that needs to be available to a view (controllers,
       7  * {@link DataObject}s, page controls) should inherit from this class.
       8  *
       9  * @package sapphire
      10  * @subpackage view
      11  */
      12 class ViewableData extends Object implements IteratorAggregate {
      13
      14     /**
      15      * An array of objects to cast certain fields to. This is set up as an array in the format:
      16      *
      17      * <code>
      18      * public static $casting = array (
      19      *     'FieldName' => 'ClassToCastTo(Arguments)'
      20      * );
      21      * </code>
      22      *
      23      * @var array
      24      */
      25     public static $casting = array (
      26         'BaseHref'   => 'Varchar',
      27         'CSSClasses' => 'Varchar'
      28     );
      29
      30     /**
      31      * The default object to cast scalar fields to if casting information is not specified, and casting to an object
      32      * is required.
      33      *
      34      * @var string
      35      */
      36     public static $default_cast = 'HTMLVarchar';
      37
      38     /**
      39      * @var array
      40      */
      41     private static $casting_cache = array();
      42
      43     // -----------------------------------------------------------------------------------------------------------------
      44
      45     /**
      46      * @var int
      47      */
      48     protected $iteratorPos, $iteratorTotalItems;
      49
      50     /**
      51      * A failover object to attempt to get data from if it is not present on this object.
      52      *
      53      * @var ViewableData
      54      */
      55     protected $failover;
      56
      57     /**
      58      * @var ViewableData
      59      */
      60     protected $customisedObject;
      61
      62     /**
      63      * @var array
      64      */
      65     private $objCache = array();
      66
      67     // -----------------------------------------------------------------------------------------------------------------
      68
      69     /**
      70      * Converts a field spec into an object creator. For example: "Int" becomes "new Int($fieldName);" and "Varchar(50)"
      71      * becomes "new Varchar($fieldName, 50);".
      72      *
      73      * @param string $fieldSchema The field spec
      74      * @return string
      75      */
      76     public static function castingObjectCreator($fieldSchema) {
      77         user_error("Deprecated in a breaking way, use Object::create_from_string()", E_USER_WARNING);
      78     }
      79
      80     /**
      81      * Convert a field schema (e.g. "Varchar(50)") into a casting object creator array that contains both a className
      82      * and castingHelper constructor code. See {@link castingObjectCreator} for more information about the constructor.
      83      *
      84      * @param string $fieldSchema
      85      * @return array
      86      */
      87     public static function castingObjectCreatorPair($fieldSchema) {
      88         user_error("Deprecated in a breaking way, use Object::create_from_string()", E_USER_WARNING);
      89     }
      90
      91     // FIELD GETTERS & SETTERS -----------------------------------------------------------------------------------------
      92
      93     /**
      94      * Check if a field exists on this object or its failover.
      95      *
      96      * @param string $property
      97      * @return bool
      98      */
      99     public function __isset($property) {
     100         return $this->hasField($property) || ($this->failover && $this->failover->hasField($property));
     101     }
     102
     103     /**
     104      * Get the value of a property/field on this object. This will check if a method called get{$property} exists, then
     105      * check if a field is available using {@link ViewableData::getField()}, then fall back on a failover object.
     106      *
     107      * @param string $property
     108      * @return mixed
     109      */
     110     public function __get($property) {
     111         if($this->hasMethod($method = "get$property")) {
     112             return $this->$method();
     113         } elseif($this->hasField($property)) {
     114             return $this->getField($property);
     115         } elseif($this->failover) {
     116             return $this->failover->$property;
     117         }
     118     }
     119
     120     /**
     121      * Set a property/field on this object. This will check for the existence of a method called set{$property}, then
     122      * use the {@link ViewableData::setField()} method.
     123      *
     124      * @param string $property
     125      * @param mixed $value
     126      */
     127     public function __set($property, $value) {
     128         if($this->hasMethod($method = "set$property")) {
     129             $this->$method($value);
     130         } else {
     131             $this->setField($property, $value);
     132         }
     133     }
     134
     135     /**
     136      * Check if a field exists on this object. This should be overloaded in child classes.
     137      *
     138      * @param string $field
     139      * @return bool
     140      */
     141     public function hasField($field) {
     142         return property_exists($this, $field);
     143     }
     144
     145     /**
     146      * Get the value of a field on this object. This should be overloaded in child classes.
     147      *
     148      * @param string $field
     149      * @return mixed
     150      */
     151     public function getField($field) {
     152         return $this->$field;
     153     }
     154
     155     /**
     156      * Set a field on this object. This should be overloaded in child classes.
     157      *
     158      * @param string $field
     159      * @param mixed $value
     160      */
     161     public function setField($field, $value) {
     162         $this->$field = $value;
     163     }
     164
     165     // -----------------------------------------------------------------------------------------------------------------
     166
     167     /**
     168      * Add methods from the {@link ViewableData::$failover} object, as well as wrapping any methods prefixed with an
     169      * underscore into a {@link ViewableData::cachedCall()}.
     170      */
     171     public function defineMethods() {
     172         if($this->failover) {
     173             if(is_object($this->failover)) $this->addMethodsFrom('failover');
     174             else user_error("ViewableData::\$failover set to a non-object", E_USER_WARNING);
     175
     176             if(isset($_REQUEST['debugfailover'])) {
     177                 Debug::message("$this->class created with a failover class of {$this->failover->class}");
     178             }
     179         }
     180
     181         foreach($this->allMethodNames() as $method) {
     182             if($method[0] == '_' && $method[1] != '_') {
     183                 $this->createMethod (
     184                     substr($method, 1), "return \$obj->cachedCall('$method', '" . substr($method, 1) . "', \$args);"
     185                 );
     186             }
     187         }
     188
     189         parent::defineMethods();
     190     }
     191
     192     /**
     193      * Merge some arbitrary data in with this object. This method returns a {@link ViewableData_Customised} instance
     194      * with references to both this and the new custom data.
     195      *
     196      * Note that any fields you specify will take precedence over the fields on this object.
     197      *
     198      * @param array|ViewableData $data
     199      * @return ViewableData_Customised
     200      */
     201     public function customise($data) {
     202         if(is_array($data) && (empty($data) || ArrayLib::is_associative($data))) {
     203             $data = new ArrayData($data);
     204         }
     205
     206         if($data instanceof ViewableData) {
     207             return new ViewableData_Customised($this, $data);
     208         }
     209
     210         throw new InvalidArgumentException (
     211             'ViewableData->customise(): $data must be an associative array or a ViewableData instance'
     212         );
     213     }
     214
     215     /**
     216      * @param ViewableData $object
     217      */
     218     public function setCustomisedObj(ViewableData $object) {
     219         $this->customisedObject = $object;
     220     }
     221
     222     // CASTING ---------------------------------------------------------------------------------------------------------
     223
     224     /**
     225      * Get the class a field on this object would be casted to, as well as the casting helper for casting a field to
     226      * an object (see {@link ViewableData::castingHelper()} for information on casting helpers).
     227      *
     228      * The returned array contains two keys:
     229      *  - className: the class the field would be casted to (e.g. "Varchar")
     230      *  - castingHelper: the casting helper for casting the field (e.g. "return new Varchar($fieldName)")
     231      *
     232      * @param string $field
     233      * @return array
     234      */
     235     public function castingHelperPair($field) {
     236         user_error("castingHelperPair() Deprecated, use castingHelper() instead", E_USER_NOTICE);
     237         return $this->castingHelper($field);
     238     }
     239
     240     /**
     241      * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field
     242      * on this object.
     243      *
     244      * @param string $field
     245      * @return string
     246      */
     247     public function castingHelper($field) {
     248         if($this->hasMethod('db') && $fieldSpec = $this->db($field)) {
     249             return $fieldSpec;
     250         }
     251
     252         $specs = Object::combined_static(get_class($this), 'casting');
     253         if(isset($specs[$field])) return $specs[$field];
     254
     255         if($this->failover) return $this->failover->castingHelper($field);
     256     }
     257
     258     /**
     259      * Get the class name a field on this object will be casted to
     260      *
     261      * @param string $field
     262      * @return string
     263      */
     264     public function castingClass($field) {
     265         $spec = $this->castingHelper($field);
     266         if(!$spec) return null;
     267
     268         $bPos = strpos($spec,'(');
     269         if($bPos === false) return $spec;
     270         else return substr($spec, 0, $bPos-1);
     271     }
     272
     273     /**
     274      * Return the string-format type for the given field.
     275      *
     276      * @param string $field
     277      * @return string 'xml'|'raw'
     278      */
     279     public function escapeTypeForField($field) {
     280         if(!$class = $this->castingClass($field)) {
     281             $class = self::$default_cast;
     282         }
     283
     284         return Object::get_static($class, 'escape_type');
     285     }
     286
     287     /**
     288      * Save the casting cache for this object (including data from any failovers) into a variable
     289      *
     290      * @param reference $cache
     291      */
     292     public function buildCastingCache(&$cache) {
     293         $ancestry = array_reverse(ClassInfo::ancestry($this->class));
     294         $merge    = true;
     295
     296         foreach($ancestry as $class) {
     297             if(!isset(self::$casting_cache[$class]) && $merge) {
     298                 $mergeFields = is_subclass_of($class, 'DataObject') ? array('db', 'casting') : array('casting');
     299
     300                 if($mergeFields) foreach($mergeFields as $field) {
     301                     $casting = Object::uninherited_static($class, $field);
     302
     303                     if($casting) foreach($casting as $field => $cast) {
     304                         if(!isset($cache[$field])) $cache[$field] = self::castingObjectCreatorPair($cast);
     305                     }
     306                 }
     307
     308                 if($class == 'ViewableData') $merge = false;
     309             } elseif($merge) {
     310                 $cache = ($cache) ? array_merge(self::$casting_cache[$class], $cache) : self::$casting_cache[$class];
     311             }
     312
     313             if($class == 'ViewableData') break;
     314         }
     315     }
     316
     317     // TEMPLATE ACCESS LAYER -------------------------------------------------------------------------------------------
     318
     319     /**
     320      * Render this object into the template, and get the result as a string. You can pass one of the following as the
     321      * $template parameter:
     322      *  - a template name (e.g. Page)
     323      *  - an array of possible template names - the first valid one will be used
     324      *  - an SSViewer instance
     325      *
     326      * @param string|array|SSViewer $template the template to render into
     327      * @param array $customFields fields to customise() the object with before rendering
     328      * @return string
     329      */
     330     public function renderWith($template, $customFields = null) {
     331         if(!is_object($template)) {
     332             $template = new SSViewer($template);
     333         }
     334
     335         $data = ($this->customisedObject) ? $this->customisedObject : $this;
     336
     337         if(is_array($customFields) || $customFields instanceof ViewableData) {
     338             $data = $data->customise($customFields);
     339         }
     340
     341         if($template instanceof SSViewer) {
     342             return $template->process($data);
     343         }
     344
     345         throw new UnexpectedValueException (
     346             "ViewableData::renderWith(): unexpected $template->class object, expected an SSViewer instance"
     347         );
     348     }
     349
     350     /**
     351      * Get the value of a field on this object, automatically inserting the value into any available casting objects
     352      * that have been specified.
     353      *
     354      * @param string $fieldName
     355      * @param array $arguments
     356      * @param bool $forceReturnedObject if TRUE, the value will ALWAYS be casted to an object before being returned,
     357      *        even if there is no explicit casting information
     358      * @param string $cacheName a custom cache name
     359      */
     360     public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
     361         if(isset($_REQUEST['debug_profile'])) {
     362             Profiler::mark("obj.$fieldName", "on a $this->class object");
     363         }
     364
     365         if(!$cacheName) $cacheName = $arguments ? $fieldName . implode(',', $arguments) : $fieldName;
     366
     367         if(!isset($this->objCache[$cacheName])) {
     368             if($this->hasMethod($fieldName)) {
     369                 $value = $arguments ? call_user_func_array(array($this, $fieldName), $arguments) : $this->$fieldName();
     370             } else {
     371                 $value = $this->$fieldName;
     372             }
     373
     374             if(!is_object($value) && ($this->castingClass($fieldName) || $forceReturnedObject)) {
     375                 if(!$castConstructor = $this->castingHelper($fieldName)) {
     376                     $castConstructor = $this->stat('default_cast');
     377                 }
     378
     379                 $valueObject = Object::create_from_string($castConstructor, $fieldName);
     380                 $valueObject->setValue($value, ($this->hasMethod('getAllFields') ? $this->getAllFields() : null));
     381
     382                 $value = $valueObject;
     383             }
     384
     385             if($cache) $this->objCache[$cacheName] = $value;
     386         } else {
     387             $value = $this->objCache[$cacheName];
     388         }
     389
     390         if(isset($_REQUEST['debug_profile'])) {
     391             Profiler::unmark("obj.$fieldName", "on a $this->class object");
     392         }
     393
     394         if(!is_object($value) && $forceReturnedObject) {
     395             $default = Object::get_static('ViewableData', 'default_cast');
     396             $value   = new $default($fieldName);
     397         }
     398
     399         return $value;
     400     }
     401
     402     /**
     403      * A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again
     404      * without re-running the method.
     405      *
     406      * @param string $field
     407      * @param array $arguments
     408      * @param string $identifier an optional custom cache identifier
     409      */
     410     public function cachedCall($field, $arguments = null, $identifier = null) {
     411         return $this->obj($field, $arguments, false, true, $identifier);
     412     }
     413
     414     /**
     415      * Checks if a given method/field has a valid value. If the result is an object, this will return the result of the
     416      * exists method, otherwise will check if the result is not just an empty paragraph tag.
     417      *
     418      * @param string $field
     419      * @param array $arguments
     420      * @param bool $cache
     421      * @return bool
     422      */
     423     public function hasValue($field, $arguments = null, $cache = true) {
     424         if (!$field) {
     425             return false;
     426         }
     427         $result = $cache ? $this->cachedCall($field, $arguments) : $this->obj($field, $arguments, false, false);
     428
     429         if(is_object($result)) {
     430             return $result->exists();
     431         } else {
     432             return ($result && $result !== '<p></p>');
     433         }
     434     }
     435
     436     /**#@+
     437      * @param string $field
     438      * @param array $arguments
     439      * @param bool $cache
     440      * @return string
     441      */
     442
     443     /**
     444      * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
     445      * template.
     446      */
     447     public function XML_val($field, $arguments = null, $cache = false) {
     448         $result = $this->obj($field, $arguments, false, $cache);
     449         return (is_object($result) && method_exists($result, 'forTemplate')) ? $result->forTemplate() : $result;
     450     }
     451
     452     /**
     453      * Return the value of the field without any escaping being applied.
     454      */
     455     public function RAW_val($field, $arguments = null, $cache = true) {
     456         return Convert::xml2raw($this->XML_val($field, $arguments, $cache));
     457     }
     458
     459     /**
     460      * Return the value of a field in an SQL-safe format.
     461      */
     462     public function SQL_val($field, $arguments = null, $cache = true) {
     463         return Convert::raw2sql($this->RAW_val($field, $arguments, $cache));
     464     }
     465
     466     /**
     467      * Return the value of a field in a JavaScript-save format.
     468      */
     469     public function JS_val($field, $arguments = null, $cache = true) {
     470         return Convert::raw2js($this->RAW_val($field, $arguments, $cache));
     471     }
     472
     473     /**
     474      * Return the value of a field escaped suitable to be inserted into an XML node attribute.
     475      */
     476     public function ATT_val($field, $arguments = null, $cache = true) {
     477         return Convert::raw2att($this->RAW_val($field, $arguments, $cache));
     478     }
     479
     480     /**#@-*/
     481
     482     /**
     483      * Get an array of XML-escaped values by field name
     484      *
     485      * @param array $elements an array of field names
     486      * @return array
     487      */
     488     public function getXMLValues($fields) {
     489         $result = array();
     490
     491         foreach($fields as $field) {
     492             $result[$field] = $this->XML_val($field);
     493         }
     494
     495         return $result;
     496     }
     497
     498     // ITERATOR SUPPORT ------------------------------------------------------------------------------------------------
     499
     500     /**
     501      * Return a single-item iterator so you can iterate over the fields of a single record.
     502      *
     503      * This is useful so you can use a single record inside a <% control %> block in a template - and then use
     504      * to access individual fields on this object.
     505      *
     506      * @return ArrayIterator
     507      */
     508     public function getIterator() {
     509         return new ArrayIterator(array($this));
     510     }
     511
     512     /**
     513      * Set the current iterator properties - where we are on the iterator.
     514      *
     515      * @param int $pos position in iterator
     516      * @param int $totalItems total number of items
     517      */
     518     public function iteratorProperties($pos, $totalItems) {
     519         $this->iteratorPos        = $pos;
     520         $this->iteratorTotalItems = $totalItems;
     521     }
     522
     523     /**
     524      * Returns true if this object is the first in a set.
     525      *
     526      * @return bool
     527      */
     528     public function First() {
     529         return $this->iteratorPos == 0;
     530     }
     531
     532     /**
     533      * Returns true if this object is the last in a set.
     534      *
     535      * @return bool
     536      */
     537     public function Last() {
     538         return $this->iteratorPos == $this->iteratorTotalItems - 1;
     539     }
     540
     541     /**
     542      * Returns 'first' or 'last' if this is the first or last object in the set.
     543      *
     544      * @return string|null
     545      */
     546     public function FirstLast() {
     547         if($this->First()) return 'first';
     548         if($this->Last())  return 'last';
     549     }
     550
     551     /**
     552      * Return true if this object is between the first & last objects.
     553      *
     554      * @return bool
     555      */
     556     public function Middle() {
     557         return !$this->First() && !$this->Last();
     558     }
     559
     560     /**
     561      * Return 'middle' if this object is between the first & last objects.
     562      *
     563      * @return string|null
     564      */
     565     public function MiddleString() {
     566         if($this->Middle()) return 'middle';
     567     }
     568
     569     /**
     570      * Return true if this object is an even item in the set.
     571      *
     572      * @return bool
     573      */
     574     public function Even() {
     575         return (bool) ($this->iteratorPos % 2);
     576     }
     577
     578     /**
     579      * Return true if this is an odd item in the set.
     580      *
     581      * @return bool
     582      */
     583     public function Odd() {
     584         return !$this->Even();
     585     }
     586
     587     /**
     588      * Return 'even' or 'odd' if this object is in an even or odd position in the set respectively.
     589      *
     590      * @return string
     591      */
     592     public function EvenOdd() {
     593         return ($this->Even()) ? 'even' : 'odd';
     594     }
     595
     596     /**
     597      * Return the numerical position of this object in the container set. The count starts at $startIndex.
     598      *
     599      * @param int $startIndex Number to start count from.
     600      * @return int
     601      */
     602     public function Pos($startIndex = 1) {
     603         return $this->iteratorPos + $startIndex;
     604     }
     605
     606     /**
     607      * Return the total number of "sibling" items in the dataset.
     608      *
     609      * @return int
     610      */
     611     public function TotalItems() {
     612         return $this->iteratorTotalItems;
     613     }
     614
     615     /**
     616      * Returns the modulus of the numerical position of the item in the data set.
     617      * The count starts from $startIndex, which defaults to 1.
     618      * @param int $Mod The number to perform Mod operation to.
     619      * @param int $startIndex Number to start count from.
     620      * @return int
     621      */
     622     public function Modulus($mod, $startIndex = 1) {
     623         return ($this->iteratorPos + $startIndex) % $mod;
     624     }
     625
     626     public function MultipleOf($factor, $offset = 1) {
     627         return ($this->Modulus($factor, $offset) == 0);
     628     }
     629
     630
     631     // UTILITY METHODS -------------------------------------------------------------------------------------------------
     632
     633     /**
     634      * When rendering some objects it is necessary to iterate over the object being rendered, to do this, you need
     635      * access to itself.
     636      *
     637      * @return ViewableData
     638      */
     639     public function Me() {
     640         return $this;
     641     }
     642
     643     /**
     644      * Return the directory if the current active theme (relative to the site root).
     645      *
     646      * This method is useful for things such as accessing theme images from your template without hardcoding the theme
     647      * page - e.g. <img src="$ThemeDir/images/something.gif">.
     648      *
     649      * This method should only be used when a theme is currently active. However, it will fall over to the current
     650      * project directory.
     651      *
     652      * @param string $subtheme the subtheme path to get
     653      * @return string
     654      */
     655     public function ThemeDir($subtheme = false) {
     656         if($theme = SSViewer::current_theme()) {
     657             return THEMES_DIR . "/$theme" . ($subtheme ? "_$subtheme" : null);
     658         }
     659
     660         return project();
     661     }
     662
     663     /**
     664      * Возвращает имя текущей темы
     665      *
     666      * Если текущая тема не выбрана то возвращается site
     667      *
     668      * @return string
     669      */
     670     public function ThemeName() {
     671         if($theme = SSViewer::current_theme()) {
     672             return $theme;
     673         }
     674         return project();
     675     }
     676
     677     /**
     678      * Get part of the current classes ancestry to be used as a CSS class.
     679      *
     680      * This method returns an escaped string of CSS classes representing the current classes ancestry until it hits a
     681      * stop point - e.g. "Page DataObject ViewableData".
     682      *
     683      * @param string $stopAtClass the class to stop at (default: ViewableData)
     684      * @return string
     685      * @uses ClassInfo
     686      */
     687     public function CSSClasses($stopAtClass = 'ViewableData') {
     688         $classes       = array();
     689         $classAncestry = array_reverse(ClassInfo::ancestry($this->class));
     690         $stopClasses   = ClassInfo::ancestry($stopAtClass);
     691
     692         foreach($classAncestry as $class) {
     693             if(in_array($class, $stopClasses)) break;
     694             $classes[] = $class;
     695         }
     696
     697         // optionally add template identifier
     698         if(isset($this->template) && !in_array($this->template, $classes)) {
     699             $classes[] = $this->template;
     700         }
     701
     702         return Convert::raw2att(implode(' ', $classes));
     703     }
     704
     705     /**
     706      * @see Member::currentUser()
     707      */
     708     public function CurrentMember() {
     709         return Member::currentUser();
     710     }
     711
     712     /**
     713      * Return a CSRF-preventing ID to insert into a form.
     714      *
     715      * @return string
     716      */
     717     public function getSecurityID() {
     718         if(!$id = Session::get('SecurityID')) {
     719             $id = rand();
     720             Session::set('SecurityID', $id);
     721         }
     722
     723         return $id;
     724     }
     725
     726     /**
     727      * @see Permission::check()
     728      */
     729     public function HasPerm($code) {
     730         return Permission::check($code);
     731     }
     732
     733     /**
     734      * @see Director::absoluteBaseURL()
     735      */
     736     public function BaseHref() {
     737         return Director::absoluteBaseURL();
     738     }
     739
     740     /**
     741      * @see Director::is_ajax()
     742      */
     743     public function IsAjax() {
     744         return Director::is_ajax();
     745     }
     746
     747     /**
     748      * @see i18n::get_locale()
     749      */
     750     public function i18nLocale() {
     751         return i18n::get_locale();
     752     }
     753
     754     /**
     755      * Return debug information about this object that can be rendered into a template
     756      *
     757      * @return ViewableData_Debugger
     758      */
     759     public function Debug() {
     760         return new ViewableData_Debugger($this);
     761     }
     762
     763     /**
     764      * @see Controller::curr()
     765      */
     766     public function CurrentPage() {
     767         return Controller::curr();
     768     }
     769
     770     /**
     771      * @see SSViewer::topLevel()
     772      */
     773     public function Top() {
     774         return SSViewer::topLevel();
     775     }
     776
     777     function ColumnCalc($col) {
     778         $pos = $this->Pos();
     779         $total = $this->TotalItems();
     780
     781         $long = $total % $col;
     782         $minSize = floor($total / $col);
     783         if ($minSize == 0 ) $minSize = 1;
     784         $maxSize = ($long) ? $minSize + 1 : $minSize;
     785         $maxLongPos = $long * $maxSize;
     786
     787         if (!$long) {
     788             $colNum = ceil($pos / $minSize);
     789             $colPos = $pos - $minSize * ($colNum - 1);
     790             return array(
     791                 'col' => $colNum,
     792                 'pos' => $colPos,
     793                 'break' => ($pos < $total && $colPos == $minSize) ? 1 : 0,
     794                 'pad' => 0,
     795             );
     796         }
     797         if ($pos <= $maxLongPos)    {
     798             $colNum = ceil($pos / $maxSize);
     799             $colPos = $pos - $maxSize * ($colNum - 1);
     800             return array(
     801                 'col' => $colNum,
     802                 'pos' => $colPos,
     803                 'break' => ($pos < $total && $colPos == $maxSize) ? 1 : 0,
     804                 'pad' => 0,
     805             );
     806         }
     807         $pos -= $maxLongPos;
     808         $total -= $maxLongPos;
     809         $colNum = ceil($pos / $minSize);
     810         $colPos = $pos - $minSize * ($colNum - 1);
     811         $colBreak = ($pos < $total && $colPos == $minSize) ? 1 : 0;
     812         return array(
     813             'col' => $colNum + $long,
     814             'pos' => $colPos,
     815             'break' => $colBreak,
     816             'pad' => ($colBreak || $pos == $total) ? 1 : 0,
     817         );
     818     }
     819
     820     function ColumnNumber($col) {
     821         $info = $this->cachedCall('ColumnCalc', array($col));
     822         return $info['col'];
     823     }
     824
     825     function ColumnPos($col) {
     826         $info = $this->cachedCall('ColumnCalc', array($col));
     827         return $info['pos'];
     828     }
     829
     830     function ColumnBreak($col) {
     831         $info = $this->cachedCall('ColumnCalc', array($col));
     832         return $info['break'];
     833     }
     834
     835     function ColumnPad($col) {
     836         $info = $this->cachedCall('ColumnCalc', array($col));
     837         return $info['pad'];
     838     }
     839 }
     840
     841 /**
     842  * @package sapphire
     843  * @subpackage view
     844  */
     845 class ViewableData_Customised extends ViewableData {
     846
     847     /**
     848      * @var ViewableData
     849      */
     850     protected $original, $customised;
     851
     852     /**
     853      * Instantiate a new customised ViewableData object
     854      *
     855      * @param ViewableData $originalObject
     856      * @param ViewableData $customisedObject
     857      */
     858     public function __construct(ViewableData $originalObject, ViewableData $customisedObject) {
     859         $this->original   = $originalObject;
     860         $this->customised = $customisedObject;
     861
     862         $this->original->setCustomisedObj($this);
     863
     864         parent::__construct();
     865     }
     866
     867     public function __call($method, $arguments) {
     868         if($this->customised->hasMethod($method)) {
     869             return call_user_func_array(array($this->customised, $method), $arguments);
     870         }
     871
     872         return call_user_func_array(array($this->original, $method), $arguments);
     873     }
     874
     875     public function __get($property) {
     876         if(isset($this->customised->$property)) {
     877             return $this->customised->$property;
     878         }
     879
     880         return $this->original->$property;
     881     }
     882
     883     public function __set($property, $value) {
     884         $this->customised->$property = $this->original->$property = $value;
     885     }
     886
     887     public function hasMethod($method) {
     888         return $this->customised->hasMethod($method) || $this->original->hasMethod($method);
     889     }
     890
     891     public function cachedCall($field, $arguments = null, $identifier = null) {
     892         if($this->customised->hasMethod($field) || $this->customised->hasField($field)) {
     893             $result = $this->customised->cachedCall($field, $arguments, $identifier);
     894         } else {
     895             $result = $this->original->cachedCall($field, $arguments, $identifier);
     896         }
     897
     898         return $result;
     899     }
     900
     901     public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
     902         // don't proxy Debug() method!
     903         if (strcasecmp($fieldName, 'debug') == 0) return parent::Debug();
     904
     905         if($this->customised->hasField($fieldName) || $this->customised->hasMethod($fieldName)) {
     906             return $this->customised->obj($fieldName, $arguments, $forceReturnedObject, $cache, $cacheName);
     907         }
     908
     909         return $this->original->obj($fieldName, $arguments, $forceReturnedObject, $cache, $cacheName);
     910     }
     911
     912     function getAllFields() {
     913         $res = array();
     914         if ($this->original->hasMethod('getAllFields'))
     915             $res = array_merge($res, $this->original->getAllFields());
     916
     917         if ($this->customised->hasMethod('getAllFields'))
     918             $res = array_merge($res, $this->customised->getAllFields());
     919         return array_unique($res);
     920     }
     921
     922     function allMethodNames() {
     923         return array_unique(array_merge($this->original->allMethodNames(), $this->customised->allMethodNames()));
     924     }
     925
     926     function getOriginal() {
     927         return $this->original;
     928     }
     929 }
     930
     931 /**
     932  * Allows you to render debug information about a {@link ViewableData} object into a template.
     933  *
     934  * @package sapphire
     935  * @subpackage view
     936  */
     937 class ViewableData_Debugger extends ViewableData {
     938
     939     protected static $hidden_methods = array('getField', 'getCustomClass', 'getExtensionInstance', 'getExtensionInstances', 'getIterator', 'getXMLValues');
     940
     941     /**
     942      * @var ViewableData
     943      */
     944     protected $object;
     945
     946     /**
     947      * @param ViewableData $object
     948      */
     949     public function __construct(ViewableData $object) {
     950         $this->object = $object;
     951         parent::__construct();
     952     }
     953
     954     /**
     955      * Return debugging information, as XHTML. If a field name is passed, it will show debugging information on that
     956      * field, otherwise it will show information on all methods and fields.
     957      *
     958      * @param string $field the field name
     959      * @return string
     960      */
     961     public function forTemplate($field = null) {
     962         // debugging info for a specific field
     963         if($field) return "<b>Debugging Information for {$this->class}->{$field}</b><br/>" .
     964             ($this->object->hasMethod($field)? "Has method '$field'<br/>" : null)             .
     965             ($this->object->hasField($field) ? "Has field '$field'<br/>"  : null)             ;
     966
     967         // debugging information for the entire class
     968         if ($this->object->class == 'ViewableData_Customised') {
     969             $object = $this->object->getOriginal();
     970             $reflector = new ReflectionObject($object);
     971             $class =  $object->class . ' (customized)';
     972         }
     973         else {
     974             $reflector = new ReflectionObject($this->object);
     975             $class =  $this->object->class;
     976         }
     977         $debug     = "<b>Debugging Information: all methods available in '{$class}'</b><br/><ul>";
     978
     979         $vars = array();
     980         $methods = $this->object->allMethodNames();
     981         sort($methods);
     982         foreach($methods as $method) {
     983             // check that the method is public
     984             if ($method[0] == '_') continue;
     985
     986             if ($reflector->hasMethod($method) && $method = $reflector->getMethod($method)) {
     987                 if (!$method->isPublic()) continue;
     988
     989                 $methodName = $method->getName();
     990                 if (array_search($methodName, self::$hidden_methods) !== false) continue;
     991                 // only MethodForTemplate() not (internalMethod() or Method_xxx())
     992                 if ($methodName[0] === strtoupper($methodName[0]) && strpos($methodName, '_') === false) {
     993                     $debug .= "<li>\${$method->getName()}";
     994
     995                     if(count($method->getParameters())) {
     996                         $debug .= ' <small>(' . implode(', ', $method->getParameters()) . ')</small>';
     997                     }
     998
     999                     $debug .= '</li>';
    1000                 }
    1001                 // getVar() not (get_xxx() or getxxx()) -> $vars
    1002                 elseif (strlen($methodName) > 3 && substr($methodName, 0 , 3) == 'get' && $methodName[3] != '_' && $methodName[3] === strtoupper($methodName[3])) {
    1003                     $vars[] = substr($methodName, 3);
    1004                 }
    1005             }
    1006         }
    1007
    1008         $debug .= '</ul>';
    1009
    1010         if($this->object->hasMethod('getAllFields'))
    1011             $vars = array_unique(array_merge($vars, array_keys($this->object->getAllFields())));
    1012
    1013         if (count($vars)) {
    1014             sort($vars);
    1015             $debug .= "<b>Debugging Information: all fields available in '{$class}'</b><br/><ul>";
    1016
    1017             foreach($vars as $field) {
    1018                 $debug .= "<li>\$$field</li>";
    1019             }
    1020
    1021             $debug .= "</ul>";
    1022         }
    1023
    1024         // check for an extra attached data
    1025         if($this->object->hasMethod('data') && $this->object->data() != $this->object) {
    1026             $debug .= Object::create('ViewableData_Debugger', $this->object->data())->forTemplate();
    1027         }
    1028
    1029         return $debug;
    1030     }
    1031
    1032 }
    1033

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -