Completed
Push — 3.5 ( 1a9180...1bec8a )
by Daniel
24s
created

ViewableData::__isset()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 2
nc 3
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * A ViewableData object is any object that can be rendered into a template/view.
4
 *
5
 * A view interrogates the object being currently rendered in order to get data to render into the template. This data
6
 * is provided and automatically escaped by ViewableData. Any class that needs to be available to a view (controllers,
7
 * {@link DataObject}s, page controls) should inherit from this class.
8
 *
9
 * @package framework
10
 * @subpackage view
11
 */
12
class ViewableData extends Object implements IteratorAggregate {
13
14
	/**
15
	 * An array of objects to cast certain fields to. This is set up as an array in the format:
16
	 *
17
	 * <code>
18
	 * public static $casting = array (
19
	 *     'FieldName' => 'ClassToCastTo(Arguments)'
20
	 * );
21
	 * </code>
22
	 *
23
	 * @var array
24
	 * @config
25
	 */
26
	private static $casting = array(
27
		'CSSClasses' => 'Varchar'
28
	);
29
30
	/**
31
	 * The default object to cast scalar fields to if casting information is not specified, and casting to an object
32
	 * is required.
33
	 *
34
	 * @var string
35
	 * @config
36
	 */
37
	private static $default_cast = 'Text';
38
39
	/**
40
	 * @var array
41
	 */
42
	private static $casting_cache = array();
43
44
	// -----------------------------------------------------------------------------------------------------------------
45
46
	/**
47
	 * A failover object to attempt to get data from if it is not present on this object.
48
	 *
49
	 * @var ViewableData
50
	 */
51
	protected $failover;
52
53
	/**
54
	 * @var ViewableData
55
	 */
56
	protected $customisedObject;
57
58
	/**
59
	 * @var array
60
	 */
61
	private $objCache = array();
62
63
	// -----------------------------------------------------------------------------------------------------------------
64
65
	/**
66
	 * Converts a field spec into an object creator. For example: "Int" becomes "new Int($fieldName);" and "Varchar(50)"
67
	 * becomes "new Varchar($fieldName, 50);".
68
	 *
69
	 * @param string $fieldSchema The field spec
70
	 * @return string
71
	 */
72
	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...
73
		Deprecation::notice('2.5', 'Use Object::create_from_string() instead');
74
	}
75
76
	/**
77
	 * Convert a field schema (e.g. "Varchar(50)") into a casting object creator array that contains both a className
78
	 * and castingHelper constructor code. See {@link castingObjectCreator} for more information about the constructor.
79
	 *
80
	 * @param string $fieldSchema
81
	 * @return array
82
	 */
83
	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...
84
		Deprecation::notice('2.5', 'Use Object::create_from_string() instead');
85
	}
86
87
	// FIELD GETTERS & SETTERS -----------------------------------------------------------------------------------------
88
89
	/**
90
	 * Check if a field exists on this object or its failover.
91
	 *
92
	 * @param string $property
93
	 * @return bool
94
	 */
95
	public function __isset($property) {
96
		return $this->hasField($property) || ($this->failover && $this->failover->hasField($property));
97
	}
98
99
	/**
100
	 * Get the value of a property/field on this object. This will check if a method called get{$property} exists, then
101
	 * check if a field is available using {@link ViewableData::getField()}, then fall back on a failover object.
102
	 *
103
	 * @param string $property
104
	 * @return mixed
105
	 */
106
	public function __get($property) {
107
		if($this->hasMethod($method = "get$property")) {
108
			return $this->$method();
109
		} elseif($this->hasField($property)) {
110
			return $this->getField($property);
111
		} elseif($this->failover) {
112
			return $this->failover->$property;
113
		}
114
	}
115
116
	/**
117
	 * Set a property/field on this object. This will check for the existence of a method called set{$property}, then
118
	 * use the {@link ViewableData::setField()} method.
119
	 *
120
	 * @param string $property
121
	 * @param mixed $value
122
	 */
123
	public function __set($property, $value) {
124
		if($this->hasMethod($method = "set$property")) {
125
			$this->$method($value);
126
		} else {
127
			$this->setField($property, $value);
128
		}
129
	}
130
131
	/**
132
	 * Set a failover object to attempt to get data from if it is not present on this object.
133
	 *
134
	 * @param ViewableData $failover
135
	 */
136
	public function setFailover(ViewableData $failover) {
137
		// Ensure cached methods from previous failover are removed
138
		if ($this->failover) {
139
			$this->removeMethodsFrom('failover');
140
		}
141
142
		$this->failover = $failover;
143
		$this->defineMethods();
144
	}
145
146
	/**
147
	 * Get the current failover object if set
148
	 *
149
	 * @return ViewableData|null
150
	 */
151
	public function getFailover() {
152
		return $this->failover;
153
	}
154
155
	/**
156
	 * Check if a field exists on this object. This should be overloaded in child classes.
157
	 *
158
	 * @param string $field
159
	 * @return bool
160
	 */
161
	public function hasField($field) {
162
		return property_exists($this, $field);
163
	}
164
165
	/**
166
	 * Get the value of a field on this object. This should be overloaded in child classes.
167
	 *
168
	 * @param string $field
169
	 * @return mixed
170
	 */
171
	public function getField($field) {
172
		return $this->$field;
173
	}
174
175
	/**
176
	 * Set a field on this object. This should be overloaded in child classes.
177
	 *
178
	 * @param string $field
179
	 * @param mixed $value
180
	 */
181
	public function setField($field, $value) {
182
		$this->$field = $value;
183
	}
184
185
	// -----------------------------------------------------------------------------------------------------------------
186
187
	/**
188
	 * Add methods from the {@link ViewableData::$failover} object, as well as wrapping any methods prefixed with an
189
	 * underscore into a {@link ViewableData::cachedCall()}.
190
	 */
191
	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...
192
		if($this->failover) {
193
			if(is_object($this->failover)) $this->addMethodsFrom('failover');
194
			else user_error("ViewableData::\$failover set to a non-object", E_USER_WARNING);
195
196
			if(isset($_REQUEST['debugfailover'])) {
197
				Debug::message("$this->class created with a failover class of {$this->failover->class}");
198
			}
199
		}
200
201
		foreach($this->allMethodNames() as $method) {
202
			if($method[0] == '_' && $method[1] != '_') {
203
				$this->createMethod(
204
					substr($method, 1),
205
					"return \$obj->deprecatedCachedCall('$method', \$args, '" . substr($method, 1) . "');"
206
				);
207
			}
208
		}
209
210
		parent::defineMethods();
211
	}
212
213
	/**
214
	 * Method to facilitate deprecation of underscore-prefixed methods automatically being cached.
215
	 *
216
	 * @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...
217
	 * @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...
218
	 * @param string $identifier an optional custom cache identifier
219
	 * @return unknown
220
	 */
221
	public function deprecatedCachedCall($method, $args = null, $identifier = null) {
222
		Deprecation::notice(
223
			'4.0',
224
			'You are calling an underscore-prefixed method (e.g. _mymethod()) without the underscore. This behaviour,
225
				and the caching logic behind it, has been deprecated.',
226
			Deprecation::SCOPE_GLOBAL
227
		);
228
		return $this->cachedCall($method, $args, $identifier);
229
	}
230
231
	/**
232
	 * Merge some arbitrary data in with this object. This method returns a {@link ViewableData_Customised} instance
233
	 * with references to both this and the new custom data.
234
	 *
235
	 * Note that any fields you specify will take precedence over the fields on this object.
236
	 *
237
	 * @param array|ViewableData $data
238
	 * @return ViewableData_Customised
239
	 */
240
	public function customise($data) {
241
		if(is_array($data) && (empty($data) || ArrayLib::is_associative($data))) {
242
			$data = new ArrayData($data);
243
		}
244
245
		if($data instanceof ViewableData) {
246
			return new ViewableData_Customised($this, $data);
247
		}
248
249
		throw new InvalidArgumentException (
250
			'ViewableData->customise(): $data must be an associative array or a ViewableData instance'
251
		);
252
	}
253
254
	/**
255
	 * @return ViewableData
256
	 */
257
	public function getCustomisedObj() {
258
		return $this->customisedObject;
259
	}
260
261
	/**
262
	 * @param ViewableData $object
263
	 */
264
	public function setCustomisedObj(ViewableData $object) {
265
		$this->customisedObject = $object;
266
	}
267
268
	// CASTING ---------------------------------------------------------------------------------------------------------
269
270
	/**
271
	 * Get the class a field on this object would be casted to, as well as the casting helper for casting a field to
272
	 * an object (see {@link ViewableData::castingHelper()} for information on casting helpers).
273
	 *
274
	 * The returned array contains two keys:
275
	 *  - className: the class the field would be casted to (e.g. "Varchar")
276
	 *  - castingHelper: the casting helper for casting the field (e.g. "return new Varchar($fieldName)")
277
	 *
278
	 * @param string $field
279
	 * @return array
280
	 */
281
	public function castingHelperPair($field) {
282
		Deprecation::notice('2.5', 'use castingHelper() instead');
283
		return $this->castingHelper($field);
284
	}
285
286
	/**
287
	 * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field
288
	 * on this object.
289
	 *
290
	 * @param string $field
291
	 * @return string
292
	 */
293
	public function castingHelper($field) {
294
		if($this->hasMethod('db') && $fieldSpec = $this->db($field)) {
295
			Deprecation::notice(
296
				'4.0',
297
				'ViewableData::castingHelper() will no longer extract casting information "db". Please override
298
				castingHelper in your ViewableData subclass.',
299
				Deprecation::SCOPE_GLOBAL
300
			);
301
			return $fieldSpec;
302
		}
303
304
		$specs = Config::inst()->get(get_class($this), 'casting');
305
		if(isset($specs[$field])) return $specs[$field];
306
307
		if($this->failover) return $this->failover->castingHelper($field);
308
	}
309
310
	/**
311
	 * Get the class name a field on this object will be casted to
312
	 *
313
	 * @param string $field
314
	 * @return string
315
	 */
316
	public function castingClass($field) {
317
		$spec = $this->castingHelper($field);
318
		if(!$spec) return null;
319
320
		$bPos = strpos($spec,'(');
321
		if($bPos === false) return $spec;
322
		else return substr($spec, 0, $bPos);
323
	}
324
325
	/**
326
	 * Return the string-format type for the given field.
327
	 *
328
	 * @param string $field
329
	 * @return string 'xml'|'raw'
330
	 */
331
	public function escapeTypeForField($field) {
332
		$class = $this->castingClass($field) ?: $this->config()->default_cast;
333
334
		return Config::inst()->get($class, 'escape_type', Config::FIRST_SET);
335
	}
336
337
	/**
338
	 * Save the casting cache for this object (including data from any failovers) into a variable
339
	 *
340
	 * @param reference $cache
341
	 */
342
	public function buildCastingCache(&$cache) {
343
		$ancestry = array_reverse(ClassInfo::ancestry($this->class));
344
		$merge    = true;
345
346
		foreach($ancestry as $class) {
347
			if(!isset(self::$casting_cache[$class]) && $merge) {
348
				$mergeFields = is_subclass_of($class, 'DataObject') ? array('db', 'casting') : array('casting');
349
350
				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...
351
					$casting = Config::inst()->get($class, $field, Config::UNINHERITED);
352
					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...
353
						if(!isset($cache[$field])) $cache[$field] = self::castingObjectCreatorPair($cast);
354
					}
355
				}
356
357
				if($class == 'ViewableData') $merge = false;
358
			} elseif($merge) {
359
				$cache = ($cache) ? array_merge(self::$casting_cache[$class], $cache) : self::$casting_cache[$class];
360
			}
361
362
			if($class == 'ViewableData') break;
363
		}
364
	}
365
366
	// TEMPLATE ACCESS LAYER -------------------------------------------------------------------------------------------
367
368
	/**
369
	 * Render this object into the template, and get the result as a string. You can pass one of the following as the
370
	 * $template parameter:
371
	 *  - a template name (e.g. Page)
372
	 *  - an array of possible template names - the first valid one will be used
373
	 *  - an SSViewer instance
374
	 *
375
	 * @param string|array|SSViewer $template the template to render into
376
	 * @param array $customFields fields to customise() the object with before rendering
377
	 * @return HTMLText
378
	 */
379
	public function renderWith($template, $customFields = null) {
380
		if(!is_object($template)) {
381
			$template = new SSViewer($template);
382
		}
383
384
		$data = ($this->customisedObject) ? $this->customisedObject : $this;
385
386
		if($customFields instanceof ViewableData) {
387
			$data = $data->customise($customFields);
388
		}
389
		if($template instanceof SSViewer) {
390
			return $template->process($data, is_array($customFields) ? $customFields : null);
391
		}
392
393
		throw new UnexpectedValueException (
394
			"ViewableData::renderWith(): unexpected $template->class object, expected an SSViewer instance"
395
		);
396
	}
397
398
	/**
399
	 * Generate the cache name for a field
400
	 *
401
	 * @param string $fieldName Name of field
402
	 * @param array $arguments List of optional arguments given
403
	 */
404
	protected function objCacheName($fieldName, $arguments) {
405
		return $arguments
406
			? $fieldName . ":" . implode(',', $arguments)
407
			: $fieldName;
408
	}
409
410
	/**
411
	 * Get a cached value from the field cache
412
	 *
413
	 * @param string $key Cache key
414
	 * @return mixed
415
	 */
416
	protected function objCacheGet($key) {
417
		if(isset($this->objCache[$key])) return $this->objCache[$key];
418
	}
419
420
	/**
421
	 * Store a value in the field cache
422
	 *
423
	 * @param string $key Cache key
424
	 * @param mixed $value
425
	 */
426
	protected function objCacheSet($key, $value) {
427
		$this->objCache[$key] = $value;
428
	}
429
430
	/**
431
	 * Get the value of a field on this object, automatically inserting the value into any available casting objects
432
	 * that have been specified.
433
	 *
434
	 * @param string $fieldName
435
	 * @param array $arguments
436
	 * @param bool $forceReturnedObject if TRUE, the value will ALWAYS be casted to an object before being returned,
437
	 *        even if there is no explicit casting information
438
	 * @param bool $cache Cache this object
439
	 * @param string $cacheName a custom cache name
440
	 */
441
	public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
442
		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 441 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...
443
444
		$value = $cache ? $this->objCacheGet($cacheName) : null;
445
		if(!isset($value)) {
446
			// HACK: Don't call the deprecated FormField::Name() method
447
			$methodIsAllowed = true;
448
			if($this instanceof FormField && $fieldName == 'Name') $methodIsAllowed = false;
449
450
			if($methodIsAllowed && $this->hasMethod($fieldName)) {
451
				$value = $arguments ? call_user_func_array(array($this, $fieldName), $arguments) : $this->$fieldName();
452
			} else {
453
				$value = $this->$fieldName;
454
			}
455
456
			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...
457
				if(!$castConstructor = $this->castingHelper($fieldName)) {
458
					$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...
459
				}
460
461
				$valueObject = Object::create_from_string($castConstructor, $fieldName);
462
				$valueObject->setValue($value, $this);
463
464
				$value = $valueObject;
465
			}
466
467
			if($cache) $this->objCacheSet($cacheName, $value);
468
		}
469
470
		if(!is_object($value) && $forceReturnedObject) {
471
			$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...
472
			$castedValue = new $default($fieldName);
473
			$castedValue->setValue($value);
474
			$value = $castedValue;
475
		}
476
477
		return $value;
478
	}
479
480
	/**
481
	 * A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again
482
	 * without re-running the method.
483
	 *
484
	 * @param string $field
485
	 * @param array $arguments
486
	 * @param string $identifier an optional custom cache identifier
487
	 */
488
	public function cachedCall($field, $arguments = null, $identifier = null) {
489
		return $this->obj($field, $arguments, false, true, $identifier);
490
	}
491
492
	/**
493
	 * Checks if a given method/field has a valid value. If the result is an object, this will return the result of the
494
	 * exists method, otherwise will check if the result is not just an empty paragraph tag.
495
	 *
496
	 * @param string $field
497
	 * @param array $arguments
498
	 * @param bool $cache
499
	 * @return bool
500
	 */
501
	public function hasValue($field, $arguments = null, $cache = true) {
502
		$result = $cache ? $this->cachedCall($field, $arguments) : $this->obj($field, $arguments, false, false);
503
504
		if(is_object($result) && $result instanceof Object) {
505
			return $result->exists();
506
		} else {
507
			// Empty paragraph checks are a workaround for TinyMCE
508
			return ($result && $result !== '<p></p>');
509
		}
510
	}
511
512
	/**#@+
513
	 * @param string $field
514
	 * @param array $arguments
515
	 * @param bool $cache
516
	 * @return string
517
	 */
518
519
	/**
520
	 * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
521
	 * template.
522
	 */
523
	public function XML_val($field, $arguments = null, $cache = false) {
524
		$result = $this->obj($field, $arguments, false, $cache);
525
		return (is_object($result) && $result instanceof Object) ? $result->forTemplate() : $result;
526
	}
527
528
	/**
529
	 * Return the value of the field without any escaping being applied.
530
	 */
531
	public function RAW_val($field, $arguments = null, $cache = true) {
532
		return Convert::xml2raw($this->XML_val($field, $arguments, $cache));
533
	}
534
535
	/**
536
	 * Return the value of a field in an SQL-safe format.
537
	 */
538
	public function SQL_val($field, $arguments = null, $cache = true) {
539
		return Convert::raw2sql($this->RAW_val($field, $arguments, $cache));
540
	}
541
542
	/**
543
	 * Return the value of a field in a JavaScript-save format.
544
	 */
545
	public function JS_val($field, $arguments = null, $cache = true) {
546
		return Convert::raw2js($this->RAW_val($field, $arguments, $cache));
547
	}
548
549
	/**
550
	 * Return the value of a field escaped suitable to be inserted into an XML node attribute.
551
	 */
552
	public function ATT_val($field, $arguments = null, $cache = true) {
553
		return Convert::raw2att($this->RAW_val($field, $arguments, $cache));
554
	}
555
556
	/**#@-*/
557
558
	/**
559
	 * Get an array of XML-escaped values by field name
560
	 *
561
	 * @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...
562
	 * @return array
563
	 */
564
	public function getXMLValues($fields) {
565
		$result = array();
566
567
		foreach($fields as $field) {
568
			$result[$field] = $this->XML_val($field);
569
		}
570
571
		return $result;
572
	}
573
574
	// ITERATOR SUPPORT ------------------------------------------------------------------------------------------------
575
576
	/**
577
	 * Return a single-item iterator so you can iterate over the fields of a single record.
578
	 *
579
	 * This is useful so you can use a single record inside a <% control %> block in a template - and then use
580
	 * to access individual fields on this object.
581
	 *
582
	 * @return ArrayIterator
583
	 */
584
	public function getIterator() {
585
		return new ArrayIterator(array($this));
586
	}
587
588
	// UTILITY METHODS -------------------------------------------------------------------------------------------------
589
590
	/**
591
	 * When rendering some objects it is necessary to iterate over the object being rendered, to do this, you need
592
	 * access to itself.
593
	 *
594
	 * @return ViewableData
595
	 */
596
	public function Me() {
597
		return $this;
598
	}
599
600
	/**
601
	 * Return the directory if the current active theme (relative to the site root).
602
	 *
603
	 * This method is useful for things such as accessing theme images from your template without hardcoding the theme
604
	 * page - e.g. <img src="$ThemeDir/images/something.gif">.
605
	 *
606
	 * This method should only be used when a theme is currently active. However, it will fall over to the current
607
	 * project directory.
608
	 *
609
	 * @param string $subtheme the subtheme path to get
610
	 * @return string
611
	 */
612
	public function ThemeDir($subtheme = false) {
613
		if(
614
			Config::inst()->get('SSViewer', 'theme_enabled')
615
			&& $theme = Config::inst()->get('SSViewer', 'theme')
616
		) {
617
			return THEMES_DIR . "/$theme" . ($subtheme ? "_$subtheme" : null);
618
		}
619
620
		return project();
621
	}
622
623
	/**
624
	 * Get part of the current classes ancestry to be used as a CSS class.
625
	 *
626
	 * This method returns an escaped string of CSS classes representing the current classes ancestry until it hits a
627
	 * stop point - e.g. "Page DataObject ViewableData".
628
	 *
629
	 * @param string $stopAtClass the class to stop at (default: ViewableData)
630
	 * @return string
631
	 * @uses ClassInfo
632
	 */
633
	public function CSSClasses($stopAtClass = 'ViewableData') {
634
		$classes       = array();
635
		$classAncestry = array_reverse(ClassInfo::ancestry($this->class));
636
		$stopClasses   = ClassInfo::ancestry($stopAtClass);
637
638
		foreach($classAncestry as $class) {
639
			if(in_array($class, $stopClasses)) break;
640
			$classes[] = $class;
641
		}
642
643
		// optionally add template identifier
644
		if(isset($this->template) && !in_array($this->template, $classes)) {
645
			$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...
646
		}
647
648
		return Convert::raw2att(implode(' ', $classes));
649
	}
650
651
	/**
652
	 * Return debug information about this object that can be rendered into a template
653
	 *
654
	 * @return ViewableData_Debugger
655
	 */
656
	public function Debug() {
657
		return new ViewableData_Debugger($this);
658
	}
659
660
}
661
662
/**
663
 * @package framework
664
 * @subpackage view
665
 */
666
class ViewableData_Customised extends ViewableData {
667
668
	/**
669
	 * @var ViewableData
670
	 */
671
	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...
672
673
	/**
674
	 * Instantiate a new customised ViewableData object
675
	 *
676
	 * @param ViewableData $originalObject
677
	 * @param ViewableData $customisedObject
678
	 */
679
	public function __construct(ViewableData $originalObject, ViewableData $customisedObject) {
680
		$this->original   = $originalObject;
681
		$this->customised = $customisedObject;
682
683
		$this->original->setCustomisedObj($this);
684
685
		parent::__construct();
686
	}
687
688
	public function __call($method, $arguments) {
689
		if($this->customised->hasMethod($method)) {
690
			return call_user_func_array(array($this->customised, $method), $arguments);
691
		}
692
693
		return call_user_func_array(array($this->original, $method), $arguments);
694
	}
695
696
	public function __get($property) {
697
		if(isset($this->customised->$property)) {
698
			return $this->customised->$property;
699
		}
700
701
		return $this->original->$property;
702
	}
703
704
	public function __set($property, $value) {
705
		$this->customised->$property = $this->original->$property = $value;
706
	}
707
708
	public function hasMethod($method) {
709
		return $this->customised->hasMethod($method) || $this->original->hasMethod($method);
710
	}
711
712
	public function cachedCall($field, $arguments = null, $identifier = null) {
713
		if($this->customised->hasMethod($field) || $this->customised->hasField($field)) {
714
			$result = $this->customised->cachedCall($field, $arguments, $identifier);
715
		} else {
716
			$result = $this->original->cachedCall($field, $arguments, $identifier);
717
		}
718
719
		return $result;
720
	}
721
722
	public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
723
		if($this->customised->hasField($fieldName) || $this->customised->hasMethod($fieldName)) {
724
			return $this->customised->obj($fieldName, $arguments, $forceReturnedObject, $cache, $cacheName);
725
		}
726
727
		return $this->original->obj($fieldName, $arguments, $forceReturnedObject, $cache, $cacheName);
728
	}
729
730
}
731
732
/**
733
 * Allows you to render debug information about a {@link ViewableData} object into a template.
734
 *
735
 * @package framework
736
 * @subpackage view
737
 */
738
class ViewableData_Debugger extends ViewableData {
739
740
	/**
741
	 * @var ViewableData
742
	 */
743
	protected $object;
744
745
	/**
746
	 * @param ViewableData $object
747
	 */
748
	public function __construct(ViewableData $object) {
749
		$this->object = $object;
750
		parent::__construct();
751
	}
752
753
	/**
754
	 * @return string The rendered debugger
755
	 */
756
	public function __toString() {
757
		return $this->forTemplate();
758
	}
759
760
	/**
761
	 * Return debugging information, as XHTML. If a field name is passed, it will show debugging information on that
762
	 * field, otherwise it will show information on all methods and fields.
763
	 *
764
	 * @param string $field the field name
765
	 * @return string
766
	 */
767
	public function forTemplate($field = null) {
768
		// debugging info for a specific field
769
		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...
770
			($this->object->hasMethod($field)? "Has method '$field'<br/>" : null)             .
771
			($this->object->hasField($field) ? "Has field '$field'<br/>"  : null)             ;
772
773
		// debugging information for the entire class
774
		$reflector = new ReflectionObject($this->object);
775
		$debug     = "<b>Debugging Information: all methods available in '{$this->object->class}'</b><br/><ul>";
776
777
		foreach($this->object->allMethodNames() as $method) {
778
			// check that the method is public
779
			if($method[0] === strtoupper($method[0]) && $method[0] != '_') {
780
				if($reflector->hasMethod($method) && $method = $reflector->getMethod($method)) {
781
					if($method->isPublic()) {
782
						$debug .= "<li>\${$method->getName()}";
783
784
						if(count($method->getParameters())) {
785
							$debug .= ' <small>(' . implode(', ', $method->getParameters()) . ')</small>';
786
						}
787
788
						$debug .= '</li>';
789
					}
790
				} else {
791
					$debug .= "<li>\$$method</li>";
792
				}
793
			}
794
		}
795
796
		$debug .= '</ul>';
797
798
		if($this->object->hasMethod('toMap')) {
799
			$debug .= "<b>Debugging Information: all fields available in '{$this->object->class}'</b><br/><ul>";
800
801
			foreach($this->object->toMap() as $field => $value) {
802
				$debug .= "<li>\$$field</li>";
803
			}
804
805
			$debug .= "</ul>";
806
		}
807
808
		// check for an extra attached data
809
		if($this->object->hasMethod('data') && $this->object->data() != $this->object) {
810
			$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...
811
		}
812
813
		return $debug;
814
	}
815
816
}
817