diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
new file mode 100644
index 0000000..285643a
--- /dev/null
+++ b/.github/workflows/run-tests.yml
@@ -0,0 +1,42 @@
+name: run-tests
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ os: [ ubuntu-latest ]
+ php: [ 8.3, 8.2, 8.1 ]
+ laravel: [ 9.*, 10.* ]
+ stability: [ prefer-stable ]
+ include:
+ - laravel: 10.*
+ testbench: 8.*
+ - laravel: 9.*
+ testbench: 7.*
+
+ name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo
+ coverage: none
+
+ - name: Install dependencies
+ run: |
+ composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
+ composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
+ env:
+ COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
+
+ - name: Execute tests
+ run: vendor/bin/phpunit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2745433
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+composer.lock
+
+/vendor/
+/node_modules/
+
+.env
+
+/var/cache/
+.phpunit.result.cache
+/coverage/
+/reports/
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e5bed16
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+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).
+
+## [0.1.0] 2023-10-27
+### Created
+- Initial setup of this library
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..6043954
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,74 @@
+
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to make participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies within all project spaces, and it also applies when
+an individual is representing the project or its community in public spaces.
+Examples of representing a project or community include using an official
+project e-mail address, posting via an official social media account, or acting
+as an appointed representative at an online or offline event. Representation of
+a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at info@coddin.nl. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..6d3bd03
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,9 @@
+Thank you for showing interest in contributing to this project.
+
+**Please submit your pull request against the `main` branch only**
+
+Please ensure that before you do that you ran `composer checkup` from the project root after you've made any changes.
+
+We're trying to ensure there is **100%** coverage, including Integration tests for the Repositories, so please ensure any new and or updated tests cover all of your changes.
+
+Happy contributing!
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9c30575
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 CoderG33k
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d9cbd94
--- /dev/null
+++ b/README.md
@@ -0,0 +1,67 @@
+A Laravel typed config generator
+========================================
+[![Latest Version](http://img.shields.io/packagist/v/MJTheOne/typed-config-generator.svg?style=flat-square)](https://github.com/MJTheOne/typed-config-generator/releases)
+![Build](https://github.com/MJTheOne/typed-config-generator/actions/workflows/run-tests.yml/badge.svg?event=push)
+[![codecov](https://codecov.io/gh/MJTheOne/typed-config-generator/branch/main/graph/badge.svg?token=BRH4XEU1VK)](https://codecov.io/gh/MJTheOne/typed-config-generator)
+
+Are you a PHPStan lovin' strict programmer?! Say no more! This package will generate typed config classes for you based on your config files.
+
+We all struggle with the `mixed` return type of the `config()` helper function. This package will stop your struggle and leave all your (unnecessary?) type checks behind you!
+
+Installation
+============
+Make sure Composer is installed globally, as explained in the
+[installation chapter](https://getcomposer.org/doc/00-intro.md)
+of the Composer documentation.
+
+### Step 1: Download the module
+Open a command console, enter your project directory and execute:
+
+```console
+$ composer require coderg33k/typed-config-generator
+```
+
+### Step 2: Enable the module
+*@todo: Autodiscover the ServiceProvider*
+
+Then, enable the library by adding the service provider to the list of registered providers
+in the `config/app.php` file of your project:
+
+```php
+// config/app.php
+
+'providers' => [
+ // ...
+ CoderG33k\TypedConfigServiceProvider::class,
+ // ...
+];
+```
+
+Testing
+-------
+This bundle uses [PHPUnit](https://phpunit.de) for unit and integration tests.
+
+It can be run standalone by `composer phpunit` or within the complete checkup by `composer checkup`
+
+### Checkup
+The above-mentioned checkup runs multiple analyses of the bundle's code. This includes [Squizlab's Codesniffer](https://github.com/squizlabs/PHP_CodeSniffer), [PHPStan](https://phpstan.org) and a [coverage check](https://github.com/richardregeer/phpunit-coverage-check).
+
+Continuous Integration
+----------------------
+[GitHub actions](https://github.com/features/actions) are used for continuous integration. Check out the [configuration file](https://github.com/coddin-web/idp-openid-connect-bundle/blob/main/.github/workflows/ci.yml) if you'd like to know more.
+
+Changelog
+---------
+See the [project changelog](https://github.com/coddin-web/idp-openid-connect-bundle/blob/main/CHANGELOG.md)
+
+Contributing
+------------
+Contributions are always welcome. Please see [CONTRIBUTING.md](https://github.com/coddin-web/idp-openid-connect-bundle/blob/main/CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](https://github.com/coddin-web/idp-openid-connect-bundle/blob/main/CODE_OF_CONDUCT.md) for details.
+
+License
+-------
+The MIT License (MIT). Please see [License File](https://github.com/coddin-web/idp-openid-connect-bundle/blob/main/LICENSE) for more information.
+
+Credits
+-------
+This code is principally developed and maintained by [Marius Posthumus](https://github.com/MJTheOne)
diff --git a/assets/laravel.json b/assets/laravel.json
new file mode 100644
index 0000000..63e17a6
--- /dev/null
+++ b/assets/laravel.json
@@ -0,0 +1,6 @@
+[
+ {
+ "package": "laravel/framework",
+ "config_file": "auth"
+ }
+]
diff --git a/assets/spatie.json b/assets/spatie.json
new file mode 100644
index 0000000..f77e523
--- /dev/null
+++ b/assets/spatie.json
@@ -0,0 +1,6 @@
+[
+ {
+ "package": "spatie/laravel-data",
+ "config_file": "data"
+ }
+]
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..82bd4d9
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,42 @@
+{
+ "name": "coderg33k/typed-config-generator",
+ "description": "Typed Classes for your Laravel configs!",
+ "type": "library",
+ "require": {
+ "php": "^8.1",
+ "illuminate/support": "^9.0|^10.0"
+ },
+ "require-dev": {
+ "dg/bypass-finals": "^1.5",
+ "nunomaduro/larastan": "^2.6",
+ "orchestra/testbench": "^7.0|^8.0",
+ "phpstan/phpstan": "^1.10",
+ "phpstan/phpstan-deprecation-rules": "^1.1",
+ "phpstan/phpstan-phpunit": "^1.3",
+ "phpstan/phpstan-strict-rules": "^1.5",
+ "phpunit/phpunit": "^9.0|^10.4",
+ "rregeer/phpunit-coverage-check": "^0.3.1",
+ "slevomat/coding-standard": "^8.14",
+ "squizlabs/php_codesniffer": "^3.7",
+ "thecodingmachine/phpstan-strict-rules": "^1.0"
+ },
+ "license": "MIT",
+ "autoload": {
+ "psr-4": {
+ "Coderg33k\\TypedConfigGenerator\\": "src/"
+ }
+ },
+ "authors": [
+ {
+ "name": "Marius",
+ "homepage": "https://github.com/MJTheOne"
+ }
+ ],
+ "minimum-stability": "stable",
+ "config": {
+ "sort-packages": true,
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
+ }
+}
diff --git a/phpcs_codestyle.xml b/phpcs_codestyle.xml
new file mode 100644
index 0000000..fcb52b0
--- /dev/null
+++ b/phpcs_codestyle.xml
@@ -0,0 +1,157 @@
+
+
+ Check for code style.
+
+
+
+
+
+
+
+
+ *.js
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ error
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests/**/*.php
+
+
+
+ tests/**/*.php
+
+
+ tests/**/*.php
+
+
+
+ 0
+
+
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..11200d1
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,21 @@
+includes:
+ - vendor/phpstan/phpstan-phpunit/extension.neon
+ - vendor/phpstan/phpstan-strict-rules/rules.neon
+ - vendor/phpstan/phpstan-deprecation-rules/rules.neon
+ - vendor/phpstan/phpstan-phpunit/rules.neon
+ - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon
+
+ - phpstan/project.neon
+ - phpstan/laravel.neon
+ - phpstan/ergebnis.neon
+
+parameters:
+ tmpDir: var/cache/phpstan
+ paths:
+ - src
+
+ level: 9
+
+ checkMissingIterableValueType: true
+ treatPhpDocTypesAsCertain: true
+ reportMaybesInPropertyPhpDocTypes: false
diff --git a/phpstan/ergebnis.neon b/phpstan/ergebnis.neon
new file mode 100644
index 0000000..e008dd1
--- /dev/null
+++ b/phpstan/ergebnis.neon
@@ -0,0 +1,9 @@
+includes:
+ - ../vendor/ergebnis/phpstan-rules/rules.neon
+
+parameters:
+ ergebnis:
+ noExtends:
+ classesAllowedToBeExtended:
+ - Exception
+ - LogicException
diff --git a/phpstan/laravel.neon b/phpstan/laravel.neon
new file mode 100644
index 0000000..f1e60d5
--- /dev/null
+++ b/phpstan/laravel.neon
@@ -0,0 +1,12 @@
+includes:
+ - ../vendor/nunomaduro/larastan/extension.neon
+
+parameters:
+ strictRules:
+ # strictCalls clashes with larastan and will still throw errors on things larastan fixes
+ strictCalls: false
+
+ ergebnis:
+ noExtends:
+ classesAllowedToBeExtended:
+ - Tests\TestCase
diff --git a/phpstan/project.neon b/phpstan/project.neon
new file mode 100644
index 0000000..6f4bb2d
--- /dev/null
+++ b/phpstan/project.neon
@@ -0,0 +1,8 @@
+parameters:
+ ignoreErrors:
+ # Add errors to ignore here.
+
+ ergebnis:
+ noExtends:
+ classesAllowedToBeExtended:
+ # Add classes here that are allowed to be extended.
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..369a392
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,34 @@
+
+
+
+
+ ./tests/Unit
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Actions/GetClassForConfig.php b/src/Actions/GetClassForConfig.php
new file mode 100644
index 0000000..f129f8f
--- /dev/null
+++ b/src/Actions/GetClassForConfig.php
@@ -0,0 +1,21 @@
+
+ */
+ public static function execute(
+ string $namespace,
+ string $config,
+ ): string {
+ return $namespace . 'Config\\' . \ucfirst(Str::camel($config));
+ }
+}
diff --git a/src/Actions/GetConfigsForPredeterminedPackage.php b/src/Actions/GetConfigsForPredeterminedPackage.php
new file mode 100644
index 0000000..c04a0b3
--- /dev/null
+++ b/src/Actions/GetConfigsForPredeterminedPackage.php
@@ -0,0 +1,40 @@
+ [
+ 'app',
+ 'auth',
+ 'broadcasting',
+ 'cache',
+ 'cors',
+ 'database',
+ 'filesystems',
+ 'hashing',
+ 'logging',
+ 'mail',
+ 'queue',
+ 'services',
+ 'session',
+ 'view',
+ ],
+ Package::Spatie => [
+ 'cors',
+ 'csp',
+ 'data',
+ 'flare',
+ 'ignition',
+ 'permission',
+ ],
+ };
+ }
+}
diff --git a/src/Console/Commands/GenerateTypedConfig.php b/src/Console/Commands/GenerateTypedConfig.php
new file mode 100644
index 0000000..d5422b1
--- /dev/null
+++ b/src/Console/Commands/GenerateTypedConfig.php
@@ -0,0 +1,275 @@
+determineWhatShouldBeGenerated();
+
+ $this->generateTypedClasses();
+ }
+
+ private function determineWhatShouldBeGenerated(): void
+ {
+ if ($this->option('all')) {
+ return;
+ }
+
+ $this->configs = (array) $this->option('config');
+
+ if (\count($this->configs) === 0) {
+ $this->promptForConfigs();
+ }
+ }
+
+ private function setupChoices(): array
+ {
+ return \array_merge(
+ ['All configurations'],
+ \preg_filter('/^/', 'Package: > ', Arr::sort(['Laravel', 'Spatie'])),
+ \preg_filter('/^/', 'Config: > ', Arr::sort(\array_keys(config()->all()))),
+ );
+ }
+
+ private function promptForConfigs(): void
+ {
+ $choices = $this->setupChoices();
+
+ $choice = windows_os()
+ ? select(
+ label: 'For which config do you want to generate a typed (guestimated) class?',
+ options: $choices,
+ scroll: 15,
+ )
+ : search(
+ label: 'For which config do you want to generate a typed (guestimated) class?',
+ options: fn ($search) => \array_values(\array_filter(
+ $choices,
+ fn ($choice) => \str_contains(\strtolower($choice), \strtolower($search))
+ )),
+ placeholder: 'Search...',
+ scroll: 15,
+ );
+
+ if ($choice == $choices[0] || !\is_string($choice)) {
+ return;
+ }
+
+ $this->parseChoice($choice);
+ }
+
+ private function parseChoice(string $choice): void
+ {
+ [$type, $value] = \explode(': ', \strip_tags($choice));
+
+ switch ($type) {
+ case 'Config':
+ $this->configs = [$value];
+ break;
+ case 'Package':
+ $this->package = $value;
+ break;
+ }
+ }
+
+ private function generateTypedClasses(): void
+ {
+ if (\count($this->configs) === 0 && !\is_string($this->package)) {
+ $this->configs = [\implode(',', \array_keys(config()->all()))];
+ }
+
+ if (\is_string($this->package)) {
+ $this->discoverConfigsForPackage();
+ }
+
+ $configsToProcess = \explode(',', $this->configs[0]);
+
+ // Clean up configs.
+ $configsToProcess = \array_map(
+ fn (string $config): string => \trim($config),
+ $configsToProcess,
+ );
+
+ $rootNamespace = $this->laravel->getNamespace();
+ $namespace = $rootNamespace . self::GENERATED_CONFIG_NAMESPACE_BASE;
+
+ // @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(
+ config: $config,
+ namespace: $namespace,
+ properties: $properties,
+ );
+ }
+
+ if (\count($nullValues) > 0) {
+ $this->components->warn('Some properties have null values, please check the generated class(es).');
+ $this->table(
+ ['Config', 'Key'],
+ $this->arrayFlatMap->execute($nullValues),
+ );
+ }
+
+ $this->newLine();
+ $this->line('Class(es) created successfully!', 'info');
+ }
+
+ private function discoverConfigsForPackage(): void
+ {
+ $allConfigs = \array_keys(config()->all());
+
+ $this->configs = [
+ \implode(
+ ',',
+ \array_intersect(
+ $this->getConfigsForPredeterminedPackage->execute(
+ package: Package::getByValue($this->package),
+ ),
+ $allConfigs,
+ ),
+ )
+ ];
+ }
+
+ private function buildStub(
+ string $config,
+ string $namespace,
+ array $properties,
+ ): void {
+ $stub = \file_get_contents($this->getStub());
+
+ $stub = \str_replace(
+ search: [
+ '{{ namespace }}',
+ '{{ properties }}',
+ '{{ class }}',
+ ],
+ replace: [
+ $namespace,
+ $this->buildProperties($properties),
+ \ucfirst(Str::camel($config)),
+ ],
+ 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/Console/Commands/stubs/typed_config/default.stub b/src/Console/Commands/stubs/typed_config/default.stub
new file mode 100644
index 0000000..24b0441
--- /dev/null
+++ b/src/Console/Commands/stubs/typed_config/default.stub
@@ -0,0 +1,13 @@
+ self::Laravel,
+ 'spatie' => self::Spatie,
+ };
+ }
+}
diff --git a/src/Helper/ArrayFlatMap.php b/src/Helper/ArrayFlatMap.php
new file mode 100644
index 0000000..5ea87b7
--- /dev/null
+++ b/src/Helper/ArrayFlatMap.php
@@ -0,0 +1,25 @@
+ $value) {
+ if (\is_array($value)) {
+ $result = \array_merge($result, $this->execute($value, $key));
+ } else {
+ $result[] = [$prefix, $value];
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/src/Helper/DoesTypedConfigClassExist.php b/src/Helper/DoesTypedConfigClassExist.php
new file mode 100644
index 0000000..c8490ed
--- /dev/null
+++ b/src/Helper/DoesTypedConfigClassExist.php
@@ -0,0 +1,27 @@
+ Str::camel($key),
+ \array_keys($properties),
+ ),
+ $properties,
+ );
+
+ return new static(...$properties);
+ }
+}
diff --git a/src/TypedConfigServiceProvider.php b/src/TypedConfigServiceProvider.php
new file mode 100644
index 0000000..16dd1d9
--- /dev/null
+++ b/src/TypedConfigServiceProvider.php
@@ -0,0 +1,36 @@
+all()) as $config) {
+ $doesConfigClassExist = DoesTypedConfigClassExist::determine(
+ namespace: $this->app->getNamespace(),
+ config: $config,
+ );
+
+ if (!$doesConfigClassExist) {
+ continue;
+ }
+
+ $class = GetClassForConfig::execute(
+ namespace: $this->app->getNamespace(),
+ config: $config,
+ );
+
+ $this->app->singleton(
+ $class,
+ fn () => $class::fromConfig(...config($config)),
+ );
+ }
+ }
+}
diff --git a/tests/.gitkeep b/tests/.gitkeep
new file mode 100644
index 0000000..e69de29