PageRenderTime 79ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/app/protected/core/models/RedBeanModel.php

https://bitbucket.org/mstowe/zurmo
PHP | 3070 lines | 2182 code | 151 blank | 737 comment | 270 complexity | 12bf324e583465a0ae4c636f75cb4bff MD5 | raw file
Possible License(s): GPL-3.0, BSD-3-Clause, LGPL-3.0, LGPL-2.1, BSD-2-Clause

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /*********************************************************************************
  3. * Zurmo is a customer relationship management program developed by
  4. * Zurmo, Inc. Copyright (C) 2012 Zurmo Inc.
  5. *
  6. * Zurmo is free software; you can redistribute it and/or modify it under
  7. * the terms of the GNU General Public License version 3 as published by the
  8. * Free Software Foundation with the addition of the following permission added
  9. * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
  10. * IN WHICH THE COPYRIGHT IS OWNED BY ZURMO, ZURMO DISCLAIMS THE WARRANTY
  11. * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
  12. *
  13. * Zurmo is distributed in the hope that it will be useful, but WITHOUT
  14. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  15. * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  16. * details.
  17. *
  18. * You should have received a copy of the GNU General Public License along with
  19. * this program; if not, see http://www.gnu.org/licenses or write to the Free
  20. * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  21. * 02110-1301 USA.
  22. *
  23. * You can contact Zurmo, Inc. with a mailing address at 113 McHenry Road Suite 207,
  24. * Buffalo Grove, IL 60089, USA. or at email address contact@zurmo.com.
  25. ********************************************************************************/
  26. /**
  27. * Abstraction over the top of an application database accessed via
  28. * <a href="http://www.redbeanphp.com/">RedBean</a>. The base class for
  29. * an MVC model. Replaces the M part of MVC in Yii. Yii maps from the
  30. * database scheme to the objects, (good for database guys, not so good
  31. * for OO guys), this maps from objects to the database schema.
  32. *
  33. * A domain model is created by extending RedBeanModel and supplying
  34. * a getDefaultMetadata() method.
  35. *
  36. * Static getXxxx() methods can be supplied to query for the given domain
  37. * models, and instance methods should supply additional behaviour.
  38. *
  39. * getDefaultMetadata() returns an array of the class name mapped to
  40. * an array containing 'members' mapped to an array of member names,
  41. * (to be accessed as $model->memberName).
  42. *
  43. * It can then optionally have, 'relations' mapped
  44. * to an array of relation names, (to be accessed as $model->relationName),
  45. * mapped to its type, (the extending model class to which it relates).
  46. *
  47. * And it can then optionally have as well, 'rules' mapped to an array of
  48. * attribute names, (attributes are members and relations), a validator name,
  49. * and the parameters to the validator, if any, as per the Yii::CModel::rules()
  50. * method.See http://www.yiiframework.com/wiki/56/reference-model-rules-validation.
  51. *
  52. * These are used to automatically and dynamically create the database
  53. * schema on the fly as opposed to Yii's getting attributes from an
  54. * already existing schema.
  55. */
  56. abstract class RedBeanModel extends ObservableComponent implements Serializable
  57. {
  58. /**
  59. * Models that have not been saved yet have no id as far
  60. * as the database is concerned. Until they are saved they are
  61. * assigned a negative id, so that they have identity.
  62. * @var integer
  63. */
  64. private static $nextPseudoId = -1;
  65. /**
  66. * Array of static models. Used by Observers @see ObservableComponent to add events to a class.
  67. * @var array
  68. */
  69. private static $_models = array();
  70. /*
  71. * The id of an unsaved model.
  72. * @var integer
  73. */
  74. private $pseudoId;
  75. /**
  76. * When creating the class heirarchy for bean creation and maintenence, which class is the last class in the
  77. * lineage to create a bean for? Normally the RedBeanModel is the lastClass in the line, and therefore there
  78. * will not be a table redbeanmodel. Some classes that extend RedBeanModel might want the line to stop before
  79. * RedBeanModel since creating a table with just an 'id' would be pointless. @see OwnedModel
  80. * @var string
  81. */
  82. protected static $lastClassInBeanHeirarchy = 'RedBeanModel';
  83. // A model maps to one or more beans. If Person extends RedBeanModel
  84. // there is one bean, but if User then extends Person a User model
  85. // has two beans, the one holding the person data and the one holding
  86. // the extended User data. In this way in inheritance hierarchy from
  87. // model is normalized over several tables, one for each extending
  88. // class.
  89. private $modelClassNameToBean = array();
  90. private $attributeNameToBeanAndClassName = array();
  91. private $attributeNamesNotBelongsToOrManyMany = array();
  92. private $relationNameToRelationTypeModelClassNameAndOwns = array();
  93. private $relationNameToRelatedModel = array();
  94. private $unlinkedRelationNames = array();
  95. private $validators = array();
  96. private $attributeNameToErrors = array();
  97. private $scenarioName = '';
  98. // An object is automatcally savable if it is new or contains
  99. // modified members or related objects.
  100. // If it is newly created and has never had any data put into it
  101. // it can be saved explicitly but it wont be saved automatically
  102. // when it is a related model and will be redispensed next
  103. // time it is referenced.
  104. protected $modified = false;
  105. protected $deleted = false;
  106. protected $isInIsModified = false;
  107. protected $isInHasErrors = false;
  108. protected $isInGetErrors = false;
  109. protected $isValidating = false;
  110. protected $isSaving = false;
  111. protected $isNewModel = false;
  112. /**
  113. * Can this model be saved when save is called from a related model? True if it can, false if it cannot.
  114. * Setting this value to false can reduce unnecessary queries to the database. If the models of a class do
  115. * not change often then it can make sense to set this to false. An example is @see Currency.
  116. * @var boolean
  117. */
  118. protected $isSavableFromRelation = true;
  119. // Mapping of Yii validators to validators doing things that
  120. // are either required for RedBean, or that simply implement
  121. // The semantics that we want.
  122. private static $yiiValidatorsToRedBeanValidators = array(
  123. 'CDefaultValueValidator' => 'RedBeanModelDefaultValueValidator',
  124. 'CNumberValidator' => 'RedBeanModelNumberValidator',
  125. 'CTypeValidator' => 'RedBeanModelTypeValidator',
  126. 'CRequiredValidator' => 'RedBeanModelRequiredValidator',
  127. 'CUniqueValidator' => 'RedBeanModelUniqueValidator',
  128. 'defaultCalculatedDate' => 'RedBeanModelDefaultCalculatedDateValidator',
  129. 'readOnly' => 'RedBeanModelReadOnlyValidator',
  130. 'dateTimeDefault' => 'RedBeanModelDateTimeDefaultValueValidator',
  131. );
  132. /**
  133. * Used in an extending class's getDefaultMetadata() method to specify
  134. * that a relation is 1:1 and that the class on the side of the relationship where this is not a column in that
  135. * model's table. Example: model X HAS_ONE Y. There will be a y_id on the x table. But in Y you would have
  136. * HAS_ONE_BELONGS_TO X and there would be no column in the y table.
  137. */
  138. const HAS_ONE_BELONGS_TO = 0;
  139. /**
  140. * Used in an extending class's getDefaultMetadata() method to specify
  141. * that a relation is 1:M and that the class on the M side of the
  142. * relation.
  143. * Note: Currently if you have a relation that is set to HAS_MANY_BELONGS_TO, then that relation name
  144. * must be the strtolower() same as the related model class name. This is the current support for this
  145. * relation type. If something different is set, an exception will be thrown.
  146. */
  147. const HAS_MANY_BELONGS_TO = 1;
  148. /**
  149. * Used in an extending class's getDefaultMetadata() method to specify
  150. * that a relation is 1:1.
  151. */
  152. const HAS_ONE = 2;
  153. /**
  154. * Used in an extending class's getDefaultMetadata() method to specify
  155. * that a relation is 1:M and that the class is on the 1 side of the
  156. * relation.
  157. */
  158. const HAS_MANY = 3;
  159. /**
  160. * Used in an extending class's getDefaultMetadata() method to specify
  161. * that a relation is M:N and that the class on the either side of the
  162. * relation.
  163. */
  164. const MANY_MANY = 4;
  165. /**
  166. * Used in an extending class's getDefaultMetadata() method to specify
  167. * that a 1:1 or 1:M relation is one in which the left side of the relation
  168. * owns the model or models on the right side, meaning that if the model
  169. * is deleted it owns the related models and they are deleted along with it.
  170. * If not specified the related model is independent and is not deleted.
  171. */
  172. const OWNED = true;
  173. /**
  174. * @see const OWNED for more information.
  175. * @var boolean
  176. */
  177. const NOT_OWNED = false;
  178. /**
  179. * Returns the static model of the specified AR class.
  180. * The model returned is a static instance of the AR class.
  181. * It is provided for invoking class-level methods (something similar to static class methods.)
  182. *
  183. * EVERY derived AR class must override this method as follows,
  184. * <pre>
  185. * public static function model($className=__CLASS__)
  186. * {
  187. * return parent::model($className);
  188. * }
  189. * </pre>
  190. *
  191. * @param string $className active record class name.
  192. * @return CActiveRecord active record model instance.
  193. */
  194. public static function model($className = null)
  195. {
  196. if ($className == null)
  197. {
  198. $className = get_called_class();
  199. }
  200. if (isset(self::$_models[$className]))
  201. {
  202. return self::$_models[$className];
  203. }
  204. else
  205. {
  206. $model = self::$_models[$className] = new $className(false);
  207. return $model;
  208. }
  209. }
  210. /**
  211. * Gets all the models from the database of the named model type.
  212. * @param $orderBy TODO
  213. * @param $modelClassName Pass only when getting it at runtime
  214. * gets the wrong name.
  215. * @return An array of models of the type of the extending model.
  216. */
  217. public static function getAll($orderBy = null, $sortDescending = false, $modelClassName = null)
  218. {
  219. assert('$orderBy === null || is_string($orderBy) && $orderBy != ""');
  220. assert('is_bool($sortDescending)');
  221. assert('$modelClassName === null || is_string($modelClassName) && $modelClassName != ""');
  222. $quote = DatabaseCompatibilityUtil::getQuote();
  223. $orderBySql = null;
  224. if ($orderBy !== null)
  225. {
  226. $orderBySql = "$quote$orderBy$quote";
  227. if ($sortDescending)
  228. {
  229. $orderBySql .= ' desc';
  230. }
  231. }
  232. return static::getSubset(null, null, null, null, $orderBySql, $modelClassName);
  233. }
  234. /**
  235. * Gets a range of models from the database of the named model type.
  236. * @param $modelClassName
  237. * @param $joinTablesAdapter null or instance of joinTablesAdapter.
  238. * @param $offset The zero based index of the first model to be returned.
  239. * @param $count The number of models to be returned.
  240. * @param $where
  241. * @param $orderBy - sql string. Example 'a desc' or 'a.b desc'. Currently only supports non-related attributes
  242. * @param $modelClassName Pass only when getting it at runtime gets the wrong name.
  243. * @return An array of models of the type of the extending model.
  244. */
  245. public static function getSubset(RedBeanModelJoinTablesQueryAdapter $joinTablesAdapter = null,
  246. $offset = null, $count = null,
  247. $where = null, $orderBy = null,
  248. $modelClassName = null,
  249. $selectDistinct = false)
  250. {
  251. assert('$offset === null || is_integer($offset) && $offset >= 0');
  252. assert('$count === null || is_integer($count) && $count >= 1');
  253. assert('$where === null || is_string ($where) && $where != ""');
  254. assert('$orderBy === null || is_string ($orderBy) && $orderBy != ""');
  255. assert('$modelClassName === null || is_string($modelClassName) && $modelClassName != ""');
  256. if ($modelClassName === null)
  257. {
  258. $modelClassName = get_called_class();
  259. }
  260. if ($joinTablesAdapter == null)
  261. {
  262. $joinTablesAdapter = new RedBeanModelJoinTablesQueryAdapter($modelClassName);
  263. }
  264. $tableName = self::getTableName($modelClassName);
  265. $sql = static::makeSubsetOrCountSqlQuery($tableName, $joinTablesAdapter, $offset, $count, $where,
  266. $orderBy, false, $selectDistinct);
  267. $ids = R::getCol($sql);
  268. $tableName = self::getTableName($modelClassName);
  269. $beans = R::batch ($tableName, $ids);
  270. return self::makeModels($beans, $modelClassName);
  271. }
  272. /**
  273. * @param boolean $selectCount If true then make this a count query. If false, select ids from rows.
  274. * @param array $quotedExtraSelectColumnNameAndAliases - extra columns to select.
  275. * @return string - sql statement.
  276. */
  277. public static function makeSubsetOrCountSqlQuery($tableName,
  278. RedBeanModelJoinTablesQueryAdapter $joinTablesAdapter,
  279. $offset = null, $count = null,
  280. $where = null, $orderBy = null,
  281. $selectCount = false,
  282. $selectDistinct = false,
  283. array $quotedExtraSelectColumnNameAndAliases = array())
  284. {
  285. assert('is_string($tableName) && $tableName != ""');
  286. assert('$offset === null || is_integer($offset) && $offset >= 0');
  287. assert('$count === null || is_integer($count) && $count >= 1');
  288. assert('$where === null || is_string ($where) && $where != ""');
  289. assert('$orderBy === null || is_string ($orderBy) && $orderBy != ""');
  290. assert('is_bool($selectCount)');
  291. assert('is_bool($selectDistinct)');
  292. $selectQueryAdapter = new RedBeanModelSelectQueryAdapter($selectDistinct);
  293. if ($selectCount)
  294. {
  295. $selectQueryAdapter->addCountClause($tableName);
  296. }
  297. else
  298. {
  299. $selectQueryAdapter->addClause($tableName, 'id', 'id');
  300. }
  301. foreach ($quotedExtraSelectColumnNameAndAliases as $columnName => $columnAlias)
  302. {
  303. $selectQueryAdapter->addClauseWithColumnNameOnlyAndNoEnclosure($columnName, $columnAlias);
  304. }
  305. return SQLQueryUtil::
  306. makeQuery($tableName, $selectQueryAdapter, $joinTablesAdapter, $offset, $count, $where, $orderBy);
  307. }
  308. /**
  309. * @param $modelClassName
  310. * @param $joinTablesAdapter null or instance of joinTablesAdapter.
  311. * @param $modelClassName Pass only when getting it at runtime gets the wrong name.
  312. */
  313. public static function getCount(RedBeanModelJoinTablesQueryAdapter $joinTablesAdapter = null,
  314. $where = null, $modelClassName = null, $selectDistinct = false)
  315. {
  316. assert('$where === null || is_string($where)');
  317. assert('$modelClassName === null || is_string($modelClassName) && $modelClassName != ""');
  318. if ($modelClassName === null)
  319. {
  320. $modelClassName = get_called_class();
  321. }
  322. if ($joinTablesAdapter == null)
  323. {
  324. $joinTablesAdapter = new RedBeanModelJoinTablesQueryAdapter($modelClassName);
  325. }
  326. $tableName = self::getTableName($modelClassName);
  327. $sql = static::makeSubsetOrCountSqlQuery($tableName, $joinTablesAdapter, null, null, $where, null, true,
  328. $selectDistinct);
  329. $count = R::getCell($sql);
  330. if ($count === null || empty($count))
  331. {
  332. $count = 0;
  333. }
  334. return $count;
  335. }
  336. /**
  337. * Gets a model from the database by Id.
  338. * @param $id Integer Id.
  339. * @param $modelClassName Pass only when getting it at runtime
  340. * gets the wrong name.
  341. * @return A model of the type of the extending model.
  342. */
  343. public static function getById($id, $modelClassName = null)
  344. {
  345. assert('is_integer($id) && $id > 0');
  346. assert('$modelClassName === null || is_string($modelClassName) && $modelClassName != ""');
  347. // I would have thought it was correct to user R::load() and get
  348. // a null, or error or something if the bean doesn't exist, but
  349. // it still returns a bean. So until I've investigated further
  350. // I'm using Finder.
  351. if ($modelClassName === null)
  352. {
  353. $modelClassName = get_called_class();
  354. }
  355. $tableName = self::getTableName($modelClassName);
  356. $beans = R::find($tableName, "id = '$id'");
  357. assert('count($beans) <= 1');
  358. if (count($beans) == 0)
  359. {
  360. throw new NotFoundException();
  361. }
  362. return RedBeanModel::makeModel(end($beans), $modelClassName);
  363. }
  364. public function getIsNewModel()
  365. {
  366. return $this->isNewModel;
  367. }
  368. /**
  369. * Constructs a new model.
  370. * Important:
  371. * Models are only constructed with beans by the RedBeanModel. Beans are
  372. * never used by the application directly.
  373. * The application can construct a new model object by constructing a
  374. * model without specifying a bean. In other words, if Php had
  375. * overloading a constructor with $setDefaults would be public, and
  376. * a constructor taking a $bean and $forceTreatAsCreation would be private.
  377. * @param $setDefaults. If false the default validators will not be run
  378. * on construction. The Yii way is that defaults are
  379. * filled in after the fact, which is counter the usual
  380. * for objects.
  381. * @param $bean A bean. Never specified by an application.
  382. * @param $forceTreatAsCreation. Never specified by an application.
  383. * @see getById()
  384. * @see makeModel()
  385. * @see makeModels()
  386. */
  387. public function __construct($setDefaults = true, RedBean_OODBBean $bean = null, $forceTreatAsCreation = false)
  388. {
  389. $this->pseudoId = self::$nextPseudoId--;
  390. $this->init();
  391. if ($bean === null)
  392. {
  393. foreach (array_reverse(RuntimeUtil::getClassHierarchy(get_class($this), static::$lastClassInBeanHeirarchy)) as $modelClassName)
  394. {
  395. $tableName = self::getTableName($modelClassName);
  396. $newBean = R::dispense($tableName);
  397. $this->modelClassNameToBean[$modelClassName] = $newBean;
  398. $this->mapAndCacheMetadataAndSetHints($modelClassName, $newBean);
  399. }
  400. // The yii way of doing defaults is the the default validator
  401. // fills in the defaults on attributes that don't have values
  402. // when you validator, or save. This weird, since when you get
  403. // a model the things with defaults have not been defaulted!
  404. // We want that semantic.
  405. if ($setDefaults)
  406. {
  407. $this->runDefaultValidators();
  408. }
  409. $forceTreatAsCreation = true;
  410. }
  411. else
  412. {
  413. assert('$bean->id > 0');
  414. $first = true;
  415. foreach (RuntimeUtil::getClassHierarchy(get_class($this), static::$lastClassInBeanHeirarchy) as $modelClassName)
  416. {
  417. if ($first)
  418. {
  419. $lastBean = $bean;
  420. $first = false;
  421. }
  422. else
  423. {
  424. $tableName = self::getTableName($modelClassName);
  425. $lastBean = ZurmoRedBeanLinkManager::getBean($lastBean, $tableName);
  426. if ($lastBean === null)
  427. {
  428. throw new MissingBeanException();
  429. }
  430. assert('$lastBean->id > 0');
  431. }
  432. $this->modelClassNameToBean[$modelClassName] = $lastBean;
  433. $this->mapAndCacheMetadataAndSetHints($modelClassName, $lastBean);
  434. }
  435. $this->modelClassNameToBean = array_reverse($this->modelClassNameToBean);
  436. }
  437. $this->constructDerived($bean, $setDefaults);
  438. if ($forceTreatAsCreation)
  439. {
  440. $this->onCreated();
  441. }
  442. else
  443. {
  444. $this->onLoaded();
  445. RedBeanModelsCache::cacheModel($this);
  446. }
  447. $this->modified = false;
  448. }
  449. // Derived classes can insert additional steps into the construction.
  450. protected function constructDerived($bean, $setDefaults)
  451. {
  452. assert('$bean === null || $bean instanceof RedBean_OODBBean');
  453. assert('is_bool($setDefaults)');
  454. }
  455. /**
  456. * Utilized when pieces of information need to be constructed on an existing model, that can potentially be
  457. * missing. For example, if a model is created, then a custom field is added, it is possible the cached model
  458. * is missing the custom field customFieldData.
  459. * @param unknown_type $bean
  460. */
  461. protected function constructIncomplete($bean)
  462. {
  463. assert('$bean === null || $bean instanceof RedBean_OODBBean');
  464. $this->init();
  465. }
  466. public function serialize()
  467. {
  468. return serialize(array(
  469. $this->pseudoId,
  470. $this->modelClassNameToBean,
  471. $this->attributeNameToBeanAndClassName,
  472. $this->attributeNamesNotBelongsToOrManyMany,
  473. $this->relationNameToRelationTypeModelClassNameAndOwns,
  474. $this->validators,
  475. ));
  476. }
  477. public function unserialize($data)
  478. {
  479. try
  480. {
  481. $data = unserialize($data);
  482. assert('is_array($data)');
  483. if (count($data) != 6)
  484. {
  485. return null;
  486. }
  487. $this->pseudoId = $data[0];
  488. $this->modelClassNameToBean = $data[1];
  489. $this->attributeNameToBeanAndClassName = $data[2];
  490. $this->attributeNamesNotBelongsToOrManyMany = $data[3];
  491. $this->relationNameToRelationTypeModelClassNameAndOwns = $data[4];
  492. $this->validators = $data[5];
  493. $this->relationNameToRelatedModel = array();
  494. $this->unlinkedRelationNames = array();
  495. $this->attributeNameToErrors = array();
  496. $this->scenarioName = '';
  497. $this->modified = false;
  498. $this->deleted = false;
  499. $this->isInIsModified = false;
  500. $this->isInHasErrors = false;
  501. $this->isInGetErrors = false;
  502. $this->isValidating = false;
  503. $this->isSaving = false;
  504. }
  505. catch (Exception $e)
  506. {
  507. return null;
  508. }
  509. }
  510. /**
  511. * Overriding constructors must call this function to ensure that
  512. * they leave the newly constructed instance not modified since
  513. * anything modifying the class during constructionm will set it
  514. * modified automatically.
  515. */
  516. protected function setNotModified()
  517. {
  518. $this->modified = false; // This sets this class to the right state.
  519. assert('!$this->isModified()'); // This tests that related classes are in the right state.
  520. }
  521. /**
  522. * By default the table name is the lowercased class name. If this
  523. * conflicts with a database keyword override to return true.
  524. * RedBean does not quote table names in most cases.
  525. */
  526. // Public for unit testing.
  527. public static function mangleTableName()
  528. {
  529. return false;
  530. }
  531. /**
  532. * Returns the table name for a class.
  533. * For use by RedBeanModelDataProvider. It will not
  534. * be of any use to an application. Applications
  535. * should not be doing anything table related.
  536. * Derived classes can refer directly to the
  537. * table name.
  538. */
  539. public static function getTableName($modelClassName)
  540. {
  541. assert('is_string($modelClassName) && $modelClassName != ""');
  542. $tableName = strtolower($modelClassName);
  543. if ($modelClassName::mangleTableName())
  544. {
  545. $tableName = '_' . $tableName;
  546. }
  547. return $tableName;
  548. }
  549. /**
  550. * Returns the table names for an array of classes.
  551. * For use by RedBeanModelDataProvider. It will not
  552. * be of any use to an application.
  553. */
  554. public static function getTableNames($classNames)
  555. {
  556. $tableNames = array();
  557. foreach ($classNames as $className)
  558. {
  559. $tableNames[] = self::getTableName($className);
  560. }
  561. return $tableNames;
  562. }
  563. /**
  564. * Used by classes such as containers which use sql to
  565. * optimize getting models from the database.
  566. */
  567. public static function getForeignKeyName($modelClassName, $relationName)
  568. {
  569. assert('is_string($modelClassName)');
  570. assert('$modelClassName != ""');
  571. $metadata = $modelClassName::getMetadata();
  572. foreach ($metadata as $modelClassName => $modelClassMetadata)
  573. {
  574. if (isset($metadata[$modelClassName]["relations"]) &&
  575. array_key_exists($relationName, $metadata[$modelClassName]["relations"]))
  576. {
  577. $relatedModelClassName = $metadata[$modelClassName]['relations'][$relationName][1];
  578. $relatedModelTableName = self::getTableName($relatedModelClassName);
  579. $columnName = '';
  580. if (strtolower($relationName) != strtolower($relatedModelClassName))
  581. {
  582. $columnName = strtolower($relationName) . '_';
  583. }
  584. $columnName .= $relatedModelTableName . '_id';
  585. return $columnName;
  586. }
  587. }
  588. throw new NotSupportedException;
  589. }
  590. /**
  591. * Called on construction when a new model is created.
  592. */
  593. protected function onCreated()
  594. {
  595. }
  596. /**
  597. * Called on construction when a model is loaded.
  598. */
  599. protected function onLoaded()
  600. {
  601. }
  602. /**
  603. * Called when a model is modified.
  604. */
  605. protected function onModified()
  606. {
  607. }
  608. /**
  609. * Used for mixins.
  610. */
  611. protected function mapAndCacheMetadataAndSetHints($modelClassName, RedBean_OODBBean $bean)
  612. {
  613. assert('is_string($modelClassName)');
  614. assert('$modelClassName != ""');
  615. $metadata = $this->getMetadata();
  616. if (isset($metadata[$modelClassName]))
  617. {
  618. $hints = array();
  619. if (isset($metadata[$modelClassName]['members']))
  620. {
  621. foreach ($metadata[$modelClassName]['members'] as $memberName)
  622. {
  623. $this->attributeNameToBeanAndClassName[$memberName] = array($bean, $modelClassName);
  624. $this->attributeNamesNotBelongsToOrManyMany[] = $memberName;
  625. if (substr($memberName, -2) == 'Id')
  626. {
  627. $columnName = strtolower($memberName);
  628. $hints[$columnName] = 'id';
  629. }
  630. }
  631. }
  632. if (isset($metadata[$modelClassName]['relations']))
  633. {
  634. foreach ($metadata[$modelClassName]['relations'] as $relationName => $relationTypeModelClassNameAndOwns)
  635. {
  636. assert('in_array(count($relationTypeModelClassNameAndOwns), array(2, 3, 4))');
  637. $relationType = $relationTypeModelClassNameAndOwns[0];
  638. $relationModelClassName = $relationTypeModelClassNameAndOwns[1];
  639. if ($relationType == self::HAS_MANY_BELONGS_TO &&
  640. strtolower($relationName) != strtolower($relationModelClassName))
  641. {
  642. $label = 'Relations of type HAS_MANY_BELONGS_TO must have the relation name ' .
  643. 'the same as the related model class name. Relation: {relationName} ' .
  644. 'Relation model class name: {relationModelClassName}';
  645. throw new NotSupportedException(Yii::t('Default', $label,
  646. array('{relationName}' => $relationName,
  647. '{relationModelClassName}' => $relationModelClassName)));
  648. }
  649. if (count($relationTypeModelClassNameAndOwns) >= 3 &&
  650. $relationTypeModelClassNameAndOwns[2] == self::OWNED)
  651. {
  652. $owns = true;
  653. }
  654. else
  655. {
  656. $owns = false;
  657. }
  658. if (count($relationTypeModelClassNameAndOwns) == 4 && $relationType != self::HAS_MANY)
  659. {
  660. throw new NotSupportedException();
  661. }
  662. if (count($relationTypeModelClassNameAndOwns) == 4)
  663. {
  664. $relationPolyOneToManyName = $relationTypeModelClassNameAndOwns[3];
  665. }
  666. else
  667. {
  668. $relationPolyOneToManyName = null;
  669. }
  670. assert('in_array($relationType, array(self::HAS_ONE_BELONGS_TO, self::HAS_MANY_BELONGS_TO, ' .
  671. 'self::HAS_ONE, self::HAS_MANY, self::MANY_MANY))');
  672. $this->attributeNameToBeanAndClassName[$relationName] = array($bean, $modelClassName);
  673. $this->relationNameToRelationTypeModelClassNameAndOwns[$relationName] = array($relationType,
  674. $relationModelClassName,
  675. $owns,
  676. $relationPolyOneToManyName);
  677. if (!in_array($relationType, array(self::HAS_ONE_BELONGS_TO, self::HAS_MANY_BELONGS_TO, self::MANY_MANY)))
  678. {
  679. $this->attributeNamesNotBelongsToOrManyMany[] = $relationName;
  680. }
  681. }
  682. }
  683. // Add model validators. Parent validators are already applied.
  684. if (isset($metadata[$modelClassName]['rules']))
  685. {
  686. foreach ($metadata[$modelClassName]['rules'] as $validatorMetadata)
  687. {
  688. assert('isset($validatorMetadata[0])');
  689. assert('isset($validatorMetadata[1])');
  690. $attributeName = $validatorMetadata[0];
  691. // Each rule in RedBeanModel must specify one attribute name.
  692. // This was just better style, now it is mandatory.
  693. assert('strpos($attributeName, " ") === false');
  694. $validatorName = $validatorMetadata[1];
  695. $validatorParameters = array_slice($validatorMetadata, 2);
  696. if (isset(CValidator::$builtInValidators[$validatorName]))
  697. {
  698. $validatorName = CValidator::$builtInValidators[$validatorName];
  699. }
  700. if (isset(self::$yiiValidatorsToRedBeanValidators[$validatorName]))
  701. {
  702. $validatorName = self::$yiiValidatorsToRedBeanValidators[$validatorName];
  703. }
  704. $validator = CValidator::createValidator($validatorName, $this, $attributeName, $validatorParameters);
  705. switch ($validatorName)
  706. {
  707. case 'RedBeanModelTypeValidator':
  708. case 'TypeValidator':
  709. $columnName = strtolower($attributeName);
  710. if (array_key_exists($columnName, $hints))
  711. {
  712. unset($hints[$columnName]);
  713. }
  714. if (in_array($validator->type, array('date', 'datetime', 'blob', 'longblob', 'string')))
  715. {
  716. $hints[$columnName] = $validator->type;
  717. }
  718. break;
  719. case 'CBooleanValidator':
  720. $columnName = strtolower($attributeName);
  721. $hints[$columnName] = 'boolean';
  722. break;
  723. case 'RedBeanModelUniqueValidator':
  724. if (!$this->isRelation($attributeName))
  725. {
  726. $bean->setMeta("buildcommand.unique", array(array($attributeName)));
  727. }
  728. else
  729. {
  730. $relatedModelClassName = $this->relationNameToRelationTypeModelClassNameAndOwns[$attributeName][1];
  731. $relatedModelTableName = self::getTableName($relatedModelClassName);
  732. $columnName = strtolower($attributeName);
  733. if ($columnName != $relatedModelTableName)
  734. {
  735. $columnName .= '_' . $relatedModelTableName;
  736. }
  737. $columnName .= '_id';
  738. $bean->setMeta("buildcommand.unique", array(array($columnName)));
  739. }
  740. break;
  741. }
  742. $this->validators[] = $validator;
  743. }
  744. // Check if we need to update string type to long string type, based on validators.
  745. if (isset($metadata[$modelClassName]['members']))
  746. {
  747. foreach ($metadata[$modelClassName]['members'] as $memberName)
  748. {
  749. $allValidators = $this->getValidators($memberName);
  750. foreach ($allValidators as $validator)
  751. {
  752. if ((get_class($validator) == 'RedBeanModelTypeValidator' ||
  753. get_class($validator) == 'TypeValidator') &&
  754. $validator->type == 'string')
  755. {
  756. $columnName = strtolower($validator->attributes[0]);
  757. if (count($allValidators) > 1)
  758. {
  759. $haveCStringValidator = false;
  760. foreach ($allValidators as $innerValidator)
  761. {
  762. if (get_class($innerValidator) == 'CStringValidator' &&
  763. isset($innerValidator->max) &&
  764. $innerValidator->max > 255)
  765. {
  766. if ($innerValidator->max > 65535)
  767. {
  768. $hints[$columnName] = 'longtext';
  769. }
  770. else
  771. {
  772. $hints[$columnName] = 'text';
  773. }
  774. }
  775. if (get_class($innerValidator) == 'CStringValidator')
  776. {
  777. $haveCStringValidator = true;
  778. }
  779. }
  780. if (!$haveCStringValidator)
  781. {
  782. $hints[$columnName] = 'text';
  783. }
  784. }
  785. else
  786. {
  787. $hints[$columnName] = 'text';
  788. }
  789. }
  790. }
  791. }
  792. }
  793. }
  794. $bean->setMeta('hint', $hints);
  795. }
  796. }
  797. /**
  798. * Used for mixins.
  799. */
  800. protected function runDefaultValidators()
  801. {
  802. foreach ($this->validators as $validator)
  803. {
  804. if ($validator instanceof CDefaultValueValidator)
  805. {
  806. $validator->validate($this);
  807. }
  808. }
  809. }
  810. /**
  811. * For use only by RedBeanModel and RedBeanModels. Beans are
  812. * never used by the application directly.
  813. */
  814. public function getPrimaryBean()
  815. {
  816. return end($this->modelClassNameToBean);
  817. }
  818. /**
  819. * Used for optimization.
  820. */
  821. public function getClassId($modelClassName)
  822. {
  823. assert('array_key_exists($modelClassName, $this->modelClassNameToBean)');
  824. return intval($this->getClassBean($modelClassName)->id); // Trying to combat the slop.
  825. }
  826. public function getClassBean($modelClassName)
  827. {
  828. assert('is_string($modelClassName)');
  829. assert('$modelClassName != ""');
  830. assert('array_key_exists($modelClassName, $this->modelClassNameToBean)');
  831. return $this->modelClassNameToBean[$modelClassName];
  832. }
  833. /**
  834. * Used for mixins.
  835. */
  836. protected function setClassBean($modelClassName, RedBean_OODBBean $bean)
  837. {
  838. assert('is_string($modelClassName)');
  839. assert('$modelClassName != ""');
  840. assert('!array_key_exists($modelClassName, $this->modelClassNameToBean)');
  841. $this->modelClassNameToBean = array_merge(array($modelClassName => $bean),
  842. $this->modelClassNameToBean);
  843. }
  844. public function getModelIdentifier()
  845. {
  846. return get_class($this) . strval($this->getPrimaryBean()->id);
  847. }
  848. /**
  849. * Returns metadata for the model. Attempts to cache metadata, if it is not already cached.
  850. * @see getDefaultMetadata()
  851. * @returns An array of metadata.
  852. */
  853. public static function getMetadata()
  854. {
  855. try
  856. {
  857. return GeneralCache::getEntry(get_called_class() . 'Metadata');
  858. }
  859. catch (NotFoundException $e)
  860. {
  861. $className = get_called_Class();
  862. $defaultMetadata = $className::getDefaultMetadata();
  863. $metadata = array();
  864. foreach (array_reverse(RuntimeUtil::getClassHierarchy($className, static::$lastClassInBeanHeirarchy)) as $modelClassName)
  865. {
  866. if ($modelClassName::canSaveMetadata())
  867. {
  868. try
  869. {
  870. $globalMetadata = GlobalMetadata::getByClassName($modelClassName);
  871. $metadata[$modelClassName] = unserialize($globalMetadata->serializedMetadata);
  872. }
  873. catch (NotFoundException $e)
  874. {
  875. if (isset($defaultMetadata[$modelClassName]))
  876. {
  877. $metadata[$modelClassName] = $defaultMetadata[$modelClassName];
  878. }
  879. }
  880. }
  881. else
  882. {
  883. if (isset($defaultMetadata[$modelClassName]))
  884. {
  885. $metadata[$modelClassName] = $defaultMetadata[$modelClassName];
  886. }
  887. }
  888. }
  889. if (YII_DEBUG)
  890. {
  891. self::assertMetadataIsValid($metadata);
  892. }
  893. GeneralCache::cacheEntry(get_called_class() . 'Metadata', $metadata);
  894. return $metadata;
  895. }
  896. }
  897. /**
  898. * By default models cannot save their metadata, allowing
  899. * them to be loaded quickly because the loading of of
  900. * metadata can be avoided as much as possible.
  901. * To make a model able to save its metadata override
  902. * this method to return true. PUT it before the
  903. * getDefaultMetadata in the derived class.
  904. */
  905. public static function canSaveMetadata()
  906. {
  907. return false;
  908. }
  909. /**
  910. * Sets metadata for the model.
  911. * @see getDefaultMetadata()
  912. * @returns An array of metadata.
  913. */
  914. public static function setMetadata(array $metadata)
  915. {
  916. if (YII_DEBUG)
  917. {
  918. self::assertMetadataIsValid($metadata);
  919. }
  920. $className = get_called_class();
  921. foreach (array_reverse(RuntimeUtil::getClassHierarchy($className, static::$lastClassInBeanHeirarchy)) as $modelClassName)
  922. {
  923. if ($modelClassName::canSaveMetadata())
  924. {
  925. if (isset($metadata[$modelClassName]))
  926. {
  927. try
  928. {
  929. $globalMetadata = GlobalMetadata::getByClassName($modelClassName);
  930. }
  931. catch (NotFoundException $e)
  932. {
  933. $globalMetadata = new GlobalMetadata();
  934. $globalMetadata->className = $modelClassName;
  935. }
  936. $globalMetadata->serializedMetadata = serialize($metadata[$modelClassName]);
  937. $saved = $globalMetadata->save();
  938. // TODO: decide how to deal with this properly if it fails.
  939. // ie: throw or return false, or something other than
  940. // this naughty assert.
  941. assert('$saved');
  942. }
  943. }
  944. }
  945. RedBeanModelsCache::forgetAllByModelType(get_called_class());
  946. GeneralCache::forgetEntry(get_called_class() . 'Metadata');
  947. }
  948. /**
  949. * Returns the default meta data for the class.
  950. * It must be appended to the meta data
  951. * from the parent model, if any.
  952. */
  953. public static function getDefaultMetadata()
  954. {
  955. return array();
  956. }
  957. protected static function assertMetadataIsValid(array $metadata)
  958. {
  959. $className = get_called_Class();
  960. foreach (RuntimeUtil::getClassHierarchy($className, static::$lastClassInBeanHeirarchy) as $modelClassName)
  961. {
  962. if (isset($metadata[$modelClassName]['members']))
  963. {
  964. assert('is_array($metadata[$modelClassName]["members"])');
  965. foreach ($metadata[$modelClassName]["members"] as $memberName)
  966. {
  967. assert('ctype_lower($memberName{0})');
  968. }
  969. }
  970. if (isset($metadata[$modelClassName]['relations']))
  971. {
  972. assert('is_array($metadata[$modelClassName]["relations"])');
  973. foreach ($metadata[$modelClassName]["relations"] as $relationName => $notUsed)
  974. {
  975. assert('ctype_lower($relationName{0})');
  976. }
  977. }
  978. if (isset($metadata[$modelClassName]['rules']))
  979. {
  980. assert('is_array($metadata[$modelClassName]["rules"])');
  981. }
  982. if (isset($metadata[$modelClassName]['defaultSortAttribute']))
  983. {
  984. assert('is_string($metadata[$modelClassName]["defaultSortAttribute"])');
  985. }
  986. if (isset($metadata[$modelClassName]['rollupRelations']))
  987. {
  988. assert('is_array($metadata[$modelClassName]["rollupRelations"])');
  989. }
  990. // Todo: add more rules here as I think of them.
  991. }
  992. }
  993. /**
  994. * Downcasting in general is a bad concept, but when pulling
  995. * a Person from the database it would require a lot of
  996. * jumping through hoops to make the RedBeanModel automatically
  997. * figure out if that person is really a User, Contact, Customer
  998. * or whatever might be derived from Person. So to avoid that
  999. * complication and performance hit where it is not necessary
  1000. * this method can be used to convert a model to one of
  1001. * a given set of derivatives. If model is not one
  1002. * of those NotFoundException is thrown.
  1003. */
  1004. public function castDown(array $derivedModelClassNames)
  1005. {
  1006. $bean = $this->getPrimaryBean();
  1007. $thisModelClassName = get_called_class();
  1008. $key = strtolower($thisModelClassName) . '_id';
  1009. foreach ($derivedModelClassNames as $modelClassNames)
  1010. {
  1011. if (is_string($modelClassNames))
  1012. {
  1013. $nextModelClassName = $modelClassNames;
  1014. if (get_class($this) == $nextModelClassName)

Large files files are truncated, but you can click here to view the full file