PageRenderTime 54ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/Mad/Model/Base.php

https://github.com/DKarp/framework
PHP | 3474 lines | 1761 code | 261 blank | 1452 comment | 141 complexity | 3a4a4c4ec80ad293ddd106ed696fcb3f MD5 | raw file

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

  1. <?php
  2. /**
  3. * @category Mad
  4. * @package Mad_Model
  5. * @copyright (c) 2007-2009 Maintainable Software, LLC
  6. * @license http://opensource.org/licenses/bsd-license.php BSD
  7. */
  8. /**
  9. * Object Relation Mapper (ORM) Layer. Tables are represented as classes, rows in
  10. * the table correspond to objects from that class, and columns map to the object
  11. * attributes. Handles all basic CRUD operations (Create, Read, Update, Delete).
  12. *
  13. * Model subclasses should always be created with the generator to ensure creation of
  14. * all correct components (including data objects, unit tests, and fixtures):
  15. *
  16. * <code>
  17. * php ./script/generate.php model {ModelName} {table_name}
  18. * </code>
  19. *
  20. * @category Mad
  21. * @package Mad_Model
  22. * @copyright (c) 2007-2009 Maintainable Software, LLC
  23. * @license http://opensource.org/licenses/bsd-license.php BSD
  24. */
  25. abstract class Mad_Model_Base extends Mad_Support_Object
  26. {
  27. /*##########################################################################
  28. # Configuration options
  29. ##########################################################################*/
  30. /**
  31. * Should the table introspection data be cached
  32. * - true: Cache table introspection data to /tmp/cache/tables
  33. * - false: Introspect database table on every request
  34. */
  35. public static $cacheTables = true;
  36. /**
  37. * Include the root level in json serialization
  38. */
  39. public static $includeRootInJson = false;
  40. /*##########################################################################
  41. # Connection
  42. ##########################################################################*/
  43. /**
  44. * @var object
  45. */
  46. protected static $_connectionSpec;
  47. /**
  48. * @var array
  49. */
  50. protected static $_activeConnection;
  51. /**
  52. * @var Logger
  53. */
  54. protected static $_logger;
  55. /**
  56. * Database adapter instance
  57. * @var Mad_Model_ConnectionAdapter_Abstract
  58. */
  59. public $connection;
  60. /*##########################################################################
  61. # Attributes
  62. ##########################################################################*/
  63. /**
  64. * List of attributes excluded from mass assignment
  65. * @var array
  66. */
  67. protected $_attrProtected = array();
  68. /**
  69. * List of attribute name=>value pairs
  70. * @var array
  71. */
  72. protected $_attributes = array();
  73. /**
  74. * Name of this class
  75. * @var string
  76. */
  77. protected $_className = null;
  78. /**
  79. * Name of the database table
  80. * @var string
  81. */
  82. protected $_tableName = null;
  83. /**
  84. * Name of the primary key db column
  85. * @var string
  86. */
  87. protected $_primaryKey = null;
  88. /**
  89. * Has subclasses through a types table with class_name column
  90. * @var boolean
  91. */
  92. protected $_inheritanceColumn = 'type';
  93. /**
  94. * @var array
  95. */
  96. protected $_columns = array();
  97. /**
  98. * @var array
  99. */
  100. protected $_columnsHash = array();
  101. /**
  102. * @var array
  103. */
  104. protected $_columnNames = array();
  105. /**
  106. * An object cannot allow attribute access once it has been destroyed
  107. * @var boolean
  108. */
  109. protected $_frozen = false;
  110. /**
  111. * Is this a new record to be inserted?
  112. * @var boolean
  113. */
  114. protected $_newRecord = true;
  115. /*##########################################################################
  116. # Associations
  117. ##########################################################################*/
  118. /**
  119. * Has the association changed (even though the actual model might not have)
  120. * @var boolean
  121. */
  122. protected $_assocChanged = false;
  123. /**
  124. * A list of associations for this model define in concrete _initialize()
  125. * Lazy initialized if an unknown property/method is called
  126. *
  127. * @var array
  128. */
  129. protected $_associationList;
  130. /**
  131. * The list of association objects for this model
  132. * Lazy initialized if an unknown property/method is called
  133. *
  134. * @var array
  135. */
  136. protected $_associations;
  137. /**
  138. * The list of methods that are available for the associations of this model
  139. * $_associationMethods['createDocument'] = $hasOneAssociationObject;
  140. * This is lazy initialized if an unknown propery/method is called
  141. *
  142. * @var array
  143. */
  144. protected $_associationMethods;
  145. /*##########################################################################
  146. # Validations
  147. ##########################################################################*/
  148. /**
  149. * The list of validations that thie model enforces before an update/insert
  150. * @var array
  151. */
  152. protected $_validations = array();
  153. /**
  154. * Should we throw exceptions when validations fail
  155. * @var array
  156. */
  157. protected $_throw = false;
  158. /**
  159. * An array of messages stored when validations fail
  160. * @var array
  161. */
  162. public $errors;
  163. /*##########################################################################
  164. # Construct/Destruct
  165. ##########################################################################*/
  166. /**
  167. * Initialize any values given for the model.
  168. *
  169. * Load the model by attributes
  170. * <code>
  171. * <?php
  172. * ...
  173. * $attributes = array('documentname' => 'My Folder',
  174. * 'description' => 'My Description');
  175. * $folder = new Folder($attributes);
  176. * ...
  177. * ?>
  178. * </code>
  179. *
  180. * @param array $attributes construct by attribute list
  181. * @param array $options 'include' associations
  182. * @throws Mad_Model_Exception
  183. */
  184. public function __construct($attributes=null, $options=null)
  185. {
  186. $this->_className = get_class($this);
  187. // establish connection to db
  188. $this->connection = $this->retrieveConnection();
  189. $this->errors = new Mad_Model_Errors($this);
  190. // Initialize relationships/data-validation from subclass
  191. $this->_initialize();
  192. // init table/fields
  193. $this->_tableName = $this->tableName();
  194. $this->_primaryKey = $this->primaryKey();
  195. $this->_attributes = $this->_attributesFromColumnDefinition();
  196. // set values by attribute list
  197. if (isset($attributes)) {
  198. $this->setAttributes($attributes);
  199. }
  200. }
  201. /**
  202. * Clone the object without the values. All objects need to be explicitly
  203. * copied or we get them referencing the same data
  204. */
  205. public function __clone()
  206. {
  207. // reset attributes, errors, and associations
  208. $this->_attributes = $this->_attributesFromColumnDefinition();
  209. $this->errors->clear();
  210. $this->_resetAssociations();
  211. // only need to clone validations if they exist
  212. if (isset($this->_validations)) {
  213. foreach ($this->_validations as &$validation) {
  214. $validation = clone $validation;
  215. }
  216. }
  217. }
  218. /**
  219. * Initialize relationships and Data validation from subclass
  220. */
  221. abstract protected function _initialize();
  222. /*##########################################################################
  223. # Magic Accessor methods
  224. ##########################################################################*/
  225. /**
  226. * Dynamically get value for a attribute. Attributes cannot be retrieved once
  227. * an object has been destroyed.
  228. *
  229. * @param string $name
  230. * @return string
  231. * @throws Mad_Model_Exception
  232. */
  233. public function _get($name)
  234. {
  235. // active-record primary key value
  236. if ($name == 'id') { $name = $this->primaryKey(); }
  237. // active-record || attribute-reader value
  238. if (array_key_exists($name, $this->_attributes)) {
  239. return $this->readAttribute($name);
  240. }
  241. // dynamic attribute added by an association
  242. $this->_initAssociations();
  243. if (isset($this->_associationMethods[$name])) {
  244. return $this->_associationMethods[$name]
  245. ->callMethod($name, array());
  246. // unknown attribute
  247. } else {
  248. throw new Mad_Model_Exception("Unrecognized attribute '$name'");
  249. }
  250. }
  251. /**
  252. * Dynamically set value for a attribute. Attributes cannot be set once an
  253. * object has been destroyed. Primary Key cannot be changed if the data was
  254. * loaded from a database row
  255. *
  256. * @param string $name
  257. * @param mixed $value
  258. * @throws Mad_Model_Exception
  259. */
  260. public function _set($name, $value)
  261. {
  262. if ($this->_frozen) {
  263. $msg = "You cannot set attributes of a destroyed object";
  264. throw new Mad_Model_Exception($msg);
  265. }
  266. // active-record primary key value
  267. if ($name == 'id') { $name = $this->primaryKey(); }
  268. // cannot change pk if it's already set
  269. if (($name == $this->primaryKey()) && !$this->isNewRecord()) {
  270. // ignore assignment of pk so that this works with activeresource
  271. return;
  272. }
  273. // active-record || attribute-reader value
  274. if (array_key_exists($name, $this->_attributes)) {
  275. return $this->writeAttribute($name, $value);
  276. }
  277. // dynamic attribute added by an association
  278. $this->_initAssociations();
  279. if (isset($this->_associationMethods[$name.'='])) {
  280. return $this->_associationMethods[$name.'=']
  281. ->callMethod($name.'=', array($value));
  282. // unknown attribute
  283. } else {
  284. throw new Mad_Model_Exception("Unrecognized attribute '$name'");
  285. }
  286. }
  287. /**
  288. * Allows testing with empty() and isset() to work inside templates
  289. *
  290. * @param string $key
  291. * @return boolean
  292. */
  293. public function _isset($name)
  294. {
  295. // association methods
  296. $this->_initAssociations();
  297. if (isset($this->_associationMethods[$name])) {
  298. return count($this->_get($name)) > 0;
  299. // active-record attribue
  300. } else {
  301. return isset($this->_attributes[$name]);
  302. }
  303. return isset($this->_attributes[$name]);
  304. }
  305. /**
  306. * Association methods are added at runtime and use dynamic methods.
  307. *
  308. * @param string $name
  309. * @param array $args
  310. */
  311. public function __call($name, $args)
  312. {
  313. // dynamic attribute added by an association
  314. $this->_initAssociations();
  315. if (isset($this->_associationMethods[$name])) {
  316. return $this->_associationMethods[$name]->callMethod($name, $args);
  317. // unknown method
  318. } else {
  319. throw new Mad_Model_Exception("Unrecognized method '$name'");
  320. }
  321. }
  322. /**
  323. * Print out a string describing this object's attributes
  324. *
  325. * @return string
  326. */
  327. public function __toString()
  328. {
  329. foreach ($this->_attributes as $name => $value) {
  330. $str[] = "$name => ".(isset($value) ? "'$value'" : 'null');
  331. }
  332. return isset($str) ? "\n".$this->_className." Object: \n".join(", \n", $str) : null;
  333. }
  334. /*##########################################################################
  335. # Serialization
  336. ##########################################################################*/
  337. /**
  338. * Serialize only needs attributes
  339. */
  340. public function __sleep()
  341. {
  342. return array('_attributes', '_attrReaders',
  343. '_attrWriters', '_attrValues');
  344. }
  345. /**
  346. * Enables models to be used as URL parameters for routes automatically.
  347. *
  348. * @return null|string
  349. */
  350. public function toParam()
  351. {
  352. $pk = $this->primaryKey();
  353. if ($pk && isset($this->_attributes[$pk])) {
  354. return (string)$this->_attributes[$pk];
  355. }
  356. }
  357. /*##########################################################################
  358. # Logger
  359. ##########################################################################*/
  360. /**
  361. * Set a logger object, defaulting to mad_default_logger. This needs to
  362. * reset connection so that the correct log is passed to the connection
  363. * adapter.
  364. *
  365. * @param object $logger
  366. */
  367. public static function setLogger($logger=null)
  368. {
  369. self::$_logger = isset($logger) ? $logger : $GLOBALS['MAD_DEFAULT_LOGGER'];
  370. self::establishConnection(self::removeConnection());
  371. }
  372. /**
  373. * Returns the logger object.
  374. *
  375. * @return object
  376. */
  377. public static function logger()
  378. {
  379. // set default logger
  380. if (!isset(self::$_logger)) {
  381. self::setLogger();
  382. }
  383. return self::$_logger;
  384. }
  385. /*##########################################################################
  386. # Connection Management
  387. ##########################################################################*/
  388. /**
  389. * Establishes the connection to the database. Accepts a hash as input where
  390. * the :adapter key must be specified with the name of a database adapter (in lower-case)
  391. *
  392. * Example for regular databases (MySQL, Postgresql, etc):
  393. * <code>
  394. * Mad_Model_Base::establishConnection(array(
  395. * "adapter" => "mysql",
  396. * "host" => "localhost",
  397. * "username" => "myuser",
  398. * "password" => "mypass",
  399. * "database" => "somedatabase"
  400. * ));
  401. * </code>
  402. *
  403. * Example for SQLite database:
  404. * <code>
  405. * Mad_Model_Base::establishConnection(array(
  406. * "adapter" => "sqlite",
  407. * "database" => "path/to/dbfile"
  408. * ));
  409. * </code>
  410. *
  411. * The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
  412. * may be returned on an error.
  413. *
  414. * @param array $spec
  415. * @return Connection
  416. */
  417. public static function establishConnection($spec=null)
  418. {
  419. // $spec is empty: $spec defaults to MAD_ENV string like "development"
  420. // keep going to read YAML for this environment string
  421. if (empty($spec)) {
  422. if ( !defined('MAD_ENV') || !MAD_ENV ) {
  423. throw new Mad_Model_Exception('Adapter Not Specified');
  424. }
  425. $spec = MAD_ENV;
  426. }
  427. // $spec is string: read YAML config for environment named by string
  428. // keep going to process the resulting array
  429. if (is_string($spec)) {
  430. $config = Horde_Yaml::loadFile(MAD_ROOT.'/config/database.yml');
  431. $spec = $config[$spec];
  432. }
  433. // $spec is an associative array
  434. if (is_array($spec)) {
  435. // validation of array is handled by horde_db
  436. self::$_connectionSpec = $spec;
  437. } else {
  438. throw new Mad_Model_Exception("Invalid Connection Specification");
  439. }
  440. }
  441. /**
  442. * Returns true if a connection that's accessible to this class have already
  443. * been opened.
  444. *
  445. * @return boolean
  446. */
  447. public static function isConnected()
  448. {
  449. return isset(self::$_activeConnection);
  450. }
  451. /**
  452. * Locate/Activate the connection
  453. *
  454. * @return Mad_Model_ConnectionAdapter_Abstract
  455. */
  456. public static function retrieveConnection()
  457. {
  458. // already have active connection
  459. if (self::$_activeConnection) {
  460. $conn = self::$_activeConnection;
  461. // connection based on spec
  462. } elseif ($spec = self::$_connectionSpec) {
  463. if (empty($spec['logger'])) {
  464. $spec['logger'] = self::logger();
  465. }
  466. $adapter = Horde_Db_Adapter::getInstance($spec);
  467. $conn = self::$_activeConnection = $adapter;
  468. }
  469. if (empty($conn)) {
  470. throw new Mad_Model_Exception("Connection Not Established");
  471. }
  472. return $conn;
  473. }
  474. /**
  475. * Remove the connection for this class. This will close the active
  476. * connection and the defined connection (if they exist). The result
  477. * can be used as argument for establishConnection, for easy
  478. */
  479. public static function removeConnection()
  480. {
  481. $spec = self::$_connectionSpec;
  482. $conn = self::$_activeConnection;
  483. self::$_connectionSpec = null;
  484. self::$_activeConnection = null;
  485. if ($conn) { $conn->disconnect(); }
  486. return $spec ? $spec : '';
  487. }
  488. /**
  489. * Returns the connection currently associated with the class. This can
  490. * also be used to "borrow" the connection to do database work unrelated
  491. * to any of the specific Active Records.
  492. *
  493. * @return Mad_Model_ConnectionAdapter_Abstract
  494. */
  495. public static function connection()
  496. {
  497. if (self::$_activeConnection) {
  498. return self::$_activeConnection;
  499. } else {
  500. return self::$_activeConnection = self::retrieveConnection();
  501. }
  502. }
  503. /*##########################################################################
  504. # DB Table column/keys
  505. ##########################################################################*/
  506. /**
  507. * Get the name of the table
  508. * @return string
  509. */
  510. public function tableName()
  511. {
  512. if (isset($this->_tableName)) {
  513. return $this->_tableName;
  514. } else {
  515. return $this->resetTableName();
  516. }
  517. }
  518. /**
  519. * Reset the table name based on conventions
  520. *
  521. */
  522. public function resetTableName()
  523. {
  524. return $this->_tableName =
  525. Mad_Support_Inflector::tableize($this->baseClass());
  526. }
  527. /**
  528. * Get the name of the primary key column
  529. * @return string
  530. */
  531. public function primaryKey()
  532. {
  533. if (isset($this->_primaryKey)) {
  534. return $this->_primaryKey;
  535. } else {
  536. return $this->resetPrimaryKey();
  537. }
  538. }
  539. /**
  540. * Rest primary key name based on conventions.
  541. */
  542. public function resetPrimaryKey()
  543. {
  544. return $this->_primaryKey = 'id';
  545. }
  546. /**
  547. * Get class name column used for single-table inheritance
  548. *
  549. * @return string
  550. */
  551. public function inheritanceColumn()
  552. {
  553. return $this->_inheritanceColumn;
  554. }
  555. /**
  556. * Set the name of the table for the model
  557. * @param string $table
  558. */
  559. public function setTableName($value)
  560. {
  561. $this->_tableName = $value;
  562. }
  563. /**
  564. * Set the name of the table for the model
  565. * @param string $value
  566. */
  567. public function setPrimaryKey($value)
  568. {
  569. $this->_primaryKey = $value;
  570. }
  571. /**
  572. * Change the default column used for single-table inheritance
  573. * @param string $col
  574. */
  575. public function setInheritanceColumn($col)
  576. {
  577. $this->_inheritanceColumn = $col;
  578. }
  579. /**
  580. * Returns an array of column objects for the table associated
  581. * with this class.
  582. *
  583. * @return array
  584. */
  585. public function columns()
  586. {
  587. if (empty($this->_columns)) {
  588. $this->_columns = $this->connection->columns($this->tableName(),
  589. "$this->_className Columns");
  590. foreach ($this->_columns as $col) {
  591. $col->setPrimary($col->getName() == $this->_primaryKey);
  592. }
  593. }
  594. return $this->_columns;
  595. }
  596. /**
  597. * Returns a hash of column objects for the table associated with
  598. * this class.
  599. *
  600. * @return array
  601. */
  602. public function columnsHash()
  603. {
  604. if (empty($this->_columnsHash)) {
  605. foreach ($this->columns() as $col) {
  606. $this->_columnsHash[$col->getName()] = $col;
  607. }
  608. }
  609. return $this->_columnsHash;
  610. }
  611. /**
  612. * Returns an array of column names as strings.
  613. *
  614. * @return array
  615. */
  616. public function columnNames()
  617. {
  618. if (empty($this->_columnNames)) {
  619. foreach ($this->columns() as $col) {
  620. $this->_columnNames[] = $col->getName();
  621. }
  622. }
  623. return $this->_columnNames;
  624. }
  625. /**
  626. * Reset the column info
  627. */
  628. public function resetColumnInformation()
  629. {
  630. $this->_columns = $this->_columnsHash =
  631. $this->_columnNames = $this->_inheritanceColumn = null;
  632. }
  633. /**
  634. * Get the base class for this model. Defined by subclass
  635. *
  636. * @return string
  637. */
  638. public function baseClass()
  639. {
  640. // go up single hierarchy if this is an STI model
  641. $parentClass = get_parent_class($this);
  642. if ($parentClass != 'Mad_Model_Base') {
  643. return $parentClass;
  644. }
  645. return $this->_className;
  646. }
  647. /*##########################################################################
  648. # Attributes
  649. ##########################################################################*/
  650. /**
  651. * Set list of attributes protected from mass assignment
  652. *
  653. * @todo implement this in save statements
  654. * @param string $attribute
  655. */
  656. public function attrProtected($attributes)
  657. {
  658. $names = func_get_args();
  659. $this->_attrProtected = array_unique(
  660. array_merge($this->_attrProtected, $names));
  661. }
  662. /**
  663. * Get the value for an attribute in this model
  664. *
  665. * @param string $name
  666. * @return string
  667. */
  668. public function readAttribute($name)
  669. {
  670. // active-record attributes
  671. if (array_key_exists($name, $this->_attributes)) {
  672. return $this->_attributes[$name];
  673. // no value set yet
  674. } else {
  675. return null;
  676. }
  677. }
  678. /**
  679. * Set the value for an attribute in this model
  680. *
  681. * @param string $name
  682. * @param mixed $value
  683. */
  684. public function writeAttribute($name, $value)
  685. {
  686. // active-record attributes
  687. if (array_key_exists($name, $this->_attributes)) {
  688. $this->_attributes[$name] = $value;
  689. }
  690. }
  691. /**
  692. * Get the human attribute name for a given attribute
  693. *
  694. * @return string
  695. */
  696. public function humanAttributeName($attr)
  697. {
  698. $col = $this->columnForAttribute($attr);
  699. return Mad_Support_Inflector::humanize($col->getName());
  700. }
  701. /**
  702. * Get the array of attribute fields
  703. * @return array
  704. */
  705. public function getAttributes()
  706. {
  707. return $this->_attributes;
  708. }
  709. /**
  710. * Mass assign attributes for this model
  711. * @param array $attributes
  712. */
  713. public function setAttributes($attributes = array())
  714. {
  715. // Set attributes by array
  716. if (is_array($attributes)) {
  717. foreach ($attributes as $attribute => $value) {
  718. $this->$attribute = $value;
  719. }
  720. // Set primary key (Beware this does not instantiate other properties)
  721. } elseif (is_numeric($attributes)) {
  722. $this->{$this->primaryKey()} = $attributes;
  723. }
  724. }
  725. /**
  726. * Finder methods must instantiate through this method to work with the
  727. * single-table inheritance model that makes it possible to create
  728. * objects of different types from the same table.
  729. *
  730. * @param array $record
  731. */
  732. public function instantiate($record)
  733. {
  734. // single table inheritance
  735. $column = $this->inheritanceColumn();
  736. if (isset($record[$column]) && $className = $record[$column]) {
  737. if (!class_exists($className)) {
  738. $msg = "The single-table inheritance mechanism failed to ".
  739. "locate the subclass: '$className'. This error is raised ".
  740. "because the column '$column' is reserved for storing the ".
  741. "class in case of inheritance. Please rename this column ".
  742. "if you didn't intend it to be used for storing the ".
  743. "inheritance class.";
  744. throw new Mad_Model_Exception($msg);
  745. }
  746. $model = new $className;
  747. } else {
  748. $model = clone $this;
  749. }
  750. return $model->setValuesByRow($record);
  751. }
  752. /**
  753. * Set the values for this object using a db result set.
  754. *
  755. * <code>
  756. * <?php
  757. * ...
  758. * $folder = new Folder();
  759. * $row = $result->fetchRow();
  760. * $folder->setValuesByRow($row)
  761. * ...
  762. * ?>
  763. * </code>
  764. *
  765. * @param array $dbValues
  766. * @return Mad_Model_Base
  767. */
  768. public function setValuesByRow($values)
  769. {
  770. // active-record attributes
  771. foreach ($this->_attributes as $name => $value) {
  772. if (array_key_exists($name, $values)) {
  773. $this->writeAttribute($name, $values[$name]);
  774. }
  775. }
  776. // attr-writers
  777. foreach ($this->_attrWriters as $name) {
  778. if (array_key_exists($name, $values)) {
  779. $this->$name = $values[$name];
  780. }
  781. }
  782. // this isn't a new record if we've loaded it from the db
  783. $this->_newRecord = false;
  784. return $this;
  785. }
  786. /**
  787. * Returns an array of names for the attributes available on this
  788. * object sorted alphabetically.
  789. *
  790. * @return array
  791. */
  792. public function attributeNames()
  793. {
  794. $attrs = array_keys($this->_attributes);
  795. sort($attrs);
  796. return $attrs;
  797. }
  798. /**
  799. * Returns the column object for the named attribute
  800. *
  801. * @param string $name
  802. * @return object
  803. */
  804. public function columnForAttribute($name)
  805. {
  806. $colHash = $this->columnsHash();
  807. return $colHash[$name];
  808. }
  809. /*##########################################################################
  810. # Deprecated column accessors
  811. ##########################################################################*/
  812. /**
  813. * Get an array of columns
  814. * @deprecated
  815. * @param string $tblAlias prepend table alias to columns
  816. * @param boolean $colAlias Generate column aliases for TO_CHAR()s
  817. * @return array
  818. */
  819. public function getColumns($tblAlias=null, $colAlias=true)
  820. {
  821. $tblAlias = isset($tblAlias) ? "$tblAlias." : null;
  822. foreach ($this->_attributes as $name => $value) {
  823. $cols[] = $tblAlias.($name);
  824. }
  825. return isset($cols) ? $cols : array();
  826. }
  827. /**
  828. * Construct the column string from the columns. Convert timestamps to string (TO_CHAR)
  829. * @deprecated
  830. * @param string $tblAlias prepend table alias to columns
  831. * @param boolean $colAlias Generate column aliases for TO_CHAR()s
  832. * @return string
  833. */
  834. public function getColumnStr($tblAlias=null, $colAlias=true)
  835. {
  836. foreach ($this->getColumns($tblAlias, $colAlias) as $col) {
  837. $parts = explode('.', $col);
  838. // has table alias
  839. if (isset($parts[1])) {
  840. $quoted[] = $this->connection->quoteColumnName($parts[0]).'.'.
  841. $this->connection->quoteColumnName($parts[1]);
  842. // column only
  843. } else {
  844. $quoted[] = $this->connection->quoteColumnName($parts[0]);
  845. }
  846. }
  847. return join(', ', $quoted);
  848. }
  849. /**
  850. * Get the insert values string from the columns.
  851. * @deprecated
  852. * @return string
  853. */
  854. public function getInsertValuesStr()
  855. {
  856. $vals = array();
  857. foreach ($this->_attributes as $name => $value) {
  858. $vals[] = $this->_quoteValue($value);
  859. }
  860. return join(', ', $vals);
  861. }
  862. /*##########################################################################
  863. # Associations
  864. ##########################################################################*/
  865. /**
  866. * Returns the Association object for the named association
  867. *
  868. * @param string $name
  869. * @return Mad_Model_Association_Base
  870. */
  871. public function reflectOnAssociation($name)
  872. {
  873. $this->_initAssociations();
  874. if (! isset($this->_associations[$name])) {
  875. throw new Mad_Model_Exception("Association $name does not exist for ".get_class($this));
  876. }
  877. return $this->_associations[$name];
  878. }
  879. /**
  880. * Since the value associated with the association has change, force it to
  881. * reload
  882. */
  883. public function reloadAssociation($name)
  884. {
  885. if (isset($this->_associationMethods)) {
  886. $this->_associationMethods = null;
  887. $this->_associations = null;
  888. }
  889. }
  890. /**
  891. * Set as association as being loaded
  892. * @param string $name
  893. */
  894. public function setAssociationLoaded($name)
  895. {
  896. $this->_initAssociations();
  897. if (isset($this->_associations[$name])) {
  898. $this->_associations[$name]->setLoaded();
  899. }
  900. }
  901. /*##########################################################################
  902. # CRUD Class methods
  903. ##########################################################################*/
  904. /**
  905. * <b>FIND BY PRIMARY KEY.</b>
  906. *
  907. * <code>
  908. * $binder = Binder::find(123);
  909. * $binders = Binder::find(array(123, 234));
  910. * </code>
  911. *
  912. *
  913. * <b>FIND ALL</b>
  914. *
  915. * Retrieve using WHERE conditions using SQL:
  916. * <code>
  917. * $binders = Binder::find('all', array(
  918. * 'conditions' => "name = 'Stubbed Images'")
  919. * );
  920. * </code>
  921. *
  922. * Retrieve using WHERE conditions and LIMIT:
  923. * <code>
  924. * $binders = Binder::find('all', array('conditions' => 'name = :name',
  925. * 'order' => 'name DESC'
  926. * 'limit' => 10),
  927. * array(':name' => 'Stubbed Images'));
  928. * </code>
  929. *
  930. * Retrieve using WHERE conditions and OFFSET (same as mysql LIMIT 20, 10):
  931. * <code>
  932. * $binders = Binder::find('all', array('conditions' => 'name = :name',
  933. * 'order' => 'name DESC'
  934. * 'offset' => 20,
  935. * 'limit' => 10),
  936. * array(':name' => 'Stubbed Images'));
  937. * </code>
  938. *
  939. * Retrieve using WHERE conditions and FROM tables:
  940. * <code>
  941. * $folders = Folder::find('all', array('conditions' => 'f.folderid=d.parent_folderid',
  942. * 'from' => 'folders f, documents d'));
  943. * </code>
  944. *
  945. *
  946. * <b>FIND FIRST</b>
  947. *
  948. * Find the first record that matches the given criteria. (same options as find('all')
  949. * <code>
  950. * $binder = Binder::find('first', array('conditions' => 'f.folderid=d.parent_folderid',
  951. * 'from' => 'folders f, documents d'));
  952. * </code>
  953. *
  954. *
  955. * @param mixed $type (pk/pks/all/first/count)
  956. * @param array $options
  957. * @param array $bindVars
  958. * @throws Mad_Model_Exception_RecordNotFound
  959. */
  960. public static function find($type, $options=null, $bindVars=null)
  961. {
  962. // hack to get name of this class (because of static)
  963. $bt = debug_backtrace();
  964. $m = new $bt[1]['class'];
  965. return $m->_find($type, $options, $bindVars);
  966. }
  967. /**
  968. * A convenience wrapper for find('first'). You can pass in all the
  969. * same arguments to this method as you can to find('first').
  970. *
  971. * @see Mad_Model_Base::find()
  972. *
  973. * @param array $options
  974. * @param array $bindVars
  975. */
  976. public static function first($options=null, $bindVars=null)
  977. {
  978. // hack to get name of this class (because of static)
  979. $bt = debug_backtrace();
  980. $m = new $bt[1]['class'];
  981. return $m->_find('first', $options, $bindVars);
  982. }
  983. /**
  984. * Count how many records match the given criteria
  985. * <code>
  986. * $binderCnt = Binder::count(array('name' => 'Stubbed Images'));
  987. * </code>
  988. */
  989. public static function count($options=null, $bindVars=null)
  990. {
  991. // hack to get name of this class (because of static)
  992. $bt = debug_backtrace();
  993. $m = new $bt[1]['class'];
  994. return $m->_count($options, $bindVars);
  995. }
  996. /**
  997. * This method provides an interface for finding records using direct sql instead of
  998. * the componentized api of find(). This is however not always desired as find() does
  999. * some magic that this method cannot do.
  1000. *
  1001. * <b>FIND ALL RECORDS BY SQL</b>
  1002. *
  1003. * <code>
  1004. * $sql = 'SELECT *
  1005. * FROM briefcases
  1006. * WHERE name=:name';
  1007. * $collection = Binder::findBySql('all', $sql, array(':name'=>'Stubbed Images'));
  1008. * </code>
  1009. *
  1010. *
  1011. * <b>FIND FIRST RECORD BY SQL</b>
  1012. *
  1013. * <code>
  1014. * $sql = 'SELECT *
  1015. * FROM briefcases
  1016. * WHERE name=:name';
  1017. * $binder = Binder::findBySql('first', $sql, array(':name'=>'Stubbed Images'));
  1018. * </code>
  1019. *
  1020. *
  1021. * @param string $type
  1022. * @param string $sql
  1023. * @param array $bindVars
  1024. */
  1025. protected static function findBySql($type, $sql, $bindVars=null)
  1026. {
  1027. // hack to get name of this class (because of static)
  1028. $bt = debug_backtrace();
  1029. $m = new $bt[1]['class'];
  1030. return $m->_findBySql($type, $sql, $bindVars);
  1031. }
  1032. /**
  1033. * This method provides an interface for counting records using direct sql
  1034. * instead of the componentized api of find(). This is however not always
  1035. * desired as find() does some magic that this method cannot do.
  1036. *
  1037. * <b>COUNT RECORDS BY SQL</b>
  1038. *
  1039. * <code>
  1040. * $sql = 'SELECT COUNT(1)
  1041. * FROM briefcases
  1042. * WHERE name=:name';
  1043. * $binder = Binder::countBySql($sql, array(':name'=>'Stubbed Images'));
  1044. * </code>
  1045. *
  1046. * @param string $sql
  1047. * @param array $bindVars
  1048. */
  1049. protected static function countBySql($sql, $bindVars=null)
  1050. {
  1051. // hack to get name of this class (because of static)
  1052. $bt = debug_backtrace();
  1053. $m = new $bt[1]['class'];
  1054. return $m->_countBySql($sql, $bindVars);
  1055. }
  1056. /**
  1057. * Paginate records for find()
  1058. *
  1059. * @param array $options
  1060. * @param array $bindVars
  1061. * @return Mad_Model_Collection
  1062. */
  1063. protected static function paginate($options=null, $bindVars=null)
  1064. {
  1065. // hack to get name of this class (because of static)
  1066. $bt = debug_backtrace();
  1067. $m = new $bt[1]['class'];
  1068. return $m->_paginate($options, $bindVars);
  1069. }
  1070. /**
  1071. * Check if this record exists.
  1072. *
  1073. * <code>
  1074. * $folderExists = Folder::exists(123);
  1075. * </code>
  1076. *
  1077. * @param int $id
  1078. * @return boolean
  1079. */
  1080. public static function exists($id)
  1081. {
  1082. // hack to get name of this class (because of static)
  1083. $bt = debug_backtrace();
  1084. $m = new $bt[1]['class'];
  1085. return $m->_exists($id);
  1086. }
  1087. /**
  1088. * Create a new record in the db from the attributes of the model
  1089. *
  1090. * Create single record
  1091. * <code>
  1092. * $binder = Binder::create(array('name' => "derek's binder"));
  1093. * </code>
  1094. *
  1095. * Create multiple records
  1096. * <code>
  1097. * $binders = Binder::create(array(array('name' => "derek's binder"),
  1098. * array('name' => "dallas' binder")));
  1099. * </code>
  1100. *
  1101. * @param array $attributes
  1102. * @return mixed single model object OR array of model objects
  1103. */
  1104. public static function create($attributes)
  1105. {
  1106. // hack to get name of this class (because of static)
  1107. $bt = debug_backtrace();
  1108. $m = new $bt[1]['class'];
  1109. return $m->_create($attributes);
  1110. }
  1111. /**
  1112. * Update record in the db directly by pk or array of pks
  1113. *
  1114. * Single record update
  1115. * <code>
  1116. * $binder = Binder::update(123, array('name' => 'My new name'));
  1117. * </code>
  1118. *
  1119. * Multiple record update
  1120. * <code>
  1121. * $binders = Binder::update(array(123, 456), array('name' => 'My new name'));
  1122. * </code>
  1123. *
  1124. * @param int $id
  1125. * @param array $attributes
  1126. * @return void
  1127. */
  1128. public static function update($id, $attributes=null)
  1129. {
  1130. // hack to get name of this class (because of static)
  1131. $bt = debug_backtrace();
  1132. $m = new $bt[1]['class'];
  1133. return $m->_update($id, $attributes);
  1134. }
  1135. /**
  1136. * Delete record(s) from the database by primary key
  1137. *
  1138. * Delete single record
  1139. * <code>
  1140. * Binder::delete(123);
  1141. * </code>
  1142. *
  1143. * Delete multiple records
  1144. * <code>
  1145. * Binder::delete(array(123, 234));
  1146. * </code>
  1147. *
  1148. * @param mixed $id (int or array of ints)
  1149. * @return void
  1150. */
  1151. public static function delete($id)
  1152. {
  1153. // hack to get name of this class (because of static)
  1154. $bt = debug_backtrace();
  1155. $m = new $bt[1]['class'];
  1156. return $m->_delete($id);
  1157. }
  1158. /**
  1159. * Update multiple records that match the given conditions
  1160. *
  1161. * <code>
  1162. * Binder::update("description = 'my tests'", 'name = :name',
  1163. * array(':name' => 'My test binder'));
  1164. * </code>
  1165. *
  1166. * @param string $set
  1167. * @param string $conditions
  1168. * @param array $bindVars
  1169. * @return void
  1170. */
  1171. public static function updateAll($set, $conditions=null, $bindVars=null)
  1172. {
  1173. // hack to get name of this class (because of static)
  1174. $bt = debug_backtrace();
  1175. $m = new $bt[1]['class'];
  1176. return $m->_updateAll($set, $conditions, $bindVars);
  1177. }
  1178. /**
  1179. * Delete multiple records that match the given conditions
  1180. *
  1181. * <code>
  1182. * Binder::delete('name = :name', array(':name' => 'My test binder'));
  1183. * </code>
  1184. *
  1185. * @param string $conditions
  1186. * @param array $bindVars
  1187. */
  1188. public static function deleteAll($conditions=null, $bindVars=null)
  1189. {
  1190. // hack to get name of this class (because of static)
  1191. $bt = debug_backtrace();
  1192. $m = new $bt[1]['class'];
  1193. return $m->_deleteAll($conditions, $bindVars);
  1194. }
  1195. /*##########################################################################
  1196. # CRUD Instance methods
  1197. ##########################################################################*/
  1198. /**
  1199. * Save data stored in memory (the object) back into the database. Performs either
  1200. * an insert or an update depending on if this is a new record
  1201. *
  1202. * Insert a row
  1203. * <code>
  1204. * $binder = new Binder(array('name' => "Derek's binder"));
  1205. * $binder->save();
  1206. * </code>
  1207. *
  1208. * Update a row
  1209. * <code>
  1210. * $binder = Binder::find(123);
  1211. * $binder->name = "Derek's updated binder";
  1212. * $binder->save();
  1213. * </code>
  1214. *
  1215. * @return mixed boolean or Mad_Model_Base
  1216. * @throws Mad_Model_Exception_Validation
  1217. */
  1218. public function save()
  1219. {
  1220. // All saves are atomic - only start transaction if one hasn't been
  1221. $started = $this->connection->transactionStarted();
  1222. if (!$started) { $this->connection->beginDbTransaction(); }
  1223. try {
  1224. // save associated models this model depends on & validate data
  1225. $this->_saveAssociations('before');
  1226. $this->_validateData();
  1227. $this->_createOrUpdate();
  1228. $this->_saveAssociations('after');
  1229. $this->_newRecord = false;
  1230. if (!$started) { $this->connection->commitDbTransaction(); }
  1231. $this->_throw = false;
  1232. return $this;
  1233. } catch (Exception $e) {
  1234. $this->connection->rollbackDbTransaction();
  1235. if ($this->_throw) {
  1236. $this->_throw = false;
  1237. throw $e;
  1238. }
  1239. return false;
  1240. }
  1241. }
  1242. /**
  1243. * Attempts to save the record, but instead of just returning false if it
  1244. * couldn't happen, it throws a Mad_Model_Exception_Validation
  1245. *
  1246. * @see Mad_Model_Base::save()
  1247. *
  1248. * @return object
  1249. * @throws Mad_Model_Exception_Validation
  1250. */
  1251. public function saveEx()
  1252. {
  1253. $this->_throw = true;
  1254. $this->save();
  1255. }
  1256. /**
  1257. * Update specific attributes for the current object
  1258. *
  1259. * Update single attribute
  1260. * <code>
  1261. * $binder = Binder::find(123);
  1262. * $binder->updateAttributes('name', 'My New Briefcase');
  1263. * </code>
  1264. *
  1265. * @param string $name
  1266. * @param string $value
  1267. * @return void
  1268. */
  1269. public function updateAttribute($name, $value)
  1270. {
  1271. $this->$name = $value;
  1272. return $this->save();
  1273. }
  1274. /**
  1275. * Update multiple attributes for the current object
  1276. *
  1277. * Update multiple attributes
  1278. * <code>
  1279. * $binder = Binder::find(123);
  1280. * $binder->updateAttributes(array('name' => 'The new name',
  1281. * 'description' => 'The new description'));
  1282. * </code>
  1283. *
  1284. * @param array|Traversable $attributes
  1285. * @return void
  1286. */
  1287. public function updateAttributes($attributes)
  1288. {
  1289. if (! is_array($attributes)) {
  1290. if (! $attributes instanceof Traversable) {
  1291. return false;
  1292. }
  1293. }
  1294. foreach ($attributes as $attribute => $value) {
  1295. $this->$attribute = $value;
  1296. }
  1297. return $this->save();
  1298. }
  1299. /**
  1300. * Destroy a record (delete from db)
  1301. *
  1302. * A custom implementation of destroy() can be written for a model by overriding
  1303. * the _destroy() method. This will ensure that all callbacks are still executed
  1304. *
  1305. * <code>
  1306. * $binder = Binder::find(123);
  1307. * $binder->destroy();
  1308. * </code>
  1309. *
  1310. * @return boolean
  1311. */
  1312. public function destroy()
  1313. {
  1314. // All deletes are atomic
  1315. $started = $this->connection->transactionStarted();
  1316. if (!$started) { $this->connection->beginDbTransaction(); }
  1317. try {
  1318. $this->_beforeDestroy();
  1319. $this->_destroy();
  1320. $this->_afterDestroy();
  1321. if (!$started) { $this->connection->commitDbTransaction(); }
  1322. return true;
  1323. } catch (Exception $e) {
  1324. $this->connection->rollbackDbTransaction(false);
  1325. return false;
  1326. }
  1327. }
  1328. /**
  1329. * Replace bind variables in the sql string.
  1330. *
  1331. * @param string $sql
  1332. * @param array $bindVars
  1333. */
  1334. public function sanitizeSql($sql, $bindVars)
  1335. {
  1336. preg_match_all("/(:\w+)/", $sql, $matches);
  1337. if (!isset($matches[1])) return;
  1338. foreach ($matches[1] as $replacement) {
  1339. if (!array_key_exists($replacement, $bindVars)) {
  1340. $msg = "missing value for $replacement in $sql";
  1341. throw new Mad_Model_Exception($msg);
  1342. }
  1343. $sql = str_replace(
  1344. $replacement,
  1345. $this->_quoteValue($bindVars[$replacement]),
  1346. $sql
  1347. );
  1348. }
  1349. return $sql;
  1350. }
  1351. /**
  1352. * Reload values from the database
  1353. */
  1354. public function reload()
  1355. {
  1356. $model = $this->find($this->id);
  1357. foreach ($model->getAttributes() as $name => $value) {
  1358. $this->writeAttribute($name, $value);
  1359. }
  1360. // reset associations
  1361. if (isset($this->_associations)) {
  1362. foreach ($this->_associations as $assoc) {
  1363. $assoc->setLoaded(false);
  1364. }
  1365. }
  1366. return $this;
  1367. }
  1368. /**
  1369. * Check if this is a record that hasn't been inserted yet
  1370. *
  1371. * @return boolean
  1372. */
  1373. public function isNewRecord()
  1374. {
  1375. return $this->_newRecord;
  1376. }
  1377. /**
  1378. * This flag allows us to set explicitly that the association has changed and needs
  1379. * to be saved even if the object itself hasn't been changed
  1380. *
  1381. * @param boolean $assocSaved
  1382. */
  1383. public function setIsAssocChanged($assocChanged=true)
  1384. {
  1385. $this->_assocChanged = $assocChanged;
  1386. }
  1387. /**
  1388. * Check if the association has changed
  1389. *
  1390. * @return boolean
  1391. */
  1392. public function isAssocChanged()
  1393. {
  1394. return $this->_assocChanged;
  1395. }
  1396. /**
  1397. * Check if this object is frozen for modification
  1398. *
  1399. * @return boolean
  1400. */
  1401. public function isDestroyed()
  1402. {
  1403. return $this->_frozen;
  1404. }
  1405. /*##########################################################################
  1406. # Associations - These are set in _initialize() method of subclass
  1407. ##########################################################################*/
  1408. /**
  1409. * This defines a one-to-one relationship with another model class. It declares
  1410. * that the given class has a parent relationship to this model.
  1411. *
  1412. * The foreign key must be specified in the options of the declaration
  1413. * using 'foreignKey'
  1414. *
  1415. * For Document model
  1416. * <code>
  1417. * <?php
  1418. * ...
  1419. * protected function _initialize()
  1420. * {
  1421. * // specify that the Document has a parent Folder
  1422. * $this->belongsTo('Folder', array('foreignKey' => 'parent_folderid'));
  1423. * }
  1424. * ...
  1425. * ?>
  1426. * </code>
  1427. *
  1428. * When we specify this relationship, special attributes and methods are dynamically
  1429. * added to the Document model.
  1430. *
  1431. *
  1432. * Access the parent folder. This performs a query to get the parent folder
  1433. * object of the document.
  1434. *
  1435. * <code>
  1436. * <?php
  1437. * ...
  1438. * // the very verbose..
  1439. * $folderId = $document->parent_folderid;
  1440. * $parentFolder = Folder::find($folderId);
  1441. * $folderName = $parentFolder->folder_name;
  1442. *
  1443. * // can now be simply written as
  1444. * $folderName = $document->folder->folder_name;
  1445. * ...
  1446. * ?>
  1447. * </code>
  1448. *
  1449. * The parent class name is assumed to be the mixed-case singular form of the
  1450. * class name. The association name however can be defined as any name you wish
  1451. * by specifying 'className' option.
  1452. *
  1453. * For Document model
  1454. * <code>
  1455. * <?php
  1456. * ...
  1457. * protected function _initialize()
  1458. * {
  1459. * // specify that the Document has a parent Folder
  1460. * $this->belongsTo('Parent', array('foreignKey' => 'parent_folderid',
  1461. * array('className' => 'Folder')));
  1462. * }
  1463. * ...
  1464. * // now we can access the property using the name 'parent'
  1465. * $parentFolder = $document->parent;
  1466. * ...
  1467. * ?>
  1468. * </code>
  1469. *
  1470. * @param string $associationId
  1471. * @param array $options
  1472. */
  1473. protected function belongsTo($associationId, $options=null)
  1474. {
  1475. $this->_addAssociation('belongsTo', $associationId, $options);
  1476. }
  1477. /**
  1478. * This defines a one-to-one relationship with another model class. It declares
  1479. * that a given class is a child of this model.
  1480. *
  1481. * The foreign key must be specified in the options of the declaration using
  1482. * 'foreignKey'. This declaration defines the same set of methods in the model
  1483. * object as belongsTo, So given the MdMetadata class example..
  1484. *
  1485. * Any given metadata can have a single icon associated with it
  1486. *
  1487. * For MdMetadata model
  1488. * <code>
  1489. * <?php
  1490. * ...
  1491. * protected function _initialize()
  1492. * {
  1493. * // specify that the Metadata has an associated metadata icon
  1494. * $this->hasOne('MdIcon');
  1495. * }
  1496. * ...
  1497. * ?>
  1498. * </code>
  1499. *
  1500. * Now we can refer to the new object through the association
  1501. * <code>
  1502. * <?php
  1503. * ...
  1504. * // the very verbose..
  1505. * $metadataId = $metadata->metadataid;
  1506. * $mdIcon = MdIcon::find($metadataId);
  1507. * $altText = $mdIcon->alt_text;
  1508. *
  1509. * // can now be simply written as
  1510. * $altText = $metadata->mdIcon->alt_text;
  1511. * ...
  1512. * ?>
  1513. * </code>
  1514. *
  1515. * The child class name is assumed to be the mixed-case singular form of the
  1516. * class name. The association name however can be defined as any name you wish
  1517. * by specifying 'className' option similar to belongsTo().
  1518. *
  1519. * Another options available to hasOne is 'dependent'. You can define if the associated
  1520. * object is dependent on this object existing. This can be one of two options,
  1521. * 1. destroy (the default)
  1522. * 2. nullify
  1523. *
  1524. * A metadata Icon can't exist without it's associated metadata. Because of this, we
  1525. * can tell metadata to destroy all metadata icons before
  1526. *
  1527. * @see Mad_Model_Base::belongsTo()
  1528. *
  1529. * @param string $associationId
  1530. * @param array $options
  1531. */
  1532. protected function hasOne($associationId, $options=null)
  1533. {
  1534. $this->_addAssociation('hasOne', $associationId, $options);
  1535. }
  1536. /**
  1537. * This defines a one-to-many relationship with another model class.
  1538. * Define an attribute that behaves like a collection of the child objects.
  1539. *
  1540. * The foreign key must be specified in the options of the declaration using
  1541. * 'foreignKey'. Ordering of children objects can also be specified using the
  1542. * 'order' option.
  1543. *
  1544. * The child class name is assumed to be the mixed-case plural form of the
  1545. * class name. The association name however can be defined as any name you wish
  1546. * by specifying 'className' option similar to belongsTo()
  1547. *
  1548. * For Folder model with multiple documents
  1549. * <code>
  1550. * <?…

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