5 use Consolidation\AnnotatedCommand\AnnotationData;
6 use Robo\Common\ConfigAwareTrait;
7 use DrupalFinder\DrupalFinder;
8 use Drush\Log\LogLevel;
9 use Psr\Log\LoggerAwareInterface;
10 use Psr\Log\LoggerAwareTrait;
11 use Robo\Contract\ConfigAwareInterface;
13 class BootstrapManager implements LoggerAwareInterface, AutoloaderAwareInterface, ConfigAwareInterface
16 use AutoloaderAwareTrait;
22 protected $drupalFinder;
25 * @var \Drush\Boot\Boot[]
27 protected $bootstrapCandidates = [];
30 * @var \Drush\Boot\Boot
32 protected $defaultBootstrapObject;
35 * @var \Drush\Boot\Boot
52 * @param \Drush\Boot\Boot
53 * The default bootstrap object to use when there are
54 * no viable candidates to use (e.g. no selected site)
56 public function __construct(Boot $default)
58 $this->defaultBootstrapObject = $default;
60 // Reset our bootstrap phase to the beginning
61 drush_set_context('DRUSH_BOOTSTRAP_PHASE', DRUSH_BOOTSTRAP_NONE);
65 * Add a bootstrap object to the list of candidates
67 * @param \Drush\Boot\Boot|Array
68 * List of boot candidates
70 public function add($candidateList)
72 foreach (func_get_args() as $candidate) {
73 $this->bootstrapCandidates[] = $candidate;
77 public function drupalFinder()
79 if (!isset($this->drupalFinder)) {
80 $this->drupalFinder = new DrupalFinder();
82 return $this->drupalFinder;
85 public function setDrupalFinder(DrupalFinder $drupalFinder)
87 $this->drupalFinder = $drupalFinder;
91 * Return the framework root selected by the user.
93 public function getRoot()
95 return $this->drupalFinder()->getDrupalRoot();
99 * Return the composer root for the selected Drupal site.
101 public function getComposerRoot()
103 return $this->drupalFinder()->getComposerRoot();
106 public function locateRoot($root, $start_path = null)
108 // TODO: Throw if we already bootstrapped a framework?
111 $root = $this->getConfig()->cwd();
113 if (!$this->drupalFinder()->locateRoot($root)) {
114 // echo ' Drush must be executed within a Drupal site.'. PHP_EOL;
120 * Return the framework uri selected by the user.
122 public function getUri()
128 * This method is called by the Application iff the user
129 * did not explicitly provide a URI.
131 public function selectUri($cwd)
133 $uri = $this->bootstrap()->findUri($this->getRoot(), $cwd);
138 public function setUri($uri)
140 // TODO: Throw if we already bootstrapped a framework?
141 // n.b. site-install needs to set the uri.
143 if ($this->bootstrap) {
144 $this->bootstrap->setUri($this->getUri());
149 * Return the bootstrap object in use. This will
150 * be the latched bootstrap object if we have started
151 * bootstrapping; otherwise, it will be whichever bootstrap
152 * object is best for the selected root.
154 * @return \Drush\Boot\Boot
156 public function bootstrap()
158 if ($this->bootstrap) {
159 return $this->bootstrap;
161 return $this->selectBootstrapClass();
165 * Look up the best bootstrap class for the given location
166 * from the set of available candidates.
168 * @return \Drush\Boot\Boot
170 public function bootstrapObjectForRoot($path)
172 foreach ($this->bootstrapCandidates as $candidate) {
173 if ($candidate->validRoot($path)) {
174 // This is not necessary when the autoloader is inflected
175 // TODO: The autoloader is inflected in the symfony dispatch, but not the traditional Drush dispatcher
176 if ($candidate instanceof AutoloaderAwareInterface) {
177 $candidate->setAutoloader($this->autoloader());
179 $candidate->setUri($this->getUri());
187 * Select the bootstrap class to use. If this is called multiple
188 * times, the bootstrap class returned might change on subsequent
189 * calls, if the root directory changes. Once the bootstrap object
190 * starts changing the state of the system, however, it will
191 * be 'latched', and further calls to Drush::bootstrapf()
192 * will always return the same object.
194 protected function selectBootstrapClass()
196 // Once we have selected a Drupal root, we will reduce our bootstrap
197 // candidates down to just the one used to select this site root.
198 $bootstrap = $this->bootstrapObjectForRoot($this->getRoot());
199 // If we have not found a bootstrap class by this point,
200 // then return our default bootstrap object. The default bootstrap object
201 // should pass through all calls without doing anything that
202 // changes state in a CMS-specific way.
203 if ($bootstrap == null) {
204 $bootstrap = $this->defaultBootstrapObject;
211 * Once bootstrapping has started, we stash the bootstrap
212 * object being used, and do not allow it to change any
215 public function latch($bootstrap)
217 $this->bootstrap = $bootstrap;
221 * Returns an array that determines what bootstrap phases
222 * are necessary to bootstrap the CMS.
224 * @param bool $function_names
225 * (optional) If TRUE, return an array of method names index by their
226 * corresponding phase values. Otherwise return an array of phase values.
230 * @see \Drush\Boot\Boot::bootstrapPhases()
232 public function bootstrapPhases($function_names = false)
236 if ($bootstrap = $this->bootstrap()) {
237 $result = $bootstrap->bootstrapPhases();
238 if (!$function_names) {
239 $result = array_keys($result);
246 * Bootstrap Drush to the desired phase.
248 * This function will sequentially bootstrap each
249 * lower phase up to the phase that has been requested.
252 * The bootstrap phase to bootstrap to.
253 * @param int $phase_max
254 * (optional) The maximum level to boot to. This does not have a use in this
255 * function itself but can be useful for other code called from within this
256 * function, to know if e.g. a caller is in the process of booting to the
257 * specified level. If specified, it should never be lower than $phase.
258 * @param \Consolidation\AnnotatedCommand\AnnotationData $annotationData
259 * Optional annotation data from the command.
262 * TRUE if the specified bootstrap phase has completed.
264 * @see \Drush\Boot\Boot::bootstrapPhases()
266 public function doBootstrap($phase, $phase_max = false, AnnotationData $annotationData = null)
268 $bootstrap = $this->bootstrap();
269 $phases = $this->bootstrapPhases(true);
272 // If the requested phase does not exist in the list of available
273 // phases, it means that the command requires bootstrap to a certain
274 // level, but no site root could be found.
275 if (!isset($phases[$phase])) {
276 $result = drush_bootstrap_error('DRUSH_NO_SITE', dt("We could not find an applicable site for that command."));
279 // Once we start bootstrapping past the DRUSH_BOOTSTRAP_DRUSH phase, we
280 // will latch the bootstrap object, and prevent it from changing.
281 if ($phase > DRUSH_BOOTSTRAP_DRUSH) {
282 $this->latch($bootstrap);
285 drush_set_context('DRUSH_BOOTSTRAPPING', true);
286 foreach ($phases as $phase_index => $current_phase) {
287 $bootstrapped_phase = drush_get_context('DRUSH_BOOTSTRAP_PHASE', -1);
288 if ($phase_index > $phase) {
291 if ($phase_index > $bootstrapped_phase) {
292 if ($result = $this->bootstrapValidate($phase_index)) {
293 if (method_exists($bootstrap, $current_phase) && !drush_get_error()) {
294 $this->logger->log(LogLevel::BOOTSTRAP, 'Drush bootstrap phase: {function}()', ['function' => $current_phase]);
295 $bootstrap->{$current_phase}($annotationData);
297 drush_set_context('DRUSH_BOOTSTRAP_PHASE', $phase_index);
301 drush_set_context('DRUSH_BOOTSTRAPPING', false);
302 if (!$result || drush_get_error()) {
303 $errors = drush_get_context('DRUSH_BOOTSTRAP_ERRORS', []);
304 foreach ($errors as $code => $message) {
305 drush_set_error($code, $message);
308 return !drush_get_error();
312 * Determine whether a given bootstrap phase has been completed
314 * This function name has a typo which makes me laugh so we choose not to
315 * fix it. Take a deep breath, and smile. See
316 * http://en.wikipedia.org/wiki/HTTP_referer
320 * The bootstrap phase to test
323 * TRUE if the specified bootstrap phase has completed.
325 public function hasBootstrapped($phase)
327 $phase_index = drush_get_context('DRUSH_BOOTSTRAP_PHASE');
329 return isset($phase_index) && ($phase_index >= $phase);
333 * Validate whether a bootstrap phase can be reached.
335 * This function will validate the settings that will be used
336 * during the actual bootstrap process, and allow commands to
337 * progressively bootstrap to the highest level that can be reached.
339 * This function will only run the validation function once, and
340 * store the result from that execution in a local static. This avoids
341 * validating phases multiple times.
344 * The bootstrap phase to validate to.
347 * TRUE if bootstrap is possible, FALSE if the validation failed.
349 * @see \Drush\Boot\Boot::bootstrapPhases()
351 public function bootstrapValidate($phase)
353 $bootstrap = $this->bootstrap();
354 $phases = $this->bootstrapPhases(true);
355 static $result_cache = [];
357 if (!array_key_exists($phase, $result_cache)) {
358 drush_set_context('DRUSH_BOOTSTRAP_ERRORS', []);
359 drush_set_context('DRUSH_BOOTSTRAP_VALUES', []);
361 foreach ($phases as $phase_index => $current_phase) {
362 $validated_phase = drush_get_context('DRUSH_BOOTSTRAP_VALIDATION_PHASE', -1);
363 if ($phase_index > $phase) {
366 if ($phase_index > $validated_phase) {
367 $current_phase .= 'Validate';
368 if (method_exists($bootstrap, $current_phase)) {
369 $result_cache[$phase_index] = $bootstrap->{$current_phase}();
371 $result_cache[$phase_index] = true;
373 drush_set_context('DRUSH_BOOTSTRAP_VALIDATION_PHASE', $phase_index);
377 return $result_cache[$phase];
381 * Bootstrap to the specified phase.
383 * @param string $bootstrapPhase
384 * Name of phase to bootstrap to. Will be converted to appropriate index.
385 * @param \Consolidation\AnnotatedCommand\AnnotationData $annotationData
386 * Optional annotation data from the command.
389 * TRUE if the specified bootstrap phase has completed.
392 * Thrown when an unknown bootstrap phase is passed in the annotation
395 public function bootstrapToPhase($bootstrapPhase, AnnotationData $annotationData = null)
397 $this->logger->log(LogLevel::BOOTSTRAP, 'Bootstrap to {phase}', ['phase' => $bootstrapPhase]);
398 $phase = $this->bootstrap()->lookUpPhaseIndex($bootstrapPhase);
399 if (!isset($phase)) {
400 throw new \Exception(dt('Bootstrap phase !phase unknown.', ['!phase' => $bootstrapPhase]));
402 // Do not attempt to bootstrap to a phase that is unknown to the selected bootstrap object.
403 $phases = $this->bootstrapPhases();
404 if (!array_key_exists($phase, $phases) && ($phase >= 0)) {
407 return $this->bootstrapToPhaseIndex($phase, $annotationData);
410 protected function maxPhaseLimit($bootstrap_str)
412 $bootstrap_words = explode(' ', $bootstrap_str);
413 array_shift($bootstrap_words);
414 if (empty($bootstrap_words)) {
417 $stop_phase_name = array_shift($bootstrap_words);
418 return $this->bootstrap()->lookUpPhaseIndex($stop_phase_name);
422 * Bootstrap to the specified phase.
424 * @param int $max_phase_index
425 * Only attempt bootstrap to the specified level.
426 * @param \Consolidation\AnnotatedCommand\AnnotationData $annotationData
427 * Optional annotation data from the command.
430 * TRUE if the specified bootstrap phase has completed.
432 public function bootstrapToPhaseIndex($max_phase_index, AnnotationData $annotationData = null)
434 if ($max_phase_index == DRUSH_BOOTSTRAP_MAX) {
435 // Try get a max phase.
436 $bootstrap_str = $annotationData->get('bootstrap');
437 $stop_phase = $this->maxPhaseLimit($bootstrap_str);
438 $this->bootstrapMax($stop_phase);
442 $this->logger->log(LogLevel::BOOTSTRAP, 'Drush bootstrap phase {phase}', ['phase' => $max_phase_index]);
443 $phases = $this->bootstrapPhases();
446 // Try to bootstrap to the maximum possible level, without generating errors
447 foreach ($phases as $phase_index) {
448 if ($phase_index > $max_phase_index) {
449 // Stop trying, since we achieved what was specified.
453 $this->logger->log(LogLevel::BOOTSTRAP, 'Try to validate bootstrap phase {phase}', ['phase' => $max_phase_index]);
455 if ($this->bootstrapValidate($phase_index)) {
456 if ($phase_index > drush_get_context('DRUSH_BOOTSTRAP_PHASE', DRUSH_BOOTSTRAP_NONE)) {
457 $this->logger->log(LogLevel::BOOTSTRAP, 'Try to bootstrap at phase {phase}', ['phase' => $max_phase_index]);
458 $result = $this->doBootstrap($phase_index, $max_phase_index, $annotationData);
461 $this->logger->log(LogLevel::BOOTSTRAP, 'Could not bootstrap at phase {phase}', ['phase' => $max_phase_index]);
471 * Bootstrap to the highest level possible, without triggering any errors.
473 * @param int $max_phase_index
474 * (optional) Only attempt bootstrap to the specified level.
475 * @param \Consolidation\AnnotatedCommand\AnnotationData $annotationData
476 * Optional annotation data from the command.
479 * The maximum phase to which we bootstrapped.
481 public function bootstrapMax($max_phase_index = false, AnnotationData $annotationData = null)
483 // Bootstrap as far as we can without throwing an error, but log for
484 // debugging purposes.
486 $phases = $this->bootstrapPhases(true);
487 if (!$max_phase_index) {
488 $max_phase_index = count($phases);
491 if ($max_phase_index >= count($phases)) {
492 $this->logger->log(LogLevel::DEBUG, 'Trying to bootstrap as far as we can');
495 // Try to bootstrap to the maximum possible level, without generating errors.
496 foreach ($phases as $phase_index => $current_phase) {
497 if ($phase_index > $max_phase_index) {
498 // Stop trying, since we achieved what was specified.
502 if ($this->bootstrapValidate($phase_index)) {
503 if ($phase_index > drush_get_context('DRUSH_BOOTSTRAP_PHASE')) {
504 $this->doBootstrap($phase_index, $max_phase_index, $annotationData);
507 // $this->bootstrapValidate() only logs successful validations. For us,
508 // knowing what failed can also be important.
509 $previous = drush_get_context('DRUSH_BOOTSTRAP_PHASE');
510 $this->logger->log(LogLevel::DEBUG, 'Bootstrap phase {function}() failed to validate; continuing at {current}()', ['function' => $current_phase, 'current' => $phases[$previous]]);
515 return drush_get_context('DRUSH_BOOTSTRAP_PHASE');