Completed
Push — master ( deca00...5388ff )
by Sam
24s
created

ViewableData::getXMLValues()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
rs 9.4285
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()
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...
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);
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 = 'SilverStripe\\View\\ViewableData')
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