Completed
Push — master ( 427fc3...899ed1 )
by Ingo
02:39
created

FixtureContext   F

Complexity

Total Complexity 104

Size/Duplication

Total Lines 651
Duplicated Lines 5.99 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 16
Bugs 7 Features 0
Metric Value
wmc 104
c 16
b 7
f 0
lcom 1
cbo 16
dl 39
loc 651
rs 1.4178

32 Methods

Rating   Name   Duplication   Size   Complexity  
A stepIAssignObjToObj() 0 3 1
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
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 __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
B stepUpdateRecordState() 0 32 6
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 19 6
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
11
// PHPUnit
12
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
13
14
/**
15
 * Context used to create fixtures in the SilverStripe ORM.
16
 */
17
class FixtureContext extends BehatContext
18
{
19
	protected $context;
20
21
	/**
22
	 * @var \FixtureFactory
23
	 */
24
	protected $fixtureFactory;
25
26
	/**
27
	 * @var String Absolute path where file fixtures are located.
28
	 * These will automatically get copied to their location
29
	 * declare through the 'Given a file "..."' step defition.
30
	 */
31
	protected $filesPath;
32
33
	/**
34
	 * @var String Tracks all files and folders created from fixtures, for later cleanup.
35
	 */
36
	protected $createdFilesPaths = array();
37
38
	/**
39
	 * @var array Stores the asset tuples.
40
	 */
41
	protected $createdAssets = array();
42
43
	public function __construct(array $parameters) {
44
		$this->context = $parameters;
45
	}
46
47
	public function getSession($name = null) {
48
		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...
49
	}
50
51
	/**
52
	 * @return \FixtureFactory
53
	 */
54
	public function getFixtureFactory() {
55
		if(!$this->fixtureFactory) {
56
			$this->fixtureFactory = \Injector::inst()->create('FixtureFactory', 'FixtureContextFactory');
57
		}
58
		return $this->fixtureFactory;
59
	}
60
61
	/**
62
	 * @param \FixtureFactory $factory
63
	 */
64
	public function setFixtureFactory(\FixtureFactory $factory) {
65
		$this->fixtureFactory = $factory;
66
	}
67
68
	/**
69
	 * @param String
70
	 */
71
	public function setFilesPath($path) {
72
		$this->filesPath = $path;
73
	}
74
75
	/**
76
	 * @return String
77
	 */
78
	public function getFilesPath() {
79
		return $this->filesPath;
80
	}
81
82
	/**
83
	 * @BeforeScenario @database-defaults
84
	 */
85
	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...
86
		\SapphireTest::empty_temp_db();
87
		\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...
88
		$dataClasses = \ClassInfo::subclassesFor('DataObject');
89
		array_shift($dataClasses);
90
		foreach ($dataClasses as $dataClass) {
91
			\singleton($dataClass)->requireDefaultRecords();
92
		}
93
	}
94
95
	/**
96
	 * @AfterScenario
97
	 */
98
	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...
99
		\SapphireTest::empty_temp_db();
100
	}
101
102
	/**
103
	 * @AfterScenario
104
	 */
105
	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...
106
		$store = $this->getAssetStore();
107
		if (is_array($this->createdAssets)) {
108
			foreach ($this->createdAssets as $asset) {
109
				$store->delete($asset['FileFilename'], $asset['FileHash']);
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
	 * Checks that a file exists in the asset store with a given filename and hash
478
	 *
479
	 * Example: there should be a filename "Uploads/test.jpg" with hash "59de0c841f"
480
	 *
481
	 * @Then /^there should be a filename "(?<filename>[^"]*)" with hash "(?<hash>[a-fA-Z0-9]+)"/
482
	 */
483
	public function stepThereShouldBeAFileWithTuple($filename, $hash) {
484
		$exists = $this->getAssetStore()->exists($filename, $hash);
485
		assertTrue((bool)$exists, "A file exists with filename $filename and hash $hash");
486
	}
487
488
	/**
489
	 * Replaces fixture references in values with their respective database IDs, 
490
	 * with the notation "=><class>.<identifier>". Example: "=>Page.My Page".
491
	 * 
492
	 * @Transform /^([^"]+)$/
493
	 */
494
	public function lookupFixtureReference($string) {
495
		if(preg_match('/^=>/', $string)) {
496
			list($className, $identifier) = explode('.', preg_replace('/^=>/', '', $string), 2);
497
			$id = $this->fixtureFactory->getId($className, $identifier);
498
			if(!$id) {
499
				throw new \InvalidArgumentException(sprintf(
500
					'Cannot resolve reference "%s", no matching fixture found',
501
					$string
502
				));
503
			}
504
			return $id;
505
		} else {
506
			return $string;
507
		}
508
	}
509
510
	/**
511
	 * @Given /^(?:(an|a|the) )"(?<type>[^"]*)" "(?<id>[^"]*)" was (?<mod>(created|last edited)) "(?<time>[^"]*)"$/
512
	 */
513
	public function aRecordWasLastEditedRelative($type, $id, $mod, $time) {
514
		$class = $this->convertTypeToClass($type);
515
		$fields = $this->prepareFixture($class, $id);
516
		$record = $this->fixtureFactory->createObject($class, $id, $fields);
517
		$date = date("Y-m-d H:i:s",strtotime($time));
518
		$table = \ClassInfo::baseDataClass(get_class($record));
519
		$field = ($mod == 'created') ? 'Created' : 'LastEdited';
520
		\DB::query(sprintf(
521
			'UPDATE "%s" SET "%s" = \'%s\' WHERE "ID" = \'%d\'',
522
			$table,
523
			$field,
524
			$date,
525
			$record->ID
526
		)); 
527
		// Support for Versioned extension, by checking for a "Live" stage
528
		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...
529
			\DB::query(sprintf(
530
				'UPDATE "%s_Live" SET "%s" = \'%s\' WHERE "ID" = \'%d\'',
531
				$table,
532
				$field,
533
				$date,
534
				$record->ID
535
			)); 
536
		}
537
	}
538
	
539
	/**
540
	 * Prepares a fixture for use
541
	 * 
542
	 * @param string $class
543
	 * @param string $identifier
544
	 * @param array $data
545
	 * @return array Prepared $data with additional injected fields
546
	 */
547
	protected function prepareFixture($class, $identifier, $data = array()) {
548
		if($class == 'File' || is_subclass_of($class, 'File')) {
549
			$data =  $this->prepareAsset($class, $identifier, $data);
550
		}
551
		return $data;
552
	}
553
554
	protected function prepareAsset($class, $identifier, $data = null) {
555
		if(!$data) $data = array();
556
		$relativeTargetPath = (isset($data['Filename'])) ? $data['Filename'] : $identifier;
557
		$relativeTargetPath = preg_replace('/^' . ASSETS_DIR . '\/?/', '', $relativeTargetPath);
558
		$sourcePath = $this->joinPaths($this->getFilesPath(), basename($relativeTargetPath));
559
		
560
		// Create file or folder on filesystem
561
		if($class == 'Folder' || is_subclass_of($class, 'Folder')) {
562
			$parent = \Folder::find_or_make($relativeTargetPath);
563
			$data['ID'] = $parent->ID;
564
		} else {
565
			$parent = \Folder::find_or_make(dirname($relativeTargetPath));
566
			if(!file_exists($sourcePath)) {
567
				throw new \InvalidArgumentException(sprintf(
568
					'Source file for "%s" cannot be found in "%s"',
569
					$relativeTargetPath,
570
					$sourcePath
571
				));
572
			}
573
			$data['ParentID'] = $parent->ID;
574
			
575
			// Load file into APL and retrieve tuple
576
			$asset = $this->getAssetStore()->setFromLocalFile(
577
				$sourcePath,
578
				$relativeTargetPath,
579
				null,
580
				null,
581
				array(
582
					'conflict' => AssetStore::CONFLICT_OVERWRITE,
583
					'visibility' => AssetStore::VISIBILITY_PUBLIC
584
				)
585
			);
586
			$data['FileFilename'] = $asset['Filename'];
587
			$data['FileHash'] = $asset['Hash'];
588
			$data['FileVariant'] = $asset['Variant'];
589
		}
590
		if(!isset($data['Name'])) {
591
			$data['Name'] = basename($relativeTargetPath);
592
		}
593
594
		// Save assets
595
		if(isset($data['FileFilename'])) {
596
			$this->createdAssets[] = $data;
597
		}
598
599
		return $data;
600
	}
601
602
	/**
603
	 *
604
	 * @return AssetStore
605
	 */
606
	protected function getAssetStore() {
607
		return singleton('AssetStore');
608
	}
609
610
	/**
611
	 * Converts a natural language class description to an actual class name.
612
	 * Respects {@link DataObject::$singular_name} variations.
613
	 * Example: "redirector page" -> "RedirectorPage"
614
	 * 
615
	 * @param String 
616
	 * @return String Class name
617
	 */
618
	protected function convertTypeToClass($type)  {
619
		$type = trim($type);
620
621
		// Try direct mapping
622
		$class = str_replace(' ', '', ucwords($type));
623
		if(class_exists($class) || !($class == 'DataObject' || is_subclass_of($class, 'DataObject'))) {
624
			return $class;
625
		}
626
627
		// Fall back to singular names
628
		foreach(array_values(\ClassInfo::subclassesFor('DataObject')) as $candidate) {
629
			if(singleton($candidate)->singular_name() == $type) return $candidate;
630
		}
631
632
		throw new \InvalidArgumentException(sprintf(
633
			'Class "%s" does not exist, or is not a subclass of DataObjet',
634
			$class
635
		));
636
	}
637
638
	/**
639
	 * Updates an object with values, resolving aliases set through
640
	 * {@link DataObject->fieldLabels()}.
641
	 * 
642
	 * @param String Class name
643
	 * @param Array Map of field names or aliases to their values.
644
	 * @return Array Map of actual object properties to their values.
645
	 */
646
	protected function convertFields($class, $fields) {
647
		$labels = singleton($class)->fieldLabels();
648
		foreach($fields as $fieldName => $fieldVal) {
649
			if($fieldLabelKey = array_search($fieldName, $labels)) {
650
				unset($fields[$fieldName]);
651
				$fields[$labels[$fieldLabelKey]] = $fieldVal;
652
				
653
			}
654
		}
655
		return $fields;
656
	}
657
658
	protected function joinPaths() {
659
		$args = func_get_args();
660
		$paths = array();
661
		foreach($args as $arg) $paths = array_merge($paths, (array)$arg);
662
		foreach($paths as &$path) $path = trim($path, '/');
663
		if (substr($args[0], 0, 1) == '/') $paths[0] = '/' . $paths[0];
664
		return join('/', $paths);
665
	}
666
   
667
}