Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -270,18 +270,6 @@ parameters:
count: 2
path: src/Installing/InstallForPhpProject/FindMatchingPackages.php

-
message: '#^Parameter \#2 \$callback of function array_walk expects callable\(array\|float\|int\|string\|true, int\|string\)\: mixed, Closure\(string, string\)\: void given\.$#'
identifier: argument.type
count: 1
path: src/Installing/InstallForPhpProject/InstallSelectedPackage.php

-
message: '#^Parameter \#2 \$workingDirectory of static method Php\\Pie\\Util\\Process\:\:run\(\) expects string\|null, string\|false given\.$#'
identifier: argument.type
count: 1
path: src/Installing/InstallForPhpProject/InstallSelectedPackage.php

-
message: '#^Negated boolean expression is always false\.$#'
identifier: booleanNot.alwaysFalse
Expand Down Expand Up @@ -480,12 +468,6 @@ parameters:
count: 1
path: test/unit/Installing/Ini/StandardSinglePhpIniTest.php

-
message: '#^Parameter \#1 \$originalCwd of class Php\\Pie\\File\\FullPathToSelf constructor expects string, string\|false given\.$#'
identifier: argument.type
count: 1
path: test/unit/Installing/InstallForPhpProject/InstallSelectedPackageTest.php

-
message: '#^Method Php\\PieUnitTest\\SelfManage\\Verify\\FallbackVerificationUsingOpenSslTest\:\:prepareCertificateAndSignature\(\) should return array\{string, string\} but returns array\{mixed, mixed\}\.$#'
identifier: return.type
Expand Down
26 changes: 17 additions & 9 deletions src/Command/InstallExtensionsForProjectCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Php\Pie\ComposerIntegration\PieComposerFactory;
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\PieJsonEditor;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
use Php\Pie\Installing\InstallForPhpProject\ComposerFactoryForProject;
Expand All @@ -28,6 +29,7 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use Webmozart\Assert\Assert;

use function array_column;
use function array_key_exists;
Expand Down Expand Up @@ -268,20 +270,26 @@ static function (array $match): string {
$selectedPackageName = $matches[0]['name'];
}

$requestInstallConstraint = '';
if ($linkRequiresConstraint !== '*') {
$requestInstallConstraint = ':' . $linkRequiresConstraint;
}
assert($selectedPackageName !== '');
$requestedPackageAndVersion = new RequestedPackageAndVersion(
$selectedPackageName,
$linkRequiresConstraint === '*' || $linkRequiresConstraint === '' ? null : $linkRequiresConstraint,
);

try {
$this->io->write(
sprintf('Invoking pie install of %s%s', $selectedPackageName, $requestInstallConstraint),
sprintf('Invoking pie install of %s', $requestedPackageAndVersion->prettyNameAndVersion()),
verbosity: IOInterface::VERBOSE,
);
$this->installSelectedPackage->withPieCli(
$selectedPackageName . $requestInstallConstraint,
$input,
$this->io,
Assert::same(
0,
$this->installSelectedPackage->withSubCommand(
ExtensionName::normaliseFromString($link->getTarget()),
$requestedPackageAndVersion,
$this,
$input,
),
'Non-zero exit code %s whilst installing ' . $requestedPackageAndVersion->package,
);
} catch (Throwable $t) {
$anyErrorsHappened = true;
Expand Down
17 changes: 16 additions & 1 deletion src/Command/InvokeSubCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Php\Pie\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
Expand All @@ -29,6 +30,7 @@ public function __invoke(
Command $command,
array $subCommandInput,
InputInterface $originalCommandInput,
OutputFormatter|null $formatter = null,
): int {
$originalSuppliedOptions = array_filter($originalCommandInput->getOptions());
$installForProjectInput = new ArrayInput(array_merge(
Expand All @@ -42,6 +44,19 @@ public function __invoke(
$application = $command->getApplication();
Assert::notNull($application);

return $application->doRun($installForProjectInput, $this->output);
if ($formatter instanceof OutputFormatter) {
$oldFormatter = $this->output->getFormatter();
$this->output->setFormatter($formatter);
}

try {
$result = $application->doRun($installForProjectInput, $this->output);
} finally {
if ($formatter instanceof OutputFormatter) {
$this->output->setFormatter($oldFormatter);
}
}

return $result;
}
}
9 changes: 9 additions & 0 deletions src/DependencyResolver/RequestedPackageAndVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,13 @@ public function __construct(
throw InvalidPackageName::fromMissingForwardSlash($this);
}
}

public function prettyNameAndVersion(): string
{
if ($this->version === null) {
return $this->package;
}

return $this->package . ':' . $this->version;
}
}
78 changes: 22 additions & 56 deletions src/Installing/InstallForPhpProject/InstallSelectedPackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,37 @@

namespace Php\Pie\Installing\InstallForPhpProject;

use Composer\IO\IOInterface;
use Php\Pie\Command\CommandHelper;
use Php\Pie\File\FullPathToSelf;
use Php\Pie\Util\Process;
use Php\Pie\Command\InvokeSubCommand;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
use Php\Pie\ExtensionName;
use Php\Pie\Util\OutputFormatterWithPrefix;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;

use function array_filter;
use function array_walk;
use function getcwd;
use function in_array;

use const ARRAY_FILTER_USE_BOTH;

/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
class InstallSelectedPackage
{
public function __construct(private readonly FullPathToSelf $fullPathToSelf)
{
public function __construct(
private readonly InvokeSubCommand $invokeSubCommand,
) {
}

public function withPieCli(string $selectedPackage, InputInterface $input, IOInterface $io): void
{
$process = [
($this->fullPathToSelf)(),
'install',
$selectedPackage,
public function withSubCommand(
ExtensionName $ext,
RequestedPackageAndVersion $selectedPackage,
Command $command,
InputInterface $input,
): int {
$params = [
'command' => 'install',
'requested-package-and-version' => $selectedPackage->prettyNameAndVersion(),
];

$phpPathOptions = array_filter(
$input->getOptions(),
static function (mixed $value, string|int $key): bool {
return $value !== null
&& $value !== false
&& in_array(
$key,
[
CommandHelper::OPTION_WITH_PHP_CONFIG,
CommandHelper::OPTION_WITH_PHP_PATH,
CommandHelper::OPTION_WITH_PHPIZE_PATH,
],
);
},
ARRAY_FILTER_USE_BOTH,
);

array_walk(
$phpPathOptions,
static function (string $value, string $key) use (&$process): void {
$process[] = '--' . $key;
$process[] = $value;
},
);

Process::run(
$process,
getcwd(),
outputCallback: static function (string $outOrErr, string $message) use ($io): void {
if ($outOrErr === \Symfony\Component\Process\Process::ERR) {
$io->writeError(' > ' . $message);

return;
}

$io->write(' > ' . $message);
},
return ($this->invokeSubCommand)(
$command,
$params,
$input,
OutputFormatterWithPrefix::newWithPrefix(' ' . $ext->name() . '> '),
);
}
}
38 changes: 38 additions & 0 deletions src/Util/OutputFormatterWithPrefix.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Php\Pie\Util;

use Composer\Factory;
use Symfony\Component\Console\Formatter\OutputFormatter;

class OutputFormatterWithPrefix extends OutputFormatter
{
/**
* @param non-empty-string $linePrefix
*
* @inheritDoc
*/
public function __construct(private readonly string $linePrefix, bool $decorated = false, array $styles = [])
{
parent::__construct($decorated, $styles);
}

/** @param non-empty-string $linePrefix */
public static function newWithPrefix(string $linePrefix): self
{
return new self($linePrefix, false, Factory::createAdditionalStyles());
}

public function format(string|null $message): string|null
{
$formatted = parent::format($message);

if ($formatted === null) {
return null;
}

return $this->linePrefix . $formatted;
}
}
5 changes: 0 additions & 5 deletions test/assets/fake-pie-cli/happy.bat

This file was deleted.

4 changes: 0 additions & 4 deletions test/assets/fake-pie-cli/happy.sh

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
use Php\Pie\ComposerIntegration\PieJsonEditor;
use Php\Pie\ComposerIntegration\QuieterConsoleIO;
use Php\Pie\Container;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
use Php\Pie\Installing\InstallForPhpProject\ComposerFactoryForProject;
use Php\Pie\Installing\InstallForPhpProject\DetermineExtensionsRequired;
Expand Down Expand Up @@ -125,8 +127,15 @@ public function testInstallingExtensionsForPhpProject(): void
$this->questionHelper->method('ask')->willReturn('vendor1/foobar: The official foobar implementation');

$this->installSelectedPackage->expects(self::once())
->method('withPieCli')
->with('vendor1/foobar:^1.2');
->method('withSubCommand')
->with(
ExtensionName::normaliseFromString('foobar'),
new RequestedPackageAndVersion(
'vendor1/foobar',
'^1.2',
),
)
->willReturn(0);

$this->commandTester->execute(
['--allow-non-interactive-project-install' => true],
Expand Down Expand Up @@ -173,7 +182,7 @@ public function testInstallingExtensionsForPhpProjectWithMultipleMatches(): void
$this->questionHelper->method('ask')->willReturn('vendor1/foobar: The official foobar implementation');

$this->installSelectedPackage->expects(self::never())
->method('withPieCli');
->method('withSubCommand');

$this->commandTester->execute(
['--allow-non-interactive-project-install' => true],
Expand Down
75 changes: 75 additions & 0 deletions test/unit/Command/InvokeSubCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Php\PieUnitTest\Command;

use Php\Pie\Command\InvokeSubCommand;
use Php\Pie\Util\OutputFormatterWithPrefix;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;

use function trim;

#[CoversClass(InvokeSubCommand::class)]
final class InvokeSubCommandTest extends TestCase
{
public function testInvokeWithNoOutputFormatterRunsSubCommand(): void
{
$inputDefinition = new InputDefinition();
$inputDefinition->addOption(new InputOption('verbose', 'v', InputOption::VALUE_NONE, 'Verbose option'));
$input = new ArrayInput(['--verbose' => true], $inputDefinition);

$output = new BufferedOutput();

$application = $this->createMock(Application::class);
$application->expects(self::once())
->method('doRun')
->willReturnCallback(static function (ArrayInput $newInput, OutputInterface $output) {
self::assertSame('foo --verbose=1', (string) $newInput);
$output->writeln('command output here');

return 0;
});

$command = $this->createMock(Command::class);
$command->method('getApplication')->willReturn($application);

$invoker = new InvokeSubCommand($output);
self::assertSame(0, ($invoker)($command, ['command' => 'foo'], $input));
self::assertSame('command output here', trim($output->fetch()));
}

public function testInvokeWithPrefixOutputFormatterRunsSubCommand(): void
{
$inputDefinition = new InputDefinition();
$inputDefinition->addOption(new InputOption('verbose', 'v', InputOption::VALUE_NONE, 'Verbose option'));
$input = new ArrayInput(['--verbose' => true], $inputDefinition);

$output = new BufferedOutput();

$application = $this->createMock(Application::class);
$application->expects(self::once())
->method('doRun')
->willReturnCallback(static function (ArrayInput $newInput, OutputInterface $output) {
self::assertSame('foo --verbose=1', (string) $newInput);
$output->writeln('command output here');

return 0;
});

$command = $this->createMock(Command::class);
$command->method('getApplication')->willReturn($application);

$invoker = new InvokeSubCommand($output);
self::assertSame(0, ($invoker)($command, ['command' => 'foo'], $input, new OutputFormatterWithPrefix('prefix> ')));
self::assertSame('prefix> command output here', trim($output->fetch()));
}
}
Loading