diff --git a/doc/faqs/triggers.md b/doc/faqs/triggers.md
new file mode 100644
index 000000000..65fc3c846
--- /dev/null
+++ b/doc/faqs/triggers.md
@@ -0,0 +1,70 @@
+# Triggers
+
+## What is a trigger?
+
+A trigger is an event that runs a script in a static method, defined by a
+package or project. This event is raised before and after each action (install,
+update).
+
+
+## Where are the event types defined?
+
+It is in the constant property in `Composer\Trigger\TriggerEvents` class.
+
+
+## How is it defined?
+
+It is defined by adding the `triggers` key in the `extra` key to a project's
+`composer.json` or package's `composer.json`.
+
+It is specified as an associative array of classes with her static method,
+associated with the event's type.
+
+The PSR-0 must be defined, otherwise the trigger will not be triggered.
+
+For any given package:
+
+```json
+{
+ "extra": {
+ "triggers": {
+ "MyVendor\MyPackage\MyClass::myStaticMethod" : "post_install",
+ "MyVendor\MyPackage\MyClass::myStaticMethod2" : "post_update",
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "MyVendor\MyPackage": ""
+ }
+ }
+}
+```
+
+For any given project:
+```json
+{
+ "extra": {
+ "triggers": {
+ "MyVendor\MyPackage2\MyClass2::myStaticMethod2" : "post_install",
+ "MyVendor\MyPackage2\MyClass2::myStaticMethod3" : "post_update",
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "MyVendor\MyPackage": "my/folder/path/that/contains/triggers/from/the/root/project"
+ }
+ }
+}
+```
+
+## Informations:
+
+The project's triggers are executed after the package's triggers.
+A declared trigger with non existent file will be ignored.
+
+For example:
+If you declare a trigger for a package pre install, as this trigger isn't
+downloaded yet, it won't run.
+
+On the other hand, if you declare a pre-update package trigger, as the file
+already exist, the actual vendor's version of the trigger will be run.
diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php
index bf8bca726..c10b0cfb7 100644
--- a/src/Composer/Command/InstallCommand.php
+++ b/src/Composer/Command/InstallCommand.php
@@ -12,6 +12,10 @@
namespace Composer\Command;
+use Composer\Trigger\TriggerEvents;
+
+use Composer\Trigger\TriggerDispatcher;
+
use Composer\Autoload\AutoloadGenerator;
use Composer\DependencyResolver;
use Composer\DependencyResolver\Pool;
@@ -65,6 +69,7 @@ EOT
$dryRun = (Boolean) $input->getOption('dry-run');
$verbose = $dryRun || $input->getOption('verbose');
$composer = $this->getComposer();
+ $dispatcher = new TriggerDispatcher($this->getApplication());
if ($preferSource) {
$composer->getDownloadManager()->setPreferSource(true);
@@ -82,6 +87,12 @@ EOT
$pool->addRepository($repository);
}
+ // dispatch pre event
+ if (!$dryRun) {
+ $eventName = $update ? TriggerEvents::PRE_UPDATE : TriggerEvents::PRE_INSTALL;
+ $dispatcher->dispatch($eventName);
+ }
+
// creating requirements request
$request = new Request($pool);
if ($update) {
@@ -177,6 +188,10 @@ EOT
$output->writeln('Generating autoload files');
$generator = new AutoloadGenerator;
$generator->dump($localRepo, $composer->getPackage(), $installationManager, $installationManager->getVendorPath().'/.composer');
+
+ // dispatch post event
+ $eventName = $update ? TriggerEvents::POST_UPDATE : TriggerEvents::POST_INSTALL;
+ $dispatcher->dispatch($eventName);
}
}
diff --git a/src/Composer/Trigger/GetTriggerEvent.php b/src/Composer/Trigger/GetTriggerEvent.php
new file mode 100644
index 000000000..940e0d0e0
--- /dev/null
+++ b/src/Composer/Trigger/GetTriggerEvent.php
@@ -0,0 +1,108 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Trigger;
+
+use Composer\Console\Application;
+
+/**
+ * The Trigger Event.
+ *
+ * @author François Pluchino
+ */
+class GetTriggerEvent
+{
+ /**
+ * @var TriggerDispatcher Dispatcher that dispatched this event
+ */
+ private $dispatcher;
+
+ /**
+ * @var string This event's name
+ */
+ private $name;
+
+ /**
+ * @var Application The application instance
+ */
+ private $application;
+
+ /**
+ * Returns the TriggerDispatcher that dispatches this Event
+ *
+ * @return TriggerDispatcher
+ */
+ public function getDispatcher()
+ {
+ return $this->dispatcher;
+ }
+
+ /**
+ * Stores the TriggerDispatcher that dispatches this Event
+ *
+ * @param TriggerDispatcher $dispatcher
+ */
+ public function setDispatcher(TriggerDispatcher $dispatcher)
+ {
+ $this->dispatcher = $dispatcher;
+ }
+
+ /**
+ * Returns the event's name.
+ *
+ * @return string The event name
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Stores the event's name.
+ *
+ * @param string $name The event name
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * Returns the application instance.
+ *
+ * @return Application
+ */
+ public function getApplication()
+ {
+ return $this->application;
+ }
+
+ /**
+ * Stores the application instance.
+ *
+ * @param Application $application
+ */
+ public function setApplication(Application $application)
+ {
+ $this->application = $application;
+ }
+
+ /**
+ * Returns the composer instance.
+ *
+ * @return Composer
+ */
+ public function getComposer()
+ {
+ return $this->application->getComposer();
+ }
+}
diff --git a/src/Composer/Trigger/TriggerDispatcher.php b/src/Composer/Trigger/TriggerDispatcher.php
new file mode 100644
index 000000000..ad80d7bd3
--- /dev/null
+++ b/src/Composer/Trigger/TriggerDispatcher.php
@@ -0,0 +1,184 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Trigger;
+
+use Composer\Json\JsonFile;
+use Composer\Repository\FilesystemRepository;
+use Composer\Autoload\ClassLoader;
+use Composer\Package\PackageInterface;
+use Composer\Console\Application;
+use Composer\Composer;
+
+/**
+ * The Trigger Dispatcher.
+ *
+ * Example in command:
+ * $dispatcher = new TriggerDispatcher($this->getApplication());
+ * // ...
+ * $dispatcher->dispatch(TriggerEvents::PRE_INSTALL);
+ * // ...
+ *
+ * @author François Pluchino
+ */
+class TriggerDispatcher
+{
+ protected $application;
+ protected $loader;
+
+ /**
+ * Constructor.
+ *
+ * @param Application $application
+ */
+ public function __construct(Application $application)
+ {
+ $this->application = $application;
+ $this->loader = new ClassLoader();
+ }
+
+ /**
+ * Dispatch the event.
+ *
+ * @param string $eventName The constant in TriggerEvents
+ */
+ public function dispatch($eventName)
+ {
+ $event = new GetTriggerEvent();
+
+ $event->setDispatcher($this);
+ $event->setName($eventName);
+ $event->setApplication($this->application);
+
+ $this->doDispatch($event);
+ }
+
+ /**
+ * Triggers the listeners of an event.
+ *
+ * @param GetTriggerEvent $event The event object to pass to the event handlers/listeners.
+ */
+ protected function doDispatch(GetTriggerEvent $event)
+ {
+ $listeners = $this->getListeners($event);
+
+ foreach ($listeners as $method => $eventType) {
+ if ($eventType === $event->getName()) {
+ $className = substr($method, 0, strpos($method, '::'));
+ $methodName = substr($method, strpos($method, '::') + 2);
+
+ try {
+ $refMethod = new \ReflectionMethod($className, $methodName);
+
+ // execute only if all conditions are validates
+ if ($refMethod->isPublic()
+ && $refMethod->isStatic()
+ && !$refMethod->isAbstract()
+ && 1 === $refMethod->getNumberOfParameters()) {
+ $className::$methodName($event);
+ }
+
+ } catch (\ReflectionException $ex) {}//silent execpetion
+ }
+ }
+ }
+
+ /**
+ * Register namespaces in ClassLoader.
+ *
+ * @param GetTriggerEvent $event The event object
+ *
+ * @return array The listener classes with event type
+ */
+ protected function getListeners(GetTriggerEvent $event)
+ {
+ $listeners = array();
+ $composer = $this->application->getComposer();
+ $vendorDir = $composer->getInstallationManager()->getVendorPath(true);
+ $installedFile = $vendorDir . '/.composer/installed.json';
+
+ // get the list of package installed
+ // $composer->getRepositoryManager()->getLocalRepository() not used
+ // because the list is not refreshed for the post event
+ $fsr = new FilesystemRepository(new JsonFile($installedFile));
+ $packages = $fsr->getPackages();
+
+ foreach ($packages as $package) {
+ $listeners = array_merge_recursive($listeners, $this->getListenerClasses($package));
+ }
+
+ // add root package
+ $listeners = array_merge_recursive($listeners, $this->getListenerClasses($composer->getPackage(), true));
+
+ return $listeners;
+ }
+
+ /**
+ * Get listeners and register the namespace on Classloader.
+ *
+ * @param PackageInterface $package The package objet
+ * @param boolean $root For root composer
+ *
+ * @return array The listener classes with event type
+ */
+ private function getListenerClasses(PackageInterface $package, $root = false)
+ {
+ $composer = $this->application->getComposer();
+ $installDir = $composer->getInstallationManager()->getVendorPath(true)
+ . '/' . $package->getName();
+ $ex = $package->getExtra();
+ $al = $package->getAutoload();
+ $searchListeners = array();
+ $searchNamespaces = array();
+ $listeners = array();
+ $namespaces = array();
+
+ // get classes
+ if (isset($ex['triggers'])) {
+ foreach ($ex['triggers'] as $method => $event) {
+ $searchListeners[$method] = $event;
+ }
+ }
+
+ // get namespaces
+ if (isset($al['psr-0'])) {
+ foreach ($al['psr-0'] as $ns => $path) {
+ $dir = $root ? realpath('.') : $installDir;
+
+ $path = trim($dir . '/' . $path, '/');
+ $searchNamespaces[$ns] = $path;
+ }
+ }
+
+ // filter class::method have not a namespace registered
+ foreach ($searchNamespaces as $ns => $path) {
+ foreach ($searchListeners as $method => $event) {
+ if (0 === strpos($method, $ns)) {
+ $listeners[$method] = $event;
+
+ if (!in_array($ns, array_keys($namespaces))) {
+ $namespaces[$ns] = $path;
+ }
+ }
+ }
+ }
+
+ // register namespaces in class loader
+ foreach ($namespaces as $ns => $path) {
+ $this->loader->add($ns, $path);
+ }
+
+ $this->loader->register();
+
+ return $listeners;
+ }
+}
diff --git a/src/Composer/Trigger/TriggerEvents.php b/src/Composer/Trigger/TriggerEvents.php
new file mode 100644
index 000000000..19d542938
--- /dev/null
+++ b/src/Composer/Trigger/TriggerEvents.php
@@ -0,0 +1,65 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Trigger;
+
+/**
+ * The Trigger Events.
+ *
+ * @author François Pluchino
+ */
+class TriggerEvents
+{
+ /**
+ * The PRE_INSTALL event occurs at begging installation packages.
+ *
+ * This event allows you to execute a trigger before any other code in the
+ * composer is executed. The event listener method receives a
+ * Composer\Trigger\GetTriggerEvent instance.
+ *
+ * @var string
+ */
+ const PRE_INSTALL = 'pre_install';
+
+ /**
+ * The POST_INSTALL event occurs at end installation packages.
+ *
+ * This event allows you to execute a trigger after any other code in the
+ * composer is executed. The event listener method receives a
+ * Composer\Trigger\GetTriggerEvent instance.
+ *
+ * @var string
+ */
+ const POST_INSTALL = 'post_install';
+
+ /**
+ * The PRE_UPDATE event occurs at begging update packages.
+ *
+ * This event allows you to execute a trigger before any other code in the
+ * composer is executed. The event listener method receives a
+ * Composer\Trigger\GetTriggerEvent instance.
+ *
+ * @var string
+ */
+ const PRE_UPDATE = 'pre_update';
+
+ /**
+ * The POST_UPDATE event occurs at end update packages.
+ *
+ * This event allows you to execute a trigger after any other code in the
+ * composer is executed. The event listener method receives a
+ * Composer\Trigger\GetTriggerEvent instance.
+ *
+ * @var string
+ */
+ const POST_UPDATE = 'post_update';
+}