Browse Source

Merge remote-tracking branch 'giosh94mhz/script_groups'

Jordi Boggiano 9 years ago
parent
commit
db5ef12540

+ 15 - 0
doc/articles/scripts.md

@@ -200,3 +200,18 @@ simply running `composer test`:
 
 
 > **Note:** Composer's bin-dir is pushed on top of the PATH so that binaries
 > **Note:** Composer's bin-dir is pushed on top of the PATH so that binaries
 > of dependencies are easily accessible as CLI commands when writing scripts.
 > of dependencies are easily accessible as CLI commands when writing scripts.
+
+Composer script can also called from other scripts, by prefixing the command name
+by `@`. For example the following syntax is valid:
+
+```json
+{
+    "scripts": {
+        "test": [
+            "@clearCache",
+            "phpunit"
+        ],
+        "clearCache": "rm -rf cache/*"
+    }
+}
+```

+ 52 - 0
src/Composer/EventDispatcher/EventDispatcher.php

@@ -45,6 +45,7 @@ class EventDispatcher
     protected $loader;
     protected $loader;
     protected $process;
     protected $process;
     protected $listeners;
     protected $listeners;
+    private $eventStack;
 
 
     /**
     /**
      * Constructor.
      * Constructor.
@@ -58,6 +59,7 @@ class EventDispatcher
         $this->composer = $composer;
         $this->composer = $composer;
         $this->io = $io;
         $this->io = $io;
         $this->process = $process ?: new ProcessExecutor($io);
         $this->process = $process ?: new ProcessExecutor($io);
+        $this->eventStack = array();
     }
     }
 
 
     /**
     /**
@@ -145,11 +147,21 @@ class EventDispatcher
     {
     {
         $listeners = $this->getListeners($event);
         $listeners = $this->getListeners($event);
 
 
+        $this->pushEvent($event);
+
         $return = 0;
         $return = 0;
         foreach ($listeners as $callable) {
         foreach ($listeners as $callable) {
             if (!is_string($callable) && is_callable($callable)) {
             if (!is_string($callable) && is_callable($callable)) {
                 $event = $this->checkListenerExpectedEvent($callable, $event);
                 $event = $this->checkListenerExpectedEvent($callable, $event);
                 $return = false === call_user_func($callable, $event) ? 1 : 0;
                 $return = false === call_user_func($callable, $event) ? 1 : 0;
+            } elseif ($this->isComposerScript($callable)) {
+                if ($this->io->isVerbose()) {
+                    $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable));
+                } else {
+                    $this->io->writeError(sprintf('> %s', $callable));
+                }
+                $scriptName = substr($callable, 1);
+                $return = $this->dispatch($scriptName, new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode()));
             } elseif ($this->isPhpScript($callable)) {
             } elseif ($this->isPhpScript($callable)) {
                 $className = substr($callable, 0, strpos($callable, '::'));
                 $className = substr($callable, 0, strpos($callable, '::'));
                 $methodName = substr($callable, strpos($callable, '::') + 2);
                 $methodName = substr($callable, strpos($callable, '::') + 2);
@@ -190,6 +202,8 @@ class EventDispatcher
             }
             }
         }
         }
 
 
+        $this->popEvent();
+
         return $return;
         return $return;
     }
     }
 
 
@@ -362,4 +376,42 @@ class EventDispatcher
     {
     {
         return false === strpos($callable, ' ') && false !== strpos($callable, '::');
         return false === strpos($callable, ' ') && false !== strpos($callable, '::');
     }
     }
+
+    /**
+     * Checks if string given references a composer run-script
+     *
+     * @param  string $callable
+     * @return bool
+     */
+    protected function isComposerScript($callable)
+    {
+        return '@' === substr($callable, 0, 1);
+    }
+
+    /**
+     * Push an event to the stack of active event
+     *
+     * @param  Event             $event
+     * @throws \RuntimeException
+     * @return number
+     */
+    protected function pushEvent(Event $event)
+    {
+        $eventName = $event->getName();
+        if (in_array($eventName, $this->eventStack)) {
+            throw new \RuntimeException(sprintf("Recursive call to '%s' detected", $eventName));
+        }
+
+        return array_push($this->eventStack, $eventName);
+    }
+
+    /**
+     * Pops the active event from the stack
+     *
+     * @return mixed
+     */
+    protected function popEvent()
+    {
+        return array_pop($this->eventStack);
+    }
 }
 }

+ 91 - 0
tests/Composer/Test/EventDispatcher/EventDispatcherTest.php

@@ -142,6 +142,97 @@ class EventDispatcherTest extends TestCase
         $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false);
         $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false);
     }
     }
 
 
+    public function testDispatcherCanExecuteComposerScriptGroups()
+    {
+        $process = $this->getMock('Composer\Util\ProcessExecutor');
+            $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
+            ->setConstructorArgs(array(
+                $composer = $this->getMock('Composer\Composer'),
+                $io = $this->getMock('Composer\IO\IOInterface'),
+                $process,
+            ))
+            ->setMethods(array(
+                'getListeners',
+            ))
+            ->getMock();
+
+        $process->expects($this->exactly(3))
+            ->method('execute')
+            ->will($this->returnValue(0));
+
+        $dispatcher->expects($this->atLeastOnce())
+            ->method('getListeners')
+            ->will($this->returnCallback(function (Event $event) {
+                if ($event->getName() === 'root') {
+                    return array('@group');
+                } elseif ($event->getName() === 'group') {
+                    return array('echo -n foo', '@subgroup', 'echo -n bar');
+                } elseif ($event->getName() === 'subgroup') {
+                    return array('echo -n baz');
+                }
+
+                return array();
+            }));
+
+        $io->expects($this->any())
+            ->method('isVerbose')
+            ->willReturn(1);
+
+        $io->expects($this->at(1))
+            ->method('writeError')
+            ->with($this->equalTo('> root: @group'));
+
+        $io->expects($this->at(3))
+            ->method('writeError')
+            ->with($this->equalTo('> group: echo -n foo'));
+
+        $io->expects($this->at(5))
+            ->method('writeError')
+            ->with($this->equalTo('> group: @subgroup'));
+
+        $io->expects($this->at(7))
+            ->method('writeError')
+            ->with($this->equalTo('> subgroup: echo -n baz'));
+
+        $io->expects($this->at(9))
+            ->method('writeError')
+            ->with($this->equalTo('> group: echo -n bar'));
+
+        $dispatcher->dispatch('root', new CommandEvent('root', $composer, $io));
+    }
+
+    /**
+     * @expectedException RuntimeException
+     */
+    public function testDispatcherDetectInfiniteRecursion()
+    {
+        $process = $this->getMock('Composer\Util\ProcessExecutor');
+        $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
+        ->setConstructorArgs(array(
+            $composer = $this->getMock('Composer\Composer'),
+            $io = $this->getMock('Composer\IO\IOInterface'),
+            $process,
+        ))
+        ->setMethods(array(
+            'getListeners',
+        ))
+        ->getMock();
+
+        $dispatcher->expects($this->atLeastOnce())
+            ->method('getListeners')
+            ->will($this->returnCallback(function (Event $event) {
+                if ($event->getName() === 'root') {
+                    return array('@recurse');
+                } elseif ($event->getName() === 'recurse') {
+                    return array('@root');
+                }
+
+                return array();
+            }));
+
+        $dispatcher->dispatch('root', new CommandEvent('root', $composer, $io));
+    }
+
     private function getDispatcherStubForListenersTest($listeners, $io)
     private function getDispatcherStubForListenersTest($listeners, $io)
     {
     {
         $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')
         $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')