2 namespace Robo\Collection;
4 use Consolidation\Config\Inject\ConfigForSetters;
5 use Robo\Config\Config;
7 use Robo\Contract\InflectionInterface;
8 use Robo\Contract\TaskInterface;
9 use Robo\Contract\CompletionInterface;
10 use Robo\Contract\WrappedTaskInterface;
11 use Robo\Task\Simulator;
13 use Robo\Task\BaseTask;
14 use Robo\Contract\BuilderAwareInterface;
15 use Robo\Contract\CommandInterface;
16 use Robo\Contract\VerbosityThresholdInterface;
17 use Robo\State\StateAwareInterface;
18 use Robo\State\StateAwareTrait;
22 * Creates a collection, and adds tasks to it. The collection builder
23 * offers a streamlined chained-initialization mechanism for easily
24 * creating task groups. Facilities for creating working and temporary
25 * directories are also provided.
29 * $result = $this->collectionBuilder()
30 * ->taskFilesystemStack()
34 * $this->taskDeleteDir('g')
36 * ->taskFilesystemStack()
38 * ->touch('g/h/h.txt')
39 * ->taskFilesystemStack()
41 * ->touch('g/h/i/i.txt')
45 * In the example above, the `taskDeleteDir` will be called if
48 class CollectionBuilder extends BaseTask implements NestedCollectionInterface, WrappedTaskInterface, CommandInterface, StateAwareInterface
55 protected $commandFile;
58 * @var CollectionInterface
60 protected $collection;
65 protected $currentTask;
73 * @param \Robo\Tasks $commandFile
75 public function __construct($commandFile)
77 $this->commandFile = $commandFile;
81 public static function create($container, $commandFile)
83 $builder = new self($commandFile);
85 $builder->setLogger($container->get('logger'));
86 $builder->setProgressIndicator($container->get('progressIndicator'));
87 $builder->setConfig($container->get('config'));
88 $builder->setOutputAdapter($container->get('outputAdapter'));
94 * @param bool $simulated
98 public function simulated($simulated = true)
100 $this->simulated = $simulated;
107 public function isSimulated()
109 if (!isset($this->simulated)) {
110 $this->simulated = $this->getConfig()->get(Config::SIMULATE);
112 return $this->simulated;
116 * Create a temporary directory to work in. When the collection
117 * completes or rolls back, the temporary directory will be deleted.
118 * Returns the path to the location where the directory will be
121 * @param string $prefix
122 * @param string $base
123 * @param bool $includeRandomPart
127 public function tmpDir($prefix = 'tmp', $base = '', $includeRandomPart = true)
129 // n.b. Any task that the builder is asked to create is
130 // automatically added to the builder's collection, and
131 // wrapped in the builder object. Therefore, the result
132 // of any call to `taskFoo()` from within the builder will
133 // always be `$this`.
134 return $this->taskTmpDir($prefix, $base, $includeRandomPart)->getPath();
138 * Create a working directory to hold results. A temporary directory
139 * is first created to hold the intermediate results. After the
140 * builder finishes, the work directory is moved into its final location;
141 * any results already in place will be moved out of the way and
144 * @param string $finalDestination The path where the working directory
145 * will be moved once the task collection completes.
149 public function workDir($finalDestination)
151 // Creating the work dir task in this context adds it to our task collection.
152 return $this->taskWorkDir($finalDestination)->getPath();
155 public function addTask(TaskInterface $task)
157 $this->getCollection()->add($task);
162 * Add arbitrary code to execute as a task.
164 * @see \Robo\Collection\CollectionInterface::addCode
166 * @param callable $code
167 * @param int|string $name
170 public function addCode(callable $code, $name = \Robo\Collection\CollectionInterface::UNNAMEDTASK)
172 $this->getCollection()->addCode($code, $name);
177 * Add a list of tasks to our task collection.
179 * @param TaskInterface[] $tasks
180 * An array of tasks to run with rollback protection
184 public function addTaskList(array $tasks)
186 $this->getCollection()->addTaskList($tasks);
190 public function rollback(TaskInterface $task)
192 // Ensure that we have a collection if we are going to add
193 // a rollback function.
194 $this->getCollection()->rollback($task);
198 public function rollbackCode(callable $rollbackCode)
200 $this->getCollection()->rollbackCode($rollbackCode);
204 public function completion(TaskInterface $task)
206 $this->getCollection()->completion($task);
210 public function completionCode(callable $completionCode)
212 $this->getCollection()->completionCode($completionCode);
217 * @param string $text
218 * @param array $context
219 * @param string $level
223 public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
225 $this->getCollection()->progressMessage($text, $context, $level);
230 * @param \Robo\Collection\NestedCollectionInterface $parentCollection
234 public function setParentCollection(NestedCollectionInterface $parentCollection)
236 $this->getCollection()->setParentCollection($parentCollection);
241 * Called by the factory method of each task; adds the current
242 * task to the task builder.
246 * @param TaskInterface $task
250 public function addTaskToCollection($task)
252 // Postpone creation of the collection until the second time
253 // we are called. At that time, $this->currentTask will already
254 // be populated. We call 'getCollection()' so that it will
255 // create the collection and add the current task to it.
256 // Note, however, that if our only tasks implements NestedCollectionInterface,
257 // then we should force this builder to use a collection.
258 if (!$this->collection && (isset($this->currentTask) || ($task instanceof NestedCollectionInterface))) {
259 $this->getCollection();
261 $this->currentTask = $task;
262 if ($this->collection) {
263 $this->collection->add($task);
268 public function getState()
270 $collection = $this->getCollection();
271 return $collection->getState();
274 public function storeState($key, $source = '')
276 return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
279 public function deferTaskConfiguration($functionName, $stateKey)
281 return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
284 public function defer($callback)
286 return $this->callCollectionStateFuntion(__FUNCTION__, func_get_args());
289 protected function callCollectionStateFuntion($functionName, $args)
291 $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
293 array_unshift($args, $currentTask);
294 $collection = $this->getCollection();
295 $fn = [$collection, $functionName];
297 call_user_func_array($fn, $args);
301 public function setVerbosityThreshold($verbosityThreshold)
303 $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
305 $currentTask->setVerbosityThreshold($verbosityThreshold);
308 parent::setVerbosityThreshold($verbosityThreshold);
314 * Return the current task for this collection builder.
317 * @return \Robo\Contract\TaskInterface
319 public function getCollectionBuilderCurrentTask()
321 return $this->currentTask;
325 * Create a new builder with its own task collection
327 * @return CollectionBuilder
329 public function newBuilder()
331 $collectionBuilder = new self($this->commandFile);
332 $collectionBuilder->inflect($this);
333 $collectionBuilder->simulated($this->isSimulated());
334 $collectionBuilder->setVerbosityThreshold($this->verbosityThreshold());
335 $collectionBuilder->setState($this->getState());
337 return $collectionBuilder;
341 * Calling the task builder with methods of the current
342 * task calls through to that method of the task.
344 * There is extra complexity in this function that could be
345 * simplified if we attached the 'LoadAllTasks' and custom tasks
346 * to the collection builder instead of the RoboFile. While that
347 * change would be a better design overall, it would require that
348 * the user do a lot more work to set up and use custom tasks.
349 * We therefore take on some additional complexity here in order
350 * to allow users to maintain their tasks in their RoboFile, which
351 * is much more convenient.
353 * Calls to $this->collectionBuilder()->taskFoo() cannot be made
354 * directly because all of the task methods are protected. These
355 * calls will therefore end up here. If the method name begins
356 * with 'task', then it is eligible to be used with the builder.
358 * When we call getBuiltTask, below, it will use the builder attached
359 * to the commandfile to build the task. However, this is not what we
360 * want: the task needs to be built from THIS collection builder, so that
361 * it will be affected by whatever state is active in this builder.
362 * To do this, we have two choices: 1) save and restore the builder
363 * in the commandfile, or 2) clone the commandfile and set this builder
364 * on the copy. 1) is vulnerable to failure in multithreaded environments
365 * (currently not supported), while 2) might cause confusion if there
366 * is shared state maintained in the commandfile, which is in the
367 * domain of the user.
369 * Note that even though we are setting up the commandFile to
370 * use this builder, getBuiltTask always creates a new builder
371 * (which is constructed using all of the settings from the
372 * commandFile's builder), and the new task is added to that.
373 * We therefore need to transfer the newly built task into this
374 * builder. The temporary builder is discarded.
379 * @return $this|mixed
381 public function __call($fn, $args)
383 if (preg_match('#^task[A-Z]#', $fn) && (method_exists($this->commandFile, 'getBuiltTask'))) {
384 $saveBuilder = $this->commandFile->getBuilder();
385 $this->commandFile->setBuilder($this);
386 $temporaryBuilder = $this->commandFile->getBuiltTask($fn, $args);
387 $this->commandFile->setBuilder($saveBuilder);
388 if (!$temporaryBuilder) {
389 throw new \BadMethodCallException("No such method $fn: task does not exist in " . get_class($this->commandFile));
391 $temporaryBuilder->getCollection()->transferTasks($this);
394 if (!isset($this->currentTask)) {
395 throw new \BadMethodCallException("No such method $fn: current task undefined in collection builder.");
397 // If the method called is a method of the current task,
398 // then call through to the current task's setter method.
399 $result = call_user_func_array([$this->currentTask, $fn], $args);
401 // If something other than a setter method is called, then return its result.
402 $currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
403 if (isset($result) && ($result !== $currentTask)) {
411 * Construct the desired task and add it to this builder.
413 * @param string|object $name
416 * @return \Robo\Collection\CollectionBuilder
418 public function build($name, $args)
420 $reflection = new ReflectionClass($name);
421 $task = $reflection->newInstanceArgs($args);
423 throw new RuntimeException("Can not construct task $name");
425 $task = $this->fixTask($task, $args);
426 $this->configureTask($name, $task);
427 return $this->addTaskToCollection($task);
431 * @param InflectionInterface $task
434 * @return \Robo\Collection\CompletionWrapper|\Robo\Task\Simulator
436 protected function fixTask($task, $args)
438 if ($task instanceof InflectionInterface) {
439 $task->inflect($this);
441 if ($task instanceof BuilderAwareInterface) {
442 $task->setBuilder($this);
444 if ($task instanceof VerbosityThresholdInterface) {
445 $task->setVerbosityThreshold($this->verbosityThreshold());
448 // Do not wrap our wrappers.
449 if ($task instanceof CompletionWrapper || $task instanceof Simulator) {
453 // Remember whether or not this is a task before
454 // it gets wrapped in any decorator.
455 $isTask = $task instanceof TaskInterface;
456 $isCollection = $task instanceof NestedCollectionInterface;
458 // If the task implements CompletionInterface, ensure
459 // that its 'complete' method is called when the application
460 // terminates -- but only if its 'run' method is called
461 // first. If the task is added to a collection, then the
462 // task will be unwrapped via its `original` method, and
463 // it will be re-wrapped with a new completion wrapper for
464 // its new collection.
465 if ($task instanceof CompletionInterface) {
466 $task = new CompletionWrapper(Temporary::getCollection(), $task);
469 // If we are in simulated mode, then wrap any task in
471 if ($isTask && !$isCollection && ($this->isSimulated())) {
472 $task = new \Robo\Task\Simulator($task, $args);
473 $task->inflect($this);
480 * Check to see if there are any setter methods defined in configuration
483 protected function configureTask($taskClass, $task)
485 $taskClass = static::configClassIdentifier($taskClass);
486 $configurationApplier = new ConfigForSetters($this->getConfig(), $taskClass, 'task.');
487 $configurationApplier->apply($task, 'settings');
489 // TODO: If we counted each instance of $taskClass that was called from
490 // this builder, then we could also apply configuration from
491 // "task.{$taskClass}[$N].settings"
493 // TODO: If the builder knew what the current command name was,
494 // then we could also search for task configuration under
495 // command-specific keys such as "command.{$commandname}.task.{$taskClass}.settings".
499 * When we run the collection builder, run everything in the collection.
501 * @return \Robo\Result
503 public function run()
506 $result = $this->runTasks();
508 $result['time'] = $this->getExecutionTime();
509 $result->mergeData($this->getState()->getData());
514 * If there is a single task, run it; if there is a collection, run
517 * @return \Robo\Result
519 protected function runTasks()
521 if (!$this->collection && $this->currentTask) {
522 $result = $this->currentTask->run();
523 return Result::ensureResult($this->currentTask, $result);
525 return $this->getCollection()->run();
531 public function getCommand()
533 if (!$this->collection && $this->currentTask) {
534 $task = $this->currentTask;
535 $task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
536 if ($task instanceof CommandInterface) {
537 return $task->getCommand();
541 return $this->getCollection()->getCommand();
545 * @return \Robo\Collection\Collection
547 public function original()
549 return $this->getCollection();
553 * Return the collection of tasks associated with this builder.
555 * @return CollectionInterface
557 public function getCollection()
559 if (!isset($this->collection)) {
560 $this->collection = new Collection();
561 $this->collection->inflect($this);
562 $this->collection->setState($this->getState());
563 $this->collection->setProgressBarAutoDisplayInterval($this->getConfig()->get(Config::PROGRESS_BAR_AUTO_DISPLAY_INTERVAL));
565 if (isset($this->currentTask)) {
566 $this->collection->add($this->currentTask);
569 return $this->collection;