Completed
Push — master ( 6014c3...8d5ff9 )
by Damian
09:40
created

ViewableData::ThemeDir()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 5
nc 3
nop 1
dl 0
loc 10
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
    /**
31
     * An array of objects to cast certain fields to. This is set up as an array in the format:
32
     *
33
     * <code>
34
     * public static $casting = array (
35
     *     'FieldName' => 'ClassToCastTo(Arguments)'
36
     * );
37
     * </code>
38
     *
39
     * @var array
40
     * @config
41
     */
42
    private static $casting = array(
43
        'CSSClasses' => 'Varchar'
44
    );
45
46
    /**
47
     * The default object to cast scalar fields to if casting information is not specified, and casting to an object
48
     * is required.
49
     *
50
     * @var string
51
     * @config
52
     */
53
    private static $default_cast = 'Text';
54
55
    /**
56
     * @var array
57
     */
58
    private static $casting_cache = array();
59
60
    // -----------------------------------------------------------------------------------------------------------------
61
62
    /**
63
     * A failover object to attempt to get data from if it is not present on this object.
64
     *
65
     * @var ViewableData
66
     */
67
    protected $failover;
68
69
    /**
70
     * @var ViewableData
71
     */
72
    protected $customisedObject;
73
74
    /**
75
     * @var array
76
     */
77
    private $objCache = array();
78
79
    // -----------------------------------------------------------------------------------------------------------------
80
81
    // FIELD GETTERS & SETTERS -----------------------------------------------------------------------------------------
82
83
    /**
84
     * Check if a field exists on this object or its failover.
85
     * Note that, unlike the core isset() implementation, this will return true if the property is defined
86
     * and set to null.
87
     *
88
     * @param string $property
89
     * @return bool
90
     */
91
    public function __isset($property)
92
    {
93
        // getField() isn't a field-specific getter and shouldn't be treated as such
94
        if (strtolower($property) !== 'field' && $this->hasMethod($method = "get$property")) {
95
            return true;
96
        } elseif ($this->hasField($property)) {
97
            return true;
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
    {
114
        // getField() isn't a field-specific getter and shouldn't be treated as such
115
        if (strtolower($property) !== 'field' && $this->hasMethod($method = "get$property")) {
116
            return $this->$method();
117
        } elseif ($this->hasField($property)) {
118
            return $this->getField($property);
119
        } elseif ($this->failover) {
120
            return $this->failover->$property;
121
        }
122
123
        return null;
124
    }
125
126
    /**
127
     * Set a property/field on this object. This will check for the existence of a method called set{$property}, then
128
     * use the {@link ViewableData::setField()} method.
129
     *
130
     * @param string $property
131
     * @param mixed $value
132
     */
133
    public function __set($property, $value)
134
    {
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
    {
150
        // Ensure cached methods from previous failover are removed
151
        if ($this->failover) {
152
            $this->removeMethodsFrom('failover');
153
        }
154
155
        $this->failover = $failover;
156
        $this->defineMethods();
157
    }
158
159
    /**
160
     * Get the current failover object if set
161
     *
162
     * @return ViewableData|null
163
     */
164
    public function getFailover()
165
    {
166
        return $this->failover;
167
    }
168
169
    /**
170
     * Check if a field exists on this object. This should be overloaded in child classes.
171
     *
172
     * @param string $field
173
     * @return bool
174
     */
175
    public function hasField($field)
176
    {
177
        return property_exists($this, $field);
178
    }
179
180
    /**
181
     * Get the value of a field on this object. This should be overloaded in child classes.
182
     *
183
     * @param string $field
184
     * @return mixed
185
     */
186
    public function getField($field)
187
    {
188
        return $this->$field;
189
    }
190
191
    /**
192
     * Set a field on this object. This should be overloaded in child classes.
193
     *
194
     * @param string $field
195
     * @param mixed $value
196
     * @return $this
197
     */
198
    public function setField($field, $value)
199
    {
200
        $this->objCacheClear();
201
        $this->$field = $value;
202
        return $this;
203
    }
204
205
    // -----------------------------------------------------------------------------------------------------------------
206
207
    /**
208
     * Add methods from the {@link ViewableData::$failover} object, as well as wrapping any methods prefixed with an
209
     * underscore into a {@link ViewableData::cachedCall()}.
210
     *
211
     * @throws LogicException
212
     */
213
    public function defineMethods()
0 ignored issues
show
Coding Style introduced by
defineMethods uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
214
    {
215
        if ($this->failover && !is_object($this->failover)) {
216
            throw new LogicException("ViewableData::\$failover set to a non-object");
217
        }
218
        if ($this->failover) {
219
            $this->addMethodsFrom('failover');
220
221
            if (isset($_REQUEST['debugfailover'])) {
222
                Debug::message("$this->class created with a failover class of {$this->failover->class}");
223
            }
224
        }
225
226
        parent::defineMethods();
227
    }
228
229
    /**
230
     * Merge some arbitrary data in with this object. This method returns a {@link ViewableData_Customised} instance
231
     * with references to both this and the new custom data.
232
     *
233
     * Note that any fields you specify will take precedence over the fields on this object.
234
     *
235
     * @param array|ViewableData $data
236
     * @return ViewableData_Customised
237
     */
238
    public function customise($data)
239
    {
240
        if (is_array($data) && (empty($data) || ArrayLib::is_associative($data))) {
241
            $data = new ArrayData($data);
242
        }
243
244
        if ($data instanceof ViewableData) {
245
            return new ViewableData_Customised($this, $data);
246
        }
247
248
        throw new InvalidArgumentException(
249
            'ViewableData->customise(): $data must be an associative array or a ViewableData instance'
250
        );
251
    }
252
253
    /**
254
     * @return ViewableData
255
     */
256
    public function getCustomisedObj()
257
    {
258
        return $this->customisedObject;
259
    }
260
261
    /**
262
     * @param ViewableData $object
263
     */
264
    public function setCustomisedObj(ViewableData $object)
265
    {
266
        $this->customisedObject = $object;
267
    }
268
269
    // CASTING ---------------------------------------------------------------------------------------------------------
270
271
    /**
272
     * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object)
273
     * for a field on this object. This helper will be a subclass of DBField.
274
     *
275
     * @param string $field
276
     * @return string Casting helper As a constructor pattern, and may include arguments.
277
     */
278
    public function castingHelper($field)
279
    {
280
        $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...
281
        if (isset($specs[$field])) {
282
            return $specs[$field];
283
        }
284
285
        // If no specific cast is declared, fall back to failover.
286
        // Note that if there is a failover, the default_cast will always
287
        // be drawn from this object instead of the top level object.
288
        $failover = $this->getFailover();
289
        if ($failover) {
290
            $cast = $failover->castingHelper($field);
291
            if ($cast) {
292
                return $cast;
293
            }
294
        }
295
296
        // Fall back to default_cast
297
        return $this->config()->get('default_cast');
298
    }
299
300
    /**
301
     * Get the class name a field on this object will be casted to.
302
     *
303
     * @param string $field
304
     * @return string
305
     */
306
    public function castingClass($field)
307
    {
308
        // Strip arguments
309
        $spec = $this->castingHelper($field);
310
        return trim(strtok($spec, '('));
311
    }
312
313
    /**
314
     * Return the string-format type for the given field.
315
     *
316
     * @param string $field
317
     * @return string 'xml'|'raw'
318
     */
319
    public function escapeTypeForField($field)
320
    {
321
        $class = $this->castingClass($field) ?: $this->config()->default_cast;
322
323
        // TODO: It would be quicker not to instantiate the object, but to merely
324
        // get its class from the Injector
325
        return Injector::inst()->get($class, true)->config()->escape_type;
326
    }
327
328
    // TEMPLATE ACCESS LAYER -------------------------------------------------------------------------------------------
329
330
    /**
331
     * Render this object into the template, and get the result as a string. You can pass one of the following as the
332
     * $template parameter:
333
     *  - a template name (e.g. Page)
334
     *  - an array of possible template names - the first valid one will be used
335
     *  - an SSViewer instance
336
     *
337
     * @param string|array|SSViewer $template the template to render into
338
     * @param array $customFields fields to customise() the object with before rendering
339
     * @return DBHTMLText
340
     */
341
    public function renderWith($template, $customFields = null)
342
    {
343
        if (!is_object($template)) {
344
            $template = new SSViewer($template);
345
        }
346
347
        $data = ($this->customisedObject) ? $this->customisedObject : $this;
348
349
        if ($customFields instanceof ViewableData) {
350
            $data = $data->customise($customFields);
351
        }
352
        if ($template instanceof SSViewer) {
353
            return $template->process($data, is_array($customFields) ? $customFields : null);
354
        }
355
356
        throw new UnexpectedValueException(
357
            "ViewableData::renderWith(): unexpected ".get_class($template)." object, expected an SSViewer instance"
358
        );
359
    }
360
361
    /**
362
     * Generate the cache name for a field
363
     *
364
     * @param string $fieldName Name of field
365
     * @param array $arguments List of optional arguments given
366
     * @return string
367
     */
368
    protected function objCacheName($fieldName, $arguments)
369
    {
370
        return $arguments
371
            ? $fieldName . ":" . implode(',', $arguments)
372
            : $fieldName;
373
    }
374
375
    /**
376
     * Get a cached value from the field cache
377
     *
378
     * @param string $key Cache key
379
     * @return mixed
380
     */
381
    protected function objCacheGet($key)
382
    {
383
        if (isset($this->objCache[$key])) {
384
            return $this->objCache[$key];
385
        }
386
        return null;
387
    }
388
389
    /**
390
     * Store a value in the field cache
391
     *
392
     * @param string $key Cache key
393
     * @param mixed $value
394
     * @return $this
395
     */
396
    protected function objCacheSet($key, $value)
397
    {
398
        $this->objCache[$key] = $value;
399
        return $this;
400
    }
401
402
    /**
403
     * Clear object cache
404
     *
405
     * @return $this
406
     */
407
    protected function objCacheClear()
408
    {
409
        $this->objCache = [];
410
        return $this;
411
    }
412
413
    /**
414
     * Get the value of a field on this object, automatically inserting the value into any available casting objects
415
     * that have been specified.
416
     *
417
     * @param string $fieldName
418
     * @param array $arguments
419
     * @param bool $cache Cache this object
420
     * @param string $cacheName a custom cache name
421
     * @return Object|DBField
422
     */
423
    public function obj($fieldName, $arguments = [], $cache = false, $cacheName = null)
424
    {
425
        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...
426
            $cacheName = $this->objCacheName($fieldName, $arguments);
427
        }
428
429
        // Check pre-cached value
430
        $value = $cache ? $this->objCacheGet($cacheName) : null;
431
        if ($value !== null) {
432
            return $value;
433
        }
434
435
        // Load value from record
436
        if ($this->hasMethod($fieldName)) {
437
            $value = call_user_func_array(array($this, $fieldName), $arguments ?: []);
438
        } else {
439
            $value = $this->$fieldName;
440
        }
441
442
        // Cast object
443
        if (!is_object($value)) {
444
            // Force cast
445
            $castingHelper = $this->castingHelper($fieldName);
446
            $valueObject = Object::create_from_string($castingHelper, $fieldName);
447
            $valueObject->setValue($value, $this);
448
            $value = $valueObject;
449
        }
450
451
        // Record in cache
452
        if ($cache) {
453
            $this->objCacheSet($cacheName, $value);
454
        }
455
456
        return $value;
457
    }
458
459
    /**
460
     * A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again
461
     * without re-running the method.
462
     *
463
     * @param string $field
464
     * @param array $arguments
465
     * @param string $identifier an optional custom cache identifier
466
     * @return Object|DBField
467
     */
468
    public function cachedCall($field, $arguments = [], $identifier = null)
469
    {
470
        return $this->obj($field, $arguments, true, $identifier);
471
    }
472
473
    /**
474
     * Checks if a given method/field has a valid value. If the result is an object, this will return the result of the
475
     * exists method, otherwise will check if the result is not just an empty paragraph tag.
476
     *
477
     * @param string $field
478
     * @param array $arguments
479
     * @param bool $cache
480
     * @return bool
481
     */
482
    public function hasValue($field, $arguments = [], $cache = true)
483
    {
484
        $result = $this->obj($field, $arguments, $cache);
485
            return $result->exists();
486
    }
487
488
    /**
489
     * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
490
     * template.
491
     *
492
     * @param string $field
493
     * @param array $arguments
494
     * @param bool $cache
495
     * @return string
496
     */
497
    public function XML_val($field, $arguments = [], $cache = false)
498
    {
499
        $result = $this->obj($field, $arguments, $cache);
500
        // Might contain additional formatting over ->XML(). E.g. parse shortcodes, nl2br()
501
        return $result->forTemplate();
502
    }
503
504
    /**
505
     * Get an array of XML-escaped values by field name
506
     *
507
     * @param array $fields an array of field names
508
     * @return array
509
     */
510
    public function getXMLValues($fields)
511
    {
512
        $result = array();
513
514
        foreach ($fields as $field) {
515
            $result[$field] = $this->XML_val($field);
516
        }
517
518
        return $result;
519
    }
520
521
    // ITERATOR SUPPORT ------------------------------------------------------------------------------------------------
522
523
    /**
524
     * Return a single-item iterator so you can iterate over the fields of a single record.
525
     *
526
     * This is useful so you can use a single record inside a <% control %> block in a template - and then use
527
     * to access individual fields on this object.
528
     *
529
     * @return ArrayIterator
530
     */
531
    public function getIterator()
532
    {
533
        return new ArrayIterator(array($this));
534
    }
535
536
    // UTILITY METHODS -------------------------------------------------------------------------------------------------
537
538
    /**
539
     * When rendering some objects it is necessary to iterate over the object being rendered, to do this, you need
540
     * access to itself.
541
     *
542
     * @return ViewableData
543
     */
544
    public function Me()
545
    {
546
        return $this;
547
    }
548
549
    /**
550
     * Get part of the current classes ancestry to be used as a CSS class.
551
     *
552
     * This method returns an escaped string of CSS classes representing the current classes ancestry until it hits a
553
     * stop point - e.g. "Page DataObject ViewableData".
554
     *
555
     * @param string $stopAtClass the class to stop at (default: ViewableData)
556
     * @return string
557
     * @uses ClassInfo
558
     */
559
    public function CSSClasses($stopAtClass = 'SilverStripe\\View\\ViewableData')
560
    {
561
        $classes       = array();
562
        $classAncestry = array_reverse(ClassInfo::ancestry($this->class));
563
        $stopClasses   = ClassInfo::ancestry($stopAtClass);
564
565
        foreach ($classAncestry as $class) {
566
            if (in_array($class, $stopClasses)) {
567
                break;
568
            }
569
            $classes[] = $class;
570
        }
571
572
        // optionally add template identifier
573
        if (isset($this->template) && !in_array($this->template, $classes)) {
574
            $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...
575
        }
576
577
        // Strip out namespaces
578
        $classes = preg_replace('#.*\\\\#', '', $classes);
579
580
        return Convert::raw2att(implode(' ', $classes));
581
    }
582
583
    /**
584
     * Return debug information about this object that can be rendered into a template
585
     *
586
     * @return ViewableData_Debugger
587
     */
588
    public function Debug()
589
    {
590
        return new ViewableData_Debugger($this);
591
    }
592
}
593