From 5750543f9cab2c57d806e4f77242cf5719b7ea8e Mon Sep 17 00:00:00 2001 From: Marius Date: Fri, 3 Nov 2023 21:38:10 +0100 Subject: [PATCH] Refactor + addition of recursive type guestimation --- CHANGELOG.md | 10 + README.md | 2 +- composer.json | 2 +- src/Console/Commands/GenerateTypedConfig.php | 127 ++------- src/Pipe/PipeInterface.php | 10 + src/Services/Stub/Builder.php | 252 ++++++++++++++++++ src/Services/Stub/Config.php | 30 +++ .../Stub}/stubs/typed_config/default.stub | 2 +- .../Stub}/stubs/typed_config/not_final.stub | 0 .../typed_config/not_final_not_strict.stub | 0 .../Stub}/stubs/typed_config/not_strict.stub | 0 src/TypedConfigServiceProvider.php | 8 +- 12 files changed, 330 insertions(+), 113 deletions(-) create mode 100644 src/Pipe/PipeInterface.php create mode 100644 src/Services/Stub/Builder.php create mode 100644 src/Services/Stub/Config.php rename src/{Console/Commands => Services/Stub}/stubs/typed_config/default.stub (70%) rename src/{Console/Commands => Services/Stub}/stubs/typed_config/not_final.stub (100%) rename src/{Console/Commands => Services/Stub}/stubs/typed_config/not_final_not_strict.stub (100%) rename src/{Console/Commands => Services/Stub}/stubs/typed_config/not_strict.stub (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5bed16..3e84c45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.0] 2023-11-03 +### Added +- Refactor of how the stubs are processed +- Addition of recursive property generation +- Added a few flags to the `generate` command + +## [0.X.0] 2023-10-XX +### Changed +- Lots of small fixes + ## [0.1.0] 2023-10-27 ### Created - Initial setup of this library diff --git a/README.md b/README.md index 4c1b09c..24d2fa1 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The above-mentioned checkup runs multiple analyses of the package's code. This i Continuous Integration ---------------------- -[GitHub actions](https://github.com/features/actions) are used for continuous integration. Check out the [configuration file](https://github.com/mjtheone/typed-config-generator/blob/main/.github/workflows/ci.yml) if you'd like to know more. +[GitHub actions](https://github.com/features/actions) are used for continuous integration. Check out the [configuration file](https://github.com/mjtheone/typed-config-generator/blob/main/.github/workflows/run-tests.yml) if you'd like to know more. Changelog --------- diff --git a/composer.json b/composer.json index 82bd4d9..475f22f 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Typed Classes for your Laravel configs!", "type": "library", "require": { - "php": "^8.1", + "php": "^8.2", "illuminate/support": "^9.0|^10.0" }, "require-dev": { diff --git a/src/Console/Commands/GenerateTypedConfig.php b/src/Console/Commands/GenerateTypedConfig.php index d84aa53..1771f3c 100644 --- a/src/Console/Commands/GenerateTypedConfig.php +++ b/src/Console/Commands/GenerateTypedConfig.php @@ -7,10 +7,10 @@ use Coderg33k\TypedConfigGenerator\Actions\GetConfigsForPredeterminedPackage; use Coderg33k\TypedConfigGenerator\Enums\Package; use Coderg33k\TypedConfigGenerator\Helper\ArrayFlatMap; +use Coderg33k\TypedConfigGenerator\Services\Stub\Builder; +use Coderg33k\TypedConfigGenerator\Services\Stub\Config; use Illuminate\Console\Command; -use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Arr; -use Illuminate\Support\Str; use Symfony\Component\Console\Attribute\AsCommand; use function Laravel\Prompts\search; @@ -21,13 +21,18 @@ final class GenerateTypedConfig extends Command { private const GENERATED_CONFIG_NAMESPACE_BASE = 'Config'; + /** @var array */ private array $configs = []; - private ?string $package = null; /** @var string */ protected $signature = 'coderg33k:generate-typed-config {--all : Generate guestimated classes for all configurations} + {--flat : Don\'t try to generate classes for nested configurations} + {--no-strict : Don\'t add declare(strict_types=1) to the generated classes} + {--no-final : Don\'t make the generated classes final} + {--no-readonly : Don\'t make the generated classes readonly} + {--package= : Generate classes for all configs in a package} {--config=* : One or more configurations to generate classes for}'; /** @var string */ @@ -39,8 +44,8 @@ final class GenerateTypedConfig extends Command EOF; public function __construct( - private readonly Filesystem $files, private readonly GetConfigsForPredeterminedPackage $getConfigsForPredeterminedPackage, + private readonly Builder $stubBuilder, private readonly ArrayFlatMap $arrayFlatMap, ) { parent::__construct(); @@ -61,7 +66,7 @@ private function determineWhatShouldBeGenerated(): void $this->configs = (array) $this->option('config'); - if (\count($this->configs) === 0) { + if (\count($this->configs) === 0 && !\is_string($this->option('package'))) { $this->promptForConfigs(); } } @@ -137,37 +142,22 @@ private function generateTypedClasses(): void $rootNamespace = $this->laravel->getNamespace(); $namespace = $rootNamespace . self::GENERATED_CONFIG_NAMESPACE_BASE; + $stubConfiguration = Config::make( + $this->option('flat'), + !$this->option('no-strict'), + !$this->option('no-final'), + !$this->option('no-readonly'), + ); + // @todo: Get known namespaces for package configs. $nullValues = []; - $properties = []; foreach ($configsToProcess as $config) { - $configData = config($config); - - foreach ($configData as $key => $value) { - // Let's start guestimating... - if (\is_array($value)) { - $properties[$key] = 'array'; - } else if (\is_bool($value)) { - $properties[$key] = 'bool'; - } else if (\is_float($value)) { - $properties[$key] = 'float'; - } else if (\is_int($value)) { - $properties[$key] = 'int'; - } else if (\is_null($value)) { - $properties[$key] = 'mixed'; - $nullValues[$config][] = $key; - } else if (\is_string($value)) { - $properties[$key] = 'string'; - } else { - $properties[$key] = 'mixed'; - } - } - - $this->buildStub( + $this->stubBuilder->handle( config: $config, namespace: $namespace, - properties: $properties, + nullValues: $nullValues, + stubConfiguration: $stubConfiguration, ); } @@ -196,82 +186,7 @@ private function discoverConfigsForPackage(): void ), $allConfigs, ), - ) + ), ]; } - - private function buildStub( - string $config, - string $namespace, - array $properties, - ): void { - $stub = \file_get_contents($this->getStub()); - - $stub = \str_replace( - search: [ - '{{ namespace }}', - '{{ properties }}', - '{{ class }}', - '{{ parent }}', - ], - replace: [ - $namespace, - $this->buildProperties($properties), - \ucfirst(Str::camel($config)), - '\\' . \Coderg33k\TypedConfigGenerator\TypedConfig::class, - ], - subject: $stub, - ); - - $this->writeClass( - $stub, - $config, - $namespace, - ); - } - - private function getStub(): string - { - $relativePath = '/stubs/typed_config/default.stub'; - - return \file_exists($customPath = $this->laravel->basePath(\trim($relativePath, '/'))) - ? $customPath - : __DIR__ . $relativePath; - } - - private function buildProperties(array $properties): string - { - $properties = \array_map( - fn (string $type, string $name): string => - \sprintf(' public %s $%s,', $type, Str::camel($name)), - $properties, - \array_keys($properties), - ); - - return \implode(PHP_EOL, $properties); - } - - private function writeClass( - string $stub, - string $config, - string $namespace, - ): void { - $classDirectoryPath = \str_replace($this->laravel->getNamespace(), '', $namespace); - - $this->files->makeDirectory( - path: app_path($classDirectoryPath), - recursive: true, - force: true, - ); - - $classPath = app_path( - \sprintf( - '%s/%s.php', - $classDirectoryPath, - \ucfirst(Str::camel($config)), - ) - ); - - $this->files->put($classPath, $stub); - } } diff --git a/src/Pipe/PipeInterface.php b/src/Pipe/PipeInterface.php new file mode 100644 index 0000000..9531adf --- /dev/null +++ b/src/Pipe/PipeInterface.php @@ -0,0 +1,10 @@ +> $nullValues + */ + public function handle( + string $config, + string $namespace, + array &$nullValues, + Config $stubConfiguration, + ): string { + return $this->determineProperties( + config: $config, + namespace: $namespace, + nullValues: $nullValues, + stubConfiguration: $stubConfiguration, + ); + } + + private function determineProperties( + string $config, + string $namespace, + array &$nullValues, + Config $stubConfiguration, + ): string { + $properties = []; + $configData = config($config); + + foreach ($configData as $key => $value) { + if (\is_array($value)) { + if ($stubConfiguration->useFlat) { + $properties[$key] = 'array'; + continue; + } + + if (IsArrayConfiguration::execute($value)) { + $properties[$key] = 'array'; + continue; + } + + if ($this->specialCase($config, $key)) { + $properties[$key] = 'array'; + } else { + $properties[$key] = $this->handle( + config: \sprintf('%s.%s', $config, $key), + namespace: $namespace, + nullValues: $nullValues, + stubConfiguration: $stubConfiguration, + ); + } + } else if (\is_bool($value)) { + $properties[$key] = 'bool'; + } else if (\is_float($value)) { + $properties[$key] = 'float'; + } else if (\is_int($value) || \is_numeric($value)) { + $properties[$key] = 'int'; + } else if (\is_null($value)) { + $properties[$key] = 'mixed'; + $nullValues[$config][] = $key; + } else if (\is_string($value)) { + $properties[$key] = 'string'; + } else { + $properties[$key] = 'mixed'; + } + } + + return $this->buildStub( + config: $config, + namespace: $namespace, + properties: $properties, + stubConfiguration: $stubConfiguration, + ); + } + + private function getStub(): string + { + $relativePath = '/stubs/typed_config/default.stub'; + + return \file_exists($customPath = base_path(\trim($relativePath, '/'))) + ? $customPath + : __DIR__ . $relativePath; + } + + private function buildProperties(array $properties): string + { + $properties = \array_map( + fn (string $type, string $name): string => + \sprintf( + '%spublic %s $%s,', + \str_repeat(' ', 8), + $type, + Str::camel($name) + ), + $properties, + \array_keys($properties), + ); + + return \implode(PHP_EOL, $properties); + } + + private function buildStub( + string $config, + string $namespace, + array $properties, + Config $stubConfiguration, + ): string { + $stub = \file_get_contents($this->getStub()); + + $configParts = \explode('.', $config); + $classPart = \ucfirst(Str::camel(\array_pop($configParts))); + + $classNamespace = \implode( + '\\', + \array_map( + fn (string $configPart): string => \ucfirst(Str::camel($configPart)), + $configParts, + ), + ); + + $stub = \str_replace( + search: [ + '{{ namespace }}', + '{{ properties }}', + '{{ class }}', + '{{ parent }}', + ], + replace: [ + \rtrim( + \sprintf( + '%s\\%s', + $namespace, + $classNamespace, + ), + '\\', + ), + $this->buildProperties($properties), + $classPart, + '\\' . TypedConfig::class, + ], + subject: $stub, + ); + + if (!$stubConfiguration->useFinal) { + $stub = \str_replace( + search: 'final ', + replace: '', + subject: $stub, + ); + } + + if (!$stubConfiguration->useReadonly) { + $stub = \str_replace( + search: 'readonly ', + replace: '', + subject: $stub, + ); + } + + if (!$stubConfiguration->useStrict) { + $stub = \str_replace( + search: 'declare(strict_types=1);' . PHP_EOL . PHP_EOL, + replace: '', + subject: $stub, + ); + } + + $this->writeClass( + $stub, + $config, + $namespace, + ); + + return '\\' . $namespace . '\\' . $classNamespace . '\\' . $classPart; + } + + private function writeClass( + string $stub, + string $config, + string $namespace, + ): void { + $configParts = \explode('.', $config); + $classPart = \ucfirst(Str::camel(\array_pop($configParts))); + + $classDirectoryPath = \str_replace( + search: $this->laravel->getNamespace(), + replace: '', + subject: $namespace, + ); + $classDirectoryPath .= '/' . \implode( + '/', + \array_map( + fn (string $configPart): string => \ucfirst(Str::camel($configPart)), + $configParts, + ), + ); + + $this->files->makeDirectory( + path: app_path($classDirectoryPath), + recursive: true, + force: true, + ); + + $classPath = app_path( + \sprintf( + '%s/%s.php', + $classDirectoryPath, + $classPart, + ), + ); + + $this->files->put($classPath, $stub); + } + + private function specialCase( + string $config, + string $key, + ): bool { + $specialCases = [ + 'app' => [ + 'providers', + 'aliases', + ], + 'cache' => [ + 'servers', + ], + ]; + + if (!\array_key_exists($config, $specialCases)) { + return false; + } + + return \in_array($key, $specialCases[$config]); + } +} diff --git a/src/Services/Stub/Config.php b/src/Services/Stub/Config.php new file mode 100644 index 0000000..71fc575 --- /dev/null +++ b/src/Services/Stub/Config.php @@ -0,0 +1,30 @@ +app->singleton( - $class, - fn () => $class::fromConfig(...config($config)), - ); +// $this->app->singleton( +// $class, +// fn () => $class::fromConfig(...config($config)), +// ); } } }