Completed
Push — authenticator-refactor ( 0a18bb...b9e528 )
by Simon
08:12
created

ViewableData::getCustomisedObj()   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 Exception;
6
use SilverStripe\Core\Config\Configurable;
7
use SilverStripe\Core\Extensible;
8
use SilverStripe\Core\Injector\Injectable;
9
use SilverStripe\ORM\ArrayLib;
10
use SilverStripe\ORM\FieldType\DBField;
11
use SilverStripe\ORM\FieldType\DBHTMLText;
12
use SilverStripe\Core\ClassInfo;
13
use SilverStripe\Core\Convert;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\Dev\Debug;
16
use IteratorAggregate;
17
use LogicException;
18
use InvalidArgumentException;
19
use UnexpectedValueException;
20
use ArrayIterator;
21
22
/**
23
 * A ViewableData object is any object that can be rendered into a template/view.
24
 *
25
 * A view interrogates the object being currently rendered in order to get data to render into the template. This data
26
 * is provided and automatically escaped by ViewableData. Any class that needs to be available to a view (controllers,
27
 * {@link DataObject}s, page controls) should inherit from this class.
28
 */
29
class ViewableData implements IteratorAggregate
30
{
31
    use Extensible {
32
        defineMethods as extensibleDefineMethods;
33
    }
34
    use Injectable;
35
    use Configurable;
36
37
    /**
38
     * An array of objects to cast certain fields to. This is set up as an array in the format:
39
     *
40
     * <code>
41
     * public static $casting = array (
42
     *     'FieldName' => 'ClassToCastTo(Arguments)'
43
     * );
44
     * </code>
45
     *
46
     * @var array
47
     * @config
48
     */
49
    private static $casting = array(
50
        'CSSClasses' => 'Varchar'
51
    );
52
53
    /**
54
     * The default object to cast scalar fields to if casting information is not specified, and casting to an object
55
     * is required.
56
     *
57
     * @var string
58
     * @config
59
     */
60
    private static $default_cast = 'Text';
61
62
    /**
63
     * @var array
64
     */
65
    private static $casting_cache = array();
66
67
    // -----------------------------------------------------------------------------------------------------------------
68
69
    /**
70
     * A failover object to attempt to get data from if it is not present on this object.
71
     *
72
     * @var ViewableData
73
     */
74
    protected $failover;
75
76
    /**
77
     * @var ViewableData
78
     */
79
    protected $customisedObject;
80
81
    /**
82
     * @var array
83
     */
84
    private $objCache = array();
85
86
    public function __construct()
87
    {
88
        $this->constructExtensions();
89
    }
90
91
    // -----------------------------------------------------------------------------------------------------------------
92
93
    // FIELD GETTERS & SETTERS -----------------------------------------------------------------------------------------
94
95
    /**
96
     * Check if a field exists on this object or its failover.
97
     * Note that, unlike the core isset() implementation, this will return true if the property is defined
98
     * and set to null.
99
     *
100
     * @param string $property
101
     * @return bool
102
     */
103
    public function __isset($property)
104
    {
105
        // getField() isn't a field-specific getter and shouldn't be treated as such
106
        if (strtolower($property) !== 'field' && $this->hasMethod($method = "get$property")) {
107
            return true;
108
        } elseif ($this->hasField($property)) {
109
            return true;
110
        } elseif ($this->failover) {
111
            return isset($this->failover->$property);
112
        }
113
114
        return false;
115
    }
116
117
    /**
118
     * Get the value of a property/field on this object. This will check if a method called get{$property} exists, then
119
     * check if a field is available using {@link ViewableData::getField()}, then fall back on a failover object.
120
     *
121
     * @param string $property
122
     * @return mixed
123
     */
124
    public function __get($property)
125
    {
126
        // getField() isn't a field-specific getter and shouldn't be treated as such
127
        if (strtolower($property) !== 'field' && $this->hasMethod($method = "get$property")) {
128
            return $this->$method();
129
        } elseif ($this->hasField($property)) {
130
            return $this->getField($property);
131
        } elseif ($this->failover) {
132
            return $this->failover->$property;
133
        }
134
135
        return null;
136
    }
137
138
    /**
139
     * Set a property/field on this object. This will check for the existence of a method called set{$property}, then
140
     * use the {@link ViewableData::setField()} method.
141
     *
142
     * @param string $property
143
     * @param mixed $value
144
     */
145
    public function __set($property, $value)
146
    {
147
        $this->objCacheClear();
148
        if ($this->hasMethod($method = "set$property")) {
149
            $this->$method($value);
150
        } else {
151
            $this->setField($property, $value);
152
        }
153
    }
154
155
    /**
156
     * Set a failover object to attempt to get data from if it is not present on this object.
157
     *
158
     * @param ViewableData $failover
159
     */
160
    public function setFailover(ViewableData $failover)
161
    {
162
        // Ensure cached methods from previous failover are removed
163
        if ($this->failover) {
164
            $this->removeMethodsFrom('failover');
165
        }
166
167
        $this->failover = $failover;
168
        $this->defineMethods();
169
    }
170
171
    /**
172
     * Get the current failover object if set
173
     *
174
     * @return ViewableData|null
175
     */
176
    public function getFailover()
177
    {
178
        return $this->failover;
179
    }
180
181
    /**
182
     * Check if a field exists on this object. This should be overloaded in child classes.
183
     *
184
     * @param string $field
185
     * @return bool
186
     */
187
    public function hasField($field)
188
    {
189
        return property_exists($this, $field);
190
    }
191
192
    /**
193
     * Get the value of a field on this object. This should be overloaded in child classes.
194
     *
195
     * @param string $field
196
     * @return mixed
197
     */
198
    public function getField($field)
199
    {
200
        return $this->$field;
201
    }
202
203
    /**
204
     * Set a field on this object. This should be overloaded in child classes.
205
     *
206
     * @param string $field
207
     * @param mixed $value
208
     * @return $this
209
     */
210
    public function setField($field, $value)
211
    {
212
        $this->objCacheClear();
213
        $this->$field = $value;
214
        return $this;
215
    }
216
217
    // -----------------------------------------------------------------------------------------------------------------
218
219
    /**
220
     * Add methods from the {@link ViewableData::$failover} object, as well as wrapping any methods prefixed with an
221
     * underscore into a {@link ViewableData::cachedCall()}.
222
     *
223
     * @throws LogicException
224
     */
225
    public function defineMethods()
226
    {
227
        if ($this->failover && !is_object($this->failover)) {
228
            throw new LogicException("ViewableData::\$failover set to a non-object");
229
        }
230
        if ($this->failover) {
231
            $this->addMethodsFrom('failover');
232
233 View Code Duplication
            if (isset($_REQUEST['debugfailover'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
234
                $class = static::class;
235
                $failoverClass = get_class($this->failover);
236
                Debug::message("$class created with a failover class of {$failoverClass}");
237
            }
238
        }
239
        $this->extensibleDefineMethods();
240
    }
241
242
    /**
243
     * Merge some arbitrary data in with this object. This method returns a {@link ViewableData_Customised} instance
244
     * with references to both this and the new custom data.
245
     *
246
     * Note that any fields you specify will take precedence over the fields on this object.
247
     *
248
     * @param array|ViewableData $data
249
     * @return ViewableData_Customised
250
     */
251
    public function customise($data)
252
    {
253
        if (is_array($data) && (empty($data) || ArrayLib::is_associative($data))) {
254
            $data = new ArrayData($data);
255
        }
256
257
        if ($data instanceof ViewableData) {
258
            return new ViewableData_Customised($this, $data);
259
        }
260
261
        throw new InvalidArgumentException(
262
            'ViewableData->customise(): $data must be an associative array or a ViewableData instance'
263
        );
264
    }
265
266
    /**
267
     * Return true if this object "exists" i.e. has a sensible value
268
     *
269
     * This method should be overriden in subclasses to provide more context about the classes state. For example, a
270
     * {@link DataObject} class could return false when it is deleted from the database
271
     *
272
     * @return bool
273
     */
274
    public function exists()
275
    {
276
        return true;
277
    }
278
279
    /**
280
     * @return string the class name
281
     */
282
    public function __toString()
283
    {
284
        return static::class;
285
    }
286
287
    /**
288
     * @return ViewableData
289
     */
290
    public function getCustomisedObj()
291
    {
292
        return $this->customisedObject;
293
    }
294
295
    /**
296
     * @param ViewableData $object
297
     */
298
    public function setCustomisedObj(ViewableData $object)
299
    {
300
        $this->customisedObject = $object;
301
    }
302
303
    // CASTING ---------------------------------------------------------------------------------------------------------
304
305
    /**
306
     * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object)
307
     * for a field on this object. This helper will be a subclass of DBField.
308
     *
309
     * @param string $field
310
     * @return string Casting helper As a constructor pattern, and may include arguments.
311
     * @throws Exception
312
     */
313
    public function castingHelper($field)
314
    {
315
        $specs = $this->config()->get('casting');
316
        if (isset($specs[$field])) {
317
            return $specs[$field];
318
        }
319
320
        // If no specific cast is declared, fall back to failover.
321
        // Note that if there is a failover, the default_cast will always
322
        // be drawn from this object instead of the top level object.
323
        $failover = $this->getFailover();
324
        if ($failover) {
325
            $cast = $failover->castingHelper($field);
326
            if ($cast) {
327
                return $cast;
328
            }
329
        }
330
331
        // Fall back to default_cast
332
        $default = self::config()->get('default_cast');
333
        if (empty($default)) {
334
            throw new Exception("No default_cast");
335
        }
336
        return $default;
337
    }
338
339
    /**
340
     * Get the class name a field on this object will be casted to.
341
     *
342
     * @param string $field
343
     * @return string
344
     */
345
    public function castingClass($field)
346
    {
347
        // Strip arguments
348
        $spec = $this->castingHelper($field);
349
        return trim(strtok($spec, '('));
350
    }
351
352
    /**
353
     * Return the string-format type for the given field.
354
     *
355
     * @param string $field
356
     * @return string 'xml'|'raw'
357
     */
358
    public function escapeTypeForField($field)
359
    {
360
        $class = $this->castingClass($field) ?: $this->config()->get('default_cast');
361
362
        // TODO: It would be quicker not to instantiate the object, but to merely
363
        // get its class from the Injector
364
        /** @var DBField $type */
365
        $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...
366
        return $type->config()->get('escape_type');
367
    }
368
369
    // TEMPLATE ACCESS LAYER -------------------------------------------------------------------------------------------
370
371
    /**
372
     * Render this object into the template, and get the result as a string. You can pass one of the following as the
373
     * $template parameter:
374
     *  - a template name (e.g. Page)
375
     *  - an array of possible template names - the first valid one will be used
376
     *  - an SSViewer instance
377
     *
378
     * @param string|array|SSViewer $template the template to render into
379
     * @param array $customFields fields to customise() the object with before rendering
380
     * @return DBHTMLText
381
     */
382
    public function renderWith($template, $customFields = null)
383
    {
384
        if (!is_object($template)) {
385
            $template = new SSViewer($template);
386
        }
387
388
        $data = ($this->customisedObject) ? $this->customisedObject : $this;
389
390
        if ($customFields instanceof ViewableData) {
391
            $data = $data->customise($customFields);
392
        }
393
        if ($template instanceof SSViewer) {
394
            return $template->process($data, is_array($customFields) ? $customFields : null);
395
        }
396
397
        throw new UnexpectedValueException(
398
            "ViewableData::renderWith(): unexpected ".get_class($template)." object, expected an SSViewer instance"
399
        );
400
    }
401
402
    /**
403
     * Generate the cache name for a field
404
     *
405
     * @param string $fieldName Name of field
406
     * @param array $arguments List of optional arguments given
407
     * @return string
408
     */
409
    protected function objCacheName($fieldName, $arguments)
410
    {
411
        return $arguments
412
            ? $fieldName . ":" . implode(',', $arguments)
413
            : $fieldName;
414
    }
415
416
    /**
417
     * Get a cached value from the field cache
418
     *
419
     * @param string $key Cache key
420
     * @return mixed
421
     */
422
    protected function objCacheGet($key)
423
    {
424
        if (isset($this->objCache[$key])) {
425
            return $this->objCache[$key];
426
        }
427
        return null;
428
    }
429
430
    /**
431
     * Store a value in the field cache
432
     *
433
     * @param string $key Cache key
434
     * @param mixed $value
435
     * @return $this
436
     */
437
    protected function objCacheSet($key, $value)
438
    {
439
        $this->objCache[$key] = $value;
440
        return $this;
441
    }
442
443
    /**
444
     * Clear object cache
445
     *
446
     * @return $this
447
     */
448
    protected function objCacheClear()
449
    {
450
        $this->objCache = [];
451
        return $this;
452
    }
453
454
    /**
455
     * Get the value of a field on this object, automatically inserting the value into any available casting objects
456
     * that have been specified.
457
     *
458
     * @param string $fieldName
459
     * @param array $arguments
460
     * @param bool $cache Cache this object
461
     * @param string $cacheName a custom cache name
462
     * @return Object|DBField
463
     */
464
    public function obj($fieldName, $arguments = [], $cache = false, $cacheName = null)
465
    {
466
        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...
467
            $cacheName = $this->objCacheName($fieldName, $arguments);
468
        }
469
470
        // Check pre-cached value
471
        $value = $cache ? $this->objCacheGet($cacheName) : null;
472
        if ($value !== null) {
473
            return $value;
474
        }
475
476
        // Load value from record
477
        if ($this->hasMethod($fieldName)) {
478
            $value = call_user_func_array(array($this, $fieldName), $arguments ?: []);
479
        } else {
480
            $value = $this->$fieldName;
481
        }
482
483
        // Cast object
484
        if (!is_object($value)) {
485
            // Force cast
486
            $castingHelper = $this->castingHelper($fieldName);
487
            $valueObject = Injector::inst()->create($castingHelper, $fieldName);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Psr\Container\ContainerInterface as the method create() does only exist in the following implementations of said interface: SilverStripe\Core\Injector\Injector.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
488
            $valueObject->setValue($value, $this);
489
            $value = $valueObject;
490
        }
491
492
        // Record in cache
493
        if ($cache) {
494
            $this->objCacheSet($cacheName, $value);
495
        }
496
497
        return $value;
498
    }
499
500
    /**
501
     * A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again
502
     * without re-running the method.
503
     *
504
     * @param string $field
505
     * @param array $arguments
506
     * @param string $identifier an optional custom cache identifier
507
     * @return Object|DBField
508
     */
509
    public function cachedCall($field, $arguments = [], $identifier = null)
510
    {
511
        return $this->obj($field, $arguments, true, $identifier);
512
    }
513
514
    /**
515
     * Checks if a given method/field has a valid value. If the result is an object, this will return the result of the
516
     * exists method, otherwise will check if the result is not just an empty paragraph tag.
517
     *
518
     * @param string $field
519
     * @param array $arguments
520
     * @param bool $cache
521
     * @return bool
522
     */
523
    public function hasValue($field, $arguments = [], $cache = true)
524
    {
525
        $result = $this->obj($field, $arguments, $cache);
526
            return $result->exists();
527
    }
528
529
    /**
530
     * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
531
     * template.
532
     *
533
     * @param string $field
534
     * @param array $arguments
535
     * @param bool $cache
536
     * @return string
537
     */
538
    public function XML_val($field, $arguments = [], $cache = false)
539
    {
540
        $result = $this->obj($field, $arguments, $cache);
541
        // Might contain additional formatting over ->XML(). E.g. parse shortcodes, nl2br()
542
        return $result->forTemplate();
543
    }
544
545
    /**
546
     * Get an array of XML-escaped values by field name
547
     *
548
     * @param array $fields an array of field names
549
     * @return array
550
     */
551
    public function getXMLValues($fields)
552
    {
553
        $result = array();
554
555
        foreach ($fields as $field) {
556
            $result[$field] = $this->XML_val($field);
557
        }
558
559
        return $result;
560
    }
561
562
    // ITERATOR SUPPORT ------------------------------------------------------------------------------------------------
563
564
    /**
565
     * Return a single-item iterator so you can iterate over the fields of a single record.
566
     *
567
     * This is useful so you can use a single record inside a <% control %> block in a template - and then use
568
     * to access individual fields on this object.
569
     *
570
     * @return ArrayIterator
571
     */
572
    public function getIterator()
573
    {
574
        return new ArrayIterator(array($this));
575
    }
576
577
    // UTILITY METHODS -------------------------------------------------------------------------------------------------
578
579
    /**
580
     * When rendering some objects it is necessary to iterate over the object being rendered, to do this, you need
581
     * access to itself.
582
     *
583
     * @return ViewableData
584
     */
585
    public function Me()
586
    {
587
        return $this;
588
    }
589
590
    /**
591
     * Get part of the current classes ancestry to be used as a CSS class.
592
     *
593
     * This method returns an escaped string of CSS classes representing the current classes ancestry until it hits a
594
     * stop point - e.g. "Page DataObject ViewableData".
595
     *
596
     * @param string $stopAtClass the class to stop at (default: ViewableData)
597
     * @return string
598
     * @uses ClassInfo
599
     */
600
    public function CSSClasses($stopAtClass = self::class)
601
    {
602
        $classes       = array();
603
        $classAncestry = array_reverse(ClassInfo::ancestry(static::class));
604
        $stopClasses   = ClassInfo::ancestry($stopAtClass);
605
606
        foreach ($classAncestry as $class) {
607
            if (in_array($class, $stopClasses)) {
608
                break;
609
            }
610
            $classes[] = $class;
611
        }
612
613
        // optionally add template identifier
614
        if (isset($this->template) && !in_array($this->template, $classes)) {
615
            $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...
616
        }
617
618
        // Strip out namespaces
619
        $classes = preg_replace('#.*\\\\#', '', $classes);
620
621
        return Convert::raw2att(implode(' ', $classes));
622
    }
623
624
    /**
625
     * Return debug information about this object that can be rendered into a template
626
     *
627
     * @return ViewableData_Debugger
628
     */
629
    public function Debug()
630
    {
631
        return new ViewableData_Debugger($this);
632
    }
633
}
634