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 |
||
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() |
||
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); |
||
|
|||
120 | } |
||
121 | if ($this->controller->hasMethod('_bindView')) { |
||
122 | $this->controller->_bindView(); |
||
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() |
||
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(); |
||
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() |
||
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() |
||
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); |
||
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() |
||
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) |
||
670 | // }}} |
||
671 | } |
||
672 |
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: