Refactoring

This commit is contained in:
gamonoid
2017-09-03 20:39:22 +02:00
parent af40881847
commit a7274d3cfd
5075 changed files with 238202 additions and 16291 deletions

View File

@@ -0,0 +1,56 @@
<?php
namespace Robo;
use Symfony\Component\Console\Application as SymfonyApplication;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
class Application extends SymfonyApplication
{
/**
* @param string $name
* @param string $version
*/
public function __construct($name, $version)
{
parent::__construct($name, $version);
$this->getDefinition()
->addOption(
new InputOption('--simulate', null, InputOption::VALUE_NONE, 'Run in simulated mode (show what would have happened).')
);
$this->getDefinition()
->addOption(
new InputOption('--progress-delay', null, InputOption::VALUE_REQUIRED, 'Number of seconds before progress bar is displayed in long-running task collections. Default: 2s.', Config::DEFAULT_PROGRESS_DELAY)
);
}
/**
* @param string $roboFile
* @param string $roboClass
*/
public function addInitRoboFileCommand($roboFile, $roboClass)
{
$createRoboFile = new Command('init');
$createRoboFile->setDescription("Intitalizes basic RoboFile in current dir");
$createRoboFile->setCode(function () use ($roboClass, $roboFile) {
$output = Robo::output();
$output->writeln("<comment> ~~~ Welcome to Robo! ~~~~ </comment>");
$output->writeln("<comment> ". basename($roboFile) ." will be created in the current directory </comment>");
file_put_contents(
$roboFile,
'<?php'
. "\n/**"
. "\n * This is project's console commands configuration for Robo task runner."
. "\n *"
. "\n * @see http://robo.li/"
. "\n */"
. "\nclass " . $roboClass . " extends \\Robo\\Tasks\n{\n // define public methods as commands\n}"
);
$output->writeln("<comment> Edit this file to add your commands! </comment>");
});
$this->add($createRoboFile);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Robo\Collection;
use Robo\Result;
use Robo\Contract\TaskInterface;
use Robo\Collection\Collection;
/**
* Creates a task wrapper that converts any Callable into an
* object that can be used directly with a task collection.
*
* It is not necessary to use this class directly; Collection will
* automatically wrap Callables when they are added.
*/
class CallableTask implements TaskInterface
{
/**
* @var callable
*/
protected $fn;
/**
* @var \Robo\Contract\TaskInterface
*/
protected $reference;
public function __construct(callable $fn, TaskInterface $reference)
{
$this->fn = $fn;
$this->reference = $reference;
}
/**
* @return \Robo\Result
*/
public function run()
{
$result = call_user_func($this->fn);
// If the function returns no result, then count it
// as a success.
if (!isset($result)) {
$result = Result::success($this->reference);
}
// If the function returns a result, it must either return
// a \Robo\Result or an exit code. In the later case, we
// convert it to a \Robo\Result.
if (!$result instanceof Result) {
$result = new Result($this->reference, $result);
}
return $result;
}
}

View File

@@ -0,0 +1,683 @@
<?php
namespace Robo\Collection;
use Robo\Result;
use Psr\Log\LogLevel;
use Robo\Contract\TaskInterface;
use Robo\Task\StackBasedTask;
use Robo\Task\BaseTask;
use Robo\TaskInfo;
use Robo\Contract\WrappedTaskInterface;
use Robo\Exception\TaskException;
use Robo\Exception\TaskExitException;
use Robo\Contract\CommandInterface;
use Robo\Common\ProgressIndicatorAwareTrait;
use Robo\Contract\InflectionInterface;
/**
* Group tasks into a collection that run together. Supports
* rollback operations for handling error conditions.
*
* This is an internal class. Clients should use a CollectionBuilder
* rather than direct use of the Collection class. @see CollectionBuilder.
*
* Below, the example FilesystemStack task is added to a collection,
* and associated with a rollback task. If any of the operations in
* the FilesystemStack, or if any of the other tasks also added to
* the task collection should fail, then the rollback function is
* called. Here, taskDeleteDir is used to remove partial results
* of an unfinished task.
*/
class Collection extends BaseTask implements CollectionInterface, CommandInterface
{
/**
* @var \Robo\Collection\Element[]
*/
protected $taskList = [];
/**
* @var TaskInterface[]
*/
protected $rollbackStack = [];
/**
* @var TaskInterface[]
*/
protected $completionStack = [];
/**
* @var CollectionInterface
*/
protected $parentCollection;
/**
* Constructor.
*/
public function __construct()
{
}
public function setProgressBarAutoDisplayInterval($interval)
{
if (!$this->progressIndicator) {
return;
}
return $this->progressIndicator->setProgressBarAutoDisplayInterval($interval);
}
/**
* {@inheritdoc}
*/
public function add(TaskInterface $task, $name = self::UNNAMEDTASK)
{
$task = new CompletionWrapper($this, $task);
$this->addToTaskList($name, $task);
return $this;
}
/**
* {@inheritdoc}
*/
public function addCode(callable $code, $name = self::UNNAMEDTASK)
{
return $this->add(new CallableTask($code, $this), $name);
}
/**
* {@inheritdoc}
*/
public function addIterable($iterable, callable $code)
{
$callbackTask = (new IterationTask($iterable, $code, $this))->inflect($this);
return $this->add($callbackTask);
}
/**
* {@inheritdoc}
*/
public function rollback(TaskInterface $rollbackTask)
{
// Rollback tasks always try as hard as they can, and never report failures.
$rollbackTask = $this->ignoreErrorsTaskWrapper($rollbackTask);
return $this->wrapAndRegisterRollback($rollbackTask);
}
/**
* {@inheritdoc}
*/
public function rollbackCode(callable $rollbackCode)
{
// Rollback tasks always try as hard as they can, and never report failures.
$rollbackTask = $this->ignoreErrorsCodeWrapper($rollbackCode);
return $this->wrapAndRegisterRollback($rollbackTask);
}
/**
* {@inheritdoc}
*/
public function completion(TaskInterface $completionTask)
{
$collection = $this;
$completionRegistrationTask = new CallableTask(
function () use ($collection, $completionTask) {
$collection->registerCompletion($completionTask);
},
$this
);
$this->addToTaskList(self::UNNAMEDTASK, $completionRegistrationTask);
return $this;
}
/**
* {@inheritdoc}
*/
public function completionCode(callable $completionTask)
{
$completionTask = new CallableTask($completionTask, $this);
return $this->completion($completionTask);
}
/**
* {@inheritdoc}
*/
public function before($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK)
{
return $this->addBeforeOrAfter(__FUNCTION__, $name, $task, $nameOfTaskToAdd);
}
/**
* {@inheritdoc}
*/
public function after($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK)
{
return $this->addBeforeOrAfter(__FUNCTION__, $name, $task, $nameOfTaskToAdd);
}
/**
* {@inheritdoc}
*/
public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
{
$context += ['name' => 'Progress'];
$context += TaskInfo::getTaskContext($this);
return $this->addCode(
function () use ($level, $text, $context) {
$this->printTaskOutput($level, $text, $context);
}
);
}
/**
* @param \Robo\Contract\TaskInterface $rollbackTask
*
* @return $this
*/
protected function wrapAndRegisterRollback(TaskInterface $rollbackTask)
{
$collection = $this;
$rollbackRegistrationTask = new CallableTask(
function () use ($collection, $rollbackTask) {
$collection->registerRollback($rollbackTask);
},
$this
);
$this->addToTaskList(self::UNNAMEDTASK, $rollbackRegistrationTask);
return $this;
}
/**
* Add either a 'before' or 'after' function or task.
*
* @param string $method
* @param string $name
* @param callable|TaskInterface $task
* @param string $nameOfTaskToAdd
*
* @return $this
*/
protected function addBeforeOrAfter($method, $name, $task, $nameOfTaskToAdd)
{
if (is_callable($task)) {
$task = new CallableTask($task, $this);
}
$existingTask = $this->namedTask($name);
$fn = [$existingTask, $method];
call_user_func($fn, $task, $nameOfTaskToAdd);
return $this;
}
/**
* Wrap the provided task in a wrapper that will ignore
* any errors or exceptions that may be produced. This
* is useful, for example, in adding optional cleanup tasks
* at the beginning of a task collection, to remove previous
* results which may or may not exist.
*
* TODO: Provide some way to specify which sort of errors
* are ignored, so that 'file not found' may be ignored,
* but 'permission denied' reported?
*
* @param \Robo\Contract\TaskInterface $task
*
* @return \Robo\Collection\CallableTask
*/
public function ignoreErrorsTaskWrapper(TaskInterface $task)
{
// If the task is a stack-based task, then tell it
// to try to run all of its operations, even if some
// of them fail.
if ($task instanceof StackBasedTask) {
$task->stopOnFail(false);
}
$ignoreErrorsInTask = function () use ($task) {
$data = [];
try {
$result = $this->runSubtask($task);
$message = $result->getMessage();
$data = $result->getData();
$data['exitcode'] = $result->getExitCode();
} catch (\Exception $e) {
$message = $e->getMessage();
}
return Result::success($task, $message, $data);
};
// Wrap our ignore errors callable in a task.
return new CallableTask($ignoreErrorsInTask, $this);
}
/**
* @param callable $task
*
* @return \Robo\Collection\CallableTask
*/
public function ignoreErrorsCodeWrapper(callable $task)
{
return $this->ignoreErrorsTaskWrapper(new CallableTask($task, $this));
}
/**
* Return the list of task names added to this collection.
*
* @return array
*/
public function taskNames()
{
return array_keys($this->taskList);
}
/**
* Test to see if a specified task name exists.
* n.b. before() and after() require that the named
* task exist; use this function to test first, if
* unsure.
*
* @param string $name
*
* @return bool
*/
public function hasTask($name)
{
return array_key_exists($name, $this->taskList);
}
/**
* Find an existing named task.
*
* @param string $name
* The name of the task to insert before. The named task MUST exist.
*
* @return Element
* The task group for the named task. Generally this is only
* used to call 'before()' and 'after()'.
*/
protected function namedTask($name)
{
if (!$this->hasTask($name)) {
throw new \RuntimeException("Could not find task named $name");
}
return $this->taskList[$name];
}
/**
* Add a list of tasks to our task collection.
*
* @param TaskInterface[] $tasks
* An array of tasks to run with rollback protection
*
* @return $this
*/
public function addTaskList(array $tasks)
{
foreach ($tasks as $name => $task) {
$this->add($task, $name);
}
return $this;
}
/**
* Add the provided task to our task list.
*
* @param string $name
* @param \Robo\Contract\TaskInterface $task
*
* @return \Robo\Collection\Collection
*/
protected function addToTaskList($name, TaskInterface $task)
{
// All tasks are stored in a task group so that we have a place
// to hang 'before' and 'after' tasks.
$taskGroup = new Element($task);
return $this->addCollectionElementToTaskList($name, $taskGroup);
}
/**
* @param int|string $name
* @param \Robo\Collection\Element $taskGroup
*
* @return $this
*/
protected function addCollectionElementToTaskList($name, Element $taskGroup)
{
// If a task name is not provided, then we'll let php pick
// the array index.
if (Result::isUnnamed($name)) {
$this->taskList[] = $taskGroup;
return $this;
}
// If we are replacing an existing task with the
// same name, ensure that our new task is added to
// the end.
$this->taskList[$name] = $taskGroup;
return $this;
}
/**
* Set the parent collection. This is necessary so that nested
* collections' rollback and completion tasks can be added to the
* top-level collection, ensuring that the rollbacks for a collection
* will run if any later task fails.
*
* @param \Robo\Collection\NestedCollectionInterface $parentCollection
*
* @return $this
*/
public function setParentCollection(NestedCollectionInterface $parentCollection)
{
$this->parentCollection = $parentCollection;
return $this;
}
/**
* Get the appropriate parent collection to use
*
* @return CollectionInterface
*/
public function getParentCollection()
{
return $this->parentCollection ? $this->parentCollection : $this;
}
/**
* Register a rollback task to run if there is any failure.
*
* Clients are free to add tasks to the rollback stack as
* desired; however, usually it is preferable to call
* Collection::rollback() instead. With that function,
* the rollback function will only be called if all of the
* tasks added before it complete successfully, AND some later
* task fails.
*
* One example of a good use-case for registering a callback
* function directly is to add a task that sends notification
* when a task fails.
*
* @param TaskInterface $rollbackTask
* The rollback task to run on failure.
*/
public function registerRollback(TaskInterface $rollbackTask)
{
if ($this->parentCollection) {
return $this->parentCollection->registerRollback($rollbackTask);
}
if ($rollbackTask) {
$this->rollbackStack[] = $rollbackTask;
}
}
/**
* Register a completion task to run once all other tasks finish.
* Completion tasks run whether or not a rollback operation was
* triggered. They do not trigger rollbacks if they fail.
*
* The typical use-case for a completion function is to clean up
* temporary objects (e.g. temporary folders). The preferred
* way to do that, though, is to use Temporary::wrap().
*
* On failures, completion tasks will run after all rollback tasks.
* If one task collection is nested inside another task collection,
* then the nested collection's completion tasks will run as soon as
* the nested task completes; they are not deferred to the end of
* the containing collection's execution.
*
* @param TaskInterface $completionTask
* The completion task to run at the end of all other operations.
*/
public function registerCompletion(TaskInterface $completionTask)
{
if ($this->parentCollection) {
return $this->parentCollection->registerCompletion($completionTask);
}
if ($completionTask) {
// Completion tasks always try as hard as they can, and never report failures.
$completionTask = $this->ignoreErrorsTaskWrapper($completionTask);
$this->completionStack[] = $completionTask;
}
}
/**
* Return the count of steps in this collection
*
* @return int
*/
public function progressIndicatorSteps()
{
$steps = 0;
foreach ($this->taskList as $name => $taskGroup) {
$steps += $taskGroup->progressIndicatorSteps();
}
return $steps;
}
/**
* A Collection of tasks can provide a command via `getCommand()`
* if it contains a single task, and that task implements CommandInterface.
*
* @return string
*
* @throws \Robo\Exception\TaskException
*/
public function getCommand()
{
if (empty($this->taskList)) {
return '';
}
if (count($this->taskList) > 1) {
// TODO: We could potentially iterate over the items in the collection
// and concatenate the result of getCommand() from each one, and fail
// only if we encounter a command that is not a CommandInterface.
throw new TaskException($this, "getCommand() does not work on arbitrary collections of tasks.");
}
$taskElement = reset($this->taskList);
$task = $taskElement->getTask();
$task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
if ($task instanceof CommandInterface) {
return $task->getCommand();
}
throw new TaskException($task, get_class($task) . " does not implement CommandInterface, so can't be used to provide a command");
}
/**
* Run our tasks, and roll back if necessary.
*
* @return \Robo\Result
*/
public function run()
{
$result = $this->runWithoutCompletion();
$this->complete();
return $result;
}
/**
* @return \Robo\Result
*/
private function runWithoutCompletion()
{
$result = Result::success($this);
if (empty($this->taskList)) {
return $result;
}
$this->startProgressIndicator();
if ($result->wasSuccessful()) {
foreach ($this->taskList as $name => $taskGroup) {
$taskList = $taskGroup->getTaskList();
$result = $this->runTaskList($name, $taskList, $result);
if (!$result->wasSuccessful()) {
$this->fail();
return $result;
}
}
$this->taskList = [];
}
$this->stopProgressIndicator();
$result['time'] = $this->getExecutionTime();
return $result;
}
/**
* Run every task in a list, but only up to the first failure.
* Return the failing result, or success if all tasks run.
*
* @param string $name
* @param TaskInterface[] $taskList
* @param \Robo\Result $result
*
* @return \Robo\Result
*
* @throws \Robo\Exception\TaskExitException
*/
private function runTaskList($name, array $taskList, Result $result)
{
try {
foreach ($taskList as $taskName => $task) {
$taskResult = $this->runSubtask($task);
$this->advanceProgressIndicator();
// If the current task returns an error code, then stop
// execution and signal a rollback.
if (!$taskResult->wasSuccessful()) {
return $taskResult;
}
// We accumulate our results into a field so that tasks that
// have a reference to the collection may examine and modify
// the incremental results, if they wish.
$key = Result::isUnnamed($taskName) ? $name : $taskName;
$result->accumulate($key, $taskResult);
}
} catch (TaskExitException $exitException) {
$this->fail();
throw $exitException;
} catch (\Exception $e) {
// Tasks typically should not throw, but if one does, we will
// convert it into an error and roll back.
return Result::fromException($task, $e, $result->getData());
}
return $result;
}
/**
* Force the rollback functions to run
*
* @return $this
*/
public function fail()
{
$this->disableProgressIndicator();
$this->runRollbackTasks();
$this->complete();
return $this;
}
/**
* Force the completion functions to run
*
* @return $this
*/
public function complete()
{
$this->detatchProgressIndicator();
$this->runTaskListIgnoringFailures($this->completionStack);
$this->reset();
return $this;
}
/**
* Reset this collection, removing all tasks.
*
* @return $this
*/
public function reset()
{
$this->taskList = [];
$this->completionStack = [];
$this->rollbackStack = [];
return $this;
}
/**
* Run all of our rollback tasks.
*
* Note that Collection does not implement RollbackInterface, but
* it may still be used as a task inside another task collection
* (i.e. you can nest task collections, if desired).
*/
protected function runRollbackTasks()
{
$this->runTaskListIgnoringFailures($this->rollbackStack);
// Erase our rollback stack once we have finished rolling
// everything back. This will allow us to potentially use
// a command collection more than once (e.g. to retry a
// failed operation after doing some error recovery).
$this->rollbackStack = [];
}
/**
* @param TaskInterface|NestedCollectionInterface|WrappedTaskInterface $task
*
* @return \Robo\Result
*/
protected function runSubtask($task)
{
$original = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
$this->setParentCollectionForTask($original, $this->getParentCollection());
if ($original instanceof InflectionInterface) {
$original->inflect($this);
}
$taskResult = $task->run();
return $taskResult;
}
/**
* @param TaskInterface|NestedCollectionInterface|WrappedTaskInterface $task
* @param $parentCollection
*/
protected function setParentCollectionForTask($task, $parentCollection)
{
if ($task instanceof NestedCollectionInterface) {
$task->setParentCollection($parentCollection);
}
}
/**
* Run all of the tasks in a provided list, ignoring failures.
* This is used to roll back or complete.
*
* @param TaskInterface[] $taskList
*/
protected function runTaskListIgnoringFailures(array $taskList)
{
foreach ($taskList as $task) {
try {
$this->runSubtask($task);
} catch (\Exception $e) {
// Ignore rollback failures.
}
}
}
/**
* Give all of our tasks to the provided collection builder.
*
* @param CollectionBuilder $builder
*/
public function transferTasks($builder)
{
foreach ($this->taskList as $name => $taskGroup) {
// TODO: We are abandoning all of our before and after tasks here.
// At the moment, transferTasks is only called under conditions where
// there will be none of these, but care should be taken if that changes.
$task = $taskGroup->getTask();
$builder->addTaskToCollection($task);
}
$this->reset();
}
}

View File

@@ -0,0 +1,475 @@
<?php
namespace Robo\Collection;
use Guzzle\Inflection\InflectorInterface;
use Robo\Config;
use Robo\Common\Timer;
use Psr\Log\LogLevel;
use Robo\Contract\InflectionInterface;
use Robo\Contract\TaskInterface;
use Robo\Contract\CompletionInterface;
use Robo\Contract\WrappedTaskInterface;
use Robo\Collection\NestedCollectionInterface;
use Robo\LoadAllTasks;
use Robo\Task\Simulator;
use Robo\Collection\CompletionWrapper;
use Robo\Collection\Temporary;
use Robo\Contract\ConfigAwareInterface;
use Robo\Common\ConfigAwareTrait;
use ReflectionClass;
use Robo\Task\BaseTask;
use Robo\Contract\BuilderAwareInterface;
use Robo\Contract\CommandInterface;
use Robo\Exception\TaskException;
/**
* Creates a collection, and adds tasks to it. The collection builder
* offers a streamlined chained-initialization mechanism for easily
* creating task groups. Facilities for creating working and temporary
* directories are also provided.
*
* ``` php
* <?php
* $result = $this->collectionBuilder()
* ->taskFilesystemStack()
* ->mkdir('g')
* ->touch('g/g.txt')
* ->rollback(
* $this->taskDeleteDir('g')
* )
* ->taskFilesystemStack()
* ->mkdir('g/h')
* ->touch('g/h/h.txt')
* ->taskFilesystemStack()
* ->mkdir('g/h/i/c')
* ->touch('g/h/i/i.txt')
* ->run()
* ?>
*
* In the example above, the `taskDeleteDir` will be called if
* ```
*/
class CollectionBuilder extends BaseTask implements NestedCollectionInterface, WrappedTaskInterface, CommandInterface
{
/**
* @var \Robo\Tasks
*/
protected $commandFile;
/**
* @var CollectionInterface
*/
protected $collection;
/**
* @var TaskInterface
*/
protected $currentTask;
/**
* @var bool
*/
protected $simulated;
/**
* @param \Robo\Tasks $commandFile
*/
public function __construct($commandFile)
{
$this->commandFile = $commandFile;
}
/**
* @param bool $simulated
*
* @return $this
*/
public function simulated($simulated = true)
{
$this->simulated = $simulated;
return $this;
}
/**
* @return bool
*/
public function isSimulated()
{
if (!isset($this->simulated)) {
$this->simulated = $this->getConfig()->get(Config::SIMULATE);
}
return $this->simulated;
}
/**
* Create a temporary directory to work in. When the collection
* completes or rolls back, the temporary directory will be deleted.
* Returns the path to the location where the directory will be
* created.
*
* @param string $prefix
* @param string $base
* @param bool $includeRandomPart
*
* @return string
*/
public function tmpDir($prefix = 'tmp', $base = '', $includeRandomPart = true)
{
// n.b. Any task that the builder is asked to create is
// automatically added to the builder's collection, and
// wrapped in the builder object. Therefore, the result
// of any call to `taskFoo()` from within the builder will
// always be `$this`.
return $this->taskTmpDir($prefix, $base, $includeRandomPart)->getPath();
}
/**
* Create a working directory to hold results. A temporary directory
* is first created to hold the intermediate results. After the
* builder finishes, the work directory is moved into its final location;
* any results already in place will be moved out of the way and
* then deleted.
*
* @param string $finalDestination The path where the working directory
* will be moved once the task collection completes.
*
* @return string
*/
public function workDir($finalDestination)
{
// Creating the work dir task in this context adds it to our task collection.
return $this->taskWorkDir($finalDestination)->getPath();
}
public function addTask(TaskInterface $task)
{
$this->getCollection()->add($task);
return $this;
}
public function addCode(callable $code)
{
$this->getCollection()->addCode($code);
return $this;
}
/**
* Add a list of tasks to our task collection.
*
* @param TaskInterface[] $tasks
* An array of tasks to run with rollback protection
*
* @return $this
*/
public function addTaskList(array $tasks)
{
$this->getCollection()->addTaskList($tasks);
return $this;
}
public function rollback(TaskInterface $task)
{
// Ensure that we have a collection if we are going to add
// a rollback function.
$this->getCollection()->rollback($task);
return $this;
}
public function rollbackCode(callable $rollbackCode)
{
$this->getCollection()->rollbackCode($rollbackCode);
return $this;
}
public function completion(TaskInterface $task)
{
$this->getCollection()->completion($task);
return $this;
}
public function completionCode(callable $completionCode)
{
$this->getCollection()->completionCode($completionCode);
return $this;
}
/**
* @param string $text
* @param array $context
* @param string $level
*
* @return $this
*/
public function progressMessage($text, $context = [], $level = LogLevel::NOTICE)
{
$this->getCollection()->progressMessage($text, $context, $level);
return $this;
}
/**
* @param \Robo\Collection\NestedCollectionInterface $parentCollection
*
* @return $this
*/
public function setParentCollection(NestedCollectionInterface $parentCollection)
{
$this->getCollection()->setParentCollection($parentCollection);
return $this;
}
/**
* Called by the factory method of each task; adds the current
* task to the task builder.
*
* TODO: protected
*
* @param TaskInterface $task
*
* @return $this
*/
public function addTaskToCollection($task)
{
// Postpone creation of the collection until the second time
// we are called. At that time, $this->currentTask will already
// be populated. We call 'getCollection()' so that it will
// create the collection and add the current task to it.
// Note, however, that if our only tasks implements NestedCollectionInterface,
// then we should force this builder to use a collection.
if (!$this->collection && (isset($this->currentTask) || ($task instanceof NestedCollectionInterface))) {
$this->getCollection();
}
$this->currentTask = $task;
if ($this->collection) {
$this->collection->add($task);
}
return $this;
}
/**
* Return the current task for this collection builder.
* TODO: Not needed?
*
* @return \Robo\Contract\TaskInterface
*/
public function getCollectionBuilderCurrentTask()
{
return $this->currentTask;
}
/**
* Create a new builder with its own task collection
*
* @return CollectionBuilder
*/
public function newBuilder()
{
$collectionBuilder = new self($this->commandFile);
$collectionBuilder->inflect($this);
$collectionBuilder->simulated($this->isSimulated());
return $collectionBuilder;
}
/**
* Calling the task builder with methods of the current
* task calls through to that method of the task.
*
* There is extra complexity in this function that could be
* simplified if we attached the 'LoadAllTasks' and custom tasks
* to the collection builder instead of the RoboFile. While that
* change would be a better design overall, it would require that
* the user do a lot more work to set up and use custom tasks.
* We therefore take on some additional complexity here in order
* to allow users to maintain their tasks in their RoboFile, which
* is much more convenient.
*
* Calls to $this->collectionBuilder()->taskFoo() cannot be made
* directly because all of the task methods are protected. These
* calls will therefore end up here. If the method name begins
* with 'task', then it is eligible to be used with the builder.
*
* When we call getBuiltTask, below, it will use the builder attached
* to the commandfile to build the task. However, this is not what we
* want: the task needs to be built from THIS collection builder, so that
* it will be affected by whatever state is active in this builder.
* To do this, we have two choices: 1) save and restore the builder
* in the commandfile, or 2) clone the commandfile and set this builder
* on the copy. 1) is vulnerable to failure in multithreaded environments
* (currently not supported), while 2) might cause confusion if there
* is shared state maintained in the commandfile, which is in the
* domain of the user.
*
* Note that even though we are setting up the commandFile to
* use this builder, getBuiltTask always creates a new builder
* (which is constructed using all of the settings from the
* commandFile's builder), and the new task is added to that.
* We therefore need to transfer the newly built task into this
* builder. The temporary builder is discarded.
*
* @param string $fn
* @param array $args
*
* @return $this|mixed
*/
public function __call($fn, $args)
{
if (preg_match('#^task[A-Z]#', $fn) && (method_exists($this->commandFile, 'getBuiltTask'))) {
$saveBuilder = $this->commandFile->getBuilder();
$this->commandFile->setBuilder($this);
$temporaryBuilder = $this->commandFile->getBuiltTask($fn, $args);
$this->commandFile->setBuilder($saveBuilder);
if (!$temporaryBuilder) {
throw new \BadMethodCallException("No such method $fn: task does not exist in " . get_class($this->commandFile));
}
$temporaryBuilder->getCollection()->transferTasks($this);
return $this;
}
if (!isset($this->currentTask)) {
throw new \BadMethodCallException("No such method $fn: current task undefined in collection builder.");
}
// If the method called is a method of the current task,
// then call through to the current task's setter method.
$result = call_user_func_array([$this->currentTask, $fn], $args);
// If something other than a setter method is called, then return its result.
$currentTask = ($this->currentTask instanceof WrappedTaskInterface) ? $this->currentTask->original() : $this->currentTask;
if (isset($result) && ($result !== $currentTask)) {
return $result;
}
return $this;
}
/**
* Construct the desired task and add it to this builder.
*
* @param string|object $name
* @param array $args
*
* @return \Robo\Collection\CollectionBuilder
*/
public function build($name, $args)
{
$reflection = new ReflectionClass($name);
$task = $reflection->newInstanceArgs($args);
if (!$task) {
throw new RuntimeException("Can not construct task $name");
}
$task = $this->fixTask($task, $args);
return $this->addTaskToCollection($task);
}
/**
* @param InflectionInterface $task
* @param array $args
*
* @return \Robo\Collection\CompletionWrapper|\Robo\Task\Simulator
*/
protected function fixTask($task, $args)
{
$task->inflect($this);
if ($task instanceof BuilderAwareInterface) {
$task->setBuilder($this);
}
// Do not wrap our wrappers.
if ($task instanceof CompletionWrapper || $task instanceof Simulator) {
return $task;
}
// Remember whether or not this is a task before
// it gets wrapped in any decorator.
$isTask = $task instanceof TaskInterface;
$isCollection = $task instanceof NestedCollectionInterface;
// If the task implements CompletionInterface, ensure
// that its 'complete' method is called when the application
// terminates -- but only if its 'run' method is called
// first. If the task is added to a collection, then the
// task will be unwrapped via its `original` method, and
// it will be re-wrapped with a new completion wrapper for
// its new collection.
if ($task instanceof CompletionInterface) {
$task = new CompletionWrapper(Temporary::getCollection(), $task);
}
// If we are in simulated mode, then wrap any task in
// a TaskSimulator.
if ($isTask && !$isCollection && ($this->isSimulated())) {
$task = new \Robo\Task\Simulator($task, $args);
$task->inflect($this);
}
return $task;
}
/**
* When we run the collection builder, run everything in the collection.
*
* @return \Robo\Result
*/
public function run()
{
$this->startTimer();
$result = $this->runTasks();
$this->stopTimer();
$result['time'] = $this->getExecutionTime();
return $result;
}
/**
* If there is a single task, run it; if there is a collection, run
* all of its tasks.
*
* @return \Robo\Result
*/
protected function runTasks()
{
if (!$this->collection && $this->currentTask) {
return $this->currentTask->run();
}
return $this->getCollection()->run();
}
/**
* @return string
*/
public function getCommand()
{
if (!$this->collection && $this->currentTask) {
$task = $this->currentTask;
$task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
if ($task instanceof CommandInterface) {
return $task->getCommand();
}
}
return $this->getCollection()->getCommand();
}
/**
* @return \Robo\Collection\Collection
*/
public function original()
{
return $this->getCollection();
}
/**
* Return the collection of tasks associated with this builder.
*
* @return CollectionInterface
*/
public function getCollection()
{
if (!isset($this->collection)) {
$this->collection = new Collection();
$this->collection->inflect($this);
$this->collection->setProgressBarAutoDisplayInterval($this->getConfig()->get(Config::PROGRESS_BAR_AUTO_DISPLAY_INTERVAL));
if (isset($this->currentTask)) {
$this->collection->add($this->currentTask);
}
}
return $this->collection;
}
}

View File

@@ -0,0 +1,150 @@
<?php
namespace Robo\Collection;
use Psr\Log\LogLevel;
use Robo\Contract\TaskInterface;
interface CollectionInterface extends NestedCollectionInterface
{
/**
* Unnamed tasks are assigned an arbitrary numeric index
* in the task list. Any numeric value may be used, but the
* UNNAMEDTASK constant is recommended for clarity.
*
* @var int
*/
const UNNAMEDTASK = 0;
/**
* Add a task or a list of tasks to our task collection. Each task
* will run via its 'run()' method once (and if) all of the tasks
* added before it complete successfully. If the task also implements
* RollbackInterface, then it will be rolled back via its 'rollback()'
* method ONLY if its 'run()' method completes successfully, and some
* task added after it fails.
*
* @param TaskInterface $task
* The task to add to our collection.
* @param int|string $name
* An optional name for the task -- missing or UNNAMEDTASK for unnamed tasks.
* Names are used for positioning before and after tasks.
*
* @return CollectionInterface
*/
public function add(TaskInterface $task, $name = self::UNNAMEDTASK);
/**
* Add arbitrary code to execute as a task.
*
* @param callable $code Code to execute as a task
* @param int|string $name
* An optional name for the task -- missing or UNNAMEDTASK for unnamed tasks.
* Names are used for positioning before and after tasks.
*
* @return $this
*/
public function addCode(callable $code, $name = self::UNNAMEDTASK);
/**
* Add arbitrary code that will be called once for every item in the
* provided array or iterable object. If the function result of the
* provided callback is a TaskInterface or Collection, then it will be
* executed.
*
* @param CollectionInterface|array $iterable A collection of things to iterate
* @param $code $code A callback function to call for each item in the collection.
*
* @return $this
*/
public function addIterable($iterable, callable $code);
/**
* Add a rollback task to our task collection. A rollback task
* will execute ONLY if all of the tasks added before it complete
* successfully, AND some task added after it fails.
*
* @param TaskInterface $rollbackTask
* The rollback task to add. Note that the 'run()' method of the
* task executes, not its 'rollback()' method. To use the 'rollback()'
* method, add the task via 'Collection::add()' instead.
*
* @return $this
*/
public function rollback(TaskInterface $rollbackTask);
/**
* Add arbitrary code to execute as a rollback.
*
* @param callable $rollbackTask Code to execute during rollback processing
*
* @return $this
*/
public function rollbackCode(callable $rollbackTask);
/**
* Add a completion task to our task collection. A completion task
* will execute EITHER after all tasks succeed, OR immediatley after
* any task fails. Completion tasks never cause errors to be returned
* from Collection::run(), even if they fail.
*
* @param TaskInterface $completionTask
* The completion task to add. Note that the 'run()' method of the
* task executes, just as if the task was added normally.
*
* @return $this
*/
public function completion(TaskInterface $completionTask);
/**
* Add arbitrary code to execute as a completion.
*
* @param callable $completionTask Code to execute after collection completes
*
* @return $this
*/
public function completionCode(callable $completionTask);
/**
* Add a task before an existing named task.
*
* @param string $name
* The name of the task to insert before. The named task MUST exist.
* @param callable|TaskInterface $task
* The task to add.
* @param int|string $nameOfTaskToAdd
* The name of the task to add. If not provided, will be associated
* with the named task it was added before.
*
* @return $this
*/
public function before($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK);
/**
* Add a task after an existing named task.
*
* @param string $name
* The name of the task to insert before. The named task MUST exist.
* @param callable|TaskInterface $task
* The task to add.
* @param int|string $nameOfTaskToAdd
* The name of the task to add. If not provided, will be associated
* with the named task it was added after.
*
* @return $this
*/
public function after($name, $task, $nameOfTaskToAdd = self::UNNAMEDTASK);
/**
* Print a progress message after Collection::run() has executed
* all of the tasks that were added prior to the point when this
* method was called. If one of the previous tasks fail, then this
* message will not be printed.
*
* @param string $text Message to print.
* @param array $context Extra context data for use by the logger.
* @param \Psr\Log\LogLevel|string $level The log level to print the information at. Default is NOTICE.
*
* @return $this
*/
public function progressMessage($text, $context = [], $level = LogLevel::NOTICE);
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Robo\Collection;
use Consolidation\AnnotatedCommand\Hooks\ProcessResultInterface;
use Consolidation\AnnotatedCommand\CommandData;
use Robo\Contract\TaskInterface;
use Robo\Result;
/**
* The collection process hook is added to the annotation command
* hook manager in Runner::configureContainer(). This hook will be
* called every time a command runs. If the command result is a
* \Robo\Contract\TaskInterface (in particular, \Robo\Collection\Collection),
* then we run the collection, and return the result. We ignore results
* of any other type.
*/
class CollectionProcessHook implements ProcessResultInterface
{
/**
* @param \Robo\Result|\Robo\Contract\TaskInterface $result
* @param \Consolidation\AnnotatedCommand\CommandData $commandData
*
* @return null|\Robo\Result
*/
public function process($result, CommandData $commandData)
{
if ($result instanceof TaskInterface) {
try {
return $result->run();
} catch (\Exception $e) {
return Result::fromException($result, $e);
}
}
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Robo\Collection;
use Robo\Task\BaseTask;
use Robo\Contract\TaskInterface;
use Robo\Contract\RollbackInterface;
use Robo\Contract\CompletionInterface;
use Robo\Contract\WrappedTaskInterface;
/**
* Creates a task wrapper that will manage rollback and collection
* management to a task when it runs. Tasks are automatically
* wrapped in a CompletionWrapper when added to a task collection.
*
* Clients may need to wrap their task in a CompletionWrapper if it
* creates temporary objects.
*
* @see \Robo\Task\Filesystem\loadTasks::taskTmpDir
*/
class CompletionWrapper extends BaseTask implements WrappedTaskInterface
{
/**
* @var \Robo\Collection\Collection
*/
private $collection;
/**
* @var \Robo\Contract\TaskInterface
*/
private $task;
/**
* @var NULL|\Robo\Contract\TaskInterface
*/
private $rollbackTask;
/**
* Create a CompletionWrapper.
*
* Temporary tasks are always wrapped in a CompletionWrapper, as are
* any tasks that are added to a collection. If a temporary task
* is added to a collection, then it is first unwrapped from its
* CompletionWrapper (via its original() method), and then added to a
* new CompletionWrapper for the collection it is added to.
*
* In this way, when the CompletionWrapper is finally executed, the
* task's rollback and completion handlers will be registered on
* whichever collection it was registered on.
*
* @todo Why not CollectionInterface the type of the $collection argument?
*
* @param \Robo\Collection\Collection $collection
* @param \Robo\Contract\TaskInterface $task
* @param \Robo\Contract\TaskInterface|NULL $rollbackTask
*/
public function __construct(Collection $collection, TaskInterface $task, TaskInterface $rollbackTask = null)
{
$this->collection = $collection;
$this->task = ($task instanceof WrappedTaskInterface) ? $task->original() : $task;
$this->rollbackTask = $rollbackTask;
}
/**
* {@inheritdoc}
*/
public function original()
{
return $this->task;
}
/**
* Before running this task, register its rollback and completion
* handlers on its collection. The reason this class exists is to
* defer registration of rollback and completion tasks until 'run()' time.
*
* @return \Robo\Result
*/
public function run()
{
if ($this->rollbackTask) {
$this->collection->registerRollback($this->rollbackTask);
}
if ($this->task instanceof RollbackInterface) {
$this->collection->registerRollback(new CallableTask([$this->task, 'rollback'], $this->task));
}
if ($this->task instanceof CompletionInterface) {
$this->collection->registerCompletion(new CallableTask([$this->task, 'complete'], $this->task));
}
return $this->task->run();
}
/**
* Make this wrapper object act like the class it wraps.
*
* @param string $function
* @param array $args
*
* @return mixed
*/
public function __call($function, $args)
{
return call_user_func_array(array($this->task, $function), $args);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Robo\Collection;
use Robo\Contract\TaskInterface;
use Robo\Contract\WrappedTaskInterface;
use Robo\Contract\ProgressIndicatorAwareInterface;
/**
* One element in a collection. Each element consists of a task
* all of its before tasks, and all of its after tasks.
*
* This class is internal to Collection; it should not be used directly.
*/
class Element
{
/**
* @var \Robo\Contract\TaskInterface
*/
protected $task;
/**
* @var array
*/
protected $before = [];
/**
* @var array
*/
protected $after = [];
public function __construct(TaskInterface $task)
{
$this->task = $task;
}
/**
* @param mixed $before
* @param string $name
*/
public function before($before, $name)
{
if ($name) {
$this->before[$name] = $before;
} else {
$this->before[] = $before;
}
}
/**
* @param mixed $after
* @param string $name
*/
public function after($after, $name)
{
if ($name) {
$this->after[$name] = $after;
} else {
$this->after[] = $after;
}
}
/**
* @return array
*/
public function getBefore()
{
return $this->before;
}
/**
* @return array
*/
public function getAfter()
{
return $this->after;
}
/**
* @return \Robo\Contract\TaskInterface
*/
public function getTask()
{
return $this->task;
}
/**
* @return array
*/
public function getTaskList()
{
return array_merge($this->getBefore(), [$this->getTask()], $this->getAfter());
}
/**
* @return int
*/
public function progressIndicatorSteps()
{
$steps = 0;
foreach ($this->getTaskList() as $task) {
if ($task instanceof WrappedTaskInterface) {
$task = $task->original();
}
// If the task is a ProgressIndicatorAwareInterface, then it
// will advance the progress indicator a number of times.
if ($task instanceof ProgressIndicatorAwareInterface) {
$steps += $task->progressIndicatorSteps();
}
// We also advance the progress indicator once regardless
// of whether it is progress-indicator aware or not.
$steps++;
}
return $steps;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Robo\Collection;
use Psr\Log\LogLevel;
use Robo\Contract\TaskInterface;
interface NestedCollectionInterface
{
/**
* @param \Robo\Collection\NestedCollectionInterface $parentCollection
*
* @return $this
*/
public function setParentCollection(NestedCollectionInterface $parentCollection);
}

View File

@@ -0,0 +1,198 @@
<?php
namespace Robo\Collection;
use Robo\Collection\NestedCollectionInterface;
use Robo\Result;
use Robo\TaskInfo;
use Robo\Task\BaseTask;
use Robo\Contract\TaskInterface;
use Robo\Contract\BuilderAwareInterface;
use Robo\Common\BuilderAwareTrait;
/**
* Creates a task wrapper that converts any Callable into an
* object that will execute the callback once for each item in the
* provided collection.
*
* It is not necessary to use this class directly; Collection::addIterable
* will automatically create one when it is called.
*/
class TaskForEach extends BaseTask implements NestedCollectionInterface, BuilderAwareInterface
{
use BuilderAwareTrait;
/**
* @var callable[]
*/
protected $functionStack = [];
/**
* @var callable[]
*/
protected $countingStack = [];
/**
* @var string
*/
protected $message;
/**
* @var array
*/
protected $context = [];
protected $iterable;
/**
* @var \Robo\Collection\NestedCollectionInterface
*/
protected $parentCollection;
public function __construct($iterable)
{
$this->iterable = $iterable;
}
/**
* @param string $message
* @param array $context
*
* @return $this
*/
public function iterationMessage($message, $context = [])
{
$this->message = $message;
$this->context = $context + ['name' => 'Progress'];
return $this;
}
/**
* @param int|string $key
* @param mixed $value
*/
protected function showIterationMessage($key, $value)
{
if ($this->message) {
$context = ['key' => $key, 'value' => $value];
$context += $this->context;
$context += TaskInfo::getTaskContext($this);
$this->printTaskInfo($this->message, $context);
}
}
/**
* @param callable $fn
*
* @return $this
*/
public function withEachKeyValueCall(callable $fn)
{
$this->functionStack[] = $fn;
return $this;
}
/**
* @param callable $fn
*
* @return \Robo\Collection\TaskForEach
*/
public function call(callable $fn)
{
return $this->withEachKeyValueCall(
function ($key, $value) use ($fn) {
return call_user_func($fn, $value);
}
);
}
/**
* @param callable $fn
*
* @return \Robo\Collection\TaskForEach
*/
public function withBuilder(callable $fn)
{
$this->countingStack[] =
function ($key, $value) use ($fn) {
// Create a new builder for every iteration
$builder = $this->collectionBuilder();
// The user function should build task operations using
// the $key / $value parameters; we will call run() on
// the builder thus constructed.
call_user_func($fn, $builder, $key, $value);
return $builder->getCollection()->progressIndicatorSteps();
};
return $this->withEachKeyValueCall(
function ($key, $value) use ($fn) {
// Create a new builder for every iteration
$builder = $this->collectionBuilder()
->setParentCollection($this->parentCollection);
// The user function should build task operations using
// the $key / $value parameters; we will call run() on
// the builder thus constructed.
call_user_func($fn, $builder, $key, $value);
return $builder->run();
}
);
}
/**
* {@inheritdoc}
*/
public function setParentCollection(NestedCollectionInterface $parentCollection)
{
$this->parentCollection = $parentCollection;
return $this;
}
/**
* {@inheritdoc}
*/
public function progressIndicatorSteps()
{
$multiplier = count($this->functionStack);
if (!empty($this->countingStack)) {
$value = reset($this->iterable);
$key = key($this->iterable);
foreach ($this->countingStack as $fn) {
$multiplier += call_user_func($fn, $key, $value);
}
}
return count($this->iterable) * $multiplier;
}
/**
* {@inheritdoc}
*/
public function run()
{
$finalResult = Result::success($this);
$this->startProgressIndicator();
foreach ($this->iterable as $key => $value) {
$this->showIterationMessage($key, $value);
try {
foreach ($this->functionStack as $fn) {
$result = call_user_func($fn, $key, $value);
$this->advanceProgressIndicator();
if (!isset($result)) {
$result = Result::success($this);
}
// If the function returns a result, it must either return
// a \Robo\Result or an exit code. In the later case, we
// convert it to a \Robo\Result.
if (!$result instanceof Result) {
$result = new Result($this, $result);
}
if (!$result->wasSuccessful()) {
return $result;
}
$finalResult = $result->merge($finalResult);
}
} catch (\Exception $e) {
return Result::fromException($result, $e);
}
}
$this->stopProgressIndicator();
return $finalResult;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Robo\Collection;
use Robo\Contract\TaskInterface;
/**
* The temporary collection keeps track of the global collection of
* temporary cleanup tasks in instances where temporary-generating
* tasks are executed directly via their run() method, rather than
* as part of a collection.
*
* In general, temporary-generating tasks should always be run in
* a collection, as the cleanup functions registered with the
* Temporary collection will not run until requested.
*
* Since the results could be undefined if cleanup functions were called
* at arbitrary times during a program's execution, cleanup should only
* be done immeidately prior to program termination, when there is no
* danger of cleaning up after some unrelated task.
*
* An application need never use Temporary directly, save to
* call Temporary::wrap() inside loadTasks or loadShortcuts, and
* to call Temporary::complete() immediately prior to terminating.
* This is recommended, but not required; this function will be
* registered as a shutdown function, and called on termination.
*/
class Temporary
{
private static $collection;
/**
* Provides direct access to the collection of temporaries, if necessary.
*/
public static function getCollection()
{
if (!static::$collection) {
static::$collection = \Robo\Robo::getContainer()->get('collection');
register_shutdown_function(function () {
static::complete();
});
}
return static::$collection;
}
/**
* Call the complete method of all of the registered objects.
*/
public static function complete()
{
// Run the collection of tasks. This will also run the
// completion tasks.
$collection = static::getCollection();
$collection->run();
// Make sure that our completion functions do not run twice.
$collection->reset();
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Robo\Collection;
trait loadTasks
{
/**
* Run a callback function on each item in a collection
*
* @param array $collection
*
* @return \Robo\Collection\TaskForEach
*/
protected function taskForEach($collection)
{
return $this->task(TaskForEach::class, $collection);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Robo\Common;
use Robo\Robo;
use Robo\Collection\CollectionBuilder;
trait BuilderAwareTrait
{
/**
* @var \Robo\Collection\CollectionBuilder
*/
protected $builder;
/**
* @see \Robo\Contract\BuilderAwareInterface::setBuilder()
*
* @param \Robo\Collection\CollectionBuilder $builder
*
* @return $this
*/
public function setBuilder(CollectionBuilder $builder)
{
$this->builder = $builder;
return $this;
}
/**
* @see \Robo\Contract\BuilderAwareInterface::getBuilder()
*
* @return \Robo\Collection\CollectionBuilder
*/
public function getBuilder()
{
return $this->builder;
}
/**
* @return \Robo\Collection\CollectionBuilder
*/
protected function collectionBuilder()
{
return $this->getBuilder()->newBuilder();
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Robo\Common;
use Symfony\Component\Process\ProcessUtils;
/**
* Use this to add arguments and options to the $arguments property.
*/
trait CommandArguments
{
/**
* @var string
*/
protected $arguments = '';
/**
* Pass argument to executable. Its value will be automatically escaped.
*
* @param string $arg
*
* @return $this
*/
public function arg($arg)
{
return $this->args($arg);
}
/**
* Pass methods parameters as arguments to executable. Argument values
* are automatically escaped.
*
* @param string|string[] $args
*
* @return $this
*/
public function args($args)
{
if (!is_array($args)) {
$args = func_get_args();
}
$this->arguments .= ' ' . implode(' ', array_map('static::escape', $args));
return $this;
}
/**
* Pass the provided string in its raw (as provided) form as an argument to executable.
*
* @param string $arg
*/
public function rawArg($arg)
{
$this->arguments .= " $arg";
}
/**
* Escape the provided value, unless it contains only alphanumeric
* plus a few other basic characters.
*
* @param string $value
*
* @return string
*/
public static function escape($value)
{
if (preg_match('/^[a-zA-Z0-9\/\.@~_-]+$/', $value)) {
return $value;
}
return ProcessUtils::escapeArgument($value);
}
/**
* Pass option to executable. Options are prefixed with `--` , value can be provided in second parameter.
* Option values are automatically escaped.
*
* @param string $option
* @param string $value
*
* @return $this
*/
public function option($option, $value = null)
{
if ($option !== null and strpos($option, '-') !== 0) {
$option = "--$option";
}
$this->arguments .= null == $option ? '' : " " . $option;
$this->arguments .= null == $value ? '' : " " . static::escape($value);
return $this;
}
/**
* Pass multiple options to executable. Value can be a string or array.
* Option values are automatically escaped.
*
* @param string $option
* @param string|array $value
*
* @return $this
*/
public function optionList($option, $value = array())
{
if (is_array($value)) {
foreach ($value as $item) {
$this->optionList($option, $item);
}
} else {
$this->option($option, $value);
}
return $this;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Robo\Common;
use Robo\Contract\CommandInterface;
use Robo\Exception\TaskException;
/**
* This task can receive commands from task implementing CommandInterface.
*/
trait CommandReceiver
{
/**
* @param string|\Robo\Contract\CommandInterface $command
*
* @return string
*
* @throws \Robo\Exception\TaskException
*/
protected function receiveCommand($command)
{
if (!is_object($command)) {
return $command;
}
if ($command instanceof CommandInterface) {
return $command->getCommand();
} else {
throw new TaskException($this, get_class($command) . " does not implement CommandInterface, so can't be passed into this task");
}
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Robo\Common;
use Robo\Robo;
use Robo\Config;
trait ConfigAwareTrait
{
/**
* @var \Robo\Config
*/
protected $config;
/**
* Set the config management object.
*
* @param \Robo\Config $config
*
* @return $this
*
* @see \Robo\Contract\ConfigAwareInterface::setConfig()
*/
public function setConfig(Config $config)
{
$this->config = $config;
return $this;
}
/**
* Get the config management object.
*
* @return \Robo\Config
*
* @see \Robo\Contract\ConfigAwareInterface::getConfig()
*/
public function getConfig()
{
return $this->config;
}
/**
* @param string $key
*
* @return string
*/
private static function getClassKey($key)
{
return sprintf('%s.%s', get_called_class(), $key);
}
/**
* @param string $key
* @param mixed $value
*
* @deprecated
*/
public static function configure($key, $value)
{
Robo::config()->set(static::getClassKey($key), $value);
}
/**
* @param string $key
* @param mixed|null $default
*
* @return mixed|null
*/
protected function getConfigValue($key, $default = null)
{
if (!$this->getConfig()) {
return $default;
}
return $this->getConfig()->get(static::getClassKey($key), $default);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Robo\Common;
/**
* Simplifies generating of configuration chanined methods.
* You can only define configuration properties and use magic methods to set them.
* Methods will be named the same way as properties.
* * Boolean properties are switched on/off if no values is provided.
* * Array properties can accept non-array values, in this case value will be appended to array.
* You should also define phpdoc for methods.
*/
trait DynamicParams
{
/**
* @param string $property
* @param array $args
*
* @return $this
*/
public function __call($property, $args)
{
if (!property_exists($this, $property)) {
throw new \RuntimeException("Property $property in task ".get_class($this).' does not exists');
}
// toggle boolean values
if (!isset($args[0]) and (is_bool($this->$property))) {
$this->$property = !$this->$property;
return $this;
}
// append item to array
if (is_array($this->$property)) {
if (is_array($args[0])) {
$this->$property = $args[0];
} else {
array_push($this->$property, $args[0]);
}
return $this;
}
$this->$property = $args[0];
return $this;
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace Robo\Common;
use Robo\Result;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;
/**
* This task is supposed to be executed as shell command.
* You can specify working directory and if output is printed.
*/
trait ExecCommand
{
/**
* @var bool
*/
protected $isPrinted = true;
/**
* @var string
*/
protected $workingDirectory;
/**
* @var \Robo\Common\TimeKeeper
*/
protected $execTimer;
/**
* @return \Robo\Common\TimeKeeper
*/
protected function getExecTimer()
{
if (!isset($this->execTimer)) {
$this->execTimer = new TimeKeeper();
}
return $this->execTimer;
}
/**
* Is command printing its output to screen
*
* @return bool
*/
public function getPrinted()
{
return $this->isPrinted;
}
/**
* Changes working directory of command
*
* @param string $dir
*
* @return $this
*/
public function dir($dir)
{
$this->workingDirectory = $dir;
return $this;
}
/**
* Should command output be printed
*
* @param bool $arg
*
* @return $this
*/
public function printed($arg)
{
if (is_bool($arg)) {
$this->isPrinted = $arg;
}
return $this;
}
/**
* Look for a "{$cmd}.phar" in the current working
* directory; return a string to exec it if it is
* found. Otherwise, look for an executable command
* of the same name via findExecutable.
*
* @param string $cmd
*
* @return bool|string
*/
protected function findExecutablePhar($cmd)
{
if (file_exists("{$cmd}.phar")) {
return "php {$cmd}.phar";
}
return $this->findExecutable($cmd);
}
/**
* Return the best path to the executable program
* with the provided name. Favor vendor/bin in the
* current project. If not found there, use
* whatever is on the $PATH.
*
* @param string $cmd
*
* @return bool|string
*/
protected function findExecutable($cmd)
{
$pathToCmd = $this->searchForExecutable($cmd);
if ($pathToCmd) {
return $this->useCallOnWindows($pathToCmd);
}
return false;
}
/**
* @param string $cmd
*
* @return string
*/
private function searchForExecutable($cmd)
{
$projectBin = $this->findProjectBin();
$localComposerInstallation = $projectBin . DIRECTORY_SEPARATOR . $cmd;
if (file_exists($localComposerInstallation)) {
return $localComposerInstallation;
}
$finder = new ExecutableFinder();
return $finder->find($cmd, null, []);
}
/**
* @return bool|string
*/
protected function findProjectBin()
{
$candidates = [ __DIR__ . '/../../vendor/bin', __DIR__ . '/../../bin' ];
// If this project is inside a vendor directory, give highest priority
// to that directory.
$vendorDirContainingUs = realpath(__DIR__ . '/../../../..');
if (is_dir($vendorDirContainingUs) && (basename($vendorDirContainingUs) == 'vendor')) {
array_unshift($candidates, $vendorDirContainingUs . '/bin');
}
foreach ($candidates as $dir) {
if (is_dir("$dir")) {
return realpath($dir);
}
}
return false;
}
/**
* Wrap Windows executables in 'call' per 7a88757d
*
* @param string $cmd
*
* @return string
*/
protected function useCallOnWindows($cmd)
{
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
if (file_exists("{$cmd}.bat")) {
$cmd = "{$cmd}.bat";
}
return "call $cmd";
}
return $cmd;
}
/**
* @param string $command
*
* @return \Robo\Result
*/
protected function executeCommand($command)
{
$process = new Process($command);
$process->setTimeout(null);
if ($this->workingDirectory) {
$process->setWorkingDirectory($this->workingDirectory);
}
$this->getExecTimer()->start();
if ($this->isPrinted) {
$process->run(function ($type, $buffer) {
print $buffer;
});
} else {
$process->run();
}
$this->getExecTimer()->stop();
return new Result($this, $process->getExitCode(), $process->getOutput(), ['time' => $this->getExecTimer()->elapsed()]);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Robo\Common;
/**
* This task specifies exactly one shell command.
* It can take additional arguments and options as config parameters.
*/
trait ExecOneCommand
{
use ExecCommand;
use CommandArguments;
}

View File

@@ -0,0 +1,176 @@
<?php
namespace Robo\Common;
use Robo\Robo;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
trait IO
{
use InputAwareTrait;
use OutputAwareTrait;
/**
* @var \Symfony\Component\Console\Style\SymfonyStyle
*/
protected $io;
/**
* Provide access to SymfonyStyle object.
*
* @return SymfonyStyle
*
* @see http://symfony.com/blog/new-in-symfony-2-8-console-style-guide
*/
protected function io()
{
if (!$this->io) {
$this->io = new SymfonyStyle($this->input(), $this->output());
}
return $this->io;
}
/**
* @param string $nonDecorated
* @param string $decorated
*
* @return string
*/
protected function decorationCharacter($nonDecorated, $decorated)
{
if (!$this->output()->isDecorated() || (strncasecmp(PHP_OS, 'WIN', 3) == 0)) {
return $nonDecorated;
}
return $decorated;
}
/**
* @param string $text
*/
protected function say($text)
{
$char = $this->decorationCharacter('>', '➜');
$this->writeln("$char $text");
}
/**
* @param string $text
* @param int $length
* @param string $color
*/
protected function yell($text, $length = 40, $color = 'green')
{
$char = $this->decorationCharacter(' ', '➜');
$format = "$char <fg=white;bg=$color;options=bold>%s</fg=white;bg=$color;options=bold>";
$this->formattedOutput($text, $length, $format);
}
/**
* @param string $text
* @param int $length
* @param string $format
*/
private function formattedOutput($text, $length, $format)
{
$lines = explode("\n", trim($text, "\n"));
$maxLineLength = array_reduce(array_map('strlen', $lines), 'max');
$length = max($length, $maxLineLength);
$len = $length + 2;
$space = str_repeat(' ', $len);
$this->writeln(sprintf($format, $space));
foreach ($lines as $line) {
$line = str_pad($line, $length, ' ', STR_PAD_BOTH);
$this->writeln(sprintf($format, " $line "));
}
$this->writeln(sprintf($format, $space));
}
/**
* @param string $question
* @param bool $hideAnswer
*
* @return string
*/
protected function ask($question, $hideAnswer = false)
{
if ($hideAnswer) {
return $this->askHidden($question);
}
return $this->doAsk(new Question($this->formatQuestion($question)));
}
/**
* @param string $question
*
* @return string
*/
protected function askHidden($question)
{
$question = new Question($this->formatQuestion($question));
$question->setHidden(true);
return $this->doAsk($question);
}
/**
* @param string $question
* @param string $default
*
* @return string
*/
protected function askDefault($question, $default)
{
return $this->doAsk(new Question($this->formatQuestion("$question [$default]"), $default));
}
/**
* @param string $question
*
* @return string
*/
protected function confirm($question)
{
return $this->doAsk(new ConfirmationQuestion($this->formatQuestion($question . ' (y/n)'), false));
}
/**
* @param \Symfony\Component\Console\Question\Question $question
*
* @return string
*/
private function doAsk(Question $question)
{
return $this->getDialog()->ask($this->input(), $this->output(), $question);
}
/**
* @param string $message
*
* @return string
*/
private function formatQuestion($message)
{
return "<question>? $message</question> ";
}
/**
* @return \Symfony\Component\Console\Helper\QuestionHelper
*/
protected function getDialog()
{
return new QuestionHelper();
}
/**
* @param $text
*/
private function writeln($text)
{
$this->output()->writeln($text);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Robo\Common;
use Robo\Contract\InflectionInterface;
trait InflectionTrait
{
/**
* Ask the provided parent class to inject all of the dependencies
* that it has and we need.
*
* @param \Robo\Contract\InflectionInterface $parent
*
* @return $this
*/
public function inflect(InflectionInterface $parent)
{
$parent->injectDependencies($this);
return $this;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Robo\Common;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
trait InputAwareTrait
{
/**
* @var \Symfony\Component\Console\Input\InputInterface
*/
protected $input;
/**
* @param \Symfony\Component\Console\Input\InputInterface $input
*
* @return $this
*
* @see \Symfony\Component\Console\Input\InputAwareInterface::setInput()
*/
public function setInput(InputInterface $input)
{
$this->input = $input;
return $this;
}
/**
* @return \Symfony\Component\Console\Input\InputInterface
*/
protected function input()
{
if (!isset($this->input)) {
$this->setInput(new ArgvInput());
}
return $this->input;
}
/**
* Backwards compatibility.
*
* @return \Symfony\Component\Console\Input\InputInterface
*
* @deprecated
*/
protected function getInput()
{
return $this->input();
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Robo\Common;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
trait OutputAwareTrait
{
/**
* @var \Symfony\Component\Console\Output\OutputInterface
*/
protected $output;
/**
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return $this
*
* @see \Robo\Contract\OutputAwareInterface::setOutput()
*/
public function setOutput(OutputInterface $output)
{
$this->output = $output;
return $this;
}
/**
* @return \Symfony\Component\Console\Output\OutputInterface
*/
protected function output()
{
if (!isset($this->output)) {
$this->setOutput(new NullOutput());
}
return $this->output;
}
/**
* Backwards compatibility
*
* @return \Symfony\Component\Console\Output\OutputInterface
*
* @deprecated
*/
protected function getOutput()
{
return $this->output();
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace Robo\Common;
/**
* Wrapper around \Symfony\Component\Console\Helper\ProgressBar
*/
class ProgressIndicator
{
use Timer;
/**
* @var \Symfony\Component\Console\Helper\ProgressBar
*/
protected $progressBar;
/**
* @var \Symfony\Component\Console\Output\OutputInterface
*/
protected $output;
/**
* @var bool
*/
protected $progressIndicatorRunning = false;
/**
* @var int
*/
protected $autoDisplayInterval = 0;
/**
* @var int
*/
protected $cachedSteps = 0;
/**
* @var int
*/
protected $totalSteps = 0;
/**
* @var bool
*/
protected $progressBarDisplayed = false;
/**
* @var \Robo\Contract\TaskInterface
*/
protected $owner;
/**
* @param \Symfony\Component\Console\Helper\ProgressBar $progressBar
* @param \Symfony\Component\Console\Output\OutputInterface $output
*/
public function __construct($progressBar, \Symfony\Component\Console\Output\OutputInterface $output)
{
$this->progressBar = $progressBar;
$this->output = $output;
}
/**
* @param int $interval
*/
public function setProgressBarAutoDisplayInterval($interval)
{
if ($this->progressIndicatorRunning) {
return;
}
$this->autoDisplayInterval = $interval;
}
/**
* @return bool
*/
public function hideProgressIndicator()
{
$result = $this->progressBarDisplayed;
if ($this->progressIndicatorRunning && $this->progressBarDisplayed) {
$this->progressBar->clear();
// Hack: progress indicator does not reset cursor to beginning of line on 'clear'
$this->output->write("\x0D");
$this->progressBarDisplayed = false;
}
return $result;
}
public function showProgressIndicator()
{
if ($this->progressIndicatorRunning && !$this->progressBarDisplayed && isset($this->progressBar)) {
$this->progressBar->display();
$this->progressBarDisplayed = true;
$this->advanceProgressIndicatorCachedSteps();
}
}
/**
* @param bool $visible
*/
public function restoreProgressIndicator($visible)
{
if ($visible) {
$this->showProgressIndicator();
}
}
/**
* @param int $totalSteps
* @param \Robo\Contract\TaskInterface $owner
*/
public function startProgressIndicator($totalSteps, $owner)
{
if (!isset($this->progressBar)) {
return;
}
$this->progressIndicatorRunning = true;
if (!isset($this->owner)) {
$this->owner = $owner;
$this->startTimer();
$this->totalSteps = $totalSteps;
$this->autoShowProgressIndicator();
}
}
public function autoShowProgressIndicator()
{
if (($this->autoDisplayInterval < 0) || !isset($this->progressBar) || !$this->output->isDecorated()) {
return;
}
if ($this->autoDisplayInterval <= $this->getExecutionTime()) {
$this->autoDisplayInterval = -1;
$this->progressBar->start($this->totalSteps);
$this->showProgressIndicator();
}
}
/**
* @return bool
*/
public function inProgress()
{
return $this->progressIndicatorRunning;
}
/**
* @param \Robo\Contract\TaskInterface $owner
*/
public function stopProgressIndicator($owner)
{
if ($this->progressIndicatorRunning && ($this->owner === $owner)) {
$this->cleanup();
}
}
protected function cleanup()
{
$this->progressIndicatorRunning = false;
$this->owner = null;
if ($this->progressBarDisplayed) {
$this->progressBar->finish();
// Hack: progress indicator does not always finish cleanly
$this->output->writeln('');
$this->progressBarDisplayed = false;
}
$this->stopTimer();
}
/**
* Erase progress indicator and ensure it never returns. Used
* only during error handlers.
*/
public function disableProgressIndicator()
{
$this->cleanup();
// ProgressIndicator is shared, so this permanently removes
// the program's ability to display progress bars.
$this->progressBar = null;
}
/**
* @param int $steps
*/
public function advanceProgressIndicator($steps = 1)
{
$this->cachedSteps += $steps;
if ($this->progressIndicatorRunning) {
$this->autoShowProgressIndicator();
// We only want to call `advance` if the progress bar is visible,
// because it always displays itself when it is advanced.
if ($this->progressBarDisplayed) {
return $this->advanceProgressIndicatorCachedSteps();
}
}
}
protected function advanceProgressIndicatorCachedSteps()
{
$this->progressBar->advance($this->cachedSteps);
$this->cachedSteps = 0;
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Robo\Common;
trait ProgressIndicatorAwareTrait
{
use Timer;
/**
* @var null|\Robo\Common\ProgressIndicator
*/
protected $progressIndicator;
/**
* @return int
*/
public function progressIndicatorSteps()
{
return 0;
}
/**
* @param null|\Robo\Common\ProgressIndicator $progressIndicator
*/
public function setProgressIndicator($progressIndicator)
{
$this->progressIndicator = $progressIndicator;
}
/**
* @return null|bool
*/
protected function hideProgressIndicator()
{
if (!$this->progressIndicator) {
return;
}
return $this->progressIndicator->hideProgressIndicator();
}
protected function showProgressIndicator()
{
if (!$this->progressIndicator) {
return;
}
$this->progressIndicator->showProgressIndicator();
}
/**
* @param bool $visible
*/
protected function restoreProgressIndicator($visible)
{
if (!$this->progressIndicator) {
return;
}
$this->progressIndicator->restoreProgressIndicator($visible);
}
/**
* @return int
*/
protected function getTotalExecutionTime()
{
if (!$this->progressIndicator) {
return 0;
}
return $this->progressIndicator->getExecutionTime();
}
protected function startProgressIndicator()
{
$this->startTimer();
if (!$this->progressIndicator) {
return;
}
$totalSteps = $this->progressIndicatorSteps();
$this->progressIndicator->startProgressIndicator($totalSteps, $this);
}
/**
* @return bool
*/
protected function inProgress()
{
if (!$this->progressIndicator) {
return false;
}
return $this->progressIndicator->inProgress();
}
protected function stopProgressIndicator()
{
$this->stopTimer();
if (!$this->progressIndicator) {
return;
}
$this->progressIndicator->stopProgressIndicator($this);
}
protected function disableProgressIndicator()
{
$this->stopTimer();
if (!$this->progressIndicator) {
return;
}
$this->progressIndicator->disableProgressIndicator();
}
protected function detatchProgressIndicator()
{
$this->setProgressIndicator(null);
}
/**
* @param int $steps
*/
protected function advanceProgressIndicator($steps = 1)
{
if (!$this->progressIndicator) {
return;
}
$this->progressIndicator->advanceProgressIndicator($steps);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Robo\Common;
trait ResourceExistenceChecker
{
/**
* Checks if the given input is a file or folder.
*
* @param string|string[] $resources
* @param string $type "file", "dir", "fileAndDir"
*
* @return bool True if no errors were encountered otherwise false.
*/
protected function checkResources($resources, $type = 'fileAndDir')
{
if (!in_array($type, ['file', 'dir', 'fileAndDir'])) {
throw new \InvalidArgumentException(sprintf('Invalid resource check of type "%s" used!', $type));
}
if (is_string($resources)) {
$resources = [$resources];
}
$success = true;
foreach ($resources as $resource) {
$glob = glob($resource);
if ($glob === false) {
$this->printTaskError(sprintf('Invalid glob "%s"!', $resource), $this);
$success = false;
continue;
}
foreach ($glob as $resource) {
if (!$this->checkResource($resource, $type)) {
$success = false;
}
}
}
return $success;
}
/**
* Checks a single resource, file or directory.
*
* It will print an error as well on the console.
*
* @param string $resource File or folder.
* @param string $type "file", "dir", "fileAndDir"
*
* @return bool
*/
protected function checkResource($resource, $type)
{
switch ($type) {
case 'file':
if (!$this->isFile($resource)) {
$this->printTaskError(sprintf('File "%s" does not exist!', $resource), $this);
return false;
}
return true;
case 'dir':
if (!$this->isDir($resource)) {
$this->printTaskError(sprintf('Directory "%s" does not exist!', $resource), $this);
return false;
}
return true;
case 'fileAndDir':
if (!$this->isDir($resource) && !$this->isFile($resource)) {
$this->printTaskError(sprintf('File or directory "%s" does not exist!', $resource), $this);
return false;
}
return true;
}
}
/**
* Convenience method to check the often uses "source => target" file / folder arrays.
*
* @param string|array $resources
*/
protected function checkSourceAndTargetResource($resources)
{
if (is_string($resources)) {
$resources = [$resources];
}
$sources = [];
$targets = [];
foreach ($resources as $source => $target) {
$sources[] = $source;
$target[] = $target;
}
$this->checkResources($sources);
$this->checkResources($targets);
}
/**
* Wrapper method around phps is_dir()
*
* @param string $directory
*
* @return bool
*/
protected function isDir($directory)
{
return is_dir($directory);
}
/**
* Wrapper method around phps file_exists()
*
* @param string $file
*
* @return bool
*/
protected function isFile($file)
{
return file_exists($file);
}
}

View File

@@ -0,0 +1,235 @@
<?php
namespace Robo\Common;
use Robo\Robo;
use Robo\TaskInfo;
use Consolidation\Log\ConsoleLogLevel;
use Robo\Common\ConfigAwareTrait;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Robo\Contract\ProgressIndicatorAwareInterface;
/**
* Task input/output methods. TaskIO is 'used' in BaseTask, so any
* task that extends this class has access to all of the methods here.
* printTaskInfo, printTaskSuccess, and printTaskError are the three
* primary output methods that tasks are encouraged to use. Tasks should
* avoid using the IO trait output methods.
*/
trait TaskIO
{
use LoggerAwareTrait;
use ConfigAwareTrait;
/**
* @return mixed|null|\Psr\Log\LoggerInterface
*/
public function logger()
{
// $this->logger should always be set in Robo core tasks.
if ($this->logger) {
return $this->logger;
}
// TODO: Remove call to Robo::logger() once maintaining backwards
// compatibility with legacy external Robo tasks is no longer desired.
if (!Robo::logger()) {
return null;
}
static $gaveDeprecationWarning = false;
if (!$gaveDeprecationWarning) {
trigger_error('No logger set for ' . get_class($this) . '. Use $this->task(Foo::class) rather than new Foo() in loadTasks to ensure the builder can initialize task the task, or use $this->collectionBuilder()->taskFoo() if creating one task from within another.', E_USER_DEPRECATED);
$gaveDeprecationWarning = true;
}
return Robo::logger();
}
/**
* Print information about a task in progress.
*
* With the Symfony Console logger, NOTICE is displayed at VERBOSITY_VERBOSE
* and INFO is displayed at VERBOSITY_VERY_VERBOSE.
*
* Robo overrides the default such that NOTICE is displayed at
* VERBOSITY_NORMAL and INFO is displayed at VERBOSITY_VERBOSE.
*
* n.b. We should probably have printTaskNotice for our ordinary
* output, and use printTaskInfo for less interesting messages.
*
* @param string $text
* @param null|array $context
*/
protected function printTaskInfo($text, $context = null)
{
// The 'note' style is used for both 'notice' and 'info' log levels;
// However, 'notice' is printed at VERBOSITY_NORMAL, whereas 'info'
// is only printed at VERBOSITY_VERBOSE.
$this->printTaskOutput(LogLevel::NOTICE, $text, $this->getTaskContext($context));
}
/**
* Provide notification that some part of the task succeeded.
*
* With the Symfony Console logger, success messages are remapped to NOTICE,
* and displayed in VERBOSITY_VERBOSE. When used with the Robo logger,
* success messages are displayed at VERBOSITY_NORMAL.
*
* @param string $text
* @param null|array $context
*/
protected function printTaskSuccess($text, $context = null)
{
// Not all loggers will recognize ConsoleLogLevel::SUCCESS.
// We therefore log as LogLevel::NOTICE, and apply a '_level'
// override in the context so that this message will be
// logged as SUCCESS if that log level is recognized.
$context['_level'] = ConsoleLogLevel::SUCCESS;
$this->printTaskOutput(LogLevel::NOTICE, $text, $this->getTaskContext($context));
}
/**
* Provide notification that there is something wrong, but
* execution can continue.
*
* Warning messages are displayed at VERBOSITY_NORMAL.
*
* @param string $text
* @param null|array $context
*/
protected function printTaskWarning($text, $context = null)
{
$this->printTaskOutput(LogLevel::WARNING, $text, $this->getTaskContext($context));
}
/**
* Provide notification that some operation in the task failed,
* and the task cannot continue.
*
* Error messages are displayed at VERBOSITY_NORMAL.
*
* @param string $text
* @param null|array $context
*/
protected function printTaskError($text, $context = null)
{
$this->printTaskOutput(LogLevel::ERROR, $text, $this->getTaskContext($context));
}
/**
* Provide debugging notification. These messages are only
* displayed if the log level is VERBOSITY_DEBUG.
*
* @param string$text
* @param null|array $context
*/
protected function printTaskDebug($text, $context = null)
{
$this->printTaskOutput(LogLevel::DEBUG, $text, $this->getTaskContext($context));
}
/**
* @param string $level
* One of the \Psr\Log\LogLevel constant
* @param string $text
* @param null|array $context
*/
protected function printTaskOutput($level, $text, $context)
{
$logger = $this->logger();
if (!$logger) {
return;
}
// Hide the progress indicator, if it is visible.
$inProgress = $this->hideTaskProgress();
$logger->log($level, $text, $this->getTaskContext($context));
// After we have printed our log message, redraw the progress indicator.
$this->showTaskProgress($inProgress);
}
/**
* @return bool
*/
protected function hideTaskProgress()
{
$inProgress = false;
if ($this instanceof ProgressIndicatorAwareInterface) {
$inProgress = $this->inProgress();
}
// If a progress indicator is running on this task, then we mush
// hide it before we print anything, or its display will be overwritten.
if ($inProgress) {
$inProgress = $this->hideProgressIndicator();
}
return $inProgress;
}
/**
* @param $inProgress
*/
protected function showTaskProgress($inProgress)
{
if ($inProgress) {
$this->restoreProgressIndicator($inProgress);
}
}
/**
* Format a quantity of bytes.
*
* @param int $size
* @param int $precision
*
* @return string
*/
protected function formatBytes($size, $precision = 2)
{
if ($size === 0) {
return 0;
}
$base = log($size, 1024);
$suffixes = array('', 'k', 'M', 'G', 'T');
return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
}
/**
* Get the formatted task name for use in task output.
* This is placed in the task context under 'name', and
* used as the log label by Robo\Common\RoboLogStyle,
* which is inserted at the head of log messages by
* Robo\Common\CustomLogStyle::formatMessage().
*
* @param null|object $task
*
* @return string
*/
protected function getPrintedTaskName($task = null)
{
if (!$task) {
$task = $this;
}
return TaskInfo::formatTaskName($task);
}
/**
* @param null|array $context
*
* @return array with context information
*/
protected function getTaskContext($context = null)
{
if (!$context) {
$context = [];
}
if (!is_array($context)) {
$context = ['task' => $context];
}
if (!array_key_exists('task', $context)) {
$context['task'] = $this;
}
return $context + TaskInfo::getTaskContext($context['task']);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Robo\Common;
class TimeKeeper
{
const MINUTE = 60;
const HOUR = 3600;
const DAY = 86400;
/**
* @var float
*/
protected $startedAt;
/**
* @var float
*/
protected $finishedAt;
public function start()
{
if ($this->startedAt) {
return;
}
// Get time in seconds as a float, accurate to the microsecond.
$this->startedAt = microtime(true);
}
public function stop()
{
$this->finishedAt = microtime(true);
}
/**
* @return float|null
*/
public function elapsed()
{
$finished = $this->finishedAt ? $this->finishedAt : microtime(true);
if ($finished - $this->startedAt <= 0) {
return null;
}
return $finished - $this->startedAt;
}
/**
* Format a duration into a human-readable time
*
* @param float $duration Duration in seconds, with fractional component
*
* @return string
*/
public static function formatDuration($duration)
{
if ($duration >= self::DAY * 2) {
return gmdate('z \d\a\y\s H:i:s', $duration);
}
if ($duration > self::DAY) {
return gmdate('\1 \d\a\y H:i:s', $duration);
}
if ($duration > self::HOUR) {
return gmdate("H:i:s", $duration);
}
if ($duration > self::MINUTE) {
return gmdate("i:s", $duration);
}
return round($duration, 3).'s';
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Robo\Common;
trait Timer
{
/**
* @var \Robo\Common\TimeKeeper
*/
protected $timer;
protected function startTimer()
{
if (!isset($this->timer)) {
$this->timer = new TimeKeeper();
}
$this->timer->start();
}
protected function stopTimer()
{
if (!isset($this->timer)) {
return;
}
$this->timer->stop();
}
/**
* @return float|null
*/
protected function getExecutionTime()
{
if (!isset($this->timer)) {
return null;
}
return $this->timer->elapsed();
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Robo;
class Config
{
const PROGRESS_BAR_AUTO_DISPLAY_INTERVAL = 'progress-delay';
const DEFAULT_PROGRESS_DELAY = 2;
const SIMULATE = 'simulate';
const DECORATED = 'decorated';
/**
* @var array
*/
protected $config = [];
/**
* Fet a configuration value
*
* @param string $key Which config item to look up
* @param string|null $defaultOverride Override usual default value with a different default
*
* @return mixed
*/
public function get($key, $defaultOverride = null)
{
if (isset($this->config[$key])) {
return $this->config[$key];
}
return $this->getDefault($key, $defaultOverride);
}
/**
* Set a config value
*
* @param string $key
* @param mixed $value
*
* @return $this
*/
public function set($key, $value)
{
$this->config[$key] = $value;
return $this;
}
/**
* Return an associative array containing all of the global configuration
* options and their default values.
*
* @return array
*/
public function getGlobalOptionDefaultValues()
{
$globalOptions =
[
self::PROGRESS_BAR_AUTO_DISPLAY_INTERVAL => self::DEFAULT_PROGRESS_DELAY,
self::SIMULATE => false,
];
return $globalOptions;
}
/**
* Return the default value for a given configuration item.
*
* @param string $key
* @param mixed $defaultOverride
*
* @return mixed
*/
public function getDefault($key, $defaultOverride = null)
{
$globalOptions = $this->getGlobalOptionDefaultValues();
return isset($globalOptions[$key]) ? $globalOptions[$key] : $defaultOverride;
}
/**
* @return bool
*/
public function isSimulated()
{
return $this->get(self::SIMULATE);
}
/**
* @param bool $simulated
*
* @return $this
*/
public function setSimulated($simulated = true)
{
return $this->set(self::SIMULATE, $simulated);
}
/**
* @return bool
*/
public function isDecorated()
{
return $this->get(self::DECORATED);
}
/**
* @param bool $decorated
*
* @return $this
*/
public function setDecorated($decorated = true)
{
return $this->set(self::DECORATED, $decorated);
}
/**
* @param int $interval
*
* @return $this
*/
public function setProgressBarAutoDisplayInterval($interval)
{
return $this->set(self::PROGRESS_BAR_AUTO_DISPLAY_INTERVAL, $interval);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Robo\Contract;
use Robo\Collection\CollectionBuilder;
interface BuilderAwareInterface
{
/**
* Set the builder reference
*
* @param \Robo\Collection\CollectionBuilder $builder
*/
public function setBuilder(CollectionBuilder $builder);
/**
* Get the builder reference
*
* @return \Robo\Collection\CollectionBuilder
*/
public function getBuilder();
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Robo\Contract;
/**
* Task that implements this interface can be injected as a parameter for other task.
* This task can be represented as executable command.
*
* @package Robo\Contract
*/
interface CommandInterface
{
/**
* Returns command that can be executed.
* This method is used to pass generated command from one task to another.
*
* @return string
*/
public function getCommand();
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Robo\Contract;
/**
* Any Robo tasks that implements this interface will
* be called when the task collection it is added to
* completes.
*
* Interface CompletionInterface
* @package Robo\Contract
*/
interface CompletionInterface extends TaskInterface
{
/**
* Revert an operation that can be rolled back
*/
public function complete();
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Robo\Contract;
use Robo\Config;
interface ConfigAwareInterface
{
/**
* Set the config reference
*
* @param \Robo\Config $config
*
* @return $this
*/
public function setConfig(Config $config);
/**
* Get the config reference
*
* @return \Robo\Config
*/
public function getConfig();
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* Marker interface for tasks that use the IO trait
*/
namespace Robo\Contract;
use \Symfony\Component\Console\Input\InputAwareInterface;
interface IOAwareInterface extends OutputAwareInterface, InputAwareInterface
{
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Robo\Contract;
interface InflectionInterface
{
/**
* Based on league/container inflection: http://container.thephpleague.com/inflectors/
*
* This allows us to run:
*
* (new SomeTask($args))
* ->inflect($this)
* ->initializer()
* ->...
*
* Instead of:
*
* (new SomeTask($args))
* ->setLogger($this->logger)
* ->initializer()
* ->...
*
* The reason `inflect` is better than the more explicit alternative is
* that subclasses of BaseTask that implement a new FooAwareInterface
* can override injectDependencies() as explained below, and add more
* dependencies that can be injected as needed.
*
* @param \Robo\Contract\InflectionInterface $parent
*/
public function inflect(InflectionInterface $parent);
/**
* Take all dependencies availble to this task and inject any that are
* needed into the provided task. The general pattern is that, for every
* FooAwareInterface that this class implements, it should test to see
* if the child also implements the same interface, and if so, should call
* $child->setFoo($this->foo).
*
* The benefits of this are pretty large. Any time an object that implements
* InflectionInterface is created, just call `$child->inflect($this)`, and
* any available optional dependencies will be hooked up via setter injection.
*
* The required dependencies of an object should be provided via constructor
* injection, not inflection.
*
* @param InflectionInterface $child An object created by this class that
* should have its dependencies injected.
*
* @see https://mwop.net/blog/2016-04-26-on-locators.html
*/
public function injectDependencies(InflectionInterface $child);
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* Provide OutputAwareInterface, not present in Symfony Console
*/
namespace Robo\Contract;
use Symfony\Component\Console\Output\OutputInterface;
interface OutputAwareInterface
{
/**
* Sets the Console Output.
*
* @param \Symfony\Component\Console\Output\OutputInterface $output
*/
public function setOutput(OutputInterface $output);
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Robo\Contract;
/**
* If task prints anything to console
*
* Interface PrintedInterface
* @package Robo\Contract
*/
interface PrintedInterface
{
/**
* @return bool
*/
public function getPrinted();
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Robo\Contract;
/**
* Any Robo task that uses the Timer trait and
* implements ProgressIndicatorAwareInterface will
* display a progress bar while the timer is running.
* Call advanceProgressIndicator to advance the indicator.
*
* Interface ProgressIndicatorAwareInterface
* @package Robo\Contract
*/
interface ProgressIndicatorAwareInterface
{
/**
* @return int
*/
public function progressIndicatorSteps();
/**
* @param \Robo\Common\ProgressIndicator $progressIndicator
*/
public function setProgressIndicator($progressIndicator);
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Robo\Contract;
/**
* Robo tasks that take multiple steps to complete should
* implement this interface.
*
* Interface ProgressInterface
* @package Robo\Contract
*/
interface ProgressInterface
{
/**
*
* @return int
*/
public function progressSteps();
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Robo\Contract;
/**
* Any Robo tasks that implements this interface will
* be called when the task collection it is added to
* fails, and runs its rollback operation.
*
* Interface RollbackInterface
* @package Robo\Contract
*/
interface RollbackInterface extends TaskInterface
{
/**
* Revert an operation that can be rolled back
*/
public function rollback();
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Robo\Contract;
/**
* Task that implements this interface can be injected as a parameter for other task.
* This task can be represented as executable command.
*
* @package Robo\Contract
*/
interface SimulatedInterface extends TaskInterface
{
/**
* Called in place of `run()` for simulated tasks.
*
* @param null|array $context
*/
public function simulate($context);
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Robo\Contract;
/**
* All Robo tasks should implement this interface.
* Task should be configured by chained methods.
*
* Interface TaskInterface
* @package Robo\Contract
*/
interface TaskInterface
{
/**
* @return \Robo\Result
*/
public function run();
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Robo\Contract;
interface WrappedTaskInterface extends TaskInterface
{
/**
* @return \Robo\Contract\TaskInterface
*/
public function original();
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Robo\Exception;
class TaskException extends \Exception
{
public function __construct($class, $message)
{
if (is_object($class)) {
$class = get_class($class);
}
parent::__construct(" in task $class \n\n $message");
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Robo\Exception;
class TaskExitException extends \Exception
{
public function __construct($class, $message, $status)
{
if (is_object($class)) {
$class = get_class($class);
}
parent::__construct(" in task $class \n\n $message", $status);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Robo;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Robo\Contract\ConfigAwareInterface;
use Robo\Common\ConfigAwareTrait;
class GlobalOptionsEventListener implements EventSubscriberInterface, ConfigAwareInterface
{
use ConfigAwareTrait;
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [ConsoleEvents::COMMAND => 'setGlobalOptions'];
}
/**
* Before a Console command runs, examine the global
* commandline options from the event Input, and set
* configuration values as appropriate.
*
* @param \Symfony\Component\Console\Event\ConsoleCommandEvent $event
*/
public function setGlobalOptions(ConsoleCommandEvent $event)
{
$config = $this->getConfig();
$input = $event->getInput();
$globalOptions = $config->getGlobalOptionDefaultValues();
foreach ($globalOptions as $option => $default) {
$value = $input->hasOption($option) ? $input->getOption($option) : null;
// Unfortunately, the `?:` operator does not differentate between `0` and `null`
if (!isset($value)) {
$value = $default;
}
$config->set($option, $value);
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Robo;
trait LoadAllTasks
{
use TaskAccessor;
use Collection\loadTasks;
// standard tasks
use Task\Base\loadTasks;
use Task\Development\loadTasks;
use Task\Filesystem\loadTasks;
use Task\File\loadTasks;
use Task\Archive\loadTasks;
use Task\Vcs\loadTasks;
// package managers
use Task\Composer\loadTasks;
use Task\Bower\loadTasks;
use Task\Npm\loadTasks;
// assets
use Task\Assets\loadTasks;
// 3rd-party tools
use Task\Remote\loadTasks;
use Task\Testing\loadTasks;
use Task\ApiGen\loadTasks;
use Task\Docker\loadTasks;
// task runners
use Task\Gulp\loadTasks;
// shortcuts
use Task\Base\loadShortcuts;
use Task\Filesystem\loadShortcuts;
use Task\Vcs\loadShortcuts;
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Robo\Log;
use Robo\Result;
use Robo\TaskInfo;
use Robo\Contract\PrintedInterface;
use Robo\Contract\ProgressIndicatorAwareInterface;
use Robo\Common\ProgressIndicatorAwareTrait;
use Psr\Log\LogLevel;
use Psr\Log\LoggerInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Consolidation\Log\ConsoleLogLevel;
/**
* Log the creation of Result objects.
*/
class ResultPrinter implements LoggerAwareInterface, ProgressIndicatorAwareInterface
{
use LoggerAwareTrait;
use ProgressIndicatorAwareTrait;
/**
* Log the result of a Robo task.
*
* Returns 'true' if the message is printed, or false if it isn't.
*
* @param \Robo\Result $result
*
* @return bool
*/
public function printResult(Result $result)
{
if (!$result->wasSuccessful()) {
return $this->printError($result);
} else {
return $this->printSuccess($result);
}
}
/**
* Log that we are about to abort due to an error being encountered
* in 'stop on fail' mode.
*
* @param \Robo\Result $result
*/
public function printStopOnFail($result)
{
$this->printMessage(LogLevel::NOTICE, 'Stopping on fail. Exiting....');
$this->printMessage(LogLevel::ERROR, 'Exit Code: {code}', ['code' => $result->getExitCode()]);
}
/**
* Log the result of a Robo task that returned an error.
*
* @param \Robo\Result $result
*
* @return bool
*/
protected function printError(Result $result)
{
$task = $result->getTask();
$context = $result->getContext() + ['timer-label' => 'Time', '_style' => []];
$context['_style']['message'] = '';
$printOutput = true;
if ($task instanceof PrintedInterface) {
$printOutput = !$task->getPrinted();
}
if ($printOutput) {
$this->printMessage(LogLevel::ERROR, "{message}", $context);
}
$this->printMessage(LogLevel::ERROR, 'Exit code {code}', $context);
return true;
}
/**
* Log the result of a Robo task that was successful.
*
* @param \Robo\Result $result
*
* @return bool
*/
protected function printSuccess(Result $result)
{
$task = $result->getTask();
$context = $result->getContext() + ['timer-label' => 'in'];
$time = $result->getExecutionTime();
if ($time) {
$this->printMessage(ConsoleLogLevel::SUCCESS, 'Done', $context);
}
return false;
}
/**
* @param string $level
* @param string $message
* @param array $context
*/
protected function printMessage($level, $message, $context = [])
{
$inProgress = $this->hideProgressIndicator();
$this->logger->log($level, $message, $context);
if ($inProgress) {
$this->restoreProgressIndicator($inProgress);
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Robo\Log;
class RoboLogLevel extends \Consolidation\Log\ConsoleLogLevel
{
/**
* Command did something in simulated mode.
* Displayed at VERBOSITY_NORMAL.
*/
const SIMULATED_ACTION = 'simulated';
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Robo\Log;
use Robo\Common\TimeKeeper;
use Consolidation\Log\LogOutputStyler;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\OutputStyle;
/**
* Robo Log Styler.
*/
class RoboLogStyle extends LogOutputStyler
{
const TASK_STYLE_SIMULATED = 'options=reverse;bold';
/**
* RoboLogStyle constructor.
*
* @param array $labelStyles
* @param array $messageStyles
*/
public function __construct($labelStyles = [], $messageStyles = [])
{
parent::__construct($labelStyles, $messageStyles);
$this->labelStyles += [
RoboLogLevel::SIMULATED_ACTION => self::TASK_STYLE_SIMULATED,
];
$this->messageStyles += [
RoboLogLevel::SIMULATED_ACTION => '',
];
}
/**
* Log style customization for Robo: replace the log level with
* the task name.
*
* @param string $level
* @param string $message
* @param array $context
*
* @return string
*/
protected function formatMessageByLevel($level, $message, $context)
{
$label = $level;
if (array_key_exists('name', $context)) {
$label = $context['name'];
}
return $this->formatMessage($label, $message, $context, $this->labelStyles[$level], $this->messageStyles[$level]);
}
/**
* Log style customization for Robo: add the time indicator to the
* end of the log message if it exists in the context.
*
* @param string $label
* @param string $message
* @param array $context
* @param string $taskNameStyle
* @param string $messageStyle
*
* @return string
*/
protected function formatMessage($label, $message, $context, $taskNameStyle, $messageStyle = '')
{
$message = parent::formatMessage($label, $message, $context, $taskNameStyle, $messageStyle);
if (array_key_exists('time', $context) && !empty($context['time']) && array_key_exists('timer-label', $context)) {
$duration = TimeKeeper::formatDuration($context['time']);
$message .= ' ' . $context['timer-label'] . ' ' . $this->wrapFormatString($duration, 'fg=yellow');
}
return $message;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Robo\Log;
use Robo\Result;
use Robo\TaskInfo;
use Robo\Contract\PrintedInterface;
use Robo\Contract\LogResultInterface;
use Consolidation\Log\ConsoleLogLevel;
use Consolidation\Log\Logger;
use Psr\Log\LogLevel;
use Psr\Log\AbstractLogger;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Logger\ConsoleLogger;
/**
* Robo's default logger
*/
class RoboLogger extends Logger
{
/**
* @param \Symfony\Component\Console\Output\OutputInterface $output
*/
public function __construct(OutputInterface $output)
{
// In Robo, we use log level 'notice' for messages that appear all
// the time, and 'info' for messages that appear only during verbose
// output. We have no 'very verbose' (-vv) level. 'Debug' is -vvv, as usual.
$roboVerbosityOverrides = [
RoboLogLevel::SIMULATED_ACTION => OutputInterface::VERBOSITY_NORMAL, // Default is "verbose"
LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL, // Default is "verbose"
LogLevel::INFO => OutputInterface::VERBOSITY_VERBOSE, // Default is "very verbose"
];
parent::__construct($output, $roboVerbosityOverrides);
}
}

View File

@@ -0,0 +1,234 @@
<?php
namespace Robo;
use Robo\Contract\TaskInterface;
use Robo\Contract\LogResultInterface;
use Robo\Exception\TaskExitException;
class Result extends ResultData
{
/**
* @var bool
*/
public static $stopOnFail = false;
/**
* @var \Robo\Contract\TaskInterface
*/
protected $task;
/**
* @param \Robo\Contract\TaskInterface $task
* @param string $exitCode
* @param string $message
* @param array $data
*/
public function __construct(TaskInterface $task, $exitCode, $message = '', $data = [])
{
parent::__construct($exitCode, $message, $data);
$this->task = $task;
$this->printResult();
if (self::$stopOnFail) {
$this->stopOnFail();
}
}
protected function printResult()
{
// For historic reasons, the Result constructor is responsible
// for printing task results.
// TODO: Make IO the responsibility of some other class. Maintaining
// existing behavior for backwards compatibility. This is undesirable
// in the long run, though, as it can result in unwanted repeated input
// in task collections et. al.
$resultPrinter = Robo::resultPrinter();
if ($resultPrinter) {
if ($resultPrinter->printResult($this)) {
$this->data['already-printed'] = true;
}
}
}
/**
* @param \Robo\Contract\TaskInterface $task
* @param string $extension
* @param string $service
*
* @return \Robo\Result
*/
public static function errorMissingExtension(TaskInterface $task, $extension, $service)
{
$messageTpl = 'PHP extension required for %s. Please enable %s';
$message = sprintf($messageTpl, $service, $extension);
return self::error($task, $message);
}
/**
* @param \Robo\Contract\TaskInterface $task
* @param string $class
* @param string $package
*
* @return \Robo\Result
*/
public static function errorMissingPackage(TaskInterface $task, $class, $package)
{
$messageTpl = 'Class %s not found. Please install %s Composer package';
$message = sprintf($messageTpl, $class, $package);
return self::error($task, $message);
}
/**
* @param \Robo\Contract\TaskInterface $task
* @param string $message
* @param array $data
*
* @return \Robo\Result
*/
public static function error(TaskInterface $task, $message, $data = [])
{
return new self($task, self::EXITCODE_ERROR, $message, $data);
}
/**
* @param \Robo\Contract\TaskInterface $task
* @param \Exception $e
* @param array $data
*
* @return \Robo\Result
*/
public static function fromException(TaskInterface $task, \Exception $e, $data = [])
{
$exitCode = $e->getCode();
if (!$exitCode) {
$exitCode = self::EXITCODE_ERROR;
}
return new self($task, $exitCode, $e->getMessage(), $data);
}
/**
* @param \Robo\Contract\TaskInterface $task
* @param string $message
* @param array $data
*
* @return \Robo\Result
*/
public static function success(TaskInterface $task, $message = '', $data = [])
{
return new self($task, self::EXITCODE_OK, $message, $data);
}
/**
* Return a context useful for logging messages.
*
* @return array
*/
public function getContext()
{
$task = $this->getTask();
return TaskInfo::getTaskContext($task) + [
'code' => $this->getExitCode(),
'data' => $this->getArrayCopy(),
'time' => $this->getExecutionTime(),
'message' => $this->getMessage(),
];
}
/**
* Add the results from the most recent task to the accumulated
* results from all tasks that have run so far, merging data
* as necessary.
*
* @param int|string $key
* @param \Robo\Result $taskResult
*/
public function accumulate($key, Result $taskResult)
{
// If the task is unnamed, then all of its data elements
// just get merged in at the top-level of the final Result object.
if (static::isUnnamed($key)) {
$this->merge($taskResult);
} elseif (isset($this[$key])) {
// There can only be one task with a given name; however, if
// there are tasks added 'before' or 'after' the named task,
// then the results from these will be stored under the same
// name unless they are given a name of their own when added.
$current = $this[$key];
$this[$key] = $taskResult->merge($current);
} else {
$this[$key] = $taskResult;
}
}
/**
* We assume that named values (e.g. for associative array keys)
* are non-numeric; numeric keys are presumed to simply be the
* index of an array, and therefore insignificant.
*
* @param int|string $key
*
* @return bool
*/
public static function isUnnamed($key)
{
return is_numeric($key);
}
/**
* @return \Robo\Contract\TaskInterface
*/
public function getTask()
{
return $this->task;
}
/**
* @return \Robo\Contract\TaskInterface
*/
public function cloneTask()
{
$reflect = new \ReflectionClass(get_class($this->task));
return $reflect->newInstanceArgs(func_get_args());
}
/**
* @return bool
*
* @deprecated since 1.0.
*
* @see wasSuccessful()
*/
public function __invoke()
{
trigger_error(__METHOD__ . ' is deprecated: use wasSuccessful() instead.', E_USER_DEPRECATED);
return $this->wasSuccessful();
}
/**
* @return $this
*/
public function stopOnFail()
{
if (!$this->wasSuccessful()) {
$resultPrinter = Robo::resultPrinter();
if ($resultPrinter) {
$resultPrinter->printStopOnFail($this);
}
$this->exitEarly($this->getExitCode());
}
return $this;
}
/**
* @param int $status
*
* @throws \Robo\Exception\TaskExitException
*/
private function exitEarly($status)
{
throw new TaskExitException($this->getTask(), $this->getMessage(), $status);
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Robo;
use Robo\Contract\LogResultInterface;
use Consolidation\AnnotatedCommand\ExitCodeInterface;
use Consolidation\AnnotatedCommand\OutputDataInterface;
class ResultData extends \ArrayObject implements ExitCodeInterface, OutputDataInterface
{
/**
* @var int
*/
protected $exitCode;
/**
* @var string
*/
protected $message;
const EXITCODE_OK = 0;
const EXITCODE_ERROR = 1;
/** Symfony Console handles these conditions; Robo returns the status
code selected by Symfony. These are here for documentation purposes. */
const EXITCODE_MISSING_OPTIONS = 2;
const EXITCODE_COMMAND_NOT_FOUND = 127;
/** The command was aborted because the user chose to cancel it at some prompt.
This exit code is arbitrarily the same as EX_TEMPFAIL in sysexits.h, although
note that shell error codes are distinct from C exit codes, so this alignment
not particularly meaningful. */
const EXITCODE_USER_CANCEL = 75;
/**
* @param int $exitCode
* @param string $message
* @param array $data
*/
public function __construct($exitCode = self::EXITCODE_OK, $message = '', $data = [])
{
$this->exitCode = $exitCode;
$this->message = $message;
parent::__construct($data);
}
/**
* @param string $message
* @param array $data
*
* @return \Robo\ResultData
*/
public static function message($message, $data = [])
{
return new self(self::EXITCODE_OK, $message, $data);
}
/**
* @param string $message
* @param array $data
*
* @return \Robo\ResultData
*/
public static function cancelled($message = '', $data = [])
{
return new ResultData(self::EXITCODE_USER_CANCEL, $message, $data);
}
/**
* @return array
*/
public function getData()
{
return $this->getArrayCopy();
}
/**
* @return int
*/
public function getExitCode()
{
return $this->exitCode;
}
/**
* @return null|string
*/
public function getOutputData()
{
if (!empty($this->message) && !isset($this['already-printed'])) {
return $this->message;
}
}
/**
* @return string
*/
public function getMessage()
{
return $this->message;
}
/**
* @return bool
*/
public function wasSuccessful()
{
return $this->exitCode === self::EXITCODE_OK;
}
/**
* @return bool
*/
public function wasCancelled()
{
return $this->exitCode == self::EXITCODE_USER_CANCEL;
}
/**
* Merge another result into this result. Data already
* existing in this result takes precedence over the
* data in the Result being merged.
*
* @param \Robo\ResultData $result
*
* @return $this
*/
public function merge(ResultData $result)
{
$mergedData = $this->getArrayCopy() + $result->getArrayCopy();
$this->exchangeArray($mergedData);
return $this;
}
/**
* @return bool
*/
public function hasExecutionTime()
{
return isset($this['time']);
}
/**
* @return null|float
*/
public function getExecutionTime()
{
if (!$this->hasExecutionTime()) {
return null;
}
return $this['time'];
}
}

View File

@@ -0,0 +1,337 @@
<?php
namespace Robo;
use League\Container\Container;
use League\Container\ContainerInterface;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Application as SymfonyApplication;
/**
* Manages the container reference and other static data. Favor
* using dependency injection wherever possible. Avoid using
* this class directly, unless setting up a custom DI container.
*/
class Robo
{
const APPLICATION_NAME = 'Robo';
const VERSION = '1.0.4';
/**
* The currently active container object, or NULL if not initialized yet.
*
* @var ContainerInterface|null
*/
protected static $container;
/**
* Entrypoint for standalone Robo-based tools. See docs/framework.md.
*
* @param string[] $argv
* @param string $commandClasses
* @param null|string $appName
* @param null|string $appVersion
* @param null|\Symfony\Component\Console\Output\OutputInterface $output
*
* @return int
*/
public static function run($argv, $commandClasses, $appName = null, $appVersion = null, $output = null)
{
$runner = new \Robo\Runner($commandClasses);
$statusCode = $runner->execute($argv, $appName, $appVersion, $output);
return $statusCode;
}
/**
* Sets a new global container.
*
* @param ContainerInterface $container
* A new container instance to replace the current.
*/
public static function setContainer(ContainerInterface $container)
{
static::$container = $container;
}
/**
* Unsets the global container.
*/
public static function unsetContainer()
{
static::$container = null;
}
/**
* Returns the currently active global container.
*
* @return \League\Container\ContainerInterface
*
* @throws \RuntimeException
*/
public static function getContainer()
{
if (static::$container === null) {
throw new \RuntimeException('container is not initialized yet. \Robo\Robo::setContainer() must be called with a real container.');
}
return static::$container;
}
/**
* Returns TRUE if the container has been initialized, FALSE otherwise.
*
* @return bool
*/
public static function hasContainer()
{
return static::$container !== null;
}
/**
* Create a container and initiailze it. If you wish to *change*
* anything defined in the container, then you should call
* \Robo::configureContainer() instead of this function.
*
* @param null|\Symfony\Component\Console\Input\InputInterface $input
* @param null|\Symfony\Component\Console\Output\OutputInterface $output
* @param null|\Robo\Application $app
* @param null|\Robo\Config $config
*
* @return \League\Container\Container|\League\Container\ContainerInterface
*/
public static function createDefaultContainer($input = null, $output = null, $app = null, $config = null)
{
// Do not allow this function to be called more than once.
if (static::hasContainer()) {
return static::getContainer();
}
if (!$app) {
$app = static::createDefaultApplication();
}
if (!$config) {
$config = new Config();
}
// Set up our dependency injection container.
$container = new Container();
static::configureContainer($container, $app, $config, $input, $output);
// Set the application dispatcher
$app->setDispatcher($container->get('eventDispatcher'));
return $container;
}
/**
* Initialize a container with all of the default Robo services.
* IMPORTANT: after calling this method, clients MUST call:
*
* $dispatcher = $container->get('eventDispatcher');
* $app->setDispatcher($dispatcher);
*
* Any modification to the container should be done prior to fetching
* objects from it.
*
* It is recommended to use \Robo::createDefaultContainer()
* instead, which does all required setup for the caller, but has
* the limitation that the container it creates can only be
* extended, not modified.
*
* @param \League\Container\ContainerInterface $container
* @param \Symfony\Component\Console\Application $app
* @param \Robo\Config $config
* @param null|\Symfony\Component\Console\Input\InputInterface $input
* @param null|\Symfony\Component\Console\Output\OutputInterface $output
*/
public static function configureContainer(ContainerInterface $container, SymfonyApplication $app, Config $config, $input = null, $output = null)
{
// Self-referential container refernce for the inflector
$container->add('container', $container);
static::setContainer($container);
// Create default input and output objects if they were not provided
if (!$input) {
$input = new StringInput('');
}
if (!$output) {
$output = new \Symfony\Component\Console\Output\ConsoleOutput();
}
$config->setDecorated($output->isDecorated());
$container->share('application', $app);
$container->share('config', $config);
$container->share('input', $input);
$container->share('output', $output);
// Register logging and related services.
$container->share('logStyler', \Robo\Log\RoboLogStyle::class);
$container->share('logger', \Robo\Log\RoboLogger::class)
->withArgument('output')
->withMethodCall('setLogOutputStyler', ['logStyler']);
$container->add('progressBar', \Symfony\Component\Console\Helper\ProgressBar::class)
->withArgument('output');
$container->share('progressIndicator', \Robo\Common\ProgressIndicator::class)
->withArgument('progressBar')
->withArgument('output');
$container->share('resultPrinter', \Robo\Log\ResultPrinter::class);
$container->add('simulator', \Robo\Task\Simulator::class);
$container->share('globalOptionsEventListener', \Robo\GlobalOptionsEventListener::class);
$container->share('collectionProcessHook', \Robo\Collection\CollectionProcessHook::class);
$container->share('hookManager', \Consolidation\AnnotatedCommand\Hooks\HookManager::class)
->withMethodCall('addResultProcessor', ['collectionProcessHook', '*']);
$container->share('alterOptionsCommandEvent', \Consolidation\AnnotatedCommand\Options\AlterOptionsCommandEvent::class)
->withArgument('application');
$container->share('eventDispatcher', \Symfony\Component\EventDispatcher\EventDispatcher::class)
->withMethodCall('addSubscriber', ['globalOptionsEventListener'])
->withMethodCall('addSubscriber', ['alterOptionsCommandEvent'])
->withMethodCall('addSubscriber', ['hookManager']);
$container->share('formatterManager', \Consolidation\OutputFormatters\FormatterManager::class)
->withMethodCall('addDefaultFormatters', [])
->withMethodCall('addDefaultSimplifiers', []);
$container->share('commandProcessor', \Consolidation\AnnotatedCommand\CommandProcessor::class)
->withArgument('hookManager')
->withMethodCall('setFormatterManager', ['formatterManager'])
->withMethodCall(
'setDisplayErrorFunction',
[
function ($output, $message) use ($container) {
$logger = $container->get('logger');
$logger->error($message);
}
]
);
$container->share('commandFactory', \Consolidation\AnnotatedCommand\AnnotatedCommandFactory::class)
->withMethodCall('setCommandProcessor', ['commandProcessor']);
$container->add('collection', \Robo\Collection\Collection::class);
$container->add('collectionBuilder', \Robo\Collection\CollectionBuilder::class);
static::addInflectors($container);
// Make sure the application is appropriately initialized.
$app->setAutoExit(false);
}
/**
* @param null|string $appName
* @param null|string $appVersion
*
* @return \Robo\Application
*/
public static function createDefaultApplication($appName = null, $appVersion = null)
{
$appName = $appName ?: self::APPLICATION_NAME;
$appVersion = $appVersion ?: self::VERSION;
$app = new \Robo\Application($appName, $appVersion);
$app->setAutoExit(false);
return $app;
}
/**
* Add the Robo League\Container inflectors to the container
*
* @param \League\Container\ContainerInterface $container
*/
public static function addInflectors($container)
{
// Register our various inflectors.
$container->inflector(\Robo\Contract\ConfigAwareInterface::class)
->invokeMethod('setConfig', ['config']);
$container->inflector(\Psr\Log\LoggerAwareInterface::class)
->invokeMethod('setLogger', ['logger']);
$container->inflector(\League\Container\ContainerAwareInterface::class)
->invokeMethod('setContainer', ['container']);
$container->inflector(\Symfony\Component\Console\Input\InputAwareInterface::class)
->invokeMethod('setInput', ['input']);
$container->inflector(\Robo\Contract\OutputAwareInterface::class)
->invokeMethod('setOutput', ['output']);
$container->inflector(\Robo\Contract\ProgressIndicatorAwareInterface::class)
->invokeMethod('setProgressIndicator', ['progressIndicator']);
}
/**
* Retrieves a service from the container.
*
* Use this method if the desired service is not one of those with a dedicated
* accessor method below. If it is listed below, those methods are preferred
* as they can return useful type hints.
*
* @param string $id
* The ID of the service to retrieve.
*
* @return mixed
* The specified service.
*/
public static function service($id)
{
return static::getContainer()->get($id);
}
/**
* Indicates if a service is defined in the container.
*
* @param string $id
* The ID of the service to check.
*
* @return bool
* TRUE if the specified service exists, FALSE otherwise.
*/
public static function hasService($id)
{
// Check hasContainer() first in order to always return a Boolean.
return static::hasContainer() && static::getContainer()->has($id);
}
/**
* Return the result printer object.
*
* @return \Robo\Log\ResultPrinter
*/
public static function resultPrinter()
{
return static::service('resultPrinter');
}
/**
* @return \Robo\Config
*/
public static function config()
{
return static::service('config');
}
/**
* @return \Consolidation\Log\Logger
*/
public static function logger()
{
return static::service('logger');
}
/**
* @return \Robo\Application
*/
public static function application()
{
return static::service('application');
}
/**
* Return the output object.
*
* @return \Symfony\Component\Console\Output\OutputInterface
*/
public static function output()
{
return static::service('output');
}
/**
* Return the input object.
*
* @return \Symfony\Component\Console\Input\InputInterface
*/
public static function input()
{
return static::service('input');
}
}

View File

@@ -0,0 +1,419 @@
<?php
namespace Robo;
use League\Container\Container;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\StringInput;
use Consolidation\AnnotatedCommand\PassThroughArgsInput;
use Robo\Contract\BuilderAwareInterface;
use Robo\Common\IO;
use Robo\Exception\TaskExitException;
use League\Container\ContainerInterface;
use League\Container\ContainerAwareInterface;
use League\Container\ContainerAwareTrait;
class Runner implements ContainerAwareInterface
{
const ROBOCLASS = 'RoboFile';
const ROBOFILE = 'RoboFile.php';
use IO;
use ContainerAwareTrait;
/**
* @var string
*/
protected $roboClass;
/**
* @var string
*/
protected $roboFile;
/**
* @var string working dir of Robo
*/
protected $dir;
/**
* Class Constructor
*
* @param null|string $roboClass
* @param null|string $roboFile
*/
public function __construct($roboClass = null, $roboFile = null)
{
// set the const as class properties to allow overwriting in child classes
$this->roboClass = $roboClass ? $roboClass : self::ROBOCLASS ;
$this->roboFile = $roboFile ? $roboFile : self::ROBOFILE;
$this->dir = getcwd();
}
/**
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return bool
*/
protected function loadRoboFile($output)
{
// If we have not been provided an output object, make a temporary one.
if (!$output) {
$output = new \Symfony\Component\Console\Output\ConsoleOutput();
}
// If $this->roboClass is a single class that has not already
// been loaded, then we will try to obtain it from $this->roboFile.
// If $this->roboClass is an array, we presume all classes requested
// are available via the autoloader.
if (is_array($this->roboClass) || class_exists($this->roboClass)) {
return true;
}
if (!file_exists($this->dir)) {
$output->writeln("<error>Path `{$this->dir}` is invalid; please provide a valid absolute path to the Robofile to load.</error>");
return false;
}
$realDir = realpath($this->dir);
$roboFilePath = $realDir . DIRECTORY_SEPARATOR . $this->roboFile;
if (!file_exists($roboFilePath)) {
$requestedRoboFilePath = $this->dir . DIRECTORY_SEPARATOR . $this->roboFile;
$output->writeln("<error>Requested RoboFile `$requestedRoboFilePath` is invalid, please provide valid absolute path to load Robofile</error>");
return false;
}
require_once $roboFilePath;
if (!class_exists($this->roboClass)) {
$output->writeln("<error>Class ".$this->roboClass." was not loaded</error>");
return false;
}
return true;
}
/**
* @param array $argv
* @param null|string $appName
* @param null|string $appVersion
* @param null|\Symfony\Component\Console\Output\OutputInterface $output
*
* @return int
*/
public function execute($argv, $appName = null, $appVersion = null, $output = null)
{
$argv = $this->shebang($argv);
$argv = $this->processRoboOptions($argv);
$app = null;
if ($appName && $appVersion) {
$app = Robo::createDefaultApplication($appName, $appVersion);
}
$commandFiles = $this->getRoboFileCommands($output);
return $this->run($argv, $output, $app, $commandFiles);
}
/**
* @param null|\Symfony\Component\Console\Input\InputInterface $input
* @param null|\Symfony\Component\Console\Output\OutputInterface $output
* @param null|\Robo\Application $app
* @param array[] $commandFiles
*
* @return int
*/
public function run($input = null, $output = null, $app = null, $commandFiles = [])
{
// Create default input and output objects if they were not provided
if (!$input) {
$input = new StringInput('');
}
if (is_array($input)) {
$input = new ArgvInput($input);
}
if (!$output) {
$output = new \Symfony\Component\Console\Output\ConsoleOutput();
}
$this->setInput($input);
$this->setOutput($output);
// If we were not provided a container, then create one
if (!$this->getContainer()) {
$container = Robo::createDefaultContainer($input, $output, $app);
$this->setContainer($container);
// Automatically register a shutdown function and
// an error handler when we provide the container.
$this->installRoboHandlers();
}
if (!$app) {
$app = Robo::application();
}
if (!isset($commandFiles)) {
$this->yell("Robo is not initialized here. Please run `robo init` to create a new RoboFile", 40, 'yellow');
$app->addInitRoboFileCommand($this->roboFile, $this->roboClass);
$commandFiles = [];
}
$this->registerCommandClasses($app, $commandFiles);
try {
$statusCode = $app->run($input, $output);
} catch (TaskExitException $e) {
$statusCode = $e->getCode() ?: 1;
}
return $statusCode;
}
/**
* @param \Symfony\Component\Console\Output\OutputInterface $output
*
* @return null|string
*/
protected function getRoboFileCommands($output)
{
if (!$this->loadRoboFile($output)) {
return;
}
return $this->roboClass;
}
/**
* @param \Robo\Application $app
* @param array $commandClasses
*/
public function registerCommandClasses($app, $commandClasses)
{
foreach ((array)$commandClasses as $commandClass) {
$this->registerCommandClass($app, $commandClass);
}
}
/**
* @param \Robo\Application $app
* @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
*
* @return mixed|void
*/
public function registerCommandClass($app, $commandClass)
{
$container = Robo::getContainer();
$roboCommandFileInstance = $this->instantiateCommandClass($commandClass);
if (!$roboCommandFileInstance) {
return;
}
// Register commands for all of the public methods in the RoboFile.
$commandFactory = $container->get('commandFactory');
$commandList = $commandFactory->createCommandsFromClass($roboCommandFileInstance);
foreach ($commandList as $command) {
$app->add($command);
}
return $roboCommandFileInstance;
}
/**
* @param string|BuilderAwareInterface|ContainerAwareInterface $commandClass
*
* @return null|object
*/
protected function instantiateCommandClass($commandClass)
{
$container = Robo::getContainer();
// Register the RoboFile with the container and then immediately
// fetch it; this ensures that all of the inflectors will run.
// If the command class is already an instantiated object, then
// just use it exactly as it was provided to us.
if (is_string($commandClass)) {
$reflectionClass = new \ReflectionClass($commandClass);
if ($reflectionClass->isAbstract()) {
return;
}
$commandFileName = "{$commandClass}Commands";
$container->share($commandFileName, $commandClass);
$commandClass = $container->get($commandFileName);
}
// If the command class is a Builder Aware Interface, then
// ensure that it has a builder. Every command class needs
// its own collection builder, as they have references to each other.
if ($commandClass instanceof BuilderAwareInterface) {
$builder = $container->get('collectionBuilder', [$commandClass]);
$commandClass->setBuilder($builder);
}
if ($commandClass instanceof ContainerAwareInterface) {
$commandClass->setContainer($container);
}
return $commandClass;
}
public function installRoboHandlers()
{
register_shutdown_function(array($this, 'shutdown'));
set_error_handler(array($this, 'handleError'));
}
/**
* Process a shebang script, if one was used to launch this Runner.
*
* @param array $args
*
* @return array $args with shebang script removed
*/
protected function shebang($args)
{
// Option 1: Shebang line names Robo, but includes no parameters.
// #!/bin/env robo
// The robo class may contain multiple commands; the user may
// select which one to run, or even get a list of commands or
// run 'help' on any of the available commands as usual.
if ((count($args) > 1) && $this->isShebangFile($args[1])) {
return array_merge([$args[0]], array_slice($args, 2));
}
// Option 2: Shebang line stipulates which command to run.
// #!/bin/env robo mycommand
// The robo class must contain a public method named 'mycommand'.
// This command will be executed every time. Arguments and options
// may be provided on the commandline as usual.
if ((count($args) > 2) && $this->isShebangFile($args[2])) {
return array_merge([$args[0]], explode(' ', $args[1]), array_slice($args, 3));
}
return $args;
}
/**
* Determine if the specified argument is a path to a shebang script.
* If so, load it.
*
* @param string $filepath file to check
*
* @return bool Returns TRUE if shebang script was processed
*/
protected function isShebangFile($filepath)
{
if (!is_file($filepath)) {
return false;
}
$fp = fopen($filepath, "r");
if ($fp === false) {
return false;
}
$line = fgets($fp);
$result = $this->isShebangLine($line);
if ($result) {
while ($line = fgets($fp)) {
$line = trim($line);
if ($line == '<?php') {
$script = stream_get_contents($fp);
if (preg_match('#^class *([^ ]+)#m', $script, $matches)) {
$this->roboClass = $matches[1];
eval($script);
$result = true;
}
}
}
}
fclose($fp);
return $result;
}
/**
* Test to see if the provided line is a robo 'shebang' line.
*
* @param string $line
*
* @return bool
*/
protected function isShebangLine($line)
{
return ((substr($line, 0, 2) == '#!') && (strstr($line, 'robo') !== false));
}
/**
* Check for Robo-specific arguments such as --load-from, process them,
* and remove them from the array. We have to process --load-from before
* we set up Symfony Console.
*
* @param array $argv
*
* @return array
*/
protected function processRoboOptions($argv)
{
// loading from other directory
$pos = $this->arraySearchBeginsWith('--load-from', $argv) ?: array_search('-f', $argv);
if ($pos === false) {
return $argv;
}
$passThru = array_search('--', $argv);
if (($passThru !== false) && ($passThru < $pos)) {
return $argv;
}
if (substr($argv[$pos], 0, 12) == '--load-from=') {
$this->dir = substr($argv[$pos], 12);
} elseif (isset($argv[$pos +1])) {
$this->dir = $argv[$pos +1];
unset($argv[$pos +1]);
}
unset($argv[$pos]);
// Make adjustments if '--load-from' points at a file.
if (is_file($this->dir) || (substr($this->dir, -4) == '.php')) {
$this->roboFile = basename($this->dir);
$this->dir = dirname($this->dir);
$className = basename($this->roboFile, '.php');
if ($className != $this->roboFile) {
$this->roboClass = $className;
}
}
// Convert directory to a real path, but only if the
// path exists. We do not want to lose the original
// directory if the user supplied a bad value.
$realDir = realpath($this->dir);
if ($realDir) {
chdir($realDir);
$this->dir = $realDir;
}
return $argv;
}
/**
* @param string $needle
* @param string[] $haystack
*
* @return bool|int
*/
protected function arraySearchBeginsWith($needle, $haystack)
{
for ($i = 0; $i < count($haystack); ++$i) {
if (substr($haystack[$i], 0, strlen($needle)) == $needle) {
return $i;
}
}
return false;
}
public function shutdown()
{
$error = error_get_last();
if (!is_array($error)) {
return;
}
$this->writeln(sprintf("<error>ERROR: %s \nin %s:%d\n</error>", $error['message'], $error['file'], $error['line']));
}
/**
* This is just a proxy error handler that checks the current error_reporting level.
* In case error_reporting is disabled the error is marked as handled, otherwise
* the normal internal error handling resumes.
*
* @return bool
*/
public function handleError()
{
if (error_reporting() === 0) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,482 @@
<?php
namespace Robo\Task\ApiGen;
use Robo\Contract\CommandInterface;
use Robo\Exception\TaskException;
use Robo\Task\BaseTask;
use Traversable;
/**
* Executes ApiGen command to generate documentation
*
* ``` php
* <?php
* // ApiGen Command
* $this->taskApiGen('./apigen.neon')
* ->templateConfig('vendor/apigen/apigen/templates/bootstrap/config.neon')
* ->wipeout(true)
* ->run();
* ?>
* ```
*/
class ApiGen extends BaseTask implements CommandInterface
{
use \Robo\Common\ExecOneCommand;
const BOOL_NO = 'no';
const BOOL_YES = 'yes';
/**
* @var string
*/
protected $command;
/**
* @param null|string $pathToApiGen
*
* @throws \Robo\Exception\TaskException
*/
public function __construct($pathToApiGen = null)
{
$this->command = $pathToApiGen;
if (!$this->command) {
$this->command = $this->findExecutablePhar('apigen');
}
if (!$this->command) {
throw new TaskException(__CLASS__, "No apigen installation found");
}
}
/**
* @param array|Traversable|string $arg a single object or something traversable
*
* @return array|Traversable the provided argument if it was already traversable, or the given
* argument returned as a one-element array
*/
protected static function forceTraversable($arg)
{
$traversable = $arg;
if (!is_array($traversable) && !($traversable instanceof \Traversable)) {
$traversable = array($traversable);
}
return $traversable;
}
/**
* @param array|string $arg a single argument or an array of multiple string values
*
* @return string a comma-separated string of all of the provided arguments, suitable
* as a command-line "list" type argument for ApiGen
*/
protected static function asList($arg)
{
$normalized = is_array($arg) ? $arg : array($arg);
return implode(',', $normalized);
}
/**
* @param bool|string $val an argument to be normalized
* @param string $default one of self::BOOL_YES or self::BOOK_NO if the provided
* value could not deterministically be converted to a
* yes or no value
*
* @return string the given value as a command-line "yes|no" type of argument for ApiGen,
* or the default value if none could be determined
*/
protected static function asTextBool($val, $default)
{
if ($val === self::BOOL_YES || $val === self::BOOL_NO) {
return $val;
}
if (!$val) {
return self::BOOL_NO;
}
if ($val === true) {
return self::BOOL_YES;
}
if (is_numeric($val) && $val != 0) {
return self::BOOL_YES;
}
if (strcasecmp($val[0], 'y') === 0) {
return self::BOOL_YES;
}
if (strcasecmp($val[0], 'n') === 0) {
return self::BOOL_NO;
}
// meh, good enough, let apigen sort it out
return $default;
}
/**
* @param string $config
*
* @return $this
*/
public function config($config)
{
$this->option('config', $config);
return $this;
}
/**
* @param array|string|Traversable $src one or more source values
*
* @return $this
*/
public function source($src)
{
foreach (self::forceTraversable($src) as $source) {
$this->option('source', $source);
}
return $this;
}
/**
* @param string $dest
*
* @return $this
*/
public function destination($dest)
{
$this->option('destination', $dest);
return $this;
}
/**
* @param array|string $exts one or more extensions
*
* @return $this
*/
public function extensions($exts)
{
$this->option('extensions', self::asList($exts));
return $this;
}
/**
* @param array|string $exclude one or more exclusions
*
* @return $this
*/
public function exclude($exclude)
{
foreach (self::forceTraversable($exclude) as $excl) {
$this->option('exclude', $excl);
}
return $this;
}
/**
* @param array|string|Traversable $path one or more skip-doc-path values
*
* @return $this
*/
public function skipDocPath($path)
{
foreach (self::forceTraversable($path) as $skip) {
$this->option('skip-doc-path', $skip);
}
return $this;
}
/**
* @param array|string|Traversable $prefix one or more skip-doc-prefix values
*
* @return $this
*/
public function skipDocPrefix($prefix)
{
foreach (self::forceTraversable($prefix) as $skip) {
$this->option('skip-doc-prefix', $skip);
}
return $this;
}
/**
* @param array|string $charset one or more charsets
*
* @return $this
*/
public function charset($charset)
{
$this->option('charset', self::asList($charset));
return $this;
}
/**
* @param string $name
*
* @return $this
*/
public function mainProjectNamePrefix($name)
{
$this->option('main', $name);
return $this;
}
/**
* @param string $title
*
* @return $this
*/
public function title($title)
{
$this->option('title', $title);
return $this;
}
/**
* @param string $baseUrl
*
* @return $this
*/
public function baseUrl($baseUrl)
{
$this->option('base-url', $baseUrl);
return $this;
}
/**
* @param string $id
*
* @return $this
*/
public function googleCseId($id)
{
$this->option('google-cse-id', $id);
return $this;
}
/**
* @param string $trackingCode
*
* @return $this
*/
public function googleAnalytics($trackingCode)
{
$this->option('google-analytics', $trackingCode);
return $this;
}
/**
* @param mixed $templateConfig
*
* @return $this
*/
public function templateConfig($templateConfig)
{
$this->option('template-config', $templateConfig);
return $this;
}
/**
* @param array|string $tags one or more supported html tags
*
* @return $this
*/
public function allowedHtml($tags)
{
$this->option('allowed-html', self::asList($tags));
return $this;
}
/**
* @param string $groups
*
* @return $this
*/
public function groups($groups)
{
$this->option('groups', $groups);
return $this;
}
/**
* @param array|string $types or more supported autocomplete types
*
* @return $this
*/
public function autocomplete($types)
{
$this->option('autocomplete', self::asList($types));
return $this;
}
/**
* @param array|string $levels one or more access levels
*
* @return $this
*/
public function accessLevels($levels)
{
$this->option('access-levels', self::asList($levels));
return $this;
}
/**
* @param boolean|string $internal 'yes' or true if internal, 'no' or false if not
*
* @return $this
*/
public function internal($internal)
{
$this->option('internal', self::asTextBool($internal, self::BOOL_NO));
return $this;
}
/**
* @param boolean|string $php 'yes' or true to generate documentation for internal php classes,
* 'no' or false otherwise
*
* @return $this
*/
public function php($php)
{
$this->option('php', self::asTextBool($php, self::BOOL_YES));
return $this;
}
/**
* @param bool|string $tree 'yes' or true to generate a tree view of classes, 'no' or false otherwise
*
* @return $this
*/
public function tree($tree)
{
$this->option('tree', self::asTextBool($tree, self::BOOL_YES));
return $this;
}
/**
* @param bool|string $dep 'yes' or true to generate documentation for deprecated classes, 'no' or false otherwise
*
* @return $this
*/
public function deprecated($dep)
{
$this->option('deprecated', self::asTextBool($dep, self::BOOL_NO));
return $this;
}
/**
* @param bool|string $todo 'yes' or true to document tasks, 'no' or false otherwise
*
* @return $this
*/
public function todo($todo)
{
$this->option('todo', self::asTextBool($todo, self::BOOL_NO));
return $this;
}
/**
* @param bool|string $src 'yes' or true to generate highlighted source code, 'no' or false otherwise
*
* @return $this
*/
public function sourceCode($src)
{
$this->option('source-code', self::asTextBool($src, self::BOOL_YES));
return $this;
}
/**
* @param bool|string $zipped 'yes' or true to generate downloadable documentation, 'no' or false otherwise
*
* @return $this
*/
public function download($zipped)
{
$this->option('download', self::asTextBool($zipped, self::BOOL_NO));
return $this;
}
public function report($path)
{
$this->option('report', $path);
return $this;
}
/**
* @param bool|string $wipeout 'yes' or true to clear out the destination directory, 'no' or false otherwise
*
* @return $this
*/
public function wipeout($wipeout)
{
$this->option('wipeout', self::asTextBool($wipeout, self::BOOL_YES));
return $this;
}
/**
* @param bool|string $quiet 'yes' or true for quiet, 'no' or false otherwise
*
* @return $this
*/
public function quiet($quiet)
{
$this->option('quiet', self::asTextBool($quiet, self::BOOL_NO));
return $this;
}
/**
* @param bool|string $bar 'yes' or true to display a progress bar, 'no' or false otherwise
*
* @return $this
*/
public function progressbar($bar)
{
$this->option('progressbar', self::asTextBool($bar, self::BOOL_YES));
return $this;
}
/**
* @param bool|string $colors 'yes' or true colorize the output, 'no' or false otherwise
*
* @return $this
*/
public function colors($colors)
{
$this->option('colors', self::asTextBool($colors, self::BOOL_YES));
return $this;
}
/**
* @param bool|string $check 'yes' or true to check for updates, 'no' or false otherwise
*
* @return $this
*/
public function updateCheck($check)
{
$this->option('update-check', self::asTextBool($check, self::BOOL_YES));
return $this;
}
/**
* @param bool|string $debug 'yes' or true to enable debug mode, 'no' or false otherwise
*
* @return $this
*/
public function debug($debug)
{
$this->option('debug', self::asTextBool($debug, self::BOOL_NO));
return $this;
}
/**
* {@inheritdoc}
*/
public function getCommand()
{
return $this->command . $this->arguments;
}
/**
* {@inheritdoc}
*/
public function run()
{
$this->printTaskInfo('Running ApiGen {args}', ['args' => $this->arguments]);
return $this->executeCommand($this->getCommand());
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Robo\Task\ApiGen;
trait loadTasks
{
/**
* @param null|string $pathToApiGen
*
* @return \Robo\Task\ApiGen\ApiGen
*/
protected function taskApiGen($pathToApiGen = null)
{
return $this->task(ApiGen::class, $pathToApiGen);
}
}

View File

@@ -0,0 +1,281 @@
<?php
namespace Robo\Task\Archive;
use Robo\Result;
use Robo\Task\BaseTask;
use Robo\Task\Filesystem\FilesystemStack;
use Robo\Task\Filesystem\DeleteDir;
use Robo\Contract\BuilderAwareInterface;
use Robo\Common\BuilderAwareTrait;
/**
* Extracts an archive.
*
* Note that often, distributions are packaged in tar or zip archives
* where the topmost folder may contain variable information, such as
* the release date, or the version of the package. This information
* is very useful when unpacking by hand, but arbitrarily-named directories
* are much less useful to scripts. Therefore, by default, Extract will
* remove the top-level directory, and instead store all extracted files
* into the directory specified by $archivePath.
*
* To keep the top-level directory when extracting, use
* `preserveTopDirectory(true)`.
*
* ``` php
* <?php
* $this->taskExtract($archivePath)
* ->to($destination)
* ->preserveTopDirectory(false) // the default
* ->run();
* ?>
* ```
*
* @method to(string) location to store extracted files
*/
class Extract extends BaseTask implements BuilderAwareInterface
{
use BuilderAwareTrait;
/**
* @var string
*/
protected $filename;
/**
* @var string
*/
protected $to;
/**
* @var bool
*/
private $preserveTopDirectory = false;
/**
* @param string $filename
*/
public function __construct($filename)
{
$this->filename = $filename;
}
/**
* Location to store extracted files.
*
* @param string $to
*
* @return $this
*/
public function to($to)
{
$this->to = $to;
return $this;
}
/**
* @param bool $preserve
*
* @return $this
*/
public function preserveTopDirectory($preserve = true)
{
$this->preserveTopDirectory = $preserve;
return $this;
}
/**
* {@inheritdoc}
*/
public function run()
{
if (!file_exists($this->filename)) {
$this->printTaskError("File {filename} does not exist", ['filename' => $this->filename]);
return false;
}
if (!($mimetype = static::archiveType($this->filename))) {
$this->printTaskError("Could not determine type of archive for {filename}", ['filename' => $this->filename]);
return false;
}
// We will first extract to $extractLocation and then move to $this->to
$extractLocation = static::getTmpDir();
@mkdir($extractLocation);
@mkdir(dirname($this->to));
$this->startTimer();
$this->printTaskInfo("Extracting {filename}", ['filename' => $this->filename]);
$result = $this->extractAppropriateType($mimetype, $extractLocation);
if ($result->wasSuccessful()) {
$this->printTaskInfo("{filename} extracted", ['filename' => $this->filename]);
// Now, we want to move the extracted files to $this->to. There
// are two possibilities that we must consider:
//
// (1) Archived files were encapsulated in a folder with an arbitrary name
// (2) There was no encapsulating folder, and all the files in the archive
// were extracted into $extractLocation
//
// In the case of (1), we want to move and rename the encapsulating folder
// to $this->to.
//
// In the case of (2), we will just move and rename $extractLocation.
$filesInExtractLocation = glob("$extractLocation/*");
$hasEncapsulatingFolder = ((count($filesInExtractLocation) == 1) && is_dir($filesInExtractLocation[0]));
if ($hasEncapsulatingFolder && !$this->preserveTopDirectory) {
$result = (new FilesystemStack())
->inflect($this)
->rename($filesInExtractLocation[0], $this->to)
->run();
(new DeleteDir($extractLocation))
->inflect($this)
->run();
} else {
$result = (new FilesystemStack())
->inflect($this)
->rename($extractLocation, $this->to)
->run();
}
}
$this->stopTimer();
$result['time'] = $this->getExecutionTime();
return $result;
}
/**
* @param string $mimetype
* @param string $extractLocation
*
* @return \Robo\Result
*/
protected function extractAppropriateType($mimetype, $extractLocation)
{
// Perform the extraction of a zip file.
if (($mimetype == 'application/zip') || ($mimetype == 'application/x-zip')) {
return $this->extractZip($extractLocation);
}
return $this->extractTar($extractLocation);
}
/**
* @param string $extractLocation
*
* @return \Robo\Result
*/
protected function extractZip($extractLocation)
{
if (!extension_loaded('zlib')) {
return Result::errorMissingExtension($this, 'zlib', 'zip extracting');
}
$zip = new \ZipArchive();
if (($status = $zip->open($this->filename)) !== true) {
return Result::error($this, "Could not open zip archive {$this->filename}");
}
if (!$zip->extractTo($extractLocation)) {
return Result::error($this, "Could not extract zip archive {$this->filename}");
}
$zip->close();
return Result::success($this);
}
/**
* @param string $extractLocation
*
* @return \Robo\Result
*/
protected function extractTar($extractLocation)
{
if (!class_exists('Archive_Tar')) {
return Result::errorMissingPackage($this, 'Archive_Tar', 'pear/archive_tar');
}
$tar_object = new \Archive_Tar($this->filename);
if (!$tar_object->extract($extractLocation)) {
return Result::error($this, "Could not extract tar archive {$this->filename}");
}
return Result::success($this);
}
/**
* @param string $filename
*
* @return bool|string
*/
protected static function archiveType($filename)
{
$content_type = false;
if (class_exists('finfo')) {
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$content_type = $finfo->file($filename);
// If finfo cannot determine the content type, then we will try other methods
if ($content_type == 'application/octet-stream') {
$content_type = false;
}
}
// Examing the file's magic header bytes.
if (!$content_type) {
if ($file = fopen($filename, 'rb')) {
$first = fread($file, 2);
fclose($file);
if ($first !== false) {
// Interpret the two bytes as a little endian 16-bit unsigned int.
$data = unpack('v', $first);
switch ($data[1]) {
case 0x8b1f:
// First two bytes of gzip files are 0x1f, 0x8b (little-endian).
// See http://www.gzip.org/zlib/rfc-gzip.html#header-trailer
$content_type = 'application/x-gzip';
break;
case 0x4b50:
// First two bytes of zip files are 0x50, 0x4b ('PK') (little-endian).
// See http://en.wikipedia.org/wiki/Zip_(file_format)#File_headers
$content_type = 'application/zip';
break;
case 0x5a42:
// First two bytes of bzip2 files are 0x5a, 0x42 ('BZ') (big-endian).
// See http://en.wikipedia.org/wiki/Bzip2#File_format
$content_type = 'application/x-bzip2';
break;
}
}
}
}
// 3. Lastly if above methods didn't work, try to guess the mime type from
// the file extension. This is useful if the file has no identificable magic
// header bytes (for example tarballs).
if (!$content_type) {
// Remove querystring from the filename, if present.
$filename = basename(current(explode('?', $filename, 2)));
$extension_mimetype = array(
'.tar.gz' => 'application/x-gzip',
'.tgz' => 'application/x-gzip',
'.tar' => 'application/x-tar',
);
foreach ($extension_mimetype as $extension => $ct) {
if (substr($filename, -strlen($extension)) === $extension) {
$content_type = $ct;
break;
}
}
}
return $content_type;
}
/**
* @return string
*/
protected static function getTmpDir()
{
return getcwd().'/tmp'.rand().time();
}
}

View File

@@ -0,0 +1,257 @@
<?php
namespace Robo\Task\Archive;
use Robo\Contract\PrintedInterface;
use Robo\Result;
use Robo\Task\BaseTask;
use Symfony\Component\Finder\Finder;
/**
* Creates a zip or tar archive.
*
* ``` php
* <?php
* $this->taskPack(
* <archiveFile>)
* ->add('README') // Puts file 'README' in archive at the root
* ->add('project') // Puts entire contents of directory 'project' in archinve inside 'project'
* ->addFile('dir/file.txt', 'file.txt') // Takes 'file.txt' from cwd and puts it in archive inside 'dir'.
* ->run();
* ?>
* ```
*/
class Pack extends BaseTask implements PrintedInterface
{
/**
* The list of items to be packed into the archive.
*
* @var array
*/
private $items = [];
/**
* The full path to the archive to be created.
*
* @var string
*/
private $archiveFile;
/**
* Construct the class.
*
* @param string $archiveFile The full path and name of the archive file to create.
*
* @since 1.0
*/
public function __construct($archiveFile)
{
$this->archiveFile = $archiveFile;
}
/**
* Satisfy the parent requirement.
*
* @return bool Always returns true.
*
* @since 1.0
*/
public function getPrinted()
{
return true;
}
/**
* @param string $archiveFile
*
* @return $this
*/
public function archiveFile($archiveFile)
{
$this->archiveFile = $archiveFile;
return $this;
}
/**
* Add an item to the archive. Like file_exists(), the parameter
* may be a file or a directory.
*
* @var string
* Relative path and name of item to store in archive
* @var string
* Absolute or relative path to file or directory's location in filesystem
*
* @return $this
*/
public function addFile($placementLocation, $filesystemLocation)
{
$this->items[$placementLocation] = $filesystemLocation;
return $this;
}
/**
* Alias for addFile, in case anyone has angst about using
* addFile with a directory.
*
* @var string
* Relative path and name of directory to store in archive
* @var string
* Absolute or relative path to directory or directory's location in filesystem
*
* @return $this
*/
public function addDir($placementLocation, $filesystemLocation)
{
$this->addFile($placementLocation, $filesystemLocation);
return $this;
}
/**
* Add a file or directory, or list of same to the archive.
*
* @var string|array
* If given a string, should contain the relative filesystem path to the
* the item to store in archive; this will also be used as the item's
* path in the archive, so absolute paths should not be used here.
* If given an array, the key of each item should be the path to store
* in the archive, and the value should be the filesystem path to the
* item to store.
* @return $this
*/
public function add($item)
{
if (is_array($item)) {
$this->items = array_merge($this->items, $item);
} else {
$this->addFile($item, $item);
}
return $this;
}
/**
* Create a zip archive for distribution.
*
* @return \Robo\Result
*
* @since 1.0
*/
public function run()
{
$this->startTimer();
// Use the file extension to determine what kind of archive to create.
$fileInfo = new \SplFileInfo($this->archiveFile);
$extension = strtolower($fileInfo->getExtension());
if (empty($extension)) {
return Result::error($this, "Archive filename must use an extension (e.g. '.zip') to specify the kind of archive to create.");
}
try {
// Inform the user which archive we are creating
$this->printTaskInfo("Creating archive {filename}", ['filename' => $this->archiveFile]);
if ($extension == 'zip') {
$result = $this->archiveZip($this->archiveFile, $this->items);
} else {
$result = $this->archiveTar($this->archiveFile, $this->items);
}
$this->printTaskSuccess("{filename} created.", ['filename' => $this->archiveFile]);
} catch (\Exception $e) {
$this->printTaskError("Could not create {filename}. {exception}", ['filename' => $this->archiveFile, 'exception' => $e->getMessage(), '_style' => ['exception' => '']]);
$result = Result::error($this, sprintf('Could not create %s. %s', $this->archiveFile, $e->getMessage()));
}
$this->stopTimer();
$result['time'] = $this->getExecutionTime();
return $result;
}
/**
* @param string $archiveFile
* @param array $items
*
* @return \Robo\Result
*/
protected function archiveTar($archiveFile, $items)
{
if (!class_exists('Archive_Tar')) {
return Result::errorMissingPackage($this, 'Archive_Tar', 'pear/archive_tar');
}
$tar_object = new \Archive_Tar($archiveFile);
foreach ($items as $placementLocation => $filesystemLocation) {
$p_remove_dir = $filesystemLocation;
$p_add_dir = $placementLocation;
if (is_file($filesystemLocation)) {
$p_remove_dir = dirname($filesystemLocation);
$p_add_dir = dirname($placementLocation);
if (basename($filesystemLocation) != basename($placementLocation)) {
return Result::error($this, "Tar archiver does not support renaming files during extraction; could not add $filesystemLocation as $placementLocation.");
}
}
if (!$tar_object->addModify([$filesystemLocation], $p_add_dir, $p_remove_dir)) {
return Result::error($this, "Could not add $filesystemLocation to the archive.");
}
}
return Result::success($this);
}
/**
* @param string $archiveFile
* @param array $items
*
* @return \Robo\Result
*/
protected function archiveZip($archiveFile, $items)
{
if (!extension_loaded('zlib')) {
return Result::errorMissingExtension($this, 'zlib', 'zip packing');
}
$zip = new \ZipArchive($archiveFile, \ZipArchive::CREATE);
if (!$zip->open($archiveFile, \ZipArchive::CREATE)) {
return Result::error($this, "Could not create zip archive {$archiveFile}");
}
$result = $this->addItemsToZip($zip, $items);
$zip->close();
return $result;
}
/**
* @param \ZipArchive $zip
* @param array $items
*
* @return \Robo\Result
*/
protected function addItemsToZip($zip, $items)
{
foreach ($items as $placementLocation => $filesystemLocation) {
if (is_dir($filesystemLocation)) {
$finder = new Finder();
$finder->files()->in($filesystemLocation)->ignoreDotFiles(false);
foreach ($finder as $file) {
// Replace Windows slashes or resulting zip will have issues on *nixes.
$relativePathname = str_replace('\\', '/', $file->getRelativePathname());
if (!$zip->addFile($file->getRealpath(), "{$placementLocation}/{$relativePathname}")) {
return Result::error($this, "Could not add directory $filesystemLocation to the archive; error adding {$file->getRealpath()}.");
}
}
} elseif (is_file($filesystemLocation)) {
if (!$zip->addFile($filesystemLocation, $placementLocation)) {
return Result::error($this, "Could not add file $filesystemLocation to the archive.");
}
} else {
return Result::error($this, "Could not find $filesystemLocation for the archive.");
}
}
return Result::success($this);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Robo\Task\Archive;
trait loadTasks
{
/**
* @param $filename
*
* @return Pack
*/
protected function taskPack($filename)
{
return $this->task(Pack::class, $filename);
}
/**
* @param $filename
*
* @return Extract
*/
protected function taskExtract($filename)
{
return $this->task(Extract::class, $filename);
}
}

View File

@@ -0,0 +1,214 @@
<?php
namespace Robo\Task\Assets;
use Robo\Result;
use Robo\Task\BaseTask;
abstract class CssPreprocessor extends BaseTask
{
const FORMAT_NAME = '';
/**
* Default compiler to use.
*
* @var string
*/
protected $compiler;
/**
* Available compilers list
*
* @var string[]
*/
protected $compilers = [];
/**
* Compiler options.
*
* @var array
*/
protected $compilerOptions = [];
/**
* @var array
*/
protected $files = [];
/**
* Constructor. Accepts array of file paths.
*
* @param array $input
*/
public function __construct(array $input)
{
$this->files = $input;
$this->setDefaultCompiler();
}
protected function setDefaultCompiler()
{
if (isset($this->compilers[0])) {
//set first compiler as default
$this->compiler = $this->compilers[0];
}
}
/**
* Sets import directories
* Alias for setImportPaths
* @see CssPreprocessor::setImportPaths
*
* @param array|string $dirs
*
* @return $this
*/
public function importDir($dirs)
{
return $this->setImportPaths($dirs);
}
/**
* Adds import directory
*
* @param string $dir
*
* @return $this
*/
public function addImportPath($dir)
{
if (!isset($this->compilerOptions['importDirs'])) {
$this->compilerOptions['importDirs'] = [];
}
if (!in_array($dir, $this->compilerOptions['importDirs'], true)) {
$this->compilerOptions['importDirs'][] = $dir;
}
return $this;
}
/**
* Sets import directories
*
* @param array|string $dirs
*
* @return $this
*/
public function setImportPaths($dirs)
{
if (!is_array($dirs)) {
$dirs = [$dirs];
}
$this->compilerOptions['importDirs'] = $dirs;
return $this;
}
/**
* @param string $formatterName
*
* @return $this
*/
public function setFormatter($formatterName)
{
$this->compilerOptions['formatter'] = $formatterName;
return $this;
}
/**
* Sets the compiler.
*
* @param string $compiler
* @param array $options
*
* @return $this
*/
public function compiler($compiler, array $options = [])
{
$this->compiler = $compiler;
$this->compilerOptions = array_merge($this->compilerOptions, $options);
return $this;
}
/**
* Compiles file
*
* @param $file
*
* @return bool|mixed
*/
protected function compile($file)
{
if (is_callable($this->compiler)) {
return call_user_func($this->compiler, $file, $this->compilerOptions);
}
if (method_exists($this, $this->compiler)) {
return $this->{$this->compiler}($file);
}
return false;
}
/**
* {@inheritdoc}
*/
public function run()
{
if (!in_array($this->compiler, $this->compilers, true)
&& !is_callable($this->compiler)
) {
$message = sprintf('Invalid ' . static::FORMAT_NAME . ' compiler %s!', $this->compiler);
return Result::error($this, $message);
}
foreach ($this->files as $in => $out) {
if (!file_exists($in)) {
$message = sprintf('File %s not found.', $in);
return Result::error($this, $message);
}
if (file_exists($out) && !is_writable($out)) {
return Result::error($this, 'Destination already exists and cannot be overwritten.');
}
}
foreach ($this->files as $in => $out) {
$css = $this->compile($in);
if ($css instanceof Result) {
return $css;
} elseif (false === $css) {
$message = sprintf(
ucfirst(static::FORMAT_NAME) . ' compilation failed for %s.',
$in
);
return Result::error($this, $message);
}
$dst = $out . '.part';
$write_result = file_put_contents($dst, $css);
if (false === $write_result) {
$message = sprintf('File write failed: %s', $out);
@unlink($dst);
return Result::error($this, $message);
}
// Cannot be cross-volume: should always succeed
@rename($dst, $out);
$this->printTaskSuccess('Wrote CSS to {filename}', ['filename' => $out]);
}
return Result::success($this, 'All ' . static::FORMAT_NAME . ' files compiled.');
}
}

View File

@@ -0,0 +1,716 @@
<?php
namespace Robo\Task\Assets;
use Robo\Result;
use Robo\Exception\TaskException;
use Robo\Task\BaseTask;
use Robo\Task\Base\Exec;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Filesystem\Filesystem as sfFilesystem;
/**
* Minifies images. When the required minifier is not installed on the system
* the task will try to download it from the [imagemin](https://github.com/imagemin) repository.
*
* When the task is run without any specified minifier it will compress the images
* based on the extension.
*
* ```php
* $this->taskImageMinify('assets/images/*')
* ->to('dist/images/')
* ->run();
* ```
*
* This will use the following minifiers:
*
* - PNG: optipng
* - GIF: gifsicle
* - JPG, JPEG: jpegtran
* - SVG: svgo
*
* When the minifier is specified the task will use that for all the input files. In that case
* it is useful to filter the files with the extension:
*
* ```php
* $this->taskImageMinify('assets/images/*.png')
* ->to('dist/images/')
* ->minifier('pngcrush');
* ->run();
* ```
*
* The task supports the following minifiers:
*
* - optipng
* - pngquant
* - advpng
* - pngout
* - zopflipng
* - pngcrush
* - gifsicle
* - jpegoptim
* - jpeg-recompress
* - jpegtran
* - svgo (only minification, no downloading)
*
* You can also specifiy extra options for the minifiers:
*
* ```php
* $this->taskImageMinify('assets/images/*.jpg')
* ->to('dist/images/')
* ->minifier('jpegtran', ['-progressive' => null, '-copy' => 'none'])
* ->run();
* ```
*
* This will execute as:
* `jpegtran -copy none -progressive -optimize -outfile "dist/images/test.jpg" "/var/www/test/assets/images/test.jpg"`
*/
class ImageMinify extends BaseTask
{
/**
* Destination directory for the minified images.
*
* @var string
*/
protected $to;
/**
* Array of the source files.
*
* @var array
*/
protected $dirs = [];
/**
* Symfony 2 filesystem.
*
* @var sfFilesystem
*/
protected $fs;
/**
* Target directory for the downloaded binary executables.
*
* @var string
*/
protected $executableTargetDir;
/**
* Array for the downloaded binary executables.
*
* @var array
*/
protected $executablePaths = [];
/**
* Array for the individual results of all the files.
*
* @var array
*/
protected $results = [];
/**
* Default minifier to use.
*
* @var string
*/
protected $minifier;
/**
* Array for minifier options.
*
* @var array
*/
protected $minifierOptions = [];
/**
* Supported minifiers.
*
* @var array
*/
protected $minifiers = [
// Default 4
'optipng',
'gifsicle',
'jpegtran',
'svgo',
// PNG
'pngquant',
'advpng',
'pngout',
'zopflipng',
'pngcrush',
// JPG
'jpegoptim',
'jpeg-recompress',
];
/**
* Binary repositories of Imagemin.
*
* @link https://github.com/imagemin
*
* @var array
*/
protected $imageminRepos = [
// PNG
'optipng' => 'https://github.com/imagemin/optipng-bin',
'pngquant' => 'https://github.com/imagemin/pngquant-bin',
'advpng' => 'https://github.com/imagemin/advpng-bin',
'pngout' => 'https://github.com/imagemin/pngout-bin',
'zopflipng' => 'https://github.com/imagemin/zopflipng-bin',
'pngcrush' => 'https://github.com/imagemin/pngcrush-bin',
// Gif
'gifsicle' => 'https://github.com/imagemin/gifsicle-bin',
// JPG
'jpegtran' => 'https://github.com/imagemin/jpegtran-bin',
'jpegoptim' => 'https://github.com/imagemin/jpegoptim-bin',
'cjpeg' => 'https://github.com/imagemin/mozjpeg-bin', // note: we do not support this minifier because it creates JPG from non-JPG files
'jpeg-recompress' => 'https://github.com/imagemin/jpeg-recompress-bin',
// WebP
'cwebp' => 'https://github.com/imagemin/cwebp-bin', // note: we do not support this minifier because it creates WebP from non-WebP files
];
public function __construct($dirs)
{
is_array($dirs)
? $this->dirs = $dirs
: $this->dirs[] = $dirs;
$this->fs = new sfFilesystem();
// guess the best path for the executables based on __DIR__
if (($pos = strpos(__DIR__, 'consolidation/robo')) !== false) {
// the executables should be stored in vendor/bin
$this->executableTargetDir = substr(__DIR__, 0, $pos).'bin';
}
// check if the executables are already available
foreach ($this->imageminRepos as $exec => $url) {
$path = $this->executableTargetDir.'/'.$exec;
// if this is Windows add a .exe extension
if (substr($this->getOS(), 0, 3) == 'win') {
$path .= '.exe';
}
if (is_file($path)) {
$this->executablePaths[$exec] = $path;
}
}
}
/**
* {@inheritdoc}
*/
public function run()
{
// find the files
$files = $this->findFiles($this->dirs);
// minify the files
$result = $this->minify($files);
// check if there was an error
if ($result instanceof Result) {
return $result;
}
$amount = (count($files) == 1 ? 'image' : 'images');
$message = "Minified {filecount} out of {filetotal} $amount into {destination}";
$context = ['filecount' => count($this->results['success']), 'filetotal' => count($files), 'destination' => $this->to];
if (count($this->results['success']) == count($files)) {
$this->printTaskSuccess($message, $context);
return Result::success($this, $message, $context);
} else {
return Result::error($this, $message, $context);
}
}
/**
* Sets the target directory where the files will be copied to.
*
* @param string $target
*
* @return $this
*/
public function to($target)
{
$this->to = rtrim($target, '/');
return $this;
}
/**
* Sets the minifier.
*
* @param string $minifier
* @param array $options
*
* @return $this
*/
public function minifier($minifier, array $options = [])
{
$this->minifier = $minifier;
$this->minifierOptions = array_merge($this->minifierOptions, $options);
return $this;
}
/**
* @param array $dirs
*
* @return array|\Robo\Result
*
* @throws \Robo\Exception\TaskException
*/
protected function findFiles($dirs)
{
$files = array();
// find the files
foreach ($dirs as $k => $v) {
// reset finder
$finder = new Finder();
$dir = $k;
$to = $v;
// check if target was given with the to() method instead of key/value pairs
if (is_int($k)) {
$dir = $v;
if (isset($this->to)) {
$to = $this->to;
} else {
throw new TaskException($this, 'target directory is not defined');
}
}
try {
$finder->files()->in($dir);
} catch (\InvalidArgumentException $e) {
// if finder cannot handle it, try with in()->name()
if (strpos($dir, '/') === false) {
$dir = './'.$dir;
}
$parts = explode('/', $dir);
$new_dir = implode('/', array_slice($parts, 0, -1));
try {
$finder->files()->in($new_dir)->name(array_pop($parts));
} catch (\InvalidArgumentException $e) {
return Result::fromException($this, $e);
}
}
foreach ($finder as $file) {
// store the absolute path as key and target as value in the files array
$files[$file->getRealpath()] = $this->getTarget($file->getRealPath(), $to);
}
$fileNoun = count($finder) == 1 ? ' file' : ' files';
$this->printTaskInfo("Found {filecount} $fileNoun in {dir}", ['filecount' => count($finder), 'dir' => $dir]);
}
return $files;
}
/**
* @param string $file
* @param string $to
*
* @return string
*/
protected function getTarget($file, $to)
{
$target = $to.'/'.basename($file);
return $target;
}
/**
* @param array $files
*
* @return \Robo\Result
*/
protected function minify($files)
{
// store the individual results into the results array
$this->results = [
'success' => [],
'error' => [],
];
// loop through the files
foreach ($files as $from => $to) {
if (!isset($this->minifier)) {
// check filetype based on the extension
$extension = strtolower(pathinfo($from, PATHINFO_EXTENSION));
// set the default minifiers based on the extension
switch ($extension) {
case 'png':
$minifier = 'optipng';
break;
case 'jpg':
case 'jpeg':
$minifier = 'jpegtran';
break;
case 'gif':
$minifier = 'gifsicle';
break;
case 'svg':
$minifier = 'svgo';
break;
}
} else {
if (!in_array($this->minifier, $this->minifiers, true)
&& !is_callable(strtr($this->minifier, '-', '_'))
) {
$message = sprintf('Invalid minifier %s!', $this->minifier);
return Result::error($this, $message);
}
$minifier = $this->minifier;
}
// Convert minifier name to camelCase (e.g. jpeg-recompress)
$funcMinifier = $this->camelCase($minifier);
// call the minifier method which prepares the command
if (is_callable($funcMinifier)) {
$command = call_user_func($funcMinifier, $from, $to, $this->minifierOptions);
} elseif (method_exists($this, $funcMinifier)) {
$command = $this->{$funcMinifier}($from, $to);
} else {
$message = sprintf('Minifier method <info>%s</info> cannot be found!', $funcMinifier);
return Result::error($this, $message);
}
// launch the command
$this->printTaskInfo('Minifying {filepath} with {minifier}', ['filepath' => $from, 'minifier' => $minifier]);
$result = $this->executeCommand($command);
// check the return code
if ($result->getExitCode() == 127) {
$this->printTaskError('The {minifier} executable cannot be found', ['minifier' => $minifier]);
// try to install from imagemin repository
if (array_key_exists($minifier, $this->imageminRepos)) {
$result = $this->installFromImagemin($minifier);
if ($result instanceof Result) {
if ($result->wasSuccessful()) {
$this->printTaskSuccess($result->getMessage());
// retry the conversion with the downloaded executable
if (is_callable($minifier)) {
$command = call_user_func($minifier, $from, $to, $minifierOptions);
} elseif (method_exists($this, $minifier)) {
$command = $this->{$minifier}($from, $to);
}
// launch the command
$this->printTaskInfo('Minifying {filepath} with {minifier}', ['filepath' => $from, 'minifier' => $minifier]);
$result = $this->executeCommand($command);
} else {
$this->printTaskError($result->getMessage());
// the download was not successful
return $result;
}
}
} else {
return $result;
}
}
// check the success of the conversion
if ($result->getExitCode() !== 0) {
$this->results['error'][] = $from;
} else {
$this->results['success'][] = $from;
}
}
}
/**
* @return string
*/
protected function getOS()
{
$os = php_uname('s');
$os .= '/'.php_uname('m');
// replace x86_64 to x64, because the imagemin repo uses that
$os = str_replace('x86_64', 'x64', $os);
// replace i386, i686, etc to x86, because of imagemin
$os = preg_replace('/i[0-9]86/', 'x86', $os);
// turn info to lowercase, because of imagemin
$os = strtolower($os);
return $os;
}
/**
* @param string $command
*
* @return \Robo\Result
*/
protected function executeCommand($command)
{
// insert the options into the command
$a = explode(' ', $command);
$executable = array_shift($a);
foreach ($this->minifierOptions as $key => $value) {
// first prepend the value
if (!empty($value)) {
array_unshift($a, $value);
}
// then add the key
if (!is_numeric($key)) {
array_unshift($a, $key);
}
}
// check if the executable can be replaced with the downloaded one
if (array_key_exists($executable, $this->executablePaths)) {
$executable = $this->executablePaths[$executable];
}
array_unshift($a, $executable);
$command = implode(' ', $a);
// execute the command
$exec = new Exec($command);
return $exec->inflect($this)->printed(false)->run();
}
/**
* @param string $executable
*
* @return \Robo\Result
*/
protected function installFromImagemin($executable)
{
// check if there is an url defined for the executable
if (!array_key_exists($executable, $this->imageminRepos)) {
$message = sprintf('The executable %s cannot be found in the defined imagemin repositories', $executable);
return Result::error($this, $message);
}
$this->printTaskInfo('Downloading the {executable} executable from the imagemin repository', ['executable' => $executable]);
$os = $this->getOS();
$url = $this->imageminRepos[$executable].'/blob/master/vendor/'.$os.'/'.$executable.'?raw=true';
if (substr($os, 0, 3) == 'win') {
// if it is win, add a .exe extension
$url = $this->imageminRepos[$executable].'/blob/master/vendor/'.$os.'/'.$executable.'.exe?raw=true';
}
$data = @file_get_contents($url, false, null);
if ($data === false) {
// there is something wrong with the url, try it without the version info
$url = preg_replace('/x[68][64]\//', '', $url);
$data = @file_get_contents($url, false, null);
if ($data === false) {
// there is still something wrong with the url if it is win, try with win32
if (substr($os, 0, 3) == 'win') {
$url = preg_replace('win/', 'win32/', $url);
$data = @file_get_contents($url, false, null);
if ($data === false) {
// there is nothing more we can do
$message = sprintf('Could not download the executable <info>%s</info>', $executable);
return Result::error($this, $message);
}
}
// if it is not windows there is nothing we can do
$message = sprintf('Could not download the executable <info>%s</info>', $executable);
return Result::error($this, $message);
}
}
// check if target directory exists
if (!is_dir($this->executableTargetDir)) {
mkdir($this->executableTargetDir);
}
// save the executable into the target dir
$path = $this->executableTargetDir.'/'.$executable;
if (substr($os, 0, 3) == 'win') {
// if it is win, add a .exe extension
$path = $this->executableTargetDir.'/'.$executable.'.exe';
}
$result = file_put_contents($path, $data);
if ($result === false) {
$message = sprintf('Could not copy the executable <info>%s</info> to %s', $executable, $target_dir);
return Result::error($this, $message);
}
// set the binary to executable
chmod($path, 0755);
// if everything successful, store the executable path
$this->executablePaths[$executable] = $this->executableTargetDir.'/'.$executable;
// if it is win, add a .exe extension
if (substr($os, 0, 3) == 'win') {
$this->executablePaths[$executable] .= '.exe';
}
$message = sprintf('Executable <info>%s</info> successfully downloaded', $executable);
return Result::success($this, $message);
}
/**
* @param string $from
* @param string $to
*
* @return string
*/
protected function optipng($from, $to)
{
$command = sprintf('optipng -quiet -out "%s" -- "%s"', $to, $from);
if ($from != $to && is_file($to)) {
// earlier versions of optipng do not overwrite the target without a backup
// http://sourceforge.net/p/optipng/bugs/37/
unlink($to);
}
return $command;
}
/**
* @param string $from
* @param string $to
*
* @return string
*/
protected function jpegtran($from, $to)
{
$command = sprintf('jpegtran -optimize -outfile "%s" "%s"', $to, $from);
return $command;
}
protected function gifsicle($from, $to)
{
$command = sprintf('gifsicle -o "%s" "%s"', $to, $from);
return $command;
}
/**
* @param string $from
* @param string $to
*
* @return string
*/
protected function svgo($from, $to)
{
$command = sprintf('svgo "%s" "%s"', $from, $to);
return $command;
}
/**
* @param string $from
* @param string $to
*
* @return string
*/
protected function pngquant($from, $to)
{
$command = sprintf('pngquant --force --output "%s" "%s"', $to, $from);
return $command;
}
/**
* @param string $from
* @param string $to
*
* @return string
*/
protected function advpng($from, $to)
{
// advpng does not have any output parameters, copy the file and then compress the copy
$command = sprintf('advpng --recompress --quiet "%s"', $to);
$this->fs->copy($from, $to, true);
return $command;
}
/**
* @param string $from
* @param string $to
*
* @return string
*/
protected function pngout($from, $to)
{
$command = sprintf('pngout -y -q "%s" "%s"', $from, $to);
return $command;
}
/**
* @param string $from
* @param string $to
*
* @return string
*/
protected function zopflipng($from, $to)
{
$command = sprintf('zopflipng -y "%s" "%s"', $from, $to);
return $command;
}
/**
* @param string $from
* @param string $to
*
* @return string
*/
protected function pngcrush($from, $to)
{
$command = sprintf('pngcrush -q -ow "%s" "%s"', $from, $to);
return $command;
}
/**
* @param string $from
* @param string $to
*
* @return string
*/
protected function jpegoptim($from, $to)
{
// jpegoptim only takes the destination directory as an argument
$command = sprintf('jpegoptim --quiet -o --dest "%s" "%s"', dirname($to), $from);
return $command;
}
/**
* @param string $from
* @param string $to
*
* @return string
*/
protected function jpegRecompress($from, $to)
{
$command = sprintf('jpeg-recompress --quiet "%s" "%s"', $from, $to);
return $command;
}
/**
* @param string $text
*
* @return string
*/
public static function camelCase($text)
{
// non-alpha and non-numeric characters become spaces
$text = preg_replace('/[^a-z0-9]+/i', ' ', $text);
$text = trim($text);
// uppercase the first character of each word
$text = ucwords($text);
$text = str_replace(" ", "", $text);
$text = lcfirst($text);
return $text;
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Robo\Task\Assets;
use Robo\Result;
/**
* Compiles less files.
*
* ```php
* <?php
* $this->taskLess([
* 'less/default.less' => 'css/default.css'
* ])
* ->run();
* ?>
* ```
*
* Use one of both less compilers in your project:
*
* ```
* "leafo/lessphp": "~0.5",
* "oyejorge/less.php": "~1.5"
* ```
*
* Specify directory (string or array) for less imports lookup:
*
* ```php
* <?php
* $this->taskLess([
* 'less/default.less' => 'css/default.css'
* ])
* ->importDir('less')
* ->compiler('lessphp')
* ->run();
* ?>
* ```
*
* You can implement additional compilers by extending this task and adding a
* method named after them and overloading the lessCompilers() method to
* inject the name there.
*/
class Less extends CssPreprocessor
{
const FORMAT_NAME = 'less';
/**
* @var string[]
*/
protected $compilers = [
'less', // https://github.com/oyejorge/less.php
'lessphp', //https://github.com/leafo/lessphp
];
/**
* lessphp compiler
* @link https://github.com/leafo/lessphp
*
* @param string $file
*
* @return string
*/
protected function lessphp($file)
{
if (!class_exists('\lessc')) {
return Result::errorMissingPackage($this, 'lessc', 'leafo/lessphp');
}
$lessCode = file_get_contents($file);
$less = new \lessc();
if (isset($this->compilerOptions['importDirs'])) {
$less->setImportDir($this->compilerOptions['importDirs']);
}
return $less->compile($lessCode);
}
/**
* less compiler
* @link https://github.com/oyejorge/less.php
*
* @param string $file
*
* @return string
*/
protected function less($file)
{
if (!class_exists('\Less_Parser')) {
return Result::errorMissingPackage($this, 'Less_Parser', 'oyejorge/less.php');
}
$lessCode = file_get_contents($file);
$parser = new \Less_Parser();
$parser->SetOptions($this->compilerOptions);
if (isset($this->compilerOptions['importDirs'])) {
$importDirs = [];
foreach ($this->compilerOptions['importDirs'] as $dir) {
$importDirs[$dir] = $dir;
}
$parser->SetImportDirs($importDirs);
}
$parser->parse($lessCode);
return $parser->getCss();
}
}

View File

@@ -0,0 +1,297 @@
<?php
namespace Robo\Task\Assets;
use Robo\Result;
use Robo\Task\BaseTask;
/**
* Minifies asset file (CSS or JS).
*
* ``` php
* <?php
* $this->taskMinify( 'web/assets/theme.css' )
* ->run()
* ?>
* ```
* Please install additional dependencies to use:
*
* ```
* "patchwork/jsqueeze": "~1.0",
* "natxet/CssMin": "~3.0"
* ```
*/
class Minify extends BaseTask
{
/**
* @var array
*/
protected $types = ['css', 'js'];
/**
* @var string
*/
protected $text;
/**
* @var string
*/
protected $dst;
/**
* @var string
*/
protected $type;
/**
* @var array
*/
protected $squeezeOptions = [
'singleLine' => true,
'keepImportantComments' => true,
'specialVarRx' => false,
];
/**
* Constructor. Accepts asset file path or string source.
*
* @param string $input
*/
public function __construct($input)
{
if (file_exists($input)) {
$this->fromFile($input);
return;
}
$this->fromText($input);
}
/**
* Sets destination. Tries to guess type from it.
*
* @param string $dst
*
* @return $this
*/
public function to($dst)
{
$this->dst = $dst;
if (!empty($this->dst) && empty($this->type)) {
$this->type($this->getExtension($this->dst));
}
return $this;
}
/**
* Sets type with validation.
*
* @param string $type css|js
*
* @return $this
*/
public function type($type)
{
$type = strtolower($type);
if (in_array($type, $this->types)) {
$this->type = $type;
}
return $this;
}
/**
* Sets text from string source.
*
* @param string $text
*
* @return $this
*/
protected function fromText($text)
{
$this->text = (string)$text;
unset($this->type);
return $this;
}
/**
* Sets text from asset file path. Tries to guess type and set default destination.
*
* @param string $path
*
* @return $this
*/
protected function fromFile($path)
{
$this->text = file_get_contents($path);
unset($this->type);
$this->type($this->getExtension($path));
if (empty($this->dst) && !empty($this->type)) {
$ext_length = strlen($this->type) + 1;
$this->dst = substr($path, 0, -$ext_length) . '.min.' . $this->type;
}
return $this;
}
/**
* Gets file extension from path.
*
* @param string $path
*
* @return string
*/
protected function getExtension($path)
{
return pathinfo($path, PATHINFO_EXTENSION);
}
/**
* Minifies and returns text.
*
* @return string|bool
*/
protected function getMinifiedText()
{
switch ($this->type) {
case 'css':
if (!class_exists('\CssMin')) {
return Result::errorMissingPackage($this, 'CssMin', 'natxet/CssMin');
}
return \CssMin::minify($this->text);
break;
case 'js':
if (!class_exists('\JSqueeze') && !class_exists('\Patchwork\JSqueeze')) {
return Result::errorMissingPackage($this, 'Patchwork\JSqueeze', 'patchwork/jsqueeze');
}
if (class_exists('\JSqueeze')) {
$jsqueeze = new \JSqueeze();
} else {
$jsqueeze = new \Patchwork\JSqueeze();
}
return $jsqueeze->squeeze(
$this->text,
$this->squeezeOptions['singleLine'],
$this->squeezeOptions['keepImportantComments'],
$this->squeezeOptions['specialVarRx']
);
break;
}
return false;
}
/**
* Single line option for the JS minimisation.
*
* @param bool $singleLine
*
* @return $this
*/
public function singleLine($singleLine)
{
$this->squeezeOptions['singleLine'] = (bool)$singleLine;
return $this;
}
/**
* keepImportantComments option for the JS minimisation.
*
* @param bool $keepImportantComments
*
* @return $this
*/
public function keepImportantComments($keepImportantComments)
{
$this->squeezeOptions['keepImportantComments'] = (bool)$keepImportantComments;
return $this;
}
/**
* specialVarRx option for the JS minimisation.
*
* @param bool $specialVarRx
*
* @return $this ;
*/
public function specialVarRx($specialVarRx)
{
$this->squeezeOptions['specialVarRx'] = (bool)$specialVarRx;
return $this;
}
/**
* @return string
*/
public function __toString()
{
return (string) $this->getMinifiedText();
}
/**
* {@inheritdoc}
*/
public function run()
{
if (empty($this->type)) {
return Result::error($this, 'Unknown asset type.');
}
if (empty($this->dst)) {
return Result::error($this, 'Unknown file destination.');
}
if (file_exists($this->dst) && !is_writable($this->dst)) {
return Result::error($this, 'Destination already exists and cannot be overwritten.');
}
$size_before = strlen($this->text);
$minified = $this->getMinifiedText();
if ($minified instanceof Result) {
return $minified;
} elseif (false === $minified) {
return Result::error($this, 'Minification failed.');
}
$size_after = strlen($minified);
// Minification did not reduce file size, so use original file.
if ($size_after > $size_before) {
$minified = $this->text;
$size_after = $size_before;
}
$dst = $this->dst . '.part';
$write_result = file_put_contents($dst, $minified);
if (false === $write_result) {
@unlink($dst);
return Result::error($this, 'File write failed.');
}
// Cannot be cross-volume; should always succeed.
@rename($dst, $this->dst);
if ($size_before === 0) {
$minified_percent = 0;
} else {
$minified_percent = number_format(100 - ($size_after / $size_before * 100), 1);
}
$this->printTaskSuccess('Wrote {filepath}', ['filepath' => $this->dst]);
$context = [
'bytes' => $this->formatBytes($size_after),
'reduction' => $this->formatBytes(($size_before - $size_after)),
'percentage' => $minified_percent,
];
$this->printTaskSuccess('Wrote {bytes} (reduced by {reduction} / {percentage})', $context);
return Result::success($this, 'Asset minified.');
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Robo\Task\Assets;
use Robo\Result;
/**
* Compiles scss files.
*
* ```php
* <?php
* $this->taskScss([
* 'scss/default.scss' => 'css/default.css'
* ])
* ->importDir('assets/styles')
* ->run();
* ?>
* ```
*
* Use the following scss compiler in your project:
*
* ```
* "leafo/scssphp": "~0.1",
* ```
*
* You can implement additional compilers by extending this task and adding a
* method named after them and overloading the scssCompilers() method to
* inject the name there.
*/
class Scss extends CssPreprocessor
{
const FORMAT_NAME = 'scss';
/**
* @var string[]
*/
protected $compilers = [
'scssphp', // https://github.com/leafo/scssphp
];
/**
* scssphp compiler
* @link https://github.com/leafo/scssphp
*
* @param string $file
*
* @return string
*/
protected function scssphp($file)
{
if (!class_exists('\Leafo\ScssPhp\Compiler')) {
return Result::errorMissingPackage($this, 'scssphp', 'leafo/scssphp');
}
$scssCode = file_get_contents($file);
$scss = new \Leafo\ScssPhp\Compiler();
// set options for the scssphp compiler
if (isset($this->compilerOptions['importDirs'])) {
$scss->setImportPaths($this->compilerOptions['importDirs']);
}
if (isset($this->compilerOptions['formatter'])) {
$scss->setFormatter($this->compilerOptions['formatter']);
}
return $scss->compile($scssCode);
}
/**
* Sets the formatter for scssphp
*
* The method setFormatter($formatterName) sets the current formatter to $formatterName,
* the name of a class as a string that implements the formatting interface. See the source
* for Leafo\ScssPhp\Formatter\Expanded for an example.
*
* Five formatters are included with leafo/scssphp:
* - Leafo\ScssPhp\Formatter\Expanded
* - Leafo\ScssPhp\Formatter\Nested (default)
* - Leafo\ScssPhp\Formatter\Compressed
* - Leafo\ScssPhp\Formatter\Compact
* - Leafo\ScssPhp\Formatter\Crunched
*
* @link http://leafo.github.io/scssphp/docs/#output-formatting
*
* @param string $formatterName
*
* @return $this
*/
public function setFormatter($formatterName)
{
return parent::setFormatter($formatterName);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Robo\Task\Assets;
trait loadTasks
{
/**
* @param string $input
*
* @return \Robo\Task\Assets\Minify
*/
protected function taskMinify($input)
{
return $this->task(Minify::class, $input);
}
/**
* @param string|string[] $input
*
* @return \Robo\Task\Assets\ImageMinify
*/
protected function taskImageMinify($input)
{
return $this->task(ImageMinify::class, $input);
}
/**
* @param array $input
*
* @return \Robo\Task\Assets\Less
*/
protected function taskLess($input)
{
return $this->task(Less::class, $input);
}
/**
* @param array $input
*
* @return \Robo\Task\Assets\Scss
*/
protected function taskScss($input)
{
return $this->task(Scss::class, $input);
}
}

View File

@@ -0,0 +1,224 @@
<?php
namespace Robo\Task\Base;
use Robo\Contract\CommandInterface;
use Robo\Contract\PrintedInterface;
use Robo\Contract\SimulatedInterface;
use Robo\Task\BaseTask;
use Symfony\Component\Process\Process;
use Robo\Result;
/**
* Executes shell script. Closes it when running in background mode.
*
* ``` php
* <?php
* $this->taskExec('compass')->arg('watch')->run();
* // or use shortcut
* $this->_exec('compass watch');
*
* $this->taskExec('compass watch')->background()->run();
*
* if ($this->taskExec('phpunit .')->run()->wasSuccessful()) {
* $this->say('tests passed');
* }
*
* ?>
* ```
*/
class Exec extends BaseTask implements CommandInterface, PrintedInterface, SimulatedInterface
{
use \Robo\Common\CommandReceiver;
use \Robo\Common\ExecOneCommand;
/**
* @var static[]
*/
protected static $instances = [];
/**
* @var string|\Robo\Contract\CommandInterface
*/
protected $command;
/**
* @var bool
*/
protected $background = false;
/**
* @var null|int
*/
protected $timeout = null;
/**
* @var null|int
*/
protected $idleTimeout = null;
/**
* @var null|array
*/
protected $env = null;
/**
* @var Process
*/
protected $process;
/**
* @param string|\Robo\Contract\CommandInterface $command
*/
public function __construct($command)
{
$this->command = $this->receiveCommand($command);
}
/**
* {@inheritdoc}
*/
public function getCommand()
{
return trim($this->command . $this->arguments);
}
/**
* Executes command in background mode (asynchronously)
*
* @return $this
*/
public function background()
{
self::$instances[] = $this;
$this->background = true;
return $this;
}
/**
* Stop command if it runs longer then $timeout in seconds
*
* @param int $timeout
*
* @return $this
*/
public function timeout($timeout)
{
$this->timeout = $timeout;
return $this;
}
/**
* Stops command if it does not output something for a while
*
* @param int $timeout
*
* @return $this
*/
public function idleTimeout($timeout)
{
$this->idleTimeout = $timeout;
return $this;
}
/**
* Sets the environment variables for the command
*
* @param array $env
*
* @return $this
*/
public function env(array $env)
{
$this->env = $env;
return $this;
}
public function __destruct()
{
$this->stop();
}
protected function stop()
{
if ($this->background && $this->process->isRunning()) {
$this->process->stop();
$this->printTaskInfo("Stopped {command}", ['command' => $this->getCommand()]);
}
}
/**
* @param array $context
*/
protected function printAction($context = [])
{
$command = $this->getCommand();
$dir = $this->workingDirectory ? " in {dir}" : "";
$this->printTaskInfo("Running {command}$dir", ['command' => $command, 'dir' => $this->workingDirectory] + $context);
}
/**
* {@inheritdoc}
*/
public function run()
{
$this->printAction();
$this->process = new Process($this->getCommand());
$this->process->setTimeout($this->timeout);
$this->process->setIdleTimeout($this->idleTimeout);
$this->process->setWorkingDirectory($this->workingDirectory);
if (isset($this->env)) {
$this->process->setEnv($this->env);
}
if (!$this->background and !$this->isPrinted) {
$this->startTimer();
$this->process->run();
$this->stopTimer();
return new Result($this, $this->process->getExitCode(), $this->process->getOutput(), ['time' => $this->getExecutionTime()]);
}
if (!$this->background and $this->isPrinted) {
$this->startTimer();
$this->process->run(
function ($type, $buffer) {
$progressWasVisible = $this->hideTaskProgress();
print($buffer);
$this->showTaskProgress($progressWasVisible);
}
);
$this->stopTimer();
return new Result($this, $this->process->getExitCode(), $this->process->getOutput(), ['time' => $this->getExecutionTime()]);
}
try {
$this->process->start();
} catch (\Exception $e) {
return Result::fromException($this, $e);
}
return Result::success($this);
}
/**
* {@inheritdoc}
*/
public function simulate($context)
{
$this->printAction($context);
}
public static function stopRunningJobs()
{
foreach (self::$instances as $instance) {
if ($instance) {
unset($instance);
}
}
}
}
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, ['Robo\Task\Base\Exec', 'stopRunningJobs']);
}
register_shutdown_function(['Robo\Task\Base\Exec', 'stopRunningJobs']);

View File

@@ -0,0 +1,26 @@
<?php
namespace Robo\Task\Base;
use Robo\Task\CommandStack;
use Robo\Task\Base;
/**
* Execute commands one by one in stack.
* Stack can be stopped on first fail if you call `stopOnFail()`.
*
* ```php
* <?php
* $this->taskExecStack()
* ->stopOnFail()
* ->exec('mkdir site')
* ->exec('cd site')
* ->run();
*
* ?>
* ```
*
* @method $this stopOnFail()
*/
class ExecStack extends CommandStack
{
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Robo\Task\Base;
use Robo\Contract\ProgressIndicatorAwareInterface;
use Robo\Common\ProgressIndicatorAwareTrait;
use Robo\Contract\CommandInterface;
use Robo\Contract\PrintedInterface;
use Robo\Result;
use Robo\Task\BaseTask;
use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Process;
/**
* Class ParallelExecTask
*
* ``` php
* <?php
* $this->taskParallelExec()
* ->process('php ~/demos/script.php hey')
* ->process('php ~/demos/script.php hoy')
* ->process('php ~/demos/script.php gou')
* ->run();
* ?>
* ```
*
*
* @method \Robo\Task\Base\ParallelExec timeout(int $timeout) stops process if it runs longer then `$timeout` (seconds)
* @method \Robo\Task\Base\ParallelExec idleTimeout(int $timeout) stops process if it does not output for time longer then `$timeout` (seconds)
*/
class ParallelExec extends BaseTask implements CommandInterface, PrintedInterface
{
use \Robo\Common\CommandReceiver;
/**
* @var Process[]
*/
protected $processes = [];
/**
* @var null|int
*/
protected $timeout = null;
/**
* @var null|int
*/
protected $idleTimeout = null;
/**
* @var bool
*/
protected $isPrinted = false;
/**
* {@inheritdoc}
*/
public function getPrinted()
{
return $this->isPrinted;
}
/**
* @param bool $isPrinted
*
* @return $this
*/
public function printed($isPrinted = true)
{
$this->isPrinted = $isPrinted;
return $this;
}
/**
* @param string|\Robo\Contract\CommandInterface $command
*
* @return $this
*/
public function process($command)
{
$this->processes[] = new Process($this->receiveCommand($command));
return $this;
}
/**
* @param int $timeout
*
* @return $this
*/
public function timeout($timeout)
{
$this->timeout = $timeout;
return $this;
}
/**
* @param int $idleTimeout
*
* @return $this
*/
public function idleTimeout($idleTimeout)
{
$this->idleTimeout = $idleTimeout;
return $this;
}
/**
* {@inheritdoc}
*/
public function getCommand()
{
return implode(' && ', $this->processes);
}
/**
* @return int
*/
public function progressIndicatorSteps()
{
return count($this->processes);
}
/**
* {@inheritdoc}
*/
public function run()
{
foreach ($this->processes as $process) {
$process->setIdleTimeout($this->idleTimeout);
$process->setTimeout($this->timeout);
$process->start();
$this->printTaskInfo($process->getCommandLine());
}
$this->startProgressIndicator();
$running = $this->processes;
while (true) {
foreach ($running as $k => $process) {
try {
$process->checkTimeout();
} catch (ProcessTimedOutException $e) {
$this->printTaskWarning("Process timed out for {command}", ['command' => $process->getCommandLine(), '_style' => ['command' => 'fg=white;bg=magenta']]);
}
if (!$process->isRunning()) {
$this->advanceProgressIndicator();
if ($this->isPrinted) {
$this->printTaskInfo("Output for {command}:\n\n{output}", ['command' => $process->getCommandLine(), 'output' => $process->getOutput(), '_style' => ['command' => 'fg=white;bg=magenta']]);
$errorOutput = $process->getErrorOutput();
if ($errorOutput) {
$this->printTaskError(rtrim($errorOutput));
}
}
unset($running[$k]);
}
}
if (empty($running)) {
break;
}
usleep(1000);
}
$this->stopProgressIndicator();
$errorMessage = '';
$exitCode = 0;
foreach ($this->processes as $p) {
if ($p->getExitCode() === 0) {
continue;
}
$errorMessage .= "'" . $p->getCommandLine() . "' exited with code ". $p->getExitCode()." \n";
$exitCode = max($exitCode, $p->getExitCode());
}
if (!$errorMessage) {
$this->printTaskSuccess('{process-count} processes finished running', ['process-count' => count($this->processes)]);
}
return new Result($this, $exitCode, $errorMessage, ['time' => $this->getExecutionTime()]);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Robo\Task\Base;
use Robo\Robo;
use Robo\Result;
use Robo\Task\BaseTask;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
/**
* Executes Symfony Command
*
* ``` php
* <?php
* // Symfony Command
* $this->taskSymfonyCommand(new \Codeception\Command\Run('run'))
* ->arg('suite','acceptance')
* ->opt('debug')
* ->run();
*
* // Artisan Command
* $this->taskSymfonyCommand(new ModelGeneratorCommand())
* ->arg('name', 'User')
* ->run();
* ?>
* ```
*/
class SymfonyCommand extends BaseTask
{
/**
* @var \Symfony\Component\Console\Command\Command
*/
protected $command;
/**
* @var string[]
*/
protected $input;
public function __construct(Command $command)
{
$this->command = $command;
$this->input = [];
}
/**
* @param string $arg
* @param string $value
*
* @return $this
*/
public function arg($arg, $value)
{
$this->input[$arg] = $value;
return $this;
}
public function opt($option, $value = null)
{
$this->input["--$option"] = $value;
return $this;
}
/**
* {@inheritdoc}
*/
public function run()
{
$this->printTaskInfo('Running command {command}', ['command' => $this->command->getName()]);
return new Result(
$this,
$this->command->run(new ArrayInput($this->input), Robo::output())
);
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Robo\Task\Base;
use Lurker\Event\FilesystemEvent;
use Lurker\ResourceWatcher;
use Robo\Result;
use Robo\Task\BaseTask;
/**
* Runs task when specified file or dir was changed.
* Uses Lurker library.
*
* ``` php
* <?php
* $this->taskWatch()
* ->monitor('composer.json', function() {
* $this->taskComposerUpdate()->run();
* })->monitor('src', function() {
* $this->taskExec('phpunit')->run();
* })->run();
* ?>
* ```
*/
class Watch extends BaseTask
{
/**
* @var \Closure
*/
protected $closure;
/**
* @var array
*/
protected $monitor = [];
/**
* @var object
*/
protected $bindTo;
/**
* @param $bindTo
*/
public function __construct($bindTo)
{
$this->bindTo = $bindTo;
}
/**
* @param string|string[] $paths
* @param \Closure $callable
*
* @return $this
*/
public function monitor($paths, \Closure $callable)
{
if (!is_array($paths)) {
$paths = [$paths];
}
$this->monitor[] = [$paths, $callable];
return $this;
}
/**
* {@inheritdoc}
*/
public function run()
{
if (!class_exists('Lurker\\ResourceWatcher')) {
return Result::errorMissingPackage($this, 'ResourceWatcher', 'henrikbjorn/lurker');
}
$watcher = new ResourceWatcher();
foreach ($this->monitor as $k => $monitor) {
/** @var \Closure $closure */
$closure = $monitor[1];
$closure->bindTo($this->bindTo);
foreach ($monitor[0] as $i => $dir) {
$watcher->track("fs.$k.$i", $dir, FilesystemEvent::MODIFY);
$this->printTaskInfo('Watching {dir} for changes...', ['dir' => $dir]);
$watcher->addListener("fs.$k.$i", $closure);
}
}
$watcher->start();
return Result::success($this);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Robo\Task\Base;
trait loadShortcuts
{
/**
* Executes shell command
*
* @param string|\Robo\Contract\CommandInterface $command
*
* @return \Robo\Result
*/
protected function _exec($command)
{
return $this->taskExec($command)->run();
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Robo\Task\Base;
trait loadTasks
{
/**
* @param string|\Robo\Contract\CommandInterface $command
*
* @return Exec
*/
protected function taskExec($command)
{
return $this->task(Exec::class, $command);
}
protected function taskExecStack()
{
return $this->task(ExecStack::class);
}
/**
* @return ParallelExec
*/
protected function taskParallelExec()
{
return $this->task(ParallelExec::class);
}
/**
* @param $command
* @return SymfonyCommand
*/
protected function taskSymfonyCommand($command)
{
return $this->task(SymfonyCommand::class, $command);
}
/**
* @return Watch
*/
protected function taskWatch()
{
return $this->task(Watch::class, $this);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Robo\Task;
use Robo\Common\InflectionTrait;
use Robo\Contract\InflectionInterface;
use Robo\Common\TaskIO;
use Robo\Contract\TaskInterface;
use Robo\Contract\ProgressIndicatorAwareInterface;
use Robo\Common\ProgressIndicatorAwareTrait;
use Robo\Contract\ConfigAwareInterface;
use Psr\Log\LoggerAwareInterface;
abstract class BaseTask implements TaskInterface, LoggerAwareInterface, ConfigAwareInterface, ProgressIndicatorAwareInterface, InflectionInterface
{
use TaskIO; // uses LoggerAwareTrait and ConfigAwareTrait
use ProgressIndicatorAwareTrait;
use InflectionTrait;
/**
* {@inheritdoc}
*/
public function injectDependencies(InflectionInterface $child)
{
if ($child instanceof LoggerAwareInterface && $this->logger) {
$child->setLogger($this->logger);
}
if ($child instanceof ProgressIndicatorAwareInterface && $this->progressIndicator) {
$child->setProgressIndicator($this->progressIndicator);
}
if ($child instanceof ConfigAwareInterface && $this->getConfig()) {
$child->setConfig($this->getConfig());
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Robo\Task\Bower;
use Robo\Task\BaseTask;
use Robo\Exception\TaskException;
abstract class Base extends BaseTask
{
use \Robo\Common\ExecOneCommand;
protected $opts = [];
protected $action = '';
/**
* @var string
*/
protected $command = '';
/**
* adds `allow-root` option to bower
*
* @return $this
*/
public function allowRoot()
{
$this->option('allow-root');
return $this;
}
/**
* adds `force-latest` option to bower
*
* @return $this
*/
public function forceLatest()
{
$this->option('force-latest');
return $this;
}
/**
* adds `production` option to bower
*
* @return $this
*/
public function noDev()
{
$this->option('production');
return $this;
}
/**
* adds `offline` option to bower
*
* @return $this
*/
public function offline()
{
$this->option('offline');
return $this;
}
/**
* Base constructor.
*
* @param null|string $pathToBower
*
* @throws \Robo\Exception\TaskException
*/
public function __construct($pathToBower = null)
{
$this->command = $pathToBower;
if (!$this->command) {
$this->command = $this->findExecutable('bower');
}
if (!$this->command) {
throw new TaskException(__CLASS__, "Bower executable not found.");
}
}
/**
* @return string
*/
public function getCommand()
{
return "{$this->command} {$this->action}{$this->arguments}";
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Robo\Task\Bower;
use Robo\Task\Bower;
use Robo\Contract\CommandInterface;
/**
* Bower Install
*
* ``` php
* <?php
* // simple execution
* $this->taskBowerInstall()->run();
*
* // prefer dist with custom path
* $this->taskBowerInstall('path/to/my/bower')
* ->noDev()
* ->run();
* ?>
* ```
*/
class Install extends Base implements CommandInterface
{
/**
* {@inheritdoc}
*/
protected $action = 'install';
/**
* {@inheritdoc}
*/
public function run()
{
$this->printTaskInfo('Install Bower packages: {arguments}', ['arguments' => $this->arguments]);
return $this->executeCommand($this->getCommand());
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Robo\Task\Bower;
use Robo\Task\Bower;
/**
* Bower Update
*
* ``` php
* <?php
* // simple execution
* $this->taskBowerUpdate->run();
*
* // prefer dist with custom path
* $this->taskBowerUpdate('path/to/my/bower')
* ->noDev()
* ->run();
* ?>
* ```
*/
class Update extends Base
{
/**
* {@inheritdoc}
*/
protected $action = 'update';
/**
* {@inheritdoc}
*/
public function run()
{
$this->printTaskInfo('Update Bower packages: {arguments}', ['arguments' => $this->arguments]);
return $this->executeCommand($this->getCommand());
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Robo\Task\Bower;
trait loadTasks
{
/**
* @param null|string $pathToBower
*
* @return Install
*/
protected function taskBowerInstall($pathToBower = null)
{
return $this->task(Install::class, $pathToBower);
}
/**
* @param null|string $pathToBower
*
* @return Update
*/
protected function taskBowerUpdate($pathToBower = null)
{
return $this->task(Update::class, $pathToBower);
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace Robo\Task;
use Robo\Common\ExecCommand;
use Robo\Contract\PrintedInterface;
use Robo\Result;
use Robo\Contract\CommandInterface;
use Robo\Common\DynamicParams;
use Robo\Exception\TaskException;
abstract class CommandStack extends BaseTask implements CommandInterface, PrintedInterface
{
use ExecCommand;
/**
* @var string
*/
protected $executable;
protected $result;
/**
* @var string[]
*/
protected $exec = [];
/**
* @var bool
*/
protected $stopOnFail = false;
/**
* {@inheritdoc}
*/
public function getCommand()
{
return implode(' && ', $this->exec);
}
/**
* @param string $executable
*
* @return $this
*/
public function executable($executable)
{
$this->executable = $executable;
return $this;
}
/**
* @param string|string[] $command
*
* @return $this
*/
public function exec($command)
{
if (is_array($command)) {
$command = implode(' ', array_filter($command));
}
$command = $this->executable . ' ' . $this->stripExecutableFromCommand($command);
array_push($this->exec, trim($command));
return $this;
}
/**
* @param bool $stopOnFail
*
* @return $this
*/
public function stopOnFail($stopOnFail = true)
{
$this->stopOnFail = $stopOnFail;
return $this;
}
public function result($result)
{
$this->result = $result;
return $this;
}
/**
* @param string $command
*
* @return string
*/
protected function stripExecutableFromCommand($command)
{
$command = trim($command);
$executable = $this->executable . ' ';
if (strpos($command, $executable) === 0) {
$command = substr($command, strlen($executable));
}
return $command;
}
/**
* {@inheritdoc}
*/
public function run()
{
if (empty($this->exec)) {
throw new TaskException($this, 'You must add at least one command');
}
if (!$this->stopOnFail) {
$this->printTaskInfo('{command}', ['command' => $this->getCommand()]);
return $this->executeCommand($this->getCommand());
}
foreach ($this->exec as $command) {
$this->printTaskInfo("Executing {command}", ['command' => $command]);
$result = $this->executeCommand($command);
if (!$result->wasSuccessful()) {
return $result;
}
}
return Result::success($this);
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace Robo\Task\Composer;
use Robo\Robo;
use Robo\Task\BaseTask;
use Robo\Exception\TaskException;
abstract class Base extends BaseTask
{
use \Robo\Common\ExecOneCommand;
/**
* @var string
*/
protected $command = '';
/**
* @var string
*/
protected $prefer;
/**
* @var string
*/
protected $dev;
/**
* @var string
*/
protected $optimizeAutoloader;
/**
* @var string
*/
protected $ansi;
/**
* @var string
*/
protected $dir;
/**
* Action to use
*
* @var string
*/
protected $action = '';
/**
* adds `prefer-dist` option to composer
*
* @return $this
*/
public function preferDist()
{
$this->prefer = '--prefer-dist';
return $this;
}
/**
* adds `prefer-source` option to composer
*
* @return $this
*/
public function preferSource()
{
$this->prefer = '--prefer-source';
return $this;
}
/**
* adds `no-dev` option to composer
*
* @return $this
*/
public function noDev()
{
$this->dev = '--no-dev';
return $this;
}
/**
* adds `no-ansi` option to composer
*
* @return $this
*/
public function noAnsi()
{
$this->ansi = '--no-ansi';
return $this;
}
/**
* adds `ansi` option to composer
*
* @return $this
*/
public function ansi()
{
$this->ansi = '--ansi';
return $this;
}
/**
* adds `optimize-autoloader` option to composer
*
* @return $this
*/
public function optimizeAutoloader()
{
$this->optimizeAutoloader = '--optimize-autoloader';
return $this;
}
/**
* @param null|string $pathToComposer
*
* @throws \Robo\Exception\TaskException
*/
public function __construct($pathToComposer = null)
{
$this->command = $pathToComposer;
if (!$this->command) {
$this->command = $this->findExecutablePhar('composer');
}
if (!$this->command) {
throw new TaskException(__CLASS__, "Neither local composer.phar nor global composer installation could be found.");
}
}
/**
* {@inheritdoc}
*/
public function getCommand()
{
if (!isset($this->ansi) && $this->getConfig()->isDecorated()) {
$this->ansi();
}
$this->option($this->prefer)
->option($this->dev)
->option($this->optimizeAutoloader)
->option($this->ansi);
return "{$this->command} {$this->action}{$this->arguments}";
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Robo\Task\Composer;
/**
* Composer Dump Autoload
*
* ``` php
* <?php
* // simple execution
* $this->taskComposerDumpAutoload()->run();
*
* // dump auto loader with custom path
* $this->taskComposerDumpAutoload('path/to/my/composer.phar')
* ->preferDist()
* ->run();
*
* // optimize autoloader dump with custom path
* $this->taskComposerDumpAutoload('path/to/my/composer.phar')
* ->optimize()
* ->run();
*
* // optimize autoloader dump with custom path and no dev
* $this->taskComposerDumpAutoload('path/to/my/composer.phar')
* ->optimize()
* ->noDev()
* ->run();
* ?>
* ```
*/
class DumpAutoload extends Base
{
/**
* {@inheritdoc}
*/
protected $action = 'dump-autoload';
/**
* @var string
*/
protected $optimize;
/**
* @return $this
*/
public function optimize()
{
$this->optimize = "--optimize";
return $this;
}
/**
* {@inheritdoc}
*/
public function getCommand()
{
$this->option($this->optimize);
return parent::getCommand();
}
/**
* {@inheritdoc}
*/
public function run()
{
$command = $this->getCommand();
$this->printTaskInfo('Dumping Autoloader: {command}', ['command' => $command]);
return $this->executeCommand($command);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Robo\Task\Composer;
/**
* Composer Install
*
* ``` php
* <?php
* // simple execution
* $this->taskComposerInstall()->run();
*
* // prefer dist with custom path
* $this->taskComposerInstall('path/to/my/composer.phar')
* ->preferDist()
* ->run();
*
* // optimize autoloader with custom path
* $this->taskComposerInstall('path/to/my/composer.phar')
* ->optimizeAutoloader()
* ->run();
* ?>
* ```
*/
class Install extends Base
{
/**
* {@inheritdoc}
*/
protected $action = 'install';
/**
* {@inheritdoc}
*/
public function run()
{
$command = $this->getCommand();
$this->printTaskInfo('Installing Packages: {command}', ['command' => $command]);
return $this->executeCommand($command);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Robo\Task\Composer;
/**
* Composer Validate
*
* ``` php
* <?php
* // simple execution
* $this->taskComposerValidate()->run();
* ?>
* ```
*/
class Remove extends Base
{
/**
* {@inheritdoc}
*/
protected $action = 'remove';
/**
* @return $this
*/
public function dev()
{
$this->option('--dev');
return $this;
}
/**
* @return $this
*/
public function noProgress()
{
$this->option('--no-progress');
return $this;
}
/**
* @return $this
*/
public function noUpdate()
{
$this->option('--no-update');
return $this;
}
/**
* @return $this
*/
public function updateNoDev()
{
$this->option('--update-no-dev');
return $this;
}
/**
* @return $this
*/
public function noUpdateWithDependencies()
{
$this->option('--no-update-with-dependencies');
return $this;
}
/**
* {@inheritdoc}
*/
public function run()
{
$command = $this->getCommand();
$this->printTaskInfo('Removing packages: {command}', ['command' => $command]);
return $this->executeCommand($command);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Robo\Task\Composer;
/**
* Composer Update
*
* ``` php
* <?php
* // simple execution
* $this->taskComposerUpdate()->run();
*
* // prefer dist with custom path
* $this->taskComposerUpdate('path/to/my/composer.phar')
* ->preferDist()
* ->run();
*
* // optimize autoloader with custom path
* $this->taskComposerUpdate('path/to/my/composer.phar')
* ->optimizeAutoloader()
* ->run();
* ?>
* ```
*/
class Update extends Base
{
/**
* {@inheritdoc}
*/
protected $action = 'update';
/**
* {@inheritdoc}
*/
public function run()
{
$command = $this->getCommand();
$this->printTaskInfo('Updating Packages: {command}', ['command' => $command]);
return $this->executeCommand($command);
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Robo\Task\Composer;
/**
* Composer Validate
*
* ``` php
* <?php
* // simple execution
* $this->taskComposerValidate()->run();
* ?>
* ```
*/
class Validate extends Base
{
/**
* {@inheritdoc}
*/
protected $action = 'validate';
/**
* @var string
*/
protected $noCheckAll;
/**
* @var string
*/
protected $noCheckLock;
/**
* @var string
*/
protected $noCheckPublish;
/**
* @var string
*/
protected $withDependencies;
/**
* @var string
*/
protected $strict;
/**
* @return $this
*/
public function noCheckAll()
{
$this->noCheckAll = '--no-check-all';
return $this;
}
/**
* @return $this
*/
public function noCheckLock()
{
$this->noCheckLock = '--no-check-lock';
return $this;
}
/**
* @return $this
*/
public function noCheckPublish()
{
$this->noCheckPublish = '--no-check-publish';
return $this;
}
/**
* @return $this
*/
public function withDependencies()
{
$this->withDependencies = '--with-dependencies';
return $this;
}
/**
* @return $this
*/
public function strict()
{
$this->strict = '--strict';
return $this;
}
/**
* {@inheritdoc}
*/
public function getCommand()
{
$this->option($this->noCheckAll);
$this->option($this->noCheckLock);
$this->option($this->noCheckPublish);
$this->option($this->withDependencies);
$this->option($this->strict);
return parent::getCommand();
}
/**
* {@inheritdoc}
*/
public function run()
{
$command = $this->getCommand();
$this->printTaskInfo('Validating composer.json: {command}', ['command' => $command]);
return $this->executeCommand($command);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Robo\Task\Composer;
trait loadTasks
{
/**
* @param null|string $pathToComposer
*
* @return Install
*/
protected function taskComposerInstall($pathToComposer = null)
{
return $this->task(Install::class, $pathToComposer);
}
/**
* @param null|string $pathToComposer
*
* @return Update
*/
protected function taskComposerUpdate($pathToComposer = null)
{
return $this->task(Update::class, $pathToComposer);
}
/**
* @param null|string $pathToComposer
*
* @return DumpAutoload
*/
protected function taskComposerDumpAutoload($pathToComposer = null)
{
return $this->task(DumpAutoload::class, $pathToComposer);
}
/**
* @param null|string $pathToComposer
*
* @return Validate
*/
protected function taskComposerValidate($pathToComposer = null)
{
return $this->task(Validate::class, $pathToComposer);
}
/**
* @param null|string $pathToComposer
*
* @return Remove
*/
protected function taskComposerRemove($pathToComposer = null)
{
return $this->task(Remove::class, $pathToComposer);
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace Robo\Task\Development;
use Robo\Task\BaseTask;
use Robo\Task\File\Replace;
use Robo\Task\Filesystem;
use Robo\Result;
use Robo\Task\Development;
use Robo\Contract\BuilderAwareInterface;
use Robo\Common\BuilderAwareTrait;
/**
* Helps to manage changelog file.
* Creates or updates `changelog.md` file with recent changes in current version.
*
* ``` php
* <?php
* $version = "0.1.0";
* $this->taskChangelog()
* ->version($version)
* ->change("released to github")
* ->run();
* ?>
* ```
*
* Changes can be asked from Console
*
* ``` php
* <?php
* $this->taskChangelog()
* ->version($version)
* ->askForChanges()
* ->run();
* ?>
* ```
*
* @method Development\Changelog filename(string $filename)
* @method Development\Changelog anchor(string $anchor)
* @method Development\Changelog version(string $version)
*/
class Changelog extends BaseTask implements BuilderAwareInterface
{
use BuilderAwareTrait;
/**
* @var string
*/
protected $filename;
/**
* @var array
*/
protected $log = [];
/**
* @var string
*/
protected $anchor = "# Changelog";
/**
* @var string
*/
protected $version = "";
/**
* @param string $filename
*
* @return $this
*/
public function filename($filename)
{
$this->filename = $filename;
return $this;
}
/**
* @param string $item
*
* @return $this
*/
public function log($item)
{
$this->log[] = $item;
return $this;
}
/**
* @param string $anchor
*
* @return $this
*/
public function anchor($anchor)
{
$this->anchor = $anchor;
return $this;
}
/**
* @param string $version
*
* @return $this
*/
public function version($version)
{
$this->version = $version;
return $this;
}
/**
* @param string $filename
*/
public function __construct($filename)
{
$this->filename = $filename;
}
/**
* @param array $data
*
* @return $this
*/
public function changes(array $data)
{
$this->log = array_merge($this->log, $data);
return $this;
}
/**
* @param string $change
*
* @return $this
*/
public function change($change)
{
$this->log[] = $change;
return $this;
}
/**
* @return array
*/
public function getChanges()
{
return $this->log;
}
/**
* {@inheritdoc}
*/
public function run()
{
if (empty($this->log)) {
return Result::error($this, "Changelog is empty");
}
$text = implode(
"\n",
array_map(
function ($i) {
return "* $i *" . date('Y-m-d') . "*";
},
$this->log
)
) . "\n";
$ver = "#### {$this->version}\n\n";
$text = $ver . $text;
if (!file_exists($this->filename)) {
$this->printTaskInfo('Creating {filename}', ['filename' => $this->filename]);
$res = file_put_contents($this->filename, $this->anchor);
if ($res === false) {
return Result::error($this, "File {filename} cant be created", ['filename' => $this->filename]);
}
}
/** @var \Robo\Result $result */
// trying to append to changelog for today
$result = $this->collectionBuilder()->taskReplace($this->filename)
->from($ver)
->to($text)
->run();
if (!isset($result['replaced']) || !$result['replaced']) {
$result = $this->collectionBuilder()->taskReplace($this->filename)
->from($this->anchor)
->to($this->anchor . "\n\n" . $text)
->run();
}
return new Result($this, $result->getExitCode(), $result->getMessage(), $this->log);
}
}

View File

@@ -0,0 +1,769 @@
<?php
namespace Robo\Task\Development;
use Robo\Task\BaseTask;
use Robo\Task\File\Write;
use Robo\Task\Filesystem;
use Robo\Result;
use Robo\Task\Development;
use Robo\Contract\BuilderAwareInterface;
use Robo\Common\BuilderAwareTrait;
/**
* Simple documentation generator from source files.
* Takes classes, properties and methods with their docblocks and writes down a markdown file.
*
* ``` php
* <?php
* $this->taskGenDoc('models.md')
* ->docClass('Model\User') // take class Model\User
* ->docClass('Model\Post') // take class Model\Post
* ->filterMethods(function(\ReflectionMethod $r) {
* return $r->isPublic() or $r->isProtected(); // process public and protected methods
* })->processClass(function(\ReflectionClass $r, $text) {
* return "Class ".$r->getName()."\n\n$text\n\n###Methods\n";
* })->run();
* ```
*
* By default this task generates a documentation for each public method of a class.
* It combines method signature with a docblock. Both can be post-processed.
*
* ``` php
* <?php
* $this->taskGenDoc('models.md')
* ->docClass('Model\User')
* ->processClassSignature(false) // false can be passed to not include class signature
* ->processClassDocBlock(function(\ReflectionClass $r, $text) {
* return "[This is part of application model]\n" . $text;
* })->processMethodSignature(function(\ReflectionMethod $r, $text) {
* return "#### {$r->name}()";
* })->processMethodDocBlock(function(\ReflectionMethod $r, $text) {
* return strpos($r->name, 'save')===0 ? "[Saves to the database]\n" . $text : $text;
* })->run();
* ```
*
* @method \Robo\Task\Development\GenerateMarkdownDoc docClass(string $classname) put a class you want to be documented
* @method \Robo\Task\Development\GenerateMarkdownDoc filterMethods(\Closure $func) using callback function filter out methods that won't be documented
* @method \Robo\Task\Development\GenerateMarkdownDoc filterClasses(\Closure $func) using callback function filter out classes that won't be documented
* @method \Robo\Task\Development\GenerateMarkdownDoc filterProperties(\Closure $func) using callback function filter out properties that won't be documented
* @method \Robo\Task\Development\GenerateMarkdownDoc processClass(\Closure $func) post-process class documentation
* @method \Robo\Task\Development\GenerateMarkdownDoc processClassSignature(\Closure $func) post-process class signature. Provide *false* to skip.
* @method \Robo\Task\Development\GenerateMarkdownDoc processClassDocBlock(\Closure $func) post-process class docblock contents. Provide *false* to skip.
* @method \Robo\Task\Development\GenerateMarkdownDoc processMethod(\Closure $func) post-process method documentation. Provide *false* to skip.
* @method \Robo\Task\Development\GenerateMarkdownDoc processMethodSignature(\Closure $func) post-process method signature. Provide *false* to skip.
* @method \Robo\Task\Development\GenerateMarkdownDoc processMethodDocBlock(\Closure $func) post-process method docblock contents. Provide *false* to skip.
* @method \Robo\Task\Development\GenerateMarkdownDoc processProperty(\Closure $func) post-process property documentation. Provide *false* to skip.
* @method \Robo\Task\Development\GenerateMarkdownDoc processPropertySignature(\Closure $func) post-process property signature. Provide *false* to skip.
* @method \Robo\Task\Development\GenerateMarkdownDoc processPropertyDocBlock(\Closure $func) post-process property docblock contents. Provide *false* to skip.
* @method \Robo\Task\Development\GenerateMarkdownDoc reorder(\Closure $func) use a function to reorder classes
* @method \Robo\Task\Development\GenerateMarkdownDoc reorderMethods(\Closure $func) use a function to reorder methods in class
* @method \Robo\Task\Development\GenerateMarkdownDoc prepend($text) inserts text into beginning of markdown file
* @method \Robo\Task\Development\GenerateMarkdownDoc append($text) inserts text in the end of markdown file
*/
class GenerateMarkdownDoc extends BaseTask implements BuilderAwareInterface
{
use BuilderAwareTrait;
/**
* @var string[]
*/
protected $docClass = [];
/**
* @var callable
*/
protected $filterMethods;
/**
* @var callable
*/
protected $filterClasses;
/**
* @var callable
*/
protected $filterProperties;
/**
* @var callable
*/
protected $processClass;
/**
* @var callable|false
*/
protected $processClassSignature;
/**
* @var callable|false
*/
protected $processClassDocBlock;
/**
* @var callable|false
*/
protected $processMethod;
/**
* @var callable|false
*/
protected $processMethodSignature;
/**
* @var callable|false
*/
protected $processMethodDocBlock;
/**
* @var callable|false
*/
protected $processProperty;
/**
* @var callable|false
*/
protected $processPropertySignature;
/**
* @var callable|false
*/
protected $processPropertyDocBlock;
/**
* @var callable
*/
protected $reorder;
/**
* @var callable
*/
protected $reorderMethods;
/**
* @todo Unused property.
*
* @var callable
*/
protected $reorderProperties;
/**
* @var string
*/
protected $filename;
/**
* @var string
*/
protected $prepend = "";
/**
* @var string
*/
protected $append = "";
/**
* @var string
*/
protected $text;
/**
* @var string[]
*/
protected $textForClass = [];
/**
* @param string $filename
*
* @return static
*/
public static function init($filename)
{
return new static($filename);
}
/**
* @param string $filename
*/
public function __construct($filename)
{
$this->filename = $filename;
}
/**
* @param string $item
*
* @return $this
*/
public function docClass($item)
{
$this->docClass[] = $item;
return $this;
}
/**
* @param callable $filterMethods
*
* @return $this
*/
public function filterMethods($filterMethods)
{
$this->filterMethods = $filterMethods;
return $this;
}
/**
* @param callable $filterClasses
*
* @return $this
*/
public function filterClasses($filterClasses)
{
$this->filterClasses = $filterClasses;
return $this;
}
/**
* @param callable $filterProperties
*
* @return $this
*/
public function filterProperties($filterProperties)
{
$this->filterProperties = $filterProperties;
return $this;
}
/**
* @param callable $processClass
*
* @return $this
*/
public function processClass($processClass)
{
$this->processClass = $processClass;
return $this;
}
/**
* @param callable|false $processClassSignature
*
* @return $this
*/
public function processClassSignature($processClassSignature)
{
$this->processClassSignature = $processClassSignature;
return $this;
}
/**
* @param callable|false $processClassDocBlock
*
* @return $this
*/
public function processClassDocBlock($processClassDocBlock)
{
$this->processClassDocBlock = $processClassDocBlock;
return $this;
}
/**
* @param callable|false $processMethod
*
* @return $this
*/
public function processMethod($processMethod)
{
$this->processMethod = $processMethod;
return $this;
}
/**
* @param callable|false $processMethodSignature
*
* @return $this
*/
public function processMethodSignature($processMethodSignature)
{
$this->processMethodSignature = $processMethodSignature;
return $this;
}
/**
* @param callable|false $processMethodDocBlock
*
* @return $this
*/
public function processMethodDocBlock($processMethodDocBlock)
{
$this->processMethodDocBlock = $processMethodDocBlock;
return $this;
}
/**
* @param callable|false $processProperty
*
* @return $this
*/
public function processProperty($processProperty)
{
$this->processProperty = $processProperty;
return $this;
}
/**
* @param callable|false $processPropertySignature
*
* @return $this
*/
public function processPropertySignature($processPropertySignature)
{
$this->processPropertySignature = $processPropertySignature;
return $this;
}
/**
* @param callable|false $processPropertyDocBlock
*
* @return $this
*/
public function processPropertyDocBlock($processPropertyDocBlock)
{
$this->processPropertyDocBlock = $processPropertyDocBlock;
return $this;
}
/**
* @param callable $reorder
*
* @return $this
*/
public function reorder($reorder)
{
$this->reorder = $reorder;
return $this;
}
/**
* @param callable $reorderMethods
*
* @return $this
*/
public function reorderMethods($reorderMethods)
{
$this->reorderMethods = $reorderMethods;
return $this;
}
/**
* @param callable $reorderProperties
*
* @return $this
*/
public function reorderProperties($reorderProperties)
{
$this->reorderProperties = $reorderProperties;
return $this;
}
/**
* @param string $filename
*
* @return $this
*/
public function filename($filename)
{
$this->filename = $filename;
return $this;
}
/**
* @param string $prepend
*
* @return $this
*/
public function prepend($prepend)
{
$this->prepend = $prepend;
return $this;
}
/**
* @param string $append
*
* @return $this
*/
public function append($append)
{
$this->append = $append;
return $this;
}
/**
* @param string $text
*
* @return $this
*/
public function text($text)
{
$this->text = $text;
return $this;
}
/**
* @param string $item
*
* @return $this
*/
public function textForClass($item)
{
$this->textForClass[] = $item;
return $this;
}
/**
* {@inheritdoc}
*/
public function run()
{
foreach ($this->docClass as $class) {
$this->printTaskInfo("Processing {class}", ['class' => $class]);
$this->textForClass[$class] = $this->documentClass($class);
}
if (is_callable($this->reorder)) {
$this->printTaskInfo("Applying reorder function");
call_user_func_array($this->reorder, [$this->textForClass]);
}
$this->text = implode("\n", $this->textForClass);
/** @var \Robo\Result $result */
$result = $this->collectionBuilder()->taskWriteToFile($this->filename)
->line($this->prepend)
->text($this->text)
->line($this->append)
->run();
$this->printTaskSuccess('{filename} created. {class-count} classes documented', ['filename' => $this->filename, 'class-count' => count($this->docClass)]);
return new Result($this, $result->getExitCode(), $result->getMessage(), $this->textForClass);
}
/**
* @param string $class
*
* @return null|string
*/
protected function documentClass($class)
{
if (!class_exists($class)) {
return "";
}
$refl = new \ReflectionClass($class);
if (is_callable($this->filterClasses)) {
$ret = call_user_func($this->filterClasses, $refl);
if (!$ret) {
return;
}
}
$doc = $this->documentClassSignature($refl);
$doc .= "\n" . $this->documentClassDocBlock($refl);
$doc .= "\n";
if (is_callable($this->processClass)) {
$doc = call_user_func($this->processClass, $refl, $doc);
}
$properties = [];
foreach ($refl->getProperties() as $reflProperty) {
$properties[] = $this->documentProperty($reflProperty);
}
$properties = array_filter($properties);
$doc .= implode("\n", $properties);
$methods = [];
foreach ($refl->getMethods() as $reflMethod) {
$methods[$reflMethod->name] = $this->documentMethod($reflMethod);
}
if (is_callable($this->reorderMethods)) {
call_user_func_array($this->reorderMethods, [&$methods]);
}
$methods = array_filter($methods);
$doc .= implode("\n", $methods)."\n";
return $doc;
}
/**
* @param \ReflectionClass $reflectionClass
*
* @return string
*/
protected function documentClassSignature(\ReflectionClass $reflectionClass)
{
if ($this->processClassSignature === false) {
return "";
}
$signature = "## {$reflectionClass->name}\n\n";
if ($parent = $reflectionClass->getParentClass()) {
$signature .= "* *Extends* `{$parent->name}`";
}
$interfaces = $reflectionClass->getInterfaceNames();
if (count($interfaces)) {
$signature .= "\n* *Implements* `" . implode('`, `', $interfaces) . '`';
}
$traits = $reflectionClass->getTraitNames();
if (count($traits)) {
$signature .= "\n* *Uses* `" . implode('`, `', $traits) . '`';
}
if (is_callable($this->processClassSignature)) {
$signature = call_user_func($this->processClassSignature, $reflectionClass, $signature);
}
return $signature;
}
/**
* @param \ReflectionClass $reflectionClass
*
* @return string
*/
protected function documentClassDocBlock(\ReflectionClass $reflectionClass)
{
if ($this->processClassDocBlock === false) {
return "";
}
$doc = self::indentDoc($reflectionClass->getDocComment());
if (is_callable($this->processClassDocBlock)) {
$doc = call_user_func($this->processClassDocBlock, $reflectionClass, $doc);
}
return $doc;
}
/**
* @param \ReflectionMethod $reflectedMethod
*
* @return string
*/
protected function documentMethod(\ReflectionMethod $reflectedMethod)
{
if ($this->processMethod === false) {
return "";
}
if (is_callable($this->filterMethods)) {
$ret = call_user_func($this->filterMethods, $reflectedMethod);
if (!$ret) {
return "";
}
} else {
if (!$reflectedMethod->isPublic()) {
return "";
}
}
$signature = $this->documentMethodSignature($reflectedMethod);
$docblock = $this->documentMethodDocBlock($reflectedMethod);
$methodDoc = "$signature $docblock";
if (is_callable($this->processMethod)) {
$methodDoc = call_user_func($this->processMethod, $reflectedMethod, $methodDoc);
}
return $methodDoc;
}
/**
* @param \ReflectionProperty $reflectedProperty
*
* @return string
*/
protected function documentProperty(\ReflectionProperty $reflectedProperty)
{
if ($this->processProperty === false) {
return "";
}
if (is_callable($this->filterProperties)) {
$ret = call_user_func($this->filterProperties, $reflectedProperty);
if (!$ret) {
return "";
}
} else {
if (!$reflectedProperty->isPublic()) {
return "";
}
}
$signature = $this->documentPropertySignature($reflectedProperty);
$docblock = $this->documentPropertyDocBlock($reflectedProperty);
$propertyDoc = $signature . $docblock;
if (is_callable($this->processProperty)) {
$propertyDoc = call_user_func($this->processProperty, $reflectedProperty, $propertyDoc);
}
return $propertyDoc;
}
/**
* @param \ReflectionProperty $reflectedProperty
*
* @return string
*/
protected function documentPropertySignature(\ReflectionProperty $reflectedProperty)
{
if ($this->processPropertySignature === false) {
return "";
}
$modifiers = implode(' ', \Reflection::getModifierNames($reflectedProperty->getModifiers()));
$signature = "#### *$modifiers* {$reflectedProperty->name}";
if (is_callable($this->processPropertySignature)) {
$signature = call_user_func($this->processPropertySignature, $reflectedProperty, $signature);
}
return $signature;
}
/**
* @param \ReflectionProperty $reflectedProperty
*
* @return string
*/
protected function documentPropertyDocBlock(\ReflectionProperty $reflectedProperty)
{
if ($this->processPropertyDocBlock === false) {
return "";
}
$propertyDoc = $reflectedProperty->getDocComment();
// take from parent
if (!$propertyDoc) {
$parent = $reflectedProperty->getDeclaringClass();
while ($parent = $parent->getParentClass()) {
if ($parent->hasProperty($reflectedProperty->name)) {
$propertyDoc = $parent->getProperty($reflectedProperty->name)->getDocComment();
}
}
}
$propertyDoc = self::indentDoc($propertyDoc, 7);
$propertyDoc = preg_replace("~^@(.*?)([$\s])~", ' * `$1` $2', $propertyDoc); // format annotations
if (is_callable($this->processPropertyDocBlock)) {
$propertyDoc = call_user_func($this->processPropertyDocBlock, $reflectedProperty, $propertyDoc);
}
return ltrim($propertyDoc);
}
/**
* @param \ReflectionParameter $param
*
* @return string
*/
protected function documentParam(\ReflectionParameter $param)
{
$text = "";
if ($param->isArray()) {
$text .= 'array ';
}
if ($param->isCallable()) {
$text .= 'callable ';
}
$text .= '$' . $param->name;
if ($param->isDefaultValueAvailable()) {
if ($param->allowsNull()) {
$text .= ' = null';
} else {
$text .= ' = ' . str_replace("\n", ' ', print_r($param->getDefaultValue(), true));
}
}
return $text;
}
/**
* @param string $doc
* @param int $indent
*
* @return string
*/
public static function indentDoc($doc, $indent = 3)
{
if (!$doc) {
return $doc;
}
return implode(
"\n",
array_map(
function ($line) use ($indent) {
return substr($line, $indent);
},
explode("\n", $doc)
)
);
}
/**
* @param \ReflectionMethod $reflectedMethod
*
* @return string
*/
protected function documentMethodSignature(\ReflectionMethod $reflectedMethod)
{
if ($this->processMethodSignature === false) {
return "";
}
$modifiers = implode(' ', \Reflection::getModifierNames($reflectedMethod->getModifiers()));
$params = implode(
', ',
array_map(
function ($p) {
return $this->documentParam($p);
},
$reflectedMethod->getParameters()
)
);
$signature = "#### *$modifiers* {$reflectedMethod->name}($params)";
if (is_callable($this->processMethodSignature)) {
$signature = call_user_func($this->processMethodSignature, $reflectedMethod, $signature);
}
return $signature;
}
/**
* @param \ReflectionMethod $reflectedMethod
*
* @return string
*/
protected function documentMethodDocBlock(\ReflectionMethod $reflectedMethod)
{
if ($this->processMethodDocBlock === false) {
return "";
}
$methodDoc = $reflectedMethod->getDocComment();
// take from parent
if (!$methodDoc) {
$parent = $reflectedMethod->getDeclaringClass();
while ($parent = $parent->getParentClass()) {
if ($parent->hasMethod($reflectedMethod->name)) {
$methodDoc = $parent->getMethod($reflectedMethod->name)->getDocComment();
}
}
}
// take from interface
if (!$methodDoc) {
$interfaces = $reflectedMethod->getDeclaringClass()->getInterfaces();
foreach ($interfaces as $interface) {
$i = new \ReflectionClass($interface->name);
if ($i->hasMethod($reflectedMethod->name)) {
$methodDoc = $i->getMethod($reflectedMethod->name)->getDocComment();
break;
}
}
}
$methodDoc = self::indentDoc($methodDoc, 7);
$methodDoc = preg_replace("~^@(.*?) ([$\s])~m", ' * `$1` $2', $methodDoc); // format annotations
if (is_callable($this->processMethodDocBlock)) {
$methodDoc = call_user_func($this->processMethodDocBlock, $reflectedMethod, $methodDoc);
}
return $methodDoc;
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Robo\Task\Development;
use Robo\Task\BaseTask;
use Symfony\Component\Process\ProcessUtils;
use Robo\Result;
/**
* Generate a Robo Task that is a wrapper around an existing class.
*
* ``` php
* <?php
* $this->taskGenerateTask('Symfony\Component\Filesystem\Filesystem', 'FilesystemStack')
* ->run();
* ```
*/
class GenerateTask extends BaseTask
{
/**
* @var string
*/
protected $className;
/**
* @var string
*/
protected $wrapperClassName;
/**
* @param string $className
* @param string $wrapperClassName
*/
public function __construct($className, $wrapperClassName = '')
{
$this->className = $className;
$this->wrapperClassName = $wrapperClassName;
}
/**
* {@inheritdoc}
*/
public function run()
{
$delegate = new \ReflectionClass($this->className);
$replacements = [];
$leadingCommentChars = " * ";
$methodDescriptions = [];
$methodImplementations = [];
$immediateMethods = [];
foreach ($delegate->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
$methodName = $method->name;
$getter = preg_match('/^(get|has|is)/', $methodName);
$setter = preg_match('/^(set|unset)/', $methodName);
$argPrototypeList = [];
$argNameList = [];
$needsImplementation = false;
foreach ($method->getParameters() as $arg) {
$argDescription = '$' . $arg->name;
$argNameList[] = $argDescription;
if ($arg->isOptional()) {
$argDescription = $argDescription . ' = ' . str_replace("\n", "", var_export($arg->getDefaultValue(), true));
// We will create wrapper methods for any method that
// has default parameters.
$needsImplementation = true;
}
$argPrototypeList[] = $argDescription;
}
$argPrototypeString = implode(', ', $argPrototypeList);
$argNameListString = implode(', ', $argNameList);
if ($methodName[0] != '_') {
$methodDescriptions[] = "@method $methodName($argPrototypeString)";
if ($getter) {
$immediateMethods[] = " public function $methodName($argPrototypeString)\n {\n return \$this->delegate->$methodName($argNameListString);\n }";
} elseif ($setter) {
$immediateMethods[] = " public function $methodName($argPrototypeString)\n {\n \$this->delegate->$methodName($argNameListString);\n return \$this;\n }";
} elseif ($needsImplementation) {
// Include an implementation for the wrapper method if necessary
$methodImplementations[] = " protected function _$methodName($argPrototypeString)\n {\n \$this->delegate->$methodName($argNameListString);\n }";
}
}
}
$classNameParts = explode('\\', $this->className);
$delegate = array_pop($classNameParts);
$delegateNamespace = implode('\\', $classNameParts);
if (empty($this->wrapperClassName)) {
$this->wrapperClassName = $delegate;
}
$replacements['{delegateNamespace}'] = $delegateNamespace;
$replacements['{delegate}'] = $delegate;
$replacements['{wrapperClassName}'] = $this->wrapperClassName;
$replacements['{taskname}'] = "task$delegate";
$replacements['{methodList}'] = $leadingCommentChars . implode("\n$leadingCommentChars", $methodDescriptions);
$replacements['{immediateMethods}'] = "\n\n" . implode("\n\n", $immediateMethods);
$replacements['{methodImplementations}'] = "\n\n" . implode("\n\n", $methodImplementations);
$template = file_get_contents(__DIR__ . '/../../../data/Task/Development/GeneratedWrapper.tmpl');
$template = str_replace(array_keys($replacements), array_values($replacements), $template);
// Returning data in the $message will cause it to be printed.
return Result::success($this, $template);
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Robo\Task\Development;
use Robo\Exception\TaskException;
use Robo\Task\BaseTask;
/**
* @method \Robo\Task\Development\GitHub repo(string)
* @method \Robo\Task\Development\GitHub owner(string)
*/
abstract class GitHub extends BaseTask
{
const GITHUB_URL = 'https://Api.github.com';
/**
* @var string
*/
protected $user = '';
/**
* @var string
*/
protected $password = '';
/**
* @var string
*/
protected $repo;
/**
* @var string
*/
protected $owner;
/**
* @param string $repo
*
* @return $this
*/
public function repo($repo)
{
$this->repo = $repo;
return $this;
}
/**
* @param string $owner
*
* @return $this
*/
public function owner($owner)
{
$this->owner = $owner;
return $this;
}
/**
* @param string $uri
*
* @return $this
*/
public function uri($uri)
{
list($this->owner, $this->repo) = explode('/', $uri);
return $this;
}
/**
* @return string
*/
protected function getUri()
{
return $this->owner . '/' . $this->repo;
}
/**
* @param string $user
*
* @return $this
*/
public function user($user)
{
$this->user = $user;
return $this;
}
/**
* @param $password
*
* @return $this
*/
public function password($password)
{
$this->password = $password;
return $this;
}
/**
* @param string $uri
* @param array $params
* @param string $method
*
* @return array
*
* @throws \Robo\Exception\TaskException
*/
protected function sendRequest($uri, $params = [], $method = 'POST')
{
if (!$this->owner or !$this->repo) {
throw new TaskException($this, 'Repo URI is not set');
}
$ch = curl_init();
$url = sprintf('%s/repos/%s/%s', self::GITHUB_URL, $this->getUri(), $uri);
$this->printTaskInfo($url);
$this->printTaskInfo('{method} {url}', ['method' => $method, 'url' => $url]);
if (!empty($this->user)) {
curl_setopt($ch, CURLOPT_USERPWD, $this->user . ':' . $this->password);
}
curl_setopt_array(
$ch,
array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => $method != 'GET',
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_USERAGENT => "Robo"
)
);
$output = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$response = json_decode($output);
$this->printTaskInfo($output);
return [$code, $response];
}
}

View File

@@ -0,0 +1,208 @@
<?php
namespace Robo\Task\Development;
use Robo\Result;
/**
* Publishes new GitHub release.
*
* ``` php
* <?php
* $this->taskGitHubRelease('0.1.0')
* ->uri('consolidation-org/Robo')
* ->description('Add stuff people need.')
* ->change('Fix #123')
* ->change('Add frobulation method to all widgets')
* ->run();
* ?>
* ```
*/
class GitHubRelease extends GitHub
{
/**
* @var string
*/
protected $tag;
/**
* @var string
*/
protected $name;
/**
* @var string
*/
protected $description = '';
/**
* @var string[]
*/
protected $changes = [];
/**
* @var bool
*/
protected $draft = false;
/**
* @var bool
*/
protected $prerelease = false;
/**
* @var string
*/
protected $comittish = 'master';
/**
* @param string $tag
*/
public function __construct($tag)
{
$this->tag = $tag;
}
/**
* @param string $tag
*
* @return $this
*/
public function tag($tag)
{
$this->tag = $tag;
return $this;
}
/**
* @param bool $draft
*
* @return $this
*/
public function draft($draft)
{
$this->draft = $draft;
return $this;
}
/**
* @param string $name
*
* @return $this
*/
public function name($name)
{
$this->name = $name;
return $this;
}
/**
* @param string $description
*
* @return $this
*/
public function description($description)
{
$this->description = $description;
return $this;
}
/**
* @param bool $prerelease
*
* @return $this
*/
public function prerelease($prerelease)
{
$this->prerelease = $prerelease;
return $this;
}
/**
* @param string $comittish
*
* @return $this
*/
public function comittish($comittish)
{
$this->comittish = $comittish;
return $this;
}
/**
* @param string $description
*
* @return $this
*/
public function appendDescription($description)
{
if (!empty($this->description)) {
$this->description .= "\n\n";
}
$this->description .= $description;
return $this;
}
public function changes(array $changes)
{
$this->changes = array_merge($this->changes, $changes);
return $this;
}
/**
* @param string $change
*
* @return $this
*/
public function change($change)
{
$this->changes[] = $change;
return $this;
}
/**
* @return string
*/
protected function getBody()
{
$body = $this->description;
if (!empty($this->changes)) {
$changes = array_map(
function ($line) {
return "* $line";
},
$this->changes
);
$changesText = implode("\n", $changes);
$body .= "### Changelog \n\n$changesText";
}
return $body;
}
/**
* {@inheritdoc}
*/
public function run()
{
$this->printTaskInfo('Releasing {tag}', ['tag' => $this->tag]);
$this->startTimer();
list($code, $data) = $this->sendRequest(
'releases',
[
"tag_name" => $this->tag,
"target_commitish" => $this->comittish,
"name" => $this->tag,
"body" => $this->getBody(),
"draft" => $this->draft,
"prerelease" => $this->prerelease
]
);
$this->stopTimer();
return new Result(
$this,
in_array($code, [200, 201]) ? 0 : 1,
isset($data->message) ? $data->message : '',
['response' => $data, 'time' => $this->getExecutionTime()]
);
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Robo\Task\Development;
use Robo\Task\BaseTask;
use Symfony\Component\Process\ProcessUtils;
use Robo\Result;
/**
* Opens the default's user browser
* code inspired from openBrowser() function in https://github.com/composer/composer/blob/master/src/Composer/Command/HomeCommand.php
*
* ``` php
* <?php
* // open one browser window
* $this->taskOpenBrowser('http://localhost')
* ->run();
*
* // open two browser windows
* $this->taskOpenBrowser([
* 'http://localhost/mysite',
* 'http://localhost/mysite2'
* ])
* ->run();
* ```
*/
class OpenBrowser extends BaseTask
{
/**
* @var string[]
*/
protected $urls = [];
/**
* @param string|array $url
*/
public function __construct($url)
{
$this->urls = (array) $url;
}
/**
* {@inheritdoc}
*/
public function run()
{
$openCommand = $this->getOpenCommand();
if (empty($openCommand)) {
return Result::error($this, 'no suitable browser opening command found');
}
foreach ($this->urls as $url) {
passthru(sprintf($openCommand, ProcessUtils::escapeArgument($url)));
$this->printTaskInfo('Opened {url}', ['url' => $url]);
}
return Result::success($this);
}
/**
* @return null|string
*/
private function getOpenCommand()
{
if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
return 'start "web" explorer "%s"';
}
passthru('which xdg-open', $linux);
passthru('which open', $osx);
if (0 === $linux) {
return 'xdg-open %s';
}
if (0 === $osx) {
return 'open %s';
}
}
}

View File

@@ -0,0 +1,253 @@
<?php
namespace Robo\Task\Development;
use Robo\Contract\ProgressIndicatorAwareInterface;
use Robo\Common\ProgressIndicatorAwareTrait;
use Robo\Contract\PrintedInterface;
use Robo\Result;
use Robo\Task\BaseTask;
/**
* Creates Phar.
*
* ``` php
* <?php
* $pharTask = $this->taskPackPhar('package/codecept.phar')
* ->compress()
* ->stub('package/stub.php');
*
* $finder = Finder::create()
* ->name('*.php')
* ->in('src');
*
* foreach ($finder as $file) {
* $pharTask->addFile('src/'.$file->getRelativePathname(), $file->getRealPath());
* }
*
* $finder = Finder::create()->files()
* ->name('*.php')
* ->in('vendor');
*
* foreach ($finder as $file) {
* $pharTask->addStripped('vendor/'.$file->getRelativePathname(), $file->getRealPath());
* }
* $pharTask->run();
*
* // verify Phar is packed correctly
* $code = $this->_exec('php package/codecept.phar');
* ?>
* ```
*/
class PackPhar extends BaseTask implements PrintedInterface, ProgressIndicatorAwareInterface
{
/**
* @var \Phar
*/
protected $phar;
/**
* @var null|string
*/
protected $compileDir = null;
/**
* @var string
*/
protected $filename;
/**
* @var bool
*/
protected $compress = false;
protected $stub;
protected $bin;
/**
* @var string
*/
protected $stubTemplate = <<<EOF
#!/usr/bin/env php
<?php
Phar::mapPhar();
%s
__HALT_COMPILER();
EOF;
/**
* @var string[]
*/
protected $files = [];
/**
* {@inheritdoc}
*/
public function getPrinted()
{
return true;
}
/**
* @param string $filename
*/
public function __construct($filename)
{
$file = new \SplFileInfo($filename);
$this->filename = $filename;
if (file_exists($file->getRealPath())) {
@unlink($file->getRealPath());
}
$this->phar = new \Phar($file->getPathname(), 0, $file->getFilename());
}
/**
* @param bool $compress
*
* @return $this
*/
public function compress($compress = true)
{
$this->compress = $compress;
return $this;
}
/**
* @param string $stub
*
* @return $this
*/
public function stub($stub)
{
$this->phar->setStub(file_get_contents($stub));
return $this;
}
/**
* {@inheritdoc}
*/
public function progressIndicatorSteps()
{
// run() will call advanceProgressIndicator() once for each
// file, one after calling stopBuffering, and again after compression.
return count($this->files)+2;
}
/**
* {@inheritdoc}
*/
public function run()
{
$this->printTaskInfo('Creating {filename}', ['filename' => $this->filename]);
$this->phar->setSignatureAlgorithm(\Phar::SHA1);
$this->phar->startBuffering();
$this->printTaskInfo('Packing {file-count} files into phar', ['file-count' => count($this->files)]);
$this->startProgressIndicator();
foreach ($this->files as $path => $content) {
$this->phar->addFromString($path, $content);
$this->advanceProgressIndicator();
}
$this->phar->stopBuffering();
$this->advanceProgressIndicator();
if ($this->compress and in_array('GZ', \Phar::getSupportedCompression())) {
if (count($this->files) > 1000) {
$this->printTaskInfo('Too many files. Compression DISABLED');
} else {
$this->printTaskInfo('{filename} compressed', ['filename' => $this->filename]);
$this->phar = $this->phar->compressFiles(\Phar::GZ);
}
}
$this->advanceProgressIndicator();
$this->stopProgressIndicator();
$this->printTaskSuccess('{filename} produced', ['filename' => $this->filename]);
return Result::success($this, '', ['time' => $this->getExecutionTime()]);
}
/**
* @param string $path
* @param string $file
*
* @return $this
*/
public function addStripped($path, $file)
{
$this->files[$path] = $this->stripWhitespace(file_get_contents($file));
return $this;
}
/**
* @param string $path
* @param string $file
*
* @return $this
*/
public function addFile($path, $file)
{
$this->files[$path] = file_get_contents($file);
return $this;
}
/**
* @param \Symfony\Component\Finder\SplFileInfo[] $files
*/
public function addFiles($files)
{
foreach ($files as $file) {
$this->addFile($file->getRelativePathname(), $file->getRealPath());
}
}
/**
* @param string $file
*
* @return $this
*/
public function executable($file)
{
$source = file_get_contents($file);
if (strpos($source, '#!/usr/bin/env php') === 0) {
$source = substr($source, strpos($source, '<?php') + 5);
}
$this->phar->setStub(sprintf($this->stubTemplate, $source));
return $this;
}
/**
* Strips whitespace from source. Taken from composer
*
* @param string $source
*
* @return string
*/
private function stripWhitespace($source)
{
if (!function_exists('token_get_all')) {
return $source;
}
$output = '';
foreach (token_get_all($source) as $token) {
if (is_string($token)) {
$output .= $token;
} elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
// $output .= $token[1];
$output .= str_repeat("\n", substr_count($token[1], "\n"));
} elseif (T_WHITESPACE === $token[0]) {
// reduce wide spaces
$whitespace = preg_replace('{[ \t]+}', ' ', $token[1]);
// normalize newlines to \n
$whitespace = preg_replace('{(?:\r\n|\r|\n)}', "\n", $whitespace);
// trim leading spaces
$whitespace = preg_replace('{\n +}', "\n", $whitespace);
$output .= $whitespace;
} else {
$output .= $token[1];
}
}
return $output;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Robo\Task\Development;
use Robo\Task\Base\Exec;
/**
* Runs PHP server and stops it when task finishes.
*
* ``` php
* <?php
* // run server in /public directory
* $this->taskServer(8000)
* ->dir('public')
* ->run();
*
* // run with IP 0.0.0.0
* $this->taskServer(8000)
* ->host('0.0.0.0')
* ->run();
*
* // execute server in background
* $this->taskServer(8000)
* ->background()
* ->run();
* ?>
* ```
*/
class PhpServer extends Exec
{
/**
* @var int
*/
protected $port;
/**
* @var string
*/
protected $host = '127.0.0.1';
/**
* {@inheritdoc}
*/
protected $command = 'php -S %s:%d ';
/**
* @param int $port
*/
public function __construct($port)
{
$this->port = $port;
if (strtolower(PHP_OS) === 'linux') {
$this->command = 'exec php -S %s:%d ';
}
}
/**
* @param string $host
*
* @return $this
*/
public function host($host)
{
$this->host = $host;
return $this;
}
/**
* @param string $path
*
* @return $this
*/
public function dir($path)
{
$this->command .= "-t $path";
return $this;
}
/**
* {@inheritdoc}
*/
public function getCommand()
{
return sprintf($this->command . $this->arguments, $this->host, $this->port);
}
}

View File

@@ -0,0 +1,233 @@
<?php
namespace Robo\Task\Development;
use Robo\Result;
use Robo\Contract\TaskInterface;
use Robo\Exception\TaskException;
/**
* Helps to maintain `.semver` file.
*
* ```php
* <?php
* $this->taskSemVer('.semver')
* ->increment()
* ->run();
* ?>
* ```
*
*/
class SemVer implements TaskInterface
{
const SEMVER = "---\n:major: %d\n:minor: %d\n:patch: %d\n:special: '%s'\n:metadata: '%s'";
const REGEX = "/^\-\-\-\n:major:\s(0|[1-9]\d*)\n:minor:\s(0|[1-9]\d*)\n:patch:\s(0|[1-9]\d*)\n:special:\s'([a-zA-z0-9]*\.?(?:0|[1-9]\d*)?)'\n:metadata:\s'((?:0|[1-9]\d*)?(?:\.[a-zA-z0-9\.]*)?)'/";
/**
* @var string
*/
protected $format = 'v%M.%m.%p%s';
/**
* @var string
*/
protected $specialSeparator = '-';
/**
* @var string
*/
protected $metadataSeparator = '+';
/**
* @var string
*/
protected $path;
/**
* @var array
*/
protected $version = [
'major' => 0,
'minor' => 0,
'patch' => 0,
'special' => '',
'metadata' => ''
];
/**
* @param string $filename
*/
public function __construct($filename = '')
{
$this->path = $filename;
if (file_exists($this->path)) {
$this->parse();
}
}
/**
* @return string
*/
public function __toString()
{
$search = ['%M', '%m', '%p', '%s'];
$replace = $this->version + ['extra' => ''];
foreach (['special', 'metadata'] as $key) {
if (!empty($replace[$key])) {
$separator = $key . 'Separator';
$replace['extra'] .= $this->{$separator} . $replace[$key];
}
unset($replace[$key]);
}
return str_replace($search, $replace, $this->format);
}
/**
* @param string $format
*
* @return $this
*/
public function setFormat($format)
{
$this->format = $format;
return $this;
}
/**
* @param string $separator
*
* @return $this
*/
public function setMetadataSeparator($separator)
{
$this->metadataSeparator = $separator;
return $this;
}
/**
* @param string $separator
*
* @return $this
*/
public function setPrereleaseSeparator($separator)
{
$this->specialSeparator = $separator;
return $this;
}
/**
* @param string $what
*
* @return $this
*
* @throws \Robo\Exception\TaskException
*/
public function increment($what = 'patch')
{
switch ($what) {
case 'major':
$this->version['major']++;
$this->version['minor'] = 0;
$this->version['patch'] = 0;
break;
case 'minor':
$this->version['minor']++;
$this->version['patch'] = 0;
break;
case 'patch':
$this->version['patch']++;
break;
default:
throw new TaskException(
$this,
'Bad argument, only one of the following is allowed: major, minor, patch'
);
}
return $this;
}
/**
* @param string $tag
*
* @return $this
*
* @throws \Robo\Exception\TaskException
*/
public function prerelease($tag = 'RC')
{
if (!is_string($tag)) {
throw new TaskException($this, 'Bad argument, only strings allowed.');
}
$number = 0;
if (!empty($this->version['special'])) {
list($current, $number) = explode('.', $this->version['special']);
if ($tag != $current) {
$number = 0;
}
}
$number++;
$this->version['special'] = implode('.', [$tag, $number]);
return $this;
}
/**
* @param array|string $data
*
* @return $this
*/
public function metadata($data)
{
if (is_array($data)) {
$data = implode('.', $data);
}
$this->version['metadata'] = $data;
return $this;
}
/**
* {@inheritdoc}
*/
public function run()
{
$written = $this->dump();
return new Result($this, (int)($written === false), $this->__toString());
}
/**
* @return bool
*
* @throws \Robo\Exception\TaskException
*/
protected function dump()
{
extract($this->version);
$semver = sprintf(self::SEMVER, $major, $minor, $patch, $special, $metadata);
if (is_writeable($this->path) === false || file_put_contents($this->path, $semver) === false) {
throw new TaskException($this, 'Failed to write semver file.');
}
return true;
}
/**
* @throws \Robo\Exception\TaskException
*/
protected function parse()
{
$output = file_get_contents($this->path);
if (!preg_match_all(self::REGEX, $output, $matches)) {
throw new TaskException($this, 'Bad semver file.');
}
list(, $major, $minor, $patch, $special, $metadata) = array_map('current', $matches);
$this->version = compact('major', 'minor', 'patch', 'special', 'metadata');
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Robo\Task\Development;
trait loadTasks
{
/**
* @param string $filename
*
* @return Changelog
*/
protected function taskChangelog($filename = 'CHANGELOG.md')
{
return $this->task(Changelog::class, $filename);
}
/**
* @param string $filename
*
* @return GenerateMarkdownDoc
*/
protected function taskGenDoc($filename)
{
return $this->task(GenerateMarkdownDoc::class, $filename);
}
/**
* @param string $className
* @param string $wrapperClassName
*
* @return \Robo\Task\Development\GenerateTask
*/
protected function taskGenTask($className, $wrapperClassName = '')
{
return $this->task(GenerateTask::class, $className, $wrapperClassName);
}
/**
* @param string $pathToSemVer
*
* @return SemVer
*/
protected function taskSemVer($pathToSemVer = '.semver')
{
return $this->task(SemVer::class, $pathToSemVer);
}
/**
* @param int $port
*
* @return PhpServer
*/
protected function taskServer($port = 8000)
{
return $this->task(PhpServer::class, $port);
}
/**
* @param string $filename
*
* @return PackPhar
*/
protected function taskPackPhar($filename)
{
return $this->task(PackPhar::class, $filename);
}
/**
* @param string $tag
*
* @return GitHubRelease
*/
protected function taskGitHubRelease($tag)
{
return $this->task(GitHubRelease::class, $tag);
}
/**
* @param string|array $url
*
* @return OpenBrowser
*/
protected function taskOpenBrowser($url)
{
return $this->task(OpenBrowser::class, $url);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Robo\Task\Docker;
use Robo\Common\ExecOneCommand;
use Robo\Contract\PrintedInterface;
use Robo\Task\BaseTask;
abstract class Base extends BaseTask implements PrintedInterface
{
use ExecOneCommand;
/**
* @var string
*/
protected $command = '';
/**
* {@inheritdoc}
*/
public function run()
{
$command = $this->getCommand();
$this->printTaskInfo('Running {command}', ['command' => $command]);
return $this->executeCommand($command);
}
abstract public function getCommand();
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Robo\Task\Docker;
/**
* Builds Docker image
*
* ```php
* <?php
* $this->taskDockerBuild()->run();
*
* $this->taskDockerBuild('path/to/dir')
* ->tag('database')
* ->run();
*
* ?>
*
* ```
*
* Class Build
* @package Robo\Task\Docker
*/
class Build extends Base
{
/**
* @var string
*/
protected $path;
/**
* @param string $path
*/
public function __construct($path = '.')
{
$this->command = "docker build";
$this->path = $path;
}
/**
* {@inheritdoc}
*/
public function getCommand()
{
return $this->command . ' ' . $this->arguments . ' ' . $this->path;
}
/**
* @param string $tag
*
* @return $this
*/
public function tag($tag)
{
return $this->option('-t', $tag);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Robo\Task\Docker;
/**
* Commits docker container to an image
*
* ```
* $this->taskDockerCommit($containerId)
* ->name('my/database')
* ->run();
*
* // alternatively you can take the result from DockerRun task:
*
* $result = $this->taskDockerRun('db')
* ->exec('./prepare_database.sh')
* ->run();
*
* $task->dockerCommit($result)
* ->name('my/database')
* ->run();
* ```
*/
class Commit extends Base
{
/**
* @var string
*/
protected $command = "docker commit";
/**
* @var string
*/
protected $name;
/**
* @var string
*/
protected $cid;
/**
* @param string|\Robo\Task\Docker\Result $cidOrResult
*/
public function __construct($cidOrResult)
{
$this->cid = $cidOrResult instanceof Result ? $cidOrResult->getCid() : $cidOrResult;
}
/**
* {@inheritdoc}
*/
public function getCommand()
{
return $this->command . ' ' . $this->cid . ' ' . $this->name . ' ' . $this->arguments;
}
/**
* @param $name
*
* @return $this
*/
public function name($name)
{
$this->name = $name;
return $this;
}
}

Some files were not shown because too many files have changed in this diff Show More