├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── UPGRADING.md ├── composer.json ├── config └── settings.php ├── database └── migrations │ └── create_settings_table.php.stub └── src ├── Console ├── CacheDiscoveredSettingsCommand.php ├── ClearCachedSettingsCommand.php ├── ClearDiscoveredSettingsCacheCommand.php ├── MakeSettingCommand.php └── MakeSettingsMigrationCommand.php ├── Events ├── LoadingSettings.php ├── SavingSettings.php ├── SettingsLoaded.php └── SettingsSaved.php ├── Exceptions ├── CouldNotResolveDocblockType.php ├── CouldNotUnserializeSettings.php ├── InvalidSettingName.php ├── MissingSettings.php ├── SettingAlreadyExists.php ├── SettingDoesNotExist.php └── SettingsCacheDisabled.php ├── Factories ├── SettingsCastFactory.php └── SettingsRepositoryFactory.php ├── LaravelSettingsServiceProvider.php ├── Migrations ├── SettingsBlueprint.php ├── SettingsMigration.php └── SettingsMigrator.php ├── Models └── SettingsProperty.php ├── Settings.php ├── SettingsCache.php ├── SettingsCasts ├── ArraySettingsCast.php ├── CollectionCast.php ├── DataCast.php ├── DateTimeInterfaceCast.php ├── DateTimeZoneCast.php ├── DtoCast.php ├── EnumCast.php └── SettingsCast.php ├── SettingsConfig.php ├── SettingsContainer.php ├── SettingsEventSubscriber.php ├── SettingsMapper.php ├── SettingsRepositories ├── DatabaseSettingsRepository.php ├── RedisSettingsRepository.php └── SettingsRepository.php └── Support ├── Composer.php ├── Crypto.php ├── DiscoverSettings.php ├── PropertyReflector.php └── SettingsCacheFactory.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-settings` will be documented in this file 4 | 5 | # Unreleased 6 | 7 | - Make `spatie/data-transfer-object` dependency optional. (#160) 8 | 9 | ## 3.4.4 - 2025-04-11 10 | 11 | - Fix #319 12 | 13 | **Full Changelog**: https://github.com/spatie/laravel-settings/compare/3.4.3...3.4.4 14 | 15 | ## 3.4.3 - 2025-04-11 16 | 17 | ### What's Changed 18 | 19 | * Do not save settings with missing migrations by @gazben in https://github.com/spatie/laravel-settings/pull/313 20 | 21 | **Full Changelog**: https://github.com/spatie/laravel-settings/compare/3.4.2...3.4.3 22 | 23 | ## 3.4.2 - 2025-02-14 24 | 25 | ### What's Changed 26 | 27 | * Fill missing settings with default values by @gazben in https://github.com/spatie/laravel-settings/pull/298 28 | 29 | ### New Contributors 30 | 31 | * @gazben made their first contribution in https://github.com/spatie/laravel-settings/pull/298 32 | 33 | **Full Changelog**: https://github.com/spatie/laravel-settings/compare/3.4.1...3.4.2 34 | 35 | ## 3.4.1 - 2025-01-31 36 | 37 | ### What's Changed 38 | 39 | * chore(deps): bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/laravel-settings/pull/309 40 | * Change out of date stubs in README by @GrandadEvans in https://github.com/spatie/laravel-settings/pull/310 41 | * Support Illuminate\Support\Carbon as cast by @Propaganistas in https://github.com/spatie/laravel-settings/pull/311 42 | * chore: fix typo by @danjohnson95 in https://github.com/spatie/laravel-settings/pull/306 43 | 44 | ### New Contributors 45 | 46 | * @GrandadEvans made their first contribution in https://github.com/spatie/laravel-settings/pull/310 47 | * @Propaganistas made their first contribution in https://github.com/spatie/laravel-settings/pull/311 48 | * @danjohnson95 made their first contribution in https://github.com/spatie/laravel-settings/pull/306 49 | 50 | **Full Changelog**: https://github.com/spatie/laravel-settings/compare/3.4.0...3.4.1 51 | 52 | ## 3.4.0 - 2024-09-20 53 | 54 | ### What's Changed 55 | 56 | * Update README.md by @marventhieme in https://github.com/spatie/laravel-settings/pull/290 57 | * Update README.md by @marventhieme in https://github.com/spatie/laravel-settings/pull/291 58 | * Feat: add exists in migrator by @akshit-arora in https://github.com/spatie/laravel-settings/pull/289 59 | 60 | ### New Contributors 61 | 62 | * @marventhieme made their first contribution in https://github.com/spatie/laravel-settings/pull/290 63 | * @akshit-arora made their first contribution in https://github.com/spatie/laravel-settings/pull/289 64 | 65 | **Full Changelog**: https://github.com/spatie/laravel-settings/compare/3.3.3...3.4.0 66 | 67 | ## 3.3.3 - 2024-08-13 68 | 69 | ### What's Changed 70 | 71 | * Handle Parentheses On Anonymous Settings Migrations by @Magnesium38 in https://github.com/spatie/laravel-settings/pull/280 72 | 73 | ### New Contributors 74 | 75 | * @Magnesium38 made their first contribution in https://github.com/spatie/laravel-settings/pull/280 76 | 77 | **Full Changelog**: https://github.com/spatie/laravel-settings/compare/3.3.2...3.3.3 78 | 79 | ## 3.3.2 - 2024-03-22 80 | 81 | ### What's Changed 82 | 83 | * [3.x] Fix PHP 7.4 Compatibilty by @Rizky92 in https://github.com/spatie/laravel-settings/pull/264 84 | * Update MakeSettingCommand.php by @hamzaelmaghari in https://github.com/spatie/laravel-settings/pull/262 85 | 86 | **Full Changelog**: https://github.com/spatie/laravel-settings/compare/3.3.1...3.3.2 87 | 88 | ## 3.3.1 - 2024-03-13 89 | 90 | ### What's Changed 91 | 92 | * fix when base path is app path by @mvenghaus in https://github.com/spatie/laravel-settings/pull/259 93 | 94 | **Full Changelog**: https://github.com/spatie/laravel-settings/compare/3.3.0...3.3.1 95 | 96 | ## 3.3.0 - 2024-02-19 97 | 98 | ### What's Changed 99 | 100 | * Update composer.json to use Larastan Org by @arnebr in https://github.com/spatie/laravel-settings/pull/252 101 | * Add support for laravel 11 by @shuvroroy in https://github.com/spatie/laravel-settings/pull/256 102 | * Added settings driven custom encoder/decoder by @naxvog in https://github.com/spatie/laravel-settings/pull/250 103 | 104 | **Full Changelog**: https://github.com/spatie/laravel-settings/compare/3.2.3...3.3.0 105 | 106 | ## 3.2.3 - 2023-12-04 107 | 108 | - Revert "Use Illuminate\Database\Eloquent\Casts\Json if possible" (#249) 109 | 110 | ## 3.2.2 - 2023-12-01 111 | 112 | - Use Illuminate\Database\Eloquent\Casts\Json if possible (#241) 113 | 114 | ## 3.2.1 - 2023-09-15 115 | 116 | - Change provider tag name for config (#233) 117 | 118 | ## 3.2.0 - 2023-07-05 119 | 120 | - Add support for database-less fakes 121 | 122 | ## 3.1.0 - 2023-05-11 123 | 124 | - Add support for nullable enum properties 125 | - Updates to the upgrade guide 126 | 127 | ## 3.0.0 - 2023-04-28 128 | 129 | - Allow repositories to update multiple settings at once (#213 ) 130 | - The default location where searching for settings happens is now `app_path('Settings')` instead of `app_path()` 131 | - The default `discovered_settings_cache_path` is changed 132 | 133 | ## 2.8.3 - 2023-03-30 134 | 135 | - Remove doctrine as a dependency 136 | 137 | ## 2.8.2 - 2023-03-10 138 | 139 | - Fix remigration problems with anonymous settings migrations 140 | 141 | ## 2.8.1 - 2023-03-02 142 | 143 | - Show message and target path after setting migration created (#203) 144 | - Follow Laravel's namespace convention in MakeSettingCommand (#200) 145 | - Update MakeSettingsMigrationCommand.php (#205) 146 | - Revert "Add support for structure discoverer"( #207) 147 | 148 | ## 2.8.0 - 2023-02-10 149 | 150 | - Drop Laravel 8 support 151 | - Drop PHP 8.0 support 152 | - Use spatie/structures-discoverer for finding settings 153 | 154 | ## 2.7.0 - 2023-02-01 155 | 156 | - Add Laravel 10 Support (#192) 157 | - Update make:settings migration class as anonymous class (#189) 158 | - Use correct namespace in make:settings command (#190) 159 | 160 | ## 2.6.1 - 2023-01-06 161 | 162 | - Add current date to the settings migration file (#178) 163 | - Add command to make new settings (#181) 164 | 165 | ## 1.6.1 - 2022-12-21 166 | 167 | - create settings migration with current date (#179) 168 | 169 | ## 2.6.0 - 2022-11-24 170 | 171 | - Add support for caching on repository level 172 | 173 | ## 2.5.0 - 2022-11-10 174 | 175 | - Remove deprecated package 176 | - Add laravel data cast 177 | - Add support for PHP 8.2 178 | - Remove PHP 7.4 support 179 | - Remove dto cast from default config 180 | 181 | ## 2.4.5 - 2022-09-28 182 | 183 | - Add deleteIfExists() method to migrator (#154) 184 | 185 | ## 2.4.4 - 2022-09-07 186 | 187 | - cache encrypted settings 188 | 189 | Please, be sure to clear your cache since settings classes with encrypted properties will crash due to the cached versions missing a proper encrypted version of the property. Clearing and caching again after installing this version resolves this problem and is something you probably should always do when deploying to production! 190 | 191 | ## 2.4.3 - 2022-08-10 192 | 193 | - add rollback to migration 194 | 195 | ## 2.4.2 - 2022-06-17 196 | 197 | - use Facade imports instead of aliases (#132) 198 | 199 | ## 2.4.1 - 2022-04-07 200 | 201 | - Switch to using scoped instances instead of singletons (#129) 202 | 203 | ## 2.4.0 - 2022-03-22 204 | 205 | ## What's Changed 206 | 207 | - Add TTL config for settings cache by @AlexVanderbist in https://github.com/spatie/laravel-settings/pull/122 208 | 209 | ## New Contributors 210 | 211 | - @AlexVanderbist made their first contribution in https://github.com/spatie/laravel-settings/pull/122 212 | 213 | **Full Changelog**: https://github.com/spatie/laravel-settings/compare/2.3.3...2.4.0 214 | 215 | ## 2.3.3 - 2022-03-18 216 | 217 | - fix debug info method 218 | - convert PHPUnit to Pest (#118) 219 | 220 | ## 2.3.2 - 2022-02-25 221 | 222 | - Allow migrations without a value (#113) 223 | 224 | ## 2.3.1 - 2022-02-04 225 | 226 | - Add support for Laravel 9 227 | - Fix cache implementation with casts 228 | - Remove Psalm 229 | - Add PHPStan 230 | 231 | ## 2.2.0 - 2021-10-22 232 | 233 | - add support for multiple migration paths (#92) 234 | 235 | ## 2.1.12 - 2021-10-14 236 | 237 | - add possibility to check if setting is locked or unlocked (#89) 238 | 239 | ## 2.1.11 - 2021-08-23 240 | 241 | - ignore abstract classes when discovering settings (#84) 242 | 243 | ## 2.1.10 - 2021-08-17 244 | 245 | - add support for `null` in DateTime casts 246 | 247 | ## 2.1.9 - 2021-07-08 248 | 249 | - fix `empty` call not working when properties weren't loaded 250 | 251 | ## 2.1.8 - 2021-06-21 252 | 253 | - fix fake settings not working with `Arrayable` 254 | 255 | ## 2.1.7 - 2021-06-08 256 | 257 | - add support for refreshing settings 258 | 259 | ## 2.1.6 - 2021-06-03 260 | 261 | - add support for defining the database connection table 262 | 263 | ## 2.1.5 - 2021-05-21 264 | 265 | - fix some casting problems 266 | - update php-cs-fixer 267 | 268 | ## 2.1.4 - 2021-04-28 269 | 270 | - added fallback for settings.auto_discover_settings (#63) 271 | - add support for spatie/data-transfer-object v3 (#62) 272 | 273 | ## 2.1.3 - 2021-04-14 274 | 275 | - add support for spatie/temporary-directory v2 276 | 277 | ## 2.1.2 - 2021-04-08 278 | 279 | - skip classes with errors when discovering settings 280 | 281 | ## 2.1.1 - 2021-04-07 282 | 283 | - add better support for nullable types in docblocks 284 | 285 | ## 2.1.0 - 2021-04-07 286 | 287 | - add casts to migrations (#53) 288 | - add original properties to `SavingSettings` event (#57) 289 | 290 | ## 2.0.1 - 2021-03-05 291 | 292 | - add support for lumen 293 | 294 | ## 2.0.0 - 2021-03-03 295 | 296 | - settings classes: 297 | - properties won't be loaded when constructed but when requested 298 | - receive a `SettingsMapper` when constructed 299 | - faking settings will now only request non-given properties from the repository 300 | - rewritten `SettingsMapper` from scratch 301 | - removed `SettingsPropertyData` and `ettingsPropertyDataCollection` 302 | - changed signatures of `SavingSettings` and `LoadingSettings` events 303 | - added support for caching settings 304 | - renamed `cache_path` in settings.php to `discovered_settings_cache_path` 305 | 306 | ## 1.0.8 - 2021-03-03 307 | 308 | - fix for properties without defined type 309 | 310 | ## 1.0.7 - 2021-02-19 311 | 312 | - fix correct 'Event' facade (#30) 313 | 314 | ## 1.0.6 - 2021-02-05 315 | 316 | - add support for restoring settings after a Laravel schema:dump 317 | 318 | ## 1.0.5 - 2021-01-29 319 | 320 | - bump the `doctrine/dbal` dependency 321 | 322 | ## 1.0.4 - 2021-01-08 323 | 324 | - add support for getting the locked settings 325 | 326 | ## 1.0.3 - 2020-11-26 327 | 328 | - add PHP 8 support 329 | 330 | ## 1.0.2 - 2020-11-26 331 | 332 | - fix package namespace within migrations (#9) 333 | 334 | ## 1.0.1 - 2020-11-18 335 | 336 | - fix config file tag (#4) 337 | - fix database migration path exists (#7) 338 | 339 | ## 1.0.0 - 2020-11-09 340 | 341 | - initial release 342 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Store strongly typed application settings 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-settings.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-settings) 4 | [![Tests](https://github.com/spatie/laravel-settings/actions/workflows/run-tests.yml/badge.svg)](https://github.com/spatie/laravel-settings/actions/workflows/run-tests.yml) 5 | [![PHPStan](https://github.com/spatie/laravel-settings/actions/workflows/phpstan.yml/badge.svg)](https://github.com/spatie/laravel-settings/actions/workflows/phpstan.yml) 6 | [![Style](https://github.com/spatie/laravel-settings/workflows/Check%20&%20fix%20styling/badge.svg)](https://github.com/spatie/laravel-settings/actions?query=workflow%3A%22Check+%26+fix+styling%22) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-settings.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-settings) 8 | 9 | This package allows you to store settings in a repository (database, Redis, ...) and use them through an application without hassle. You can create a settings class as such: 10 | 11 | ```php 12 | class GeneralSettings extends Settings 13 | { 14 | public string $site_name; 15 | 16 | public bool $site_active; 17 | 18 | public static function group(): string 19 | { 20 | return 'general'; 21 | } 22 | } 23 | ``` 24 | 25 | If you want to use these settings somewhere in your application, you can inject them, since we register them in the Laravel Container. For example, in a controller: 26 | 27 | ```php 28 | class GeneralSettingsController 29 | { 30 | public function show(GeneralSettings $settings){ 31 | return view('settings.show', [ 32 | 'site_name' => $settings->site_name, 33 | 'site_active' => $settings->site_active 34 | ]); 35 | } 36 | } 37 | ``` 38 | 39 | You can update the settings as such: 40 | 41 | ```php 42 | class GeneralSettingsController 43 | { 44 | public function update( 45 | GeneralSettingsRequest $request, 46 | GeneralSettings $settings 47 | ){ 48 | $settings->site_name = $request->input('site_name'); 49 | $settings->site_active = $request->input('site_active'); 50 | 51 | $settings->save(); 52 | 53 | return redirect()->back(); 54 | } 55 | } 56 | ``` 57 | 58 | Let's take a look at how to create your own settings classes. 59 | 60 | ## Support us 61 | 62 | [![Image](https://github-ads.s3.eu-central-1.amazonaws.com/laravel-settings.jpg)](https://spatie.be/github-ad-click/laravel-settings) 63 | 64 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 65 | 66 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 67 | 68 | ## Installation 69 | 70 | You can install the package via composer: 71 | 72 | ```bash 73 | composer require spatie/laravel-settings 74 | ``` 75 | 76 | You can publish and run the migrations with: 77 | 78 | ```bash 79 | php artisan vendor:publish --provider="Spatie\LaravelSettings\LaravelSettingsServiceProvider" --tag="migrations" 80 | php artisan migrate 81 | ``` 82 | 83 | You can publish the config file with: 84 | 85 | ```bash 86 | php artisan vendor:publish --provider="Spatie\LaravelSettings\LaravelSettingsServiceProvider" --tag="config" 87 | ``` 88 | 89 | This is the contents of the published config file: 90 | 91 | ```php 92 | 93 | return [ 94 | 95 | /* 96 | * Each settings class used in your application must be registered, you can 97 | * put them (manually) here. 98 | */ 99 | 'settings' => [ 100 | 101 | ], 102 | 103 | /* 104 | * The path where the settings classes will be created. 105 | */ 106 | 'setting_class_path' => app_path('Settings'), 107 | 108 | /* 109 | * In these directories settings migrations will be stored and ran when migrating. A settings 110 | * migration created via the make:settings-migration command will be stored in the first path or 111 | * a custom defined path when running the command. 112 | */ 113 | 'migrations_paths' => [ 114 | database_path('settings'), 115 | ], 116 | 117 | /* 118 | * When no repository was set for a settings class the following repository 119 | * will be used for loading and saving settings. 120 | */ 121 | 'default_repository' => 'database', 122 | 123 | /* 124 | * Settings will be stored and loaded from these repositories. 125 | */ 126 | 'repositories' => [ 127 | 'database' => [ 128 | 'type' => Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository::class, 129 | 'model' => null, 130 | 'table' => null, 131 | 'connection' => null, 132 | ], 133 | 'redis' => [ 134 | 'type' => Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository::class, 135 | 'connection' => null, 136 | 'prefix' => null, 137 | ], 138 | ], 139 | 140 | /* 141 | * The encoder and decoder will determine how settings are stored and 142 | * retrieved in the database. By default, `json_encode` and `json_decode` 143 | * are used. 144 | */ 145 | 'encoder' => null, 146 | 'decoder' => null, 147 | 148 | /* 149 | * The contents of settings classes can be cached through your application, 150 | * settings will be stored within a provided Laravel store and can have an 151 | * additional prefix. 152 | */ 153 | 'cache' => [ 154 | 'enabled' => env('SETTINGS_CACHE_ENABLED', false), 155 | 'store' => null, 156 | 'prefix' => null, 157 | 'ttl' => null, 158 | ], 159 | 160 | /* 161 | * These global casts will be automatically used whenever a property within 162 | * your settings class isn't a default PHP type. 163 | */ 164 | 'global_casts' => [ 165 | DateTimeInterface::class => Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast::class, 166 | DateTimeZone::class => Spatie\LaravelSettings\SettingsCasts\DateTimeZoneCast::class, 167 | // Spatie\DataTransferObject\DataTransferObject::class => Spatie\LaravelSettings\SettingsCasts\DtoCast::class, 168 | Spatie\LaravelData\Data::class => Spatie\LaravelSettings\SettingsCasts\DataCast::class, 169 | ], 170 | 171 | /* 172 | * The package will look for settings in these paths and automatically 173 | * register them. 174 | */ 175 | 'auto_discover_settings' => [ 176 | app_path('Settings'), 177 | ], 178 | 179 | /* 180 | * Automatically discovered settings classes can be cached, so they don't 181 | * need to be searched each time the application boots up. 182 | */ 183 | 'discovered_settings_cache_path' => base_path('bootstrap/cache'), 184 | ]; 185 | ``` 186 | 187 | ## Usage 188 | 189 | The package is built around settings classes, which are classes with public properties that extend from `Settings`. They also have a static method `group` that should return a string. 190 | 191 | You can create multiple groups of settings, each with their settings class. You could, for example, have `GeneralSettings` with the `general` group and `BlogSettings` with the `blog` group. It's up to you how to structure these groups. 192 | 193 | Although it is possible to use the same group for different settings classes, we advise you not to use the same group for multiple settings classes. 194 | 195 | 196 | ```php 197 | use Spatie\LaravelSettings\Settings; 198 | 199 | class GeneralSettings extends Settings 200 | { 201 | public string $site_name; 202 | 203 | public bool $site_active; 204 | 205 | public static function group(): string 206 | { 207 | return 'general'; 208 | } 209 | } 210 | ``` 211 | 212 | You can generate a new settings class using this artisan command. Before you do, please check if the `setting_class_path` is correctly set. You can also specify a `path` option, which is optional. 213 | 214 | ```bash 215 | php artisan make:setting SettingName --group=groupName 216 | ``` 217 | 218 | Now, you will have to add this settings class to the `settings.php` config file in the `settings` array, so it can be loaded by Laravel: 219 | 220 | ```php 221 | /* 222 | * Each settings class used in your application must be registered, you can 223 | * add them (manually) here. 224 | */ 225 | 'settings' => [ 226 | GeneralSettings::class 227 | ], 228 | ``` 229 | 230 | Each property in a settings class needs a default value that should be set in its migration. You can create a migration as such: 231 | 232 | ```bash 233 | php artisan make:settings-migration CreateGeneralSettings 234 | ``` 235 | 236 | This command will create a new file in `database/settings` where you can add the properties and their default values: 237 | 238 | ```php 239 | use Spatie\LaravelSettings\Migrations\SettingsMigration; 240 | 241 | return new class extends SettingsMigration 242 | { 243 | public function up(): void 244 | { 245 | $this->migrator->add('general.site_name', 'Spatie'); 246 | $this->migrator->add('general.site_active', true); 247 | } 248 | } 249 | ``` 250 | 251 | We add the properties `site_name` and `site_active` here to the `general` group with values `Spatie` and `true`. More on migrations [later](https://github.com/spatie/laravel-settings#creating-settings-migrations). 252 | 253 | You should migrate your database to add the properties: 254 | 255 | ```bash 256 | php artisan migrate 257 | ``` 258 | 259 | Now, when you want to use the `site_name` property of the `GeneralSettings` settings class, you can inject it into your application: 260 | 261 | ```php 262 | class IndexController 263 | { 264 | public function __invoke(GeneralSettings $settings){ 265 | return view('index', [ 266 | 'site_name' => $settings->site_name, 267 | ]); 268 | } 269 | } 270 | ``` 271 | 272 | Or use it to load it somewhere in your application as such: 273 | 274 | ```php 275 | function getName(): string{ 276 | return app(GeneralSettings::class)->site_name; 277 | } 278 | ``` 279 | 280 | Updating the settings can be done as such: 281 | 282 | ```php 283 | class SettingsController 284 | { 285 | public function __invoke(GeneralSettings $settings, GeneralSettingsRequest $request){ 286 | $settings->site_name = $request->input('site_name'); 287 | $settings->site_active = $request->boolean('site_active'); 288 | 289 | $settings->save(); 290 | 291 | return redirect()->back(); 292 | } 293 | } 294 | ``` 295 | 296 | ### Selecting a repository 297 | 298 | Settings will be stored and loaded from the repository. There are two types of repositories `database` and `redis`. And it is possible to create multiple repositories for these types. For example, you could have two `database` repositories, one that goes to a `settings` table in your database and another that goes to a `global_settings` table. 299 | 300 | You can explicitly set the repository of a settings class by implementing the `repository` method: 301 | 302 | ```php 303 | class GeneralSettings extends Settings 304 | { 305 | public string $site_name; 306 | 307 | public bool $site_active; 308 | 309 | public static function group(): string 310 | { 311 | return 'general'; 312 | } 313 | 314 | public static function repository(): ?string 315 | { 316 | return 'global_settings'; 317 | } 318 | } 319 | ``` 320 | 321 | When a repository is not set for a settings class, the `default_repository` in the `settings.php` config file will be used. 322 | 323 | ### Creating settings migrations 324 | 325 | Before you can load/update settings, you will have to migrate them. Though this might sound a bit strange at the beginning, it is quite logical. You want to have some default settings to start with when you're creating a new application. And what would happen if we change a property of a settings class? Our code would change, but our data doesn't. 326 | 327 | That's why the package requires migrations each time you're changing/creating your settings classes' structure. These migrations will run next to the regular Laravel database migrations, and we've added some tooling to write them as quickly as possible. 328 | 329 | Creating a settings migration works just like you would create a regular database migration. You can run the following command: 330 | 331 | ```bash 332 | php artisan make:settings-migration CreateGeneralSettings 333 | ``` 334 | 335 | This will add a migration to the `application/database/settings` directory: 336 | 337 | ```php 338 | use Spatie\LaravelSettings\Migrations\SettingsMigration; 339 | 340 | class CreateGeneralSettings extends SettingsMigration 341 | { 342 | public function up(): void 343 | { 344 | 345 | } 346 | } 347 | ``` 348 | 349 | We haven't added a `down` method, but this can be added if desired. In the `up` method, you can change the settings data in the repository when migrating. There are a few default operations supported: 350 | 351 | #### Adding a property 352 | 353 | You can add a property to a settings group as such: 354 | 355 | ```php 356 | public function up(): void 357 | { 358 | $this->migrator->add('general.timezone', 'Europe/Brussels'); 359 | } 360 | ``` 361 | 362 | We've added a `timezone` property to the `general` group, which is being used by `GeneralSettings`. You should always give a default value for a newly created setting. In this case, this is the `Europe/Brussels` timezone. 363 | 364 | If the property in the settings class is nullable, it's possible to give `null` as a default value. 365 | 366 | #### Renaming a property 367 | 368 | It is possible to rename a property: 369 | 370 | ```php 371 | public function up(): void 372 | { 373 | $this->migrator->rename('general.timezone', 'general.local_timezone'); 374 | } 375 | ``` 376 | 377 | You can also move a property to another group: 378 | 379 | ```php 380 | public function up(): void 381 | { 382 | $this->migrator->rename('general.timezone', 'country.timezone'); 383 | } 384 | ``` 385 | 386 | #### Updating a property 387 | 388 | It is possible to update the contents of a property: 389 | 390 | ```php 391 | public function up(): void 392 | { 393 | $this->migrator->update( 394 | 'general.timezone', 395 | fn(string $timezone) => return 'America/New_York' 396 | ); 397 | } 398 | ``` 399 | 400 | As you can see, this method takes a closure as an argument, which makes it possible to update a value based upon its old value. 401 | 402 | #### Deleting a property 403 | 404 | ```php 405 | public function up(): void 406 | { 407 | $this->migrator->delete('general.timezone'); 408 | } 409 | ``` 410 | 411 | #### Checking a property if it exists 412 | 413 | There might be times when you want to check if a property exists in the database. This can be done as such: 414 | 415 | ```php 416 | public function up(): void 417 | { 418 | if ($this->migrator->exists('general.timezone')) { 419 | // do something 420 | } 421 | } 422 | ``` 423 | 424 | #### Operations in group 425 | 426 | When you're working on a big settings class with many properties, it can be a bit cumbersome always to have to prepend the settings group. That's why you can also perform operations within a settings group: 427 | 428 | ```php 429 | public function up(): void 430 | { 431 | $this->migrator->inGroup('general', function (SettingsBlueprint $blueprint): void { 432 | $blueprint->add('timezone', 'Europe/Brussels'); 433 | 434 | $blueprint->rename('timezone', 'local_timezone'); 435 | 436 | $blueprint->update('timezone', fn(string $timezone) => return 'America/New_York'); 437 | 438 | $blueprint->delete('timezone'); 439 | }); 440 | } 441 | ``` 442 | 443 | ### Typing properties 444 | 445 | It is possible to create a settings class with regular PHP types: 446 | 447 | 448 | ```php 449 | class RegularTypeSettings extends Settings 450 | { 451 | public string $a_string; 452 | 453 | public bool $a_bool; 454 | 455 | public int $an_int; 456 | 457 | public float $a_float; 458 | 459 | public array $an_array; 460 | 461 | public static function group(): string 462 | { 463 | return 'regular_type'; 464 | } 465 | } 466 | ``` 467 | 468 | Internally the package will convert these types to JSON and save them as such in the repository. But what about types like `DateTime` and `Carbon` or your own created types? Although these types can be converted to JSON, building them back up again from JSON isn't supported. 469 | 470 | That's why you can specify casts within this package. There are two ways to define these casts: locally or globally. 471 | 472 | #### Local casts 473 | 474 | Local casts work on one specific settings class and should be defined for each property: 475 | 476 | ```php 477 | class DateSettings extends Settings 478 | { 479 | public DateTime $birth_date; 480 | 481 | public static function group(): string 482 | { 483 | return 'date'; 484 | } 485 | 486 | public static function casts(): array 487 | { 488 | return [ 489 | 'birth_date' => DateTimeInterfaceCast::class 490 | ]; 491 | } 492 | } 493 | ``` 494 | 495 | The `DateTimeInterfaceCast` can be used for properties with types like `DateTime`, `DateTimeImmutable`, `Carbon` and `CarbonImmutable`. You can also use an already constructed cast. It becomes handy when you need to pass some extra arguments to the cast: 496 | 497 | 498 | 499 | ```php 500 | class DateSettings extends Settings 501 | { 502 | public $birth_date; 503 | 504 | public static function group(): string 505 | { 506 | return 'date'; 507 | } 508 | 509 | public static function casts(): array 510 | { 511 | return [ 512 | 'birth_date' => new DateTimeInterfaceWithTimeZoneCast(DateTime::class, 'Europe/Brussels') 513 | ]; 514 | } 515 | } 516 | ``` 517 | 518 | As you can see, we provide `DateTime::class` to the cast, so it knows what type of `DateTime` it should use because the `birth_date` property was not typed, and the cast couldn't infer the type to use. 519 | 520 | You can also provide arguments to a cast without constructing it: 521 | 522 | ```php 523 | class DateSettings extends Settings 524 | { 525 | public $birth_date; 526 | 527 | public static function group(): string 528 | { 529 | return 'date'; 530 | } 531 | 532 | public static function casts(): array 533 | { 534 | return [ 535 | 'birth_date' => DateTimeInterfaceCast::class.':'.DateTime::class 536 | ]; 537 | } 538 | } 539 | ``` 540 | 541 | #### Global casts 542 | 543 | Local casts are great for defining types for specific properties of the settings class. But it's a lot of work to define a local cast for each regularly used type like a `DateTime`. Global casts try to simplify this process. 544 | 545 | You can define global casts in the `global_casts` array of the package configuration. We've added some default casts to the configuration that can be handy: 546 | 547 | ```php 548 | 'global_casts' => [ 549 | DateTimeInterface::class => Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast::class, 550 | DateTimeZone::class => Spatie\LaravelSettings\SettingsCasts\DateTimeZoneCast::class, 551 | // Spatie\DataTransferObject\DataTransferObject::class => Spatie\LaravelSettings\SettingsCasts\DtoCast::class, 552 | Spatie\LaravelData\Data::class => Spatie\LaravelSettings\SettingsCasts\DataCast::class, 553 | ], 554 | ``` 555 | 556 | A global cast can work on: 557 | 558 | - a specific type (`DateTimeZone::class`) 559 | - a type that implements an interface (`DateTimeInterface::class`) 560 | - a type that extends from another class (`Data::class`) 561 | 562 | In your settings class, when you use a `DateTime` property (which implements `DateTimeInterface`), you no longer have to define local casts: 563 | 564 | ```php 565 | class DateSettings extends Settings 566 | { 567 | public DateTime $birth_date; 568 | 569 | public static function group(): string 570 | { 571 | return 'date'; 572 | } 573 | } 574 | ``` 575 | 576 | The package will automatically find the cast and will use it to transform the types between the settings class and repository. 577 | 578 | #### Typing properties 579 | 580 | There are quite a few options to type properties. You could type them in PHP: 581 | 582 | ```php 583 | class DateSettings extends Settings 584 | { 585 | public DateTime $birth_date; 586 | 587 | public ?int $a_nullable_int; 588 | 589 | public static function group(): string 590 | { 591 | return 'date'; 592 | } 593 | } 594 | ``` 595 | 596 | Or you can use docblocks: 597 | 598 | ```php 599 | class DateSettings extends Settings 600 | { 601 | /** @var \DateTime */ 602 | public $birth_date; 603 | 604 | /** @var ?int */ 605 | public $a_nullable_int; 606 | 607 | /** @var int|null */ 608 | public $another_nullable_int; 609 | 610 | /** @var int[]|null */ 611 | public $an_array_of_ints_or_null; 612 | 613 | public static function group(): string 614 | { 615 | return 'date'; 616 | } 617 | } 618 | ``` 619 | 620 | Docblocks can be very useful to type arrays of objects: 621 | 622 | ```php 623 | class DateSettings extends Settings 624 | { 625 | /** @var array<\DateTime> */ 626 | public array $birth_dates; 627 | 628 | // OR 629 | 630 | /** @var \DateTime[] */ 631 | public array $birth_dates_alternative; 632 | 633 | public static function group(): string 634 | { 635 | return 'date'; 636 | } 637 | } 638 | ``` 639 | 640 | ### Default values 641 | 642 | As we've seen earlier, it is required to define migrations for each property of your setting classes otherwise a `MissingSettings` exception is thrown. 643 | 644 | Sometimes, certain setting classes are used in paths throughout your application which run before migrations, or sometimes it can take quite a while before the migrations are run. In these cases, it can be useful to define default values for your properties: 645 | 646 | ```php 647 | class GeneralSettings extends Settings 648 | { 649 | public string $site_name = 'Spatie'; 650 | 651 | public bool $site_active = true; 652 | 653 | public static function group(): string 654 | { 655 | return 'general'; 656 | } 657 | } 658 | ``` 659 | 660 | These default properties will then be used when no migrated value is found in the repository. This way, you can avoid the `MissingSettings` exception. 661 | 662 | In order to get no `MissingSettings` exception make sure to add default values to every property of your settings class, since property values are resolved in one go. 663 | 664 | ### Locking properties 665 | 666 | When you want to disable the ability to update the value of a setting, you can add a lock to it: 667 | 668 | ```php 669 | $dateSettings->lock('birth_date'); 670 | ``` 671 | 672 | It is now impossible to update the value of `birth_date`. When trying to overwrite `birth_date` and saving settings, the package will load the old value of `birth_date` from the repository, and it looks like nothing happened. 673 | 674 | You can also lock multiple settings at once: 675 | 676 | ```php 677 | $dateSettings->lock('birth_date', 'name', 'email'); 678 | ``` 679 | 680 | You can get all the locked settings: 681 | 682 | ```php 683 | $dateSettings->getLockedProperties(); // ['birth_date'] 684 | ``` 685 | 686 | Unlocking settings can be done as such: 687 | 688 | ```php 689 | $dateSettings->unlock('birth_date', 'name', 'email'); 690 | ``` 691 | 692 | Checking if a setting is currently locked can be done as such: 693 | 694 | ```php 695 | $dateSettings->isLocked('birth_date'); 696 | ``` 697 | 698 | Checking if a setting is currently unlocked can be done as such: 699 | 700 | ```php 701 | $dateSettings->isUnlocked('birth_date'); 702 | ``` 703 | 704 | ### Encrypting properties 705 | 706 | Some properties in your settings class can be confidential, like API keys, for example. It is possible to encrypt some of your properties, so it won't be possible to read them when your repository data was compromised. 707 | 708 | Adding encryption to the properties of your settings class can be done as such. By adding the `encrypted` static method to your settings class and list all the properties that should be encrypted: 709 | 710 | ```php 711 | class GeneralSettings extends Settings 712 | { 713 | public string $site_name; 714 | 715 | public bool $site_active; 716 | 717 | public static function group(): string 718 | { 719 | return 'general'; 720 | } 721 | 722 | public static function encrypted(): array 723 | { 724 | return [ 725 | 'site_name' 726 | ]; 727 | } 728 | } 729 | ``` 730 | 731 | #### Using encryption in migrations 732 | 733 | Creating and updating encrypted properties in migrations works a bit differently than non-encrypted properties. 734 | 735 | Instead of calling the `add` method to create a new property, you should use the `addEncrypted` method: 736 | 737 | ```php 738 | public function up(): void 739 | { 740 | $this->migrator->addEncrypted('general.site_name', 'Spatie'); 741 | } 742 | ``` 743 | 744 | The same goes for the `update` method, which should be replaced by `updateEncrypted`: 745 | 746 | ```php 747 | public function up(): void 748 | { 749 | $this->migrator->updateEncrypted( 750 | 'general.site_name', 751 | fn(string $siteName) => return 'Space' 752 | ); 753 | } 754 | ``` 755 | 756 | You can make a non-encrypted property encrypted in a migration: 757 | 758 | ```php 759 | public function up(): void 760 | { 761 | $this->migrator->add('general.site_name', 'Spatie'); 762 | 763 | $this->migrator->encrypt('general.site_name'); 764 | } 765 | ``` 766 | 767 | Or make an encrypted property non-encrypted: 768 | 769 | ```php 770 | public function up(): void 771 | { 772 | $this->migrator->addEncrypted('general.site_name', 'Spatie'); 773 | 774 | $this->migrator->decrypt('general.site_name'); 775 | } 776 | ``` 777 | 778 | Of course, you can use these methods when using `inGroup` migration operations. 779 | 780 | ### Custom encoders and decoders 781 | 782 | It is possible to define custom encoders and decoders instead of the built-in `json_encode` and `json_decode` ones by 783 | changing the package configuration like so: 784 | 785 | ```php 786 | ... 787 | 'encoder' => fn($value): string => str_rot13(json_encode($value)), 788 | 'decoder' => fn(string $payload, bool $associative) => json_decode(str_rot13($payload), $associative), 789 | ... 790 | ``` 791 | 792 | ### Faking settings classes 793 | 794 | In tests, it is sometimes desired that some settings classes can be quickly used with values different from the default ones you've written in your migrations. That's why you can fake settings. Faked settings classes will be registered in the container. And you can overwrite some or all the properties in the settings class: 795 | 796 | ```php 797 | DateSettings::fake([ 798 | 'birth_date' => new DateTime('16-05-1994') 799 | ]); 800 | ``` 801 | 802 | Now, when the `DateSettings` settings class is injected somewhere in your application, the `birth_date` property will be `DateTime('16-05-1994')`. 803 | 804 | If all properties are overwritten, no calls to repositories will be made. If only some properties are overwritten, the package will first add the overwritten properties and then load the missing settings from the repository. It is possible to explicitly throw an MissingSettings exception when a property is not overwritten in a fake method call like this: 805 | 806 | ```php 807 | DateSettings::fake([ 808 | 'birth_date' => new DateTime('16-05-1994') 809 | ], false); 810 | ``` 811 | 812 | ### Caching settings 813 | 814 | It takes a small amount of time to load a settings class from a repository. When you've got many settings classes, these added small amounts of time can grow quickly out of hand. The package has built-in support for caching stored settings using the Laravel cache. 815 | 816 | You should first enable the cache within the `settings.php` config file: 817 | 818 | ```php 819 | 'cache' => [ 820 | 'enabled' => env('SETTINGS_CACHE_ENABLED', false), 821 | 'store' => null, 822 | 'prefix' => null, 823 | ], 824 | ``` 825 | 826 | We suggest you enable caching in production by adding `SETTINGS_CACHE_ENABLED=true` to your `.env` file. It is also possible to define a store for the cache, which should be one of the stores you defined in the `cache.php` config file. If no store were defined, the default cache store would be taken. To avoid conflicts within the cache, you can also define a prefix that will be added to each cache entry. 827 | 828 | That's it. The package is now smart enough to cache the settings the first time they're loaded. Whenever the settings are edited, the package will refresh the settings. 829 | 830 | You can always clear the cached settings with the following command: 831 | 832 | ```bash 833 | php artisan settings:clear-cache 834 | ``` 835 | 836 | ### Auto discovering settings classes 837 | 838 | Each settings class you create should be added to the `settings` array within the `settings.php` config file. When you've got a lot of settings, this can be quickly forgotten. 839 | 840 | That's why it is also possible to auto-discover settings classes. The package will look through your application and tries to discover settings classes. You can specify the paths where will be searched in the config `auto_discover_settings` array. By default, this is the application's app path. 841 | 842 | Autodiscovering settings require some extra time before your application is booted up. That's why it is possible to cache them using the following command: 843 | 844 | ```bash 845 | php artisan settings:discover 846 | ``` 847 | 848 | You can clear this cache by running: 849 | 850 | ```bash 851 | php artisan settings:clear-discovered 852 | ``` 853 | 854 | ### Writing your own casters 855 | 856 | A caster is a class implementing the `SettingsCast` interface: 857 | 858 | ```php 859 | interface SettingsCast 860 | { 861 | /** 862 | * Will be used to when retrieving a value from the repository, and 863 | * inserting it into the settings class. 864 | */ 865 | public function get($payload); 866 | 867 | /** 868 | * Will be used to when retrieving a value from the settings class, and 869 | * inserting it into the repository. 870 | */ 871 | public function set($payload); 872 | } 873 | ``` 874 | 875 | A created caster can be used for local and global casts, but there are slight differences between them. The package will always try to inject the type of property it is casting. This type is a class string and will be provided as a first argument when constructing the caster. When it cannot deduce the type, `null` will be used as the first argument. 876 | 877 | An example of such caster with a type injected is a simplified `DtoCast`: 878 | 879 | ```php 880 | class DtoCast implements SettingsCast 881 | { 882 | private string $type; 883 | 884 | public function __construct(?string $type) 885 | { 886 | $this->type = $type; 887 | } 888 | 889 | public function get($payload): Data 890 | { 891 | return $this->type::from($payload); 892 | } 893 | 894 | public function set($payload): array 895 | { 896 | return $payload->toArray(); 897 | } 898 | } 899 | ``` 900 | 901 | The above is a caster for the [spatie/laravel-data](https://github.com/spatie/laravel-data) package, within its constructor, the type will be a specific Data class, for example, `SongData::class`. In the `get` method, the caster will construct a `Data::class` with the repository properties. The caster receives a `Data::class` as payload in the `set` method and converts it to an array for safe storing in the repository. 902 | 903 | #### Local casts 904 | 905 | When using a local cast, there are a few different possibilities to deduce the type: 906 | 907 | ```php 908 | // By the type of property 909 | 910 | class CastSettings extends Settings 911 | { 912 | public DateTime $birth_date; 913 | 914 | public static function casts(): array 915 | { 916 | return [ 917 | 'birth_date' => DateTimeInterfaceCast::class 918 | ]; 919 | } 920 | 921 | ... 922 | } 923 | ``` 924 | 925 | ```php 926 | // By the docblock of a property 927 | 928 | class CastSettings extends Settings 929 | { 930 | /** @var \DateTime */ 931 | public $birth_date; 932 | 933 | public static function casts(): array 934 | { 935 | return [ 936 | 'birth_date' => DateTimeInterfaceCast::class 937 | ]; 938 | } 939 | 940 | ... 941 | } 942 | ``` 943 | 944 | 945 | ```php 946 | // By explicit definition 947 | 948 | class CastSettings extends Settings 949 | { 950 | public $birth_date; 951 | 952 | public static function casts(): array 953 | { 954 | return [ 955 | 'birth_date' => DateTimeInterfaceCast::class.':'.DateTime::class 956 | ]; 957 | } 958 | 959 | ... 960 | } 961 | ``` 962 | 963 | In that last case: by explicit definition, it is possible to provide extra arguments that will be passed to the constructor: 964 | 965 | ```php 966 | class CastSettings extends Settings 967 | { 968 | public $birth_date; 969 | 970 | public static function casts(): array 971 | { 972 | return [ 973 | 'birth_date' => DateTimeWthTimeZoneInterfaceCast::class.':'.DateTime::class.',Europe/Brussels' 974 | ]; 975 | } 976 | 977 | ... 978 | } 979 | ``` 980 | 981 | Although in this case, it might be more readable to construct the caster within the settings class: 982 | 983 | ```php 984 | class CastSettings extends Settings 985 | { 986 | public $birth_date; 987 | 988 | public static function casts(): array 989 | { 990 | return [ 991 | 'birth_date' => new DateTimeWthTimeZoneInterfaceCast(DateTime::class, 'Europe/Brussels') 992 | ]; 993 | } 994 | 995 | ... 996 | } 997 | ``` 998 | 999 | #### Global casts 1000 | 1001 | When using global casts, the package will again try to deduce the type of property it's casting. In this case, it can only use the property type or infer the type of the property's docblock. 1002 | 1003 | A global cast should be configured in the `settings.php` config file and always has a specific (set) of type(s) it works on. These types can be a particular class, a group of classes implementing an interface, or a group of classes extending from another class. 1004 | 1005 | A good example here is the `DateTimeInterfaceCast` we've added by default in the config. It is defined in the config as such: 1006 | 1007 | ```php 1008 | ... 1009 | 1010 | 'global_casts' => [ 1011 | DateTimeInterface::class => Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast::class, 1012 | ], 1013 | 1014 | ... 1015 | ``` 1016 | 1017 | Whenever the package detects a `Carbon`, `CarbonImmutable`, `DateTime`, or `DateTimeImmutable` type as the type of one of a settings class's properties. It will use the `DateTimeInterfaceCast` as a caster. This because `Carbon`, `CarbonImmutable`, `DateTime` and `DateTimeImmutable` all implement `DateTimeInterface`. The key that was used in `settings.php` to represent the cast. 1018 | 1019 | The type injected in the caster will be the type of the property. So let's say you have a property with the type `DateTime` within your settings class. When casting this property, the `DateTimeInterfaceCast` will receive `DateTime:class` as a type. 1020 | 1021 | 1022 | ### Repositories 1023 | 1024 | There are two types of repositories included in the package, the `redis` and `database` repository. You can create multiple repositories for one type in the `setting.php` config file. And each repository can be configured. 1025 | 1026 | #### Database repository 1027 | 1028 | The database repository has two optional configuration options: 1029 | 1030 | - `model` the Eloquent model used to load/save properties to the database 1031 | - `table` the table used in the database 1032 | - `connection` the connection to use when interacting with the database 1033 | 1034 | #### Redis repository 1035 | 1036 | The Redis repository also has two optional configuration options: 1037 | 1038 | - `prefix` an optional prefix that will be prepended to the keys 1039 | - `connection` the connection to use when interacting with Redis 1040 | 1041 | #### Caching 1042 | 1043 | It is possible to add a custom caching configuration per repository, by adding a cache configuration like the default one to your repository config within the `settings.php` config file: 1044 | 1045 | ```php 1046 | 'repositories' => [ 1047 | 'landlord' => [ 1048 | 'type' => Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository::class, 1049 | 'model' => null, 1050 | 'table' => null, 1051 | 'connection' => 'landlord', 1052 | 'cache' => [ 1053 | 'enabled' => env('SETTINGS_CACHE_ENABLED', false), 1054 | 'store' => null, 1055 | 'prefix' => 'landlord', 1056 | 'ttl' => null, 1057 | ], 1058 | ], 1059 | 1060 | ... 1061 | ], 1062 | ``` 1063 | 1064 | #### Creating your own repository type 1065 | 1066 | It is possible to create your own types of repositories. A repository is a class which implements `SettingsRepository`: 1067 | 1068 | ```php 1069 | interface SettingsRepository 1070 | { 1071 | /** 1072 | * Get all the properties in the repository for a single group 1073 | */ 1074 | public function getPropertiesInGroup(string $group): array; 1075 | 1076 | /** 1077 | * Check if a property exists in a group 1078 | */ 1079 | public function checkIfPropertyExists(string $group, string $name): bool; 1080 | 1081 | /** 1082 | * Get the payload of a property 1083 | */ 1084 | public function getPropertyPayload(string $group, string $name); 1085 | 1086 | /** 1087 | * Create a property within a group with a payload 1088 | */ 1089 | public function createProperty(string $group, string $name, $payload): void; 1090 | 1091 | /** 1092 | * Update the payloads of properties within a group. 1093 | */ 1094 | public function updatePropertiesPayload(string $group, array $properties): void; 1095 | 1096 | /** 1097 | * Delete a property from a group 1098 | */ 1099 | public function deleteProperty(string $group, string $name): void; 1100 | 1101 | /** 1102 | * Lock a set of properties for a specific group 1103 | */ 1104 | public function lockProperties(string $group, array $properties): void; 1105 | 1106 | /** 1107 | * Unlock a set of properties for a group 1108 | */ 1109 | public function unlockProperties(string $group, array $properties): void; 1110 | 1111 | /** 1112 | * Get all the locked properties within a group 1113 | */ 1114 | public function getLockedProperties(string $group): array; 1115 | } 1116 | ``` 1117 | 1118 | All these functions should be implemented to interact with the type of storage you're using. The `payload` parameters are raw values(`int`, `bool`, `float`, `string`, `array`). Within the `database`, and `redis` repository types, These raw values are converted to JSON. But this is not required. 1119 | 1120 | It is required to return raw values again in the `getPropertiesInGroup` and `getPropertyPayload` methods. 1121 | 1122 | Each repository's constructor will receive a `$config` array that the user-defined for the repository within the application `settings.php` config file. It is possible to add other dependencies to the constructor. They will be injected when the repository is created. 1123 | 1124 | #### Refreshing settings 1125 | 1126 | You can refresh the values and locked properties within the settings class. This can be useful if you change something within your repository and want to see it reflected within your settings: 1127 | 1128 | ```php 1129 | $settings->refresh(); 1130 | ``` 1131 | 1132 | You should only refresh settings when the repository values were changed when the settings class was already loaded. 1133 | 1134 | ### Events 1135 | 1136 | The package will emit a series of events when loading/saving settings classes: 1137 | 1138 | - `LoadingSettings` whenever settings are loaded from the repository but not yet inserted in the settings class 1139 | - `SettingsLoaded` after settings are loaded into the settings class 1140 | - `SavingSettings` whenever settings are saved to the repository but are not yet cast or encrypted 1141 | - `SettingsSaved` after settings are stored within the repository 1142 | 1143 | ## Testing 1144 | 1145 | ``` bash 1146 | composer test 1147 | ``` 1148 | 1149 | ## Changelog 1150 | 1151 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 1152 | 1153 | ## Contributing 1154 | 1155 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 1156 | 1157 | ## Security 1158 | 1159 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 1160 | 1161 | ## Credits 1162 | 1163 | - [Ruben Van Assche](https://github.com/rubenvanassche) 1164 | - [All Contributors](../../contributors) 1165 | 1166 | ## License 1167 | 1168 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 1169 | 1170 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | Because there are many breaking changes an upgrade is not that easy. There are many edge cases this guide does not cover. We accept PRs to improve this guide. 4 | 5 | ## From v2 to v3 6 | 7 | This should be a quick update: 8 | 9 | - When creating a new project, the default search location for settings classes will be in the `app_path('Settings')` directory. If you want to keep the old location, then you can set the `auto_discover_settings` option to `app_path()`. For applications which already have published their config, nothing changes. 10 | - If you're implementing custom repositories, then update them according to the interface. The method `updatePropertyPayload` is renamed to `updatePropertiesPayload` and should now update multiple properties at once. 11 | - Add a new migration with the following content 12 | 13 | ```php 14 | boolean('locked')->default(false)->change(); 29 | 30 | $table->unique(['group', 'name']); 31 | 32 | $table->dropIndex(['group']); 33 | }); 34 | } 35 | 36 | /** 37 | * Reverse the migrations. 38 | */ 39 | public function down(): void 40 | { 41 | Schema::table('settings', function (Blueprint $table): void { 42 | $table->boolean('locked')->default(null)->change(); 43 | 44 | $table->dropUnique(['group', 'name']); 45 | 46 | $table->index('group'); 47 | }); 48 | } 49 | }; 50 | ``` 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "spatie/laravel-settings", 3 | "description" : "Store your application settings", 4 | "keywords" : [ 5 | "spatie", 6 | "laravel-settings" 7 | ], 8 | "homepage" : "https://github.com/spatie/laravel-settings", 9 | "license" : "MIT", 10 | "authors" : [ 11 | { 12 | "name" : "Ruben Van Assche", 13 | "email" : "ruben@spatie.be", 14 | "homepage" : "https://spatie.be", 15 | "role" : "Developer" 16 | } 17 | ], 18 | "require" : { 19 | "php" : "^8.2", 20 | "ext-json" : "*", 21 | "illuminate/database" : "^11.0|^12.0", 22 | "phpdocumentor/type-resolver" : "^1.5", 23 | "spatie/temporary-directory" : "^1.3|^2.0" 24 | }, 25 | "require-dev" : { 26 | "ext-redis": "*", 27 | "mockery/mockery": "^1.4", 28 | "orchestra/testbench": "^9.0|^10.0", 29 | "pestphp/pest": "^2.0|^3.0", 30 | "pestphp/pest-plugin-laravel": "^2.0|^3.0", 31 | "phpstan/extension-installer": "^1.1", 32 | "phpstan/phpstan-deprecation-rules": "^1.0", 33 | "phpstan/phpstan-phpunit": "^1.0", 34 | "spatie/laravel-data": "^2.0.0|^4.0.0", 35 | "spatie/pest-plugin-snapshots": "^2.0", 36 | "spatie/phpunit-snapshot-assertions": "^4.2|^5.0", 37 | "spatie/ray": "^1.36" 38 | }, 39 | "suggest" : { 40 | "spatie/data-transfer-object" : "Allows for DTO casting to settings. (deprecated)" 41 | }, 42 | "autoload" : { 43 | "psr-4" : { 44 | "Spatie\\LaravelSettings\\" : "src" 45 | } 46 | }, 47 | "autoload-dev" : { 48 | "psr-4" : { 49 | "Spatie\\LaravelSettings\\Tests\\" : "tests" 50 | } 51 | }, 52 | "scripts" : { 53 | "analyse" : "vendor/bin/phpstan analyse", 54 | "test" : "vendor/bin/pest", 55 | "test-coverage" : "vendor/bin/pest --coverage" 56 | }, 57 | "config" : { 58 | "sort-packages" : true, 59 | "allow-plugins" : { 60 | "pestphp/pest-plugin" : true, 61 | "phpstan/extension-installer" : true 62 | } 63 | }, 64 | "extra" : { 65 | "laravel" : { 66 | "providers" : [ 67 | "Spatie\\LaravelSettings\\LaravelSettingsServiceProvider" 68 | ] 69 | } 70 | }, 71 | "minimum-stability" : "dev", 72 | "prefer-stable" : true 73 | } 74 | -------------------------------------------------------------------------------- /config/settings.php: -------------------------------------------------------------------------------- 1 | [ 10 | 11 | ], 12 | 13 | /* 14 | * The path where the settings classes will be created. 15 | */ 16 | 'setting_class_path' => app_path('Settings'), 17 | 18 | /* 19 | * In these directories settings migrations will be stored and ran when migrating. A settings 20 | * migration created via the make:settings-migration command will be stored in the first path or 21 | * a custom defined path when running the command. 22 | */ 23 | 'migrations_paths' => [ 24 | database_path('settings'), 25 | ], 26 | 27 | /* 28 | * When no repository was set for a settings class the following repository 29 | * will be used for loading and saving settings. 30 | */ 31 | 'default_repository' => 'database', 32 | 33 | /* 34 | * Settings will be stored and loaded from these repositories. 35 | */ 36 | 'repositories' => [ 37 | 'database' => [ 38 | 'type' => Spatie\LaravelSettings\SettingsRepositories\DatabaseSettingsRepository::class, 39 | 'model' => null, 40 | 'table' => null, 41 | 'connection' => null, 42 | ], 43 | 'redis' => [ 44 | 'type' => Spatie\LaravelSettings\SettingsRepositories\RedisSettingsRepository::class, 45 | 'connection' => null, 46 | 'prefix' => null, 47 | ], 48 | ], 49 | 50 | /* 51 | * The encoder and decoder will determine how settings are stored and 52 | * retrieved in the database. By default, `json_encode` and `json_decode` 53 | * are used. 54 | */ 55 | 'encoder' => null, 56 | 'decoder' => null, 57 | 58 | /* 59 | * The contents of settings classes can be cached through your application, 60 | * settings will be stored within a provided Laravel store and can have an 61 | * additional prefix. 62 | */ 63 | 'cache' => [ 64 | 'enabled' => env('SETTINGS_CACHE_ENABLED', false), 65 | 'store' => null, 66 | 'prefix' => null, 67 | 'ttl' => null, 68 | ], 69 | 70 | /* 71 | * These global casts will be automatically used whenever a property within 72 | * your settings class isn't a default PHP type. 73 | */ 74 | 'global_casts' => [ 75 | DateTimeInterface::class => Spatie\LaravelSettings\SettingsCasts\DateTimeInterfaceCast::class, 76 | DateTimeZone::class => Spatie\LaravelSettings\SettingsCasts\DateTimeZoneCast::class, 77 | // Spatie\DataTransferObject\DataTransferObject::class => Spatie\LaravelSettings\SettingsCasts\DtoCast::class, 78 | Spatie\LaravelData\Data::class => Spatie\LaravelSettings\SettingsCasts\DataCast::class, 79 | ], 80 | 81 | /* 82 | * The package will look for settings in these paths and automatically 83 | * register them. 84 | */ 85 | 'auto_discover_settings' => [ 86 | app_path('Settings'), 87 | ], 88 | 89 | /* 90 | * Automatically discovered settings classes can be cached, so they don't 91 | * need to be searched each time the application boots up. 92 | */ 93 | 'discovered_settings_cache_path' => base_path('bootstrap/cache'), 94 | ]; 95 | -------------------------------------------------------------------------------- /database/migrations/create_settings_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->string('group'); 15 | $table->string('name'); 16 | $table->boolean('locked')->default(false); 17 | $table->json('payload'); 18 | 19 | $table->timestamps(); 20 | 21 | $table->unique(['group', 'name']); 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/Console/CacheDiscoveredSettingsCommand.php: -------------------------------------------------------------------------------- 1 | info('Caching registered settings...'); 19 | 20 | $container 21 | ->clearCache() 22 | ->getSettingClasses() 23 | ->pipe(function (Collection $settingClasses) use ($files) { 24 | $cachePath = config('settings.discovered_settings_cache_path'); 25 | 26 | $files->makeDirectory($cachePath, 0755, true, true); 27 | 28 | $files->put( 29 | $cachePath . '/settings.php', 30 | 'toArray(), true) . ';' 31 | ); 32 | }); 33 | 34 | $this->info('All done!'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Console/ClearCachedSettingsCommand.php: -------------------------------------------------------------------------------- 1 | all() as $settingsCache) { 17 | $settingsCache->clear(); 18 | } 19 | 20 | $this->info('Cached settings cleared!'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Console/ClearDiscoveredSettingsCacheCommand.php: -------------------------------------------------------------------------------- 1 | delete(config('settings.discovered_settings_cache_path') . '/settings.php'); 17 | 18 | $this->info('Cached discovered settings cleared!'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Console/MakeSettingCommand.php: -------------------------------------------------------------------------------- 1 | files = $files; 43 | } 44 | 45 | public function handle() 46 | { 47 | $name = trim($this->input->getArgument('name')); 48 | $group = trim($this->input->getOption('group')); 49 | $path = trim($this->input->getOption('path')); 50 | 51 | if (empty($path)) { 52 | $path = $this->resolveSettingsPath(); 53 | } 54 | 55 | $this->ensureSettingClassDoesntAlreadyExist($name, $path); 56 | 57 | $this->files->ensureDirectoryExists($path); 58 | 59 | $this->files->put( 60 | $this->getPath($name, $path), 61 | $this->getContent($name, $group, $path) 62 | ); 63 | } 64 | 65 | protected function getStub(): string 66 | { 67 | return <<getNamespace($path), $name, $group], 90 | $this->getStub() 91 | ); 92 | } 93 | 94 | protected function ensureSettingClassDoesntAlreadyExist($name, $path): void 95 | { 96 | if ($this->files->exists($this->getPath($name, $path))) { 97 | throw new InvalidArgumentException(sprintf('%s already exists!', $name)); 98 | } 99 | } 100 | 101 | protected function resolveSettingsPath(): string 102 | { 103 | return config('settings.setting_class_path', app_path('Settings')); 104 | } 105 | 106 | protected function getPath($name, $path): string 107 | { 108 | return $path . '/' . $name . '.php'; 109 | } 110 | 111 | protected function getNamespace($path): string 112 | { 113 | $path = preg_replace( 114 | [ 115 | '/^(' . preg_quote(base_path(), '/') . ')/', 116 | '/\//', 117 | ], 118 | [ 119 | '', 120 | '\\', 121 | ], 122 | $path 123 | ); 124 | 125 | $namespace = implode('\\', array_map(fn ($directory) => ucfirst($directory), explode('\\', $path))); 126 | 127 | // Remove leading backslash if present 128 | if (substr($namespace, 0, 1) === '\\') { 129 | $namespace = substr($namespace, 1); 130 | } 131 | 132 | return $namespace; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Console/MakeSettingsMigrationCommand.php: -------------------------------------------------------------------------------- 1 | files = $files; 24 | } 25 | 26 | public function handle(): void 27 | { 28 | $name = trim($this->input->getArgument('name')); 29 | $path = trim($this->input->getArgument('path')); 30 | 31 | // If path is still empty we get the first path from new settings.migrations_paths config 32 | if (empty($path)) { 33 | $path = $this->resolveMigrationPaths()[0]; 34 | } 35 | 36 | $this->ensureMigrationDoesntAlreadyExist($name, $path); 37 | 38 | $this->files->ensureDirectoryExists($path); 39 | 40 | $this->files->put( 41 | $file = $this->getPath($name, $path), 42 | $this->getStub() 43 | ); 44 | 45 | $this->info(sprintf('Setting migration [%s] created successfully.', $file)); 46 | } 47 | 48 | protected function getStub(): string 49 | { 50 | return <<files->glob($migrationPath . '/*.php'); 70 | 71 | foreach ($migrationFiles as $migrationFile) { 72 | $this->files->requireOnce($migrationFile); 73 | } 74 | } 75 | 76 | if (class_exists($className = Str::studly($name))) { 77 | throw new InvalidArgumentException("A {$className} class already exists."); 78 | } 79 | } 80 | 81 | protected function getPath($name, $path): string 82 | { 83 | return $path . '/' . Carbon::now()->format('Y_m_d_His') . '_' . Str::snake($name) . '.php'; 84 | } 85 | 86 | protected function resolveMigrationPaths(): array 87 | { 88 | return ! empty(config('settings.migrations_path')) 89 | ? [config('settings.migrations_path')] 90 | : config('settings.migrations_paths'); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Events/LoadingSettings.php: -------------------------------------------------------------------------------- 1 | settingsClass = $settingsClass; 16 | $this->properties = $properties; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Events/SavingSettings.php: -------------------------------------------------------------------------------- 1 | properties = $properties; 22 | 23 | $this->originalValues = $originalValues; 24 | 25 | $this->settings = $settings; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Events/SettingsLoaded.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Events/SettingsSaved.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/CouldNotResolveDocblockType.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass()->getName()}::{$reflectionProperty->getName()}`"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/CouldNotUnserializeSettings.php: -------------------------------------------------------------------------------- 1 | getName(); 27 | 28 | $reflectedType = PropertyReflector::resolveType($reflectionProperty); 29 | 30 | if (array_key_exists($name, $localCasts)) { 31 | return self::createLocalCast($localCasts[$name], $reflectedType); 32 | } 33 | 34 | if ($reflectedType === null) { 35 | return null; 36 | } 37 | 38 | return self::createDefaultCast($reflectedType); 39 | } 40 | 41 | /** 42 | * @param string|SettingsCast $castDefinition 43 | * @param \phpDocumentor\Reflection\Type|null $type 44 | * 45 | * @return \Spatie\LaravelSettings\SettingsCasts\SettingsCast 46 | */ 47 | protected static function createLocalCast( 48 | $castDefinition, 49 | ?Type $type 50 | ): SettingsCast { 51 | if ($castDefinition instanceof SettingsCast) { 52 | return $castDefinition; 53 | } 54 | 55 | $castClass = Str::before($castDefinition, ':'); 56 | 57 | $arguments = Str::contains($castDefinition, ':') 58 | ? explode(',', Str::after($castDefinition, ':')) 59 | : []; 60 | 61 | $reflectedType = self::getLocalCastReflectedType($type); 62 | 63 | if ($reflectedType) { 64 | array_push($arguments, $reflectedType); 65 | } 66 | 67 | return new $castClass(...$arguments); 68 | } 69 | 70 | protected static function createDefaultCast( 71 | Type $type 72 | ): ?SettingsCast { 73 | $noCastRequired = self::isTypeWithNoCastRequired($type) 74 | || ($type instanceof AbstractList && self::isTypeWithNoCastRequired($type->getValueType())) 75 | || ($type instanceof Nullable && self::isTypeWithNoCastRequired($type->getActualType())); 76 | 77 | if ($noCastRequired) { 78 | return null; 79 | } 80 | 81 | if ($type instanceof AbstractList) { 82 | return new ArraySettingsCast(self::createDefaultCast($type->getValueType())); 83 | } 84 | 85 | if ($type instanceof Nullable) { 86 | return self::createDefaultCast($type->getActualType()); 87 | } 88 | 89 | if (! $type instanceof Object_) { 90 | return null; 91 | } 92 | 93 | $className = self::getObjectClassName($type); 94 | 95 | if (enum_exists($className)) { 96 | return new EnumCast($className); 97 | } 98 | 99 | foreach (config('settings.global_casts', []) as $base => $cast) { 100 | if (self::shouldCast($className, $base)) { 101 | return new $cast($className); 102 | } 103 | } 104 | 105 | return null; 106 | } 107 | 108 | protected static function isTypeWithNoCastRequired(Type $type): bool 109 | { 110 | return $type instanceof Integer 111 | || $type instanceof Boolean 112 | || $type instanceof Float_ 113 | || $type instanceof String_; 114 | } 115 | 116 | protected static function shouldCast(string $type, string $base): bool 117 | { 118 | return $type === $base 119 | || in_array($type, class_implements($base)) 120 | || is_subclass_of($type, $base); 121 | } 122 | 123 | protected static function getLocalCastReflectedType(?Type $type): ?string 124 | { 125 | if ($type instanceof Object_) { 126 | return self::getObjectClassName($type); 127 | } 128 | 129 | if ($type instanceof Nullable) { 130 | return self::getLocalCastReflectedType($type->getActualType()); 131 | } 132 | 133 | return null; 134 | } 135 | 136 | protected static function getObjectClassName(Object_ $type): string 137 | { 138 | return ltrim((string ) $type->getFqsen(), '\\'); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Factories/SettingsRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | $config, 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/LaravelSettingsServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 25 | $this->publishes([ 26 | __DIR__ . '/../config/settings.php' => config_path('settings.php'), 27 | ], 'config'); 28 | 29 | if (! class_exists('CreateSettingsTable')) { 30 | $this->publishes([ 31 | __DIR__ . '/../database/migrations/create_settings_table.php.stub' => database_path('migrations/2022_12_14_083707_create_settings_table.php'), 32 | ], 'migrations'); 33 | } 34 | 35 | $this->commands([ 36 | MakeSettingCommand::class, 37 | MakeSettingsMigrationCommand::class, 38 | CacheDiscoveredSettingsCommand::class, 39 | ClearDiscoveredSettingsCacheCommand::class, 40 | ClearCachedSettingsCommand::class, 41 | ]); 42 | } 43 | 44 | Event::subscribe(SettingsEventSubscriber::class); 45 | Event::listen(SchemaLoaded::class, fn ($event) => $this->removeMigrationsWhenSchemaLoaded($event)); 46 | 47 | $this->loadMigrationsFrom($this->resolveMigrationPaths()); 48 | } 49 | 50 | public function register(): void 51 | { 52 | $this->mergeConfigFrom(__DIR__ . '/../config/settings.php', 'settings'); 53 | 54 | $this->app->bind(SettingsRepository::class, fn () => SettingsRepositoryFactory::create()); 55 | 56 | $this->app->bind(SettingsCacheFactory::class, fn () => new SettingsCacheFactory( 57 | config('settings'), 58 | )); 59 | 60 | $this->app->scoped(SettingsMapper::class); 61 | 62 | $settingsContainer = app(SettingsContainer::class); 63 | $settingsContainer->registerBindings(); 64 | } 65 | 66 | private function removeMigrationsWhenSchemaLoaded(SchemaLoaded $event) 67 | { 68 | $files = Finder::create() 69 | ->files() 70 | ->ignoreDotFiles(true) 71 | ->in($this->resolveMigrationPaths()) 72 | ->depth(0); 73 | 74 | $migrations = collect(iterator_to_array($files)) 75 | ->map(function (SplFileInfo $file) { 76 | $contents = file_get_contents($file->getRealPath()); 77 | 78 | if ( 79 | str_contains($contents, 'return new class extends '.SettingsMigration::class) 80 | || str_contains($contents, 'return new class extends SettingsMigration') 81 | || str_contains($contents, 'return new class() extends '.SettingsMigration::class) 82 | || str_contains($contents, 'return new class() extends SettingsMigration') 83 | ) { 84 | return $file->getBasename('.php'); 85 | } 86 | 87 | preg_match('/class\s*(?P\w*)\s*extends/', $contents, $found); 88 | 89 | if (empty($found['className'])) { 90 | return null; 91 | } 92 | 93 | require_once $file->getRealPath(); 94 | 95 | if (! is_subclass_of($found['className'], SettingsMigration::class)) { 96 | return null; 97 | } 98 | 99 | return $file->getBasename('.php'); 100 | }) 101 | ->filter() 102 | ->values(); 103 | 104 | $migrationsConfig = config()->get('database.migrations'); 105 | 106 | $migrationsTable = is_array($migrationsConfig) ? ($migrationsConfig['table'] ?? null) : $migrationsConfig; 107 | 108 | $event->connection 109 | ->table($migrationsTable) 110 | ->useWritePdo() 111 | ->whereIn('migration', $migrations) 112 | ->delete(); 113 | } 114 | 115 | protected function resolveMigrationPaths(): array 116 | { 117 | return ! empty(config('settings.migrations_path')) 118 | ? [config('settings.migrations_path')] 119 | : config('settings.migrations_paths'); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Migrations/SettingsBlueprint.php: -------------------------------------------------------------------------------- 1 | group = $group; 16 | 17 | $this->migrator = $migrator; 18 | } 19 | 20 | public function rename(string $from, string $to): void 21 | { 22 | $this->migrator->rename( 23 | $this->prependWithGroup($from), 24 | $this->prependWithGroup($to) 25 | ); 26 | } 27 | 28 | public function add(string $name, $value = null, bool $encrypted = false): void 29 | { 30 | $this->migrator->add($this->prependWithGroup($name), $value, $encrypted); 31 | } 32 | 33 | public function delete(string $name): void 34 | { 35 | $this->migrator->delete($this->prependWithGroup($name)); 36 | } 37 | 38 | public function update(string $name, Closure $closure, bool $encrypted = false): void 39 | { 40 | $this->migrator->update($this->prependWithGroup($name), $closure, $encrypted); 41 | } 42 | 43 | public function addEncrypted(string $name, $value = null): void 44 | { 45 | $this->migrator->addEncrypted($this->prependWithGroup($name), $value); 46 | } 47 | 48 | public function updateEncrypted(string $name, Closure $closure): void 49 | { 50 | $this->migrator->updateEncrypted($this->prependWithGroup($name), $closure); 51 | } 52 | 53 | public function encrypt(string $name): void 54 | { 55 | $this->migrator->encrypt($this->prependWithGroup($name)); 56 | } 57 | 58 | public function decrypt(string $name): void 59 | { 60 | $this->migrator->decrypt($this->prependWithGroup($name)); 61 | } 62 | 63 | protected function prependWithGroup(string $name): string 64 | { 65 | return "{$this->group}.{$name}"; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Migrations/SettingsMigration.php: -------------------------------------------------------------------------------- 1 | migrator = app(SettingsMigrator::class); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Migrations/SettingsMigrator.php: -------------------------------------------------------------------------------- 1 | repository = $connection; 24 | } 25 | 26 | public function repository(string $name): self 27 | { 28 | $this->repository = SettingsRepositoryFactory::create($name); 29 | 30 | return $this; 31 | } 32 | 33 | public function rename(string $from, string $to): void 34 | { 35 | if (! $this->checkIfPropertyExists($from)) { 36 | throw SettingDoesNotExist::whenRenaming($from, $to); 37 | } 38 | 39 | if ($this->checkIfPropertyExists($to)) { 40 | throw SettingAlreadyExists::whenRenaming($from, $to); 41 | } 42 | 43 | $this->createProperty( 44 | $to, 45 | $this->getPropertyPayload($from) 46 | ); 47 | 48 | $this->deleteProperty($from); 49 | } 50 | 51 | public function add(string $property, $value = null, bool $encrypted = false): void 52 | { 53 | if ($this->checkIfPropertyExists($property)) { 54 | throw SettingAlreadyExists::whenAdding($property); 55 | } 56 | 57 | if ($encrypted) { 58 | $value = Crypto::encrypt($value); 59 | } 60 | 61 | $this->createProperty($property, $value); 62 | } 63 | 64 | public function delete(string $property): void 65 | { 66 | if (! $this->checkIfPropertyExists($property)) { 67 | throw SettingDoesNotExist::whenDeleting($property); 68 | } 69 | 70 | $this->deleteProperty($property); 71 | } 72 | 73 | public function deleteIfExists(string $property): void 74 | { 75 | if ($this->checkIfPropertyExists($property)) { 76 | $this->deleteProperty($property); 77 | } 78 | } 79 | 80 | public function update(string $property, Closure $closure, bool $encrypted = false): void 81 | { 82 | if (! $this->checkIfPropertyExists($property)) { 83 | throw SettingDoesNotExist::whenEditing($property); 84 | } 85 | 86 | $originalPayload = $encrypted 87 | ? Crypto::decrypt($this->getPropertyPayload($property)) 88 | : $this->getPropertyPayload($property); 89 | 90 | $updatedPayload = $encrypted 91 | ? Crypto::encrypt($closure($originalPayload)) 92 | : $closure($originalPayload); 93 | 94 | $this->updatePropertyPayload($property, $updatedPayload); 95 | } 96 | 97 | public function addEncrypted(string $property, $value = null): void 98 | { 99 | $this->add($property, $value, true); 100 | } 101 | 102 | public function updateEncrypted(string $property, Closure $closure): void 103 | { 104 | $this->update($property, $closure, true); 105 | } 106 | 107 | public function encrypt(string $property): void 108 | { 109 | $this->update($property, fn ($payload) => Crypto::encrypt($payload)); 110 | } 111 | 112 | public function decrypt(string $property): void 113 | { 114 | $this->update($property, fn ($payload) => Crypto::decrypt($payload)); 115 | } 116 | 117 | public function exists(string $property): bool 118 | { 119 | return $this->checkIfPropertyExists($property); 120 | } 121 | 122 | public function inGroup(string $group, Closure $closure): void 123 | { 124 | $closure(new SettingsBlueprint($group, $this)); 125 | } 126 | 127 | protected function getPropertyParts(string $property): array 128 | { 129 | $propertyParts = explode('.', $property); 130 | 131 | if (count($propertyParts) !== 2) { 132 | throw InvalidSettingName::create($property); 133 | } 134 | 135 | return ['group' => $propertyParts[0], 'name' => $propertyParts[1]]; 136 | } 137 | 138 | protected function checkIfPropertyExists(string $property): bool 139 | { 140 | ['group' => $group, 'name' => $name] = $this->getPropertyParts($property); 141 | 142 | return $this->repository->checkIfPropertyExists($group, $name); 143 | } 144 | 145 | protected function getPropertyPayload(string $property) 146 | { 147 | ['group' => $group, 'name' => $name] = $this->getPropertyParts($property); 148 | 149 | $payload = $this->repository->getPropertyPayload($group, $name); 150 | 151 | return $this->getCast($group, $name)?->get($payload) ?: $payload; 152 | } 153 | 154 | protected function createProperty(string $property, $payload): void 155 | { 156 | ['group' => $group, 'name' => $name] = $this->getPropertyParts($property); 157 | 158 | if (is_object($payload)) { 159 | $payload = $this->getCast($group, $name)?->set($payload) ?: $payload; 160 | } 161 | 162 | $this->repository->createProperty($group, $name, $payload); 163 | } 164 | 165 | protected function updatePropertyPayload(string $property, $payload): void 166 | { 167 | ['group' => $group, 'name' => $name] = $this->getPropertyParts($property); 168 | 169 | if (is_object($payload)) { 170 | $payload = $this->getCast($group, $name)?->set($payload) ?: $payload; 171 | } 172 | 173 | $this->repository->updatePropertiesPayload($group, [$name => $payload]); 174 | } 175 | 176 | protected function deleteProperty(string $property): void 177 | { 178 | ['group' => $group, 'name' => $name] = $this->getPropertyParts($property); 179 | 180 | $this->repository->deleteProperty($group, $name); 181 | } 182 | 183 | protected function getCast(string $group, string $name): ?SettingsCast 184 | { 185 | return $this->settingsGroups()->get($group)?->getCast($name); 186 | } 187 | 188 | protected function settingsGroups(): Collection 189 | { 190 | return app(SettingsContainer::class) 191 | ->getSettingClasses() 192 | ->mapWithKeys(fn (string $settingsClass) => [ 193 | $settingsClass::group() => new SettingsConfig($settingsClass), 194 | ]); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Models/SettingsProperty.php: -------------------------------------------------------------------------------- 1 | 'boolean', 15 | ]; 16 | 17 | public static function get(string $property) 18 | { 19 | [$group, $name] = explode('.', $property); 20 | 21 | $setting = self::query() 22 | ->where('group', $group) 23 | ->where('name', $name) 24 | ->first('payload'); 25 | 26 | return json_decode($setting->getAttribute('payload')); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Settings.php: -------------------------------------------------------------------------------- 1 | initialize(static::class) 58 | ->getReflectedProperties() 59 | ->keys() 60 | ->reject(fn (string $name) => array_key_exists($name, $values)); 61 | 62 | if ($propertiesToLoad->isEmpty()) { 63 | return app(Container::class)->instance(static::class, new static( 64 | $values 65 | )); 66 | } 67 | 68 | if ($propertiesToLoad->isNotEmpty() && $loadMissingValues === false) { 69 | throw MissingSettings::create(static::class, $propertiesToLoad->toArray(), 'loading fake'); 70 | } 71 | 72 | $mergedValues = $settingsMapper 73 | ->fetchProperties(static::class, $propertiesToLoad) 74 | ->merge($values) 75 | ->all(); 76 | 77 | return app(Container::class)->instance(static::class, new static( 78 | $mergedValues 79 | )); 80 | } 81 | 82 | final public function __construct(array $values = []) 83 | { 84 | $this->ensureConfigIsLoaded(); 85 | 86 | foreach ($this->config->getReflectedProperties() as $name => $property) { 87 | if (method_exists($property, 'isReadOnly') && $property->isReadOnly()) { 88 | continue; 89 | } 90 | 91 | unset($this->{$name}); 92 | } 93 | 94 | if (! empty($values)) { 95 | $this->loadValues($values); 96 | } 97 | } 98 | 99 | public function __get($name) 100 | { 101 | $this->loadValues(); 102 | 103 | return $this->{$name}; 104 | } 105 | 106 | public function __set($name, $value) 107 | { 108 | $this->loadValues(); 109 | 110 | $this->{$name} = $value; 111 | } 112 | 113 | public function __debugInfo(): array 114 | { 115 | try { 116 | $this->loadValues(); 117 | 118 | return $this->toArray(); 119 | } catch (Exception $exception) { 120 | return [ 121 | 'Could not load values', 122 | ]; 123 | } 124 | } 125 | 126 | public function __isset($name) 127 | { 128 | $this->loadValues(); 129 | 130 | return isset($this->{$name}); 131 | } 132 | 133 | public function __serialize(): array 134 | { 135 | /** @var Collection $encrypted */ 136 | /** @var Collection $nonEncrypted */ 137 | [$encrypted, $nonEncrypted] = $this->toCollection()->partition( 138 | fn ($value, string $name) => $this->config->isEncrypted($name) 139 | ); 140 | 141 | return array_merge( 142 | $encrypted->map(fn ($value) => Crypto::encrypt($value))->all(), 143 | $nonEncrypted->all() 144 | ); 145 | } 146 | 147 | public function __unserialize(array $data): void 148 | { 149 | $this->loaded = false; 150 | 151 | $this->ensureConfigIsLoaded(); 152 | 153 | /** @var Collection $encrypted */ 154 | /** @var Collection $nonEncrypted */ 155 | [$encrypted, $nonEncrypted] = collect($data)->partition( 156 | fn ($value, string $name) => $this->config->isEncrypted($name) 157 | ); 158 | 159 | $data = array_merge( 160 | $encrypted->map(fn ($value) => Crypto::decrypt($value))->all(), 161 | $nonEncrypted->all() 162 | ); 163 | 164 | $this->loadValues($data); 165 | } 166 | 167 | /** 168 | * @param \Illuminate\Support\Collection|array $properties 169 | * 170 | * @return $this 171 | */ 172 | public function fill($properties): self 173 | { 174 | foreach ($properties as $name => $payload) { 175 | $this->{$name} = $payload; 176 | } 177 | 178 | return $this; 179 | } 180 | 181 | public function save(): self 182 | { 183 | $properties = $this->toCollection(); 184 | 185 | event(new SavingSettings($properties, $this->originalValues, $this)); 186 | 187 | $values = $this->mapper->save(static::class, $properties); 188 | 189 | $this->fill($values); 190 | $this->originalValues = $values; 191 | 192 | event(new SettingsSaved($this)); 193 | 194 | return $this; 195 | } 196 | 197 | public function lock(string ...$properties) 198 | { 199 | $this->ensureConfigIsLoaded(); 200 | 201 | $this->config->lock(...$properties); 202 | } 203 | 204 | public function unlock(string ...$properties) 205 | { 206 | $this->ensureConfigIsLoaded(); 207 | 208 | $this->config->unlock(...$properties); 209 | } 210 | 211 | public function isLocked(string $property): bool 212 | { 213 | return in_array($property, $this->getLockedProperties()); 214 | } 215 | 216 | public function isUnlocked(string $property): bool 217 | { 218 | return ! $this->isLocked($property); 219 | } 220 | 221 | public function getLockedProperties(): array 222 | { 223 | $this->ensureConfigIsLoaded(); 224 | 225 | return $this->config->getLocked()->toArray(); 226 | } 227 | 228 | public function toCollection(): Collection 229 | { 230 | $this->ensureConfigIsLoaded(); 231 | 232 | return $this->config 233 | ->getReflectedProperties() 234 | ->mapWithKeys(fn (ReflectionProperty $property) => [ 235 | $property->getName() => $this->{$property->getName()}, 236 | ]); 237 | } 238 | 239 | public function toArray(): array 240 | { 241 | return $this->toCollection()->toArray(); 242 | } 243 | 244 | public function toJson($options = 0): string 245 | { 246 | return json_encode($this->toArray(), $options); 247 | } 248 | 249 | public function toResponse($request) 250 | { 251 | return response()->json($this->toJson()); 252 | } 253 | 254 | public function getRepository(): SettingsRepository 255 | { 256 | $this->ensureConfigIsLoaded(); 257 | 258 | return $this->config->getRepository(); 259 | } 260 | 261 | public function refresh(): self 262 | { 263 | $this->config->clearCachedLockedProperties(); 264 | 265 | $this->loaded = false; 266 | $this->loadValues(); 267 | 268 | return $this; 269 | } 270 | 271 | private function loadValues(?array $values = null): self 272 | { 273 | if ($this->loaded) { 274 | return $this; 275 | } 276 | 277 | $values ??= $this->mapper->load(static::class); 278 | 279 | $this->loaded = true; 280 | 281 | $this->fill($values); 282 | $this->originalValues = collect($values); 283 | 284 | event(new SettingsLoaded($this)); 285 | 286 | return $this; 287 | } 288 | 289 | private function ensureConfigIsLoaded(): self 290 | { 291 | if ($this->configInitialized) { 292 | return $this; 293 | } 294 | 295 | $this->mapper = app(SettingsMapper::class); 296 | $this->config = $this->mapper->initialize(static::class); 297 | $this->configInitialized = true; 298 | 299 | return $this; 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/SettingsCache.php: -------------------------------------------------------------------------------- 1 | enabled = $enabled; 28 | $this->store = $store; 29 | $this->prefix = $prefix; 30 | $this->ttl = $ttl; 31 | } 32 | 33 | public function isEnabled(): bool 34 | { 35 | return $this->enabled; 36 | } 37 | 38 | public function has(string $settingsClass): bool 39 | { 40 | if ($this->enabled === false) { 41 | return false; 42 | } 43 | 44 | return Cache::store($this->store)->has($this->resolveCacheKey($settingsClass)); 45 | } 46 | 47 | public function get(string $settingsClass): Settings 48 | { 49 | if ($this->enabled === false) { 50 | throw SettingsCacheDisabled::create(); 51 | } 52 | 53 | $serialized = Cache::store($this->store)->get($this->resolveCacheKey($settingsClass)); 54 | 55 | $settings = unserialize($serialized); 56 | 57 | if (! $settings instanceof Settings) { 58 | throw new CouldNotUnserializeSettings(); 59 | } 60 | 61 | return $settings; 62 | } 63 | 64 | public function put(Settings $settings): void 65 | { 66 | if ($this->enabled === false) { 67 | return; 68 | } 69 | 70 | $serialized = serialize($settings); 71 | 72 | Cache::store($this->store)->put( 73 | $this->resolveCacheKey(get_class($settings)), 74 | $serialized, 75 | $this->ttl 76 | ); 77 | } 78 | 79 | public function clear(): void 80 | { 81 | app(SettingsContainer::class) 82 | ->getSettingClasses() 83 | ->map(fn (string $class) => $this->resolveCacheKey($class)) 84 | ->pipe(fn (Collection $keys) => Cache::store($this->store)->deleteMultiple($keys)); 85 | } 86 | 87 | private function resolveCacheKey(string $settingsClass): string 88 | { 89 | $prefix = $this->prefix ? "{$this->prefix}." : ''; 90 | 91 | return "{$prefix}settings.{$settingsClass}"; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/SettingsCasts/ArraySettingsCast.php: -------------------------------------------------------------------------------- 1 | cast = $cast; 12 | } 13 | 14 | public function getCast(): ?SettingsCast 15 | { 16 | return $this->cast; 17 | } 18 | 19 | public function get($payload): array 20 | { 21 | return array_map( 22 | fn ($data) => $this->cast->get($data), 23 | $payload 24 | ); 25 | } 26 | 27 | public function set($payload) 28 | { 29 | return array_map( 30 | fn ($data) => $this->cast->set($data), 31 | $payload 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/SettingsCasts/CollectionCast.php: -------------------------------------------------------------------------------- 1 | toArray(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/SettingsCasts/DataCast.php: -------------------------------------------------------------------------------- 1 | type = $this->ensureDataTypeExists($type); 15 | } 16 | 17 | public function get($payload): Data 18 | { 19 | return $this->type::from($payload); 20 | } 21 | 22 | /** 23 | * @param Data $payload 24 | * 25 | * @return array 26 | */ 27 | public function set($payload): array 28 | { 29 | return $payload->toArray(); 30 | } 31 | 32 | protected function ensureDataTypeExists(?string $type): string 33 | { 34 | if ($type === null) { 35 | throw new Exception('Cannot create a data cast because no data class was given'); 36 | } 37 | 38 | if (! class_exists($type)) { 39 | throw new Exception("Cannot create a data cast for `{$type}` because the data does not exist"); 40 | } 41 | 42 | return $type; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/SettingsCasts/DateTimeInterfaceCast.php: -------------------------------------------------------------------------------- 1 | type = $type ?? DateTime::class; 20 | } 21 | 22 | public function get($payload): ?DateTimeInterface 23 | { 24 | if ($payload === null) { 25 | return null; 26 | } 27 | 28 | if ($this->type === Carbon::class) { 29 | return new Carbon($payload); 30 | } 31 | 32 | if ($this->type === CarbonImmutable::class) { 33 | return new CarbonImmutable($payload); 34 | } 35 | 36 | if ($this->type === IlluminateCarbon::class) { 37 | return new IlluminateCarbon($payload); 38 | } 39 | 40 | if ($this->type === DateTimeImmutable::class) { 41 | return new DateTimeImmutable($payload); 42 | } 43 | 44 | if ($this->type === DateTime::class) { 45 | return new DateTime($payload); 46 | } 47 | 48 | throw new Exception("Could not cast DateTime type `{$this->type}`"); 49 | } 50 | 51 | /** @param DateTimeInterface|null $payload */ 52 | public function set($payload): ?string 53 | { 54 | return $payload !== null 55 | ? $payload->format(DATE_ATOM) 56 | : null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/SettingsCasts/DateTimeZoneCast.php: -------------------------------------------------------------------------------- 1 | type = $type ?? DateTimeZone::class; 14 | } 15 | 16 | public function get($payload): ?DateTimeZone 17 | { 18 | return $payload !== null 19 | ? new DateTimeZone($payload) 20 | : null; 21 | } 22 | 23 | /** 24 | * @param DateTimeZone|null $payload 25 | * 26 | * @return string 27 | */ 28 | public function set($payload): ?string 29 | { 30 | return $payload !== null 31 | ? $payload->getName() 32 | : null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/SettingsCasts/DtoCast.php: -------------------------------------------------------------------------------- 1 | type = $this->ensureDtoTypeExists($type); 16 | } 17 | 18 | public function get($payload): DataTransferObject 19 | { 20 | return new $this->type($payload); 21 | } 22 | 23 | /** 24 | * @param \Spatie\DataTransferObject\DataTransferObject $payload 25 | * 26 | * @return array 27 | */ 28 | public function set($payload): array 29 | { 30 | return $payload->toArray(); 31 | } 32 | 33 | protected function ensureDtoTypeExists(?string $type): string 34 | { 35 | if ($type === null) { 36 | throw new Exception('Cannot create a DTO cast because no DTO class was given'); 37 | } 38 | 39 | if (! class_exists($type)) { 40 | throw new Exception("Cannot create a DTO cast for `{$type}` because the DTO does not exist"); 41 | } 42 | 43 | return $type; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/SettingsCasts/EnumCast.php: -------------------------------------------------------------------------------- 1 | enum = $enum; 16 | } 17 | 18 | public function get($payload): ?UnitEnum 19 | { 20 | if ($payload === null) { 21 | return null; 22 | } 23 | 24 | if (is_a($this->enum, BackedEnum::class, true)) { 25 | return $this->enum::from($payload); 26 | } 27 | 28 | if (is_a($this->enum, UnitEnum::class, true)) { 29 | foreach ($this->enum::cases() as $enum) { 30 | if ($enum->name === $payload) { 31 | return $enum; 32 | } 33 | } 34 | } 35 | 36 | throw new Exception('Invalid enum'); 37 | } 38 | 39 | public function set($payload): string|int|null 40 | { 41 | if ($payload === null) { 42 | return null; 43 | } 44 | 45 | if ($payload instanceof BackedEnum) { 46 | return $payload->value; 47 | } 48 | 49 | if ($payload instanceof UnitEnum) { 50 | return $payload->name; 51 | } 52 | 53 | throw new Exception('Invalid enum'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/SettingsCasts/SettingsCast.php: -------------------------------------------------------------------------------- 1 | */ 17 | private string $settingsClass; 18 | 19 | /** @var array */ 20 | private array $defaultValueLoadedProperties = []; 21 | 22 | /** @var Collection */ 23 | private Collection $casts; 24 | 25 | /** @var Collection */ 26 | private Collection $reflectionProperties; 27 | 28 | /** @var string[]|\Illuminate\Support\Collection */ 29 | private Collection $encrypted; 30 | 31 | /** @var string[]|\Illuminate\Support\Collection */ 32 | private Collection $locked; 33 | 34 | private SettingsRepository $repository; 35 | 36 | public function __construct(string $settingsClass) 37 | { 38 | if (! is_subclass_of($settingsClass, Settings::class)) { 39 | throw new Exception("Tried decorating {$settingsClass} which is not extending `Spatie\LaravelSettings\Settings::class`"); 40 | } 41 | 42 | $this->settingsClass = $settingsClass; 43 | 44 | $this->reflectionProperties = collect( 45 | (new ReflectionClass($settingsClass))->getProperties(ReflectionProperty::IS_PUBLIC) 46 | )->mapWithKeys(fn (ReflectionProperty $property) => [$property->getName() => $property]); 47 | 48 | $this->casts = $this->reflectionProperties 49 | ->map(fn (ReflectionProperty $reflectionProperty) => SettingsCastFactory::resolve( 50 | $reflectionProperty, 51 | $this->settingsClass::casts() 52 | )); 53 | 54 | $this->encrypted = collect($this->settingsClass::encrypted()); 55 | 56 | $this->repository = SettingsRepositoryFactory::create($this->settingsClass::repository()); 57 | } 58 | 59 | public function getName(): string 60 | { 61 | return $this->settingsClass; 62 | } 63 | 64 | public function getReflectedProperties(): Collection 65 | { 66 | return $this->reflectionProperties; 67 | } 68 | 69 | public function getRepository(): SettingsRepository 70 | { 71 | return $this->repository; 72 | } 73 | 74 | public function getGroup(): string 75 | { 76 | return $this->settingsClass::group(); 77 | } 78 | 79 | public function isEncrypted(string $name): bool 80 | { 81 | return $this->encrypted->contains($name); 82 | } 83 | 84 | public function isLocked(string $name): bool 85 | { 86 | return $this->getLocked()->contains($name); 87 | } 88 | 89 | public function getCast(string $name): ?SettingsCast 90 | { 91 | return $this->casts->get($name); 92 | } 93 | 94 | public function lock(string ...$names): self 95 | { 96 | $this->locked = $this->getLocked()->merge($names); 97 | 98 | $this->repository->lockProperties( 99 | $this->getGroup(), 100 | $names 101 | ); 102 | 103 | return $this; 104 | } 105 | 106 | public function unlock(string ...$names): self 107 | { 108 | $this->locked = $this->getLocked()->diff($names); 109 | 110 | $this->repository->unlockProperties( 111 | $this->getGroup(), 112 | $names 113 | ); 114 | 115 | return $this; 116 | } 117 | 118 | public function getLocked(): Collection 119 | { 120 | if (! empty($this->locked)) { 121 | return $this->locked; 122 | } 123 | 124 | return $this->locked = collect( 125 | $this->repository->getLockedProperties($this->settingsClass::group()) 126 | ); 127 | } 128 | 129 | public function markPropertyAsDefaultValueLoaded(string $name): self 130 | { 131 | $this->defaultValueLoadedProperties[] = $name; 132 | 133 | return $this; 134 | } 135 | 136 | public function getDefaultValueLoadedProperties(): array 137 | { 138 | return $this->defaultValueLoadedProperties; 139 | } 140 | 141 | public function resetDefaultValueLoadedProperties(): self 142 | { 143 | $this->defaultValueLoadedProperties = []; 144 | 145 | return $this; 146 | } 147 | 148 | public function clearCachedLockedProperties(): self 149 | { 150 | unset($this->locked); 151 | 152 | return $this; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/SettingsContainer.php: -------------------------------------------------------------------------------- 1 | container = $container; 22 | } 23 | 24 | public function registerBindings(): void 25 | { 26 | $cacheFactory = $this->container->make(SettingsCacheFactory::class); 27 | 28 | $this->getSettingClasses()->each(function (string $settingClass) use ($cacheFactory) { 29 | $this->container->scoped($settingClass, function () use ($cacheFactory, $settingClass) { 30 | $cache = $cacheFactory->build($settingClass::repository()); 31 | 32 | if ($cache->isEnabled() && $cache->has($settingClass)) { 33 | try { 34 | return $cache->get($settingClass); 35 | } catch (CouldNotUnserializeSettings $exception) { 36 | Log::error("Could not unserialize settings class: `{$settingClass}` from cache"); 37 | } 38 | } 39 | 40 | return new $settingClass(); 41 | }); 42 | }); 43 | } 44 | 45 | public function getSettingClasses(): Collection 46 | { 47 | if (self::$settingsClasses !== null) { 48 | return self::$settingsClasses; 49 | } 50 | 51 | $cachedDiscoveredSettings = config('settings.discovered_settings_cache_path') . '/settings.php'; 52 | 53 | if (file_exists($cachedDiscoveredSettings)) { 54 | $classes = require $cachedDiscoveredSettings; 55 | 56 | return self::$settingsClasses = collect($classes); 57 | } 58 | 59 | /** @var \Spatie\LaravelSettings\Settings[] $settings */ 60 | $settings = array_merge( 61 | $this->discoverSettings(), 62 | config('settings.settings', []) 63 | ); 64 | 65 | return self::$settingsClasses = collect($settings)->unique(); 66 | } 67 | 68 | public function clearCache(): self 69 | { 70 | self::$settingsClasses = null; 71 | 72 | return $this; 73 | } 74 | 75 | protected function discoverSettings(): array 76 | { 77 | return (new DiscoverSettings()) 78 | ->within(config('settings.auto_discover_settings', [])) 79 | ->useBasePath(base_path()) 80 | ->ignoringFiles(Composer::getAutoloadedFiles(base_path('composer.json'))) 81 | ->discover(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/SettingsEventSubscriber.php: -------------------------------------------------------------------------------- 1 | settingsCacheFactory = $settingsCacheFactory; 17 | } 18 | 19 | public function subscribe(Dispatcher $dispatcher) 20 | { 21 | $dispatcher->listen( 22 | SettingsSaved::class, 23 | function (SettingsSaved $event) { 24 | $cache = $this->settingsCacheFactory->build( 25 | $event->settings::repository() 26 | ); 27 | 28 | if ($cache->isEnabled()) { 29 | $cache->put($event->settings); 30 | } 31 | } 32 | ); 33 | 34 | $dispatcher->listen( 35 | SettingsLoaded::class, 36 | function (SettingsLoaded $event) { 37 | $cache = $this->settingsCacheFactory->build( 38 | $event->settings::repository() 39 | ); 40 | 41 | if ($cache->has(get_class($event->settings))) { 42 | return; 43 | } 44 | 45 | $cache->put($event->settings); 46 | } 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SettingsMapper.php: -------------------------------------------------------------------------------- 1 | */ 14 | private array $configs = []; 15 | 16 | public function initialize(string $settingsClass): SettingsConfig 17 | { 18 | if ($this->has($settingsClass)) { 19 | return $this->configs[$settingsClass]; 20 | } 21 | 22 | $config = new SettingsConfig($settingsClass); 23 | 24 | return $this->configs[$settingsClass] = $config; 25 | } 26 | 27 | public function has(string $settingsClass): bool 28 | { 29 | return array_key_exists($settingsClass, $this->configs); 30 | } 31 | 32 | public function load(string $settingsClass): Collection 33 | { 34 | $config = $this->getConfig($settingsClass); 35 | 36 | $properties = $this->fetchProperties( 37 | $settingsClass, 38 | $config->getReflectedProperties()->keys() 39 | ); 40 | 41 | event(new LoadingSettings($settingsClass, $properties)); 42 | 43 | $properties = $this->fillMissingSettingsWithDefaultValues($config, $properties); 44 | 45 | $this->ensureNoMissingSettings($config, $properties, 'loading'); 46 | 47 | return $properties; 48 | } 49 | 50 | public function save( 51 | string $settingsClass, 52 | Collection $properties 53 | ): Collection { 54 | $config = $this->getConfig($settingsClass); 55 | 56 | $this->ensureNoMissingSettings($config, $properties, 'saving'); 57 | 58 | $notRejectedProperties = $properties 59 | ->reject(fn ($payload, string $name) => $config->isLocked($name)); 60 | 61 | $changedProperties = $notRejectedProperties 62 | ->map(function ($payload, string $name) use ($config) { 63 | if ($cast = $config->getCast($name)) { 64 | $payload = $cast->set($payload); 65 | } 66 | 67 | if ($config->isEncrypted($name)) { 68 | $payload = Crypto::encrypt($payload); 69 | } 70 | 71 | return $payload; 72 | }) 73 | ->toArray(); 74 | 75 | $config->getRepository()->updatePropertiesPayload( 76 | $config->getGroup(), 77 | $changedProperties 78 | ); 79 | 80 | return $this 81 | ->fetchProperties($settingsClass, $config->getLocked()) 82 | ->merge($notRejectedProperties); 83 | } 84 | 85 | public function fetchProperties(string $settingsClass, Collection $names): Collection 86 | { 87 | $config = $this->getConfig($settingsClass); 88 | 89 | return collect($config->getRepository()->getPropertiesInGroup($config->getGroup())) 90 | ->filter(fn ($payload, string $name) => $names->contains($name)) 91 | ->map(function ($payload, string $name) use ($config) { 92 | if ($config->isEncrypted($name)) { 93 | $payload = Crypto::decrypt($payload); 94 | } 95 | 96 | if ($cast = $config->getCast($name)) { 97 | $payload = $cast->get($payload); 98 | } 99 | 100 | return $payload; 101 | }); 102 | } 103 | 104 | private function getConfig(string $settingsClass): SettingsConfig 105 | { 106 | if (! $this->has($settingsClass)) { 107 | $this->initialize($settingsClass); 108 | } 109 | 110 | return $this->configs[$settingsClass]; 111 | } 112 | 113 | private function fillMissingSettingsWithDefaultValues(SettingsConfig $config, Collection $properties): Collection 114 | { 115 | $config->resetDefaultValueLoadedProperties(); 116 | 117 | $config 118 | ->getReflectedProperties() 119 | ->keys() 120 | ->diff($properties->keys()) 121 | ->each(function (string $missingSetting) use ($config, &$properties) { 122 | /** @var ReflectionProperty $reflectionProperty */ 123 | $reflectionProperty = $config->getReflectedProperties()[$missingSetting]; 124 | 125 | if ($reflectionProperty->hasDefaultValue()) { 126 | $config->markPropertyAsDefaultValueLoaded($missingSetting); 127 | 128 | $properties->put($missingSetting, $reflectionProperty->getDefaultValue()); 129 | } 130 | }); 131 | 132 | return $properties; 133 | } 134 | 135 | private function ensureNoMissingSettings( 136 | SettingsConfig $config, 137 | Collection $properties, 138 | string $operation 139 | ): void { 140 | $missingSettings = $config 141 | ->getReflectedProperties() 142 | ->keys() 143 | ->diff($properties->keys()) 144 | ->when($operation === 'saving', fn (Collection $collection) => $collection->concat($config->getDefaultValueLoadedProperties())) 145 | ->toArray(); 146 | 147 | if (! empty($missingSettings)) { 148 | throw MissingSettings::create($config->getName(), $missingSettings, $operation); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/SettingsRepositories/DatabaseSettingsRepository.php: -------------------------------------------------------------------------------- 1 | */ 11 | protected string $propertyModel; 12 | 13 | protected ?string $connection; 14 | 15 | protected ?string $table; 16 | 17 | public function __construct(array $config) 18 | { 19 | $this->propertyModel = $config['model'] ?? SettingsProperty::class; 20 | $this->connection = $config['connection'] ?? null; 21 | $this->table = $config['table'] ?? null; 22 | } 23 | 24 | public function getPropertiesInGroup(string $group): array 25 | { 26 | return $this->getBuilder() 27 | ->where('group', $group) 28 | ->get(['name', 'payload']) 29 | ->mapWithKeys(function (object $object) { 30 | return [$object->name => $this->decode($object->payload, true)]; 31 | }) 32 | ->toArray(); 33 | } 34 | 35 | public function checkIfPropertyExists(string $group, string $name): bool 36 | { 37 | return $this->getBuilder() 38 | ->where('group', $group) 39 | ->where('name', $name) 40 | ->exists(); 41 | } 42 | 43 | public function getPropertyPayload(string $group, string $name) 44 | { 45 | $setting = $this->getBuilder() 46 | ->where('group', $group) 47 | ->where('name', $name) 48 | ->first('payload') 49 | ->toArray(); 50 | 51 | return $this->decode($setting['payload']); 52 | } 53 | 54 | public function createProperty(string $group, string $name, $payload): void 55 | { 56 | $this->getBuilder()->create([ 57 | 'group' => $group, 58 | 'name' => $name, 59 | 'payload' => $this->encode($payload), 60 | 'locked' => false, 61 | ]); 62 | } 63 | 64 | public function updatePropertiesPayload(string $group, array $properties): void 65 | { 66 | $propertiesInBatch = collect($properties)->map(function ($payload, $name) use ($group) { 67 | return [ 68 | 'group' => $group, 69 | 'name' => $name, 70 | 'payload' => $this->encode($payload), 71 | ]; 72 | })->values()->toArray(); 73 | 74 | $this->getBuilder() 75 | ->where('group', $group) 76 | ->upsert($propertiesInBatch, ['group', 'name'], ['payload']); 77 | } 78 | 79 | public function deleteProperty(string $group, string $name): void 80 | { 81 | $this->getBuilder() 82 | ->where('group', $group) 83 | ->where('name', $name) 84 | ->delete(); 85 | } 86 | 87 | public function lockProperties(string $group, array $properties): void 88 | { 89 | $this->getBuilder() 90 | ->where('group', $group) 91 | ->whereIn('name', $properties) 92 | ->update(['locked' => true]); 93 | } 94 | 95 | public function unlockProperties(string $group, array $properties): void 96 | { 97 | $this->getBuilder() 98 | ->where('group', $group) 99 | ->whereIn('name', $properties) 100 | ->update(['locked' => false]); 101 | } 102 | 103 | public function getLockedProperties(string $group): array 104 | { 105 | return $this->getBuilder() 106 | ->where('group', $group) 107 | ->where('locked', true) 108 | ->pluck('name') 109 | ->toArray(); 110 | } 111 | 112 | public function getBuilder(): Builder 113 | { 114 | $model = new $this->propertyModel; 115 | 116 | if ($this->connection) { 117 | $model->setConnection($this->connection); 118 | } 119 | 120 | if ($this->table) { 121 | $model->setTable($this->table); 122 | } 123 | 124 | return $model->newQuery(); 125 | } 126 | 127 | /** 128 | * @param mixed $value 129 | * @return mixed 130 | */ 131 | protected function encode($value) 132 | { 133 | $encoder = config('settings.encoder') ?? fn ($value) => json_encode($value); 134 | 135 | return $encoder($value); 136 | } 137 | 138 | /** 139 | * @return mixed 140 | */ 141 | protected function decode(string $payload, bool $associative = false) 142 | { 143 | $decoder = config('settings.decoder') ?? fn ($payload, $associative) => json_decode($payload, $associative); 144 | 145 | return $decoder($payload, $associative); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/SettingsRepositories/RedisSettingsRepository.php: -------------------------------------------------------------------------------- 1 | connection = $connection 17 | ->connection($config['connection'] ?? null) 18 | ->client(); 19 | 20 | $this->prefix = array_key_exists('prefix', $config) 21 | ? "{$config['prefix']}." 22 | : ''; 23 | } 24 | 25 | public function getPropertiesInGroup(string $group): array 26 | { 27 | return collect($this->connection->hGetAll($this->prefix . $group)) 28 | ->mapWithKeys(function ($payload, string $name) { 29 | return [$name => json_decode($payload, true)]; 30 | })->toArray(); 31 | } 32 | 33 | public function checkIfPropertyExists(string $group, string $name): bool 34 | { 35 | return $this->connection->hExists($this->prefix . $group, $name); 36 | } 37 | 38 | public function getPropertyPayload(string $group, string $name) 39 | { 40 | return json_decode($this->connection->hGet($this->prefix . $group, $name)); 41 | } 42 | 43 | public function createProperty(string $group, string $name, $payload): void 44 | { 45 | $this->connection->hSet($this->prefix . $group, $name, json_encode($payload)); 46 | } 47 | 48 | public function updatePropertiesPayload(string $group, array $properties): void 49 | { 50 | $properties = collect($properties)->mapWithKeys(function ($payload, $name) { 51 | return [$name => json_encode($payload)]; 52 | })->toArray(); 53 | 54 | $this->connection->hmset($this->prefix . $group, $properties); 55 | } 56 | 57 | public function deleteProperty(string $group, string $name): void 58 | { 59 | $this->connection->hDel($this->prefix . $group, $name); 60 | } 61 | 62 | public function lockProperties(string $group, array $properties): void 63 | { 64 | $this->connection->sAdd($this->getLocksSetKey($group), ...$properties); 65 | } 66 | 67 | public function unlockProperties(string $group, array $properties): void 68 | { 69 | $this->connection->sRem($this->getLocksSetKey($group), ...$properties); 70 | } 71 | 72 | public function getLockedProperties(string $group): array 73 | { 74 | return $this->connection->sMembers($this->getLocksSetKey($group)); 75 | } 76 | 77 | protected function getLocksSetKey(string $group): string 78 | { 79 | return $this->prefix . 'locks.' . $group; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/SettingsRepositories/SettingsRepository.php: -------------------------------------------------------------------------------- 1 | realpath($basePath.$path), $paths); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Support/Crypto.php: -------------------------------------------------------------------------------- 1 | basePath = app_path(); 25 | } 26 | 27 | public function within(array $directories): self 28 | { 29 | $this->directories = array_values( 30 | array_filter($directories, fn (string $directory) => is_dir($directory)) 31 | ); 32 | 33 | return $this; 34 | } 35 | 36 | public function useBasePath(string $basePath): self 37 | { 38 | $this->basePath = $basePath; 39 | 40 | return $this; 41 | } 42 | 43 | public function useRootNamespace(string $rootNamespace): self 44 | { 45 | $this->rootNamespace = $rootNamespace; 46 | 47 | return $this; 48 | } 49 | 50 | public function ignoringFiles(array $ignoredFiles): self 51 | { 52 | $this->ignoredFiles = $ignoredFiles; 53 | 54 | return $this; 55 | } 56 | 57 | public function discover(): array 58 | { 59 | if (empty($this->directories)) { 60 | return []; 61 | } 62 | 63 | $files = (new Finder())->files()->in($this->directories); 64 | 65 | return collect($files) 66 | ->reject(fn (SplFileInfo $file) => in_array($file->getPathname(), $this->ignoredFiles)) 67 | ->map(fn (SplFileInfo $file) => $this->fullQualifiedClassNameFromFile($file)) 68 | ->filter(function (string $settingsClass) { 69 | try { 70 | return is_subclass_of($settingsClass, Settings::class) && 71 | (new ReflectionClass($settingsClass))->isInstantiable(); 72 | } catch (Throwable $e) { 73 | return false; 74 | } 75 | }) 76 | ->flatten() 77 | ->toArray(); 78 | } 79 | 80 | protected function fullQualifiedClassNameFromFile(SplFileInfo $file): string 81 | { 82 | $class = trim(Str::replaceFirst($this->basePath, '', $file->getRealPath()), DIRECTORY_SEPARATOR); 83 | 84 | $class = str_replace( 85 | [DIRECTORY_SEPARATOR, 'App\\'], 86 | ['\\', app()->getNamespace()], 87 | ucfirst(Str::replaceLast('.php', '', $class)) 88 | ); 89 | 90 | return $this->rootNamespace.$class; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Support/PropertyReflector.php: -------------------------------------------------------------------------------- 1 | getType(); 29 | $docblock = $reflectionProperty->getDocComment(); 30 | 31 | if ($reflectionType === null && empty($docblock)) { 32 | return null; 33 | } 34 | 35 | if ($docblock) { 36 | preg_match('/@var ((?:(?:[\w?|\\\\<>,\s])+(?:\[])?)+)/', $docblock, $output_array); 37 | 38 | return count($output_array) === 2 39 | ? self::reflectDocblock($reflectionProperty, $output_array[1]) 40 | : null; 41 | } 42 | 43 | if (! $reflectionType instanceof ReflectionNamedType) { 44 | return null; 45 | } 46 | 47 | $builtInTypes = [ 48 | 'int', 49 | 'string', 50 | 'float', 51 | 'bool', 52 | 'mixed', 53 | 'array', 54 | ]; 55 | 56 | if (in_array($reflectionType->getName(), $builtInTypes)) { 57 | return null; 58 | } 59 | 60 | $type = new Object_(new Fqsen('\\' . $reflectionType->getName())); 61 | 62 | return $reflectionType->allowsNull() 63 | ? new Nullable($type) 64 | : $type; 65 | } 66 | 67 | protected static function reflectDocblock( 68 | ReflectionProperty $reflectionProperty, 69 | string $type 70 | ): Type { 71 | $resolvedType = (new TypeResolver())->resolve($type); 72 | 73 | $isValidPrimitive = $resolvedType instanceof Boolean 74 | || $resolvedType instanceof Float_ 75 | || $resolvedType instanceof Integer 76 | || $resolvedType instanceof String_; 77 | 78 | if ($isValidPrimitive) { 79 | return $resolvedType; 80 | } 81 | 82 | if ($resolvedType instanceof Object_) { 83 | return self::reflectObject($reflectionProperty, $resolvedType); 84 | } 85 | 86 | if ($resolvedType instanceof Compound) { 87 | return self::reflectCompound($reflectionProperty, $resolvedType); 88 | } 89 | 90 | if ($resolvedType instanceof Nullable) { 91 | return new Nullable(self::reflectDocblock($reflectionProperty, (string) $resolvedType->getActualType())); 92 | } 93 | 94 | if ($resolvedType instanceof AbstractList) { 95 | $listType = get_class($resolvedType); 96 | 97 | return new $listType( 98 | self::reflectDocblock($reflectionProperty, (string) $resolvedType->getValueType()), 99 | $resolvedType->getKeyType() 100 | ); 101 | } 102 | 103 | throw CouldNotResolveDocblockType::create($type, $reflectionProperty); 104 | } 105 | 106 | private static function reflectCompound( 107 | ReflectionProperty $reflectionProperty, 108 | Compound $compound 109 | ): Nullable { 110 | if ($compound->getIterator()->count() !== 2 || ! $compound->contains(new Null_())) { 111 | throw CouldNotResolveDocblockType::create((string) $compound, $reflectionProperty); 112 | } 113 | 114 | $other = current(array_filter( 115 | iterator_to_array($compound->getIterator()), 116 | fn (Type $type) => ! $type instanceof Null_ 117 | )); 118 | 119 | return new Nullable(self::reflectDocblock($reflectionProperty, (string) $other)); 120 | } 121 | 122 | private static function reflectObject( 123 | ReflectionProperty $reflectionProperty, 124 | Object_ $object 125 | ): Object_ { 126 | if (class_exists((string) $object->getFqsen())) { 127 | return $object; 128 | } 129 | 130 | $context = (new ContextFactory)->createFromReflector($reflectionProperty); 131 | 132 | $className = ltrim((string) $object->getFqsen(), '\\'); 133 | 134 | $fqsen = (new FqsenResolver)->resolve($className, $context); 135 | 136 | return new Object_($fqsen); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Support/SettingsCacheFactory.php: -------------------------------------------------------------------------------- 1 | config = $settingsConfig; 18 | 19 | $this->initializeCaches(); 20 | } 21 | 22 | public function build(?string $repository = null): SettingsCache 23 | { 24 | if ($repository === null) { 25 | return $this->defaultCache; 26 | } 27 | 28 | if (array_key_exists($repository, $this->repositoryCaches)) { 29 | return $this->repositoryCaches[$repository]; 30 | } 31 | 32 | return $this->defaultCache; 33 | } 34 | 35 | /** @return array */ 36 | public function all(): array 37 | { 38 | return array_merge( 39 | ['default' => $this->defaultCache], 40 | $this->repositoryCaches 41 | ); 42 | } 43 | 44 | protected function initializeCaches(): void 45 | { 46 | $this->defaultCache = $this->initializeCache($this->config['cache']); 47 | 48 | foreach ($this->config['repositories'] as $name => $repositoryConfig) { 49 | if (array_key_exists('cache', $repositoryConfig)) { 50 | $this->repositoryCaches[$name] = $this->initializeCache($repositoryConfig['cache']); 51 | } 52 | } 53 | } 54 | 55 | protected function initializeCache(array $config): SettingsCache 56 | { 57 | return new SettingsCache( 58 | $config['enabled'] ?? false, 59 | $config['store'] ?? null, 60 | $config['prefix'] ?? null, 61 | $config['ttl'] ?? null, 62 | ); 63 | } 64 | } 65 | --------------------------------------------------------------------------------