View_CRUD   F
last analyzed

Complexity

Total Complexity 76

Size/Duplication

Total Lines 721
Duplicated Lines 2.22 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 0
Metric Value
dl 16
loc 721
rs 2.2222
c 0
b 0
f 0
wmc 76
lcom 1
cbo 14

13 Methods

Rating   Name   Duplication   Size   Complexity  
A configureDel() 0 4 1
A formSubmitSuccess() 0 5 1
D addFrame() 0 39 9
A addButton() 0 8 2
B configureAdd() 8 30 5
A configureGrid() 0 4 1
A formSubmit() 0 12 2
B isEditing() 0 22 6
C addRef() 0 61 13
A configureEdit() 8 23 3
B init() 0 53 4
C setModel() 0 44 12
F addAction() 0 127 17

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like View_CRUD 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 View_CRUD, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * CRUD stands for Create, Read, Update and Delete. This view combines
4
 * both "Grid", "Form" and "VirtualPage" to bring you a seamless editing
5
 * control. You would need to supply a model.
6
 *
7
 * IMPORTANT NOTE: While you can disable adding and editing, if you do that
8
 * you must simply use Grid!
9
 */
10
class View_CRUD extends View
11
{
12
    /**
13
     * After CRUD is initialized, this will point to a form object IF
14
     * CRUD goes into editing mode. Typically the same code is initialized
15
     * for editing pop-up, but only form is rendered. You can enhance the
16
     * form all you want.
17
     *
18
     * IMPORTANT: check isEditing() method
19
     *
20
     * @var Form
21
     */
22
    public $form = null;
23
24
    /**
25
     * After CRUD is initialized, this will point do a Grid object, IF
26
     * CRUD is in "read" mode. You can add more columns or actions to the
27
     * grid.
28
     *
29
     * IMPORTANT: check isEditing() method
30
     *
31
     * @var Grid
32
     */
33
    public $grid = null;
34
35
    /**
36
     * By default, CRUD will simply use "Grid" class, but if you would like
37
     * to use your custom grid class for listing, specify it inside associative
38
     * array as second argument to add().
39
     *
40
     * $this->add('CRUD', array('grid_class'=>'MyGrid'));
41
     *
42
     * @var string
43
     */
44
    public $grid_class = 'Grid';
45
46
    /**
47
     * By default, CRUD will simply use "Form" class for editing and adding,
48
     * but if you would like to use your custom form, specify it inside
49
     * associative array as second argument to add().
50
     *
51
     * $this->add('CRUD', array('form_class'=>'MyForm'));
52
     *
53
     * @var string
54
     */
55
    public $form_class = 'Form';
56
57
    /**
58
     * You can pass additional options for grid using this array.
59
     *
60
     * $this->add('CRUD', array('grid_options'=>array('show_header'=>false)));
61
     *
62
     * @var array
63
     */
64
    public $grid_options = array();
65
66
    /**
67
     * You can pass additional options for form using this array.
68
     *
69
     * $this->add('CRUD', array('form_options'=>array('js_widget'=>'ui.atk4_form')));
70
     *
71
     * @var array
72
     */
73
    public $form_options = array();
74
75
    /**
76
     * Grid will contain an "Add X" button and will allow user to add records.
77
     *
78
     * $this->add('CRUD', array('allow_add'=>false')); // to disable
79
     *
80
     * @var bool
81
     */
82
    public $allow_add = true;
83
84
    /**
85
     * Grid will contain "EDIT" button for each row allowing usir to edit
86
     * records.
87
     *
88
     * $this->add('CRUD', array('allow_edit'=>false')); // to disable
89
     *
90
     * @var bool
91
     */
92
    public $allow_edit = true;
93
94
    /**
95
     * Grid will contain a "DELETE" button for each row. If you don't want
96
     * thes set this option to false.
97
     *
98
     * $this->add('CRUD', array('allow_del'=>false')); // to disable
99
     *
100
     * @var bool
101
     */
102
    public $allow_del = true;
103
104
    /**
105
     * For ->setModel('User'), your add button would contain "Add User". If
106
     * you want add button and frames to use different label, then change
107
     * this property.
108
     *
109
     * If you set this to 'false' then CRUD will not attempt to change
110
     * default label ("Add")
111
     *
112
     * @var string
113
     */
114
    public $entity_name = null;
115
116
    /**
117
     * This points to a Button object, which you can change if you want
118
     * a different label or anything else on it.
119
     *
120
     * @var Button
121
     */
122
    public $add_button;
123
124
    /**
125
     * VirtualPage object will be used to display popup content. That is to ensure
126
     * that none of your other content you put AROUND the CRUD would mess
127
     * with the forms.
128
     *
129
     * If isEditing() then you can add more stuff on this page, by calling
130
     * virtual_page->getPage()->add('Hello!');
131
     *
132
     * @var VirtualPage
133
     */
134
    public $virtual_page = null;
135
136
    /**
137
     * When clicking on EDIT or ADD the frameURL is used. If you want to pass
138
     * some arguments to it, put your hash here.
139
     *
140
     * @var array
141
     */
142
    public $frame_options = null;
143
144
    /**
145
     * This is set to ID of the model when are in editing mode. In theory
146
     * this can also be 0, so use is_null().
147
     *
148
     * @var mixed
149
     */
150
    public $id = null;
151
152
    /**
153
     * Contains reload javascript, used occassionally throughout the object.
154
     *
155
     * @var jQuery_Chain
156
     */
157
    public $js_reload = null;
158
159
    // {{ type-hint inherited properties
160
    /** @var View */
161
    public $owner;
162
163
    /** @var App_Web */
164
    public $app;
165
    // }}
166
167
    /**
168
     * {@inheritdoc}
169
     *
170
     * CRUD's init() will create either a grid or form, depending on
171
     * isEditing(). You can then do the necessary changes after
172
     *
173
     * Note, that the form or grid will not be populated until you
174
     * call setModel()
175
     */
176
    public function init()
177
    {
178
        parent::init();
179
180
        $this->js_reload = $this->js()->reload();
181
182
        // Virtual Page would receive 3 types of requests - add, delete, edit
183
        $this->virtual_page = $this->add('VirtualPage', array(
184
            'frame_options' => $this->frame_options,
185
        ));
186
        /** @type VirtualPage $this->virtual_page */
187
188
        $name_id = $this->virtual_page->name.'_id';
189
190
        /*
191
        if ($_GET['edit'] && !isset($_GET[$name_id])) {
192
            $_GET[$name_id] = $_GET['edit'];
193
        }
194
         */
195
196
        if (isset($_GET[$name_id])) {
197
            $this->app->stickyGET($name_id);
198
            $this->id = $_GET[$name_id];
199
        }
200
201
        if ($this->isEditing()) {
202
            $this->form = $this
203
                ->virtual_page
204
                ->getPage()
205
                ->add($this->form_class, $this->form_options)
206
                //->addClass('atk-form-stacked')
207
                ;
208
            /** @type Form $this->form */
209
210
            $this->grid = new Dummy();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Dummy() of type object<Dummy> is incompatible with the declared type object<Grid> of property $grid.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
211
            /** @type Grid $this->grid */
212
213
            return;
214
        }
215
216
        $this->grid = $this->add($this->grid_class, $this->grid_options);
217
        /** @type Grid $this->grid */
218
219
        $this->form = new Dummy();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Dummy() of type object<Dummy> is incompatible with the declared type object<Form> of property $form.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
220
        /** @type Form $this->form */
221
222
        // Left for compatibility
223
        $this->js('reload', $this->grid->js()->reload());
224
225
        if ($this->allow_add) {
226
            $this->add_button = $this->grid->addButton('Add');
227
        }
228
    }
229
230
    /**
231
     * Returns if CRUD is in editing mode or not. It's preferable over
232
     * checking if($grid->form).
233
     *
234
     * @param string $mode Specify which editing mode you expect
235
     *
236
     * @return bool true if editing.
237
     */
238
    public function isEditing($mode = null)
239
    {
240
        $page_mode = $this->virtual_page->isActive();
241
242
        // Requested edit, but not allowed
243
        if ($page_mode == 'edit' && !$this->allow_edit) {
244
            throw $this->exception('Editing is not allowed');
245
        }
246
247
        // Requested add but not allowed
248
        if ($page_mode == 'add' && !$this->allow_add) {
249
            throw $this->exception('Adding is not allowed');
250
        }
251
252
        // Request matched argument exactly
253
        if (!is_null($mode)) {
254
            return $mode === $page_mode;
255
        }
256
257
        // Argument was blank, then edit/add is OK
258
        return (boolean) $page_mode;
259
    }
260
261
    /**
262
     * Assign model to your CRUD and specify list of fields to use from model.
263
     *
264
     * {@inheritdoc}
265
     *
266
     * @param string|Model $model       Same as parent
267
     * @param array        $fields      Specify list of fields for form and grid
268
     * @param array        $grid_fields Overide list of fields for the grid
269
     *
270
     * @return AbstractModel $model
271
     */
272
    public function setModel($model, $fields = null, $grid_fields = null)
273
    {
274
        $model = parent::setModel($model);
275
276
        if ($this->entity_name === null) {
277
            if ($model->caption === null) {
278
279
                // Calculates entity name
280
                $class = get_class($this->model);
281
                $class = substr(strrchr($class, '\\') ?: ' '.$class, 1); // strip namespace
282
                $this->entity_name = str_replace(
283
                    array('Model_', '_'),
284
                    array('', ' '),
285
                    $class
286
                );
287
            } else {
288
                $this->entity_name = $model->caption;
289
            }
290
        }
291
292
        if (!$this->isEditing()) {
293
            $this->configureGrid(is_null($grid_fields) ? $fields : $grid_fields);
294
        }
295
296
        if ($this->allow_add) {
297
            if ($this->configureAdd($fields)) {
298
                return $model;
299
            }
300
        } elseif (isset($this->add_button)) {
301
            $this->add_button->destroy();
302
        }
303
304
        if ($this->allow_edit) {
305
            if ($this->configureEdit($fields)) {
306
                return $model;
307
            }
308
        }
309
310
        if ($this->allow_del) {
311
            $this->configureDel();
312
        }
313
314
        return $model;
315
    }
316
317
    /**
318
     * Assuming that your model has a $relation defined, this method will add a button
319
     * into a separate column. When clicking, it will expand the grid and will present
320
     * either another CRUD with related model contents (one to many) or a form preloaded
321
     * with related model data (many to one).
322
     *
323
     * Adds expander to the crud, which edits references under the specified
324
     * name. Returns object of nested CRUD when active, or null
325
     *
326
     * The format of $options is the following:
327
     * array (
328
     *   'view_class' => 'CRUD',  // Which View to use inside expander
329
     *   'view_options' => ..     // Second arg when adding view.
330
     *   'view_model' => model or callback // Use custom model for sub-View, by default ref($name) will be used
331
     *   'fields' => array()      // Used as second argument for setModel()
332
     *   'extra_fields' => array() // Third arguments to setModel() used by CRUDs
333
     *   'label'=> 'Click Me'     // Label for a button inside a grid
334
     * )
335
     *
336
     * @param string $name    Name of the reference. If you leave blank adds all
337
     * @param array  $options Customizations, see above
338
     *
339
     * @return View_CRUD|null Returns crud object, when expanded page is rendered
340
     */
341
    public function addRef($name, $options = array())
342
    {
343
        if (!$this->model) {
344
            throw $this->exception('Must set CRUD model first');
345
        }
346
347
        if (!is_array($options)) {
348
            throw $this->exception('Must be array');
349
        }
350
351
        // if(!$this->grid || $this->grid instanceof Dummy)return;
352
353
        $s = $this->app->normalizeName($name);
354
355
        if ($this->isEditing('ex_'.$s)) {
356
            $n = $this->virtual_page->name.'_'.$s;
357
358
            if ($_GET[$n]) {
359
                $this->id = $_GET[$n];
360
                $this->app->stickyGET($n);
361
            }
362
363
            $idfield = $this->model->table.'_'.$this->model->id_field;
364
            if ($_GET[$idfield]) {
365
                $this->id = $_GET[$idfield];
366
                $this->app->stickyGET($idfield);
367
            }
368
369
            $view_class = (is_null($options['view_class'])) ?
370
                get_class($this) :
371
                $options['view_class'];
372
373
            $subview = $this->virtual_page->getPage()->add(
374
                $view_class,
375
                $options['view_options']
376
            );
377
378
            $this->model->load($this->id);
379
            $subview->setModel(
380
                $options['view_model']
381
                    ? (is_callable($options['view_model'])
382
                        ? call_user_func($options['view_model'], $this->model)
383
                        : $options['view_model']
384
                    )
385
                    : $this->model->ref($name),
386
                $options['fields'],
387
                $options['grid_fields'] ?: $options['extra_fields']
388
            );
389
390
            return $subview;
391
        } elseif ($this->grid instanceof Grid) {
392
            $this->grid->addColumn('expander', 'ex_'.$s, $options['label'] ?: $s);
393
            $this->grid->columns['ex_'.$s]['page']
394
                = $this->virtual_page->getURL('ex_'.$s);
395
            // unused: $idfield = $this->grid->columns['ex_'.$s]['refid'].'_'.$this->model->id_field;
396
        }
397
398
        if ($this->isEditing()) {
399
            return;
400
        }
401
    }
402
403
    /**
404
     * Adds button to the crud, which opens a new frame and returns page to
405
     * you. Add anything into the page as you see fit. The ID of the record
406
     * will be inside $crud->id.
407
     *
408
     * The format of $options is the following:
409
     * array (
410
     *   'title'=> 'Click Me'     // Header for the column
411
     *   'label'=> 'Click Me'     // Text to put on the button
412
     *   'icon' => 'click-me'     // Icon for button
413
     * )
414
     *
415
     * @param string $name    Unique name, also button and title default
416
     * @param array  $options Options
417
     *
418
     * @return Page|bool Returns object if clicked on popup.
419
     */
420
    public function addFrame($name, $options = array())
421
    {
422
        if (!$this->model) {
423
            throw $this->exception('Must set CRUD model first');
424
        }
425
426
        if (!is_array($options)) {
427
            throw $this->exception('Must be array');
428
        }
429
430
        $s = $this->app->normalizeName($name);
431
432
        if ($this->isEditing('fr_'.$s)) {
433
            $n = $this->virtual_page->name.'_'.$s;
434
435
            if ($_GET[$n]) {
436
                $this->id = $_GET[$n];
437
                $this->app->stickyGET($n);
438
            }
439
440
            return $this->virtual_page->getPage();
441
        }
442
443
        if ($this->isEditing()) {
444
            return false;
445
        }
446
447
        $this
448
            ->virtual_page
449
            ->addColumn(
450
                'fr_'.$s,
451
                $options['title'] ?: $name,
452
                array(
453
                    'descr' => $options['label'] ?: null,
454
                    'icon' => $options['icon'] ?: null,
455
                ),
456
                $this->grid
457
            );
458
    }
459
460
    /**
461
     * Assuming that your model contains a certain method, this allows
462
     * you to create a frame which will pop you a new frame with
463
     * a form representing model method arguments. Once the form
464
     * is submitted, the action will be evaluated.
465
     *
466
     * @param string $method_name
467
     * @param array $options
468
     */
469
    public function addAction($method_name, $options = array())
470
    {
471
        if (!$this->model) {
472
            throw $this->exception('Must set CRUD model first');
473
        }
474
        if ($options == 'toolbar') {
475
            $options = array('column' => false);
476
        }
477
        if ($options == 'column') {
478
            $options = array('toolbar' => false);
479
        }
480
481
        $descr = $options['descr'] ?: ucwords(str_replace('_', ' ', $method_name));
482
        $icon = $options['icon'] ?: 'target';
483
484
        $show_toolbar = isset($options['toolbar']) ? $options['toolbar'] : true;
485
        $show_column = isset($options['column']) ? $options['column'] : true;
486
487
        if ($this->isEditing($method_name)) {
488
            /** @type View_Console $c */
489
            $c = $this->virtual_page->getPage()->add('View_Console');
490
            $self = $this;
491
492
            // Callback for the function
493
            $c->set(function ($c) use ($show_toolbar, $show_column, $options, $self, $method_name) {
494
                if ($show_toolbar && !$self->id) {
495
                    $self->model->unload();
496
                } elseif ($show_column && $self->id) {
497
                    $c->out('Loading record '.$self->id, array('class' => 'atk-effect-info'));
498
                    $self->model->load($self->id);
499
                } else {
500
                    return;
501
                }
502
503
                $ret = $self->model->$method_name();
504
505
                $c->out('Returned: '.json_encode($ret, JSON_UNESCAPED_UNICODE), array('class' => 'atk-effect-success'));
506
507
                /*
508
                if (isset($options['args'])) {
509
                    $params = $options['args'];
510
                } elseif (!method_exists($self->model, $method_name)) {
511
                    // probably a dynamic method
512
                    $params = array();
513
                } else {
514
                    $reflection = new ReflectionMethod($self->model, $method_name);
515
516
                    $params = $reflection->getParameters();
517
                }
518
                */
519
            });
520
521
            return;
522
523
            /* unused code below
524
525
            $has_parameters = (bool) $params;
526
            foreach ($params as $i => $param) {
527
                $this->form->addField($param->name);
528
                $this->has_parameters = true;
529
            }
530
531
            if (!$has_parameters) {
532
                $this->form->destroy();
533
                $ret = $this->model->$method_name();
534
                if (is_object($ret) && $ret == $this->model) {
535
                    $this->virtual_page->getPage()->add('P')->set('Executed successfully');
536
                    $this->virtual_page->getPage()->js(true, $this->js_reload);
537
                } else {
538
                    $this->virtual_page->getPage()->js(true, $this->js_reload);
539
                    if (is_object($ret)) {
540
                        $ret = (string) $ret;
541
                    }
542
                    $this->virtual_page->getPage()
543
                        ->add('P')->set('Returned: '.json_encode($ret, JSON_UNESCAPED_UNICODE));
544
                }
545
                $this->virtual_page->getPage()
546
                    ->add('Button')->set(array('Close', 'icon' => 'cross', 'swatch' => 'green'))
547
                    ->js('click')->univ()->closeDialog();
548
549
                return true;
550
            }
551
552
            $this->form->addSubmit('Execute');
553
            if ($this->form->isSubmitted()) {
554
                $ret = call_user_func_array(array($this->model, $method_name), array_values($this->form->get()));
555
                if (is_object($ret)) {
556
                    $ret = (string) $ret;
557
                }
558
                $this->js(null, $this->js()->reload())->univ()
559
                    ->successMessage('Returned: '.json_encode($ret, JSON_UNESCAPED_UNICODE))
560
                    ->closeDialog()
561
                    ->execute();
562
            }
563
564
            return true;
565
            */
566
567
        } elseif ($this->isEditing()) {
568
            return;
569
        }
570
571
        $frame_options = array_merge(array(), $this->frame_options ?: array());
572
573
        if ($show_column) {
574
            $this
575
                ->virtual_page
576
                ->addColumn(
577
                    $method_name,
578
                    $descr.' '.$this->entity_name,
579
                    array('descr' => $descr, 'icon' => $icon),
580
                    $this->grid
581
                );
582
        }
583
584
        if ($show_toolbar) {
585
            $button = $this->addButton(array($descr, 'icon' => $icon));
586
587
            // Configure Add Button on Grid and JS
588
            $button->js('click')->univ()
589
                ->frameURL(
590
                    $this->app->_($this->entity_name.'::'.$descr),
591
                    $this->virtual_page->getURL($method_name),
592
                    $frame_options
593
                );
594
        }
595
    }
596
597
    /**
598
     * Transparent method for adding buttons to a crud.
599
     *
600
     * @param string|array $label
601
     * @param string $class
602
     *
603
     * @return Button
604
     */
605
    public function addButton($label, $class = 'Button')
606
    {
607
        if (!$this->grid) {
608
            return new Dummy();
609
        }
610
611
        return $this->grid->addButton($label, $class);
612
    }
613
614
    /**
615
     * Configures necessary components when CRUD is in the adding mode.
616
     *
617
     * @param array $fields List of fields for add form
618
     *
619
     * @return void|Model If model, then bail out, no greed needed
620
     */
621
    protected function configureAdd($fields = null)
622
    {
623
        // We are actually in the frame!
624 View Code Duplication
        if ($this->isEditing('add')) {
625
            $this->model->unload();
626
            $m = $this->form->setModel($this->model, $fields);
627
            $this->form->addSubmit('Add');
628
            $this->form->onSubmit(array($this, 'formSubmit'));
629
630
            return $m;
631
        } elseif ($this->isEditing()) {
632
            return;
633
        }
634
635
        // Configure Add Button on Grid and JS
636
        $this->add_button->js('click')->univ()
637
            ->frameURL(
638
                $this->app->_(
639
                    $this->entity_name === false
640
                    ? 'New Record'
641
                    : 'Adding new '.$this->entity_name
642
                ),
643
                $this->virtual_page->getURL('add'),
644
                $this->frame_options
645
            );
646
647
        if ($this->entity_name !== false) {
648
            $this->add_button->setHTML('<i class="icon-plus"></i> Add '.htmlspecialchars($this->entity_name));
649
        }
650
    }
651
652
    /**
653
     * Configures necessary components when CRUD is in the editing mode.
654
     *
655
     * @param array $fields List of fields for add form
656
     *
657
     * @return void|Model If model, then bail out, no greed needed
658
     */
659
    protected function configureEdit($fields = null)
660
    {
661
        // We are actually in the frame!
662 View Code Duplication
        if ($this->isEditing('edit')) {
663
            $m = $this->form->setModel($this->model, $fields);
664
            $m->load($this->id);
665
            $this->form->addSubmit();
666
            $this->form->onSubmit(array($this, 'formSubmit'));
667
668
            return $m;
669
        } elseif ($this->isEditing()) {
670
            return;
671
        }
672
673
        $this
674
            ->virtual_page
675
            ->addColumn(
676
                'edit',
677
                'Editing '.$this->entity_name,
678
                array('descr' => 'Edit', 'icon' => 'pencil'),
679
                $this->grid
680
            );
681
    }
682
683
    /**
684
     * Configures grid's model itself.
685
     *
686
     * @param array $fields List of fields for grid
687
     */
688
    protected function configureGrid($fields)
689
    {
690
        $this->grid->setModel($this->model, $fields);
691
    }
692
693
    /**
694
     * Configures deleting functionality for grid.
695
     */
696
    protected function configureDel()
697
    {
698
        $this->grid->addColumn('delete', 'delete', array('icon' => 'trash', 'descr' => 'Delete'));
699
    }
700
701
    /**
702
     * Called after on post-init hook when form is submitted.
703
     *
704
     * @param Form $form Form which was submitted
705
     */
706
    protected function formSubmit($form)
707
    {
708
        try {
709
            $form->update();
710
            $self = $this;
711
            $this->app->addHook('pre-render', function () use ($self) {
712
                $self->formSubmitSuccess()->execute();
713
            });
714
        } catch (Exception_ValidityCheck $e) {
715
            $form->displayError($e->getField(), $e->getMessage());
716
        }
717
    }
718
719
    /**
720
     * Returns JavaScript action which should be executed on form successfull
721
     * submission.
722
     *
723
     * @return jQuery_Chain to be executed on successful submit
724
     */
725
    public function formSubmitSuccess()
726
    {
727
        return $this->form->js(null, $this->js()->trigger('reload'))
728
            ->univ()->closeDialog();
729
    }
730
}
731