Completed
Push — master ( abb31c...1f5f93 )
by Damian
7s
created

FixtureContext::prepareAsset()   B

Complexity

Conditions 7
Paths 20

Size

Total Lines 53
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 4 Features 0
Metric Value
c 5
b 4
f 0
dl 0
loc 53
rs 7.5251
cc 7
eloc 38
nc 20
nop 3

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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['Filename'], $asset['Hash']);
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
	 * Replaces fixture references in values with their respective database IDs, 
479
	 * with the notation "=><class>.<identifier>". Example: "=>Page.My Page".
480
	 * 
481
	 * @Transform /^([^"]+)$/
482
	 */
483
	public function lookupFixtureReference($string) {
484
		if(preg_match('/^=>/', $string)) {
485
			list($className, $identifier) = explode('.', preg_replace('/^=>/', '', $string), 2);
486
			$id = $this->fixtureFactory->getId($className, $identifier);
487
			if(!$id) {
488
				throw new \InvalidArgumentException(sprintf(
489
					'Cannot resolve reference "%s", no matching fixture found',
490
					$string
491
				));
492
			}
493
			return $id;
494
		} else {
495
			return $string;
496
		}
497
	}
498
499
	/**
500
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]*)" "(?<id>[^"]*)" was (?<mod>(created|last edited)) "(?<time>[^"]*)"$/
501
	 */
502
	public function aRecordWasLastEditedRelative($type, $id, $mod, $time) {
503
		$class = $this->convertTypeToClass($type);
504
		$fields = $this->prepareFixture($class, $id);
505
		$record = $this->fixtureFactory->createObject($class, $id, $fields);
506
		$date = date("Y-m-d H:i:s",strtotime($time));
507
		$table = \ClassInfo::baseDataClass(get_class($record));
508
		$field = ($mod == 'created') ? 'Created' : 'LastEdited';
509
		\DB::query(sprintf(
510
			'UPDATE "%s" SET "%s" = \'%s\' WHERE "ID" = \'%d\'',
511
			$table,
512
			$field,
513
			$date,
514
			$record->ID
515
		)); 
516
		// Support for Versioned extension, by checking for a "Live" stage
517
		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...
518
			\DB::query(sprintf(
519
				'UPDATE "%s_Live" SET "%s" = \'%s\' WHERE "ID" = \'%d\'',
520
				$table,
521
				$field,
522
				$date,
523
				$record->ID
524
			)); 
525
		}
526
	}
527
	
528
	/**
529
	 * Prepares a fixture for use
530
	 * 
531
	 * @param string $class
532
	 * @param string $identifier
533
	 * @param array $data
534
	 * @return array Prepared $data with additional injected fields
535
	 */
536
	protected function prepareFixture($class, $identifier, $data = array()) {
537
		if($class == 'File' || is_subclass_of($class, 'File')) {
538
			$data =  $this->prepareAsset($class, $identifier, $data);
539
		}
540
		return $data;
541
	}
542
543
	protected function prepareAsset($class, $identifier, $data = null) {
544
		if(!$data) $data = array();
545
		$relativeTargetPath = (isset($data['Filename'])) ? $data['Filename'] : $identifier;
546
		$relativeTargetPath = preg_replace('/^' . ASSETS_DIR . '\/?/', '', $relativeTargetPath);
547
		$sourcePath = $this->joinPaths($this->getFilesPath(), basename($relativeTargetPath));
548
		
549
		// Create file or folder on filesystem
550
		$parent = null;
0 ignored issues
show
Unused Code introduced by
$parent is not used, you could remove the assignment.

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

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

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

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

Loading history...
551
		if($class == 'Folder' || is_subclass_of($class, 'Folder')) {
552
			$parent = \Folder::find_or_make($relativeTargetPath);
553
			$targetPath = $this->joinPaths(ASSETS_PATH, $relativeTargetPath);
554
			$data['ID'] = $parent->ID;
555
			$this->createdAssets[] = $parent->File->getValue();
556
		} else {
557
			$parent = \Folder::find_or_make(dirname($relativeTargetPath));
558
			$this->createdAssets[] = $parent->File->getValue();
559
			if(!file_exists($sourcePath)) {
560
				throw new \InvalidArgumentException(sprintf(
561
					'Source file for "%s" cannot be found in "%s"',
562
					$targetPath,
563
					$sourcePath
564
				));
565
			}
566
			$data['ParentID'] = $parent->ID;
567
			
568
			// Load file into APL and retrieve tuple
569
			$asset = $this->getAssetStore()->setFromLocalFile(
570
				$sourcePath,
571
				$relativeTargetPath,
572
				null,
573
				null,
574
				array(
575
					'conflict' => AssetStore::CONFLICT_OVERWRITE,
576
					'visibility' => AssetStore::VISIBILITY_PUBLIC
577
				)
578
			);
579
			$data['FileFilename'] = $asset['Filename'];
580
			$data['FileHash'] = $asset['Hash'];
581
			$data['FileVariant'] = $asset['Variant'];
582
			
583
			// Strip base from url to get dir relative to base
584
			$url = $this->getAssetStore()->getAsURL($asset['Filename'], $asset['Hash'], $asset['Variant']);
585
			$targetPath = $this->joinPaths(BASE_PATH, substr($url, strlen(\Director::baseURL())));
586
		}
587
		if(!isset($data['Name'])) {
588
			$data['Name'] = basename($relativeTargetPath);
589
		}
590
591
		$this->createdFilesPaths[] = $targetPath;
592
		$this->createdAssets[] = $data;
593
594
		return $data;
595
	}
596
597
	/**
598
	 *
599
	 * @return AssetStore
600
	 */
601
	protected function getAssetStore() {
602
		return singleton('AssetStore');
603
	}
604
605
	/**
606
	 * Converts a natural language class description to an actual class name.
607
	 * Respects {@link DataObject::$singular_name} variations.
608
	 * Example: "redirector page" -> "RedirectorPage"
609
	 * 
610
	 * @param String 
611
	 * @return String Class name
612
	 */
613
	protected function convertTypeToClass($type)  {
614
		$type = trim($type);
615
616
		// Try direct mapping
617
		$class = str_replace(' ', '', ucwords($type));
618
		if(class_exists($class) || !($class == 'DataObject' || is_subclass_of($class, 'DataObject'))) {
619
			return $class;
620
		}
621
622
		// Fall back to singular names
623
		foreach(array_values(\ClassInfo::subclassesFor('DataObject')) as $candidate) {
624
			if(singleton($candidate)->singular_name() == $type) return $candidate;
625
		}
626
627
		throw new \InvalidArgumentException(sprintf(
628
			'Class "%s" does not exist, or is not a subclass of DataObjet',
629
			$class
630
		));
631
	}
632
633
	/**
634
	 * Updates an object with values, resolving aliases set through
635
	 * {@link DataObject->fieldLabels()}.
636
	 * 
637
	 * @param String Class name
638
	 * @param Array Map of field names or aliases to their values.
639
	 * @return Array Map of actual object properties to their values.
640
	 */
641
	protected function convertFields($class, $fields) {
642
		$labels = singleton($class)->fieldLabels();
643
		foreach($fields as $fieldName => $fieldVal) {
644
			if($fieldLabelKey = array_search($fieldName, $labels)) {
645
				unset($fields[$fieldName]);
646
				$fields[$labels[$fieldLabelKey]] = $fieldVal;
647
				
648
			}
649
		}
650
		return $fields;
651
	}
652
653
	protected function joinPaths() {
654
		$args = func_get_args();
655
		$paths = array();
656
		foreach($args as $arg) $paths = array_merge($paths, (array)$arg);
657
		foreach($paths as &$path) $path = trim($path, '/');
658
		if (substr($args[0], 0, 1) == '/') $paths[0] = '/' . $paths[0];
659
		return join('/', $paths);
660
	}
661
   
662
}