Completed
Push — master ( 0fa727...78892f )
by Damian
11:35
created

ViewableData::defineMethods()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 12
rs 9.2
cc 4
eloc 7
nc 5
nop 0
1
<?php
2
3
use SilverStripe\Model\FieldType\DBVarchar;
4
5
/**
6
 * A ViewableData object is any object that can be rendered into a template/view.
7
 *
8
 * A view interrogates the object being currently rendered in order to get data to render into the template. This data
9
 * is provided and automatically escaped by ViewableData. Any class that needs to be available to a view (controllers,
10
 * {@link DataObject}s, page controls) should inherit from this class.
11
 *
12
 * @package framework
13
 * @subpackage view
14
 */
15
class ViewableData extends Object implements IteratorAggregate {
16
	
17
	/**
18
	 * An array of objects to cast certain fields to. This is set up as an array in the format:
19
	 *
20
	 * <code>
21
	 * public static $casting = array (
22
	 *     'FieldName' => 'ClassToCastTo(Arguments)'
23
	 * );
24
	 * </code>
25
	 *
26
	 * @var array
27
	 * @config
28
	 */
29
	private static $casting = array(
30
		'CSSClasses' => 'Varchar'
31
	);
32
	
33
	/**
34
	 * The default object to cast scalar fields to if casting information is not specified, and casting to an object
35
	 * is required.
36
	 *
37
	 * @var string
38
	 * @config
39
	 */
40
	private static $default_cast = 'Text';
41
	
42
	/**
43
	 * @var array
44
	 */
45
	private static $casting_cache = array();
46
	
47
	// -----------------------------------------------------------------------------------------------------------------
48
49
	/**
50
	 * A failover object to attempt to get data from if it is not present on this object.
51
	 *
52
	 * @var ViewableData
53
	 */
54
	protected $failover;
55
	
56
	/**
57
	 * @var ViewableData
58
	 */
59
	protected $customisedObject;
60
	
61
	/**
62
	 * @var array
63
	 */
64
	private $objCache = array();
65
	
66
	// -----------------------------------------------------------------------------------------------------------------
67
	
68
	/**
69
	 * Converts a field spec into an object creator. For example: "Int" becomes "new Int($fieldName);" and "Varchar(50)"
70
	 * becomes "new DBVarchar($fieldName, 50);".
71
	 *
72
	 * @param string $fieldSchema The field spec
73
	 * @return string
74
	 */
75
	public static function castingObjectCreator($fieldSchema) {
0 ignored issues
show
Unused Code introduced by
The parameter $fieldSchema is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
76
		Deprecation::notice('2.5', 'Use Object::create_from_string() instead');
77
	}
78
	
79
	/**
80
	 * Convert a field schema (e.g. "Varchar(50)") into a casting object creator array that contains both a className
81
	 * and castingHelper constructor code. See {@link castingObjectCreator} for more information about the constructor.
82
	 *
83
	 * @param string $fieldSchema
84
	 * @return array
85
	 */
86
	public static function castingObjectCreatorPair($fieldSchema) {
0 ignored issues
show
Unused Code introduced by
The parameter $fieldSchema is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
87
		Deprecation::notice('2.5', 'Use Object::create_from_string() instead');
88
	}
89
	
90
	// FIELD GETTERS & SETTERS -----------------------------------------------------------------------------------------
91
	
92
	/**
93
	 * Check if a field exists on this object or its failover.
94
	 *
95
	 * @param string $property
96
	 * @return bool
97
	 */
98
	public function __isset($property) {
99
		return $this->hasField($property) || ($this->failover && $this->failover->hasField($property));
100
	}
101
	
102
	/**
103
	 * Get the value of a property/field on this object. This will check if a method called get{$property} exists, then
104
	 * check if a field is available using {@link ViewableData::getField()}, then fall back on a failover object.
105
	 *
106
	 * @param string $property
107
	 * @return mixed
108
	 */
109
	public function __get($property) {
110
		if($this->hasMethod($method = "get$property")) {
111
			return $this->$method();
112
		} elseif($this->hasField($property)) {
113
			return $this->getField($property);
114
		} elseif($this->failover) {
115
			return $this->failover->$property;
116
		}
117
	}
118
	
119
	/**
120
	 * Set a property/field on this object. This will check for the existence of a method called set{$property}, then
121
	 * use the {@link ViewableData::setField()} method.
122
	 *
123
	 * @param string $property
124
	 * @param mixed $value
125
	 */
126
	public function __set($property, $value) {
127
		if($this->hasMethod($method = "set$property")) {
128
			$this->$method($value);
129
		} else {
130
			$this->setField($property, $value);
131
		}
132
	}
133
	
134
	/**
135
	 * Set a failover object to attempt to get data from if it is not present on this object.
136
	 *
137
	 * @param ViewableData $failover
138
	 */
139
	public function setFailover(ViewableData $failover) {
140
		$this->failover = $failover;
141
		$this->defineMethods();
142
	}
143
144
	/**
145
	 * Get the current failover object if set
146
	 *
147
	 * @return ViewableData|null
148
	 */
149
	public function getFailover() {
150
		return $this->failover;
151
	}
152
153
	/**
154
	 * Check if a field exists on this object. This should be overloaded in child classes.
155
	 *
156
	 * @param string $field
157
	 * @return bool
158
	 */
159
	public function hasField($field) {
160
		return property_exists($this, $field);
161
	}
162
	
163
	/**
164
	 * Get the value of a field on this object. This should be overloaded in child classes.
165
	 *
166
	 * @param string $field
167
	 * @return mixed
168
	 */
169
	public function getField($field) {
170
		return $this->$field;
171
	}
172
	
173
	/**
174
	 * Set a field on this object. This should be overloaded in child classes.
175
	 *
176
	 * @param string $field
177
	 * @param mixed $value
178
	 */
179
	public function setField($field, $value) {
180
		$this->$field = $value;
181
	}
182
	
183
	// -----------------------------------------------------------------------------------------------------------------
184
	
185
	/**
186
	 * Add methods from the {@link ViewableData::$failover} object, as well as wrapping any methods prefixed with an
187
	 * underscore into a {@link ViewableData::cachedCall()}.
188
	 */
189
	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...
190
		if($this->failover) {
191
			if(is_object($this->failover)) $this->addMethodsFrom('failover');
192
			else user_error("ViewableData::\$failover set to a non-object", E_USER_WARNING);
193
			
194
			if(isset($_REQUEST['debugfailover'])) {
195
				Debug::message("$this->class created with a failover class of {$this->failover->class}");
196
			}
197
		}
198
		
199
		parent::defineMethods();
200
	}
201
202
	/**
203
	 * Merge some arbitrary data in with this object. This method returns a {@link ViewableData_Customised} instance
204
	 * with references to both this and the new custom data.
205
	 *
206
	 * Note that any fields you specify will take precedence over the fields on this object.
207
	 *
208
	 * @param array|ViewableData $data
209
	 * @return ViewableData_Customised
210
	 */
211
	public function customise($data) {
212
		if(is_array($data) && (empty($data) || ArrayLib::is_associative($data))) {
213
			$data = new ArrayData($data);
214
		}
215
		
216
		if($data instanceof ViewableData) {
217
			return new ViewableData_Customised($this, $data);
218
		}
219
		
220
		throw new InvalidArgumentException (
221
			'ViewableData->customise(): $data must be an associative array or a ViewableData instance'
222
		);
223
	}
224
	
225
	/**
226
	 * @return ViewableData
227
	 */
228
	public function getCustomisedObj() {
229
		return $this->customisedObject;
230
	}
231
232
	/**
233
	 * @param ViewableData $object
234
	 */
235
	public function setCustomisedObj(ViewableData $object) {
236
		$this->customisedObject = $object;
237
	}
238
	
239
	// CASTING ---------------------------------------------------------------------------------------------------------
240
	
241
	/**
242
	 * Get the class a field on this object would be casted to, as well as the casting helper for casting a field to
243
	 * an object (see {@link ViewableData::castingHelper()} for information on casting helpers).
244
	 *
245
	 * The returned array contains two keys:
246
	 *  - className: the class the field would be casted to (e.g. "Varchar")
247
	 *  - castingHelper: the casting helper for casting the field (e.g. "return new Varchar($fieldName)")
248
	 *
249
	 * @param string $field
250
	 * @return array
251
	 */
252
	public function castingHelperPair($field) {
253
		Deprecation::notice('2.5', 'use castingHelper() instead');
254
		return $this->castingHelper($field);
255
	}
256
257
	/**
258
	 * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field
259
	 * on this object.
260
	 *
261
	 * @param string $field
262
	 * @return string Casting helper
263
	 */
264
	public function castingHelper($field) {
265
		$specs = $this->config()->casting;
0 ignored issues
show
Documentation introduced by
The property casting does not exist on object<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...
266
		if(isset($specs[$field])) {
267
			return $specs[$field];
268
		} elseif($this->failover) {
269
			return $this->failover->castingHelper($field);
270
		}
271
	}
272
	
273
	/**
274
	 * Get the class name a field on this object will be casted to
275
	 *
276
	 * @param string $field
277
	 * @return string
278
	 */
279
	public function castingClass($field) {
280
		$spec = $this->castingHelper($field);
281
		if(!$spec) return null;
282
		
283
		$bPos = strpos($spec,'(');
284
		if($bPos === false) return $spec;
285
		else return substr($spec, 0, $bPos);
286
	}
287
	
288
	/**
289
	 * Return the string-format type for the given field.
290
	 *
291
	 * @param string $field
292
	 * @return string 'xml'|'raw'
293
	 */
294
	public function escapeTypeForField($field) {
295
		$class = $this->castingClass($field) ?: $this->config()->default_cast;
296
297
		// TODO: It would be quicker not to instantiate the object, but to merely
298
		// get its class from the Injector
299
		return Injector::inst()->get($class, true)->config()->escape_type;
300
	}
301
302
	/**
303
	 * Save the casting cache for this object (including data from any failovers) into a variable
304
	 *
305
	 * @param reference $cache
306
	 */
307
	public function buildCastingCache(&$cache) {
308
		$ancestry = array_reverse(ClassInfo::ancestry($this->class));
309
		$merge    = true;
310
		
311
		foreach($ancestry as $class) {
312
			if(!isset(self::$casting_cache[$class]) && $merge) {
313
				$mergeFields = is_subclass_of($class, 'DataObject') ? array('db', 'casting') : array('casting');
314
				
315
				if($mergeFields) foreach($mergeFields as $field) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mergeFields of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
316
					$casting = Config::inst()->get($class, $field, Config::UNINHERITED);
317
					if($casting) foreach($casting as $field => $cast) {
0 ignored issues
show
Bug introduced by
The expression $casting of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
318
						if(!isset($cache[$field])) $cache[$field] = self::castingObjectCreatorPair($cast);
319
					}
320
				}
321
				
322
				if($class == 'ViewableData') $merge = false;
323
			} elseif($merge) {
324
				$cache = ($cache) ? array_merge(self::$casting_cache[$class], $cache) : self::$casting_cache[$class];
325
			}
326
			
327
			if($class == 'ViewableData') break;
328
		}
329
	}
330
	
331
	// TEMPLATE ACCESS LAYER -------------------------------------------------------------------------------------------
332
	
333
	/**
334
	 * Render this object into the template, and get the result as a string. You can pass one of the following as the
335
	 * $template parameter:
336
	 *  - a template name (e.g. Page)
337
	 *  - an array of possible template names - the first valid one will be used
338
	 *  - an SSViewer instance
339
	 *
340
	 * @param string|array|SSViewer $template the template to render into
341
	 * @param array $customFields fields to customise() the object with before rendering
342
	 * @return HTMLText
343
	 */
344
	public function renderWith($template, $customFields = null) {
345
		if(!is_object($template)) {
346
			$template = new SSViewer($template);
347
		}
348
		
349
		$data = ($this->customisedObject) ? $this->customisedObject : $this;
350
		
351
		if($customFields instanceof ViewableData) {
352
			$data = $data->customise($customFields);
353
		}
354
		if($template instanceof SSViewer) {
355
			return $template->process($data, is_array($customFields) ? $customFields : null);
356
		}
357
		
358
		throw new UnexpectedValueException (
359
			"ViewableData::renderWith(): unexpected $template->class object, expected an SSViewer instance"
360
		);
361
	}
362
363
	/**
364
	 * Generate the cache name for a field
365
	 *
366
	 * @param string $fieldName Name of field
367
	 * @param array $arguments List of optional arguments given
368
	 */
369
	protected function objCacheName($fieldName, $arguments) {
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
		if(isset($this->objCache[$key])) return $this->objCache[$key];
383
	}
384
385
	/**
386
	 * Store a value in the field cache
387
	 *
388
	 * @param string $key Cache key
389
	 * @param mixed $value
390
	 */
391
	protected function objCacheSet($key, $value) {
392
		$this->objCache[$key] = $value;
393
	}
394
	
395
	/**
396
	 * Get the value of a field on this object, automatically inserting the value into any available casting objects
397
	 * that have been specified.
398
	 *
399
	 * @param string $fieldName
400
	 * @param array $arguments
401
	 * @param bool $forceReturnedObject if TRUE, the value will ALWAYS be casted to an object before being returned,
402
	 *        even if there is no explicit casting information
403
	 * @param bool $cache Cache this object
404
	 * @param string $cacheName a custom cache name
405
	 */
406
	public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
407
		if(!$cacheName && $cache) $cacheName = $this->objCacheName($fieldName, $arguments);
0 ignored issues
show
Bug introduced by
It seems like $arguments defined by parameter $arguments on line 406 can also be of type null; however, ViewableData::objCacheName() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug Best Practice introduced by
The expression $cacheName of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
408
409
		$value = $cache ? $this->objCacheGet($cacheName) : null;
410
		if(!isset($value)) {
411
			// HACK: Don't call the deprecated FormField::Name() method
412
			$methodIsAllowed = true;
413
			if($this instanceof FormField && $fieldName == 'Name') $methodIsAllowed = false;
414
			
415
			if($methodIsAllowed && $this->hasMethod($fieldName)) {
416
				$value = $arguments ? call_user_func_array(array($this, $fieldName), $arguments) : $this->$fieldName();
417
			} else {
418
				$value = $this->$fieldName;
419
			}
420
			
421
			if(!is_object($value) && ($this->castingClass($fieldName) || $forceReturnedObject)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->castingClass($fieldName) of type null|string is loosely compared to true; 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...
422
				if(!$castConstructor = $this->castingHelper($fieldName)) {
423
					$castConstructor = $this->config()->default_cast;
0 ignored issues
show
Documentation introduced by
The property default_cast does not exist on object<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...
424
				}
425
				
426
				$valueObject = Object::create_from_string($castConstructor, $fieldName);
427
				$valueObject->setValue($value, $this);
428
				
429
				$value = $valueObject;
430
			}
431
			
432
			if($cache) $this->objCacheSet($cacheName, $value);
433
		}
434
		
435
		if(!is_object($value) && $forceReturnedObject) {
436
			$default = $this->config()->default_cast;
0 ignored issues
show
Documentation introduced by
The property default_cast does not exist on object<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...
437
			$castedValue = new $default($fieldName);
438
			$castedValue->setValue($value);
439
			$value = $castedValue;
440
		}
441
		
442
		return $value;
443
	}
444
	
445
	/**
446
	 * A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again
447
	 * without re-running the method.
448
	 *
449
	 * @param string $field
450
	 * @param array $arguments
451
	 * @param string $identifier an optional custom cache identifier
452
	 */
453
	public function cachedCall($field, $arguments = null, $identifier = null) {
454
		return $this->obj($field, $arguments, false, true, $identifier);
455
	}
456
	
457
	/**
458
	 * Checks if a given method/field has a valid value. If the result is an object, this will return the result of the
459
	 * exists method, otherwise will check if the result is not just an empty paragraph tag.
460
	 *
461
	 * @param string $field
462
	 * @param array $arguments
463
	 * @param bool $cache
464
	 * @return bool
465
	 */
466
	public function hasValue($field, $arguments = null, $cache = true) {
467
		$result = $cache ? $this->cachedCall($field, $arguments) : $this->obj($field, $arguments, false, false);
468
		
469
		if(is_object($result) && $result instanceof Object) {
470
			return $result->exists();
471
		} else {
472
			// Empty paragraph checks are a workaround for TinyMCE
473
			return ($result && $result !== '<p></p>');
474
		}
475
	}
476
	
477
	/**#@+
478
	 * @param string $field
479
	 * @param array $arguments
480
	 * @param bool $cache
481
	 * @return string
482
	 */
483
	
484
	/**
485
	 * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
486
	 * template.
487
	 */
488
	public function XML_val($field, $arguments = null, $cache = false) {
489
		$result = $this->obj($field, $arguments, false, $cache);
490
		return (is_object($result) && $result instanceof Object) ? $result->forTemplate() : $result;
491
	}
492
	
493
	/**
494
	 * Return the value of the field without any escaping being applied.
495
	 */
496
	public function RAW_val($field, $arguments = null, $cache = true) {
497
		return Convert::xml2raw($this->XML_val($field, $arguments, $cache));
498
	}
499
	
500
	/**
501
	 * Return the value of a field in an SQL-safe format.
502
	 */
503
	public function SQL_val($field, $arguments = null, $cache = true) {
504
		return Convert::raw2sql($this->RAW_val($field, $arguments, $cache));
505
	}
506
	
507
	/**
508
	 * Return the value of a field in a JavaScript-save format.
509
	 */
510
	public function JS_val($field, $arguments = null, $cache = true) {
511
		return Convert::raw2js($this->RAW_val($field, $arguments, $cache));
512
	}
513
	
514
	/**
515
	 * Return the value of a field escaped suitable to be inserted into an XML node attribute.
516
	 */
517
	public function ATT_val($field, $arguments = null, $cache = true) {
518
		return Convert::raw2att($this->RAW_val($field, $arguments, $cache));
519
	}
520
	
521
	/**#@-*/
522
	
523
	/**
524
	 * Get an array of XML-escaped values by field name
525
	 *
526
	 * @param array $elements an array of field names
0 ignored issues
show
Bug introduced by
There is no parameter named $elements. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
527
	 * @return array
528
	 */
529
	public function getXMLValues($fields) {
530
		$result = array();
531
		
532
		foreach($fields as $field) {
533
			$result[$field] = $this->XML_val($field);
534
		}
535
		
536
		return $result;
537
	}
538
	
539
	// ITERATOR SUPPORT ------------------------------------------------------------------------------------------------
540
	
541
	/**
542
	 * Return a single-item iterator so you can iterate over the fields of a single record.
543
	 *
544
	 * This is useful so you can use a single record inside a <% control %> block in a template - and then use
545
	 * to access individual fields on this object.
546
	 *
547
	 * @return ArrayIterator
548
	 */
549
	public function getIterator() {
550
		return new ArrayIterator(array($this));
551
	}
552
	
553
	// UTILITY METHODS -------------------------------------------------------------------------------------------------
554
	
555
	/**
556
	 * When rendering some objects it is necessary to iterate over the object being rendered, to do this, you need
557
	 * access to itself.
558
	 *
559
	 * @return ViewableData
560
	 */
561
	public function Me() {
562
		return $this;
563
	}
564
	
565
	/**
566
	 * Return the directory if the current active theme (relative to the site root).
567
	 *
568
	 * This method is useful for things such as accessing theme images from your template without hardcoding the theme
569
	 * page - e.g. <img src="$ThemeDir/images/something.gif">.
570
	 *
571
	 * This method should only be used when a theme is currently active. However, it will fall over to the current
572
	 * project directory.
573
	 *
574
	 * @param string $subtheme the subtheme path to get
575
	 * @return string
576
	 */
577
	public function ThemeDir($subtheme = false) {
578
		if(
579
			Config::inst()->get('SSViewer', 'theme_enabled') 
580
			&& $theme = Config::inst()->get('SSViewer', 'theme')
581
		) {
582
			return THEMES_DIR . "/$theme" . ($subtheme ? "_$subtheme" : null);
583
		}
584
		
585
		return project();
586
	}
587
	
588
	/**
589
	 * Get part of the current classes ancestry to be used as a CSS class.
590
	 *
591
	 * This method returns an escaped string of CSS classes representing the current classes ancestry until it hits a
592
	 * stop point - e.g. "Page DataObject ViewableData".
593
	 *
594
	 * @param string $stopAtClass the class to stop at (default: ViewableData)
595
	 * @return string
596
	 * @uses ClassInfo
597
	 */
598
	public function CSSClasses($stopAtClass = 'ViewableData') {
599
		$classes       = array();
600
		$classAncestry = array_reverse(ClassInfo::ancestry($this->class));
601
		$stopClasses   = ClassInfo::ancestry($stopAtClass);
602
		
603
		foreach($classAncestry as $class) {
604
			if(in_array($class, $stopClasses)) break;
605
			$classes[] = $class;
606
		}
607
		
608
		// optionally add template identifier
609
		if(isset($this->template) && !in_array($this->template, $classes)) {
610
			$classes[] = $this->template;
0 ignored issues
show
Documentation introduced by
The property template does not exist on object<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...
611
		}
612
		
613
		return Convert::raw2att(implode(' ', $classes));
614
	}
615
616
	/**
617
	 * Return debug information about this object that can be rendered into a template
618
	 *
619
	 * @return ViewableData_Debugger
620
	 */
621
	public function Debug() {
622
		return new ViewableData_Debugger($this);
623
	}
624
	
625
}
626
627
/**
628
 * @package framework
629
 * @subpackage view
630
 */
631
class ViewableData_Customised extends ViewableData {
632
	
633
	/**
634
	 * @var ViewableData
635
	 */
636
	protected $original, $customised;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
637
	
638
	/**
639
	 * Instantiate a new customised ViewableData object
640
	 *
641
	 * @param ViewableData $originalObject
642
	 * @param ViewableData $customisedObject
643
	 */
644
	public function __construct(ViewableData $originalObject, ViewableData $customisedObject) {
645
		$this->original   = $originalObject;
646
		$this->customised = $customisedObject;
647
		
648
		$this->original->setCustomisedObj($this);
649
		
650
		parent::__construct();
651
	}
652
	
653
	public function __call($method, $arguments) {
654
		if($this->customised->hasMethod($method)) {
655
			return call_user_func_array(array($this->customised, $method), $arguments);
656
		}
657
		
658
		return call_user_func_array(array($this->original, $method), $arguments);
659
	}
660
	
661
	public function __get($property) {
662
		if(isset($this->customised->$property)) {
663
			return $this->customised->$property;
664
		}
665
		
666
		return $this->original->$property;
667
	}
668
	
669
	public function __set($property, $value) {
670
		$this->customised->$property = $this->original->$property = $value;
671
	}
672
	
673
	public function hasMethod($method) {
674
		return $this->customised->hasMethod($method) || $this->original->hasMethod($method);
675
	}
676
	
677
	public function cachedCall($field, $arguments = null, $identifier = null) {
678
		if($this->customised->hasMethod($field) || $this->customised->hasField($field)) {
679
			$result = $this->customised->cachedCall($field, $arguments, $identifier);
680
		} else {
681
			$result = $this->original->cachedCall($field, $arguments, $identifier);
682
		}
683
		
684
		return $result;
685
	}
686
	
687
	public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
688
		if($this->customised->hasField($fieldName) || $this->customised->hasMethod($fieldName)) {
689
			return $this->customised->obj($fieldName, $arguments, $forceReturnedObject, $cache, $cacheName);
690
		}
691
		
692
		return $this->original->obj($fieldName, $arguments, $forceReturnedObject, $cache, $cacheName);
693
	}
694
	
695
}
696
697
/**
698
 * Allows you to render debug information about a {@link ViewableData} object into a template.
699
 *
700
 * @package framework
701
 * @subpackage view
702
 */
703
class ViewableData_Debugger extends ViewableData {
704
	
705
	/**
706
	 * @var ViewableData
707
	 */
708
	protected $object;
709
	
710
	/**
711
	 * @param ViewableData $object
712
	 */
713
	public function __construct(ViewableData $object) {
714
		$this->object = $object;
715
		parent::__construct();
716
	}
717
718
	/**
719
	 * @return string The rendered debugger
720
	 */
721
	public function __toString() {
722
		return $this->forTemplate();
723
	}
724
725
	/**
726
	 * Return debugging information, as XHTML. If a field name is passed, it will show debugging information on that
727
	 * field, otherwise it will show information on all methods and fields.
728
	 *
729
	 * @param string $field the field name
730
	 * @return string
731
	 */
732
	public function forTemplate($field = null) {
733
		// debugging info for a specific field
734
		if($field) return "<b>Debugging Information for {$this->class}->{$field}</b><br/>" .
0 ignored issues
show
Bug Best Practice introduced by
The expression $field of type string|null is loosely compared to true; 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...
735
			($this->object->hasMethod($field)? "Has method '$field'<br/>" : null)             .
736
			($this->object->hasField($field) ? "Has field '$field'<br/>"  : null)             ;
737
		
738
		// debugging information for the entire class
739
		$reflector = new ReflectionObject($this->object);
740
		$debug     = "<b>Debugging Information: all methods available in '{$this->object->class}'</b><br/><ul>";
741
		
742
		foreach($this->object->allMethodNames() as $method) {
743
			// check that the method is public
744
			if($method[0] === strtoupper($method[0]) && $method[0] != '_') {
745
				if($reflector->hasMethod($method) && $method = $reflector->getMethod($method)) {
746
					if($method->isPublic()) {
747
						$debug .= "<li>\${$method->getName()}";
748
						
749
						if(count($method->getParameters())) {
750
							$debug .= ' <small>(' . implode(', ', $method->getParameters()) . ')</small>';
751
						}
752
						
753
						$debug .= '</li>';
754
					}
755
				} else {
756
					$debug .= "<li>\$$method</li>";
757
				}
758
			}
759
		}
760
		
761
		$debug .= '</ul>';
762
		
763
		if($this->object->hasMethod('toMap')) {
764
			$debug .= "<b>Debugging Information: all fields available in '{$this->object->class}'</b><br/><ul>";
765
			
766
			foreach($this->object->toMap() as $field => $value) {
767
				$debug .= "<li>\$$field</li>";
768
			}
769
			
770
			$debug .= "</ul>";
771
		}
772
		
773
		// check for an extra attached data
774
		if($this->object->hasMethod('data') && $this->object->data() != $this->object) {
775
			$debug .= ViewableData_Debugger::create($this->object->data())->forTemplate();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
776
		}
777
		
778
		return $debug;
779
	}
780
781
}
782