diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md
index 02226526a..f6d1729a3 100644
--- a/doc/articles/scripts.md
+++ b/doc/articles/scripts.md
@@ -216,3 +216,23 @@ one by prefixing the command name with `@`:
}
}
```
+
+## Calling Composer commands
+
+To call Composer commands, you can use `@composer` which will automatically
+resolve to whatever composer.phar is currently being used:
+
+```json
+{
+ "scripts": {
+ "test": [
+ "@composer install",
+ "phpunit"
+ ],
+ }
+}
+```
+
+One limitation of this is that you can not call multiple composer commands in
+a row like `@composer install && @composer foo`. You must split them up in a
+JSON array of commands.
diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php
index 5e5061e6f..57218f676 100644
--- a/src/Composer/EventDispatcher/EventDispatcher.php
+++ b/src/Composer/EventDispatcher/EventDispatcher.php
@@ -24,6 +24,7 @@ use Composer\Script;
use Composer\Script\CommandEvent;
use Composer\Script\PackageEvent;
use Composer\Util\ProcessExecutor;
+use Symfony\Component\Process\PhpExecutableFinder;
/**
* The Event Dispatcher.
@@ -172,10 +173,25 @@ class EventDispatcher
$scriptName = substr($callable, 1);
$args = $event->getArguments();
$flags = $event->getFlags();
- if (!$this->getListeners(new Event($scriptName))) {
- $this->io->writeError(sprintf('You made a reference to a non-existent script %s', $callable));
+ if (substr($callable, 0, 10) === '@composer ') {
+ $finder = new PhpExecutableFinder();
+ $phpPath = $finder->find();
+ if (!$phpPath) {
+ throw new \RuntimeException('Failed to locate PHP binary to execute '.$scriptName);
+ }
+ $exec = $phpPath . ' ' . realpath($_SERVER['argv'][0]) . substr($callable, 9);
+ if (0 !== ($exitCode = $this->process->execute($exec))) {
+ $this->io->writeError(sprintf('Script %s handling the %s event returned with an error', $callable, $event->getName()));
+
+ throw new \RuntimeException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
+ }
+ } else {
+ if (!$this->getListeners(new Event($scriptName))) {
+ $this->io->writeError(sprintf('You made a reference to a non-existent script %s', $callable));
+ }
+
+ $return = $this->dispatch($scriptName, new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags));
}
- $return = $this->dispatch($scriptName, new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags));
} elseif ($this->isPhpScript($callable)) {
$className = substr($callable, 0, strpos($callable, '::'));
$methodName = substr($callable, strpos($callable, '::') + 2);
diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php
index ff2ba70d8..10ad4376a 100644
--- a/src/Composer/Package/Loader/ArrayLoader.php
+++ b/src/Composer/Package/Loader/ArrayLoader.php
@@ -171,6 +171,9 @@ class ArrayLoader implements LoaderInterface
foreach ($config['scripts'] as $event => $listeners) {
$config['scripts'][$event] = (array) $listeners;
}
+ if (isset($config['scripts']['composer'])) {
+ trigger_error('The `composer` script name is reserved for internal use, please avoid defining it', E_USER_DEPRECATED);
+ }
$package->setScripts($config['scripts']);
}