From 95d734a6608772aa70a523cd4cd6788505b22062 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 19 Dec 2024 12:36:21 +0000 Subject: [PATCH 1/8] Added event/extension support to base command --- src/Console/Command.php | 117 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/src/Console/Command.php b/src/Console/Command.php index 3aa4d6aaf..6d0174727 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -1,18 +1,33 @@ -replaces)) { $this->setAliases($this->replaces); } + + $this->extendableConstruct(); + } + + /** + * Override the laravel run function to allow us to run callbacks on the command prior to excution. + * Run the console command. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return int + */ + public function run(InputInterface $input, OutputInterface $output): int + { + $this->output = $this->laravel->make( + OutputStyle::class, ['input' => $input, 'output' => $output] + ); + + $this->components = $this->laravel->make(Factory::class, ['output' => $this->output]); + + $this->fireEvent('beforeRun', [$this]); + + $renderer = Termwind::getRenderer(); + renderUsing($this->output->getOutput()); + + try { + // Wizardy + return SymfonyCommand::run( + $this->input = $input, $this->output + ); + } finally { + $this->untrap(); + // Restore the original termwind renderer + renderUsing($renderer); + } } /** @@ -59,4 +109,69 @@ public function error($string, $verbosity = null) { $this->components->error($string, $this->parseVerbosity($verbosity)); } + + /** + * Magic allowing for extendable properties + * + * @param $name + * @return mixed|null + */ + public function __get($name) + { + return $this->extendableGet($name); + } + + /** + * Magic allowing for extendable properties + * + * @param $name + * @param $value + * @return void + */ + public function __set($name, $value) + { + $this->extendableSet($name, $value); + } + + /** + * Magic allowing for dynamic extension + * + * @param $name + * @param $params + * @return mixed + */ + public function __call($name, $params) + { + if ($name === 'extend') { + if (empty($params[0]) || !is_callable($params[0])) { + throw new \InvalidArgumentException('The extend() method requires a callback parameter or closure.'); + } + if ($params[0] instanceof \Closure) { + return $params[0]->call($this, $params[1] ?? $this); + } + return \Closure::fromCallable($params[0])->call($this, $params[1] ?? $this); + } + + return $this->extendableCall($name, $params); + } + + /** + * Magic allowing for dynamic static extension + * + * @param $name + * @param $params + * @return mixed|void + */ + public static function __callStatic($name, $params) + { + if ($name === 'extend') { + if (empty($params[0])) { + throw new \InvalidArgumentException('The extend() method requires a callback parameter or closure.'); + } + self::extendableExtendCallback($params[0], $params[1] ?? false, $params[2] ?? null); + return; + } + + return parent::__callStatic($name, $params); + } } From 5d606325f216554777b3053b95a8ec21b2fb7c7e Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Thu, 19 Dec 2024 13:18:49 +0000 Subject: [PATCH 2/8] Added support for module service provider instances to be bound to the application container --- src/Support/ModuleServiceProvider.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Support/ModuleServiceProvider.php b/src/Support/ModuleServiceProvider.php index 96d63ced4..a1fec4240 100644 --- a/src/Support/ModuleServiceProvider.php +++ b/src/Support/ModuleServiceProvider.php @@ -31,6 +31,9 @@ public function boot() if (File::isFile($routesFile)) { $this->loadRoutesFrom($routesFile); } + + // Bind the service provider to the application container + $this->app->instance($this::class, $this); } /** From 93882dadf210b647bc48ffcc1fe194f0f724f406 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 20 Dec 2024 12:15:07 +0000 Subject: [PATCH 3/8] Migrated code from winter to storm --- src/Foundation/Extension/WinterExtension.php | 12 ++ src/Packager/Commands/InfoCommand.php | 74 ++++++++++ src/Packager/Commands/RemoveCommand.php | 70 ++++++++++ src/Packager/Commands/RequireCommand.php | 82 +++++++++++ src/Packager/Commands/SearchCommand.php | 22 +++ src/Packager/Commands/ShowCommand.php | 66 +++++++++ src/Packager/Commands/UpdateCommand.php | 84 ++++++++++++ src/Packager/Composer.php | 135 +++++++++++++++++++ src/Support/ModuleServiceProvider.php | 14 +- src/Support/Traits/HasComposerPackage.php | 35 +++++ 10 files changed, 591 insertions(+), 3 deletions(-) create mode 100644 src/Foundation/Extension/WinterExtension.php create mode 100644 src/Packager/Commands/InfoCommand.php create mode 100644 src/Packager/Commands/RemoveCommand.php create mode 100644 src/Packager/Commands/RequireCommand.php create mode 100644 src/Packager/Commands/SearchCommand.php create mode 100644 src/Packager/Commands/ShowCommand.php create mode 100644 src/Packager/Commands/UpdateCommand.php create mode 100644 src/Packager/Composer.php create mode 100644 src/Support/Traits/HasComposerPackage.php diff --git a/src/Foundation/Extension/WinterExtension.php b/src/Foundation/Extension/WinterExtension.php new file mode 100644 index 000000000..675d5c3a1 --- /dev/null +++ b/src/Foundation/Extension/WinterExtension.php @@ -0,0 +1,12 @@ +package = $package; + } + + /** + * @inheritDoc + */ + public function arguments(): array + { + $arguments = [ + '--format' => 'json', + ]; + + if (!$this->package) { + return $arguments; + } + + $arguments['package'] = $this->package; + + return $arguments; + } + + /** + * @throws CommandException + * @throws WorkDirException + */ + public function execute(): array + { + $output = $this->runComposerCommand(); + $message = implode(PHP_EOL, $output['output']); + + if ($output['code'] !== 0) { + throw new CommandException($message); + } + + $result = json_decode($message, JSON_OBJECT_AS_ARRAY); + + return $this->package + ? $result ?? [] + : $result['installed'] ?? []; + } + + /** + * @inheritDoc + */ + public function getCommandName(): string + { + return 'info'; + } +} diff --git a/src/Packager/Commands/RemoveCommand.php b/src/Packager/Commands/RemoveCommand.php new file mode 100644 index 000000000..8659fb384 --- /dev/null +++ b/src/Packager/Commands/RemoveCommand.php @@ -0,0 +1,70 @@ +package = $package; + $this->dryRun = $dryRun; + } + + /** + * @inheritDoc + */ + public function arguments(): array + { + $arguments = []; + + if ($this->dryRun) { + $arguments['--dry-run'] = true; + } + + $arguments['packages'] = [$this->package]; + + return $arguments; + } + + public function execute() + { + $output = $this->runComposerCommand(); + $message = implode(PHP_EOL, $output['output']); + + if ($output['code'] !== 0) { + throw new CommandException($message); + } + + Cache::forget(Composer::COMPOSER_CACHE_KEY); + + return $message; + } + + /** + * @inheritDoc + */ + public function getCommandName(): string + { + return 'remove'; + } +} diff --git a/src/Packager/Commands/RequireCommand.php b/src/Packager/Commands/RequireCommand.php new file mode 100644 index 000000000..b31fe8f8b --- /dev/null +++ b/src/Packager/Commands/RequireCommand.php @@ -0,0 +1,82 @@ +package = $package; + $this->dryRun = $dryRun; + $this->dev = $dev; + } + + /** + * @inheritDoc + */ + public function arguments(): array + { + $arguments = []; + + if ($this->dryRun) { + $arguments['--dry-run'] = true; + } + + if ($this->dev) { + $arguments['--dev'] = true; + } + + $arguments['packages'] = [$this->package]; + + return $arguments; + } + + /** + * @throws CommandException + * @throws WorkDirException + */ + public function execute(): string + { + $output = $this->runComposerCommand(); + $message = implode(PHP_EOL, $output['output']); + + if ($output['code'] !== 0) { + throw new CommandException($message); + } + + Cache::forget(Composer::COMPOSER_CACHE_KEY); + + return $message; + } + + /** + * @inheritDoc + */ + public function getCommandName(): string + { + return 'require'; + } +} diff --git a/src/Packager/Commands/SearchCommand.php b/src/Packager/Commands/SearchCommand.php new file mode 100644 index 000000000..be8728738 --- /dev/null +++ b/src/Packager/Commands/SearchCommand.php @@ -0,0 +1,22 @@ +runComposerCommand(); + + if ($output['code'] !== 0) { + throw new CommandException(implode(PHP_EOL, $output['output'])); + } + + $this->results = json_decode(implode(PHP_EOL, $output['output']), true) ?? []; + + return $this; + } +} diff --git a/src/Packager/Commands/ShowCommand.php b/src/Packager/Commands/ShowCommand.php new file mode 100644 index 000000000..23f45a35b --- /dev/null +++ b/src/Packager/Commands/ShowCommand.php @@ -0,0 +1,66 @@ +path = $path; + } + + /** + * @inheritDoc + */ + public function arguments(): array + { + $arguments = []; + + if (!empty($this->package)) { + $arguments['package'] = $this->package; + } + + if ($this->mode !== 'installed') { + $arguments['--' . $this->mode] = true; + } + + if ($this->noDev) { + $arguments['--no-dev'] = true; + } + + if ($this->path) { + $arguments['--path'] = true; + } + + $arguments['--format'] = 'json'; + + return $arguments; + } +} diff --git a/src/Packager/Commands/UpdateCommand.php b/src/Packager/Commands/UpdateCommand.php new file mode 100644 index 000000000..c28e12103 --- /dev/null +++ b/src/Packager/Commands/UpdateCommand.php @@ -0,0 +1,84 @@ +executed) { + return; + } + + $this->includeDev = $includeDev; + $this->lockFileOnly = $lockFileOnly; + $this->ignorePlatformReqs = $ignorePlatformReqs; + $this->ignoreScripts = $ignoreScripts; + $this->dryRun = $dryRun; + $this->package = $package; + + if (in_array($installPreference, [self::PREFER_NONE, self::PREFER_DIST, self::PREFER_SOURCE])) { + $this->installPreference = $installPreference; + } + } + + /** + * @inheritDoc + */ + public function arguments(): array + { + $arguments = []; + + if ($this->package) { + $arguments['packages'] = [$this->package]; + } + + if ($this->dryRun) { + $arguments['--dry-run'] = true; + } + + if ($this->lockFileOnly) { + $arguments['--no-install'] = true; + } + + if ($this->ignorePlatformReqs) { + $arguments['--ignore-platform-reqs'] = true; + } + + if ($this->ignoreScripts) { + $arguments['--no-scripts'] = true; + } + + if (in_array($this->installPreference, [self::PREFER_DIST, self::PREFER_SOURCE])) { + $arguments['--prefer-' . $this->installPreference] = true; + } + + return $arguments; + } + + public function execute() + { + return parent::execute(); + } +} diff --git a/src/Packager/Composer.php b/src/Packager/Composer.php new file mode 100644 index 000000000..248150dbd --- /dev/null +++ b/src/Packager/Composer.php @@ -0,0 +1,135 @@ +|string + */ +class Composer +{ + public const COMPOSER_CACHE_KEY = 'winter.system.composer'; + + protected static PackagerComposer $composer; + + protected static array $winterPackages; + + public static function make(bool $fresh = false): PackagerComposer + { + if (!$fresh && isset(static::$composer)) { + return static::$composer; + } + + static::$composer = new PackagerComposer(); + static::$composer->setWorkDir(base_path()); + + static::$composer->setCommand('remove', new RemoveCommand(static::$composer)); + static::$composer->setCommand('require', new RequireCommand(static::$composer)); + static::$composer->setCommand('search', new SearchCommand(static::$composer)); + static::$composer->setCommand('show', new ShowCommand(static::$composer)); + static::$composer->setCommand('info', new InfoCommand(static::$composer)); + static::$composer->setCommand('update', new UpdateCommand(static::$composer)); + + return static::$composer; + } + + public static function __callStatic(string $name, array $args = []): mixed + { + if (!isset(static::$composer)) { + static::make(); + } + + return static::$composer->{$name}(...$args); + } + + public static function getWinterPackages(): array + { + $key = static::COMPOSER_CACHE_KEY . File::lastModified(base_path('composer.lock')); + return static::$winterPackages = Cache::rememberForever($key, function () { + $installed = static::info(); + $packages = []; + foreach ($installed as $package) { + $details = static::info($package['name']); + + $type = match ($details['type']) { + 'winter-plugin', 'october-plugin' => 'plugins', + 'winter-module', 'october-module' => 'modules', + 'winter-theme', 'october-theme' => 'themes', + default => null + }; + + if (!$type) { + continue; + } + + $packages[$type][$details['path']] = $details; + } + + return $packages; + }); + } + + public static function getAvailableUpdates(): array + { + $upgrades = Cache::remember( + static::COMPOSER_CACHE_KEY . '.updates', + 60 * 5, + fn () => static::update(dryRun: true)->getUpgraded() + ); + + $packages = static::getWinterPackageNames(); + + return array_filter($upgrades, function ($key) use ($packages) { + return in_array($key, $packages); + }, ARRAY_FILTER_USE_KEY); + } + + public static function updateAvailable(string $package): bool + { + return isset(static::getAvailableUpdates()[$package]); + } + + public static function getPackageInfoByExtension(WinterExtension $extension): array + { + return static::getPackageInfoByPath($extension->getPath()); + } + + public static function getPackageNameByExtension(WinterExtension $extension): ?string + { + return static::getPackageInfoByPath($extension->getPath())['name']; + } + + public static function getPackageInfoByPath(string $path): array + { + return array_merge(...array_values(static::getWinterPackages()))[$path] ?? []; + } + + public static function getWinterPackageNames(): array + { + return array_values( + array_map( + fn ($package) => $package['name'], + array_merge(...array_values(static::getWinterPackages())) + ) + ); + } +} diff --git a/src/Support/ModuleServiceProvider.php b/src/Support/ModuleServiceProvider.php index a1fec4240..2f3574380 100644 --- a/src/Support/ModuleServiceProvider.php +++ b/src/Support/ModuleServiceProvider.php @@ -1,12 +1,17 @@ app->instance($this::class, $this); + + // Register the composer package if exists + $this->setComposerPackage(Composer::getPackageInfoByExtension($this)); } /** diff --git a/src/Support/Traits/HasComposerPackage.php b/src/Support/Traits/HasComposerPackage.php new file mode 100644 index 000000000..d80b40633 --- /dev/null +++ b/src/Support/Traits/HasComposerPackage.php @@ -0,0 +1,35 @@ +composerPackage = $package; + } + + /** + * Get the composer package details + */ + public function getComposerPackage(): ?array + { + return $this->composerPackage; + } + + /** + * Get the composer package name + */ + public function getComposerPackageName(): ?string + { + return $this->composerPackage['name'] ?? null; + } +} From 407e8426effdbbe2bacf05fd2ca1ae81326e545a Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Sat, 4 Jan 2025 19:06:44 +0000 Subject: [PATCH 4/8] Added local remember function to avoid system cache --- src/Packager/Composer.php | 44 +++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Packager/Composer.php b/src/Packager/Composer.php index 248150dbd..f60c6e565 100644 --- a/src/Packager/Composer.php +++ b/src/Packager/Composer.php @@ -64,7 +64,7 @@ public static function __callStatic(string $name, array $args = []): mixed public static function getWinterPackages(): array { $key = static::COMPOSER_CACHE_KEY . File::lastModified(base_path('composer.lock')); - return static::$winterPackages = Cache::rememberForever($key, function () { + return static::$winterPackages = static::remember($key, function () { $installed = static::info(); $packages = []; foreach ($installed as $package) { @@ -90,10 +90,10 @@ public static function getWinterPackages(): array public static function getAvailableUpdates(): array { - $upgrades = Cache::remember( + $upgrades = static::remember( static::COMPOSER_CACHE_KEY . '.updates', - 60 * 5, - fn () => static::update(dryRun: true)->getUpgraded() + fn () => static::update(dryRun: true)->getUpgraded(), + 60 * 5 ); $packages = static::getWinterPackageNames(); @@ -132,4 +132,40 @@ public static function getWinterPackageNames(): array ) ); } + + /** + * This method moves the composer caching out of cache, this is so it is not invalidated during tests. @TODO: fix. + * + * @param string $key + * @param callable $callable + * @param int|null $expires + * @return mixed + */ + protected static function remember(string $key, callable $callable, ?int $expires = null): mixed + { + $dir = temp_path('composer'); + + if (!File::exists($dir)) { + File::makeDirectory($dir); + } + + $file = $dir . '/' . md5($key) . '.cache'; + + if (File::exists($file)) { + $cache = unserialize(File::get($file)); + + if (is_null($cache['expires']) || time() < $cache['expires']) { + return $cache['result']; + } + } + + $result = $callable(); + + File::put($file, serialize([ + 'expires' => $expires ? time() + $expires : null, + 'result' => $result + ])); + + return $result; + } } From 48eddecb50b13e32329b14d9e545d9c502732c39 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Sat, 4 Jan 2025 19:07:33 +0000 Subject: [PATCH 5/8] Added base methods for modules to inherit --- src/Support/ModuleServiceProvider.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Support/ModuleServiceProvider.php b/src/Support/ModuleServiceProvider.php index 2f3574380..d158d7196 100644 --- a/src/Support/ModuleServiceProvider.php +++ b/src/Support/ModuleServiceProvider.php @@ -101,4 +101,14 @@ protected function loadConfigFrom($path, $namespace) $config = $this->app['config']; $config->package($namespace, $path); } + + public function getVersion(): string + { + return $this->composerPackage['versions'][0] ?? 'dev-unknown'; + } + + public function __toString(): string + { + return $this->getIdentifier(); + } } From b1d4f06c2d39fbdf622ed1b6c8f6e652e17a4cea Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Sat, 4 Jan 2025 19:10:34 +0000 Subject: [PATCH 6/8] Added packager to storm deps --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0e6c31cb4..be5960ecd 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,8 @@ "twig/twig": "^3.14", "wikimedia/less.php": "~3.0", "wikimedia/minify": "~2.2", - "winter/laravel-config-writer": "^1.0.1" + "winter/laravel-config-writer": "^1.0.1", + "winter/packager": "~0.2" }, "require-dev": { "phpunit/phpunit": "^9.5.8", From 6b03b16ff901b953db6322930aab4859b1d53235 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Sun, 5 Jan 2025 14:22:04 +0000 Subject: [PATCH 7/8] Added identifier and path logic to module provider --- src/Support/ModuleServiceProvider.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Support/ModuleServiceProvider.php b/src/Support/ModuleServiceProvider.php index d158d7196..3777205db 100644 --- a/src/Support/ModuleServiceProvider.php +++ b/src/Support/ModuleServiceProvider.php @@ -1,6 +1,7 @@ composerPackage['versions'][0] ?? 'dev-unknown'; } + public function getPath(): string + { + return $this->path ?? $this->path = dirname((new \ReflectionClass(get_called_class()))->getFileName()); + } + + public function getIdentifier(): string + { + return $this->identifier ?? $this->identifier = (new \ReflectionClass(get_called_class()))->getNamespaceName(); + } + public function __toString(): string { return $this->getIdentifier(); From 43f0339c5074b919c93c8b5f88176f369aaab7b2 Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Mon, 6 Jan 2025 13:48:52 +0000 Subject: [PATCH 8/8] Added method for retrieving packages from composer api --- src/Packager/Composer.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Packager/Composer.php b/src/Packager/Composer.php index f60c6e565..5244f910b 100644 --- a/src/Packager/Composer.php +++ b/src/Packager/Composer.php @@ -4,7 +4,9 @@ use Illuminate\Support\Facades\Cache; use Winter\Packager\Composer as PackagerComposer; +use Winter\Storm\Exception\ApplicationException; use Winter\Storm\Foundation\Extension\WinterExtension; +use Winter\Storm\Network\Http; use Winter\Storm\Packager\Commands\InfoCommand; use Winter\Storm\Packager\Commands\RemoveCommand; use Winter\Storm\Packager\Commands\RequireCommand; @@ -168,4 +170,31 @@ protected static function remember(string $key, callable $callable, ?int $expire return $result; } + + public static function listPackages(string $type): array + { + return Cache::remember(static::COMPOSER_CACHE_KEY . '.packages.' . $type, 60 * 60 * 24, function () use ($type) { + $page = 0; + $packages = []; + do { + $result = Http::get('https://packagist.org/search.json', function (Http $http) use (&$page, $type) { + $http->data([ + 'q' => '', + 'page' => ++$page, + 'type' => $type + ]); + }); + + if ($result->code != '200') { + throw new ApplicationException('Unable to retrieve packages, failed with code: ' . $result->code); + } + + $data = json_decode($result->body, JSON_OBJECT_AS_ARRAY); + + $packages = array_merge($packages, $data['results']); + } while (isset($data['next'])); + + return $packages; + }); + } }