Completed
Pull Request — 3.1 (#290)
by
unknown
09:17
created

RawDrupalContext::getDrupalParameter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 1
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
   * {@inheritDoc}
93
   */
94
  public function setDrupal(DrupalDriverManager $drupal) {
95
    $this->drupal = $drupal;
96
  }
97
98
  /**
99
   * {@inheritDoc}
100
   */
101
  public function getDrupal() {
102
    return $this->drupal;
103
  }
104
105
  /**
106
   * {@inheritDoc}
107
   */
108
  public function setDispatcher(HookDispatcher $dispatcher) {
109
    $this->dispatcher = $dispatcher;
110
  }
111
112
  /**
113
   * Set parameters provided for Drupal.
114
   */
115
  public function setDrupalParameters(array $parameters) {
116
    $this->drupalParameters = $parameters;
117
  }
118
119
  /**
120
   * Returns a specific Drupal parameter.
121
   *
122
   * @param string $name
123
   *   Parameter name.
124
   *
125
   * @return mixed
126
   */
127
  public function getDrupalParameter($name) {
128
    return isset($this->drupalParameters[$name]) ? $this->drupalParameters[$name] : NULL;
129
  }
130
131
  /**
132
   * Returns a specific Drupal text value.
133
   *
134
   * @param string $name
135
   *   Text value name, such as 'log_out', which corresponds to the default 'Log
136
   *   out' link text.
137
   * @throws \Exception
138
   * @return
139
   */
140 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...
141
    $text = $this->getDrupalParameter('text');
142
    if (!isset($text[$name])) {
143
      throw new \Exception(sprintf('No such Drupal string: %s', $name));
144
    }
145
    return $text[$name];
146
  }
147
148
  /**
149
   * Get active Drupal Driver.
150
   */
151
  public function getDriver($name = NULL) {
152
    return $this->getDrupal()->getDriver($name);
153
  }
154
155
  /**
156
   * Get driver's random generator.
157
   */
158
  public function getRandom() {
159
    return $this->getDriver()->getRandom();
160
  }
161
162
  /**
163
   * Remove any created nodes.
164
   *
165
   * @AfterScenario
166
   */
167
  public function cleanNodes() {
168
    // Remove any nodes that were created.
169
    foreach ($this->nodes as $node) {
170
      $this->getDriver()->nodeDelete($node);
171
    }
172
    $this->nodes = array();
173
  }
174
175
  /**
176
   * Remove any created users.
177
   *
178
   * @AfterScenario
179
   */
180
  public function cleanUsers() {
181
    // Remove any users that were created.
182
    if (!empty($this->users)) {
183
      foreach ($this->users as $user) {
184
        $this->getDriver()->userDelete($user);
185
      }
186
      $this->getDriver()->processBatch();
187
      $this->users = array();
188
    }
189
  }
190
191
  /**
192
   * Remove any created terms.
193
   *
194
   * @AfterScenario
195
   */
196
  public function cleanTerms() {
197
    // Remove any terms that were created.
198
    foreach ($this->terms as $term) {
199
      $this->getDriver()->termDelete($term);
200
    }
201
    $this->terms = array();
202
  }
203
204
  /**
205
   * Remove any created roles.
206
   *
207
   * @AfterScenario
208
   */
209
  public function cleanRoles() {
210
    // Remove any roles that were created.
211
    foreach ($this->roles as $rid) {
212
      $this->getDriver()->roleDelete($rid);
213
    }
214
    $this->roles = array();
215
  }
216
217
  /**
218
   * Remove any created languages.
219
   *
220
   * @AfterScenario
221
   */
222
  public function cleanLanguages() {
223
    // Delete any languages that were created.
224
    foreach ($this->languages as $language) {
225
      $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...
226
      unset($this->languages[$language->langcode]);
227
    }
228
  }
229
230
  /**
231
   * Clear static caches.
232
   *
233
   * @AfterScenario @api
234
   */
235
  public function clearStaticCaches() {
236
    $this->getDriver()->clearStaticCaches();
237
  }
238
239
  /**
240
   * Dispatch scope hooks.
241
   *
242
   * @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...
243
   *   The entity scope to dispatch.
244
   * @param stdClass $entity
245
   *   The entity.
246
   */
247
  protected function dispatchHooks($scopeType, \stdClass $entity) {
248
    $fullScopeClass = 'Drupal\\DrupalExtension\\Hook\\Scope\\' . $scopeType;
249
    $scope = new $fullScopeClass($this->getDrupal()->getEnvironment(), $this, $entity);
250
    $callResults = $this->dispatcher->dispatchScopeHooks($scope);
251
252
    // The dispatcher suppresses exceptions, throw them here if there are any.
253
    foreach ($callResults as $result) {
254
      if ($result->hasException()) {
255
        $exception = $result->getException();
256
        throw $exception;
257
      }
258
    }
259
  }
260
261
  /**
262
   * Create a node.
263
   *
264
   * @return object
265
   *   The created node.
266
   */
267 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...
268
    $this->dispatchHooks('BeforeNodeCreateScope', $node);
269
    $this->parseEntityFields('node', $node);
270
    $saved = $this->getDriver()->createNode($node);
271
    $this->dispatchHooks('AfterNodeCreateScope', $saved);
272
    $this->nodes[] = $saved;
273
    return $saved;
274
  }
275
276
  /**
277
   * Parse multi-value fields. Possible formats:
278
   *    A, B, C
279
   *    A - B, C - D, E - F
280
   *
281
   * @param string $entity_type
282
   *   The entity type.
283
   * @param \stdClass $entity
284
   *   An object containing the entity properties and fields as properties.
285
   */
286
  public function parseEntityFields($entity_type, \stdClass $entity) {
287
    $multicolumn_field = '';
288
    $multicolumn_fields = array();
289
290
    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...
291
      // Reset the multicolumn field if the field name does not contain a column.
292
      if (strpos($field, ':') === FALSE) {
293
        $multicolumn_field = '';
294
      }
295
      // Start tracking a new multicolumn field if the field name contains a ':'
296
      // which is preceded by at least 1 character.
297
      elseif (strpos($field, ':', 1) !== FALSE) {
298
        list($multicolumn_field, $multicolumn_column) = explode(':', $field);
299
      }
300
      // If a field name starts with a ':' but we are not yet tracking a
301
      // multicolumn field we don't know to which field this belongs.
302
      elseif (empty($multicolumn_field)) {
303
        throw new \Exception('Field name missing for ' . $field);
304
      }
305
      // Update the column name if the field name starts with a ':' and we are
306
      // already tracking a multicolumn field.
307
      else {
308
        $multicolumn_column = substr($field, 1);
309
      }
310
311
      $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...
312
      $field_name = $multicolumn_field ?: $field;
313
      if ($this->getDriver()->isField($entity_type, $field_name)) {
314
        // Split up multiple values in multi-value fields.
315
        $values = array();
316
        foreach (explode(', ', $field_value) as $key => $value) {
317
          $columns = $value;
318
          // Split up field columns if the ' - ' separator is present.
319
          if (strstr($value, ' - ') !== FALSE) {
320
            $columns = array();
321
            foreach (explode(' - ', $value) as $column) {
322
              // Check if it is an inline named column.
323
              if (!$is_multicolumn && strpos($column, ': ', 1) !== FALSE) {
324
                list ($key, $column) = explode(': ', $column);
325
                $columns[$key] = $column;
326
              }
327
              else {
328
                $columns[] = $column;
329
              }
330
            }
331
          }
332
          // Use the column name if we are tracking a multicolumn field.
333
          if ($is_multicolumn) {
334
            $multicolumn_fields[$multicolumn_field][$key][$multicolumn_column] = $columns;
335
            unset($entity->$field);
336
          }
337
          else {
338
            $values[] = $columns;
339
          }
340
        }
341
        // Replace regular fields inline in the entity after parsing.
342
        if (!$is_multicolumn) {
343
          $entity->$field_name = $values;
344
        }
345
      }
346
    }
347
348
    // Add the multicolumn fields to the entity.
349
    foreach ($multicolumn_fields as $field_name => $columns) {
350
      $entity->$field_name = $columns;
351
    }
352
  }
353
354
  /**
355
   * Create a user.
356
   *
357
   * @return object
358
   *   The created user.
359
   */
360 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...
361
    $this->dispatchHooks('BeforeUserCreateScope', $user);
362
    $this->parseEntityFields('user', $user);
363
    $this->getDriver()->userCreate($user);
364
    $this->dispatchHooks('AfterUserCreateScope', $user);
365
    $this->users[$user->name] = $this->user = $user;
366
    return $user;
367
  }
368
369
  /**
370
   * Create a term.
371
   *
372
   * @return object
373
   *   The created term.
374
   */
375 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...
376
    $this->dispatchHooks('BeforeTermCreateScope', $term);
377
    $this->parseEntityFields('taxonomy_term', $term);
378
    $saved = $this->getDriver()->createTerm($term);
379
    $this->dispatchHooks('AfterTermCreateScope', $saved);
380
    $this->terms[] = $saved;
381
    return $saved;
382
  }
383
384
  /**
385
   * Creates a language.
386
   *
387
   * @param \stdClass $language
388
   *   An object with the following properties:
389
   *   - langcode: the langcode of the language to create.
390
   *
391
   * @return object|FALSE
392
   *   The created language, or FALSE if the language was already created.
393
   */
394
  public function languageCreate(\stdClass $language) {
395
    $this->dispatchHooks('BeforeLanguageCreateScope', $language);
396
    $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...
397
    if ($language) {
398
      $this->dispatchHooks('AfterLanguageCreateScope', $language);
399
      $this->languages[$language->langcode] = $language;
400
    }
401
    return $language;
402
  }
403
404
  /**
405
   * Log-in the current user.
406
   */
407
  public function login() {
408
    // Check if logged in.
409
    if ($this->loggedIn()) {
410
      $this->logout();
411
    }
412
413
    if (!$this->user) {
414
      throw new \Exception('Tried to login without a user.');
415
    }
416
417
    $this->getSession()->visit($this->locatePath('/user'));
418
    $element = $this->getSession()->getPage();
419
    $element->fillField($this->getDrupalText('username_field'), $this->user->name);
420
    $element->fillField($this->getDrupalText('password_field'), $this->user->pass);
421
    $submit = $element->findButton($this->getDrupalText('log_in'));
422
    if (empty($submit)) {
423
      throw new \Exception(sprintf("No submit button at %s", $this->getSession()->getCurrentUrl()));
424
    }
425
426
    // Log in.
427
    $submit->click();
428
429
    if (!$this->loggedIn()) {
430
      throw new \Exception(sprintf("Failed to log in as user '%s' with role '%s'", $this->user->name, $this->user->role));
431
    }
432
  }
433
434
  /**
435
   * Logs the current user out.
436
   */
437
  public function logout() {
438
    $this->getSession()->visit($this->locatePath('/user/logout'));
439
  }
440
441
  /**
442
   * Determine if the a user is already logged in.
443
   *
444
   * @return boolean
445
   *   Returns TRUE if a user is logged in for this session.
446
   */
447
  public function loggedIn() {
448
    $session = $this->getSession();
449
    $session->visit($this->locatePath('/'));
450
451
    // If a logout link is found, we are logged in. While not perfect, this is
452
    // how Drupal SimpleTests currently work as well.
453
    $element = $session->getPage();
454
    return $element->findLink($this->getDrupalText('log_out'));
455
  }
456
457
  /**
458
   * User with a given role is already logged in.
459
   *
460
   * @param string $role
461
   *   A single role, or multiple comma-separated roles in a single string.
462
   *
463
   * @return boolean
464
   *   Returns TRUE if the current logged in user has this role (or roles).
465
   */
466
  public function loggedInWithRole($role) {
467
    return $this->loggedIn() && $this->user && isset($this->user->role) && $this->user->role == $role;
468
  }
469
470
}
471