/
EventDispatcher.php
682 lines (605 loc) · 30.4 KB
/
EventDispatcher.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
<?php declare(strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\EventDispatcher;
use Composer\DependencyResolver\Transaction;
use Composer\Installer\InstallerEvent;
use Composer\IO\BufferIO;
use Composer\IO\ConsoleIO;
use Composer\IO\IOInterface;
use Composer\Composer;
use Composer\PartialComposer;
use Composer\Pcre\Preg;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PreCommandRunEvent;
use Composer\Util\Platform;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\Repository\RepositoryInterface;
use Composer\Script;
use Composer\Installer\PackageEvent;
use Composer\Installer\BinaryInstaller;
use Composer\Util\ProcessExecutor;
use Composer\Script\Event as ScriptEvent;
use Composer\Autoload\ClassLoader;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\ExecutableFinder;
/**
* The Event Dispatcher.
*
* Example in command:
* $dispatcher = new EventDispatcher($this->requireComposer(), $this->getApplication()->getIO());
* // ...
* $dispatcher->dispatch(ScriptEvents::POST_INSTALL_CMD);
* // ...
*
* @author François Pluchino <francois.pluchino@opendisplay.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @author Nils Adermann <naderman@naderman.de>
*/
class EventDispatcher
{
/** @var PartialComposer */
protected $composer;
/** @var IOInterface */
protected $io;
/** @var ?ClassLoader */
protected $loader;
/** @var ProcessExecutor */
protected $process;
/** @var array<string, array<int, array<callable|string>>> */
protected $listeners = [];
/** @var bool */
protected $runScripts = true;
/** @var list<string> */
private $eventStack;
/**
* Constructor.
*
* @param PartialComposer $composer The composer instance
* @param IOInterface $io The IOInterface instance
* @param ProcessExecutor $process
*/
public function __construct(PartialComposer $composer, IOInterface $io, ?ProcessExecutor $process = null)
{
$this->composer = $composer;
$this->io = $io;
$this->process = $process ?? new ProcessExecutor($io);
$this->eventStack = [];
}
/**
* Set whether script handlers are active or not
*
* @return $this
*/
public function setRunScripts(bool $runScripts = true): self
{
$this->runScripts = (bool) $runScripts;
return $this;
}
/**
* Dispatch an event
*
* @param string|null $eventName The event name, required if no $event is provided
* @param Event $event An event instance, required if no $eventName is provided
* @return int return code of the executed script if any, for php scripts a false return
* value is changed to 1, anything else to 0
*/
public function dispatch(?string $eventName, ?Event $event = null): int
{
if (null === $event) {
if (null === $eventName) {
throw new \InvalidArgumentException('If no $event is passed in to '.__METHOD__.' you have to pass in an $eventName, got null.');
}
$event = new Event($eventName);
}
return $this->doDispatch($event);
}
/**
* Dispatch a script event.
*
* @param string $eventName The constant in ScriptEvents
* @param array<int, mixed> $additionalArgs Arguments passed by the user
* @param array<string, mixed> $flags Optional flags to pass data not as argument
* @return int return code of the executed script if any, for php scripts a false return
* value is changed to 1, anything else to 0
*/
public function dispatchScript(string $eventName, bool $devMode = false, array $additionalArgs = [], array $flags = []): int
{
assert($this->composer instanceof Composer, new \LogicException('This should only be reached with a fully loaded Composer'));
return $this->doDispatch(new Script\Event($eventName, $this->composer, $this->io, $devMode, $additionalArgs, $flags));
}
/**
* Dispatch a package event.
*
* @param string $eventName The constant in PackageEvents
* @param bool $devMode Whether or not we are in dev mode
* @param RepositoryInterface $localRepo The installed repository
* @param OperationInterface[] $operations The list of operations
* @param OperationInterface $operation The package being installed/updated/removed
*
* @return int return code of the executed script if any, for php scripts a false return
* value is changed to 1, anything else to 0
*/
public function dispatchPackageEvent(string $eventName, bool $devMode, RepositoryInterface $localRepo, array $operations, OperationInterface $operation): int
{
assert($this->composer instanceof Composer, new \LogicException('This should only be reached with a fully loaded Composer'));
return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $localRepo, $operations, $operation));
}
/**
* Dispatch a installer event.
*
* @param string $eventName The constant in InstallerEvents
* @param bool $devMode Whether or not we are in dev mode
* @param bool $executeOperations True if operations will be executed, false in --dry-run
* @param Transaction $transaction The transaction contains the list of operations
*
* @return int return code of the executed script if any, for php scripts a false return
* value is changed to 1, anything else to 0
*/
public function dispatchInstallerEvent(string $eventName, bool $devMode, bool $executeOperations, Transaction $transaction): int
{
assert($this->composer instanceof Composer, new \LogicException('This should only be reached with a fully loaded Composer'));
return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $executeOperations, $transaction));
}
/**
* Triggers the listeners of an event.
*
* @param Event $event The event object to pass to the event handlers/listeners.
* @throws \RuntimeException|\Exception
* @return int return code of the executed script if any, for php scripts a false return
* value is changed to 1, anything else to 0
*/
protected function doDispatch(Event $event)
{
if (Platform::getEnv('COMPOSER_DEBUG_EVENTS')) {
$details = null;
if ($event instanceof PackageEvent) {
$details = (string) $event->getOperation();
} elseif ($event instanceof CommandEvent) {
$details = $event->getCommandName();
} elseif ($event instanceof PreCommandRunEvent) {
$details = $event->getCommand();
}
$this->io->writeError('Dispatching <info>'.$event->getName().'</info>'.($details ? ' ('.$details.')' : '').' event');
}
$listeners = $this->getListeners($event);
$this->pushEvent($event);
$autoloadersBefore = spl_autoload_functions();
try {
$returnMax = 0;
foreach ($listeners as $callable) {
$return = 0;
$this->ensureBinDirIsInPath();
$formattedEventNameWithArgs = $event->getName() . ($event->getArguments() !== [] ? ' (' . implode(', ', $event->getArguments()) . ')' : '');
if (!is_string($callable)) {
if (!is_callable($callable)) {
$className = is_object($callable[0]) ? get_class($callable[0]) : $callable[0];
throw new \RuntimeException('Subscriber '.$className.'::'.$callable[1].' for event '.$event->getName().' is not callable, make sure the function is defined and public');
}
if (is_array($callable) && (is_string($callable[0]) || is_object($callable[0])) && is_string($callable[1])) {
$this->io->writeError(sprintf('> %s: %s', $formattedEventNameWithArgs, (is_object($callable[0]) ? get_class($callable[0]) : $callable[0]).'->'.$callable[1]), true, IOInterface::VERBOSE);
}
$return = false === $callable($event) ? 1 : 0;
} elseif ($this->isComposerScript($callable)) {
$this->io->writeError(sprintf('> %s: %s', $formattedEventNameWithArgs, $callable), true, IOInterface::VERBOSE);
$script = explode(' ', substr($callable, 1));
$scriptName = $script[0];
unset($script[0]);
$args = array_merge($script, $event->getArguments());
$flags = $event->getFlags();
if (isset($flags['script-alias-input'])) {
$argsString = implode(' ', array_map(static function ($arg) { return ProcessExecutor::escape($arg); }, $script));
$flags['script-alias-input'] = $argsString . ' ' . $flags['script-alias-input'];
unset($argsString);
}
if (strpos($callable, '@composer ') === 0) {
$exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(Platform::getEnv('COMPOSER_BINARY')) . ' ' . implode(' ', $args);
if (0 !== ($exitCode = $this->executeTty($exec))) {
$this->io->writeError(sprintf('<error>Script %s handling the %s event returned with error code '.$exitCode.'</error>', $callable, $event->getName()), true, IOInterface::QUIET);
throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
}
} else {
if (!$this->getListeners(new Event($scriptName))) {
$this->io->writeError(sprintf('<warning>You made a reference to a non-existent script %s</warning>', $callable), true, IOInterface::QUIET);
}
try {
/** @var InstallerEvent $event */
$scriptEvent = new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags);
$scriptEvent->setOriginatingEvent($event);
$return = $this->dispatch($scriptName, $scriptEvent);
} catch (ScriptExecutionException $e) {
$this->io->writeError(sprintf('<error>Script %s was called via %s</error>', $callable, $event->getName()), true, IOInterface::QUIET);
throw $e;
}
}
} elseif ($this->isPhpScript($callable)) {
$className = substr($callable, 0, strpos($callable, '::'));
$methodName = substr($callable, strpos($callable, '::') + 2);
if (!class_exists($className)) {
$this->io->writeError('<warning>Class '.$className.' is not autoloadable, can not call '.$event->getName().' script</warning>', true, IOInterface::QUIET);
continue;
}
if (!is_callable($callable)) {
$this->io->writeError('<warning>Method '.$callable.' is not callable, can not call '.$event->getName().' script</warning>', true, IOInterface::QUIET);
continue;
}
try {
$return = false === $this->executeEventPhpScript($className, $methodName, $event) ? 1 : 0;
} catch (\Exception $e) {
$message = "Script %s handling the %s event terminated with an exception";
$this->io->writeError('<error>'.sprintf($message, $callable, $event->getName()).'</error>', true, IOInterface::QUIET);
throw $e;
}
} elseif ($this->isCommandClass($callable)) {
$className = $callable;
if (!class_exists($className)) {
$this->io->writeError('<warning>Class '.$className.' is not autoloadable, can not call '.$event->getName().' script</warning>', true, IOInterface::QUIET);
continue;
}
if (!is_a($className, Command::class, true)) {
$this->io->writeError('<warning>Class '.$className.' does not extend '.Command::class.', can not call '.$event->getName().' script</warning>', true, IOInterface::QUIET);
continue;
}
if (defined('Composer\Script\ScriptEvents::'.str_replace('-', '_', strtoupper($event->getName())))) {
$this->io->writeError('<warning>You cannot bind '.$event->getName().' to a Command class, use a non-reserved name</warning>', true, IOInterface::QUIET);
continue;
}
$app = new Application();
$app->setCatchExceptions(false);
if (method_exists($app, 'setCatchErrors')) {
$app->setCatchErrors(false);
}
$app->setAutoExit(false);
$cmd = new $className($event->getName());
$app->add($cmd);
$app->setDefaultCommand((string) $cmd->getName(), true);
try {
$args = implode(' ', array_map(static function ($arg) { return ProcessExecutor::escape($arg); }, $event->getArguments()));
// reusing the output from $this->io is mostly needed for tests, but generally speaking
// it does not hurt to keep the same stream as the current Application
if ($this->io instanceof ConsoleIO) {
$reflProp = new \ReflectionProperty($this->io, 'output');
if (PHP_VERSION_ID < 80100) {
$reflProp->setAccessible(true);
}
$output = $reflProp->getValue($this->io);
} else {
$output = new ConsoleOutput();
}
$return = $app->run(new StringInput($event->getFlags()['script-alias-input'] ?? $args), $output);
} catch (\Exception $e) {
$message = "Script %s handling the %s event terminated with an exception";
$this->io->writeError('<error>'.sprintf($message, $callable, $event->getName()).'</error>', true, IOInterface::QUIET);
throw $e;
}
} else {
$args = implode(' ', array_map(['Composer\Util\ProcessExecutor', 'escape'], $event->getArguments()));
// @putenv does not receive arguments
if (strpos($callable, '@putenv ') === 0) {
$exec = $callable;
} else {
$exec = $callable . ($args === '' ? '' : ' '.$args);
}
if ($this->io->isVerbose()) {
$this->io->writeError(sprintf('> %s: %s', $event->getName(), $exec));
} elseif ($event->getName() !== '__exec_command') {
// do not output the command being run when using `composer exec` as it is fairly obvious the user is running it
$this->io->writeError(sprintf('> %s', $exec));
}
$possibleLocalBinaries = $this->composer->getPackage()->getBinaries();
if ($possibleLocalBinaries) {
foreach ($possibleLocalBinaries as $localExec) {
if (Preg::isMatch('{\b'.preg_quote($callable).'$}', $localExec)) {
$caller = BinaryInstaller::determineBinaryCaller($localExec);
$exec = Preg::replace('{^'.preg_quote($callable).'}', $caller . ' ' . $localExec, $exec);
break;
}
}
}
if (strpos($exec, '@putenv ') === 0) {
if (false === strpos($exec, '=')) {
Platform::clearEnv(substr($exec, 8));
} else {
[$var, $value] = explode('=', substr($exec, 8), 2);
Platform::putEnv($var, $value);
}
continue;
}
if (strpos($exec, '@php ') === 0) {
$pathAndArgs = substr($exec, 5);
if (Platform::isWindows()) {
$pathAndArgs = Preg::replaceCallback('{^\S+}', static function ($path) {
return str_replace('/', '\\', (string) $path[0]);
}, $pathAndArgs);
}
// match somename (not in quote, and not a qualified path) and if it is not a valid path from CWD then try to find it
// in $PATH. This allows support for `@php foo` where foo is a binary name found in PATH but not an actual relative path
$matched = Preg::isMatchStrictGroups('{^[^\'"\s/\\\\]+}', $pathAndArgs, $match);
if ($matched && !file_exists($match[0])) {
$finder = new ExecutableFinder;
if ($pathToExec = $finder->find($match[0])) {
if (Platform::isWindows()) {
$execWithoutExt = Preg::replace('{\.(exe|bat|cmd|com)$}i', '', $pathToExec);
// prefer non-extension file if it exists when executing with PHP
if (file_exists($execWithoutExt)) {
$pathToExec = $execWithoutExt;
}
unset($execWithoutExt);
}
$pathAndArgs = $pathToExec . substr($pathAndArgs, strlen($match[0]));
}
}
$exec = $this->getPhpExecCommand() . ' ' . $pathAndArgs;
} else {
$finder = new PhpExecutableFinder();
$phpPath = $finder->find(false);
if ($phpPath) {
Platform::putEnv('PHP_BINARY', $phpPath);
}
if (Platform::isWindows()) {
$exec = Preg::replaceCallback('{^\S+}', static function ($path) {
assert(is_string($path[0]));
return str_replace('/', '\\', $path[0]);
}, $exec);
}
}
// if composer is being executed, make sure it runs the expected composer from current path
// resolution, even if bin-dir contains composer too because the project requires composer/composer
// see https://github.com/composer/composer/issues/8748
if (strpos($exec, 'composer ') === 0) {
$exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(Platform::getEnv('COMPOSER_BINARY')) . substr($exec, 8);
}
if (0 !== ($exitCode = $this->executeTty($exec))) {
$this->io->writeError(sprintf('<error>Script %s handling the %s event returned with error code '.$exitCode.'</error>', $callable, $event->getName()), true, IOInterface::QUIET);
throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode);
}
}
$returnMax = max($returnMax, $return);
if ($event->isPropagationStopped()) {
break;
}
}
} finally {
$this->popEvent();
$knownIdentifiers = [];
foreach ($autoloadersBefore as $key => $cb) {
$knownIdentifiers[$this->getCallbackIdentifier($cb)] = ['key' => $key, 'callback' => $cb];
}
foreach (spl_autoload_functions() as $cb) {
// once we get to the first known autoloader, we can leave any appended autoloader without problems
if (isset($knownIdentifiers[$this->getCallbackIdentifier($cb)]) && $knownIdentifiers[$this->getCallbackIdentifier($cb)]['key'] === 0) {
break;
}
// other newly appeared prepended autoloaders should be appended instead to ensure Composer loads its classes first
if ($cb instanceof ClassLoader) {
$cb->unregister();
$cb->register(false);
} else {
spl_autoload_unregister($cb);
spl_autoload_register($cb);
}
}
}
return $returnMax;
}
protected function executeTty(string $exec): int
{
if ($this->io->isInteractive()) {
return $this->process->executeTty($exec);
}
return $this->process->execute($exec);
}
protected function getPhpExecCommand(): string
{
$finder = new PhpExecutableFinder();
$phpPath = $finder->find(false);
if (!$phpPath) {
throw new \RuntimeException('Failed to locate PHP binary to execute '.$phpPath);
}
$phpArgs = $finder->findArguments();
$phpArgs = $phpArgs ? ' ' . implode(' ', $phpArgs) : '';
$allowUrlFOpenFlag = ' -d allow_url_fopen=' . ProcessExecutor::escape(ini_get('allow_url_fopen'));
$disableFunctionsFlag = ' -d disable_functions=' . ProcessExecutor::escape(ini_get('disable_functions'));
$memoryLimitFlag = ' -d memory_limit=' . ProcessExecutor::escape(ini_get('memory_limit'));
return ProcessExecutor::escape($phpPath) . $phpArgs . $allowUrlFOpenFlag . $disableFunctionsFlag . $memoryLimitFlag;
}
/**
* @param Event $event Event invoking the PHP callable
*
* @return mixed
*/
protected function executeEventPhpScript(string $className, string $methodName, Event $event)
{
if ($this->io->isVerbose()) {
$this->io->writeError(sprintf('> %s: %s::%s', $event->getName(), $className, $methodName));
} else {
$this->io->writeError(sprintf('> %s::%s', $className, $methodName));
}
return $className::$methodName($event);
}
/**
* Add a listener for a particular event
*
* @param string $eventName The event name - typically a constant
* @param callable|string $listener A callable expecting an event argument, or a command string to be executed (same as a composer.json "scripts" entry)
* @param int $priority A higher value represents a higher priority
*/
public function addListener(string $eventName, $listener, int $priority = 0): void
{
$this->listeners[$eventName][$priority][] = $listener;
}
/**
* @param callable|object $listener A callable or an object instance for which all listeners should be removed
*/
public function removeListener($listener): void
{
foreach ($this->listeners as $eventName => $priorities) {
foreach ($priorities as $priority => $listeners) {
foreach ($listeners as $index => $candidate) {
if ($listener === $candidate || (is_array($candidate) && is_object($listener) && $candidate[0] === $listener)) {
unset($this->listeners[$eventName][$priority][$index]);
}
}
}
}
}
/**
* Adds object methods as listeners for the events in getSubscribedEvents
*
* @see EventSubscriberInterface
*/
public function addSubscriber(EventSubscriberInterface $subscriber): void
{
foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
if (is_string($params)) {
$this->addListener($eventName, [$subscriber, $params]);
} elseif (is_string($params[0])) {
$this->addListener($eventName, [$subscriber, $params[0]], $params[1] ?? 0);
} else {
foreach ($params as $listener) {
$this->addListener($eventName, [$subscriber, $listener[0]], $listener[1] ?? 0);
}
}
}
}
/**
* Retrieves all listeners for a given event
*
* @return array<callable|string> All listeners: callables and scripts
*/
protected function getListeners(Event $event): array
{
$scriptListeners = $this->runScripts ? $this->getScriptListeners($event) : [];
if (!isset($this->listeners[$event->getName()][0])) {
$this->listeners[$event->getName()][0] = [];
}
krsort($this->listeners[$event->getName()]);
$listeners = $this->listeners;
$listeners[$event->getName()][0] = array_merge($listeners[$event->getName()][0], $scriptListeners);
return array_merge(...$listeners[$event->getName()]);
}
/**
* Checks if an event has listeners registered
*/
public function hasEventListeners(Event $event): bool
{
$listeners = $this->getListeners($event);
return count($listeners) > 0;
}
/**
* Finds all listeners defined as scripts in the package
*
* @param Event $event Event object
* @return string[] Listeners
*/
protected function getScriptListeners(Event $event): array
{
$package = $this->composer->getPackage();
$scripts = $package->getScripts();
if (empty($scripts[$event->getName()])) {
return [];
}
assert($this->composer instanceof Composer, new \LogicException('This should only be reached with a fully loaded Composer'));
if ($this->loader) {
$this->loader->unregister();
}
$generator = $this->composer->getAutoloadGenerator();
if ($event instanceof ScriptEvent) {
$generator->setDevMode($event->isDevMode());
}
$packages = $this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages();
$packageMap = $generator->buildPackageMap($this->composer->getInstallationManager(), $package, $packages);
$map = $generator->parseAutoloads($packageMap, $package);
$this->loader = $generator->createLoader($map, $this->composer->getConfig()->get('vendor-dir'));
$this->loader->register(false);
return $scripts[$event->getName()];
}
/**
* Checks if string given references a class path and method
*/
protected function isPhpScript(string $callable): bool
{
return false === strpos($callable, ' ') && false !== strpos($callable, '::');
}
/**
* Checks if string given references a command class
*/
protected function isCommandClass(string $callable): bool
{
return str_contains($callable, '\\') && !str_contains($callable, ' ') && str_ends_with($callable, 'Command');
}
/**
* Checks if string given references a composer run-script
*/
protected function isComposerScript(string $callable): bool
{
return strpos($callable, '@') === 0 && strpos($callable, '@php ') !== 0 && strpos($callable, '@putenv ') !== 0;
}
/**
* Push an event to the stack of active event
*
* @throws \RuntimeException
*/
protected function pushEvent(Event $event): int
{
$eventName = $event->getName();
if (in_array($eventName, $this->eventStack)) {
throw new \RuntimeException(sprintf("Circular call to script handler '%s' detected", $eventName));
}
return array_push($this->eventStack, $eventName);
}
/**
* Pops the active event from the stack
*/
protected function popEvent(): ?string
{
return array_pop($this->eventStack);
}
private function ensureBinDirIsInPath(): void
{
$pathEnv = 'PATH';
// checking if only Path and not PATH is set then we probably need to update the Path env
// on Windows getenv is case-insensitive so we cannot check it via Platform::getEnv and
// we need to check in $_SERVER directly
if (!isset($_SERVER[$pathEnv]) && isset($_SERVER['Path'])) {
$pathEnv = 'Path';
}
// add the bin dir to the PATH to make local binaries of deps usable in scripts
$binDir = $this->composer->getConfig()->get('bin-dir');
if (is_dir($binDir)) {
$binDir = realpath($binDir);
$pathValue = (string) Platform::getEnv($pathEnv);
if (!Preg::isMatch('{(^|'.PATH_SEPARATOR.')'.preg_quote($binDir).'($|'.PATH_SEPARATOR.')}', $pathValue)) {
Platform::putEnv($pathEnv, $binDir.PATH_SEPARATOR.$pathValue);
}
}
}
/**
* @param callable $cb DO NOT MOVE TO TYPE HINT as private autoload callbacks are not technically callable
*/
private function getCallbackIdentifier($cb): string
{
if (is_string($cb)) {
return 'fn:'.$cb;
}
if (is_object($cb)) {
return 'obj:'.spl_object_hash($cb);
}
if (is_array($cb)) {
return 'array:'.(is_string($cb[0]) ? $cb[0] : get_class($cb[0]) .'#'.spl_object_hash($cb[0])).'::'.$cb[1];
}
// not great but also do not want to break everything here
return 'unsupported';
}
}