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
72 changes: 72 additions & 0 deletions src/Composer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace TypistTech\PhpMatrix;

use Composer\Semver\VersionParser;
use TypistTech\PhpMatrix\Exceptions\InvalidArgumentException;
use TypistTech\PhpMatrix\Exceptions\JsonException;
use TypistTech\PhpMatrix\Exceptions\UnexpectedValueException;

readonly class Composer
{
public static function fromFile(string $path): self
{
if (! is_readable($path)) {
$message = sprintf(
'The file is not readable or does not exist at path "%s".',
$path,
);
throw new InvalidArgumentException($message);
}

$content = (string) file_get_contents($path);

if (! json_validate($content)) {
$message = sprintf(
'The file is not a valid JSON at path "%s".',
$path,
);
throw new JsonException($message);
}

/** @var mixed[] $data */
$data = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

return new self($data);
}

/**
* @param mixed[] $data
*/
private function __construct(
private array $data,
private VersionParser $versionParser = new VersionParser,
) {}

public function requiredPhpConstraint(): string
{
$require = $this->data['require'] ?? [];
if (! is_array($require)) {
$require = [];
}
$constraint = $require['php'] ?? null;

if (! is_string($constraint)) {
throw new UnexpectedValueException('The "require.php" field is not set or not a string.');
}

try {
$this->versionParser->parseConstraints($constraint);
} catch (\UnexpectedValueException $e) {
$message = sprintf(
'The "require.php" field is not a valid version constraint: %s',
$e->getMessage(),
);
throw new UnexpectedValueException($message, previous: $e);
}

return $constraint;
}
}
1 change: 1 addition & 0 deletions src/Console/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static function make(): ConsoleApplication
);

$app->addCommands([
new ComposerCommand,
new ConstraintCommand,
]);

Expand Down
50 changes: 50 additions & 0 deletions src/Console/ComposerCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace TypistTech\PhpMatrix\Console;

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
use TypistTech\PhpMatrix\Composer;
use TypistTech\PhpMatrix\Exceptions\ExceptionInterface;

#[AsCommand(
name: 'composer',
description: 'List PHP versions that satisfy the required PHP constraint in composer.json',
)]
class ComposerCommand extends Command
{
use PrintErrorTrait;

public function __invoke(
SymfonyStyle $io,
Application $application,
#[Argument(description: 'Path to composer.json file.')]
string $path = './composer.json',
#[SourceOption]
string $source = Source::Auto->value,
#[ModeOption]
string $mode = Mode::MinorOnly->value,
): int {
try {
$composer = Composer::fromFile($path);

$constraint = $composer->requiredPhpConstraint();

/** @phpstan-ignore method.notFound,return.type */
return $application->get('constraint')
->__invoke($io, $constraint, $source, $mode);
} catch (ExceptionInterface $e) {
$this->printError(
$io,
$e->getMessage()
);

return Command::FAILURE;
}
}
}
53 changes: 25 additions & 28 deletions src/Console/ConstraintCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
use TypistTech\PhpMatrix\Exceptions\ExceptionInterface;
use TypistTech\PhpMatrix\Exceptions\RuntimeException;
use TypistTech\PhpMatrix\Versions;
use UnexpectedValueException;

#[AsCommand(
name: 'constraint',
Expand All @@ -34,43 +35,39 @@ public function __invoke(
#[ModeOption]
string $mode = Mode::MinorOnly->value,
): int {
$matrix = $this->matrixFactory->make(
Source::fromValue($source),
Mode::fromValue($mode),
);

try {
$matrix = $this->matrixFactory->make(
Source::fromValue($source),
Mode::fromValue($mode),
);

$versions = $matrix->satisfiedBy($constraint);
} catch (UnexpectedValueException $e) {
$this->printError(
$io,
$e->getMessage()
if ($versions === []) {
throw new RuntimeException(
sprintf('No PHP versions could satisfy the constraint "%s".', $constraint),
);
}

$result = json_encode(
(object) [
'constraint' => $constraint,
'versions' => Versions::sort(...$versions),
'lowest' => Versions::lowest(...$versions),
'highest' => Versions::highest(...$versions),
],
JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT
);

return Command::FAILURE;
}
$io->writeln($result);

if ($versions === []) {
return Command::SUCCESS;
} catch (ExceptionInterface $e) {
$this->printError(
$io,
sprintf('No PHP versions could satisfy the constraint "%s".', $constraint)
$e->getMessage()
);

return Command::FAILURE;
}

$result = json_encode(
(object) [
'constraint' => $constraint,
'versions' => Versions::sort(...$versions),
'lowest' => Versions::lowest(...$versions),
'highest' => Versions::highest(...$versions),
],
JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT
);

$io->writeln($result);

return Command::SUCCESS;
}
}
7 changes: 7 additions & 0 deletions src/Exceptions/ExceptionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace TypistTech\PhpMatrix\Exceptions;

interface ExceptionInterface {}
7 changes: 7 additions & 0 deletions src/Exceptions/InvalidArgumentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace TypistTech\PhpMatrix\Exceptions;

class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface {}
7 changes: 7 additions & 0 deletions src/Exceptions/JsonException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace TypistTech\PhpMatrix\Exceptions;

class JsonException extends \JsonException implements ExceptionInterface {}
7 changes: 7 additions & 0 deletions src/Exceptions/RuntimeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace TypistTech\PhpMatrix\Exceptions;

class RuntimeException extends \RuntimeException implements ExceptionInterface {}
7 changes: 7 additions & 0 deletions src/Exceptions/UnexpectedValueException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

declare(strict_types=1);

namespace TypistTech\PhpMatrix\Exceptions;

class UnexpectedValueException extends \UnexpectedValueException implements ExceptionInterface {}
17 changes: 13 additions & 4 deletions src/Matrix.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace TypistTech\PhpMatrix;

use Composer\Semver\Semver;
use UnexpectedValueException;

readonly class Matrix implements MatrixInterface
{
Expand All @@ -17,9 +18,17 @@ public function __construct(
*/
public function satisfiedBy(string $constraint): array
{
return Semver::satisfiedBy(
$this->releases->all(),
$constraint
);
try {
return Semver::satisfiedBy(
$this->releases->all(),
$constraint
);
} catch (UnexpectedValueException $e) {
throw new Exceptions\UnexpectedValueException(
$e->getMessage(),
previous: $e
);
}

}
}
2 changes: 1 addition & 1 deletion src/Releases/OfflineReleases.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace TypistTech\PhpMatrix\Releases;

use RuntimeException;
use TypistTech\PhpMatrix\Exceptions\RuntimeException;
use TypistTech\PhpMatrix\ReleasesInterface;

class OfflineReleases implements ReleasesInterface
Expand Down
67 changes: 0 additions & 67 deletions tests/Feature/Console/ConstraintCommandTest.php

This file was deleted.

25 changes: 0 additions & 25 deletions tests/Feature/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Mockery;
use Tests\TestCase as BaseTestCase;
use TypistTech\PhpMatrix\MatrixInterface;

abstract class TestCase extends BaseTestCase
{
Expand All @@ -36,27 +34,4 @@ protected function mockHttpClient(): Http

return new Http(['handler' => $handlerStack]);
}

public function mockMatrix(): array
{
$constraint = '^1.2.3';
$expectedObject = (object) [
'constraint' => $constraint,
'versions' => ['1.2.2', '1.2.4', '1.3.3', '1.4.4'],
'lowest' => '1.2.2',
'highest' => '1.4.4',
];

$matrix = Mockery::mock(MatrixInterface::class);

$matrix->expects()
->satisfiedBy($constraint)
->andReturn($expectedObject->versions);

return [
'matrix' => $matrix,
'constraint' => $constraint,
'expectedObject' => $expectedObject,
];
}
}
Loading