Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
86 / 86 |
|
100.00% |
8 / 8 |
CRAP | |
100.00% |
1 / 1 |
ConfigController | |
100.00% |
86 / 86 |
|
100.00% |
8 / 8 |
28 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
index | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
edit | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
save | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
4 | |||
validation | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
6 | |||
parseOptions | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
activePage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
validateEvent | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
7 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Engelsystem\Controllers\Admin; |
6 | |
7 | use Engelsystem\Config\Config; |
8 | use Engelsystem\Controllers\BaseController; |
9 | use Engelsystem\Controllers\HasUserNotifications; |
10 | use Engelsystem\Helpers\Carbon; |
11 | use Engelsystem\Http\Exceptions\HttpNotFound; |
12 | use Engelsystem\Http\Redirector; |
13 | use Engelsystem\Http\Request; |
14 | use Engelsystem\Http\Response; |
15 | use Engelsystem\Http\UrlGeneratorInterface; |
16 | use Engelsystem\Models\EventConfig; |
17 | use Illuminate\Support\Str; |
18 | use InvalidArgumentException; |
19 | use Psr\Log\LoggerInterface; |
20 | |
21 | class ConfigController extends BaseController |
22 | { |
23 | use HasUserNotifications; |
24 | |
25 | protected array $permissions = [ |
26 | 'config.edit', |
27 | ]; |
28 | |
29 | protected array $options = [ |
30 | /** |
31 | * '[name]' => [ |
32 | * 'title' => '[title], # Optional, default config.[name] |
33 | * 'permission' => '[permission]' # Optional, string or array |
34 | * 'icon' => '[icon]', # Optional, default gear-fill |
35 | * 'validation' => callable, # Optional. callable to validate the request |
36 | * 'config' => [ |
37 | * '[name]' => [ |
38 | * 'name' => 'some.value', # Optional, default: config.[name] |
39 | * 'type' => 'string', # string, text, datetime-local, ... |
40 | * 'default' => '[value]', # Optional |
41 | * 'required' => true, # Optional, default false |
42 | * # Optional config.[name].info for information messages |
43 | * # Optionally other options used by the correlating field |
44 | * ], |
45 | * ], |
46 | * ], |
47 | */ |
48 | 'event' => [ |
49 | 'config' => [ |
50 | 'name' => [ |
51 | 'type' => 'string', |
52 | ], |
53 | 'welcome_msg' => [ |
54 | 'type' => 'text', |
55 | 'rows' => 5, |
56 | ], |
57 | 'buildup_start' => [ |
58 | 'type' => 'datetime-local', |
59 | ], |
60 | 'event_start' => [ |
61 | 'type' => 'datetime-local', |
62 | ], |
63 | 'event_end' => [ |
64 | 'type' => 'datetime-local', |
65 | ], |
66 | 'teardown_end' => [ |
67 | 'type' => 'datetime-local', |
68 | ], |
69 | ], |
70 | ], |
71 | ]; |
72 | |
73 | public function __construct( |
74 | protected Response $response, |
75 | protected Config $config, |
76 | protected Redirector $redirect, |
77 | protected UrlGeneratorInterface $url, |
78 | protected LoggerInterface $log, |
79 | array $options = [], |
80 | ) { |
81 | $this->options += $options; |
82 | $this->parseOptions(); |
83 | } |
84 | |
85 | public function index(): Response |
86 | { |
87 | return $this->redirect->to('/admin/config/' . array_key_first($this->options)); |
88 | } |
89 | |
90 | public function edit(Request $request): Response |
91 | { |
92 | $page = $this->activePage($request); |
93 | |
94 | return $this->response->withView( |
95 | 'admin/config/index', |
96 | [ |
97 | 'page' => $page, |
98 | 'title' => $this->options[$page]['title'], |
99 | 'config' => $this->options[$page]['config'], |
100 | 'options' => $this->options, |
101 | ] |
102 | ); |
103 | } |
104 | |
105 | public function save(Request $request): Response |
106 | { |
107 | $page = $this->activePage($request); |
108 | $data = $this->validation($page, $request); |
109 | $settings = $this->options[$page]['config']; |
110 | |
111 | $changes = []; |
112 | foreach ($settings as $key => $options) { |
113 | $value = $data[$key] ?? $options['default'] ?? null; |
114 | |
115 | $value = match ($options['type']) { |
116 | 'datetime-local' => $value ? Carbon::createFromDatetime($value) : $value, |
117 | default => $value, |
118 | }; |
119 | |
120 | if ($this->config->get($key) == $value) { |
121 | continue; |
122 | } |
123 | |
124 | $changes[] = sprintf('%s = "%s"', $key, $value); |
125 | |
126 | (new EventConfig()) |
127 | ->findOrNew($key) |
128 | ->setAttribute('name', $key) |
129 | ->setAttribute('value', $value) |
130 | ->save(); |
131 | } |
132 | |
133 | $this->log->info( |
134 | 'Updated {page} configuration: {changes}', |
135 | [ |
136 | 'page' => $page, |
137 | 'changes' => implode(', ', $changes), |
138 | ] |
139 | ); |
140 | |
141 | $this->addNotification('config.edit.success'); |
142 | |
143 | return $this->redirect->back(); |
144 | } |
145 | |
146 | protected function validation(string $page, Request $request): array |
147 | { |
148 | $rules = []; |
149 | $config = $this->options[$page]; |
150 | $settings = $config['config']; |
151 | |
152 | // Generate validation rules |
153 | foreach ($settings as $key => $setting) { |
154 | $validation = []; |
155 | $validation[] = empty($setting['required']) ? 'optional' : 'required'; |
156 | |
157 | match ($setting['type']) { |
158 | 'string', 'text' => null, // Anything is valid here when optional |
159 | 'datetime-local' => $validation[] = 'date_time', |
160 | default => throw new InvalidArgumentException( |
161 | 'Type ' . $setting['type'] . ' of ' . $key . ' not defined' |
162 | ), |
163 | }; |
164 | |
165 | $rules[$key] = implode('|', $validation); |
166 | } |
167 | |
168 | if (!empty($config['validation']) || method_exists($this, 'validate' . Str::ucfirst($page))) { |
169 | $callback = $config['validation'] ?? null; |
170 | if (!is_callable($callback)) { |
171 | // Used until proper dynamic config loading is implemented |
172 | $callback = [$this, 'validate' . Str::ucfirst($page)]; |
173 | } |
174 | |
175 | return $callback($request, $rules); |
176 | } |
177 | |
178 | return $this->validate($request, $rules); |
179 | } |
180 | |
181 | protected function parseOptions(): void |
182 | { |
183 | foreach ($this->options as $key => $value) { |
184 | // Add page URLs |
185 | $this->options[$key]['url'] = $this->url->to('/admin/config/' . $key); |
186 | |
187 | // Configure page translation names |
188 | if (empty($this->options[$key]['title'])) { |
189 | $this->options[$key]['title'] = 'config.' . $key; |
190 | } |
191 | |
192 | // Iterate over settings |
193 | foreach ($this->options[$key]['config'] as $name => $config) { |
194 | // Set name for translation |
195 | if (empty($this->options[$key]['config'][$name]['name'])) { |
196 | $this->options[$key]['config'][$name]['name'] = 'config.' . $name; |
197 | } |
198 | |
199 | // Configure required icon |
200 | if (!empty($this->options[$key]['config'][$name]['required'])) { |
201 | $this->options[$key]['config'][$name]['required_icon'] = true; |
202 | } |
203 | } |
204 | } |
205 | } |
206 | |
207 | protected function activePage(Request $request): string |
208 | { |
209 | $page = $request->getAttribute('page'); |
210 | |
211 | if (empty($this->options[$page])) { |
212 | throw new HttpNotFound(); |
213 | } |
214 | |
215 | return $page; |
216 | } |
217 | |
218 | protected function validateEvent(Request $request, array $rules): array |
219 | { |
220 | // Run general validation |
221 | $data = $this->validate($request, $rules); |
222 | $addedRules = []; |
223 | |
224 | // Ensure event dates are after each other |
225 | $dates = ['buildup_start', 'event_start', 'event_end', 'teardown_end']; |
226 | foreach ($dates as $i => $dateField) { |
227 | if (!$i || !$data[$dateField]) { |
228 | continue; |
229 | } |
230 | |
231 | foreach (array_slice($dates, 0, $i) as $previousDateField) { |
232 | if (!$data[$previousDateField]) { |
233 | continue; |
234 | } |
235 | |
236 | $addedRules[$dateField][] = ['after', $data[$previousDateField], 'true']; |
237 | } |
238 | } |
239 | |
240 | if (!empty($addedRules)) { |
241 | $this->validate($request, $addedRules); |
242 | } |
243 | |
244 | return $data; |
245 | } |
246 | } |