PageRenderTime 37ms CodeModel.GetById 38ms RepoModel.GetById 0ms app.codeStats 1ms

/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
  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. * <?php
  1551. * ...
  1552. * protected function _initialize()
  1553. * {
  1554. * // specify that the Document has a parent Folder
  1555. * $this->hasMany('Documents', array('foreignKey' => 'parent_folderid',
  1556. * 'order' => 'document_path'));
  1557. * }
  1558. * ...
  1559. * ?>
  1560. * </code>
  1561. *
  1562. * Now we can refer to the new object through the association
  1563. * <code>
  1564. * <?php
  1565. * ...
  1566. * // the very verbose..
  1567. * $folderId = $folder->folderid;
  1568. * $documents = Document::find('all',
  1569. * array('conditions' => 'parent_folderid=:id'),
  1570. * array(':id' => $folderId));
  1571. * foreach ($documents as $document) {
  1572. * print $document->document_name;
  1573. * }
  1574. *
  1575. * // can now be simply written as
  1576. * foreach ($folder->documents as $document) {
  1577. * print $document->document_name;
  1578. * }
  1579. * ...
  1580. * ?>
  1581. * </code>
  1582. *
  1583. * @see Mad_Model_Base::belongsTo()
  1584. * @param string $associationId
  1585. * @param array $options
  1586. */
  1587. protected function hasMany($associationId, $options=null)
  1588. {
  1589. $this->_addAssociation('hasMany', $associationId, $options);
  1590. }
  1591. /**
  1592. * This defines a many-to-many relationship with another model class. It acts
  1593. * in many ways similar to hasMany(), but allows us to specify an association table
  1594. * between the two associated classes.
  1595. *
  1596. * The join table must be specified using the 'joinTable' option. The foreign keys
  1597. * in the join table will be assumed to be the same name as the primary key from
  1598. * the two respective tables. If this is not the case, the foreign key columns can
  1599. * be specified using the 'foreignKey' or 'associationForeignKey' options. Ordering
  1600. * of children objects can also be specified using the 'order' option.
  1601. *
  1602. * The child class name is assumed to be the mixed-case plural form of the
  1603. * class name. The association name however can be defined as any name you wish
  1604. * by specifying 'className' option similar to belongsTo()
  1605. *
  1606. * For Folder model with multiple documents
  1607. * <code>
  1608. * <?php
  1609. * ...
  1610. * protected function _initialize()
  1611. * {
  1612. * // specify that a briefcase has many documents,
  1613. * // and also belongs to many documents
  1614. * $this->hasAndBelongsToMany('Documents',
  1615. * array('joinTable' => 'briefcase_documents',
  1616. * 'order' => 'briefcase_documents.ordering'));
  1617. *
  1618. * }
  1619. * ...
  1620. * ?>
  1621. * </code>
  1622. *
  1623. * If the foreign key names didn't match our convention, we'd have to specify them
  1624. * as follows:
  1625. *
  1626. * <code>
  1627. * <?php
  1628. * ...
  1629. * protected function _initialize()
  1630. * {
  1631. * // specify that a briefcase has many documents,
  1632. * // and also belongs to many documents
  1633. * $this->hasAndBelongsToMany('Documents',
  1634. * array('joinTable' => 'briefcase_documents',
  1635. * 'foreignKey' => 'briefcaseid',
  1636. * 'associationForeignKey' => 'documentid',
  1637. * 'order' => 'briefcase_documents.ordering'));
  1638. * }
  1639. * ...
  1640. * ?>
  1641. * </code>
  1642. *
  1643. * Now we can refer to the new object through the association
  1644. * <code>
  1645. * <?php
  1646. * ...
  1647. * // the very verbose..
  1648. * $id = $binder->briefcaseid;
  1649. * $documents = Document::find('all',
  1650. * array('select' => 'd.*',
  1651. * 'from' => 'documents d, briefcase_documents bd',
  1652. * 'conditions' => 'd.documentid=bd.documentid
  1653. * AND bd.briefcaseid=:id'),
  1654. * array(':id' => $id));
  1655. * foreach ($documents as $document) {
  1656. * print $document->document_name;
  1657. * }
  1658. *
  1659. * // can now be simply written as
  1660. * foreach ($binder->documents as $document) {
  1661. * print $document->document_name;
  1662. * }
  1663. * ...
  1664. * ?>
  1665. * </code>
  1666. *
  1667. * @see Mad_Model_Base::belongsTo()
  1668. * @param string $associationId
  1669. * @param array $options
  1670. */
  1671. protected function hasAndBelongsToMany($associationId, $options=null)
  1672. {
  1673. $this->_addAssociation('hasAndBelongsToMany', $associationId, $options);
  1674. }
  1675. /*##########################################################################
  1676. # Validation - These are set in _initialize() method of subclass
  1677. ##########################################################################*/
  1678. /**
  1679. * Check for errors, and throw exception if found
  1680. * @throws Mad_Model_Exception_Validation
  1681. */
  1682. protected function checkErrors()
  1683. {
  1684. if (!$this->errors->isEmpty()) {
  1685. throw new Mad_Model_Exception_Validation($this->errors->fullMessages());
  1686. }
  1687. }
  1688. /**
  1689. * Check if the data for this method is valid. This will also
  1690. * populate the errors property
  1691. * @return boolean
  1692. */
  1693. public function isValid()
  1694. {
  1695. return $this->_validateData();
  1696. }
  1697. /**
  1698. * This method is invoked on every save() operation. Override
  1699. * this in concrete subclasses to implement your own insert/update validation
  1700. */
  1701. protected function validate(){}
  1702. /**
  1703. * This method is invoked when a record is being inserted. Override
  1704. * this in concrete subclasses to implement your own insert validation
  1705. */
  1706. protected function validateOnCreate(){}
  1707. /**
  1708. * This method is invoked when a record is bieng updated. Override
  1709. * this in concrete subclasses to implement your own update validation.
  1710. */
  1711. protected function validateOnUpdate(){}
  1712. /**
  1713. * Validate the format of the data using ctype or regex.
  1714. * Options:
  1715. * - on: string save, create, or update. Defaults to: save
  1716. * - with: string The ctype/regex to validate against
  1717. * [alpha], [digit], [alnum], or /regex/
  1718. * - message: string Custom error message (default is: "is invalid")
  1719. *
  1720. * <code>
  1721. * <?php
  1722. * ...
  1723. * // make sure parent_id attribute is a digit only on inserts
  1724. * $this->validatesFormatOf('parent_id', array('on' => 'insert', 'with' => '[digit]');
  1725. *
  1726. * // make sure length attribute matches regexp
  1727. * $this->validatesFormatOf('length', array('with' => '/\d+(in|cm)/i');
  1728. * ...
  1729. * ?>
  1730. * </code>
  1731. *
  1732. * @param mixed $attributes
  1733. * @param array $options
  1734. */
  1735. protected function validatesFormatOf($attributes, $options=array())
  1736. {
  1737. $attributes = func_get_args();
  1738. $last = end($attributes);
  1739. $options = is_array($last) ? array_pop($attributes) : array();
  1740. $this->_addValidation('format', $attributes, $options);
  1741. }
  1742. /**
  1743. * Validate the length of the data.
  1744. * Options:
  1745. * - on: string save, create, or update. Defaults to: save
  1746. * - minimum: int Value may not be greater than this int
  1747. * - maximum: int Value may not be less than this int
  1748. * - is: int Value must be specific length
  1749. * - within: array The length of value must be in range: eg. array(3, 5)
  1750. * - allowNull: bool Allow null values through
  1751. *
  1752. * - tooLong: string Message when 'maximum' is violated
  1753. * (default is: "%s is too long (maximum is %d characters)")
  1754. * - tooShort: string Message when 'minimum' is violated
  1755. * (default is: "%s is too short (minimum is %d characters)")
  1756. * - wrongLength: string Message when 'is' is invalid.
  1757. * (default is: "%s is the wrong length (should be %d characters)")
  1758. * - message: string Message to use for a 'minimum', 'maximum', or 'is violation.
  1759. * An alias of the appropriate tooLong/tooShort/wrongLength msg
  1760. *
  1761. * <code>
  1762. * <?php
  1763. * ...
  1764. * // validate name is between 20 and 255 chars
  1765. * $this->validatesLengthOf('name', array('within' => '20..255');
  1766. *
  1767. * // validate is_locked is 1 char
  1768. * $this->validatesLengthOf('is_locked', array('is' => 1);
  1769. *
  1770. * // validate password is more than or equal to 8 chars
  1771. * $this->validatesLengthOf('password', array('minimum' => 8);
  1772. * ...
  1773. * ?>
  1774. * </code>
  1775. *
  1776. * @param mixed $attributes
  1777. * @param int $minLength
  1778. * @param int $maxLength
  1779. */
  1780. protected function validatesLengthOf($attributes, $options=array())
  1781. {
  1782. $attributes = func_get_args();
  1783. $last = end($attributes);
  1784. $options = is_array($last) ? array_pop($attributes) : array();
  1785. $this->_addValidation('length', $attributes, $options);
  1786. }
  1787. /**
  1788. * Validate that the data is numeric. (Yes I'm aware numericality is not a real word)
  1789. * Options:
  1790. * - on: string save, create, or update. Defaults to: save
  1791. * - onlyInteger: bool Don't allow floats
  1792. * - allowNull: bool Are null values valid. Defaults to: false
  1793. * - message: string Defaults to: "%s is not a number."
  1794. *
  1795. * <code>
  1796. * <?php
  1797. * ...
  1798. * // validate that height is a number
  1799. * $this->validatesNumericalityOf('height');
  1800. * $this->validatesNumericalityOf('age', array('only_integer' => true));
  1801. * ...
  1802. * ?>
  1803. * </code>
  1804. *
  1805. * @param mixed $attributes
  1806. */
  1807. protected function validatesNumericalityOf($attributes, $options=array())
  1808. {
  1809. $attributes = func_get_args();
  1810. $last = end($attributes);
  1811. $options = is_array($last) ? array_pop($attributes) : array();
  1812. $this->_addValidation('numericality', $attributes, $options);
  1813. }
  1814. /**
  1815. * Validate that the data isn't empty
  1816. * Options:
  1817. * - on: string save, create, or update. Defaults to: save
  1818. * - message: string Defaults to: "%s can't be empty."
  1819. *
  1820. * <code>
  1821. * <?php
  1822. * ...
  1823. * $this->validatesPresenceOf(array('name', 'description'));
  1824. * ...
  1825. * ?>
  1826. * </code>
  1827. *
  1828. * @param mixed $attributes
  1829. */
  1830. protected function validatesPresenceOf($attributes, $options=array())
  1831. {
  1832. $attributes = func_get_args();
  1833. $last = end($attributes);
  1834. $options = is_array($last) ? array_pop($attributes) : array();
  1835. $this->_addValidation('presence', $attributes, $options);
  1836. }
  1837. /**
  1838. * Validate that the data is unique.
  1839. * Options:
  1840. * - on: string save, create, or update. Defaults to: save
  1841. * - scope: string Limits the check to rows having the same value in the column
  1842. * as the row being checked.
  1843. * - message: string Defaults to: "The value for %s has already been taken."
  1844. *
  1845. * <code>
  1846. * <?php
  1847. * ...
  1848. * $this->validatesUniquenessOf('name', array('scope' => 'parent_id'));
  1849. * ...
  1850. * ?>
  1851. * </code>
  1852. *
  1853. * @param mixed $attributes
  1854. */
  1855. protected function validatesUniquenessOf($attributes, $options=array())
  1856. {
  1857. $attributes = func_get_args();
  1858. $last = end($attributes);
  1859. $options = is_array($last) ? array_pop($attributes) : array();
  1860. $this->_addValidation('uniqueness', $attributes, $options);
  1861. }
  1862. /**
  1863. * Validates an item is included in the list.
  1864. * Options:
  1865. * - on: string save, create, or update. Defaults to: save
  1866. * - in: array|object array or traversable object
  1867. * - allowNull: bool Are null values valid. Defaults to: false
  1868. * - strict: bool If true, use === comparison. Defaults to: false (==).
  1869. * - message: string Defaults to: "%s is not included in the list."
  1870. *
  1871. * <code>
  1872. * <?php
  1873. * ...
  1874. * $this->validatesInclusionOf('name', array('in' => array('foo', 'bar')));
  1875. * ...
  1876. * ?>
  1877. * </code>
  1878. *
  1879. * @param mixed $attributes
  1880. */
  1881. protected function validatesInclusionOf($attributes, $options = array())
  1882. {
  1883. $attributes = func_get_args();
  1884. $last = end($attributes);
  1885. $options = is_array($last) ? array_pop($attributes) : array();
  1886. $this->_addValidation('inclusion', $attributes, $options);
  1887. }
  1888. /**
  1889. * Validate that the email address is formatted correctly
  1890. * Options:
  1891. * - on: string
  1892. * - message:
  1893. *
  1894. *
  1895. * <code>
  1896. * <?php
  1897. * ...
  1898. * $this->validatesEmailAddress('name', array('scope' => 'parent_id'));
  1899. * ...
  1900. * ?>
  1901. * </code>
  1902. */
  1903. protected function validatesEmailAddress($attributes, $options=array())
  1904. {
  1905. $attributes = func_get_args();
  1906. $last = end($attributes);
  1907. $options = is_array($last) ? array_pop($attributes) : array();
  1908. $with = "/^[0-9a-z_\.-]+@(([0-9]{1,3}\.){3}[0-9]{1,3}|".
  1909. "([0-9a-z][0-9a-z-]*[0-9a-z]\.)+[a-z]{2,3})$/i";
  1910. $msg = "must be a valid address";
  1911. $options = array_merge(array('with' => $with, 'message' => $msg), $options);
  1912. $this->_addValidation('format', $attributes, $options);
  1913. }
  1914. /*##########################################################################
  1915. # Serialization
  1916. ##########################################################################*/
  1917. /**
  1918. * Builds an XML document to represent the model. Some configuration is
  1919. * available through <code>options</code>. However more complicated cases should
  1920. * override <code>Mad_Model_Base#toXml</code>.
  1921. *
  1922. * By default the generated XML document will include the processing
  1923. * instruction and all the object's attributes. For example:
  1924. *
  1925. * <?xml version="1.0" encoding="UTF-8"?>
  1926. * <topic>
  1927. * <title>The First Topic</title>
  1928. * <author-name>David</author-name>
  1929. * <id type="integer">1</id>
  1930. * <approved type="boolean">false</approved>
  1931. * <replies-count type="integer">0</replies-count>
  1932. * <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
  1933. * <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
  1934. * <content>Have a nice day</content>
  1935. * <author-email-address>david@loudthinking.com</author-email-address>
  1936. * <parent-id></parent-id>
  1937. * <last-read type="date">2004-04-15</last-read>
  1938. * </topic>
  1939. *
  1940. * This behavior can be controlled with <code>only</code>, <code>except</code>,
  1941. * <code>skip_instruct</code>, <code>skip_types</code> and <code>dasherize</code>.
  1942. * The <code>only</code> and <code>except</code> options are the same as for the
  1943. * <code>attributes</code> method. The default is to dasherize all column names, but you
  1944. * can disable this setting <code>dasherize</code> to <code>false</code>. To not have the
  1945. * column type included in the XML output set <code>:skip_types</code> to <code>true</code>.
  1946. *
  1947. * For instance:
  1948. *
  1949. * $topic->toXml(array('skip_instruct' => true,
  1950. * 'except' => array('id', 'bonus_time', 'written_on', 'replies_count'));
  1951. *
  1952. * <topic>
  1953. * <title>The First Topic</title>
  1954. * <author-name>David</author-name>
  1955. * <approved type="boolean">false</approved>
  1956. * <content>Have a nice day</content>
  1957. * <author-email-address>david@loudthinking.com</author-email-address>
  1958. * <parent-id></parent-id>
  1959. * <last-read type="date">2004-04-15</last-read>
  1960. * </topic>
  1961. *
  1962. * To include first level associations use <code>include</code>:
  1963. *
  1964. * $firm->toXml(array('include' => array('Account', 'Clients')));
  1965. *
  1966. * <?xml version="1.0" encoding="UTF-8"?>
  1967. * <firm>
  1968. * <id type="integer">1</id>
  1969. * <rating type="integer">1</rating>
  1970. * <name>37signals</name>
  1971. * <clients type="array">
  1972. * <client>
  1973. * <rating type="integer">1</rating>
  1974. * <name>Summit</name>
  1975. * </client>
  1976. * <client>
  1977. * <rating type="integer">1</rating>
  1978. * <name>Microsoft</name>
  1979. * </client>
  1980. * </clients>
  1981. * <account>
  1982. * <id type="integer">1</id>
  1983. * <credit-limit type="integer">50</credit-limit>
  1984. * </account>
  1985. * </firm>
  1986. *
  1987. * To include deeper levels of associations pass a hash like this:
  1988. *
  1989. * $firm->toXml(array('include' => array('Account' => array(),
  1990. * 'Clients' => array('include' => 'Address'))));
  1991. *
  1992. * <?xml version="1.0" encoding="UTF-8"?>
  1993. * <firm>
  1994. * <id type="integer">1</id>
  1995. * <rating type="integer">1</rating>
  1996. * <name>37signals</name>
  1997. * <clients type="array">
  1998. * <client>
  1999. * <rating type="integer">1</rating>
  2000. * <name>Summit</name>
  2001. * <address>
  2002. * ...
  2003. * </address>
  2004. * </client>
  2005. * <client>
  2006. * <rating type="integer">1</rating>
  2007. * <name>Microsoft</name>
  2008. * <address>
  2009. * ...
  2010. * </address>
  2011. * </client>
  2012. * </clients>
  2013. * <account>
  2014. * <id type="integer">1</id>
  2015. * <credit-limit type="integer">50</credit-limit>
  2016. * </account>
  2017. * </firm>
  2018. *
  2019. * To include any methods on the model being called use <code>methods</code>:
  2020. *
  2021. * $firm->toXml(array('methods' => array('calculated_earnings', 'real_earnings')));
  2022. *
  2023. * <firm>
  2024. * # ... normal attributes as shown above ...
  2025. * <calculated-earnings>100000000000000000</calculated-earnings>
  2026. * <real-earnings>5</real-earnings>
  2027. * </firm>
  2028. *
  2029. * As noted above, you may override <code>toXml()</code> in your <code>Mad_Model_Base</code>
  2030. * subclasses to have complete control about what's generated. The general
  2031. * form of doing this is:
  2032. *
  2033. * class IHaveMyOwnXML extends Mad_Model_Base
  2034. * {
  2035. * public function toXml($options = array)
  2036. * {
  2037. * // ...
  2038. * }
  2039. * }
  2040. */
  2041. public function toXml($options = array())
  2042. {
  2043. $serializer = new Mad_Model_Serializer_Xml($this, $options);
  2044. return $serializer->serialize();
  2045. }
  2046. /**
  2047. * Convert XML to an Mad_Model record
  2048. *
  2049. * @see Mad_Model_Base::toXml()
  2050. * @param string $xml
  2051. * @return Mad_Model_Base
  2052. */
  2053. public function fromXml($xml)
  2054. {
  2055. $converted = Mad_Support_ArrayObject::fromXml($xml);
  2056. $values = array_values($converted);
  2057. $attributes = $values[0];
  2058. $this->setAttributes($attributes);
  2059. return $this;
  2060. }
  2061. public function getXmlClassName()
  2062. {
  2063. return Mad_Support_Inflector::underscore($this->_className);
  2064. }
  2065. /**
  2066. * Returns a JSON string representing the model. Some configuration is
  2067. * available through <code>$options</code>.
  2068. *
  2069. * Without any <code>$options</code>, the returned JSON string will include all
  2070. * the model's attributes. For example:
  2071. *
  2072. * $konata = User::find(1);
  2073. * $konata->toJson();
  2074. * # => {"id": 1, "name": "Konata Izumi", "age": 16,
  2075. * "created_at": "2006/08/01", "awesome": true}
  2076. *
  2077. * The <code>only</code> and <code>except</code> options can be used to limit
  2078. * the attributes included, and work similar to the <code>attributes</code>
  2079. * method. For example:
  2080. *
  2081. * $konata->toJson(array('only' => array('id', 'name')));
  2082. * # => {"id": 1, "name": "Konata Izumi"}
  2083. *
  2084. * $konata->toJson(array('except' => array('id', 'created_at', 'age')));
  2085. * # => {"name": "Konata Izumi", "awesome": true}
  2086. *
  2087. * To include any methods on the model, use <code>:methods</code>.
  2088. *
  2089. * $konata->toJson(array('methods' => 'permalink'));
  2090. * # => {"id": 1, "name": "Konata Izumi", "age": 16,
  2091. * "created_at": "2006/08/01", "awesome": true,
  2092. * "permalink": "1-konata-izumi"}
  2093. *
  2094. * To include associations, use <code>:include</code>.
  2095. *
  2096. * $konata->toJson(array('include' => 'Posts'));
  2097. * # => {"id": 1, "name": "Konata Izumi", "age": 16,
  2098. * "created_at": "2006/08/01", "awesome": true,
  2099. * "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
  2100. * {"id": 2, author_id: 1, "title": "So I was thinking"}]}
  2101. *
  2102. * 2nd level and higher order associations work as well:
  2103. *
  2104. * $konata->toJson(array('include' => array('Posts' => array(
  2105. * 'include' => array('Comments' => array(
  2106. * 'only' => 'body')),
  2107. * 'only' => 'title'))));
  2108. * # => {"id": 1, "name": "Konata Izumi", "age": 16,
  2109. * "created_at": "2006/08/01", "awesome": true,
  2110. * "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}],
  2111. * "title": "Welcome to the weblog"},
  2112. * {"comments": [{"body": "Don't think too hard"}],
  2113. * "title": "So I was thinking"}]}
  2114. *
  2115. * @param array $options
  2116. * @return string
  2117. */
  2118. public function toJson($options = array())
  2119. {
  2120. $serializer = new Mad_Model_Serializer_Json($this, $options);
  2121. $serialized = $serializer->serialize();
  2122. if (self::$includeRootInJson) {
  2123. $jsonName = $this->getJsonClassName();
  2124. return "{ $jsonName: $serialized }";
  2125. } else {
  2126. return $serialized;
  2127. }
  2128. }
  2129. /**
  2130. * Convert Json notation to an Mad_Model record
  2131. *
  2132. * @see Mad_Model_Base::toJson()
  2133. * @param string $json
  2134. * @return Mad_Model_Base
  2135. */
  2136. public function fromJson($json)
  2137. {
  2138. if (! function_exists('json_decode')) {
  2139. throw new Mad_Model_Exception('json_decode() function required');
  2140. }
  2141. $attributes = (array)json_decode($json);
  2142. $this->setAttributes($attributes);
  2143. return $this;
  2144. }
  2145. public function getJsonClassName()
  2146. {
  2147. return '"'.Mad_Support_Inflector::underscore($this->_className).'"';
  2148. }
  2149. /*##########################################################################
  2150. # Private methods
  2151. ##########################################################################*/
  2152. /**
  2153. * @return string
  2154. */
  2155. protected function _quotedId()
  2156. {
  2157. return $this->_quoteValue(
  2158. $this->id, $this->columnForAttribute($this->primaryKey())
  2159. );
  2160. }
  2161. /**
  2162. * Quote strings appropriately for SQL statements.
  2163. */
  2164. protected function _quoteValue($value, $column=null)
  2165. {
  2166. return $this->connection->quote($value, $column);
  2167. }
  2168. /**
  2169. * Initializes the attributes array with keys matching the columns
  2170. * from the linked table and the values matching the corresponding
  2171. * default value of that column, so that a new instance, or one
  2172. * populated from a passed-in Hash, still has all the attributes
  2173. * that instances loaded from the database would.
  2174. *
  2175. * @todo finish
  2176. */
  2177. protected function _attributesFromColumnDefinition()
  2178. {
  2179. $attributes = array();
  2180. foreach ($this->columns() as $col) {
  2181. $attributes[$col->getName()] = null;
  2182. if ($col->getName() != $this->primaryKey()) {
  2183. $attributes[$col->getName()] = $col->getDefault();
  2184. }
  2185. }
  2186. return $attributes;
  2187. }
  2188. /*##########################################################################
  2189. # Find Private methods
  2190. ##########################################################################*/
  2191. /**
  2192. * Check if a record exists.
  2193. *
  2194. * @see Mad_Model_Base::exists()
  2195. * @param array|int $ids
  2196. * @return boolean
  2197. */
  2198. protected function _exists($ids)
  2199. {
  2200. try {
  2201. $this->_findFromIds($ids);
  2202. return true;
  2203. } catch (Mad_Model_Exception_RecordNotFound $e) {
  2204. return false;
  2205. }
  2206. }
  2207. /**
  2208. * Where the actual work is done for find() method
  2209. *
  2210. * @see Mad_Model_Base::find()
  2211. * @param mixed $type (pk or array of pks)
  2212. * @param array $options
  2213. * @param array $bindVars
  2214. * @throws Mad_Model_Exception_RecordNotFound
  2215. */
  2216. protected function _find($type, $options, $bindVars)
  2217. {
  2218. $bindVars = !empty($bindVars) ? $bindVars : array();
  2219. // find the first record that match the options
  2220. if ($type == 'first') {
  2221. return $this->_findInitial($options, $bindVars);
  2222. // find all records that match the options
  2223. } elseif ($type == 'all') {
  2224. return $this->_findEvery($options, $bindVars);
  2225. // type must match one of the above options
  2226. } else {
  2227. return $this->_findFromIds($type, $options, $bindVars);
  2228. }
  2229. }
  2230. /**
  2231. * Find by primary key values. Will either find by a single or multiple pks.
  2232. * Single id returns a single Mad_Model_Base subclass
  2233. * Multple ids return a Mad_Model_Collection of Mad_Model_Base subclasses
  2234. *
  2235. * @see Mad_Model_Base::find()
  2236. * @param array|int $ids
  2237. * @param array $options
  2238. * @param array $bindVars
  2239. *
  2240. * @return Mad_Model_Collection|Mad_Model_Base
  2241. * @throws Mad_Model_Exception_RecordNotFound
  2242. */
  2243. protected function _findFromIds($ids, $options=array(), $bindVars=array())
  2244. {
  2245. $expectsArray = is_array($ids);
  2246. $ids = (array)$ids;
  2247. foreach ($ids as &$id) {
  2248. if (!is_int($id)) $id = trim($id);
  2249. }
  2250. $selectStr = $this->getColumnStr();
  2251. if (count($ids) == 0 || !isset($ids[0])) {
  2252. $msg = "Couldn't find ".get_class($this)." without an ID";
  2253. throw new Mad_Model_Exception_RecordNotFound($msg);
  2254. } elseif (count($ids) == 1) {
  2255. $result = $this->_findOne($ids[0], $options, $bindVars);
  2256. return $expectsArray ? new Mad_Model_Collection($this, array($result)) : $result;
  2257. } else {
  2258. return $this->_findSome($ids, $options, $bindVars);
  2259. }
  2260. }
  2261. /**
  2262. * Find using a single pk
  2263. *
  2264. * @param int $id
  2265. * @param array $options
  2266. * @param array $bindVars
  2267. * @return Mad_Model_Base
  2268. * @throws Mad_Model_Exception_RecordNotFound
  2269. */
  2270. protected function _findOne($id, $options, $bindVars)
  2271. {
  2272. $conditions = null;
  2273. if (isset($options['conditions'])) {
  2274. $conditions = " AND (".$options['conditions'].")";
  2275. }
  2276. $options['conditions'] = "$this->_tableName.$this->_primaryKey = :pkId".
  2277. " $conditions";
  2278. $bindVars[':pkId'] = $id;
  2279. if ($result = $this->_findInitial($options, $bindVars)) {
  2280. return $result;
  2281. } else {
  2282. $msg = "The record for id=$id was not found";
  2283. throw new Mad_Model_Exception_RecordNotFound($msg);
  2284. }
  2285. }
  2286. /**
  2287. * Find using mutiple pks
  2288. *
  2289. * @param int $id
  2290. * @param array $options
  2291. * @return Mad_Model_Collection
  2292. * @throws Mad_Model_Exception_RecordNotFound
  2293. */
  2294. protected function _findSome($ids, $options, $bindVars)
  2295. {
  2296. // build list of ids/binds
  2297. $size = count($ids);
  2298. for ($i = 0; $i < $size; $i++) $inStr[] = ":id{$i}";
  2299. for ($i = 0; $i < $size; $i++) $bindVars[":id{$i}"] = (int) $ids[$i];
  2300. $conditions = null;
  2301. if (isset($options['conditions'])) {
  2302. $conditions = " AND (".$options['conditions'].")";
  2303. }
  2304. $options['conditions'] = "$this->_tableName.$this->_primaryKey IN (".
  2305. join(', ', $inStr).") $conditions";
  2306. $result = $this->_findEvery($options, $bindVars);
  2307. // we should always get back the same number of rows as ids
  2308. if ($result->count() == $size) {
  2309. return $result;
  2310. } else {
  2311. $msg = 'A record id IN ('.join(', ', $ids).') was not found';
  2312. throw new Mad_Model_Exception_RecordNotFound($msg);
  2313. }
  2314. }
  2315. /**
  2316. * Find the first record matching the given options
  2317. *
  2318. * @see Mad_Model_Base::find()
  2319. * @param mixed $options
  2320. * @param array $bindVars
  2321. * @return Mad_Model_Base
  2322. */
  2323. protected function _findInitial($options, $bindVars)
  2324. {
  2325. $result = $this->_findEvery($options, $bindVars);
  2326. return !empty($result[0]) ? $result[0] : null;
  2327. }
  2328. /**
  2329. * Find all records matching the given options
  2330. *
  2331. * @see Mad_Model_Base::find()
  2332. * @param array $options
  2333. * @param array $bindVars
  2334. * @return array {@link Mad_Model_Base}s
  2335. */
  2336. protected function _findEvery($options, $bindVars)
  2337. {
  2338. // use eager loading associations
  2339. if (isset($options['include'])) {
  2340. return $this->_findWithAssociations($options, $bindVars);
  2341. // no eager loading
  2342. } else {
  2343. return $this->_findEveryBySql($this->_constructFinderSql($options), $bindVars);
  2344. }
  2345. }
  2346. /**
  2347. * Count how many records match the given options
  2348. *
  2349. * @see Mad_Model_Base::find()
  2350. * @param mixed $options
  2351. * @param array $bindVars
  2352. * @return int
  2353. */
  2354. protected function _count($options, $bindVars)
  2355. {
  2356. // if $options is a string, default it to be the conditions
  2357. if (is_string($options)) {
  2358. $options = array('conditions' => $options);
  2359. }
  2360. if (!isset($options['select'])) $options['select'] = 'COUNT(1)';
  2361. // use eager loading associations
  2362. if (isset($options['include'])) {
  2363. $options['select'] = 'COUNT(DISTINCT('.$this->tableName().'.'.
  2364. $this->primaryKey().'))';
  2365. return $this->_countWithAssociations($options, $bindVars);
  2366. // no eager loading
  2367. } else {
  2368. $sql = $this->_constructFinderSql($options);
  2369. $sql = $this->sanitizeSql($sql, $bindVars);
  2370. return $this->connection->selectValue($sql, "$this->_className Count");
  2371. }
  2372. }
  2373. /*##########################################################################
  2374. # FindBySql Private methods
  2375. ##########################################################################*/
  2376. /**
  2377. * Where the actual work is done for findBySql() calls
  2378. *
  2379. * @see Mad_Model_Base::findBySql()
  2380. * @param string $type
  2381. * @param string $sql
  2382. * @param array $bindVars
  2383. */
  2384. protected function _findBySql($type, $sql, $bindVars)
  2385. {
  2386. $bindVars = !empty($bindVars) ? $bindVars : array();
  2387. // find all records that match the options
  2388. if ($type == 'all') {
  2389. return $this->_findEveryBySql($sql, $bindVars);
  2390. // find the first record that match the options
  2391. } elseif ($type == 'first') {
  2392. return $this->_findInitialBySql($sql, $bindVars);
  2393. }
  2394. }
  2395. /**
  2396. * Find all records that are retrieved by the given sql
  2397. *
  2398. * @see Mad_Model_Base::findBySql()
  2399. * @param string $sql
  2400. * @param array $bindVars
  2401. */
  2402. protected function _findEveryBySql($sql, $bindVars)
  2403. {
  2404. $sql = $this->sanitizeSql($sql, $bindVars);
  2405. $result = $this->connection->selectAll($sql, "$this->_className Load");
  2406. return new Mad_Model_Collection($this, $result);
  2407. }
  2408. /**
  2409. * Find the first record that is retrieved by the given sql
  2410. *
  2411. * @see Mad_Model_Base::findBySql()
  2412. * @param string $sql
  2413. * @param array $bindVars
  2414. */
  2415. protected function _findInitialBySql($sql, $bindVars)
  2416. {
  2417. $sql = $this->sanitizeSql($sql, $bindVars);
  2418. $sql = $this->connection->addLimitOffset($sql, array('limit' => 1,
  2419. 'offset' => 0));
  2420. if ($row = $this->connection->selectOne($sql, "$this->_className Load")) {
  2421. return $this->instantiate($row);
  2422. } else {
  2423. return null;
  2424. }
  2425. }
  2426. /**
  2427. * Count how many records are retrieved by the given sql
  2428. *
  2429. * @see Mad_Model_Base::findBySql()
  2430. * @param string $sql
  2431. * @param array $bindVars
  2432. */
  2433. protected function _countBySql($sql, $bindVars)
  2434. {
  2435. // execute query
  2436. $sql = $this->sanitizeSql($sql, $bindVars);
  2437. return $this->connection->selectValue($sql, "$this->_className Count");
  2438. }
  2439. /**
  2440. * Paginate is a proxy to find, but determines offset/limit based on
  2441. *
  2442. * @see Mad_Model_Base::paginate()
  2443. * @param array $options
  2444. * @param array $bindVars
  2445. * @return Mad_Model_Collection
  2446. */
  2447. protected function _paginate($options=null, $bindVars=null)
  2448. {
  2449. // determine offset/limit based on page/perPage
  2450. $page = isset($options['page']) ? $options['page'] : 1;
  2451. $perPage = isset($options['perPage']) ? $options['perPage'] : 15;
  2452. unset($options['page']);
  2453. unset($options['perPage']);
  2454. // count records
  2455. $countOptions = $options;
  2456. unset($countOptions['select']);
  2457. $total = $this->_count($countOptions, $bindVars);
  2458. if ($total == 0) { $page = 0; }
  2459. // find records
  2460. if ($total) {
  2461. $options['offset'] = $page * $perPage - $perPage;
  2462. $options['limit'] = $perPage;
  2463. // default to page 1 if out of range
  2464. if ($options['offset'] > $total) {
  2465. $page = 1;
  2466. $options['offset'] = 0;
  2467. }
  2468. $results = $this->_find('all', $options, $bindVars);
  2469. } else {
  2470. $results = new Mad_Model_Collection($this, array());
  2471. }
  2472. // paginated collection
  2473. return new Mad_Model_PaginatedCollection($results, $page, $perPage, $total);
  2474. }
  2475. /*##########################################################################
  2476. # Finder SQL Construction
  2477. ##########################################################################*/
  2478. /**
  2479. * Find model objects with eager loaded associations
  2480. * @param array $options
  2481. * @param array $bindVars
  2482. */
  2483. protected function _findWithAssociations($options, $bindVars)
  2484. {
  2485. $joinDependency = new Mad_Model_Join_Dependency($this, $options['include']);
  2486. $sql = $this->_constructFinderSqlWithAssoc($options, $joinDependency, $bindVars);
  2487. $sql = $this->sanitizeSql($sql, $bindVars);
  2488. $rows = $this->connection->selectAll($sql, "$this->_className Load");
  2489. return new Mad_Model_Collection($this, $joinDependency->instantiate($rows));
  2490. }
  2491. /**
  2492. * Count model objects with eager loaded associations
  2493. * @param array $options
  2494. * @param array $bindVars
  2495. */
  2496. protected function _countWithAssociations($options, $bindVars)
  2497. {
  2498. $joinDependency = new Mad_Model_Join_Dependency($this, $options['include']);
  2499. $sql = $this->_constructFinderSqlWithAssoc($options, $joinDependency, $bindVars);
  2500. $sql = $this->sanitizeSql($sql, $bindVars);
  2501. return $this->connection->selectValue($sql, "$this->_className Count");
  2502. }
  2503. /**
  2504. * Construct the sql to retrieve all models w/eager associations
  2505. * @param array $options
  2506. * @param object $joinDependency
  2507. * @param array $bindVars
  2508. * @return string
  2509. */
  2510. protected function _constructFinderSqlWithAssoc($options, $joinDependency, $bindVars)
  2511. {
  2512. $valid = array('select', 'from', 'conditions', 'include',
  2513. 'order', 'group', 'limit', 'offset');
  2514. $options = Mad_Support_Base::assertValidKeys($options, $valid);
  2515. // get columns from dependency
  2516. foreach ($joinDependency->joins() as $join) {
  2517. foreach ($join->columnNamesWithAliasForSelect() as $colAlias) {
  2518. $cols[] = $colAlias[0].' AS '.$colAlias[1];
  2519. }
  2520. }
  2521. $selectStr = isset($options['select']) ? $options['select'] : join(', ', $cols);
  2522. $sql = "SELECT ".$selectStr;
  2523. $sql .= " FROM ". ($options['from'] ? $options['from'] : $this->tableName());
  2524. $sql .= $this->_constructAssociationJoinSql($joinDependency);
  2525. $sql = $this->_addConditions($sql, $options['conditions']);
  2526. // certain association outer joins will truncate results using 'limit'
  2527. if (isset($options['limit']) && !$this->_usingLimitableReflections($joinDependency->reflections())) {
  2528. $sql = $this->_addLimitedIdsCondition($sql, $options, $joinDependency, $bindVars);
  2529. }
  2530. if ($options['order']) $sql .= ' ORDER BY '.$options['order'];
  2531. if ($this->_usingLimitableReflections($joinDependency->reflections())) {
  2532. $sql = $this->connection->addLimitOffset($sql, $options);
  2533. }
  2534. return $sql;
  2535. }
  2536. /**
  2537. * Add condition to limit our query by a specific set of ids
  2538. * @param string $sql
  2539. * @param array $options
  2540. * @param object $joinDependency
  2541. * @param array $bindVars
  2542. * @return string
  2543. */
  2544. protected function _addLimitedIdsCondition($sql, $options, $joinDependency, $bindVars)
  2545. {
  2546. $idList = $this->_selectLimitedIdsList($options, $joinDependency, $bindVars);
  2547. if (empty($idList)) { throw new Mad_Model_Exception('Invalid Query'); }
  2548. $conditionWord = stristr($sql, 'where') ? ' AND ' : 'WHERE ';
  2549. $sql .= "$conditionWord ".$this->tableName().'.'.
  2550. $this->primaryKey()." IN ($idList)";
  2551. return $sql;
  2552. }
  2553. /**
  2554. * @param array $options
  2555. * @param object $joinDependency
  2556. * @param array $bindVars
  2557. * @return string
  2558. */
  2559. protected function _selectLimitedIdsList($options, $joinDependency, $bindVars)
  2560. {
  2561. $result = $this->connection->selectAll(
  2562. $this->_constructFinderSqlForAssocLimiting($options, $joinDependency, $bindVars),
  2563. "$this->_className Load IDs For Limited Eager Loading");
  2564. $ids = array();
  2565. foreach ($result as $row) {
  2566. $ids[] = $this->connection->quote($row[$this->primaryKey()]);
  2567. }
  2568. return join(', ', $ids);
  2569. }
  2570. /**
  2571. * @param array $options
  2572. * @param object $joinDependency
  2573. * @param array $bindVars
  2574. * @return string
  2575. */
  2576. protected function _constructFinderSqlForAssocLimiting($options, $joinDependency, $bindVars)
  2577. {
  2578. $isDistinct = $this->_includeEagerConditions($options) ||
  2579. $this->_includeEagerOrder($options);
  2580. $sql = "SELECT ";
  2581. if ($isDistinct) {
  2582. $sql .= $this->connection->distinct($this->tableName().'.'.$this->primaryKey());
  2583. } else {
  2584. $sql .= $this->primaryKey();
  2585. }
  2586. $sql .= ' FROM '.$this->tableName().' ';
  2587. // add join tables/conditions/ordering
  2588. if ($isDistinct) {
  2589. $sql .= $this->_constructAssociationJoinSql($joinDependency);
  2590. }
  2591. $sql = $this->_addConditions($sql, $options['conditions']);
  2592. if (!empty($options['order'])) {
  2593. if ($isDistinct) {
  2594. $sql = $this->connection->addOrderByForAssocLimiting($sql, $options);
  2595. } else {
  2596. $sql .= "ORDER BY ".$options['order'];
  2597. }
  2598. }
  2599. $sql = $this->connection->addLimitOffset($sql, $options);
  2600. return $this->sanitizeSql($sql, $bindVars);
  2601. }
  2602. /**
  2603. * Checks if the conditions reference a table other than the
  2604. * current model table
  2605. *
  2606. * @param array $options
  2607. * @return boolean
  2608. */
  2609. protected function _includeEagerConditions($options)
  2610. {
  2611. if (!$conditions = $options['conditions']) { return false; }
  2612. preg_match_all("/([\.\w]+)\.\w+/", $conditions, $matches);
  2613. foreach ($matches[1] as $conditionTableName) {
  2614. if ($conditionTableName != $this->tableName()) { return true; }
  2615. }
  2616. return false;
  2617. }
  2618. /**
  2619. * Checks if the query order references a table other than the
  2620. * current model's table.
  2621. *
  2622. * @param array $options
  2623. * @return boolean
  2624. */
  2625. protected function _includeEagerOrder($options)
  2626. {
  2627. if (!$order = $options['order']) { return false; }
  2628. preg_match_all("/([\.\w]+)\.\w+/", $order, $matches);
  2629. foreach ($matches[1] as $orderTableName) {
  2630. if ($orderTableName != $this->tableName()) { return true; }
  2631. }
  2632. return false;
  2633. }
  2634. /**
  2635. * Cannot use LIMIT/OFFSET on certain associations
  2636. *
  2637. * @param array $reflections
  2638. * @return boolean
  2639. */
  2640. protected function _usingLimitableReflections($reflections)
  2641. {
  2642. foreach ($reflections as $r) {
  2643. $macro = $r->getMacro();
  2644. if ($macro != 'belongsTo' || $macro != 'hasOne') { return false; }
  2645. }
  2646. return true;
  2647. }
  2648. /**
  2649. * Construct 'OUTER JOIN' sql fragments from associations
  2650. *
  2651. * @param object $joinDependency
  2652. */
  2653. protected function _constructAssociationJoinSql($joinDependency)
  2654. {
  2655. // get joins from dependency
  2656. $joins = array();
  2657. foreach ($joinDependency->joinAssociations() as $joinAssoc) {
  2658. $joins[] = $joinAssoc->associationJoin();
  2659. }
  2660. return join('', $joins);
  2661. }
  2662. /**
  2663. * Construct the sql used to do a find() method
  2664. *
  2665. * @param array $options
  2666. * @return string the SQL
  2667. */
  2668. protected function _constructFinderSql($options)
  2669. {
  2670. $valid = array('select', 'from', 'conditions', 'include',
  2671. 'order', 'group', 'limit', 'offset');
  2672. $options = Mad_Support_Base::assertValidKeys($options, $valid);
  2673. $sql = "SELECT ".($options['select'] ? $options['select'] : $this->getColumnStr());
  2674. $sql .= " FROM ". ($options['from'] ? $options['from'] : $this->tableName());
  2675. $sql = $this->_addConditions($sql, $options['conditions']);
  2676. if ($options['group']) $sql .= ' GROUP BY '.$options['group'];
  2677. if ($options['order']) $sql .= ' ORDER BY '.$options['order'];
  2678. return $this->connection->addLimitOffset($sql, $options);
  2679. }
  2680. /**
  2681. * Add 'where' conditions to the sql
  2682. *
  2683. * @param string $sql
  2684. * @param array $options
  2685. */
  2686. private function _addConditions($sql, $conditions)
  2687. {
  2688. $segments = array();
  2689. if (!empty($conditions)) $segments[] = $conditions;
  2690. if (!empty($segments)) $sql .= ' WHERE ('.join(') AND (', $segments).')';
  2691. return $sql;
  2692. }
  2693. /*##########################################################################
  2694. # Create/Update/Delete Private methods
  2695. ##########################################################################*/
  2696. /**
  2697. * Perform save operation. Only save if model data has changed.
  2698. * This method will perform all callback hooks for the save/update/create
  2699. * operation.
  2700. */
  2701. protected function _createOrUpdate()
  2702. {
  2703. // before save callback
  2704. $this->_beforeSave();
  2705. if ($this->isNewRecord()) {
  2706. $this->_beforeCreate();
  2707. $this->_saveCreate();
  2708. $this->_afterCreate();
  2709. } else {
  2710. $this->_beforeUpdate();
  2711. $this->_saveUpdate();
  2712. $this->_afterUpdate();
  2713. }
  2714. // after save callback
  2715. $this->_afterSave();
  2716. }
  2717. /**
  2718. * Create object during save
  2719. *
  2720. * @throws Mad_Model_Exception_Validation
  2721. */
  2722. protected function _saveCreate()
  2723. {
  2724. $this->_recordTimestamps();
  2725. // build & execute SQL
  2726. $sql = "INSERT INTO $this->_tableName (".
  2727. " ".$this->getColumnStr().
  2728. ") VALUES (".
  2729. " ".$this->getInsertValuesStr().
  2730. ")";
  2731. $insertId = $this->connection->insert($sql, "$this->_className Insert");
  2732. // only set the pk if it's not already set
  2733. if ($this->primaryKey() && $this->{$this->primaryKey()} == null) {
  2734. $this->_attributes[$this->primaryKey()] = $insertId;
  2735. }
  2736. return $insertId;
  2737. }
  2738. /**
  2739. * Update object during save
  2740. *
  2741. * @throws Mad_Model_Exception_Validation
  2742. */
  2743. protected function _saveUpdate()
  2744. {
  2745. $this->_recordTimestamps();
  2746. foreach ($this->_attributes as $column => $value) {
  2747. if ($column != $this->primaryKey()) {
  2748. $sets[] = $this->connection->quoteColumnName($column)." = ".
  2749. $this->_quoteValue($value);
  2750. } elseif ($column == $this->primaryKey()) {
  2751. $pkVal = $this->_quoteValue($value);
  2752. }
  2753. }
  2754. $sql = "UPDATE $this->_tableName ".
  2755. " SET ".join(', ', $sets).
  2756. " WHERE $this->_primaryKey = $pkVal";
  2757. return $this->connection->update($sql, "$this->_className Update");
  2758. }
  2759. /**
  2760. * Automatic timestamps for magic columns
  2761. */
  2762. protected function _recordTimestamps()
  2763. {
  2764. $date = date("Y-m-d");
  2765. $time = date("Y-m-d H:i:s");
  2766. $attr = $this->getAttributes();
  2767. // new records
  2768. if (array_key_exists('created_at', $attr) &&
  2769. (empty($this->created_at) || $this->created_at == '0000-00-00 00:00:00')) {
  2770. $this->writeAttribute('created_at', $time);
  2771. }
  2772. if (array_key_exists('created_on', $attr) &&
  2773. (empty($this->created_on) || $this->created_on == '0000-00-00')) {
  2774. $this->writeAttribute('created_on', $date);
  2775. }
  2776. // all saves
  2777. if (array_key_exists('updated_at', $attr)) {
  2778. $this->writeAttribute('updated_at', $time);
  2779. }
  2780. if (array_key_exists('updated_on', $attr)) {
  2781. $this->writeAttribute('updated_on', $date);
  2782. }
  2783. }
  2784. /**
  2785. * Create a new record
  2786. *
  2787. * @see Mad_Model_Base::findBySql()
  2788. * @param array $attributes
  2789. * @return mixed single model object OR array of model objects
  2790. */
  2791. protected function _create($attributes)
  2792. {
  2793. $this->_newRecord = true;
  2794. // MULTIPLE
  2795. if (isset($attributes[0])) {
  2796. $attributeList = $attributes;
  2797. foreach ($attributeList as $attributes) {
  2798. $obj = new $this->_className($attributes);
  2799. $objs[] = $obj->save();
  2800. }
  2801. return $objs;
  2802. // SINGLE
  2803. } else {
  2804. $obj = new $this->_className($attributes);
  2805. $obj->save();
  2806. return $obj;
  2807. }
  2808. }
  2809. /**
  2810. * Update a record
  2811. *
  2812. * @see Mad_Model_Base::update()
  2813. * @param int $id
  2814. * @param array $attributes
  2815. * @return void
  2816. */
  2817. protected function _update($id, $attributes)
  2818. {
  2819. // MULTIPLE
  2820. if (is_array($id)) {
  2821. $ids = $id;
  2822. foreach ($ids as $id) {
  2823. $model = $this->find($id);
  2824. $model->updateAttributes($attributes);
  2825. $objs[] = $model;
  2826. }
  2827. return new Mad_Model_Collection($model, $objs);
  2828. // SINGLE
  2829. } else {
  2830. $model = $this->find($id);
  2831. return $model->updateAttributes($attributes);
  2832. }
  2833. }
  2834. /**
  2835. * Update multiple records matching the given criteria.
  2836. *
  2837. * @todo replacements for bindvars
  2838. *
  2839. * @see Mad_Model_Base::updateAll()
  2840. * @param string $set
  2841. * @param string $conditions
  2842. * @param array $bindVars
  2843. * @return void
  2844. */
  2845. protected function _updateAll($set, $conditions=null, $bindVars=null)
  2846. {
  2847. $setStr = $this->sanitizeSql($set, $bindVars);
  2848. $conditionStr = $this->sanitizeSql($conditions, $bindVars);
  2849. $conditionStr = !empty($conditions) ? "WHERE $conditionStr " : null;
  2850. $sql = "UPDATE $this->_tableName ".
  2851. " SET $setStr ".
  2852. $conditionStr;
  2853. return $this->connection->update($sql, "$this->_className Update");
  2854. }
  2855. /**
  2856. * Perform destroy operation
  2857. */
  2858. protected function _destroy()
  2859. {
  2860. // only delete if not already deleted
  2861. $sql = "DELETE FROM $this->_tableName ".
  2862. " WHERE $this->_primaryKey = ".$this->_quotedId();
  2863. return $this->connection->delete($sql, "$this->_className Delete");
  2864. }
  2865. /**
  2866. * Delete a given record
  2867. *
  2868. * @see Mad_Model_Base::delete()
  2869. * @param mixed $id (int or array of ints)
  2870. * @return boolean
  2871. */
  2872. protected function _delete($id)
  2873. {
  2874. // MULTIPLE
  2875. if (is_array($id)) {
  2876. $ids = $id;
  2877. foreach ($ids as $id) {
  2878. $obj = new $this->_className();
  2879. $obj->id = $id;
  2880. $obj->destroy();
  2881. }
  2882. // SINGLE
  2883. } else {
  2884. $obj = new $this->_className();
  2885. $obj->id = $id;
  2886. $result = $obj->destroy();
  2887. if (!$result) return false;
  2888. }
  2889. return true;
  2890. }
  2891. /**
  2892. * Delete multiple records by the given conditions
  2893. *
  2894. * @todo replacements for bindvars
  2895. *
  2896. * @see Mad_Model_Base::deleteAll()
  2897. * @param string $conditions
  2898. * @param array $bindVars
  2899. */
  2900. protected function _deleteAll($conditions=null, $bindVars=null)
  2901. {
  2902. $conditionStr = $this->sanitizeSql($conditions, $bindVars);
  2903. $conditionStr = !empty($conditions) ? "WHERE $conditionStr " : null;
  2904. $sql = "DELETE FROM $this->_tableName $conditionStr";
  2905. return $this->connection->delete($sql, "$this->_className Delete");
  2906. }
  2907. /*##########################################################################
  2908. # Callback Methods
  2909. ##########################################################################*/
  2910. /**
  2911. * Execute this callback before records are inserted
  2912. */
  2913. protected function _beforeValidation()
  2914. {
  2915. // Execute callback if it exists
  2916. if (method_exists($this, 'beforeValidation')) {
  2917. $this->beforeValidation();
  2918. }
  2919. }
  2920. /**
  2921. * Execute this callback after records are inserted
  2922. */
  2923. protected function _afterValidation()
  2924. {
  2925. // Execute callback if it exists
  2926. if (method_exists($this, 'afterValidation')) {
  2927. $this->afterValidation();
  2928. }
  2929. }
  2930. /**
  2931. * Execute this callback before records are saved
  2932. */
  2933. protected function _beforeSave()
  2934. {
  2935. $this->checkErrors();
  2936. // Execute callback if it exists
  2937. if (method_exists($this, 'beforeSave')) {
  2938. $result = $this->beforeSave();
  2939. if ($result === false) { $this->checkErrors(); }
  2940. }
  2941. }
  2942. /**
  2943. * Execute this callback before records are inserted
  2944. */
  2945. protected function _beforeCreate()
  2946. {
  2947. // Execute callback if it exists
  2948. if (method_exists($this, 'beforeCreate')) {
  2949. $result = $this->beforeCreate();
  2950. if ($result === false) { $this->checkErrors(); }
  2951. }
  2952. }
  2953. /**
  2954. * Execute this callback before records are updated
  2955. */
  2956. protected function _beforeUpdate()
  2957. {
  2958. // Execute callback if it exists
  2959. if (method_exists($this, 'beforeUpdate')) {
  2960. $result = $this->beforeUpdate();
  2961. if ($result === false) { $this->checkErrors(); }
  2962. }
  2963. }
  2964. /**
  2965. * Execute this callback after records are saved
  2966. */
  2967. protected function _afterSave()
  2968. {
  2969. // Execute callback if it exists
  2970. if (method_exists($this, 'afterSave')) {
  2971. $this->afterSave();
  2972. }
  2973. }
  2974. /**
  2975. * Execute this callback after records are inserted
  2976. */
  2977. protected function _afterCreate()
  2978. {
  2979. // Execute callback if it exists
  2980. if (method_exists($this, 'afterCreate')) {
  2981. $this->afterCreate();
  2982. }
  2983. }
  2984. /**
  2985. * Execute this callback after records are updated
  2986. */
  2987. protected function _afterUpdate()
  2988. {
  2989. // Execute callback if it exists
  2990. if (method_exists($this, 'afterUpdate')) {
  2991. $this->afterUpdate();
  2992. }
  2993. }
  2994. /**
  2995. * Execute this callback before records are destroyed
  2996. */
  2997. protected function _beforeDestroy()
  2998. {
  2999. $this->_initAssociations();
  3000. if (isset($this->_associations)) {
  3001. foreach ($this->_associations as $association) {
  3002. $association->destroyDependent();
  3003. }
  3004. }
  3005. // reset error stack
  3006. $this->errors->clear();
  3007. // Execute callback if it exists
  3008. if (method_exists($this, 'beforeDestroy')) {
  3009. $result = $this->beforeDestroy();
  3010. if ($result === false) { $this->checkErrors(); }
  3011. }
  3012. }
  3013. /**
  3014. * Execute this callback after records are destroyed
  3015. */
  3016. protected function _afterDestroy()
  3017. {
  3018. // Execute callback if it exists
  3019. if (method_exists($this, 'afterDestroy')) {
  3020. $this->afterDestroy();
  3021. }
  3022. $this->_frozen = true;
  3023. }
  3024. /*##########################################################################
  3025. # Validation methods
  3026. ##########################################################################*/
  3027. /**
  3028. * Add a validation rule to this controller
  3029. *
  3030. * @param string $type
  3031. * @param string|array $attributes
  3032. * @param array $options
  3033. */
  3034. protected function _addValidation($type, $attributes, $options)
  3035. {
  3036. foreach ((array)$attributes as $attribute) {
  3037. $this->_validations[] = Mad_Model_Validation_Base::factory($type, $attribute, $options);
  3038. }
  3039. }
  3040. /**
  3041. * Validate data that we are about to save
  3042. * @return boolean true for valid, false for invalid
  3043. */
  3044. protected function _validateData()
  3045. {
  3046. // reset error stack
  3047. $this->errors->clear();
  3048. $this->_beforeValidation();
  3049. // validate all
  3050. $this->validate();
  3051. foreach ($this->_validations as $validation) {
  3052. $validation->validate('save', $this);
  3053. }
  3054. // validate create
  3055. if ($this->isNewRecord()) {
  3056. $this->validateOnCreate();
  3057. foreach ($this->_validations as $validation) {
  3058. $validation->validate('create', $this);
  3059. }
  3060. // validate update
  3061. } else {
  3062. $this->validateOnUpdate();
  3063. foreach ($this->_validations as $validation) {
  3064. $validation->validate('update', $this);
  3065. }
  3066. }
  3067. $this->_afterValidation();
  3068. return $this->errors->isEmpty();
  3069. }
  3070. /*##########################################################################
  3071. # Association methods
  3072. ##########################################################################*/
  3073. /**
  3074. * Associations are lazy initialized as needed. This function is called when needed
  3075. * to check if we need an association method
  3076. */
  3077. protected function _initAssociations()
  3078. {
  3079. // only initialize if we haven't already
  3080. if (!isset($this->_associationMethods) && isset($this->_associationList)) {
  3081. // loop thru each define association
  3082. foreach ($this->_associationList as $associationId => $info) {
  3083. list($type, $options) = $info;
  3084. $association = Mad_Model_Association_Base::factory($type, $associationId, $options, $this);
  3085. $this->_associations[$associationId] = $association;
  3086. // add list of dynamic methods this association adds
  3087. foreach ($association->getMethods() as $methodName => $methodCall) {
  3088. $this->_associationMethods[$methodName] = $association;
  3089. }
  3090. }
  3091. }
  3092. }
  3093. /**
  3094. * Force a reload of all associations.
  3095. */
  3096. protected function _resetAssociations()
  3097. {
  3098. if (isset($this->_associationMethods)) {
  3099. $this->_associationMethods = null;
  3100. $this->_associations = null;
  3101. }
  3102. }
  3103. /**
  3104. * Add an association to this model. This creates the appropriate Mad_Model_Association_Base
  3105. * object and adds the object to the stack of associations for this model.
  3106. * it also adds a list of dynamic methods that are added to this object by the
  3107. * association.
  3108. *
  3109. * @param string $type
  3110. * @param string $associationId
  3111. * @param array $options
  3112. */
  3113. protected function _addAssociation($type, $associationId, $options)
  3114. {
  3115. $options = !empty($options) ? $options : array();
  3116. $this->_associationList[$associationId] = array($type, $options);
  3117. }
  3118. /**
  3119. * Save association model data for this model
  3120. *
  3121. * @param string $type (before|after)
  3122. */
  3123. protected function _saveAssociations($type)
  3124. {
  3125. if (!isset($this->_associations)) return;
  3126. // save belongsTo before, and all others after
  3127. foreach ($this->_associations as $association) {
  3128. if ($association instanceof Mad_Model_Association_BelongsTo && $type == 'before') {
  3129. $association->save();
  3130. } elseif (!$association instanceof Mad_Model_Association_BelongsTo && $type == 'after') {
  3131. $association->save();
  3132. }
  3133. }
  3134. }
  3135. }