Completed
Push — master ( bd90c3...f3164d )
by Jonathan
9s
created

DrupalExtension/Context/RawDrupalContext.php (2 issues)

strict.coding_against_concrete_implementation

Bug Minor

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Drupal\DrupalExtension\Context;
4
5
use Behat\MinkExtension\Context\RawMinkContext;
6
use Behat\Mink\Exception\DriverException;
7
use Behat\Testwork\Hook\HookDispatcher;
8
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
9
10
use Drupal\DrupalDriverManager;
11
use Drupal\DrupalExtension\DrupalParametersTrait;
12
use Drupal\DrupalExtension\Manager\DrupalAuthenticationManagerInterface;
13
use Drupal\DrupalExtension\Manager\DrupalUserManagerInterface;
14
15
use Drupal\DrupalExtension\Hook\Scope\AfterLanguageEnableScope;
16
use Drupal\DrupalExtension\Hook\Scope\AfterNodeCreateScope;
17
use Drupal\DrupalExtension\Hook\Scope\AfterTermCreateScope;
18
use Drupal\DrupalExtension\Hook\Scope\AfterUserCreateScope;
19
use Drupal\DrupalExtension\Hook\Scope\BaseEntityScope;
20
use Drupal\DrupalExtension\Hook\Scope\BeforeLanguageEnableScope;
21
use Drupal\DrupalExtension\Hook\Scope\BeforeNodeCreateScope;
22
use Drupal\DrupalExtension\Hook\Scope\BeforeUserCreateScope;
23
use Drupal\DrupalExtension\Hook\Scope\BeforeTermCreateScope;
24
use Drupal\DrupalExtension\Manager\FastLogoutInterface;
25
26
/**
27
 * Provides the raw functionality for interacting with Drupal.
28
 */
29
class RawDrupalContext extends RawMinkContext implements DrupalAwareInterface
30
{
31
32
    use DrupalParametersTrait;
33
34
  /**
35
   * Drupal driver manager.
36
   *
37
   * @var \Drupal\DrupalDriverManager
38
   */
39
    private $drupal;
40
41
  /**
42
   * Event dispatcher object.
43
   *
44
   * @var \Behat\Testwork\Hook\HookDispatcher
45
   */
46
    protected $dispatcher;
47
48
  /**
49
   * Drupal authentication manager.
50
   *
51
   * @var \Drupal\DrupalExtension\Manager\DrupalAuthenticationManagerInterface
52
   */
53
    protected $authenticationManager;
54
55
  /**
56
   * Drupal user manager.
57
   *
58
   * @var \Drupal\DrupalExtension\Manager\DrupalUserManagerInterface
59
   */
60
    protected $userManager;
61
62
  /**
63
   * Keep track of nodes so they can be cleaned up.
64
   *
65
   * @var array
66
   */
67
    protected $nodes = array();
68
69
  /**
70
   * Keep track of all terms that are created so they can easily be removed.
71
   *
72
   * @var array
73
   */
74
    protected $terms = array();
75
76
  /**
77
   * Keep track of any roles that are created so they can easily be removed.
78
   *
79
   * @var array
80
   */
81
    protected $roles = array();
82
83
  /**
84
   * Keep track of any languages that are created so they can easily be removed.
85
   *
86
   * @var array
87
   */
88
    protected $languages = array();
89
90
  /**
91
   * {@inheritDoc}
92
   */
93
    public function setDrupal(DrupalDriverManager $drupal)
94
    {
95
        $this->drupal = $drupal;
96
    }
97
98
  /**
99
   * {@inheritDoc}
100
   */
101
    public function getDrupal()
102
    {
103
        return $this->drupal;
104
    }
105
106
  /**
107
   * {@inheritDoc}
108
   */
109
    public function setUserManager(DrupalUserManagerInterface $userManager)
110
    {
111
        $this->userManager = $userManager;
112
    }
113
114
  /**
115
   * {@inheritdoc}
116
   */
117
    public function getUserManager()
118
    {
119
        return $this->userManager;
120
    }
121
122
  /**
123
   * {@inheritdoc}
124
   */
125
    public function setAuthenticationManager(DrupalAuthenticationManagerInterface $authenticationManager)
126
    {
127
        $this->authenticationManager = $authenticationManager;
128
    }
129
130
  /**
131
   * {@inheritdoc}
132
   */
133
    public function getAuthenticationManager()
134
    {
135
        return $this->authenticationManager;
136
    }
137
138
  /**
139
   * Magic setter.
140
   */
141
    public function __set($name, $value)
142
    {
143
        switch ($name) {
144
            case 'user':
145
                trigger_error('Interacting directly with the RawDrupalContext::$user property has been deprecated. Use RawDrupalContext::getUserManager->setCurrentUser() instead.', E_USER_DEPRECATED);
146
                // Set the user on the user manager service, so it is shared between all
147
                // contexts.
148
                $this->getUserManager()->setCurrentUser($value);
149
                break;
150
151
            case 'users':
152
                trigger_error('Interacting directly with the RawDrupalContext::$users property has been deprecated. Use RawDrupalContext::getUserManager->addUser() instead.', E_USER_DEPRECATED);
153
                // Set the user on the user manager service, so it is shared between all
154
                // contexts.
155
                if (empty($value)) {
156
                    $this->getUserManager()->clearUsers();
157
                } else {
158
                    foreach ($value as $user) {
159
                        $this->getUserManager()->addUser($user);
160
                    }
161
                }
162
                break;
163
        }
164
    }
165
166
  /**
167
   * Magic getter.
168
   */
169
    public function __get($name)
170
    {
171
        switch ($name) {
172
            case 'user':
173
                trigger_error('Interacting directly with the RawDrupalContext::$user property has been deprecated. Use RawDrupalContext::getUserManager->getCurrentUser() instead.', E_USER_DEPRECATED);
174
                // Returns the current user from the user manager service. This is shared
175
                // between all contexts.
176
                return $this->getUserManager()->getCurrentUser();
177
178
            case 'users':
179
                trigger_error('Interacting directly with the RawDrupalContext::$users property has been deprecated. Use RawDrupalContext::getUserManager->getUsers() instead.', E_USER_DEPRECATED);
180
                // Returns the current user from the user manager service. This is shared
181
                // between all contexts.
182
                return $this->getUserManager()->getUsers();
183
        }
184
    }
185
186
  /**
187
   * {@inheritdoc}
188
   */
189
    public function setDispatcher(HookDispatcher $dispatcher)
190
    {
191
        $this->dispatcher = $dispatcher;
192
    }
193
194
  /**
195
   * Get active Drupal Driver.
196
   *
197
   * @return \Drupal\Driver\DrupalDriver
198
   */
199
    public function getDriver($name = null)
200
    {
201
        return $this->getDrupal()->getDriver($name);
202
    }
203
204
  /**
205
   * Get driver's random generator.
206
   */
207
    public function getRandom()
208
    {
209
        return $this->getDriver()->getRandom();
210
    }
211
212
  /**
213
   * Massage node values to match the expectations on different Drupal versions.
214
   *
215
   * @beforeNodeCreate
216
   */
217
    public static function alterNodeParameters(BeforeNodeCreateScope $scope)
218
    {
219
        $node = $scope->getEntity();
220
221
        // Get the Drupal API version if available. This is not available when
222
        // using e.g. the BlackBoxDriver or DrushDriver.
223
        $api_version = null;
224
        $driver = $scope->getContext()->getDrupal()->getDriver();
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\Context as the method getDrupal() does only exist in the following implementations of said interface: Drupal\DrupalExtension\Context\ConfigContext, Drupal\DrupalExtension\Context\DrupalContext, Drupal\DrupalExtension\C...xt\DrupalSubContextBase, Drupal\DrupalExtension\Context\DrushContext, Drupal\DrupalExtension\Context\MailContext, Drupal\DrupalExtension\Context\MessageContext, Drupal\DrupalExtension\Context\RandomContext, Drupal\DrupalExtension\Context\RawDrupalContext, Drupal\DrupalExtension\Context\RawMailContext, FeatureContext, FooFoo.

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...
225
        if ($driver instanceof \Drupal\Driver\DrupalDriver) {
226
            $api_version = $scope->getContext()->getDrupal()->getDriver()->version;
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface Behat\Behat\Context\Context as the method getDrupal() does only exist in the following implementations of said interface: Drupal\DrupalExtension\Context\ConfigContext, Drupal\DrupalExtension\Context\DrupalContext, Drupal\DrupalExtension\C...xt\DrupalSubContextBase, Drupal\DrupalExtension\Context\DrushContext, Drupal\DrupalExtension\Context\MailContext, Drupal\DrupalExtension\Context\MessageContext, Drupal\DrupalExtension\Context\RandomContext, Drupal\DrupalExtension\Context\RawDrupalContext, Drupal\DrupalExtension\Context\RawMailContext, FeatureContext, FooFoo.

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...
227
        }
228
229
        // On Drupal 8 the timestamps should be in UNIX time.
230
        switch ($api_version) {
231
            case 8:
232
                foreach (array('changed', 'created', 'revision_timestamp') as $field) {
233
                    if (!empty($node->$field) && !is_numeric($node->$field)) {
234
                        $node->$field = strtotime($node->$field);
235
                    }
236
                }
237
                break;
238
        }
239
    }
240
241
  /**
242
   * Remove any created nodes.
243
   *
244
   * @AfterScenario
245
   */
246
    public function cleanNodes()
247
    {
248
        // Remove any nodes that were created.
249
        foreach ($this->nodes as $node) {
250
            $this->getDriver()->nodeDelete($node);
251
        }
252
        $this->nodes = array();
253
    }
254
255
  /**
256
   * Remove any created users.
257
   *
258
   * @AfterScenario
259
   */
260
    public function cleanUsers()
261
    {
262
        // Remove any users that were created.
263
        if ($this->userManager->hasUsers()) {
264
            foreach ($this->userManager->getUsers() as $user) {
265
                $this->getDriver()->userDelete($user);
266
            }
267
            $this->getDriver()->processBatch();
268
            $this->userManager->clearUsers();
269
            // If the authentication manager supports logout, no need to check if the user is logged in.
270
            if ($this->getAuthenticationManager() instanceof FastLogoutInterface) {
271
                $this->logout(true);
272
            } elseif ($this->loggedIn()) {
273
                $this->logout();
274
            }
275
        }
276
    }
277
278
  /**
279
   * Remove any created terms.
280
   *
281
   * @AfterScenario
282
   */
283
    public function cleanTerms()
284
    {
285
        // Remove any terms that were created.
286
        foreach ($this->terms as $term) {
287
            $this->getDriver()->termDelete($term);
288
        }
289
        $this->terms = array();
290
    }
291
292
  /**
293
   * Remove any created roles.
294
   *
295
   * @AfterScenario
296
   */
297
    public function cleanRoles()
298
    {
299
        // Remove any roles that were created.
300
        foreach ($this->roles as $rid) {
301
            $this->getDriver()->roleDelete($rid);
302
        }
303
        $this->roles = array();
304
    }
305
306
  /**
307
   * Remove any created languages.
308
   *
309
   * @AfterScenario
310
   */
311
    public function cleanLanguages()
312
    {
313
        // Delete any languages that were created.
314
        foreach ($this->languages as $language) {
315
            $this->getDriver()->languageDelete($language);
316
            unset($this->languages[$language->langcode]);
317
        }
318
    }
319
320
  /**
321
   * Clear static caches.
322
   *
323
   * @AfterScenario @api
324
   */
325
    public function clearStaticCaches()
326
    {
327
        $this->getDriver()->clearStaticCaches();
328
    }
329
330
  /**
331
   * Dispatch scope hooks.
332
   *
333
   * @param string $scope
334
   *   The entity scope to dispatch.
335
   * @param \stdClass $entity
336
   *   The entity.
337
   */
338
    protected function dispatchHooks($scopeType, \stdClass $entity)
339
    {
340
        $fullScopeClass = 'Drupal\\DrupalExtension\\Hook\\Scope\\' . $scopeType;
341
        $scope = new $fullScopeClass($this->getDrupal()->getEnvironment(), $this, $entity);
342
        $callResults = $this->dispatcher->dispatchScopeHooks($scope);
343
344
        // The dispatcher suppresses exceptions, throw them here if there are any.
345
        foreach ($callResults as $result) {
346
            if ($result->hasException()) {
347
                $exception = $result->getException();
348
                throw $exception;
349
            }
350
        }
351
    }
352
353
  /**
354
   * Create a node.
355
   *
356
   * @return object
357
   *   The created node.
358
   */
359 View Code Duplication
    public function nodeCreate($node)
360
    {
361
        $this->dispatchHooks('BeforeNodeCreateScope', $node);
362
        $this->parseEntityFields('node', $node);
363
        $saved = $this->getDriver()->createNode($node);
364
        $this->dispatchHooks('AfterNodeCreateScope', $saved);
365
        $this->nodes[] = $saved;
366
        return $saved;
367
    }
368
369
  /**
370
   * Parses the field values and turns them into the format expected by Drupal.
371
   *
372
   * Multiple values in a single field must be separated by commas. Wrap the
373
   * field value in double quotes in case it should contain a comma.
374
   *
375
   * Compound field properties are identified using a ':' operator, either in
376
   * the column heading or in the cell. If multiple properties are present in a
377
   * single cell, they must be separated using ' - ', and values should not
378
   * contain ':' or ' - '.
379
   *
380
   * Possible formats for the values:
381
   *   A
382
   *   A, B, "a value, containing a comma"
383
   *   A - B
384
   *   x: A - y: B
385
   *   A - B, C - D, "E - F"
386
   *   x: A - y: B,  x: C - y: D,  "x: E - y: F"
387
   *
388
   * See field_handlers.feature for examples of usage.
389
   *
390
   * @param string $entity_type
391
   *   The entity type.
392
   * @param \stdClass $entity
393
   *   An object containing the entity properties and fields as properties.
394
   *
395
   * @throws \Exception
396
   *   Thrown when a field name is invalid.
397
   */
398
    public function parseEntityFields($entity_type, \stdClass $entity)
399
    {
400
        $multicolumn_field = '';
401
        $multicolumn_fields = array();
402
403
        foreach (clone $entity as $field => $field_value) {
404
            // Reset the multicolumn field if the field name does not contain a column.
405
            if (strpos($field, ':') === false) {
406
                $multicolumn_field = '';
407
            } // Start tracking a new multicolumn field if the field name contains a ':'
408
            // which is preceded by at least 1 character.
409
            elseif (strpos($field, ':', 1) !== false) {
410
                list($multicolumn_field, $multicolumn_column) = explode(':', $field);
411
            } // If a field name starts with a ':' but we are not yet tracking a
412
            // multicolumn field we don't know to which field this belongs.
413
            elseif (empty($multicolumn_field)) {
414
                throw new \Exception('Field name missing for ' . $field);
415
            } // Update the column name if the field name starts with a ':' and we are
416
            // already tracking a multicolumn field.
417
            else {
418
                $multicolumn_column = substr($field, 1);
419
            }
420
421
            $is_multicolumn = $multicolumn_field && $multicolumn_column;
422
            $field_name = $multicolumn_field ?: $field;
423
            if ($this->getDriver()->isField($entity_type, $field_name)) {
424
                // Split up multiple values in multi-value fields.
425
                $values = array();
426
                foreach (str_getcsv($field_value) as $key => $value) {
427
                    $value = trim($value);
428
                    $columns = $value;
429
                    // Split up field columns if the ' - ' separator is present.
430
                    if (strstr($value, ' - ') !== false) {
431
                        $columns = array();
432
                        foreach (explode(' - ', $value) as $column) {
433
                            // Check if it is an inline named column.
434
                            if (!$is_multicolumn && strpos($column, ': ', 1) !== false) {
435
                                list ($key, $column) = explode(': ', $column);
436
                                $columns[$key] = $column;
437
                            } else {
438
                                $columns[] = $column;
439
                            }
440
                        }
441
                    }
442
                    // Use the column name if we are tracking a multicolumn field.
443
                    if ($is_multicolumn) {
444
                        $multicolumn_fields[$multicolumn_field][$key][$multicolumn_column] = $columns;
445
                        unset($entity->$field);
446
                    } else {
447
                        $values[] = $columns;
448
                    }
449
                }
450
                // Replace regular fields inline in the entity after parsing.
451
                if (!$is_multicolumn) {
452
                    $entity->$field_name = $values;
453
                    // Don't specify any value if the step author has left it blank.
454
                    if ($field_value === '') {
455
                        unset($entity->$field_name);
456
                    }
457
                }
458
            }
459
        }
460
461
        // Add the multicolumn fields to the entity.
462
        foreach ($multicolumn_fields as $field_name => $columns) {
463
            // Don't specify any value if the step author has left it blank.
464
            if (count(array_filter($columns, function ($var) {
465
                return ($var !== '');
466
            })) > 0) {
467
                $entity->$field_name = $columns;
468
            }
469
        }
470
    }
471
472
  /**
473
   * Create a user.
474
   *
475
   * @return object
476
   *   The created user.
477
   */
478
    public function userCreate($user)
479
    {
480
        $this->dispatchHooks('BeforeUserCreateScope', $user);
481
        $this->parseEntityFields('user', $user);
482
        $this->getDriver()->userCreate($user);
483
        $this->dispatchHooks('AfterUserCreateScope', $user);
484
        $this->userManager->addUser($user);
485
        return $user;
486
    }
487
488
  /**
489
   * Create a term.
490
   *
491
   * @return object
492
   *   The created term.
493
   */
494 View Code Duplication
    public function termCreate($term)
495
    {
496
        $this->dispatchHooks('BeforeTermCreateScope', $term);
497
        $this->parseEntityFields('taxonomy_term', $term);
498
        $saved = $this->getDriver()->createTerm($term);
499
        $this->dispatchHooks('AfterTermCreateScope', $saved);
500
        $this->terms[] = $saved;
501
        return $saved;
502
    }
503
504
  /**
505
   * Creates a language.
506
   *
507
   * @param \stdClass $language
508
   *   An object with the following properties:
509
   *   - langcode: the langcode of the language to create.
510
   *
511
   * @return object|FALSE
512
   *   The created language, or FALSE if the language was already created.
513
   */
514
    public function languageCreate(\stdClass $language)
515
    {
516
        $this->dispatchHooks('BeforeLanguageCreateScope', $language);
517
        $language = $this->getDriver()->languageCreate($language);
518
        if ($language) {
519
            $this->dispatchHooks('AfterLanguageCreateScope', $language);
520
            $this->languages[$language->langcode] = $language;
521
        }
522
        return $language;
523
    }
524
525
  /**
526
   * Log-in the given user.
527
   *
528
   * @param \stdClass $user
529
   *   The user to log in.
530
   */
531
    public function login(\stdClass $user)
532
    {
533
        $this->getAuthenticationManager()->logIn($user);
534
    }
535
536
  /**
537
   * Logs the current user out.
538
   *
539
   * @param bool $fast
540
   *   Utilize direct logout by session if available.
541
   */
542
    public function logout($fast = false)
543
    {
544
        if ($fast && $this->getAuthenticationManager() instanceof FastLogoutInterface) {
545
            $this->getAuthenticationManager()->fastLogout();
546
        } else {
547
            $this->getAuthenticationManager()->logOut();
548
        }
549
    }
550
551
  /**
552
   * Determine if the a user is already logged in.
553
   *
554
   * @return boolean
555
   *   Returns TRUE if a user is logged in for this session.
556
   */
557
    public function loggedIn()
558
    {
559
        return $this->getAuthenticationManager()->loggedIn();
560
    }
561
562
  /**
563
   * User with a given role is already logged in.
564
   *
565
   * @param string $role
566
   *   A single role, or multiple comma-separated roles in a single string.
567
   *
568
   * @return boolean
569
   *   Returns TRUE if the current logged in user has this role (or roles).
570
   */
571
    public function loggedInWithRole($role)
572
    {
573
        return $this->loggedIn() && $this->getUserManager()->currentUserHasRole($role);
574
    }
575
576
  /**
577
   * Returns the Behat context that corresponds with the given class name.
578
   *
579
   * This is inspired by InitializedContextEnvironment::getContext() but also
580
   * returns subclasses of the given class name. This allows us to retrieve for
581
   * example DrupalContext even if it is overridden in a project.
582
   *
583
   * @param string $class
584
   *   A fully namespaced class name.
585
   *
586
   * @return \Behat\Behat\Context\Context|false
587
   *   The requested context, or FALSE if the context is not registered.
588
   *
589
   * @throws \Exception
590
   *   Thrown when the environment is not yet initialized, meaning that contexts
591
   *   cannot yet be retrieved.
592
   */
593
    protected function getContext($class)
594
    {
595
        /** @var InitializedContextEnvironment $environment */
596
        $environment = $this->drupal->getEnvironment();
597
        // Throw an exception if the environment is not yet initialized. To make
598
        // sure state doesn't leak between test scenarios, the environment is
599
        // reinitialized at the start of every scenario. If this code is executed
600
        // before a test scenario starts (e.g. in a `@BeforeScenario` hook) then the
601
        // contexts cannot yet be retrieved.
602
        if (!$environment instanceof InitializedContextEnvironment) {
603
            throw new \Exception('Cannot retrieve contexts when the environment is not yet initialized.');
604
        }
605
        foreach ($environment->getContexts() as $context) {
606
            if ($context instanceof $class) {
607
                return $context;
608
            }
609
        }
610
611
        return false;
612
    }
613
}
614