Completed
Push — master ( cfd104...361933 )
by Ingo
11:14 queued 07:45
created

FixtureContext::getAssetStore()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 0
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
	public function __construct(array $parameters) {
40
		$this->context = $parameters;
41
	}
42
43
	public function getSession($name = null) {
44
		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...
45
	}
46
47
	/**
48
	 * @return \FixtureFactory
49
	 */
50
	public function getFixtureFactory() {
51
		if(!$this->fixtureFactory) {
52
			$this->fixtureFactory = \Injector::inst()->create('FixtureFactory', 'FixtureContextFactory');
53
		}
54
		return $this->fixtureFactory;
55
	}
56
57
	/**
58
	 * @param \FixtureFactory $factory
59
	 */
60
	public function setFixtureFactory(\FixtureFactory $factory) {
61
		$this->fixtureFactory = $factory;
62
	}
63
64
	/**
65
	 * @param String
66
	 */
67
	public function setFilesPath($path) {
68
		$this->filesPath = $path;
69
	}
70
71
	/**
72
	 * @return String
73
	 */
74
	public function getFilesPath() {
75
		return $this->filesPath;
76
	}
77
78
	/**
79
	 * @BeforeScenario @database-defaults
80
	 */
81
	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...
82
		\SapphireTest::empty_temp_db();
83
		\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...
84
		$dataClasses = \ClassInfo::subclassesFor('DataObject');
85
		array_shift($dataClasses);
86
		foreach ($dataClasses as $dataClass) {
87
			\singleton($dataClass)->requireDefaultRecords();
88
		}
89
	}
90
91
	/**
92
	 * @AfterScenario
93
	 */
94
	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...
95
		\SapphireTest::empty_temp_db();
96
	}
97
98
	/**
99
	 * @AfterScenario
100
	 */
101
	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...
102
		if (is_array($this->createdFilesPaths)) {
103
			$createdFiles = array_reverse($this->createdFilesPaths);
104
			foreach ($createdFiles as $path) {
105
				if (is_dir($path)) {
106
					\Filesystem::removeFolder($path);
107
				} else {
108
					@unlink($path);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
109
				}
110
			}
111
		}
112
	}
113
114
	/**
115
	 * Example: Given a "page" "Page 1"
116
	 * 
117
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)"$/
118
	 */
119
	public function stepCreateRecord($type, $id) {
120
		$class = $this->convertTypeToClass($type);
121
		$fields = $this->prepareFixture($class, $id);
122
		$this->fixtureFactory->createObject($class, $id, $fields);
123
	}
124
125
	/**
126
	 * Example: Given a "page" "Page 1" has the "content" "My content" 
127
	 * 
128
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" has (?:(an|a|the) )"(?<field>.*)" "(?<value>.*)"$/
129
	 */
130
	public function stepCreateRecordHasField($type, $id, $field, $value) {
131
		$class = $this->convertTypeToClass($type);
132
		$fields = $this->convertFields(
133
			$class,
134
			array($field => $value)
135
		);
136
		// We should check if this fixture object already exists - if it does, we update it. If not, we create it
137 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...
138
			// Merge existing data with new data, and create new object to replace existing object
139
			foreach($fields as $k => $v) {
140
				$existingFixture->$k = $v;
141
			}
142
			$existingFixture->write();
143
		} else {
144
			$this->fixtureFactory->createObject($class, $id, $fields);
145
		}
146
	}
147
   
148
	/**
149
	 * Example: Given a "page" "Page 1" with "URL"="page-1" and "Content"="my page 1" 
150
	 * Example: Given the "page" "Page 1" has "URL"="page-1" and "Content"="my page 1" 
151
	 * 
152
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" (?:(with|has)) (?<data>".*)$/
153
	 */
154
	public function stepCreateRecordWithData($type, $id, $data) {
155
		$class = $this->convertTypeToClass($type);
156
		preg_match_all(
157
			'/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/', 
158
			$data,
159
			$matches
160
		);
161
		$fields = $this->convertFields(
162
			$class,
163
			array_combine($matches['key'], $matches['value'])
164
		);
165
		$fields = $this->prepareFixture($class, $id, $fields);
166
		// We should check if this fixture object already exists - if it does, we update it. If not, we create it
167 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...
168
			// Merge existing data with new data, and create new object to replace existing object
169
			foreach($fields as $k => $v) {
170
				$existingFixture->$k = $v;
171
			}
172
			$existingFixture->write();
173
		} else {
174
			$this->fixtureFactory->createObject($class, $id, $fields);
175
		}
176
	}
177
178
	/**
179
	 * Example: And the "page" "Page 2" has the following data 
180
	 * | Content | <blink> |
181
	 * | My Property | foo |
182
	 * | My Boolean | bar |
183
	 * 
184
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" has the following data$/
185
	 */
186
	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...
187
		$class = $this->convertTypeToClass($type);
188
		// TODO Support more than one record
189
		$fields = $this->convertFields($class, $fieldsTable->getRowsHash());
190
		$fields = $this->prepareFixture($class, $id, $fields);
191
192
		// We should check if this fixture object already exists - if it does, we update it. If not, we create it
193 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...
194
			// Merge existing data with new data, and create new object to replace existing object
195
			foreach($fields as $k => $v) {
196
				$existingFixture->$k = $v;
197
			}
198
			$existingFixture->write();
199
		} else {
200
			$this->fixtureFactory->createObject($class, $id, $fields);
201
		}
202
	}
203
204
	/**
205
	 * Example: Given the "page" "Page 1.1" is a child of the "page" "Page1".
206
	 * Note that this change is not published by default
207
	 * 
208
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" is a (?<relation>[^\s]*) of (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)"/
209
	 */
210
	public function stepUpdateRecordRelation($type, $id, $relation, $relationType, $relationId) {
211
		$class = $this->convertTypeToClass($type);
212
213
		$relationClass = $this->convertTypeToClass($relationType);
214
		$relationObj = $this->fixtureFactory->get($relationClass, $relationId);
215
		if(!$relationObj) $relationObj = $this->fixtureFactory->createObject($relationClass, $relationId);
216
217
		$data = array();
218
		if($relation == 'child') {
219
			$data['ParentID'] = $relationObj->ID;
220
		}
221
222
		$obj = $this->fixtureFactory->get($class, $id);
223
		if($obj) {
224
			$obj->update($data);
225
			$obj->write();
226
		} else {
227
			$obj = $this->fixtureFactory->createObject($class, $id, $data);
228
		}
229
230
		switch($relation) {
231
			case 'parent':
232
				$relationObj->ParentID = $obj->ID;
233
				$relationObj->write();
234
				break;
235
			case 'child':
236
				// already written through $data above
237
				break;
238
			default:
239
				throw new \InvalidArgumentException(sprintf(
240
					'Invalid relation "%s"', $relation
241
				));
242
		}
243
	}
244
245
	/**
246
	 * Assign a type of object to another type of object. The base object will be created if it does not exist already.
247
	 * If the last part of the string (in the "X" relation) is omitted, then the first matching relation will be used.
248
	 *
249
	 * @example I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1"
250
	 * @Given /^I assign (?:(an|a|the) )"(?<type>[^"]+)" "(?<value>[^"]+)" to (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)"$/
251
	 */
252
	public function stepIAssignObjToObj($type, $value, $relationType, $relationId) {
253
		self::stepIAssignObjToObjInTheRelation($type, $value, $relationType, $relationId, null);
254
	}
255
256
 	/**
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...
257
	 * Assign a type of object to another type of object. The base object will be created if it does not exist already.
258
	 * If the last part of the string (in the "X" relation) is omitted, then the first matching relation will be used.
259
	 * Assumption: one object has relationship  (has_one, has_many or many_many ) with the other object
260
	 * 
261
	 * @example I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1" in the "Terms" relation
262
	 * @Given /^I assign (?:(an|a|the) )"(?<type>[^"]+)" "(?<value>[^"]+)" to (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)" in the "(?<relationName>[^"]+)" relation$/
263
	 */
264
	public function stepIAssignObjToObjInTheRelation($type, $value, $relationType, $relationId, $relationName) {
265
		$class = $this->convertTypeToClass($type);
266
		$relationClass = $this->convertTypeToClass($relationType);
267
268
		// Check if this fixture object already exists - if not, we create it
269
		$relationObj = $this->fixtureFactory->get($relationClass, $relationId);
270
		if(!$relationObj) $relationObj = $this->fixtureFactory->createObject($relationClass, $relationId);
271
272
		// Check if there is relationship defined in many_many (includes belongs_many_many)
273
		$manyField = null;
274
		$oneField = null;
275 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...
276
			$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...
277
			if($manyField && strlen($relationName) > 0) $manyField = $relationName;
278
		}
279 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...
280
			$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...
281
			if($manyField && strlen($relationName) > 0) $manyField = $relationName;
282
		}
283 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...
284
			$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...
285
			if($oneField && strlen($relationName) > 0) $oneField = $relationName;
286
		}
287
		if(empty($manyField) && empty($oneField)) {
288
			throw new \Exception("'$relationClass' has no relationship (has_one, has_many and many_many) with '$class'!");
289
		}
290
291
		// Get the searchable field to check if the fixture object already exists 
292
		$temObj = new $class;
293
		if(isset($temObj->Name)) $field = "Name";
294
		else if(isset($temObj->Title)) $field = "Title";
295
		else $field = "ID";
296
297
		// Check if the fixture object exists - if not, we create it
298
		$obj = \DataObject::get($class)->filter($field, $value)->first();
299
		if(!$obj) $obj = $this->fixtureFactory->createObject($class, $value);
300
		// If has_many or many_many, add this fixture object to the relation object
301
		// If has_one, set value to the joint field with this fixture object's ID
302
		if($manyField) {
303
			$relationObj->$manyField()->add($obj);
304
		} else if($oneField) {
305
			// E.g. $has_one = array('PanelOffer' => 'Offer');
306
			// then the join field is PanelOfferID. This is the common rule in the CMS
307
			$relationObj->{$oneField . 'ID'} = $obj->ID;
308
		} 
309
		
310
		$relationObj->write();
311
	}
312
 
313
	 /**
314
	 * Example: Given the "page" "Page 1" is not published 
315
	 * 
316
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" is (?<state>[^"]*)$/
317
	 */
318
	public function stepUpdateRecordState($type, $id, $state) {
319
		$class = $this->convertTypeToClass($type);
320
		$obj = $this->fixtureFactory->get($class, $id);
321
		if(!$obj) {
322
			throw new \InvalidArgumentException(sprintf(
323
				'Can not find record "%s" with identifier "%s"',
324
				$type,
325
				$id
326
			));
327
		}
328
329
		switch($state) {
330
			case 'published':
331
				$obj->publish('Stage', 'Live');
332
				break;
333
			case 'not published':
334
			case 'unpublished':
335
				$oldMode = \Versioned::get_reading_mode();
336
				\Versioned::reading_stage('Live');
337
				$clone = clone $obj;
338
				$clone->delete();
339
				\Versioned::reading_stage($oldMode);
340
				break;
341
			case 'deleted':
342
				$obj->delete();
343
				break;
344
			default:
345
				throw new \InvalidArgumentException(sprintf(
346
					'Invalid state: "%s"', $state
347
				));    
348
		}
349
	}
350
351
	/**
352
	 * Accepts YAML fixture definitions similar to the ones used in SilverStripe unit testing.
353
	 * 
354
	 * Example: Given there are the following member records:
355
	 *  member1:
356
	 *    Email: [email protected]
357
	 *  member2:
358
	 *    Email: [email protected]
359
	 * 
360
	 * @Given /^there are the following ([^\s]*) records$/
361
	 */
362
	public function stepThereAreTheFollowingRecords($dataObject, PyStringNode $string) {
363
		$yaml = array_merge(array($dataObject . ':'), $string->getLines());
364
		$yaml = implode("\n  ", $yaml);
365
366
		// Save fixtures into database
367
		// TODO Run prepareAsset() for each File and Folder record
368
		$yamlFixture = new \YamlFixture($yaml);
369
		$yamlFixture->writeInto($this->getFixtureFactory());
370
	}
371
372
	/**
373
	 * Example: Given a "member" "Admin" belonging to "Admin Group"
374
	 * 
375
	 * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)"$/
376
	 */
377
	public function stepCreateMemberWithGroup($id, $groupId) {
378
		$group = $this->fixtureFactory->get('Group', $groupId);
379
		if(!$group) $group = $this->fixtureFactory->createObject('Group', $groupId);
380
		
381
		$member = $this->fixtureFactory->createObject('Member', $id);
382
		$member->Groups()->add($group);
383
	}
384
385
	/**
386
	 * Example: Given a "member" "Admin" belonging to "Admin Group" with "Email"="[email protected]"
387
	 * 
388
	 * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)" with (?<data>.*)$/
389
	 */
390
	public function stepCreateMemberWithGroupAndData($id, $groupId, $data) {
391
		$class = 'Member';
392
		preg_match_all(
393
			'/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/', 
394
			$data,
395
			$matches
396
		);
397
		$fields = $this->convertFields(
398
			$class,
399
			array_combine($matches['key'], $matches['value'])
400
		);
401
		
402
		$group = $this->fixtureFactory->get('Group', $groupId);
403
		if(!$group) $group = $this->fixtureFactory->createObject('Group', $groupId);
404
405
		$member = $this->fixtureFactory->createObject($class, $id, $fields);
406
		$member->Groups()->add($group);
407
	}
408
409
	/**
410
	 * Example: Given a "group" "Admin" with permissions "Access to 'Pages' section" and "Access to 'Files' section"
411
	 * 
412
	 * @Given /^(?:(an|a|the) )"group" "(?<id>[^"]+)" (?:(with|has)) permissions (?<permissionStr>.*)$/
413
	 */
414
	public function stepCreateGroupWithPermissions($id, $permissionStr) {
415
		// Convert natural language permissions to codes
416
		preg_match_all('/"([^"]+)"/', $permissionStr, $matches);
417
		$permissions = $matches[1];
418
		$codes = \Permission::get_codes(false);
419
420
		$group = $this->fixtureFactory->get('Group', $id);
421
		if(!$group) $group = $this->fixtureFactory->createObject('Group', $id);
422
		
423
		foreach($permissions as $permission) {
424
			$found = false;
425
			foreach($codes as $code => $details) {
426
				if(
427
					$permission == $code
428
					|| $permission == $details['name']
429
				) {
430
					\Permission::grant($group->ID, $code);
431
					$found = true;
432
				}
433
			}
434
			if(!$found) {
435
				throw new \InvalidArgumentException(sprintf(
436
					'No permission found for "%s"', $permission
437
				));    
438
			}
439
		}
440
	}
441
442
	/**
443
	 * Navigates to a record based on its identifier set during fixture creation,
444
	 * using its RelativeLink() method to map the record to a URL.
445
	 * Example: Given I go to the "page" "My Page"
446
	 * 
447
	 * @Given /^I go to (?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)"/
448
	 */
449
	public function stepGoToNamedRecord($type, $id) {
450
		$class = $this->convertTypeToClass($type);
451
		$record = $this->fixtureFactory->get($class, $id);
452
		if(!$record) {
453
			throw new \InvalidArgumentException(sprintf(
454
				'Cannot resolve reference "%s", no matching fixture found',
455
				$id
456
			));
457
		}
458
		if(!$record->hasMethod('RelativeLink')) {
459
			throw new \InvalidArgumentException('URL for record cannot be determined, missing RelativeLink() method');
460
		}
461
462
		$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...
463
	}
464
465
466
	/**
467
	 * Checks that a file or folder exists in the webroot.
468
	 * Example: There should be a file "assets/Uploads/test.jpg"
469
	 * 
470
	 * @Then /^there should be a (?<type>(file|folder) )"(?<path>[^"]*)"/
471
	 */
472
	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...
473
		assertFileExists($this->joinPaths(BASE_PATH, $path));
474
	}
475
476
	/**
477
	 * Replaces fixture references in values with their respective database IDs, 
478
	 * with the notation "=><class>.<identifier>". Example: "=>Page.My Page".
479
	 * 
480
	 * @Transform /^([^"]+)$/
481
	 */
482
	public function lookupFixtureReference($string) {
483
		if(preg_match('/^=>/', $string)) {
484
			list($className, $identifier) = explode('.', preg_replace('/^=>/', '', $string), 2);
485
			$id = $this->fixtureFactory->getId($className, $identifier);
486
			if(!$id) {
487
				throw new \InvalidArgumentException(sprintf(
488
					'Cannot resolve reference "%s", no matching fixture found',
489
					$string
490
				));
491
			}
492
			return $id;
493
		} else {
494
			return $string;
495
		}
496
	}
497
498
	/**
499
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]*)" "(?<id>[^"]*)" was (?<mod>(created|last edited)) "(?<time>[^"]*)"$/
500
	 */
501
	public function aRecordWasLastEditedRelative($type, $id, $mod, $time) {
502
		$class = $this->convertTypeToClass($type);
503
		$fields = $this->prepareFixture($class, $id);
504
		$record = $this->fixtureFactory->createObject($class, $id, $fields);
505
		$date = date("Y-m-d H:i:s",strtotime($time));
506
		$table = \ClassInfo::baseDataClass(get_class($record));
507
		$field = ($mod == 'created') ? 'Created' : 'LastEdited';
508
		\DB::query(sprintf(
509
			'UPDATE "%s" SET "%s" = \'%s\' WHERE "ID" = \'%d\'',
510
			$table,
511
			$field,
512
			$date,
513
			$record->ID
514
		)); 
515
		// Support for Versioned extension, by checking for a "Live" stage
516
		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...
517
			\DB::query(sprintf(
518
				'UPDATE "%s_Live" SET "%s" = \'%s\' WHERE "ID" = \'%d\'',
519
				$table,
520
				$field,
521
				$date,
522
				$record->ID
523
			)); 
524
		}
525
	}
526
	
527
	/**
528
	 * Prepares a fixture for use
529
	 * 
530
	 * @param string $class
531
	 * @param string $identifier
532
	 * @param array $data
533
	 * @return array Prepared $data with additional injected fields
534
	 */
535
	protected function prepareFixture($class, $identifier, $data = array()) {
536
		if($class == 'File' || is_subclass_of($class, 'File')) {
537
			$data =  $this->prepareAsset($class, $identifier, $data);
538
		}
539
		return $data;
540
	}
541
542
	protected function prepareAsset($class, $identifier, $data = null) {
543
		if(!$data) $data = array();
544
		$relativeTargetPath = (isset($data['Filename'])) ? $data['Filename'] : $identifier;
545
		$relativeTargetPath = preg_replace('/^' . ASSETS_DIR . '\/?/', '', $relativeTargetPath);
546
		$sourcePath = $this->joinPaths($this->getFilesPath(), basename($relativeTargetPath));
547
		
548
		// Create file or folder on filesystem
549
		$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...
550
		if($class == 'Folder' || is_subclass_of($class, 'Folder')) {
551
			$parent = \Folder::find_or_make($relativeTargetPath);
552
			$targetPath = $this->joinPaths(ASSETS_PATH, $relativeTargetPath);
553
			$data['ID'] = $parent->ID;
554
		} else {
555
			$parent = \Folder::find_or_make(dirname($relativeTargetPath));
556
			if(!file_exists($sourcePath)) {
557
				throw new \InvalidArgumentException(sprintf(
558
					'Source file for "%s" cannot be found in "%s"',
559
					$targetPath,
560
					$sourcePath
561
				));
562
			}
563
			$data['ParentID'] = $parent->ID;
564
			
565
			// Load file into APL and retrieve tuple
566
			$asset = $this->getAssetStore()->setFromLocalFile(
567
				$sourcePath,
568
				$relativeTargetPath,
569
				null,
570
				null,
571
				AssetStore::CONFLICT_OVERWRITE
572
			);
573
			$data['FileFilename'] = $asset['Filename'];
574
			$data['FileHash'] = $asset['Hash'];
575
			$data['FileVariant'] = $asset['Variant'];
576
			
577
			// Strip base from url to get dir relative to base
578
			$url = $this->getAssetStore()->getAsURL($asset['Filename'], $asset['Hash'], $asset['Variant']);
579
			$targetPath = $this->joinPaths(BASE_PATH, substr($url, strlen(\Director::baseURL())));
580
		}
581
		if(!isset($data['Name'])) {
582
			$data['Name'] = basename($relativeTargetPath);
583
		}
584
585
		$this->createdFilesPaths[] = $targetPath;
586
587
		return $data;
588
	}
589
590
	/**
591
	 *
592
	 * @return AssetStore
593
	 */
594
	protected function getAssetStore() {
595
		return singleton('AssetStore');
596
	}
597
598
	/**
599
	 * Converts a natural language class description to an actual class name.
600
	 * Respects {@link DataObject::$singular_name} variations.
601
	 * Example: "redirector page" -> "RedirectorPage"
602
	 * 
603
	 * @param String 
604
	 * @return String Class name
605
	 */
606
	protected function convertTypeToClass($type)  {
607
		$type = trim($type);
608
609
		// Try direct mapping
610
		$class = str_replace(' ', '', ucwords($type));
611
		if(class_exists($class) || !($class == 'DataObject' || is_subclass_of($class, 'DataObject'))) {
612
			return $class;
613
		}
614
615
		// Fall back to singular names
616
		foreach(array_values(\ClassInfo::subclassesFor('DataObject')) as $candidate) {
617
			if(singleton($candidate)->singular_name() == $type) return $candidate;
618
		}
619
620
		throw new \InvalidArgumentException(sprintf(
621
			'Class "%s" does not exist, or is not a subclass of DataObjet',
622
			$class
623
		));
624
	}
625
626
	/**
627
	 * Updates an object with values, resolving aliases set through
628
	 * {@link DataObject->fieldLabels()}.
629
	 * 
630
	 * @param String Class name
631
	 * @param Array Map of field names or aliases to their values.
632
	 * @return Array Map of actual object properties to their values.
633
	 */
634
	protected function convertFields($class, $fields) {
635
		$labels = singleton($class)->fieldLabels();
636
		foreach($fields as $fieldName => $fieldVal) {
637
			if($fieldLabelKey = array_search($fieldName, $labels)) {
638
				unset($fields[$fieldName]);
639
				$fields[$labels[$fieldLabelKey]] = $fieldVal;
640
				
641
			}
642
		}
643
		return $fields;
644
	}
645
646
	protected function joinPaths() {
647
		$args = func_get_args();
648
		$paths = array();
649
		foreach($args as $arg) $paths = array_merge($paths, (array)$arg);
650
		foreach($paths as &$path) $path = trim($path, '/');
651
		if (substr($args[0], 0, 1) == '/') $paths[0] = '/' . $paths[0];
652
		return join('/', $paths);
653
	}
654
   
655
}