Completed
Pull Request — master (#111)
by Damian
02:20
created

FixtureContext::stepThereShouldBeAFileWithTuple()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 4
rs 10
cc 1
eloc 3
nc 1
nop 2
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
11
// PHPUnit
12
require_once 'PHPUnit/Autoload.php';
13
require_once 'PHPUnit/Framework/Assert/Functions.php';
14
15
/**
16
 * Context used to create fixtures in the SilverStripe ORM.
17
 */
18
class FixtureContext extends BehatContext
19
{
20
	protected $context;
21
22
	/**
23
	 * @var \FixtureFactory
24
	 */
25
	protected $fixtureFactory;
26
27
	/**
28
	 * @var String Absolute path where file fixtures are located.
29
	 * These will automatically get copied to their location
30
	 * declare through the 'Given a file "..."' step defition.
31
	 */
32
	protected $filesPath;
33
34
	/**
35
	 * @var String Tracks all files and folders created from fixtures, for later cleanup.
36
	 */
37
	protected $createdFilesPaths = array();
38
39
	/**
40
	 * @var array Stores the asset tuples.
41
	 */
42
	protected $createdAssets = array();
43
44
	public function __construct(array $parameters) {
45
		$this->context = $parameters;
46
	}
47
48
	public function getSession($name = null) {
49
		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...
50
	}
51
52
	/**
53
	 * @return \FixtureFactory
54
	 */
55
	public function getFixtureFactory() {
56
		if(!$this->fixtureFactory) {
57
			$this->fixtureFactory = \Injector::inst()->create('FixtureFactory', 'FixtureContextFactory');
58
		}
59
		return $this->fixtureFactory;
60
	}
61
62
	/**
63
	 * @param \FixtureFactory $factory
64
	 */
65
	public function setFixtureFactory(\FixtureFactory $factory) {
66
		$this->fixtureFactory = $factory;
67
	}
68
69
	/**
70
	 * @param String
71
	 */
72
	public function setFilesPath($path) {
73
		$this->filesPath = $path;
74
	}
75
76
	/**
77
	 * @return String
78
	 */
79
	public function getFilesPath() {
80
		return $this->filesPath;
81
	}
82
83
	/**
84
	 * @BeforeScenario @database-defaults
85
	 */
86
	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...
87
		\SapphireTest::empty_temp_db();
88
		\DB::getConn()->quiet();
0 ignored issues
show
Deprecated Code introduced by
The method DB::getConn() has been deprecated with message: since version 4.0 Use DB::get_conn instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
89
		$dataClasses = \ClassInfo::subclassesFor('DataObject');
90
		array_shift($dataClasses);
91
		foreach ($dataClasses as $dataClass) {
92
			\singleton($dataClass)->requireDefaultRecords();
93
		}
94
	}
95
96
	/**
97
	 * @AfterScenario
98
	 */
99
	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...
100
		\SapphireTest::empty_temp_db();
101
	}
102
103
	/**
104
	 * @AfterScenario
105
	 */
106
	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...
107
		$store = $this->getAssetStore();
108
		if (is_array($this->createdAssets)) {
109
			foreach ($this->createdAssets as $asset) {
110
				$store->delete($asset['FileFilename'], $asset['FileHash']);
111
			}
112
		}
113
	}
114
115
	/**
116
	 * Example: Given a "page" "Page 1"
117
	 * 
118
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)"$/
119
	 */
120
	public function stepCreateRecord($type, $id) {
121
		$class = $this->convertTypeToClass($type);
122
		$fields = $this->prepareFixture($class, $id);
123
		$this->fixtureFactory->createObject($class, $id, $fields);
124
	}
125
126
	/**
127
	 * Example: Given a "page" "Page 1" has the "content" "My content" 
128
	 * 
129
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" has (?:(an|a|the) )"(?<field>.*)" "(?<value>.*)"$/
130
	 */
131
	public function stepCreateRecordHasField($type, $id, $field, $value) {
132
		$class = $this->convertTypeToClass($type);
133
		$fields = $this->convertFields(
134
			$class,
135
			array($field => $value)
136
		);
137
		// We should check if this fixture object already exists - if it does, we update it. If not, we create it
138 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...
139
			// Merge existing data with new data, and create new object to replace existing object
140
			foreach($fields as $k => $v) {
141
				$existingFixture->$k = $v;
142
			}
143
			$existingFixture->write();
144
		} else {
145
			$this->fixtureFactory->createObject($class, $id, $fields);
146
		}
147
	}
148
   
149
	/**
150
	 * Example: Given a "page" "Page 1" with "URL"="page-1" and "Content"="my page 1" 
151
	 * Example: Given the "page" "Page 1" has "URL"="page-1" and "Content"="my page 1" 
152
	 * 
153
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" (?:(with|has)) (?<data>".*)$/
154
	 */
155
	public function stepCreateRecordWithData($type, $id, $data) {
156
		$class = $this->convertTypeToClass($type);
157
		preg_match_all(
158
			'/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/', 
159
			$data,
160
			$matches
161
		);
162
		$fields = $this->convertFields(
163
			$class,
164
			array_combine($matches['key'], $matches['value'])
165
		);
166
		$fields = $this->prepareFixture($class, $id, $fields);
167
		// We should check if this fixture object already exists - if it does, we update it. If not, we create it
168 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...
169
			// Merge existing data with new data, and create new object to replace existing object
170
			foreach($fields as $k => $v) {
171
				$existingFixture->$k = $v;
172
			}
173
			$existingFixture->write();
174
		} else {
175
			$this->fixtureFactory->createObject($class, $id, $fields);
176
		}
177
	}
178
179
	/**
180
	 * Example: And the "page" "Page 2" has the following data 
181
	 * | Content | <blink> |
182
	 * | My Property | foo |
183
	 * | My Boolean | bar |
184
	 * 
185
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" has the following data$/
186
	 */
187
	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...
188
		$class = $this->convertTypeToClass($type);
189
		// TODO Support more than one record
190
		$fields = $this->convertFields($class, $fieldsTable->getRowsHash());
191
		$fields = $this->prepareFixture($class, $id, $fields);
192
193
		// We should check if this fixture object already exists - if it does, we update it. If not, we create it
194 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...
195
			// Merge existing data with new data, and create new object to replace existing object
196
			foreach($fields as $k => $v) {
197
				$existingFixture->$k = $v;
198
			}
199
			$existingFixture->write();
200
		} else {
201
			$this->fixtureFactory->createObject($class, $id, $fields);
202
		}
203
	}
204
205
	/**
206
	 * Example: Given the "page" "Page 1.1" is a child of the "page" "Page1".
207
	 * Note that this change is not published by default
208
	 * 
209
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" is a (?<relation>[^\s]*) of (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)"/
210
	 */
211
	public function stepUpdateRecordRelation($type, $id, $relation, $relationType, $relationId) {
212
		$class = $this->convertTypeToClass($type);
213
214
		$relationClass = $this->convertTypeToClass($relationType);
215
		$relationObj = $this->fixtureFactory->get($relationClass, $relationId);
216
		if(!$relationObj) $relationObj = $this->fixtureFactory->createObject($relationClass, $relationId);
217
218
		$data = array();
219
		if($relation == 'child') {
220
			$data['ParentID'] = $relationObj->ID;
221
		}
222
223
		$obj = $this->fixtureFactory->get($class, $id);
224
		if($obj) {
225
			$obj->update($data);
226
			$obj->write();
227
		} else {
228
			$obj = $this->fixtureFactory->createObject($class, $id, $data);
229
		}
230
231
		switch($relation) {
232
			case 'parent':
233
				$relationObj->ParentID = $obj->ID;
234
				$relationObj->write();
235
				break;
236
			case 'child':
237
				// already written through $data above
238
				break;
239
			default:
240
				throw new \InvalidArgumentException(sprintf(
241
					'Invalid relation "%s"', $relation
242
				));
243
		}
244
	}
245
246
	/**
247
	 * Assign a type of object to another type of object. The base object will be created if it does not exist already.
248
	 * If the last part of the string (in the "X" relation) is omitted, then the first matching relation will be used.
249
	 *
250
	 * @example I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1"
251
	 * @Given /^I assign (?:(an|a|the) )"(?<type>[^"]+)" "(?<value>[^"]+)" to (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)"$/
252
	 */
253
	public function stepIAssignObjToObj($type, $value, $relationType, $relationId) {
254
		self::stepIAssignObjToObjInTheRelation($type, $value, $relationType, $relationId, null);
255
	}
256
257
 	/**
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...
258
	 * Assign a type of object to another type of object. The base object will be created if it does not exist already.
259
	 * If the last part of the string (in the "X" relation) is omitted, then the first matching relation will be used.
260
	 * Assumption: one object has relationship  (has_one, has_many or many_many ) with the other object
261
	 * 
262
	 * @example I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1" in the "Terms" relation
263
	 * @Given /^I assign (?:(an|a|the) )"(?<type>[^"]+)" "(?<value>[^"]+)" to (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)" in the "(?<relationName>[^"]+)" relation$/
264
	 */
265
	public function stepIAssignObjToObjInTheRelation($type, $value, $relationType, $relationId, $relationName) {
266
		$class = $this->convertTypeToClass($type);
267
		$relationClass = $this->convertTypeToClass($relationType);
268
269
		// Check if this fixture object already exists - if not, we create it
270
		$relationObj = $this->fixtureFactory->get($relationClass, $relationId);
271
		if(!$relationObj) $relationObj = $this->fixtureFactory->createObject($relationClass, $relationId);
272
273
		// Check if there is relationship defined in many_many (includes belongs_many_many)
274
		$manyField = null;
275
		$oneField = null;
276 View Code Duplication
		if ($relationObj->many_many()) {
0 ignored issues
show
Deprecated Code introduced by
The method DataObject::many_many() has been deprecated with message: 4.0 Method has been renamed to manyMany()

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
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...
277
			$manyField = array_search($class, $relationObj->many_many());
0 ignored issues
show
Deprecated Code introduced by
The method DataObject::many_many() has been deprecated with message: 4.0 Method has been renamed to manyMany()

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
278
			if($manyField && strlen($relationName) > 0) $manyField = $relationName;
279
		}
280 View Code Duplication
		if(empty($manyField) && $relationObj->has_many()) {
0 ignored issues
show
Deprecated Code introduced by
The method DataObject::has_many() has been deprecated with message: 4.0 Method has been replaced by hasMany() and hasManyComponent()

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
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...
281
			$manyField = array_search($class, $relationObj->has_many());
0 ignored issues
show
Deprecated Code introduced by
The method DataObject::has_many() has been deprecated with message: 4.0 Method has been replaced by hasMany() and hasManyComponent()

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
282
			if($manyField && strlen($relationName) > 0) $manyField = $relationName;
283
		}
284 View Code Duplication
		if(empty($manyField) && $relationObj->has_one()) {
0 ignored issues
show
Deprecated Code introduced by
The method DataObject::has_one() has been deprecated with message: 4.0 Method has been replaced by hasOne() and hasOneComponent()

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
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...
285
			$oneField = array_search($class, $relationObj->has_one());
0 ignored issues
show
Deprecated Code introduced by
The method DataObject::has_one() has been deprecated with message: 4.0 Method has been replaced by hasOne() and hasOneComponent()

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
286
			if($oneField && strlen($relationName) > 0) $oneField = $relationName;
287
		}
288
		if(empty($manyField) && empty($oneField)) {
289
			throw new \Exception("'$relationClass' has no relationship (has_one, has_many and many_many) with '$class'!");
290
		}
291
292
		// Get the searchable field to check if the fixture object already exists 
293
		$temObj = new $class;
294
		if(isset($temObj->Name)) $field = "Name";
295
		else if(isset($temObj->Title)) $field = "Title";
296
		else $field = "ID";
297
298
		// Check if the fixture object exists - if not, we create it
299
		$obj = \DataObject::get($class)->filter($field, $value)->first();
300
		if(!$obj) $obj = $this->fixtureFactory->createObject($class, $value);
301
		// If has_many or many_many, add this fixture object to the relation object
302
		// If has_one, set value to the joint field with this fixture object's ID
303
		if($manyField) {
304
			$relationObj->$manyField()->add($obj);
305
		} else if($oneField) {
306
			// E.g. $has_one = array('PanelOffer' => 'Offer');
307
			// then the join field is PanelOfferID. This is the common rule in the CMS
308
			$relationObj->{$oneField . 'ID'} = $obj->ID;
309
		} 
310
		
311
		$relationObj->write();
312
	}
313
 
314
	 /**
315
	 * Example: Given the "page" "Page 1" is not published 
316
	 * 
317
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" is (?<state>[^"]*)$/
318
	 */
319
	public function stepUpdateRecordState($type, $id, $state) {
320
		$class = $this->convertTypeToClass($type);
321
		$obj = $this->fixtureFactory->get($class, $id);
322
		if(!$obj) {
323
			throw new \InvalidArgumentException(sprintf(
324
				'Can not find record "%s" with identifier "%s"',
325
				$type,
326
				$id
327
			));
328
		}
329
330
		switch($state) {
331
			case 'published':
332
				$obj->publish('Stage', 'Live');
333
				break;
334
			case 'not published':
335
			case 'unpublished':
336
				$oldMode = \Versioned::get_reading_mode();
337
				\Versioned::reading_stage('Live');
338
				$clone = clone $obj;
339
				$clone->delete();
340
				\Versioned::reading_stage($oldMode);
341
				break;
342
			case 'deleted':
343
				$obj->delete();
344
				break;
345
			default:
346
				throw new \InvalidArgumentException(sprintf(
347
					'Invalid state: "%s"', $state
348
				));    
349
		}
350
	}
351
352
	/**
353
	 * Accepts YAML fixture definitions similar to the ones used in SilverStripe unit testing.
354
	 * 
355
	 * Example: Given there are the following member records:
356
	 *  member1:
357
	 *    Email: [email protected]
358
	 *  member2:
359
	 *    Email: [email protected]
360
	 * 
361
	 * @Given /^there are the following ([^\s]*) records$/
362
	 */
363
	public function stepThereAreTheFollowingRecords($dataObject, PyStringNode $string) {
364
		$yaml = array_merge(array($dataObject . ':'), $string->getLines());
365
		$yaml = implode("\n  ", $yaml);
366
367
		// Save fixtures into database
368
		// TODO Run prepareAsset() for each File and Folder record
369
		$yamlFixture = new \YamlFixture($yaml);
370
		$yamlFixture->writeInto($this->getFixtureFactory());
371
	}
372
373
	/**
374
	 * Example: Given a "member" "Admin" belonging to "Admin Group"
375
	 * 
376
	 * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)"$/
377
	 */
378
	public function stepCreateMemberWithGroup($id, $groupId) {
379
		$group = $this->fixtureFactory->get('Group', $groupId);
380
		if(!$group) $group = $this->fixtureFactory->createObject('Group', $groupId);
381
		
382
		$member = $this->fixtureFactory->createObject('Member', $id);
383
		$member->Groups()->add($group);
384
	}
385
386
	/**
387
	 * Example: Given a "member" "Admin" belonging to "Admin Group" with "Email"="[email protected]"
388
	 * 
389
	 * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)" with (?<data>.*)$/
390
	 */
391
	public function stepCreateMemberWithGroupAndData($id, $groupId, $data) {
392
		$class = 'Member';
393
		preg_match_all(
394
			'/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/', 
395
			$data,
396
			$matches
397
		);
398
		$fields = $this->convertFields(
399
			$class,
400
			array_combine($matches['key'], $matches['value'])
401
		);
402
		
403
		$group = $this->fixtureFactory->get('Group', $groupId);
404
		if(!$group) $group = $this->fixtureFactory->createObject('Group', $groupId);
405
406
		$member = $this->fixtureFactory->createObject($class, $id, $fields);
407
		$member->Groups()->add($group);
408
	}
409
410
	/**
411
	 * Example: Given a "group" "Admin" with permissions "Access to 'Pages' section" and "Access to 'Files' section"
412
	 * 
413
	 * @Given /^(?:(an|a|the) )"group" "(?<id>[^"]+)" (?:(with|has)) permissions (?<permissionStr>.*)$/
414
	 */
415
	public function stepCreateGroupWithPermissions($id, $permissionStr) {
416
		// Convert natural language permissions to codes
417
		preg_match_all('/"([^"]+)"/', $permissionStr, $matches);
418
		$permissions = $matches[1];
419
		$codes = \Permission::get_codes(false);
420
421
		$group = $this->fixtureFactory->get('Group', $id);
422
		if(!$group) $group = $this->fixtureFactory->createObject('Group', $id);
423
		
424
		foreach($permissions as $permission) {
425
			$found = false;
426
			foreach($codes as $code => $details) {
427
				if(
428
					$permission == $code
429
					|| $permission == $details['name']
430
				) {
431
					\Permission::grant($group->ID, $code);
432
					$found = true;
433
				}
434
			}
435
			if(!$found) {
436
				throw new \InvalidArgumentException(sprintf(
437
					'No permission found for "%s"', $permission
438
				));    
439
			}
440
		}
441
	}
442
443
	/**
444
	 * Navigates to a record based on its identifier set during fixture creation,
445
	 * using its RelativeLink() method to map the record to a URL.
446
	 * Example: Given I go to the "page" "My Page"
447
	 * 
448
	 * @Given /^I go to (?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)"/
449
	 */
450
	public function stepGoToNamedRecord($type, $id) {
451
		$class = $this->convertTypeToClass($type);
452
		$record = $this->fixtureFactory->get($class, $id);
453
		if(!$record) {
454
			throw new \InvalidArgumentException(sprintf(
455
				'Cannot resolve reference "%s", no matching fixture found',
456
				$id
457
			));
458
		}
459
		if(!$record->hasMethod('RelativeLink')) {
460
			throw new \InvalidArgumentException('URL for record cannot be determined, missing RelativeLink() method');
461
		}
462
463
		$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...
464
	}
465
466
467
	/**
468
	 * Checks that a file or folder exists in the webroot.
469
	 * Example: There should be a file "assets/Uploads/test.jpg"
470
	 * 
471
	 * @Then /^there should be a (?<type>(file|folder) )"(?<path>[^"]*)"/
472
	 */
473
	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...
474
		assertFileExists($this->joinPaths(BASE_PATH, $path));
475
	}
476
477
	/**
478
	 * Checks that a file exists in the asset store with a given filename and hash
479
	 *
480
	 * Example: there should be a filename "Uploads/test.jpg" with hash "59de0c841f"
481
	 *
482
	 * @Then /^there should be a filename "(?<filename>[^"]*)" with hash "(?<sha>[a-fA-Z0-9]+)"/
483
	 */
484
	public function stepThereShouldBeAFileWithTuple($filename, $hash) {
485
		$exists = $this->getAssetStore()->exists($filename, $hash);
486
		assertTrue((bool)$exists, "A file exists with filename $filename and hash $hash");
487
	}
488
489
	/**
490
	 * Replaces fixture references in values with their respective database IDs, 
491
	 * with the notation "=><class>.<identifier>". Example: "=>Page.My Page".
492
	 * 
493
	 * @Transform /^([^"]+)$/
494
	 */
495
	public function lookupFixtureReference($string) {
496
		if(preg_match('/^=>/', $string)) {
497
			list($className, $identifier) = explode('.', preg_replace('/^=>/', '', $string), 2);
498
			$id = $this->fixtureFactory->getId($className, $identifier);
499
			if(!$id) {
500
				throw new \InvalidArgumentException(sprintf(
501
					'Cannot resolve reference "%s", no matching fixture found',
502
					$string
503
				));
504
			}
505
			return $id;
506
		} else {
507
			return $string;
508
		}
509
	}
510
511
	/**
512
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]*)" "(?<id>[^"]*)" was (?<mod>(created|last edited)) "(?<time>[^"]*)"$/
513
	 */
514
	public function aRecordWasLastEditedRelative($type, $id, $mod, $time) {
515
		$class = $this->convertTypeToClass($type);
516
		$fields = $this->prepareFixture($class, $id);
517
		$record = $this->fixtureFactory->createObject($class, $id, $fields);
518
		$date = date("Y-m-d H:i:s",strtotime($time));
519
		$table = \ClassInfo::baseDataClass(get_class($record));
520
		$field = ($mod == 'created') ? 'Created' : 'LastEdited';
521
		\DB::query(sprintf(
522
			'UPDATE "%s" SET "%s" = \'%s\' WHERE "ID" = \'%d\'',
523
			$table,
524
			$field,
525
			$date,
526
			$record->ID
527
		)); 
528
		// Support for Versioned extension, by checking for a "Live" stage
529
		if(\DB::getConn()->hasTable($table . '_Live')) {
0 ignored issues
show
Deprecated Code introduced by
The method DB::getConn() has been deprecated with message: since version 4.0 Use DB::get_conn instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
Deprecated Code introduced by
The method SS_Database::hasTable() has been deprecated with message: since version 4.0 Use DB::get_schema()->hasTable() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
530
			\DB::query(sprintf(
531
				'UPDATE "%s_Live" SET "%s" = \'%s\' WHERE "ID" = \'%d\'',
532
				$table,
533
				$field,
534
				$date,
535
				$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) || !($class == 'DataObject' || is_subclass_of($class, 'DataObject'))) {
625
			return $class;
626
		}
627
628
		// Fall back to singular names
629
		foreach(array_values(\ClassInfo::subclassesFor('DataObject')) as $candidate) {
630
			if(singleton($candidate)->singular_name() == $type) return $candidate;
631
		}
632
633
		throw new \InvalidArgumentException(sprintf(
634
			'Class "%s" does not exist, or is not a subclass of DataObjet',
635
			$class
636
		));
637
	}
638
639
	/**
640
	 * Updates an object with values, resolving aliases set through
641
	 * {@link DataObject->fieldLabels()}.
642
	 * 
643
	 * @param String Class name
644
	 * @param Array Map of field names or aliases to their values.
645
	 * @return Array Map of actual object properties to their values.
646
	 */
647
	protected function convertFields($class, $fields) {
648
		$labels = singleton($class)->fieldLabels();
649
		foreach($fields as $fieldName => $fieldVal) {
650
			if($fieldLabelKey = array_search($fieldName, $labels)) {
651
				unset($fields[$fieldName]);
652
				$fields[$labels[$fieldLabelKey]] = $fieldVal;
653
				
654
			}
655
		}
656
		return $fields;
657
	}
658
659
	protected function joinPaths() {
660
		$args = func_get_args();
661
		$paths = array();
662
		foreach($args as $arg) $paths = array_merge($paths, (array)$arg);
663
		foreach($paths as &$path) $path = trim($path, '/');
664
		if (substr($args[0], 0, 1) == '/') $paths[0] = '/' . $paths[0];
665
		return join('/', $paths);
666
	}
667
   
668
}