Completed
Pull Request — master (#123)
by Damian
03:30 queued 49s
created

FixtureContext::stepCreateRecordWithData()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 16

Duplication

Lines 9
Ratio 37.5 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 3
eloc 16
c 3
b 0
f 0
nc 3
nop 3
dl 9
loc 24
rs 8.9713
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"', $relation
261
                ));
262
        }
263
    }
264
265
    /**
266
     * Assign a type of object to another type of object. The base object will be created if it does not exist already.
267
     * If the last part of the string (in the "X" relation) is omitted, then the first matching relation will be used.
268
     *
269
     * @example I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1"
270
     * @Given /^I assign (?:(an|a|the) )"(?<type>[^"]+)" "(?<value>[^"]+)" to (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)"$/
271
     */
272
    public function stepIAssignObjToObj($type, $value, $relationType, $relationId)
273
    {
274
        self::stepIAssignObjToObjInTheRelation($type, $value, $relationType, $relationId, null);
275
    }
276
277
    /**
278
     * Assign a type of object to another type of object. The base object will be created if it does not exist already.
279
     * If the last part of the string (in the "X" relation) is omitted, then the first matching relation will be used.
280
     * Assumption: one object has relationship  (has_one, has_many or many_many ) with the other object
281
     *
282
     * @example I assign the "TaxonomyTerm" "For customers" to the "Page" "Page1" in the "Terms" relation
283
     * @Given /^I assign (?:(an|a|the) )"(?<type>[^"]+)" "(?<value>[^"]+)" to (?:(an|a|the) )"(?<relationType>[^"]+)" "(?<relationId>[^"]+)" in the "(?<relationName>[^"]+)" relation$/
284
     */
285
    public function stepIAssignObjToObjInTheRelation($type, $value, $relationType, $relationId, $relationName)
286
    {
287
        $class = $this->convertTypeToClass($type);
288
        $relationClass = $this->convertTypeToClass($relationType);
289
290
        // Check if this fixture object already exists - if not, we create it
291
        $relationObj = $this->fixtureFactory->get($relationClass, $relationId);
292
        if (!$relationObj) {
293
            $relationObj = $this->fixtureFactory->createObject($relationClass, $relationId);
294
        }
295
296
        // Check if there is relationship defined in many_many (includes belongs_many_many)
297
        $manyField = null;
298
        $oneField = null;
299 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...
300
            $manyField = array_search($class, $relationObj->many_many());
301
            if ($manyField && strlen($relationName) > 0) {
302
                $manyField = $relationName;
303
            }
304
        }
305 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...
306
            $manyField = array_search($class, $relationObj->has_many());
307
            if ($manyField && strlen($relationName) > 0) {
308
                $manyField = $relationName;
309
            }
310
        }
311 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...
312
            $oneField = array_search($class, $relationObj->has_one());
313
            if ($oneField && strlen($relationName) > 0) {
314
                $oneField = $relationName;
315
            }
316
        }
317
        if (empty($manyField) && empty($oneField)) {
318
            throw new \Exception("'$relationClass' has no relationship (has_one, has_many and many_many) with '$class'!");
319
        }
320
321
        // Get the searchable field to check if the fixture object already exists
322
        $temObj = new $class;
323
        if (isset($temObj->Name)) {
324
            $field = "Name";
325
        } elseif (isset($temObj->Title)) {
326
            $field = "Title";
327
        } else {
328
            $field = "ID";
329
        }
330
331
        // Check if the fixture object exists - if not, we create it
332
        $obj = DataObject::get($class)->filter($field, $value)->first();
333
        if (!$obj) {
334
            $obj = $this->fixtureFactory->createObject($class, $value);
335
        }
336
        // If has_many or many_many, add this fixture object to the relation object
337
        // If has_one, set value to the joint field with this fixture object's ID
338
        if ($manyField) {
339
            $relationObj->$manyField()->add($obj);
340
        } elseif ($oneField) {
341
            // E.g. $has_one = array('PanelOffer' => 'Offer');
342
            // then the join field is PanelOfferID. This is the common rule in the CMS
343
            $relationObj->{$oneField . 'ID'} = $obj->ID;
344
        }
345
346
        $relationObj->write();
347
    }
348
349
     /**
350
     * Example: Given the "page" "Page 1" is not published
351
     *
352
     * @Given /^(?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)" is (?<state>[^"]*)$/
353
     */
354
    public function stepUpdateRecordState($type, $id, $state)
355
    {
356
        $class = $this->convertTypeToClass($type);
357
        /** @var DataObject|Versioned $obj */
358
        $obj = $this->fixtureFactory->get($class, $id);
359
        if (!$obj) {
360
            throw new \InvalidArgumentException(sprintf(
361
                'Can not find record "%s" with identifier "%s"',
362
                $type,
363
                $id
364
            ));
365
        }
366
367
        switch ($state) {
368
            case 'published':
369
                $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...
370
                break;
371
            case 'not published':
372
            case 'unpublished':
373
                $oldMode = Versioned::get_reading_mode();
374
                Versioned::set_stage(Versioned::LIVE);
375
                $clone = clone $obj;
376
                $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...
377
                Versioned::set_reading_mode($oldMode);
378
                break;
379
            case 'deleted':
380
                $obj->delete();
381
                break;
382
            default:
383
                throw new \InvalidArgumentException(sprintf(
384
                    'Invalid state: "%s"', $state
385
                ));
386
        }
387
    }
388
389
    /**
390
     * Accepts YAML fixture definitions similar to the ones used in SilverStripe unit testing.
391
     *
392
     * Example: Given there are the following member records:
393
     *  member1:
394
     *    Email: [email protected]
395
     *  member2:
396
     *    Email: [email protected]
397
     *
398
     * @Given /^there are the following ([^\s]*) records$/
399
     */
400
    public function stepThereAreTheFollowingRecords($dataObject, PyStringNode $string)
401
    {
402
        $yaml = array_merge(array($dataObject . ':'), $string->getLines());
403
        $yaml = implode("\n  ", $yaml);
404
405
        // Save fixtures into database
406
        // TODO Run prepareAsset() for each File and Folder record
407
        $yamlFixture = new \YamlFixture($yaml);
408
        $yamlFixture->writeInto($this->getFixtureFactory());
409
    }
410
411
    /**
412
     * Example: Given a "member" "Admin" belonging to "Admin Group"
413
     *
414
     * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)"$/
415
     */
416
    public function stepCreateMemberWithGroup($id, $groupId)
417
    {
418
        $group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $groupId);
419
        if (!$group) {
420
            $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $groupId);
421
        }
422
423
        $member = $this->fixtureFactory->createObject('SilverStripe\\Security\\Member', $id);
424
        $member->Groups()->add($group);
425
    }
426
427
    /**
428
     * Example: Given a "member" "Admin" belonging to "Admin Group" with "Email"="[email protected]"
429
     *
430
     * @Given /^(?:(an|a|the) )"member" "(?<id>[^"]+)" belonging to "(?<groupId>[^"]+)" with (?<data>.*)$/
431
     */
432
    public function stepCreateMemberWithGroupAndData($id, $groupId, $data)
433
    {
434
        $class = 'SilverStripe\\Security\\Member';
435
        preg_match_all(
436
            '/"(?<key>[^"]+)"\s*=\s*"(?<value>[^"]+)"/',
437
            $data,
438
            $matches
439
        );
440
        $fields = $this->convertFields(
441
            $class,
442
            array_combine($matches['key'], $matches['value'])
443
        );
444
445
        $group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $groupId);
446
        if (!$group) {
447
            $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $groupId);
448
        }
449
450
        $member = $this->fixtureFactory->createObject($class, $id, $fields);
451
        $member->Groups()->add($group);
452
    }
453
454
    /**
455
     * Example: Given a "group" "Admin" with permissions "Access to 'Pages' section" and "Access to 'Files' section"
456
     *
457
     * @Given /^(?:(an|a|the) )"group" "(?<id>[^"]+)" (?:(with|has)) permissions (?<permissionStr>.*)$/
458
     */
459
    public function stepCreateGroupWithPermissions($id, $permissionStr)
460
    {
461
        // Convert natural language permissions to codes
462
        preg_match_all('/"([^"]+)"/', $permissionStr, $matches);
463
        $permissions = $matches[1];
464
        $codes = Permission::get_codes(false);
465
466
        $group = $this->fixtureFactory->get('SilverStripe\\Security\\Group', $id);
467
        if (!$group) {
468
            $group = $this->fixtureFactory->createObject('SilverStripe\\Security\\Group', $id);
469
        }
470
471
        foreach ($permissions as $permission) {
472
            $found = false;
473
            foreach ($codes as $code => $details) {
474
                if (
475
                    $permission == $code
476
                    || $permission == $details['name']
477
                ) {
478
                    Permission::grant($group->ID, $code);
479
                    $found = true;
480
                }
481
            }
482
            if (!$found) {
483
                throw new \InvalidArgumentException(sprintf(
484
                    'No permission found for "%s"', $permission
485
                ));
486
            }
487
        }
488
    }
489
490
    /**
491
     * Navigates to a record based on its identifier set during fixture creation,
492
     * using its RelativeLink() method to map the record to a URL.
493
     * Example: Given I go to the "page" "My Page"
494
     *
495
     * @Given /^I go to (?:(an|a|the) )"(?<type>[^"]+)" "(?<id>[^"]+)"/
496
     */
497
    public function stepGoToNamedRecord($type, $id)
498
    {
499
        $class = $this->convertTypeToClass($type);
500
        $record = $this->fixtureFactory->get($class, $id);
501
        if (!$record) {
502
            throw new \InvalidArgumentException(sprintf(
503
                'Cannot resolve reference "%s", no matching fixture found',
504
                $id
505
            ));
506
        }
507
        if (!$record->hasMethod('RelativeLink')) {
508
            throw new \InvalidArgumentException('URL for record cannot be determined, missing RelativeLink() method');
509
        }
510
511
        $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...
512
    }
513
514
515
    /**
516
     * Checks that a file or folder exists in the webroot.
517
     * Example: There should be a file "assets/Uploads/test.jpg"
518
     *
519
     * @Then /^there should be a (?<type>(file|folder) )"(?<path>[^"]*)"/
520
     */
521
    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...
522
    {
523
        assertFileExists($this->joinPaths(BASE_PATH, $path));
524
    }
525
526
    /**
527
     * Checks that a file exists in the asset store with a given filename and hash
528
     *
529
     * Example: there should be a filename "Uploads/test.jpg" with hash "59de0c841f"
530
     *
531
     * @Then /^there should be a filename "(?<filename>[^"]*)" with hash "(?<hash>[a-fA-Z0-9]+)"/
532
     */
533
    public function stepThereShouldBeAFileWithTuple($filename, $hash)
534
    {
535
        $exists = $this->getAssetStore()->exists($filename, $hash);
536
        assertTrue((bool)$exists, "A file exists with filename $filename and hash $hash");
537
    }
538
539
    /**
540
     * Replaces fixture references in values with their respective database IDs,
541
     * with the notation "=><class>.<identifier>". Example: "=>Page.My Page".
542
     *
543
     * @Transform /^([^"]+)$/
544
     */
545
    public function lookupFixtureReference($string)
546
    {
547
        if (preg_match('/^=>/', $string)) {
548
            list($className, $identifier) = explode('.', preg_replace('/^=>/', '', $string), 2);
549
            $id = $this->fixtureFactory->getId($className, $identifier);
550
            if (!$id) {
551
                throw new \InvalidArgumentException(sprintf(
552
                    'Cannot resolve reference "%s", no matching fixture found',
553
                    $string
554
                ));
555
            }
556
            return $id;
557
        } else {
558
            return $string;
559
        }
560
    }
561
562
    /**
563
     * @Given /^(?:(an|a|the) )"(?<type>[^"]*)" "(?<id>[^"]*)" was (?<mod>(created|last edited)) "(?<time>[^"]*)"$/
564
     */
565
    public function aRecordWasLastEditedRelative($type, $id, $mod, $time)
566
    {
567
        $class = $this->convertTypeToClass($type);
568
        $fields = $this->prepareFixture($class, $id);
569
        $record = $this->fixtureFactory->createObject($class, $id, $fields);
570
        $date = date("Y-m-d H:i:s", strtotime($time));
571
        $table = $record->baseTable();
572
        $field = ($mod == 'created') ? 'Created' : 'LastEdited';
573
        DB::prepared_query(
574
            "UPDATE \"{$table}\" SET \"{$field}\" = ? WHERE \"ID\" = ?",
575
            [$date, $record->ID]
576
        );
577
        // Support for Versioned extension, by checking for a "Live" stage
578
        if (DB::get_schema()->hasTable($table . '_Live')) {
579
            DB::prepared_query(
580
                "UPDATE \"{$table}_Live\" SET \"{$field}\" = ? WHERE \"ID\" = ?",
581
                [$date, $record->ID]
582
            );
583
        }
584
    }
585
586
    /**
587
     * Prepares a fixture for use
588
     *
589
     * @param string $class
590
     * @param string $identifier
591
     * @param array $data
592
     * @return array Prepared $data with additional injected fields
593
     */
594
    protected function prepareFixture($class, $identifier, $data = array())
595
    {
596
        if ($class == 'File' || is_subclass_of($class, 'File')) {
597
            $data =  $this->prepareAsset($class, $identifier, $data);
598
        }
599
        return $data;
600
    }
601
602
    protected function prepareAsset($class, $identifier, $data = null)
603
    {
604
        if (!$data) {
605
            $data = array();
606
        }
607
        $relativeTargetPath = (isset($data['Filename'])) ? $data['Filename'] : $identifier;
608
        $relativeTargetPath = preg_replace('/^' . ASSETS_DIR . '\/?/', '', $relativeTargetPath);
609
        $sourcePath = $this->joinPaths($this->getFilesPath(), basename($relativeTargetPath));
610
611
        // Create file or folder on filesystem
612
        if ($class == 'Folder' || is_subclass_of($class, 'Folder')) {
613
            $parent = \Folder::find_or_make($relativeTargetPath);
614
            $data['ID'] = $parent->ID;
615
        } else {
616
            $parent = \Folder::find_or_make(dirname($relativeTargetPath));
617
            if (!file_exists($sourcePath)) {
618
                throw new \InvalidArgumentException(sprintf(
619
                    'Source file for "%s" cannot be found in "%s"',
620
                    $relativeTargetPath,
621
                    $sourcePath
622
                ));
623
            }
624
            $data['ParentID'] = $parent->ID;
625
626
            // Load file into APL and retrieve tuple
627
            $asset = $this->getAssetStore()->setFromLocalFile(
628
                $sourcePath,
629
                $relativeTargetPath,
630
                null,
631
                null,
632
                array(
633
                    'conflict' => AssetStore::CONFLICT_OVERWRITE,
634
                    'visibility' => AssetStore::VISIBILITY_PUBLIC
635
                )
636
            );
637
            $data['FileFilename'] = $asset['Filename'];
638
            $data['FileHash'] = $asset['Hash'];
639
            $data['FileVariant'] = $asset['Variant'];
640
        }
641
        if (!isset($data['Name'])) {
642
            $data['Name'] = basename($relativeTargetPath);
643
        }
644
645
        // Save assets
646
        if (isset($data['FileFilename'])) {
647
            $this->createdAssets[] = $data;
648
        }
649
650
        return $data;
651
    }
652
653
    /**
654
     *
655
     * @return AssetStore
656
     */
657
    protected function getAssetStore()
658
    {
659
        return singleton('AssetStore');
660
    }
661
662
    /**
663
     * Converts a natural language class description to an actual class name.
664
     * Respects {@link DataObject::$singular_name} variations.
665
     * Example: "redirector page" -> "RedirectorPage"
666
     *
667
     * @param String
668
     * @return String Class name
669
     */
670
    protected function convertTypeToClass($type)
671
    {
672
        $type = trim($type);
673
674
        // Try direct mapping
675
        $class = str_replace(' ', '', ucwords($type));
676
        if (class_exists($class) && is_subclass_of($class, 'SilverStripe\\ORM\\DataObject')) {
677
            return \ClassInfo::class_name($class);
678
        }
679
680
        // Fall back to singular names
681
        foreach (array_values(\ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject')) as $candidate) {
682
            if (strcasecmp(singleton($candidate)->singular_name(), $type) === 0) {
683
                return $candidate;
684
            }
685
        }
686
687
        throw new \InvalidArgumentException(sprintf(
688
            'Class "%s" does not exist, or is not a subclass of DataObjet',
689
            $class
690
        ));
691
    }
692
693
    /**
694
     * Updates an object with values, resolving aliases set through
695
     * {@link DataObject->fieldLabels()}.
696
     *
697
     * @param string $class Class name
698
     * @param array $fields Map of field names or aliases to their values.
699
     * @return array Map of actual object properties to their values.
700
     */
701
    protected function convertFields($class, $fields)
702
    {
703
        $labels = singleton($class)->fieldLabels();
704
        foreach ($fields as $fieldName => $fieldVal) {
705
            if ($fieldLabelKey = array_search($fieldName, $labels)) {
706
                unset($fields[$fieldName]);
707
                $fields[$labels[$fieldLabelKey]] = $fieldVal;
708
            }
709
        }
710
        return $fields;
711
    }
712
713
    protected function joinPaths()
714
    {
715
        $args = func_get_args();
716
        $paths = array();
717
        foreach ($args as $arg) {
718
            $paths = array_merge($paths, (array)$arg);
719
        }
720
        foreach ($paths as &$path) {
721
            $path = trim($path, '/');
722
        }
723
        if (substr($args[0], 0, 1) == '/') {
724
            $paths[0] = '/' . $paths[0];
725
        }
726
        return join('/', $paths);
727
    }
728
}
729