Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
ConfigController
100.00% covered (success)
100.00%
86 / 86
100.00% covered (success)
100.00%
8 / 8
28
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 index
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 edit
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
4
 validation
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
6
 parseOptions
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 activePage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 validateEvent
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2
3declare(strict_types=1);
4
5namespace Engelsystem\Controllers\Admin;
6
7use Engelsystem\Config\Config;
8use Engelsystem\Controllers\BaseController;
9use Engelsystem\Controllers\HasUserNotifications;
10use Engelsystem\Helpers\Carbon;
11use Engelsystem\Http\Exceptions\HttpNotFound;
12use Engelsystem\Http\Redirector;
13use Engelsystem\Http\Request;
14use Engelsystem\Http\Response;
15use Engelsystem\Http\UrlGeneratorInterface;
16use Engelsystem\Models\EventConfig;
17use Illuminate\Support\Str;
18use InvalidArgumentException;
19use Psr\Log\LoggerInterface;
20
21class 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}