mirror of
https://github.com/php/pie.git
synced 2026-03-23 23:12:17 +01:00
Merge branch '1.3.x' into 1.4.x
This commit is contained in:
@@ -264,18 +264,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
|
||||
@@ -468,12 +456,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: '#^Access to an undefined property Php\\PieUnitTest\\SelfManage\\BuildTools\\PhpizeBuildToolFinderTest\:\:\$phpBinaryPath\.$#'
|
||||
identifier: property.notFound
|
||||
|
||||
@@ -12,6 +12,7 @@ use OutOfRangeException;
|
||||
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;
|
||||
@@ -28,6 +29,7 @@ use Symfony\Component\Console\Command\Command;
|
||||
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;
|
||||
@@ -268,20 +270,26 @@ final class InstallExtensionsForProjectCommand extends Command
|
||||
$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;
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
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;
|
||||
@@ -29,6 +30,7 @@ class InvokeSubCommand
|
||||
Command $command,
|
||||
array $subCommandInput,
|
||||
InputInterface $originalCommandInput,
|
||||
OutputFormatter|null $formatter = null,
|
||||
): int {
|
||||
$originalSuppliedOptions = array_filter($originalCommandInput->getOptions());
|
||||
$installForProjectInput = new ArrayInput(array_merge(
|
||||
@@ -42,6 +44,19 @@ class InvokeSubCommand
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,4 +27,13 @@ final class RequestedPackageAndVersion
|
||||
throw InvalidPackageName::fromMissingForwardSlash($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function prettyNameAndVersion(): string
|
||||
{
|
||||
if ($this->version === null) {
|
||||
return $this->package;
|
||||
}
|
||||
|
||||
return $this->package . ':' . $this->version;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,71 +4,37 @@ declare(strict_types=1);
|
||||
|
||||
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
src/Util/OutputFormatterWithPrefix.php
Normal file
38
src/Util/OutputFormatterWithPrefix.php
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
@echo off
|
||||
|
||||
echo Params passed: %*
|
||||
|
||||
exit /b 0
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo "Params passed: ${*}"
|
||||
exit 0
|
||||
@@ -17,6 +17,8 @@ use Php\Pie\ComposerIntegration\MinimalHelperSet;
|
||||
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;
|
||||
@@ -125,8 +127,15 @@ final class InstallExtensionsForProjectCommandTest extends TestCase
|
||||
$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],
|
||||
@@ -173,7 +182,7 @@ final class InstallExtensionsForProjectCommandTest extends TestCase
|
||||
$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],
|
||||
|
||||
75
test/unit/Command/InvokeSubCommandTest.php
Normal file
75
test/unit/Command/InvokeSubCommandTest.php
Normal file
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Php\PieUnitTest\DependencyResolver;
|
||||
|
||||
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[CoversClass(RequestedPackageAndVersion::class)]
|
||||
final class RequestedPackageAndVersionTest extends TestCase
|
||||
{
|
||||
public function testPrettyNameAndVersionWithVersion(): void
|
||||
{
|
||||
self::assertSame(
|
||||
'foo/foo:^1.2.3',
|
||||
(new RequestedPackageAndVersion('foo/foo', '^1.2.3'))->prettyNameAndVersion(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testPrettyNameAndVersionWithoutVersion(): void
|
||||
{
|
||||
self::assertSame(
|
||||
'foo/foo',
|
||||
(new RequestedPackageAndVersion('foo/foo', null))->prettyNameAndVersion(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,46 +4,46 @@ declare(strict_types=1);
|
||||
|
||||
namespace Php\PieUnitTest\Installing\InstallForPhpProject;
|
||||
|
||||
use Composer\IO\BufferIO;
|
||||
use Composer\Util\Platform;
|
||||
use Php\Pie\File\FullPathToSelf;
|
||||
use Php\Pie\Command\InvokeSubCommand;
|
||||
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
|
||||
use Php\Pie\ExtensionName;
|
||||
use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage;
|
||||
use Php\Pie\Util\OutputFormatterWithPrefix;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
use function getcwd;
|
||||
use function trim;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
#[CoversClass(InstallSelectedPackage::class)]
|
||||
final class InstallSelectedPackageTest extends TestCase
|
||||
{
|
||||
private const FAKE_HAPPY_SH = __DIR__ . '/../../../assets/fake-pie-cli/happy.sh';
|
||||
private const FAKE_HAPPY_BAT = __DIR__ . '/../../../assets/fake-pie-cli/happy.bat';
|
||||
|
||||
public function testWithPieCli(): void
|
||||
public function testSubCommandIsInvoked(): void
|
||||
{
|
||||
$_SERVER['PHP_SELF'] = Platform::isWindows() ? self::FAKE_HAPPY_BAT : self::FAKE_HAPPY_SH;
|
||||
$command = $this->createMock(Command::class);
|
||||
$input = $this->createMock(InputInterface::class);
|
||||
$invoker = $this->createMock(InvokeSubCommand::class);
|
||||
$invoker->expects(self::once())
|
||||
->method('__invoke')
|
||||
->with(
|
||||
$command,
|
||||
[
|
||||
'command' => 'install',
|
||||
'requested-package-and-version' => 'foo/foo:^1.0',
|
||||
],
|
||||
$input,
|
||||
self::isInstanceOf(OutputFormatterWithPrefix::class),
|
||||
)
|
||||
->willReturn(0);
|
||||
|
||||
$input = new ArrayInput(
|
||||
['--with-php-config' => '/path/to/php/config'],
|
||||
new InputDefinition([
|
||||
new InputOption('with-php-config', null, InputOption::VALUE_REQUIRED),
|
||||
]),
|
||||
);
|
||||
$io = new BufferIO();
|
||||
|
||||
(new InstallSelectedPackage(new FullPathToSelf(getcwd())))->withPieCli(
|
||||
'foo/bar',
|
||||
$installer = new InstallSelectedPackage($invoker);
|
||||
$installer->withSubCommand(
|
||||
ExtensionName::normaliseFromString('foo'),
|
||||
new RequestedPackageAndVersion(
|
||||
'foo/foo',
|
||||
'^1.0',
|
||||
),
|
||||
$command,
|
||||
$input,
|
||||
$io,
|
||||
);
|
||||
|
||||
self::assertSame(
|
||||
'> Params passed: install foo/bar --with-php-config /path/to/php/config',
|
||||
trim($io->getOutput()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user