Skip to content

Commit

Permalink
Refactor the import layer
Browse files Browse the repository at this point in the history
  • Loading branch information
pyrech committed Mar 28, 2024
1 parent 5cfa1de commit 059e1a7
Show file tree
Hide file tree
Showing 72 changed files with 16,944 additions and 166 deletions.
9 changes: 5 additions & 4 deletions bin/generate-tests.php
Expand Up @@ -123,7 +123,7 @@
;
foreach ($dirs as $dir) {
$class = u($dir->getRelativePath())->camel()->title()->append('Test')->toString();
add_test([], $class, '{{ base }}/tests/Examples/fixtures/broken/' . $dir->getRelativePath());
add_test([], $class, '{{ base }}/tests/Examples/fixtures/broken/' . $dir->getRelativePath(), true);
}

add_test(['args:passthru', 'a', 'b', '--no', '--foo', 'bar', '-x'], 'ArgPassthruExpanded');
Expand All @@ -142,7 +142,7 @@
add_test(['unknown:task'], 'NoConfigUnknownTest', '/tmp');
add_test([], 'NewProjectTest', '/tmp');

function add_test(array $args, string $class, ?string $cwd = null)
function add_test(array $args, string $class, ?string $cwd = null, bool $needRemote = false)
{
$fp = fopen(__FILE__, 'r');
fseek($fp, __COMPILER_HALT_OFFSET__ + 1);
Expand All @@ -154,7 +154,7 @@ function add_test(array $args, string $class, ?string $cwd = null)
env: [
'COLUMNS' => 120,
'ENDPOINT' => $_SERVER['ENDPOINT'],
'CASTOR_NO_REMOTE' => 1,
'CASTOR_NO_REMOTE' => $needRemote ? 0 : 1,
],
timeout: null,
);
Expand All @@ -166,6 +166,7 @@ function add_test(array $args, string $class, ?string $cwd = null)
'{{ args }}' => implode(', ', array_map(fn ($arg) => var_export($arg, true), $args)),
'{{ exitCode }}' => $process->getExitCode(),
'{{ cwd }}' => $cwd ? ', ' . var_export($cwd, true) : '',
'{{ needRemote }}' => $needRemote ? ', needRemote: true' : '',
]);

file_put_contents(__DIR__ . '/../tests/Examples/Generated/' . $class . '.php', $code);
Expand All @@ -188,7 +189,7 @@ class {{ class_name }} extends TaskTestCase
// {{ task }}
public function test(): void
{
$process = $this->runTask([{{ args }}]{{ cwd }});
$process = $this->runTask([{{ args }}]{{ cwd }}{{ needRemote }});

$this->assertSame({{ exitCode }}, $process->getExitCode());
$this->assertStringEqualsFile(__FILE__ . '.output.txt', $process->getOutput());
Expand Down
5 changes: 4 additions & 1 deletion composer.json
Expand Up @@ -18,7 +18,10 @@
"psr-4": {
"Castor\\": "src/"
},
"files": ["src/functions.php"]
"files": [
"src/functions.php",
"src/functions-internal.php"
]
},
"autoload-dev": {
"psr-4": {
Expand Down
2 changes: 1 addition & 1 deletion doc/going-further/extending-castor/remote-imports.md
Expand Up @@ -75,7 +75,7 @@ import('composer://vendor-name/project-name', source: [
```

> [!NOTE]
> The "vendor-name/project-name" name can be whatever you want and we only be
> The "vendor-name/project-name" name can be whatever you want, and will only be
> used internally by Castor and Composer to make the repository behave like a
> normal Composer package.
Expand Down
7 changes: 7 additions & 0 deletions phpstan.neon
Expand Up @@ -19,3 +19,10 @@ parameters:
foo?: string,
}
'''
ImportSource: '''
array{
url?: string,
type?: "git" | "svn",
reference?: string,
}
'''
2 changes: 1 addition & 1 deletion src/Console/Application.php
Expand Up @@ -21,8 +21,8 @@
use Castor\Fingerprint\FingerprintHelper;
use Castor\FunctionFinder;
use Castor\GlobalHelper;
use Castor\Import\Importer;
use Castor\PlatformHelper;
use Castor\Remote\Importer;
use Castor\WaitForHelper;
use Monolog\Logger;
use Psr\Cache\CacheItemPoolInterface;
Expand Down
17 changes: 9 additions & 8 deletions src/Console/ApplicationFactory.php
Expand Up @@ -10,15 +10,15 @@
use Castor\ExpressionLanguage;
use Castor\Fingerprint\FingerprintHelper;
use Castor\FunctionFinder;
use Castor\HasherHelper;
use Castor\Import\Importer;
use Castor\Import\Listener\RemoteImportListener;
use Castor\Import\Remote\Composer;
use Castor\Import\Remote\PackageImporter;
use Castor\Listener\GenerateStubsListener;
use Castor\Listener\UpdateCastorListener;
use Castor\Monolog\Processor\ProcessProcessor;
use Castor\PathHelper;
use Castor\PlatformHelper;
use Castor\Remote\Composer;
use Castor\Remote\Importer;
use Castor\Remote\Listener\RemoteImportListener;
use Castor\Stub\StubsGenerator;
use Castor\WaitForHelper;
use Monolog\Logger;
Expand Down Expand Up @@ -54,7 +54,8 @@ public static function create(): SymfonyApplication
$logger = new Logger('castor', [], [new ProcessProcessor()]);
$fs = new Filesystem();
$fingerprintHelper = new FingerprintHelper($cache);
$importer = new Importer($logger, new Composer($fs, $logger, $fingerprintHelper));
$packageImporter = new PackageImporter($logger, new Composer($fs, $logger, $fingerprintHelper));
$importer = new Importer($packageImporter, $logger);
$eventDispatcher = new EventDispatcher(logger: $logger);
$eventDispatcher->addSubscriber(new UpdateCastorListener(
$cache,
Expand All @@ -65,9 +66,9 @@ public static function create(): SymfonyApplication
new StubsGenerator($logger),
$rootDir,
));
$eventDispatcher->addSubscriber(new RemoteImportListener($importer));
$eventDispatcher->addSubscriber(new RemoteImportListener($packageImporter));

/** @var SymfonyApplication */
/** @var Application */
// @phpstan-ignore-next-line
$application = new $class(
$rootDir,
Expand All @@ -85,7 +86,7 @@ public static function create(): SymfonyApplication
);

// Avoid dependency cycle
$importer->setApplication($application);
$packageImporter->setApplication($application);

$application->setDispatcher($eventDispatcher);
$application->add(new DebugCommand($rootDir, $cacheDir, $contextRegistry));
Expand Down
13 changes: 0 additions & 13 deletions src/FunctionFinder.php
Expand Up @@ -243,16 +243,3 @@ private function resolveListeners(\ReflectionFunction $reflectionFunction): iter
}
}
}

// Don't leak internal variables
/** @internal */
function castor_require(string $file): void
{
if (!is_file($file)) {
throw new \RuntimeException(sprintf('Could not find file "%s".', $file));
}

FunctionFinder::$files[] = $file;

require_once $file;
}
7 changes: 7 additions & 0 deletions src/Import/Exception/ComposerError.php
@@ -0,0 +1,7 @@
<?php

namespace Castor\Import\Exception;

class ComposerError extends ImportError
{
}
@@ -1,6 +1,6 @@
<?php

namespace Castor\Remote\Exception;
namespace Castor\Import\Exception;

class ImportError extends \RuntimeException
{
Expand Down
7 changes: 7 additions & 0 deletions src/Import/Exception/InvalidImportFormat.php
@@ -0,0 +1,7 @@
<?php

namespace Castor\Import\Exception;

class InvalidImportFormat extends ImportError
{
}
7 changes: 7 additions & 0 deletions src/Import/Exception/RemoteNotAllowed.php
@@ -0,0 +1,7 @@
<?php

namespace Castor\Import\Exception;

class RemoteNotAllowed extends \RuntimeException
{
}
97 changes: 97 additions & 0 deletions src/Import/Importer.php
@@ -0,0 +1,97 @@
<?php

namespace Castor\Import;

use Castor\Import\Exception\ImportError;
use Castor\Import\Exception\RemoteNotAllowed;
use Castor\Import\Remote\PackageImporter;
use Psr\Log\LoggerInterface;
use Symfony\Component\Finder\Finder;

use function Castor\castor_require;
use function Castor\fix_exception;

/** @internal */
class Importer
{
public function __construct(
private readonly PackageImporter $packageImporter,
private readonly LoggerInterface $logger,
) {
}

/** @phpstan-param ImportSource $source */
public function import(string $path, ?string $file = null, ?string $version = null, ?string $vcs = null, ?array $source = null): void
{
$scheme = parse_url($path, \PHP_URL_SCHEME);

if ($scheme) {
$package = mb_substr($path, mb_strlen($scheme) + 3);

try {
$this->packageImporter->importPackage(
$scheme,
$package,
$file,
$version,
$vcs,
$source,
);

return;
} catch (ImportError $e) {
throw $this->createImportException($package, $e->getMessage(), $e);
} catch (RemoteNotAllowed $e) {
$this->logger->warning($this->getImportLocatedMessage($path, $e->getMessage(), 1));

return;
}
} elseif (null !== $file || null !== $version || null !== $vcs || null !== $source) {
throw $this->createImportException($path, 'The "file", "version", "vcs" and "source" arguments can only be used with a remote import.');
}

if (!file_exists($path)) {
throw $this->createImportException($path, sprintf('The file "%s" does not exist.', $path));
}

if (is_file($path)) {
castor_require($path);
}

if (is_dir($path)) {
$files = Finder::create()
->files()
->name('*.php')
->in($path)
;

foreach ($files as $file) {
castor_require($file->getPathname());
}
}
}

private function getImportLocatedMessage(string $path, string $reason, int $depth): string
{
/** @var array{file: string, line: int} $caller */
$caller = debug_backtrace()[$depth + 1];

return sprintf(
'Could not import "%s" in "%s" on line %d. Reason: %s',
$path,
$caller['file'],
$caller['line'],
$reason,
);
}

private function createImportException(string $path, string $message, ?\Throwable $e = null): \Throwable
{
$depth = 2;

return fix_exception(
new \InvalidArgumentException($this->getImportLocatedMessage($path, $message, $depth), previous: $e),
$depth
);
}
}
@@ -1,16 +1,16 @@
<?php

namespace Castor\Remote\Listener;
namespace Castor\Import\Listener;

use Castor\Event\AfterApplicationInitializationEvent;
use Castor\Remote\Importer;
use Castor\Import\Remote\PackageImporter;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/** @internal */
class RemoteImportListener implements EventSubscriberInterface
{
public function __construct(
private readonly Importer $importer,
private readonly PackageImporter $packageImporter,
) {
}

Expand All @@ -23,6 +23,6 @@ public static function getSubscribedEvents(): array

public function afterInitialize(AfterApplicationInitializationEvent $event): void
{
$this->importer->fetchPackages($event->application->getInput());
$this->packageImporter->fetchPackages($event->application->getInput());
}
}
8 changes: 3 additions & 5 deletions src/Remote/Composer.php → src/Import/Remote/Composer.php
@@ -1,20 +1,18 @@
<?php

namespace Castor\Remote;
namespace Castor\Import\Remote;

use Castor\Console\Application;
use Castor\Fingerprint\FingerprintHelper;
use Castor\GlobalHelper;
use Castor\Import\Exception\ComposerError;
use Castor\PathHelper;
use Castor\Remote\Exception\ComposerError;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Helper\ProgressIndicator;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;

use function Castor\fingerprint;

/** @internal */
class Composer
{
Expand Down Expand Up @@ -71,7 +69,7 @@ public function update(bool $force = false, bool $displayProgress = true): void
$this->writeJsonFile($dir . 'composer.json', $this->configuration);

$ran = false;
$fingerprint = json_encode($this->configuration, \JSON_THROW_ON_ERROR);
$fingerprint = json_encode($this->configuration, \JSON_THROW_ON_ERROR);

if ($force || !$this->fingerprintHelper->verifyFingerprintFromHash($fingerprint)) {
$progressIndicator = null;
Expand Down

0 comments on commit 059e1a7

Please sign in to comment.