diff --git a/CHANGELOG.md b/CHANGELOG.md index 61b9cc9..ee7700e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +2.0.0 +===== + +* (bc) Remove deprecated init commands +* (feature) Automatically detect the package type according to the type in `composer.json`. +* (feature) Write back the package type, if none was set. +* (improvement) Add option to not automatically run `composer update` after Janus finished. +* (feature) Add composer plugin to run composer automatically. +* (bug) Update config to fix bug in PHPStan config reader. + + 1.5.1 ===== diff --git a/LICENSE b/LICENSE index 59466da..3b72341 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 21TORR +Copyright (c) 2025 21TORR Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/composer.json b/composer.json index 8edd838..6fcffbc 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "21torr/janus", "description": "Code style, encoded as rules for common tools.", "license": "BSD-3-Clause", - "type": "lib", + "type": "composer-plugin", "authors": [ { "name": "21TORR", @@ -12,12 +12,14 @@ "homepage": "https://github.com/21TORR/janus-php", "require": { "php": ">= 8.4", + "composer-plugin-api": "^2.6", "21torr/cli": "^1.2.3", "symfony/console": "^7.3", "symfony/process": "^7.3" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", + "composer/composer": "^2.8.9", "roave/security-advisories": "dev-latest" }, "autoload": { @@ -40,7 +42,8 @@ "bamarni-bin": { "bin-links": false, "forward-command": true - } + }, + "class": "Janus\\Composer\\JanusPlugin" }, "scripts": { "fix-lint": [ diff --git a/phpstan/symfony.neon b/phpstan/symfony.neon index a53fb25..10330eb 100644 --- a/phpstan/symfony.neon +++ b/phpstan/symfony.neon @@ -15,8 +15,11 @@ parameters: identifier: return.type path: %currentWorkingDirectory%/src/Storyblok/Component/*Story.php + # If you're using PHP config files for Symfony 5.3+, you also need this for auto-loading of `Symfony\Config` scanDirectories: - - %currentWorkingDirectory%/var/cache/dev/Symfony/Config (?) + - %currentWorkingDirectory%/var/cache/dev/Symfony/Config + # If you're using PHP config files (including the ones under packages/*.php) for Symfony 5.3+, + # you need this to load the helper functions (i.e. service(), env()): scanFiles: - %currentWorkingDirectory%/vendor/symfony/dependency-injection/Loader/Configurator/ContainerConfigurator.php diff --git a/src/Command/InitializeCommand.php b/src/Command/InitializeCommand.php index 4c1b326..caf0973 100644 --- a/src/Command/InitializeCommand.php +++ b/src/Command/InitializeCommand.php @@ -2,24 +2,25 @@ namespace Janus\Command; -use Janus\Initializer\LibraryInitializer; -use Janus\Initializer\SymfonyInitializer; +use Janus\Composer\ComposerJson; +use Janus\Exception\InvalidCallException; +use Janus\Exception\JanusException; +use Janus\Package\PackageInitializer; +use Janus\Package\PackageManager; +use Janus\Package\PackageType; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Torr\Cli\Console\Style\TorrStyle; final class InitializeCommand extends Command { - private const array ALLOWED_TYPES = [ + public const array ALLOWED_TYPES = [ "symfony", "library", ]; - private const array LEGACY_COMMANDS = [ - "init-symfony", - "init-library", - ]; /** */ @@ -35,13 +36,17 @@ protected function configure () : void { $this ->setDescription("Initializes a given command") - ->setAliases(self::LEGACY_COMMANDS) ->addArgument( "type", InputArgument::OPTIONAL, "The project type to initialize", default: null, - suggestedValues: self::ALLOWED_TYPES, + suggestedValues: PackageType::values(), + ) + ->addOption( + "no-auto-install", + mode: InputOption::VALUE_NONE, + description: "Whether to automatically run composer after changing anything", ); } @@ -51,41 +56,115 @@ protected function configure () : void protected function execute (InputInterface $input, OutputInterface $output) : int { $io = new TorrStyle($input, $output); + $projectHelper = new PackageManager($io); + $packageInitializer = new PackageInitializer(); + $io->title("Janus: Initialize"); + $runComposerAutomatically = !$input->getOption('no-auto-install'); - if (\in_array($input->getFirstArgument(), self::LEGACY_COMMANDS, true)) + try { - $io->caution("You are using a deprecated command. Use the `init` command instead."); - } + $composerJson = $projectHelper->loadComposerJson(); + $packageType = $this->fetchPackageType($io, $composerJson, $input->getArgument("type")); - $type = $input->getArgument("type"); - \assert(null === $type || \is_string($type)); + $io->writeln(\sprintf( + "• Initializing janus for type %s", + $packageType->value, + )); - if (!\in_array($type, self::ALLOWED_TYPES, true)) - { - if (null !== $type) + $io->writeln("• Copying main init files"); + $projectHelper->copyInitFilesIntoProject($packageType); + + $io->writeln("• Updating composer.json"); + + // write basics + match ($packageType) + { + PackageType::Symfony => $packageInitializer->initializeSymfony($composerJson), + PackageType::Library => $packageInitializer->initializeLibrary($composerJson), + }; + + // set project type (only if it is not yet set. We want to keep even unknown values here, so only set it if it is unset) + if (!$composerJson->hasType()) { - $io->error("Used invalid type: {$type}"); + $io->writeln("• Your composer.json has no type set"); + $io->writeln(\sprintf( + "• Setting it to the type %s (according to your selection %s)", + $packageType->getComposerType(), + $packageType->value, + )); + + $composerJson->replaceConfig([ + "type" => $packageType->getComposerType(), + ]); } - $type = $io->choice("Please select the type to initialize", self::ALLOWED_TYPES); + $projectHelper->writeComposerJson($composerJson); + + if ($runComposerAutomatically) + { + $io->writeln("• Running composer update..."); + $projectHelper->runComposerInProject(["update"]); + } + else + { + $io->caution("Your project was updated, you should run `composer update`."); + } + + return self::SUCCESS; + } + catch (JanusException $exception) + { + $io->error($exception->getMessage()); + + return self::FAILURE; } + } - \assert(\is_string($type)); + /** + * + */ + private function fetchPackageType ( + TorrStyle $io, + ComposerJson $composerJson, + mixed $typeCliArgument, + ) : PackageType + { + // first try CLI parameter + $packageType = \is_string($typeCliArgument) + ? PackageType::tryFrom($typeCliArgument) + : null; - try + if (null !== $packageType) { - return match ($type) - { - "symfony" => (new SymfonyInitializer())->initialize($io), - "library" => (new LibraryInitializer())->initialize($io), - }; + return $packageType; } - catch (\Throwable $exception) + + // then error out if the user explicitly passed an invalid value + if (null !== $typeCliArgument) { - $io->error("Running janus failed: {$exception->getMessage()}"); + throw new InvalidCallException(\sprintf( + "Invalid type selected: %s", + \is_scalar($typeCliArgument) + ? $typeCliArgument + : get_debug_type($typeCliArgument), + )); + } - return 2; + // no CLI parameter passed, so test if we can detect the type from composer.json + $packageType = $composerJson->getType(); + + if (null !== $packageType) + { + $io->writeln("• Automatically detected type from the package type in your composer.json"); + + return $packageType; } + + // could not detect, so just ask for it + $type = $io->choice("Please select the type to initialize", PackageType::values()); + \assert(\is_string($type)); + + return PackageType::from($type); } } diff --git a/src/Composer/ComposerJson.php b/src/Composer/ComposerJson.php new file mode 100644 index 0000000..24f76eb --- /dev/null +++ b/src/Composer/ComposerJson.php @@ -0,0 +1,102 @@ + */ + public private(set) array $content, + ) {} + + /** + * Takes a list of scripts to replace and updates the configs. + * + * The $scripts array has a keywords as key, and replaces the line containing that keyword. + * So for example the key "phpunit" would replace the line that contains "phpunit". + * If there are multiple lines matching, all will be replaced. + * If there are no lines matching, the call will just be appended. + * + * @param string $key the scripts key to update + * @param array $scripts the scripts to replace + */ + public function updateScripts (string $key, array $scripts) : void + { + $allExistingScripts = $this->content["scripts"] ?? []; + + if (!\is_array($allExistingScripts)) + { + throw new InvalidSetupException(\sprintf( + "Invalid composer.json: scripts must be an array, %s given", + get_debug_type($allExistingScripts), + )); + } + + $existingScripts = $allExistingScripts[$key] ?? []; + // keep existing scripts + $result = []; + \assert(\is_array($existingScripts)); + + foreach ($existingScripts as $line) + { + \assert(\is_string($line)); + + foreach ($scripts as $replacedKeyword => $newLine) + { + if (str_contains($line, $replacedKeyword)) + { + continue 2; + } + } + + // append the line if no replacement matches + $result[] = $line; + } + + // append all new lines + foreach ($scripts as $newLine) + { + $result[] = $newLine; + } + + $allExistingScripts[$key] = $result; + $this->content["scripts"] = $allExistingScripts; + } + + /** + * Add the given config to the projects composer.json + * + * @param array $config + */ + public function replaceConfig (array $config) : void + { + $this->content = array_replace_recursive( + $this->content, + $config, + ); + } + + /** + * + */ + public function getType () : ?PackageType + { + return PackageType::tryFromComposerType($this->content["type"] ?? null); + } + + /** + * + */ + public function hasType () : bool + { + return \array_key_exists("type", $this->content); + } +} diff --git a/src/Composer/JanusPlugin.php b/src/Composer/JanusPlugin.php new file mode 100644 index 0000000..f79d0ce --- /dev/null +++ b/src/Composer/JanusPlugin.php @@ -0,0 +1,196 @@ +getOperations() as $operation) + { + if ($this->isJanusUpdate($operation)) + { + $this->hadJanusOperation = true; + break; + } + } + } + + /** + * + */ + private function isJanusUpdate (OperationInterface $operation) : bool + { + if ($operation instanceof InstallOperation) + { + return "21torr/janus" === $operation->getPackage()->getName(); + } + + if ($operation instanceof UpdateOperation) + { + return "21torr/janus" === $operation->getTargetPackage()->getName(); + } + + return false; + } + + /** + * Callback after the autoloader was dumped + */ + public function afterAutoloadDump (Event $event) : void + { + if (!$this->hadJanusOperation) + { + return; + } + + $this->hadJanusOperation = false; + + $io = $event->getIO(); + $io->write("\nJanus update detected, running janus update\n"); + + // please note, that the detection can fail: composer defaults to "library", if it's not set + $packageType = PackageType::tryFromComposerType( + $event->getComposer()->getPackage()->getType(), + ); + + if (null === $packageType) + { + $selected = $io->select( + "What are you currently using?", + InitializeCommand::ALLOWED_TYPES, + "library", + ); + $packageType = PackageType::tryFromComposerType(InitializeCommand::ALLOWED_TYPES[$selected] ?? null); + } + else + { + $io->write(\sprintf( + "Detected package type %s", + $packageType->value, + )); + } + + $vendorDir = $event->getComposer()->getConfig()->get('vendor-dir'); + \assert(\is_string($vendorDir)); + + $success = $this->runJanus($io, $vendorDir, $packageType); + + if ($success) + { + $io->write("\nJanus installation complete.\n"); + } + else + { + $io->writeError("\nJanus installation failed, please run it manually: `composer exec janus init`\n"); + } + } + + /** + * Runs Janus + */ + private function runJanus ( + IOInterface $io, + string $vendorDir, + ?PackageType $type, + ) : bool + { + $command = [ + $this->findJanusExecutable($vendorDir), + "init", + ]; + + if (null !== $type) + { + $command[] = $type->value; + } + + $command[] = "--no-auto-install"; + $command[] = "--ansi"; + + $output = new Process($command); + $output->run( + static function ($type, $buffer) use ($io) : void + { + $io->write($buffer); + }, + ); + + return $output->isSuccessful(); + } + + /** + * Finds the path to the janus executable + */ + private function findJanusExecutable (string $vendorDir) : string + { + // first check if it's installed in the project via composer + if (is_dir("{$vendorDir}/bin/janus")) + { + return "{$vendorDir}/bin/janus"; + } + + // otherwise just fetch the executable from the library and run it + return \dirname(__DIR__, 2) . "/bin/janus"; + } + + /** + * @inheritDoc + */ + #[\Override] + public static function getSubscribedEvents () : array + { + return [ + PackageEvents::POST_PACKAGE_INSTALL => "checkForJanusOperations", + PackageEvents::POST_PACKAGE_UPDATE => "checkForJanusOperations", + ScriptEvents::POST_AUTOLOAD_DUMP => "afterAutoloadDump", + ]; + } +} diff --git a/src/Exception/ComposerNotFoundException.php b/src/Exception/ComposerNotFoundException.php new file mode 100644 index 0000000..36f24da --- /dev/null +++ b/src/Exception/ComposerNotFoundException.php @@ -0,0 +1,10 @@ +cwd = (string) getcwd(); - } - - /** - * Copies the files from the given init dir to the project dir - */ - public function copyFilesIntoProject (string $directory) : void - { - $sourceDir = self::INIT_DIR . "/{$directory}/."; - - $this->runProcessInProject([ - "cp", - "-a", - $sourceDir, - ".", - ]); - } - - /** - * Add the given config to the projects composer.json - * - * @param array $config - */ - public function addToProjectComposerJson (array $config) : void - { - $jsonContent = array_replace_recursive( - $this->readProjectComposerJson(), - $config, - ); - - $this->writeProjectComposerJson($jsonContent); - } - - /** - * Takes a list of scripts to replace and updates the configs. - * - * The $scripts array has a keywords as key, and replaces the line containing that keyword. - * So for example the key "phpunit" would replace the line that contains "phpunit". - * If there are multiple lines matching, all will be replaced. - * If there are no lines matching, the call will just be appended. - * - * @param string $key the scripts key to update - * @param array $scripts the scripts to replace - */ - public function updateProjectComposerJsonScripts (string $key, array $scripts) : void - { - $jsonContent = $this->readProjectComposerJson(); - \assert(!isset($jsonContent["scripts"]) || \is_array($jsonContent["scripts"])); - - $existingScripts = $jsonContent["scripts"][$key] ?? []; - // keep existing scripts - $result = []; - - foreach ($existingScripts as $line) - { - foreach ($scripts as $replacedKeyword => $newLine) - { - if (str_contains($line, $replacedKeyword)) - { - continue 2; - } - } - - // append the line if no replacement matches - $result[] = $line; - } - - // append all new lines - foreach ($scripts as $newLine) - { - $result[] = $newLine; - } - - $jsonContent["scripts"][$key] = $result; - $this->writeProjectComposerJson($jsonContent); - } - - /** - * Writes the given config to the project composer.json - * - * @param array $config - */ - private function writeProjectComposerJson (array $config) : void - { - $filePath = "{$this->cwd}/composer.json"; - - file_put_contents( - $filePath, - json_encode( - $config, - \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_THROW_ON_ERROR, - ), - ); - } - - /** - * @return array - */ - private function readProjectComposerJson () : array - { - $filePath = "{$this->cwd}/composer.json"; - - $result = json_decode( - (string) file_get_contents($filePath), - true, - flags: \JSON_THROW_ON_ERROR, - ); - \assert(\is_array($result)); - - return $result; - } - - /** - * Runs a composer command in the project - * - * @param string[] $cmd - */ - public function runComposerInProject (array $cmd) : void - { - $finder = new ExecutableFinder(); - $composer = $finder->find("composer"); - - if (null === $composer) - { - throw new InvalidSetupException("Could not find locally installed composer"); - } - - array_unshift($cmd, $composer); - $cmd[] = "--ansi"; - $this->runProcessInProject($cmd); - } - - /** - * Runs the given command in the project directory - * - * @param string[] $cmd - */ - public function runProcessInProject (array $cmd) : void - { - $this->io->writeln(\sprintf( - "$> Running command %s", - implode(" ", $cmd), - )); - - $process = new Process( - $cmd, - cwd: $this->cwd, - ); - $process->mustRun(); - - $output = trim(\sprintf("%s\n%s", $process->getErrorOutput(), $process->getOutput())); - - if ("" !== $output) - { - $this->io->block( - trim(\sprintf("%s\n%s", $process->getErrorOutput(), $process->getOutput())), - prefix: " │ ", - ); - } - } -} diff --git a/src/Initializer/LibraryInitializer.php b/src/Initializer/LibraryInitializer.php deleted file mode 100644 index 1138d8a..0000000 --- a/src/Initializer/LibraryInitializer.php +++ /dev/null @@ -1,55 +0,0 @@ -writeln("• Copying config files to the project..."); - $helper->copyFilesIntoProject("library"); - - $io->writeln("• Updating composer.json..."); - $helper->addToProjectComposerJson([ - "config" => [ - "allow-plugins" => [ - "bamarni/composer-bin-plugin" => true, - ], - "sort-packages" => true, - ], - "extra" => [ - "bamarni-bin" => [ - "bin-links" => false, - "forward-command" => true, - ], - ], - "require-dev" => [ - "bamarni/composer-bin-plugin" => "^1.8.2", - "roave/security-advisories" => "dev-latest", - ], - ]); - $helper->updateProjectComposerJsonScripts("fix-lint", [ - "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --ansi", - "cs-fixer" => "PHP_CS_FIXER_IGNORE_ENV=1 vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", - ]); - $helper->updateProjectComposerJsonScripts("lint", [ - "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --dry-run --ansi", - "cs-fixer" => "PHP_CS_FIXER_IGNORE_ENV=1 vendor-bin/cs-fixer/vendor/bin/php-cs-fixer check --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", - ]); - $helper->updateProjectComposerJsonScripts("test", [ - "phpstan" => "vendor-bin/phpstan/vendor/bin/phpstan analyze -c phpstan.neon . --ansi -v", - ]); - - $io->writeln("• Running composer update..."); - $helper->runComposerInProject(["update"]); - - return 0; - } -} diff --git a/src/Initializer/SymfonyInitializer.php b/src/Initializer/SymfonyInitializer.php deleted file mode 100644 index b372b83..0000000 --- a/src/Initializer/SymfonyInitializer.php +++ /dev/null @@ -1,57 +0,0 @@ -writeln("• Copying config files to the project..."); - $helper->copyFilesIntoProject("symfony"); - - $io->writeln("• Updating composer.json..."); - $helper->addToProjectComposerJson([ - "config" => [ - "allow-plugins" => [ - "bamarni/composer-bin-plugin" => true, - ], - "sort-packages" => true, - ], - "extra" => [ - "bamarni-bin" => [ - "bin-links" => false, - "forward-command" => true, - ], - ], - "require-dev" => [ - "bamarni/composer-bin-plugin" => "^1.8.2", - "roave/security-advisories" => "dev-latest", - ], - ]); - $helper->updateProjectComposerJsonScripts("fix-lint", [ - "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --ansi", - "cs-fixer" => "PHP_CS_FIXER_IGNORE_ENV=1 vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", - ]); - $helper->updateProjectComposerJsonScripts("lint", [ - "lint:yaml" => "bin/console lint:yaml config --parse-tags", - "lint:twig" => "bin/console lint:twig templates", - "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --dry-run --ansi", - "cs-fixer" => "PHP_CS_FIXER_IGNORE_ENV=1 vendor-bin/cs-fixer/vendor/bin/php-cs-fixer check --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", - ]); - $helper->updateProjectComposerJsonScripts("test", [ - "phpstan" => "vendor-bin/phpstan/vendor/bin/phpstan analyze -c phpstan.neon . --ansi -v", - ]); - - $io->writeln("• Running composer update..."); - $helper->runComposerInProject(["update"]); - - return 0; - } -} diff --git a/src/Package/PackageInitializer.php b/src/Package/PackageInitializer.php new file mode 100644 index 0000000..42dad95 --- /dev/null +++ b/src/Package/PackageInitializer.php @@ -0,0 +1,92 @@ +replaceConfig([ + "config" => [ + "allow-plugins" => [ + "21torr/janus" => true, + "bamarni/composer-bin-plugin" => true, + ], + "sort-packages" => true, + ], + "extra" => [ + "bamarni-bin" => [ + "bin-links" => false, + "forward-command" => true, + ], + ], + "require-dev" => [ + "bamarni/composer-bin-plugin" => "^1.8.2", + "roave/security-advisories" => "dev-latest", + ], + ]); + + $composerJson->updateScripts("fix-lint", [ + "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --ansi", + "cs-fixer" => "PHP_CS_FIXER_IGNORE_ENV=1 vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", + ]); + + $composerJson->updateScripts("lint", [ + "lint:yaml" => "bin/console lint:yaml config --parse-tags", + "lint:twig" => "bin/console lint:twig templates", + "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --dry-run --ansi", + "cs-fixer" => "PHP_CS_FIXER_IGNORE_ENV=1 vendor-bin/cs-fixer/vendor/bin/php-cs-fixer check --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", + ]); + + $composerJson->updateScripts("test", [ + "phpstan" => "vendor-bin/phpstan/vendor/bin/phpstan analyze -c phpstan.neon . --ansi -v", + ]); + } + + /** + * + */ + public function initializeLibrary (ComposerJson $composerJson) : void + { + $composerJson->replaceConfig([ + "config" => [ + "allow-plugins" => [ + "bamarni/composer-bin-plugin" => true, + ], + "sort-packages" => true, + ], + "extra" => [ + "bamarni-bin" => [ + "bin-links" => false, + "forward-command" => true, + ], + ], + "require-dev" => [ + "bamarni/composer-bin-plugin" => "^1.8.2", + "roave/security-advisories" => "dev-latest", + ], + ]); + + $composerJson->updateScripts("fix-lint", [ + "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --ansi", + "cs-fixer" => "PHP_CS_FIXER_IGNORE_ENV=1 vendor-bin/cs-fixer/vendor/bin/php-cs-fixer fix --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", + ]); + + $composerJson->updateScripts("lint", [ + "normalize" => "@composer bin c-norm normalize \"$(pwd)/composer.json\" --indent-style tab --indent-size 1 --dry-run --ansi", + "cs-fixer" => "PHP_CS_FIXER_IGNORE_ENV=1 vendor-bin/cs-fixer/vendor/bin/php-cs-fixer check --diff --config vendor-bin/cs-fixer/vendor/21torr/php-cs-fixer/.php-cs-fixer.dist.php --no-interaction --ansi", + ]); + + $composerJson->updateScripts("test", [ + "phpstan" => "vendor-bin/phpstan/vendor/bin/phpstan analyze -c phpstan.neon . --ansi -v", + ]); + } +} diff --git a/src/Package/PackageManager.php b/src/Package/PackageManager.php new file mode 100644 index 0000000..fce92ff --- /dev/null +++ b/src/Package/PackageManager.php @@ -0,0 +1,157 @@ +initDir = \dirname(__DIR__, 2) . "/_init"; + $this->cwd = (string) getcwd(); + } + + /** + * Copies the files from the given init dir to the project dir + */ + public function copyInitFilesIntoProject (PackageType $packageType) : void + { + $sourceDir = "{$this->initDir}/{$packageType->value}/."; + + $this->runProcessInProject([ + "cp", + "-a", + $sourceDir, + ".", + ]); + } + + /** + */ + public function loadComposerJson () : ComposerJson + { + $filePath = "{$this->cwd}/composer.json"; + + if (!is_file($filePath) || !is_readable($filePath)) + { + throw new ComposerNotFoundException(\sprintf( + "composer.json not found at '%s'", + $filePath, + )); + } + + try + { + $result = json_decode( + (string) file_get_contents($filePath), + true, + depth: 512, + flags: \JSON_THROW_ON_ERROR, + ); + \assert(\is_array($result)); + + return new ComposerJson($result); + } + catch (\JsonException $exception) + { + throw new ComposerNotFoundException( + \sprintf( + "composer.json at '%s' contains invalid JSON", + $filePath, + ), + previous: $exception, + ); + } + } + + /** + * Writes the given config to the project composer.json + */ + public function writeComposerJson (ComposerJson $composerJson) : void + { + $filePath = "{$this->cwd}/composer.json"; + + try + { + file_put_contents( + $filePath, + json_encode( + $composerJson->content, + \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_THROW_ON_ERROR, + ), + ); + } + catch (\JsonException $exception) + { + throw new InvalidSetupException( + \sprintf( + "Could not write back composer.json at '%s'", + $filePath, + ), + previous: $exception, + ); + } + } + + /** + * Runs a composer command in the project + * + * @param string[] $cmd + */ + public function runComposerInProject (array $cmd) : void + { + $finder = new ExecutableFinder(); + $composer = $finder->find("composer"); + + if (null === $composer) + { + throw new InvalidSetupException("Could not find locally installed composer"); + } + + array_unshift($cmd, $composer); + $cmd[] = "--ansi"; + $this->runProcessInProject($cmd); + } + + /** + * Runs the given command in the project directory + * + * @param string[] $cmd + */ + public function runProcessInProject (array $cmd) : void + { + $this->io?->writeln(\sprintf( + "$> Running command %s", + implode(" ", $cmd), + )); + + $process = new Process( + $cmd, + cwd: $this->cwd, + ); + $process->mustRun(); + + $output = trim(\sprintf("%s\n%s", $process->getErrorOutput(), $process->getOutput())); + + if ("" !== $output) + { + $this->io?->block( + trim(\sprintf("%s\n%s", $process->getErrorOutput(), $process->getOutput())), + prefix: " │ ", + ); + } + } +} diff --git a/src/Package/PackageType.php b/src/Package/PackageType.php new file mode 100644 index 0000000..85c7d0e --- /dev/null +++ b/src/Package/PackageType.php @@ -0,0 +1,48 @@ + "project", + self::Library => "library", + }; + } + + /** + * @return list + */ + public static function values () : array + { + return array_map( + static fn (self $type) => $type->value, + self::cases(), + ); + } + + /** + * + */ + public static function tryFromComposerType (mixed $type) : ?self + { + return match ($type) + { + "project" => self::Symfony, + + "symfony-bundle", + "library" => self::Library, + + default => null, + }; + } +} diff --git a/vendor-bin/phpstan/composer.json b/vendor-bin/phpstan/composer.json index cf165ff..4eb9387 100644 --- a/vendor-bin/phpstan/composer.json +++ b/vendor-bin/phpstan/composer.json @@ -3,14 +3,14 @@ "php": "^8.3" }, "require-dev": { - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-deprecation-rules": "^1.2", - "phpstan/phpstan-doctrine": "^1.4", - "phpstan/phpstan-phpunit": "^1.4", - "phpstan/phpstan-symfony": "^1.4", + "phpstan/extension-installer": "^1.4.2", + "phpstan/phpstan": "^2.1.11", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpstan/phpstan-doctrine": "^2.0.2", + "phpstan/phpstan-phpunit": "^2.0.6", + "phpstan/phpstan-symfony": "^2.0.4", "roave/security-advisories": "dev-latest", - "staabm/phpstan-todo-by": "^0.1.25" + "staabm/phpstan-todo-by": "^0.2" }, "config": { "sort-packages": true,