Completed
Pull Request — master (#190)
by
unknown
12:23
created

DrushDriver::isLegacyDrush()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 3
nop 0
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
    $type = array($type);
230
    return $this->drush('cache-clear', $type, array());
231
  }
232
233
  /**
234
   * {@inheritdoc}
235
   */
236
  public function clearStaticCaches() {
237
    // The drush driver does each operation as a separate request;
238
    // therefore, 'clearStaticCaches' can be a no-op.
239
  }
240
241
  /**
242
   * Decodes JSON object returned by Drush.
243
   *
244
   * It will clean up any junk that may have appeared before or after the
245
   * JSON object. This can happen with remote Drush aliases.
246
   *
247
   * @param string $output
248
   *   The output from Drush.
249
   *
250
   * @return object
251
   *   The decoded JSON object.
252
   */
253
  protected function decodeJsonObject($output) {
254
    // Remove anything before the first '{'.
255
    $output = preg_replace('/^[^\{]*/', '', $output);
256
    // Remove anything after the last '}'.
257
    $output = preg_replace('/[^\}]*$/s', '', $output);
258
    return json_decode($output);
259
  }
260
261
  /**
262
   * {@inheritdoc}
263
   */
264 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...
265
    $options = array(
266
      'entity_type' => $entity_type,
267
      'entity' => $entity,
268
    );
269
    $result = $this->drush('behat', array('create-entity', escapeshellarg(json_encode($options))), array());
270
    return $this->decodeJsonObject($result);
271
  }
272
273
  /**
274
   * {@inheritdoc}
275
   */
276 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...
277
    $options = array(
278
      'entity_type' => $entity_type,
279
      'entity' => $entity,
280
    );
281
    $this->drush('behat', array('delete-entity', escapeshellarg(json_encode($options))), array());
282
  }
283
284
  /**
285
   * {@inheritdoc}
286
   */
287
  public function createNode($node) {
288
    // Look up author by name.
289
    if (isset($node->author)) {
290
      $user_info = $this->drush('user-information', array(sprintf('"%s"', $node->author)));
291
      if ($uid = $this->parseUserId($user_info)) {
292
        $node->uid = $uid;
293
      }
294
    }
295
    $result = $this->drush('behat', array('create-node', escapeshellarg(json_encode($node))), array());
296
    return $this->decodeJsonObject($result);
297
  }
298
299
  /**
300
   * {@inheritdoc}
301
   */
302
  public function nodeDelete($node) {
303
    $this->drush('behat', array('delete-node', escapeshellarg(json_encode($node))), array());
304
  }
305
306
  /**
307
   * {@inheritdoc}
308
   */
309
  public function createTerm(\stdClass $term) {
310
    $result = $this->drush('behat', array('create-term', escapeshellarg(json_encode($term))), array());
311
    return $this->decodeJsonObject($result);
312
  }
313
314
  /**
315
   * {@inheritdoc}
316
   */
317
  public function termDelete(\stdClass $term) {
318
    $this->drush('behat', array('delete-term', escapeshellarg(json_encode($term))), array());
319
  }
320
321
  /**
322
   * {@inheritdoc}
323
   */
324
  public function isField($entity_type, $field_name) {
325
    // If the Behat Drush Endpoint is not installed on the site-under-test,
326
    // then the drush() method will throw an exception. In this instance, we
327
    // want to treat all potential fields as non-fields.  This allows the
328
    // Drush Driver to work with certain built-in Drush capabilities (e.g.
329
    // creating users) even if the Behat Drush Endpoint is not available.
330
    try {
331
      $value = array($entity_type, $field_name);
332
      $arguments = array('is-field', escapeshellarg(json_encode($value)));
333
      $result = $this->drush('behat', $arguments, array());
334
      return strpos($result, "true\n") !== FALSE;
335
    }
336
    catch (\Exception $e) {
337
      return FALSE;
338
    }
339
  }
340
341
  /**
342
   * Sets common drush arguments or options.
343
   *
344
   * @param string $arguments
345
   *   Global arguments to add to every drush command.
346
   */
347
  public function setArguments($arguments) {
348
    $this->arguments = $arguments;
349
  }
350
351
  /**
352
   * Get common drush arguments.
353
   */
354
  public function getArguments() {
355
    return $this->arguments;
356
  }
357
358
  /**
359
   * Parse arguments into a string.
360
   *
361
   * @param array $arguments
362
   *   An array of argument/option names to values.
363
   *
364
   * @return string
365
   *   The parsed arguments.
366
   */
367
  protected static function parseArguments(array $arguments) {
368
    $string = '';
369
    foreach ($arguments as $name => $value) {
370
      if (is_null($value)) {
371
        $string .= ' --' . $name;
372
      }
373
      else {
374
        $string .= ' --' . $name . '=' . $value;
375
      }
376
    }
377
    return $string;
378
  }
379
380
  /**
381
   * Execute a drush command.
382
   */
383
  public function drush($command, array $arguments = array(), array $options = array()) {
384
    $arguments = implode(' ', $arguments);
385
386
    // Disable colored output from drush.
387
    if (isset(static::$isLegacyDrush) && static::$isLegacyDrush) {
388
      $options['nocolor'] = TRUE;
389
    }
390
    else {
391
      $options['no-ansi'] = NULL;
392
    }
393
    $string_options = $this->parseArguments($options);
394
395
    $alias = isset($this->alias) ? "@{$this->alias}" : '--root=' . $this->root;
396
397
    // Add any global arguments.
398
    $global = $this->getArguments();
399
400
    $process = new Process("{$this->binary} {$alias} {$string_options} {$global} {$command} {$arguments}");
401
    $process->setTimeout(3600);
402
    $process->run();
403
404
    if (!$process->isSuccessful()) {
405
      throw new \RuntimeException($process->getErrorOutput());
406
    }
407
408
    // Some drush commands write to standard error output (for example enable
409
    // use drush_log which default to _drush_print_log) instead of returning a
410
    // string (drush status use drush_print_pipe).
411
    if (!$process->getOutput()) {
412
      return $process->getErrorOutput();
413
    }
414
    else {
415
      return $process->getOutput();
416
    }
417
418
  }
419
420
  /**
421
   * {@inheritdoc}
422
   */
423
  public function processBatch() {
424
    // Do nothing. Drush should internally handle any needs for processing
425
    // batch ops.
426
  }
427
428
  /**
429
   * {@inheritdoc}
430
   */
431
  public function runCron() {
432
    $this->drush('cron');
433
  }
434
435
  /**
436
   * Run Drush commands dynamically from a DrupalContext.
437
   */
438
  public function __call($name, $arguments) {
439
    return $this->drush($name, $arguments);
440
  }
441
442
}
443