Completed
Push — master ( 03a324...633eb0 )
by Damian
21s
created

ViewableData::getFailover()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
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
		foreach($this->allMethodNames() as $method) {
200
			if($method[0] == '_' && $method[1] != '_') {
201
				$this->createMethod(
202
					substr($method, 1),
203
					"return \$obj->deprecatedCachedCall('$method', \$args, '" . substr($method, 1) . "');"
204
				);
205
			}
206
		}
207
		
208
		parent::defineMethods();
209
	}
210
211
	/**
212
	 * Method to facilitate deprecation of underscore-prefixed methods automatically being cached.
213
	 * 
214
	 * @param string $field
0 ignored issues
show
Bug introduced by
There is no parameter named $field. 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...
215
	 * @param array $arguments
0 ignored issues
show
Bug introduced by
There is no parameter named $arguments. 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...
216
	 * @param string $identifier an optional custom cache identifier
217
	 * @return unknown
218
	 */
219
	public function deprecatedCachedCall($method, $args = null, $identifier = null) {
220
		Deprecation::notice(
221
			'4.0',
222
			'You are calling an underscore-prefixed method (e.g. _mymethod()) without the underscore. This behaviour,
223
				and the caching logic behind it, has been deprecated.',
224
			Deprecation::SCOPE_GLOBAL
225
		);
226
		return $this->cachedCall($method, $args, $identifier);
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
		if(is_array($data) && (empty($data) || ArrayLib::is_associative($data))) {
239
			$data = new ArrayData($data);
240
		}
241
		
242
		if($data instanceof ViewableData) {
243
			return new ViewableData_Customised($this, $data);
244
		}
245
		
246
		throw new InvalidArgumentException (
247
			'ViewableData->customise(): $data must be an associative array or a ViewableData instance'
248
		);
249
	}
250
	
251
	/**
252
	 * @return ViewableData
253
	 */
254
	public function getCustomisedObj() {
255
		return $this->customisedObject;
256
	}
257
258
	/**
259
	 * @param ViewableData $object
260
	 */
261
	public function setCustomisedObj(ViewableData $object) {
262
		$this->customisedObject = $object;
263
	}
264
	
265
	// CASTING ---------------------------------------------------------------------------------------------------------
266
	
267
	/**
268
	 * Get the class a field on this object would be casted to, as well as the casting helper for casting a field to
269
	 * an object (see {@link ViewableData::castingHelper()} for information on casting helpers).
270
	 *
271
	 * The returned array contains two keys:
272
	 *  - className: the class the field would be casted to (e.g. "Varchar")
273
	 *  - castingHelper: the casting helper for casting the field (e.g. "return new Varchar($fieldName)")
274
	 *
275
	 * @param string $field
276
	 * @return array
277
	 */
278
	public function castingHelperPair($field) {
279
		Deprecation::notice('2.5', 'use castingHelper() instead');
280
		return $this->castingHelper($field);
281
	}
282
283
	/**
284
	 * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field
285
	 * on this object.
286
	 *
287
	 * @param string $field
288
	 * @return string Casting helper
289
	 */
290
	public function castingHelper($field) {
291
		$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...
292
		if(isset($specs[$field])) {
293
			return $specs[$field];
294
		} elseif($this->failover) {
295
			return $this->failover->castingHelper($field);
296
		}
297
	}
298
	
299
	/**
300
	 * Get the class name a field on this object will be casted to
301
	 *
302
	 * @param string $field
303
	 * @return string
304
	 */
305
	public function castingClass($field) {
306
		$spec = $this->castingHelper($field);
307
		if(!$spec) return null;
308
		
309
		$bPos = strpos($spec,'(');
310
		if($bPos === false) return $spec;
311
		else return substr($spec, 0, $bPos);
312
	}
313
	
314
	/**
315
	 * Return the string-format type for the given field.
316
	 *
317
	 * @param string $field
318
	 * @return string 'xml'|'raw'
319
	 */
320
	public function escapeTypeForField($field) {
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
	/**
329
	 * Save the casting cache for this object (including data from any failovers) into a variable
330
	 *
331
	 * @param reference $cache
332
	 */
333
	public function buildCastingCache(&$cache) {
334
		$ancestry = array_reverse(ClassInfo::ancestry($this->class));
335
		$merge    = true;
336
		
337
		foreach($ancestry as $class) {
338
			if(!isset(self::$casting_cache[$class]) && $merge) {
339
				$mergeFields = is_subclass_of($class, 'DataObject') ? array('db', 'casting') : array('casting');
340
				
341
				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...
342
					$casting = Config::inst()->get($class, $field, Config::UNINHERITED);
343
					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...
344
						if(!isset($cache[$field])) $cache[$field] = self::castingObjectCreatorPair($cast);
345
					}
346
				}
347
				
348
				if($class == 'ViewableData') $merge = false;
349
			} elseif($merge) {
350
				$cache = ($cache) ? array_merge(self::$casting_cache[$class], $cache) : self::$casting_cache[$class];
351
			}
352
			
353
			if($class == 'ViewableData') break;
354
		}
355
	}
356
	
357
	// TEMPLATE ACCESS LAYER -------------------------------------------------------------------------------------------
358
	
359
	/**
360
	 * Render this object into the template, and get the result as a string. You can pass one of the following as the
361
	 * $template parameter:
362
	 *  - a template name (e.g. Page)
363
	 *  - an array of possible template names - the first valid one will be used
364
	 *  - an SSViewer instance
365
	 *
366
	 * @param string|array|SSViewer $template the template to render into
367
	 * @param array $customFields fields to customise() the object with before rendering
368
	 * @return HTMLText
369
	 */
370
	public function renderWith($template, $customFields = null) {
371
		if(!is_object($template)) {
372
			$template = new SSViewer($template);
373
		}
374
		
375
		$data = ($this->customisedObject) ? $this->customisedObject : $this;
376
		
377
		if($customFields instanceof ViewableData) {
378
			$data = $data->customise($customFields);
379
		}
380
		if($template instanceof SSViewer) {
381
			return $template->process($data, is_array($customFields) ? $customFields : null);
382
		}
383
		
384
		throw new UnexpectedValueException (
385
			"ViewableData::renderWith(): unexpected $template->class object, expected an SSViewer instance"
386
		);
387
	}
388
389
	/**
390
	 * Generate the cache name for a field
391
	 *
392
	 * @param string $fieldName Name of field
393
	 * @param array $arguments List of optional arguments given
394
	 */
395
	protected function objCacheName($fieldName, $arguments) {
396
		return $arguments
397
			? $fieldName . ":" . implode(',', $arguments)
398
			: $fieldName;
399
	}
400
401
	/**
402
	 * Get a cached value from the field cache
403
	 *
404
	 * @param string $key Cache key
405
	 * @return mixed
406
	 */
407
	protected function objCacheGet($key) {
408
		if(isset($this->objCache[$key])) return $this->objCache[$key];
409
	}
410
411
	/**
412
	 * Store a value in the field cache
413
	 *
414
	 * @param string $key Cache key
415
	 * @param mixed $value
416
	 */
417
	protected function objCacheSet($key, $value) {
418
		$this->objCache[$key] = $value;
419
	}
420
	
421
	/**
422
	 * Get the value of a field on this object, automatically inserting the value into any available casting objects
423
	 * that have been specified.
424
	 *
425
	 * @param string $fieldName
426
	 * @param array $arguments
427
	 * @param bool $forceReturnedObject if TRUE, the value will ALWAYS be casted to an object before being returned,
428
	 *        even if there is no explicit casting information
429
	 * @param bool $cache Cache this object
430
	 * @param string $cacheName a custom cache name
431
	 */
432
	public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
433
		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 432 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...
434
435
		$value = $cache ? $this->objCacheGet($cacheName) : null;
436
		if(!isset($value)) {
437
			// HACK: Don't call the deprecated FormField::Name() method
438
			$methodIsAllowed = true;
439
			if($this instanceof FormField && $fieldName == 'Name') $methodIsAllowed = false;
440
			
441
			if($methodIsAllowed && $this->hasMethod($fieldName)) {
442
				$value = $arguments ? call_user_func_array(array($this, $fieldName), $arguments) : $this->$fieldName();
443
			} else {
444
				$value = $this->$fieldName;
445
			}
446
			
447
			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...
448
				if(!$castConstructor = $this->castingHelper($fieldName)) {
449
					$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...
450
				}
451
				
452
				$valueObject = Object::create_from_string($castConstructor, $fieldName);
453
				$valueObject->setValue($value, $this);
454
				
455
				$value = $valueObject;
456
			}
457
			
458
			if($cache) $this->objCacheSet($cacheName, $value);
459
		}
460
		
461
		if(!is_object($value) && $forceReturnedObject) {
462
			$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...
463
			$castedValue = new $default($fieldName);
464
			$castedValue->setValue($value);
465
			$value = $castedValue;
466
		}
467
		
468
		return $value;
469
	}
470
	
471
	/**
472
	 * A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again
473
	 * without re-running the method.
474
	 *
475
	 * @param string $field
476
	 * @param array $arguments
477
	 * @param string $identifier an optional custom cache identifier
478
	 */
479
	public function cachedCall($field, $arguments = null, $identifier = null) {
480
		return $this->obj($field, $arguments, false, true, $identifier);
481
	}
482
	
483
	/**
484
	 * Checks if a given method/field has a valid value. If the result is an object, this will return the result of the
485
	 * exists method, otherwise will check if the result is not just an empty paragraph tag.
486
	 *
487
	 * @param string $field
488
	 * @param array $arguments
489
	 * @param bool $cache
490
	 * @return bool
491
	 */
492
	public function hasValue($field, $arguments = null, $cache = true) {
493
		$result = $cache ? $this->cachedCall($field, $arguments) : $this->obj($field, $arguments, false, false);
494
		
495
		if(is_object($result) && $result instanceof Object) {
496
			return $result->exists();
497
		} else {
498
			// Empty paragraph checks are a workaround for TinyMCE
499
			return ($result && $result !== '<p></p>');
500
		}
501
	}
502
	
503
	/**#@+
504
	 * @param string $field
505
	 * @param array $arguments
506
	 * @param bool $cache
507
	 * @return string
508
	 */
509
	
510
	/**
511
	 * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
512
	 * template.
513
	 */
514
	public function XML_val($field, $arguments = null, $cache = false) {
515
		$result = $this->obj($field, $arguments, false, $cache);
516
		return (is_object($result) && $result instanceof Object) ? $result->forTemplate() : $result;
517
	}
518
	
519
	/**
520
	 * Return the value of the field without any escaping being applied.
521
	 */
522
	public function RAW_val($field, $arguments = null, $cache = true) {
523
		return Convert::xml2raw($this->XML_val($field, $arguments, $cache));
524
	}
525
	
526
	/**
527
	 * Return the value of a field in an SQL-safe format.
528
	 */
529
	public function SQL_val($field, $arguments = null, $cache = true) {
530
		return Convert::raw2sql($this->RAW_val($field, $arguments, $cache));
531
	}
532
	
533
	/**
534
	 * Return the value of a field in a JavaScript-save format.
535
	 */
536
	public function JS_val($field, $arguments = null, $cache = true) {
537
		return Convert::raw2js($this->RAW_val($field, $arguments, $cache));
538
	}
539
	
540
	/**
541
	 * Return the value of a field escaped suitable to be inserted into an XML node attribute.
542
	 */
543
	public function ATT_val($field, $arguments = null, $cache = true) {
544
		return Convert::raw2att($this->RAW_val($field, $arguments, $cache));
545
	}
546
	
547
	/**#@-*/
548
	
549
	/**
550
	 * Get an array of XML-escaped values by field name
551
	 *
552
	 * @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...
553
	 * @return array
554
	 */
555
	public function getXMLValues($fields) {
556
		$result = array();
557
		
558
		foreach($fields as $field) {
559
			$result[$field] = $this->XML_val($field);
560
		}
561
		
562
		return $result;
563
	}
564
	
565
	// ITERATOR SUPPORT ------------------------------------------------------------------------------------------------
566
	
567
	/**
568
	 * Return a single-item iterator so you can iterate over the fields of a single record.
569
	 *
570
	 * This is useful so you can use a single record inside a <% control %> block in a template - and then use
571
	 * to access individual fields on this object.
572
	 *
573
	 * @return ArrayIterator
574
	 */
575
	public function getIterator() {
576
		return new ArrayIterator(array($this));
577
	}
578
	
579
	// UTILITY METHODS -------------------------------------------------------------------------------------------------
580
	
581
	/**
582
	 * When rendering some objects it is necessary to iterate over the object being rendered, to do this, you need
583
	 * access to itself.
584
	 *
585
	 * @return ViewableData
586
	 */
587
	public function Me() {
588
		return $this;
589
	}
590
	
591
	/**
592
	 * Return the directory if the current active theme (relative to the site root).
593
	 *
594
	 * This method is useful for things such as accessing theme images from your template without hardcoding the theme
595
	 * page - e.g. <img src="$ThemeDir/images/something.gif">.
596
	 *
597
	 * This method should only be used when a theme is currently active. However, it will fall over to the current
598
	 * project directory.
599
	 *
600
	 * @param string $subtheme the subtheme path to get
601
	 * @return string
602
	 */
603
	public function ThemeDir($subtheme = false) {
604
		if(
605
			Config::inst()->get('SSViewer', 'theme_enabled') 
606
			&& $theme = Config::inst()->get('SSViewer', 'theme')
607
		) {
608
			return THEMES_DIR . "/$theme" . ($subtheme ? "_$subtheme" : null);
609
		}
610
		
611
		return project();
612
	}
613
	
614
	/**
615
	 * Get part of the current classes ancestry to be used as a CSS class.
616
	 *
617
	 * This method returns an escaped string of CSS classes representing the current classes ancestry until it hits a
618
	 * stop point - e.g. "Page DataObject ViewableData".
619
	 *
620
	 * @param string $stopAtClass the class to stop at (default: ViewableData)
621
	 * @return string
622
	 * @uses ClassInfo
623
	 */
624
	public function CSSClasses($stopAtClass = 'ViewableData') {
625
		$classes       = array();
626
		$classAncestry = array_reverse(ClassInfo::ancestry($this->class));
627
		$stopClasses   = ClassInfo::ancestry($stopAtClass);
628
		
629
		foreach($classAncestry as $class) {
630
			if(in_array($class, $stopClasses)) break;
631
			$classes[] = $class;
632
		}
633
		
634
		// optionally add template identifier
635
		if(isset($this->template) && !in_array($this->template, $classes)) {
636
			$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...
637
		}
638
		
639
		return Convert::raw2att(implode(' ', $classes));
640
	}
641
642
	/**
643
	 * Return debug information about this object that can be rendered into a template
644
	 *
645
	 * @return ViewableData_Debugger
646
	 */
647
	public function Debug() {
648
		return new ViewableData_Debugger($this);
649
	}
650
	
651
}
652
653
/**
654
 * @package framework
655
 * @subpackage view
656
 */
657
class ViewableData_Customised extends ViewableData {
658
	
659
	/**
660
	 * @var ViewableData
661
	 */
662
	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...
663
	
664
	/**
665
	 * Instantiate a new customised ViewableData object
666
	 *
667
	 * @param ViewableData $originalObject
668
	 * @param ViewableData $customisedObject
669
	 */
670
	public function __construct(ViewableData $originalObject, ViewableData $customisedObject) {
671
		$this->original   = $originalObject;
672
		$this->customised = $customisedObject;
673
		
674
		$this->original->setCustomisedObj($this);
675
		
676
		parent::__construct();
677
	}
678
	
679
	public function __call($method, $arguments) {
680
		if($this->customised->hasMethod($method)) {
681
			return call_user_func_array(array($this->customised, $method), $arguments);
682
		}
683
		
684
		return call_user_func_array(array($this->original, $method), $arguments);
685
	}
686
	
687
	public function __get($property) {
688
		if(isset($this->customised->$property)) {
689
			return $this->customised->$property;
690
		}
691
		
692
		return $this->original->$property;
693
	}
694
	
695
	public function __set($property, $value) {
696
		$this->customised->$property = $this->original->$property = $value;
697
	}
698
	
699
	public function hasMethod($method) {
700
		return $this->customised->hasMethod($method) || $this->original->hasMethod($method);
701
	}
702
	
703
	public function cachedCall($field, $arguments = null, $identifier = null) {
704
		if($this->customised->hasMethod($field) || $this->customised->hasField($field)) {
705
			$result = $this->customised->cachedCall($field, $arguments, $identifier);
706
		} else {
707
			$result = $this->original->cachedCall($field, $arguments, $identifier);
708
		}
709
		
710
		return $result;
711
	}
712
	
713
	public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
714
		if($this->customised->hasField($fieldName) || $this->customised->hasMethod($fieldName)) {
715
			return $this->customised->obj($fieldName, $arguments, $forceReturnedObject, $cache, $cacheName);
716
		}
717
		
718
		return $this->original->obj($fieldName, $arguments, $forceReturnedObject, $cache, $cacheName);
719
	}
720
	
721
}
722
723
/**
724
 * Allows you to render debug information about a {@link ViewableData} object into a template.
725
 *
726
 * @package framework
727
 * @subpackage view
728
 */
729
class ViewableData_Debugger extends ViewableData {
730
	
731
	/**
732
	 * @var ViewableData
733
	 */
734
	protected $object;
735
	
736
	/**
737
	 * @param ViewableData $object
738
	 */
739
	public function __construct(ViewableData $object) {
740
		$this->object = $object;
741
		parent::__construct();
742
	}
743
744
	/**
745
	 * @return string The rendered debugger
746
	 */
747
	public function __toString() {
748
		return $this->forTemplate();
749
	}
750
751
	/**
752
	 * Return debugging information, as XHTML. If a field name is passed, it will show debugging information on that
753
	 * field, otherwise it will show information on all methods and fields.
754
	 *
755
	 * @param string $field the field name
756
	 * @return string
757
	 */
758
	public function forTemplate($field = null) {
759
		// debugging info for a specific field
760
		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...
761
			($this->object->hasMethod($field)? "Has method '$field'<br/>" : null)             .
762
			($this->object->hasField($field) ? "Has field '$field'<br/>"  : null)             ;
763
		
764
		// debugging information for the entire class
765
		$reflector = new ReflectionObject($this->object);
766
		$debug     = "<b>Debugging Information: all methods available in '{$this->object->class}'</b><br/><ul>";
767
		
768
		foreach($this->object->allMethodNames() as $method) {
769
			// check that the method is public
770
			if($method[0] === strtoupper($method[0]) && $method[0] != '_') {
771
				if($reflector->hasMethod($method) && $method = $reflector->getMethod($method)) {
772
					if($method->isPublic()) {
773
						$debug .= "<li>\${$method->getName()}";
774
						
775
						if(count($method->getParameters())) {
776
							$debug .= ' <small>(' . implode(', ', $method->getParameters()) . ')</small>';
777
						}
778
						
779
						$debug .= '</li>';
780
					}
781
				} else {
782
					$debug .= "<li>\$$method</li>";
783
				}
784
			}
785
		}
786
		
787
		$debug .= '</ul>';
788
		
789
		if($this->object->hasMethod('toMap')) {
790
			$debug .= "<b>Debugging Information: all fields available in '{$this->object->class}'</b><br/><ul>";
791
			
792
			foreach($this->object->toMap() as $field => $value) {
793
				$debug .= "<li>\$$field</li>";
794
			}
795
			
796
			$debug .= "</ul>";
797
		}
798
		
799
		// check for an extra attached data
800
		if($this->object->hasMethod('data') && $this->object->data() != $this->object) {
801
			$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...
802
		}
803
		
804
		return $debug;
805
	}
806
807
}
808