Completed
Push — master ( 6ea51d...b6ee21 )
by Sam
02:13
created

FixtureContext   D

Complexity

Total Complexity 103

Size/Duplication

Total Lines 653
Duplicated Lines 5.97 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 17
Bugs 7 Features 0
Metric Value
c 17
b 7
f 0
dl 39
loc 653
rs 4.2532
wmc 103
lcom 1
cbo 11

32 Methods

Rating   Name   Duplication   Size   Complexity  
A stepIAssignObjToObj() 0 3 1
A __construct() 0 3 1
A getSession() 0 3 1
A getFixtureFactory() 0 6 2
A setFixtureFactory() 0 3 1
A setFilesPath() 0 3 1
A getFilesPath() 0 3 1
A beforeDatabaseDefaults() 0 9 2
A afterResetDatabase() 0 3 1
A afterResetAssets() 0 8 3
A stepCreateRecord() 0 5 1
A stepCreateRecordHasField() 9 17 3
A stepCreateRecordWithData() 9 23 3
A stepCreateRecordWithTable() 9 17 3
B stepUpdateRecordRelation() 0 34 6
F stepIAssignObjToObjInTheRelation() 12 48 20
B stepUpdateRecordState() 0 32 6
A stepThereAreTheFollowingRecords() 0 9 1
A stepCreateMemberWithGroup() 0 7 2
A stepCreateMemberWithGroupAndData() 0 18 2
C stepCreateGroupWithPermissions() 0 27 7
A stepGoToNamedRecord() 0 15 3
A stepThereShouldBeAFileOrFolder() 0 3 1
A stepThereShouldBeAFileWithTuple() 0 4 1
A lookupFixtureReference() 0 15 3
B aRecordWasLastEditedRelative() 0 25 3
A prepareFixture() 0 6 3
C prepareAsset() 0 47 8
A getAssetStore() 0 3 1
B convertTypeToClass() 0 21 5
A convertFields() 0 11 3
A joinPaths() 0 8 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

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
		$obj = $this->fixtureFactory->get($class, $id);
327
		if(!$obj) {
328
			throw new \InvalidArgumentException(sprintf(
329
				'Can not find record "%s" with identifier "%s"',
330
				$type,
331
				$id
332
			));
333
		}
334
335
		switch($state) {
336
			case 'published':
337
				$obj->publish('Stage', 'Live');
338
				break;
339
			case 'not published':
340
			case 'unpublished':
341
				$oldMode = Versioned::get_reading_mode();
342
				Versioned::set_stage(Versioned::LIVE);
343
				$clone = clone $obj;
344
				$clone->delete();
345
				Versioned::set_reading_mode($oldMode);
346
				break;
347
			case 'deleted':
348
				$obj->delete();
349
				break;
350
			default:
351
				throw new \InvalidArgumentException(sprintf(
352
					'Invalid state: "%s"', $state
353
				));
354
		}
355
	}
356
357
	/**
358
	 * Accepts YAML fixture definitions similar to the ones used in SilverStripe unit testing.
359
	 *
360
	 * Example: Given there are the following member records:
361
	 *  member1:
362
	 *    Email: [email protected]
363
	 *  member2:
364
	 *    Email: [email protected]
365
	 *
366
	 * @Given /^there are the following ([^\s]*) records$/
367
	 */
368
	public function stepThereAreTheFollowingRecords($dataObject, PyStringNode $string) {
369
		$yaml = array_merge(array($dataObject . ':'), $string->getLines());
370
		$yaml = implode("\n  ", $yaml);
371
372
		// Save fixtures into database
373
		// TODO Run prepareAsset() for each File and Folder record
374
		$yamlFixture = new \YamlFixture($yaml);
375
		$yamlFixture->writeInto($this->getFixtureFactory());
376
	}
377
378
	/**
379
	 * Example: Given a "member" "Admin" belonging to "Admin Group"
380
	 *
381
	 * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)"$/
382
	 */
383
	public function stepCreateMemberWithGroup($id, $groupId) {
384
		$group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $groupId);
385
		if(!$group) $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $groupId);
386
387
		$member = $this->fixtureFactory->createObject('SilverStripe\\Security\\Member', $id);
388
		$member->Groups()->add($group);
389
	}
390
391
	/**
392
	 * Example: Given a "member" "Admin" belonging to "Admin Group" with "Email"="[email protected]"
393
	 *
394
	 * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)" with (?<data>.*)$/
395
	 */
396
	public function stepCreateMemberWithGroupAndData($id, $groupId, $data) {
397
		$class = 'SilverStripe\\Security\\Member';
398
		preg_match_all(
399
			'/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/',
400
			$data,
401
			$matches
402
		);
403
		$fields = $this->convertFields(
404
			$class,
405
			array_combine($matches['key'], $matches['value'])
406
		);
407
408
		$group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $groupId);
409
		if(!$group) $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $groupId);
410
411
		$member = $this->fixtureFactory->createObject($class, $id, $fields);
412
		$member->Groups()->add($group);
413
	}
414
415
	/**
416
	 * Example: Given a "group" "Admin" with permissions "Access to 'Pages' section" and "Access to 'Files' section"
417
	 *
418
	 * @Given /^(?:(an|a|the) )"group" "(?<id>[^"]+)" (?:(with|has)) permissions (?<permissionStr>.*)$/
419
	 */
420
	public function stepCreateGroupWithPermissions($id, $permissionStr) {
421
		// Convert natural language permissions to codes
422
		preg_match_all('/"([^"]+)"/', $permissionStr, $matches);
423
		$permissions = $matches[1];
424
		$codes = Permission::get_codes(false);
425
426
		$group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $id);
427
		if(!$group) $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $id);
428
429
		foreach($permissions as $permission) {
430
			$found = false;
431
			foreach($codes as $code => $details) {
432
				if(
433
					$permission == $code
434
					|| $permission == $details['name']
435
				) {
436
					Permission::grant($group->ID, $code);
437
					$found = true;
438
				}
439
			}
440
			if(!$found) {
441
				throw new \InvalidArgumentException(sprintf(
442
					'No permission found for "%s"', $permission
443
				));
444
			}
445
		}
446
	}
447
448
	/**
449
	 * Navigates to a record based on its identifier set during fixture creation,
450
	 * using its RelativeLink() method to map the record to a URL.
451
	 * Example: Given I go to the "page" "My Page"
452
	 *
453
	 * @Given /^I go to (?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)"/
454
	 */
455
	public function stepGoToNamedRecord($type, $id) {
456
		$class = $this->convertTypeToClass($type);
457
		$record = $this->fixtureFactory->get($class, $id);
458
		if(!$record) {
459
			throw new \InvalidArgumentException(sprintf(
460
				'Cannot resolve reference "%s", no matching fixture found',
461
				$id
462
			));
463
		}
464
		if(!$record->hasMethod('RelativeLink')) {
465
			throw new \InvalidArgumentException('URL for record cannot be determined, missing RelativeLink() method');
466
		}
467
468
		$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...
469
	}
470
471
472
	/**
473
	 * Checks that a file or folder exists in the webroot.
474
	 * Example: There should be a file "assets/Uploads/test.jpg"
475
	 *
476
	 * @Then /^there should be a (?<type>(file|folder) )"(?<path>[^"]*)"/
477
	 */
478
	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...
479
		assertFileExists($this->joinPaths(BASE_PATH, $path));
480
	}
481
482
	/**
483
	 * Checks that a file exists in the asset store with a given filename and hash
484
	 *
485
	 * Example: there should be a filename "Uploads/test.jpg" with hash "59de0c841f"
486
	 *
487
	 * @Then /^there should be a filename "(?<filename>[^"]*)" with hash "(?<hash>[a-fA-Z0-9]+)"/
488
	 */
489
	public function stepThereShouldBeAFileWithTuple($filename, $hash) {
490
		$exists = $this->getAssetStore()->exists($filename, $hash);
491
		assertTrue((bool)$exists, "A file exists with filename $filename and hash $hash");
492
	}
493
494
	/**
495
	 * Replaces fixture references in values with their respective database IDs,
496
	 * with the notation "=><class>.<identifier>". Example: "=>Page.My Page".
497
	 *
498
	 * @Transform /^([^"]+)$/
499
	 */
500
	public function lookupFixtureReference($string) {
501
		if(preg_match('/^=>/', $string)) {
502
			list($className, $identifier) = explode('.', preg_replace('/^=>/', '', $string), 2);
503
			$id = $this->fixtureFactory->getId($className, $identifier);
504
			if(!$id) {
505
				throw new \InvalidArgumentException(sprintf(
506
					'Cannot resolve reference "%s", no matching fixture found',
507
					$string
508
				));
509
			}
510
			return $id;
511
		} else {
512
			return $string;
513
		}
514
	}
515
516
	/**
517
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]*)" "(?<id>[^"]*)" was (?<mod>(created|last edited)) "(?<time>[^"]*)"$/
518
	 */
519
	public function aRecordWasLastEditedRelative($type, $id, $mod, $time) {
520
		$class = $this->convertTypeToClass($type);
521
		$fields = $this->prepareFixture($class, $id);
522
		$record = $this->fixtureFactory->createObject($class, $id, $fields);
523
		$date = date("Y-m-d H:i:s",strtotime($time));
524
		$table = \ClassInfo::baseDataClass(get_class($record));
0 ignored issues
show
Deprecated Code introduced by
The method ClassInfo::baseDataClass() has been deprecated with message: 4.0..5.0

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...
525
		$field = ($mod == 'created') ? 'Created' : 'LastEdited';
526
		DB::query(sprintf(
527
			'UPDATE "%s" SET "%s" = \'%s\' WHERE "ID" = \'%d\'',
528
			$table,
529
			$field,
530
			$date,
531
			$record->ID
532
		));
533
		// Support for Versioned extension, by checking for a "Live" stage
534
		if(DB::getConn()->hasTable($table . '_Live')) {
535
			DB::query(sprintf(
536
				'UPDATE "%s_Live" SET "%s" = \'%s\' WHERE "ID" = \'%d\'',
537
				$table,
538
				$field,
539
				$date,
540
				$record->ID
541
			));
542
		}
543
	}
544
545
	/**
546
	 * Prepares a fixture for use
547
	 *
548
	 * @param string $class
549
	 * @param string $identifier
550
	 * @param array $data
551
	 * @return array Prepared $data with additional injected fields
552
	 */
553
	protected function prepareFixture($class, $identifier, $data = array()) {
554
		if($class == 'File' || is_subclass_of($class, 'File')) {
555
			$data =  $this->prepareAsset($class, $identifier, $data);
556
		}
557
		return $data;
558
	}
559
560
	protected function prepareAsset($class, $identifier, $data = null) {
561
		if(!$data) $data = array();
562
		$relativeTargetPath = (isset($data['Filename'])) ? $data['Filename'] : $identifier;
563
		$relativeTargetPath = preg_replace('/^' . ASSETS_DIR . '\/?/', '', $relativeTargetPath);
564
		$sourcePath = $this->joinPaths($this->getFilesPath(), basename($relativeTargetPath));
565
566
		// Create file or folder on filesystem
567
		if($class == 'Folder' || is_subclass_of($class, 'Folder')) {
568
			$parent = \Folder::find_or_make($relativeTargetPath);
569
			$data['ID'] = $parent->ID;
570
		} else {
571
			$parent = \Folder::find_or_make(dirname($relativeTargetPath));
572
			if(!file_exists($sourcePath)) {
573
				throw new \InvalidArgumentException(sprintf(
574
					'Source file for "%s" cannot be found in "%s"',
575
					$relativeTargetPath,
576
					$sourcePath
577
				));
578
			}
579
			$data['ParentID'] = $parent->ID;
580
581
			// Load file into APL and retrieve tuple
582
			$asset = $this->getAssetStore()->setFromLocalFile(
583
				$sourcePath,
584
				$relativeTargetPath,
585
				null,
586
				null,
587
				array(
588
					'conflict' => AssetStore::CONFLICT_OVERWRITE,
589
					'visibility' => AssetStore::VISIBILITY_PUBLIC
590
				)
591
			);
592
			$data['FileFilename'] = $asset['Filename'];
593
			$data['FileHash'] = $asset['Hash'];
594
			$data['FileVariant'] = $asset['Variant'];
595
		}
596
		if(!isset($data['Name'])) {
597
			$data['Name'] = basename($relativeTargetPath);
598
		}
599
600
		// Save assets
601
		if(isset($data['FileFilename'])) {
602
			$this->createdAssets[] = $data;
603
		}
604
605
		return $data;
606
	}
607
608
	/**
609
	 *
610
	 * @return AssetStore
611
	 */
612
	protected function getAssetStore() {
613
		return singleton('AssetStore');
614
	}
615
616
	/**
617
	 * Converts a natural language class description to an actual class name.
618
	 * Respects {@link DataObject::$singular_name} variations.
619
	 * Example: "redirector page" -> "RedirectorPage"
620
	 *
621
	 * @param String
622
	 * @return String Class name
623
	 */
624
	protected function convertTypeToClass($type)  {
625
		$type = trim($type);
626
627
		// Try direct mapping
628
		$class = str_replace(' ', '', ucwords($type));
629
		if(class_exists($class) && is_subclass_of($class, 'SilverStripe\\ORM\\DataObject')) {
630
			return \ClassInfo::class_name($class);
631
		}
632
633
		// Fall back to singular names
634
		foreach(array_values(\ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject')) as $candidate) {
635
			if(strcasecmp(singleton($candidate)->singular_name(), $type) === 0) {
636
                return $candidate;
637
            }
638
		}
639
640
		throw new \InvalidArgumentException(sprintf(
641
			'Class "%s" does not exist, or is not a subclass of DataObjet',
642
			$class
643
		));
644
	}
645
646
	/**
647
	 * Updates an object with values, resolving aliases set through
648
	 * {@link DataObject->fieldLabels()}.
649
	 *
650
	 * @param string $class Class name
651
	 * @param array $fields Map of field names or aliases to their values.
652
	 * @return array Map of actual object properties to their values.
653
	 */
654
	protected function convertFields($class, $fields) {
655
		$labels = singleton($class)->fieldLabels();
656
		foreach($fields as $fieldName => $fieldVal) {
657
			if($fieldLabelKey = array_search($fieldName, $labels)) {
658
				unset($fields[$fieldName]);
659
				$fields[$labels[$fieldLabelKey]] = $fieldVal;
660
661
			}
662
		}
663
		return $fields;
664
	}
665
666
	protected function joinPaths() {
667
		$args = func_get_args();
668
		$paths = array();
669
		foreach($args as $arg) $paths = array_merge($paths, (array)$arg);
670
		foreach($paths as &$path) $path = trim($path, '/');
671
		if (substr($args[0], 0, 1) == '/') $paths[0] = '/' . $paths[0];
672
		return join('/', $paths);
673
	}
674
675
}
676