diff --git a/doc/faqs/triggers.md b/doc/faqs/triggers.md new file mode 100644 index 000000000..14bccbef5 --- /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 +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 array of classes with her static method, +in associative array define the event's type. + +The PSR-0 must be defined, otherwise the trigger will not be triggered. + +For any given project: + +```json +{ + "extra": { + "triggers": { + "post_install": [ + "MyVendor\\MyRootPackage\\MyClass::myStaticMethod" + ], + "post_update": [ + "MyVendor\\MyRootPackage\\MyClass::myStaticMethod2" + ] + } + }, + "autoload": { + "psr-0": { + "MyVendor\\MyRootPackage": "my/folder/path/that/contains/triggers/from/the/root/project" + } + } +} +``` + +Trigger Example: + +```php +getOption('dry-run'); $verbose = $dryRun || $input->getOption('verbose'); $composer = $this->getComposer(); + $io = $this->getApplication()->getIO(); + $dispatcher = new TriggerDispatcher($this->getComposer(), $io); if ($preferSource) { $composer->getDownloadManager()->setPreferSource(true); @@ -82,6 +86,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 +187,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/TriggerDispatcher.php b/src/Composer/Trigger/TriggerDispatcher.php new file mode 100644 index 000000000..e63410da5 --- /dev/null +++ b/src/Composer/Trigger/TriggerDispatcher.php @@ -0,0 +1,147 @@ + + * 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\IO\IOInterface; +use Composer\Composer; + +/** + * The Trigger Dispatcher. + * + * Example in command: + * $dispatcher = new TriggerDispatcher($this->getComposer(), $this->getApplication()->getIO()); + * // ... + * $dispatcher->dispatch(TriggerEvents::POST_INSTALL); + * // ... + * + * @author François Pluchino + */ +class TriggerDispatcher +{ + protected $composer; + protected $io; + protected $loader; + + /** + * Constructor. + * + * @param Composer $composer The composer instance + * @param IOInterface $io The IOInterface instance + */ + public function __construct(Composer $composer, IOInterface $io) + { + $this->composer = $composer; + $this->io = $io; + $this->loader = new ClassLoader(); + } + + /** + * Dispatch the event. + * + * @param string $eventName The constant in TriggerEvents + */ + public function dispatch($eventName) + { + $event = new TriggerEvent($eventName, $this->composer, $this->io); + + $this->doDispatch($event); + } + + /** + * Triggers the listeners of an event. + * + * @param TriggerEvent $event The event object to pass to the event handlers/listeners. + */ + protected function doDispatch(TriggerEvent $event) + { + $listeners = $this->getListeners($event); + + foreach ($listeners as $method) { + $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 TriggerEvent $event The event object + * + * @return array The listener classes with event type + */ + protected function getListeners(TriggerEvent $event) + { + $package = $this->composer->getPackage(); + $vendorDir = $this->composer->getInstallationManager()->getVendorPath(true); + $autoloadFile = $vendorDir . '/.composer/autoload.php'; + $ex = $package->getExtra(); + $al = $package->getAutoload(); + $searchListeners = array(); + $listeners = array(); + $namespaces = array(); + + // get classes + if (isset($ex['triggers'][$event->getName()])) { + foreach ($ex['triggers'][$event->getName()] as $method) { + $searchListeners[] = $method; + } + } + + // get autoload namespaces + if (file_exists($autoloadFile)) { + $this->loader = require $autoloadFile; + } + + $namespaces = $this->loader->getPrefixes(); + + // get namespaces in composer.json project + if (isset($al['psr-0'])) { + foreach ($al['psr-0'] as $ns => $path) { + if (!isset($namespaces[str_replace('\\', '\\\\', $ns)])) { + $this->loader->add($ns, trim(realpath('.').'/'.$path, '/')); + } + } + + $this->loader->register(); + $namespaces = $this->loader->getPrefixes(); + } + + // filter class::method have not a namespace registered + foreach ($namespaces as $ns => $path) { + foreach ($searchListeners as $method) { + if (0 === strpos($method, $ns)) { + $listeners[] = $method; + } + } + } + + return $listeners; + } +} diff --git a/src/Composer/Trigger/TriggerEvent.php b/src/Composer/Trigger/TriggerEvent.php new file mode 100644 index 000000000..dce3fa549 --- /dev/null +++ b/src/Composer/Trigger/TriggerEvent.php @@ -0,0 +1,83 @@ + + * 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\Composer; +use Composer\IO\IOInterface; + +/** + * The Trigger Event. + * + * @author François Pluchino + */ +class TriggerEvent +{ + /** + * @var string This event's name + */ + private $name; + + /** + * @var Composer The composer instance + */ + private $composer; + + /** + * @var IOInterface The IO instance + */ + private $io; + + /** + * Constructor. + * + * @param string $name The event name + * @param Composer $composer The composer objet + * @param IOInterface $io The IOInterface object + */ + public function __construct($name, Composer $composer, IOInterface $io) + { + $this->name = $name; + $this->composer = $composer; + $this->io = $io; + } + + /** + * Returns the event's name. + * + * @return string The event name + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the composer instance. + * + * @return Composer + */ + public function getComposer() + { + return $this->composer; + } + + /** + * Returns the IO instance. + * + * @return IOInterface + */ + public function getIO() + { + return $this->io; + } +} diff --git a/src/Composer/Trigger/TriggerEvents.php b/src/Composer/Trigger/TriggerEvents.php new file mode 100644 index 000000000..61392405f --- /dev/null +++ b/src/Composer/Trigger/TriggerEvents.php @@ -0,0 +1,89 @@ + + * 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'; + + /** + * The PRE_UNINSTALL event occurs at begging uninstallation 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\TriggerEvent instance. + * + * @var string + */ + const PRE_UNINSTALL = 'pre_uninstall'; + //TODO add the dispatcher when the uninstall command will be doing + + /** + * The PRE_UNINSTALL event occurs at end uninstallation 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\TriggerEvent instance. + * + * @var string + */ + const POST_UNINSTALL = 'post_uninstall'; + //TODO add the dispatcher when the uninstall command will be doing +}