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

SSViewerTest   F

Complexity

Total Complexity 58

Size/Duplication

Total Lines 1526
Duplicated Lines 1.97 %

Coupling/Cohesion

Components 3
Dependencies 25

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 30
loc 1526
rs 0.5217
c 1
b 0
f 0
wmc 58
lcom 3
cbo 25

49 Methods

Rating   Name   Duplication   Size   Complexity  
A setUp() 0 7 1
A tearDown() 0 5 1
A testCurrentTheme() 0 6 1
A testTemplateWithoutHeadRenders() 0 8 1
B testIncludeScopeInheritance() 0 27 1
B testIncludeTruthyness() 0 27 1
A getScopeInheritanceTestData() 0 13 1
A assertExpectedStrings() 0 8 2
A render() 0 5 2
A testRequirements() 0 16 1
B testRequirementsCombine() 0 31 3
A testComments() 0 21 1
A testBasicText() 0 6 1
A testBasicInjection() 0 14 1
A testGlobalVariableCalls() 0 5 1
A testGlobalVariableCallsWithArguments() 0 8 1
A testGlobalVariablesAreEscaped() 0 9 1
B testCoreGlobalVariableCalls() 0 39 1
A testNonFieldCastingHelpersNotUsedInHasValue() 0 11 1
A testLocalFunctionsTakePriorityOverGlobals() 0 23 1
B testCurrentScopeLoopWith() 0 40 1
A testObjectDotArguments() 0 20 1
A testEscapedArguments() 0 22 1
B testLoopWhitespace() 0 44 1
B testControls() 0 32 1
B testIfBlocks() 0 116 1
B testBaseTagGeneration() 0 41 1
A testIncludeWithArguments() 0 69 1
B testRecursiveInclude() 0 24 1
A assertEqualIgnoringWhitespace() 0 3 1
A testCastingHelpers() 0 54 1
B testSSViewerBasicIteratorSupport() 0 116 1
A testUpInWith() 0 50 1
A testUpInLoop() 0 72 1
B testNestedLoops() 0 37 1
A testLayout() 0 11 1
B testGetTemplatesByClass() 0 27 1
B testThemeRetrieval() 0 28 2
A testRewriteHashlinks() 0 57 1
B testRewriteHashlinksInPhpMode() 0 40 1
B testRenderWithSourceFileComments() 0 89 2
A _renderWithSourceFileComments() 0 8 1
A testLoopIteratorIterator() 0 6 1
B testProcessOnlyIncludesRequirementsOnce() 0 24 1
A testRequireCallInTemplateInclude() 0 17 2
B testCallsWithArguments() 0 38 2
A testClosedBlockExtension() 15 15 1
A testOpenBlockExtension() 15 15 1
B testFromStringCaching() 0 24 2

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 SSViewerTest 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 SSViewerTest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
class SSViewerTest extends SapphireTest {
4
5
	/**
6
	 * Backup of $_SERVER global
7
	 *
8
	 * @var array
9
	 */
10
	protected $oldServer = array();
11
12
	protected $extraDataObjects = array(
13
		'SSViewerTest_Object',
14
	);
15
16
	public function setUp() {
17
		parent::setUp();
18
		Config::inst()->update('SSViewer', 'source_file_comments', false);
19
		Config::inst()->update('SSViewer_FromString', 'cache_template', false);
20
		AssetStoreTest_SpyStore::activate('SSViewerTest');
21
		$this->oldServer = $_SERVER;
22
	}
23
24
	public function tearDown() {
25
		$_SERVER = $this->oldServer;
26
		AssetStoreTest_SpyStore::reset();
27
		parent::tearDown();
28
	}
29
30
	/**
31
	 * Tests for {@link Config::inst()->get('SSViewer', 'theme')} for different behaviour
32
	 * of user defined themes via {@link SiteConfig} and default theme
33
	 * when no user themes are defined.
34
	 */
35
	public function testCurrentTheme() {
36
		//TODO: SiteConfig moved to CMS
37
		Config::inst()->update('SSViewer', 'theme', 'mytheme');
38
		$this->assertEquals('mytheme', Config::inst()->get('SSViewer', 'theme'),
39
			'Current theme is the default - user has not defined one');
40
	}
41
42
	/**
43
	 * Test that a template without a <head> tag still renders.
44
	 */
45
	public function testTemplateWithoutHeadRenders() {
46
		$data = new ArrayData(array(
47
			'Var' => 'var value'
48
		));
49
50
		$result = $data->renderWith("SSViewerTestPartialTemplate");
51
		$this->assertEquals('Test partial template: var value', trim(preg_replace("/<!--.*-->/U",'',$result)));
52
	}
53
54
	public function testIncludeScopeInheritance() {
55
		$data = $this->getScopeInheritanceTestData();
56
		$expected = array(
57
			'Item 1 - First-ODD top:Item 1',
58
			'Item 2 - EVEN top:Item 2',
59
			'Item 3 - ODD top:Item 3',
60
			'Item 4 - EVEN top:Item 4',
61
			'Item 5 - ODD top:Item 5',
62
			'Item 6 - Last-EVEN top:Item 6',
63
		);
64
65
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritance');
66
		$this->assertExpectedStrings($result, $expected);
67
68
		// reset results for the tests that include arguments (the title is passed as an arg)
69
		$expected = array(
70
			'Item 1 _ Item 1 - First-ODD top:Item 1',
71
			'Item 2 _ Item 2 - EVEN top:Item 2',
72
			'Item 3 _ Item 3 - ODD top:Item 3',
73
			'Item 4 _ Item 4 - EVEN top:Item 4',
74
			'Item 5 _ Item 5 - ODD top:Item 5',
75
			'Item 6 _ Item 6 - Last-EVEN top:Item 6',
76
		);
77
78
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
79
		$this->assertExpectedStrings($result, $expected);
80
	}
81
82
	public function testIncludeTruthyness() {
83
		$data = new ArrayData(array(
84
			'Title' => 'TruthyTest',
85
			'Items' => new ArrayList(array(
86
				new ArrayData(array('Title' => 'Item 1')),
87
				new ArrayData(array('Title' => '')),
88
				new ArrayData(array('Title' => true)),
89
				new ArrayData(array('Title' => false)),
90
				new ArrayData(array('Title' => null)),
91
				new ArrayData(array('Title' => 0)),
92
				new ArrayData(array('Title' => 7))
93
			))
94
		));
95
		$result = $data->renderWith('SSViewerTestIncludeScopeInheritanceWithArgs');
96
97
		// We should not end up with empty values appearing as empty
98
		$expected = array(
99
			'Item 1 _ Item 1 - First-ODD top:Item 1',
100
			'Untitled - EVEN top:',
101
			'1 _ 1 - ODD top:1',
102
			'Untitled - EVEN top:',
103
			'Untitled - ODD top:',
104
			'Untitled - EVEN top:0',
105
			'7 _ 7 - Last-ODD top:7'
106
		);
107
		$this->assertExpectedStrings($result, $expected);
108
	}
109
110
	private function getScopeInheritanceTestData() {
111
		return new ArrayData(array(
112
			'Title' => 'TopTitleValue',
113
			'Items' => new ArrayList(array(
114
				new ArrayData(array('Title' => 'Item 1')),
115
				new ArrayData(array('Title' => 'Item 2')),
116
				new ArrayData(array('Title' => 'Item 3')),
117
				new ArrayData(array('Title' => 'Item 4')),
118
				new ArrayData(array('Title' => 'Item 5')),
119
				new ArrayData(array('Title' => 'Item 6'))
120
			))
121
		));
122
	}
123
124
	private function assertExpectedStrings($result, $expected) {
125
		foreach ($expected as $expectedStr) {
126
			$this->assertTrue(
127
				(boolean) preg_match("/{$expectedStr}/", $result),
128
				"Didn't find '{$expectedStr}' in:\n{$result}"
129
			);
130
		}
131
	}
132
133
	/**
134
	 * Small helper to render templates from strings
135
	 */
136
	public function render($templateString, $data = null, $cacheTemplate = false) {
137
		$t = SSViewer::fromString($templateString, $cacheTemplate);
138
		if(!$data) $data = new SSViewerTestFixture();
139
		return trim(''.$t->process($data));
140
	}
141
142
	public function testRequirements() {
143
		$requirements = $this->getMock("Requirements_Backend", array("javascript", "css"));
144
		$jsFile = FRAMEWORK_DIR . '/tests/forms/a.js';
145
		$cssFile = FRAMEWORK_DIR . '/tests/forms/a.js';
146
147
		$requirements->expects($this->once())->method('javascript')->with($jsFile);
148
		$requirements->expects($this->once())->method('css')->with($cssFile);
149
150
		$origReq = Requirements::backend();
151
		Requirements::set_backend($requirements);
152
		$template = $this->render("<% require javascript($jsFile) %>
153
		<% require css($cssFile) %>");
154
		Requirements::set_backend($origReq);
155
156
		$this->assertFalse((bool)trim($template), "Should be no content in this return.");
157
	}
158
159
	public function testRequirementsCombine(){
160
		$testBackend = Injector::inst()->create('Requirements_Backend');
161
		$testBackend->setSuffixRequirements(false);
162
		//$combinedTestFilePath = BASE_PATH . '/' . $testBackend->getCombinedFilesFolder() . '/testRequirementsCombine.js';
163
164
		$jsFile = FRAMEWORK_DIR . '/tests/view/themes/javascript/bad.js';
165
		$jsFileContents = file_get_contents(BASE_PATH . '/' . $jsFile);
166
		$testBackend->combineFiles('testRequirementsCombine.js', array($jsFile));
167
168
		// first make sure that our test js file causes an exception to be thrown
169
		try{
170
			require_once('thirdparty/jsmin/jsmin.php');
171
			$content = JSMin::minify($content);
0 ignored issues
show
Bug introduced by
The variable $content seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Unused Code introduced by
$content is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
172
			$this->fail('JSMin did not throw exception on minify bad file: ');
173
		} catch(Exception $e) {
174
			// exception thrown... good
175
		}
176
177
		// secondly, make sure that requirements is generated, even though minification failed
178
		$testBackend->processCombinedFiles();
179
		$js = $testBackend->getJavascript();
180
		$combinedTestFilePath = BASE_PATH . reset($js);
181
		$this->assertContains('_combinedfiles/testRequirementsCombine-4c0e97a.js', $combinedTestFilePath);
182
183
		// and make sure the combined content matches the input content, i.e. no loss of functionality
184
		if(!file_exists($combinedTestFilePath)) {
185
			$this->fail('No combined file was created at expected path: '.$combinedTestFilePath);
186
		}
187
		$combinedTestFileContents = file_get_contents($combinedTestFilePath);
188
		$this->assertContains($jsFileContents, $combinedTestFileContents);
189
	}
190
191
192
193
	public function testComments() {
194
		$output = $this->render(<<<SS
195
This is my template<%-- this is a comment --%>This is some content<%-- this is another comment --%>Final content
196
<%-- Alone multi
197
	line comment --%>
198
Some more content
199
Mixing content and <%-- multi
200
	line comment --%> Final final
201
content
202
SS
203
);
204
		$shouldbe = <<<SS
205
This is my templateThis is some contentFinal content
206
207
Some more content
208
Mixing content and  Final final
209
content
210
SS;
211
212
		$this->assertEquals($shouldbe, $output);
213
	}
214
215
	public function testBasicText() {
216
		$this->assertEquals('"', $this->render('"'), 'Double-quotes are left alone');
217
		$this->assertEquals("'", $this->render("'"), 'Single-quotes are left alone');
218
		$this->assertEquals('A', $this->render('\\A'), 'Escaped characters are unescaped');
219
		$this->assertEquals('\\A', $this->render('\\\\A'), 'Escaped back-slashed are correctly unescaped');
220
	}
221
222
	public function testBasicInjection() {
223
		$this->assertEquals('[out:Test]', $this->render('$Test'), 'Basic stand-alone injection');
224
		$this->assertEquals('[out:Test]', $this->render('{$Test}'), 'Basic stand-alone wrapped injection');
225
		$this->assertEquals('A[out:Test]!', $this->render('A$Test!'), 'Basic surrounded injection');
226
		$this->assertEquals('A[out:Test]B', $this->render('A{$Test}B'), 'Basic surrounded wrapped injection');
227
228
		$this->assertEquals('A$B', $this->render('A\\$B'), 'No injection as $ escaped');
229
		$this->assertEquals('A$ B', $this->render('A$ B'), 'No injection as $ not followed by word character');
230
		$this->assertEquals('A{$ B', $this->render('A{$ B'), 'No injection as {$ not followed by word character');
231
232
		$this->assertEquals('{$Test}', $this->render('{\\$Test}'), 'Escapes can be used to avoid injection');
233
		$this->assertEquals('{\\[out:Test]}', $this->render('{\\\\$Test}'),
234
			'Escapes before injections are correctly unescaped');
235
	}
236
237
238
	public function testGlobalVariableCalls() {
239
		$this->assertEquals('automatic', $this->render('$SSViewerTest_GlobalAutomatic'));
240
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedByString'));
241
		$this->assertEquals('reference', $this->render('$SSViewerTest_GlobalReferencedInArray'));
242
	}
243
244
	public function testGlobalVariableCallsWithArguments() {
245
		$this->assertEquals('zz', $this->render('$SSViewerTest_GlobalThatTakesArguments'));
246
		$this->assertEquals('zFooz', $this->render('$SSViewerTest_GlobalThatTakesArguments("Foo")'));
247
		$this->assertEquals('zFoo:Bar:Bazz',
248
			$this->render('$SSViewerTest_GlobalThatTakesArguments("Foo", "Bar", "Baz")'));
249
		$this->assertEquals('zreferencez',
250
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalReferencedByString)'));
251
	}
252
253
	public function testGlobalVariablesAreEscaped() {
254
		$this->assertEquals('<div></div>', $this->render('$SSViewerTest_GlobalHTMLFragment'));
255
		$this->assertEquals('&lt;div&gt;&lt;/div&gt;', $this->render('$SSViewerTest_GlobalHTMLEscaped'));
256
257
		$this->assertEquals('z<div></div>z',
258
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLFragment)'));
259
		$this->assertEquals('z&lt;div&gt;&lt;/div&gt;z',
260
			$this->render('$SSViewerTest_GlobalThatTakesArguments($SSViewerTest_GlobalHTMLEscaped)'));
261
	}
262
263
	public function testCoreGlobalVariableCalls() {
264
		$this->assertEquals(Director::absoluteBaseURL(),
265
			$this->render('{$absoluteBaseURL}'), 'Director::absoluteBaseURL can be called from within template');
266
		$this->assertEquals(Director::absoluteBaseURL(), $this->render('{$AbsoluteBaseURL}'),
267
			'Upper-case %AbsoluteBaseURL can be called from within template');
268
269
		$this->assertEquals(Director::is_ajax(), $this->render('{$isAjax}'),
270
			'All variations of is_ajax result in the correct call');
271
		$this->assertEquals(Director::is_ajax(), $this->render('{$IsAjax}'),
272
			'All variations of is_ajax result in the correct call');
273
		$this->assertEquals(Director::is_ajax(), $this->render('{$is_ajax}'),
274
			'All variations of is_ajax result in the correct call');
275
		$this->assertEquals(Director::is_ajax(), $this->render('{$Is_ajax}'),
276
			'All variations of is_ajax result in the correct call');
277
278
		$this->assertEquals(i18n::get_locale(), $this->render('{$i18nLocale}'),
279
			'i18n template functions result correct result');
280
		$this->assertEquals(i18n::get_locale(), $this->render('{$get_locale}'),
281
			'i18n template functions result correct result');
282
283
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentMember}'),
284
			'Member template functions result correct result');
285
		$this->assertEquals((string)Member::currentUser(), $this->render('{$CurrentUser}'),
286
			'Member template functions result correct result');
287
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentMember}'),
288
			'Member template functions result correct result');
289
		$this->assertEquals((string)Member::currentUser(), $this->render('{$currentUser}'),
290
			'Member template functions result correct result');
291
292
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$getSecurityID}'),
293
			'SecurityToken template functions result correct result');
294
		$this->assertEquals(SecurityToken::getSecurityID(), $this->render('{$SecurityID}'),
295
			'SecurityToken template functions result correct result');
296
297
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$HasPerm(\'ADMIN\')}'),
298
			'Permissions template functions result correct result');
299
		$this->assertEquals(Permission::check("ADMIN"), (bool)$this->render('{$hasPerm(\'ADMIN\')}'),
300
			'Permissions template functions result correct result');
301
	}
302
303
	public function testNonFieldCastingHelpersNotUsedInHasValue() {
304
		// check if Link without $ in front of variable
305
		$result = $this->render(
306
			'A<% if Link %>$Link<% end_if %>B', new SSViewerTest_Object());
307
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if Link %>');
308
309
		// check if Link with $ in front of variable
310
		$result = $this->render(
311
			'A<% if $Link %>$Link<% end_if %>B', new SSViewerTest_Object());
312
		$this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if $Link %>');
313
	}
314
315
	public function testLocalFunctionsTakePriorityOverGlobals() {
316
		$data = new ArrayData(array(
317
			'Page' => new SSViewerTest_Object()
318
		));
319
320
		//call method with lots of arguments
321
		$result = $this->render(
322
			'<% with Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_with %>',$data);
323
		$this->assertEquals("abcdefghijk",$result, "public function can accept up to 11 arguments");
324
325
		//call method that does not exist
326
		$result = $this->render('<% with Page %><% if IDoNotExist %>hello<% end_if %><% end_with %>',$data);
327
		$this->assertEquals("",$result, "Method does not exist - empty result");
328
329
		//call if that does not exist
330
		$result = $this->render('<% with Page %>$IDoNotExist("hello")<% end_with %>',$data);
331
		$this->assertEquals("",$result, "Method does not exist - empty result");
332
333
		//call method with same name as a global method (local call should take priority)
334
		$result = $this->render('<% with Page %>$absoluteBaseURL<% end_with %>',$data);
335
		$this->assertEquals("testLocalFunctionPriorityCalled",$result,
336
			"Local Object's public function called. Did not return the actual baseURL of the current site");
337
	}
338
339
	public function testCurrentScopeLoopWith() {
340
		// Data to run the loop tests on - one sequence of three items, each with a subitem
341
		$data = new ArrayData(array(
342
			'Foo' => new ArrayList(array(
343
				'Subocean' => new ArrayData(array(
344
						'Name' => 'Higher'
345
					)),
346
				new ArrayData(array(
347
					'Sub' => new ArrayData(array(
348
						'Name' => 'SubKid1'
349
					))
350
				)),
351
				new ArrayData(array(
352
					'Sub' => new ArrayData(array(
353
						'Name' => 'SubKid2'
354
					))
355
				)),
356
				new SSViewerTest_Object('Number6')
357
			))
358
		));
359
360
		$result = $this->render(
361
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
362
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
363
364
		$result = $this->render(
365
			'<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>',$data);
366
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop works");
367
368
		$result = $this->render('<% with Foo %>$Count<% end_with %>',$data);
369
		$this->assertEquals("4",$result, "4 items in the DataObjectSet");
370
371
		$result = $this->render('<% with Foo %><% loop Up.Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
372
			. '<% end_if %><% end_loop %><% end_with %>',$data);
373
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in with Up.Foo scope works");
374
375
		$result = $this->render('<% with Foo %><% loop %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>'
376
			. '<% end_if %><% end_loop %><% end_with %>',$data);
377
		$this->assertEquals("SubKid1SubKid2Number6",$result, "Loop in current scope works");
378
	}
379
380
	public function testObjectDotArguments() {
381
		$this->assertEquals(
382
			'[out:TestObject.methodWithOneArgument(one)]
383
				[out:TestObject.methodWithTwoArguments(one,two)]
384
				[out:TestMethod(Arg1,Arg2).Bar.Val]
385
				[out:TestMethod(Arg1,Arg2).Bar]
386
				[out:TestMethod(Arg1,Arg2)]
387
				[out:TestMethod(Arg1).Bar.Val]
388
				[out:TestMethod(Arg1).Bar]
389
				[out:TestMethod(Arg1)]',
390
			$this->render('$TestObject.methodWithOneArgument(one)
391
				$TestObject.methodWithTwoArguments(one,two)
392
				$TestMethod(Arg1, Arg2).Bar.Val
393
				$TestMethod(Arg1, Arg2).Bar
394
				$TestMethod(Arg1, Arg2)
395
				$TestMethod(Arg1).Bar.Val
396
				$TestMethod(Arg1).Bar
397
				$TestMethod(Arg1)')
398
		);
399
	}
400
401
	public function testEscapedArguments() {
402
		$this->assertEquals(
403
			'[out:Foo(Arg1,Arg2).Bar.Val].Suffix
404
				[out:Foo(Arg1,Arg2).Val]_Suffix
405
				[out:Foo(Arg1,Arg2)]/Suffix
406
				[out:Foo(Arg1).Bar.Val]textSuffix
407
				[out:Foo(Arg1).Bar].Suffix
408
				[out:Foo(Arg1)].Suffix
409
				[out:Foo.Bar.Val].Suffix
410
				[out:Foo.Bar].Suffix
411
				[out:Foo].Suffix',
412
			$this->render('{$Foo(Arg1, Arg2).Bar.Val}.Suffix
413
				{$Foo(Arg1, Arg2).Val}_Suffix
414
				{$Foo(Arg1, Arg2)}/Suffix
415
				{$Foo(Arg1).Bar.Val}textSuffix
416
				{$Foo(Arg1).Bar}.Suffix
417
				{$Foo(Arg1)}.Suffix
418
				{$Foo.Bar.Val}.Suffix
419
				{$Foo.Bar}.Suffix
420
				{$Foo}.Suffix')
421
		);
422
	}
423
424
	public function testLoopWhitespace() {
425
		$this->assertEquals(
426
			'before[out:SingleItem.Test]after
427
				beforeTestafter',
428
			$this->render('before<% loop SingleItem %>$Test<% end_loop %>after
429
				before<% loop SingleItem %>Test<% end_loop %>after')
430
		);
431
432
		// The control tags are removed from the output, but no whitespace
433
		// This is a quirk that could be changed, but included in the test to make the current
434
		// behaviour explicit
435
		$this->assertEquals(
436
			'before
437
438
[out:SingleItem.ItemOnItsOwnLine]
439
440
after',
441
			$this->render('before
442
<% loop SingleItem %>
443
$ItemOnItsOwnLine
444
<% end_loop %>
445
after')
446
		);
447
448
		// The whitespace within the control tags is preserve in a loop
449
		// This is a quirk that could be changed, but included in the test to make the current
450
		// behaviour explicit
451
		$this->assertEquals(
452
			'before
453
454
[out:Loop3.ItemOnItsOwnLine]
455
456
[out:Loop3.ItemOnItsOwnLine]
457
458
[out:Loop3.ItemOnItsOwnLine]
459
460
after',
461
			$this->render('before
462
<% loop Loop3 %>
463
$ItemOnItsOwnLine
464
<% end_loop %>
465
after')
466
		);
467
	}
468
469
	public function testControls() {
470
		// Single item controls
471
		$this->assertEquals(
472
			'a[out:Foo.Bar.Item]b
473
				[out:Foo.Bar(Arg1).Item]
474
				[out:Foo(Arg1).Item]
475
				[out:Foo(Arg1,Arg2).Item]
476
				[out:Foo(Arg1,Arg2,Arg3).Item]',
477
			$this->render('<% with Foo.Bar %>a{$Item}b<% end_with %>
478
				<% with Foo.Bar(Arg1) %>$Item<% end_with %>
479
				<% with Foo(Arg1) %>$Item<% end_with %>
480
				<% with Foo(Arg1, Arg2) %>$Item<% end_with %>
481
				<% with Foo(Arg1, Arg2, Arg3) %>$Item<% end_with %>')
482
		);
483
484
		// Loop controls
485
		$this->assertEquals('a[out:Foo.Loop2.Item]ba[out:Foo.Loop2.Item]b',
486
			$this->render('<% loop Foo.Loop2 %>a{$Item}b<% end_loop %>'));
487
488
		$this->assertEquals('[out:Foo.Loop2(Arg1).Item][out:Foo.Loop2(Arg1).Item]',
489
			$this->render('<% loop Foo.Loop2(Arg1) %>$Item<% end_loop %>'));
490
491
		$this->assertEquals('[out:Loop2(Arg1).Item][out:Loop2(Arg1).Item]',
492
			$this->render('<% loop Loop2(Arg1) %>$Item<% end_loop %>'));
493
494
		$this->assertEquals('[out:Loop2(Arg1,Arg2).Item][out:Loop2(Arg1,Arg2).Item]',
495
			$this->render('<% loop Loop2(Arg1, Arg2) %>$Item<% end_loop %>'));
496
497
		$this->assertEquals('[out:Loop2(Arg1,Arg2,Arg3).Item][out:Loop2(Arg1,Arg2,Arg3).Item]',
498
			$this->render('<% loop Loop2(Arg1, Arg2, Arg3) %>$Item<% end_loop %>'));
499
500
	}
501
502
	public function testIfBlocks() {
503
		// Basic test
504
		$this->assertEquals('AC',
505
			$this->render('A<% if NotSet %>B$NotSet<% end_if %>C'));
506
507
		// Nested test
508
		$this->assertEquals('AB1C',
509
			$this->render('A<% if IsSet %>B$NotSet<% if IsSet %>1<% else %>2<% end_if %><% end_if %>C'));
510
511
		// else_if
512
		$this->assertEquals('ACD',
513
			$this->render('A<% if NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
514
		$this->assertEquals('AD',
515
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% end_if %>D'));
516
		$this->assertEquals('ADE',
517
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
518
519
		$this->assertEquals('ADE',
520
			$this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E'));
521
522
		// Dot syntax
523
		$this->assertEquals('ACD',
524
			$this->render('A<% if Foo.NotSet %>B<% else_if Foo.IsSet %>C<% end_if %>D'));
525
		$this->assertEquals('ACD',
526
			$this->render('A<% if Foo.Bar.NotSet %>B<% else_if Foo.Bar.IsSet %>C<% end_if %>D'));
527
528
		// Params
529
		$this->assertEquals('ACD',
530
			$this->render('A<% if NotSet(Param) %>B<% else %>C<% end_if %>D'));
531
		$this->assertEquals('ABD',
532
			$this->render('A<% if IsSet(Param) %>B<% else %>C<% end_if %>D'));
533
534
		// Negation
535
		$this->assertEquals('AC',
536
			$this->render('A<% if not IsSet %>B<% end_if %>C'));
537
		$this->assertEquals('ABC',
538
			$this->render('A<% if not NotSet %>B<% end_if %>C'));
539
540
		// Or
541
		$this->assertEquals('ABD',
542
			$this->render('A<% if IsSet || NotSet %>B<% else_if A %>C<% end_if %>D'));
543
		$this->assertEquals('ACD',
544
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet %>C<% end_if %>D'));
545
		$this->assertEquals('AD',
546
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet3 %>C<% end_if %>D'));
547
		$this->assertEquals('ACD',
548
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet || NotSet %>C<% end_if %>D'));
549
		$this->assertEquals('AD',
550
			$this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet2 || NotSet3 %>C<% end_if %>D'));
551
552
		// Negated Or
553
		$this->assertEquals('ACD',
554
			$this->render('A<% if not IsSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
555
		$this->assertEquals('ABD',
556
			$this->render('A<% if not NotSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
557
		$this->assertEquals('ABD',
558
			$this->render('A<% if NotSet || not AlsoNotSet %>B<% else_if A %>C<% end_if %>D'));
559
560
		// And
561
		$this->assertEquals('ABD',
562
			$this->render('A<% if IsSet && AlsoSet %>B<% else_if A %>C<% end_if %>D'));
563
		$this->assertEquals('ACD',
564
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet %>C<% end_if %>D'));
565
		$this->assertEquals('AD',
566
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if NotSet3 %>C<% end_if %>D'));
567
		$this->assertEquals('ACD',
568
			$this->render('A<% if IsSet && NotSet %>B<% else_if IsSet && AlsoSet %>C<% end_if %>D'));
569
		$this->assertEquals('AD',
570
			$this->render('A<% if NotSet && NotSet2 %>B<% else_if IsSet && NotSet3 %>C<% end_if %>D'));
571
572
		// Equality
573
		$this->assertEquals('ABC',
574
			$this->render('A<% if RawVal == RawVal %>B<% end_if %>C'));
575
		$this->assertEquals('ACD',
576
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal == RawVal %>C<% end_if %>D'));
577
		$this->assertEquals('ABC',
578
			$this->render('A<% if Right != Wrong %>B<% end_if %>C'));
579
		$this->assertEquals('AD',
580
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D'));
581
582
		// test inequalities with simple numbers
583
		$this->assertEquals('ABD', $this->render('A<% if 5 > 3 %>B<% else %>C<% end_if %>D'));
584
		$this->assertEquals('ABD', $this->render('A<% if 5 >= 3 %>B<% else %>C<% end_if %>D'));
585
		$this->assertEquals('ACD', $this->render('A<% if 3 > 5 %>B<% else %>C<% end_if %>D'));
586
		$this->assertEquals('ACD', $this->render('A<% if 3 >= 5 %>B<% else %>C<% end_if %>D'));
587
588
		$this->assertEquals('ABD', $this->render('A<% if 3 < 5 %>B<% else %>C<% end_if %>D'));
589
		$this->assertEquals('ABD', $this->render('A<% if 3 <= 5 %>B<% else %>C<% end_if %>D'));
590
		$this->assertEquals('ACD', $this->render('A<% if 5 < 3 %>B<% else %>C<% end_if %>D'));
591
		$this->assertEquals('ACD', $this->render('A<% if 5 <= 3 %>B<% else %>C<% end_if %>D'));
592
593
		$this->assertEquals('ABD', $this->render('A<% if 4 <= 4 %>B<% else %>C<% end_if %>D'));
594
		$this->assertEquals('ABD', $this->render('A<% if 4 >= 4 %>B<% else %>C<% end_if %>D'));
595
		$this->assertEquals('ACD', $this->render('A<% if 4 > 4 %>B<% else %>C<% end_if %>D'));
596
		$this->assertEquals('ACD', $this->render('A<% if 4 < 4 %>B<% else %>C<% end_if %>D'));
597
598
		// empty else_if and else tags, if this would not be supported,
599
		// the output would stop after A, thereby failing the assert
600
		$this->assertEquals('AD', $this->render('A<% if IsSet %><% else %><% end_if %>D'));
601
		$this->assertEquals('AD',
602
			$this->render('A<% if NotSet %><% else_if IsSet %><% else %><% end_if %>D'));
603
		$this->assertEquals('AD',
604
			$this->render('A<% if NotSet %><% else_if AlsoNotSet %><% else %><% end_if %>D'));
605
606
		// Bare words with ending space
607
		$this->assertEquals('ABC',
608
			$this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C'));
609
610
		// Else
611
		$this->assertEquals('ADE',
612
			$this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E'));
613
614
		// Empty if with else
615
		$this->assertEquals('ABC',
616
			$this->render('A<% if NotSet %><% else %>B<% end_if %>C'));
617
	}
618
619
	public function testBaseTagGeneration() {
620
		// XHTML wil have a closed base tag
621
		$tmpl1 = '<?xml version="1.0" encoding="UTF-8"?>
622
			<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
623
				. ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
624
			<html>
625
				<head><% base_tag %></head>
626
				<body><p>test</p><body>
627
			</html>';
628
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $this->render($tmpl1));
629
630
		// HTML4 and 5 will only have it for IE
631
		$tmpl2 = '<!DOCTYPE html>
632
			<html>
633
				<head><% base_tag %></head>
634
				<body><p>test</p><body>
635
			</html>';
636
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
637
			$this->render($tmpl2));
638
639
640
		$tmpl3 = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
641
			<html>
642
				<head><% base_tag %></head>
643
				<body><p>test</p><body>
644
			</html>';
645
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
646
			$this->render($tmpl3));
647
648
		// Check that the content negotiator converts to the equally legal formats
649
		$negotiator = new ContentNegotiator();
650
651
		$response = new SS_HTTPResponse($this->render($tmpl1));
652
		$negotiator->html($response);
653
		$this->assertRegExp('/<head><base href=".*"><!--\[if lte IE 6\]><\/base><!\[endif\]--><\/head>/',
654
			$response->getBody());
655
656
		$response = new SS_HTTPResponse($this->render($tmpl1));
657
		$negotiator->xhtml($response);
658
		$this->assertRegExp('/<head><base href=".*" \/><\/head>/', $response->getBody());
659
	}
660
661
	public function testIncludeWithArguments() {
662
		$this->assertEquals(
663
			$this->render('<% include SSViewerTestIncludeWithArguments %>'),
664
			'<p>[out:Arg1]</p><p>[out:Arg2]</p>'
665
		);
666
667
		$this->assertEquals(
668
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A %>'),
669
			'<p>A</p><p>[out:Arg2]</p>'
670
		);
671
672
		$this->assertEquals(
673
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A, Arg2=B %>'),
674
			'<p>A</p><p>B</p>'
675
		);
676
677
		$this->assertEquals(
678
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>'),
679
			'<p>A Bare String</p><p>B Bare String</p>'
680
		);
681
682
		$this->assertEquals(
683
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A", Arg2=$B %>',
684
				new ArrayData(array('B' => 'Bar'))),
685
			'<p>A</p><p>Bar</p>'
686
		);
687
688
		$this->assertEquals(
689
			$this->render('<% include SSViewerTestIncludeWithArguments Arg1="A" %>',
690
				new ArrayData(array('Arg1' => 'Foo', 'Arg2' => 'Bar'))),
691
			'<p>A</p><p>Bar</p>'
692
		);
693
694
		$this->assertEquals(
695
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>',
696
				new ArrayData(array('Items' => new ArrayList(array(
697
					new ArrayData(array('Title' => 'Foo')),
698
					new ArrayData(array('Title' => 'Bar'))
699
				))))),
700
			'SomeArg - Foo - Bar - SomeArg'
701
		);
702
703
		$this->assertEquals(
704
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInWith Title="A" %>',
705
				new ArrayData(array('Item' => new ArrayData(array('Title' =>'B'))))),
706
			'A - B - A'
707
		);
708
709
		$this->assertEquals(
710
			$this->render('<% include SSViewerTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>',
711
				new ArrayData(array(
712
					'Item' => new ArrayData(array(
713
						'Title' =>'B', 'NestedItem' => new ArrayData(array('Title' => 'C'))
714
					)))
715
				)),
716
			'A - B - C - B - A'
717
		);
718
719
		$data = new ArrayData(array(
720
			'Nested' => new ArrayData(array(
721
				'Object' => new ArrayData(array('Key' => 'A'))
722
			)),
723
			'Object' => new ArrayData(array('Key' => 'B'))
724
		));
725
726
		$tmpl = SSViewer::fromString('<% include SSViewerTestIncludeObjectArguments A=$Nested.Object, B=$Object %>');
727
		$res  = $tmpl->process($data);
728
		$this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments');
0 ignored issues
show
Unused Code introduced by
The call to SSViewerTest::assertEqualIgnoringWhitespace() has too many arguments starting with 'Objects can be passed as named arguments'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
729
	}
730
731
732
	public function testRecursiveInclude() {
733
		$view = new SSViewer(array('SSViewerTestRecursiveInclude'));
734
735
		$data = new ArrayData(array(
736
			'Title' => 'A',
737
			'Children' => new ArrayList(array(
738
				new ArrayData(array(
739
					'Title' => 'A1',
740
					'Children' => new ArrayList(array(
741
						new ArrayData(array( 'Title' => 'A1 i', )),
742
						new ArrayData(array( 'Title' => 'A1 ii', )),
743
					)),
744
				)),
745
				new ArrayData(array( 'Title' => 'A2', )),
746
				new ArrayData(array( 'Title' => 'A3', )),
747
			)),
748
		));
749
750
		$result = $view->process($data);
751
		// We don't care about whitespace
752
		$rationalisedResult = trim(preg_replace('/\s+/', ' ', $result));
753
754
		$this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult);
755
	}
756
757
	public function assertEqualIgnoringWhitespace($a, $b) {
758
		$this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b));
759
	}
760
761
	/**
762
	 * See {@link ViewableDataTest} for more extensive casting tests,
763
	 * this test just ensures that basic casting is correctly applied during template parsing.
764
	 */
765
	public function testCastingHelpers() {
766
		$vd = new SSViewerTest_ViewableData();
767
		$vd->TextValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property TextValue does not exist on object<SSViewerTest_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...
768
		$vd->HTMLValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property HTMLValue does not exist on object<SSViewerTest_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...
769
		$vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SSViewerTest_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...
770
771
		// Value casted as "Text"
772
		$this->assertEquals(
773
			'&lt;b&gt;html&lt;/b&gt;',
774
			$t = SSViewer::fromString('$TextValue')->process($vd)
775
		);
776
		$this->assertEquals(
777
			'<b>html</b>',
778
			$t = SSViewer::fromString('$TextValue.RAW')->process($vd)
779
		);
780
		$this->assertEquals(
781
			'&lt;b&gt;html&lt;/b&gt;',
782
			$t = SSViewer::fromString('$TextValue.XML')->process($vd)
783
		);
784
785
		// Value casted as "HTMLText"
786
		$this->assertEquals(
787
			'<b>html</b>',
788
			$t = SSViewer::fromString('$HTMLValue')->process($vd)
789
		);
790
		$this->assertEquals(
791
			'<b>html</b>',
792
			$t = SSViewer::fromString('$HTMLValue.RAW')->process($vd)
793
		);
794
		$this->assertEquals(
795
			'&lt;b&gt;html&lt;/b&gt;',
796
			$t = SSViewer::fromString('$HTMLValue.XML')->process($vd)
797
		);
798
799
		// Uncasted value (falls back to ViewableData::$default_cast="HTMLText")
800
		$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
801
		$vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SSViewerTest_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...
802
		$this->assertEquals(
803
			'<b>html</b>',
804
			$t = SSViewer::fromString('$UncastedValue')->process($vd)
805
		);
806
		$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
807
		$vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SSViewerTest_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...
808
		$this->assertEquals(
809
			'<b>html</b>',
810
			$t = SSViewer::fromString('$UncastedValue.RAW')->process($vd)
811
		);
812
		$vd = new SSViewerTest_ViewableData(); // TODO Fix caching
813
		$vd->UncastedValue = '<b>html</b>';
0 ignored issues
show
Documentation introduced by
The property UncastedValue does not exist on object<SSViewerTest_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...
814
		$this->assertEquals(
815
			'&lt;b&gt;html&lt;/b&gt;',
816
			$t = SSViewer::fromString('$UncastedValue.XML')->process($vd)
817
		);
818
	}
819
820
	public function testSSViewerBasicIteratorSupport() {
821
		$data = new ArrayData(array(
822
			'Set' => new ArrayList(array(
823
				new SSViewerTest_Object("1"),
824
				new SSViewerTest_Object("2"),
825
				new SSViewerTest_Object("3"),
826
				new SSViewerTest_Object("4"),
827
				new SSViewerTest_Object("5"),
828
				new SSViewerTest_Object("6"),
829
				new SSViewerTest_Object("7"),
830
				new SSViewerTest_Object("8"),
831
				new SSViewerTest_Object("9"),
832
				new SSViewerTest_Object("10"),
833
			))
834
		));
835
836
		//base test
837
		$result = $this->render('<% loop Set %>$Number<% end_loop %>',$data);
838
		$this->assertEquals("12345678910",$result,"Numbers rendered in order");
839
840
		//test First
841
		$result = $this->render('<% loop Set %><% if First %>$Number<% end_if %><% end_loop %>',$data);
842
		$this->assertEquals("1",$result,"Only the first number is rendered");
843
844
		//test Last
845
		$result = $this->render('<% loop Set %><% if Last %>$Number<% end_if %><% end_loop %>',$data);
846
		$this->assertEquals("10",$result,"Only the last number is rendered");
847
848
		//test Even
849
		$result = $this->render('<% loop Set %><% if Even() %>$Number<% end_if %><% end_loop %>',$data);
850
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
851
852
		//test Even with quotes
853
		$result = $this->render('<% loop Set %><% if Even("1") %>$Number<% end_if %><% end_loop %>',$data);
854
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
855
856
		//test Even without quotes
857
		$result = $this->render('<% loop Set %><% if Even(1) %>$Number<% end_if %><% end_loop %>',$data);
858
		$this->assertEquals("246810",$result,"Even numbers rendered in order");
859
860
		//test Even with zero-based start index
861
		$result = $this->render('<% loop Set %><% if Even("0") %>$Number<% end_if %><% end_loop %>',$data);
862
		$this->assertEquals("13579",$result,"Even (with zero-based index) numbers rendered in order");
863
864
		//test Odd
865
		$result = $this->render('<% loop Set %><% if Odd %>$Number<% end_if %><% end_loop %>',$data);
866
		$this->assertEquals("13579",$result,"Odd numbers rendered in order");
867
868
		//test FirstLast
869
		$result = $this->render('<% loop Set %><% if FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>',$data);
870
		$this->assertEquals("1first10last",$result,"First and last numbers rendered in order");
871
872
		//test Middle
873
		$result = $this->render('<% loop Set %><% if Middle %>$Number<% end_if %><% end_loop %>',$data);
874
		$this->assertEquals("23456789",$result,"Middle numbers rendered in order");
875
876
		//test MiddleString
877
		$result = $this->render('<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %>'
878
			. '<% end_loop %>',$data);
879
		$this->assertEquals("2middle3middle4middle5middle6middle7middle8middle9middle",$result,
880
			"Middle numbers rendered in order");
881
882
		//test EvenOdd
883
		$result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>',$data);
884
		$this->assertEquals("oddevenoddevenoddevenoddevenoddeven",$result,
885
			"Even and Odd is returned in sequence numbers rendered in order");
886
887
		//test Pos
888
		$result = $this->render('<% loop Set %>$Pos<% end_loop %>',$data);
889
		$this->assertEquals("12345678910", $result, '$Pos is rendered in order');
890
891
		//test Pos
892
		$result = $this->render('<% loop Set %>$Pos(0)<% end_loop %>',$data);
893
		$this->assertEquals("0123456789", $result, '$Pos(0) is rendered in order');
894
895
		//test FromEnd
896
		$result = $this->render('<% loop Set %>$FromEnd<% end_loop %>',$data);
897
		$this->assertEquals("10987654321", $result, '$FromEnd is rendered in order');
898
899
		//test FromEnd
900
		$result = $this->render('<% loop Set %>$FromEnd(0)<% end_loop %>',$data);
901
		$this->assertEquals("9876543210", $result, '$FromEnd(0) rendered in order');
902
903
		//test Total
904
		$result = $this->render('<% loop Set %>$TotalItems<% end_loop %>',$data);
905
		$this->assertEquals("10101010101010101010",$result,"10 total items X 10 are returned");
906
907
		//test Modulus
908
		$result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>',$data);
909
		$this->assertEquals("1010101010",$result,"1-indexed pos modular divided by 2 rendered in order");
910
911
		//test MultipleOf 3
912
		$result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>',$data);
913
		$this->assertEquals("369",$result,"Only numbers that are multiples of 3 are returned");
914
915
		//test MultipleOf 4
916
		$result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>',$data);
917
		$this->assertEquals("48",$result,"Only numbers that are multiples of 4 are returned");
918
919
		//test MultipleOf 5
920
		$result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>',$data);
921
		$this->assertEquals("510",$result,"Only numbers that are multiples of 5 are returned");
922
923
		//test MultipleOf 10
924
		$result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>',$data);
925
		$this->assertEquals("10",$result,"Only numbers that are multiples of 10 (with 1-based indexing) are returned");
926
927
		//test MultipleOf 9 zero-based
928
		$result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>',$data);
929
		$this->assertEquals("110",$result,
930
			"Only numbers that are multiples of 9 with zero-based indexing are returned. (The first and last item)");
931
932
		//test MultipleOf 11
933
		$result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>',$data);
934
		$this->assertEquals("",$result,"Only numbers that are multiples of 11 are returned. I.e. nothing returned");
935
	}
936
937
	/**
938
	 * Test $Up works when the scope $Up refers to was entered with a "with" block
939
	 */
940
	public function testUpInWith() {
941
942
		// Data to run the loop tests on - three levels deep
943
		$data = new ArrayData(array(
944
			'Name' => 'Top',
945
			'Foo' => new ArrayData(array(
946
				'Name' => 'Foo',
947
				'Bar' => new ArrayData(array(
948
					'Name' => 'Bar',
949
					'Baz' => new ArrayData(array(
950
						'Name' => 'Baz'
951
					)),
952
					'Qux' => new ArrayData(array(
953
						'Name' => 'Qux'
954
					))
955
				))
956
			))
957
		));
958
959
		// Basic functionality
960
		$this->assertEquals('BarFoo',
961
			$this->render('<% with Foo %><% with Bar %>{$Name}{$Up.Name}<% end_with %><% end_with %>', $data));
962
963
		// Two level with block, up refers to internally referenced Bar
964
		$this->assertEquals('BarFoo',
965
			$this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data));
966
967
		// Stepping up & back down the scope tree
968
		$this->assertEquals('BazBarQux',
969
			$this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Name}{$Up.Qux.Name}<% end_with %>', $data));
970
971
		// Using $Up in a with block
972
		$this->assertEquals('BazBarQux',
973
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Qux.Name}<% end_with %>'
974
				.'<% end_with %>', $data));
975
976
		// Stepping up & back down the scope tree with with blocks
977
		$this->assertEquals('BazBarQuxBarBaz',
978
			$this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Qux %>{$Name}<% end_with %>'
979
				. '{$Name}<% end_with %>{$Name}<% end_with %>', $data));
980
981
		// Using $Up.Up, where first $Up points to a previous scope entered using $Up, thereby skipping up to Foo
982
		$this->assertEquals('Foo',
983
			$this->render('<% with Foo.Bar.Baz %><% with Up %><% with Qux %>{$Up.Up.Name}<% end_with %><% end_with %>'
984
				. '<% end_with %>', $data));
985
986
		// Using $Up.Up, where first $Up points to an Up used in a local scope lookup, should still skip to Foo
987
		$this->assertEquals('Foo',
988
			$this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Up.Name}<% end_with %>', $data));
989
	}
990
991
	/**
992
	 * Test $Up works when the scope $Up refers to was entered with a "loop" block
993
	 */
994
	public function testUpInLoop(){
995
996
		// Data to run the loop tests on - one sequence of three items, each with a subitem
997
		$data = new ArrayData(array(
998
			'Name' => 'Top',
999
			'Foo' => new ArrayList(array(
1000
				new ArrayData(array(
1001
					'Name' => '1',
1002
					'Sub' => new ArrayData(array(
1003
						'Name' => 'Bar'
1004
					))
1005
				)),
1006
				new ArrayData(array(
1007
					'Name' => '2',
1008
					'Sub' => new ArrayData(array(
1009
						'Name' => 'Baz'
1010
					))
1011
				)),
1012
				new ArrayData(array(
1013
					'Name' => '3',
1014
					'Sub' => new ArrayData(array(
1015
						'Name' => 'Qux'
1016
					))
1017
				))
1018
			))
1019
		));
1020
1021
		// Make sure inside a loop, $Up refers to the current item of the loop
1022
		$this->assertEqualIgnoringWhitespace(
1023
			'111 222 333',
1024
			$this->render(
1025
				'<% loop $Foo %>$Name<% with $Sub %>$Up.Name<% end_with %>$Name<% end_loop %>',
1026
				$data
1027
			)
1028
		);
1029
1030
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1031
		// and doesn't interfere with the original iterator
1032
		$this->assertEqualIgnoringWhitespace(
1033
			'1Bar123Bar1 2Baz123Baz2 3Qux123Qux3',
1034
			$this->render(
1035
				'<% loop $Foo %>
1036
					$Name
1037
					<% with $Sub %>
1038
						$Name
1039
						<% loop $Up %>$Name<% end_loop %>
1040
						$Name
1041
					<% end_with %>
1042
					$Name
1043
				<% end_loop %>',
1044
				$data
1045
			)
1046
		);
1047
1048
		// Make sure inside a loop, looping over $Up uses a separate iterator,
1049
		// and doesn't interfere with the original iterator or local lookups
1050
		$this->assertEqualIgnoringWhitespace(
1051
			'1 Bar1 123 1Bar 1   2 Baz2 123 2Baz 2   3 Qux3 123 3Qux 3',
1052
			$this->render(
1053
				'<% loop $Foo %>
1054
					$Name
1055
					<% with $Sub %>
1056
						{$Name}{$Up.Name}
1057
						<% loop $Up %>$Name<% end_loop %>
1058
						{$Up.Name}{$Name}
1059
					<% end_with %>
1060
					$Name
1061
				<% end_loop %>',
1062
				$data
1063
			)
1064
		);
1065
	}
1066
1067
	/**
1068
	 * Test that nested loops restore the loop variables correctly when pushing and popping states
1069
	 */
1070
	public function testNestedLoops(){
1071
1072
		// Data to run the loop tests on - one sequence of three items, one with child elements
1073
		// (of a different size to the main sequence)
1074
		$data = new ArrayData(array(
1075
			'Foo' => new ArrayList(array(
1076
				new ArrayData(array(
1077
					'Name' => '1',
1078
					'Children' => new ArrayList(array(
1079
						new ArrayData(array(
1080
							'Name' => 'a'
1081
						)),
1082
						new ArrayData(array(
1083
							'Name' => 'b'
1084
						)),
1085
					)),
1086
				)),
1087
				new ArrayData(array(
1088
					'Name' => '2',
1089
					'Children' => new ArrayList(),
1090
				)),
1091
				new ArrayData(array(
1092
					'Name' => '3',
1093
					'Children' => new ArrayList(),
1094
				)),
1095
			)),
1096
		));
1097
1098
		// Make sure that including a loop inside a loop will not destroy the internal count of
1099
		// items, checked by using "Last"
1100
		$this->assertEqualIgnoringWhitespace(
1101
			'1ab23last',
1102
			$this->render('<% loop $Foo %>$Name<% loop Children %>$Name<% end_loop %><% if Last %>last<% end_if %>'
1103
				. '<% end_loop %>', $data
1104
			)
1105
		);
1106
	}
1107
1108
	public function testLayout() {
1109
		$self = $this;
1110
1111
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1112
			$template = new SSViewer(array('Page'));
1113
			$self->assertEquals("Foo\n\n", $template->process(new ArrayData(array())));
1114
1115
			$template = new SSViewer(array('Shortcodes', 'Page'));
1116
			$self->assertEquals("[file_link]\n\n", $template->process(new ArrayData(array())));
1117
		});
1118
	}
1119
1120
	/**
1121
	 * @covers SSViewer::get_templates_by_class()
1122
	 */
1123
	public function testGetTemplatesByClass() {
1124
		$self = $this;
1125
		$this->useTestTheme(dirname(__FILE__), 'layouttest', function() use ($self) {
1126
			// Test passing a string
1127
			$templates = SSViewer::get_templates_by_class(
1128
				'TestNamespace\SSViewerTest_Controller',
1129
				'',
1130
				'Controller'
1131
			);
1132
			$self->assertEquals([
1133
				'TestNamespace\SSViewerTest_Controller',
1134
    			'Controller',
1135
			], $templates);
1136
1137
			// Test to ensure we're stopping at the base class.
1138
			$templates = SSViewer::get_templates_by_class('TestNamespace\SSViewerTest_Controller', '', 'TestNamespace\SSViewerTest_Controller');
1139
			$self->assertCount(1, $templates);
1140
1141
			// Make sure we can filter our templates by suffix.
1142
			$templates = SSViewer::get_templates_by_class('SSViewerTest', '_Controller');
1143
			$self->assertCount(1, $templates);
1144
1145
			// Let's throw something random in there.
1146
			$self->setExpectedException('InvalidArgumentException');
1147
			$templates = SSViewer::get_templates_by_class(array());
0 ignored issues
show
Unused Code introduced by
$templates is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1148
		});
1149
	}
1150
1151
	/**
1152
	 * @covers SSViewer::get_themes()
1153
	 */
1154
	public function testThemeRetrieval() {
1155
		$ds = DIRECTORY_SEPARATOR;
1156
		$testThemeBaseDir = TEMP_FOLDER . $ds . 'test-themes';
1157
1158
		if(file_exists($testThemeBaseDir)) Filesystem::removeFolder($testThemeBaseDir);
1159
1160
		mkdir($testThemeBaseDir);
1161
		mkdir($testThemeBaseDir . $ds . 'blackcandy');
1162
		mkdir($testThemeBaseDir . $ds . 'blackcandy_blog');
1163
		mkdir($testThemeBaseDir . $ds . 'darkshades');
1164
		mkdir($testThemeBaseDir . $ds . 'darkshades_blog');
1165
1166
		$this->assertEquals(array(
1167
			'blackcandy' => 'blackcandy',
1168
			'darkshades' => 'darkshades'
1169
		), SSViewer::get_themes($testThemeBaseDir), 'Our test theme directory contains 2 themes');
1170
1171
		$this->assertEquals(array(
1172
			'blackcandy' => 'blackcandy',
1173
			'blackcandy_blog' => 'blackcandy_blog',
1174
			'darkshades' => 'darkshades',
1175
			'darkshades_blog' => 'darkshades_blog'
1176
		), SSViewer::get_themes($testThemeBaseDir, true),
1177
			'Our test theme directory contains 2 themes and 2 sub-themes');
1178
1179
		// Remove all the test themes we created
1180
		Filesystem::removeFolder($testThemeBaseDir);
1181
	}
1182
1183
	public function testRewriteHashlinks() {
1184
		$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1185
		Config::inst()->update('SSViewer', 'rewrite_hash_links', true);
1186
1187
		$_SERVER['HTTP_HOST'] = 'www.mysite.com';
1188
		$_SERVER['REQUEST_URI'] = '//file.com?foo"onclick="alert(\'xss\')""';
1189
1190
		// Emulate SSViewer::process()
1191
		// Note that leading double slashes have been rewritten to prevent these being mis-interepreted
1192
		// as protocol-less absolute urls
1193
		$base = Convert::raw2att('/file.com?foo"onclick="alert(\'xss\')""');
1194
1195
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinks_' . sha1(rand()) . '.ss';
1196
1197
		// Note: SSViewer_FromString doesn't rewrite hash links.
1198
		file_put_contents($tmplFile, '<!DOCTYPE html>
1199
			<html>
1200
				<head><% base_tag %></head>
1201
				<body>
1202
				<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>
1203
				$ExternalInsertedLink
1204
				<a class="inline" href="#anchor">InlineLink</a>
1205
				$InsertedLink
1206
				<svg><use xlink:href="#sprite"></use></svg>
1207
				<body>
1208
			</html>');
1209
		$tmpl = new SSViewer($tmplFile);
1210
		$obj = new ViewableData();
1211
		$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
0 ignored issues
show
Documentation introduced by
The property InsertedLink 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...
1212
		$obj->ExternalInsertedLink = '<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>';
0 ignored issues
show
Documentation introduced by
The property ExternalInsertedLink 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...
1213
		$result = $tmpl->process($obj);
1214
		$this->assertContains(
1215
			'<a class="inserted" href="' . $base . '#anchor">InsertedLink</a>',
1216
			$result
1217
		);
1218
		$this->assertContains(
1219
			'<a class="external-inserted" href="http://google.com#anchor">ExternalInsertedLink</a>',
1220
			$result
1221
		);
1222
		$this->assertContains(
1223
			'<a class="inline" href="' . $base . '#anchor">InlineLink</a>',
1224
			$result
1225
		);
1226
		$this->assertContains(
1227
			'<a class="external-inline" href="http://google.com#anchor">ExternalInlineLink</a>',
1228
			$result
1229
		);
1230
		$this->assertContains(
1231
			'<svg><use xlink:href="#sprite"></use></svg>',
1232
			$result,
1233
			'SSTemplateParser should only rewrite anchor hrefs'
1234
		);
1235
1236
		unlink($tmplFile);
1237
1238
		Config::inst()->update('SSViewer', 'rewrite_hash_links', $orig);
1239
	}
1240
1241
	public function testRewriteHashlinksInPhpMode() {
1242
		$orig = Config::inst()->get('SSViewer', 'rewrite_hash_links');
1243
		Config::inst()->update('SSViewer', 'rewrite_hash_links', 'php');
1244
1245
		$tmplFile = TEMP_FOLDER . '/SSViewerTest_testRewriteHashlinksInPhpMode_' . sha1(rand()) . '.ss';
1246
1247
		// Note: SSViewer_FromString doesn't rewrite hash links.
1248
		file_put_contents($tmplFile, '<!DOCTYPE html>
1249
			<html>
1250
				<head><% base_tag %></head>
1251
				<body>
1252
				<a class="inline" href="#anchor">InlineLink</a>
1253
				$InsertedLink
1254
				<svg><use xlink:href="#sprite"></use></svg>
1255
				<body>
1256
			</html>');
1257
		$tmpl = new SSViewer($tmplFile);
1258
		$obj = new ViewableData();
1259
		$obj->InsertedLink = '<a class="inserted" href="#anchor">InsertedLink</a>';
0 ignored issues
show
Documentation introduced by
The property InsertedLink 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...
1260
		$result = $tmpl->process($obj);
1261
1262
		$code = <<<'EOC'
1263
<a class="inserted" href="<?php echo Convert::raw2att(preg_replace("/^(\/)+/", "/", $_SERVER['REQUEST_URI'])); ?>#anchor">InsertedLink</a>
1264
EOC;
1265
		$this->assertContains($code, $result);
1266
		// TODO Fix inline links in PHP mode
1267
		// $this->assertContains(
1268
		// 	'<a class="inline" href="<?php echo str_replace(',
1269
		// 	$result
1270
		// );
1271
		$this->assertContains(
1272
			'<svg><use xlink:href="#sprite"></use></svg>',
1273
			$result,
1274
			'SSTemplateParser should only rewrite anchor hrefs'
1275
		);
1276
1277
		unlink($tmplFile);
1278
1279
		Config::inst()->update('SSViewer', 'rewrite_hash_links', $orig);
1280
	}
1281
1282
	public function testRenderWithSourceFileComments() {
1283
		$origEnv = Config::inst()->get('Director', 'environment_type');
1284
		Config::inst()->update('Director', 'environment_type', 'dev');
1285
		Config::inst()->update('SSViewer', 'source_file_comments', true);
1286
		$f = FRAMEWORK_PATH . '/tests/templates/SSViewerTestComments';
1287
		$templates = array(
1288
			array(
1289
				'name' => 'SSViewerTestCommentsFullSource',
1290
				'expected' => ""
1291
					. "<!doctype html>"
1292
					. "<!-- template $f/SSViewerTestCommentsFullSource.ss -->"
1293
					. "<html>"
1294
					. "\t<head></head>"
1295
					. "\t<body></body>"
1296
					. "</html>"
1297
					. "<!-- end template $f/SSViewerTestCommentsFullSource.ss -->",
1298
			),
1299
			array(
1300
				'name' => 'SSViewerTestCommentsFullSourceHTML4Doctype',
1301
				'expected' => ""
1302
					. "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML "
1303
					. "4.01//EN\"\t\t\"http://www.w3.org/TR/html4/strict.dtd\">"
1304
					. "<!-- template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->"
1305
					. "<html>"
1306
					. "\t<head></head>"
1307
					. "\t<body></body>"
1308
					. "</html>"
1309
					. "<!-- end template $f/SSViewerTestCommentsFullSourceHTML4Doctype.ss -->",
1310
			),
1311
			array(
1312
				'name' => 'SSViewerTestCommentsFullSourceNoDoctype',
1313
				'expected' => ""
1314
					. "<html><!-- template $f/SSViewerTestCommentsFullSourceNoDoctype.ss -->"
1315
					. "\t<head></head>"
1316
					. "\t<body></body>"
1317
					. "<!-- end template $f/SSViewerTestCommentsFullSourceNoDoctype.ss --></html>",
1318
			),
1319
			array(
1320
				'name' => 'SSViewerTestCommentsFullSourceIfIE',
1321
				'expected' => ""
1322
					. "<!doctype html>"
1323
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIE.ss -->"
1324
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1325
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1326
					. "<!--[if !IE]><!--> <html class='no-ie'> <!--<![endif]-->"
1327
					. "\t<head></head>"
1328
					. "\t<body></body>"
1329
					. "</html>"
1330
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIE.ss -->",
1331
			),
1332
			array(
1333
				'name' => 'SSViewerTestCommentsFullSourceIfIENoDoctype',
1334
				'expected' => ""
1335
					. "<!--[if lte IE 8]> <html class='old-ie'> <![endif]-->"
1336
					. "<!--[if gt IE 8]> <html class='new-ie'> <![endif]-->"
1337
					. "<!--[if !IE]><!--> <html class='no-ie'>"
1338
					. "<!-- template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss -->"
1339
					. " <!--<![endif]-->"
1340
					. "\t<head></head>"
1341
					. "\t<body></body>"
1342
					. "<!-- end template $f/SSViewerTestCommentsFullSourceIfIENoDoctype.ss --></html>",
1343
			),
1344
			array(
1345
				'name' => 'SSViewerTestCommentsPartialSource',
1346
				'expected' =>
1347
				"<!-- template $f/SSViewerTestCommentsPartialSource.ss -->"
1348
					. "<div class='typography'></div>"
1349
					. "<!-- end template $f/SSViewerTestCommentsPartialSource.ss -->",
1350
			),
1351
			array(
1352
				'name' => 'SSViewerTestCommentsWithInclude',
1353
				'expected' =>
1354
				"<!-- template $f/SSViewerTestCommentsWithInclude.ss -->"
1355
					. "<div class='typography'>"
1356
					. "<!-- include 'SSViewerTestCommentsInclude' -->"
1357
					. "<!-- template $f/SSViewerTestCommentsInclude.ss -->"
1358
					. "Included"
1359
					. "<!-- end template $f/SSViewerTestCommentsInclude.ss -->"
1360
					. "<!-- end include 'SSViewerTestCommentsInclude' -->"
1361
					. "</div>"
1362
					. "<!-- end template $f/SSViewerTestCommentsWithInclude.ss -->",
1363
			),
1364
		);
1365
		foreach ($templates as $template) {
1366
			$this->_renderWithSourceFileComments($template['name'], $template['expected']);
1367
		}
1368
		Config::inst()->update('SSViewer', 'source_file_comments', false);
1369
		Config::inst()->update('Director', 'environment_type', $origEnv);
1370
	}
1371
	private function _renderWithSourceFileComments($name, $expected) {
1372
		$viewer = new SSViewer(array($name));
1373
		$data = new ArrayData(array());
1374
		$result = $viewer->process($data);
1375
		$expected = str_replace(array("\r", "\n"), '', $expected);
1376
		$result = str_replace(array("\r", "\n"), '', $result);
1377
		$this->assertEquals($result, $expected);
1378
	}
1379
1380
	public function testLoopIteratorIterator() {
1381
		$list = new PaginatedList(new ArrayList());
1382
		$viewer = new SSViewer_FromString('<% loop List %>$ID - $FirstName<br /><% end_loop %>');
1383
		$result = $viewer->process(new ArrayData(array('List' => $list)));
1384
		$this->assertEquals($result, '');
1385
	}
1386
1387
	public function testProcessOnlyIncludesRequirementsOnce() {
1388
		$template = new SSViewer(array('SSViewerTestProcess'));
1389
		$basePath = dirname($this->getCurrentRelativePath()) . '/forms';
1390
1391
		$backend = Injector::inst()->create('Requirements_Backend');
1392
		$backend->setCombinedFilesEnabled(false);
1393
		$backend->combineFiles(
1394
			'RequirementsTest_ab.css',
1395
			array(
1396
				$basePath . '/RequirementsTest_a.css',
1397
				$basePath . '/RequirementsTest_b.css'
1398
			)
1399
		);
1400
1401
		Requirements::set_backend($backend);
1402
1403
		$this->assertEquals(1, substr_count($template->process(array()), "a.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

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...
1404
		$this->assertEquals(1, substr_count($template->process(array()), "b.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

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...
1405
1406
		// if we disable the requirements then we should get nothing
1407
		$template->includeRequirements(false);
1408
		$this->assertEquals(0, substr_count($template->process(array()), "a.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

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...
1409
		$this->assertEquals(0, substr_count($template->process(array()), "b.css"));
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

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...
1410
	}
1411
1412
	public function testRequireCallInTemplateInclude() {
1413
		//TODO undo skip test on the event that templates ever obtain the ability to reference MODULE_DIR (or something to that effect)
1414
		if(FRAMEWORK_DIR === 'framework') {
1415
			$template = new SSViewer(array('SSViewerTestProcess'));
1416
1417
			Requirements::set_suffix_requirements(false);
1418
1419
			$this->assertEquals(1, substr_count(
1420
				$template->process(array()),
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<ViewableData>.

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...
1421
				"tests/forms/RequirementsTest_a.js"
1422
			));
1423
		}
1424
		else {
1425
			$this->markTestSkipped('Requirement will always fail if the framework dir is not '.
1426
				'named \'framework\', since templates require hard coded paths');
1427
		}
1428
	}
1429
1430
	public function testCallsWithArguments() {
1431
		$data = new ArrayData(array(
1432
			'Set' => new ArrayList(array(
1433
				new SSViewerTest_Object("1"),
1434
				new SSViewerTest_Object("2"),
1435
				new SSViewerTest_Object("3"),
1436
				new SSViewerTest_Object("4"),
1437
				new SSViewerTest_Object("5"),
1438
			)),
1439
			'Level' => new SSViewerTest_LevelTest(1),
1440
			'Nest' => array(
1441
				'Level' => new SSViewerTest_LevelTest(2),
1442
			),
1443
		));
1444
1445
		$tests = array(
1446
			'$Level.output(1)' => '1-1',
1447
			'$Nest.Level.output($Set.First.Number)' => '2-1',
1448
			'<% with $Set %>$Up.Level.output($First.Number)<% end_with %>' => '1-1',
1449
			'<% with $Set %>$Top.Nest.Level.output($First.Number)<% end_with %>' => '2-1',
1450
			'<% loop $Set %>$Up.Nest.Level.output($Number)<% end_loop %>' => '2-12-22-32-42-5',
1451
			'<% loop $Set %>$Top.Level.output($Number)<% end_loop %>' => '1-11-21-31-41-5',
1452
			'<% with $Nest %>$Level.output($Top.Set.First.Number)<% end_with %>' => '2-1',
1453
			'<% with $Level %>$output($Up.Set.Last.Number)<% end_with %>' => '1-5',
1454
			'<% with $Level.forWith($Set.Last.Number) %>$output("hi")<% end_with %>' => '5-hi',
1455
			'<% loop $Level.forLoop($Set.First.Number) %>$Number<% end_loop %>' => '!0',
1456
			'<% with $Nest %>
1457
				<% with $Level.forWith($Up.Set.First.Number) %>$output("hi")<% end_with %>
1458
			<% end_with %>' => '1-hi',
1459
			'<% with $Nest %>
1460
				<% loop $Level.forLoop($Top.Set.Last.Number) %>$Number<% end_loop %>
1461
			<% end_with %>' => '!0!1!2!3!4',
1462
		);
1463
1464
		foreach($tests as $template => $expected) {
1465
			$this->assertEquals($expected, trim($this->render($template, $data)));
1466
		}
1467
	}
1468
1469 View Code Duplication
	public function testClosedBlockExtension() {
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...
1470
		$count = 0;
1471
		$parser = new SSTemplateParser();
1472
		$parser->addClosedBlock(
1473
			'test',
1474
			function (&$res) use (&$count) {
0 ignored issues
show
Unused Code introduced by
The parameter $res 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...
1475
				$count++;
1476
			}
1477
		);
1478
1479
		$template = new SSViewer_FromString("<% test %><% end_test %>", $parser);
1480
		$template->process(new SSViewerTestFixture());
1481
1482
		$this->assertEquals(1, $count);
1483
	}
1484
1485 View Code Duplication
	public function testOpenBlockExtension() {
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...
1486
		$count = 0;
1487
		$parser = new SSTemplateParser();
1488
		$parser->addOpenBlock(
1489
			'test',
1490
			function (&$res) use (&$count) {
0 ignored issues
show
Unused Code introduced by
The parameter $res 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...
1491
				$count++;
1492
			}
1493
		);
1494
1495
		$template = new SSViewer_FromString("<% test %>", $parser);
1496
		$template->process(new SSViewerTestFixture());
1497
1498
		$this->assertEquals(1, $count);
1499
	}
1500
1501
	/**
1502
	 * Tests if caching for SSViewer_FromString is working
1503
	 */
1504
	public function testFromStringCaching() {
1505
		$content = 'Test content';
1506
		$cacheFile = TEMP_FOLDER . '/.cache.' . sha1($content);
1507
		if (file_exists($cacheFile)) {
1508
			unlink($cacheFile);
1509
		}
1510
1511
		// Test global behaviors
1512
		$this->render($content, null, null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

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...
1513
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1514
1515
		Config::inst()->update('SSViewer_FromString', 'cache_template', true);
1516
		$this->render($content, null, null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

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...
1517
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1518
		unlink($cacheFile);
1519
1520
		// Test instance behaviors
1521
		$this->render($content, null, false);
1522
		$this->assertFalse(file_exists($cacheFile), 'Cache file was created when caching was off');
1523
1524
		$this->render($content, null, true);
1525
		$this->assertTrue(file_exists($cacheFile), 'Cache file wasn\'t created when it was meant to');
1526
		unlink($cacheFile);
1527
	}
1528
}
1529
1530
/**
1531
 * A test fixture that will echo back the template item
1532
 */
1533
class SSViewerTestFixture extends ViewableData {
1534
	protected $name;
1535
1536
	public function __construct($name = null) {
1537
		$this->name = $name;
1538
		parent::__construct();
1539
	}
1540
1541
1542
	private function argedName($fieldName, $arguments) {
1543
		$childName = $this->name ? "$this->name.$fieldName" : $fieldName;
1544
		if($arguments) return $childName . '(' . implode(',', $arguments) . ')';
1545
		else return $childName;
1546
	}
1547
	public function obj($fieldName, $arguments=null, $forceReturnedObject=true, $cache=false, $cacheName=null) {
1548
		$childName = $this->argedName($fieldName, $arguments);
1549
1550
		// Special field name Loop### to create a list
1551
		if(preg_match('/^Loop([0-9]+)$/', $fieldName, $matches)) {
1552
			$output = new ArrayList();
1553
			for($i=0;$i<$matches[1];$i++) $output->push(new SSViewerTestFixture($childName));
1554
			return $output;
1555
1556
		} else if(preg_match('/NotSet/i', $fieldName)) {
1557
			return new ViewableData();
1558
1559
		} else {
1560
			return new SSViewerTestFixture($childName);
1561
		}
1562
	}
1563
1564
1565
	public function XML_val($fieldName, $arguments = null, $cache = false) {
1566
		if(preg_match('/NotSet/i', $fieldName)) {
1567
			return '';
1568
		} else if(preg_match('/Raw/i', $fieldName)) {
1569
			return $fieldName;
1570
		} else {
1571
			return '[out:' . $this->argedName($fieldName, $arguments) . ']';
1572
		}
1573
	}
1574
1575
	public function hasValue($fieldName, $arguments = null, $cache = true) {
1576
		return (bool)$this->XML_val($fieldName, $arguments);
1577
	}
1578
}
1579
1580
class SSViewerTest_ViewableData extends ViewableData implements TestOnly {
1581
1582
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
1583
		'TextValue' => 'Text',
1584
		'HTMLValue' => 'HTMLText'
1585
	);
1586
1587
	public function methodWithOneArgument($arg1) {
1588
		return "arg1:{$arg1}";
1589
	}
1590
1591
	public function methodWithTwoArguments($arg1, $arg2) {
1592
		return "arg1:{$arg1},arg2:{$arg2}";
1593
	}
1594
}
1595
1596
class SSViewerTest_Object extends DataObject implements TestOnly {
1597
1598
	public $number = null;
1599
1600
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
1601
		'Link' => 'Text',
1602
	);
1603
1604
1605
	public function __construct($number = null) {
1606
		parent::__construct();
1607
		$this->number = $number;
1608
	}
1609
1610
	public function Number() {
1611
		return $this->number;
1612
	}
1613
1614
	public function absoluteBaseURL() {
1615
		return "testLocalFunctionPriorityCalled";
1616
	}
1617
1618
	public function lotsOfArguments11($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k) {
1619
		return $a. $b. $c. $d. $e. $f. $g. $h. $i. $j. $k;
1620
	}
1621
1622
	public function Link() {
1623
		return 'some/url.html';
1624
	}
1625
}
1626
1627
class SSViewerTest_GlobalProvider implements TemplateGlobalProvider, TestOnly {
1628
1629
	public static function get_template_global_variables() {
1630
		return array(
1631
			'SSViewerTest_GlobalHTMLFragment' => array('method' => 'get_html', 'casting' => 'HTMLText'),
1632
			'SSViewerTest_GlobalHTMLEscaped' => array('method' => 'get_html'),
1633
1634
			'SSViewerTest_GlobalAutomatic',
1635
			'SSViewerTest_GlobalReferencedByString' => 'get_reference',
1636
			'SSViewerTest_GlobalReferencedInArray' => array('method' => 'get_reference'),
1637
1638
			'SSViewerTest_GlobalThatTakesArguments' => array('method' => 'get_argmix', 'casting' => 'HTMLText')
1639
1640
		);
1641
	}
1642
1643
	public static function get_html() {
1644
		return '<div></div>';
1645
	}
1646
1647
	public static function SSViewerTest_GlobalAutomatic() {
1648
		return 'automatic';
1649
	}
1650
1651
	public static function get_reference() {
1652
		return 'reference';
1653
	}
1654
1655
	public static function get_argmix() {
1656
		$args = func_get_args();
1657
		return 'z' . implode(':', $args) . 'z';
1658
	}
1659
1660
}
1661
1662
class SSViewerTest_LevelTest extends ViewableData implements TestOnly {
1663
	protected $depth;
1664
1665
	public function __construct($depth = 1) {
1666
		$this->depth = $depth;
1667
	}
1668
1669
	public function output($val) {
1670
		return "$this->depth-$val";
1671
	}
1672
1673
	public function forLoop($number) {
1674
		$ret = array();
1675
		for($i = 0; $i < (int)$number; ++$i) {
1676
			$ret[] = new SSViewerTest_Object("!$i");
1677
		}
1678
		return new ArrayList($ret);
1679
	}
1680
1681
	public function forWith($number) {
1682
		return new self($number);
1683
	}
1684
}
1685