applicationFactory = $applicationFactory ?? new FreshInstanceApplicationFactory();
$this->logger = $logger ?? new Logger(new NullIO());
}
protected function configure(): void
{
$this
->setDescription('Run a command inside a bin namespace')
->addArgument(
self::NAMESPACE_ARG,
InputArgument::REQUIRED
)
->ignoreValidationErrors();
}
public function setIO(IOInterface $io): void
{
parent::setIO($io);
$this->logger = new Logger($io);
}
public function getIO(): IOInterface
{
$io = parent::getIO();
$this->logger = new Logger($io);
return $io;
}
public function isProxyCommand(): bool
{
return true;
}
public function execute(InputInterface $input, OutputInterface $output): int
{
// Switch to requireComposer() once Composer 2.3 is set as the minimum
$composer = method_exists($this, 'requireComposer')
? $this->requireComposer()
: $this->getComposer();
$config = Config::fromComposer($composer);
$currentWorkingDir = getcwd();
$this->logger->logDebug(
sprintf(
'Current working directory: %s',
$currentWorkingDir
)
);
// Ensures Composer is reset – we are setting some environment variables
// & co. so a fresh Composer instance is required.
$this->resetComposers();
$this->configureBinLinksDir($config);
$vendorRoot = $config->getTargetDirectory();
$namespace = $input->getArgument(self::NAMESPACE_ARG);
$binInput = BinInputFactory::createInput(
$namespace,
$input
);
return (self::ALL_NAMESPACES !== $namespace)
? $this->executeInNamespace(
$currentWorkingDir,
$vendorRoot.'/'.$namespace,
$binInput,
$output
)
: $this->executeAllNamespaces(
$currentWorkingDir,
$vendorRoot,
$binInput,
$output
);
}
/**
* @return list
*/
private static function getBinNamespaces(string $binVendorRoot): array
{
return glob($binVendorRoot.'/*', GLOB_ONLYDIR);
}
private function executeAllNamespaces(
string $originalWorkingDir,
string $binVendorRoot,
InputInterface $input,
OutputInterface $output
): int {
$namespaces = self::getBinNamespaces($binVendorRoot);
if (count($namespaces) === 0) {
$this->logger->logStandard('Could not find any bin namespace.');
// Is a valid scenario: the user may not have set up any bin
// namespace yet
return 0;
}
$exitCode = 0;
foreach ($namespaces as $namespace) {
$exitCode += $this->executeInNamespace(
$originalWorkingDir,
$namespace,
$input,
$output
);
}
return min($exitCode, 255);
}
private function executeInNamespace(
string $originalWorkingDir,
string $namespace,
InputInterface $input,
OutputInterface $output
): int {
$this->logger->logStandard(
sprintf(
'Checking namespace %s',
$namespace
)
);
try {
self::createNamespaceDirIfDoesNotExist($namespace);
} catch (CouldNotCreateNamespaceDir $exception) {
$this->logger->logStandard(
sprintf(
'%s',
$exception->getMessage()
)
);
return 1;
}
// Use a new application: this avoids a variety of issues:
// - A command may be added in a namespace which may cause side effects
// when executed in another namespace afterwards (since it is the same
// process).
// - Different plugins may be registered in the namespace in which case
// an already executed application will not pick that up.
$namespaceApplication = $this->applicationFactory->create(
$this->getApplication()
);
// It is important to clean up the state either for follow-up plugins
// or for example the execution in the next namespace.
$cleanUp = function () use ($originalWorkingDir): void {
$this->chdir($originalWorkingDir);
$this->resetComposers();
};
$this->chdir($namespace);
$this->ensureComposerFileExists();
$namespaceInput = BinInputFactory::createNamespaceInput($input);
$this->logger->logDebug(
sprintf(
'Running `@composer %s`.',
$namespaceInput->__toString()
)
);
try {
$exitCode = $namespaceApplication->doRun($namespaceInput, $output);
} catch (Throwable $executionFailed) {
// Ensure we do the cleanup even in case of failure
$cleanUp();
throw $executionFailed;
}
$cleanUp();
return $exitCode;
}
/**
* @throws CouldNotCreateNamespaceDir
*/
private static function createNamespaceDirIfDoesNotExist(string $namespace): void
{
if (file_exists($namespace)) {
return;
}
$mkdirResult = mkdir($namespace, 0777, true);
if (!$mkdirResult && !is_dir($namespace)) {
throw CouldNotCreateNamespaceDir::forNamespace($namespace);
}
}
private function configureBinLinksDir(Config $config): void
{
if (!$config->binLinksAreEnabled()) {
return;
}
$binDir = ConfigFactory::createConfig()->get('bin-dir');
putenv(
sprintf(
'COMPOSER_BIN_DIR=%s',
$binDir
)
);
$this->logger->logDebug(
sprintf(
'Configuring bin directory to %s.',
$binDir
)
);
}
private function ensureComposerFileExists(): void
{
// Some plugins require access to the Composer file e.g. Symfony Flex
$namespaceComposerFile = Factory::getComposerFile();
if (file_exists($namespaceComposerFile)) {
return;
}
file_put_contents($namespaceComposerFile, '{}');
$this->logger->logDebug(
sprintf(
'Created the file %s.',
$namespaceComposerFile
)
);
}
private function resetComposers(): void
{
$this->getApplication()->resetComposer();
foreach ($this->getApplication()->all() as $command) {
if ($command instanceof BaseCommand) {
$command->resetComposer();
}
}
}
private function chdir(string $dir): void
{
chdir($dir);
$this->logger->logDebug(
sprintf(
'Changed current directory to %s.',
$dir
)
);
}
}