Issues (13)

Controllers/ORM.php (3 issues)

Severity
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\Configuration;
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(Configuration::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
            $listing_fields = $this->listing_fields_from_table($class_name);
149
        } else {
150
            $listing_fields = $this->listing_fields_from_listing($class_name, $listing);
151
        }
152
153
        $this->viewport('listing', $listing);
154
        $this->viewport('listing_title', $this->l(sprintf('MODEL_%s_INSTANCES', $class_name::model_type())));
155
        $this->viewport('listing_fields', $listing_fields);
156
        $this->viewport('listing_template', $listing_template);
157
158
        $this->viewport('route_new', $this->router()->hyp($class_name::model_type() . '_new'));
159
        $this->viewport('route_export', $this->router()->hyp($class_name::model_type() . '_export'));
160
    }
161
162
    private function listing_fields_from_listing($class_name, $listing){
163
      $ret = [];
164
165
      $current = current($listing);
166
      if (is_object($current)) {
167
          $current = get_object_vars($current);
168
      }
169
170
      foreach (array_keys($current) as $field) {
171
          $ret[$field] = $this->l(sprintf('MODEL_%s_FIELD_%s', $class_name::model_type(), $field));
172
      }
173
      return $ret;
174
    }
175
176
    private function listing_fields_from_table($class_name){
177
      $ret = [];
178
      $hidden_columns = ['created_by', 'created_on', 'password'];
179
      foreach ($class_name::table()->columns() as $column) {
180
          if (!$column->isAutoIncremented() && !in_array($column->name(), $hidden_columns)) {
181
              $ret[$column->name()] = $this->l(sprintf('MODEL_%s_FIELD_%s', $class_name::model_type(), $column->name()));
182
          }
183
      }
184
      return $ret;
185
    }
186
187
    private function listing_fields($class_name, $listing){
0 ignored issues
show
The parameter $listing is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

187
    private function listing_fields($class_name, /** @scrutinizer ignore-unused */ $listing){

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
The method listing_fields() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
The parameter $class_name is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

187
    private function listing_fields(/** @scrutinizer ignore-unused */ $class_name, $listing){

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
188
      $listing_fields = [];
189
190
191
192
      return $listing_fields;
193
    }
194
    public function copy()
195
    {
196
        $this->formModel($this->load_model->copy());
197
198
        $this->routeBack($this->load_model);
199
        $this->edit();
200
    }
201
202
    public function edit()
203
    {
204
    }
205
206
    public function save()
207
    {
208
        $model = $this->persist_model($this->formModel());
209
210
        if (empty($this->errors())) {
211
            $this->routeBack($model);
212
        } else {
213
            $this->edit();
214
            return 'edit';
215
        }
216
    }
217
218
    public function persist_model($model): ?ModelInterface
219
    {
220
        $this->errors = $model->save($this->operator()->getId()); // returns [errors]
221
        if (empty($this->errors())) {
222
            $this->logger()->notice($this->l('CRUDITES_INSTANCE_ALTERED', [$this->l('MODEL_' . get_class($model)::model_type() . '_INSTANCE')]));
223
            return $model;
224
        }
225
        foreach ($this->errors() as $field => $error_msg) {
226
            $this->logger()->warning($this->l($error_msg, [$field]));
227
        }
228
229
        return null;
230
    }
231
232
    public function before_edit()
233
    {
234
        if (!is_null($this->router()->params('id')) && is_null($this->load_model)) {
235
            $this->logger()->warning($this->l('CRUDITES_ERR_INSTANCE_NOT_FOUND', [$this->l('MODEL_' . $this->modelClassName()::model_type() . '_INSTANCE')]));
236
            $this->router()->hop($this->modelClassName()::model_type());
237
        }
238
    }
239
240
    public function before_save()
241
    {
242
        return [];
243
    }
244
245
  // default: hop to altered object
246
    public function after_save()
247
    {
248
        $this->router()->hop($this->routeBack());
249
    }
250
251
    public function destroy_confirm()
252
    {
253
        if (is_null($this->load_model)) {
254
            $this->logger()->warning($this->l('CRUDITES_ERR_INSTANCE_NOT_FOUND', [$this->l('MODEL_' . $this->model_type . '_INSTANCE')]));
255
            $this->router()->hop($this->model_type);
256
        }
257
258
        $this->before_destroy();
259
260
        return 'destroy';
261
    }
262
263
    public function before_destroy() // default: checks for load_model and immortality, hops back to object on failure
264
    {
265
        if (is_null($this->load_model)) {
266
            $this->logger()->warning($this->l('CRUDITES_ERR_INSTANCE_NOT_FOUND', [$this->l('MODEL_' . $this->model_type . '_INSTANCE')]));
267
            $this->router()->hop($this->model_type);
268
        } elseif ($this->load_model->immortal()) {
269
            $this->logger()->warning($this->l('CRUDITES_ERR_INSTANCE_IS_IMMORTAL', [$this->l('MODEL_' . $this->model_type . '_INSTANCE')]));
270
            $this->router()->hop($this->route_model($this->load_model));
271
        }
272
    }
273
274
    public function destroy()
275
    {
276
        if (!$this->router()->submits()) {
277
            throw new \Exception('KADRO_ROUTER_MUST_SUBMIT');
278
        }
279
280
        if ($this->load_model->destroy($this->operator()->getId()) === false) {
281
            $this->logger()->info($this->l('CRUDITES_ERR_INSTANCE_IS_UNDELETABLE', ['' . $this->load_model]));
282
            $this->routeBack($this->load_model);
283
        } else {
284
            $this->logger()->notice($this->l('CRUDITES_INSTANCE_DESTROYED', [$this->l('MODEL_' . $this->model_type . '_INSTANCE')]));
285
            $this->routeBack($this->model_type);
286
        }
287
    }
288
289
    public function after_destroy()
290
    {
291
        $this->router()->hop($this->routeBack());
292
    }
293
294
    public function conclude()
295
    {
296
        $this->viewport('errors', $this->errors());
297
        $this->viewport('form_model_type', $this->model_type);
298
        $this->viewport('form_model', $this->formModel());
299
300
        if (isset($this->load_model)) {
301
            $this->viewport('load_model', $this->load_model);
302
        }
303
    }
304
305
    public function collection_to_csv($collection, $filename)
306
    {
307
      // TODO use Format/File/CSV class to generate file
308
        $file_path = $this->get('settings.export.directory') . $filename . '.csv';
309
        $fp = fopen($file_path, 'w');
310
311
        $header = false;
312
313
        foreach ($collection as $line) {
314
            $line = get_object_vars($line);
315
            if ($header === false) {
316
                fputcsv($fp, array_keys($line));
317
                $header = true;
318
            }
319
            fputcsv($fp, $line);
320
        }
321
        fclose($fp);
322
323
        return $file_path;
324
    }
325
326
    public function export()
327
    {
328
        $format = $this->router()->params('format');
329
        switch ($format) {
330
            case null:
331
                $filename = $this->model_type;
332
                $collection = $this->modelClassName()::listing();
333
                $file_path = $this->collection_to_csv($collection, $filename);
334
                $this->router()->sendFile($file_path);
335
                break;
336
337
            case 'xlsx':
338
                $report_controller = $this->get('HexMakina\koral\Controllers\ReportController');
339
                return $report_controller->collection($this->modelClassName());
340
        }
341
    }
342
343
    public function route_new(ModelInterface $model): string
344
    {
345
        return $this->router()->hyp(get_class($model)::model_type() . '_new');
346
    }
347
348
    public function route_list(ModelInterface $model): string
349
    {
350
        return $this->router()->hyp(get_class($model)::model_type());
351
    }
352
353
    public function route_model(ModelInterface $model): string
354
    {
355
        $route_params = [];
356
357
        $route_name = get_class($model)::model_type() . '_';
358
        if ($model->isNew()) {
359
            $route_name .= 'new';
360
        } else {
361
            $route_name .= 'default';
362
            $route_params = ['id' => $model->getId()];
363
        }
364
        $res = $this->router()->hyp($route_name, $route_params);
365
        return $res;
366
    }
367
368
    public function routeFactory($route = null, $route_params = []): string
369
    {
370
        if (is_null($route) && $this->router()->submits()) {
371
            $route = $this->formModel();
372
        }
373
374
        if (!is_null($route) && is_subclass_of($route, '\HexMakina\BlackBox\ORM\ModelInterface')) {
375
            $route = $this->route_model($route);
376
        }
377
378
        return parent::routeFactory($route, $route_params);
379
    }
380
381
    private function sanitize_post_data($post_data = [])
382
    {
383
        foreach ($this->modelClassName()::table()->columns() as $col) {
384
            if ($col->type()->isBoolean()) {
385
                $post_data[$col->name()] = !empty($post_data[$col->name()]);
386
            }
387
        }
388
389
        return $post_data;
390
    }
391
392
    // overriding displaycontroller
393
    protected function template_base()
394
    {
395
        return $this->modelClassName()::model_type();
396
    }
397
}
398