Cancelled
Branch main (caeba9)
by Sammy
12:38 queued 10:31
created

ORM::addErrors()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace HexMakina\kadro\Controllers;
4
5
use HexMakina\BlackBox\ORM\ModelInterface;
6
use HexMakina\BlackBox\Controllers\ORMInterface;
7
use HexMakina\LeMarchand\LeMarchand;
8
9
abstract class ORM extends Kadro implements ORMInterface
10
{
11
    protected $model_class_name = null;
12
    protected $model_type = null;
13
14
    protected $load_model = null;
15
    protected $form_model = null;
16
17
18
    public function addErrors($errors)
19
    {
20
        foreach ($errors as $err) {
21
            if (is_array($err)) {
22
                $this->addError(array_unshift($err), array_unshift($err));
23
            } else {
24
                $this->addError($err);
25
            }
26
        }
27
    }
28
29
    public function loadModel(): ?ModelInterface
30
    {
31
        return $this->load_model;
32
    }
33
34
    public function formModel(ModelInterface $setter = null): ModelInterface
35
    {
36
        if (!is_null($setter)) {
37
            $this->form_model = $setter;
38
        } elseif (is_null($this->form_model)) {
39
            $reflection = new \ReflectionClass($this->modelClassName());
40
            $this->form_model = $reflection->newInstanceWithoutConstructor(); //That's it!
41
        }
42
        return $this->form_model;
43
    }
44
45
    // shortcut to model_type
46
    public function modelType(): string
47
    {
48
      // have to go through the model to garantee model_type existence via interface
49
        if (is_null($this->model_type)) {
50
            $this->model_type = get_class($this->formModel())::model_type();
51
        }
52
53
        return $this->model_type;
54
    }
55
56
    public function modelPrefix($suffix = null): string
57
    {
58
        $ret = $this->modelType();
59
60
        if (!is_null($suffix)) {
61
            $ret .= '_' . $suffix;
62
        }
63
64
        return $ret;
65
    }
66
67
    public function prepare()
68
    {
69
        parent::prepare();
70
71
        $this->model_type = $this->modelClassName()::model_type();
72
73
        $pk_values = [];
74
75
        if ($this->router()->submits()) {
76
            $this->formModel()->import($this->sanitize_post_data($this->router()->submitted()));
77
            $pk_values = $this->modelClassName()::table()->primaryKeysMatch($this->router()->submitted());
78
79
            $this->load_model = $this->modelClassName()::exists($pk_values);
80
        } elseif ($this->router()->requests()) {
81
            $pk_values = $this->modelClassName()::table()->primaryKeysMatch($this->router()->params());
82
83
            if (!is_null($this->load_model = $this->modelClassName()::exists($pk_values))) {
84
                $this->formModel(clone $this->load_model);
85
            }
86
        }
87
        // TODO restore model history
88
        // if (!is_null($this->load_model) && is_subclass_of($this->load_model, '\HexMakina\Tracer\TraceableInterface') && $this->load_model->traceable()) {
89
        //   // $traces = $this->tracer()->traces_by_model($this->load_model);
90
        //     $traces = $this->load_model->traces();
91
        //     //$this->tracer()->history_by_model($this->load_model);
92
        //     $this->viewport('load_model_history', $traces ?? []);
93
        // }
94
    }
95
96
    // ----------- META -----------
97
98
    // CoC class name by
99
    // 1. replacing namespace Controllers by Models
100
    // 2. removing the  from classname
101
    // overwrite this behavior by setting the model_class_name at controller construction
102
    public function modelClassName(): string
103
    {
104
        if (is_null($this->model_class_name)) {
105
            preg_match(LeMarchand::RX_MVC, get_called_class(), $m);
106
            $this->model_class_name = $this->get('Models\\'.$m[2].'::class');
107
        }
108
109
        return $this->model_class_name;
110
    }
111
112
    public function model_type_to_label($model = null)
113
    {
114
        $model = $model ?? $this->load_model ?? $this->formModel();
115
        return $this->l(sprintf('MODEL_%s_INSTANCE', get_class($model)::model_type()));
116
    }
117
    public function field_name_to_label($model, $field_name)
118
    {
119
        $model = $model ?? $this->load_model ?? $this->formModel();
120
        return $this->l(sprintf('MODEL_%s_FIELD_%s', (get_class($model))::model_type(), $field_name));
121
    }
122
123
    public function dashboard()
124
    {
125
        $this->listing(); //default dashboard is a listing
126
    }
127
128
    public function listing($model = null, $filters = [], $options = [])
129
    {
130
        $class_name = is_null($model) ? $this->modelClassName() : get_class($model);
131
132
        if (!isset($filters['date_start'])) {
133
            $filters['date_start'] = $this->get('HexMakina\BlackBox\StateAgentInterface')->filters('date_start');
134
        }
135
        if (!isset($filters['date_stop'])) {
136
            $filters['date_stop'] = $this->get('HexMakina\BlackBox\StateAgentInterface')->filters('date_stop');
137
        }
138
139
        $listing = $this->modelClassName()::filter($filters);
140
141
        $this->viewport_listing($class_name, $listing, $this->find_template($this->get('\Smarty'), __FUNCTION__));
142
    }
143
144
    public function viewport_listing($class_name, $listing, $listing_template)
145
    {
146
        $listing_fields = [];
147
        if (empty($listing)) {
148
            $hidden_columns = ['created_by', 'created_on', 'password'];
149
            foreach ($class_name::table()->columns() as $column) {
150
                if (!$column->isAutoIncremented() && !in_array($column->name(), $hidden_columns)) {
151
                    $listing_fields[$column->name()] = $this->l(sprintf('MODEL_%s_FIELD_%s', $class_name::model_type(), $column->name()));
152
                }
153
            }
154
        } else {
155
            $current = current($listing);
156
            if (is_object($current)) {
157
                $current = get_object_vars($current);
158
            }
159
160
            foreach (array_keys($current) as $field) {
161
                $listing_fields[$field] = $this->l(sprintf('MODEL_%s_FIELD_%s', $class_name::model_type(), $field));
162
            }
163
        }
164
165
        $this->viewport('listing', $listing);
166
        $this->viewport('listing_title', $this->l(sprintf('MODEL_%s_INSTANCES', $class_name::model_type())));
167
        $this->viewport('listing_fields', $listing_fields);
168
        $this->viewport('listing_template', $listing_template);
169
170
        $this->viewport('route_new', $this->router()->hyp($class_name::model_type() . '_new'));
171
        $this->viewport('route_export', $this->router()->hyp($class_name::model_type() . '_export'));
172
    }
173
174
    public function copy()
175
    {
176
        $this->formModel($this->load_model->copy());
177
178
        $this->routeBack($this->load_model);
179
        $this->edit();
180
    }
181
182
    public function edit()
183
    {
184
    }
185
186
    public function save()
187
    {
188
        $model = $this->persist_model($this->formModel());
189
190
        if (empty($this->errors())) {
191
            $this->routeBack($model);
192
        } else {
193
            $this->edit();
194
            return 'edit';
195
        }
196
    }
197
198
    public function persist_model($model): ?ModelInterface
199
    {
200
        $this->errors = $model->save($this->operator()->operatorId()); // returns [errors]
201
        if (empty($this->errors())) {
202
            $this->logger()->notice($this->l('CRUDITES_INSTANCE_ALTERED', [$this->l('MODEL_' . get_class($model)::model_type() . '_INSTANCE')]));
203
            return $model;
204
        }
205
        foreach ($this->errors() as $field => $error_msg) {
206
            $this->logger()->warning($this->l($error_msg, [$field]));
207
        }
208
209
        return null;
210
    }
211
212
    public function before_edit()
213
    {
214
        if (!is_null($this->router()->params('id')) && is_null($this->load_model)) {
215
            $this->logger()->warning($this->l('CRUDITES_ERR_INSTANCE_NOT_FOUND', [$this->l('MODEL_' . $this->modelClassName()::model_type() . '_INSTANCE')]));
216
            $this->router()->hop($this->modelClassName()::model_type());
217
        }
218
    }
219
220
    public function before_save()
221
    {
222
        return [];
223
    }
224
225
  // default: hop to altered object
226
    public function after_save()
227
    {
228
        $this->router()->hop($this->routeBack());
229
    }
230
231
    public function destroy_confirm()
232
    {
233
        if (is_null($this->load_model)) {
234
            $this->logger()->warning($this->l('CRUDITES_ERR_INSTANCE_NOT_FOUND', [$this->l('MODEL_' . $this->model_type . '_INSTANCE')]));
235
            $this->router()->hop($this->model_type);
236
        }
237
238
        $this->before_destroy();
239
240
        return 'destroy';
241
    }
242
243
    public function before_destroy() // default: checks for load_model and immortality, hops back to object on failure
244
    {
245
        if (is_null($this->load_model)) {
246
            $this->logger()->warning($this->l('CRUDITES_ERR_INSTANCE_NOT_FOUND', [$this->l('MODEL_' . $this->model_type . '_INSTANCE')]));
247
            $this->router()->hop($this->model_type);
248
        } elseif ($this->load_model->immortal()) {
249
            $this->logger()->warning($this->l('CRUDITES_ERR_INSTANCE_IS_IMMORTAL', [$this->l('MODEL_' . $this->model_type . '_INSTANCE')]));
250
            $this->router()->hop($this->route_model($this->load_model));
251
        }
252
    }
253
254
    public function destroy()
255
    {
256
        if (!$this->router()->submits()) {
257
            throw new \Exception('KADRO_ROUTER_MUST_SUBMIT');
258
        }
259
260
        if ($this->load_model->destroy($this->operator()->operatorId()) === false) {
261
            $this->logger()->info($this->l('CRUDITES_ERR_INSTANCE_IS_UNDELETABLE', ['' . $this->load_model]));
262
            $this->routeBack($this->load_model);
263
        } else {
264
            $this->logger()->notice($this->l('CRUDITES_INSTANCE_DESTROYED', [$this->l('MODEL_' . $this->model_type . '_INSTANCE')]));
265
            $this->routeBack($this->model_type);
266
        }
267
    }
268
269
    public function after_destroy()
270
    {
271
        $this->router()->hop($this->routeBack());
272
    }
273
274
    public function conclude()
275
    {
276
        $this->viewport('errors', $this->errors());
277
        $this->viewport('form_model_type', $this->model_type);
278
        $this->viewport('form_model', $this->formModel());
279
280
        if (isset($this->load_model)) {
281
            $this->viewport('load_model', $this->load_model);
282
        }
283
    }
284
285
    public function collection_to_csv($collection, $filename)
286
    {
287
      // TODO use Format/File/CSV class to generate file
288
        $file_path = $this->get('settings.export.directory') . $filename . '.csv';
289
        $fp = fopen($file_path, 'w');
290
291
        $header = false;
292
293
        foreach ($collection as $line) {
294
            $line = get_object_vars($line);
295
            if ($header === false) {
296
                fputcsv($fp, array_keys($line));
297
                $header = true;
298
            }
299
            fputcsv($fp, $line);
300
        }
301
        fclose($fp);
302
303
        return $file_path;
304
    }
305
306
    public function export()
307
    {
308
        $format = $this->router()->params('format');
309
        switch ($format) {
310
            case null:
311
                $filename = $this->model_type;
312
                $collection = $this->modelClassName()::listing();
313
                $file_path = $this->collection_to_csv($collection, $filename);
314
                $this->router()->sendFile($file_path);
315
                break;
316
317
            case 'xlsx':
318
                $report_controller = $this->get('HexMakina\koral\Controllers\ReportController');
319
                return $report_controller->collection($this->modelClassName());
320
        }
321
    }
322
323
    public function route_new(ModelInterface $model): string
324
    {
325
        return $this->router()->hyp(get_class($model)::model_type() . '_new');
326
    }
327
328
    public function route_list(ModelInterface $model): string
329
    {
330
        return $this->router()->hyp(get_class($model)::model_type());
331
    }
332
333
    public function route_model(ModelInterface $model): string
334
    {
335
        $route_params = [];
336
337
        $route_name = get_class($model)::model_type() . '_';
338
        if ($model->isNew()) {
339
            $route_name .= 'new';
340
        } else {
341
            $route_name .= 'default';
342
            $route_params = ['id' => $model->getId()];
343
        }
344
        $res = $this->router()->hyp($route_name, $route_params);
345
        return $res;
346
    }
347
348
    public function routeFactory($route = null, $route_params = []): string
349
    {
350
        if (is_null($route) && $this->router()->submits()) {
351
            $route = $this->formModel();
352
        }
353
354
        if (!is_null($route) && is_subclass_of($route, '\HexMakina\BlackBox\ORM\ModelInterface')) {
355
            $route = $this->route_model($route);
356
        }
357
358
        return parent::routeFactory($route, $route_params);
359
    }
360
361
    private function sanitize_post_data($post_data = [])
362
    {
363
        foreach ($this->modelClassName()::table()->columns() as $col) {
364
            if ($col->type()->isBoolean()) {
365
                $post_data[$col->name()] = !empty($post_data[$col->name()]);
366
            }
367
        }
368
369
        return $post_data;
370
    }
371
372
    // overriding displaycontroller
373
    protected function template_base()
374
    {
375
        return $this->modelClassName()::model_type();
376
    }
377
}
378