Completed
Push — namespace-template ( 7967f2...367a36 )
by Sam
10:48
created

SSViewer_DataPresenter   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 246
Duplicated Lines 0.81 %

Coupling/Cohesion

Components 1
Dependencies 5
Metric Value
dl 2
loc 246
rs 8.3999
wmc 46
lcom 1
cbo 5

8 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 23 5
C createCallableArray() 0 30 8
C getInjectedValue() 0 68 16
A pushScope() 0 11 1
A popScope() 0 6 1
C obj() 0 26 7
A getObj() 0 5 2
B __call() 2 25 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SSViewer_DataPresenter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SSViewer_DataPresenter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
use SilverStripe\Model\FieldType\DBField;
4
5
/**
6
 * This tracks the current scope for an SSViewer instance. It has three goals:
7
 *   - Handle entering & leaving sub-scopes in loops and withs
8
 *   - Track Up and Top
9
 *   - (As a side effect) Inject data that needs to be available globally (used to live in ViewableData)
10
 *
11
 * In order to handle up, rather than tracking it using a tree, which would involve constructing new objects
12
 * for each step, we use indexes into the itemStack (which already has to exist).
13
 *
14
 * Each item has three indexes associated with it
15
 *
16
 *   - Pop. Which item should become the scope once the current scope is popped out of
17
 *   - Up. Which item is up from this item
18
 *   - Current. Which item is the first time this object has appeared in the stack
19
 *
20
 * We also keep the index of the current starting point for lookups. A lookup is a sequence of obj calls -
21
 * when in a loop or with tag the end result becomes the new scope, but for injections, we throw away the lookup
22
 * and revert back to the original scope once we've got the value we're after
23
 *
24
 * @package framework
25
 * @subpackage view
26
 */
27
class SSViewer_Scope {
28
29
	const ITEM = 0;
30
	const ITEM_ITERATOR = 1;
31
	const ITEM_ITERATOR_TOTAL = 2;
32
	const POP_INDEX = 3;
33
	const UP_INDEX = 4;
34
	const CURRENT_INDEX = 5;
35
	const ITEM_OVERLAY = 6;
36
	
37
	// The stack of previous "global" items
38
	// An indexed array of item, item iterator, item iterator total, pop index, up index, current index & parent overlay
39
	private $itemStack = array(); 
40
41
	// The current "global" item (the one any lookup starts from)
42
	protected $item;
43
44
	// If we're looping over the current "global" item, here's the iterator that tracks with item we're up to
45
	protected $itemIterator;
46
47
	//Total number of items in the iterator
48
	protected $itemIteratorTotal;
49
50
	// A pointer into the item stack for which item should be scope on the next pop call
51
	private $popIndex;
52
53
	// A pointer into the item stack for which item is "up" from this one
54
	private $upIndex = null;
55
56
	// A pointer into the item stack for which item is this one (or null if not in stack yet)
57
	private $currentIndex = null;
58
59
	private $localIndex;
60
61
	public function __construct($item, $inheritedScope = null) {
62
		$this->item = $item;
63
		$this->localIndex = 0;
64
		$this->localStack = array();
0 ignored issues
show
Bug introduced by
The property localStack does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
65
		if ($inheritedScope instanceof SSViewer_Scope) {
66
			$this->itemIterator = $inheritedScope->itemIterator;
67
			$this->itemIteratorTotal = $inheritedScope->itemIteratorTotal;
68
			$this->itemStack[] = array($this->item, $this->itemIterator, $this->itemIteratorTotal, null, null, 0);
69
		} else {
70
			$this->itemStack[] = array($this->item, null, 0, null, null, 0);
71
		}
72
	}
73
74
	public function getItem(){
75
		return $this->itemIterator ? $this->itemIterator->current() : $this->item;
76
	}
77
78
	/** Called at the start of every lookup chain by SSTemplateParser to indicate a new lookup from local scope */
79
	public function locally() {
80
		list($this->item, $this->itemIterator, $this->itemIteratorTotal, $this->popIndex, $this->upIndex,
81
			$this->currentIndex) = $this->itemStack[$this->localIndex];
82
83
		// Remember any  un-completed (resetLocalScope hasn't been called) lookup chain. Even if there isn't an
84
		// un-completed chain we need to store an empty item, as resetLocalScope doesn't know the difference later
85
		$this->localStack[] = array_splice($this->itemStack, $this->localIndex+1);
86
87
		return $this;
88
	}
89
90
	public function resetLocalScope(){
91
		$previousLocalState = $this->localStack ? array_pop($this->localStack) : null;
92
93
		array_splice($this->itemStack, $this->localIndex+1, count($this->itemStack), $previousLocalState);
94
95
		list($this->item, $this->itemIterator, $this->itemIteratorTotal, $this->popIndex, $this->upIndex,
96
			$this->currentIndex) = end($this->itemStack);
97
	}
98
99
	public function getObj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
100
		$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
101
		return $on->obj($name, $arguments, $forceReturnedObject, $cache, $cacheName);
102
	}
103
104
	public function obj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
105
		switch ($name) {
106
			case 'Up':
107
				if ($this->upIndex === null) {
108
					user_error('Up called when we\'re already at the top of the scope', E_USER_ERROR);
109
				}
110
111
				list($this->item, $this->itemIterator, $this->itemIteratorTotal, $unused2, $this->upIndex,
0 ignored issues
show
Unused Code introduced by
The assignment to $unused2 is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
112
					$this->currentIndex) = $this->itemStack[$this->upIndex];
113
				break;
114
115
			case 'Top':
116
				list($this->item, $this->itemIterator, $this->itemIteratorTotal, $unused2, $this->upIndex,
0 ignored issues
show
Unused Code introduced by
The assignment to $unused2 is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
117
					$this->currentIndex) = $this->itemStack[0];
118
				break;
119
120
			default:
121
				$this->item = $this->getObj($name, $arguments, $forceReturnedObject, $cache, $cacheName);
122
				$this->itemIterator = null;
123
				$this->upIndex = $this->currentIndex ? $this->currentIndex : count($this->itemStack)-1;
124
				$this->currentIndex = count($this->itemStack);
125
				break;
126
		}
127
128
		$this->itemStack[] = array($this->item, $this->itemIterator, $this->itemIteratorTotal, null,
129
			$this->upIndex, $this->currentIndex);
130
		return $this;
131
	}
132
133
	/**
134
	 * Gets the current object and resets the scope.
135
	 *
136
	 * @return object
137
	 */
138
	public function self() {
139
		$result = $this->itemIterator ? $this->itemIterator->current() : $this->item;
140
		$this->resetLocalScope();
141
142
		return $result;
143
	}
144
145
	public function pushScope(){
146
		$newLocalIndex = count($this->itemStack)-1;
147
148
		$this->popIndex = $this->itemStack[$newLocalIndex][SSViewer_Scope::POP_INDEX] = $this->localIndex;
149
		$this->localIndex = $newLocalIndex;
150
151
		// We normally keep any previous itemIterator around, so local $Up calls reference the right element. But
152
		// once we enter a new global scope, we need to make sure we use a new one
153
		$this->itemIterator = $this->itemStack[$newLocalIndex][SSViewer_Scope::ITEM_ITERATOR] = null;
154
155
		return $this;
156
	}
157
158
	public function popScope(){
159
		$this->localIndex = $this->popIndex;
160
		$this->resetLocalScope();
161
162
		return $this;
163
	}
164
165
	public function next(){
166
		if (!$this->item) return false;
167
168
		if (!$this->itemIterator) {
169
			if (is_array($this->item)) $this->itemIterator = new ArrayIterator($this->item);
170
			else $this->itemIterator = $this->item->getIterator();
171
172
			$this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR] = $this->itemIterator;
173
			$this->itemIteratorTotal = iterator_count($this->itemIterator); //count the total number of items
174
			$this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR_TOTAL] = $this->itemIteratorTotal;
175
			$this->itemIterator->rewind();
176
		}
177
		else {
178
			$this->itemIterator->next();
179
		}
180
181
		$this->resetLocalScope();
182
183
		if (!$this->itemIterator->valid()) return false;
184
		return $this->itemIterator->key();
185
	}
186
187
	public function __call($name, $arguments) {
188
		$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
189
		$retval = $on ? call_user_func_array(array($on, $name), $arguments) : null;
190
191
		$this->resetLocalScope();
192
		return $retval;
193
	}
194
195
	/**
196
	 * @return array
197
	 */
198
	protected function getItemStack() {
199
		return $this->itemStack;
200
	}
201
202
	/**
203
	 * @param array
204
	 */
205
	protected function setItemStack(array $stack) {
206
		$this->itemStack = $stack;
207
	}
208
209
	/**
210
	 * @return int|null
211
	 */
212
	protected function getUpIndex() {
213
		return $this->upIndex;
214
	}
215
}
216
217
/**
218
 * Defines an extra set of basic methods that can be used in templates
219
 * that are not defined on sub-classes of {@link ViewableData}.
220
 *
221
 * @package framework
222
 * @subpackage view
223
 */
224
class SSViewer_BasicIteratorSupport implements TemplateIteratorProvider {
225
226
	protected $iteratorPos;
227
	protected $iteratorTotalItems;
228
229
	public static function get_template_iterator_variables() {
230
		return array(
231
			'First',
232
			'Last',
233
			'FirstLast',
234
			'Middle',
235
			'MiddleString',
236
			'Even',
237
			'Odd',
238
			'EvenOdd',
239
			'Pos',
240
			'FromEnd',
241
			'TotalItems',
242
			'Modulus',
243
			'MultipleOf',
244
		);
245
	}
246
247
	/**
248
	 * Set the current iterator properties - where we are on the iterator.
249
	 *
250
	 * @param int $pos position in iterator
251
	 * @param int $totalItems total number of items
252
	 */
253
	public function iteratorProperties($pos, $totalItems) {
254
		$this->iteratorPos        = $pos;
255
		$this->iteratorTotalItems = $totalItems;
256
	}
257
258
	/**
259
	 * Returns true if this object is the first in a set.
260
	 *
261
	 * @return bool
262
	 */
263
	public function First() {
264
		return $this->iteratorPos == 0;
265
	}
266
267
	/**
268
	 * Returns true if this object is the last in a set.
269
	 *
270
	 * @return bool
271
	 */
272
	public function Last() {
273
		return $this->iteratorPos == $this->iteratorTotalItems - 1;
274
	}
275
276
	/**
277
	 * Returns 'first' or 'last' if this is the first or last object in the set.
278
	 *
279
	 * @return string|null
280
	 */
281
	public function FirstLast() {
282
		if($this->First() && $this->Last()) return 'first last';
283
		if($this->First()) return 'first';
284
		if($this->Last())  return 'last';
285
	}
286
287
	/**
288
	 * Return true if this object is between the first & last objects.
289
	 *
290
	 * @return bool
291
	 */
292
	public function Middle() {
293
		return !$this->First() && !$this->Last();
294
	}
295
296
	/**
297
	 * Return 'middle' if this object is between the first & last objects.
298
	 *
299
	 * @return string|null
300
	 */
301
	public function MiddleString() {
302
		if($this->Middle()) return 'middle';
303
	}
304
305
	/**
306
	 * Return true if this object is an even item in the set.
307
	 * The count starts from $startIndex, which defaults to 1.
308
	 *
309
	 * @param int $startIndex Number to start count from.
310
	 * @return bool
311
	 */
312
	public function Even($startIndex = 1) {
313
		return !$this->Odd($startIndex);
314
	}
315
316
	/**
317
	 * Return true if this is an odd item in the set.
318
	 *
319
	 * @param int $startIndex Number to start count from.
320
	 * @return bool
321
	 */
322
	public function Odd($startIndex = 1) {
323
		return (bool) (($this->iteratorPos+$startIndex) % 2);
324
	}
325
326
	/**
327
	 * Return 'even' or 'odd' if this object is in an even or odd position in the set respectively.
328
	 *
329
	 * @param int $startIndex Number to start count from.
330
	 * @return string
331
	 */
332
	public function EvenOdd($startIndex = 1) {
333
		return ($this->Even($startIndex)) ? 'even' : 'odd';
334
	}
335
336
	/**
337
	 * Return the numerical position of this object in the container set. The count starts at $startIndex.
338
	 * The default is the give the position using a 1-based index.
339
	 *
340
	 * @param int $startIndex Number to start count from.
341
	 * @return int
342
	 */
343
	public function Pos($startIndex = 1) {
344
		return $this->iteratorPos + $startIndex;
345
	}
346
347
	/**
348
	 * Return the position of this item from the last item in the list. The position of the final
349
	 * item is $endIndex, which defaults to 1.
350
	 *
351
	 * @param integer $endIndex Value of the last item
352
	 * @return int
353
	 */
354
	public function FromEnd($endIndex = 1) {
355
		return $this->iteratorTotalItems - $this->iteratorPos + $endIndex - 1;
356
	}
357
358
	/**
359
	 * Return the total number of "sibling" items in the dataset.
360
	 *
361
	 * @return int
362
	 */
363
	public function TotalItems() {
364
		return $this->iteratorTotalItems;
365
	}
366
367
	/**
368
	 * Returns the modulus of the numerical position of the item in the data set.
369
	 * The count starts from $startIndex, which defaults to 1.
370
	 * @param int $Mod The number to perform Mod operation to.
0 ignored issues
show
Bug introduced by
There is no parameter named $Mod. 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...
371
	 * @param int $startIndex Number to start count from.
372
	 * @return int
373
	 */
374
	public function Modulus($mod, $startIndex = 1) {
375
		return ($this->iteratorPos + $startIndex) % $mod;
376
	}
377
378
	/**
379
	 * Returns true or false depending on if the pos of the iterator is a multiple of a specific number.
380
	 * So, <% if MultipleOf(3) %> would return true on indexes: 3,6,9,12,15, etc.
381
	 * The count starts from $offset, which defaults to 1.
382
	 * @param int $factor The multiple of which to return
383
	 * @param int $offset Number to start count from.
384
	 * @return bool
385
	 */
386
	public function MultipleOf($factor, $offset = 1) {
387
		return (bool) ($this->Modulus($factor, $offset) == 0);
388
	}
389
390
391
392
}
393
/**
394
 * This extends SSViewer_Scope to mix in data on top of what the item provides. This can be "global"
395
 * data that is scope-independant (like BaseURL), or type-specific data that is layered on top cross-cut like
396
 * (like $FirstLast etc).
397
 *
398
 * It's separate from SSViewer_Scope to keep that fairly complex code as clean as possible.
399
 *
400
 * @package framework
401
 * @subpackage view
402
 */
403
class SSViewer_DataPresenter extends SSViewer_Scope {
404
405
	private static $globalProperties = null;
406
	private static $iteratorProperties = null;
407
408
	/**
409
	 * Overlay variables. Take precedence over anything from the current scope
410
	 * @var array|null
411
	 */
412
	protected $overlay;
413
414
	/**
415
	 * Underlay variables. Concede precedence to overlay variables or anything from the current scope
416
	 * @var array|null
417
	 */
418
	protected $underlay;
419
420
	public function __construct($item, $overlay = null, $underlay = null, $inheritedScope = null) {
421
		parent::__construct($item, $inheritedScope);
422
423
		// Build up global property providers array only once per request
424
		if (self::$globalProperties === null) {
425
			self::$globalProperties = array();
426
			// Get all the exposed variables from all classes that implement the TemplateGlobalProvider interface
427
			$this->createCallableArray(self::$globalProperties, "TemplateGlobalProvider",
428
				"get_template_global_variables");
429
		}
430
431
		// Build up iterator property providers array only once per request
432
		if (self::$iteratorProperties === null) {
433
			self::$iteratorProperties = array();
434
			// Get all the exposed variables from all classes that implement the TemplateIteratorProvider interface
435
			// //call non-statically
436
			$this->createCallableArray(self::$iteratorProperties, "TemplateIteratorProvider",
437
				"get_template_iterator_variables", true);
438
		}
439
440
		$this->overlay = $overlay ? $overlay : array();
441
		$this->underlay = $underlay ? $underlay : array();
442
	}
443
444
	protected function createCallableArray(&$extraArray, $interfaceToQuery, $variableMethod, $createObject = false) {
445
		$implementers = ClassInfo::implementorsOf($interfaceToQuery);
446
		if($implementers) foreach($implementers as $implementer) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $implementers of type array 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...
447
448
			// Create a new instance of the object for method calls
449
			if ($createObject) $implementer = new $implementer();
450
451
			// Get the exposed variables
452
			$exposedVariables = call_user_func(array($implementer, $variableMethod));
453
454
			foreach($exposedVariables as $varName => $details) {
455
				if (!is_array($details)) $details = array('method' => $details,
456
					'casting' => Config::inst()->get('ViewableData', 'default_cast', Config::FIRST_SET));
457
458
				// If just a value (and not a key => value pair), use it for both key and value
459
				if (is_numeric($varName)) $varName = $details['method'];
460
461
				// Add in a reference to the implementing class (might be a string class name or an instance)
462
				$details['implementer'] = $implementer;
463
464
				// And a callable array
465
				if (isset($details['method'])) $details['callable'] = array($implementer, $details['method']);
466
467
				// Save with both uppercase & lowercase first letter, so either works
468
				$lcFirst = strtolower($varName[0]) . substr($varName,1);
469
				$extraArray[$lcFirst] = $details;
470
				$extraArray[ucfirst($varName)] = $details;
471
			}
472
		}
473
	}
474
475
	/**
476
	 * Get the injected value
477
	 *
478
	 * @param string $property Name of property
479
	 * @param array $params
480
	 * @param bool $cast If true, an object is always returned even if not an object.
481
	 * @return array Result array with the keys 'value' for raw value, or 'obj' if contained in an object
482
	 * @throws InvalidArgumentException
483
	 */
484
	public function getInjectedValue($property, $params, $cast = true) {
485
		$on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
486
487
		// Find the source of the value
488
		$source = null;
489
490
		// Check for a presenter-specific override
491
		if (array_key_exists($property, $this->overlay)) {
492
			$source = array('value' => $this->overlay[$property]);
493
		}
494
		// Check if the method to-be-called exists on the target object - if so, don't check any further
495
		// injection locations
496
		else if (isset($on->$property) || method_exists($on, $property)) {
497
			$source = null;
498
		}
499
		// Check for a presenter-specific override
500
		else if (array_key_exists($property, $this->underlay)) {
501
			$source = array('value' => $this->underlay[$property]);
502
		}
503
		// Then for iterator-specific overrides
504
		else if (array_key_exists($property, self::$iteratorProperties)) {
505
			$source = self::$iteratorProperties[$property];
506
			if ($this->itemIterator) {
507
				// Set the current iterator position and total (the object instance is the first item in
508
				// the callable array)
509
				$source['implementer']->iteratorProperties($this->itemIterator->key(), $this->itemIteratorTotal);
510
			} else {
511
				// If we don't actually have an iterator at the moment, act like a list of length 1
512
				$source['implementer']->iteratorProperties(0, 1);
513
			}
514
		}
515
		// And finally for global overrides
516
		else if (array_key_exists($property, self::$globalProperties)) {
517
			$source = self::$globalProperties[$property];  //get the method call
518
		}
519
520
		if ($source) {
521
			$res = array();
522
523
			// Look up the value - either from a callable, or from a directly provided value
524
			if (isset($source['callable'])) $res['value'] = call_user_func_array($source['callable'], $params);
525
			elseif (isset($source['value'])) $res['value'] = $source['value'];
526
			else throw new InvalidArgumentException("Injected property $property does't have a value or callable " .
527
				"value source provided");
528
529
			// If we want to provide a casted object, look up what type object to use
530
			if ($cast) {
531
				// If the handler returns an object, then we don't need to cast.
532
				if(is_object($res['value'])) {
533
					$res['obj'] = $res['value'];
534
				} else {
535
					// Get the object to cast as
536
					$casting = isset($source['casting']) ? $source['casting'] : null;
537
538
					// If not provided, use default
539
					if (!$casting) $casting = Config::inst()->get('ViewableData', 'default_cast', Config::FIRST_SET);
540
541
					$obj = Injector::inst()->get($casting, false, array($property));
542
					$obj->setValue($res['value']);
543
544
					$res['obj'] = $obj;
545
				}
546
			}
547
548
			return $res;
549
		}
550
551
	}
552
553
	/**
554
	 * Store the current overlay (as it doesn't directly apply to the new scope
555
	 * that's being pushed). We want to store the overlay against the next item
556
	 * "up" in the stack (hence upIndex), rather than the current item, because
557
	 * SSViewer_Scope::obj() has already been called and pushed the new item to
558
	 * the stack by this point
559
	 * @return SSViewer_Scope
560
	 */
561
	public function pushScope() {
562
		$scope = parent::pushScope();
563
564
		$itemStack = $this->getItemStack();
565
		$itemStack[$this->getUpIndex()][SSViewer_Scope::ITEM_OVERLAY] = $this->overlay;
566
567
		$this->setItemStack($itemStack);
568
		$this->overlay = array();
569
570
		return $scope;
571
	}
572
573
	/**
574
	 * Now that we're going to jump up an item in the item stack, we need to
575
	 * restore the overlay that was previously stored against the next item "up"
576
	 * in the stack from the current one
577
	 * @return SSViewer_Scope
578
	 */
579
	public function popScope() {
580
		$itemStack = $this->getItemStack();
581
		$this->overlay = $itemStack[$this->getUpIndex()][SSViewer_Scope::ITEM_OVERLAY];
582
583
		return parent::popScope();
584
	}
585
586
	/**
587
	 * $Up and $Top need to restore the overlay from the parent and top-level
588
	 * scope respectively.
589
	 */
590
	public function obj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
591
		$overlayIndex = false;
592
593
		switch($name) {
594
			case 'Up':
595
				$upIndex = $this->getUpIndex();
596
				if ($upIndex === null) {
597
					user_error('Up called when we\'re already at the top of the scope', E_USER_ERROR);
598
				}
599
600
				$overlayIndex = $upIndex; // Parent scope
601
				break;
602
			case 'Top':
603
				$overlayIndex = 0; // Top-level scope
604
				break;
605
		}
606
607
		if ($overlayIndex !== false) {
608
			$itemStack = $this->getItemStack();
609
			if (!$this->overlay && isset($itemStack[$overlayIndex][SSViewer_Scope::ITEM_OVERLAY])) {
610
				$this->overlay = $itemStack[$overlayIndex][SSViewer_Scope::ITEM_OVERLAY];
611
			}
612
		}
613
614
		return parent::obj($name, $arguments, $forceReturnedObject, $cache, $cacheName);
615
	}
616
617
	public function getObj($name, $arguments = null, $forceReturnedObject = true, $cache = false, $cacheName = null) {
618
		$result = $this->getInjectedValue($name, (array)$arguments);
619
		if($result) return $result['obj'];
620
		else return parent::getObj($name, $arguments, $forceReturnedObject, $cache, $cacheName);
621
	}
622
623
	public function __call($name, $arguments) {
624
		//extract the method name and parameters
625
		$property = $arguments[0];  //the name of the public function being called
626
627
		//the public function parameters in an array
628 View Code Duplication
		if (isset($arguments[1]) && $arguments[1] != null) $params = $arguments[1];
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
629
		else $params = array();
630
631
		$val = $this->getInjectedValue($property, $params);
632
		if ($val) {
633
			$obj = $val['obj'];
634
			if ($name === 'hasValue') {
635
				$res = $obj instanceof Object
636
					? $obj->exists()
637
					: (bool)$obj;
638
			} else {
639
				// XML_val
640
				$res = $obj->forTemplate();
641
			}
642
			$this->resetLocalScope();
643
			return $res;
644
		} else {
645
			return parent::__call($name, $arguments);
646
		}
647
	}
648
}
649
650
/**
651
 * Parses a template file with an *.ss file extension.
652
 *
653
 * In addition to a full template in the templates/ folder, a template in
654
 * templates/Content or templates/Layout will be rendered into $Content and
655
 * $Layout, respectively.
656
 *
657
 * A single template can be parsed by multiple nested {@link SSViewer} instances
658
 * through $Layout/$Content placeholders, as well as <% include MyTemplateFile %> template commands.
659
 *
660
 * <b>Themes</b>
661
 *
662
 * See http://doc.silverstripe.org/themes and http://doc.silverstripe.org/themes:developing
663
 *
664
 * <b>Caching</b>
665
 *
666
 * Compiled templates are cached via {@link SS_Cache}, usually on the filesystem.
667
 * If you put ?flush=1 on your URL, it will force the template to be recompiled.
668
 *
669
 * @see http://doc.silverstripe.org/themes
670
 * @see http://doc.silverstripe.org/themes:developing
671
 *
672
 * @package framework
673
 * @subpackage view
674
 */
675
class SSViewer implements Flushable {
676
677
	/**
678
	 * @config
679
	 * @var boolean $source_file_comments
680
	 */
681
	private static $source_file_comments = false;
682
683
	/**
684
	 * @ignore
685
	 */
686
	private static $template_cache_flushed = false;
687
688
	/**
689
	 * @ignore
690
	 */
691
	private static $cacheblock_cache_flushed = false;
692
693
	/**
694
	 * Set whether HTML comments indicating the source .SS file used to render this page should be
695
	 * included in the output.  This is enabled by default
696
	 *
697
	 * @deprecated 4.0 Use the "SSViewer.source_file_comments" config setting instead
698
	 * @param boolean $val
699
	 */
700
	public static function set_source_file_comments($val) {
701
		Deprecation::notice('4.0', 'Use the "SSViewer.source_file_comments" config setting instead');
702
		Config::inst()->update('SSViewer', 'source_file_comments', $val);
703
	}
704
705
	/**
706
	 * @deprecated 4.0 Use the "SSViewer.source_file_comments" config setting instead
707
	 * @return boolean
708
	 */
709
	public static function get_source_file_comments() {
710
		Deprecation::notice('4.0', 'Use the "SSViewer.source_file_comments" config setting instead');
711
		return Config::inst()->get('SSViewer', 'source_file_comments');
712
	}
713
714
	/**
715
	 * @var array $chosenTemplates Associative array for the different
716
	 * template containers: "main" and "Layout". Values are absolute file paths to *.ss files.
717
	 */
718
	private $chosenTemplates = array();
719
720
	/**
721
	 * @var boolean
722
	 */
723
	protected $rewriteHashlinks = true;
724
725
	/**
726
	 * @config
727
	 * @var string The used "theme", which usually consists of templates, images and stylesheets.
728
	 * Only used when {@link $theme_enabled} is set to TRUE.
729
	 */
730
	private static $theme = null;
731
732
	/**
733
	 * @config
734
	 * @var boolean Use the theme. Set to FALSE in order to disable themes,
735
	 * which can be useful for scenarios where theme overrides are temporarily undesired,
736
	 * such as an administrative interface separate from the website theme.
737
	 * It retains the theme settings to be re-enabled, for example when a website content
738
	 * needs to be rendered from within this administrative interface.
739
	 */
740
	private static $theme_enabled = true;
741
742
	/**
743
	 * @var boolean
744
	 */
745
	protected $includeRequirements = true;
746
747
	/**
748
	 * @var TemplateParser
749
	 */
750
	protected $parser;
751
752
	/*
753
	 * Default prepended cache key for partial caching
754
	 *
755
	 * @var string
756
	 * @config
757
	 */
758
	private static $global_key = '$CurrentReadingMode, $CurrentUser.ID';
759
760
	/**
761
	 * Triggered early in the request when someone requests a flush.
762
	 */
763
	public static function flush() {
764
		self::flush_template_cache(true);
765
		self::flush_cacheblock_cache(true);
766
	}
767
768
	/**
769
	 * Create a template from a string instead of a .ss file
770
	 *
771
	 * @param string $content The template content
772
	 * @param bool|void $cacheTemplate Whether or not to cache the template from string
773
	 * @return SSViewer
774
	 */
775
	public static function fromString($content, $cacheTemplate = null) {
776
		$viewer = new SSViewer_FromString($content);
777
		if ($cacheTemplate !== null) {
778
			$viewer->setCacheTemplate($cacheTemplate);
779
		}
780
		return $viewer;
781
	}
782
783
	/**
784
	 * @deprecated 4.0 Use the "SSViewer.theme" config setting instead
785
	 * @param string $theme The "base theme" name (without underscores).
786
	 */
787
	public static function set_theme($theme) {
788
		Deprecation::notice('4.0', 'Use the "SSViewer.theme" config setting instead');
789
		Config::inst()->update('SSViewer', 'theme', $theme);
790
	}
791
792
	/**
793
	 * @deprecated 4.0 Use the "SSViewer.theme" config setting instead
794
	 * @return string
795
	 */
796
	public static function current_theme() {
797
		Deprecation::notice('4.0', 'Use the "SSViewer.theme" config setting instead');
798
		return Config::inst()->get('SSViewer', 'theme');
799
	}
800
801
	/**
802
	 * Returns the path to the theme folder
803
	 *
804
	 * @return string
805
	 */
806
	public static function get_theme_folder() {
807
		$theme = Config::inst()->get('SSViewer', 'theme');
808
		return $theme ? THEMES_DIR . "/" . $theme : project();
809
	}
810
811
	/**
812
	 * Returns an array of theme names present in a directory.
813
	 *
814
	 * @param  string $path
815
	 * @param  bool   $subthemes Include subthemes (default false).
816
	 * @return array
817
	 */
818
	public static function get_themes($path = null, $subthemes = false) {
819
		$path   = rtrim($path ? $path : THEMES_PATH, '/');
820
		$themes = array();
821
822
		if (!is_dir($path)) return $themes;
823
824
		foreach (scandir($path) as $item) {
825
			if ($item[0] != '.' && is_dir("$path/$item")) {
826
				if ($subthemes || strpos($item, '_') === false) {
827
					$themes[$item] = $item;
828
				}
829
			}
830
		}
831
832
		return $themes;
833
	}
834
835
	/**
836
	 * @deprecated since version 4.0
837
	 * @return string
838
	 */
839
	public static function current_custom_theme(){
840
		Deprecation::notice('4.0', 'Use the "SSViewer.theme" and "SSViewer.theme_enabled" config settings instead');
841
		return Config::inst()->get('SSViewer', 'theme_enabled') ? Config::inst()->get('SSViewer', 'theme') : null;
842
	}
843
844
	/**
845
	 * Traverses the given the given class context looking for templates with the relevant name.
846
	 *
847
	 * @param $className string - valid class name
848
	 * @param $suffix string
849
	 * @param $baseClass string
850
	 *
851
	 * @return array
852
	 */
853
	public static function get_templates_by_class($className, $suffix = '', $baseClass = null) {
854
		// Figure out the class name from the supplied context.
855
		if(!is_string($className) || !class_exists($className)) {
856
			throw new InvalidArgumentException('SSViewer::get_templates_by_class() expects a valid class name as ' .
857
				'its first parameter.');
858
			return array();
0 ignored issues
show
Unused Code introduced by
return array(); does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
859
		}
860
		$templates = array();
861
		$classes = array_reverse(ClassInfo::ancestry($className));
862
		foreach($classes as $class) {
863
			$template = $class . $suffix;
864
			if(SSViewer::hasTemplate($template)) $templates[] = $template;
0 ignored issues
show
Documentation introduced by
$template is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
865
866
			// If the class is "Page_Controller", look for Page.ss
867
			if(stripos($class,'_controller') !== false) {
868
				$template = str_ireplace('_controller','',$class) . $suffix;
869
				if(SSViewer::hasTemplate($template)) $templates[] = $template;
0 ignored issues
show
Documentation introduced by
$template is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
870
			}
871
872
			if($baseClass && $class == $baseClass) break;
873
		}
874
		return $templates;
875
	}
876
877
	/**
878
	 * @param string|array $templateList If passed as a string with .ss extension, used as the "main" template.
879
	 *  If passed as an array, it can be used for template inheritance (first found template "wins").
880
	 *  Usually the array values are PHP class names, which directly correlate to template names.
881
	 *  <code>
882
	 *  array('MySpecificPage', 'MyPage', 'Page')
883
	 *  </code>
884
	 */
885
	public function __construct($templateList, TemplateParser $parser = null) {
886
		if ($parser) {
887
			$this->setParser($parser);
888
		}
889
890
		if(!is_array($templateList) && substr((string) $templateList,-3) == '.ss') {
891
			$this->chosenTemplates['main'] = $templateList;
892
		} else {
893 View Code Duplication
			if(Config::inst()->get('SSViewer', 'theme_enabled')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
894
				$theme = Config::inst()->get('SSViewer', 'theme');
895
			} else {
896
				$theme = null;
897
			}
898
			$this->chosenTemplates = SS_TemplateLoader::instance()->findTemplates(
899
				$templateList, $theme
900
			);
901
		}
902
903
		if(!$this->chosenTemplates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->chosenTemplates of type array 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...
904
			$templateList = (is_array($templateList)) ? $templateList : array($templateList);
905
906
			$message = 'None of the following templates could be found';
907
			if(!$theme) {
908
				$message .= ' (no theme in use)';
909
			} else {
910
				$message .= ' in theme "' . $theme . '"';
0 ignored issues
show
Bug introduced by
The variable $theme does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
911
			}
912
913
			user_error($message . ': ' . implode(".ss, ", $templateList) . ".ss", E_USER_WARNING);
914
		}
915
	}
916
917
	/**
918
	 * Set the template parser that will be used in template generation
919
	 * @param \TemplateParser $parser
920
	 */
921
	public function setParser(TemplateParser $parser)
922
	{
923
		$this->parser = $parser;
924
	}
925
926
	/**
927
	 * Returns the parser that is set for template generation
928
	 * @return \TemplateParser
929
	 */
930
	public function getParser()
931
	{
932
		if (!$this->parser) {
933
			$this->setParser(Injector::inst()->get('SSTemplateParser'));
934
		}
935
		return $this->parser;
936
	}
937
938
	/**
939
	 * Returns true if at least one of the listed templates exists.
940
	 *
941
	 * @param array $templates
942
	 *
943
	 * @return boolean
944
	 */
945
	public static function hasTemplate($templates) {
946
		$manifest = SS_TemplateLoader::instance()->getManifest();
947
948 View Code Duplication
		if(Config::inst()->get('SSViewer', 'theme_enabled')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
949
			$theme = Config::inst()->get('SSViewer', 'theme');
950
		} else {
951
			$theme = null;
952
		}
953
954
		foreach ((array) $templates as $template) {
955
			if ($manifest->getCandidateTemplate($template, $theme)) return true;
956
		}
957
958
		return false;
959
	}
960
961
	/**
962
	 * Set a global rendering option.
963
	 *
964
	 * The following options are available:
965
	 *  - rewriteHashlinks: If true (the default), <a href="#..."> will be rewritten to contain the
966
	 *    current URL.  This lets it play nicely with our <base> tag.
967
	 *  - If rewriteHashlinks = 'php' then, a piece of PHP script will be inserted before the hash
968
	 *    links: "<?php echo $_SERVER['REQUEST_URI']; ?>".  This is useful if you're generating a
969
	 *    page that will be saved to a .php file and may be accessed from different URLs.
970
	 *
971
	 * @deprecated 4.0 Use the "SSViewer.rewrite_hash_links" config setting instead
972
	 * @param string $optionName
973
	 * @param mixed $optionVal
974
	 */
975 View Code Duplication
	public static function setOption($optionName, $optionVal) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
976
		if($optionName == 'rewriteHashlinks') {
977
			Deprecation::notice('4.0', 'Use the "SSViewer.rewrite_hash_links" config setting instead');
978
			Config::inst()->update('SSViewer', 'rewrite_hash_links', $optionVal);
979
		} else {
980
			Deprecation::notice('4.0', 'Use the "SSViewer.' . $optionName . '" config setting instead');
981
			Config::inst()->update('SSViewer', $optionName, $optionVal);
982
		}
983
	}
984
985
	/**
986
 	 * @deprecated 4.0 Use the "SSViewer.rewrite_hash_links" config setting instead
987
 	 * @param string
988
 	 * @return mixed
989
	 */
990 View Code Duplication
	public static function getOption($optionName) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
991
		if($optionName == 'rewriteHashlinks') {
992
			Deprecation::notice('4.0', 'Use the "SSViewer.rewrite_hash_links" config setting instead');
993
			return Config::inst()->get('SSViewer', 'rewrite_hash_links');
994
		} else {
995
			Deprecation::notice('4.0', 'Use the "SSViewer.' . $optionName . '" config setting instead');
996
			return Config::inst()->get('SSViewer', $optionName);
997
		}
998
	}
999
1000
	/**
1001
	 * @config
1002
	 * @var boolean
1003
	 */
1004
	private static $rewrite_hash_links = true;
1005
1006
	protected static $topLevel = array();
1007
1008
	public static function topLevel() {
1009
		if(SSViewer::$topLevel) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \SSViewer::$topLevel of type array 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...
1010
			return SSViewer::$topLevel[sizeof(SSViewer::$topLevel)-1];
1011
		}
1012
	}
1013
1014
	/**
1015
	 * Call this to disable rewriting of <a href="#xxx"> links.  This is useful in Ajax applications.
1016
	 * It returns the SSViewer objects, so that you can call new SSViewer("X")->dontRewriteHashlinks()->process();
1017
	 */
1018
	public function dontRewriteHashlinks() {
1019
		$this->rewriteHashlinks = false;
1020
		Config::inst()->update('SSViewer', 'rewrite_hash_links', false);
1021
		return $this;
1022
	}
1023
1024
	public function exists() {
1025
		return $this->chosenTemplates;
1026
	}
1027
1028
	/**
1029
	 * @param string $identifier A template name without '.ss' extension or path
1030
	 * @param string $type The template type, either "main", "Includes" or "Layout"
1031
	 *
1032
	 * @return string Full system path to a template file
1033
	 */
1034
	public static function getTemplateFileByType($identifier, $type) {
1035
		$loader = SS_TemplateLoader::instance();
1036 View Code Duplication
		if(Config::inst()->get('SSViewer', 'theme_enabled')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1037
			$theme = Config::inst()->get('SSViewer', 'theme');
1038
		} else {
1039
			$theme = null;
1040
		}
1041
		$found  = $loader->findTemplates("$type/$identifier", $theme);
1042
1043
		if (isset($found['main'])) {
1044
			return $found['main'];
1045
		}
1046
		else if (!empty($found)) {
1047
			$founds = array_values($found);
1048
			return $founds[0];
1049
		}
1050
	}
1051
1052
	/**
1053
	 * Clears all parsed template files in the cache folder.
1054
	 *
1055
	 * Can only be called once per request (there may be multiple SSViewer instances).
1056
	 *
1057
	 * @param bool $force Set this to true to force a re-flush. If left to false, flushing
1058
	 * may only be performed once a request.
1059
	 */
1060
	public static function flush_template_cache($force = false) {
1061
		if (!self::$template_cache_flushed || $force) {
1062
			$dir = dir(TEMP_FOLDER);
1063
			while (false !== ($file = $dir->read())) {
1064
				if (strstr($file, '.cache')) unlink(TEMP_FOLDER . '/' . $file);
1065
			}
1066
			self::$template_cache_flushed = true;
1067
		}
1068
	}
1069
1070
	/**
1071
	 * Clears all partial cache blocks.
1072
	 *
1073
	 * Can only be called once per request (there may be multiple SSViewer instances).
1074
	 *
1075
	 * @param bool $force Set this to true to force a re-flush. If left to false, flushing
1076
	 * may only be performed once a request.
1077
	 */
1078
	public static function flush_cacheblock_cache($force = false) {
1079
		if (!self::$cacheblock_cache_flushed || $force) {
1080
			$cache = SS_Cache::factory('cacheblock');
1081
			$backend = $cache->getBackend();
1082
1083 View Code Duplication
			if(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1084
				$backend instanceof Zend_Cache_Backend_ExtendedInterface
0 ignored issues
show
Bug introduced by
The class Zend_Cache_Backend_ExtendedInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1085
				&& ($capabilities = $backend->getCapabilities())
1086
				&& $capabilities['tags']
1087
			) {
1088
				$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, $cache->getTags());
1089
			} else {
1090
				$cache->clean(Zend_Cache::CLEANING_MODE_ALL);
1091
			}
1092
1093
1094
			self::$cacheblock_cache_flushed = true;
1095
		}
1096
	}
1097
1098
	/**
1099
	 * @var Zend_Cache_Core
1100
	 */
1101
	protected $partialCacheStore = null;
1102
1103
	/**
1104
	 * Set the cache object to use when storing / retrieving partial cache blocks.
1105
	 *
1106
	 * @param Zend_Cache_Core $cache
1107
	 */
1108
	public function setPartialCacheStore($cache) {
1109
		$this->partialCacheStore = $cache;
1110
	}
1111
1112
	/**
1113
	 * Get the cache object to use when storing / retrieving partial cache blocks.
1114
	 *
1115
	 * @return Zend_Cache_Core
1116
	 */
1117
	public function getPartialCacheStore() {
1118
		return $this->partialCacheStore ? $this->partialCacheStore : SS_Cache::factory('cacheblock');
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->partialCacheStore...:factory('cacheblock'); of type Zend_Cache_Core|Zend_Cache_Frontend adds the type Zend_Cache_Frontend to the return on line 1118 which is incompatible with the return type documented by SSViewer::getPartialCacheStore of type Zend_Cache_Core.
Loading history...
1119
	}
1120
1121
	/**
1122
	 * Flag whether to include the requirements in this response.
1123
	 *
1124
	 * @param boolean
1125
	 */
1126
	public function includeRequirements($incl = true) {
1127
		$this->includeRequirements = $incl;
1128
	}
1129
1130
	/**
1131
	 * An internal utility function to set up variables in preparation for including a compiled
1132
	 * template, then do the include
1133
	 *
1134
	 * Effectively this is the common code that both SSViewer#process and SSViewer_FromString#process call
1135
	 *
1136
	 * @param string $cacheFile - The path to the file that contains the template compiled to PHP
1137
	 * @param Object $item - The item to use as the root scope for the template
1138
	 * @param array|null $overlay - Any variables to layer on top of the scope
1139
	 * @param array|null $underlay - Any variables to layer underneath the scope
1140
	 * @param Object $inheritedScope - the current scope of a parent template including a sub-template
1141
	 *
1142
	 * @return string - The result of executing the template
1143
	 */
1144
	protected function includeGeneratedTemplate($cacheFile, $item, $overlay, $underlay, $inheritedScope = null) {
1145
		if(isset($_GET['showtemplate']) && $_GET['showtemplate'] && Permission::check('ADMIN')) {
1146
			$lines = file($cacheFile);
1147
			echo "<h2>Template: $cacheFile</h2>";
1148
			echo "<pre>";
1149
			foreach($lines as $num => $line) {
1150
				echo str_pad($num+1,5) . htmlentities($line, ENT_COMPAT, 'UTF-8');
1151
			}
1152
			echo "</pre>";
1153
		}
1154
1155
		$cache = $this->getPartialCacheStore();
1156
		$scope = new SSViewer_DataPresenter($item, $overlay, $underlay, $inheritedScope);
1157
		$val = '';
1158
1159
		include($cacheFile);
1160
1161
		return $val;
1162
	}
1163
1164
	/**
1165
	 * The process() method handles the "meat" of the template processing.
1166
	 *
1167
	 * It takes care of caching the output (via {@link SS_Cache}), as well as
1168
	 * replacing the special "$Content" and "$Layout" placeholders with their
1169
	 * respective subtemplates.
1170
	 *
1171
	 * The method injects extra HTML in the header via {@link Requirements::includeInHTML()}.
1172
	 *
1173
	 * Note: You can call this method indirectly by {@link ViewableData->renderWith()}.
1174
	 *
1175
	 * @param ViewableData $item
1176
	 * @param array|null $arguments - arguments to an included template
1177
	 * @param Object $inheritedScope - the current scope of a parent template including a sub-template
1178
	 *
1179
	 * @return HTMLText Parsed template output.
1180
	 */
1181
	public function process($item, $arguments = null, $inheritedScope = null) {
1182
		SSViewer::$topLevel[] = $item;
1183
1184
		if(isset($this->chosenTemplates['main'])) {
1185
			$template = $this->chosenTemplates['main'];
1186
		} else {
1187
			$keys = array_keys($this->chosenTemplates);
1188
			$key = reset($keys);
1189
			$template = $this->chosenTemplates[$key];
1190
		}
1191
1192
		$cacheFile = TEMP_FOLDER . "/.cache"
1193
			. str_replace(array('\\','/',':'), '.', Director::makeRelative(realpath($template)));
1194
		$lastEdited = filemtime($template);
1195
1196
		if(!file_exists($cacheFile) || filemtime($cacheFile) < $lastEdited) {
1197
			$content = file_get_contents($template);
1198
			$content = $this->parseTemplateContent($content, $template);
1199
1200
			$fh = fopen($cacheFile,'w');
1201
			fwrite($fh, $content);
1202
			fclose($fh);
1203
		}
1204
1205
		$underlay = array('I18NNamespace' => basename($template));
1206
1207
		// Makes the rendered sub-templates available on the parent item,
1208
		// through $Content and $Layout placeholders.
1209
		foreach(array('Content', 'Layout') as $subtemplate) {
1210
			if(isset($this->chosenTemplates[$subtemplate])) {
1211
				$subtemplateViewer = clone $this;
1212
				// Disable requirements - this will be handled by the parent template
1213
				$subtemplateViewer->includeRequirements(false);
1214
				// The subtemplate is the only file we want to process, so set it as the "main" template file
1215
				$subtemplateViewer->chosenTemplates = array('main' => $this->chosenTemplates[$subtemplate]);
1216
1217
				$underlay[$subtemplate] = $subtemplateViewer->process($item, $arguments);
1218
			}
1219
		}
1220
1221
		$output = $this->includeGeneratedTemplate($cacheFile, $item, $arguments, $underlay, $inheritedScope);
1222
1223
		if($this->includeRequirements) {
1224
			$output = Requirements::includeInHTML($output);
1225
		}
1226
1227
		array_pop(SSViewer::$topLevel);
1228
1229
		// If we have our crazy base tag, then fix # links referencing the current page.
1230
1231
		$rewrite = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1232
		if($this->rewriteHashlinks && $rewrite) {
1233
			if(strpos($output, '<base') !== false) {
1234
				if($rewrite === 'php') {
1235
					$thisURLRelativeToBase = "<?php echo Convert::raw2att(preg_replace(\"/^(\\\\/)+/\", \"/\", \$_SERVER['REQUEST_URI'])); ?>";
1236
				} else {
1237
					$thisURLRelativeToBase = Convert::raw2att(preg_replace("/^(\\/)+/", "/", $_SERVER['REQUEST_URI']));
1238
				}
1239
1240
				$output = preg_replace('/(<a[^>]+href *= *)"#/i', '\\1"' . $thisURLRelativeToBase . '#', $output);
1241
			}
1242
		}
1243
1244
		return DBField::create_field('HTMLText', $output, null, array('shortcodes' => false));
1245
	}
1246
1247
	/**
1248
	 * Execute the given template, passing it the given data.
1249
	 * Used by the <% include %> template tag to process templates.
1250
	 *
1251
	 * @param string $template Template name
1252
	 * @param mixed $data Data context
1253
	 * @param array $arguments Additional arguments
1254
	 * @return string Evaluated result
1255
	 */
1256
	public static function execute_template($template, $data, $arguments = null, $scope = null) {
1257
		$v = new SSViewer($template);
1258
		$v->includeRequirements(false);
1259
1260
		return $v->process($data, $arguments, $scope);
1261
	}
1262
1263
	/**
1264
	 * Execute the evaluated string, passing it the given data.
1265
	 * Used by partial caching to evaluate custom cache keys expressed using
1266
	 * template expressions
1267
	 *
1268
	 * @param string $content Input string
1269
	 * @param mixed $data Data context
1270
	 * @param array $arguments Additional arguments
1271
	 * @return string Evaluated result
1272
	 */
1273
	public static function execute_string($content, $data, $arguments = null) {
1274
		$v = SSViewer::fromString($content);
1275
		$v->includeRequirements(false);
1276
1277
		return $v->process($data, $arguments);
1278
	}
1279
1280
	public function parseTemplateContent($content, $template="") {
1281
		return $this->getParser()->compileString(
1282
			$content,
1283
			$template,
1284
			Director::isDev() && Config::inst()->get('SSViewer', 'source_file_comments')
1285
		);
1286
	}
1287
1288
	/**
1289
	 * Returns the filenames of the template that will be rendered.  It is a map that may contain
1290
	 * 'Content' & 'Layout', and will have to contain 'main'
1291
	 */
1292
	public function templates() {
1293
		return $this->chosenTemplates;
1294
	}
1295
1296
	/**
1297
	 * @param string $type "Layout" or "main"
1298
	 * @param string $file Full system path to the template file
1299
	 */
1300
	public function setTemplateFile($type, $file) {
1301
		$this->chosenTemplates[$type] = $file;
1302
	}
1303
1304
	/**
1305
	 * Return an appropriate base tag for the given template.
1306
	 * It will be closed on an XHTML document, and unclosed on an HTML document.
1307
	 *
1308
	 * @param $contentGeneratedSoFar The content of the template generated so far; it should contain
1309
	 * the DOCTYPE declaration.
1310
	 */
1311
	public static function get_base_tag($contentGeneratedSoFar) {
1312
		$base = Director::absoluteBaseURL();
1313
1314
		// Is the document XHTML?
1315
		if(preg_match('/<!DOCTYPE[^>]+xhtml/i', $contentGeneratedSoFar)) {
1316
			return "<base href=\"$base\" />";
1317
		} else {
1318
			return "<base href=\"$base\"><!--[if lte IE 6]></base><![endif]-->";
1319
		}
1320
	}
1321
}
1322
1323
/**
1324
 * Special SSViewer that will process a template passed as a string, rather than a filename.
1325
 * @package framework
1326
 * @subpackage view
1327
 */
1328
class SSViewer_FromString extends SSViewer {
1329
1330
	/**
1331
	 * The global template caching behaviour if no instance override is specified
1332
	 * @config
1333
	 * @var bool
1334
	 */
1335
	private static $cache_template = true;
1336
1337
	/**
1338
	 * The template to use
1339
	 * @var string
1340
	 */
1341
	protected $content;
1342
1343
	/**
1344
	 * Indicates whether templates should be cached
1345
	 * @var bool
1346
	 */
1347
	protected $cacheTemplate;
1348
1349
	public function __construct($content, TemplateParser $parser = null) {
1350
		if ($parser) {
1351
			$this->setParser($parser);
1352
		}
1353
1354
		$this->content = $content;
1355
	}
1356
1357
	public function process($item, $arguments = null, $scope = null) {
1358
		$hash = sha1($this->content);
1359
		$cacheFile = TEMP_FOLDER . "/.cache.$hash";
1360
1361
		if(!file_exists($cacheFile) || isset($_GET['flush'])) {
1362
			$content = $this->parseTemplateContent($this->content, "string sha1=$hash");
1363
			$fh = fopen($cacheFile,'w');
1364
			fwrite($fh, $content);
1365
			fclose($fh);
1366
		}
1367
1368
		$val = $this->includeGeneratedTemplate($cacheFile, $item, $arguments, null, $scope);
1369
1370
		if ($this->cacheTemplate !== null) {
1371
			$cacheTemplate = $this->cacheTemplate;
1372
		} else {
1373
			$cacheTemplate = Config::inst()->get('SSViewer_FromString', 'cache_template');
1374
		}
1375
1376
		if (!$cacheTemplate) {
1377
			unlink($cacheFile);
1378
		}
1379
1380
		return $val;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $val; (string) is incompatible with the return type of the parent method SSViewer::process of type SilverStripe\Model\FieldType\DBField.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1381
	}
1382
1383
	/**
1384
	 * @param boolean $cacheTemplate
1385
	 */
1386
	public function setCacheTemplate($cacheTemplate) {
1387
		$this->cacheTemplate = (bool) $cacheTemplate;
1388
	}
1389
1390
	/**
1391
	 * @return boolean
1392
	 */
1393
	public function getCacheTemplate() {
1394
		return $this->cacheTemplate;
1395
	}
1396
}
1397