Skip to content

Commit

Permalink
Add experimental Robo plugin feature (#671)
Browse files Browse the repository at this point in the history
  • Loading branch information
ademarco authored and greg-1-anderson committed Feb 6, 2018
1 parent 79c04e5 commit cd88c29
Show file tree
Hide file tree
Showing 12 changed files with 473 additions and 23 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,9 @@
# Changelog

### Unreleased

* *Breaking* Typo in `\Robo\Runner:errorCondtion()` fixed as `\Robo\Runner:errorCondition()`.

### 1.2.1 12/28/2017

* Fixes to tests / build only.
Expand Down
60 changes: 60 additions & 0 deletions docs/extending.md
Expand Up @@ -33,6 +33,66 @@ Once you have done this, all of the tasks defined in the extension you selected

Note that at the moment, it is not possible to extend Robo when using the robo.phar. This capability may be added in the future via [embedded composer](https://github.com/dflydev/dflydev-embedded-composer).

## Register command files via PSR-4 autoloading

You can have your project expose extra Robo command files by providing them within your project's PSR-4 namespace.

For example, given the following PSR-4 namespace in your `composer.json`:

```json
{
"autoload": {
"psr-4": {
"MyProject\\": "./src/"
}
}
}
```

Extra command files can be exposed by creating one or more classes under `./src/Robo/Plugin/Commands`, as shown in the
example below:

```php
<?php

namespace MyProject\Robo\Plugin\Commands;

class MyCustomCommands extends \Robo\Tasks
{
/**
* @command my-project:command-one
*/
public function commandOne() { }

/**
* @command my-project:command-two
*/
public function commandTwo() { }
}
```

Please note: command files classes must be placed under `Robo/Plugin/Commands` relative namespace and their name
must end in `Commands.php`.

You can now access your new commands via Robo:

```
$ ./vendor/bin/robo
$ ./robo
Robo 1.2.2-dev
Usage:
command [options] [arguments]
...
Available commands:
help Displays help for a command
list Lists commands
my-project
my-project:command-one
my-project:command-two
```

## Creating a Robo Extension

A Robo tasks extension is created by advertising a Composer package of type `robo-tasks` on [Packagist](https://packagist.org/). For an overview on how this is done, see the article [Creating your very own Composer Package](https://knpuniversity.com/screencast/question-answer-day/create-composer-package). Specific instructions for creating Robo task extensions are provided below.
Expand Down
24 changes: 13 additions & 11 deletions robo
@@ -1,22 +1,24 @@
#!/usr/bin/env php
<?php

/**
* if we're running from phar load the phar autoload,
* else let the script 'robo' search for the autoloader
* else let the script 'robo' search for the autoloader.
* If we cannot find a vendor/autoload.php file, then
* we will go ahead and try the phar.
*/
if (strpos(basename(__FILE__), 'phar')) {
require_once 'phar://robo.phar/vendor/autoload.php';
} else {
$autoloaderPath = 'phar://robo.phar/vendor/autoload.php';
if (!strpos(basename(__FILE__), 'phar')) {
if (file_exists(__DIR__.'/vendor/autoload.php')) {
require_once __DIR__.'/vendor/autoload.php';
$autoloaderPath = __DIR__.'/vendor/autoload.php';
} elseif (file_exists(__DIR__.'/../../autoload.php')) {
require_once __DIR__ . '/../../autoload.php';
} else {
require_once 'phar://robo.phar/vendor/autoload.php';
$autoloaderPath = __DIR__ . '/../../autoload.php';
}
}
$classLoader = require $autoloaderPath;
$runner = new \Robo\Runner();
$runner->setSelfUpdateRepository('consolidation/robo');
$runner
->setRelativePluginNamespace('Robo\Plugin')
->setSelfUpdateRepository('consolidation/robo')
->setClassLoader($classLoader);
$statusCode = $runner->execute($_SERVER['argv']);
exit($statusCode);
exit($statusCode);

This comment has been minimized.

Copy link
@christopher-hopper

This comment has been minimized.

Copy link
@greg-1-anderson

greg-1-anderson Feb 9, 2018

Member

Why the sour face, @christopher-hopper? Please open a new issue with more info if you have any feedback. Thanks.

This comment has been minimized.

Copy link
@christopher-hopper

christopher-hopper May 3, 2018

necropost
Happy about plugin discovery. That's how I got here.
Just a silly face about line endings being removed at the end of the files.

26 changes: 26 additions & 0 deletions src/ClassDiscovery/AbstractClassDiscovery.php
@@ -0,0 +1,26 @@
<?php

namespace Robo\ClassDiscovery;

/**
* Class AbstractClassDiscovery
*
* @package Robo\ClassDiscovery
*/
abstract class AbstractClassDiscovery implements ClassDiscoveryInterface
{
/**
* @var string
*/
protected $searchPattern = '*.php';

/**
* {@inheritdoc}
*/
public function setSearchPattern($searchPattern)
{
$this->searchPattern = $searchPattern;

return $this;
}
}
30 changes: 30 additions & 0 deletions src/ClassDiscovery/ClassDiscoveryInterface.php
@@ -0,0 +1,30 @@
<?php

namespace Robo\ClassDiscovery;

/**
* Interface ClassDiscoveryInterface
*
* @package Robo\Plugin\ClassDiscovery
*/
interface ClassDiscoveryInterface
{
/**
* @param $searchPattern
*
* @return $this
*/
public function setSearchPattern($searchPattern);

/**
* @return string[]
*/
public function getClasses();

/**
* @param $class
*
* @return string|null
*/
public function getFile($class);
}
112 changes: 112 additions & 0 deletions src/ClassDiscovery/RelativeNamespaceDiscovery.php
@@ -0,0 +1,112 @@
<?php

namespace Robo\ClassDiscovery;

use Symfony\Component\Finder\Finder;
use Composer\Autoload\ClassLoader;

/**
* Class RelativeNamespaceDiscovery
*
* @package Robo\Plugin\ClassDiscovery
*/
class RelativeNamespaceDiscovery extends AbstractClassDiscovery
{
/**
* @var \Composer\Autoload\ClassLoader
*/
protected $classLoader;

/**
* @var string
*/
protected $relativeNamespace = '';

/**
* RelativeNamespaceDiscovery constructor.
*
* @param \Composer\Autoload\ClassLoader $classLoader
*/
public function __construct(ClassLoader $classLoader)
{
$this->classLoader = $classLoader;
}

/**
* @param string $relativeNamespace
*
* @return RelativeNamespaceDiscovery
*/
public function setRelativeNamespace($relativeNamespace)
{
$this->relativeNamespace = $relativeNamespace;

return $this;
}

/**
* @inheritDoc
*/
public function getClasses()
{
$classes = [];
$relativePath = $this->convertNamespaceToPath($this->relativeNamespace);

foreach ($this->classLoader->getPrefixesPsr4() as $baseNamespace => $directories) {
$directories = array_filter(array_map(function ($directory) use ($relativePath) {
return $directory.$relativePath;
}, $directories), 'is_dir');

if ($directories) {
foreach ($this->search($directories, $this->searchPattern) as $file) {
$relativePathName = $file->getRelativePathname();
$classes[] = $baseNamespace.$this->convertPathToNamespace($relativePath.'/'.$relativePathName);
}
}
}

return $classes;
}

/**
* {@inheritdoc}
*/
public function getFile($class)
{
return $this->classLoader->findFile($class);
}

/**
* @param $directories
* @param $pattern
*
* @return \Symfony\Component\Finder\Finder
*/
protected function search($directories, $pattern)
{
$finder = new Finder();
$finder->files()
->name($pattern)
->in($directories);

return $finder;
}

/**
* @param $path
*
* @return mixed
*/
protected function convertPathToNamespace($path)
{
return str_replace(['/', '.php'], ['\\', ''], trim($path, '/'));
}

/**
* @return string
*/
public function convertNamespaceToPath($namespace)
{
return '/'.str_replace("\\", '/', trim($namespace, '\\'));
}
}
15 changes: 12 additions & 3 deletions src/Robo.php
@@ -1,6 +1,7 @@
<?php
namespace Robo;

use Composer\Autoload\ClassLoader;
use League\Container\Container;
use League\Container\ContainerInterface;
use Robo\Common\ProcessExecutor;
Expand Down Expand Up @@ -127,10 +128,11 @@ public static function loadConfiguration($paths, $config = null)
* @param null|\Symfony\Component\Console\Output\OutputInterface $output
* @param null|\Robo\Application $app
* @param null|ConfigInterface $config
* @param null|\Composer\Autoload\ClassLoader $classLoader
*
* @return \League\Container\Container|\League\Container\ContainerInterface
*/
public static function createDefaultContainer($input = null, $output = null, $app = null, $config = null)
public static function createDefaultContainer($input = null, $output = null, $app = null, $config = null, $classLoader = null)
{
// Do not allow this function to be called more than once.
if (static::hasContainer()) {
Expand All @@ -147,7 +149,7 @@ public static function createDefaultContainer($input = null, $output = null, $ap

// Set up our dependency injection container.
$container = new Container();
static::configureContainer($container, $app, $config, $input, $output);
static::configureContainer($container, $app, $config, $input, $output, $classLoader);

// Set the application dispatcher
$app->setDispatcher($container->get('eventDispatcher'));
Expand Down Expand Up @@ -175,8 +177,9 @@ public static function createDefaultContainer($input = null, $output = null, $ap
* @param ConfigInterface $config
* @param null|\Symfony\Component\Console\Input\InputInterface $input
* @param null|\Symfony\Component\Console\Output\OutputInterface $output
* @param null|\Composer\Autoload\ClassLoader $classLoader
*/
public static function configureContainer(ContainerInterface $container, SymfonyApplication $app, ConfigInterface $config, $input = null, $output = null)
public static function configureContainer(ContainerInterface $container, SymfonyApplication $app, ConfigInterface $config, $input = null, $output = null, $classLoader = null)
{
// Self-referential container refernce for the inflector
$container->add('container', $container);
Expand All @@ -189,6 +192,9 @@ public static function configureContainer(ContainerInterface $container, Symfony
if (!$output) {
$output = new \Symfony\Component\Console\Output\ConsoleOutput();
}
if (!$classLoader) {
$classLoader = new ClassLoader();
}
$config->set(Config::DECORATED, $output->isDecorated());
$config->set(Config::INTERACTIVE, $input->isInteractive());

Expand All @@ -197,6 +203,7 @@ public static function configureContainer(ContainerInterface $container, Symfony
$container->share('input', $input);
$container->share('output', $output);
$container->share('outputAdapter', \Robo\Common\OutputAdapter::class);
$container->share('classLoader', $classLoader);

// Register logging and related services.
$container->share('logStyler', \Robo\Log\RoboLogStyle::class);
Expand Down Expand Up @@ -245,6 +252,8 @@ function ($output, $message) use ($container) {
);
$container->share('commandFactory', \Consolidation\AnnotatedCommand\AnnotatedCommandFactory::class)
->withMethodCall('setCommandProcessor', ['commandProcessor']);
$container->share('relativeNamespaceDiscovery', \Robo\ClassDiscovery\RelativeNamespaceDiscovery::class)
->withArgument('classLoader');

// Deprecated: favor using collection builders to direct use of collections.
$container->add('collection', \Robo\Collection\Collection::class);
Expand Down

0 comments on commit cd88c29

Please sign in to comment.