AbstractView   D
last analyzed

Complexity

Total Complexity 75

Size/Duplication

Total Lines 664
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
dl 0
loc 664
rs 4.6273
c 0
b 0
f 0
wmc 75
lcom 1
cbo 12

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __clone() 0 4 1
A defaultSpot() 0 4 1
C setModel() 0 30 7
A _tsBuffer() 0 4 1
A defaultTemplate() 0 4 1
A modelRender() 0 4 1
A render() 0 12 3
A getJSID() 0 4 1
A getHTML() 0 17 4
C initializeTemplate() 0 64 15
A initTemplateTags() 0 9 3
C recursiveRender() 0 56 14
A moveJStoParent() 0 5 1
B output() 0 12 5
A region_render() 0 19 1
D js() 0 35 9
B on() 0 54 7

How to fix   Complexity   

Complex Class

Complex classes like AbstractView 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 AbstractView, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * A base class for all Visual objects in Agile Toolkit. The
4
 * important distinctive property of all Views is abiltiy
5
 * to render themselves (produce HTML) automatically and
6
 * recursively.
7
 */
8
abstract class AbstractView extends AbstractObject
9
{
10
    /**
11
     * $template is an object containing indexed HTML template.
12
     *
13
     * Example:
14
     *
15
     * $view->template->set('title', $my_title);
16
     *
17
     * Assuming you have tag <?$title?> in template file associated
18
     * with this view - will insert text into this tag.
19
     *
20
     * @see AbstractObject::add();
21
     * @see AbstractView::defaultTemplate();
22
     *
23
     * @var Template
24
     */
25
    public $template = false;
26
27
    /**
28
     * @internal
29
     *
30
     * $template_flush is set to a spot on the template, which
31
     * should be flushed out. When using AJAX we want to show
32
     * only certain region from our template. However several
33
     * childs may want to put their data. This property will
34
     * be set to region's name my call_ajax_render and if it's
35
     * set, call_ajax_render will echo it and return false.
36
     *
37
     * @var string
38
     */
39
    public $template_flush = false;
40
41
    /**
42
     * $spot defines a place on a parent's template where render() will
43
     * output() resulting HTML.
44
     *
45
     * @see output()
46
     * @see render()
47
     * @see AbstractObject::add();
48
     * @see defaultSpot();
49
     *
50
     * @var string
51
     */
52
    public $spot;
53
54
    /**
55
     * When using setModel() with Views some views will want to populate
56
     * fields, columns etc corresponding to models meta-data. That is the
57
     * job of Controller. When you create a custom controller for your view
58
     * set this property to point at your controller and it will be used.
59
     * automatically.
60
     *
61
     * @var string
62
     */
63
    public $default_controller = null;
64
65
    /**
66
     * @var boolean
67
     */
68
    public $auto_track_element = true;
69
70
    /**
71
     * @var array of jQuery_Chains
72
     */
73
    public $js = array();
74
75
    /**
76
     * Using dq property looks obsolete, but left for compatibility
77
     *
78
     * @see self::setModel()
79
     * @var DB_dsql
80
     */
81
    public $dq;
82
83
84
    // {{{ Basic Operations
85
86
    /**
87
     * For safety, you can't clone views. Use $view->newInstance instead.
88
     */
89
    public function __clone()
90
    {
91
        throw $this->exception('Can\'t clone Views');
92
    }
93
    /**
94
     * Associate view with a model. Additionally may initialize a controller
95
     * which would copy fields from the model into the View.
96
     *
97
     * @param object|string $model Class without "Model_" prefix or object
98
     * @param array|string|null $actual_fields List of fields in order to populate
99
     *
100
     * @return AbstractModel object
101
     */
102
    public function setModel($model, $actual_fields = UNDEFINED)
103
    {
104
        parent::setModel($model);
105
106
        // Some models will want default controller to be associated
107
        if ($this->model->default_controller) {
108
            $this->controller
109
                = $this->model->setController($this->model->default_controller);
110
        }
111
112
        // Use our default controller if present
113
        if ($this->default_controller) {
114
            $this->controller = $this->setController($this->default_controller);
115
        }
116
117
        if ($this->controller) {
118
            if ($this->controller->hasMethod('setActualFields')) {
119
                $this->controller->setActualFields($actual_fields);
0 ignored issues
show
Documentation Bug introduced by
The method setActualFields does not exist on object<AbstractController>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
120
            }
121
            if ($this->controller->hasMethod('_bindView')) {
122
                $this->controller->_bindView();
0 ignored issues
show
Documentation Bug introduced by
The method _bindView does not exist on object<AbstractController>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
123
            }
124
        }
125
126
        if ($this->model instanceof SQL_Model) {
127
            $this->dq = $this->model->_dsql();    // compatibility
128
        }
129
130
        return $this->model;
131
    }
132
133
    /** @internal  used by getHTML */
134
    public $_tsBuffer = '';
135
    /** @internal accumulates output for getHTML */
136
    public function _tsBuffer($t, $data)
137
    {
138
        $this->_tsBuffer .= $data;
139
    }
140
141
    /**
142
     * Converting View into string will render recursively and produce HTML.
143
     * If argument is passed, JavaScript will be added into on_ready section
144
     * of your document like when rendered normally. Note that you might
145
     * require to destroy object if you don't want it's HTML to appear normally.
146
     *
147
     * @param bool $destroy    Destroy object preventing it from rendering
148
     * @param bool $execute_js Also capture JavaScript chains of object
149
     *
150
     * @return string HTML
151
     */
152
    public function getHTML($destroy = true, $execute_js = true)
153
    {
154
        $this->addHook('output', array($this, '_tsBuffer'));
155
        $this->recursiveRender();
156
        $this->removeHook('output', array($this, '_tsBuffer'));
157
        $ret = $this->_tsBuffer;
158
        $this->_tsBuffer = '';
159
        if ($execute_js && isset($this->app->jquery)) {
160
            /** @type App_Web $this->app */
161
            $this->app->jquery->getJS($this);
162
        }
163
        if ($destroy) {
164
            $this->destroy();
165
        }
166
167
        return $ret;
168
    }
169
    // }}}
170
171
    // {{{ Template Setup
172
173
    /**
174
     * Called automatically during init for template initalization.
175
     *
176
     * @param string       $template_spot   Where object's output goes
177
     * @param string|array $template_branch Where objects gets it's template
178
     *
179
     * @return AbstractView $this
180
     *
181
     * @internal
182
     */
183
    public function initializeTemplate($template_spot = null, $template_branch = null)
184
    {
185
        if ($template_spot === null) {
186
            $template_spot = $this->defaultSpot();
187
        }
188
        $this->spot = $template_spot;
189
        if (@$this->owner->template
190
            && !$this->owner->template->is_set($this->spot)
191
        ) {
192
            throw $this->owner->template->exception(
193
                'Spot is not found in owner\'s template'
194
            )->addMoreInfo('spot', $this->spot);
195
        }
196
        if (!isset($template_branch)) {
197
            $template_branch = $this->defaultTemplate();
198
        }
199
        if (isset($template_branch)) {
200
            // template branch would tell us what kind of template we have to
201
            // use. Let's look at several cases:
202
203
            if (is_object($template_branch)) {
204
                // it might be already template instance (object)
205
                $this->template = $template_branch;
206
            } elseif (is_array($template_branch)) {
207
                // it might be array with [0]=template, [1]=tag
208
                if (is_object($template_branch[0])) {
209
                    // if [0] is object, we'll use that
210
                    $this->template = $template_branch[0];
211
                } else {
212
                    $this->template = $this->app->add('Template');
213
                    /** @type Template $this->template */
214
                    $this->template->loadTemplate($template_branch[0]);
215
                }
216
                // Now that we loaded it, let's see which tag we need to cut out
217
                $this->template = $this->template->cloneRegion(
218
                    isset($template_branch[1]) ? $template_branch[1] : '_top'
219
                );
220
            } else {
221
                // brach could be just a string - a region to clone off parent
222
                if (isset($this->owner->template)) {
223
                    $this->template
224
                        = $this->owner->template->cloneRegion($template_branch);
225
                } else {
226
                    $this->template = $this->add('Template');
227
                }
228
            }
229
230
            /** @type Template $this->template */
231
            $this->template->owner = $this;
232
        }
233
234
        // Now that the template is loaded, let's take care of parent's template
235
        if ($this->owner
236
            && (isset($this->owner->template))
237
            && (!empty($this->owner->template))
238
        ) {
239
            $this->owner->template->del($this->spot);
240
        }
241
242
        // Cool, now let's set _name of this template
243
        if ($this->template) {
244
            $this->template->trySet('_name', $this->getJSID());
245
        }
246
    }
247
248
    /**
249
     * This method is called to automatically fill in some of the tags in this
250
     * view. Normally the call is bassed to $app->setTags(), however you can
251
     * extend and add more tags to fill.
252
     */
253
    public function initTemplateTags()
254
    {
255
        if ($this->template
256
            && $this->app->hasMethod('setTags')
257
        ) {
258
            /** @type App_Web $this->app */
259
            $this->app->setTags($this->template);
260
        }
261
    }
262
263
    /**
264
     * This method is commonly redefined to set a default template for an object.
265
     * If you return string, object will try to clone specified region off the
266
     * parent. If you specify array, it will load and parse a separate template.
267
     *
268
     * This is overriden by 4th argument in add() method
269
     *
270
     * @return string Template definition
271
     */
272
    public function defaultTemplate()
273
    {
274
        return $this->spot;
275
    }
276
277
    /**
278
     * Normally when you add a view, it's output is placed inside <?$Content?>
279
     * tag of its parent view. You can specify a different tag as 3rd argument
280
     * for the add() method. If you wish for object to use different tag by
281
     * default, you can override this method.
282
     *
283
     * @return string Tag / Spot in $this->owner->template
284
     */
285
    public function defaultSpot()
286
    {
287
        return 'Content';
288
    }
289
    // }}}
290
291
    // {{{ Rendering, see http://agiletoolkit.org/learn/understand/api/exec
292
    /**
293
     * Recursively renders all views. Calls render() for all or for the one
294
     * being cut. In some cases you may want to redefine this function instead
295
     * of render(). The difference is that this function is called before
296
     * sub-views are rendered, but render() is called after.
297
     *
298
     * function recursiveRender(){
299
     *   $this->add('Text')->set('test');
300
     *   return parent::recursiveRender(); // will render Text also
301
     * }
302
     *
303
     * When cut_object is specified in the GET arguments, then output
304
     * of HTML would be limited to object with matching $name or $short_name.
305
     *
306
     * This method will be called instead of default render() and it will
307
     * stop rendering process and output object's HTML once it finds
308
     * a suitable object. Exception_StopRender is used to terminate
309
     * rendering process and bubble up to the APP. This exception is
310
     * not an error.
311
     */
312
    public function recursiveRender()
313
    {
314
        if ($this->hook('pre-recursive-render')) {
315
            return;
316
        }
317
318
        $cutting_here = false;
319
        $cutting_output = '';
320
321
        $this->initTemplateTags();
322
323
        if (isset($_GET['cut_object'])
324
            && ($_GET['cut_object'] == $this->name
325
            || $_GET['cut_object'] == $this->short_name)
326
        ) {
327
            // If we are cutting here, render childs and then we are done
328
            unset($_GET['cut_object']);
329
            $cutting_here = true;
330
331
            $this->addHook('output', function ($self, $output) use (&$cutting_output) {
332
                $cutting_output .= $output;
333
            });
334
        }
335
336
        if ($this->model
337
            && is_object($this->model)
338
            && $this->model->loaded()
339
        ) {
340
            $this->modelRender();
341
        }
342
343
        foreach ($this->elements as $key => $obj) {
344
            if ($obj instanceof self) {
345
                $obj->recursiveRender();
346
                $obj->moveJStoParent();
347
            }
348
        }
349
350
        if (!isset($_GET['cut_object'])) {
351
            if (isset($_GET['cut_region'])) {
352
                $this->region_render();
0 ignored issues
show
Deprecated Code introduced by
The method AbstractView::region_render() has been deprecated with message: 4.3.1

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
353
            } else {
354
                $this->render();
355
            }
356
        }
357
358
        if ($cutting_here) {
359
            //$result=$this->owner->template->cloneRegion($this->spot)->render();
360
            if (isset($this->app->jquery)) {
361
                /** @type App_Web $this->app */
362
                $this->app->jquery->getJS($this);
363
            }
364
            throw new Exception_StopRender($cutting_output);
365
        }
366
        // if template wasn't cut, we move all JS chains to parent
367
    }
368
369
    /**
370
     * When model is specified for a view, values of the model is
371
     * inserted inside the template if corresponding tags exist.
372
     * This is used as default values and filled out before
373
     * the actual render kicks in.
374
     */
375
    public function modelRender()
376
    {
377
        $this->template->set($this->model->get());
378
    }
379
380
    /**
381
     * Append our chains to owner's chains. JS chains bubble up to
382
     * app, which plugs them into template. If the object is being
383
     * "cut" then only relevant chains will be outputed.
384
     */
385
    public function moveJStoParent()
386
    {
387
        /** @type AbstractView $this->owner */
388
        $this->owner->js = array_merge_recursive($this->owner->js, $this->js);
389
    }
390
391
    /**
392
     * Default rendering method. Generates HTML presentation of $this view.
393
     * For most views, rendering the $this->template would be sufficient.
394
     *
395
     * If your view requires to do some heavy-duty work, please be sure to do
396
     * it inside render() method. This way would save some performance in cases
397
     * when your object is not being rendered.
398
     *
399
     * render method relies on method output(), which appeends HTML chunks
400
     * to the parent's template.
401
     */
402
    public function render()
403
    {
404
        if (!($this->template)) {
405
            throw $this->exception('You should specify template for this object')
406
                ->addMoreInfo('object', $this->name)
407
                ->addMoreInfo('spot', $this->spot);
408
        }
409
        $this->output(($render = $this->template->render()));
410
        if (@$this->debug) {
411
            echo '<font color="blue">'.htmlspecialchars($render).'</font>';
412
        }
413
    }
414
415
    /**
416
     * Low level output function which append's to the parent object's
417
     * template. For normal objects, you simply need to specify a suitable
418
     * template.
419
     *
420
     * @param string $txt HTML chunk
421
     */
422
    public function output($txt)
423
    {
424
        if (!is_null($this->hook('output', array($txt)))) {
425
            if (isset($this->owner->template)
426
                && !empty($this->owner->template)
427
            ) {
428
                $this->owner->template->append($this->spot, $txt, false);
429
            } elseif ($this->owner instanceof App_CLI) {
430
                echo $txt;
431
            }
432
        }
433
    }
434
435
    /**
436
     * When "cut"-ing using cut_region we need to output only a specified
437
     * tag. This method of cutting is mostly un-used now, and should be
438
     * considered obsolete.
439
     *
440
     * @deprecated 4.3.1
441
     */
442
    public function region_render()
443
    {
444
        throw $this->exception('cut_region is now obsolete');
445
446
        /*
447
        if ($this->template_flush) {
448
            if ($this->app->jquery) {
449
                $this->app->jquery->getJS($this);
450
            }
451
            throw new Exception_StopRender(
452
                $this->template->cloneRegion($this->template_flush)->render()
453
            );
454
        }
455
        $this->render();
456
        if ($this->spot == $_GET['cut_region']) {
457
            $this->owner->template_flush = $_GET['cut_region'];
458
        }
459
        */
460
    }
461
462
    // }}}
463
464
    // {{{ Object JavaScript Interface
465
    /**
466
     * Views in Agile Toolkit can assign javascript actions to themselves. This
467
     * is done by calling $view->js() method.
468
     *
469
     * Method js() will return jQuery_Chain object which would record all calls
470
     * to it's non-existant methods and convert them into jQuery call chain.
471
     *
472
     * js([action], [other_chain]);
473
     *
474
     * Action can represent javascript event, such as "click" or "mouseenter".
475
     * If you specify action = true, then the event will ALWAYS be executed on
476
     * pageload. It will also be executed if respective view is being reloaded
477
     * by js()->reload()
478
     *
479
     * (Do not make mistake by specifying "true" instead of true)
480
     *
481
     * action = false will still return jQuery chain but will not bind it.
482
     * You can bind it by passing to a different object's js() call as 2nd
483
     * argument or output the chain in response to AJAX-ec call by calling
484
     * execute() method.
485
     *
486
     * 1. Calling with arguments:
487
     *
488
     * $view->js();                   // does nothing
489
     * $a = $view->js()->hide();      // creates chain for hiding $view but does not
490
     *                                // bind to event yet.
491
     *
492
     * 2. Binding existing chains
493
     * $img->js('mouseenter', $a);    // binds previously defined chain to event on
494
     *                                // event of $img.
495
     *
496
     * Produced code: $('#img_id').click(function(ev){ ev.preventDefault();
497
     *    $('view1').hide(); });
498
     *
499
     * 3. $button->js('click',$form->js()->submit());
500
     *                                // clicking button will result in form submit
501
     *
502
     * 4. $view->js(true)->find('.current')->text($text);
503
     *
504
     * Will convert calls to jQuery chain into JavaScript string:
505
     *  $('#view').find('.current').text('abc');    // The $text will be json-encoded
506
     *                                              // to avoid JS injection.
507
     *
508
     * 5. ON YOUR OWN RISK
509
     *
510
     *  $view->js(true,'alert(123)');
511
     *
512
     * Will inject javascript un-escaped portion of javascript into chain.
513
     * If you need to have a custom script then put it into file instead,
514
     * save into templates/js/myfile.js and then  include:
515
     *
516
     *  $view->js()->_load('myfile');
517
     *
518
     * It's highly suggested to bind your libraries with jQuery namespace by
519
     * registered them as plugins, this way you can call your function easily:
520
     *
521
     *  $view->js(true)->_load('myfile')->myplugin('myfunc',array($arg,$arg));
522
     *
523
     * This approach is compatible with jQuery UI Widget factory and will keep
524
     * your code clean
525
     *
526
     * @param string|bool|null          $when     Event when chain will be executed
527
     * @param array|jQuery_Chain|string $code     JavaScript chain(s) or code
528
     * @param string                    $instance Obsolete
529
     *
530
     * @link http://agiletoolkit.org/doc/js
531
     *
532
     * @return jQuery_Chain
533
     */
534
    public function js($when = null, $code = null, $instance = null)
535
    {
536
        // Create new jQuery_Chain object
537
        if (!isset($this->app->jquery)) {
538
            throw new BaseException('requires jQuery or jUI support');
539
        }
540
541
        /** @type App_Web $this->app */
542
543
        // Substitute $when to make it better work as a array key
544
        if ($when === true) {
545
            $when = 'always';
546
        }
547
        if ($when === false || $when === null) {
548
            $when = 'never';
549
        }
550
551
        if ($instance !== null && isset($this->js[$when][$instance])) {
552
            $js = $this->js[$when][$instance];
553
        } else {
554
            $js = $this->app->jquery->chain($this);
0 ignored issues
show
Bug introduced by
The property jquery does not seem to exist in App_CLI.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
555
        }
556
557
        if ($code) {
558
            $js->_prepend($code);
559
        }
560
561
        if ($instance !== null) {
562
            $this->js[$when][$instance] = $js;
563
        } else {
564
            $this->js[$when][] = $js;
565
        }
566
567
        return $js;
568
    }
569
570
    /**
571
     * @return string
572
     */
573
    public function getJSID()
574
    {
575
        return str_replace('/', '_', $this->name);
576
    }
577
578
    /**
579
     * Views in Agile Toolkit can assign javascript actions to themselves. This
580
     * is done by calling $view->js() or $view->on().
581
     *
582
     * on() method implements implementation of jQuery on() method.
583
     *
584
     * on(event, [selector], [other_chain])
585
     *
586
     * Returned is a javascript chain wich is executed when event is triggered
587
     * on specified selector (or all of the view if selector is ommitted).
588
     * Optional other_chain argument can contain one or more chains (in array)
589
     * which will also be executed.
590
     *
591
     * The chain returned by on() will properly select affected element. For
592
     * example if the following view would contain multiple <a> elements, then
593
     * only the clicked-one will be hidden.
594
     *
595
     * on('click','a')->hide();
596
     *
597
     *
598
     * Other_chain can also be specified as a Callable. In this case the
599
     * executable code you have specified here will be called with several
600
     * arguments:
601
     *
602
     * function($js, $data){
603
     *   $js->hide();
604
     * }
605
     *
606
     *
607
     * In this case javascript method is executed on a clicked event but
608
     * in a more AJAX-way
609
     *
610
     * If your method returns a javascript chain, it will be executed
611
     * instead. You can execute both if you embed $js inside returned
612
     * chain.
613
     *
614
     * The third argument passed to your method contains
615
     */
616
    public function on($event, $selector = null, $js = null)
617
    {
618
        /** @type App_Web $this->app */
619
620
        if (!is_string($selector) && is_null($js)) {
621
            $js = $selector;
622
            $selector = null;
623
        }
624
625
        if (is_callable($js)) {
626
            /** @type VirtualPage $p */
627
            $p = $this->add('VirtualPage');
628
629
            $p->set(function ($p) use ($js) {
630
                /** @type VirtualPage $p */
631
                // $js is an actual callable
632
                $js2 = $p->js()->_selectorRegion();
633
634
                $js3 = call_user_func($js, $js2, $_POST);
635
636
                // If method returns something, execute that instead
637
                if ($js3) {
638
                    $p->js(null, $js3)->execute();
639
                } else {
640
                    $js2->execute();
641
                }
642
            });
643
644
            $js = $this->js()->_selectorThis()->univ()->ajaxec($p->getURL(), true);
645
        }
646
647
        if ($js) {
648
            $ret_js = $this->js(null, $js)->_selectorThis();
649
        } else {
650
            $ret_js = $this->js()->_selectorThis();
651
        }
652
653
        $on_chain = $this->js(true);
654
        $fired = false;
655
656
        $this->app->jui->addHook(
657
            'pre-getJS',
658
            function ($app) use ($event, $selector, $ret_js, $on_chain, &$fired) {
659
                if ($fired) {
660
                    return;
661
                }
662
                $fired = true;
663
664
                $on_chain->on($event, $selector, $ret_js->_enclose(null, true));
665
            }
666
        );
667
668
        return $ret_js;
669
    }
670
    // }}}
671
}
672