Completed
Push — master ( 33496d...9e3f76 )
by Daniel
12:54
created

ViewableData::getCustomisedObj()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 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
		// Ensure cached methods from previous failover are removed
141
		if ($this->failover) {
142
			$this->removeMethodsFrom('failover');
143
		}
144
145
		$this->failover = $failover;
146
		$this->defineMethods();
147
	}
148
149
	/**
150
	 * Get the current failover object if set
151
	 *
152
	 * @return ViewableData|null
153
	 */
154
	public function getFailover() {
155
		return $this->failover;
156
	}
157
158
	/**
159
	 * Check if a field exists on this object. This should be overloaded in child classes.
160
	 *
161
	 * @param string $field
162
	 * @return bool
163
	 */
164
	public function hasField($field) {
165
		return property_exists($this, $field);
166
	}
167
168
	/**
169
	 * Get the value of a field on this object. This should be overloaded in child classes.
170
	 *
171
	 * @param string $field
172
	 * @return mixed
173
	 */
174
	public function getField($field) {
175
		return $this->$field;
176
	}
177
178
	/**
179
	 * Set a field on this object. This should be overloaded in child classes.
180
	 *
181
	 * @param string $field
182
	 * @param mixed $value
183
	 */
184
	public function setField($field, $value) {
185
		$this->$field = $value;
186
	}
187
188
	// -----------------------------------------------------------------------------------------------------------------
189
190
	/**
191
	 * Add methods from the {@link ViewableData::$failover} object, as well as wrapping any methods prefixed with an
192
	 * underscore into a {@link ViewableData::cachedCall()}.
193
	 */
194
	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...
195
		if($this->failover) {
196
			if(is_object($this->failover)) $this->addMethodsFrom('failover');
197
			else user_error("ViewableData::\$failover set to a non-object", E_USER_WARNING);
198
199
			if(isset($_REQUEST['debugfailover'])) {
200
				Debug::message("$this->class created with a failover class of {$this->failover->class}");
201
			}
202
		}
203
204
		parent::defineMethods();
205
	}
206
207
	/**
208
	 * Merge some arbitrary data in with this object. This method returns a {@link ViewableData_Customised} instance
209
	 * with references to both this and the new custom data.
210
	 *
211
	 * Note that any fields you specify will take precedence over the fields on this object.
212
	 *
213
	 * @param array|ViewableData $data
214
	 * @return ViewableData_Customised
215
	 */
216
	public function customise($data) {
217
		if(is_array($data) && (empty($data) || ArrayLib::is_associative($data))) {
218
			$data = new ArrayData($data);
219
		}
220
221
		if($data instanceof ViewableData) {
222
			return new ViewableData_Customised($this, $data);
223
		}
224
225
		throw new InvalidArgumentException (
226
			'ViewableData->customise(): $data must be an associative array or a ViewableData instance'
227
		);
228
	}
229
230
	/**
231
	 * @return ViewableData
232
	 */
233
	public function getCustomisedObj() {
234
		return $this->customisedObject;
235
	}
236
237
	/**
238
	 * @param ViewableData $object
239
	 */
240
	public function setCustomisedObj(ViewableData $object) {
241
		$this->customisedObject = $object;
242
	}
243
244
	// CASTING ---------------------------------------------------------------------------------------------------------
245
246
	/**
247
	 * Get the class a field on this object would be casted to, as well as the casting helper for casting a field to
248
	 * an object (see {@link ViewableData::castingHelper()} for information on casting helpers).
249
	 *
250
	 * The returned array contains two keys:
251
	 *  - className: the class the field would be casted to (e.g. "Varchar")
252
	 *  - castingHelper: the casting helper for casting the field (e.g. "return new Varchar($fieldName)")
253
	 *
254
	 * @param string $field
255
	 * @return array
256
	 */
257
	public function castingHelperPair($field) {
258
		Deprecation::notice('2.5', 'use castingHelper() instead');
259
		return $this->castingHelper($field);
260
	}
261
262
	/**
263
	 * Return the "casting helper" (a piece of PHP code that when evaluated creates a casted value object) for a field
264
	 * on this object.
265
	 *
266
	 * @param string $field
267
	 * @return string Casting helper
268
	 */
269
	public function castingHelper($field) {
270
		$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...
271
		if(isset($specs[$field])) {
272
			return $specs[$field];
273
		} elseif($this->failover) {
274
			return $this->failover->castingHelper($field);
275
		}
276
	}
277
278
	/**
279
	 * Get the class name a field on this object will be casted to
280
	 *
281
	 * @param string $field
282
	 * @return string
283
	 */
284
	public function castingClass($field) {
285
		$spec = $this->castingHelper($field);
286
		if(!$spec) return null;
287
288
		$bPos = strpos($spec,'(');
289
		if($bPos === false) return $spec;
290
		else return substr($spec, 0, $bPos);
291
	}
292
293
	/**
294
	 * Return the string-format type for the given field.
295
	 *
296
	 * @param string $field
297
	 * @return string 'xml'|'raw'
298
	 */
299
	public function escapeTypeForField($field) {
300
		$class = $this->castingClass($field) ?: $this->config()->default_cast;
301
302
		// TODO: It would be quicker not to instantiate the object, but to merely
303
		// get its class from the Injector
304
		return Injector::inst()->get($class, true)->config()->escape_type;
305
	}
306
307
	/**
308
	 * Save the casting cache for this object (including data from any failovers) into a variable
309
	 *
310
	 * @param reference $cache
311
	 */
312
	public function buildCastingCache(&$cache) {
313
		$ancestry = array_reverse(ClassInfo::ancestry($this->class));
314
		$merge    = true;
315
316
		foreach($ancestry as $class) {
317
			if(!isset(self::$casting_cache[$class]) && $merge) {
318
				$mergeFields = is_subclass_of($class, 'DataObject') ? array('db', 'casting') : array('casting');
319
320
				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...
321
					$casting = Config::inst()->get($class, $field, Config::UNINHERITED);
322
					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...
323
						if(!isset($cache[$field])) $cache[$field] = self::castingObjectCreatorPair($cast);
324
					}
325
				}
326
327
				if($class == 'ViewableData') $merge = false;
328
			} elseif($merge) {
329
				$cache = ($cache) ? array_merge(self::$casting_cache[$class], $cache) : self::$casting_cache[$class];
330
			}
331
332
			if($class == 'ViewableData') break;
333
		}
334
	}
335
336
	// TEMPLATE ACCESS LAYER -------------------------------------------------------------------------------------------
337
338
	/**
339
	 * Render this object into the template, and get the result as a string. You can pass one of the following as the
340
	 * $template parameter:
341
	 *  - a template name (e.g. Page)
342
	 *  - an array of possible template names - the first valid one will be used
343
	 *  - an SSViewer instance
344
	 *
345
	 * @param string|array|SSViewer $template the template to render into
346
	 * @param array $customFields fields to customise() the object with before rendering
347
	 * @return HTMLText
348
	 */
349
	public function renderWith($template, $customFields = null) {
350
		if(!is_object($template)) {
351
			$template = new SSViewer($template);
352
		}
353
354
		$data = ($this->customisedObject) ? $this->customisedObject : $this;
355
356
		if($customFields instanceof ViewableData) {
357
			$data = $data->customise($customFields);
358
		}
359
		if($template instanceof SSViewer) {
360
			return $template->process($data, is_array($customFields) ? $customFields : null);
361
		}
362
363
		throw new UnexpectedValueException (
364
			"ViewableData::renderWith(): unexpected $template->class object, expected an SSViewer instance"
365
		);
366
	}
367
368
	/**
369
	 * Generate the cache name for a field
370
	 *
371
	 * @param string $fieldName Name of field
372
	 * @param array $arguments List of optional arguments given
373
	 */
374
	protected function objCacheName($fieldName, $arguments) {
375
		return $arguments
376
			? $fieldName . ":" . implode(',', $arguments)
377
			: $fieldName;
378
	}
379
380
	/**
381
	 * Get a cached value from the field cache
382
	 *
383
	 * @param string $key Cache key
384
	 * @return mixed
385
	 */
386
	protected function objCacheGet($key) {
387
		if(isset($this->objCache[$key])) return $this->objCache[$key];
388
	}
389
390
	/**
391
	 * Store a value in the field cache
392
	 *
393
	 * @param string $key Cache key
394
	 * @param mixed $value
395
	 */
396
	protected function objCacheSet($key, $value) {
397
		$this->objCache[$key] = $value;
398
	}
399
400
	/**
401
	 * Get the value of a field on this object, automatically inserting the value into any available casting objects
402
	 * that have been specified.
403
	 *
404
	 * @param string $fieldName
405
	 * @param array $arguments
406
	 * @param bool $forceReturnedObject if TRUE, the value will ALWAYS be casted to an object before being returned,
407
	 *        even if there is no explicit casting information
408
	 * @param bool $cache Cache this object
409
	 * @param string $cacheName a custom cache name
410
	 */
411
	public function obj($fieldName, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
412
		if(!$cacheName && $cache) $cacheName = $this->objCacheName($fieldName, $arguments);
0 ignored issues
show
Bug Best Practice introduced by
The expression $cacheName of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug introduced by
It seems like $arguments defined by parameter $arguments on line 411 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...
413
414
		$value = $cache ? $this->objCacheGet($cacheName) : null;
415
		if(!isset($value)) {
416
			// HACK: Don't call the deprecated FormField::Name() method
417
			$methodIsAllowed = true;
418
			if($this instanceof FormField && $fieldName == 'Name') $methodIsAllowed = false;
419
420
			if($methodIsAllowed && $this->hasMethod($fieldName)) {
421
				$value = $arguments ? call_user_func_array(array($this, $fieldName), $arguments) : $this->$fieldName();
422
			} else {
423
				$value = $this->$fieldName;
424
			}
425
426
			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...
427
				if(!$castConstructor = $this->castingHelper($fieldName)) {
428
					$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...
429
				}
430
431
				$valueObject = Object::create_from_string($castConstructor, $fieldName);
432
				$valueObject->setValue($value, $this);
433
434
				$value = $valueObject;
435
			}
436
437
			if($cache) $this->objCacheSet($cacheName, $value);
438
		}
439
440
		if(!is_object($value) && $forceReturnedObject) {
441
			$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...
442
			$castedValue = new $default($fieldName);
443
			$castedValue->setValue($value);
444
			$value = $castedValue;
445
		}
446
447
		return $value;
448
	}
449
450
	/**
451
	 * A simple wrapper around {@link ViewableData::obj()} that automatically caches the result so it can be used again
452
	 * without re-running the method.
453
	 *
454
	 * @param string $field
455
	 * @param array $arguments
456
	 * @param string $identifier an optional custom cache identifier
457
	 */
458
	public function cachedCall($field, $arguments = null, $identifier = null) {
459
		return $this->obj($field, $arguments, false, true, $identifier);
460
	}
461
462
	/**
463
	 * Checks if a given method/field has a valid value. If the result is an object, this will return the result of the
464
	 * exists method, otherwise will check if the result is not just an empty paragraph tag.
465
	 *
466
	 * @param string $field
467
	 * @param array $arguments
468
	 * @param bool $cache
469
	 * @return bool
470
	 */
471
	public function hasValue($field, $arguments = null, $cache = true) {
472
		$result = $cache ? $this->cachedCall($field, $arguments) : $this->obj($field, $arguments, false, false);
473
474
		if(is_object($result) && $result instanceof Object) {
475
			return $result->exists();
476
		} else {
477
			// Empty paragraph checks are a workaround for TinyMCE
478
			return ($result && $result !== '<p></p>');
479
		}
480
	}
481
482
	/**#@+
483
	 * @param string $field
484
	 * @param array $arguments
485
	 * @param bool $cache
486
	 * @return string
487
	 */
488
489
	/**
490
	 * Get the string value of a field on this object that has been suitable escaped to be inserted directly into a
491
	 * template.
492
	 */
493
	public function XML_val($field, $arguments = null, $cache = false) {
494
		$result = $this->obj($field, $arguments, false, $cache);
495
		return (is_object($result) && $result instanceof Object) ? $result->forTemplate() : $result;
496
	}
497
498
	/**
499
	 * Return the value of the field without any escaping being applied.
500
	 */
501
	public function RAW_val($field, $arguments = null, $cache = true) {
502
		return Convert::xml2raw($this->XML_val($field, $arguments, $cache));
503
	}
504
505
	/**
506
	 * Return the value of a field in an SQL-safe format.
507
	 */
508
	public function SQL_val($field, $arguments = null, $cache = true) {
509
		return Convert::raw2sql($this->RAW_val($field, $arguments, $cache));
510
	}
511
512
	/**
513
	 * Return the value of a field in a JavaScript-save format.
514
	 */
515
	public function JS_val($field, $arguments = null, $cache = true) {
516
		return Convert::raw2js($this->RAW_val($field, $arguments, $cache));
517
	}
518
519
	/**
520
	 * Return the value of a field escaped suitable to be inserted into an XML node attribute.
521
	 */
522
	public function ATT_val($field, $arguments = null, $cache = true) {
523
		return Convert::raw2att($this->RAW_val($field, $arguments, $cache));
524
	}
525
526
	/**#@-*/
527
528
	/**
529
	 * Get an array of XML-escaped values by field name
530
	 *
531
	 * @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...
532
	 * @return array
533
	 */
534
	public function getXMLValues($fields) {
535
		$result = array();
536
537
		foreach($fields as $field) {
538
			$result[$field] = $this->XML_val($field);
539
		}
540
541
		return $result;
542
	}
543
544
	// ITERATOR SUPPORT ------------------------------------------------------------------------------------------------
545
546
	/**
547
	 * Return a single-item iterator so you can iterate over the fields of a single record.
548
	 *
549
	 * This is useful so you can use a single record inside a <% control %> block in a template - and then use
550
	 * to access individual fields on this object.
551
	 *
552
	 * @return ArrayIterator
553
	 */
554
	public function getIterator() {
555
		return new ArrayIterator(array($this));
556
	}
557
558
	// UTILITY METHODS -------------------------------------------------------------------------------------------------
559
560
	/**
561
	 * When rendering some objects it is necessary to iterate over the object being rendered, to do this, you need
562
	 * access to itself.
563
	 *
564
	 * @return ViewableData
565
	 */
566
	public function Me() {
567
		return $this;
568
	}
569
570
	/**
571
	 * Return the directory if the current active theme (relative to the site root).
572
	 *
573
	 * This method is useful for things such as accessing theme images from your template without hardcoding the theme
574
	 * page - e.g. <img src="$ThemeDir/images/something.gif">.
575
	 *
576
	 * This method should only be used when a theme is currently active. However, it will fall over to the current
577
	 * project directory.
578
	 *
579
	 * @param string $subtheme the subtheme path to get
580
	 * @return string
581
	 */
582
	public function ThemeDir($subtheme = false) {
583
		if(
584
			Config::inst()->get('SSViewer', 'theme_enabled')
585
			&& $theme = Config::inst()->get('SSViewer', 'theme')
586
		) {
587
			return THEMES_DIR . "/$theme" . ($subtheme ? "_$subtheme" : null);
588
		}
589
590
		return project();
591
	}
592
593
	/**
594
	 * Get part of the current classes ancestry to be used as a CSS class.
595
	 *
596
	 * This method returns an escaped string of CSS classes representing the current classes ancestry until it hits a
597
	 * stop point - e.g. "Page DataObject ViewableData".
598
	 *
599
	 * @param string $stopAtClass the class to stop at (default: ViewableData)
600
	 * @return string
601
	 * @uses ClassInfo
602
	 */
603
	public function CSSClasses($stopAtClass = 'ViewableData') {
604
		$classes       = array();
605
		$classAncestry = array_reverse(ClassInfo::ancestry($this->class));
606
		$stopClasses   = ClassInfo::ancestry($stopAtClass);
607
608
		foreach($classAncestry as $class) {
609
			if(in_array($class, $stopClasses)) break;
610
			$classes[] = $class;
611
		}
612
613
		// optionally add template identifier
614
		if(isset($this->template) && !in_array($this->template, $classes)) {
615
			$classes[] = $this->template;
0 ignored issues
show
Documentation introduced by
The property template does not exist on object<ViewableData>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

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