Skip to content

Commit 2724b28

Browse files
authored
Merge pull request #4062 from BookStackApp/settings_perf
Changed the way settings are loaded
2 parents 6545afa + 8bebea4 commit 2724b28

File tree

2 files changed

+85
-56
lines changed

2 files changed

+85
-56
lines changed

app/Settings/SettingService.php

Lines changed: 80 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,29 @@
33
namespace BookStack\Settings;
44

55
use BookStack\Auth\User;
6-
use Illuminate\Contracts\Cache\Repository as Cache;
76

87
/**
98
* Class SettingService
109
* The settings are a simple key-value database store.
1110
* For non-authenticated users, user settings are stored via the session instead.
11+
* A local array-based cache is used to for setting accesses across a request.
1212
*/
1313
class SettingService
1414
{
15-
protected Setting $setting;
16-
protected Cache $cache;
1715
protected array $localCache = [];
18-
protected string $cachePrefix = 'setting-';
19-
20-
public function __construct(Setting $setting, Cache $cache)
21-
{
22-
$this->setting = $setting;
23-
$this->cache = $cache;
24-
}
2516

2617
/**
2718
* Gets a setting from the database,
2819
* If not found, Returns default, Which is false by default.
2920
*/
30-
public function get(string $key, $default = null)
21+
public function get(string $key, $default = null): mixed
3122
{
3223
if (is_null($default)) {
3324
$default = config('setting-defaults.' . $key, false);
3425
}
3526

36-
if (isset($this->localCache[$key])) {
37-
return $this->localCache[$key];
38-
}
39-
4027
$value = $this->getValueFromStore($key) ?? $default;
41-
$formatted = $this->formatValue($value, $default);
42-
$this->localCache[$key] = $formatted;
43-
44-
return $formatted;
28+
return $this->formatValue($value, $default);
4529
}
4630

4731
/**
@@ -79,52 +63,78 @@ public function getForCurrentUser(string $key, $default = null)
7963
}
8064

8165
/**
82-
* Gets a setting value from the cache or database.
83-
* Looks at the system defaults if not cached or in database.
84-
* Returns null if nothing is found.
66+
* Gets a setting value from the local cache.
67+
* Will load the local cache if not previously loaded.
8568
*/
86-
protected function getValueFromStore(string $key)
69+
protected function getValueFromStore(string $key): mixed
8770
{
88-
// Check the cache
89-
$cacheKey = $this->cachePrefix . $key;
90-
$cacheVal = $this->cache->get($cacheKey, null);
91-
if ($cacheVal !== null) {
92-
return $cacheVal;
71+
$cacheCategory = $this->localCacheCategory($key);
72+
if (!isset($this->localCache[$cacheCategory])) {
73+
$this->loadToLocalCache($cacheCategory);
9374
}
9475

95-
// Check the database
96-
$settingObject = $this->getSettingObjectByKey($key);
97-
if ($settingObject !== null) {
98-
$value = $settingObject->value;
76+
return $this->localCache[$cacheCategory][$key] ?? null;
77+
}
9978

100-
if ($settingObject->type === 'array') {
101-
$value = json_decode($value, true) ?? [];
102-
}
79+
/**
80+
* Put the given value into the local cached under the given key.
81+
*/
82+
protected function putValueIntoLocalCache(string $key, mixed $value): void
83+
{
84+
$cacheCategory = $this->localCacheCategory($key);
85+
if (!isset($this->localCache[$cacheCategory])) {
86+
$this->loadToLocalCache($cacheCategory);
87+
}
10388

104-
$this->cache->forever($cacheKey, $value);
89+
$this->localCache[$cacheCategory][$key] = $value;
90+
}
10591

106-
return $value;
92+
/**
93+
* Get the category for the given setting key.
94+
* Will return 'app' for a general app setting otherwise 'user:<user_id>' for a user setting.
95+
*/
96+
protected function localCacheCategory(string $key): string
97+
{
98+
if (str_starts_with($key, 'user:')) {
99+
return implode(':', array_slice(explode(':', $key), 0, 2));
107100
}
108101

109-
return null;
102+
return 'app';
110103
}
111104

112105
/**
113-
* Clear an item from the cache completely.
106+
* For the given category, load the relevant settings from the database into the local cache.
114107
*/
115-
protected function clearFromCache(string $key)
108+
protected function loadToLocalCache(string $cacheCategory): void
116109
{
117-
$cacheKey = $this->cachePrefix . $key;
118-
$this->cache->forget($cacheKey);
119-
if (isset($this->localCache[$key])) {
120-
unset($this->localCache[$key]);
110+
$query = Setting::query();
111+
112+
if ($cacheCategory === 'app') {
113+
$query->where('setting_key', 'not like', 'user:%');
114+
} else {
115+
$query->where('setting_key', 'like', $cacheCategory . ':%');
116+
}
117+
$settings = $query->toBase()->get();
118+
119+
if (!isset($this->localCache[$cacheCategory])) {
120+
$this->localCache[$cacheCategory] = [];
121+
}
122+
123+
foreach ($settings as $setting) {
124+
$value = $setting->value;
125+
126+
if ($setting->type === 'array') {
127+
$value = json_decode($value, true) ?? [];
128+
}
129+
130+
$this->localCache[$cacheCategory][$setting->setting_key] = $value;
121131
}
122132
}
123133

124134
/**
125135
* Format a settings value.
126136
*/
127-
protected function formatValue($value, $default)
137+
protected function formatValue(mixed $value, mixed $default): mixed
128138
{
129139
// Change string booleans to actual booleans
130140
if ($value === 'true') {
@@ -155,21 +165,22 @@ public function has(string $key): bool
155165
* Add a setting to the database.
156166
* Values can be an array or a string.
157167
*/
158-
public function put(string $key, $value): bool
168+
public function put(string $key, mixed $value): bool
159169
{
160-
$setting = $this->setting->newQuery()->firstOrNew([
170+
$setting = Setting::query()->firstOrNew([
161171
'setting_key' => $key,
162172
]);
173+
163174
$setting->type = 'string';
175+
$setting->value = $value;
164176

165177
if (is_array($value)) {
166178
$setting->type = 'array';
167-
$value = $this->formatArrayValue($value);
179+
$setting->value = $this->formatArrayValue($value);
168180
}
169181

170-
$setting->value = $value;
171182
$setting->save();
172-
$this->clearFromCache($key);
183+
$this->putValueIntoLocalCache($key, $value);
173184

174185
return true;
175186
}
@@ -209,7 +220,7 @@ public function putUser(User $user, string $key, string $value): bool
209220
* Can only take string value types since this may use
210221
* the session which is less flexible to data types.
211222
*/
212-
public function putForCurrentUser(string $key, string $value)
223+
public function putForCurrentUser(string $key, string $value): bool
213224
{
214225
return $this->putUser(user(), $key, $value);
215226
}
@@ -231,15 +242,19 @@ public function remove(string $key): void
231242
if ($setting) {
232243
$setting->delete();
233244
}
234-
$this->clearFromCache($key);
245+
246+
$cacheCategory = $this->localCacheCategory($key);
247+
if (isset($this->localCache[$cacheCategory])) {
248+
unset($this->localCache[$cacheCategory][$key]);
249+
}
235250
}
236251

237252
/**
238253
* Delete settings for a given user id.
239254
*/
240-
public function deleteUserSettings(string $userId)
255+
public function deleteUserSettings(string $userId): void
241256
{
242-
return $this->setting->newQuery()
257+
Setting::query()
243258
->where('setting_key', 'like', $this->userKey($userId) . '%')
244259
->delete();
245260
}
@@ -249,7 +264,16 @@ public function deleteUserSettings(string $userId)
249264
*/
250265
protected function getSettingObjectByKey(string $key): ?Setting
251266
{
252-
return $this->setting->newQuery()
253-
->where('setting_key', '=', $key)->first();
267+
return Setting::query()
268+
->where('setting_key', '=', $key)
269+
->first();
270+
}
271+
272+
/**
273+
* Empty the local setting value cache used by this service.
274+
*/
275+
public function flushCache(): void
276+
{
277+
$this->localCache = [];
254278
}
255279
}

tests/Commands/UpdateUrlCommandTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public function test_command_updates_settings()
3939
setting()->put('my-custom-item', 'https://example.com/donkey/cat');
4040
$this->runUpdate('https://example.com', 'https://cats.example.com');
4141

42+
setting()->flushCache();
43+
4244
$settingVal = setting('my-custom-item');
4345
$this->assertEquals('https://cats.example.com/donkey/cat', $settingVal);
4446
}
@@ -47,6 +49,9 @@ public function test_command_updates_array_settings()
4749
{
4850
setting()->put('my-custom-array-item', [['name' => 'a https://example.com/donkey/cat url']]);
4951
$this->runUpdate('https://example.com', 'https://cats.example.com');
52+
53+
setting()->flushCache();
54+
5055
$settingVal = setting('my-custom-array-item');
5156
$this->assertEquals('a https://cats.example.com/donkey/cat url', $settingVal[0]['name']);
5257
}

0 commit comments

Comments
 (0)