EventDispatcher.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\EventDispatcher;
  12. use Composer\DependencyResolver\PolicyInterface;
  13. use Composer\DependencyResolver\Request;
  14. use Composer\DependencyResolver\Pool;
  15. use Composer\DependencyResolver\Transaction;
  16. use Composer\Installer\InstallerEvent;
  17. use Composer\IO\IOInterface;
  18. use Composer\Composer;
  19. use Composer\DependencyResolver\Operation\OperationInterface;
  20. use Composer\Repository\CompositeRepository;
  21. use Composer\Repository\RepositoryInterface;
  22. use Composer\Repository\RepositorySet;
  23. use Composer\Script;
  24. use Composer\Installer\PackageEvent;
  25. use Composer\Installer\BinaryInstaller;
  26. use Composer\Util\ProcessExecutor;
  27. use Composer\Script\Event as ScriptEvent;
  28. use Symfony\Component\Process\PhpExecutableFinder;
  29. /**
  30. * The Event Dispatcher.
  31. *
  32. * Example in command:
  33. * $dispatcher = new EventDispatcher($this->getComposer(), $this->getApplication()->getIO());
  34. * // ...
  35. * $dispatcher->dispatch(ScriptEvents::POST_INSTALL_CMD);
  36. * // ...
  37. *
  38. * @author François Pluchino <francois.pluchino@opendisplay.com>
  39. * @author Jordi Boggiano <j.boggiano@seld.be>
  40. * @author Nils Adermann <naderman@naderman.de>
  41. */
  42. class EventDispatcher
  43. {
  44. protected $composer;
  45. protected $io;
  46. protected $loader;
  47. protected $process;
  48. protected $listeners = array();
  49. private $eventStack;
  50. /**
  51. * Constructor.
  52. *
  53. * @param Composer $composer The composer instance
  54. * @param IOInterface $io The IOInterface instance
  55. * @param ProcessExecutor $process
  56. */
  57. public function __construct(Composer $composer, IOInterface $io, ProcessExecutor $process = null)
  58. {
  59. $this->composer = $composer;
  60. $this->io = $io;
  61. $this->process = $process ?: new ProcessExecutor($io);
  62. $this->eventStack = array();
  63. }
  64. /**
  65. * Dispatch an event
  66. *
  67. * @param string $eventName An event name
  68. * @param Event $event
  69. * @return int return code of the executed script if any, for php scripts a false return
  70. * value is changed to 1, anything else to 0
  71. */
  72. public function dispatch($eventName, Event $event = null)
  73. {
  74. if (null === $event) {
  75. $event = new Event($eventName);
  76. }
  77. return $this->doDispatch($event);
  78. }
  79. /**
  80. * Dispatch a script event.
  81. *
  82. * @param string $eventName The constant in ScriptEvents
  83. * @param bool $devMode
  84. * @param array $additionalArgs Arguments passed by the user
  85. * @param array $flags Optional flags to pass data not as argument
  86. * @return int return code of the executed script if any, for php scripts a false return
  87. * value is changed to 1, anything else to 0
  88. */
  89. public function dispatchScript($eventName, $devMode = false, $additionalArgs = array(), $flags = array())
  90. {
  91. return $this->doDispatch(new Script\Event($eventName, $this->composer, $this->io, $devMode, $additionalArgs, $flags));
  92. }
  93. /**
  94. * Dispatch a package event.
  95. *
  96. * @param string $eventName The constant in PackageEvents
  97. * @param bool $devMode Whether or not we are in dev mode
  98. * @param RepositoryInterface $localRepo The installed repository
  99. * @param array $operations The list of operations
  100. * @param OperationInterface $operation The package being installed/updated/removed
  101. *
  102. * @return int return code of the executed script if any, for php scripts a false return
  103. * value is changed to 1, anything else to 0
  104. */
  105. public function dispatchPackageEvent($eventName, $devMode, RepositoryInterface $localRepo, array $operations, OperationInterface $operation)
  106. {
  107. return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $localRepo, $operations, $operation));
  108. }
  109. /**
  110. * Dispatch a installer event.
  111. *
  112. * @param string $eventName The constant in InstallerEvents
  113. * @param bool $devMode Whether or not we are in dev mode
  114. * @param bool $executeOperations True if operations will be executed, false in --dry-run
  115. * @param Transaction $transaction The transaction contains the list of operations
  116. *
  117. * @return int return code of the executed script if any, for php scripts a false return
  118. * value is changed to 1, anything else to 0
  119. */
  120. public function dispatchInstallerEvent($eventName, $devMode, $executeOperations, Transaction $transaction)
  121. {
  122. return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $executeOperations, $transaction));
  123. }
  124. /**
  125. * Triggers the listeners of an event.
  126. *
  127. * @param Event $event The event object to pass to the event handlers/listeners.
  128. * @throws \RuntimeException|\Exception
  129. * @return int return code of the executed script if any, for php scripts a false return
  130. * value is changed to 1, anything else to 0
  131. */
  132. protected function doDispatch(Event $event)
  133. {
  134. $listeners = $this->getListeners($event);
  135. $this->pushEvent($event);
  136. $return = 0;
  137. foreach ($listeners as $callable) {
  138. $this->ensureBinDirIsInPath();
  139. if (!is_string($callable)) {
  140. if (!is_callable($callable)) {
  141. $className = is_object($callable[0]) ? get_class($callable[0]) : $callable[0];
  142. throw new \RuntimeException('Subscriber '.$className.'::'.$callable[1].' for event '.$event->getName().' is not callable, make sure the function is defined and public');
  143. }
  144. if (is_array($callable) && (is_string($callable[0]) || is_object($callable[0])) && is_string($callable[1])) {
  145. $this->io->writeError(sprintf('> %s: %s', $event->getName(), (is_object($callable[0]) ? get_class($callable[0]) : $callable[0]).'->'.$callable[1] ), true, IOInterface::VERBOSE);
  146. }
  147. $event = $this->checkListenerExpectedEvent($callable, $event);
  148. $return = false === call_user_func($callable, $event) ? 1 : 0;
  149. } elseif ($this->isComposerScript($callable)) {
  150. $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable), true, IOInterface::VERBOSE);
  151. $script = explode(' ', substr($callable, 1));
  152. $scriptName = $script[0];
  153. unset($script[0]);
  154. $args = array_merge($script, $event->getArguments());
  155. $flags = $event->getFlags();
  156. if (substr($callable, 0, 10) === '@composer ') {
  157. $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . ' ' . implode(' ', $args);
  158. if (0 !== ($exitCode = $this->executeTty($exec))) {
  159. $this->io->writeError(sprintf('<error>Script %s handling the %s event returned with error code '.$exitCode.'</error>', $callable, $event->getName()), true, IOInterface::QUIET);
  160. throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
  161. }
  162. } else {
  163. if (!$this->getListeners(new Event($scriptName))) {
  164. $this->io->writeError(sprintf('<warning>You made a reference to a non-existent script %s</warning>', $callable), true, IOInterface::QUIET);
  165. }
  166. try {
  167. /** @var InstallerEvent $event */
  168. $scriptEvent = new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags);
  169. $scriptEvent->setOriginatingEvent($event);
  170. $return = $this->dispatch($scriptName, $scriptEvent);
  171. } catch (ScriptExecutionException $e) {
  172. $this->io->writeError(sprintf('<error>Script %s was called via %s</error>', $callable, $event->getName()), true, IOInterface::QUIET);
  173. throw $e;
  174. }
  175. }
  176. } elseif ($this->isPhpScript($callable)) {
  177. $className = substr($callable, 0, strpos($callable, '::'));
  178. $methodName = substr($callable, strpos($callable, '::') + 2);
  179. if (!class_exists($className)) {
  180. $this->io->writeError('<warning>Class '.$className.' is not autoloadable, can not call '.$event->getName().' script</warning>', true, IOInterface::QUIET);
  181. continue;
  182. }
  183. if (!is_callable($callable)) {
  184. $this->io->writeError('<warning>Method '.$callable.' is not callable, can not call '.$event->getName().' script</warning>', true, IOInterface::QUIET);
  185. continue;
  186. }
  187. try {
  188. $return = false === $this->executeEventPhpScript($className, $methodName, $event) ? 1 : 0;
  189. } catch (\Exception $e) {
  190. $message = "Script %s handling the %s event terminated with an exception";
  191. $this->io->writeError('<error>'.sprintf($message, $callable, $event->getName()).'</error>', true, IOInterface::QUIET);
  192. throw $e;
  193. }
  194. } else {
  195. $args = implode(' ', array_map(array('Composer\Util\ProcessExecutor', 'escape'), $event->getArguments()));
  196. $exec = $callable . ($args === '' ? '' : ' '.$args);
  197. if ($this->io->isVerbose()) {
  198. $this->io->writeError(sprintf('> %s: %s', $event->getName(), $exec));
  199. } else {
  200. $this->io->writeError(sprintf('> %s', $exec));
  201. }
  202. $possibleLocalBinaries = $this->composer->getPackage()->getBinaries();
  203. if ($possibleLocalBinaries) {
  204. foreach ($possibleLocalBinaries as $localExec) {
  205. if (preg_match('{\b'.preg_quote($callable).'$}', $localExec)) {
  206. $caller = BinaryInstaller::determineBinaryCaller($localExec);
  207. $exec = preg_replace('{^'.preg_quote($callable).'}', $caller . ' ' . $localExec, $exec);
  208. break;
  209. }
  210. }
  211. }
  212. if (substr($exec, 0, 8) === '@putenv ') {
  213. putenv(substr($exec, 8));
  214. continue;
  215. } elseif (substr($exec, 0, 5) === '@php ') {
  216. $exec = $this->getPhpExecCommand() . ' ' . substr($exec, 5);
  217. } else {
  218. $finder = new PhpExecutableFinder();
  219. $phpPath = $finder->find(false);
  220. if ($phpPath) {
  221. $_SERVER['PHP_BINARY'] = $phpPath;
  222. putenv('PHP_BINARY=' . $_SERVER['PHP_BINARY']);
  223. }
  224. }
  225. // if composer is being executed, make sure it runs the expected composer from current path
  226. // resolution, even if bin-dir contains composer too because the project requires composer/composer
  227. // see https://github.com/composer/composer/issues/8748
  228. if (substr($exec, 0, 9) === 'composer ') {
  229. $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(getenv('COMPOSER_BINARY')) . substr($exec, 8);
  230. }
  231. if (0 !== ($exitCode = $this->executeTty($exec))) {
  232. $this->io->writeError(sprintf('<error>Script %s handling the %s event returned with error code '.$exitCode.'</error>', $callable, $event->getName()), true, IOInterface::QUIET);
  233. throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
  234. }
  235. }
  236. if ($event->isPropagationStopped()) {
  237. break;
  238. }
  239. }
  240. $this->popEvent();
  241. return $return;
  242. }
  243. protected function executeTty($exec)
  244. {
  245. if ($this->io->isInteractive()) {
  246. return $this->process->executeTty($exec);
  247. }
  248. return $this->process->execute($exec);
  249. }
  250. protected function getPhpExecCommand()
  251. {
  252. $finder = new PhpExecutableFinder();
  253. $phpPath = $finder->find(false);
  254. if (!$phpPath) {
  255. throw new \RuntimeException('Failed to locate PHP binary to execute '.$phpPath);
  256. }
  257. $phpArgs = $finder->findArguments();
  258. $phpArgs = $phpArgs ? ' ' . implode(' ', $phpArgs) : '';
  259. $allowUrlFOpenFlag = ' -d allow_url_fopen=' . ProcessExecutor::escape(ini_get('allow_url_fopen'));
  260. $disableFunctionsFlag = ' -d disable_functions=' . ProcessExecutor::escape(ini_get('disable_functions'));
  261. $memoryLimitFlag = ' -d memory_limit=' . ProcessExecutor::escape(ini_get('memory_limit'));
  262. return ProcessExecutor::escape($phpPath) . $phpArgs . $allowUrlFOpenFlag . $disableFunctionsFlag . $memoryLimitFlag;
  263. }
  264. /**
  265. * @param string $className
  266. * @param string $methodName
  267. * @param Event $event Event invoking the PHP callable
  268. */
  269. protected function executeEventPhpScript($className, $methodName, Event $event)
  270. {
  271. $event = $this->checkListenerExpectedEvent(array($className, $methodName), $event);
  272. if ($this->io->isVerbose()) {
  273. $this->io->writeError(sprintf('> %s: %s::%s', $event->getName(), $className, $methodName));
  274. } else {
  275. $this->io->writeError(sprintf('> %s::%s', $className, $methodName));
  276. }
  277. return $className::$methodName($event);
  278. }
  279. /**
  280. * @param mixed $target
  281. * @param Event $event
  282. * @return Event
  283. */
  284. protected function checkListenerExpectedEvent($target, Event $event)
  285. {
  286. if (in_array($event->getName(), array(
  287. 'init',
  288. 'command',
  289. 'pre-file-download',
  290. ), true)) {
  291. return $event;
  292. }
  293. try {
  294. $reflected = new \ReflectionParameter($target, 0);
  295. } catch (\Exception $e) {
  296. return $event;
  297. }
  298. $typehint = $reflected->getClass();
  299. if (!$typehint instanceof \ReflectionClass) {
  300. return $event;
  301. }
  302. $expected = $typehint->getName();
  303. return $event;
  304. }
  305. private function serializeCallback($cb)
  306. {
  307. if (is_array($cb) && count($cb) === 2) {
  308. if (is_object($cb[0])) {
  309. $cb[0] = get_class($cb[0]);
  310. }
  311. if (is_string($cb[0]) && is_string($cb[1])) {
  312. $cb = implode('::', $cb);
  313. }
  314. }
  315. if (is_string($cb)) {
  316. return $cb;
  317. }
  318. return var_export($cb, true);
  319. }
  320. /**
  321. * Add a listener for a particular event
  322. *
  323. * @param string $eventName The event name - typically a constant
  324. * @param callable $listener A callable expecting an event argument
  325. * @param int $priority A higher value represents a higher priority
  326. */
  327. public function addListener($eventName, $listener, $priority = 0)
  328. {
  329. $this->listeners[$eventName][$priority][] = $listener;
  330. }
  331. /**
  332. * @param callable|object $listener A callable or an object instance for which all listeners should be removed
  333. */
  334. public function removeListener($listener)
  335. {
  336. foreach ($this->listeners as $eventName => $priorities) {
  337. foreach ($priorities as $priority => $listeners) {
  338. foreach ($listeners as $index => $candidate) {
  339. if ($listener === $candidate || (is_array($candidate) && is_object($listener) && $candidate[0] === $listener)) {
  340. unset($this->listeners[$eventName][$priority][$index]);
  341. }
  342. }
  343. }
  344. }
  345. }
  346. /**
  347. * Adds object methods as listeners for the events in getSubscribedEvents
  348. *
  349. * @see EventSubscriberInterface
  350. *
  351. * @param EventSubscriberInterface $subscriber
  352. */
  353. public function addSubscriber(EventSubscriberInterface $subscriber)
  354. {
  355. foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
  356. if (is_string($params)) {
  357. $this->addListener($eventName, array($subscriber, $params));
  358. } elseif (is_string($params[0])) {
  359. $this->addListener($eventName, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0);
  360. } else {
  361. foreach ($params as $listener) {
  362. $this->addListener($eventName, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0);
  363. }
  364. }
  365. }
  366. }
  367. /**
  368. * Retrieves all listeners for a given event
  369. *
  370. * @param Event $event
  371. * @return array All listeners: callables and scripts
  372. */
  373. protected function getListeners(Event $event)
  374. {
  375. $scriptListeners = $this->getScriptListeners($event);
  376. if (!isset($this->listeners[$event->getName()][0])) {
  377. $this->listeners[$event->getName()][0] = array();
  378. }
  379. krsort($this->listeners[$event->getName()]);
  380. $listeners = $this->listeners;
  381. $listeners[$event->getName()][0] = array_merge($listeners[$event->getName()][0], $scriptListeners);
  382. return call_user_func_array('array_merge', $listeners[$event->getName()]);
  383. }
  384. /**
  385. * Checks if an event has listeners registered
  386. *
  387. * @param Event $event
  388. * @return bool
  389. */
  390. public function hasEventListeners(Event $event)
  391. {
  392. $listeners = $this->getListeners($event);
  393. return count($listeners) > 0;
  394. }
  395. /**
  396. * Finds all listeners defined as scripts in the package
  397. *
  398. * @param Event $event Event object
  399. * @return array Listeners
  400. */
  401. protected function getScriptListeners(Event $event)
  402. {
  403. $package = $this->composer->getPackage();
  404. $scripts = $package->getScripts();
  405. if (empty($scripts[$event->getName()])) {
  406. return array();
  407. }
  408. if ($this->loader) {
  409. $this->loader->unregister();
  410. }
  411. $generator = $this->composer->getAutoloadGenerator();
  412. if ($event instanceof ScriptEvent) {
  413. $generator->setDevMode($event->isDevMode());
  414. }
  415. $packages = $this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages();
  416. $packageMap = $generator->buildPackageMap($this->composer->getInstallationManager(), $package, $packages);
  417. $map = $generator->parseAutoloads($packageMap, $package);
  418. $this->loader = $generator->createLoader($map);
  419. $this->loader->register();
  420. return $scripts[$event->getName()];
  421. }
  422. /**
  423. * Checks if string given references a class path and method
  424. *
  425. * @param string $callable
  426. * @return bool
  427. */
  428. protected function isPhpScript($callable)
  429. {
  430. return false === strpos($callable, ' ') && false !== strpos($callable, '::');
  431. }
  432. /**
  433. * Checks if string given references a composer run-script
  434. *
  435. * @param string $callable
  436. * @return bool
  437. */
  438. protected function isComposerScript($callable)
  439. {
  440. return '@' === substr($callable, 0, 1) && '@php ' !== substr($callable, 0, 5) && '@putenv ' !== substr($callable, 0, 8);
  441. }
  442. /**
  443. * Push an event to the stack of active event
  444. *
  445. * @param Event $event
  446. * @throws \RuntimeException
  447. * @return int
  448. */
  449. protected function pushEvent(Event $event)
  450. {
  451. $eventName = $event->getName();
  452. if (in_array($eventName, $this->eventStack)) {
  453. throw new \RuntimeException(sprintf("Circular call to script handler '%s' detected", $eventName));
  454. }
  455. return array_push($this->eventStack, $eventName);
  456. }
  457. /**
  458. * Pops the active event from the stack
  459. *
  460. * @return mixed
  461. */
  462. protected function popEvent()
  463. {
  464. return array_pop($this->eventStack);
  465. }
  466. private function ensureBinDirIsInPath()
  467. {
  468. $pathStr = 'PATH';
  469. if (!isset($_SERVER[$pathStr]) && isset($_SERVER['Path'])) {
  470. $pathStr = 'Path';
  471. }
  472. // add the bin dir to the PATH to make local binaries of deps usable in scripts
  473. $binDir = $this->composer->getConfig()->get('bin-dir');
  474. if (is_dir($binDir)) {
  475. $binDir = realpath($binDir);
  476. if (isset($_SERVER[$pathStr]) && !preg_match('{(^|'.PATH_SEPARATOR.')'.preg_quote($binDir).'($|'.PATH_SEPARATOR.')}', $_SERVER[$pathStr])) {
  477. $_SERVER[$pathStr] = $binDir.PATH_SEPARATOR.getenv($pathStr);
  478. putenv($pathStr.'='.$_SERVER[$pathStr]);
  479. }
  480. }
  481. }
  482. }