Completed
Push — master ( a648c7...987505 )
by Hamish
7s
created

FixtureContext::aRecordWasLastEditedRelative()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 14
c 2
b 0
f 0
nc 4
nop 4
dl 0
loc 19
rs 9.4285
1
<?php
2
3
namespace SilverStripe\BehatExtension\Context;
4
5
use Behat\Behat\Context\BehatContext,
6
	Behat\Behat\Event\ScenarioEvent,
7
	Behat\Gherkin\Node\PyStringNode,
8
	Behat\Gherkin\Node\TableNode,
9
	SilverStripe\Filesystem\Storage\AssetStore;
10
use SilverStripe\ORM\DB;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\Versioning\Versioned;
13
use SilverStripe\Security\Permission;
14
15
16
17
// PHPUnit
18
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
19
20
/**
21
 * Context used to create fixtures in the SilverStripe ORM.
22
 */
23
class FixtureContext extends BehatContext
24
{
25
	protected $context;
26
27
	/**
28
	 * @var \FixtureFactory
29
	 */
30
	protected $fixtureFactory;
31
32
	/**
33
	 * @var String Absolute path where file fixtures are located.
34
	 * These will automatically get copied to their location
35
	 * declare through the 'Given a file "..."' step defition.
36
	 */
37
	protected $filesPath;
38
39
	/**
40
	 * @var String Tracks all files and folders created from fixtures, for later cleanup.
41
	 */
42
	protected $createdFilesPaths = array();
43
44
	/**
45
	 * @var array Stores the asset tuples.
46
	 */
47
	protected $createdAssets = array();
48
49
	public function __construct(array $parameters) {
50
		$this->context = $parameters;
51
	}
52
53
	public function getSession($name = null) {
54
		return $this->getMainContext()->getSession($name);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\ExtendedContextInterface as the method getSession() does only exist in the following implementations of said interface: Behat\MinkExtension\Context\MinkContext, Behat\MinkExtension\Context\RawMinkContext, SilverStripe\BehatExtension\Context\BasicContext, SilverStripe\BehatExtension\Context\EmailContext, SilverStripe\BehatExtension\Context\FixtureContext, SilverStripe\BehatExtension\Context\LoginContext, SilverStripe\BehatExtens...ext\SilverStripeContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
55
	}
56
57
	/**
58
	 * @return \FixtureFactory
59
	 */
60
	public function getFixtureFactory() {
61
		if(!$this->fixtureFactory) {
62
			$this->fixtureFactory = \Injector::inst()->create('FixtureFactory', 'FixtureContextFactory');
63
		}
64
		return $this->fixtureFactory;
65
	}
66
67
	/**
68
	 * @param \FixtureFactory $factory
69
	 */
70
	public function setFixtureFactory(\FixtureFactory $factory) {
71
		$this->fixtureFactory = $factory;
72
	}
73
74
	/**
75
	 * @param String
76
	 */
77
	public function setFilesPath($path) {
78
		$this->filesPath = $path;
79
	}
80
81
	/**
82
	 * @return String
83
	 */
84
	public function getFilesPath() {
85
		return $this->filesPath;
86
	}
87
88
	/**
89
	 * @BeforeScenario @database-defaults
90
	 */
91
	public function beforeDatabaseDefaults(ScenarioEvent $event) {
0 ignored issues
show
Unused Code introduced by
The parameter $event 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...
92
		\SapphireTest::empty_temp_db();
93
		DB::get_conn()->quiet();
94
		$dataClasses = \ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject');
95
		array_shift($dataClasses);
96
		foreach ($dataClasses as $dataClass) {
97
			\singleton($dataClass)->requireDefaultRecords();
98
		}
99
	}
100
101
    /**
102
     * @AfterScenario
103
     */
104
	public function afterResetDatabase(ScenarioEvent $event) {
0 ignored issues
show
Unused Code introduced by
The parameter $event 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...
105
		\SapphireTest::empty_temp_db();
106
	}
107
108
	/**
109
	 * @AfterScenario
110
	 */
111
	public function afterResetAssets(ScenarioEvent $event) {
0 ignored issues
show
Unused Code introduced by
The parameter $event 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...
112
		$store = $this->getAssetStore();
113
		if (is_array($this->createdAssets)) {
114
			foreach ($this->createdAssets as $asset) {
115
				$store->delete($asset['FileFilename'], $asset['FileHash']);
116
			}
117
		}
118
	}
119
120
	/**
121
	 * Example: Given a "page" "Page 1"
122
	 *
123
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)"$/
124
	 */
125
	public function stepCreateRecord($type, $id) {
126
		$class = $this->convertTypeToClass($type);
127
		$fields = $this->prepareFixture($class, $id);
128
		$this->fixtureFactory->createObject($class, $id, $fields);
129
	}
130
131
	/**
132
	 * Example: Given a "page" "Page 1" has the "content" "My content"
133
	 *
134
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" has (?:(an|a|the) )"(?<field>.*)" "(?<value>.*)"$/
135
	 */
136
	public function stepCreateRecordHasField($type, $id, $field, $value) {
137
		$class = $this->convertTypeToClass($type);
138
		$fields = $this->convertFields(
139
			$class,
140
			array($field => $value)
141
		);
142
		// We should check if this fixture object already exists - if it does, we update it. If not, we create it
143 View Code Duplication
		if($existingFixture = $this->fixtureFactory->get($class, $id)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
144
			// Merge existing data with new data, and create new object to replace existing object
145
			foreach($fields as $k => $v) {
146
				$existingFixture->$k = $v;
147
			}
148
			$existingFixture->write();
149
		} else {
150
			$this->fixtureFactory->createObject($class, $id, $fields);
151
		}
152
	}
153
154
	/**
155
	 * Example: Given a "page" "Page 1" with "URL"="page-1" and "Content"="my page 1"
156
	 * Example: Given the "page" "Page 1" has "URL"="page-1" and "Content"="my page 1"
157
	 *
158
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" (?:(with|has)) (?<data>".*)$/
159
	 */
160
	public function stepCreateRecordWithData($type, $id, $data) {
161
		$class = $this->convertTypeToClass($type);
162
		preg_match_all(
163
			'/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/',
164
			$data,
165
			$matches
166
		);
167
		$fields = $this->convertFields(
168
			$class,
169
			array_combine($matches['key'], $matches['value'])
170
		);
171
		$fields = $this->prepareFixture($class, $id, $fields);
172
		// We should check if this fixture object already exists - if it does, we update it. If not, we create it
173 View Code Duplication
		if($existingFixture = $this->fixtureFactory->get($class, $id)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
174
			// Merge existing data with new data, and create new object to replace existing object
175
			foreach($fields as $k => $v) {
176
				$existingFixture->$k = $v;
177
			}
178
			$existingFixture->write();
179
		} else {
180
			$this->fixtureFactory->createObject($class, $id, $fields);
181
		}
182
	}
183
184
	/**
185
	 * Example: And the "page" "Page 2" has the following data
186
	 * | Content | <blink> |
187
	 * | My Property | foo |
188
	 * | My Boolean | bar |
189
	 *
190
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" has the following data$/
191
	 */
192
	public function stepCreateRecordWithTable($type, $id, $null, TableNode $fieldsTable) {
0 ignored issues
show
Unused Code introduced by
The parameter $null 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...
193
		$class = $this->convertTypeToClass($type);
194
		// TODO Support more than one record
195
		$fields = $this->convertFields($class, $fieldsTable->getRowsHash());
196
		$fields = $this->prepareFixture($class, $id, $fields);
197
198
		// We should check if this fixture object already exists - if it does, we update it. If not, we create it
199 View Code Duplication
		if($existingFixture = $this->fixtureFactory->get($class, $id)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
200
			// Merge existing data with new data, and create new object to replace existing object
201
			foreach($fields as $k => $v) {
202
				$existingFixture->$k = $v;
203
			}
204
			$existingFixture->write();
205
		} else {
206
			$this->fixtureFactory->createObject($class, $id, $fields);
207
		}
208
	}
209
210
	/**
211
	 * Example: Given the "page" "Page 1.1" is a child of the "page" "Page1".
212
	 * Note that this change is not published by default
213
	 *
214
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" is a (?<relation>[^\s]*) of (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)"/
215
	 */
216
	public function stepUpdateRecordRelation($type, $id, $relation, $relationType, $relationId) {
217
		$class = $this->convertTypeToClass($type);
218
219
		$relationClass = $this->convertTypeToClass($relationType);
220
		$relationObj = $this->fixtureFactory->get($relationClass, $relationId);
221
		if(!$relationObj) $relationObj = $this->fixtureFactory->createObject($relationClass, $relationId);
222
223
		$data = array();
224
		if($relation == 'child') {
225
			$data['ParentID'] = $relationObj->ID;
226
		}
227
228
		$obj = $this->fixtureFactory->get($class, $id);
229
		if($obj) {
230
			$obj->update($data);
231
			$obj->write();
232
		} else {
233
			$obj = $this->fixtureFactory->createObject($class, $id, $data);
234
		}
235
236
		switch($relation) {
237
			case 'parent':
238
				$relationObj->ParentID = $obj->ID;
239
				$relationObj->write();
240
				break;
241
			case 'child':
242
				// already written through $data above
243
				break;
244
			default:
245
				throw new \InvalidArgumentException(sprintf(
246
					'Invalid relation "%s"', $relation
247
				));
248
		}
249
	}
250
251
	/**
252
	 * Assign a type of object to another type of object. The base object will be created if it does not exist already.
253
	 * If the last part of the string (in the "X" relation) is omitted, then the first matching relation will be used.
254
	 *
255
	 * @example I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1"
256
	 * @Given /^I assign (?:(an|a|the) )"(?<type>[^"]+)" "(?<value>[^"]+)" to (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)"$/
257
	 */
258
	public function stepIAssignObjToObj($type, $value, $relationType, $relationId) {
259
		self::stepIAssignObjToObjInTheRelation($type, $value, $relationType, $relationId, null);
260
	}
261
262
 	/**
0 ignored issues
show
Coding Style introduced by
There is some trailing whitespace on this line which should be avoided as per coding-style.
Loading history...
263
	 * Assign a type of object to another type of object. The base object will be created if it does not exist already.
264
	 * If the last part of the string (in the "X" relation) is omitted, then the first matching relation will be used.
265
	 * Assumption: one object has relationship  (has_one, has_many or many_many ) with the other object
266
	 *
267
	 * @example I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1" in the "Terms" relation
268
	 * @Given /^I assign (?:(an|a|the) )"(?<type>[^"]+)" "(?<value>[^"]+)" to (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)" in the "(?<relationName>[^"]+)" relation$/
269
	 */
270
	public function stepIAssignObjToObjInTheRelation($type, $value, $relationType, $relationId, $relationName) {
271
		$class = $this->convertTypeToClass($type);
272
		$relationClass = $this->convertTypeToClass($relationType);
273
274
		// Check if this fixture object already exists - if not, we create it
275
		$relationObj = $this->fixtureFactory->get($relationClass, $relationId);
276
		if(!$relationObj) $relationObj = $this->fixtureFactory->createObject($relationClass, $relationId);
277
278
		// Check if there is relationship defined in many_many (includes belongs_many_many)
279
		$manyField = null;
280
		$oneField = null;
281 View Code Duplication
		if ($relationObj->many_many()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
282
			$manyField = array_search($class, $relationObj->many_many());
283
			if($manyField && strlen($relationName) > 0) $manyField = $relationName;
284
		}
285 View Code Duplication
		if(empty($manyField) && $relationObj->has_many()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
286
			$manyField = array_search($class, $relationObj->has_many());
287
			if($manyField && strlen($relationName) > 0) $manyField = $relationName;
288
		}
289 View Code Duplication
		if(empty($manyField) && $relationObj->has_one()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
290
			$oneField = array_search($class, $relationObj->has_one());
291
			if($oneField && strlen($relationName) > 0) $oneField = $relationName;
292
		}
293
		if(empty($manyField) && empty($oneField)) {
294
			throw new \Exception("'$relationClass' has no relationship (has_one, has_many and many_many) with '$class'!");
295
		}
296
297
		// Get the searchable field to check if the fixture object already exists
298
		$temObj = new $class;
299
		if(isset($temObj->Name)) $field = "Name";
300
		else if(isset($temObj->Title)) $field = "Title";
301
		else $field = "ID";
302
303
		// Check if the fixture object exists - if not, we create it
304
		$obj = DataObject::get($class)->filter($field, $value)->first();
305
		if(!$obj) $obj = $this->fixtureFactory->createObject($class, $value);
306
		// If has_many or many_many, add this fixture object to the relation object
307
		// If has_one, set value to the joint field with this fixture object's ID
308
		if($manyField) {
309
			$relationObj->$manyField()->add($obj);
310
		} else if($oneField) {
311
			// E.g. $has_one = array('PanelOffer' => 'Offer');
312
			// then the join field is PanelOfferID. This is the common rule in the CMS
313
			$relationObj->{$oneField . 'ID'} = $obj->ID;
314
		}
315
316
		$relationObj->write();
317
	}
318
319
	 /**
320
	 * Example: Given the "page" "Page 1" is not published
321
	 *
322
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" is (?<state>[^"]*)$/
323
	 */
324
	public function stepUpdateRecordState($type, $id, $state) {
325
		$class = $this->convertTypeToClass($type);
326
        /** @var DataObject|Versioned $obj */
327
        $obj = $this->fixtureFactory->get($class, $id);
328
		if(!$obj) {
329
			throw new \InvalidArgumentException(sprintf(
330
				'Can not find record "%s" with identifier "%s"',
331
				$type,
332
				$id
333
			));
334
		}
335
336
		switch($state) {
337
			case 'published':
338
				$obj->copyVersionToStage('Stage', 'Live');
0 ignored issues
show
Bug introduced by
The method copyVersionToStage does only exist in SilverStripe\ORM\Versioning\Versioned, but not in SilverStripe\ORM\DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
339
				break;
340
			case 'not published':
341
			case 'unpublished':
342
				$oldMode = Versioned::get_reading_mode();
343
				Versioned::set_stage(Versioned::LIVE);
344
				$clone = clone $obj;
345
				$clone->delete();
0 ignored issues
show
Bug introduced by
The method delete does only exist in SilverStripe\ORM\DataObject, but not in SilverStripe\ORM\Versioning\Versioned.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
346
				Versioned::set_reading_mode($oldMode);
347
				break;
348
			case 'deleted':
349
				$obj->delete();
350
				break;
351
			default:
352
				throw new \InvalidArgumentException(sprintf(
353
					'Invalid state: "%s"', $state
354
				));
355
		}
356
	}
357
358
	/**
359
	 * Accepts YAML fixture definitions similar to the ones used in SilverStripe unit testing.
360
	 *
361
	 * Example: Given there are the following member records:
362
	 *  member1:
363
	 *    Email: [email protected]
364
	 *  member2:
365
	 *    Email: [email protected]
366
	 *
367
	 * @Given /^there are the following ([^\s]*) records$/
368
	 */
369
	public function stepThereAreTheFollowingRecords($dataObject, PyStringNode $string) {
370
		$yaml = array_merge(array($dataObject . ':'), $string->getLines());
371
		$yaml = implode("\n  ", $yaml);
372
373
		// Save fixtures into database
374
		// TODO Run prepareAsset() for each File and Folder record
375
		$yamlFixture = new \YamlFixture($yaml);
376
		$yamlFixture->writeInto($this->getFixtureFactory());
377
	}
378
379
	/**
380
	 * Example: Given a "member" "Admin" belonging to "Admin Group"
381
	 *
382
	 * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)"$/
383
	 */
384
	public function stepCreateMemberWithGroup($id, $groupId) {
385
		$group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $groupId);
386
		if(!$group) $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $groupId);
387
388
		$member = $this->fixtureFactory->createObject('SilverStripe\\Security\\Member', $id);
389
		$member->Groups()->add($group);
390
	}
391
392
	/**
393
	 * Example: Given a "member" "Admin" belonging to "Admin Group" with "Email"="[email protected]"
394
	 *
395
	 * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)" with (?<data>.*)$/
396
	 */
397
	public function stepCreateMemberWithGroupAndData($id, $groupId, $data) {
398
		$class = 'SilverStripe\\Security\\Member';
399
		preg_match_all(
400
			'/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/',
401
			$data,
402
			$matches
403
		);
404
		$fields = $this->convertFields(
405
			$class,
406
			array_combine($matches['key'], $matches['value'])
407
		);
408
409
		$group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $groupId);
410
		if(!$group) $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $groupId);
411
412
		$member = $this->fixtureFactory->createObject($class, $id, $fields);
413
		$member->Groups()->add($group);
414
	}
415
416
	/**
417
	 * Example: Given a "group" "Admin" with permissions "Access to 'Pages' section" and "Access to 'Files' section"
418
	 *
419
	 * @Given /^(?:(an|a|the) )"group" "(?<id>[^"]+)" (?:(with|has)) permissions (?<permissionStr>.*)$/
420
	 */
421
	public function stepCreateGroupWithPermissions($id, $permissionStr) {
422
		// Convert natural language permissions to codes
423
		preg_match_all('/"([^"]+)"/', $permissionStr, $matches);
424
		$permissions = $matches[1];
425
		$codes = Permission::get_codes(false);
426
427
		$group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $id);
428
		if(!$group) $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $id);
429
430
		foreach($permissions as $permission) {
431
			$found = false;
432
			foreach($codes as $code => $details) {
433
				if(
434
					$permission == $code
435
					|| $permission == $details['name']
436
				) {
437
					Permission::grant($group->ID, $code);
438
					$found = true;
439
				}
440
			}
441
			if(!$found) {
442
				throw new \InvalidArgumentException(sprintf(
443
					'No permission found for "%s"', $permission
444
				));
445
			}
446
		}
447
	}
448
449
	/**
450
	 * Navigates to a record based on its identifier set during fixture creation,
451
	 * using its RelativeLink() method to map the record to a URL.
452
	 * Example: Given I go to the "page" "My Page"
453
	 *
454
	 * @Given /^I go to (?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)"/
455
	 */
456
	public function stepGoToNamedRecord($type, $id) {
457
		$class = $this->convertTypeToClass($type);
458
		$record = $this->fixtureFactory->get($class, $id);
459
		if(!$record) {
460
			throw new \InvalidArgumentException(sprintf(
461
				'Cannot resolve reference "%s", no matching fixture found',
462
				$id
463
			));
464
		}
465
		if(!$record->hasMethod('RelativeLink')) {
466
			throw new \InvalidArgumentException('URL for record cannot be determined, missing RelativeLink() method');
467
		}
468
469
		$this->getSession()->visit($this->getMainContext()->locatePath($record->RelativeLink()));
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\ExtendedContextInterface as the method locatePath() does only exist in the following implementations of said interface: Behat\MinkExtension\Context\MinkContext, Behat\MinkExtension\Context\RawMinkContext, SilverStripe\BehatExtens...ext\SilverStripeContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
470
	}
471
472
473
	/**
474
	 * Checks that a file or folder exists in the webroot.
475
	 * Example: There should be a file "assets/Uploads/test.jpg"
476
	 *
477
	 * @Then /^there should be a (?<type>(file|folder) )"(?<path>[^"]*)"/
478
	 */
479
	public function stepThereShouldBeAFileOrFolder($type, $path) {
0 ignored issues
show
Unused Code introduced by
The parameter $type 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...
480
		assertFileExists($this->joinPaths(BASE_PATH, $path));
481
	}
482
483
	/**
484
	 * Checks that a file exists in the asset store with a given filename and hash
485
	 *
486
	 * Example: there should be a filename "Uploads/test.jpg" with hash "59de0c841f"
487
	 *
488
	 * @Then /^there should be a filename "(?<filename>[^"]*)" with hash "(?<hash>[a-fA-Z0-9]+)"/
489
	 */
490
	public function stepThereShouldBeAFileWithTuple($filename, $hash) {
491
		$exists = $this->getAssetStore()->exists($filename, $hash);
492
		assertTrue((bool)$exists, "A file exists with filename $filename and hash $hash");
493
	}
494
495
	/**
496
	 * Replaces fixture references in values with their respective database IDs,
497
	 * with the notation "=><class>.<identifier>". Example: "=>Page.My Page".
498
	 *
499
	 * @Transform /^([^"]+)$/
500
	 */
501
	public function lookupFixtureReference($string) {
502
		if(preg_match('/^=>/', $string)) {
503
			list($className, $identifier) = explode('.', preg_replace('/^=>/', '', $string), 2);
504
			$id = $this->fixtureFactory->getId($className, $identifier);
505
			if(!$id) {
506
				throw new \InvalidArgumentException(sprintf(
507
					'Cannot resolve reference "%s", no matching fixture found',
508
					$string
509
				));
510
			}
511
			return $id;
512
		} else {
513
			return $string;
514
		}
515
	}
516
517
	/**
518
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]*)" "(?<id>[^"]*)" was (?<mod>(created|last edited)) "(?<time>[^"]*)"$/
519
	 */
520
	public function aRecordWasLastEditedRelative($type, $id, $mod, $time) {
521
		$class = $this->convertTypeToClass($type);
522
		$fields = $this->prepareFixture($class, $id);
523
		$record = $this->fixtureFactory->createObject($class, $id, $fields);
524
		$date = date("Y-m-d H:i:s",strtotime($time));
525
		$table = $record->baseTable();
526
		$field = ($mod == 'created') ? 'Created' : 'LastEdited';
527
		DB::prepared_query(
528
            "UPDATE \"{$table}\" SET \"{$field}\" = ? WHERE \"ID\" = ?",
529
            [$date, $record->ID]
530
        );
531
		// Support for Versioned extension, by checking for a "Live" stage
532
		if(DB::get_schema()->hasTable($table . '_Live')) {
533
			DB::prepared_query(
534
			    "UPDATE \"{$table}_Live\" SET \"{$field}\" = ? WHERE \"ID\" = ?",
535
				[$date, $record->ID]
536
			);
537
		}
538
	}
539
540
	/**
541
	 * Prepares a fixture for use
542
	 *
543
	 * @param string $class
544
	 * @param string $identifier
545
	 * @param array $data
546
	 * @return array Prepared $data with additional injected fields
547
	 */
548
	protected function prepareFixture($class, $identifier, $data = array()) {
549
		if($class == 'File' || is_subclass_of($class, 'File')) {
550
			$data =  $this->prepareAsset($class, $identifier, $data);
551
		}
552
		return $data;
553
	}
554
555
	protected function prepareAsset($class, $identifier, $data = null) {
556
		if(!$data) $data = array();
557
		$relativeTargetPath = (isset($data['Filename'])) ? $data['Filename'] : $identifier;
558
		$relativeTargetPath = preg_replace('/^' . ASSETS_DIR . '\/?/', '', $relativeTargetPath);
559
		$sourcePath = $this->joinPaths($this->getFilesPath(), basename($relativeTargetPath));
560
561
		// Create file or folder on filesystem
562
		if($class == 'Folder' || is_subclass_of($class, 'Folder')) {
563
			$parent = \Folder::find_or_make($relativeTargetPath);
564
			$data['ID'] = $parent->ID;
565
		} else {
566
			$parent = \Folder::find_or_make(dirname($relativeTargetPath));
567
			if(!file_exists($sourcePath)) {
568
				throw new \InvalidArgumentException(sprintf(
569
					'Source file for "%s" cannot be found in "%s"',
570
					$relativeTargetPath,
571
					$sourcePath
572
				));
573
			}
574
			$data['ParentID'] = $parent->ID;
575
576
			// Load file into APL and retrieve tuple
577
			$asset = $this->getAssetStore()->setFromLocalFile(
578
				$sourcePath,
579
				$relativeTargetPath,
580
				null,
581
				null,
582
				array(
583
					'conflict' => AssetStore::CONFLICT_OVERWRITE,
584
					'visibility' => AssetStore::VISIBILITY_PUBLIC
585
				)
586
			);
587
			$data['FileFilename'] = $asset['Filename'];
588
			$data['FileHash'] = $asset['Hash'];
589
			$data['FileVariant'] = $asset['Variant'];
590
		}
591
		if(!isset($data['Name'])) {
592
			$data['Name'] = basename($relativeTargetPath);
593
		}
594
595
		// Save assets
596
		if(isset($data['FileFilename'])) {
597
			$this->createdAssets[] = $data;
598
		}
599
600
		return $data;
601
	}
602
603
	/**
604
	 *
605
	 * @return AssetStore
606
	 */
607
	protected function getAssetStore() {
608
		return singleton('AssetStore');
609
	}
610
611
	/**
612
	 * Converts a natural language class description to an actual class name.
613
	 * Respects {@link DataObject::$singular_name} variations.
614
	 * Example: "redirector page" -> "RedirectorPage"
615
	 *
616
	 * @param String
617
	 * @return String Class name
618
	 */
619
	protected function convertTypeToClass($type)  {
620
		$type = trim($type);
621
622
		// Try direct mapping
623
		$class = str_replace(' ', '', ucwords($type));
624
		if(class_exists($class) && is_subclass_of($class, 'SilverStripe\\ORM\\DataObject')) {
625
			return \ClassInfo::class_name($class);
626
		}
627
628
		// Fall back to singular names
629
		foreach(array_values(\ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject')) as $candidate) {
630
			if(strcasecmp(singleton($candidate)->singular_name(), $type) === 0) {
631
                return $candidate;
632
            }
633
		}
634
635
		throw new \InvalidArgumentException(sprintf(
636
			'Class "%s" does not exist, or is not a subclass of DataObjet',
637
			$class
638
		));
639
	}
640
641
	/**
642
	 * Updates an object with values, resolving aliases set through
643
	 * {@link DataObject->fieldLabels()}.
644
	 *
645
	 * @param string $class Class name
646
	 * @param array $fields Map of field names or aliases to their values.
647
	 * @return array Map of actual object properties to their values.
648
	 */
649
	protected function convertFields($class, $fields) {
650
		$labels = singleton($class)->fieldLabels();
651
		foreach($fields as $fieldName => $fieldVal) {
652
			if($fieldLabelKey = array_search($fieldName, $labels)) {
653
				unset($fields[$fieldName]);
654
				$fields[$labels[$fieldLabelKey]] = $fieldVal;
655
656
			}
657
		}
658
		return $fields;
659
	}
660
661
	protected function joinPaths() {
662
		$args = func_get_args();
663
		$paths = array();
664
		foreach($args as $arg) $paths = array_merge($paths, (array)$arg);
665
		foreach($paths as &$path) $path = trim($path, '/');
666
		if (substr($args[0], 0, 1) == '/') $paths[0] = '/' . $paths[0];
667
		return join('/', $paths);
668
	}
669
670
}
671