Completed
Push — fix-2494 ( 3153ee...40d9bb )
by Sam
13:43 queued 06:38
created

ViewableData::getFailover()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
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\ClassInfo;
10
use SilverStripe\Core\Convert;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\Dev\Debug;
13
use IteratorAggregate;
14
use LogicException;
15
use InvalidArgumentException;
16
use UnexpectedValueException;
17
use ArrayIterator;
18
19
/**
20
 * A ViewableData object is any object that can be rendered into a template/view.
21
 *
22
 * A view interrogates the object being currently rendered in order to get data to render into the template. This data
23
 * is provided and automatically escaped by ViewableData. Any class that needs to be available to a view (controllers,
24
 * {@link DataObject}s, page controls) should inherit from this class.
25
 */
26
class ViewableData extends Object implements IteratorAggregate
27
{
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
    {
92
        // getField() isn't a field-specific getter and shouldn't be treated as such
93
        if (strtolower($property) !== 'field' && $this->hasMethod($method = "get$property")) {
94
            return true;
95
        } elseif ($this->hasField($property)) {
96
            return true;
97
        } elseif ($this->failover) {
98
            return isset($this->failover->$property);
99
        }
100
101
        return false;
102
    }
103
104
    /**
105
     * Get the value of a property/field on this object. This will check if a method called get{$property} exists, then
106
     * check if a field is available using {@link ViewableData::getField()}, then fall back on a failover object.
107
     *
108
     * @param string $property
109
     * @return mixed
110
     */
111
    public function __get($property)
112
    {
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
        } elseif ($this->hasField($property)) {
117
            return $this->getField($property);
118
        } elseif ($this->failover) {
119
            return $this->failover->$property;
120
        }
121
122
        return null;
123
    }
124
125
    /**
126
     * Set a property/field on this object. This will check for the existence of a method called set{$property}, then
127
     * use the {@link ViewableData::setField()} method.
128
     *
129
     * @param string $property
130
     * @param mixed $value
131
     */
132
    public function __set($property, $value)
133
    {
134
        $this->objCacheClear();
135
        if ($this->hasMethod($method = "set$property")) {
136
            $this->$method($value);
137
        } else {
138
            $this->setField($property, $value);
139
        }
140
    }
141
142
    /**
143
     * Set a failover object to attempt to get data from if it is not present on this object.
144
     *
145
     * @param ViewableData $failover
146
     */
147
    public function setFailover(ViewableData $failover)
148
    {
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
    {
165
        return $this->failover;
166
    }
167
168
    /**
169
     * Check if a field exists on this object. This should be overloaded in child classes.
170
     *
171
     * @param string $field
172
     * @return bool
173
     */
174
    public function hasField($field)
175
    {
176
        return property_exists($this, $field);
177
    }
178
179
    /**
180
     * Get the value of a field on this object. This should be overloaded in child classes.
181
     *
182
     * @param string $field
183
     * @return mixed
184
     */
185
    public function getField($field)
186
    {
187
        return $this->$field;
188
    }
189
190
    /**
191
     * Set a field on this object. This should be overloaded in child classes.
192
     *
193
     * @param string $field
194
     * @param mixed $value
195
     * @return $this
196
     */
197
    public function setField($field, $value)
198
    {
199
        $this->objCacheClear();
200
        $this->$field = $value;
201
        return $this;
202
    }
203
204
    // -----------------------------------------------------------------------------------------------------------------
205
206
    /**
207
     * Add methods from the {@link ViewableData::$failover} object, as well as wrapping any methods prefixed with an
208
     * underscore into a {@link ViewableData::cachedCall()}.
209
     *
210
     * @throws LogicException
211
     */
212
    public function defineMethods()
213
    {
214
        if ($this->failover && !is_object($this->failover)) {
215
            throw new LogicException("ViewableData::\$failover set to a non-object");
216
        }
217
        if ($this->failover) {
218
            $this->addMethodsFrom('failover');
219
220
            if (isset($_REQUEST['debugfailover'])) {
221
                Debug::message("$this->class created with a failover class of {$this->failover->class}");
222
            }
223
        }
224
225
        parent::defineMethods();
226
    }
227
228
    /**
229
     * Merge some arbitrary data in with this object. This method returns a {@link ViewableData_Customised} instance
230
     * with references to both this and the new custom data.
231
     *
232
     * Note that any fields you specify will take precedence over the fields on this object.
233
     *
234
     * @param array|ViewableData $data
235
     * @return ViewableData_Customised
236
     */
237
    public function customise($data)
238
    {
239
        if (is_array($data) && (empty($data) || ArrayLib::is_associative($data))) {
240
            $data = new ArrayData($data);
241
        }
242
243
        if ($data instanceof ViewableData) {
244
            return new ViewableData_Customised($this, $data);
245
        }
246
247
        throw new InvalidArgumentException(
248
            'ViewableData->customise(): $data must be an associative array or a ViewableData instance'
249
        );
250
    }
251
252
    /**
253
     * @return ViewableData
254
     */
255
    public function getCustomisedObj()
256
    {
257
        return $this->customisedObject;
258
    }
259
260
    /**
261
     * @param ViewableData $object
262
     */
263
    public function setCustomisedObj(ViewableData $object)
264
    {
265
        $this->customisedObject = $object;
266
    }
267
268
    // CASTING ---------------------------------------------------------------------------------------------------------
269
270
    /**
271
     * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object)
272
     * for a field on this object. This helper will be a subclass of DBField.
273
     *
274
     * @param string $field
275
     * @return string Casting helper As a constructor pattern, and may include arguments.
276
     */
277
    public function castingHelper($field)
278
    {
279
        $specs = $this->config()->get('casting');
280
        if (isset($specs[$field])) {
281
            return $specs[$field];
282
        }
283
284
        // If no specific cast is declared, fall back to failover.
285
        // Note that if there is a failover, the default_cast will always
286
        // be drawn from this object instead of the top level object.
287
        $failover = $this->getFailover();
288
        if ($failover) {
289
            $cast = $failover->castingHelper($field);
290
            if ($cast) {
291
                return $cast;
292
            }
293
        }
294
295
        // Fall back to default_cast
296
        $default = $this->config()->get('default_cast');
297
        if (empty($default)) {
298
            throw new \Exception("No default_cast");
299
        }
300
        return $default;
301
    }
302
303
    /**
304
     * Get the class name a field on this object will be casted to.
305
     *
306
     * @param string $field
307
     * @return string
308
     */
309
    public function castingClass($field)
310
    {
311
        // Strip arguments
312
        $spec = $this->castingHelper($field);
313
        return trim(strtok($spec, '('));
314
    }
315
316
    /**
317
     * Return the string-format type for the given field.
318
     *
319
     * @param string $field
320
     * @return string 'xml'|'raw'
321
     */
322
    public function escapeTypeForField($field)
323
    {
324
        $class = $this->castingClass($field) ?: $this->config()->get('default_cast');
325
326
        // TODO: It would be quicker not to instantiate the object, but to merely
327
        // get its class from the Injector
328
        /** @var DBField $type */
329
        $type = Injector::inst()->get($class, true);
0 ignored issues
show
Unused Code introduced by
The call to ContainerInterface::get() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
330
        return $type->config()->get('escape_type');
331
    }
332
333
    // TEMPLATE ACCESS LAYER -------------------------------------------------------------------------------------------
334
335
    /**
336
     * Render this object into the template, and get the result as a string. You can pass one of the following as the
337
     * $template parameter:
338
     *  - a template name (e.g. Page)
339
     *  - an array of possible template names - the first valid one will be used
340
     *  - an SSViewer instance
341
     *
342
     * @param string|array|SSViewer $template the template to render into
343
     * @param array $customFields fields to customise() the object with before rendering
344
     * @return DBHTMLText
345
     */
346
    public function renderWith($template, $customFields = null)
347
    {
348
        if (!is_object($template)) {
349
            $template = new SSViewer($template);
350
        }
351
352
        $data = ($this->customisedObject) ? $this->customisedObject : $this;
353
354
        if ($customFields instanceof ViewableData) {
355
            $data = $data->customise($customFields);
356
        }
357
        if ($template instanceof SSViewer) {
358
            return $template->process($data, is_array($customFields) ? $customFields : null);
359
        }
360
361
        throw new UnexpectedValueException(
362
            "ViewableData::renderWith(): unexpected ".get_class($template)." object, expected an SSViewer instance"
363
        );
364
    }
365
366
    /**
367
     * Generate the cache name for a field
368
     *
369
     * @param string $fieldName Name of field
370
     * @param array $arguments List of optional arguments given
371
     * @return string
372
     */
373
    protected function objCacheName($fieldName, $arguments)
374
    {
375
        return $arguments
376
            ? $fieldName . ":" . implode(',', $arguments)
377
            : $fieldName;
378
    }
379
380
    /**
381
     * Get a cached value from the field cache
382
     *
383
     * @param string $key Cache key
384
     * @return mixed
385
     */
386
    protected function objCacheGet($key)
387
    {
388
        if (isset($this->objCache[$key])) {
389
            return $this->objCache[$key];
390
        }
391
        return null;
392
    }
393
394
    /**
395
     * Store a value in the field cache
396
     *
397
     * @param string $key Cache key
398
     * @param mixed $value
399
     * @return $this
400
     */
401
    protected function objCacheSet($key, $value)
402
    {
403
        $this->objCache[$key] = $value;
404
        return $this;
405
    }
406
407
    /**
408
     * Clear object cache
409
     *
410
     * @return $this
411
     */
412
    protected function objCacheClear()
413
    {
414
        $this->objCache = [];
415
        return $this;
416
    }
417
418
    /**
419
     * Get the value of a field on this object, automatically inserting the value into any available casting objects
420
     * that have been specified.
421
     *
422
     * @param string $fieldName
423
     * @param array $arguments
424
     * @param bool $cache Cache this object
425
     * @param string $cacheName a custom cache name
426
     * @return Object|DBField
427
     */
428
    public function obj($fieldName, $arguments = [], $cache = false, $cacheName = null)
429
    {
430
        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...
431
            $cacheName = $this->objCacheName($fieldName, $arguments);
432
        }
433
434
        // Check pre-cached value
435
        $value = $cache ? $this->objCacheGet($cacheName) : null;
436
        if ($value !== null) {
437
            return $value;
438
        }
439
440
        // Load value from record
441
        if ($this->hasMethod($fieldName)) {
442
            $value = call_user_func_array(array($this, $fieldName), $arguments ?: []);
443
        } else {
444
            $value = $this->$fieldName;
445
        }
446
447
        // Cast object
448
        if (!is_object($value)) {
449
            // Force cast
450
            $castingHelper = $this->castingHelper($fieldName);
451
            $valueObject = Object::create_from_string($castingHelper, $fieldName);
452
            $valueObject->setValue($value, $this);
453
            $value = $valueObject;
454
        }
455
456
        // Record in cache
457
        if ($cache) {
458
            $this->objCacheSet($cacheName, $value);
459
        }
460
461
        return $value;
462
    }
463
464
    /**
465
     * A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again
466
     * without re-running the method.
467
     *
468
     * @param string $field
469
     * @param array $arguments
470
     * @param string $identifier an optional custom cache identifier
471
     * @return Object|DBField
472
     */
473
    public function cachedCall($field, $arguments = [], $identifier = null)
474
    {
475
        return $this->obj($field, $arguments, true, $identifier);
476
    }
477
478
    /**
479
     * Checks if a given method/field has a valid value. If the result is an object, this will return the result of the
480
     * exists method, otherwise will check if the result is not just an empty paragraph tag.
481
     *
482
     * @param string $field
483
     * @param array $arguments
484
     * @param bool $cache
485
     * @return bool
486
     */
487
    public function hasValue($field, $arguments = [], $cache = true)
488
    {
489
        $result = $this->obj($field, $arguments, $cache);
490
            return $result->exists();
491
    }
492
493
    /**
494
     * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
495
     * template.
496
     *
497
     * @param string $field
498
     * @param array $arguments
499
     * @param bool $cache
500
     * @return string
501
     */
502
    public function XML_val($field, $arguments = [], $cache = false)
503
    {
504
        $result = $this->obj($field, $arguments, $cache);
505
        // Might contain additional formatting over ->XML(). E.g. parse shortcodes, nl2br()
506
        return $result->forTemplate();
507
    }
508
509
    /**
510
     * Get an array of XML-escaped values by field name
511
     *
512
     * @param array $fields an array of field names
513
     * @return array
514
     */
515
    public function getXMLValues($fields)
516
    {
517
        $result = array();
518
519
        foreach ($fields as $field) {
520
            $result[$field] = $this->XML_val($field);
521
        }
522
523
        return $result;
524
    }
525
526
    // ITERATOR SUPPORT ------------------------------------------------------------------------------------------------
527
528
    /**
529
     * Return a single-item iterator so you can iterate over the fields of a single record.
530
     *
531
     * This is useful so you can use a single record inside a <% control %> block in a template - and then use
532
     * to access individual fields on this object.
533
     *
534
     * @return ArrayIterator
535
     */
536
    public function getIterator()
537
    {
538
        return new ArrayIterator(array($this));
539
    }
540
541
    // UTILITY METHODS -------------------------------------------------------------------------------------------------
542
543
    /**
544
     * When rendering some objects it is necessary to iterate over the object being rendered, to do this, you need
545
     * access to itself.
546
     *
547
     * @return ViewableData
548
     */
549
    public function Me()
550
    {
551
        return $this;
552
    }
553
554
    /**
555
     * Get part of the current classes ancestry to be used as a CSS class.
556
     *
557
     * This method returns an escaped string of CSS classes representing the current classes ancestry until it hits a
558
     * stop point - e.g. "Page DataObject ViewableData".
559
     *
560
     * @param string $stopAtClass the class to stop at (default: ViewableData)
561
     * @return string
562
     * @uses ClassInfo
563
     */
564
    public function CSSClasses($stopAtClass = self::class)
565
    {
566
        $classes       = array();
567
        $classAncestry = array_reverse(ClassInfo::ancestry($this->class));
568
        $stopClasses   = ClassInfo::ancestry($stopAtClass);
569
570
        foreach ($classAncestry as $class) {
571
            if (in_array($class, $stopClasses)) {
572
                break;
573
            }
574
            $classes[] = $class;
575
        }
576
577
        // optionally add template identifier
578
        if (isset($this->template) && !in_array($this->template, $classes)) {
579
            $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...
580
        }
581
582
        // Strip out namespaces
583
        $classes = preg_replace('#.*\\\\#', '', $classes);
584
585
        return Convert::raw2att(implode(' ', $classes));
586
    }
587
588
    /**
589
     * Return debug information about this object that can be rendered into a template
590
     *
591
     * @return ViewableData_Debugger
592
     */
593
    public function Debug()
594
    {
595
        return new ViewableData_Debugger($this);
596
    }
597
}
598