PageRenderTime 49ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/framework/core/db/DbObject.php

http://zoop.googlecode.com/
PHP | 781 lines | 379 code | 88 blank | 314 comment | 42 complexity | cb9a094a3728fb4fd191f88fc0f9afa7 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1
  1. <?php
  2. class DbObject extends Object implements Iterator
  3. {
  4. /**
  5. * The assigned name of the table (defaults to the class name translated by the getDefaultTableName
  6. *
  7. * @var string
  8. */
  9. protected $tableName;
  10. /**
  11. * The field name(s) of the primary key in an array
  12. *
  13. * @var array
  14. */
  15. protected $primaryKey;
  16. protected $keyAssignedBy;
  17. private $missingKeyFields;
  18. private $bound;
  19. private $persisted;
  20. private $scalars;
  21. protected $relationships;
  22. const keyAssignedBy_db = 1;
  23. const keyAssignedBy_dev = 2;
  24. const keyAssignedBy_auto = 3;
  25. /**
  26. * This is the constructor. (honest, I swear)
  27. *
  28. * Some things to know about the defaults defined by this constructor:
  29. * Default primary key: id
  30. * Default keyAssignedBy: keyAssignedBy_db
  31. *
  32. * If keyAssignedBy is keyAssignedBy_db the primary_key array can contain no more than one field
  33. *
  34. * @param mixed $init Initial value for the primary key field (if this is supplied there can be only one field in the primary key)
  35. */
  36. function __construct($init = NULL)
  37. {
  38. // set up some sensible defaults
  39. $this->primaryKey = array('id');
  40. $this->tableName = $this->getDefaultTableName();
  41. $this->bound = false;
  42. $this->keyAssignedBy = self::keyAssignedBy_db;
  43. $this->scalars = array();
  44. $this->relationships = array();
  45. $this->persisted = NULL;
  46. $this->init($init);
  47. $this->missingKeyFields = count($this->primaryKey);
  48. if($this->keyAssignedBy == self::keyAssignedBy_db && count($this->primaryKey) != 1)
  49. trigger_error("in order for 'keyAssignedBy_db' to work you must have a single primary key field");
  50. if(is_array($init))
  51. {
  52. $this->assignScalars($init);
  53. }
  54. else if($init === NULL)
  55. {
  56. return;
  57. }
  58. else
  59. {
  60. assert(count($this->primaryKey) == 1);
  61. $this->assignScalars(array($this->primaryKey[0] => $init));
  62. }
  63. }
  64. /**
  65. * This is a second stage constructor that should be overridden in individual database objects to initialize the instance
  66. *
  67. */
  68. protected function init()
  69. {
  70. // override this function to setup relationships without having to handle the constructor chaining
  71. }
  72. /**
  73. * Returns the name of the table based on the name of the current class. If the class is called "personObject" the tablename will default to person_object.
  74. *
  75. * @return string
  76. */
  77. private function getDefaultTableName()
  78. {
  79. $name = get_class($this);
  80. // if there are any capitals after the firstone insert and underscore
  81. $name = $name[0] . preg_replace('/[A-Z]/', '_$0', substr($name, 1));
  82. // lowercase everything and return it
  83. return strtolower($name);
  84. }
  85. /**
  86. * Returns the name of the table associated with the db object
  87. *
  88. * @return string
  89. */
  90. public function getTableName()
  91. {
  92. return $this->tableName;
  93. }
  94. /**
  95. * Returns the value of the primary key of the record that this object is associated with. An error
  96. * will be thrown if there is more than one primary key.
  97. *
  98. * @return mixed
  99. */
  100. public function getId()
  101. {
  102. assert(count($this->primaryKey) == 1);
  103. return $this->scalars[$this->primaryKey[0]];
  104. }
  105. /**
  106. * Returns the field name(s) in the primary key in an array
  107. *
  108. * @return array of field names
  109. */
  110. public function getPrimaryKey()
  111. {
  112. return $this->primaryKey;
  113. }
  114. /**
  115. * Returns true if this DbObject class is set to use primary keys generated by the database
  116. *
  117. * @return boolean True if the database automatically generates primary keys
  118. */
  119. public function primaryKeyAssignedByDb()
  120. {
  121. return $this->keyAssignedBy == self::keyAssignedBy_db ? true : false;
  122. }
  123. /**
  124. * Returns a DbTable object with scheme information for the associated table (if supported by your database)
  125. *
  126. * @return DbTable DbTable object
  127. */
  128. static public function _getTableSchema($className)
  129. {
  130. $object = new $className();
  131. return $object->getSchema();
  132. }
  133. /**
  134. * Returns a DbTable object with scheme information for the associated table (if supported by your database)
  135. *
  136. * @return DbTable DbTable object
  137. */
  138. public function getSchema()
  139. {
  140. return new DbTable(self::_getConnection(get_class($this)), $this->tableName);
  141. }
  142. /**
  143. * Alias for getSchema
  144. *
  145. * @return DbTable DbTable object
  146. */
  147. public function getTable()
  148. {
  149. return $this->getSchema();
  150. }
  151. /**
  152. * Loops through all the fields in the associated table and ensures a default NULL value for each of them in the class
  153. * This feature only works with DbConnection objects that have full schema support implemented
  154. */
  155. public function forceFields()
  156. {
  157. if($this->bound)
  158. $this->loadScalars();
  159. else
  160. {
  161. foreach($this->getSchema()->fields as $thisField)
  162. $this->scalars[$thisField->name] = NULL;
  163. }
  164. }
  165. /**
  166. * Serializes all column names and values and returns them in a string of the format "<DbObject class>: <field> => <value> <field> => <value> ..."
  167. *
  168. * @return string string
  169. */
  170. public function getString()
  171. {
  172. $s = '';
  173. $this->loadScalars();
  174. foreach($this->scalars as $field => $value)
  175. $s .= " $field => $value";
  176. return get_class($this) . ':' . $s;
  177. }
  178. /**
  179. * Returns the connection associated with the DbObject
  180. *
  181. * @return DbConnection DbConnection object
  182. */
  183. public function getDb()
  184. {
  185. return self::_getConnection(get_class($this));
  186. }
  187. //
  188. // the scalar handlers
  189. //
  190. // rewrite them and make them handle primary keys with different names or more than one field
  191. //
  192. /**
  193. * Returns a $field => $value array containing all the fields and their values
  194. *
  195. * @return assoc_array assoc_array containing all fields and values
  196. */
  197. public function getFields()
  198. {
  199. return $this->scalars;
  200. }
  201. /**
  202. * Accepts a $field => $value array containing fields and their values to be set in this DbObject
  203. *
  204. * @param assoc_array $data $field => $value associative array with data to be stored in this object
  205. */
  206. public function setFields($data)
  207. {
  208. $this->assignScalars($data);
  209. }
  210. /**
  211. * Returns the value of the specified field
  212. *
  213. * @param string $field Field to retreive
  214. * @return string String value of the field
  215. */
  216. public function getField($field)
  217. {
  218. return $this->getScalar($field);
  219. }
  220. /**
  221. * Returns the value of the specified field
  222. *
  223. * @param string $field Field to retreive
  224. * @return string String value of the field
  225. */
  226. private function getScalar($field)
  227. {
  228. if(!isset($this->scalars[$field]))
  229. {
  230. if(!$this->bound)
  231. {
  232. /* TODO: Handle "getScalar" calls to unbound DbObject instances
  233. Different possibilities on how to handle this situation. Maybe we could use some flags.
  234. 1. check the metadata. (alwaysCheckMeta)
  235. 1. if its there then (useDummyDefaults requires alwaysCheckMeta)
  236. 1. return the default value
  237. 2. return NULL
  238. 2. if its not there
  239. 1. throw and error
  240. 2. dont check the metadata (useDummyNulls requires !alwaysCheckMeta)
  241. 1. return null
  242. 2. throw an error
  243. trigger_error("the field: $field is not present in memory and this object is not yet bound to a database row");
  244. */
  245. return NULL;
  246. }
  247. $this->loadScalars();
  248. }
  249. if(!array_key_exists($field, $this->scalars))
  250. trigger_error("the field $field is present neither in memory nor in the cooresponding database table");
  251. return $this->scalars[$field];
  252. }
  253. /**
  254. * Assigns a value to the specified field
  255. *
  256. * @param string $field Field to change the value of
  257. * @param mixed $value Value to assign to the specified field
  258. */
  259. private function setScalar($field, $value)
  260. {
  261. $data[$field] = $value;
  262. $this->assignScalars($data);
  263. }
  264. /*
  265. private function setScalars($data)
  266. {
  267. foreach($data as $field => $value)
  268. {
  269. $this->scalars[$field] = $value;
  270. }
  271. }
  272. */
  273. /**
  274. * Accepts a $field => $value array containing fields and their values to be set in this DbObject
  275. *
  276. * @param assoc_array $data $field => $value associative array with data to be stored in this object
  277. */
  278. private function assignScalars($data)
  279. {
  280. foreach($data as $member => $value)
  281. {
  282. if(!isset($this->scalars[$member]) && in_array($member, $this->primaryKey))
  283. {
  284. $this->missingKeyFields--;
  285. if($this->missingKeyFields == 0)
  286. $this->bound = 1;
  287. }
  288. $this->scalars[$member] = $value;
  289. }
  290. }
  291. /**
  292. * Loads values into the fields of the DbObject
  293. *
  294. */
  295. private function loadScalars()
  296. {
  297. assert($this->bound);
  298. $row = $this->fetchPersisted();
  299. $this->assignPersisted($row);
  300. }
  301. private function assignPersisted($row)
  302. {
  303. // if they manually set a field don't write over it just because they loaded one scalar
  304. foreach($row as $field => $value)
  305. {
  306. if(!isset($this->scalars[$field]))
  307. $this->scalars[$field] = $value;
  308. }
  309. }
  310. /**
  311. * Retrieves field values from the database using primary key as lookup fields
  312. *
  313. * @return unknown
  314. */
  315. private function fetchPersisted()
  316. {
  317. $wheres = array();
  318. $whereValues = array();
  319. foreach($this->primaryKey as $keyField)
  320. {
  321. $wheres[] = ":fld_$keyField:identifier = :$keyField";
  322. $whereValues["fld_$keyField"] = $keyField;
  323. $whereValues[$keyField] = $this->scalars[$keyField];
  324. }
  325. $whereClause = implode(' and ', $wheres);
  326. $row = self::_getConnection(get_class($this))->fetchRow("select * from $this->tableName where $whereClause", $whereValues);
  327. if($row)
  328. $this->persisted = true;
  329. else
  330. $this->persisted = false;
  331. return $row;
  332. }
  333. /**
  334. * Returns true if this DbObject is (and can be) saved in the database
  335. *
  336. * @return boolean True if the DbObject is/can be saved in the DB
  337. */
  338. private function _persisted()
  339. {
  340. if(!$this->bound)
  341. return false;
  342. if($this->keyAssignedBy == self::keyAssignedBy_db)
  343. return true;
  344. else
  345. {
  346. $row = $this->fetchPersisted();
  347. if($row)
  348. {
  349. // we might as well save the results
  350. $this->assignPersisted($row);
  351. return true;
  352. }
  353. return false;
  354. }
  355. }
  356. /**
  357. * Returns true if this DbObject is (and can be) saved in the database
  358. *
  359. * @return boolean True if the DbObject is/can be saved in the DB
  360. */
  361. public function persisted()
  362. {
  363. if($this->persisted !== NULL)
  364. return $this->persisted;
  365. else
  366. return $this->persisted = $this->_persisted();
  367. }
  368. /**
  369. * Saves the record in memory
  370. *
  371. */
  372. public function save()
  373. {
  374. if(!$this->bound)
  375. {
  376. if($this->keyAssignedBy == self::keyAssignedBy_db)
  377. $this->setScalar($this->primaryKey[0], self::_getConnection(get_class($this))->insertArray($this->tableName, $this->scalars));
  378. else
  379. trigger_error("you must define all foreign key fields in order by save this object");
  380. }
  381. else
  382. {
  383. if($this->keyAssignedBy == self::keyAssignedBy_db)
  384. {
  385. $updateInfo = DbConnection::generateUpdateInfo($this->tableName, $this->getKeyConditions(), $this->scalars);
  386. self::_getConnection(get_class($this))->updateRow($updateInfo['sql'], $updateInfo['params']);
  387. }
  388. else
  389. {
  390. if(!$this->persisted())
  391. self::_getConnection(get_class($this))->insertArray($this->tableName, $this->scalars, false);
  392. else
  393. {
  394. $updateInfo = DbConnection::generateUpdateInfo($this->tableName, $this->getKeyConditions(), $this->scalars);
  395. self::_getConnection(get_class($this))->updateRow($updateInfo['sql'], $updateInfo['params']);
  396. }
  397. }
  398. }
  399. }
  400. /**
  401. * Returns an array containing all primary key fields that have a value assigned to them in this DbObject instance
  402. *
  403. * @return array of fields
  404. */
  405. private function getKeyConditions()
  406. {
  407. assert($this->bound);
  408. return array_intersect_key($this->scalars, array_flip($this->primaryKey));
  409. }
  410. /**
  411. * Deletes the record from the database, deletes all fields and values from memory, and unbinds the DbObject
  412. *
  413. */
  414. public function destroy()
  415. {
  416. // have a way to destroy any existing vector fields or refuse to continue (destroy_r)
  417. $deleteInfo = DbConnection::generateDeleteInfo($this->tableName, $this->getKeyConditions());
  418. self::_getConnection(get_class($this))->deleteRow($deleteInfo['sql'], $deleteInfo['params']);
  419. $this->bound = false;
  420. $this->scalars = array();
  421. $this->persisted = false;
  422. }
  423. //
  424. // end of scalar handlers
  425. //
  426. //
  427. // vector handlers
  428. //
  429. private function addRelationship($name, $relationship)
  430. {
  431. $this->relationships[$name] = $relationship;
  432. }
  433. private function hasRelationship($name)
  434. {
  435. return isset($this->relationships[$name]) ? true : false;
  436. }
  437. private function getRelationshipInfo($name)
  438. {
  439. return $this->relationships[$name]->getInfo();
  440. }
  441. protected function hasMany($name, $params = array())
  442. {
  443. if(isset($params['through']) && $params['through'])
  444. $this->addRelationship($name, new DbRelationshipHasManyThrough($name, $params, $this));
  445. else
  446. $this->addRelationship($name, new DbRelationshipHasMany($name, $params, $this));
  447. }
  448. protected function hasOne($name, $params = array())
  449. {
  450. $this->addRelationship($name, new DbRelationshipHasOne($name, $params, $this));
  451. }
  452. protected function belongsTo($name, $params = array())
  453. {
  454. $this->addRelationship($name, new DbRelationshipBelongsTo($name, $params, $this));
  455. }
  456. protected function fieldOptions($name, $params = array())
  457. {
  458. $this->addRelationship($name, new DbRelationshipOptions($name, $params, $this));
  459. }
  460. public function getFieldOptions($field)
  461. {
  462. foreach($this->relationships as $thisRelationship)
  463. if($thisRelationship instanceof DbRelationshipOptions && $thisRelationship->isTiedToField($field))
  464. return $thisRelationship;
  465. return false;
  466. }
  467. //
  468. // end vector handlers
  469. //
  470. //
  471. // begin magic functions
  472. //
  473. /**
  474. * Automatic getter: maps unknown variables to database fields
  475. *
  476. * @param string $varname Name of the database field to get the value of
  477. * @return mixed Value of the given database field
  478. */
  479. public function __get($varname)
  480. {
  481. // check on inherited getters, setters, and mixins from Object
  482. if(parent::__isset($varname))
  483. return parent::__get($varname);
  484. if($this->hasRelationship($varname))
  485. return $this->getRelationshipInfo($varname);
  486. return $this->getScalar($varname);
  487. }
  488. /**
  489. * Automatic setter: maps unknown variables to database fields
  490. *
  491. * @param string $varname Name of the database field to set the value of
  492. * @param mixed $value New value for the given database field
  493. */
  494. function __set($varname, $value)
  495. {
  496. $this->setScalar($varname, $value);
  497. }
  498. //
  499. // end magic functions
  500. //
  501. //
  502. // begin iterator functions
  503. //
  504. /**
  505. * Resets the internal pointer to the first column
  506. *
  507. */
  508. public function rewind()
  509. {
  510. reset($this->scalars);
  511. }
  512. /**
  513. * Returns the value of the column that the internal pointer is at
  514. *
  515. * @return mixed
  516. */
  517. public function current()
  518. {
  519. $var = current($this->scalars);
  520. return $var;
  521. }
  522. /**
  523. * returns the name of the column the internal pointer is at
  524. *
  525. * @return string Column Name
  526. */
  527. public function key()
  528. {
  529. $var = key($this->scalars);
  530. return $var;
  531. }
  532. /**
  533. * Moves the internal pointer to the next column and returns the value of that column
  534. *
  535. * @return mixed Value of the next column
  536. */
  537. public function next()
  538. {
  539. $var = next($this->scalars);
  540. return $var;
  541. }
  542. /**
  543. * Returns true if this DbObject is successfully bound to a row in the database
  544. *
  545. * @return boolean True if the object is bound to a row in the database
  546. */
  547. public function valid()
  548. {
  549. $var = $this->current() !== false;
  550. return $var;
  551. }
  552. //
  553. // end iterator functions
  554. //
  555. //
  556. // static methods
  557. //
  558. /**
  559. * Returns the name of the default connection to be used with this DbObject (override in child class to default to a different connection)
  560. *
  561. * @param string $className Name of the DbObject to get the default connection name for
  562. * @return string Name of the default database connection for this object
  563. */
  564. static private function _getConnectionName($className)
  565. {
  566. return 'default';
  567. }
  568. /**
  569. * Static method to return the database connection associated with a given DbObject
  570. *
  571. * @param string $className Name of the DbObject to retreive the default connection of
  572. * @return DbConnection object
  573. */
  574. static private function _getConnection($className)
  575. {
  576. return DbModule::getConnection(call_user_func(array($className, '_getConnectionName'), $className));
  577. }
  578. /**
  579. * Returns the name of the SQL table based on the name of the DbObject class
  580. *
  581. * @param string $className
  582. * @return string Name of the SQL table to link to
  583. */
  584. static public function _getTableName($className)
  585. {
  586. // work around lack of "late static binding"
  587. $dummy = new $className();
  588. return $dummy->getTableName();
  589. }
  590. /**
  591. * Static method creates a new DbObject by name. There must be a class that inherits from DbObject of the name "className" for this to work.
  592. *
  593. * @param string $className Name of the DbObject class to use
  594. * @param array $values Associative array of $fieldName => $value to store in the table
  595. * @return DbObject
  596. */
  597. static public function _create($className, $values)
  598. {
  599. $object = new $className($values);
  600. $object->save();
  601. return $object;
  602. }
  603. /**
  604. * Inserts a row into the table associated with the given DbObject class
  605. *
  606. * @param string $className Name of the DbObject class
  607. * @param assoc_array $values $field => $value array to be inserted into the database (must contain all required fields or a SQL error will be generated)
  608. */
  609. static public function _insert($className, $values)
  610. {
  611. self::_getConnection($className)->insertArray(self::_getTableName($className), $values, false);
  612. }
  613. /**
  614. * Returns an array of DbObjects each representing a row in the database returned by the given SQL statement
  615. *
  616. * @param string $className Name of the DbObject class to retreive
  617. * @param string $sql SQL select statement to use to search
  618. * @param assoc_array $params $param => $value array with parameters to be substituted into the SQL statement
  619. * @return array of DbObject
  620. */
  621. static public function _findBySql($className, $sql, $params)
  622. {
  623. $res = self::_getConnection($className)->query($sql, $params);
  624. if(!$res->valid())
  625. return array();
  626. $objects = array();
  627. for($row = $res->current(); $res->valid(); $row = $res->next())
  628. {
  629. $objects[] = new $className($row);
  630. }
  631. return $objects;
  632. }
  633. /**
  634. * Returns an array of DbObjects each representing a row in the database returned by selecting on the DbObject specified with the given WHERE clause
  635. *
  636. * @param string $className Name of the DbObject class to retreive
  637. * @param string $where SQL "where" clause (minus the "where " at the beginning) to select on
  638. * @param assoc_array $params $param => $value array with parameters to be substituted into the WHERE clause
  639. * @return array of DbObject
  640. */
  641. static public function _findByWhere($className, $where, $params)
  642. {
  643. $tableName = DbObject::_getTableName($className);
  644. return self::_findBySql($className, "select * from $tableName where $where", $params);
  645. }
  646. /**
  647. * Searches the database and returns an array of DbObjects each representing a record in the resultset
  648. *
  649. * @param string $className Name of the DbObject class to retreive
  650. * @param assoc_array $conditions $field => $value array of conditions to search on (currently only "=" operator is supported)
  651. * @param assoc_array $params $param => $value array of parameters to be passed to generateSelectInfo
  652. * @return array of DbObject (s)
  653. */
  654. static public function _find($className, $conditions = NULL, $params = NULL)
  655. {
  656. $tableName = DbObject::_getTableName($className);
  657. $selectInfo = self::_getConnection($className)->generateSelectInfo($tableName, '*', $conditions, $params);
  658. return self::_findBySql($className, $selectInfo['sql'], $selectInfo['params']);
  659. }
  660. /**
  661. * Retrieve one object from the database and map it to an object. Throws an error if more than one row is returned.
  662. *
  663. * @param string $className The name of the class corresponding to the table in the database
  664. * @param array $conditions Key value pair for the fields you want to look up
  665. * @return DbObject
  666. */
  667. static public function _findOne($className, $conditions = NULL)
  668. {
  669. $a = DbObject::_find($className, $conditions);
  670. if(!$a)
  671. return false;
  672. assert(is_array($a));
  673. assert(count($a) == 1);
  674. return current($a);
  675. }
  676. /**
  677. * Retrieves a DbObject from the given table with a row in it, creating the row if neccesary
  678. *
  679. * @param string $className name of the DbObject class to return an instance of
  680. * @param assoc_array $conditions $field => $value for generating the where clause to
  681. * @return DbObject
  682. */
  683. static public function _getOne($className, $conditions = NULL, $default = NULL)
  684. {
  685. $tableName = DbObject::_getTableName($className);
  686. $row = self::_getConnection($className)->selsertRow($tableName, "*", $conditions, $default);
  687. return new $className($row);
  688. }
  689. //
  690. // end static methods
  691. //
  692. }