Completed
Pull Request — master (#224)
by Pieter
10:30
created

RawDrupalContext::loggedIn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 9
rs 9.6667
cc 1
eloc 5
nc 1
nop 0
1
<?php
2
3
namespace Drupal\DrupalExtension\Context;
4
5
use Behat\MinkExtension\Context\RawMinkContext;
6
use Behat\Testwork\Hook\HookDispatcher;
7
8
use Drupal\DrupalDriverManager;
9
10
use Drupal\DrupalExtension\Hook\Scope\AfterLanguageEnableScope;
11
use Drupal\DrupalExtension\Hook\Scope\AfterNodeCreateScope;
12
use Drupal\DrupalExtension\Hook\Scope\AfterTermCreateScope;
13
use Drupal\DrupalExtension\Hook\Scope\AfterUserCreateScope;
14
use Drupal\DrupalExtension\Hook\Scope\BaseEntityScope;
15
use Drupal\DrupalExtension\Hook\Scope\BeforeLanguageEnableScope;
16
use Drupal\DrupalExtension\Hook\Scope\BeforeNodeCreateScope;
17
use Drupal\DrupalExtension\Hook\Scope\BeforeUserCreateScope;
18
use Drupal\DrupalExtension\Hook\Scope\BeforeTermCreateScope;
19
20
21
/**
22
 * Provides the raw functionality for interacting with Drupal.
23
 */
24
class RawDrupalContext extends RawMinkContext implements DrupalAwareInterface {
25
26
  /**
27
   * Drupal driver manager.
28
   *
29
   * @var \Drupal\DrupalDriverManager
30
   */
31
  private $drupal;
32
33
  /**
34
   * Test parameters.
35
   *
36
   * @var array
37
   */
38
  private $drupalParameters;
39
40
  /**
41
   * Event dispatcher object.
42
   *
43
   * @var \Behat\Testwork\Hook\HookDispatcher
44
   */
45
  protected $dispatcher;
46
47
  /**
48
   * Keep track of nodes so they can be cleaned up.
49
   *
50
   * @var array
51
   */
52
  protected $nodes = array();
53
54
  /**
55
   * Current authenticated user.
56
   *
57
   * A value of FALSE denotes an anonymous user.
58
   *
59
   * @var stdClass|bool
60
   */
61
  public $user = FALSE;
62
63
  /**
64
   * Keep track of all users that are created so they can easily be removed.
65
   *
66
   * @var array
67
   */
68
  protected $users = array();
69
70
  /**
71
   * Keep track of all terms that are created so they can easily be removed.
72
   *
73
   * @var array
74
   */
75
  protected $terms = array();
76
77
  /**
78
   * Keep track of any roles that are created so they can easily be removed.
79
   *
80
   * @var array
81
   */
82
  protected $roles = array();
83
84
  /**
85
   * Keep track of any languages that are created so they can easily be removed.
86
   *
87
   * @var array
88
   */
89
  protected $languages = array();
90
91
  /**
92
   * Keep track of any modules that are installed so they can be uninstalled.
93
   *
94
   * @var array
95
   */
96
  protected $modules = array();
97
98
  /**
99
   * {@inheritDoc}
100
   */
101
  public function setDrupal(DrupalDriverManager $drupal) {
102
    $this->drupal = $drupal;
103
  }
104
105
  /**
106
   * {@inheritDoc}
107
   */
108
  public function getDrupal() {
109
    return $this->drupal;
110
  }
111
112
  /**
113
   * {@inheritDoc}
114
   */
115
  public function setDispatcher(HookDispatcher $dispatcher) {
116
    $this->dispatcher = $dispatcher;
117
  }
118
119
  /**
120
   * Set parameters provided for Drupal.
121
   */
122
  public function setDrupalParameters(array $parameters) {
123
    $this->drupalParameters = $parameters;
124
  }
125
126
  /**
127
   * Returns a specific Drupal parameter.
128
   *
129
   * @param string $name
130
   *   Parameter name.
131
   *
132
   * @return mixed
133
   */
134
  public function getDrupalParameter($name) {
135
    return isset($this->drupalParameters[$name]) ? $this->drupalParameters[$name] : NULL;
136
  }
137
138
  /**
139
   * Returns a specific Drupal text value.
140
   *
141
   * @param string $name
142
   *   Text value name, such as 'log_out', which corresponds to the default 'Log
143
   *   out' link text.
144
   * @throws \Exception
145
   * @return
146
   */
147 View Code Duplication
  public function getDrupalText($name) {
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...
148
    $text = $this->getDrupalParameter('text');
149
    if (!isset($text[$name])) {
150
      throw new \Exception(sprintf('No such Drupal string: %s', $name));
151
    }
152
    return $text[$name];
153
  }
154
155
  /**
156
   * Get active Drupal Driver.
157
   */
158
  public function getDriver($name = NULL) {
159
    return $this->getDrupal()->getDriver($name);
160
  }
161
162
  /**
163
   * Get driver's random generator.
164
   */
165
  public function getRandom() {
166
    return $this->getDriver()->getRandom();
167
  }
168
169
  /**
170
   * Remove any created nodes.
171
   *
172
   * @AfterScenario
173
   */
174
  public function cleanNodes() {
175
    // Remove any nodes that were created.
176
    foreach ($this->nodes as $node) {
177
      $this->getDriver()->nodeDelete($node);
178
    }
179
    $this->nodes = array();
180
  }
181
182
  /**
183
   * Remove any created users.
184
   *
185
   * @AfterScenario
186
   */
187
  public function cleanUsers() {
188
    // Remove any users that were created.
189
    if (!empty($this->users)) {
190
      foreach ($this->users as $user) {
191
        $this->getDriver()->userDelete($user);
192
      }
193
      $this->getDriver()->processBatch();
194
      $this->users = array();
195
    }
196
  }
197
198
  /**
199
   * Remove any created terms.
200
   *
201
   * @AfterScenario
202
   */
203
  public function cleanTerms() {
204
    // Remove any terms that were created.
205
    foreach ($this->terms as $term) {
206
      $this->getDriver()->termDelete($term);
207
    }
208
    $this->terms = array();
209
  }
210
211
  /**
212
   * Remove any created roles.
213
   *
214
   * @AfterScenario
215
   */
216
  public function cleanRoles() {
217
    // Remove any roles that were created.
218
    foreach ($this->roles as $rid) {
219
      $this->getDriver()->roleDelete($rid);
220
    }
221
    $this->roles = array();
222
  }
223
224
  /**
225
   * Remove any created languages.
226
   *
227
   * @AfterScenario
228
   */
229
  public function cleanLanguages() {
230
    // Delete any languages that were created.
231
    foreach ($this->languages as $language) {
232
      $this->getDriver()->languageDelete($language);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Drupal\Driver\DriverInterface as the method languageDelete() does only exist in the following implementations of said interface: Drupal\Driver\DrupalDriver.

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...
233
      unset($this->languages[$language->langcode]);
234
    }
235
  }
236
237
  /**
238
   * Uninstall any installed modules.
239
   *
240
   * @AfterScenario
241
   */
242
  public function cleanModules() {
243
    if (!empty($this->modules)) {
244
      $this->uninstallModules($this->modules);
245
    }
246
    $this->modules = array();
247
  }
248
249
  /**
250
   * Clear static caches.
251
   *
252
   * @AfterScenario @api
253
   */
254
  public function clearStaticCaches() {
255
    $this->getDriver()->clearStaticCaches();
256
  }
257
258
  /**
259
   * Dispatch scope hooks.
260
   *
261
   * @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...
262
   *   The entity scope to dispatch.
263
   * @param stdClass $entity
264
   *   The entity.
265
   */
266
  protected function dispatchHooks($scopeType, \stdClass $entity) {
267
    $fullScopeClass = 'Drupal\\DrupalExtension\\Hook\\Scope\\' . $scopeType;
268
    $scope = new $fullScopeClass($this->getDrupal()->getEnvironment(), $this, $entity);
269
    $callResults = $this->dispatcher->dispatchScopeHooks($scope);
270
271
    // The dispatcher suppresses exceptions, throw them here if there are any.
272
    foreach ($callResults as $result) {
273
      if ($result->hasException()) {
274
        $exception = $result->getException();
275
        throw $exception;
276
      }
277
    }
278
  }
279
280
  /**
281
   * Create a node.
282
   *
283
   * @return object
284
   *   The created node.
285
   */
286 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...
287
    $this->dispatchHooks('BeforeNodeCreateScope', $node);
288
    $this->parseEntityFields('node', $node);
289
    $saved = $this->getDriver()->createNode($node);
290
    $this->dispatchHooks('AfterNodeCreateScope', $saved);
291
    $this->nodes[] = $saved;
292
    return $saved;
293
  }
294
295
  /**
296
   * Parse multi-value fields. Possible formats:
297
   *    A, B, C
298
   *    A - B, C - D, E - F
299
   *
300
   * @param string $entity_type
301
   *   The entity type.
302
   * @param \stdClass $entity
303
   *   An object containing the entity properties and fields as properties.
304
   */
305
  public function parseEntityFields($entity_type, \stdClass $entity) {
306
    $multicolumn_field = '';
307
    $multicolumn_fields = array();
308
309
    foreach ($entity as $field => $field_value) {
0 ignored issues
show
Bug introduced by
The expression $entity of type object<stdClass> is not traversable.
Loading history...
310
      // Reset the multicolumn field if the field name does not contain a column.
311
      if (strpos($field, ':') === FALSE) {
312
        $multicolumn_field = '';
313
      }
314
      // Start tracking a new multicolumn field if the field name contains a ':'
315
      // which is preceded by at least 1 character.
316
      elseif (strpos($field, ':', 1) !== FALSE) {
317
        list($multicolumn_field, $multicolumn_column) = explode(':', $field);
318
      }
319
      // If a field name starts with a ':' but we are not yet tracking a
320
      // multicolumn field we don't know to which field this belongs.
321
      elseif (empty($multicolumn_field)) {
322
        throw new \Exception('Field name missing for ' . $field);
323
      }
324
      // Update the column name if the field name starts with a ':' and we are
325
      // already tracking a multicolumn field.
326
      else {
327
        $multicolumn_column = substr($field, 1);
328
      }
329
330
      $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...
331
      $field_name = $multicolumn_field ?: $field;
332
      if ($this->getDriver()->isField($entity_type, $field_name)) {
333
        // Split up multiple values in multi-value fields.
334
        $values = array();
335
        foreach (explode(', ', $field_value) as $key => $value) {
336
          $columns = $value;
337
          // Split up field columns if the ' - ' separator is present.
338
          if (strstr($value, ' - ') !== FALSE) {
339
            $columns = array();
340
            foreach (explode(' - ', $value) as $column) {
341
              // Check if it is an inline named column.
342
              if (!$is_multicolumn && strpos($column, ': ', 1) !== FALSE) {
343
                list ($key, $column) = explode(': ', $column);
344
                $columns[$key] = $column;
345
              }
346
              else {
347
                $columns[] = $column;
348
              }
349
            }
350
          }
351
          // Use the column name if we are tracking a multicolumn field.
352
          if ($is_multicolumn) {
353
            $multicolumn_fields[$multicolumn_field][$key][$multicolumn_column] = $columns;
354
            unset($entity->$field);
355
          }
356
          else {
357
            $values[] = $columns;
358
          }
359
        }
360
        // Replace regular fields inline in the entity after parsing.
361
        if (!$is_multicolumn) {
362
          $entity->$field_name = $values;
363
        }
364
      }
365
    }
366
367
    // Add the multicolumn fields to the entity.
368
    foreach ($multicolumn_fields as $field_name => $columns) {
369
      $entity->$field_name = $columns;
370
    }
371
  }
372
373
  /**
374
   * Create a user.
375
   *
376
   * @return object
377
   *   The created user.
378
   */
379 View Code Duplication
  public function userCreate($user) {
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...
380
    $this->dispatchHooks('BeforeUserCreateScope', $user);
381
    $this->parseEntityFields('user', $user);
382
    $this->getDriver()->userCreate($user);
383
    $this->dispatchHooks('AfterUserCreateScope', $user);
384
    $this->users[$user->name] = $this->user = $user;
385
    return $user;
386
  }
387
388
  /**
389
   * Create a term.
390
   *
391
   * @return object
392
   *   The created term.
393
   */
394 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...
395
    $this->dispatchHooks('BeforeTermCreateScope', $term);
396
    $this->parseEntityFields('taxonomy_term', $term);
397
    $saved = $this->getDriver()->createTerm($term);
398
    $this->dispatchHooks('AfterTermCreateScope', $saved);
399
    $this->terms[] = $saved;
400
    return $saved;
401
  }
402
403
  /**
404
   * Creates a language.
405
   *
406
   * @param \stdClass $language
407
   *   An object with the following properties:
408
   *   - langcode: the langcode of the language to create.
409
   *
410
   * @return object|FALSE
411
   *   The created language, or FALSE if the language was already created.
412
   */
413
  public function languageCreate(\stdClass $language) {
414
    $this->dispatchHooks('BeforeLanguageCreateScope', $language);
415
    $language = $this->getDriver()->languageCreate($language);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Drupal\Driver\DriverInterface as the method languageCreate() does only exist in the following implementations of said interface: Drupal\Driver\DrupalDriver.

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...
416
    if ($language) {
417
      $this->dispatchHooks('AfterLanguageCreateScope', $language);
418
      $this->languages[$language->langcode] = $language;
419
    }
420
    return $language;
421
  }
422
423
  /**
424
   * Log-in the current user.
425
   */
426
  public function login() {
427
    // Check if logged in.
428
    if ($this->loggedIn()) {
429
      $this->logout();
430
    }
431
432
    if (!$this->user) {
433
      throw new \Exception('Tried to login without a user.');
434
    }
435
436
    $this->getSession()->visit($this->locatePath('/user'));
437
    $element = $this->getSession()->getPage();
438
    $element->fillField($this->getDrupalText('username_field'), $this->user->name);
439
    $element->fillField($this->getDrupalText('password_field'), $this->user->pass);
440
    $submit = $element->findButton($this->getDrupalText('log_in'));
441
    if (empty($submit)) {
442
      throw new \Exception(sprintf("No submit button at %s", $this->getSession()->getCurrentUrl()));
443
    }
444
445
    // Log in.
446
    $submit->click();
447
448
    if (!$this->loggedIn()) {
449
      throw new \Exception(sprintf("Failed to log in as user '%s' with role '%s'", $this->user->name, $this->user->role));
450
    }
451
  }
452
453
  /**
454
   * Logs the current user out.
455
   */
456
  public function logout() {
457
    $this->getSession()->visit($this->locatePath('/user/logout'));
458
  }
459
460
  /**
461
   * Determine if the a user is already logged in.
462
   *
463
   * @return boolean
464
   *   Returns TRUE if a user is logged in for this session.
465
   */
466
  public function loggedIn() {
467
    $session = $this->getSession();
468
    $session->visit($this->locatePath('/'));
469
470
    // If a logout link is found, we are logged in. While not perfect, this is
471
    // how Drupal SimpleTests currently work as well.
472
    $element = $session->getPage();
473
    return $element->findLink($this->getDrupalText('log_out'));
474
  }
475
476
  /**
477
   * User with a given role is already logged in.
478
   *
479
   * @param string $role
480
   *   A single role, or multiple comma-separated roles in a single string.
481
   *
482
   * @return boolean
483
   *   Returns TRUE if the current logged in user has this role (or roles).
484
   */
485
  public function loggedInWithRole($role) {
486
    return $this->loggedIn() && $this->user && isset($this->user->role) && $this->user->role == $role;
487
  }
488
489
  /**
490
   * Installs the given modules.
491
   *
492
   * @param array $modules
493
   *   A list of machine names of modules to install.
494
   */
495
  public function installModules(array $modules) {
496
    $this->getDriver()->installModules($modules);
497
    $this->modules = array_merge($this->modules, $modules);
498
  }
499
500
  /**
501
   * Uninstalls the given modules.
502
   *
503
   * @param array $modules
504
   *   A list of machine names of modules to uninstall.
505
   */
506
  public function uninstallModules(array $modules) {
507
    $this->getDriver()->uninstallModules($modules);
508
  }
509
510
}
511