PageRenderTime 50ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/atk4/lib/Model.php

https://github.com/mahimarathore/mahi
PHP | 583 lines | 333 code | 53 blank | 197 comment | 75 complexity | 2fb393664cc454bb5fc981102f14a9b6 MD5 | raw file
Possible License(s): AGPL-3.0, MPL-2.0-no-copyleft-exception
  1. <?php // vim:ts=4:sw=4:et:fdm=marker
  2. /*
  3. * Undocumented
  4. *
  5. * @link http://agiletoolkit.org/
  6. *//*
  7. ==ATK4===================================================
  8. This file is part of Agile Toolkit 4
  9. http://agiletoolkit.org/
  10. (c) 2008-2013 Agile Toolkit Limited <info@agiletoolkit.org>
  11. Distributed under Affero General Public License v3 and
  12. commercial license.
  13. See LICENSE or LICENSE_COM for more information
  14. =====================================================ATK4=*/
  15. /**
  16. * Implementation of a Generic Model.
  17. * @link http://agiletoolkit.org/doc/model
  18. *
  19. * Model has fields which you add with addField() and access through get() and set()
  20. * You can also load and save model through different storage controllers.
  21. *
  22. * This model is designed to work with linear, non-SQL resources, if you are looking
  23. * to have support for joins, ordering, advanced SQL syntax, look into Model_Table
  24. *
  25. * It's recommended that you create your own model class based on generic model where
  26. * you define fields, but you may also use instance of generic model.
  27. *
  28. * Use:
  29. * class Model_PageCache extends Model {
  30. * function init(){
  31. * parent::init();
  32. * $this->addField('content')->allowHtml(true);
  33. * }
  34. * function generateContent(){
  35. * //complex computation
  36. * // ...
  37. * $this->set('content',$content);
  38. * }
  39. * }
  40. *
  41. *
  42. * $pc=$this->add('Model_PageCache')->addCache('Memcached');
  43. * $pc->load($this->api->page);
  44. *
  45. * if(!$pc->loaded()){
  46. * $pc->set('page',$this->api->page');
  47. * $pc->generateContent();
  48. * $pc->save();
  49. * }
  50. *
  51. *
  52. * @license See http://agiletoolkit.org/about/license
  53. *
  54. **/
  55. class Model extends AbstractModel implements ArrayAccess,Iterator {
  56. public $default_exception='Exception';
  57. /** The class prefix used by addField */
  58. public $field_class='Field';
  59. /** If true, model will now allow to set values for non-existant fields */
  60. public $strict_fields=false;
  61. /** Contains name of table, session key, collection or file, depending on a driver */
  62. public $table=null;
  63. /** Controllers store some custom informatio in here under key equal to their name */
  64. public $_table=array();
  65. /** Contains identifier of currently loaded record or null. Use load() and reset() */
  66. public $id=null; // currently loaded record
  67. /** The actual ID field of the table might now always be "id" */
  68. public $id_field='id'; // name of ID field
  69. public $title_field='name'; // name of descriptive field. If not defined, will use table+'#'+id
  70. // Curretly loaded record
  71. public $data=array();
  72. public $dirty=array();
  73. public $actual_fields=false;// Array of fields which will be used in further select operations. If not defined, all fields will be used.
  74. protected $_save_as=null;
  75. protected $_save_later=false;
  76. // {{{ Basic functionality, field definitions, set(), get() and related methods
  77. function init(){
  78. parent::init();
  79. if(method_exists($this,'defineFields'))
  80. throw $this->exception('model->defineField() is obsolete. Change to init()','Obsolete')
  81. ->addMoreInfo('class',get_class($this));
  82. }
  83. function __clone(){
  84. parent::__clone();
  85. foreach($this->elements as $key=>$el)if(is_object($el)){
  86. $this->elements[$key]=clone $el;
  87. $this->elements[$key]->owner=$this;
  88. }
  89. }
  90. /** Creates field definition object containing field meta-information such as caption, type
  91. * validation rules, default value etc */
  92. function addField($name){
  93. return $this
  94. ->add($this->field_class,$name);
  95. }
  96. /** Set value of the field. If $this->strict_fields, will throw exception for non-existant fields. Can also accept array */
  97. function set($name,$value=undefined){
  98. if(is_array($name)){
  99. foreach($name as $key=>$val)$this->set($key,$val);
  100. return $this;
  101. }
  102. if($name===false || $name===null){
  103. return $this->reset();
  104. }
  105. // Verify if such a filed exists
  106. if($this->strict_fields && !$this->hasElement($name))throw $this->exception('No such field','Logic')
  107. ->addMoreInfo('name',$name);
  108. if($value!==undefined && (
  109. is_null($value)!=is_null($this->data[$name]) ||
  110. is_object($value) ||
  111. is_object($this->data[$name]) ||
  112. (string)$value!=(string)$this->data[$name]
  113. )){
  114. $this->data[$name]=$value;
  115. $this->setDirty($name);
  116. }
  117. return $this;
  118. }
  119. /** Return value of the field. If unspecified will return array of all fields. */
  120. function get($name=null){
  121. if($name===null)return $this->data;
  122. if($this->strict_fields && !$this->hasElement($name))
  123. throw $this->exception('No such field','Logic')->addMoreInfo('field',$name);
  124. if(!isset($this->data[$name]) && !$this->hasElement($name))
  125. throw $this->exception('Model field was not loaded')
  126. ->addMoreInfo('id',$this->id)
  127. ->addMoreinfo('field',$name);
  128. if(@!array_key_exists($name,$this->data)){
  129. return $this->getElement($name)->defaultValue();
  130. }
  131. return $this->data[$name];
  132. }
  133. /**
  134. * Returs list of fields which belong to specific group. You can add fields into groups when you
  135. * define them and it can be used by the front-end to determine which fields needs to be displayed.
  136. *
  137. * If no group is specified, then all non-system fields are displayed for backwards compatibility.
  138. */
  139. function getActualFields($group=undefined){
  140. if($group===undefined && $this->actual_fields)return $this->actual_fields;
  141. $fields=array();
  142. foreach($this->elements as $el)if($el instanceof Field){
  143. if($el->hidden())continue;
  144. if($group===undefined || $el->group()==$group ||
  145. ($group=='visible' && $el->visible()) ||
  146. ($group=='editable' && $el->editable())
  147. ){
  148. $fields[]=$el->short_name;
  149. }
  150. }
  151. return $fields;
  152. }
  153. /** Default set of fields which will be included into further queries */
  154. function setActualFields(array $fields){
  155. $this->actual_fields=$fields;
  156. return $this;
  157. }
  158. /** When fields are changed, they are marked dirty. Only dirty fields are saved when save() is called */
  159. function setDirty($name){
  160. $this->dirty[$name]=true;
  161. }
  162. /** Returns if the records has been loaded successfully */
  163. function loaded(){
  164. return !is_null($this->id);
  165. }
  166. /** Forget loaded data */
  167. function unload(){
  168. if($this->loaded())$this->hook('beforeUnload');
  169. $this->data=$this->dirty=array();
  170. $this->id=null;
  171. $this->hook('afterUnload');
  172. return $this;
  173. }
  174. function reset(){
  175. return $this->unload();
  176. }
  177. // }}}
  178. // {{{ ArrayAccess support
  179. function offsetExists($name){
  180. return $this->hasElement($name);
  181. }
  182. function offsetGet($name){
  183. return $this->get($name);
  184. }
  185. function offsetSet($name,$val){
  186. $this->set($name,$val);
  187. }
  188. function offsetUnset($name){
  189. unset($this->dirty[$name]);
  190. }
  191. // }}}
  192. /// {{{ Operation with external Data Controllers
  193. /** Associates appropriate controller and loads data such as 'Array' for Controller_Data_Array class */
  194. function setSource($controller, $table=null, $id=null){
  195. if(is_string($controller)){
  196. $controller=$this->api->normalizeClassName($controller,'Data');
  197. } elseif(!$controller instanceof Controller_Data){
  198. throw $this->exception('Inapropriate Controller. Must extend Controller_Data');
  199. }
  200. $this->controller=$this->setController($controller);
  201. $this->controller->setSource($this,$table);
  202. if($id)$this->load($id);
  203. return $this;
  204. }
  205. /** Cache controller is used to attempt and load data a little faster then the primary controller */
  206. function addCache($controller, $table=null, $priority=5){
  207. $controller=$this->api->normalizeClassName($controller,'Data');
  208. return $this->setController($controller)
  209. ->addHooks($this,$priority)
  210. ->setSource($this,$table);
  211. }
  212. /** Attempt to load record with specified ID. If this fails, exception is thrown */
  213. function load($id=null){
  214. if($this->loaded())$this->unload();
  215. $this->hook('beforeLoad',array($id));
  216. if(!$this->loaded())$this->controller->load($this,$id);
  217. if(!$this->loaded())throw $this->exception('Record ID must be specified, otherwise use tryLoad()');
  218. $this->hook('afterLoad');
  219. return $this;
  220. }
  221. /** Saves record with current controller. If no argument is specified, uses $this->id. Specifying "false" will create
  222. * record with new ID. */
  223. function save($id=undefined){
  224. if($this->id_field && $id!==undefined && $id!==null){
  225. $this->data[$this->id_field]=$id;
  226. }
  227. if($id!==undefined)$this->id=$id;
  228. $this->hook('beforeSave',array($this->id));
  229. $this->id=$this->controller->save($this,$this->id);
  230. if($this->loaded())$this->hook('afterSave',array($this->id));
  231. return $this;
  232. }
  233. /** Save model and don't try to load it back */
  234. function saveAndUnload($id=undefined){
  235. // TODO: See dc032a9ae75341fb7f4ed6c4de61ca224ec0e5e6. Need to
  236. // revert and make sure save() is not re-loading the record.
  237. // (performance)
  238. $this->save($id);
  239. $this->unload();
  240. return $this;
  241. }
  242. /** Will save model later, when it's being destructed by Garbage Collector */
  243. function saveLater(){
  244. $this->_save_later=true;
  245. return $this;
  246. }
  247. function __destruct(){
  248. if($this->_save_later){
  249. $this->saveAndUnload();
  250. }
  251. }
  252. /** Deletes record associated with specified $id. If not specified, currently loaded record is deleted (and unloaded) */
  253. function delete($id=null){
  254. if($id===null)$id=$this->id;
  255. if($this->loaded() && $this->id == $id)$this->unload(); // record we are about to delete is loaded, unload it.
  256. $this->hook('beforeDelete',array($id));
  257. $this->controller->delete($this,$id);
  258. $this->hook('afterDelete',array($id));
  259. return $this;
  260. }
  261. /** Deletes all records associated with this modle. */
  262. function deleteAll(){
  263. if($this->loaded())$this->unload();
  264. $this->hook('beforeDeleteAll');
  265. $this->controller->deleteAll($this);
  266. $this->hook('afterDeleteAll');
  267. return $this;
  268. }
  269. // }}}
  270. // {{{ Load Wrappers
  271. /* Attempt to load record with specified ID. If this fails, no error is produced */
  272. function tryLoad($id=null){
  273. if($this->loaded())$this->unload();
  274. $this->hook('beforeLoad',array($id));
  275. if(!$this->loaded())$this->controller->tryLoad($this,$id);
  276. if(!$this->loaded())return $this;
  277. $this->hook('afterLoad');
  278. return $this;
  279. }
  280. function tryLoadAny(){
  281. if($this->loaded())$this->unload();
  282. if(!$this->loaded())$this->controller->tryLoadAny($this,$id);
  283. if(!$this->loaded())return $this;
  284. $this->hook('afterLoad');
  285. return $this;
  286. }
  287. function tryLoadBy($field,$cond=undefined,$value=undefined){
  288. if($this->loaded())$this->unload();
  289. $this->hook('beforeLoadBy',array($field,$cond,$value));
  290. if(!$this->loaded())$this->controller->tryLoadBy($this,$field,$cond,$value);
  291. if(!$this->loaded())return $this;
  292. $this->hook('afterLoad');
  293. return $this;
  294. }
  295. function loadBy($field,$cond=undefined,$value=undefined){
  296. if($this->loaded())$this->unload();
  297. $this->hook('beforeLoadBy',array($field,$cond,$value));
  298. if(!$this->loaded())$this->controller->loadBy($this,$field,$cond,$value);
  299. if(!$this->loaded())return $this;
  300. $this->hook('afterLoad');
  301. return $this;
  302. }
  303. // }}}
  304. // {{{ Ordering and limiting support
  305. function setLimit($a,$b=null){
  306. if($this->controller && $this->controller->hasMethod('setLimit'))
  307. $this->controller->setLimit($this,$field,$desc);
  308. return $this;
  309. }
  310. function setOrder($field,$desc=null){
  311. if($this->controller && $this->controller->hasMethod('setOrder'))
  312. $this->controller->setOrder($this,$field,$desc);
  313. return $this;
  314. }
  315. // }}}
  316. // {{{ Iterator support
  317. function rewind(){
  318. $this->reset();
  319. $this->controller->rewind($this);
  320. if($this->loaded())$this->hook('afterLoad');
  321. }
  322. function next(){
  323. $this->controller->next($this);
  324. if($this->loaded())$this->hook('afterLoad');
  325. return $this;
  326. }
  327. function current(){
  328. return $this->get();
  329. }
  330. function key(){
  331. return $this->id;
  332. }
  333. function valid(){
  334. return $this->loaded();
  335. }
  336. function getRows($fields=null){
  337. $result=array();
  338. foreach($this as $row){
  339. if (is_null($fields)) {
  340. $result[]=$row;
  341. } else {
  342. $tmp=array();
  343. foreach($fields as $field){
  344. $tmp[$field]=$row[$field];
  345. }
  346. $result[]=$tmp;
  347. }
  348. }
  349. return $result;
  350. }
  351. /**
  352. * A handy shortcut for foreach(){ .. } code. Make your callable return
  353. * "false" if you would like to break the loop.
  354. *
  355. * @param callable $callable will be executed for each member
  356. *
  357. * @return AbstractObject $this
  358. */
  359. function each($callable)
  360. {
  361. if (!($this instanceof Iterator)) {
  362. throw $this->exception('Calling each() on non-iterative model');
  363. }
  364. foreach ($this as $value) {
  365. if (call_user_func($callable, $this) === false) {
  366. break;
  367. }
  368. }
  369. return $this;
  370. }
  371. // }}}
  372. // TODO: worry about cloning!
  373. function newField($name){
  374. return $this->addField($name);
  375. }
  376. function hasField($name){
  377. return $this->hasElement($name);
  378. }
  379. function getEntityCode(){
  380. return $this->table?:$this->entity_code;
  381. }
  382. function getField($f){
  383. return $this->getElement($f);
  384. }
  385. // Reference traversal for regular models
  386. public $_references;
  387. /* defines relation between models. You can traverse the reference using ref() */
  388. function hasOne($model,$our_field=undefined,$field_class='Field'){
  389. // if our_field is not specified, let's try to guess it from other model's table
  390. if($our_field===undefined){
  391. // determine the actual class of the other model
  392. if(!is_object($model)){
  393. $tmp=$this->api->normalizeClassName($model,'Model');
  394. $tmp=new $tmp; // avoid recursion
  395. }else $tmp=$model;
  396. $our_field=($tmp->table).'_id';
  397. }
  398. $this->_references[$our_field]=$model;
  399. if($our_field !== null && $our_field!=='_id' && !$this->hasElement($our_field)){
  400. return $this->add($this->field_class,$our_field);
  401. }
  402. return null; // no field added
  403. }
  404. /* defines relation for non-sql model. You can traverse the reference using ref() */
  405. function hasMany($model,$their_field=undefined,$our_field=undefined){
  406. $model=$this->api->normalizeClassName($this->model,'Model');
  407. $this->_references[$model]=array($model,$our_field,$their_field);
  408. return null;
  409. }
  410. /*
  411. * How references work:
  412. *
  413. * $this->hasMany('Chapter'); // hasMany('Section'), hasOne('Picture');
  414. * $this->hasOne('Author'); // hasMany('Book'), hasOne('Person','father_id'), hasOne('Person','mother_id')
  415. *
  416. * $this->ref('Chapter');
  417. * 1. Creates Model_Chapter
  418. * 2. Model_Chapter -> addCondition() // meaning the traversed model must support them!
  419. * 3. Returns
  420. *
  421. * $this->ref('Chapter/Section');
  422. * 1. $b=Creates Model_Chapter
  423. * 2. Calls $c=Model_Chapter->_ref('Section'); which only returns model, no binding
  424. * 3. Decisions:
  425. * hasMany
  426. * a. $b is loaded(). $c->addCondition();
  427. * b. $b and $c are both SQL. $c->join($b);
  428. * c. load all id's from $b. $c->addCondition(field,ids);
  429. * hasOne()
  430. * a. $b is loaded(). $c->load($b[field]);
  431. * b. $b and $c are both SQL. $c->join($b);
  432. * c. load all [field] values, $c->addCondition('id',ids);
  433. *
  434. * Book/Chapter/Section both SQL:
  435. * $book->load(5);
  436. * $book->ref('Chapter/Section'); // get all sections
  437. * $c=$b->_ref('Chapter/Section')
  438. * $s=$c->_ref('Section');
  439. * if($s and $c sql){
  440. * $s->addCondition('chapter_id',$c->getElement('id'))
  441. * }
  442. /* For a current model, will resolve the reference, initialize the related model and call _refBind. If this
  443. * is a deep traversing, then it will also specify a field_out to acquire expression, which will be passed
  444. * into the further model and so on.
  445. *
  446. * if the submodel's ref() will return
  447. */
  448. function ref($ref){
  449. $id=$this->get($ref);
  450. return $this->_ref($ref,$id);
  451. }
  452. /* Join Binding
  453. * ============
  454. *
  455. * SQL generally treat Joins better, because they can create an execution plan and they don't need to wait for the
  456. * first subquery to complete before starting working on the next query.
  457. *
  458. * Join binding exists as an extension in Model_Table::_ref(). It will iterate through array of models and load
  459. * them into array until it hits non-SQL model (then selects field_out) or reaches the end of chain. In either
  460. * case it will then back-step to the start of the chain gradually joining each table and skipping tables which
  461. * have field_in same as field_out.
  462. *
  463. /* Subselect Binding
  464. * =================
  465. *
  466. * Binding conditions when traversing. The model must apply field=expression, however this might work differently
  467. * depending on the type of, the second argument and the refBind implementation.
  468. *
  469. * If the model cannot embed this type of expression into field condition, it must call $expression->get(), fetch
  470. * all the IDs and then use them instead. This insures intercompatibility between different model implementation.
  471. *
  472. * If model is using controller, it will attempt to seek controller's help for applying a condition.
  473. *
  474. * If field_out is specified, then the output should be the expression for the next join containing a set of
  475. * values from the field_out.
  476. *
  477. * SQL: select field_out from table where field_in in (expression)
  478. * Generic: foreach(expression->get() as $item){ $res[]=$m->loadBy($field_in,$item[id_field)->get($field_out) };
  479. *
  480. * If field_out is not specified, then the condition must be applied on a current model and the current model
  481. * must be returned with the condition applied. This model bubbles up and is returned through a top-most
  482. * ref / refSQL.
  483. *
  484. * Shortcuts
  485. * ---------
  486. * if field_in and field_out are the same, simply return expression
  487. *
  488. * Book -< Chapter -< Section
  489. * select * from section where chapter_id in (select id from chapter where book_id=5)
  490. *
  491. */
  492. function _refBind($field_in,$expression,$field_out=null){
  493. if($this->controller)return $this->controller->refBind($this,$field,$expression);
  494. list($myref,$rest)=explode('/',$ref,2);
  495. if(!$this->_references[$myref])throw $this->exception('No such relation')
  496. ->addMoreInfo('ref',$myref)
  497. ->addMoreInfo('rest',$rest);
  498. // Determine and populate related model
  499. if(is_array($this->_references[$myref])){
  500. $m=$this->_references[$myref][0];
  501. }else{
  502. $m=$this->_references[$myref];
  503. }
  504. $m=$this->add($m);
  505. if($rest)$m=$m->_ref($rest);
  506. $this->_refGlue();
  507. if(!isset($this->_references[$ref]))throw $this->exception('Unable to traverse, no reference defined by this name')
  508. ->addMoreInfo('name',$ref);
  509. $r=$this->_references[$ref];
  510. if(is_array($r)){
  511. list($m,$our_field,$their_field)=$r;
  512. if(is_string($m)){
  513. $m=$this->add($m);
  514. }else{
  515. $m=$m->newInstance();
  516. }
  517. return $m->addCondition($their_field,$this[$our_field]);
  518. }
  519. if(is_string($m)){
  520. $m=$this->add($m);
  521. }else{
  522. $m=$m->newInstance();
  523. }
  524. return $m->load($this[$our_field]);
  525. }
  526. }