├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── ArrayUtil.php ├── BaseServiceProvider.php ├── DatabaseSettingStore.php ├── Facade.php ├── Facades │ └── Setting.php ├── JsonSettingStore.php ├── MemorySettingStore.php ├── SaveMiddleware.php ├── ServiceProvider.php ├── SettingStore.php ├── SettingsManager.php ├── config │ └── config.php ├── helpers.php └── migrations │ ├── 2015_08_25_172600_create_settings_table.php │ └── 2019_03_16_141016_add_unique_to_key_column_in_settings_table.php └── tests ├── functional ├── AbstractFunctionalTest.php ├── DatabaseTest.php ├── JsonTest.php └── MemoryTest.php ├── tmp └── .gitkeep └── unit ├── ArrayUtilTest.php ├── DatabaseSettingStoreTest.php ├── HelperTest.php └── JsonSettingStoreTest.php /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | 'on': 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | tags: 9 | - '**' 10 | pull_request: 11 | branches: 12 | - '**' 13 | schedule: 14 | - cron: '0 8 1 * *' 15 | 16 | jobs: 17 | phpunit: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | php-version: 22 | - '7.4' 23 | - '8.0' 24 | - '8.1' 25 | - '8.2' 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: ${{ matrix.php-version }} 31 | tools: composer 32 | - run: composer install --dev 33 | - run: ./vendor/bin/phpunit 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you want to add a new feature, make an issue rather than a pull request. 4 | 5 | Code style is pretty much PSR-2, but with tabs instead of 4 spaces. Use common sense. 6 | 7 | Make separate pull requests for separate changes/issues. 8 | 9 | Please don't be offended if I don't merge a pull request :) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Andreas Lutro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Settings 2 | 3 | [![Build Status](https://api.travis-ci.org/anlutro/laravel-settings.svg?branch=master)](https://travis-ci.org/anlutro/laravel-settings) 4 | [![Latest Stable Version](https://poser.pugx.org/anlutro/l4-settings/v/stable.svg)](https://github.com/anlutro/laravel-settings/releases) 5 | [![License](https://poser.pugx.org/anlutro/l4-settings/license.svg)](http://opensource.org/licenses/MIT) 6 | 7 | Persistent, application-wide settings for Laravel. 8 | 9 | Despite the package name, this package should work with Laravel 4, 5, 6, 7 and 8 (though some versions are not automatically tested). 10 | 11 | ## Common problems 12 | 13 | - Class not found errors: https://github.com/anlutro/laravel-settings/issues/38 14 | 15 | ## Installation - Laravel >= 5.5 16 | 17 | 1. `composer require anlutro/l4-settings` 18 | 2. Publish the config file by running `php artisan vendor:publish --provider="anlutro\LaravelSettings\ServiceProvider" --tag="config"`. The config file will give you control over which storage engine to use as well as some storage-specific settings. 19 | 20 | ## Installation - Laravel < 5.5 21 | 22 | 1. `composer require anlutro/l4-settings` 23 | 2. Add `anlutro\LaravelSettings\ServiceProvider` to the array of providers in `config/app.php`. 24 | 3. Publish the config file by running `php artisan config:publish anlutro/l4-settings` (Laravel 4.x) or `php artisan vendor:publish` (Laravel 5.x). The config file will give you control over which storage engine to use as well as some storage-specific settings. 25 | 4. Optional: add `'Setting' => 'anlutro\LaravelSettings\Facade'` to the array of aliases in `config/app.php`. 26 | 27 | ## Usage 28 | 29 | You can either access the setting store via its facade or inject it by type-hinting towards the abstract class `anlutro\LaravelSettings\SettingStore`. 30 | 31 | ```php 32 | 39 | ``` 40 | 41 | Call `Setting::save()` explicitly to save changes made. 42 | 43 | You could also use the `setting()` helper: 44 | 45 | ```php 46 | // Get the store instance 47 | setting(); 48 | 49 | // Get values 50 | setting('foo'); 51 | setting('foo.bar'); 52 | setting('foo', 'default value'); 53 | setting()->get('foo'); 54 | 55 | // Set values 56 | setting(['foo' => 'bar']); 57 | setting(['foo.bar' => 'baz']); 58 | setting()->set('foo', 'bar'); 59 | 60 | // Method chaining 61 | setting(['foo' => 'bar'])->save(); 62 | ``` 63 | 64 | 65 | ### Auto-saving 66 | 67 | In Laravel 4.x, the library makes sure to auto-save every time the application shuts down if anything has been changed. 68 | 69 | In Laravel 5.x, if you add the middleware `anlutro\LaravelSettings\SaveMiddleware` to your `middleware` list in `app\Http\Kernel.php`, settings will be saved automatically at the end of all HTTP requests, but you'll still need to call `Setting::save()` explicitly in console commands, queue workers etc. 70 | 71 | 72 | ### Store cache 73 | 74 | When reading from the store, you can enable the cache. 75 | 76 | You can also configure flushing of the cache when writing and configure time to live. 77 | 78 | Reading will come from the store, and then from the cache, this can reduce load on the store. 79 | 80 | ```php 81 | // Cache usage configurations. 82 | 'enableCache' => false, 83 | 'forgetCacheByWrite' => true, 84 | 'cacheTtl' => 15, 85 | ``` 86 | 87 | ### JSON storage 88 | 89 | You can modify the path used on run-time using `Setting::setPath($path)`. 90 | 91 | 92 | ### Database storage 93 | 94 | #### Using Migration File 95 | 96 | If you use the database store you need to run `php artisan migrate --package=anlutro/l4-settings` (Laravel 4.x) or `php artisan vendor:publish --provider="anlutro\LaravelSettings\ServiceProvider" --tag="migrations" && php artisan migrate` (Laravel 5.x) to generate the table. 97 | 98 | #### Example 99 | 100 | For example, if you want to store settings for multiple users/clients in the same database you can do so by specifying extra columns: 101 | 102 | ```php 103 | Auth::user()->id 106 | )); 107 | ?> 108 | ``` 109 | 110 | `where user_id = x` will now be added to the database query when settings are retrieved, and when new settings are saved, the `user_id` will be populated. 111 | 112 | If you need more fine-tuned control over which data gets queried, you can use the `setConstraint` method which takes a closure with two arguments: 113 | 114 | - `$query` is the query builder instance 115 | - `$insert` is a boolean telling you whether the query is an insert or not. If it is an insert, you usually don't need to do anything to `$query`. 116 | 117 | ```php 118 | where(/* ... */); 122 | }); 123 | ?> 124 | ``` 125 | 126 | ### Custom stores 127 | 128 | This package uses the Laravel `Manager` class under the hood, so it's easy to add your own custom session store driver if you want to store in some other way. All you need to do is extend the abstract `SettingStore` class, implement the abstract methods and call `Setting::extend`. 129 | 130 | ```php 131 | make('MyStore'); 137 | }); 138 | ?> 139 | ``` 140 | 141 | 142 | ## Contact 143 | 144 | Open an issue on GitHub if you have any problems or suggestions. 145 | 146 | 147 | ## License 148 | 149 | The contents of this repository is released under the [MIT license](http://opensource.org/licenses/MIT). 150 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anlutro/l4-settings", 3 | "description": "Persistent settings in Laravel.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Andreas Lutro", 8 | "email": "anlutro@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "illuminate/support": "^4.2|^5|^6|^7|^8|^9|^10|^11|^12", 13 | "illuminate/cache": "^4.2|^5|^6|^7|^8|^9|^10|^11|^12" 14 | }, 15 | "suggest": { 16 | "illuminate/filesystem": "Save settings to a JSON file.", 17 | "illuminate/database": "Save settings to a database table." 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "^8.0", 21 | "mockery/mockery": "^1.2", 22 | "laravel/framework": ">=5.7" 23 | }, 24 | "autoload": { 25 | "files": [ 26 | "src/helpers.php" 27 | ], 28 | "psr-4": { 29 | "anlutro\\LaravelSettings\\": "src/" 30 | } 31 | }, 32 | "extra": { 33 | "laravel": { 34 | "aliases": { 35 | "Setting": "anlutro\\LaravelSettings\\Facade" 36 | }, 37 | "providers": [ 38 | "anlutro\\LaravelSettings\\ServiceProvider" 39 | ] 40 | } 41 | }, 42 | "minimum-stability": "dev", 43 | "prefer-stable": true 44 | } 45 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ArrayUtil.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package l4-settings 8 | */ 9 | 10 | namespace anlutro\LaravelSettings; 11 | 12 | /** 13 | * Array utility functions. 14 | */ 15 | class ArrayUtil 16 | { 17 | /** 18 | * This class is a static class and should not be instantiated. 19 | */ 20 | private function __construct() 21 | { 22 | // 23 | } 24 | 25 | /** 26 | * Get an element from an array. 27 | * 28 | * @param array $data 29 | * @param string $key Specify a nested element by separating keys with full stops. 30 | * @param mixed $default If the element is not found, return this. 31 | * 32 | * @return mixed 33 | */ 34 | public static function get(array $data, $key, $default = null) 35 | { 36 | if ($key === null) { 37 | return $data; 38 | } 39 | 40 | if (is_array($key)) { 41 | return static::getArray($data, $key, $default); 42 | } 43 | 44 | foreach (explode('.', $key) as $segment) { 45 | if (!is_array($data)) { 46 | return $default; 47 | } 48 | 49 | if (!array_key_exists($segment, $data)) { 50 | return $default; 51 | } 52 | 53 | $data = $data[$segment]; 54 | } 55 | 56 | return $data; 57 | } 58 | 59 | protected static function getArray(array $input, $keys, $default = null) 60 | { 61 | $output = array(); 62 | 63 | foreach ($keys as $key) { 64 | if ($default) { 65 | $keyDefault = static::get($default, $key); 66 | } else { 67 | $keyDefault = null; 68 | } 69 | static::set($output, $key, static::get($input, $key, $keyDefault)); 70 | } 71 | 72 | return $output; 73 | } 74 | 75 | /** 76 | * Determine if an array has a given key. 77 | * 78 | * @param array $data 79 | * @param string $key 80 | * 81 | * @return boolean 82 | */ 83 | public static function has(array $data, $key) 84 | { 85 | foreach (explode('.', $key) as $segment) { 86 | if (!is_array($data)) { 87 | return false; 88 | } 89 | 90 | if (!array_key_exists($segment, $data)) { 91 | return false; 92 | } 93 | 94 | $data = $data[$segment]; 95 | } 96 | 97 | return true; 98 | } 99 | 100 | /** 101 | * Set an element of an array. 102 | * 103 | * @param array $data 104 | * @param string $key Specify a nested element by separating keys with full stops. 105 | * @param mixed $value 106 | */ 107 | public static function set(array &$data, $key, $value) 108 | { 109 | $segments = explode('.', $key); 110 | 111 | $key = array_pop($segments); 112 | 113 | // iterate through all of $segments except the last one 114 | foreach ($segments as $segment) { 115 | if (!array_key_exists($segment, $data)) { 116 | $data[$segment] = array(); 117 | } else if (!is_array($data[$segment])) { 118 | throw new \UnexpectedValueException('Non-array segment encountered'); 119 | } 120 | 121 | $data =& $data[$segment]; 122 | } 123 | 124 | $data[$key] = $value; 125 | } 126 | 127 | /** 128 | * Unset an element from an array. 129 | * 130 | * @param array &$data 131 | * @param string $key Specify a nested element by separating keys with full stops. 132 | */ 133 | public static function forget(array &$data, $key) 134 | { 135 | $segments = explode('.', $key); 136 | 137 | $key = array_pop($segments); 138 | 139 | // iterate through all of $segments except the last one 140 | foreach ($segments as $segment) { 141 | if (!array_key_exists($segment, $data)) { 142 | return; 143 | } else if (!is_array($data[$segment])) { 144 | throw new \UnexpectedValueException('Non-array segment encountered'); 145 | } 146 | 147 | $data =& $data[$segment]; 148 | } 149 | 150 | unset($data[$key]); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/BaseServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | */ 9 | 10 | namespace anlutro\LaravelSettings; 11 | 12 | if (interface_exists('Illuminate\Contracts\Support\DeferrableProvider')) { 13 | class BaseServiceProvider extends \Illuminate\Support\ServiceProvider implements \Illuminate\Contracts\Support\DeferrableProvider 14 | { 15 | } 16 | } else { 17 | class BaseServiceProvider extends \Illuminate\Support\ServiceProvider 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/DatabaseSettingStore.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package l4-settings 8 | */ 9 | 10 | namespace anlutro\LaravelSettings; 11 | 12 | use Illuminate\Database\Connection; 13 | use Illuminate\Support\Arr; 14 | 15 | class DatabaseSettingStore extends SettingStore 16 | { 17 | /** 18 | * The database connection instance. 19 | * 20 | * @var \Illuminate\Database\Connection 21 | */ 22 | protected $connection; 23 | 24 | /** 25 | * The table to query from. 26 | * 27 | * @var string 28 | */ 29 | protected $table; 30 | 31 | /** 32 | * The key column name to query from. 33 | * 34 | * @var string 35 | */ 36 | protected $keyColumn; 37 | 38 | /** 39 | * The value column name to query from. 40 | * 41 | * @var string 42 | */ 43 | protected $valueColumn; 44 | 45 | /** 46 | * Any query constraints that should be applied. 47 | * 48 | * @var \Closure|null 49 | */ 50 | protected $queryConstraint; 51 | 52 | /** 53 | * Any extra columns that should be added to the rows. 54 | * 55 | * @var array 56 | */ 57 | protected $extraColumns = array(); 58 | 59 | /** 60 | * @param \Illuminate\Database\Connection $connection 61 | * @param string $table 62 | */ 63 | public function __construct(Connection $connection, $table = null, $keyColumn = null, $valueColumn = null) 64 | { 65 | $this->connection = $connection; 66 | $this->table = $table ?: 'persistant_settings'; 67 | $this->keyColumn = $keyColumn ?: 'key'; 68 | $this->valueColumn = $valueColumn ?: 'value'; 69 | } 70 | 71 | /** 72 | * Set the table to query from. 73 | * 74 | * @param string $table 75 | */ 76 | public function setTable($table) 77 | { 78 | $this->table = $table; 79 | } 80 | 81 | /** 82 | * Set the key column name to query from. 83 | * 84 | * @param string $key_column 85 | */ 86 | public function setKeyColumn($keyColumn) 87 | { 88 | $this->keyColumn = $keyColumn; 89 | } 90 | 91 | /** 92 | * Set the value column name to query from. 93 | * 94 | * @param string $value_column 95 | */ 96 | public function setValueColumn($valueColumn) 97 | { 98 | $this->valueColumn = $valueColumn; 99 | } 100 | 101 | /** 102 | * Set the query constraint. 103 | * 104 | * @param \Closure $callback 105 | */ 106 | public function setConstraint(\Closure $callback) 107 | { 108 | $this->data = array(); 109 | $this->loaded = false; 110 | $this->queryConstraint = $callback; 111 | } 112 | 113 | /** 114 | * Set extra columns to be added to the rows. 115 | * 116 | * @param array $columns 117 | */ 118 | public function setExtraColumns(array $columns) 119 | { 120 | $this->extraColumns = $columns; 121 | } 122 | 123 | /** 124 | * {@inheritdoc} 125 | */ 126 | public function forget($key) 127 | { 128 | parent::forget($key); 129 | 130 | // because the database store cannot store empty arrays, remove empty 131 | // arrays to keep data consistent before and after saving 132 | $segments = explode('.', $key); 133 | array_pop($segments); 134 | 135 | while ($segments) { 136 | $segment = implode('.', $segments); 137 | 138 | // non-empty array - exit out of the loop 139 | if ($this->get($segment)) { 140 | break; 141 | } 142 | 143 | // remove the empty array and move on to the next segment 144 | $this->forget($segment); 145 | array_pop($segments); 146 | } 147 | } 148 | 149 | /** 150 | * {@inheritdoc} 151 | */ 152 | protected function write(array $data) 153 | { 154 | $keysQuery = $this->newQuery(); 155 | 156 | // "lists" was removed in Laravel 5.3, at which point 157 | // "pluck" should provide the same functionality. 158 | $method = !method_exists($keysQuery, 'lists') ? 'pluck' : 'lists'; 159 | $keys = $keysQuery->$method($this->keyColumn); 160 | 161 | $insertData = Arr::dot($data); 162 | $updatedData = Arr::dot($this->updatedData); 163 | $persistedData = Arr::dot($this->persistedData); 164 | $updateData = array(); 165 | $deleteKeys = array(); 166 | 167 | foreach ($keys as $key) { 168 | if (isset($updatedData[$key]) && isset($persistedData[$key]) && (string)$updatedData[$key] !== (string)$persistedData[$key]) { 169 | $updateData[$key] = $updatedData[$key]; 170 | } elseif (!isset($insertData[$key])) { 171 | $deleteKeys[] = $key; 172 | } 173 | unset($insertData[$key]); 174 | } 175 | 176 | foreach ($updateData as $key => $value) { 177 | $this->newQuery() 178 | ->where($this->keyColumn, '=', strval($key)) 179 | ->update(array($this->valueColumn => $value)); 180 | } 181 | 182 | if ($insertData) { 183 | $this->newQuery(true) 184 | ->insert($this->prepareInsertData($insertData)); 185 | } 186 | 187 | if ($deleteKeys) { 188 | $this->newQuery() 189 | ->whereIn($this->keyColumn, $deleteKeys) 190 | ->delete(); 191 | } 192 | } 193 | 194 | /** 195 | * Transforms settings data into an array ready to be insterted into the 196 | * database. Call Arr::dot on a multidimensional array before passing it 197 | * into this method! 198 | * 199 | * @param array $data Call Arr::dot on a multidimensional array before passing it into this method! 200 | * 201 | * @return array 202 | */ 203 | protected function prepareInsertData(array $data) 204 | { 205 | $dbData = array(); 206 | 207 | if ($this->extraColumns) { 208 | foreach ($data as $key => $value) { 209 | $dbData[] = array_merge( 210 | $this->extraColumns, 211 | array($this->keyColumn => $key, $this->valueColumn => $value) 212 | ); 213 | } 214 | } else { 215 | foreach ($data as $key => $value) { 216 | $dbData[] = array($this->keyColumn => $key, $this->valueColumn => $value); 217 | } 218 | } 219 | 220 | return $dbData; 221 | } 222 | 223 | /** 224 | * {@inheritdoc} 225 | */ 226 | protected function read() 227 | { 228 | return $this->parseReadData($this->newQuery()->get()); 229 | } 230 | 231 | /** 232 | * Parse data coming from the database. 233 | * 234 | * @param array $data 235 | * 236 | * @return array 237 | */ 238 | public function parseReadData($data) 239 | { 240 | $results = array(); 241 | 242 | foreach ($data as $row) { 243 | if (is_array($row)) { 244 | $key = $row[$this->keyColumn]; 245 | $value = $row[$this->valueColumn]; 246 | } elseif (is_object($row)) { 247 | $key = $row->{$this->keyColumn}; 248 | $value = $row->{$this->valueColumn}; 249 | } else { 250 | $msg = 'Expected array or object, got '.gettype($row); 251 | throw new \UnexpectedValueException($msg); 252 | } 253 | 254 | ArrayUtil::set($results, $key, $value); 255 | } 256 | 257 | return $results; 258 | } 259 | 260 | /** 261 | * Create a new query builder instance. 262 | * 263 | * @param $insert boolean Whether the query is an insert or not. 264 | * 265 | * @return \Illuminate\Database\Query\Builder 266 | */ 267 | protected function newQuery($insert = false) 268 | { 269 | $query = $this->connection->table($this->table); 270 | 271 | if (!$insert) { 272 | foreach ($this->extraColumns as $key => $value) { 273 | $query->where($key, '=', $value); 274 | } 275 | } 276 | 277 | if ($this->queryConstraint !== null) { 278 | $callback = $this->queryConstraint; 279 | $callback($query, $insert); 280 | } 281 | 282 | return $query; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/Facade.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package l4-settings 8 | */ 9 | 10 | namespace anlutro\LaravelSettings; 11 | 12 | class Facade extends \Illuminate\Support\Facades\Facade 13 | { 14 | protected static function getFacadeAccessor() 15 | { 16 | return 'anlutro\LaravelSettings\SettingsManager'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Facades/Setting.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package l4-settings 8 | */ 9 | 10 | namespace anlutro\LaravelSettings\Facades; 11 | 12 | use \Illuminate\Support\Facades\Facade; 13 | 14 | class Setting extends Facade 15 | { 16 | protected static function getFacadeAccessor() 17 | { 18 | return 'anlutro\LaravelSettings\SettingsManager'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/JsonSettingStore.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package l4-settings 8 | */ 9 | 10 | namespace anlutro\LaravelSettings; 11 | 12 | use Illuminate\Filesystem\Filesystem; 13 | 14 | class JsonSettingStore extends SettingStore 15 | { 16 | /** \Illuminate\Filesystem\Filesystem */ 17 | public $files; 18 | 19 | /** string Path to settings file. */ 20 | public $path; 21 | 22 | /** 23 | * @param \Illuminate\Filesystem\Filesystem $files 24 | * @param string $path 25 | */ 26 | public function __construct(Filesystem $files, $path = null) 27 | { 28 | $this->files = $files; 29 | $this->setPath($path ?: storage_path() . '/settings.json'); 30 | } 31 | 32 | /** 33 | * Set the path for the JSON file. 34 | * 35 | * @param string $path 36 | */ 37 | public function setPath($path) 38 | { 39 | // If the file does not already exist, we will attempt to create it. 40 | if (!$this->files->exists($path)) { 41 | $result = $this->files->put($path, '{}'); 42 | if ($result === false) { 43 | throw new \InvalidArgumentException("Could not write to $path."); 44 | } 45 | } 46 | 47 | if (!$this->files->isWritable($path)) { 48 | throw new \InvalidArgumentException("$path is not writable."); 49 | } 50 | 51 | $this->path = $path; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | protected function read() 58 | { 59 | $contents = $this->files->get($this->path); 60 | 61 | $data = json_decode($contents, true); 62 | 63 | if ($data === null) { 64 | throw new \RuntimeException("Invalid JSON in {$this->path}"); 65 | } 66 | 67 | return $data; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | protected function write(array $data) 74 | { 75 | if ($data) { 76 | $contents = json_encode($data); 77 | } else { 78 | $contents = '{}'; 79 | } 80 | 81 | $this->files->put($this->path, $contents); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/MemorySettingStore.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package l4-settings 8 | */ 9 | 10 | namespace anlutro\LaravelSettings; 11 | 12 | class MemorySettingStore extends SettingStore 13 | { 14 | /** 15 | * @param array $data 16 | */ 17 | public function __construct(array $data = null) 18 | { 19 | if ($data) { 20 | $this->data = $data; 21 | } 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function read() 28 | { 29 | return $this->data; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function write(array $data) 36 | { 37 | // do nothing 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/SaveMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * @license http://opensource.org/licenses/MIT 8 | * @package l4-settings 9 | */ 10 | 11 | namespace anlutro\LaravelSettings; 12 | 13 | use Closure; 14 | use anlutro\LaravelSettings\SettingStore; 15 | use Illuminate\Contracts\Routing\TerminableMiddleware; 16 | 17 | // https://github.com/anlutro/laravel-settings/issues/43 18 | if (interface_exists('Illuminate\Contracts\Routing\TerminableMiddleware')) { 19 | interface LaravelIsStupidMiddleware extends TerminableMiddleware {} 20 | } else { 21 | interface LaravelIsStupidMiddleware {} 22 | } 23 | 24 | class SaveMiddleware implements LaravelIsStupidMiddleware 25 | { 26 | /** 27 | * @var SettingStore $settings 28 | */ 29 | private SettingStore $settings; 30 | 31 | /** 32 | * Create a new save settings middleware. 33 | * 34 | * @param SettingStore $settings 35 | */ 36 | public function __construct(SettingStore $settings) 37 | { 38 | $this->settings = $settings; 39 | } 40 | 41 | /** 42 | * Handle an incoming request. 43 | * 44 | * @param \Illuminate\Http\Request $request 45 | * @param \Closure $next 46 | * @return mixed 47 | */ 48 | public function handle($request, Closure $next) 49 | { 50 | $response = $next($request); 51 | 52 | return $response; 53 | } 54 | 55 | /** 56 | * Perform any final actions for the request lifecycle. 57 | * 58 | * @param \Illuminate\Http\Request $request 59 | * @param \Symfony\Component\HttpFoundation\Response $response 60 | * @return void 61 | */ 62 | public function terminate($request, $response) 63 | { 64 | $this->settings->save(); 65 | } 66 | } -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package l4-settings 8 | */ 9 | 10 | namespace anlutro\LaravelSettings; 11 | 12 | use Illuminate\Foundation\Application; 13 | 14 | class ServiceProvider extends BaseServiceProvider 15 | { 16 | /** 17 | * This provider is deferred and should be lazy loaded. 18 | * 19 | * @var boolean 20 | */ 21 | protected $defer = true; 22 | 23 | /** 24 | * Register IoC bindings. 25 | */ 26 | public function register() 27 | { 28 | $method = version_compare(Application::VERSION, '5.2', '>=') ? 'singleton' : 'bindShared'; 29 | 30 | // Bind the manager as a singleton on the container. 31 | $this->app->$method('anlutro\LaravelSettings\SettingsManager', function($app) { 32 | // When the class has been resolved once, make sure that settings 33 | // are saved when the application shuts down. 34 | if (version_compare(Application::VERSION, '5.0', '<')) { 35 | $app->shutdown(function($app) { 36 | $app->make('anlutro\LaravelSettings\SettingStore')->save(); 37 | }); 38 | } 39 | 40 | /** 41 | * Construct the actual manager. 42 | */ 43 | return new SettingsManager($app); 44 | }); 45 | 46 | // Provide a shortcut to the SettingStore for injecting into classes. 47 | $this->app->bind('anlutro\LaravelSettings\SettingStore', function($app) { 48 | return $app->make('anlutro\LaravelSettings\SettingsManager')->driver(); 49 | }); 50 | 51 | $this->app->alias('anlutro\LaravelSettings\SettingStore', 'setting'); 52 | 53 | if (version_compare(Application::VERSION, '5.0', '>=')) { 54 | $this->mergeConfigFrom(__DIR__ . '/config/config.php', 'settings'); 55 | } 56 | } 57 | 58 | /** 59 | * Boot the package. 60 | */ 61 | public function boot() 62 | { 63 | if (version_compare(Application::VERSION, '5.0', '>=')) { 64 | $this->publishes([ 65 | __DIR__.'/config/config.php' => config_path('settings.php') 66 | ], 'config'); 67 | $this->publishes([ 68 | __DIR__.'/migrations/2015_08_25_172600_create_settings_table.php' => database_path('migrations/'.date('Y_m_d_His').'_create_settings_table.php') 69 | ], 'migrations'); 70 | } else { 71 | $this->app['config']->package( 72 | 'anlutro/l4-settings', __DIR__ . '/config', 'anlutro/l4-settings' 73 | ); 74 | } 75 | } 76 | 77 | /** 78 | * Which IoC bindings the provider provides. 79 | * 80 | * @return array 81 | */ 82 | public function provides() 83 | { 84 | return array( 85 | 'anlutro\LaravelSettings\SettingsManager', 86 | 'anlutro\LaravelSettings\SettingStore', 87 | 'setting' 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/SettingStore.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package l4-settings 8 | */ 9 | 10 | namespace anlutro\LaravelSettings; 11 | 12 | abstract class SettingStore 13 | { 14 | /** 15 | * Cache key for save 16 | */ 17 | const CACHE_KEY = 'setting:cache'; 18 | 19 | /** 20 | * The settings data. 21 | * 22 | * @var array 23 | */ 24 | protected $data = array(); 25 | 26 | /** 27 | * The settings updated data. 28 | * 29 | * @var array 30 | */ 31 | protected $updatedData = array(); 32 | 33 | /** 34 | * The settings updated data. 35 | * 36 | * @var array 37 | */ 38 | protected $persistedData = array(); 39 | 40 | /** 41 | * Whether the store has changed since it was last loaded. 42 | * 43 | * @var boolean 44 | */ 45 | protected $unsaved = false; 46 | 47 | /** 48 | * Whether the settings data are loaded. 49 | * 50 | * @var boolean 51 | */ 52 | protected $loaded = false; 53 | 54 | /** 55 | * Default values. 56 | * 57 | * @var array 58 | */ 59 | protected $defaults = []; 60 | 61 | /** 62 | * @var \Illuminate\Contracts\Cache\Store|\Illuminate\Cache\StoreInterface 63 | */ 64 | protected $cache = null; 65 | 66 | /** 67 | * Cache TTL in seconds. 68 | * 69 | * @var int 70 | */ 71 | protected $cacheTtl = 15; 72 | 73 | /** 74 | * Whether to reset the cache when changing a setting. 75 | * 76 | * @var boolean 77 | */ 78 | protected $cacheForgetOnWrite = true; 79 | 80 | /** 81 | * Set default values. 82 | * 83 | * @param array $defaults 84 | */ 85 | public function setDefaults(array $defaults) 86 | { 87 | $this->defaults = $defaults; 88 | } 89 | 90 | /** 91 | * Set the cache. 92 | * @param \Illuminate\Contracts\Cache\Store|\Illuminate\Cache\StoreInterface $cache 93 | * @param int $ttl 94 | * @param bool $forgetOnWrite 95 | */ 96 | public function setCache($cache, $ttl = null, $forgetOnWrite = null) 97 | { 98 | $this->cache = $cache; 99 | if ($ttl !== null) { 100 | $this->cacheTtl = $ttl; 101 | } 102 | if ($forgetOnWrite !== null) { 103 | $this->cacheForgetOnWrite = $forgetOnWrite; 104 | } 105 | } 106 | 107 | /** 108 | * Get a specific key from the settings data. 109 | * 110 | * @param string|array $key 111 | * @param mixed $default Optional default value. 112 | * 113 | * @return mixed 114 | */ 115 | public function get($key, $default = null) 116 | { 117 | if ($default === NULL) { 118 | $default = ArrayUtil::get($this->defaults, $key); 119 | } elseif (is_array($key) && is_array($default)) { 120 | $default = array_merge(ArrayUtil::get($this->defaults, $key, []), $default); 121 | } 122 | 123 | $this->load(); 124 | 125 | return ArrayUtil::get($this->data, $key, $default); 126 | } 127 | 128 | /** 129 | * Determine if a key exists in the settings data. 130 | * 131 | * @param string $key 132 | * 133 | * @return boolean 134 | */ 135 | public function has($key) 136 | { 137 | $this->load(); 138 | 139 | return ArrayUtil::has($this->data, $key); 140 | } 141 | 142 | /** 143 | * Set a specific key to a value in the settings data. 144 | * 145 | * @param string|array $key Key string or associative array of key => value 146 | * @param mixed $value Optional only if the first argument is an array 147 | */ 148 | public function set($key, $value = null) 149 | { 150 | $this->load(); 151 | $this->unsaved = true; 152 | 153 | if (is_array($key)) { 154 | foreach ($key as $k => $v) { 155 | ArrayUtil::set($this->data, $k, $v); 156 | ArrayUtil::set($this->updatedData, $k, $v); 157 | } 158 | } else { 159 | ArrayUtil::set($this->data, $key, $value); 160 | ArrayUtil::set($this->updatedData, $key, $value); 161 | } 162 | } 163 | 164 | /** 165 | * Unset a key in the settings data. 166 | * 167 | * @param string $key 168 | */ 169 | public function forget($key) 170 | { 171 | $this->unsaved = true; 172 | 173 | if ($this->has($key)) { 174 | ArrayUtil::forget($this->data, $key); 175 | ArrayUtil::forget($this->updatedData, $key); 176 | } 177 | } 178 | 179 | /** 180 | * Unset all keys in the settings data. 181 | * 182 | * @return void 183 | */ 184 | public function forgetAll() 185 | { 186 | $this->unsaved = true; 187 | $this->data = array(); 188 | $this->updatedData = array(); 189 | } 190 | 191 | /** 192 | * Get all settings data. 193 | * 194 | * @return array 195 | */ 196 | public function all() 197 | { 198 | $this->load(); 199 | 200 | return $this->data; 201 | } 202 | 203 | /** 204 | * Save any changes done to the settings data. 205 | * 206 | * @return void 207 | */ 208 | public function save() 209 | { 210 | if (!$this->unsaved) { 211 | // either nothing has been changed, or data has not been loaded, so 212 | // do nothing by returning early 213 | return; 214 | } 215 | 216 | if ($this->cache && $this->cacheForgetOnWrite) { 217 | $this->cache->forget(static::CACHE_KEY); 218 | } 219 | 220 | $this->write($this->data); 221 | $this->unsaved = false; 222 | } 223 | 224 | /** 225 | * Make sure data is loaded. 226 | * 227 | * @param $force Force a reload of data. Default false. 228 | */ 229 | public function load($force = false) 230 | { 231 | if (!$this->loaded || $force) { 232 | $this->data = $this->readData(); 233 | $this->persistedData = $this->data; 234 | $this->data = $this->updatedData + $this->data; 235 | $this->loaded = true; 236 | } 237 | } 238 | 239 | /** 240 | * Read data from a store or cache 241 | * 242 | * @return array 243 | */ 244 | private function readData() 245 | { 246 | if ($this->cache) { 247 | return $this->cache->remember(static::CACHE_KEY, $this->cacheTtl, function () { 248 | return $this->read(); 249 | }); 250 | } 251 | 252 | return $this->read(); 253 | } 254 | 255 | /** 256 | * Read the data from the store. 257 | * 258 | * @return array 259 | */ 260 | abstract protected function read(); 261 | 262 | /** 263 | * Write the data into the store. 264 | * 265 | * @param array $data 266 | * 267 | * @return void 268 | */ 269 | abstract protected function write(array $data); 270 | } 271 | -------------------------------------------------------------------------------- /src/SettingsManager.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package l4-settings 8 | */ 9 | 10 | namespace anlutro\LaravelSettings; 11 | 12 | use Illuminate\Support\Manager; 13 | use Illuminate\Foundation\Application; 14 | 15 | class SettingsManager extends Manager 16 | { 17 | public function getDefaultDriver() 18 | { 19 | return $this->getConfig('anlutro/l4-settings::store'); 20 | } 21 | 22 | public function createJsonDriver() 23 | { 24 | $path = $this->getConfig('anlutro/l4-settings::path'); 25 | 26 | $store = new JsonSettingStore($this->getSupportedContainer()['files'], $path); 27 | 28 | return $this->wrapDriver($store); 29 | } 30 | 31 | public function createDatabaseDriver() 32 | { 33 | $connectionName = $this->getConfig('anlutro/l4-settings::connection'); 34 | $connection = $this->getSupportedContainer()['db']->connection($connectionName); 35 | $table = $this->getConfig('anlutro/l4-settings::table'); 36 | $keyColumn = $this->getConfig('anlutro/l4-settings::keyColumn'); 37 | $valueColumn = $this->getConfig('anlutro/l4-settings::valueColumn'); 38 | 39 | $store = new DatabaseSettingStore($connection, $table, $keyColumn, $valueColumn); 40 | 41 | return $this->wrapDriver($store); 42 | } 43 | 44 | public function createMemoryDriver() 45 | { 46 | return $this->wrapDriver(new MemorySettingStore()); 47 | } 48 | 49 | public function createArrayDriver() 50 | { 51 | return $this->createMemoryDriver(); 52 | } 53 | 54 | protected function getConfig($key) 55 | { 56 | if (version_compare(Application::VERSION, '5.0', '>=')) { 57 | $key = str_replace('anlutro/l4-settings::', 'settings.', $key); 58 | } 59 | 60 | return $this->getSupportedContainer()['config']->get($key); 61 | } 62 | 63 | protected function wrapDriver($store) 64 | { 65 | $store->setDefaults($this->getConfig('anlutro/l4-settings::defaults')); 66 | 67 | if ($this->getConfig('anlutro/l4-settings::enableCache')) { 68 | $store->setCache( 69 | $this->getSupportedContainer()['cache'], 70 | $this->getConfig('anlutro/l4-settings::cacheTtl'), 71 | $this->getConfig('anlutro/l4-settings::forgetCacheByWrite') 72 | ); 73 | } 74 | 75 | return $store; 76 | } 77 | 78 | protected function getSupportedContainer() 79 | { 80 | return isset($this->app) ? $this->app : $this->container; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | 'json', 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | JSON Store 20 | |-------------------------------------------------------------------------- 21 | | 22 | | If the store is set to "json", settings are stored in the defined 23 | | file path in JSON format. Use full path to file. 24 | | 25 | */ 26 | 'path' => storage_path().'/settings.json', 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Database Store 31 | |-------------------------------------------------------------------------- 32 | | 33 | | The settings are stored in the defined file path in JSON format. 34 | | Use full path to JSON file. 35 | | 36 | */ 37 | // If set to null, the default connection will be used. 38 | 'connection' => null, 39 | // Name of the table used. 40 | 'table' => 'settings', 41 | // If you want to use custom column names in database store you could 42 | // set them in this configuration 43 | 'keyColumn' => 'key', 44 | 'valueColumn' => 'value', 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Cache settings 49 | |-------------------------------------------------------------------------- 50 | | 51 | | If you want all setting calls to go through Laravel's cache system. 52 | | 53 | */ 54 | 'enableCache' => false, 55 | // Whether to reset the cache when changing a setting. 56 | 'forgetCacheByWrite' => true, 57 | // TTL in seconds. 58 | 'cacheTtl' => 15, 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Default Settings 63 | |-------------------------------------------------------------------------- 64 | | 65 | | Define all default settings that will be used before any settings are set, 66 | | this avoids all settings being set to false to begin with and avoids 67 | | hardcoding the same defaults in all 'Settings::get()' calls 68 | | 69 | */ 70 | 'defaults' => [ 71 | 'foo' => 'bar', 72 | ] 73 | ]; 74 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | set($key); 10 | } elseif (! is_null($key)) { 11 | return $setting->get($key, $default); 12 | } 13 | 14 | return $setting; 15 | } 16 | } -------------------------------------------------------------------------------- /src/migrations/2015_08_25_172600_create_settings_table.php: -------------------------------------------------------------------------------- 1 | =')) { 13 | $this->tablename = Config::get('settings.table'); 14 | $this->keyColumn = Config::get('settings.keyColumn'); 15 | $this->valueColumn = Config::get('settings.valueColumn'); 16 | } else { 17 | $this->tablename = Config::get('anlutro/l4-settings::table'); 18 | $this->keyColumn = Config::get('anlutro/l4-settings::keyColumn'); 19 | $this->valueColumn = Config::get('anlutro/l4-settings::valueColumn'); 20 | } 21 | } 22 | 23 | /** 24 | * Run the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function up() 29 | { 30 | Schema::create($this->tablename, function(Blueprint $table) 31 | { 32 | $table->increments('id'); 33 | $table->string($this->keyColumn)->index(); 34 | $table->text($this->valueColumn); 35 | }); 36 | } 37 | 38 | /** 39 | * Reverse the migrations. 40 | * 41 | * @return void 42 | */ 43 | public function down() 44 | { 45 | Schema::drop($this->tablename); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/migrations/2019_03_16_141016_add_unique_to_key_column_in_settings_table.php: -------------------------------------------------------------------------------- 1 | =')) { 14 | $this->tablename = Config::get('settings.table'); 15 | $this->keyColumn = Config::get('settings.keyColumn'); 16 | $this->valueColumn = Config::get('settings.valueColumn'); 17 | } else { 18 | $this->tablename = Config::get('anlutro/l4-settings::table'); 19 | $this->keyColumn = Config::get('anlutro/l4-settings::keyColumn'); 20 | $this->valueColumn = Config::get('anlutro/l4-settings::valueColumn'); 21 | } 22 | } 23 | 24 | /** 25 | * Run the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function up() 30 | { 31 | Schema::table($this->tablename, function (Blueprint $table) { 32 | $table->dropIndex($this->tablename . '_' . $this->keyColumn . '_index'); 33 | $table->unique($this->keyColumn, 'settings_key_unique'); 34 | }); 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | * 40 | * @return void 41 | */ 42 | public function down() 43 | { 44 | Schema::table($this->tablename, function (Blueprint $table) { 45 | $table->dropUnique('settings_key_unique'); 46 | $table->Index($this->keyColumn); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/functional/AbstractFunctionalTest.php: -------------------------------------------------------------------------------- 1 | createStore($data); 17 | if ($this->defaults) { 18 | $store->setDefaults($this->defaults); 19 | } 20 | return $store; 21 | } 22 | 23 | public function tearDown(): void 24 | { 25 | m::close(); 26 | $this->defaults = []; 27 | } 28 | 29 | protected function assertStoreEquals($store, $expected, $message = '') 30 | { 31 | $this->assertEquals($expected, $store->all(), $message); 32 | $store->save(); 33 | $store = $this->getStore(); 34 | $this->assertEquals($expected, $store->all(), $message); 35 | } 36 | 37 | protected function assertStoreKeyEquals($store, $key, $expected, $message = '') 38 | { 39 | $this->assertEquals($expected, $store->get($key), $message); 40 | $store->save(); 41 | $store = $this->getStore(); 42 | $this->assertEquals($expected, $store->get($key), $message); 43 | } 44 | 45 | /** @test */ 46 | public function store_is_initially_empty() 47 | { 48 | $store = $this->getStore(); 49 | $this->assertEquals(array(), $store->all()); 50 | } 51 | 52 | /** @test */ 53 | public function written_changes_are_saved() 54 | { 55 | $store = $this->getStore(); 56 | $store->set('foo', 'bar'); 57 | $this->assertStoreKeyEquals($store, 'foo', 'bar'); 58 | } 59 | 60 | /** @test */ 61 | public function nested_keys_are_nested() 62 | { 63 | $store = $this->getStore(); 64 | $store->set('foo.bar', 'baz'); 65 | $this->assertStoreEquals($store, array('foo' => array('bar' => 'baz'))); 66 | } 67 | 68 | /** @test */ 69 | public function cannot_set_nested_key_on_non_array_member() 70 | { 71 | $store = $this->getStore(); 72 | $store->set('foo', 'bar'); 73 | $this->expectException('UnexpectedValueException'); 74 | $this->expectExceptionMessage('Non-array segment encountered'); 75 | $store->set('foo.bar', 'baz'); 76 | } 77 | 78 | /** @test */ 79 | public function can_forget_key() 80 | { 81 | $store = $this->getStore(); 82 | $store->set('foo', 'bar'); 83 | $store->set('bar', 'baz'); 84 | $this->assertStoreEquals($store, array('foo' => 'bar', 'bar' => 'baz')); 85 | 86 | $store->forget('foo'); 87 | $this->assertStoreEquals($store, array('bar' => 'baz')); 88 | } 89 | 90 | /** @test */ 91 | public function can_forget_nested_key() 92 | { 93 | $store = $this->getStore(); 94 | $store->set('foo.bar', 'baz'); 95 | $store->set('foo.baz', 'bar'); 96 | $store->set('bar.foo', 'baz'); 97 | $this->assertStoreEquals($store, array( 98 | 'foo' => array( 99 | 'bar' => 'baz', 100 | 'baz' => 'bar', 101 | ), 102 | 'bar' => array( 103 | 'foo' => 'baz', 104 | ), 105 | )); 106 | 107 | $store->forget('foo.bar'); 108 | $this->assertStoreEquals($store, array( 109 | 'foo' => array( 110 | 'baz' => 'bar', 111 | ), 112 | 'bar' => array( 113 | 'foo' => 'baz', 114 | ), 115 | )); 116 | 117 | $store->forget('bar.foo'); 118 | $expected = array( 119 | 'foo' => array( 120 | 'baz' => 'bar', 121 | ), 122 | 'bar' => array( 123 | ), 124 | ); 125 | if ($store instanceof DatabaseSettingStore) { 126 | unset($expected['bar']); 127 | } 128 | $this->assertStoreEquals($store, $expected); 129 | } 130 | 131 | /** @test */ 132 | public function can_forget_all() 133 | { 134 | $store = $this->getStore(array('foo' => 'bar')); 135 | $this->assertStoreEquals($store, array('foo' => 'bar')); 136 | $store->forgetAll(); 137 | $this->assertStoreEquals($store, array()); 138 | } 139 | 140 | /** @test */ 141 | public function defaults_are_respected() 142 | { 143 | $this->defaults = ['foo' => 'default', 'bar' => 'default']; 144 | $store = $this->getStore(array('foo' => 'bar')); 145 | $this->assertStoreEquals($store, ['foo' => 'bar']); 146 | $this->assertStoreKeyEquals($store, ['foo', 'bar'], ['foo' => 'bar', 'bar' => 'default']); 147 | } 148 | 149 | /** @test */ 150 | public function numeric_keys_are_retrieved_correctly() 151 | { 152 | $store = $this->getStore(); 153 | $store->set('1234', 'foo'); 154 | $store->set('9876', 'bar'); 155 | $store->load(true); 156 | $this->assertStoreEquals($store, ['1234' => 'foo', '9876' => 'bar']); 157 | $this->assertStoreKeyEquals($store, ['1234', '9876'], ['1234' => 'foo', '9876' => 'bar']); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tests/functional/DatabaseTest.php: -------------------------------------------------------------------------------- 1 | container = new \Illuminate\Container\Container; 8 | $this->capsule = new \Illuminate\Database\Capsule\Manager($this->container); 9 | $this->capsule->setAsGlobal(); 10 | $this->container['db'] = $this->capsule; 11 | $this->capsule->addConnection(array( 12 | 'driver' => 'sqlite', 13 | 'database' => ':memory:', 14 | 'prefix' => '', 15 | )); 16 | 17 | $this->capsule->schema()->create('persistant_settings', function($t) { 18 | $t->string('key', 64)->unique(); 19 | $t->string('value', 4096); 20 | }); 21 | parent::setUp(); 22 | } 23 | 24 | public function tearDown(): void 25 | { 26 | $this->capsule->schema()->drop('persistant_settings'); 27 | unset($this->capsule); 28 | unset($this->container); 29 | parent::tearDown(); 30 | } 31 | 32 | protected function createStore(array $data = null) 33 | { 34 | if ($data) { 35 | $store = $this->createStore(); 36 | $store->set($data); 37 | $store->save(); 38 | unset($store); 39 | } 40 | 41 | return new \anlutro\LaravelSettings\DatabaseSettingStore( 42 | $this->capsule->getConnection() 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/functional/JsonTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, $store->all(), $message); 8 | // removed persistance test assertions 9 | } 10 | 11 | protected function assertStoreKeyEquals($store, $key, $expected, $message = '') 12 | { 13 | $this->assertEquals($expected, $store->get($key), $message); 14 | // removed persistance test assertions 15 | } 16 | 17 | protected function createStore(array $data = null) 18 | { 19 | return new \anlutro\LaravelSettings\MemorySettingStore($data); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anlutro/laravel-settings/2728c55b74e56c2bee4c015145eb795f2682ddfd/tests/tmp/.gitkeep -------------------------------------------------------------------------------- /tests/unit/ArrayUtilTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, ArrayUtil::get($data, $key)); 15 | } 16 | 17 | public function getGetData() 18 | { 19 | return array( 20 | // $data, $key, $expected 21 | array(array(), 'foo', null), 22 | array(array('foo' => 'bar'), 'foo', 'bar'), 23 | array(array('foo' => 'bar'), 'bar', null), 24 | array(array('foo' => 'bar'), 'foo.bar', null), 25 | array(array('foo' => array('bar' => 'baz')), 'foo.bar', 'baz'), 26 | array(array('foo' => array('bar' => 'baz')), 'foo.baz', null), 27 | array(array('foo' => array('bar' => 'baz')), 'foo', array('bar' => 'baz')), 28 | array( 29 | array('foo' => 'bar', 'bar' => 'baz'), 30 | array('foo', 'bar'), 31 | array('foo' => 'bar', 'bar' => 'baz') 32 | ), 33 | array( 34 | array('foo' => array('bar' => 'baz'), 'bar' => 'baz'), 35 | array('foo.bar', 'bar'), 36 | array('foo' => array('bar' => 'baz'), 'bar' => 'baz'), 37 | ), 38 | array( 39 | array('foo' => array('bar' => 'baz'), 'bar' => 'baz'), 40 | array('foo.bar'), 41 | array('foo' => array('bar' => 'baz')), 42 | ), 43 | array( 44 | array('foo' => array('bar' => 'baz'), 'bar' => 'baz'), 45 | array('foo.bar', 'baz'), 46 | array('foo' => array('bar' => 'baz'), 'baz' => null), 47 | ), 48 | ); 49 | } 50 | 51 | /** 52 | * @test 53 | * @dataProvider getGetWithDefaultsData 54 | */ 55 | public function getWithDefaultsReturnsCorrectValue(array $data, $key, $default, $expected) 56 | { 57 | $this->assertEquals($expected, ArrayUtil::get($data, $key, $default)); 58 | } 59 | 60 | public function getGetWithDefaultsData() 61 | { 62 | return [ 63 | // $data, $key, $default, $expected 64 | [[], 'foo', 'default', 'default'], 65 | [['foo' => 'value'], 'foo', 'default', 'value'], 66 | [[], ['foo'], ['foo' => 'default'], ['foo' => 'default']], 67 | [['foo' => 'value'], ['foo'], ['foo' => 'default'], ['foo' => 'value']], 68 | ]; 69 | } 70 | 71 | /** 72 | * @test 73 | * @dataProvider getSetData 74 | */ 75 | public function setSetsCorrectKeyToValue(array $input, $key, $value, array $expected) 76 | { 77 | ArrayUtil::set($input, $key, $value); 78 | $this->assertEquals($expected, $input); 79 | } 80 | 81 | public function getSetData() 82 | { 83 | return array( 84 | array( 85 | array('foo' => 'bar'), 86 | 'foo', 87 | 'baz', 88 | array('foo' => 'baz'), 89 | ), 90 | array( 91 | array(), 92 | 'foo', 93 | 'bar', 94 | array('foo' => 'bar'), 95 | ), 96 | array( 97 | array(), 98 | 'foo.bar', 99 | 'baz', 100 | array('foo' => array('bar' => 'baz')), 101 | ), 102 | array( 103 | array('foo' => array('bar' => 'baz')), 104 | 'foo.baz', 105 | 'foo', 106 | array('foo' => array('bar' => 'baz', 'baz' => 'foo')), 107 | ), 108 | array( 109 | array('foo' => array('bar' => 'baz')), 110 | 'foo.baz.bar', 111 | 'baz', 112 | array('foo' => array('bar' => 'baz', 'baz' => array('bar' => 'baz'))), 113 | ), 114 | array( 115 | array(), 116 | 'foo.bar.baz', 117 | 'foo', 118 | array('foo' => array('bar' => array('baz' => 'foo'))), 119 | ), 120 | ); 121 | } 122 | 123 | /** @test */ 124 | public function setThrowsExceptionOnNonArraySegment() 125 | { 126 | $data = array('foo' => 'bar'); 127 | $this->expectException('UnexpectedValueException'); 128 | $this->expectExceptionMessage('Non-array segment encountered'); 129 | ArrayUtil::set($data, 'foo.bar', 'baz'); 130 | } 131 | 132 | /** 133 | * @test 134 | * @dataProvider getHasData 135 | */ 136 | public function hasReturnsCorrectly(array $input, $key, $expected) 137 | { 138 | $this->assertEquals($expected, ArrayUtil::has($input, $key)); 139 | } 140 | 141 | public function getHasData() 142 | { 143 | return array( 144 | array(array(), 'foo', false), 145 | array(array('foo' => 'bar'), 'foo', true), 146 | array(array('foo' => 'bar'), 'bar', false), 147 | array(array('foo' => 'bar'), 'foo.bar', false), 148 | array(array('foo' => array('bar' => 'baz')), 'foo.bar', true), 149 | array(array('foo' => array('bar' => 'baz')), 'foo.baz', false), 150 | array(array('foo' => array('bar' => 'baz')), 'foo', true), 151 | array(array('foo' => null), 'foo', true), 152 | array(array('foo' => array('bar' => null)), 'foo.bar', true), 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tests/unit/DatabaseSettingStoreTest.php: -------------------------------------------------------------------------------- 1 | mockConnection(); 17 | $query = $this->mockQuery($connection); 18 | 19 | $query->shouldReceive('get')->once()->andReturn(array( 20 | array('key' => 'nest.one', 'value' => 'old'), 21 | )); 22 | $query->shouldReceive('lists')->atMost(1)->andReturn(array('nest.one')); 23 | $query->shouldReceive('pluck')->atMost(1)->andReturn(array('nest.one')); 24 | $dbData = $this->getDbData(); 25 | unset($dbData[1]); // remove the nest.one array member 26 | $query->shouldReceive('where')->with('key', '=', 'nest.one')->andReturn(m::self())->getMock() 27 | ->shouldReceive('update')->with(array('value' => 'nestone')); 28 | $self = $this; // 5.3 compatibility 29 | $query->shouldReceive('insert')->once()->andReturnUsing(function($arg) use($dbData, $self) { 30 | $self->assertEquals(count($dbData), count($arg)); 31 | foreach ($dbData as $key => $value) { 32 | $self->assertContains($value, $arg); 33 | } 34 | }); 35 | 36 | $store = $this->makeStore($connection); 37 | $store->set('foo', 'bar'); 38 | $store->set('nest.one', 'nestone'); 39 | $store->set('nest.two', 'nesttwo'); 40 | $store->set('array', array('one', 'two')); 41 | $store->save(); 42 | } 43 | 44 | /** @test */ 45 | public function extra_columns_are_queried() 46 | { 47 | $connection = $this->mockConnection(); 48 | $query = $this->mockQuery($connection); 49 | $query->shouldReceive('where')->once()->with('foo', '=', 'bar') 50 | ->andReturn(m::self())->getMock() 51 | ->shouldReceive('get')->once()->andReturn(array( 52 | array('key' => 'foo', 'value' => 'bar'), 53 | )); 54 | 55 | $store = $this->makeStore($connection); 56 | $store->setExtraColumns(array('foo' => 'bar')); 57 | $this->assertEquals('bar', $store->get('foo')); 58 | } 59 | 60 | /** */ 61 | public function extra_columns_are_inserted() 62 | { 63 | $connection = $this->mockConnection(); 64 | $query = $this->mockQuery($connection); 65 | $query->shouldReceive('where')->times(2)->with('extracol', '=', 'extradata') 66 | ->andReturn(m::self()); 67 | $query->shouldReceive('get')->once()->andReturn(array()); 68 | $query->shouldReceive('lists')->atMost(1)->andReturn(array()); 69 | $query->shouldReceive('pluck')->atMost(1)->andReturn(array()); 70 | $query->shouldReceive('insert')->once()->with(array( 71 | array('key' => 'foo', 'value' => 'bar', 'extracol' => 'extradata'), 72 | )); 73 | 74 | $store = $this->makeStore($connection); 75 | $store->set('foo', 'bar'); 76 | $store->setExtraColumns(array('extracol' => 'extradata')); 77 | $store->save(); 78 | } 79 | 80 | protected function getDbData() 81 | { 82 | return array( 83 | array('key' => 'foo', 'value' => 'bar'), 84 | array('key' => 'nest.one', 'value' => 'nestone'), 85 | array('key' => 'nest.two', 'value' => 'nesttwo'), 86 | array('key' => 'array.0', 'value' => 'one'), 87 | array('key' => 'array.1', 'value' => 'two'), 88 | ); 89 | } 90 | 91 | protected function mockConnection() 92 | { 93 | return m::mock('Illuminate\Database\Connection'); 94 | } 95 | 96 | protected function mockQuery($connection) 97 | { 98 | $query = m::mock('Illuminate\Database\Query\Builder'); 99 | $connection->shouldReceive('table')->andReturn($query); 100 | return $query; 101 | } 102 | 103 | protected function makeStore($connection) 104 | { 105 | return new anlutro\LaravelSettings\DatabaseSettingStore($connection); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/unit/HelperTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Laravel 5 feature.'); 17 | return; 18 | } 19 | 20 | //Container::setInstance(new Container); 21 | 22 | $store = m::mock('anlutro\LaravelSettings\SettingStore'); 23 | 24 | app()->bind('setting', function() use ($store) { 25 | return $store; 26 | }); 27 | } 28 | 29 | /** @test */ 30 | public function helper_without_parameters_returns_store() 31 | { 32 | $this->assertInstanceOf('anlutro\LaravelSettings\SettingStore', setting()); 33 | } 34 | 35 | /** @test */ 36 | public function single_parameter_get_a_key_from_store() 37 | { 38 | app('setting')->shouldReceive('get')->with('foo', null)->once(); 39 | 40 | $foo = setting('foo'); 41 | $this->assertEquals(null, $foo); 42 | } 43 | 44 | public function two_parameters_return_a_default_value() 45 | { 46 | app('setting')->shouldReceive('get')->with('foo', 'bar')->once(); 47 | 48 | setting('foo', 'bar'); 49 | } 50 | 51 | 52 | /** */ 53 | public function array_parameter_call_set_method_into_store() 54 | { 55 | app('setting')->shouldReceive('set')->with(['foo', 'bar'])->once(); 56 | 57 | setting(['foo', 'bar']); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/unit/JsonSettingStoreTest.php: -------------------------------------------------------------------------------- 1 | mockFilesystem(); 30 | $files->shouldReceive('exists')->once()->with('fakepath')->andReturn(true); 31 | $files->shouldReceive('isWritable')->once()->with('fakepath')->andReturn(false); 32 | $this->expectException('InvalidArgumentException'); 33 | $this->makeStore($files); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function throws_exception_when_files_put_fails() 40 | { 41 | $files = $this->mockFilesystem(); 42 | $files->shouldReceive('exists')->once()->with('fakepath')->andReturn(false); 43 | $files->shouldReceive('put')->once()->with('fakepath', '{}')->andReturn(false); 44 | $this->expectException('InvalidArgumentException'); 45 | $this->makeStore($files); 46 | } 47 | 48 | /** 49 | * @test 50 | */ 51 | public function throws_exception_when_file_contains_invalid_json() 52 | { 53 | $files = $this->mockFilesystem(); 54 | $files->shouldReceive('exists')->once()->with('fakepath')->andReturn(true); 55 | $files->shouldReceive('isWritable')->once()->with('fakepath')->andReturn(true); 56 | $files->shouldReceive('get')->once()->with('fakepath')->andReturn('[[!1!11]'); 57 | $this->expectException('RuntimeException'); 58 | $store = $this->makeStore($files); 59 | $store->get('foo'); 60 | } 61 | } 62 | --------------------------------------------------------------------------------