Completed
Push — master ( 395de8...d4355e )
by Jonathan
48:58
created

DrushDriver::createEntity()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 8
Ratio 100 %

Importance

Changes 0
Metric Value
dl 8
loc 8
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
namespace Drupal\Driver;
4
5
use Drupal\Component\Utility\Random;
6
use Drupal\Driver\Exception\BootstrapException;
7
8
use Symfony\Component\Process\Process;
9
10
/**
11
 * Implements DriverInterface.
12
 */
13
class DrushDriver extends BaseDriver {
14
  /**
15
   * Store a drush alias for tests requiring shell access.
16
   *
17
   * @var string
18
   */
19
  public $alias;
20
21
  /**
22
   * Stores the root path to a Drupal installation.
23
   *
24
   * This is an alternative to using drush aliases.
25
   *
26
   * @var string
27
   */
28
  public $root;
29
30
  /**
31
   * Store the path to drush binary.
32
   *
33
   * @var string
34
   */
35
  public $binary;
36
37
  /**
38
   * Track bootstrapping.
39
   *
40
   * @var bool
41
   */
42
  private $bootstrapped = FALSE;
43
44
  /**
45
   * Random generator.
46
   *
47
   * @var \Drupal\Component\Utility\Random
48
   */
49
  private $random;
50
51
  /**
52
   * Global arguments or options for drush commands.
53
   *
54
   * @var string
55
   */
56
  private $arguments = '';
57
58
  /**
59
   * Tracks legacy drush.
60
   *
61
   * @var bool
62
   */
63
  protected static $isLegacyDrush;
64
65
  /**
66
   * Set drush alias or root path.
67
   *
68
   * @param string $alias
69
   *   A drush alias.
70
   * @param string $root_path
71
   *   The root path of the Drupal install. This is an alternative to using
72
   *   aliases.
73
   * @param string $binary
74
   *   The path to the drush binary.
75
   * @param \Drupal\Component\Utility\Random $random
76
   *   Random generator.
77
   *
78
   * @throws \Drupal\Driver\Exception\BootstrapException
79
   *   Thrown when a required parameter is missing.
80
   */
81
  public function __construct($alias = NULL, $root_path = NULL, $binary = 'drush', Random $random = NULL) {
82
    if (!empty($alias)) {
83
      // Trim off the '@' symbol if it has been added.
84
      $alias = ltrim($alias, '@');
85
86
      $this->alias = $alias;
87
    }
88
    elseif (!empty($root_path)) {
89
      $this->root = realpath($root_path);
90
    }
91
    else {
92
      throw new BootstrapException('A drush alias or root path is required.');
93
    }
94
95
    $this->binary = $binary;
96
97
    if (!isset($random)) {
98
      $random = new Random();
99
    }
100
    $this->random = $random;
101
  }
102
103
  /**
104
   * {@inheritdoc}
105
   */
106
  public function getRandom() {
107
    return $this->random;
108
  }
109
110
  /**
111
   * {@inheritdoc}
112
   */
113
  public function bootstrap() {
114
    // Check that the given alias works.
115
    // @todo check that this is a functioning alias.
116
    // See http://drupal.org/node/1615450
117
    if (!isset($this->alias) && !isset($this->root)) {
118
      throw new BootstrapException('A drush alias or root path is required.');
119
    }
120
121
    // Determine if drush version is legacy.
122
    if (!isset(self::$isLegacyDrush)) {
123
      self::$isLegacyDrush = $this->isLegacyDrush();
124
    }
125
126
    $this->bootstrapped = TRUE;
127
  }
128
129
  /**
130
   * Determine if drush is a legacy version.
131
   *
132
   * @return bool
133
   *   Returns TRUE if drush is older than drush 9.
134
   */
135
  protected function isLegacyDrush() {
136
    try {
137
      // Try for a drush 9 version.
138
      $version = trim($this->drush('version', [], ['format' => 'string']));
139
      return version_compare($version, '9', '<=');
140
    }
141
    catch (\RuntimeException $e) {
142
      // The version of drush is old enough that only `--version` was available,
143
      // so this is a legacy version.
144
      return TRUE;
145
    }
146
  }
147
148
  /**
149
   * {@inheritdoc}
150
   */
151
  public function isBootstrapped() {
152
    return $this->bootstrapped;
153
  }
154
155
  /**
156
   * {@inheritdoc}
157
   */
158
  public function userCreate(\stdClass $user) {
159
    $arguments = array(
160
      sprintf('"%s"', $user->name),
161
    );
162
    $options = array(
163
      'password' => $user->pass,
164
      'mail' => $user->mail,
165
    );
166
    $result = $this->drush('user-create', $arguments, $options);
167
    if ($uid = $this->parseUserId($result)) {
168
      $user->uid = $uid;
169
    }
170
    if (isset($user->roles) && is_array($user->roles)) {
171
      foreach ($user->roles as $role) {
172
        $this->userAddRole($user, $role);
173
      }
174
    }
175
  }
176
177
  /**
178
   * Parse user id from drush user-information output.
179
   */
180
  protected function parseUserId($info) {
181
    // Find the row containing "User ID : xxx".
182
    preg_match('/User ID\s+:\s+\d+/', $info, $matches);
183
    if (!empty($matches)) {
184
      // Extract the ID from the row.
185
      list(, $uid) = explode(':', $matches[0]);
186
      return (int) $uid;
187
    }
188
  }
189
190
  /**
191
   * {@inheritdoc}
192
   */
193
  public function userDelete(\stdClass $user) {
194
    $arguments = array(sprintf('"%s"', $user->name));
195
    $options = array(
196
      'yes' => NULL,
197
      'delete-content' => NULL,
198
    );
199
    $this->drush('user-cancel', $arguments, $options);
200
  }
201
202
  /**
203
   * {@inheritdoc}
204
   */
205
  public function userAddRole(\stdClass $user, $role) {
206
    $arguments = array(
207
      sprintf('"%s"', $role),
208
      sprintf('"%s"', $user->name),
209
    );
210
    $this->drush('user-add-role', $arguments);
211
  }
212
213
  /**
214
   * {@inheritdoc}
215
   */
216
  public function fetchWatchdog($count = 10, $type = NULL, $severity = NULL) {
217
    $options = array(
218
      'count' => $count,
219
      'type' => $type,
220
      'severity' => $severity,
221
    );
222
    return $this->drush('watchdog-show', array(), $options);
223
  }
224
225
  /**
226
   * {@inheritdoc}
227
   */
228
  public function clearCache($type = 'all') {
229
    if (self::$isLegacyDrush) {
230
      $type = array($type);
231
      return $this->drush('cache-clear', $type, array());
232
    }
233
    if (($type == 'all') || ($type == 'drush')) {
234
      $this->drush('cache-clear', array('drush'), array());
235
      if ($type == 'drush') {
236
        return;
237
      }
238
    }
239
    return $this->drush('cache:rebuild');
240
  }
241
242
  /**
243
   * {@inheritdoc}
244
   */
245
  public function clearStaticCaches() {
246
    // The drush driver does each operation as a separate request;
247
    // therefore, 'clearStaticCaches' can be a no-op.
248
  }
249
250
  /**
251
   * Decodes JSON object returned by Drush.
252
   *
253
   * It will clean up any junk that may have appeared before or after the
254
   * JSON object. This can happen with remote Drush aliases.
255
   *
256
   * @param string $output
257
   *   The output from Drush.
258
   *
259
   * @return object
260
   *   The decoded JSON object.
261
   */
262
  protected function decodeJsonObject($output) {
263
    // Remove anything before the first '{'.
264
    $output = preg_replace('/^[^\{]*/', '', $output);
265
    // Remove anything after the last '}'.
266
    $output = preg_replace('/[^\}]*$/s', '', $output);
267
    return json_decode($output);
268
  }
269
270
  /**
271
   * {@inheritdoc}
272
   */
273 View Code Duplication
  public function createEntity($entity_type, \StdClass $entity) {
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...
274
    $options = array(
275
      'entity_type' => $entity_type,
276
      'entity' => $entity,
277
    );
278
    $result = $this->drush('behat', array('create-entity', escapeshellarg(json_encode($options))), array());
279
    return $this->decodeJsonObject($result);
280
  }
281
282
  /**
283
   * {@inheritdoc}
284
   */
285 View Code Duplication
  public function entityDelete($entity_type, \StdClass $entity) {
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...
286
    $options = array(
287
      'entity_type' => $entity_type,
288
      'entity' => $entity,
289
    );
290
    $this->drush('behat', array('delete-entity', escapeshellarg(json_encode($options))), array());
291
  }
292
293
  /**
294
   * {@inheritdoc}
295
   */
296
  public function createNode($node) {
297
    // Look up author by name.
298
    if (isset($node->author)) {
299
      $user_info = $this->drush('user-information', array(sprintf('"%s"', $node->author)));
300
      if ($uid = $this->parseUserId($user_info)) {
301
        $node->uid = $uid;
302
      }
303
    }
304
    $result = $this->drush('behat', array('create-node', escapeshellarg(json_encode($node))), array());
305
    return $this->decodeJsonObject($result);
306
  }
307
308
  /**
309
   * {@inheritdoc}
310
   */
311
  public function nodeDelete($node) {
312
    $this->drush('behat', array('delete-node', escapeshellarg(json_encode($node))), array());
313
  }
314
315
  /**
316
   * {@inheritdoc}
317
   */
318
  public function createTerm(\stdClass $term) {
319
    $result = $this->drush('behat', array('create-term', escapeshellarg(json_encode($term))), array());
320
    return $this->decodeJsonObject($result);
321
  }
322
323
  /**
324
   * {@inheritdoc}
325
   */
326
  public function termDelete(\stdClass $term) {
327
    $this->drush('behat', array('delete-term', escapeshellarg(json_encode($term))), array());
328
  }
329
330
  /**
331
   * {@inheritdoc}
332
   */
333
  public function isField($entity_type, $field_name) {
334
    // If the Behat Drush Endpoint is not installed on the site-under-test,
335
    // then the drush() method will throw an exception. In this instance, we
336
    // want to treat all potential fields as non-fields.  This allows the
337
    // Drush Driver to work with certain built-in Drush capabilities (e.g.
338
    // creating users) even if the Behat Drush Endpoint is not available.
339
    try {
340
      $value = array($entity_type, $field_name);
341
      $arguments = array('is-field', escapeshellarg(json_encode($value)));
342
      $result = $this->drush('behat', $arguments, array());
343
      return strpos($result, "true\n") !== FALSE;
344
    }
345
    catch (\Exception $e) {
346
      return FALSE;
347
    }
348
  }
349
350
  /**
351
   * Sets common drush arguments or options.
352
   *
353
   * @param string $arguments
354
   *   Global arguments to add to every drush command.
355
   */
356
  public function setArguments($arguments) {
357
    $this->arguments = $arguments;
358
  }
359
360
  /**
361
   * Get common drush arguments.
362
   */
363
  public function getArguments() {
364
    return $this->arguments;
365
  }
366
367
  /**
368
   * Parse arguments into a string.
369
   *
370
   * @param array $arguments
371
   *   An array of argument/option names to values.
372
   *
373
   * @return string
374
   *   The parsed arguments.
375
   */
376
  protected static function parseArguments(array $arguments) {
377
    $string = '';
378
    foreach ($arguments as $name => $value) {
379
      if (is_null($value)) {
380
        $string .= ' --' . $name;
381
      }
382
      else {
383
        $string .= ' --' . $name . '=' . $value;
384
      }
385
    }
386
    return $string;
387
  }
388
389
  /**
390
   * Execute a drush command.
391
   */
392
  public function drush($command, array $arguments = array(), array $options = array()) {
393
    $arguments = implode(' ', $arguments);
394
395
    // Disable colored output from drush.
396
    if (isset(static::$isLegacyDrush) && static::$isLegacyDrush) {
397
      $options['nocolor'] = TRUE;
398
    }
399
    else {
400
      $options['no-ansi'] = NULL;
401
    }
402
    $string_options = $this->parseArguments($options);
403
404
    $alias = isset($this->alias) ? "@{$this->alias}" : '--root=' . $this->root;
405
406
    // Add any global arguments.
407
    $global = $this->getArguments();
408
409
    $process = new Process("{$this->binary} {$alias} {$string_options} {$global} {$command} {$arguments}");
410
    $process->setTimeout(3600);
411
    $process->run();
412
413
    if (!$process->isSuccessful()) {
414
      throw new \RuntimeException($process->getErrorOutput());
415
    }
416
417
    // Some drush commands write to standard error output (for example enable
418
    // use drush_log which default to _drush_print_log) instead of returning a
419
    // string (drush status use drush_print_pipe).
420
    if (!$process->getOutput()) {
421
      return $process->getErrorOutput();
422
    }
423
    else {
424
      return $process->getOutput();
425
    }
426
427
  }
428
429
  /**
430
   * {@inheritdoc}
431
   */
432
  public function processBatch() {
433
    // Do nothing. Drush should internally handle any needs for processing
434
    // batch ops.
435
  }
436
437
  /**
438
   * {@inheritdoc}
439
   */
440
  public function runCron() {
441
    $this->drush('cron');
442
  }
443
444
  /**
445
   * Run Drush commands dynamically from a DrupalContext.
446
   */
447
  public function __call($name, $arguments) {
448
    return $this->drush($name, $arguments);
449
  }
450
451
}
452