Completed
Push — php7-fix ( 538bb9...015411 )
by Sam
07:21
created

ViewableData::castingHelper()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 4
nop 1
dl 0
loc 20
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\View;
4
5
use SilverStripe\Core\Object;
6
use SilverStripe\ORM\ArrayLib;
7
use SilverStripe\ORM\FieldType\DBField;
8
use SilverStripe\ORM\FieldType\DBHTMLText;
9
use SilverStripe\Core\Config\Config;
10
use SilverStripe\Core\ClassInfo;
11
use SilverStripe\Core\Convert;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\Dev\Debug;
14
use IteratorAggregate;
15
use LogicException;
16
use InvalidArgumentException;
17
use UnexpectedValueException;
18
use ArrayIterator;
19
20
/**
21
 * A ViewableData object is any object that can be rendered into a template/view.
22
 *
23
 * A view interrogates the object being currently rendered in order to get data to render into the template. This data
24
 * is provided and automatically escaped by ViewableData. Any class that needs to be available to a view (controllers,
25
 * {@link DataObject}s, page controls) should inherit from this class.
26
 */
27
class ViewableData extends Object implements IteratorAggregate {
28
29
	/**
30
	 * An array of objects to cast certain fields to. This is set up as an array in the format:
31
	 *
32
	 * <code>
33
	 * public static $casting = array (
34
	 *     'FieldName' => 'ClassToCastTo(Arguments)'
35
	 * );
36
	 * </code>
37
	 *
38
	 * @var array
39
	 * @config
40
	 */
41
	private static $casting = array(
42
		'CSSClasses' => 'Varchar'
43
	);
44
45
	/**
46
	 * The default object to cast scalar fields to if casting information is not specified, and casting to an object
47
	 * is required.
48
	 *
49
	 * @var string
50
	 * @config
51
	 */
52
	private static $default_cast = 'Text';
53
54
	/**
55
	 * @var array
56
	 */
57
	private static $casting_cache = array();
58
59
	// -----------------------------------------------------------------------------------------------------------------
60
61
	/**
62
	 * A failover object to attempt to get data from if it is not present on this object.
63
	 *
64
	 * @var ViewableData
65
	 */
66
	protected $failover;
67
68
	/**
69
	 * @var ViewableData
70
	 */
71
	protected $customisedObject;
72
73
	/**
74
	 * @var array
75
	 */
76
	private $objCache = array();
77
78
	// -----------------------------------------------------------------------------------------------------------------
79
80
	// FIELD GETTERS & SETTERS -----------------------------------------------------------------------------------------
81
82
	/**
83
	 * Check if a field exists on this object or its failover.
84
	 * Note that, unlike the core isset() implementation, this will return true if the property is defined
85
	 * and set to null.
86
	 *
87
	 * @param string $property
88
	 * @return bool
89
	 */
90
	public function __isset($property) {
91
		// getField() isn't a field-specific getter and shouldn't be treated as such
92
		if(strtolower($property) !== 'field' && $this->hasMethod($method = "get$property")) {
93
			return true;
94
95
		} elseif($this->hasField($property)) {
96
			return true;
97
98
		} elseif($this->failover) {
99
			return isset($this->failover->$property);
100
		}
101
102
		return false;
103
	}
104
105
	/**
106
	 * Get the value of a property/field on this object. This will check if a method called get{$property} exists, then
107
	 * check if a field is available using {@link ViewableData::getField()}, then fall back on a failover object.
108
	 *
109
	 * @param string $property
110
	 * @return mixed
111
	 */
112
	public function __get($property) {
113
		// getField() isn't a field-specific getter and shouldn't be treated as such
114
		if(strtolower($property) !== 'field' && $this->hasMethod($method = "get$property")) {
115
			return $this->$method();
116
117
		} elseif($this->hasField($property)) {
118
			return $this->getField($property);
119
120
		} elseif($this->failover) {
121
			return $this->failover->$property;
122
		}
123
124
		return null;
125
	}
126
127
	/**
128
	 * Set a property/field on this object. This will check for the existence of a method called set{$property}, then
129
	 * use the {@link ViewableData::setField()} method.
130
	 *
131
	 * @param string $property
132
	 * @param mixed $value
133
	 */
134
	public function __set($property, $value) {
135
		$this->objCacheClear();
136
		if($this->hasMethod($method = "set$property")) {
137
			$this->$method($value);
138
		} else {
139
			$this->setField($property, $value);
140
		}
141
	}
142
143
	/**
144
	 * Set a failover object to attempt to get data from if it is not present on this object.
145
	 *
146
	 * @param ViewableData $failover
147
	 */
148
	public function setFailover(ViewableData $failover) {
149
		// Ensure cached methods from previous failover are removed
150
		if ($this->failover) {
151
			$this->removeMethodsFrom('failover');
152
		}
153
154
		$this->failover = $failover;
155
		$this->defineMethods();
156
	}
157
158
	/**
159
	 * Get the current failover object if set
160
	 *
161
	 * @return ViewableData|null
162
	 */
163
	public function getFailover() {
164
		return $this->failover;
165
	}
166
167
	/**
168
	 * Check if a field exists on this object. This should be overloaded in child classes.
169
	 *
170
	 * @param string $field
171
	 * @return bool
172
	 */
173
	public function hasField($field) {
174
		return property_exists($this, $field);
175
	}
176
177
	/**
178
	 * Get the value of a field on this object. This should be overloaded in child classes.
179
	 *
180
	 * @param string $field
181
	 * @return mixed
182
	 */
183
	public function getField($field) {
184
		return $this->$field;
185
	}
186
187
	/**
188
	 * Set a field on this object. This should be overloaded in child classes.
189
	 *
190
	 * @param string $field
191
	 * @param mixed $value
192
	 * @return $this
193
	 */
194
	public function setField($field, $value) {
195
		$this->objCacheClear();
196
		$this->$field = $value;
197
		return $this;
198
	}
199
200
	// -----------------------------------------------------------------------------------------------------------------
201
202
	/**
203
	 * Add methods from the {@link ViewableData::$failover} object, as well as wrapping any methods prefixed with an
204
	 * underscore into a {@link ViewableData::cachedCall()}.
205
	 *
206
	 * @throws LogicException
207
	 */
208
	public function defineMethods() {
209
		if($this->failover && !is_object($this->failover)) {
210
			throw new LogicException("ViewableData::\$failover set to a non-object");
211
		}
212
		if($this->failover) {
213
			$this->addMethodsFrom('failover');
214
215
			if(isset($_REQUEST['debugfailover'])) {
216
				Debug::message("$this->class created with a failover class of {$this->failover->class}");
217
			}
218
		}
219
220
		parent::defineMethods();
221
	}
222
223
	/**
224
	 * Merge some arbitrary data in with this object. This method returns a {@link ViewableData_Customised} instance
225
	 * with references to both this and the new custom data.
226
	 *
227
	 * Note that any fields you specify will take precedence over the fields on this object.
228
	 *
229
	 * @param array|ViewableData $data
230
	 * @return ViewableData_Customised
231
	 */
232
	public function customise($data) {
233
		if(is_array($data) && (empty($data) || ArrayLib::is_associative($data))) {
234
			$data = new ArrayData($data);
235
		}
236
237
		if($data instanceof ViewableData) {
238
			return new ViewableData_Customised($this, $data);
239
		}
240
241
		throw new InvalidArgumentException (
242
			'ViewableData->customise(): $data must be an associative array or a ViewableData instance'
243
		);
244
	}
245
246
	/**
247
	 * @return ViewableData
248
	 */
249
	public function getCustomisedObj() {
250
		return $this->customisedObject;
251
	}
252
253
	/**
254
	 * @param ViewableData $object
255
	 */
256
	public function setCustomisedObj(ViewableData $object) {
257
		$this->customisedObject = $object;
258
	}
259
260
	// CASTING ---------------------------------------------------------------------------------------------------------
261
262
	/**
263
	 * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object)
264
	 * for a field on this object. This helper will be a subclass of DBField.
265
	 *
266
	 * @param string $field
267
	 * @return string Casting helper As a constructor pattern, and may include arguments.
268
	 */
269
	public function castingHelper($field) {
270
		$specs = $this->config()->casting;
0 ignored issues
show
Documentation introduced by
The property casting does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
271
		if(isset($specs[$field])) {
272
			return $specs[$field];
273
		}
274
275
		// If no specific cast is declared, fall back to failover.
276
		// Note that if there is a failover, the default_cast will always
277
		// be drawn from this object instead of the top level object.
278
		$failover = $this->getFailover();
279
		if($failover) {
280
			$cast = $failover->castingHelper($field);
281
			if($cast) {
282
				return $cast;
283
		}
284
	}
285
286
		// Fall back to default_cast
287
		return $this->config()->get('default_cast');
288
	}
289
290
	/**
291
	 * Get the class name a field on this object will be casted to.
292
	 *
293
	 * @param string $field
294
	 * @return string
295
	 */
296
	public function castingClass($field) {
297
		// Strip arguments
298
		$spec = $this->castingHelper($field);
299
		return trim(strtok($spec, '('));
300
	}
301
302
	/**
303
	 * Return the string-format type for the given field.
304
	 *
305
	 * @param string $field
306
	 * @return string 'xml'|'raw'
307
	 */
308
	public function escapeTypeForField($field) {
309
		$class = $this->castingClass($field) ?: $this->config()->default_cast;
310
311
		// TODO: It would be quicker not to instantiate the object, but to merely
312
		// get its class from the Injector
313
		return Injector::inst()->get($class, true)->config()->escape_type;
314
	}
315
316
	// TEMPLATE ACCESS LAYER -------------------------------------------------------------------------------------------
317
318
	/**
319
	 * Render this object into the template, and get the result as a string. You can pass one of the following as the
320
	 * $template parameter:
321
	 *  - a template name (e.g. Page)
322
	 *  - an array of possible template names - the first valid one will be used
323
	 *  - an SSViewer instance
324
	 *
325
	 * @param string|array|SSViewer $template the template to render into
326
	 * @param array $customFields fields to customise() the object with before rendering
327
	 * @return DBHTMLText
328
	 */
329
	public function renderWith($template, $customFields = null) {
330
		if(!is_object($template)) {
331
			$template = new SSViewer($template);
332
		}
333
334
		$data = ($this->customisedObject) ? $this->customisedObject : $this;
335
336
		if($customFields instanceof ViewableData) {
337
			$data = $data->customise($customFields);
338
		}
339
		if($template instanceof SSViewer) {
340
			return $template->process($data, is_array($customFields) ? $customFields : null);
341
		}
342
343
		throw new UnexpectedValueException(
344
			"ViewableData::renderWith(): unexpected ".get_class($template)." object, expected an SSViewer instance"
345
		);
346
	}
347
348
	/**
349
	 * Generate the cache name for a field
350
	 *
351
	 * @param string $fieldName Name of field
352
	 * @param array $arguments List of optional arguments given
353
	 * @return string
354
	 */
355
	protected function objCacheName($fieldName, $arguments) {
356
		return $arguments
357
			? $fieldName . ":" . implode(',', $arguments)
358
			: $fieldName;
359
	}
360
361
	/**
362
	 * Get a cached value from the field cache
363
	 *
364
	 * @param string $key Cache key
365
	 * @return mixed
366
	 */
367
	protected function objCacheGet($key) {
368
		if(isset($this->objCache[$key])) {
369
			return $this->objCache[$key];
370
		}
371
		return null;
372
	}
373
374
	/**
375
	 * Store a value in the field cache
376
	 *
377
	 * @param string $key Cache key
378
	 * @param mixed $value
379
	 * @return $this
380
	 */
381
	protected function objCacheSet($key, $value) {
382
		$this->objCache[$key] = $value;
383
		return $this;
384
	}
385
386
	/**
387
	 * Clear object cache
388
	 *
389
	 * @return $this
390
	 */
391
	protected function objCacheClear() {
392
		$this->objCache = [];
393
		return $this;
394
	}
395
396
	/**
397
	 * Get the value of a field on this object, automatically inserting the value into any available casting objects
398
	 * that have been specified.
399
	 *
400
	 * @param string $fieldName
401
	 * @param array $arguments
402
	 * @param bool $cache Cache this object
403
	 * @param string $cacheName a custom cache name
404
	 * @return Object|DBField
405
	 */
406
	public function obj($fieldName, $arguments = [], $cache = false, $cacheName = null) {
407
		if(!$cacheName && $cache) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cacheName of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
408
			$cacheName = $this->objCacheName($fieldName, $arguments);
409
		}
410
411
		// Check pre-cached value
412
		$value = $cache ? $this->objCacheGet($cacheName) : null;
413
		if($value !== null) {
414
			return $value;
415
		}
416
417
		// Load value from record
418
		if($this->hasMethod($fieldName)) {
419
			$value = call_user_func_array(array($this, $fieldName), $arguments ?: []);
420
		} else {
421
			$value = $this->$fieldName;
422
		}
423
424
		// Cast object
425
		if(!is_object($value)) {
426
			// Force cast
427
			$castingHelper = $this->castingHelper($fieldName);
428
			$valueObject = Object::create_from_string($castingHelper, $fieldName);
429
			$valueObject->setValue($value, $this);
430
			$value = $valueObject;
431
		}
432
433
		// Record in cache
434
		if($cache) {
435
			$this->objCacheSet($cacheName, $value);
436
		}
437
438
		return $value;
439
	}
440
441
	/**
442
	 * A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again
443
	 * without re-running the method.
444
	 *
445
	 * @param string $field
446
	 * @param array $arguments
447
	 * @param string $identifier an optional custom cache identifier
448
	 * @return Object|DBField
449
	 */
450
	public function cachedCall($field, $arguments = [], $identifier = null) {
451
		return $this->obj($field, $arguments, true, $identifier);
452
	}
453
454
	/**
455
	 * Checks if a given method/field has a valid value. If the result is an object, this will return the result of the
456
	 * exists method, otherwise will check if the result is not just an empty paragraph tag.
457
	 *
458
	 * @param string $field
459
	 * @param array $arguments
460
	 * @param bool $cache
461
	 * @return bool
462
	 */
463
	public function hasValue($field, $arguments = [], $cache = true) {
464
		$result = $this->obj($field, $arguments, $cache);
465
			return $result->exists();
466
		}
467
468
	/**
469
	 * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
470
	 * template.
471
	 *
472
	 * @param string $field
473
	 * @param array $arguments
474
	 * @param bool $cache
475
	 * @return string
476
	 */
477
	public function XML_val($field, $arguments = [], $cache = false) {
478
		$result = $this->obj($field, $arguments, $cache);
479
		// Might contain additional formatting over ->XML(). E.g. parse shortcodes, nl2br()
480
		return $result->forTemplate();
481
	}
482
483
	/**
484
	 * Get an array of XML-escaped values by field name
485
	 *
486
	 * @param array $fields an array of field names
487
	 * @return array
488
	 */
489
	public function getXMLValues($fields) {
490
		$result = array();
491
492
		foreach($fields as $field) {
493
			$result[$field] = $this->XML_val($field);
494
		}
495
496
		return $result;
497
	}
498
499
	// ITERATOR SUPPORT ------------------------------------------------------------------------------------------------
500
501
	/**
502
	 * Return a single-item iterator so you can iterate over the fields of a single record.
503
	 *
504
	 * This is useful so you can use a single record inside a <% control %> block in a template - and then use
505
	 * to access individual fields on this object.
506
	 *
507
	 * @return ArrayIterator
508
	 */
509
	public function getIterator() {
510
		return new ArrayIterator(array($this));
511
	}
512
513
	// UTILITY METHODS -------------------------------------------------------------------------------------------------
514
515
	/**
516
	 * When rendering some objects it is necessary to iterate over the object being rendered, to do this, you need
517
	 * access to itself.
518
	 *
519
	 * @return ViewableData
520
	 */
521
	public function Me() {
522
		return $this;
523
	}
524
525
	/**
526
	 * Return the directory if the current active theme (relative to the site root).
527
	 *
528
	 * This method is useful for things such as accessing theme images from your template without hardcoding the theme
529
	 * page - e.g. <img src="$ThemeDir/images/something.gif">.
530
	 *
531
	 * This method should only be used when a theme is currently active. However, it will fall over to the current
532
	 * project directory.
533
	 *
534
	 * @param string $subtheme the subtheme path to get
535
	 * @return string
536
	 */
537
	public function ThemeDir($subtheme = null) {
538
		if(
539
			Config::inst()->get('SilverStripe\\View\\SSViewer', 'theme_enabled')
540
			&& $theme = Config::inst()->get('SilverStripe\\View\\SSViewer', 'theme')
541
		) {
542
			return THEMES_DIR . "/$theme" . ($subtheme ? "_$subtheme" : null);
543
		}
544
545
		return project();
546
	}
547
548
	/**
549
	 * Get part of the current classes ancestry to be used as a CSS class.
550
	 *
551
	 * This method returns an escaped string of CSS classes representing the current classes ancestry until it hits a
552
	 * stop point - e.g. "Page DataObject ViewableData".
553
	 *
554
	 * @param string $stopAtClass the class to stop at (default: ViewableData)
555
	 * @return string
556
	 * @uses ClassInfo
557
	 */
558
	public function CSSClasses($stopAtClass = 'SilverStripe\\View\\ViewableData') {
559
		$classes       = array();
560
		$classAncestry = array_reverse(ClassInfo::ancestry($this->class));
561
		$stopClasses   = ClassInfo::ancestry($stopAtClass);
562
563
		foreach($classAncestry as $class) {
564
			if(in_array($class, $stopClasses)) break;
565
			$classes[] = $class;
566
		}
567
568
		// optionally add template identifier
569
		if(isset($this->template) && !in_array($this->template, $classes)) {
570
			$classes[] = $this->template;
0 ignored issues
show
Documentation introduced by
The property template does not exist on object<SilverStripe\View\ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
571
		}
572
573
		// Strip out namespaces
574
		$classes = preg_replace('#.*\\\\#', '', $classes);
575
576
		return Convert::raw2att(implode(' ', $classes));
577
	}
578
579
	/**
580
	 * Return debug information about this object that can be rendered into a template
581
	 *
582
	 * @return ViewableData_Debugger
583
	 */
584
	public function Debug() {
585
		return new ViewableData_Debugger($this);
586
	}
587
588
}
589