Skip to content

Commit bd755bd

Browse files
committed
Add context
1 parent c7d6c8f commit bd755bd

7 files changed

Lines changed: 206 additions & 33 deletions

File tree

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,34 @@ it effectively resets itself back to the default value in config file, if any.
8080
service('setting')->forget('App.siteName')
8181
```
8282

83+
### Contextual Settings
84+
85+
In addition to the default behavior describe above, `Settings` can can be used to define "contextual settings".
86+
A context may be anything you want, but common examples are a runtime environment or an authenticated user.
87+
In order to use a context you pass it as an additional parameter to the `get()`/`set()`/`forget()` methods; if
88+
a context setting is requested and does not exist then the default global value will be used.
89+
90+
Contexts may be any unique string you choose, but a recommended format for supplying some consistency is to
91+
given them a category and identifier, like `environment:production` or `group:42`.
92+
93+
An example... Say your App config includes the name of a theme to use to enhance your display. By default
94+
your config file specifies `App.theme = 'default'`. When a user changes their theme, you do not want this to
95+
change the theme for all visitors to the site, so you need to provide the user as the *context* for the change:
96+
97+
```php
98+
$context = 'user:' . user_id();
99+
service('setting')->set('App.theme', 'dark', $context);
100+
```
101+
102+
Now when your filter is determining which theme to apply it can check for the current user as the context:
103+
104+
```php
105+
$context = 'user:' . user_id();
106+
$theme = service('setting')->get('App.theme', $context);
107+
```
108+
109+
If that context is not found the library falls back to the global value, i.e. `service('setting')->get('App.theme')`
110+
83111
### Using the Helper
84112

85113
The helper provides a shortcut to the using the service. It must first be loaded using the `helper()` method
@@ -100,6 +128,8 @@ setting()->set('App.siteName', 'My Great Site');
100128
setting()->forget('App.siteName');
101129
```
102130

131+
> Note: Due to the shorthand nature of the helper function it cannot access contextual settings.
132+
103133
## Known Limitations
104134

105135
The following are known limitations of the library:
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Sparks\Settings\Database\Migrations;
4+
5+
use CodeIgniter\Database\Migration;
6+
7+
class AddContextColumn extends Migration
8+
{
9+
public function up()
10+
{
11+
$this->forge->addColumn(config('Settings')->database['table'], [
12+
'context' => [
13+
'type' => 'varchar',
14+
'constraint' => 255,
15+
'null' => true,
16+
'after' => 'type',
17+
],
18+
]);
19+
}
20+
21+
public function down()
22+
{
23+
$this->forge->dropColumn(config('Settings')->database['table'], 'context');
24+
}
25+
}

src/Handlers/BaseHandler.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22

33
namespace Sparks\Settings\Handlers;
44

5+
use RuntimeException;
6+
57
abstract class BaseHandler
68
{
9+
/**
10+
* Checks whether this handler has a value set.
11+
*/
12+
abstract public function has(string $class, string $property, ?string $context = null): bool;
13+
714
/**
815
* Returns a single value from the handler, if stored.
916
*
1017
* @return mixed
1118
*/
12-
abstract public function get(string $class, string $property);
19+
abstract public function get(string $class, string $property, ?string $context = null);
1320

1421
/**
1522
* If the Handler supports saving values, it
@@ -18,11 +25,27 @@ abstract public function get(string $class, string $property);
1825
*
1926
* @param mixed $value
2027
*
28+
* @throws RuntimeException
29+
*
30+
* @return mixed
31+
*/
32+
public function set(string $class, string $property, $value = null, ?string $context = null)
33+
{
34+
throw new RuntimeException('Set method not implemented for current Settings handler.');
35+
}
36+
37+
/**
38+
* If the Handler supports forgetting values, it
39+
* MUST override this method to provide that functionality.
40+
* Not all Handlers will support writing values.
41+
*
42+
* @throws RuntimeException
43+
*
2144
* @return mixed
2245
*/
23-
public function set(string $class, string $property, $value = null)
46+
public function forget(string $class, string $property, ?string $context = null)
2447
{
25-
throw new \RuntimeException('Set method not implemented for current Settings handler.');
48+
throw new RuntimeException('Forget method not implemented for current Settings handler.');
2649
}
2750

2851
/**

src/Handlers/DatabaseHandler.php

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Sparks\Settings\Handlers;
44

55
use CodeIgniter\I18n\Time;
6+
use RuntimeException;
67

78
/**
89
* Provides database storage for Settings.
@@ -34,6 +35,20 @@ class DatabaseHandler extends BaseHandler
3435
*/
3536
private $table;
3637

38+
/**
39+
* Checks whether this handler has a value set.
40+
*/
41+
public function has(string $class, string $property, ?string $context = null): bool
42+
{
43+
$this->hydrate();
44+
45+
if (! isset($this->settings[$class][$property])) {
46+
return false;
47+
}
48+
49+
return array_key_exists($context ?? 0, $this->settings[$class][$property]);
50+
}
51+
3752
/**
3853
* Attempt to retrieve a value from the database.
3954
* To boost performance, all of the values are
@@ -42,15 +57,13 @@ class DatabaseHandler extends BaseHandler
4257
*
4358
* @return mixed|null
4459
*/
45-
public function get(string $class, string $property)
60+
public function get(string $class, string $property, ?string $context = null)
4661
{
47-
$this->hydrate();
48-
49-
if (! isset($this->settings[$class]) || ! isset($this->settings[$class][$property])) {
62+
if (! $this->has($class, $property, $context)) {
5063
return null;
5164
}
5265

53-
return $this->parseValue(...$this->settings[$class][$property]);
66+
return $this->parseValue(...$this->settings[$class][$property][$context ?? 0]);
5467
}
5568

5669
/**
@@ -60,21 +73,22 @@ public function get(string $class, string $property)
6073
*
6174
* @return mixed|void
6275
*/
63-
public function set(string $class, string $property, $value = null)
76+
public function set(string $class, string $property, $value = null, ?string $context = null)
6477
{
6578
$this->hydrate();
6679
$time = Time::now()->format('Y-m-d H:i:s');
6780
$type = gettype($value);
6881
$value = $this->prepareValue($value);
6982

7083
// If we found it in our cache, then we need to update
71-
if (isset($this->settings[$class][$property])) {
84+
if (isset($this->settings[$class][$property][$context ?? 0])) {
7285
$result = db_connect()->table($this->table)
7386
->where('class', $class)
7487
->where('key', $property)
7588
->update([
7689
'value' => $value,
7790
'type' => $type,
91+
'context' => $context,
7892
'updated_at' => $time,
7993
]);
8094
} else {
@@ -84,6 +98,7 @@ public function set(string $class, string $property, $value = null)
8498
'key' => $property,
8599
'value' => $value,
86100
'type' => $type,
101+
'context' => $context,
87102
'created_at' => $time,
88103
'updated_at' => $time,
89104
]);
@@ -94,8 +109,11 @@ public function set(string $class, string $property, $value = null)
94109
if (! array_key_exists($class, $this->settings)) {
95110
$this->settings[$class] = [];
96111
}
112+
if (! array_key_exists($property, $this->settings[$class])) {
113+
$this->settings[$class][$property] = [];
114+
}
97115

98-
$this->settings[$class][$property] = [
116+
$this->settings[$class][$property][$context ?? 0] = [
99117
$value,
100118
$type,
101119
];
@@ -108,28 +126,31 @@ public function set(string $class, string $property, $value = null)
108126
* Deletes the record from persistent storage, if found,
109127
* and from the local cache.
110128
*/
111-
public function forget(string $class, string $property)
129+
public function forget(string $class, string $property, ?string $context = null)
112130
{
113131
$this->hydrate();
114132

115133
// Delete from persistent storage
116134
$result = db_connect()->table($this->table)
117135
->where('class', $class)
118136
->where('key', $property)
137+
->where('context', $context)
119138
->delete();
120139

121140
if (! $result) {
122141
return $result;
123142
}
124143

125144
// Delete from local storage
126-
unset($this->settings[$class][$property]);
145+
unset($this->settings[$class][$property][$context ?? 0]);
127146

128147
return $result;
129148
}
130149

131150
/**
132151
* Ensures we've pulled all of the values from the database.
152+
*
153+
* @throws RuntimeException
133154
*/
134155
private function hydrate()
135156
{
@@ -142,7 +163,7 @@ private function hydrate()
142163
$rawValues = db_connect()->table($this->table)->get();
143164

144165
if (is_bool($rawValues)) {
145-
throw new \RuntimeException(db_connect()->error()['message'] ?? 'Error reading from database.');
166+
throw new RuntimeException(db_connect()->error()['message'] ?? 'Error reading from database.');
146167
}
147168

148169
$rawValues = $rawValues->getResultObject();
@@ -151,8 +172,11 @@ private function hydrate()
151172
if (! array_key_exists($row->class, $this->settings)) {
152173
$this->settings[$row->class] = [];
153174
}
175+
if (! array_key_exists($row->key, $this->settings[$row->class])) {
176+
$this->settings[$row->class][$row->key] = [];
177+
}
154178

155-
$this->settings[$row->class][$row->key] = [
179+
$this->settings[$row->class][$row->key][$row->context ?? 0] = [
156180
$row->value,
157181
$row->type,
158182
];

src/Settings.php

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Sparks\Settings;
44

5+
use InvalidArgumentException;
6+
use RuntimeException;
57
use Sparks\Settings\Config\Settings as SettingsConfig;
68

79
/**
@@ -52,23 +54,26 @@ public function __construct(?SettingsConfig $config = null)
5254
}
5355

5456
/**
55-
* Retrieve a value from either the database
57+
* Retrieve a value from any handler
5658
* or from a config file matching the name
5759
* file.arg.optionalArg
5860
*/
59-
public function get(string $key)
61+
public function get(string $key, ?string $context = null)
6062
{
6163
[$class, $property, $config] = $this->prepareClassAndProperty($key);
6264

63-
// Try grabbing the values from any of our handlers
65+
// Check each of our handlers
6466
foreach ($this->handlers as $name => $handler) {
65-
$value = $handler->get($class, $property);
66-
67-
if ($value !== null) {
68-
return $value;
67+
if ($handler->has($class, $property, $context)) {
68+
return $handler->get($class, $property, $context);
6969
}
7070
}
7171

72+
// If no contextual value was found then fall back on a global
73+
if ($context !== null) {
74+
return $this->get($key);
75+
}
76+
7277
return $config->{$property} ?? null;
7378
}
7479

@@ -79,38 +84,36 @@ public function get(string $key)
7984
*
8085
* @return void|null
8186
*/
82-
public function set(string $key, $value = null)
87+
public function set(string $key, $value = null, ?string $context = null)
8388
{
8489
[$class, $property] = $this->prepareClassAndProperty($key);
8590

86-
$handler = $this->getWriteHandler();
87-
88-
return $handler->set($class, $property, $value);
91+
return $this->getWriteHandler()->set($class, $property, $value, $context);
8992
}
9093

9194
/**
9295
* Removes a setting from the persistent storage,
9396
* effectively returning the value to the default value
9497
* found in the config file, if any.
9598
*/
96-
public function forget(string $key)
99+
public function forget(string $key, ?string $context = null)
97100
{
98101
[$class, $property] = $this->prepareClassAndProperty($key);
99102

100-
$handler = $this->getWriteHandler();
101-
102-
return $handler->forget($class, $property);
103+
return $this->getWriteHandler()->forget($class, $property, $context);
103104
}
104105

105106
/**
106107
* Returns the handler that is set to store values.
107108
*
109+
* @throws RuntimeException
110+
*
108111
* @return mixed
109112
*/
110113
private function getWriteHandler()
111114
{
112115
if (empty($this->writeHandler) || ! isset($this->handlers[$this->writeHandler])) {
113-
throw new \RuntimeException('Unable to find a Settings handler that can store values.');
116+
throw new RuntimeException('Unable to find a Settings handler that can store values.');
114117
}
115118

116119
return $this->handlers[$this->writeHandler];
@@ -119,6 +122,8 @@ private function getWriteHandler()
119122
/**
120123
* Analyzes the given key and breaks it into the class.field parts.
121124
*
125+
* @throws InvalidArgumentException
126+
*
122127
* @return string[]
123128
*/
124129
private function parseDotSyntax(string $key): array
@@ -127,7 +132,7 @@ private function parseDotSyntax(string $key): array
127132
$parts = explode('.', $key);
128133

129134
if (count($parts) === 1) {
130-
throw new \RuntimeException('$field must contain both the class and field name, i.e. Foo.bar');
135+
throw new InvalidArgumentException('$key must contain both the class and field name, i.e. Foo.bar');
131136
}
132137

133138
return $parts;

tests/HelperTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public function testReturnsServiceByDefault()
2828

2929
public function testThrowsExceptionWithInvalidField()
3030
{
31-
$this->expectException(\RuntimeException::class);
31+
$this->expectException('InvalidArgumentException');
32+
$this->expectExceptionMessage('$key must contain both the class and field name, i.e. Foo.bar');
3233

3334
setting('Foobar');
3435
}

0 commit comments

Comments
 (0)