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'; +}