FixtureContext::stepIAssignObjToObjInTheRelation()   F
last analyzed

Complexity

Conditions 20
Paths 1026

Size

Total Lines 63
Code Lines 37

Duplication

Lines 12
Ratio 19.05 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 20
eloc 37
c 3
b 1
f 0
nc 1026
nop 5
dl 12
loc 63
rs 2.9432

How to fix   Long Method    Complexity   

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
use Behat\Behat\Event\ScenarioEvent;
7
use Behat\Gherkin\Node\PyStringNode;
8
use Behat\Gherkin\Node\TableNode;
9
use SilverStripe\Filesystem\Storage\AssetStore;
10
use SilverStripe\ORM\DB;
11
use SilverStripe\ORM\DataObject;
12
use SilverStripe\ORM\Versioning\Versioned;
13
use SilverStripe\Security\Permission;
14
15
// PHPUnit
16
require_once BASE_PATH . '/vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
17
18
/**
19
 * Context used to create fixtures in the SilverStripe ORM.
20
 */
21
class FixtureContext extends BehatContext
22
{
23
    protected $context;
24
25
    /**
26
     * @var \FixtureFactory
27
     */
28
    protected $fixtureFactory;
29
30
    /**
31
     * @var String Absolute path where file fixtures are located.
32
     * These will automatically get copied to their location
33
     * declare through the 'Given a file "..."' step defition.
34
     */
35
    protected $filesPath;
36
37
    /**
38
     * @var String Tracks all files and folders created from fixtures, for later cleanup.
39
     */
40
    protected $createdFilesPaths = array();
41
42
    /**
43
     * @var array Stores the asset tuples.
44
     */
45
    protected $createdAssets = array();
46
47
    public function __construct(array $parameters)
48
    {
49
        $this->context = $parameters;
50
    }
51
52
    public function getSession($name = null)
53
    {
54
        return $this->getMainContext()->getSession($name);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\ExtendedContextInterface as the method getSession() does only exist in the following implementations of said interface: Behat\MinkExtension\Context\MinkContext, Behat\MinkExtension\Context\RawMinkContext, SilverStripe\BehatExtension\Context\BasicContext, SilverStripe\BehatExtension\Context\EmailContext, SilverStripe\BehatExtension\Context\FixtureContext, SilverStripe\BehatExtension\Context\LoginContext, SilverStripe\BehatExtens...ext\SilverStripeContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
301
            $manyField = array_search($class, $relationObj->many_many());
302
            if ($manyField && strlen($relationName) > 0) {
303
                $manyField = $relationName;
304
            }
305
        }
306 View Code Duplication
        if (empty($manyField) && $relationObj->has_many()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
307
            $manyField = array_search($class, $relationObj->has_many());
308
            if ($manyField && strlen($relationName) > 0) {
309
                $manyField = $relationName;
310
            }
311
        }
312 View Code Duplication
        if (empty($manyField) && $relationObj->has_one()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
313
            $oneField = array_search($class, $relationObj->has_one());
314
            if ($oneField && strlen($relationName) > 0) {
315
                $oneField = $relationName;
316
            }
317
        }
318
        if (empty($manyField) && empty($oneField)) {
319
            throw new \Exception("'$relationClass' has no relationship (has_one, has_many and many_many) with '$class'!");
320
        }
321
322
        // Get the searchable field to check if the fixture object already exists
323
        $temObj = new $class;
324
        if (isset($temObj->Name)) {
325
            $field = "Name";
326
        } elseif (isset($temObj->Title)) {
327
            $field = "Title";
328
        } else {
329
            $field = "ID";
330
        }
331
332
        // Check if the fixture object exists - if not, we create it
333
        $obj = DataObject::get($class)->filter($field, $value)->first();
334
        if (!$obj) {
335
            $obj = $this->fixtureFactory->createObject($class, $value);
336
        }
337
        // If has_many or many_many, add this fixture object to the relation object
338
        // If has_one, set value to the joint field with this fixture object's ID
339
        if ($manyField) {
340
            $relationObj->$manyField()->add($obj);
341
        } elseif ($oneField) {
342
            // E.g. $has_one = array('PanelOffer' => 'Offer');
343
            // then the join field is PanelOfferID. This is the common rule in the CMS
344
            $relationObj->{$oneField . 'ID'} = $obj->ID;
345
        }
346
347
        $relationObj->write();
348
    }
349
350
     /**
351
     * Example: Given the "page" "Page 1" is not published
352
     *
353
     * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" is (?<state>[^"]*)$/
354
     */
355
    public function stepUpdateRecordState($type, $id, $state)
356
    {
357
        $class = $this->convertTypeToClass($type);
358
        /** @var DataObject|Versioned $obj */
359
        $obj = $this->fixtureFactory->get($class, $id);
360
        if (!$obj) {
361
            throw new \InvalidArgumentException(sprintf(
362
                'Can not find record "%s" with identifier "%s"',
363
                $type,
364
                $id
365
            ));
366
        }
367
368
        switch ($state) {
369
            case 'published':
370
                $obj->copyVersionToStage('Stage', 'Live');
0 ignored issues
show
Bug introduced by
The method copyVersionToStage does only exist in SilverStripe\ORM\Versioning\Versioned, but not in SilverStripe\ORM\DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
371
                break;
372
            case 'not published':
373
            case 'unpublished':
374
                $oldMode = Versioned::get_reading_mode();
375
                Versioned::set_stage(Versioned::LIVE);
376
                $clone = clone $obj;
377
                $clone->delete();
0 ignored issues
show
Bug introduced by
The method delete does only exist in SilverStripe\ORM\DataObject, but not in SilverStripe\ORM\Versioning\Versioned.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
378
                Versioned::set_reading_mode($oldMode);
379
                break;
380
            case 'deleted':
381
                $obj->delete();
382
                break;
383
            default:
384
                throw new \InvalidArgumentException(sprintf(
385
                    'Invalid state: "%s"',
386
                    $state
387
                ));
388
        }
389
    }
390
391
    /**
392
     * Accepts YAML fixture definitions similar to the ones used in SilverStripe unit testing.
393
     *
394
     * Example: Given there are the following member records:
395
     *  member1:
396
     *    Email: [email protected]
397
     *  member2:
398
     *    Email: [email protected]
399
     *
400
     * @Given /^there are the following ([^\s]*) records$/
401
     */
402
    public function stepThereAreTheFollowingRecords($dataObject, PyStringNode $string)
403
    {
404
        $yaml = array_merge(array($dataObject . ':'), $string->getLines());
405
        $yaml = implode("\n  ", $yaml);
406
407
        // Save fixtures into database
408
        // TODO Run prepareAsset() for each File and Folder record
409
        $yamlFixture = new \YamlFixture($yaml);
410
        $yamlFixture->writeInto($this->getFixtureFactory());
411
    }
412
413
    /**
414
     * Example: Given a "member" "Admin" belonging to "Admin Group"
415
     *
416
     * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)"$/
417
     */
418
    public function stepCreateMemberWithGroup($id, $groupId)
419
    {
420
        $group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $groupId);
421
        if (!$group) {
422
            $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $groupId);
423
        }
424
425
        $member = $this->fixtureFactory->createObject('SilverStripe\\Security\\Member', $id);
426
        $member->Groups()->add($group);
427
    }
428
429
    /**
430
     * Example: Given a "member" "Admin" belonging to "Admin Group" with "Email"="[email protected]"
431
     *
432
     * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)" with (?<data>.*)$/
433
     */
434
    public function stepCreateMemberWithGroupAndData($id, $groupId, $data)
435
    {
436
        $class = 'SilverStripe\\Security\\Member';
437
        preg_match_all(
438
            '/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/',
439
            $data,
440
            $matches
441
        );
442
        $fields = $this->convertFields(
443
            $class,
444
            array_combine($matches['key'], $matches['value'])
445
        );
446
447
        $group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $groupId);
448
        if (!$group) {
449
            $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $groupId);
450
        }
451
452
        $member = $this->fixtureFactory->createObject($class, $id, $fields);
453
        $member->Groups()->add($group);
454
    }
455
456
    /**
457
     * Example: Given a "group" "Admin" with permissions "Access to 'Pages' section" and "Access to 'Files' section"
458
     *
459
     * @Given /^(?:(an|a|the) )"group" "(?<id>[^"]+)" (?:(with|has)) permissions (?<permissionStr>.*)$/
460
     */
461
    public function stepCreateGroupWithPermissions($id, $permissionStr)
462
    {
463
        // Convert natural language permissions to codes
464
        preg_match_all('/"([^"]+)"/', $permissionStr, $matches);
465
        $permissions = $matches[1];
466
        $codes = Permission::get_codes(false);
467
468
        $group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $id);
469
        if (!$group) {
470
            $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $id);
471
        }
472
473
        foreach ($permissions as $permission) {
474
            $found = false;
475
            foreach ($codes as $code => $details) {
476
                if ($permission == $code
477
                    || $permission == $details['name']
478
                ) {
479
                    Permission::grant($group->ID, $code);
480
                    $found = true;
481
                }
482
            }
483
            if (!$found) {
484
                throw new \InvalidArgumentException(sprintf(
485
                    'No permission found for "%s"',
486
                    $permission
487
                ));
488
            }
489
        }
490
    }
491
492
    /**
493
     * Navigates to a record based on its identifier set during fixture creation,
494
     * using its RelativeLink() method to map the record to a URL.
495
     * Example: Given I go to the "page" "My Page"
496
     *
497
     * @Given /^I go to (?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)"/
498
     */
499
    public function stepGoToNamedRecord($type, $id)
500
    {
501
        $class = $this->convertTypeToClass($type);
502
        $record = $this->fixtureFactory->get($class, $id);
503
        if (!$record) {
504
            throw new \InvalidArgumentException(sprintf(
505
                'Cannot resolve reference "%s", no matching fixture found',
506
                $id
507
            ));
508
        }
509
        if (!$record->hasMethod('RelativeLink')) {
510
            throw new \InvalidArgumentException('URL for record cannot be determined, missing RelativeLink() method');
511
        }
512
513
        $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...
514
    }
515
516
517
    /**
518
     * Checks that a file or folder exists in the webroot.
519
     * Example: There should be a file "assets/Uploads/test.jpg"
520
     *
521
     * @Then /^there should be a (?<type>(file|folder) )"(?<path>[^"]*)"/
522
     */
523
    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...
524
    {
525
        assertFileExists($this->joinPaths(BASE_PATH, $path));
526
    }
527
528
    /**
529
     * Checks that a file exists in the asset store with a given filename and hash
530
     *
531
     * Example: there should be a filename "Uploads/test.jpg" with hash "59de0c841f"
532
     *
533
     * @Then /^there should be a filename "(?<filename>[^"]*)" with hash "(?<hash>[a-fA-Z0-9]+)"/
534
     */
535
    public function stepThereShouldBeAFileWithTuple($filename, $hash)
536
    {
537
        $exists = $this->getAssetStore()->exists($filename, $hash);
538
        assertTrue((bool)$exists, "A file exists with filename $filename and hash $hash");
539
    }
540
541
    /**
542
     * Replaces fixture references in values with their respective database IDs,
543
     * with the notation "=><class>.<identifier>". Example: "=>Page.My Page".
544
     *
545
     * @Transform /^([^"]+)$/
546
     */
547
    public function lookupFixtureReference($string)
548
    {
549
        if (preg_match('/^=>/', $string)) {
550
            list($className, $identifier) = explode('.', preg_replace('/^=>/', '', $string), 2);
551
            $id = $this->fixtureFactory->getId($className, $identifier);
552
            if (!$id) {
553
                throw new \InvalidArgumentException(sprintf(
554
                    'Cannot resolve reference "%s", no matching fixture found',
555
                    $string
556
                ));
557
            }
558
            return $id;
559
        } else {
560
            return $string;
561
        }
562
    }
563
564
    /**
565
     * @Given /^(?:(an|a|the) )"(?<type>[^"]*)" "(?<id>[^"]*)" was (?<mod>(created|last edited)) "(?<time>[^"]*)"$/
566
     */
567
    public function aRecordWasLastEditedRelative($type, $id, $mod, $time)
568
    {
569
        $class = $this->convertTypeToClass($type);
570
        $fields = $this->prepareFixture($class, $id);
571
        $record = $this->fixtureFactory->createObject($class, $id, $fields);
572
        $date = date("Y-m-d H:i:s", strtotime($time));
573
        $table = $record->baseTable();
574
        $field = ($mod == 'created') ? 'Created' : 'LastEdited';
575
        DB::prepared_query(
576
            "UPDATE \"{$table}\" SET \"{$field}\" = ? WHERE \"ID\" = ?",
577
            [$date, $record->ID]
578
        );
579
        // Support for Versioned extension, by checking for a "Live" stage
580
        if (DB::get_schema()->hasTable($table . '_Live')) {
581
            DB::prepared_query(
582
                "UPDATE \"{$table}_Live\" SET \"{$field}\" = ? WHERE \"ID\" = ?",
583
                [$date, $record->ID]
584
            );
585
        }
586
    }
587
588
    /**
589
     * Prepares a fixture for use
590
     *
591
     * @param string $class
592
     * @param string $identifier
593
     * @param array $data
594
     * @return array Prepared $data with additional injected fields
595
     */
596
    protected function prepareFixture($class, $identifier, $data = array())
597
    {
598
        if ($class == 'File' || is_subclass_of($class, 'File')) {
599
            $data =  $this->prepareAsset($class, $identifier, $data);
600
        }
601
        return $data;
602
    }
603
604
    protected function prepareAsset($class, $identifier, $data = null)
605
    {
606
        if (!$data) {
607
            $data = array();
608
        }
609
        $relativeTargetPath = (isset($data['Filename'])) ? $data['Filename'] : $identifier;
610
        $relativeTargetPath = preg_replace('/^' . ASSETS_DIR . '\/?/', '', $relativeTargetPath);
611
        $sourcePath = $this->joinPaths($this->getFilesPath(), basename($relativeTargetPath));
612
613
        // Create file or folder on filesystem
614
        if ($class == 'Folder' || is_subclass_of($class, 'Folder')) {
615
            $parent = \Folder::find_or_make($relativeTargetPath);
616
            $data['ID'] = $parent->ID;
617
        } else {
618
            $parent = \Folder::find_or_make(dirname($relativeTargetPath));
619
            if (!file_exists($sourcePath)) {
620
                throw new \InvalidArgumentException(sprintf(
621
                    'Source file for "%s" cannot be found in "%s"',
622
                    $relativeTargetPath,
623
                    $sourcePath
624
                ));
625
            }
626
            $data['ParentID'] = $parent->ID;
627
628
            // Load file into APL and retrieve tuple
629
            $asset = $this->getAssetStore()->setFromLocalFile(
630
                $sourcePath,
631
                $relativeTargetPath,
632
                null,
633
                null,
634
                array(
635
                    'conflict' => AssetStore::CONFLICT_OVERWRITE,
636
                    'visibility' => AssetStore::VISIBILITY_PUBLIC
637
                )
638
            );
639
            $data['FileFilename'] = $asset['Filename'];
640
            $data['FileHash'] = $asset['Hash'];
641
            $data['FileVariant'] = $asset['Variant'];
642
        }
643
        if (!isset($data['Name'])) {
644
            $data['Name'] = basename($relativeTargetPath);
645
        }
646
647
        // Save assets
648
        if (isset($data['FileFilename'])) {
649
            $this->createdAssets[] = $data;
650
        }
651
652
        return $data;
653
    }
654
655
    /**
656
     *
657
     * @return AssetStore
658
     */
659
    protected function getAssetStore()
660
    {
661
        return singleton('AssetStore');
662
    }
663
664
    /**
665
     * Converts a natural language class description to an actual class name.
666
     * Respects {@link DataObject::$singular_name} variations.
667
     * Example: "redirector page" -> "RedirectorPage"
668
     *
669
     * @param String
670
     * @return String Class name
671
     */
672
    protected function convertTypeToClass($type)
673
    {
674
        $type = trim($type);
675
676
        // Try direct mapping
677
        $class = str_replace(' ', '', ucwords($type));
678
        if (class_exists($class) && is_subclass_of($class, 'SilverStripe\\ORM\\DataObject')) {
679
            return \ClassInfo::class_name($class);
680
        }
681
682
        // Fall back to singular names
683
        foreach (array_values(\ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject')) as $candidate) {
684
            if (strcasecmp(singleton($candidate)->singular_name(), $type) === 0) {
685
                return $candidate;
686
            }
687
        }
688
689
        throw new \InvalidArgumentException(sprintf(
690
            'Class "%s" does not exist, or is not a subclass of DataObjet',
691
            $class
692
        ));
693
    }
694
695
    /**
696
     * Updates an object with values, resolving aliases set through
697
     * {@link DataObject->fieldLabels()}.
698
     *
699
     * @param string $class Class name
700
     * @param array $fields Map of field names or aliases to their values.
701
     * @return array Map of actual object properties to their values.
702
     */
703
    protected function convertFields($class, $fields)
704
    {
705
        $labels = singleton($class)->fieldLabels();
706
        foreach ($fields as $fieldName => $fieldVal) {
707
            if ($fieldLabelKey = array_search($fieldName, $labels)) {
708
                unset($fields[$fieldName]);
709
                $fields[$labels[$fieldLabelKey]] = $fieldVal;
710
            }
711
        }
712
        return $fields;
713
    }
714
715
    protected function joinPaths()
716
    {
717
        $args = func_get_args();
718
        $paths = array();
719
        foreach ($args as $arg) {
720
            $paths = array_merge($paths, (array)$arg);
721
        }
722
        foreach ($paths as &$path) {
723
            $path = trim($path, '/');
724
        }
725
        if (substr($args[0], 0, 1) == '/') {
726
            $paths[0] = '/' . $paths[0];
727
        }
728
        return join('/', $paths);
729
    }
730
}
731