Skip to content

Commit 0adb128

Browse files
authored
Add new sub-command composer (#49)
1 parent 7d7894f commit 0adb128

15 files changed

+297
-125
lines changed

src/Composer.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypistTech\PhpMatrix;
6+
7+
use Composer\Semver\VersionParser;
8+
use TypistTech\PhpMatrix\Exceptions\InvalidArgumentException;
9+
use TypistTech\PhpMatrix\Exceptions\JsonException;
10+
use TypistTech\PhpMatrix\Exceptions\UnexpectedValueException;
11+
12+
readonly class Composer
13+
{
14+
public static function fromFile(string $path): self
15+
{
16+
if (! is_readable($path)) {
17+
$message = sprintf(
18+
'The file is not readable or does not exist at path "%s".',
19+
$path,
20+
);
21+
throw new InvalidArgumentException($message);
22+
}
23+
24+
$content = (string) file_get_contents($path);
25+
26+
if (! json_validate($content)) {
27+
$message = sprintf(
28+
'The file is not a valid JSON at path "%s".',
29+
$path,
30+
);
31+
throw new JsonException($message);
32+
}
33+
34+
/** @var mixed[] $data */
35+
$data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
36+
37+
return new self($data);
38+
}
39+
40+
/**
41+
* @param mixed[] $data
42+
*/
43+
private function __construct(
44+
private array $data,
45+
private VersionParser $versionParser = new VersionParser,
46+
) {}
47+
48+
public function requiredPhpConstraint(): string
49+
{
50+
$require = $this->data['require'] ?? [];
51+
if (! is_array($require)) {
52+
$require = [];
53+
}
54+
$constraint = $require['php'] ?? null;
55+
56+
if (! is_string($constraint)) {
57+
throw new UnexpectedValueException('The "require.php" field is not set or not a string.');
58+
}
59+
60+
try {
61+
$this->versionParser->parseConstraints($constraint);
62+
} catch (\UnexpectedValueException $e) {
63+
$message = sprintf(
64+
'The "require.php" field is not a valid version constraint: %s',
65+
$e->getMessage(),
66+
);
67+
throw new UnexpectedValueException($message, previous: $e);
68+
}
69+
70+
return $constraint;
71+
}
72+
}

src/Console/Application.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public static function make(): ConsoleApplication
1919
);
2020

2121
$app->addCommands([
22+
new ComposerCommand,
2223
new ConstraintCommand,
2324
]);
2425

src/Console/ComposerCommand.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypistTech\PhpMatrix\Console;
6+
7+
use Symfony\Component\Console\Application;
8+
use Symfony\Component\Console\Attribute\Argument;
9+
use Symfony\Component\Console\Attribute\AsCommand;
10+
use Symfony\Component\Console\Command\Command;
11+
use Symfony\Component\Console\Style\SymfonyStyle;
12+
use TypistTech\PhpMatrix\Composer;
13+
use TypistTech\PhpMatrix\Exceptions\ExceptionInterface;
14+
15+
#[AsCommand(
16+
name: 'composer',
17+
description: 'List PHP versions that satisfy the required PHP constraint in composer.json',
18+
)]
19+
class ComposerCommand extends Command
20+
{
21+
use PrintErrorTrait;
22+
23+
public function __invoke(
24+
SymfonyStyle $io,
25+
Application $application,
26+
#[Argument(description: 'Path to composer.json file.')]
27+
string $path = './composer.json',
28+
#[SourceOption]
29+
string $source = Source::Auto->value,
30+
#[ModeOption]
31+
string $mode = Mode::MinorOnly->value,
32+
): int {
33+
try {
34+
$composer = Composer::fromFile($path);
35+
36+
$constraint = $composer->requiredPhpConstraint();
37+
38+
/** @phpstan-ignore method.notFound,return.type */
39+
return $application->get('constraint')
40+
->__invoke($io, $constraint, $source, $mode);
41+
} catch (ExceptionInterface $e) {
42+
$this->printError(
43+
$io,
44+
$e->getMessage()
45+
);
46+
47+
return Command::FAILURE;
48+
}
49+
}
50+
}

src/Console/ConstraintCommand.php

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
use Symfony\Component\Console\Attribute\AsCommand;
99
use Symfony\Component\Console\Command\Command;
1010
use Symfony\Component\Console\Style\SymfonyStyle;
11+
use TypistTech\PhpMatrix\Exceptions\ExceptionInterface;
12+
use TypistTech\PhpMatrix\Exceptions\RuntimeException;
1113
use TypistTech\PhpMatrix\Versions;
12-
use UnexpectedValueException;
1314

1415
#[AsCommand(
1516
name: 'constraint',
@@ -34,43 +35,39 @@ public function __invoke(
3435
#[ModeOption]
3536
string $mode = Mode::MinorOnly->value,
3637
): int {
37-
$matrix = $this->matrixFactory->make(
38-
Source::fromValue($source),
39-
Mode::fromValue($mode),
40-
);
41-
4238
try {
39+
$matrix = $this->matrixFactory->make(
40+
Source::fromValue($source),
41+
Mode::fromValue($mode),
42+
);
43+
4344
$versions = $matrix->satisfiedBy($constraint);
44-
} catch (UnexpectedValueException $e) {
45-
$this->printError(
46-
$io,
47-
$e->getMessage()
45+
if ($versions === []) {
46+
throw new RuntimeException(
47+
sprintf('No PHP versions could satisfy the constraint "%s".', $constraint),
48+
);
49+
}
50+
51+
$result = json_encode(
52+
(object) [
53+
'constraint' => $constraint,
54+
'versions' => Versions::sort(...$versions),
55+
'lowest' => Versions::lowest(...$versions),
56+
'highest' => Versions::highest(...$versions),
57+
],
58+
JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT
4859
);
4960

50-
return Command::FAILURE;
51-
}
61+
$io->writeln($result);
5262

53-
if ($versions === []) {
63+
return Command::SUCCESS;
64+
} catch (ExceptionInterface $e) {
5465
$this->printError(
5566
$io,
56-
sprintf('No PHP versions could satisfy the constraint "%s".', $constraint)
67+
$e->getMessage()
5768
);
5869

5970
return Command::FAILURE;
6071
}
61-
62-
$result = json_encode(
63-
(object) [
64-
'constraint' => $constraint,
65-
'versions' => Versions::sort(...$versions),
66-
'lowest' => Versions::lowest(...$versions),
67-
'highest' => Versions::highest(...$versions),
68-
],
69-
JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT
70-
);
71-
72-
$io->writeln($result);
73-
74-
return Command::SUCCESS;
7572
}
7673
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypistTech\PhpMatrix\Exceptions;
6+
7+
interface ExceptionInterface {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypistTech\PhpMatrix\Exceptions;
6+
7+
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface {}

src/Exceptions/JsonException.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypistTech\PhpMatrix\Exceptions;
6+
7+
class JsonException extends \JsonException implements ExceptionInterface {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypistTech\PhpMatrix\Exceptions;
6+
7+
class RuntimeException extends \RuntimeException implements ExceptionInterface {}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypistTech\PhpMatrix\Exceptions;
6+
7+
class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface {}

src/Matrix.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace TypistTech\PhpMatrix;
66

77
use Composer\Semver\Semver;
8+
use UnexpectedValueException;
89

910
readonly class Matrix implements MatrixInterface
1011
{
@@ -17,9 +18,17 @@ public function __construct(
1718
*/
1819
public function satisfiedBy(string $constraint): array
1920
{
20-
return Semver::satisfiedBy(
21-
$this->releases->all(),
22-
$constraint
23-
);
21+
try {
22+
return Semver::satisfiedBy(
23+
$this->releases->all(),
24+
$constraint
25+
);
26+
} catch (UnexpectedValueException $e) {
27+
throw new Exceptions\UnexpectedValueException(
28+
$e->getMessage(),
29+
previous: $e
30+
);
31+
}
32+
2433
}
2534
}

0 commit comments

Comments
 (0)