mahi /atk4/lib/Model.php

Language PHP Lines 584
MD5 Hash 2fb393664cc454bb5fc981102f14a9b6
Repository https://github.com/mahimarathore/mahi.git View Raw File
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
<?php // vim:ts=4:sw=4:et:fdm=marker
/*
 * Undocumented
 *
 * @link http://agiletoolkit.org/
*//*
==ATK4===================================================
   This file is part of Agile Toolkit 4
    http://agiletoolkit.org/

   (c) 2008-2013 Agile Toolkit Limited <info@agiletoolkit.org>
   Distributed under Affero General Public License v3 and
   commercial license.

   See LICENSE or LICENSE_COM for more information
 =====================================================ATK4=*/
/**
 * Implementation of a Generic Model. 
 * @link http://agiletoolkit.org/doc/model
 *
 * Model has fields which you add with addField() and access through get() and set()
 * You can also load and save model through different storage controllers.
 *
 * This model is designed to work with linear, non-SQL resources, if you are looking
 * to have support for joins, ordering, advanced SQL syntax, look into Model_Table
 *
 * It's recommended that you create your own model class based on generic model where
 * you define fields, but you may also use instance of generic model.
 *
 * Use:
 * class Model_PageCache extends Model {
 *     function init(){
 *         parent::init();
 *         $this->addField('content')->allowHtml(true);
 *     }
 *     function generateContent(){
 *         //complex computation
 *         // ...
 *         $this->set('content',$content);
 *     }
 * }
 *
 *
 * $pc=$this->add('Model_PageCache')->addCache('Memcached');
 * $pc->load($this->api->page);
 *
 * if(!$pc->loaded()){
 *     $pc->set('page',$this->api->page');
 *     $pc->generateContent();
 *     $pc->save();
 * }
 *
 *
 * @license See http://agiletoolkit.org/about/license
 * 
 **/
class Model extends AbstractModel implements ArrayAccess,Iterator {

    public $default_exception='Exception';

    /** The class prefix used by addField */
    public $field_class='Field';

    /** If true, model will now allow to set values for non-existant fields */
    public $strict_fields=false;

    /** Contains name of table, session key, collection or file, depending on a driver */
    public $table=null;

    /** Controllers store some custom informatio in here under key equal to their name */
    public $_table=array();

    /** Contains identifier of currently loaded record or null. Use load() and reset() */
    public $id=null;     // currently loaded record

    /** The actual ID field of the table might now always be "id" */
    public $id_field='id';   // name of ID field

    public $title_field='name';  // name of descriptive field. If not defined, will use table+'#'+id


    // Curretly loaded record
    public $data=array();
    public $dirty=array();

    public $actual_fields=false;// Array of fields which will be used in further select operations. If not defined, all fields will be used.

    protected $_save_as=null;
    protected $_save_later=false;

    // {{{ Basic functionality, field definitions, set(), get() and related methods
    function init(){
        parent::init();

        if(method_exists($this,'defineFields'))
            throw $this->exception('model->defineField() is obsolete. Change to init()','Obsolete')
            ->addMoreInfo('class',get_class($this));
    }
    function __clone(){
        parent::__clone();
        foreach($this->elements as $key=>$el)if(is_object($el)){
            $this->elements[$key]=clone $el;
            $this->elements[$key]->owner=$this;
        }
    }
    /** Creates field definition object containing field meta-information such as caption, type
     * validation rules, default value etc */
    function addField($name){
        return $this
            ->add($this->field_class,$name);
    }
    /** Set value of the field. If $this->strict_fields, will throw exception for non-existant fields. Can also accept array */
    function set($name,$value=undefined){
        if(is_array($name)){
            foreach($name as $key=>$val)$this->set($key,$val);
            return $this;
        }
        if($name===false || $name===null){
            return $this->reset();
        }

        // Verify if such a filed exists
        if($this->strict_fields && !$this->hasElement($name))throw $this->exception('No such field','Logic')
            ->addMoreInfo('name',$name);

        if($value!==undefined && (
            is_null($value)!=is_null($this->data[$name]) || 
            is_object($value) ||
            is_object($this->data[$name]) || 
            (string)$value!=(string)$this->data[$name]
        )){
            $this->data[$name]=$value;
            $this->setDirty($name);
        }
        return $this;
    }
    /** Return value of the field. If unspecified will return array of all fields.  */
    function get($name=null){
        if($name===null)return $this->data;
        if($this->strict_fields && !$this->hasElement($name))
            throw $this->exception('No such field','Logic')->addMoreInfo('field',$name);
        if(!isset($this->data[$name]) && !$this->hasElement($name))
            throw $this->exception('Model field was not loaded')
            ->addMoreInfo('id',$this->id)
            ->addMoreinfo('field',$name);
        if(@!array_key_exists($name,$this->data)){
            return $this->getElement($name)->defaultValue();
        }
        return $this->data[$name];
    }
    /**
     * Returs list of fields which belong to specific group. You can add fields into groups when you
     * define them and it can be used by the front-end to determine which fields needs to be displayed.
     * 
     * If no group is specified, then all non-system fields are displayed for backwards compatibility.
     */
    function getActualFields($group=undefined){
        if($group===undefined && $this->actual_fields)return $this->actual_fields;
        $fields=array();
        foreach($this->elements as $el)if($el instanceof Field){
            if($el->hidden())continue;
            if($group===undefined || $el->group()==$group ||
                ($group=='visible' && $el->visible()) ||
                ($group=='editable' && $el->editable())
            ){
                $fields[]=$el->short_name;
            }
        }
        return $fields;
    }
    /** Default set of fields which will be included into further queries */
    function setActualFields(array $fields){
        $this->actual_fields=$fields;
        return $this;
    }
    /** When fields are changed, they are marked dirty. Only dirty fields are saved when save() is called */
    function setDirty($name){
        $this->dirty[$name]=true;
    }
    /** Returns if the records has been loaded successfully */
    function loaded(){
        return !is_null($this->id);
    }
    /** Forget loaded data */
    function unload(){
        if($this->loaded())$this->hook('beforeUnload');
        $this->data=$this->dirty=array();
        $this->id=null;
        $this->hook('afterUnload');
        return $this;
    }
    function reset(){
        return $this->unload();
    }
    // }}}

    // {{{ ArrayAccess support 
    function offsetExists($name){
        return $this->hasElement($name);
    }
    function offsetGet($name){
        return $this->get($name);
    }
    function offsetSet($name,$val){
        $this->set($name,$val);
    }
    function offsetUnset($name){
        unset($this->dirty[$name]);
    }
    // }}}

    /// {{{ Operation with external Data Controllers

    /** Associates appropriate controller and loads data such as 'Array' for Controller_Data_Array class */
    function setSource($controller, $table=null, $id=null){
        if(is_string($controller)){
            $controller=$this->api->normalizeClassName($controller,'Data');
        } elseif(!$controller instanceof Controller_Data){
            throw $this->exception('Inapropriate Controller. Must extend Controller_Data');
        }
        $this->controller=$this->setController($controller);

        $this->controller->setSource($this,$table);

        if($id)$this->load($id);
        return $this;
    }
    /** Cache controller is used to attempt and load data a little faster then the primary controller */
    function addCache($controller, $table=null, $priority=5){
        $controller=$this->api->normalizeClassName($controller,'Data');
        return $this->setController($controller)
            ->addHooks($this,$priority)
            ->setSource($this,$table);
    }
    /** Attempt to load record with specified ID. If this fails, exception is thrown */
    function load($id=null){
        if($this->loaded())$this->unload();
        $this->hook('beforeLoad',array($id));
        if(!$this->loaded())$this->controller->load($this,$id);
        if(!$this->loaded())throw $this->exception('Record ID must be specified, otherwise use tryLoad()');
        $this->hook('afterLoad');
        return $this;
    }
    /** Saves record with current controller. If no argument is specified, uses $this->id. Specifying "false" will create 
     * record with new ID. */
    function save($id=undefined){
        if($this->id_field && $id!==undefined && $id!==null){
            $this->data[$this->id_field]=$id;
        }
        if($id!==undefined)$this->id=$id;


        $this->hook('beforeSave',array($this->id));

        $this->id=$this->controller->save($this,$this->id);

        if($this->loaded())$this->hook('afterSave',array($this->id));
        return $this;
    }
    /** Save model and don't try to load it back */
    function saveAndUnload($id=undefined){
        // TODO: See dc032a9ae75341fb7f4ed6c4de61ca224ec0e5e6. Need to 
        // revert and make sure save() is not re-loading the record.
        // (performance)
        $this->save($id);
        $this->unload();
        return $this;
    }
    /** Will save model later, when it's being destructed by Garbage Collector */
    function saveLater(){
        $this->_save_later=true;
        return $this;
    }
    function __destruct(){
        if($this->_save_later){
            $this->saveAndUnload();
        }
    }
    /** Deletes record associated with specified $id. If not specified, currently loaded record is deleted (and unloaded) */
    function delete($id=null){
        if($id===null)$id=$this->id;
        if($this->loaded() && $this->id == $id)$this->unload();   // record we are about to delete is loaded, unload it.
        $this->hook('beforeDelete',array($id));
        $this->controller->delete($this,$id);
        $this->hook('afterDelete',array($id));
        return $this;
    }
    /** Deletes all records associated with this modle. */
    function deleteAll(){
        if($this->loaded())$this->unload();
        $this->hook('beforeDeleteAll');
        $this->controller->deleteAll($this);
        $this->hook('afterDeleteAll');
        return $this;
    }
    // }}}

    // {{{ Load Wrappers

    /* Attempt to load record with specified ID. If this fails, no error is produced */
    function tryLoad($id=null){
        if($this->loaded())$this->unload();
        $this->hook('beforeLoad',array($id));
        if(!$this->loaded())$this->controller->tryLoad($this,$id);
        if(!$this->loaded())return $this;
        $this->hook('afterLoad');
        return $this;
    }
    function tryLoadAny(){
        if($this->loaded())$this->unload();
        if(!$this->loaded())$this->controller->tryLoadAny($this,$id);
        if(!$this->loaded())return $this;
        $this->hook('afterLoad');
        return $this;
    }
    function tryLoadBy($field,$cond=undefined,$value=undefined){
        if($this->loaded())$this->unload();
        $this->hook('beforeLoadBy',array($field,$cond,$value));
        if(!$this->loaded())$this->controller->tryLoadBy($this,$field,$cond,$value);
        if(!$this->loaded())return $this;
        $this->hook('afterLoad');
        return $this;
    }
    function loadBy($field,$cond=undefined,$value=undefined){
        if($this->loaded())$this->unload();
        $this->hook('beforeLoadBy',array($field,$cond,$value));
        if(!$this->loaded())$this->controller->loadBy($this,$field,$cond,$value);
        if(!$this->loaded())return $this;
        $this->hook('afterLoad');
        return $this;
    }
    // }}}

    // {{{ Ordering and limiting support
    function setLimit($a,$b=null){
        if($this->controller && $this->controller->hasMethod('setLimit'))
            $this->controller->setLimit($this,$field,$desc);
        return $this;
    }
    function setOrder($field,$desc=null){
        if($this->controller && $this->controller->hasMethod('setOrder'))
            $this->controller->setOrder($this,$field,$desc);
        return $this;
    }
    // }}}

    // {{{ Iterator support 
    function rewind(){
        $this->reset();
        $this->controller->rewind($this);
        if($this->loaded())$this->hook('afterLoad');
    }
    function next(){
        $this->controller->next($this);
        if($this->loaded())$this->hook('afterLoad');
        return $this;
    }
    function current(){
        return $this->get();
    }
    function key(){
        return $this->id;
    }
    function valid(){
        return $this->loaded();
    }

    function getRows($fields=null){
        $result=array();
        foreach($this as $row){
            if (is_null($fields)) {
                $result[]=$row;
            } else {
                $tmp=array();
                foreach($fields as $field){
                    $tmp[$field]=$row[$field];
                }
                $result[]=$tmp;
            }
        }
        return $result;
    }

    /**
     * A handy shortcut for foreach(){ .. } code. Make your callable return
     * "false" if you would like to break the loop.
     *
     * @param callable $callable will be executed for each member
     *
     * @return AbstractObject $this
     */
    function each($callable)
    {
        if (!($this instanceof Iterator)) {
            throw $this->exception('Calling each() on non-iterative model');
        }

        foreach ($this as $value) {
            if (call_user_func($callable, $this) === false) {
                break;
            }
        }
        return $this;
    }

    // }}}


    // TODO: worry about cloning!
    function newField($name){
        return $this->addField($name); 
    }
    function hasField($name){
        return $this->hasElement($name);
    }
    function getEntityCode(){
        return $this->table?:$this->entity_code;
    }
    function getField($f){
        return $this->getElement($f);
    }

    // Reference traversal for regular models
    public $_references;

    /* defines relation between models. You can traverse the reference using ref() */
    function hasOne($model,$our_field=undefined,$field_class='Field'){

        // if our_field is not specified, let's try to guess it from other model's table
        if($our_field===undefined){
            // determine the actual class of the other model
            if(!is_object($model)){
                $tmp=$this->api->normalizeClassName($model,'Model');
                $tmp=new $tmp; // avoid recursion
            }else $tmp=$model;
            $our_field=($tmp->table).'_id';
        }

        $this->_references[$our_field]=$model;

        if($our_field !== null && $our_field!=='_id' && !$this->hasElement($our_field)){
            return $this->add($this->field_class,$our_field);
        }

        return null; // no field added
    }
    /* defines relation for non-sql model. You can traverse the reference using ref() */
    function hasMany($model,$their_field=undefined,$our_field=undefined){
        $model=$this->api->normalizeClassName($this->model,'Model');
        $this->_references[$model]=array($model,$our_field,$their_field);
        return null;
    }
    /*
     * How references work:
     *
     * $this->hasMany('Chapter'); // hasMany('Section'), hasOne('Picture');
     * $this->hasOne('Author');   // hasMany('Book'), hasOne('Person','father_id'), hasOne('Person','mother_id')
     *
     * $this->ref('Chapter');
     *   1. Creates Model_Chapter
     *   2. Model_Chapter -> addCondition() // meaning the traversed model must support them!
     *   3. Returns
     *
     * $this->ref('Chapter/Section');
     *   1. $b=Creates Model_Chapter
     *   2. Calls $c=Model_Chapter->_ref('Section'); which only returns model, no binding
     *   3. Decisions:
     *   hasMany
     *      a. $b is loaded(). $c->addCondition();
     *      b. $b and $c are both SQL. $c->join($b);
     *      c. load all id's from $b. $c->addCondition(field,ids);
     *   hasOne()
     *      a. $b is loaded(). $c->load($b[field]);
     *      b. $b and $c are both SQL. $c->join($b);
     *      c. load all [field] values, $c->addCondition('id',ids);
     *
     *  Book/Chapter/Section both SQL:
     *    $book->load(5);
     *    $book->ref('Chapter/Section');  // get all sections
     *      $c=$b->_ref('Chapter/Section')
     *      $s=$c->_ref('Section');
     *      if($s and $c sql){
     *        $s->addCondition('chapter_id',$c->getElement('id'))
     *      }

    /* For a current model, will resolve the reference, initialize the related model and call _refBind. If this
     * is a deep traversing, then it will also specify a field_out to acquire expression, which will be passed
     * into the further model and so on. 
     *
     * if the submodel's ref() will return 
     */
    function ref($ref){
        $id=$this->get($ref);
        return $this->_ref($ref,$id);
    }
    /* Join Binding
     * ============
     *
     * SQL generally treat Joins better, because they can create an execution plan and they don't need to wait for the
     * first subquery to complete before starting working on the next query. 
     *
     * Join binding exists as an extension in Model_Table::_ref(). It will iterate through array of models and load
     * them into array until it hits non-SQL model (then selects field_out) or reaches the end of chain. In either
     * case it will then back-step to the start of the chain gradually joining each table and skipping tables which
     * have field_in same as field_out.
     *

    /* Subselect Binding
     * =================
     *
     * Binding conditions when traversing. The model must apply field=expression, however this might work differently
     * depending on the type of, the second argument and the refBind implementation. 
     *
     * If the model cannot embed this type of expression into field condition, it must call $expression->get(), fetch
     * all the IDs and then use them instead. This insures intercompatibility between different model implementation.
     *
     * If model is using controller, it will attempt to seek controller's help for applying a condition.
     *
     * If field_out is specified, then the output should be the expression for the next join containing a set of
     * values from the field_out.
     *
     * SQL: select field_out from table where field_in in (expression)
     * Generic: foreach(expression->get() as $item){ $res[]=$m->loadBy($field_in,$item[id_field)->get($field_out) };
     *
     * If field_out is not specified, then the condition must be applied on a current model and the current model
     * must be returned with the condition applied. This model bubbles up and is returned through a top-most 
     * ref / refSQL.
     *
     * Shortcuts
     * ---------
     * if field_in and field_out are the same, simply return expression
     *
     * Book -< Chapter -< Section
     * select * from section where chapter_id in (select id from chapter where book_id=5)
     *
     */
    function _refBind($field_in,$expression,$field_out=null){

        if($this->controller)return $this->controller->refBind($this,$field,$expression);

        list($myref,$rest)=explode('/',$ref,2);

        if(!$this->_references[$myref])throw $this->exception('No such relation')
            ->addMoreInfo('ref',$myref)
            ->addMoreInfo('rest',$rest);
        // Determine and populate related model

        if(is_array($this->_references[$myref])){
            $m=$this->_references[$myref][0];
        }else{
            $m=$this->_references[$myref];
        }
        $m=$this->add($m);
        if($rest)$m=$m->_ref($rest);
        $this->_refGlue();
        

        if(!isset($this->_references[$ref]))throw $this->exception('Unable to traverse, no reference defined by this name')
            ->addMoreInfo('name',$ref);

        $r=$this->_references[$ref];

        if(is_array($r)){
            list($m,$our_field,$their_field)=$r;

            if(is_string($m)){
                $m=$this->add($m);
            }else{
                $m=$m->newInstance();
            }

            return $m->addCondition($their_field,$this[$our_field]);
        }


        if(is_string($m)){
            $m=$this->add($m);
        }else{
            $m=$m->newInstance();
        }
        return $m->load($this[$our_field]);
    }
}
Back to Top