Completed
Push — master ( 4bbfb7...b92347 )
by Daniel
12:57
created

SapphireTest::getCurrentAbsolutePath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use SilverStripe\CMS\Controllers\RootURLController;
6
use SilverStripe\CMS\Model\SiteTree;
7
use SilverStripe\Control\Cookie;
8
use SilverStripe\Control\Email\Email;
9
use SilverStripe\Control\Session;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Core\Config\Config;
13
use SilverStripe\Core\ClassInfo;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\Core\Manifest\ClassManifest;
16
use SilverStripe\Core\Manifest\ClassLoader;
17
use SilverStripe\Core\Manifest\ConfigStaticManifest;
18
use SilverStripe\i18n\i18n;
19
use SilverStripe\ORM\SS_List;
20
use SilverStripe\ORM\Versioning\Versioned;
21
use SilverStripe\ORM\DataObject;
22
use SilverStripe\ORM\Hierarchy\Hierarchy;
23
use SilverStripe\ORM\DataModel;
24
use SilverStripe\ORM\FieldType\DBDatetime;
25
use SilverStripe\ORM\FieldType\DBField;
26
use SilverStripe\ORM\DB;
27
use SilverStripe\Security\Member;
28
use SilverStripe\Security\Security;
29
use SilverStripe\Security\Group;
30
use SilverStripe\Security\Permission;
31
use SilverStripe\View\Requirements;
32
use SilverStripe\View\SSViewer;
33
use SilverStripe\View\ThemeResourceLoader;
34
use SilverStripe\View\ThemeManifest;
35
use PHPUnit_Framework_TestCase;
36
use Translatable;
37
use LogicException;
38
use Exception;
39
40
41
42
43
44
45
46
47
48
/**
49
 * Test case class for the Sapphire framework.
50
 * Sapphire unit testing is based on PHPUnit, but provides a number of hooks into our data model that make it easier
51
 * to work with.
52
 */
53
class SapphireTest extends PHPUnit_Framework_TestCase {
54
55
	/** @config */
56
	private static $dependencies = array(
57
		'fixtureFactory' => '%$FixtureFactory',
58
	);
59
60
	/**
61
	 * Path to fixture data for this test run.
62
	 * If passed as an array, multiple fixture files will be loaded.
63
	 * Please note that you won't be able to refer with "=>" notation
64
	 * between the fixtures, they act independent of each other.
65
	 *
66
	 * @var string|array
67
	 */
68
	protected static $fixture_file = null;
69
70
	/**
71
	 * @var FixtureFactory
72
	 */
73
	protected $fixtureFactory;
74
75
	/**
76
	 * @var Boolean If set to TRUE, this will force a test database to be generated
77
	 * in {@link setUp()}. Note that this flag is overruled by the presence of a
78
	 * {@link $fixture_file}, which always forces a database build.
79
	 */
80
	protected $usesDatabase = null;
81
	protected $originalMemberPasswordValidator;
82
	protected $originalRequirements;
83
	protected $originalIsRunningTest;
84
	protected $originalNestedURLsState;
85
	protected $originalMemoryLimit;
86
87
	/**
88
	 * @var TestMailer
89
	 */
90
	protected $mailer;
91
92
	/**
93
	 * Pointer to the manifest that isn't a test manifest
94
	 */
95
	protected static $regular_manifest;
96
97
	/**
98
	 * @var boolean
99
	 */
100
	protected static $is_running_test = false;
101
102
	/**
103
	 * @var ClassManifest
104
	 */
105
	protected static $test_class_manifest;
106
107
	/**
108
	 * By default, setUp() does not require default records. Pass
109
	 * class names in here, and the require/augment default records
110
	 * function will be called on them.
111
	 */
112
	protected $requireDefaultRecordsFrom = array();
113
114
115
	/**
116
	 * A list of extensions that can't be applied during the execution of this run.  If they are
117
	 * applied, they will be temporarily removed and a database migration called.
118
	 *
119
	 * The keys of the are the classes that the extensions can't be applied the extensions to, and
120
	 * the values are an array of illegal extensions on that class.
121
	 */
122
	protected $illegalExtensions = array(
123
	);
124
125
	/**
126
	 * A list of extensions that must be applied during the execution of this run.  If they are
127
	 * not applied, they will be temporarily added and a database migration called.
128
	 *
129
	 * The keys of the are the classes to apply the extensions to, and the values are an array
130
	 * of required extensions on that class.
131
	 *
132
	 * Example:
133
	 * <code>
134
	 * array("MyTreeDataObject" => array("Versioned", "Hierarchy"))
135
	 * </code>
136
	 */
137
	protected $requiredExtensions = array(
138
	);
139
140
	/**
141
	 * By default, the test database won't contain any DataObjects that have the interface TestOnly.
142
	 * This variable lets you define additional TestOnly DataObjects to set up for this test.
143
	 * Set it to an array of DataObject subclass names.
144
	 */
145
	protected $extraDataObjects = array();
146
147
	/**
148
	 * We need to disabling backing up of globals to avoid overriding
149
	 * the few globals SilverStripe relies on, like $lang for the i18n subsystem.
150
	 *
151
	 * @see http://sebastian-bergmann.de/archives/797-Global-Variables-and-PHPUnit.html
152
	 */
153
	protected $backupGlobals = FALSE;
154
155
	/**
156
	 * Helper arrays for illegalExtensions/requiredExtensions code
157
	 */
158
	private $extensionsToReapply = array(), $extensionsToRemove = array();
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
159
160
161
	/**
162
	 * Determines if unit tests are currently run, flag set during test bootstrap.
163
	 * This is used as a cheap replacement for fully mockable state
164
	 * in certain contiditions (e.g. access checks).
165
	 * Caution: When set to FALSE, certain controllers might bypass
166
	 * access checks, so this is a very security sensitive setting.
167
	 *
168
	 * @return boolean
169
	 */
170
	public static function is_running_test() {
171
		return self::$is_running_test;
172
	}
173
174
	public static function set_is_running_test($bool) {
175
		self::$is_running_test = $bool;
176
	}
177
178
	/**
179
	 * Set the manifest to be used to look up test classes by helper functions
180
	 *
181
	 * @param ClassManifest $manifest
182
	 */
183
	public static function set_test_class_manifest($manifest) {
184
		self::$test_class_manifest = $manifest;
185
	}
186
187
	/**
188
	 * Return the manifest being used to look up test classes by helper functions
189
	 *
190
	 * @return ClassManifest
191
	 */
192
	public static function get_test_class_manifest() {
193
		return self::$test_class_manifest;
194
	}
195
196
	/**
197
	 * @return String
198
	 */
199
	public static function get_fixture_file() {
200
		return static::$fixture_file;
201
	}
202
203
	protected $model;
204
205
	/**
206
	 * State of Versioned before this test is run
207
	 *
208
	 * @var string
209
	 */
210
	protected $originalReadingMode = null;
211
212
	public function setUp() {
213
214
		//nest config and injector for each test so they are effectively sandboxed per test
215
		Config::nest();
216
		Injector::nest();
217
218
		$this->originalReadingMode = Versioned::get_reading_mode();
219
220
		// We cannot run the tests on this abstract class.
221
		if(get_class($this) == __CLASS__) {
222
			$this->markTestSkipped(sprintf('Skipping %s ', get_class($this)));
223
			return;
224
		}
225
226
		// Mark test as being run
227
		$this->originalIsRunningTest = self::$is_running_test;
228
		self::$is_running_test = true;
229
230
		// i18n needs to be set to the defaults or tests fail
231
		i18n::set_locale(i18n::config()->get('default_locale'));
232
		i18n::config()->date_format = null;
0 ignored issues
show
Documentation introduced by
The property date_format does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
233
		i18n::config()->time_format = null;
0 ignored issues
show
Documentation introduced by
The property time_format does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
234
235
		// Set default timezone consistently to avoid NZ-specific dependencies
236
		date_default_timezone_set('UTC');
237
238
		// Remove password validation
239
		$this->originalMemberPasswordValidator = Member::password_validator();
240
		$this->originalRequirements = Requirements::backend();
241
		Member::set_password_validator(null);
242
		Cookie::config()->update('report_errors', false);
243
		if(class_exists('SilverStripe\\CMS\\Controllers\\RootURLController')) {
244
			RootURLController::reset();
245
		}
246
		if(class_exists('Translatable')) {
247
			Translatable::reset();
248
		}
249
		Versioned::reset();
250
		DataObject::reset();
251
		if(class_exists('SilverStripe\\CMS\\Model\\SiteTree')) {
252
			SiteTree::reset();
253
		}
254
		Hierarchy::reset();
255
		if(Controller::has_curr()) {
256
			Controller::curr()->setSession(Session::create(array()));
257
		}
258
		Security::$database_is_ready = null;
0 ignored issues
show
Documentation introduced by
The property $database_is_ready is declared private in SilverStripe\Security\Security. Since you implemented __set(), maybe consider adding a @property or @property-write annotation. This makes it easier for IDEs to provide auto-completion.

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...
259
260
		// Add controller-name auto-routing
261
		// @todo Fix to work with namespaced controllers
262
		Director::config()->update('rules', array(
263
			'$Controller//$Action/$ID/$OtherID' => '*'
264
		));
265
266
		$fixtureFiles = $this->getFixturePaths();
267
268
		// Todo: this could be a special test model
269
		$this->model = DataModel::inst();
270
271
		// Set up fixture
272
		if($fixtureFiles || $this->usesDatabase) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fixtureFiles 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...
273
			if (!self::using_temp_db()) {
274
				self::create_temp_db();
275
			}
276
277
			DataObject::singleton()->flushCache();
278
279
			self::empty_temp_db();
280
281
			foreach($this->requireDefaultRecordsFrom as $className) {
282
				$instance = singleton($className);
283
				if (method_exists($instance, 'requireDefaultRecords')) $instance->requireDefaultRecords();
284
				if (method_exists($instance, 'augmentDefaultRecords')) $instance->augmentDefaultRecords();
285
			}
286
287
			foreach($fixtureFiles as $fixtureFilePath) {
288
				$fixture = YamlFixture::create($fixtureFilePath);
289
				$fixture->writeInto($this->getFixtureFactory());
290
			}
291
292
			$this->logInWithPermission("ADMIN");
293
		}
294
295
		// Preserve memory settings
296
		$this->originalMemoryLimit = ini_get('memory_limit');
297
298
		// turn off template debugging
299
		SSViewer::config()->update('source_file_comments', false);
300
301
		// Clear requirements
302
		Requirements::clear();
303
304
		// Set up email
305
		$this->mailer = new TestMailer();
306
		Injector::inst()->registerService($this->mailer, 'SilverStripe\\Control\\Email\\Mailer');
307
		Email::config()->remove('send_all_emails_to');
308
	}
309
310
	/**
311
	 * Called once per test case ({@link SapphireTest} subclass).
312
	 * This is different to {@link setUp()}, which gets called once
313
	 * per method. Useful to initialize expensive operations which
314
	 * don't change state for any called method inside the test,
315
	 * e.g. dynamically adding an extension. See {@link tearDownOnce()}
316
	 * for tearing down the state again.
317
	 */
318
	public function setUpOnce() {
319
		//nest config and injector for each suite so they are effectively sandboxed
320
		Config::nest();
321
		Injector::nest();
322
		$isAltered = false;
323
324
		if(!Director::isDev()) {
325
			user_error('Tests can only run in "dev" mode', E_USER_ERROR);
326
		}
327
328
		// Remove any illegal extensions that are present
329
		foreach($this->illegalExtensions as $class => $extensions) {
330
			foreach($extensions as $extension) {
331
				if ($class::has_extension($extension)) {
332
					if(!isset($this->extensionsToReapply[$class])) $this->extensionsToReapply[$class] = array();
333
					$this->extensionsToReapply[$class][] = $extension;
334
					$class::remove_extension($extension);
335
					$isAltered = true;
336
				}
337
			}
338
		}
339
340
		// Add any required extensions that aren't present
341
		foreach($this->requiredExtensions as $class => $extensions) {
342
			$this->extensionsToRemove[$class] = array();
343
			foreach($extensions as $extension) {
344
				if(!$class::has_extension($extension)) {
345
					if(!isset($this->extensionsToRemove[$class])) $this->extensionsToReapply[$class] = array();
346
					$this->extensionsToRemove[$class][] = $extension;
347
					$class::add_extension($extension);
348
					$isAltered = true;
349
				}
350
			}
351
		}
352
353
		// If we have made changes to the extensions present, then migrate the database schema.
354
		if($isAltered || $this->extensionsToReapply || $this->extensionsToRemove || $this->extraDataObjects) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extensionsToReapply 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...
Bug Best Practice introduced by
The expression $this->extensionsToRemove 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...
Bug Best Practice introduced by
The expression $this->extraDataObjects 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...
355
			if(!self::using_temp_db()) {
356
				self::create_temp_db();
357
			}
358
			$this->resetDBSchema(true);
359
		}
360
		// clear singletons, they're caching old extension info
361
		// which is used in DatabaseAdmin->doBuild()
362
		Injector::inst()->unregisterAllObjects();
363
364
		// Set default timezone consistently to avoid NZ-specific dependencies
365
		date_default_timezone_set('UTC');
366
	}
367
368
	/**
369
	 * tearDown method that's called once per test class rather once per test method.
370
	 */
371
	public function tearDownOnce() {
372
		// If we have made changes to the extensions present, then migrate the database schema.
373
		if($this->extensionsToReapply || $this->extensionsToRemove) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extensionsToReapply 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...
Bug Best Practice introduced by
The expression $this->extensionsToRemove 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...
374
			// @todo: This isn't strictly necessary to restore extensions, but only to ensure that
375
			// Object::$extra_methods is properly flushed. This should be replaced with a simple
376
			// flush mechanism for each $class.
377
			//
378
			// Remove extensions added for testing
379
			foreach($this->extensionsToRemove as $class => $extensions) {
380
				foreach($extensions as $extension) {
381
					$class::remove_extension($extension);
382
				}
383
			}
384
385
			// Reapply ones removed
386
			foreach($this->extensionsToReapply as $class => $extensions) {
387
				foreach($extensions as $extension) {
388
					$class::add_extension($extension);
389
				}
390
			}
391
		}
392
393
		//unnest injector / config now that the test suite is over
394
		// this will reset all the extensions on the object too (see setUpOnce)
395
		Injector::unnest();
396
		Config::unnest();
397
398
		if(!empty($this->extensionsToReapply) || !empty($this->extensionsToRemove) || !empty($this->extraDataObjects)) {
399
			$this->resetDBSchema();
400
		}
401
	}
402
403
	/**
404
	 * @return FixtureFactory
405
	 */
406
	public function getFixtureFactory() {
407
		if(!$this->fixtureFactory) $this->fixtureFactory = Injector::inst()->create('SilverStripe\\Dev\\FixtureFactory');
408
		return $this->fixtureFactory;
409
	}
410
411
	public function setFixtureFactory(FixtureFactory $factory) {
412
		$this->fixtureFactory = $factory;
413
		return $this;
414
	}
415
416
	/**
417
	 * Get the ID of an object from the fixture.
418
	 *
419
	 * @param string $className The data class, as specified in your fixture file.  Parent classes won't work
420
	 * @param string $identifier The identifier string, as provided in your fixture file
421
	 * @return int
422
	 */
423
	protected function idFromFixture($className, $identifier) {
424
		$id = $this->getFixtureFactory()->getId($className, $identifier);
425
426
		if(!$id) {
427
			user_error(sprintf(
428
				"Couldn't find object '%s' (class: %s)",
429
				$identifier,
430
				$className
431
			), E_USER_ERROR);
432
		}
433
434
		return $id;
435
	}
436
437
	/**
438
	 * Return all of the IDs in the fixture of a particular class name.
439
	 * Will collate all IDs form all fixtures if multiple fixtures are provided.
440
	 *
441
	 * @param string $className
442
	 * @return array A map of fixture-identifier => object-id
443
	 */
444
	protected function allFixtureIDs($className) {
445
		return $this->getFixtureFactory()->getIds($className);
446
	}
447
448
	/**
449
	 * Get an object from the fixture.
450
	 *
451
	 * @param string $className The data class, as specified in your fixture file. Parent classes won't work
452
	 * @param string $identifier The identifier string, as provided in your fixture file
453
	 *
454
	 * @return DataObject
455
	 */
456
	protected function objFromFixture($className, $identifier) {
457
		$obj = $this->getFixtureFactory()->get($className, $identifier);
458
459
		if(!$obj) {
460
			user_error(sprintf(
461
				"Couldn't find object '%s' (class: %s)",
462
				$identifier,
463
				$className
464
			), E_USER_ERROR);
465
		}
466
467
		return $obj;
468
	}
469
470
	/**
471
	 * Load a YAML fixture file into the database.
472
	 * Once loaded, you can use idFromFixture() and objFromFixture() to get items from the fixture.
473
	 * Doesn't clear existing fixtures.
474
	 *
475
	 * @param string $fixtureFile The location of the .yml fixture file, relative to the site base dir
476
	 */
477
	public function loadFixture($fixtureFile) {
478
		$fixture = Injector::inst()->create('SilverStripe\\Dev\\YamlFixture', $fixtureFile);
479
		$fixture->writeInto($this->getFixtureFactory());
480
	}
481
482
	/**
483
	 * Clear all fixtures which were previously loaded through
484
	 * {@link loadFixture()}
485
	 */
486
	public function clearFixtures() {
487
		$this->getFixtureFactory()->clear();
488
	}
489
490
	/**
491
	 * Useful for writing unit tests without hardcoding folder structures.
492
	 *
493
	 * @return String Absolute path to current class.
494
	 */
495
	protected function getCurrentAbsolutePath() {
496
		$filename = self::$test_class_manifest->getItemPath(get_class($this));
497
		if(!$filename) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filename of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
498
			throw new LogicException("getItemPath returned null for " . get_class($this));
499
		}
500
		return dirname($filename);
501
	}
502
503
	/**
504
	 * @return String File path relative to webroot
505
	 */
506
	protected function getCurrentRelativePath() {
507
		$base = Director::baseFolder();
508
		$path = $this->getCurrentAbsolutePath();
509
		if(substr($path,0,strlen($base)) == $base) $path = preg_replace('/^\/*/', '', substr($path,strlen($base)));
510
		return $path;
511
	}
512
513
	public function tearDown() {
514
		// Preserve memory settings
515
		ini_set('memory_limit', ($this->originalMemoryLimit) ? $this->originalMemoryLimit : -1);
516
517
		// Restore email configuration
518
		$this->mailer = null;
519
520
		// Restore password validation
521
		if($this->originalMemberPasswordValidator) {
522
			Member::set_password_validator($this->originalMemberPasswordValidator);
523
		}
524
525
		// Restore requirements
526
		if($this->originalRequirements) {
527
			Requirements::set_backend($this->originalRequirements);
528
		}
529
530
		// Mark test as no longer being run - we use originalIsRunningTest to allow for nested SapphireTest calls
531
		self::$is_running_test = $this->originalIsRunningTest;
532
		$this->originalIsRunningTest = null;
533
534
		// Reset mocked datetime
535
		DBDatetime::clear_mock_now();
536
537
		// Stop the redirection that might have been requested in the test.
538
		// Note: Ideally a clean Controller should be created for each test.
539
		// Now all tests executed in a batch share the same controller.
540
		$controller = Controller::has_curr() ? Controller::curr() : null;
541
		if ($controller && ($response = $controller->getResponse()) && $response->getHeader('Location')) {
542
			$response->setStatusCode(200);
543
			$response->removeHeader('Location');
544
		}
545
546
		Versioned::set_reading_mode($this->originalReadingMode);
547
548
		//unnest injector / config now that tests are over
549
		Injector::unnest();
550
		Config::unnest();
551
	}
552
553
	public static function assertContains(
554
		$needle,
555
		$haystack,
556
		$message = '',
557
		$ignoreCase = FALSE,
558
		$checkForObjectIdentity = TRUE,
559
		$checkForNonObjectIdentity = false
560
	) {
561
		if ($haystack instanceof DBField) $haystack = (string)$haystack;
562
		parent::assertContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
563
	}
564
565
	public static function assertNotContains(
566
		$needle,
567
		$haystack,
568
		$message = '',
569
		$ignoreCase = FALSE,
570
		$checkForObjectIdentity = TRUE,
571
		$checkForNonObjectIdentity = false
572
	) {
573
		if ($haystack instanceof DBField) $haystack = (string)$haystack;
574
		parent::assertNotContains($needle, $haystack, $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity);
575
	}
576
577
	/**
578
	 * Clear the log of emails sent
579
	 */
580
	public function clearEmails() {
581
		return $this->mailer->clearEmails();
582
	}
583
584
	/**
585
	 * Search for an email that was sent.
586
	 * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
587
	 * @param $to
588
	 * @param $from
589
	 * @param $subject
590
	 * @param $content
591
	 * @return array Contains keys: 'type', 'to', 'from', 'subject','content', 'plainContent', 'attachedFiles',
592
	 *               'customHeaders', 'htmlContent', 'inlineImages'
593
	 */
594
	public function findEmail($to, $from = null, $subject = null, $content = null) {
595
		return $this->mailer->findEmail($to, $from, $subject, $content);
596
	}
597
598
	/**
599
	 * Assert that the matching email was sent since the last call to clearEmails()
600
	 * All of the parameters can either be a string, or, if they start with "/", a PREG-compatible regular expression.
601
	 * @param $to
602
	 * @param $from
603
	 * @param $subject
604
	 * @param $content
605
	 * @return array Contains the keys: 'type', 'to', 'from', 'subject', 'content', 'plainContent', 'attachedFiles',
606
	 *               'customHeaders', 'htmlContent', inlineImages'
607
	 */
608
	public function assertEmailSent($to, $from = null, $subject = null, $content = null) {
609
		$found = (bool)$this->findEmail($to, $from, $subject, $content);
610
611
		$infoParts = "";
612
		$withParts = array();
613
		if($to) $infoParts .= " to '$to'";
614
		if($from) $infoParts .= " from '$from'";
615
		if($subject) $withParts[] = "subject '$subject'";
616
		if($content) $withParts[] = "content '$content'";
617
		if($withParts) $infoParts .= " with " . implode(" and ", $withParts);
0 ignored issues
show
Bug Best Practice introduced by
The expression $withParts 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...
618
619
		$this->assertTrue(
620
			$found,
621
			"Failed asserting that an email was sent$infoParts."
622
		);
623
	}
624
625
626
	/**
627
	 * Assert that the given {@link SS_List} includes DataObjects matching the given key-value
628
	 * pairs.  Each match must correspond to 1 distinct record.
629
	 *
630
	 * @param SS_List|array $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
631
	 * either pass a single pattern or an array of patterns.
632
	 * @param SS_List $dataObjectSet The {@link SS_List} to test.
633
	 *
634
	 * Examples
635
	 * --------
636
	 * Check that $members includes an entry with Email = [email protected]:
637
	 *      $this->assertDOSContains(array('Email' => '[email protected]'), $members);
638
	 *
639
	 * Check that $members includes entries with Email = [email protected] and with
640
	 * Email = [email protected]:
641
	 *      $this->assertDOSContains(array(
642
	 *         array('Email' => '[email protected]'),
643
	 *         array('Email' => '[email protected]'),
644
	 *      ), $members);
645
	 */
646
	public function assertDOSContains($matches, $dataObjectSet) {
647
		$extracted = array();
648
		foreach($dataObjectSet as $object) {
649
			/** @var DataObject $object */
650
			$extracted[] = $object->toMap();
651
		}
652
653
		foreach($matches as $match) {
654
			$matched = false;
655
			foreach($extracted as $i => $item) {
656
				if($this->dataObjectArrayMatch($item, $match)) {
657
					// Remove it from $extracted so that we don't get duplicate mapping.
658
					unset($extracted[$i]);
659
					$matched = true;
660
					break;
661
				}
662
			}
663
664
			// We couldn't find a match - assertion failed
665
			$this->assertTrue(
666
				$matched,
667
				"Failed asserting that the SS_List contains an item matching "
668
				. var_export($match, true) . "\n\nIn the following SS_List:\n"
669
				. $this->DOSSummaryForMatch($dataObjectSet, $match)
670
			);
671
		}
672
	}
673
674
	/**
675
	 * Assert that the given {@link SS_List} includes only DataObjects matching the given
676
	 * key-value pairs.  Each match must correspond to 1 distinct record.
677
	 *
678
	 * Example
679
	 * --------
680
	 * Check that *only* the entries Sam Minnee and Ingo Schommer exist in $members.  Order doesn't
681
	 * matter:
682
	 *     $this->assertDOSEquals(array(
683
	 *        array('FirstName' =>'Sam', 'Surname' => 'Minnee'),
684
	 *        array('FirstName' => 'Ingo', 'Surname' => 'Schommer'),
685
	 *      ), $members);
686
	 *
687
	 * @param mixed $matches The patterns to match.  Each pattern is a map of key-value pairs.  You can
688
	 * either pass a single pattern or an array of patterns.
689
	 * @param mixed $dataObjectSet The {@link SS_List} to test.
690
	 */
691
	public function assertDOSEquals($matches, $dataObjectSet) {
692
		// Extract dataobjects
693
		$extracted = array();
694
		if($dataObjectSet) {
695
			foreach ($dataObjectSet as $object) {
696
				/** @var DataObject $object */
697
				$extracted[] = $object->toMap();
698
			}
699
		}
700
701
		// Check all matches
702
		if($matches) {
703
			foreach ($matches as $match) {
704
				$matched = false;
705
				foreach ($extracted as $i => $item) {
706
					if ($this->dataObjectArrayMatch($item, $match)) {
707
						// Remove it from $extracted so that we don't get duplicate mapping.
708
						unset($extracted[$i]);
709
						$matched = true;
710
						break;
711
					}
712
				}
713
714
				// We couldn't find a match - assertion failed
715
				$this->assertTrue(
716
					$matched,
717
					"Failed asserting that the SS_List contains an item matching "
718
					. var_export($match, true) . "\n\nIn the following SS_List:\n"
719
					. $this->DOSSummaryForMatch($dataObjectSet, $match)
720
				);
721
			}
722
		}
723
724
		// If we have leftovers than the DOS has extra data that shouldn't be there
725
		$this->assertTrue(
726
			(count($extracted) == 0),
727
			// If we didn't break by this point then we couldn't find a match
728
			"Failed asserting that the SS_List contained only the given items, the "
729
			. "following items were left over:\n" . var_export($extracted, true)
730
		);
731
	}
732
733
	/**
734
	 * Assert that the every record in the given {@link SS_List} matches the given key-value
735
	 * pairs.
736
	 *
737
	 * Example
738
	 * --------
739
	 * Check that every entry in $members has a Status of 'Active':
740
	 *     $this->assertDOSAllMatch(array('Status' => 'Active'), $members);
741
	 *
742
	 * @param mixed $match The pattern to match.  The pattern is a map of key-value pairs.
743
	 * @param mixed $dataObjectSet The {@link SS_List} to test.
744
	 */
745
	public function assertDOSAllMatch($match, $dataObjectSet) {
746
		$extracted = array();
747
		foreach($dataObjectSet as $object) {
748
			/** @var DataObject $object */
749
			$extracted[] = $object->toMap();
750
		}
751
752
		foreach($extracted as $i => $item) {
753
			$this->assertTrue(
754
				$this->dataObjectArrayMatch($item, $match),
755
				"Failed asserting that the the following item matched "
756
				. var_export($match, true) . ": " . var_export($item, true)
757
			);
758
		}
759
	}
760
761
	/**
762
	 * Removes sequences of repeated whitespace characters from SQL queries
763
	 * making them suitable for string comparison
764
	 *
765
	 * @param string $sql
766
	 * @return string The cleaned and normalised SQL string
767
	 */
768
	protected function normaliseSQL($sql) {
769
		return trim(preg_replace('/\s+/m', ' ', $sql));
770
	}
771
772
	/**
773
	 * Asserts that two SQL queries are equivalent
774
	 *
775
	 * @param string $expectedSQL
776
	 * @param string $actualSQL
777
	 * @param string $message
778
	 * @param float|int $delta
779
	 * @param integer $maxDepth
780
	 * @param boolean $canonicalize
781
	 * @param boolean $ignoreCase
782
	 */
783
	public function assertSQLEquals($expectedSQL, $actualSQL, $message = '', $delta = 0, $maxDepth = 10,
784
		$canonicalize = false, $ignoreCase = false
785
	) {
786
		// Normalise SQL queries to remove patterns of repeating whitespace
787
		$expectedSQL = $this->normaliseSQL($expectedSQL);
788
		$actualSQL = $this->normaliseSQL($actualSQL);
789
790
		$this->assertEquals($expectedSQL, $actualSQL, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
791
	}
792
793
	/**
794
	 * Asserts that a SQL query contains a SQL fragment
795
	 *
796
	 * @param string $needleSQL
797
	 * @param string $haystackSQL
798
	 * @param string $message
799
	 * @param boolean $ignoreCase
800
	 * @param boolean $checkForObjectIdentity
801
	 */
802
	public function assertSQLContains($needleSQL, $haystackSQL, $message = '', $ignoreCase = false,
803
		$checkForObjectIdentity = true
804
	) {
805
		$needleSQL = $this->normaliseSQL($needleSQL);
806
		$haystackSQL = $this->normaliseSQL($haystackSQL);
807
808
		$this->assertContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
809
	}
810
811
	/**
812
	 * Asserts that a SQL query contains a SQL fragment
813
	 *
814
	 * @param string $needleSQL
815
	 * @param string $haystackSQL
816
	 * @param string $message
817
	 * @param boolean $ignoreCase
818
	 * @param boolean $checkForObjectIdentity
819
	 */
820
	public function assertSQLNotContains($needleSQL, $haystackSQL, $message = '', $ignoreCase = false,
821
		$checkForObjectIdentity = true
822
	) {
823
		$needleSQL = $this->normaliseSQL($needleSQL);
824
		$haystackSQL = $this->normaliseSQL($haystackSQL);
825
826
		$this->assertNotContains($needleSQL, $haystackSQL, $message, $ignoreCase, $checkForObjectIdentity);
827
	}
828
829
	/**
830
	 * Helper function for the DOS matchers
831
	 *
832
	 * @param array $item
833
	 * @param array $match
834
	 * @return bool
835
	 */
836
	private function dataObjectArrayMatch($item, $match) {
837
		foreach($match as $k => $v) {
838
			if(!array_key_exists($k, $item) || $item[$k] != $v) {
839
				return false;
840
			}
841
		}
842
		return true;
843
	}
844
845
	/**
846
	 * Helper function for the DOS matchers
847
	 *
848
	 * @param SS_List|array $dataObjectSet
849
	 * @param array $match
850
	 * @return string
851
	 */
852
	private function DOSSummaryForMatch($dataObjectSet, $match) {
853
		$extracted = array();
854
		foreach($dataObjectSet as $item) {
855
			$extracted[] = array_intersect_key($item->toMap(), $match);
856
		}
857
		return var_export($extracted, true);
858
	}
859
860
	/**
861
	 * Pushes a class and template manifest instance that include tests onto the
862
	 * top of the loader stacks.
863
	 */
864
	public static function use_test_manifest() {
0 ignored issues
show
Coding Style introduced by
use_test_manifest uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
865
		$flush = true;
866
		if(isset($_GET['flush']) && $_GET['flush'] === '0') {
867
			$flush = false;
868
		}
869
870
		$classManifest = new ClassManifest(
871
			BASE_PATH, true, $flush
872
		);
873
874
		ClassLoader::instance()->pushManifest($classManifest, false);
875
		SapphireTest::set_test_class_manifest($classManifest);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
876
877
		ThemeResourceLoader::instance()->addSet('$default', new ThemeManifest(
878
			BASE_PATH, project(), true, $flush
879
		));
880
881
		Config::inst()->pushConfigStaticManifest(new ConfigStaticManifest(
882
			BASE_PATH, true, $flush
0 ignored issues
show
Unused Code introduced by
The call to ConfigStaticManifest::__construct() has too many arguments starting with BASE_PATH.

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...
883
		));
884
885
		// Invalidate classname spec since the test manifest will now pull out new subclasses for each internal class
886
		// (e.g. Member will now have various subclasses of DataObjects that implement TestOnly)
887
		DataObject::reset();
888
	}
889
890
	/**
891
	 * Returns true if we are currently using a temporary database
892
	 */
893
	public static function using_temp_db() {
894
		$dbConn = DB::get_conn();
895
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
896
		return $dbConn && (substr($dbConn->getSelectedDatabase(), 0, strlen($prefix) + 5)
897
			== strtolower(sprintf('%stmpdb', $prefix)));
898
	}
899
900
	public static function kill_temp_db() {
901
		// Delete our temporary database
902
		if(self::using_temp_db()) {
903
			$dbConn = DB::get_conn();
904
			$dbName = $dbConn->getSelectedDatabase();
905
			if($dbName && DB::get_conn()->databaseExists($dbName)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dbName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
906
				// Some DataExtensions keep a static cache of information that needs to
907
				// be reset whenever the database is killed
908
				foreach(ClassInfo::subclassesFor('SilverStripe\\ORM\\DataExtension') as $class) {
909
					$toCall = array($class, 'on_db_reset');
910
					if(is_callable($toCall)) call_user_func($toCall);
911
				}
912
913
				// echo "Deleted temp database " . $dbConn->currentDatabase() . "\n";
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
914
				$dbConn->dropSelectedDatabase();
915
			}
916
		}
917
	}
918
919
	/**
920
	 * Remove all content from the temporary database.
921
	 */
922
	public static function empty_temp_db() {
923
		if(self::using_temp_db()) {
924
			DB::get_conn()->clearAllData();
925
926
			// Some DataExtensions keep a static cache of information that needs to
927
			// be reset whenever the database is cleaned out
928
			$classes = array_merge(ClassInfo::subclassesFor('SilverStripe\\ORM\\DataExtension'), ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject'));
929
			foreach($classes as $class) {
930
				$toCall = array($class, 'on_db_reset');
931
				if(is_callable($toCall)) call_user_func($toCall);
932
			}
933
		}
934
	}
935
936
	public static function create_temp_db() {
937
		// Disable PHPUnit error handling
938
		restore_error_handler();
939
940
		// Create a temporary database, and force the connection to use UTC for time
941
		global $databaseConfig;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
942
		$databaseConfig['timezone'] = '+0:00';
943
		DB::connect($databaseConfig);
944
		$dbConn = DB::get_conn();
945
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
946
		$dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000,9999999);
947
		while(!$dbname || $dbConn->databaseExists($dbname)) {
948
			$dbname = strtolower(sprintf('%stmpdb', $prefix)) . rand(1000000,9999999);
949
		}
950
951
		$dbConn->selectDatabase($dbname, true);
952
953
		/** @var static $st */
954
		$st = Injector::inst()->create(__CLASS__);
955
		$st->resetDBSchema();
956
957
		// Reinstate PHPUnit error handling
958
		set_error_handler(array('PHPUnit_Util_ErrorHandler', 'handleError'));
959
960
		return $dbname;
961
	}
962
963
	public static function delete_all_temp_dbs() {
964
		$prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_';
965
		foreach(DB::get_schema()->databaseList() as $dbName) {
966
			if(preg_match(sprintf('/^%stmpdb[0-9]+$/', $prefix), $dbName)) {
967
				DB::get_schema()->dropDatabase($dbName);
968
				if(Director::is_cli()) {
969
					echo "Dropped database \"$dbName\"" . PHP_EOL;
970
				} else {
971
					echo "<li>Dropped database \"$dbName\"</li>" . PHP_EOL;
972
				}
973
				flush();
974
			}
975
		}
976
	}
977
978
	/**
979
	 * Reset the testing database's schema.
980
	 * @param bool $includeExtraDataObjects If true, the extraDataObjects tables will also be included
981
	 */
982
	public function resetDBSchema($includeExtraDataObjects = false) {
983
		if(self::using_temp_db()) {
984
			DataObject::reset();
985
986
			// clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild()
987
			Injector::inst()->unregisterAllObjects();
988
989
			$dataClasses = ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject');
990
			array_shift($dataClasses);
991
992
			DB::quiet();
993
			$schema = DB::get_schema();
994
			$extraDataObjects = $includeExtraDataObjects ? $this->extraDataObjects : null;
995
			$schema->schemaUpdate(function() use($dataClasses, $extraDataObjects){
996
				foreach($dataClasses as $dataClass) {
997
					// Check if class exists before trying to instantiate - this sidesteps any manifest weirdness
998
					if(class_exists($dataClass)) {
999
						$SNG = singleton($dataClass);
1000
						if(!($SNG instanceof TestOnly)) $SNG->requireTable();
1001
					}
1002
				}
1003
1004
				// If we have additional dataobjects which need schema, do so here:
1005
				if($extraDataObjects) {
1006
					foreach($extraDataObjects as $dataClass) {
1007
						$SNG = singleton($dataClass);
1008
						if(singleton($dataClass) instanceof DataObject) $SNG->requireTable();
1009
					}
1010
				}
1011
			});
1012
1013
			ClassInfo::reset_db_cache();
1014
			singleton('SilverStripe\\ORM\\DataObject')->flushCache();
1015
		}
1016
	}
1017
1018
	/**
1019
	 * Create a member and group with the given permission code, and log in with it.
1020
	 * Returns the member ID.
1021
	 *
1022
	 * @param string|array $permCode Either a permission, or list of permissions
1023
	 * @return int Member ID
1024
	 */
1025
	public function logInWithPermission($permCode = "ADMIN") {
1026
		if(is_array($permCode)) {
1027
			$permArray = $permCode;
1028
			$permCode = implode('.', $permCode);
1029
		} else {
1030
			$permArray = array($permCode);
1031
		}
1032
1033
		// Check cached member
1034
		if(isset($this->cache_generatedMembers[$permCode])) {
1035
			$member = $this->cache_generatedMembers[$permCode];
1036
		} else {
1037
			// Generate group with these permissions
1038
			$group = Group::create();
1039
			$group->Title = "$permCode group";
1040
			$group->write();
1041
1042
			// Create each individual permission
1043
			foreach($permArray as $permArrayItem) {
1044
				$permission = Permission::create();
1045
				$permission->Code = $permArrayItem;
1046
				$permission->write();
1047
				$group->Permissions()->add($permission);
1048
			}
1049
1050
			$member = DataObject::get_one('SilverStripe\\Security\\Member', array(
1051
				'"Member"."Email"' => "[email protected]"
1052
			));
1053
			if (!$member) {
1054
				$member = Member::create();
1055
			}
1056
1057
			$member->FirstName = $permCode;
0 ignored issues
show
Documentation introduced by
The property FirstName does not exist on object<SilverStripe\ORM\DataObject>. 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...
1058
			$member->Surname = "User";
0 ignored issues
show
Documentation introduced by
The property Surname does not exist on object<SilverStripe\ORM\DataObject>. 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...
1059
			$member->Email = "[email protected]";
0 ignored issues
show
Documentation introduced by
The property Email does not exist on object<SilverStripe\ORM\DataObject>. 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...
1060
			$member->write();
1061
			$group->Members()->add($member);
1062
1063
			$this->cache_generatedMembers[$permCode] = $member;
1064
		}
1065
		$member->logIn();
1066
		return $member->ID;
1067
	}
1068
1069
	/**
1070
	 * Cache for logInWithPermission()
1071
	 */
1072
	protected $cache_generatedMembers = array();
1073
1074
1075
	/**
1076
	 * Test against a theme.
1077
	 *
1078
	 * @param string $themeBaseDir themes directory
1079
	 * @param string $theme Theme name
1080
	 * @param callable $callback
1081
	 * @throws Exception
1082
	 */
1083
	protected function useTestTheme($themeBaseDir, $theme, $callback) {
1084
		Config::nest();
1085
1086
		if (strpos($themeBaseDir, BASE_PATH) === 0) {
1087
			$themeBaseDir = substr($themeBaseDir, strlen(BASE_PATH));
1088
		}
1089
		SSViewer::config()->update('theme_enabled', true);
1090
		SSViewer::set_themes([$themeBaseDir.'/themes/'.$theme, '$default']);
1091
1092
		$e = null;
1093
1094
		try { $callback(); }
1095
		catch (Exception $e) { /* NOP for now, just save $e */ }
1096
1097
		Config::unnest();
1098
1099
		if ($e) {
1100
			throw $e;
1101
		}
1102
	}
1103
1104
	/**
1105
	 * Get fixture paths for this test
1106
	 *
1107
	 * @return array List of paths
1108
	 */
1109
	protected function getFixturePaths()
1110
	{
1111
		$fixtureFile = static::get_fixture_file();
1112
		if (empty($fixtureFile)) {
1113
			return [];
1114
		}
1115
1116
		$fixtureFiles = (is_array($fixtureFile)) ? $fixtureFile : [$fixtureFile];
1117
1118
		return array_map(function($fixtureFilePath) {
1119
			return $this->resolveFixturePath($fixtureFilePath);
1120
		}, $fixtureFiles);
1121
	}
1122
1123
	/**
1124
	 * Map a fixture path to a physical file
1125
	 *
1126
	 * @param string $fixtureFilePath
1127
	 * @return string
1128
	 */
1129
	protected function resolveFixturePath($fixtureFilePath)
1130
	{
1131
		// Support fixture paths relative to the test class, rather than relative to webroot
1132
		// String checking is faster than file_exists() calls.
1133
		$isRelativeToFile
1134
			= (strpos('/', $fixtureFilePath) === false)
1135
			|| preg_match('/^(\.){1,2}/', $fixtureFilePath);
1136
1137
		if ($isRelativeToFile) {
1138
			$resolvedPath = realpath($this->getCurrentAbsolutePath() . '/' . $fixtureFilePath);
1139
			if ($resolvedPath) {
1140
				return $resolvedPath;
1141
			}
1142
		}
1143
1144
		// Check if file exists relative to base dir
1145
		$resolvedPath = realpath(Director::baseFolder() . '/' . $fixtureFilePath);
1146
		if ($resolvedPath) {
1147
			return $resolvedPath;
1148
		}
1149
1150
		return $fixtureFilePath;
1151
	}
1152
1153
}
1154
1155
1156