Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.05% covered (warning)
82.05%
32 / 39
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
SettingsManager
82.05% covered (warning)
82.05%
32 / 39
75.00% covered (warning)
75.00%
3 / 4
9.47
0.00% covered (danger)
0.00%
0 / 1
 write
70.83% covered (warning)
70.83%
17 / 24
0.00% covered (danger)
0.00%
0 / 1
4.40
 read
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 clearCache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCacheConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3
4namespace App\Utility;
5
6use Cake\Cache\Cache;
7use Cake\ORM\TableRegistry;
8use InvalidArgumentException;
9
10/**
11 * Class SettingsManager
12 *
13 * This utility class manages application settings with caching capabilities.
14 * It uses CakePHP's Cache and ORM components to efficiently retrieve and store settings.
15 */
16class SettingsManager
17{
18    /**
19     * @var string The cache configuration name used for storing settings.
20     */
21    private static string $cacheConfig = 'settings_cache';
22
23    /**
24     * Writes a setting value to the database and updates the cache.
25     *
26     * This method updates a setting value identified by the given path. It updates the value in the database
27     * using the Settings table and then updates the cache to reflect the change.
28     *
29     * @param string $path The dot-separated path to the setting (e.g., 'category.key_name').
30     * @param mixed $value The value to set for the specified setting.
31     * @return bool True if the setting was successfully updated, false otherwise.
32     * @throws \InvalidArgumentException If the path format is invalid.
33     * @throws \RuntimeException If the Settings table cannot be accessed.
34     */
35    public static function write(string $path, mixed $value): bool
36    {
37        $parts = explode('.', $path);
38        if (count($parts) !== 2) {
39            throw new InvalidArgumentException(
40                'Invalid path format. Must be in the format "category.key_name"',
41            );
42        }
43
44        [$category, $keyName] = $parts;
45        $cacheKey = 'setting_' . str_replace('.', '_', $path);
46
47        $settingsTable = TableRegistry::getTableLocator()->get('Settings');
48
49        // Find the existing setting
50        $setting = $settingsTable->find()
51            ->where([
52                'category' => $category,
53                'key_name' => $keyName,
54            ])
55            ->first();
56
57        if (!$setting) {
58            throw new InvalidArgumentException(
59                sprintf('Setting not found: %s.%s', $category, $keyName),
60            );
61        }
62
63        // Update the setting
64        $setting->value = $value;
65
66        if ($settingsTable->save($setting)) {
67            // Update the cache
68            Cache::write($cacheKey, $value, self::$cacheConfig);
69
70            // Also clear the category-level cache if it exists
71            Cache::delete('setting_' . $category, self::$cacheConfig);
72
73            return true;
74        }
75
76        return false;
77    }
78
79    /**
80     * Reads a setting value from the cache or database.
81     *
82     * This method attempts to read a setting value identified by the given path. It first checks the cache
83     * for the value. If the value is not found in the cache, it retrieves the value from the database using
84     * the Settings table. The result is then cached to optimize future requests.
85     *
86     * The method supports two types of reads:
87     * 1. Reading all settings for a category (e.g., 'ImageSizes')
88     * 2. Reading a specific setting within a category (e.g., 'ImageSizes.medium')
89     *
90     * @param string $path The dot-separated path to the setting (e.g., 'category' or 'category.key_name').
91     * @param mixed $default The default value to return if the setting is not found.
92     * @return mixed The setting value if found, otherwise the default value.
93     *               For category-level requests, returns an array of all settings in that category.
94     *               For specific setting requests, returns the value of that setting.
95     * @throws \RuntimeException If the Settings table cannot be accessed.
96     */
97    public static function read(string $path, mixed $default = null): mixed
98    {
99        $cacheKey = 'setting_' . str_replace('.', '_', $path);
100
101        $value = Cache::read($cacheKey, self::$cacheConfig);
102        if ($value !== null) {
103            return $value;
104        }
105
106        $parts = explode('.', $path);
107        $category = $parts[0] ?? null;
108        $keyName = $parts[1] ?? null;
109
110        $settingsTable = TableRegistry::getTableLocator()->get('Settings');
111
112        if ($keyName === null) {
113            // Fetch all settings for the category
114            $value = $settingsTable->getSettingValue($category);
115        } else {
116            // Fetch a single setting
117            $value = $settingsTable->getSettingValue($category, $keyName);
118        }
119
120        // Cache the result, even if it's null, to avoid repeated database queries
121        Cache::write($cacheKey, $value, self::$cacheConfig);
122
123        return $value ?? $default;
124    }
125
126    /**
127     * Clears the settings cache.
128     *
129     * Removes all cached settings to ensure fresh data retrieval on subsequent reads.
130     *
131     * @return void
132     */
133    public static function clearCache(): void
134    {
135        Cache::clear(self::$cacheConfig);
136    }
137
138    /**
139     * Get the cache configuration name. Useful for testcases.
140     *
141     * @return string
142     */
143    public static function getCacheConfig(): string
144    {
145        return self::$cacheConfig;
146    }
147}