diff --git a/composer.json b/composer.json index d06b0c6..486acee 100644 --- a/composer.json +++ b/composer.json @@ -21,8 +21,11 @@ ], "require": { "php": ">=8.1", - "symfony/http-client": "^4.4.11 || ^5.0 || ^6.0 || ^7.0", - "yiisoft/injector": "^1.2" + "react/promise": "2 - 3", + "react/async": "3 - 4", + "symfony/http-client": "4 - 7", + "psr/container": "1 - 2", + "yiisoft/injector": "^1" }, "require-dev": { "buggregator/trap": "^1.10", diff --git a/composer.lock b/composer.lock index a187d2e..46998f4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2a399b2da365c5e93f15813f1b60f221", + "content-hash": "dbf39497fd94af52687a2b0354f74e8c", "packages": [ { "name": "psr/container", @@ -109,6 +109,226 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "react/async", + "version": "v4.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/async.git", + "reference": "635d50e30844a484495713e8cb8d9e079c0008a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/async/zipball/635d50e30844a484495713e8cb8d9e079c0008a5", + "reference": "635d50e30844a484495713e8cb8d9e079c0008a5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.8 || ^1.2.1" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Async\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async utilities and fibers for ReactPHP", + "keywords": [ + "async", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/async/issues", + "source": "https://github.com/reactphp/async/tree/v4.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-04T14:40:02+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, { "name": "symfony/deprecation-contracts", "version": "v3.5.0", @@ -3748,151 +3968,6 @@ ], "time": "2024-06-13T14:18:03+00:00" }, - { - "name": "react/event-loop", - "version": "v1.5.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "suggest": { - "ext-pcntl": "For signal handling support when using the StreamSelectLoop" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\EventLoop\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", - "keywords": [ - "asynchronous", - "event-loop" - ], - "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-11-13T13:48:05+00:00" - }, - { - "name": "react/promise", - "version": "v3.2.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", - "shasum": "" - }, - "require": { - "php": ">=7.1.0" - }, - "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^7.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], - "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2024-05-24T10:39:05+00:00" - }, { "name": "react/socket", "version": "v1.15.0", diff --git a/dload.xml b/dload.xml index 70ba706..ce9176c 100644 --- a/dload.xml +++ b/dload.xml @@ -1,16 +1,16 @@ - - + + - + - + diff --git a/src/Command/Get.php b/src/Command/Get.php index 4e71613..9d3b277 100644 --- a/src/Command/Get.php +++ b/src/Command/Get.php @@ -6,9 +6,14 @@ use Internal\DLoad\Bootstrap; use Internal\DLoad\Module\Common\Architecture; +use Internal\DLoad\Module\Common\Config\BuildInput; +use Internal\DLoad\Module\Common\Config\Destination; use Internal\DLoad\Module\Common\OperatingSystem; use Internal\DLoad\Module\Common\Stability; +use Internal\DLoad\Module\Downloader\Downloader; +use Internal\DLoad\Module\Downloader\SoftwareCollection; use Internal\DLoad\Module\Repository\Internal\GitHub\GitHubRepository; +use Internal\DLoad\Service\Logger; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\SignalableCommandInterface; @@ -27,6 +32,7 @@ final class Get extends Command implements SignalableCommandInterface { private bool $cancelling = false; + private Logger $logger; public function configure(): void { @@ -66,6 +72,7 @@ protected function execute( InputInterface $input, OutputInterface $output, ): int { + $this->logger = new Logger($output); $output->writeln('Binary to load: ' . $input->getArgument('binary')); $output->writeln('Path to store the binary: ' . $input->getOption('path')); @@ -75,13 +82,27 @@ protected function execute( inputArguments: $input->getArguments(), environment: \getenv(), )->finish(); + $container->set($input, InputInterface::class); + $container->set($this->logger); $output->writeln('Architecture: ' . $container->get(Architecture::class)->name); $output->writeln('Op. system: ' . $container->get(OperatingSystem::class)->name); $output->writeln('Stability: ' . $container->get(Stability::class)->name); - tr($container->get(\Internal\DLoad\Module\Common\Config\SoftwareRegistry::class)); + /** @var SoftwareCollection $softwareCollection */ + $softwareCollection = $container->get(SoftwareCollection::class); + + /** @var Downloader $downloader */ + $downloader = $container->get(Downloader::class); + $task = $downloader->download( + $softwareCollection->findSoftware('rr') ?? throw new \RuntimeException('Software not found.'), + $container->get(Destination::class), + // trap(...), + fn() => null, + ); + + ($task->handler)(); // $repo = 'roadrunner-server/roadrunner'; // trap( diff --git a/src/Module/Common/Config/DestinationInput.php b/src/Module/Common/Config/Destination.php similarity index 91% rename from src/Module/Common/Config/DestinationInput.php rename to src/Module/Common/Config/Destination.php index b10d27d..f6527c2 100644 --- a/src/Module/Common/Config/DestinationInput.php +++ b/src/Module/Common/Config/Destination.php @@ -9,7 +9,7 @@ /** * @internal */ -final class DestinationInput +final class Destination { #[InputOption('path')] public ?string $path = null; diff --git a/src/Module/Common/Config/DownloaderConfig.php b/src/Module/Common/Config/DownloaderConfig.php new file mode 100644 index 0000000..f9e9f86 --- /dev/null +++ b/src/Module/Common/Config/DownloaderConfig.php @@ -0,0 +1,10 @@ +alias ?? \strtolower($this->name); + } } diff --git a/src/Module/Common/Config/SoftwareRegistry.php b/src/Module/Common/Config/SoftwareRegistry.php index f87b206..be299ce 100644 --- a/src/Module/Common/Config/SoftwareRegistry.php +++ b/src/Module/Common/Config/SoftwareRegistry.php @@ -8,6 +8,9 @@ final class SoftwareRegistry { + /** + * @var Embed\Software[] + */ #[XPathEmbedList('/dload/registry/software', Embed\Software::class)] public array $software = []; } diff --git a/src/Module/Downloader/Downloader.php b/src/Module/Downloader/Downloader.php new file mode 100644 index 0000000..b1cf769 --- /dev/null +++ b/src/Module/Downloader/Downloader.php @@ -0,0 +1,183 @@ +repositories; + $task->handler = function () use ($repositories, $context): void { + // todo Try every repo to load software. + start: + $repositories === [] and throw new \RuntimeException('No relevant repository found.'); + $context->repoConfig = \array_shift($repositories); + $repository = $this->repositoryProvider->getByConfig($context->repoConfig); + + $this->logger->debug('Trying to load from repo `%s`', $repository->getName()); + + try { + await(coroutine($this->processRepository($repository, $context))); + } catch (\Throwable $e) { + $this->logger->exception($e); + goto start; + } + }; + return $task; + } + + /** + * @return \Closure(): ReleaseInterface + */ + private function processRepository(RepositoryInterface $repository, DownloadContext $context): \Closure + { + return function () use ($repository, $context): ReleaseInterface { + /** @var ReleaseInterface[] $releases */ + $releases = $repository->getReleases() + ->minimumStability($this->stability) + ->sortByVersion()->toArray(); + + $this->logger->debug('%d releases found.', \count($releases)); + + process_release: + $releases === [] and throw new \RuntimeException('No relevant release found.'); + $release = \array_shift($releases); + + $this->logger->debug('Trying to load release `%s`', $release->getName()); + + try { + await(coroutine($this->processRelease($release, $context))); + return $release; + } catch (\Throwable $e) { + $this->logger->exception($e); + goto process_release; + } + }; + } + + /** + * @return \Closure(): AssetInterface + */ + private function processRelease(ReleaseInterface $asset, DownloadContext $context): \Closure + { + return function () use ($asset, $context): AssetInterface { + /** @var AssetInterface[] $assets */ + $assets = $asset->getAssets() + ->whereArchitecture($this->architecture) + ->whereOperatingSystem($this->operatingSystem) + ->whereNameMatches($context->repoConfig->assetPattern) + ->toArray(); + + $this->logger->debug('%d assets found.', \count($assets)); + + process_asset: + $assets === [] and throw new \RuntimeException('No relevant asset found.'); + $asset = \array_shift($assets); + $this->logger->debug('Trying to load asset `%s`', $asset->getName()); + try { + await(coroutine($this->processAsset($asset, $context))); + return $asset; + } catch (\Throwable $e) { + $this->logger->exception($e); + goto process_asset; + } + }; + } + + private function processAsset(AssetInterface $asset, DownloadContext $context): \Closure + { + return function () use ($asset, $context): void { + // Create a file + $temp = $this->getTempDirectory() . '/' . $asset->getName(); + $file = new \SplFileObject($temp, 'wb+'); + + $this->logger->info('Downloading into ' . $temp); + + await(coroutine( + (static function () use ($asset, $context, $file): void { + $generator = $asset->download( + static fn(int $dlNow, int $dlSize, array $info) => ($context->onProgress)( + new Progress( + step: 1, + steps: 2, + total: $dlSize, + current: $dlNow, + message: 'downloading...', + ), + ), + ); + + foreach ($generator as $chunk) { + $file->fwrite($chunk); + } + }), + )->then(null, static function (\Throwable $e) use ($file): void { + @\unlink($file->getPath()); + throw $e; + })); + + // todo Unpack + $this->logger->info('Downloaded into ' . $temp); + }; + } + + private function getTempDirectory(): string + { + $temp = $this->config->tmpDir; + if ($temp !== null) { + (\is_dir($temp) && \is_writable($temp)) or throw new \LogicException( + \sprintf('Directory "%s" is not writeable.', $temp), + ); + + return $temp; + } + + return \sys_get_temp_dir(); + } +} diff --git a/src/Module/Downloader/Internal/DownloadContext.php b/src/Module/Downloader/Internal/DownloadContext.php new file mode 100644 index 0000000..a72e578 --- /dev/null +++ b/src/Module/Downloader/Internal/DownloadContext.php @@ -0,0 +1,26 @@ + + */ +final class SoftwareCollection implements IteratorAggregate +{ + public function __construct( + private SoftwareRegistry $softwareRegistry, + ) {} + + public function findSoftware(string $name): ?Software + { + foreach ($this->softwareRegistry->software as $software) { + if ($software->getId() === $name) { + return $software; + } + } + + return null; + } + + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + yield from $this->softwareRegistry->software; + } +} diff --git a/src/Module/Downloader/Task.php b/src/Module/Downloader/Task.php new file mode 100644 index 0000000..27cdba6 --- /dev/null +++ b/src/Module/Downloader/Task.php @@ -0,0 +1,10 @@ + */ + private array $tasks = []; + + public function __construct( + private Logger $logger, + ) {} + + public function addTask(\Closure $callback): void + { + $this->tasks[] = new \Fiber($callback); + } + + public function getProcessor(): \Generator + { + start: + if ($this->tasks === []) { + return; + } + + foreach ($this->tasks as $key => $task) { + try { + if ($task->isTerminated()) { + unset($this->tasks[$key]); + } + + if (!$task->isStarted()) { + yield $task->start(); + continue; + } + + yield $task->resume(); + } catch (\Throwable $e) { + $this->logger->exception($e); + unset($this->tasks[$key]); + yield $e; + } + } + + goto start; + } + + public function await(): void + { + $processor = $this->getProcessor(); + $processor->current(); + while ($processor->valid()) { + $processor->send(null); + } + } +} diff --git a/src/Module/Repository/AssetInterface.php b/src/Module/Repository/AssetInterface.php index 0e3e76f..8a61435 100644 --- a/src/Module/Repository/AssetInterface.php +++ b/src/Module/Repository/AssetInterface.php @@ -24,4 +24,11 @@ public function getUri(): string; public function getOperatingSystem(): ?OperatingSystem; public function getArchitecture(): ?Architecture; + + /** + * Load content from the asset. + * + * @return \Traversable + */ + public function download(): \Traversable; } diff --git a/src/Module/Repository/Internal/AssetsCollection.php b/src/Module/Repository/Internal/AssetsCollection.php index cdecba9..92c10af 100644 --- a/src/Module/Repository/Internal/AssetsCollection.php +++ b/src/Module/Repository/Internal/AssetsCollection.php @@ -36,4 +36,16 @@ public function whereOperatingSystem(OperatingSystem $os): self static fn(AssetInterface $asset): bool => $asset->getOperatingSystem() === $os, ); } + + /** + * Select all the assets with names that match the given pattern. + * + * @param non-empty-string $pattern + */ + public function whereNameMatches(string $pattern): self + { + return $this->filter( + static fn(AssetInterface $asset): bool => \preg_match($pattern, $asset->getName()) === 1, + ); + } } diff --git a/src/Module/Repository/Internal/GitHub/Factory.php b/src/Module/Repository/Internal/GitHub/Factory.php new file mode 100644 index 0000000..712426e --- /dev/null +++ b/src/Module/Repository/Internal/GitHub/Factory.php @@ -0,0 +1,40 @@ +createClient()); + } + + private function createClient(): HttpClientInterface + { + return HttpClient::create([ + 'headers' => \array_filter([ + 'authorization' => $this->config->token ? 'token ' . $this->config->token : null, + ]), + ]); + } +} diff --git a/src/Module/Repository/Internal/GitHub/GitHubAsset.php b/src/Module/Repository/Internal/GitHub/GitHubAsset.php index 7505208..d005823 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubAsset.php +++ b/src/Module/Repository/Internal/GitHub/GitHubAsset.php @@ -60,6 +60,11 @@ public static function fromApiResponse(HttpClientInterface $client, GitHubReleas } /** + * @param null|\Closure(int $dlNow, int $dlSize, array $info): mixed $progress + * throwing any exceptions MUST abort the request; + * it MUST be called on DNS resolution, on arrival of headers and on completion; + * it SHOULD be called on upload/download of data and at least 1/s + * * @throws ExceptionInterface */ public function download(\Closure $progress = null): \Traversable diff --git a/src/Module/Repository/Internal/GitHub/GitHubRepository.php b/src/Module/Repository/Internal/GitHub/GitHubRepository.php index a2a2797..e3e1315 100644 --- a/src/Module/Repository/Internal/GitHub/GitHubRepository.php +++ b/src/Module/Repository/Internal/GitHub/GitHubRepository.php @@ -41,24 +41,15 @@ final class GitHubRepository implements RepositoryInterface, Destroyable ]; /** - * @param non-empty-string $owner - * @param non-empty-string $repository + * @param non-empty-string $org + * @param non-empty-string $repo */ - public function __construct(string $owner, string $repository, HttpClientInterface $client = null) + public function __construct(string $org, string $repo, HttpClientInterface $client = null) { - $this->name = $owner . '/' . $repository; + $this->name = $org . '/' . $repo; $this->client = $client ?? HttpClient::create(); } - /** - * @param non-empty-string $package Package name in format "owner/repository" - */ - public static function fromDsn(string $package, HttpClientInterface $client = null): GitHubRepository - { - [$owner, $name] = \explode('/', $package); - return new GitHubRepository($owner, $name, $client); - } - /** * @throws ExceptionInterface */ diff --git a/src/Module/Repository/RepositoryProvider.php b/src/Module/Repository/RepositoryProvider.php new file mode 100644 index 0000000..78d51bf --- /dev/null +++ b/src/Module/Repository/RepositoryProvider.php @@ -0,0 +1,26 @@ +type)) { + 'github' => $this->githubFactory->create($config->uri), + default => throw new \RuntimeException("Unknown repository type `$config->type`."), + }; + } +} diff --git a/src/Service/Container.php b/src/Service/Container.php index d897b56..7a27efb 100644 --- a/src/Service/Container.php +++ b/src/Service/Container.php @@ -12,7 +12,7 @@ interface Container { /** - * @template T of object + * @template T * @param class-string $id * @param array $arguments Will be used if the object is created for the first time. * @return T