Completed
Push — 316-authentication-manager ( e36a80 )
by Jonathan
01:33
created

RawDrupalContext::dispatchHooks()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
rs 9.4285
cc 3
eloc 8
nc 3
nop 2
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
9
use Drupal\DrupalDriverManager;
10
use Drupal\DrupalExtension\DrupalParametersTrait;
11
use Drupal\DrupalExtension\Manager\DrupalAuthenticationManagerInterface;
12
use Drupal\DrupalExtension\Manager\DrupalUserManagerInterface;
13
14
use Drupal\DrupalExtension\Hook\Scope\AfterLanguageEnableScope;
15
use Drupal\DrupalExtension\Hook\Scope\AfterNodeCreateScope;
16
use Drupal\DrupalExtension\Hook\Scope\AfterTermCreateScope;
17
use Drupal\DrupalExtension\Hook\Scope\AfterUserCreateScope;
18
use Drupal\DrupalExtension\Hook\Scope\BaseEntityScope;
19
use Drupal\DrupalExtension\Hook\Scope\BeforeLanguageEnableScope;
20
use Drupal\DrupalExtension\Hook\Scope\BeforeNodeCreateScope;
21
use Drupal\DrupalExtension\Hook\Scope\BeforeUserCreateScope;
22
use Drupal\DrupalExtension\Hook\Scope\BeforeTermCreateScope;
23
24
25
/**
26
 * Provides the raw functionality for interacting with Drupal.
27
 */
28
class RawDrupalContext extends RawMinkContext implements DrupalAwareInterface {
29
30
  use DrupalParametersTrait;
31
32
  /**
33
   * Drupal driver manager.
34
   *
35
   * @var \Drupal\DrupalDriverManager
36
   */
37
  private $drupal;
38
39
  /**
40
   * Event dispatcher object.
41
   *
42
   * @var \Behat\Testwork\Hook\HookDispatcher
43
   */
44
  protected $dispatcher;
45
46
  /**
47
   * Drupal authentication manager.
48
   *
49
   * @var \Drupal\DrupalExtension\Manager\DrupalAuthenticationManagerInterface
50
   */
51
  protected $authenticationManager;
52
53
  /**
54
   * Drupal user manager.
55
   *
56
   * @var \Drupal\DrupalExtension\Manager\DrupalUserManagerInterface
57
   */
58
  protected $userManager;
59
60
  /**
61
   * Keep track of nodes so they can be cleaned up.
62
   *
63
   * @var array
64
   */
65
  protected $nodes = array();
66
67
  /**
68
   * Keep track of all terms that are created so they can easily be removed.
69
   *
70
   * @var array
71
   */
72
  protected $terms = array();
73
74
  /**
75
   * Keep track of any roles that are created so they can easily be removed.
76
   *
77
   * @var array
78
   */
79
  protected $roles = array();
80
81
  /**
82
   * Keep track of any languages that are created so they can easily be removed.
83
   *
84
   * @var array
85
   */
86
  protected $languages = array();
87
88
  /**
89
   * {@inheritDoc}
90
   */
91
  public function setDrupal(DrupalDriverManager $drupal) {
92
    $this->drupal = $drupal;
93
  }
94
95
  /**
96
   * {@inheritDoc}
97
   */
98
  public function getDrupal() {
99
    return $this->drupal;
100
  }
101
102
  /**
103
   * {@inheritDoc}
104
   */
105
  public function setUserManager(DrupalUserManagerInterface $userManager) {
106
    $this->userManager = $userManager;
107
  }
108
109
  /**
110
   * {@inheritdoc}
111
   */
112
  public function getUserManager() {
113
    return $this->userManager;
114
  }
115
116
  /**
117
   * {@inheritdoc}
118
   */
119
  public function setAuthenticationManager(DrupalAuthenticationManagerInterface $authenticationManager) {
120
    $this->authenticationManager = $authenticationManager;
121
  }
122
123
  /**
124
   * {@inheritdoc}
125
   */
126
  public function getAuthenticationManager() {
127
    return $this->authenticationManager;
128
  }
129
130
  /**
131
   * Magic setter.
132
   */
133
  public function __set($name, $value) {
134
    switch ($name) {
135
      case 'user':
136
        trigger_error('Interacting directly with the RawDrupalContext::$user property has been deprecated. Use RawDrupalContext::getUserManager->setCurrentUser() instead.', E_USER_DEPRECATED);
137
        // Set the user on the user manager service, so it is shared between all
138
        // contexts.
139
        $this->getUserManager()->setCurrentUser($value);
140
        break;
141
142
      case 'users':
143
        trigger_error('Interacting directly with the RawDrupalContext::$users property has been deprecated. Use RawDrupalContext::getUserManager->addUser() instead.', E_USER_DEPRECATED);
144
        // Set the user on the user manager service, so it is shared between all
145
        // contexts.
146
        if (empty($value)) {
147
          $this->getUserManager()->clearUsers();
148
        }
149
        else {
150
          foreach ($value as $user) {
151
            $this->getUserManager()->addUser($user);
152
          }
153
        }
154
        break;
155
    }
156
  }
157
158
  /**
159
   * Magic getter.
160
   */
161
  public function __get($name) {
162
    switch ($name) {
163
      case 'user':
164
        trigger_error('Interacting directly with the RawDrupalContext::$user property has been deprecated. Use RawDrupalContext::getUserManager->getCurrentUser() instead.', E_USER_DEPRECATED);
165
        // Returns the current user from the user manager service. This is shared
166
        // between all contexts.
167
        return $this->getUserManager()->getCurrentUser();
168
169
      case 'users':
170
        trigger_error('Interacting directly with the RawDrupalContext::$users property has been deprecated. Use RawDrupalContext::getUserManager->getUsers() instead.', E_USER_DEPRECATED);
171
        // Returns the current user from the user manager service. This is shared
172
        // between all contexts.
173
        return $this->getUserManager()->getUsers();
174
    }
175
  }
176
177
  /**
178
   * {@inheritdoc}
179
   */
180
  public function setDispatcher(HookDispatcher $dispatcher) {
181
    $this->dispatcher = $dispatcher;
182
  }
183
184
  /**
185
   * Get active Drupal Driver.
186
   *
187
   * @return \Drupal\Driver\DrupalDriver
188
   */
189
  public function getDriver($name = NULL) {
190
    return $this->getDrupal()->getDriver($name);
191
  }
192
193
  /**
194
   * Get driver's random generator.
195
   */
196
  public function getRandom() {
197
    return $this->getDriver()->getRandom();
198
  }
199
200
  /**
201
   * Massage node values to match the expectations on different Drupal versions.
202
   *
203
   * @beforeNodeCreate
204
   */
205
  public static function alterNodeParameters(BeforeNodeCreateScope $scope) {
206
    $node = $scope->getEntity();
207
208
    // Get the Drupal API version if available. This is not available when
209
    // using e.g. the BlackBoxDriver or DrushDriver.
210
    $api_version = NULL;
211
    $driver = $scope->getContext()->getDrupal()->getDriver();
0 ignored issues
show
Bug introduced by
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\MessageContext, Drupal\DrupalExtension\Context\RawDrupalContext, 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...
212
    if ($driver instanceof \Drupal\Driver\DrupalDriver) {
213
      $api_version = $scope->getContext()->getDrupal()->getDriver()->version;
0 ignored issues
show
Bug introduced by
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\MessageContext, Drupal\DrupalExtension\Context\RawDrupalContext, 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...
214
    }
215
216
    // On Drupal 8 the timestamps should be in UNIX time.
217
    switch ($api_version) {
218
      case 8:
219
        foreach (array('changed', 'created', 'revision_timestamp') as $field) {
220
          if (!empty($node->$field) && !is_numeric($node->$field)) {
221
            $node->$field = strtotime($node->$field);
222
          }
223
        }
224
      break;
225
    }
226
  }
227
228
  /**
229
   * Remove any created nodes.
230
   *
231
   * @AfterScenario
232
   */
233
  public function cleanNodes() {
234
    // Remove any nodes that were created.
235
    foreach ($this->nodes as $node) {
236
      $this->getDriver()->nodeDelete($node);
237
    }
238
    $this->nodes = array();
239
  }
240
241
  /**
242
   * Remove any created users.
243
   *
244
   * @AfterScenario
245
   */
246
  public function cleanUsers() {
247
    // Remove any users that were created.
248
    if ($this->userManager->hasUsers()) {
249
      foreach ($this->userManager->getUsers() as $user) {
250
        $this->getDriver()->userDelete($user);
251
      }
252
      $this->getDriver()->processBatch();
253
      $this->userManager->clearUsers();
254
      if ($this->loggedIn()) {
255
        $this->logout();
256
      }
257
    }
258
  }
259
260
  /**
261
   * Remove any created terms.
262
   *
263
   * @AfterScenario
264
   */
265
  public function cleanTerms() {
266
    // Remove any terms that were created.
267
    foreach ($this->terms as $term) {
268
      $this->getDriver()->termDelete($term);
269
    }
270
    $this->terms = array();
271
  }
272
273
  /**
274
   * Remove any created roles.
275
   *
276
   * @AfterScenario
277
   */
278
  public function cleanRoles() {
279
    // Remove any roles that were created.
280
    foreach ($this->roles as $rid) {
281
      $this->getDriver()->roleDelete($rid);
282
    }
283
    $this->roles = array();
284
  }
285
286
  /**
287
   * Remove any created languages.
288
   *
289
   * @AfterScenario
290
   */
291
  public function cleanLanguages() {
292
    // Delete any languages that were created.
293
    foreach ($this->languages as $language) {
294
      $this->getDriver()->languageDelete($language);
295
      unset($this->languages[$language->langcode]);
296
    }
297
  }
298
299
  /**
300
   * Clear static caches.
301
   *
302
   * @AfterScenario @api
303
   */
304
  public function clearStaticCaches() {
305
    $this->getDriver()->clearStaticCaches();
306
  }
307
308
  /**
309
   * Dispatch scope hooks.
310
   *
311
   * @param string $scope
0 ignored issues
show
Bug introduced by
There is no parameter named $scope. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
312
   *   The entity scope to dispatch.
313
   * @param \stdClass $entity
314
   *   The entity.
315
   */
316
  protected function dispatchHooks($scopeType, \stdClass $entity) {
317
    $fullScopeClass = 'Drupal\\DrupalExtension\\Hook\\Scope\\' . $scopeType;
318
    $scope = new $fullScopeClass($this->getDrupal()->getEnvironment(), $this, $entity);
319
    $callResults = $this->dispatcher->dispatchScopeHooks($scope);
320
321
    // The dispatcher suppresses exceptions, throw them here if there are any.
322
    foreach ($callResults as $result) {
323
      if ($result->hasException()) {
324
        $exception = $result->getException();
325
        throw $exception;
326
      }
327
    }
328
  }
329
330
  /**
331
   * Create a node.
332
   *
333
   * @return object
334
   *   The created node.
335
   */
336 View Code Duplication
  public function nodeCreate($node) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
337
    $this->dispatchHooks('BeforeNodeCreateScope', $node);
338
    $this->parseEntityFields('node', $node);
339
    $saved = $this->getDriver()->createNode($node);
340
    $this->dispatchHooks('AfterNodeCreateScope', $saved);
341
    $this->nodes[] = $saved;
342
    return $saved;
343
  }
344
345
  /**
346
   * Parses the field values and turns them into the format expected by Drupal.
347
   *
348
   * Multiple values in a single field must be separated by commas. Wrap the
349
   * field value in double quotes in case it should contain a comma.
350
   *
351
   * Compound field properties are identified using a ':' operator, either in
352
   * the column heading or in the cell. If multiple properties are present in a
353
   * single cell, they must be separated using ' - ', and values should not
354
   * contain ':' or ' - '.
355
   *
356
   * Possible formats for the values:
357
   *   A
358
   *   A, B, "a value, containing a comma"
359
   *   A - B
360
   *   x: A - y: B
361
   *   A - B, C - D, "E - F"
362
   *   x: A - y: B,  x: C - y: D,  "x: E - y: F"
363
   *
364
   * See field_handlers.feature for examples of usage.
365
   *
366
   * @param string $entity_type
367
   *   The entity type.
368
   * @param \stdClass $entity
369
   *   An object containing the entity properties and fields as properties.
370
   *
371
   * @throws \Exception
372
   *   Thrown when a field name is invalid.
373
   */
374
  public function parseEntityFields($entity_type, \stdClass $entity) {
375
    $multicolumn_field = '';
376
    $multicolumn_fields = array();
377
378
    foreach (clone $entity as $field => $field_value) {
0 ignored issues
show
Bug introduced by
The expression clone $entity of type object<stdClass> is not traversable.
Loading history...
379
      // Reset the multicolumn field if the field name does not contain a column.
380
      if (strpos($field, ':') === FALSE) {
381
        $multicolumn_field = '';
382
      }
383
      // Start tracking a new multicolumn field if the field name contains a ':'
384
      // which is preceded by at least 1 character.
385
      elseif (strpos($field, ':', 1) !== FALSE) {
386
        list($multicolumn_field, $multicolumn_column) = explode(':', $field);
387
      }
388
      // If a field name starts with a ':' but we are not yet tracking a
389
      // multicolumn field we don't know to which field this belongs.
390
      elseif (empty($multicolumn_field)) {
391
        throw new \Exception('Field name missing for ' . $field);
392
      }
393
      // Update the column name if the field name starts with a ':' and we are
394
      // already tracking a multicolumn field.
395
      else {
396
        $multicolumn_column = substr($field, 1);
397
      }
398
399
      $is_multicolumn = $multicolumn_field && $multicolumn_column;
0 ignored issues
show
Bug introduced by
The variable $multicolumn_column does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
400
      $field_name = $multicolumn_field ?: $field;
401
      if ($this->getDriver()->isField($entity_type, $field_name)) {
402
        // Split up multiple values in multi-value fields.
403
        $values = array();
404
        foreach (str_getcsv($field_value) as $key => $value) {
405
          $value = trim($value);
406
          $columns = $value;
407
          // Split up field columns if the ' - ' separator is present.
408
          if (strstr($value, ' - ') !== FALSE) {
409
            $columns = array();
410
            foreach (explode(' - ', $value) as $column) {
411
              // Check if it is an inline named column.
412
              if (!$is_multicolumn && strpos($column, ': ', 1) !== FALSE) {
413
                list ($key, $column) = explode(': ', $column);
414
                $columns[$key] = $column;
415
              }
416
              else {
417
                $columns[] = $column;
418
              }
419
            }
420
          }
421
          // Use the column name if we are tracking a multicolumn field.
422
          if ($is_multicolumn) {
423
            $multicolumn_fields[$multicolumn_field][$key][$multicolumn_column] = $columns;
424
            unset($entity->$field);
425
          }
426
          else {
427
            $values[] = $columns;
428
          }
429
        }
430
        // Replace regular fields inline in the entity after parsing.
431
        if (!$is_multicolumn) {
432
          $entity->$field_name = $values;
433
          // Don't specify any value if the step author has left it blank.
434
          if ($field_value === '') {
435
            unset($entity->$field_name);
436
          }
437
        }
438
      }
439
    }
440
441
    // Add the multicolumn fields to the entity.
442
    foreach ($multicolumn_fields as $field_name => $columns) {
443
      // Don't specify any value if the step author has left it blank.
444
      if (count(array_filter($columns, function ($var) {
445
        return ($var !== '');
446
      })) > 0) {
447
        $entity->$field_name = $columns;
448
      }
449
    }
450
  }
451
452
  /**
453
   * Create a user.
454
   *
455
   * @return object
456
   *   The created user.
457
   */
458
  public function userCreate($user) {
459
    $this->dispatchHooks('BeforeUserCreateScope', $user);
460
    $this->parseEntityFields('user', $user);
461
    $this->getDriver()->userCreate($user);
462
    $this->dispatchHooks('AfterUserCreateScope', $user);
463
    $this->userManager->addUser($user);
464
    return $user;
465
  }
466
467
  /**
468
   * Create a term.
469
   *
470
   * @return object
471
   *   The created term.
472
   */
473 View Code Duplication
  public function termCreate($term) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
474
    $this->dispatchHooks('BeforeTermCreateScope', $term);
475
    $this->parseEntityFields('taxonomy_term', $term);
476
    $saved = $this->getDriver()->createTerm($term);
477
    $this->dispatchHooks('AfterTermCreateScope', $saved);
478
    $this->terms[] = $saved;
479
    return $saved;
480
  }
481
482
  /**
483
   * Creates a language.
484
   *
485
   * @param \stdClass $language
486
   *   An object with the following properties:
487
   *   - langcode: the langcode of the language to create.
488
   *
489
   * @return object|FALSE
490
   *   The created language, or FALSE if the language was already created.
491
   */
492
  public function languageCreate(\stdClass $language) {
493
    $this->dispatchHooks('BeforeLanguageCreateScope', $language);
494
    $language = $this->getDriver()->languageCreate($language);
495
    if ($language) {
496
      $this->dispatchHooks('AfterLanguageCreateScope', $language);
497
      $this->languages[$language->langcode] = $language;
498
    }
499
    return $language;
500
  }
501
502
  /**
503
   * Log-in the given user.
504
   *
505
   * @param \stdClass $user
506
   *   The user to log in.
507
   */
508
  public function login(\stdClass $user) {
509
    $this->getAuthenticationManager()->logIn($user);
510
  }
511
512
  /**
513
   * Logs the current user out.
514
   */
515
  public function logout() {
516
    $this->getAuthenticationManager()->logOut();
517
  }
518
519
  /**
520
   * Determine if the a user is already logged in.
521
   *
522
   * @return boolean
523
   *   Returns TRUE if a user is logged in for this session.
524
   */
525
  public function loggedIn() {
526
    return $this->getAuthenticationManager()->loggedIn();
527
  }
528
529
  /**
530
   * User with a given role is already logged in.
531
   *
532
   * @param string $role
533
   *   A single role, or multiple comma-separated roles in a single string.
534
   *
535
   * @return boolean
536
   *   Returns TRUE if the current logged in user has this role (or roles).
537
   */
538
  public function loggedInWithRole($role) {
539
    return $this->loggedIn() && $this->getUserManager()->currentUserHasRole($role);
540
  }
541
542
}
543