ORM   F
last analyzed

Complexity

Total Complexity 76

Size/Duplication

Total Lines 387
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 166
c 3
b 0
f 0
dl 0
loc 387
rs 2.32
wmc 76

35 Methods

Rating   Name   Duplication   Size   Complexity  
A dashboard() 0 3 1
A field_name_to_label() 0 4 1
A modelClassName() 0 8 2
A prepare() 0 18 4
A addErrors() 0 7 3
A modelType() 0 8 2
A formModel() 0 9 3
A listing() 0 14 4
A loadModel() 0 3 1
A model_type_to_label() 0 4 1
A modelPrefix() 0 9 2
A after_destroy() 0 3 1
A listing_fields_from_listing() 0 12 3
A route_model() 0 13 2
A listing_fields() 0 6 1
A save() 0 9 2
A after_save() 0 3 1
A before_destroy() 0 8 3
A route_list() 0 3 1
A route_new() 0 3 1
A routeFactory() 0 11 5
A persist_model() 0 12 3
A destroy_confirm() 0 10 2
A edit() 0 2 1
A destroy() 0 12 3
A export() 0 14 3
A collection_to_csv() 0 19 3
A copy() 0 6 1
A before_edit() 0 5 3
A viewport_listing() 0 16 2
A before_save() 0 3 1
A template_base() 0 3 1
A conclude() 0 8 2
A listing_fields_from_table() 0 9 4
A sanitize_post_data() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like ORM often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ORM, and based on these observations, apply Extract Interface, too.

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
Unused Code introduced by
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...
Unused Code introduced by
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...
Unused Code introduced by
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