PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/system-apps/includes/ringside/apps/model/active_record.php

https://github.com/jkinner/ringside
PHP | 3257 lines | 1738 code | 201 blank | 1318 comment | 334 complexity | 893da4bfcc491e9314594838d95fdb3f MD5 | raw file
Possible License(s): LGPL-2.1, Apache-2.0
  1. <?php
  2. /**
  3. * File containing the ActiveRecord class
  4. *
  5. * (PHP 5)
  6. *
  7. * @package PHPonTrax
  8. * @version $Id: active_record.php 283 2007-02-17 08:54:28Z john $
  9. * @copyright (c) 2005 John Peterson
  10. *
  11. * Permission is hereby granted, free of charge, to any person obtaining
  12. * a copy of this software and associated documentation files (the
  13. * "Software"), to deal in the Software without restriction, including
  14. * without limitation the rights to use, copy, modify, merge, publish,
  15. * distribute, sublicense, and/or sell copies of the Software, and to
  16. * permit persons to whom the Software is furnished to do so, subject to
  17. * the following conditions:
  18. *
  19. * The above copyright notice and this permission notice shall be
  20. * included in all copies or substantial portions of the Software.
  21. *
  22. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  23. * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  24. * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  25. * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  26. * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  27. * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  28. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  29. */
  30. /**
  31. * Load the {@link http://pear.php.net/manual/en/package.pear.php PEAR base class}
  32. */
  33. require_once('PEAR.php');
  34. /**
  35. * Load the {@link http://pear.php.net/manual/en/package.database.mdb2.php PEAR MDB2 package}
  36. * PEAR::DB is now deprecated.
  37. * (This package(DB) been superseded by MDB2 but is still maintained for bugs and security fixes)
  38. */
  39. require_once('MDB2.php');
  40. /**
  41. * Base class for the ActiveRecord design pattern
  42. *
  43. * <p>Each subclass of this class is associated with a database table
  44. * in the Model section of the Model-View-Controller architecture.
  45. * By convention, the name of each subclass is the CamelCase singular
  46. * form of the table name, which is in the lower_case_underscore
  47. * plural notation. For example,
  48. * a table named "order_details" would be associated with a subclass
  49. * of ActiveRecord named "OrderDetail", and a table named "people"
  50. * would be associated with subclass "Person". See the tutorial
  51. * {@tutorial PHPonTrax/naming.pkg}</p>
  52. *
  53. * <p>For a discussion of the ActiveRecord design pattern, see
  54. * "Patterns of Enterprise
  55. * Application Architecture" by Martin Fowler, pp. 160-164.</p>
  56. *
  57. * <p>Unit tester: {@link ActiveRecordTest}</p>
  58. *
  59. * @tutorial PHPonTrax/ActiveRecord.cls
  60. */
  61. class ActiveRecord {
  62. /**
  63. * Reference to the database object
  64. *
  65. * Reference to the database object returned by
  66. * {@link http://pear.php.net/manual/en/package.database.mdb2.intro-connect.php PEAR MDB2::Connect()}
  67. * @var object DB
  68. * see
  69. * {@link http://pear.php.net/manual/en/package.database.mdb2.php PEAR MDB2}
  70. */
  71. protected static $db = null;
  72. /**
  73. * Description of a row in the associated table in the database
  74. *
  75. * <p>Retrieved from the RDBMS by {@link set_content_columns()}.
  76. * See {@link
  77. * http://pear.php.net/package/MDB2/docs/2.3.0/MDB2/MDB2_Driver_Reverse_Common.html#methodtableInfo
  78. * DB_common::tableInfo()} for the format. <b>NOTE:</b> Some
  79. * RDBMS's don't return all values.</p>
  80. *
  81. * <p>An additional element 'human_name' is added to each column
  82. * by {@link set_content_columns()}. The actual value contained
  83. * in each column is stored in an object variable with the name
  84. * given by the 'name' element of the column description for each
  85. * column.</p>
  86. *
  87. * <p><b>NOTE:</b>The information from the database about which
  88. * columns are primary keys is <b>not used</b>. Instead, the
  89. * primary keys in the table are listed in {@link $primary_keys},
  90. * which is maintained independently.</p>
  91. * @var string[]
  92. * @see $primary_keys
  93. * @see quoted_attributes()
  94. * @see __set()
  95. */
  96. public $content_columns = null; # info about each column in the table
  97. /**
  98. * Table Info
  99. *
  100. * Array to hold all the info about table columns. Indexed on $table_name.
  101. * @var array
  102. */
  103. public static $table_info = array();
  104. /**
  105. * Class name
  106. *
  107. * Name of the child class. (this is optional and will automatically be determined)
  108. * Normally set to the singular camel case form of the table name.
  109. * May be overridden.
  110. * @var string
  111. */
  112. public $class_name = null;
  113. /**
  114. * Table name
  115. *
  116. * Name of the table in the database associated with the subclass.
  117. * Normally set to the pluralized lower case underscore form of
  118. * the class name by the constructor. May be overridden.
  119. * @var string
  120. */
  121. public $table_name = null;
  122. /**
  123. * Table prefix
  124. *
  125. * Name to prefix to the $table_name. May be overridden.
  126. * @var string
  127. */
  128. public $table_prefix = null;
  129. /**
  130. * Database name override
  131. *
  132. * Name of the database to use, if you are not using the value
  133. * read from file config/database.ini
  134. * @var string
  135. */
  136. public $database_name = null;
  137. /**
  138. * Index into the $active_connections array
  139. *
  140. * Name of the index to use to return or set the current db connection
  141. * Mainly used if you want to connect to different databases between
  142. * different models.
  143. * @var string
  144. */
  145. public $connection_name = TRAX_ENV;
  146. /**
  147. * Stores the database settings
  148. */
  149. public static $database_settings = array();
  150. /**
  151. * Stores the active connections. Indexed on $connection_name.
  152. */
  153. public static $active_connections = array();
  154. /**
  155. * Mode to use when fetching data from database
  156. *
  157. * See {@link
  158. * http://pear.php.net/package/MDB2/docs/2.3.0/MDB2/MDB2_Driver_Common.html#methodsetFetchMode
  159. * the relevant PEAR MDB2 class documentation}
  160. * @var integer
  161. */
  162. public $fetch_mode = MDB2_FETCHMODE_ASSOC;
  163. /**
  164. * Force reconnect to database every page load
  165. *
  166. * @var boolean
  167. */
  168. public $force_reconnect = false;
  169. /**
  170. * find_all() returns an array of objects,
  171. * each object index is off of this field
  172. *
  173. * @var string
  174. */
  175. public $index_on = "id";
  176. /**
  177. * Not yet implemented (page 222 Rails books)
  178. *
  179. * @var boolean
  180. */
  181. public $lock_optimistically = true;
  182. /**
  183. * Composite custom user created objects
  184. * @var mixed
  185. */
  186. public $composed_of = null;
  187. # Table associations
  188. /**
  189. * @todo Document this variable
  190. * @var string[]
  191. */
  192. protected $has_many = null;
  193. /**
  194. * @todo Document this variable
  195. * @var string[]
  196. */
  197. protected $has_one = null;
  198. /**
  199. * @todo Document this variable
  200. * @var string[]
  201. */
  202. protected $has_and_belongs_to_many = null;
  203. /**
  204. * @todo Document this variable
  205. * @var string[]
  206. */
  207. protected $belongs_to = null;
  208. /**
  209. * @todo Document this variable
  210. * @var string[]
  211. */
  212. protected $habtm_attributes = null;
  213. /**
  214. * @todo Document this property
  215. */
  216. protected $save_associations = array();
  217. /**
  218. * Whether or not to auto save defined associations if set
  219. * @var boolean
  220. */
  221. public $auto_save_associations = true;
  222. /**
  223. * Whether this object represents a new record
  224. *
  225. * true => This object was created without reading a row from the
  226. * database, so use SQL 'INSERT' to put it in the database.
  227. * false => This object was a row read from the database, so use
  228. * SQL 'UPDATE' to update database with new values.
  229. * @var boolean
  230. */
  231. protected $new_record = true;
  232. /**
  233. * Names of automatic update timestamp columns
  234. *
  235. * When a row containing one of these columns is updated and
  236. * {@link $auto_timestamps} is true, update the contents of the
  237. * timestamp columns with the current date and time.
  238. * @see $auto_timestamps
  239. * @see $auto_create_timestamps
  240. * @var string[]
  241. */
  242. public $auto_update_timestamps = array("updated_at","updated_on");
  243. /**
  244. * Names of automatic create timestamp columns
  245. *
  246. * When a row containing one of these columns is created and
  247. * {@link $auto_timestamps} is true, store the current date and
  248. * time in the timestamp columns.
  249. * @see $auto_timestamps
  250. * @see $auto_update_timestamps
  251. * @var string[]
  252. */
  253. public $auto_create_timestamps = array("created_at","created_on");
  254. /**
  255. * Date format for use with auto timestamping
  256. *
  257. * The format for this should be compatiable with the php date() function.
  258. * http://www.php.net/date
  259. * @var string
  260. */
  261. public $date_format = "Y-m-d";
  262. /**
  263. * Time format for use with auto timestamping
  264. *
  265. * The format for this should be compatiable with the php date() function.
  266. * http://www.php.net/date
  267. * @var string
  268. */
  269. public $time_format = "H:i:s";
  270. /**
  271. * Whether to keep date/datetime fields NULL if not set
  272. *
  273. * true => If date field is not set it try to preserve NULL
  274. * false => Don't try to preserve NULL if field is already NULL
  275. * @var boolean
  276. */
  277. protected $preserve_null_dates = true;
  278. /**
  279. * SQL aggregate functions that may be applied to the associated
  280. * table.
  281. *
  282. * SQL defines aggregate functions AVG, COUNT, MAX, MIN and SUM.
  283. * Not all of these functions are implemented by all DBMS's
  284. * @var string[]
  285. */
  286. protected $aggregations = array("count","sum","avg","max","min");
  287. /**
  288. * Primary key of the associated table
  289. *
  290. * Array element(s) name the primary key column(s), as used to
  291. * specify the row to be updated or deleted. To be a primary key
  292. * a column must be listed both here and in {@link
  293. * $content_columns}. <b>NOTE:</b>This
  294. * field is maintained by hand. It is not derived from the table
  295. * description read from the database.
  296. * @var string[]
  297. * @see $content_columns
  298. * @see find()
  299. * @see find_all()
  300. * @see find_first()
  301. */
  302. public $primary_keys = array("id");
  303. /**
  304. * Default for how many rows to return from {@link find_all()}
  305. * @var integer
  306. */
  307. public $rows_per_page_default = 20;
  308. /**
  309. * Pagination how many numbers in the list << < 1 2 3 4 > >>
  310. */
  311. public $display = 10;
  312. /**
  313. * @todo Document this variable
  314. */
  315. public $pagination_count = 0;
  316. /**
  317. * Description of non-fatal errors found
  318. *
  319. * For every non-fatal error found, an element describing the
  320. * error is added to $errors. Initialized to an empty array in
  321. * {@link valid()} before validating object. When an error
  322. * message is associated with a particular attribute, the message
  323. * should be stored with the attribute name as its key. If the
  324. * message is independent of attributes, store it with a numeric
  325. * key beginning with 0.
  326. *
  327. * @var string[]
  328. * @see add_error()
  329. * @see get_errors()
  330. */
  331. public $errors = array();
  332. /**
  333. * An array with all the default error messages.
  334. */
  335. public $default_error_messages = array(
  336. 'inclusion' => "is not included in the list",
  337. 'exclusion' => "is reserved",
  338. 'invalid' => "is invalid",
  339. 'confirmation' => "doesn't match confirmation",
  340. 'accepted ' => "must be accepted",
  341. 'empty' => "can't be empty",
  342. 'blank' => "can't be blank",
  343. 'too_long' => "is too long (max is %d characters)",
  344. 'too_short' => "is too short (min is %d characters)",
  345. 'wrong_length' => "is the wrong length (should be %d characters)",
  346. 'taken' => "has already been taken",
  347. 'not_a_number' => "is not a number",
  348. 'not_an_integer' => "is not an integer"
  349. );
  350. /**
  351. * An array of all the builtin validation function calls.
  352. */
  353. protected $builtin_validation_functions = array(
  354. 'validates_acceptance_of',
  355. 'validates_confirmation_of',
  356. 'validates_exclusion_of',
  357. 'validates_format_of',
  358. 'validates_inclusion_of',
  359. 'validates_length_of',
  360. 'validates_numericality_of',
  361. 'validates_presence_of',
  362. 'validates_uniqueness_of'
  363. );
  364. /**
  365. * Whether to automatically update timestamps in certain columns
  366. *
  367. * @see $auto_create_timestamps
  368. * @see $auto_update_timestamps
  369. * @var boolean
  370. */
  371. public $auto_timestamps = true;
  372. /**
  373. * Auto insert / update $has_and_belongs_to_many tables
  374. */
  375. public $auto_save_habtm = true;
  376. /**
  377. * Auto delete $has_and_belongs_to_many associations
  378. */
  379. public $auto_delete_habtm = true;
  380. /**
  381. * Transactions (only use if your db supports it)
  382. * This is for transactions only to let query() know that a 'BEGIN' has been executed
  383. */
  384. private static $begin_executed = false;
  385. /**
  386. * Transactions (only use if your db supports it)
  387. * This will issue a rollback command if any sql fails.
  388. */
  389. public static $use_transactions = false;
  390. /**
  391. * Keep a log of queries executed if in development env
  392. */
  393. public static $query_log = array();
  394. /**
  395. * Construct an ActiveRecord object
  396. *
  397. * <ol>
  398. * <li>Establish a connection to the database</li>
  399. * <li>Find the name of the table associated with this object</li>
  400. * <li>Read description of this table from the database</li>
  401. * <li>Optionally apply update information to column attributes</li>
  402. * </ol>
  403. * @param string[] $attributes Updates to column attributes
  404. * @uses establish_connection()
  405. * @uses set_content_columns()
  406. * @uses $table_name
  407. * @uses set_table_name_using_class_name()
  408. * @uses update_attributes()
  409. */
  410. function __construct($attributes = null) {
  411. # Open the database connection
  412. $this->establish_connection();
  413. # Set $table_name
  414. if($this->table_name == null) {
  415. $this->set_table_name_using_class_name();
  416. }
  417. # Set column info
  418. if($this->table_name) {
  419. $this->set_content_columns($this->table_name);
  420. }
  421. # If $attributes array is passed in update the class with its contents
  422. if(!is_null($attributes)) {
  423. $this->update_attributes($attributes);
  424. }
  425. # If callback is defined in model run it.
  426. # this could hurt performance...
  427. if(method_exists($this, 'after_initialize')) {
  428. $this->after_initialize();
  429. }
  430. }
  431. /**
  432. * Override get() if they do $model->some_association->field_name
  433. * dynamically load the requested contents from the database.
  434. * @todo Document this API
  435. * @uses $belongs_to
  436. * @uses get_association_type()
  437. * @uses $has_and_belongs_to_many
  438. * @uses $has_many
  439. * @uses $has_one
  440. * @uses find_all_has_many()
  441. * @uses find_all_habtm()
  442. * @uses find_one_belongs_to()
  443. * @uses find_one_has_one()
  444. */
  445. function __get($key) {
  446. if($association_type = $this->get_association_type($key)) {
  447. //error_log("association_type:$association_type");
  448. switch($association_type) {
  449. case "has_many":
  450. //print("DOING HASMANY $key\n");
  451. $parameters = is_array($this->has_many) ? $this->has_many[$key] : null;
  452. //var_dump($parameters);
  453. $this->$key = $this->find_all_has_many($key, $parameters);
  454. //var_dump($this->$key);
  455. break;
  456. case "has_one":
  457. $parameters = is_array($this->has_one) ? $this->has_one[$key] : null;
  458. $this->$key = $this->find_one_has_one($key, $parameters);
  459. break;
  460. case "belongs_to":
  461. $parameters = is_array($this->belongs_to) ? $this->belongs_to[$key] : null;
  462. $this->$key = $this->find_one_belongs_to($key, $parameters);
  463. break;
  464. case "has_and_belongs_to_many":
  465. $parameters = is_array($this->has_and_belongs_to_many) ? $this->has_and_belongs_to_many[$key] : null;
  466. $this->$key = $this->find_all_habtm($key, $parameters);
  467. break;
  468. }
  469. } elseif($this->is_composite($key)) {
  470. $composite_object = $this->get_composite_object($key);
  471. if(is_object($composite_object)) {
  472. $this->$key = $composite_object;
  473. }
  474. }
  475. //echo "<pre>getting: $key = ".$this->$key."<br></pre>";
  476. return $this->$key;
  477. }
  478. /**
  479. * Store column value or description of the table format
  480. *
  481. * If called with key 'table_name', $value is stored as the
  482. * description of the table format in $content_columns.
  483. * Any other key causes an object variable with the same name to
  484. * be created and stored into. If the value of $key matches the
  485. * name of a column in content_columns, the corresponding object
  486. * variable becomes the content of the column in this row.
  487. * @uses $auto_save_associations
  488. * @uses get_association_type()
  489. * @uses set_content_columns()
  490. */
  491. function __set($key, $value) {
  492. //echo "setting: $key = $value<br>";
  493. if($key == "table_name") {
  494. $this->set_content_columns($value);
  495. # this elseif checks if first its an object if its parent is ActiveRecord
  496. } elseif(is_object($value) && get_parent_class($value) == __CLASS__ && $this->auto_save_associations) {
  497. if($association_type = $this->get_association_type($key)) {
  498. $this->save_associations[$association_type][] = $value;
  499. if($association_type == "belongs_to") {
  500. $primary_key = $value->primary_keys[0];
  501. $foreign_key = Inflector::singularize($value->table_name)."_".$primary_key;
  502. $this->$foreign_key = $value->$primary_key;
  503. }
  504. }
  505. # this elseif checks if its an array of objects and if its parent is ActiveRecord
  506. } elseif(is_array($value) && $this->auto_save_associations) {
  507. if($association_type = $this->get_association_type($key)) {
  508. $this->save_associations[$association_type][] = $value;
  509. }
  510. }
  511. // Assignment to something else, do it
  512. $this->$key = $value;
  513. }
  514. /**
  515. * Override call() to dynamically call the database associations
  516. * @todo Document this API
  517. * @uses $aggregations
  518. * @uses aggregate_all()
  519. * @uses get_association_type()
  520. * @uses $belongs_to
  521. * @uses $has_one
  522. * @uses $has_and_belongs_to_many
  523. * @uses $has_many
  524. * @uses find_all_by()
  525. * @uses find_by()
  526. */
  527. function __call($method_name, $parameters) {
  528. if(method_exists($this, $method_name)) {
  529. # If the method exists, just call it
  530. $result = call_user_func_array(array($this, $method_name), $parameters);
  531. } else {
  532. # ... otherwise, check to see if the method call is one of our
  533. # special Trax methods ...
  534. # ... first check for method names that match any of our explicitly
  535. # declared associations for this model ( e.g. public $has_many = "movies" ) ...
  536. if(is_array($parameters[0])) {
  537. $parameters = $parameters[0];
  538. }
  539. $association_type = $this->get_association_type($method_name);
  540. switch($association_type) {
  541. case "has_many":
  542. $result = $this->find_all_has_many($method_name, $parameters);
  543. break;
  544. case "has_one":
  545. $result = $this->find_one_has_one($method_name, $parameters);
  546. break;
  547. case "belongs_to":
  548. $result = $this->find_one_belongs_to($method_name, $parameters);
  549. break;
  550. case "has_and_belongs_to_many":
  551. $result = $this->find_all_habtm($method_name, $parameters);
  552. break;
  553. }
  554. # check for the [count,sum,avg,etc...]_all magic functions
  555. if(substr($method_name, -4) == "_all" && in_array(substr($method_name, 0, -4), $this->aggregations)) {
  556. //echo "calling method: $method_name<br>";
  557. $result = $this->aggregate_all($method_name, $parameters);
  558. }
  559. # check for the find_all_by_* magic functions
  560. elseif(strlen($method_name) > 11 && substr($method_name, 0, 11) == "find_all_by") {
  561. //echo "calling method: $method_name<br>";
  562. $result = $this->find_by($method_name, $parameters, "all");
  563. }
  564. # check for the find_by_* magic functions
  565. elseif(strlen($method_name) > 7 && substr($method_name, 0, 7) == "find_by") {
  566. //echo "calling method: $method_name<br>";
  567. $result = $this->find_by($method_name, $parameters);
  568. }
  569. # check for find_or_create_by_* magic functions
  570. elseif(strlen($method_name) > 17 && substr($method_name, 0, 17) == "find_or_create_by") {
  571. $result = $this->find_by($method_name, $parameters, "find_or_create");
  572. }
  573. }
  574. return $result;
  575. }
  576. /**
  577. * Find all records using a "has_and_belongs_to_many" relationship
  578. * (many-to-many with a join table in between). Note that you can also
  579. * specify an optional "paging limit" by setting the corresponding "limit"
  580. * instance variable. For example, if you want to return 10 movies from the
  581. * 5th movie on, you could set $this->movies_limit = "10, 5"
  582. *
  583. * Parameters: $this_table_name: The name of the database table that has the
  584. * one row you are interested in. E.g. genres
  585. * $other_table_name: The name of the database table that has the
  586. * many rows you are interested in. E.g. movies
  587. * Returns: An array of ActiveRecord objects. (e.g. Movie objects)
  588. * @todo Document this API
  589. */
  590. private function find_all_habtm($other_table_name, $parameters = null) {
  591. $additional_conditions = null;
  592. # Use any passed-in parameters
  593. if(!is_null($parameters)) {
  594. if(@array_key_exists("conditions", $parameters)) {
  595. $additional_conditions = " AND (".$parameters['conditions'].")";
  596. } elseif($parameters[0] != "") {
  597. $additional_conditions = " AND (".$parameters[0].")";
  598. }
  599. if(@array_key_exists("order", $parameters)) {
  600. $order = $parameters['order'];
  601. } elseif($parameters[1] != "") {
  602. $order = $parameters[1];
  603. }
  604. if(@array_key_exists("limit", $parameters)) {
  605. $limit = $parameters['limit'];
  606. } elseif($parameters[2] != "") {
  607. $limit = $parameters[2];
  608. }
  609. if(@array_key_exists("class_name", $parameters)) {
  610. $other_object_name = $parameters['class_name'];
  611. }
  612. if(@array_key_exists("join_table", $parameters)) {
  613. $join_table = $parameters['join_table'];
  614. }
  615. if(@array_key_exists("foreign_key", $parameters)) {
  616. $this_foreign_key = $parameters['foreign_key'];
  617. }
  618. if(@array_key_exists("association_foreign_key", $parameters)) {
  619. $other_foreign_key = $parameters['association_foreign_key'];
  620. }
  621. if(@array_key_exists("finder_sql", $parameters)) {
  622. $finder_sql = $parameters['finder_sql'];
  623. }
  624. }
  625. if(!is_null($other_object_name)) {
  626. $other_class_name = Inflector::camelize($other_object_name);
  627. $other_table_name = Inflector::tableize($other_object_name);
  628. } else {
  629. $other_class_name = Inflector::classify($other_table_name);
  630. }
  631. # Instantiate an object to access find_all
  632. $other_class_object = new $other_class_name();
  633. # If finder_sql is specified just use it instead of determining the joins/sql
  634. if(!is_null($finder_sql)) {
  635. $conditions = $finder_sql;
  636. $order = null;
  637. $limit = null;
  638. $joins = null;
  639. } else {
  640. # Prepare the join table name primary keys (fields) to do the join on
  641. if(is_null($join_table)) {
  642. $join_table = $this->get_join_table_name($this->table_name, $other_table_name);
  643. }
  644. # Primary keys
  645. $this_primary_key = $this->primary_keys[0];
  646. $other_primary_key = $other_class_object->primary_keys[0];
  647. # Foreign keys
  648. if(is_null($this_foreign_key)) {
  649. $this_foreign_key = Inflector::singularize($this->table_name)."_".$this_primary_key;
  650. }
  651. if(is_null($other_foreign_key)) {
  652. $other_foreign_key = Inflector::singularize($other_table_name)."_".$other_primary_key;
  653. }
  654. # Primary key value
  655. if($this->attribute_is_string($this_primary_key)) {
  656. $this_primary_key_value = "'".$this->$this_primary_key."'";
  657. } elseif(is_numeric($this->$this_primary_key)) {
  658. $this_primary_key_value = $this->$this_primary_key;
  659. } else {
  660. #$this_primary_key_value = 0;
  661. # no primary key value so just return empty array same as find_all()
  662. return array();
  663. }
  664. # Set up the SQL segments
  665. $conditions = "{$join_table}.{$this_foreign_key} = {$this_primary_key_value}".$additional_conditions;
  666. $joins = "LEFT JOIN {$join_table} ON {$other_table_name}.{$other_primary_key} = {$join_table}.{$other_foreign_key}";
  667. }
  668. # Get the list of other_class_name objects
  669. return $other_class_object->find_all($conditions, $order, $limit, $joins);
  670. }
  671. /**
  672. * Find all records using a "has_many" relationship (one-to-many)
  673. *
  674. * Parameters: $other_table_name: The name of the other table that contains
  675. * many rows relating to this object's id.
  676. * Returns: An array of ActiveRecord objects. (e.g. Contact objects)
  677. * @todo Document this API
  678. */
  679. private function find_all_has_many($other_table_name, $parameters = null) {
  680. $additional_conditions = null;
  681. # Use any passed-in parameters
  682. if(is_array($parameters)) {
  683. if(@array_key_exists("conditions", $parameters)) {
  684. $additional_conditions = " AND (".$parameters['conditions'].")";
  685. } elseif($parameters[0] != "") {
  686. $additional_conditions = " AND (".$parameters[0].")";
  687. }
  688. if(@array_key_exists("order", $parameters)) {
  689. $order = $parameters['order'];
  690. } elseif($parameters[1] != "") {
  691. $order = $parameters[1];
  692. }
  693. if(@array_key_exists("limit", $parameters)) {
  694. $limit = $parameters['limit'];
  695. } elseif($parameters[2] != "") {
  696. $limit = $parameters[2];
  697. }
  698. if(@array_key_exists("foreign_key", $parameters)) {
  699. $foreign_key = $parameters['foreign_key'];
  700. }
  701. if(@array_key_exists("class_name", $parameters)) {
  702. $other_object_name = $parameters['class_name'];
  703. }
  704. if(@array_key_exists("finder_sql", $parameters)) {
  705. $finder_sql = $parameters['finder_sql'];
  706. }
  707. }
  708. if(!is_null($other_object_name)) {
  709. $other_class_name = Inflector::camelize($other_object_name);
  710. } else {
  711. $other_class_name = Inflector::classify($other_table_name);
  712. }
  713. # Instantiate an object to access find_all
  714. $other_class_object = new $other_class_name();
  715. # If finder_sql is specified just use it instead of determining the association
  716. if(!is_null($finder_sql)) {
  717. $conditions = $finder_sql;
  718. $order = null;
  719. $limit = null;
  720. $joins = null;
  721. } else {
  722. # This class primary key
  723. $this_primary_key = $this->primary_keys[0];
  724. if(!$foreign_key) {
  725. # this should end up being like user_id or account_id but if you specified
  726. # a primaray key other than 'id' it will be like user_field
  727. $foreign_key = Inflector::singularize($this->table_name)."_".$this_primary_key;
  728. }
  729. //print("FORGIN KEY= $foreign_key\n");
  730. $foreign_key_value = $this->$this_primary_key;
  731. //print("FORGIN KEY VALUE = $foreign_key_value\n");
  732. if($other_class_object->attribute_is_string($foreign_key)) {
  733. $conditions = "{$foreign_key} = '{$foreign_key_value}'";
  734. } elseif(is_numeric($foreign_key_value)) {
  735. $conditions = "{$foreign_key} = {$foreign_key_value}";
  736. } else {
  737. #$conditions = "{$foreign_key} = 0";
  738. # no primary key value so just return empty array same as find_all()
  739. return array();
  740. }
  741. $conditions .= $additional_conditions;
  742. }
  743. # Get the list of other_class_name objects
  744. return $other_class_object->find_all($conditions, $order, $limit, $joins);
  745. }
  746. /**
  747. * Find all records using a "has_one" relationship (one-to-one)
  748. * (the foreign key being in the other table)
  749. * Parameters: $other_table_name: The name of the other table that contains
  750. * many rows relating to this object's id.
  751. * Returns: An array of ActiveRecord objects. (e.g. Contact objects)
  752. * @todo Document this API
  753. */
  754. private function find_one_has_one($other_object_name, $parameters = null) {
  755. $additional_conditions = null;
  756. # Use any passed-in parameters
  757. if(is_array($parameters)) {
  758. //echo "<pre>";print_r($parameters);
  759. if(@array_key_exists("conditions", $parameters)) {
  760. $additional_conditions = " AND (".$parameters['conditions'].")";
  761. }
  762. // Removed, WREICHARDT
  763. //elseif($parameters[0] != "") {
  764. // $additional_conditions = " AND (".$parameters[0].")";
  765. //}
  766. if(@array_key_exists("order", $parameters)) {
  767. $order = $parameters['order'];
  768. } elseif($parameters[1] != "") {
  769. $order = $parameters[1];
  770. }
  771. if(@array_key_exists("foreign_key", $parameters)) {
  772. $foreign_key = $parameters['foreign_key'];
  773. }
  774. if(@array_key_exists("class_name", $parameters)) {
  775. $other_object_name = $parameters['class_name'];
  776. }
  777. }
  778. $other_class_name = Inflector::camelize($other_object_name);
  779. # Instantiate an object to access find_all
  780. $other_class_object = new $other_class_name();
  781. # This class primary key
  782. $this_primary_key = $this->primary_keys[0];
  783. if(!$foreign_key){
  784. $foreign_key = Inflector::singularize($this->table_name)."_".$this_primary_key;
  785. }
  786. $foreign_key_value = $this->$this_primary_key;
  787. if($other_class_object->attribute_is_string($foreign_key)) {
  788. $conditions = "{$foreign_key} = '{$foreign_key_value}'";
  789. } elseif(is_numeric($foreign_key_value)) {
  790. $conditions = "{$foreign_key} = {$foreign_key_value}";
  791. } else {
  792. #$conditions = "{$foreign_key} = 0";
  793. return null;
  794. }
  795. $conditions .= $additional_conditions;
  796. # Get the list of other_class_name objects
  797. return $other_class_object->find_first($conditions, $order);
  798. }
  799. /**
  800. * Find all records using a "belongs_to" relationship (one-to-one)
  801. * (the foreign key being in the table itself)
  802. * Parameters: $other_object_name: The singularized version of a table name.
  803. * E.g. If the Contact class belongs_to the
  804. * Customer class, then $other_object_name
  805. * will be "customer".
  806. * @todo Document this API
  807. */
  808. private function find_one_belongs_to($other_object_name, $parameters = null) {
  809. $additional_conditions = null;
  810. # Use any passed-in parameters
  811. if(is_array($parameters)) {
  812. //echo "<pre>";print_r($parameters);
  813. if(@array_key_exists("conditions", $parameters)) {
  814. $additional_conditions = " AND (".$parameters['conditions'].")";
  815. } elseif($parameters[0] != "") {
  816. $additional_conditions = " AND (".$parameters[0].")";
  817. }
  818. if(@array_key_exists("order", $parameters)) {
  819. $order = $parameters['order'];
  820. } elseif($parameters[1] != "") {
  821. $order = $parameters[1];
  822. }
  823. if(@array_key_exists("foreign_key", $parameters)) {
  824. $foreign_key = $parameters['foreign_key'];
  825. }
  826. if(@array_key_exists("class_name", $parameters)) {
  827. $other_object_name = $parameters['class_name'];
  828. }
  829. }
  830. $other_class_name = Inflector::camelize($other_object_name);
  831. # Instantiate an object to access find_all
  832. $other_class_object = new $other_class_name();
  833. # This class primary key
  834. $other_primary_key = $other_class_object->primary_keys[0];
  835. if(!$foreign_key) {
  836. $foreign_key = $other_object_name."_".$other_primary_key;
  837. }
  838. $other_primary_key_value = $this->$foreign_key;
  839. if($other_class_object->attribute_is_string($other_primary_key)) {
  840. $conditions = "{$other_primary_key} = '{$other_primary_key_value}'";
  841. } elseif(is_numeric($other_primary_key_value)) {
  842. $conditions = "{$other_primary_key} = {$other_primary_key_value}";
  843. } else {
  844. #$conditions = "{$other_primary_key} = 0";
  845. return null;
  846. }
  847. $conditions .= $additional_conditions;
  848. # Get the list of other_class_name objects
  849. return $other_class_object->find_first($conditions, $order);
  850. }
  851. /**
  852. * Implement *_all() functions (SQL aggregate functions)
  853. *
  854. * Apply one of the SQL aggregate functions to a column of the
  855. * table associated with this object. The SQL aggregate
  856. * functions are AVG, COUNT, MAX, MIN and SUM. Not all DBMS's
  857. * implement all of these functions.
  858. * @param string $agrregrate_type SQL aggregate function to
  859. * apply, suffixed '_all'. The aggregate function is one of
  860. * the strings in {@link $aggregations}.
  861. * @param string[] $parameters Conditions to apply to the
  862. * aggregate function. If present, must be an array of three
  863. * strings:<ol>
  864. * <li>$parameters[0]: If present, expression to apply
  865. * the aggregate function to. Otherwise, '*' will be used.
  866. * <b>NOTE:</b>SQL uses '*' only for the COUNT() function,
  867. * where it means "including rows with NULL in this column".</li>
  868. * <li>$parameters[1]: argument to WHERE clause</li>
  869. * <li>$parameters[2]: joins??? @todo Document this parameter</li>
  870. * </ol>
  871. * @throws {@link ActiveRecordError}
  872. * @uses query()
  873. * @uses is_error()
  874. */
  875. private function aggregate_all($aggregate_type, $parameters = null) {
  876. $aggregate_type = strtoupper(substr($aggregate_type, 0, -4));
  877. ($parameters[0]) ? $field = $parameters[0] : $field = "*";
  878. $sql = "SELECT {$aggregate_type}({$field}) AS agg_result FROM {$this->table_prefix}{$this->table_name} ";
  879. # Use any passed-in parameters
  880. if(is_array($parameters[1])) {
  881. extract($parameters[1]);
  882. } elseif(!is_null($parameters)) {
  883. $conditions = $parameters[1];
  884. $order = $parameters[2];
  885. $joins = $parameters[3];
  886. }
  887. if(!empty($joins)) $sql .= " $joins ";
  888. if(!empty($conditions)) $sql .= " WHERE $conditions ";
  889. if(!empty($order)) $sql .= " ORDER BY $order ";
  890. # echo "$aggregate_type sql:$sql<br>";
  891. if($this->is_error($rs = $this->query($sql))) {
  892. $this->raise($rs->getMessage());
  893. } else {
  894. $row = $rs->fetchRow();
  895. if($row["agg_result"]) {
  896. return $row["agg_result"];
  897. }
  898. }
  899. return 0;
  900. }
  901. /**
  902. * Returns a the name of the join table that would be used for the two
  903. * tables. The join table name is decided from the alphabetical order
  904. * of the two tables. e.g. "genres_movies" because "g" comes before "m"
  905. *
  906. * Parameters: $first_table, $second_table: the names of two database tables,
  907. * e.g. "movies" and "genres"
  908. * @todo Document this API
  909. */
  910. public function get_join_table_name($first_table, $second_table) {
  911. $tables = array($first_table, $second_table);
  912. @sort($tables);
  913. return $this->table_prefix.@implode("_", $tables);
  914. }
  915. /**
  916. * Test whether this object represents a new record
  917. * @uses $new_record
  918. * @return boolean Whether this object represents a new record
  919. */
  920. function is_new_record() {
  921. return $this->new_record;
  922. }
  923. /**
  924. * get the attributes for a specific column.
  925. * @uses $content_columns
  926. * @todo Document this API
  927. */
  928. function column_for_attribute($attribute) {
  929. if(is_array($this->content_columns)) {
  930. foreach($this->content_columns as $column) {
  931. if($column['name'] == $attribute) {
  932. return $column;
  933. }
  934. }
  935. }
  936. return null;
  937. }
  938. /**
  939. * get the columns data type.
  940. * @uses column_for_attribute()
  941. * @todo Document this API
  942. */
  943. function column_type($attribute) {
  944. $column = $this->column_for_attribute($attribute);
  945. if(isset($column['type'])) {
  946. return $column['type'];
  947. }
  948. return null;
  949. }
  950. /**
  951. * Check whether a column exists in the associated table
  952. *
  953. * When called, {@link $content_columns} lists the columns in
  954. * the table described by this object.
  955. * @param string Name of the column
  956. * @return boolean true=>the column exists; false=>it doesn't
  957. * @uses content_columns
  958. */
  959. function column_attribute_exists($attribute) {
  960. if(is_array($this->content_columns)) {
  961. foreach($this->content_columns as $column) {
  962. if($column['name'] == $attribute) {
  963. return true;
  964. }
  965. }
  966. }
  967. return false;
  968. }
  969. /**
  970. * Get contents of one column of record selected by id and table
  971. *
  972. * When called, {@link $id} identifies one record in the table
  973. * identified by {@link $table}. Fetch from the database the
  974. * contents of column $column of this record.
  975. * @param string Name of column to retrieve
  976. * @uses $db
  977. * @uses column_attribute_exists()
  978. * @throws {@link ActiveRecordError}
  979. * @uses is_error()
  980. */
  981. function send($column) {
  982. if($this->column_attribute_exists($column) && ($conditions = $this->get_primary_key_conditions())) {
  983. # Run the query to grab a specific columns value.
  984. $sql = "SELECT {$column} FROM {$this->table_prefix}{$this->table_name} WHERE {$conditions}";
  985. $this->log_query($sql);
  986. $result = self::$db->queryOne($sql);
  987. if($this->is_error($result)) {
  988. $this->raise($result->getMessage());
  989. }
  990. }
  991. return $result;
  992. }
  993. /**
  994. * Only used if you want to do transactions and your db supports transactions
  995. *
  996. * @uses $db
  997. * @todo Document this API
  998. */
  999. function begin() {
  1000. self::$db->query("BEGIN");
  1001. $this->begin_executed = true;
  1002. }
  1003. /**
  1004. * Only used if you want to do transactions and your db supports transactions
  1005. *
  1006. * @uses $db
  1007. * @todo Document this API
  1008. */
  1009. function commit() {
  1010. self::$db->query("COMMIT");
  1011. $this->begin_executed = false;
  1012. }
  1013. /**
  1014. * Only used if you want to do transactions and your db supports transactions
  1015. *
  1016. * @uses $db
  1017. * @todo Document this API
  1018. */
  1019. function rollback() {
  1020. self::$db->query("ROLLBACK");
  1021. }
  1022. /**
  1023. * Perform an SQL query and return the results
  1024. *
  1025. * @param string $sql SQL for the query command
  1026. * @return $mdb2->query {@link http://pear.php.net/manual/en/package.database.mdb2.intro-query.php}
  1027. * Result set from query
  1028. * @uses $db
  1029. * @uses is_error()
  1030. * @uses log_query()
  1031. * @throws {@link ActiveRecordError}
  1032. */
  1033. function query($sql) {
  1034. //print($sql."\n");
  1035. # Run the query
  1036. $this->log_query($sql);
  1037. $rs =& self::$db->query($sql);
  1038. if ($this->is_error($rs)) {
  1039. if(self::$use_transactions && self::$begin_executed) {
  1040. $this->rollback();
  1041. }
  1042. $this->raise($rs->getMessage());
  1043. }
  1044. return $rs;
  1045. }
  1046. /**
  1047. * Implement find_by_*() and =_* methods
  1048. *
  1049. * Converts a method name beginning 'find_by_' or 'find_all_by_'
  1050. * into a query for rows matching the rest of the method name and
  1051. * the arguments to the function. The part of the method name
  1052. * after '_by' is parsed for columns and logical relationships
  1053. * (AND and OR) to match. For example, the call
  1054. * find_by_fname('Ben')
  1055. * is converted to
  1056. * SELECT * ... WHERE fname='Ben'
  1057. * and the call
  1058. * find_by_fname_and_lname('Ben','Dover')
  1059. * is converted to
  1060. * SELECT * ... WHERE fname='Ben' AND lname='Dover'
  1061. *
  1062. * @uses find_all()
  1063. * @uses find_first()
  1064. */
  1065. private function find_by($method_name, $parameters, $find_type = null) {
  1066. //print("find_by $method_name, $parameters, $find_type");
  1067. if($find_type == "find_or_create") {
  1068. $explode_len = 18;
  1069. } elseif($find_type == "all") {
  1070. $explode_len = 12;
  1071. } else {
  1072. $explode_len = 8;
  1073. }
  1074. $method_name = substr(strtolower($method_name), $explode_len);
  1075. $method_parts = explode("|", str_replace("_and_", "|AND|", $method_name));
  1076. if(count($method_parts)) {
  1077. $options = array();
  1078. $create_fields = array();
  1079. $param_index = 0;
  1080. foreach($method_parts as $part) {
  1081. if($part == "AND") {
  1082. $conditions .= " AND ";
  1083. $param_index++;
  1084. } else {
  1085. $value = $this->attribute_is_string($part) ?
  1086. "'".$parameters[$param_index]."'" :
  1087. $parameters[$param_index];
  1088. $create_fields[$part] = $parameters[$param_index];
  1089. $conditions .= "{$part} = {$value}";
  1090. }
  1091. }
  1092. # If last param exists and is a string set it as the ORDER BY clause
  1093. # or if the last param is an array set it as the $options
  1094. if($last_param = $parameters[++$param_index]) {
  1095. if(is_string($last_param)) {
  1096. $options['order'] = $last_param;
  1097. } elseif(is_array($last_param)) {
  1098. $options = $last_param;
  1099. }
  1100. }
  1101. # Set the conditions
  1102. if($options['conditions'] && $conditions) {
  1103. $options['conditions'] = "(".$options['conditions'].") AND (".$conditions.")";
  1104. } else {
  1105. $options['conditions'] = $conditions;
  1106. }
  1107. # Now do the actual find with condtions from above
  1108. if($find_type == "find_or_create") {
  1109. # see if we can find a record with specified parameters
  1110. $object = $this->find($options);
  1111. if(is_object($object)) {
  1112. # we found a record with the specified parameters so return it
  1113. return $object;
  1114. } elseif(count($create_fields)) {
  1115. # can't find a record with specified parameters so create a new record
  1116. # and return new object
  1117. foreach($create_fields as $field => $value) {
  1118. $this->$field = $value;
  1119. }
  1120. $this->save();
  1121. return $this->find($options);
  1122. }
  1123. } elseif($find_type == "all") {
  1124. return $this->find_all($options);
  1125. } else {
  1126. return $this->find($options);
  1127. }
  1128. }
  1129. }
  1130. /**
  1131. * Builds a sql statement.
  1132. *
  1133. * @uses $rows_per_page_default
  1134. * @uses $rows_per_page
  1135. * @uses $offset
  1136. * @uses $page
  1137. *
  1138. */
  1139. function build_sql($conditions = null, $order = null, $limit = null, $joins = null) {
  1140. $offset = null;
  1141. $per_page = null;
  1142. $select = null;
  1143. # this is if they passed in an associative array to emulate
  1144. # named parameters.
  1145. if(is_array($conditions)) {
  1146. if(@array_key_exists("per_page", $conditions) && !is_numeric($conditions['per_page'])) {
  1147. extract($conditions);
  1148. $per_page = 0;
  1149. } else {
  1150. extract($conditions);
  1151. }
  1152. # If conditions wasn't in the array set it to null
  1153. if(is_array($conditions)) {
  1154. $conditions = null;
  1155. }
  1156. }
  1157. # Test source of SQL for query
  1158. if(stristr($conditions, "SELECT")) {
  1159. # SQL completely specified in argument so use it as is
  1160. $sql = $conditions;
  1161. } else {
  1162. # If select fields not specified just do a SELECT *
  1163. if(is_null($select)) {
  1164. $select = "*";
  1165. }
  1166. # SQL will be built from specifications in argument
  1167. $sql = "SELECT {$select} FROM {$this->table_prefix}{$this->table_name} ";
  1168. # If join specified, include it
  1169. if(!is_null($joins)) {
  1170. $sql .= " $joins ";
  1171. }
  1172. # If conditions specified, include them
  1173. if(!is_null($conditions)) {
  1174. $sql .= "WHERE $conditions ";
  1175. }
  1176. # If ordering specified, include it
  1177. if(!is_null($order)) {
  1178. $sql .= "ORDER BY $order ";
  1179. }
  1180. # Is output to be generated in pages?
  1181. if(is_numeric($limit) || is_numeric($offset) || is_numeric($per_page)) {
  1182. if(is_numeric($limit)) {
  1183. $this->rows_per_page = $limit;
  1184. }
  1185. if(is_numeric($per_page)) {
  1186. $this->rows_per_page = $per_page;
  1187. }
  1188. # Default for rows_per_page:
  1189. if ($this->rows_per_page <= 0) {
  1190. $this->rows_per_page = $this->rows_per_page_default;
  1191. }
  1192. # Only use request's page if you are calling from find_all_with_pagination() and if it is int
  1193. if(strval(intval($_REQUEST['page'])) == $_REQUEST['page']) {
  1194. $this->page = $_REQUEST['page'];
  1195. }
  1196. if($this->page <= 0) {
  1197. $this->page = 1;
  1198. }
  1199. # Set the LIMIT string segment for the SQL
  1200. if(is_null($offset)) {
  1201. $offset = ($this->page - 1) * $this->rows_per_page;
  1202. }
  1203. $sql .= "LIMIT {$this->rows_per_page} OFFSET {$offset}";
  1204. # $sql .= "LIMIT $offset, $this->rows_per_page";
  1205. # Set number of total pages in result set
  1206. if($count = $this->count_all($this->primary_keys[0], $conditions, $joins)) {
  1207. $this->pagination_count = $count;
  1208. $this->pages = (($count % $this->rows_per_page) == 0)
  1209. ? $count / $this->rows_per_page
  1210. : floor($count / $this->rows_per_page) + 1;
  1211. }
  1212. }
  1213. }
  1214. //print("SQL:$sql\n");
  1215. return $sql;
  1216. }
  1217. /**
  1218. * Return rows selected by $conditions
  1219. *
  1220. * If no rows match, an empty array is returned.
  1221. * @param string SQL to use in the query. If
  1222. * $conditions contains "SELECT", then $order, $limit and
  1223. * $joins are ignored and the query is completely specified by
  1224. * $conditions. If $conditions is omitted or does not contain
  1225. * "SELECT", "SELECT * FROM" will be used. If $conditions is
  1226. * specified and does not contain "SELECT", the query will
  1227. * include "WHERE $conditions". If $conditions is null, the
  1228. * entire table is returned.
  1229. * @param string Argument to "ORDER BY" in query.
  1230. * If specified, the query will include
  1231. * "ORDER BY $order". If omitted, no ordering will be
  1232. * applied.
  1233. * @param integer[] Page, rows per page???
  1234. * @param string ???
  1235. * @todo Document the $limit and $joins parameters
  1236. * @uses is_error()
  1237. * @uses $new_record
  1238. * @uses query()
  1239. * @return object[] Array of objects of the same class as this
  1240. * object, one object for each row returned by the query.
  1241. * If the column 'id' was in the results, it is used as the key
  1242. * for that object in the array.
  1243. * @throws {@link ActiveRecordError}
  1244. */
  1245. function find_all($conditions = null, $order = null, $limit = null, $joins = null) {
  1246. //error_log("find_all(".(is_null($conditions)?'null':$conditions)
  1247. // .', ' . (is_null($order)?'null':$order)
  1248. // .', ' . (is_null($limit)?'null':var_export($limit,true))
  1249. // .', ' . (is_null($joins)?'null':$joins).')');
  1250. # Placed the sql building code in a separate function
  1251. $sql = $this->build_sql($conditions, $order, $limit, $joins);
  1252. # echo "ActiveRecord::find_all() - sql: $sql\n<br>";
  1253. # echo "query: $sql\n";
  1254. # error_log("ActiveRecord::find_all -> $sql");
  1255. //print("ActiveRecord::find_all -> $sql\n");
  1256. if($this->is_error($rs = $this->query($sql))) {
  1257. $this->raise($rs->getMessage());
  1258. }
  1259. $objects = array();
  1260. while($row = $rs->fetchRow()) {
  1261. $class_name = $this->get_class_name();
  1262. $object = new $class_name();
  1263. $object->new_record = false;
  1264. $objects_key = null;
  1265. foreach($row as $field => $value) {
  1266. $object->$field = $value;
  1267. if($field == $this->index_on) {
  1268. $objects_key = $value;
  1269. }
  1270. }
  1271. $objects[$objects_key] = $object;
  1272. # If callback is defined in model run it.
  1273. # this will probably hurt performance...
  1274. if(method_exists($object, 'after_find')) {
  1275. $object->after_find();
  1276. }
  1277. unset($object);
  1278. }
  1279. return $objects;
  1280. }
  1281. /**
  1282. * Find row(s) with specified value(s)
  1283. *
  1284. * Find all the rows in the table which match the argument $id.
  1285. * Return zero or more objects of the same class as this
  1286. * class representing the rows that matched the argument.
  1287. * @param mixed[] $id If $id is an array then a query will be
  1288. * generated selecting all of the array values in column "id".
  1289. * If $id is a string containing "=" then the string value of
  1290. * $id will be inserted in a WHERE clause in the query. If $id
  1291. * is a scalar not containing "=" then a query will be generated
  1292. * selecting the first row WHERE id = '$id'.
  1293. * <b>NOTE</b> The column name "id" is used regardless of the
  1294. * value of {@link $primary_keys}. Therefore if you need to
  1295. * select based on some column other than "id", you must pass a
  1296. * string argument ready to insert in the SQL SELECT.
  1297. * @param string $order Argument to "ORDER BY" in query.
  1298. * If specified, the query will include "ORDER BY
  1299. * $order". If omitted, no ordering will be applied.
  1300. * @param integer[] $limit Page, rows per page???
  1301. * @param string $joins ???
  1302. * @todo Document the $limit and $joins parameters
  1303. * @uses find_all()
  1304. * @uses find_first()
  1305. * @return mixed Results of query. If $id was a scalar then the
  1306. * result is an object of the same class as this class and
  1307. * matching $id conditions, or if no row matched the result is
  1308. * null.
  1309. *
  1310. * If $id was an array then the result is an array containing
  1311. * objects of the same class as this class and matching the
  1312. * conditions set by $id. If no rows matched, the array is
  1313. * empty.
  1314. * @throws {@link ActiveRecordError}
  1315. */
  1316. function find($id, $order = null, $limit = null, $joins = null) {
  1317. $find_all = false;
  1318. if(is_array($id)) {
  1319. if($id[0]) {
  1320. # passed in array of numbers array(1,2,4,23)
  1321. $primary_key = $this->primary_keys[0];
  1322. $primary_key_values = $this->attribute_is_string($primary_key) ?
  1323. "'".implode("','", $id)."'" :
  1324. implode(",", $id);
  1325. $options['conditions'] = "{$primary_key} IN({$primary_key_values})";
  1326. $find_all = true;
  1327. } else {
  1328. # passed in an options array
  1329. $options = $id;
  1330. }
  1331. } elseif(stristr($id, "=")) {
  1332. # has an "=" so must be a WHERE clause
  1333. $options['conditions'] = $id;
  1334. } else {
  1335. # find an single record with id = $id
  1336. $primary_key = $this->primary_keys[0];
  1337. $primary_key_value = $this->attribute_is_string($primary_key) ? "'".$id."'" : $id ;
  1338. $options['conditions'] = "{$primary_key} = {$primary_key_value}";
  1339. }
  1340. if(!is_null($order)) $options['order'] = $order;
  1341. if(!is_null($limit)) $options['limit'] = $limit;
  1342. if(!is_null($joins)) $options['joins'] = $joins;
  1343. if($find_all) {
  1344. return $this->find_all($options);
  1345. } else {
  1346. return $this->find_first($options);
  1347. }
  1348. }
  1349. /**
  1350. * Return first row selected by $conditions
  1351. *
  1352. * If no rows match, null is returned.
  1353. * @param string $conditions SQL to use in the query. If
  1354. * $conditions contains "SELECT", then $order, $limit and
  1355. * $joins are ignored and the query is completely specified by
  1356. * $conditions. If $conditions is omitted or does not contain
  1357. * "SELECT", "SELECT * FROM" will be used. If $conditions is
  1358. * specified and does not contain "SELECT", the query will
  1359. * include "WHERE $conditions". If $conditions is null, the
  1360. * entire table is returned.
  1361. * @param string $order Argument to "ORDER BY" in query.
  1362. * If specified, the query will include
  1363. * "ORDER BY $order". If omitted, no ordering will be
  1364. * applied.
  1365. * FIXME This parameter doesn't seem to make sense
  1366. * @param integer[] $limit Page, rows per page??? @todo Document this parameter
  1367. * FIXME This parameter doesn't seem to make sense
  1368. * @param string $joins ??? @todo Document this parameter
  1369. * @uses find_all()
  1370. * @return mixed An object of the same class as this class and
  1371. * matching $conditions, or null if none did.
  1372. * @throws {@link ActiveRecordError}
  1373. */
  1374. function find_first($conditions = null, $order = null, $limit = 1, $joins = null) {
  1375. if(is_array($conditions)) {
  1376. $options = $conditions;
  1377. } else {
  1378. $options['conditions'] = $conditions;
  1379. }
  1380. if(!is_null($order)) $options['order'] = $order;
  1381. if(!is_null($limit)) $options['limit'] = $limit;
  1382. if(!is_null($joins)) $options['joins'] = $joins;
  1383. $result = @current($this->find_all($options));
  1384. return (is_object($result) ? $result : null);
  1385. }
  1386. /**
  1387. * Return all the rows selected by the SQL argument
  1388. *
  1389. * If no rows match, an empty array is returned.
  1390. * @param string $sql SQL to use in the query.
  1391. */
  1392. function find_by_sql($sql) {
  1393. return $this->find_all($sql);
  1394. }
  1395. /**
  1396. * Reloads the attributes of this object from the database.
  1397. * @uses get_primary_key_conditions()
  1398. * @todo Document this API
  1399. */
  1400. function reload($conditions = null) {
  1401. if(is_null($conditions)) {
  1402. $conditions = $this->get_primary_key_conditions();
  1403. }
  1404. $object = $this->find($conditions);
  1405. if(is_object($object)) {
  1406. foreach($object as $key => $value) {
  1407. $this->$key = $value;
  1408. }
  1409. return true;
  1410. }
  1411. return false;
  1412. }
  1413. /**
  1414. * Loads into current object values from the database.
  1415. */
  1416. function load($conditions = null) {
  1417. return $this->reload($conditions);
  1418. }
  1419. /**
  1420. * @todo Document this API. What's going on here? It appears to
  1421. * either create a row with all empty values, or it tries
  1422. * to recurse once for each attribute in $attributes.
  1423. * Creates an object, instantly saves it as a record (if the validation permits it).
  1424. * If the save fails under validations it returns false and $errors array gets set.
  1425. */
  1426. function create($attributes, $dont_validate = false) {
  1427. $class_name = $this->get_class_name();
  1428. $object = new $class_name();
  1429. $result = $object->save($attributes, $dont_validate);
  1430. return ($result ? $object : false);
  1431. }
  1432. /**
  1433. * Finds the record from the passed id, instantly saves it with the passed attributes
  1434. * (if the validation permits it). Returns true on success and false on error.
  1435. * @todo Document this API
  1436. */
  1437. function update($id, $attributes, $dont_validate = false) {
  1438. if(is_array($id)) {
  1439. foreach($id as $update_id) {
  1440. $this->update($update_id, $attributes[$update_id], $dont_validate);
  1441. }
  1442. } else {
  1443. $object = $this->find($id);
  1444. return $object->save($attributes, $dont_validate);
  1445. }
  1446. }
  1447. /**
  1448. * Updates all records with the SET-part of an SQL update statement in updates and
  1449. * returns an integer with the number of rows updates. A subset of the records can
  1450. * be selected by specifying conditions.
  1451. * Example:
  1452. * $model->update_all("category = 'cooldude', approved = 1", "author = 'John'");
  1453. * @uses is_error()
  1454. * @uses query()
  1455. * @throws {@link ActiveRecordError}
  1456. * @todo Document this API
  1457. */
  1458. function update_all($updates, $conditions = null) {
  1459. $sql = "UPDATE {$this->table_prefix}{$this->table_name} SET {$updates} WHERE {$conditions}";
  1460. $result = $this->query($sql);
  1461. if ($this->is_error($result)) {
  1462. $this->raise($result->getMessage());
  1463. } else {
  1464. return true;
  1465. }
  1466. }
  1467. /**
  1468. * Save without valdiating anything.
  1469. * @todo Document this API
  1470. */
  1471. function save_without_validation($attributes = null) {
  1472. return $this->save($attributes, true);
  1473. }
  1474. /**
  1475. * Create or update a row in the table with specified attributes
  1476. *
  1477. * @param string[] $attributes List of name => value pairs giving
  1478. * name and value of attributes to set.
  1479. * @param boolean $dont_validate true => Don't call validation
  1480. * routines before saving the row. If false or omitted, all
  1481. * applicable validation routines are called.
  1482. * @uses add_record_or_update_record()
  1483. * @uses update_attributes()
  1484. * @uses valid()
  1485. * @return boolean
  1486. * <ul>
  1487. * <li>true => row was updated or inserted successfully</li>
  1488. * <li>false => insert failed</li>
  1489. * </ul>
  1490. */
  1491. function save($attributes = null, $dont_validate = false) {
  1492. //error_log("ActiveRecord::save() \$attributes="
  1493. // . var_export($attributes,true));
  1494. $this->update_attributes($attributes);
  1495. if($dont_validate || $this->valid()) {
  1496. return $this->add_record_or_update_record();
  1497. } else {
  1498. return false;
  1499. }
  1500. }
  1501. /**
  1502. * Create or update a row in the table
  1503. *
  1504. * If this object represents a new row in the table, insert it.
  1505. * Otherwise, update the exiting row. before_?() and after_?()
  1506. * routines will be called depending on whether the row is new.
  1507. * @uses add_record()
  1508. * @uses after_create()
  1509. * @uses after_update()
  1510. * @uses before_create()
  1511. * @uses before_save()
  1512. * @uses $new_record
  1513. * @uses update_record()
  1514. * @return boolean
  1515. * <ul>
  1516. * <li>true => row was updated or inserted successfully</li>
  1517. * <li>false => insert failed</li>
  1518. * </ul>
  1519. */
  1520. private function add_record_or_update_record() {
  1521. //error_log('add_record_or_update_record()');
  1522. $this->before_save();
  1523. if($this->new_record) {
  1524. $this->before_create();
  1525. $result = $this->add_record();
  1526. $this->after_create();
  1527. } else {
  1528. $this->before_update();
  1529. $result = $this->update_record();
  1530. $this->after_update();
  1531. }
  1532. $this->after_save();
  1533. return $result;
  1534. }
  1535. /**
  1536. * Insert a new row in the table associated with this object
  1537. *
  1538. * Build an SQL INSERT statement getting the table name from
  1539. * {@link $table_name}, the column names from {@link
  1540. * $content_columns} and the values from object variables.
  1541. * Send the insert to the RDBMS.
  1542. * @uses $auto_save_habtm
  1543. * @uses add_habtm_records()
  1544. * @uses before_create()
  1545. * @uses get_insert_id()
  1546. * @uses is_error()
  1547. * @uses query()
  1548. * @uses get_inserts()
  1549. * @uses raise()
  1550. * @uses $table_name
  1551. * @return boolean
  1552. * <ul>
  1553. * <li>true => row was inserted successfully</li>
  1554. * <li>false => insert failed</li>
  1555. * </ul>
  1556. * @throws {@link ActiveRecordError}
  1557. */
  1558. private function add_record() {
  1559. self::$db->loadModule('Extended', null, true);
  1560. # $primary_key_value may either be a quoted integer or php null
  1561. $primary_key_value = self::$db->getBeforeID("{$this->table_prefix}{$this->table_name}", $this->primary_keys[0]);
  1562. if($this->is_error($primary_key_value)) {
  1563. $this->raise($primary_key_value->getMessage());
  1564. }
  1565. $this->update_composite_attributes();
  1566. $attributes = $this->get_inserts();
  1567. $fields = @implode(', ', array_keys($attributes));
  1568. $values = @implode(', ', array_values($attributes));
  1569. $sql = "INSERT INTO {$this->table_prefix}{$this->table_name} ({$fields}) VALUES ({$values})";
  1570. //echo "add_record: SQL: $sql<br>";
  1571. //error_log("add_record: SQL: $sql");
  1572. $result = $this->query($sql);
  1573. if($this->is_error($result)) {
  1574. $this->raise($results->getMessage());
  1575. } else {
  1576. $habtm_result = true;
  1577. $primary_key = $this->primary_keys[0];
  1578. # $id is now equivalent to the value in the id field that was inserted
  1579. $primary_key_value = self::$db->getAfterID($primary_key_value, "{$this->table_prefix}{$this->table_name}", $this->primary_keys[0]);
  1580. if($this->is_error($primary_key_value)) {
  1581. $this->raise($primary_key_value->getMessage());
  1582. }
  1583. $this->$primary_key = $primary_key_value;
  1584. if($primary_key_value != '') {
  1585. if($this->auto_save_habtm) {
  1586. $habtm_result = $this->add_habtm_records($primary_key_value);
  1587. }
  1588. $this->save_associations();
  1589. }
  1590. return ($result && $habtm_result);
  1591. }
  1592. }
  1593. /**
  1594. * Update the row in the table described by this object
  1595. *
  1596. * The primary key attributes must exist and have appropriate
  1597. * non-null values. If a column is listed in {@link
  1598. * $content_columns} but no attribute of that name exists, the
  1599. * column will be set to the null string ''.
  1600. * @todo Describe habtm automatic update
  1601. * @uses is_error()
  1602. * @uses get_updates_sql()
  1603. * @uses get_primary_key_conditions()
  1604. * @uses query()
  1605. * @uses raise()
  1606. * @uses update_habtm_records()
  1607. * @return boolean
  1608. * <ul>
  1609. * <li>true => row was updated successfully</li>
  1610. * <li>false => update failed</li>
  1611. * </ul>
  1612. * @throws {@link ActiveRecordError}
  1613. */
  1614. private function update_record() {
  1615. //error_log('update_record()');
  1616. $this->update_composite_attributes();
  1617. $updates = $this->get_updates_sql();
  1618. $conditions = $this->get_primary_key_conditions();
  1619. $sql = "UPDATE {$this->table_prefix}{$this->table_name} SET {$updates} WHERE {$conditions}";
  1620. //echo "update_record:$sql<br>";
  1621. //error_log("update_record: SQL: $sql");
  1622. $result = $this->query($sql);
  1623. if($this->is_error($result)) {
  1624. $this->raise($results->getMessage());
  1625. } else {
  1626. $habtm_result = true;
  1627. $primary_key = $this->primary_keys[0];
  1628. $primary_key_value = $this->$primary_key;
  1629. if($primary_key_value > 0) {
  1630. if($this->auto_save_habtm) {
  1631. $habtm_result = $this->update_habtm_records($primary_key_value);
  1632. }
  1633. $this->save_associations();
  1634. }
  1635. return ($result && $habtm_result);
  1636. }
  1637. }
  1638. /**
  1639. * Loads the model values into composite object
  1640. * @todo Document this API
  1641. */
  1642. private function get_composite_object($name) {
  1643. $composite_object = null;
  1644. $composite_attributes = array();
  1645. if(is_array($this->composed_of)) {
  1646. if(array_key_exists($name, $this->composed_of)) {
  1647. $class_name = Inflector::classify(($this->composed_of[$name]['class_name'] ?
  1648. $this->composed_of[$name]['class_name'] : $name));
  1649. $mappings = $this->composed_of[$name]['mapping'];
  1650. if(is_array($mappings)) {
  1651. foreach($mappings as $database_name => $composite_name) {
  1652. $composite_attributes[$composite_name] = $this->$database_name;
  1653. }
  1654. }
  1655. }
  1656. } elseif($this->composed_of == $name) {
  1657. $class_name = $name;
  1658. $composite_attributes[$name] = $this->$name;
  1659. }
  1660. if(class_exists($class_name)) {
  1661. $composite_object = new $class_name;
  1662. if($composite_object->auto_map_attributes !== false) {
  1663. //echo "auto_map_attributes<br>";
  1664. foreach($composite_attributes as $name => $value) {
  1665. $composite_object->$name = $value;
  1666. }
  1667. }
  1668. if(method_exists($composite_object, '__construct')) {
  1669. //echo "calling constructor<br>";
  1670. $composite_object->__construct($composite_attributes);
  1671. }
  1672. }
  1673. return $composite_object;
  1674. }
  1675. /**
  1676. * returns the association type if defined in child class or null
  1677. * @todo Document this API
  1678. * @uses $belongs_to
  1679. * @uses $has_and_belongs_to_many
  1680. * @uses $has_many
  1681. * @uses $has_one
  1682. * @return mixed Association type, one of the following:
  1683. * <ul>
  1684. * <li>"belongs_to"</li>
  1685. * <li>"has_and_belongs_to_many"</li>
  1686. * <li>"has_many"</li>
  1687. * <li>"has_one"</li>
  1688. * </ul>
  1689. * if an association exists, or null if no association
  1690. */
  1691. function get_association_type($association_name) {
  1692. $type = null;
  1693. if(is_string($this->has_many)) {
  1694. if(preg_match("/\b$association_name\b/", $this->has_many)) {
  1695. $type = "has_many";
  1696. }
  1697. } elseif(is_array($this->has_many)) {
  1698. if(array_key_exists($association_name, $this->has_many)) {
  1699. $type = "has_many";
  1700. }
  1701. }
  1702. if(is_string($this->has_one)) {
  1703. if(preg_match("/\b$association_name\b/", $this->has_one)) {
  1704. $type = "has_one";
  1705. }
  1706. } elseif(is_array($this->has_one)) {
  1707. if(array_key_exists($association_name, $this->has_one)) {
  1708. $type = "has_one";
  1709. }
  1710. }
  1711. if(is_string($this->belongs_to)) {
  1712. if(preg_match("/\b$association_name\b/", $this->belongs_to)) {
  1713. $type = "belongs_to";
  1714. }
  1715. } elseif(is_array($this->belongs_to)) {
  1716. if(array_key_exists($association_name, $this->belongs_to)) {
  1717. $type = "belongs_to";
  1718. }
  1719. }
  1720. if(is_string($this->has_and_belongs_to_many)) {
  1721. if(preg_match("/\b$association_name\b/", $this->has_and_belongs_to_many)) {
  1722. $type = "has_and_belongs_to_many";
  1723. }
  1724. } elseif(is_array($this->has_and_belongs_to_many)) {
  1725. if(array_key_exists($association_name, $this->has_and_belongs_to_many)) {
  1726. $type = "has_and_belongs_to_many";
  1727. }
  1728. }
  1729. return $type;
  1730. }
  1731. /**
  1732. * Saves any associations objects assigned to this instance
  1733. * @uses $auto_save_associations
  1734. * @todo Document this API
  1735. */
  1736. private function save_associations() {
  1737. if(count($this->save_associations) && $this->auto_save_associations) {
  1738. foreach(array_keys($this->save_associations) as $type) {
  1739. if(count($this->save_associations[$type])) {
  1740. foreach($this->save_associations[$type] as $object_or_array) {
  1741. if(is_object($object_or_array)) {
  1742. $this->save_association($object_or_array, $type);
  1743. } elseif(is_array($object_or_array)) {
  1744. foreach($object_or_array as $object) {
  1745. $this->save_association($object, $type);
  1746. }
  1747. }
  1748. }
  1749. }
  1750. }
  1751. }
  1752. }
  1753. /**
  1754. * save the association to the database
  1755. * @todo Document this API
  1756. */
  1757. private function save_association($object, $type) {
  1758. if(is_object($object) && get_parent_class($object) == __CLASS__ && $type) {
  1759. //echo get_class($object)." - type:$type<br>";
  1760. switch($type) {
  1761. case "has_many":
  1762. case "has_one":
  1763. $primary_key = $this->primary_keys[0];
  1764. $foreign_key = Inflector::singularize($this->table_name)."_".$primary_key;
  1765. $object->$foreign_key = $this->$primary_key;
  1766. //echo "fk:$foreign_key = ".$this->$primary_key."<br>";
  1767. break;
  1768. }
  1769. $object->save();
  1770. }
  1771. }
  1772. /**
  1773. * Deletes the record with the given $id or if you have done a
  1774. * $model = $model->find($id), then $model->delete() it will delete
  1775. * the record it just loaded from the find() without passing anything
  1776. * to delete(). If an array of ids is provided, all ids in array are deleted.
  1777. * @uses $errors
  1778. * @todo Document this API
  1779. */
  1780. function delete($id = null) {
  1781. $deleted_ids = array();
  1782. $primary_key_value = null;
  1783. $primary_key = $this->primary_keys[0];
  1784. if(is_null($id)) {
  1785. # Primary key's where clause from already loaded values
  1786. $conditions = $this->get_primary_key_conditions();
  1787. $deleted_ids[] = $this->$primary_key;
  1788. } elseif(!is_array($id)) {
  1789. $deleted_ids[] = $id;
  1790. $id = $this->attribute_is_string($primary_key) ? "'".$id."'" : $id;
  1791. $conditions = "{$primary_key} = {$id}";
  1792. } elseif(is_array($id)) {
  1793. $deleted_ids = $id;
  1794. $ids = ($this->attribute_is_string($primary_key)) ?
  1795. "'".implode("','", $id)."'" :
  1796. implode(',', $id);
  1797. $conditions = "{$primary_key} IN ({$ids})";
  1798. }
  1799. if(is_null($conditions)) {
  1800. $this->errors[] = "No conditions specified to delete on.";
  1801. return false;
  1802. }
  1803. $this->before_delete();
  1804. if($result = $this->delete_all($conditions)) {
  1805. foreach($deleted_ids as $id) {
  1806. if($this->auto_delete_habtm && $id != '') {
  1807. if(is_string($this->has_and_belongs_to_many)) {
  1808. $habtms = explode(",", $this->has_and_belongs_to_many);
  1809. foreach($habtms as $other_table_name) {
  1810. $this->delete_all_habtm_records(trim($other_table_name), $id);
  1811. }
  1812. } elseif(is_array($this->has_and_belongs_to_many)) {
  1813. foreach($this->has_and_belongs_to_many as $other_table_name => $values) {
  1814. $this->delete_all_habtm_records($other_table_name, $id);
  1815. }
  1816. }
  1817. }
  1818. }
  1819. $this->after_delete();
  1820. }
  1821. return $result;
  1822. }
  1823. /**
  1824. * Delete from table all rows that match argument
  1825. *
  1826. * Delete the row(s), if any, matching the argument.
  1827. * @param string $conditions SQL argument to "WHERE" describing
  1828. * the rows to delete
  1829. * @return boolean
  1830. * <ul>
  1831. * <li>true => One or more rows were deleted</li>
  1832. * <li>false => $conditions was omitted</li>
  1833. * </ul>
  1834. * @uses is_error()
  1835. * @uses $new_record
  1836. * @uses $errors
  1837. * @uses query()
  1838. * @throws {@link ActiveRecordError}
  1839. */
  1840. function delete_all($conditions = null) {
  1841. if(is_null($conditions)) {
  1842. $this->errors[] = "No conditions specified to delete on.";
  1843. return false;
  1844. }
  1845. # Delete the record(s)
  1846. //print("DELETE FROM {$this->table_prefix}{$this->table_name} WHERE {$conditions}<p>");
  1847. if($this->is_error($rs = $this->query("DELETE FROM {$this->table_prefix}{$this->table_name} WHERE {$conditions}"))) {
  1848. $this->raise($rs->getMessage());
  1849. }
  1850. $this->new_record = true;
  1851. return true;
  1852. }
  1853. /**
  1854. * @uses $has_and_belongs_to_many
  1855. * @todo Document this API
  1856. */
  1857. private function set_habtm_attributes($attributes) {
  1858. if(is_array($attributes)) {
  1859. $this->habtm_attributes = array();
  1860. foreach($attributes as $key => $habtm_array) {
  1861. if(is_array($habtm_array)) {
  1862. if(is_string($this->has_and_belongs_to_many)) {
  1863. if(preg_match("/\b$key\b/", $this->has_and_belongs_to_many)) {
  1864. $this->habtm_attributes[$key] = $habtm_array;
  1865. }
  1866. } elseif(is_array($this->has_and_belongs_to_many)) {
  1867. if(array_key_exists($key, $this->has_and_belongs_to_many)) {
  1868. $this->habtm_attributes[$key] = $habtm_array;
  1869. }
  1870. }
  1871. }
  1872. }
  1873. }
  1874. }
  1875. /**
  1876. *
  1877. * @todo Document this API
  1878. */
  1879. private function update_habtm_records($this_foreign_value) {
  1880. //print("CALLING update_habtm_records<p>");
  1881. return $this->add_habtm_records($this_foreign_value);
  1882. }
  1883. /**
  1884. *
  1885. * @uses is_error()
  1886. * @uses query()
  1887. * @throws {@link ActiveRecordError}
  1888. * @todo Document this API
  1889. */
  1890. private function add_habtm_records($this_foreign_value) {
  1891. if($this_foreign_value > 0 && count($this->habtm_attributes) > 0) {
  1892. if($this->delete_habtm_records($this_foreign_value)) {
  1893. reset($this->habtm_attributes);
  1894. foreach($this->habtm_attributes as $other_table_name => $other_foreign_values) {
  1895. $table_name = $this->get_join_table_name($this->table_name,$other_table_name);
  1896. $other_foreign_key = Inflector::singularize($other_table_name)."_id";
  1897. $this_foreign_key = Inflector::singularize($this->table_name)."_id";
  1898. foreach($other_foreign_values as $other_foreign_value) {
  1899. unset($attributes);
  1900. $attributes[$this_foreign_key] = $this_foreign_value;
  1901. $attributes[$other_foreign_key] = $other_foreign_value;
  1902. $attributes = $this->quoted_attributes($attributes);
  1903. $fields = @implode(', ', array_keys($attributes));
  1904. $values = @implode(', ', array_values($attributes));
  1905. $sql = "INSERT INTO $table_name ($fields) VALUES ($values)";
  1906. //echo "add_habtm_records: SQL: $sql<br>";
  1907. $result = $this->query($sql);
  1908. if ($this->is_error($result)) {
  1909. $this->raise($result->getMessage());
  1910. }
  1911. }
  1912. }
  1913. }
  1914. }
  1915. return true;
  1916. }
  1917. /**
  1918. *
  1919. * @uses is_error()
  1920. * @uses query()
  1921. * @throws {@link ActiveRecordError}
  1922. * @todo Document this API
  1923. */
  1924. private function delete_habtm_records($this_foreign_value) {
  1925. if($this_foreign_value > 0 && count($this->habtm_attributes) > 0) {
  1926. reset($this->habtm_attributes);
  1927. foreach($this->habtm_attributes as $other_table_name => $values) {
  1928. $this->delete_all_habtm_records($other_table_name, $this_foreign_value);
  1929. }
  1930. }
  1931. return true;
  1932. }
  1933. private function delete_all_habtm_records($other_table_name, $this_foreign_value) {
  1934. if($other_table_name && $this_foreign_value > 0) {
  1935. $habtm_table_name = $this->get_join_table_name($this->table_name,$other_table_name);
  1936. $this_foreign_key = Inflector::singularize($this->table_name)."_id";
  1937. $sql = "DELETE FROM {$habtm_table_name} WHERE {$this_foreign_key} = {$this_foreign_value}";
  1938. //echo "delete_all_habtm_records: SQL: $sql<br>";
  1939. $result = $this->query($sql);
  1940. if($this->is_error($result)) {
  1941. $this->raise($result->getMessage());
  1942. }
  1943. }
  1944. }
  1945. /**
  1946. * Apply automatic timestamp updates
  1947. *
  1948. * If automatic timestamps are in effect (as indicated by
  1949. * {@link $auto_timestamps} == true) and the column named in the
  1950. * $field argument is of type "timestamp" and matches one of the
  1951. * names in {@link auto_create_timestamps} or {@link
  1952. * auto_update_timestamps}(as selected by {@link $new_record}),
  1953. * then return the current date and time as a string formatted
  1954. * to insert in the database. Otherwise return $value.
  1955. * @uses $new_record
  1956. * @uses $content_columns
  1957. * @uses $auto_timestamps
  1958. * @uses $auto_create_timestamps
  1959. * @uses $auto_update_timestamps
  1960. * @param string $field Name of a column in the table
  1961. * @param mixed $value Value to return if $field is not an
  1962. * automatic timestamp column
  1963. * @return mixed Current date and time or $value
  1964. */
  1965. private function check_datetime($field, $value) {
  1966. if($this->auto_timestamps) {
  1967. if(is_array($this->content_columns)) {
  1968. foreach($this->content_columns as $field_info) {
  1969. if(($field_info['name'] == $field) && stristr($field_info['type'], "date")) {
  1970. $format = ($field_info['type'] == "date") ? $this->date_format : "{$this->date_format} {$this->time_format}";
  1971. if($this->new_record) {
  1972. if(in_array($field, $this->auto_create_timestamps)) {
  1973. return date($format);
  1974. } elseif($this->preserve_null_dates && is_null($value) && !stristr($field_info['flags'], "not_null")) {
  1975. return null;
  1976. }
  1977. } elseif(!$this->new_record) {
  1978. if(in_array($field, $this->auto_update_timestamps)) {
  1979. return date($format);
  1980. } elseif($this->preserve_null_dates && is_null($value) && !stristr($field_info['flags'], "not_null")) {
  1981. return null;
  1982. }
  1983. }
  1984. }
  1985. }
  1986. }
  1987. }
  1988. return $value;
  1989. }
  1990. /**
  1991. * Update object attributes from list in argument
  1992. *
  1993. * The elements of $attributes are parsed and assigned to
  1994. * attributes of the ActiveRecord object. Date/time fields are
  1995. * treated according to the
  1996. * {@tutorial PHPonTrax/naming.pkg#naming.naming_forms}.
  1997. * @param string[] $attributes List of name => value pairs giving
  1998. * name and value of attributes to set.
  1999. * @uses $auto_save_associations
  2000. * @todo Figure out and document how datetime fields work
  2001. */
  2002. function update_attributes($attributes) {
  2003. //error_log('update_attributes()');
  2004. if(is_array($attributes)) {
  2005. $datetime_fields = array();
  2006. // Test each attribute to be updated
  2007. // and process according to its type
  2008. foreach($attributes as $field => $value) {
  2009. # datetime / date parts check
  2010. if(preg_match('/^\w+\(.*i\)$/i', $field)) {
  2011. // The name of this attribute ends in "(?i)"
  2012. // indicating that it's part of a date or time
  2013. $datetime_field = substr($field, 0, strpos($field, "("));
  2014. if(!in_array($datetime_field, $datetime_fields)) {
  2015. $datetime_fields[] = $datetime_field;
  2016. }
  2017. # this elseif checks if first its an object if its parent is ActiveRecord
  2018. } elseif(is_object($value) && get_parent_class($value) == __CLASS__ && $this->auto_save_associations) {
  2019. if($association_type = $this->get_association_type($field)) {
  2020. $this->save_associations[$association_type][] = $value;
  2021. if($association_type == "belongs_to") {
  2022. $primary_key = $value->primary_keys[0];
  2023. $foreign_key = Inflector::singularize($value->table_name)."_".$primary_key;
  2024. $this->$foreign_key = $value->$primary_key;
  2025. }
  2026. }
  2027. # this elseif checks if its an array of objects and if its parent is ActiveRecord
  2028. } elseif(is_array($value) && $this->auto_save_associations) {
  2029. if($association_type = $this->get_association_type($field)) {
  2030. $this->save_associations[$association_type][] = $value;
  2031. }
  2032. } else {
  2033. // Just a simple attribute, copy it
  2034. $this->$field = $value;
  2035. }
  2036. }
  2037. // If any date/time fields were found, assign the
  2038. // accumulated values to corresponding attributes
  2039. if(count($datetime_fields)) {
  2040. foreach($datetime_fields as $datetime_field) {
  2041. $datetime_format = '';
  2042. $datetime_value = '';
  2043. if($attributes[$datetime_field."(1i)"]
  2044. && $attributes[$datetime_field."(2i)"]
  2045. && $attributes[$datetime_field."(3i)"]) {
  2046. $datetime_value = $attributes[$datetime_field."(1i)"]
  2047. . "-" . $attributes[$datetime_field."(2i)"]
  2048. . "-" . $attributes[$datetime_field."(3i)"];
  2049. $datetime_format = $this->date_format;
  2050. }
  2051. $datetime_value .= " ";
  2052. if($attributes[$datetime_field."(4i)"]
  2053. && $attributes[$datetime_field."(5i)"]) {
  2054. $datetime_value .= $attributes[$datetime_field."(4i)"]
  2055. . ":" . $attributes[$datetime_field."(5i)"];
  2056. $datetime_format .= " ".$this->time_format;
  2057. }
  2058. if($datetime_value = trim($datetime_value)) {
  2059. $datetime_value = date($datetime_format, strtotime($datetime_value));
  2060. //error_log("($field) $datetime_field = $datetime_value");
  2061. $this->$datetime_field = $datetime_value;
  2062. }
  2063. }
  2064. }
  2065. $this->set_habtm_attributes($attributes);
  2066. }
  2067. }
  2068. /**
  2069. * If a composite object was specified via $composed_of, then its values
  2070. * mapped to the model will overwrite the models values.
  2071. *
  2072. */
  2073. function update_composite_attributes() {
  2074. if(is_array($this->composed_of)) {
  2075. foreach($this->composed_of as $name => $options) {
  2076. $composite_object = $this->$name;
  2077. if(is_array($options) && is_object($composite_object)) {
  2078. if(is_array($options['mapping'])) {
  2079. foreach($options['mapping'] as $database_name => $composite_name) {
  2080. $this->$database_name = $composite_object->$composite_name;
  2081. }
  2082. }
  2083. }
  2084. }
  2085. }
  2086. }
  2087. /**
  2088. * Return pairs of column-name:column-value
  2089. *
  2090. * Return the contents of the object as an array of elements
  2091. * where the key is the column name and the value is the column
  2092. * value. Relies on a previous call to
  2093. * {@link set_content_columns()} for information about the format
  2094. * of a row in the table.
  2095. * @uses $content_columns
  2096. * @see set_content_columns
  2097. * @see quoted_attributes()
  2098. */
  2099. function get_attributes() {
  2100. $attributes = array();
  2101. if(is_array($this->content_columns)) {
  2102. foreach($this->content_columns as $column) {
  2103. //echo "attribute: $info[name] -> {$this->$info[name]}<br>";
  2104. $attributes[$column['name']] = $this->$column['name'];
  2105. }
  2106. }
  2107. return $attributes;
  2108. }
  2109. /**
  2110. * Return pairs of column-name:quoted-column-value
  2111. *
  2112. * Return pairs of column-name:quoted-column-value where the key
  2113. * is the column name and the value is the column value with
  2114. * automatic timestamp updating applied and characters special to
  2115. * SQL quoted.
  2116. *
  2117. * If $attributes is null or omitted, return all columns as
  2118. * currently stored in {@link content_columns()}. Otherwise,
  2119. * return the name:value pairs in $attributes.
  2120. * @param string[] $attributes Name:value pairs to return.
  2121. * If null or omitted, return the column names and values
  2122. * of the object as stored in $content_columns.
  2123. * @return string[]
  2124. * @uses get_attributes()
  2125. * @see set_content_columns()
  2126. */
  2127. function quoted_attributes($attributes = null) {
  2128. if(is_null($attributes)) {
  2129. $attributes = $this->get_attributes();
  2130. }
  2131. $return = array();
  2132. foreach($attributes as $name => $value) {
  2133. $return[$name] = $this->quote_attribute($name, $value);
  2134. }
  2135. return $return;
  2136. }
  2137. /**
  2138. * Quotes a single attribute for use in an sql statement.
  2139. *
  2140. */
  2141. function quote_attribute($attribute, $value = null) {
  2142. $value = is_null($value) ? $this->$attribute : $value;
  2143. $value = $this->check_datetime($attribute, $value);
  2144. $column = $this->column_for_attribute($attribute);
  2145. if(isset($column['mdb2type'])) {
  2146. $type = $column['mdb2type'];
  2147. } else {
  2148. $type = $this->attribute_is_string($attribute, $column) ?
  2149. "text" : is_float($attribute) ? "float" : "integer";
  2150. }
  2151. $value = self::$db->quote($value, $type);
  2152. if($value === 'NULL' && stristr($column['flags'], "not_null")) {
  2153. $value = "''";
  2154. }
  2155. return $value;
  2156. }
  2157. /**
  2158. * Escapes a string for use in an sql statement.
  2159. *
  2160. */
  2161. function escape($string) {
  2162. return(self::$db->escape($string));
  2163. }
  2164. /**
  2165. * Return column values for SQL insert statement
  2166. *
  2167. * Return an array containing the column names and values of this
  2168. * object, filtering out the primary keys, which are not set.
  2169. *
  2170. * @uses $primary_keys
  2171. * @uses quoted_attributes()
  2172. */
  2173. function get_inserts() {
  2174. $attributes = $this->quoted_attributes();
  2175. $inserts = array();
  2176. foreach($attributes as $key => $value) {
  2177. if(!in_array($key, $this->primary_keys) || ($value != "''" && isset($value))) {
  2178. $inserts[$key] = $value;
  2179. }
  2180. }
  2181. return $inserts;
  2182. }
  2183. /**
  2184. * Return argument for a "WHERE" clause specifying this row
  2185. *
  2186. * Returns a string which specifies the column(s) and value(s)
  2187. * which describe the primary key of this row of the associated
  2188. * table. The primary key must be one or more attributes of the
  2189. * object and must be listed in {@link $content_columns} as
  2190. * columns in the row.
  2191. *
  2192. * Example: if $primary_keys = array("id", "ssn") and column "id"
  2193. * has value "5" and column "ssn" has value "123-45-6789" then
  2194. * the string "id = 5 AND ssn = '123-45-6789'" would be returned.
  2195. * @uses $primary_keys
  2196. * @uses quoted_attributes()
  2197. * @return string Column name = 'value' [ AND name = 'value']...
  2198. */
  2199. function get_primary_key_conditions($operator = "=") {
  2200. $conditions = null;
  2201. $attributes = $this->quoted_attributes();
  2202. if(count($attributes) > 0) {
  2203. $conditions = array();
  2204. # run through our fields and join them with their values
  2205. foreach($attributes as $key => $value) {
  2206. if(in_array($key, $this->primary_keys) && isset($value) && $value != "''") {
  2207. $conditions[] = "{$key} {$operator} {$value}";
  2208. }
  2209. }
  2210. $conditions = implode(" AND ", $conditions);
  2211. }
  2212. return $conditions;
  2213. }
  2214. /**
  2215. * Return column values of object formatted for SQL update statement
  2216. *
  2217. * Return a string containing the column names and values of this
  2218. * object in a format ready to be inserted in a SQL UPDATE
  2219. * statement. Automatic update has been applied to timestamps if
  2220. * enabled and characters special to SQL have been quoted.
  2221. * @uses quoted_attributes()
  2222. * @return string Column name = 'value', ... for all attributes
  2223. */
  2224. function get_updates_sql() {
  2225. $updates = null;
  2226. $attributes = $this->quoted_attributes();
  2227. if(count($attributes) > 0) {
  2228. $updates = array();
  2229. # run through our fields and join them with their values
  2230. foreach($attributes as $key => $value) {
  2231. if($key && isset($value) && !in_array($key, $this->primary_keys)) {
  2232. $updates[] = "$key = $value";
  2233. }
  2234. }
  2235. $updates = implode(", ", $updates);
  2236. }
  2237. return $updates;
  2238. }
  2239. /**
  2240. * Set {@link $table_name} from the class name of this object
  2241. *
  2242. * By convention, the name of the database table represented by
  2243. * this object is derived from the name of the class.
  2244. * @uses Inflector::tableize()
  2245. */
  2246. function set_table_name_using_class_name() {
  2247. if(!$this->table_name) {
  2248. $class_name = $this->get_class_name();
  2249. $this->table_name = Inflector::tableize($class_name);
  2250. }
  2251. }
  2252. /**
  2253. * Get class name of child object
  2254. *
  2255. * this will return the manually set name or get_class($this)
  2256. * @return string child class name
  2257. */
  2258. private function get_class_name() {
  2259. return !is_null($this->class_name) ? $this->class_name : get_class($this);
  2260. }
  2261. /**
  2262. * Populate object with information about the table it represents
  2263. *
  2264. * Call {@link
  2265. * http://pear.php.net/manual/en/package.database.db.db-common.tableinfo.php
  2266. * DB_common::tableInfo()} to get a description of the table and
  2267. * store it in {@link $content_columns}. Add a more human
  2268. * friendly name to the element for each column.
  2269. * @uses $db
  2270. * @uses $content_columns
  2271. * @uses Inflector::humanize()
  2272. * @see __set()
  2273. * @param string $table_name Name of table to get information about
  2274. */
  2275. function set_content_columns($table_name) {
  2276. if(!is_null($this->table_prefix)) {
  2277. $table_name = $this->table_prefix.$table_name;
  2278. }
  2279. if(isset(self::$table_info[$table_name])) {
  2280. $this->content_columns = self::$table_info[$table_name];
  2281. } else {
  2282. self::$db->loadModule('Reverse', null, true);
  2283. $this->content_columns = self::$db->reverse->tableInfo($table_name);
  2284. if($this->is_error($this->content_columns)) {
  2285. $this->raise($this->content_columns->getMessage());
  2286. }
  2287. if(is_array($this->content_columns)) {
  2288. $i = 0;
  2289. foreach($this->content_columns as $column) {
  2290. $this->content_columns[$i++]['human_name'] = Inflector::humanize($column['name']);
  2291. }
  2292. self::$table_info[$table_name] = $this->content_columns;
  2293. }
  2294. }
  2295. }
  2296. /**
  2297. * Returns the autogenerated id from the last insert query
  2298. *
  2299. * @uses $db
  2300. * @uses is_error()
  2301. * @uses raise()
  2302. * @throws {@link ActiveRecordError}
  2303. */
  2304. function get_insert_id() {
  2305. // fetch the last inserted id via autoincrement or current value of a sequence
  2306. if(self::$db->supports('auto_increment') === true) {
  2307. $id = self::$db->lastInsertID("{$this->table_prefix}{$this->table_name}", $this->primary_keys[0]);
  2308. if($this->is_error($id)) {
  2309. $this->raise($id->getMessage());
  2310. }
  2311. return $id;
  2312. }
  2313. return null;
  2314. }
  2315. /**
  2316. * Open a database connection if one is not currently open
  2317. *
  2318. * The name of the database normally comes from
  2319. * $database_settings which is set in {@link
  2320. * environment.php} by reading file config/database.ini. The
  2321. * database name may be overridden by assigning a different name
  2322. * to {@link $database_name}.
  2323. *
  2324. * If there is a connection now open, as indicated by the saved
  2325. * value of a MDB2 object in $active_connections[$connection_name], and
  2326. * {@link force_reconnect} is not true, then set the database
  2327. * fetch mode and return.
  2328. *
  2329. * If there is no connection, open one and save a reference to
  2330. * it in $active_connections[$connection_name].
  2331. *
  2332. * @uses $db
  2333. * @uses $database_name
  2334. * @uses $force_reconnect
  2335. * @uses $active_connections
  2336. * @uses is_error()
  2337. * @throws {@link ActiveRecordError}
  2338. */
  2339. function establish_connection() {
  2340. $connection =& self::$active_connections[$this->connection_name];
  2341. if(!is_object($connection) || $this->force_reconnect) {
  2342. $connection_settings = array();
  2343. $connection_options = array();
  2344. if(array_key_exists($this->connection_name, self::$database_settings)) {
  2345. # Use a different custom sections settings ?
  2346. if(array_key_exists("use", self::$database_settings[$this->connection_name])) {
  2347. $connection_settings = self::$database_settings[self::$database_settings[$this->connection_name]['use']];
  2348. } else {
  2349. # Custom defined db settings in database.ini
  2350. $connection_settings = self::$database_settings[$this->connection_name];
  2351. }
  2352. } else {
  2353. # Just use the current TRAX_ENV's environment db settings
  2354. # $this->connection_name's default value is TRAX_ENV so
  2355. # if should never really get here unless override $this->connection_name
  2356. # and you define a custom db section in database.ini and it can't find it.
  2357. $connection_settings = self::$database_settings[TRAX_ENV];
  2358. }
  2359. # Override database name if param is set
  2360. if($this->database_name) {
  2361. $connection_settings['database'] = $this->database_name;
  2362. }
  2363. # Set optional Pear parameters
  2364. if(isset($connection_settings['persistent'])) {
  2365. $connection_options['persistent'] = $connection_settings['persistent'];
  2366. }
  2367. # Connect to the database and throw an error if the connect fails.
  2368. //print("MDB2::Connect($connection_settings, $connection_options) \n");
  2369. $connection =& MDB2::Connect($connection_settings, $connection_options);
  2370. //var_dump($connection_settings);
  2371. //static $connect_cnt; $connect_cnt++; error_log("connection #".$connect_cnt);
  2372. # For Postgres schemas (http://www.postgresql.org/docs/8.0/interactive/ddl-schemas.html)
  2373. if(isset($connection_settings['schema_search_path'])){
  2374. if(!$this->is_error($connection)) {
  2375. # Set the schema search path to a string of comma-separated schema names.
  2376. # First strip out all the whitespace
  2377. $connection->query('SET search_path TO '.preg_replace('/\s+/', '', $connection_settings['schema_search_path']));
  2378. }
  2379. }
  2380. }
  2381. if(!$this->is_error($connection)) {
  2382. self::$active_connections[$this->connection_name] =& $connection;
  2383. self::$db =& $connection;
  2384. self::$db->setFetchMode($this->fetch_mode);
  2385. } else {
  2386. $this->raise($connection->getMessage());
  2387. }
  2388. return self::$db;
  2389. }
  2390. /**
  2391. * Determine if passed in attribute (table column) is a string
  2392. * @param string $attribute Name of the table column
  2393. * @uses column_for_attribute()
  2394. */
  2395. function attribute_is_string($attribute, $column = null) {
  2396. $column = is_null($column) ? $this->column_for_attribute($attribute) : $column;
  2397. switch(strtolower($column['mdb2type'])) {
  2398. case 'text':
  2399. case 'timestamp':
  2400. case 'date':
  2401. case 'time':
  2402. case 'blob':
  2403. case 'clob':
  2404. return true;
  2405. }
  2406. return false;
  2407. }
  2408. /**
  2409. * Determine if passed in name is a composite class or not
  2410. * @param string $name Name of the composed_of mapping
  2411. * @uses $composed_of
  2412. */
  2413. private function is_composite($name) {
  2414. if(is_array($this->composed_of)) {
  2415. if(array_key_exists($name, $this->composed_of)) {
  2416. return true;
  2417. }
  2418. }
  2419. return false;
  2420. }
  2421. /**
  2422. * Runs validation routines for update or create
  2423. *
  2424. * @uses after_validation();
  2425. * @uses after_validation_on_create();
  2426. * @uses after_validation_on_update();
  2427. * @uses before_validation();
  2428. * @uses before_validation_on_create();
  2429. * @uses before_validation_on_update();
  2430. * @uses $errors
  2431. * @uses $new_record
  2432. * @uses validate();
  2433. * @uses validate_model_attributes();
  2434. * @uses validate_builtin();
  2435. * @uses validate_on_create();
  2436. * @return boolean
  2437. * <ul>
  2438. * <li>true => Valid, no errors found.
  2439. * {@link $errors} is empty</li>
  2440. * <li>false => Not valid, errors in {@link $errors}</li>
  2441. * </ul>
  2442. */
  2443. function valid() {
  2444. # first clear the errors array
  2445. $this->errors = array();
  2446. if($this->new_record) {
  2447. $this->before_validation();
  2448. $this->before_validation_on_create();
  2449. $this->validate();
  2450. $this->validate_model_attributes();
  2451. $this->validate_builtin();
  2452. $this->after_validation();
  2453. $this->validate_on_create();
  2454. $this->after_validation_on_create();
  2455. } else {
  2456. $this->before_validation();
  2457. $this->before_validation_on_update();
  2458. $this->validate();
  2459. $this->validate_model_attributes();
  2460. $this->validate_builtin();
  2461. $this->after_validation();
  2462. $this->validate_on_update();
  2463. $this->validate_on_update_builtin();
  2464. $this->after_validation_on_update();
  2465. }
  2466. return count($this->errors) ? false : true;
  2467. }
  2468. /**
  2469. * Call every method named "validate_*()" where * is a column name
  2470. *
  2471. * Find and call every method named "validate_something()" where
  2472. * "something" is the name of a column. The "validate_something()"
  2473. * functions are expected to return an array whose first element
  2474. * is true or false (indicating whether or not the validation
  2475. * succeeded), and whose second element is the error message to
  2476. * display if the first element is false.
  2477. *
  2478. * @return boolean
  2479. * <ul>
  2480. * <li>true => Valid, no errors found.
  2481. * {@link $errors} is empty</li>
  2482. * <li>false => Not valid, errors in {@link $errors}.
  2483. * $errors is an array whose keys are the names of columns,
  2484. * and the value of each key is the error message returned
  2485. * by the corresponding validate_*() method.</li>
  2486. * </ul>
  2487. * @uses $errors
  2488. * @uses get_attributes()
  2489. */
  2490. function validate_model_attributes() {
  2491. $validated_ok = true;
  2492. $attrs = $this->get_attributes();
  2493. $methods = get_class_methods($this->get_class_name());
  2494. foreach($methods as $method) {
  2495. if(preg_match('/^validate_(.+)/', $method, $matches)) {
  2496. # If we find, for example, a method named validate_name, then
  2497. # we know that that function is validating the 'name' attribute
  2498. # (as found in the (.+) part of the regular expression above).
  2499. $validate_on_attribute = $matches[1];
  2500. # Check to see if the string found (e.g. 'name') really is
  2501. # in the list of attributes for this object...
  2502. if(array_key_exists($validate_on_attribute, $attrs)) {
  2503. # ...if so, then call the method to see if it validates to true...
  2504. $result = $this->$method();
  2505. if(is_array($result)) {
  2506. # $result[0] is true if validation went ok, false otherwise
  2507. # $result[1] is the error message if validation failed
  2508. if($result[0] == false) {
  2509. # ... and if not, then validation failed
  2510. $validated_ok = false;
  2511. # Mark the corresponding entry in the error array by
  2512. # putting the error message in for the attribute,
  2513. # e.g. $this->errors['name'] = "can't be empty"
  2514. # when 'name' was an empty string.
  2515. $this->errors[$validate_on_attribute] = $result[1];
  2516. }
  2517. }
  2518. }
  2519. }
  2520. }
  2521. return $validated_ok;
  2522. }
  2523. /**
  2524. * Overwrite this method for validation checks on all saves and
  2525. * use $this->errors[] = "My error message."; or
  2526. * for invalid attributes $this->errors['attribute'] = "Attribute is invalid.";
  2527. * @todo Document this API
  2528. */
  2529. function validate() {}
  2530. /**
  2531. * Override this method for validation checks used only on creation.
  2532. * @todo Document this API
  2533. */
  2534. function validate_on_create() {}
  2535. /**
  2536. * Override this method for validation checks used only on updates.
  2537. * @todo Document this API
  2538. */
  2539. function validate_on_update() {}
  2540. /**
  2541. * Is called before validate().
  2542. * @todo Document this API
  2543. */
  2544. function before_validation() {}
  2545. /**
  2546. * Is called after validate().
  2547. * @todo Document this API
  2548. */
  2549. function after_validation() {}
  2550. /**
  2551. * Is called before validate() on new objects that haven't been saved yet (no record exists).
  2552. * @todo Document this API
  2553. */
  2554. function before_validation_on_create() {}
  2555. /**
  2556. * Is called after validate() on new objects that haven't been saved yet (no record exists).
  2557. * @todo Document this API
  2558. */
  2559. function after_validation_on_create() {}
  2560. /**
  2561. * Is called before validate() on existing objects that has a record.
  2562. * @todo Document this API
  2563. */
  2564. function before_validation_on_update() {}
  2565. /**
  2566. * Is called after validate() on existing objects that has a record.
  2567. * @todo Document this API
  2568. */
  2569. function after_validation_on_update() {}
  2570. /**
  2571. * Is called before save() (regardless of whether its a create or update save)
  2572. * @todo Document this API
  2573. */
  2574. function before_save() {}
  2575. /**
  2576. * Is called after save (regardless of whether its a create or update save).
  2577. * @todo Document this API
  2578. */
  2579. function after_save() {}
  2580. /**
  2581. * Is called before save() on new objects that havent been saved yet (no record exists).
  2582. * @todo Document this API
  2583. */
  2584. function before_create() {}
  2585. /**
  2586. * Is called after save() on new objects that havent been saved yet (no record exists).
  2587. * @todo Document this API
  2588. */
  2589. function after_create() {}
  2590. /**
  2591. * Is called before save() on existing objects that has a record.
  2592. * @todo Document this API
  2593. */
  2594. function before_update() {}
  2595. /**
  2596. * Is called after save() on existing objects that has a record.
  2597. * @todo Document this API
  2598. */
  2599. function after_update() {}
  2600. /**
  2601. * Is called before delete().
  2602. * @todo Document this API
  2603. */
  2604. function before_delete() {}
  2605. /**
  2606. * Is called after delete().
  2607. * @todo Document this API
  2608. */
  2609. function after_delete() {}
  2610. /**
  2611. * Validates any builtin validates_* functions defined as
  2612. * class variables in child model class.
  2613. *
  2614. * eg.
  2615. * public $validates_presence_of = array(
  2616. * 'first_name' => array(
  2617. * 'message' => "is not optional.",
  2618. * 'on' => 'update'
  2619. * ),
  2620. * 'last_name' => null,
  2621. * 'password' => array(
  2622. * 'on' => 'create'
  2623. * )
  2624. * );
  2625. *
  2626. * @uses $builtin_validation_functions
  2627. */
  2628. function validate_builtin() {
  2629. foreach($this->builtin_validation_functions as $method_name) {
  2630. $validation_name = $this->$method_name;
  2631. if(is_string($validation_name)) {
  2632. $validation_name = explode(",", $validation_name);
  2633. }
  2634. if(method_exists($this, $method_name) && is_array($validation_name)) {
  2635. foreach($validation_name as $attribute_name => $options) {
  2636. if(!is_array($options)) {
  2637. $attribute_name = $options;
  2638. $options = array();
  2639. }
  2640. $attribute_name = trim($attribute_name);
  2641. $parameters = array();
  2642. $on = array_key_exists('on', $options) ?
  2643. $options['on'] : 'save';
  2644. $message = array_key_exists('message', $options) ?
  2645. $options['message'] : null;
  2646. switch($method_name) {
  2647. case 'validates_acceptance_of':
  2648. $accept = array_key_exists('accept', $options) ? $options['accept'] : 1;
  2649. $parameters = array($attribute_name, $message, $accept);
  2650. break;
  2651. case 'validates_confirmation_of':
  2652. $parameters = array($attribute_name, $message);
  2653. break;
  2654. case 'validates_exclusion_of':
  2655. $in = array_key_exists('in', $options) ? $options['in'] : array();
  2656. $parameters = array($attribute_name, $in, $message);
  2657. break;
  2658. case 'validates_format_of':
  2659. $with = array_key_exists('with', $options) ? $options['with'] : '';
  2660. $parameters = array($attribute_name, $with, $message);
  2661. break;
  2662. case 'validates_inclusion_of':
  2663. $in = array_key_exists('in', $options) ? $options['in'] : array();
  2664. $parameters = array($attribute_name, $in, $message);
  2665. break;
  2666. case 'validates_length_of':
  2667. $parameters = array($attribute_name, $options);
  2668. break;
  2669. case 'validates_numericality_of':
  2670. $only_integer = array_key_exists('only_integer', $options) ?
  2671. $options['only_integer'] : false;
  2672. $allow_null = array_key_exists('allow_null', $options) ?
  2673. $options['allow_null'] : false;
  2674. $parameters = array($attribute_name, $message, $only_integer, $allow_null);
  2675. break;
  2676. case 'validates_presence_of':
  2677. $parameters = array($attribute_name, $message);
  2678. break;
  2679. case 'validates_uniqueness_of':
  2680. $parameters = array($attribute_name, $message);
  2681. break;
  2682. }
  2683. if(count($parameters)) {
  2684. $call = false;
  2685. if($on == 'create' && $this->new_record) {
  2686. $call = true;
  2687. } elseif($on == 'update' && !$this->new_record) {
  2688. $call = true;
  2689. } elseif($on == 'save') {
  2690. $call = true;
  2691. }
  2692. if($call) {
  2693. # error_log("calling $method_name(".implode(",",$parameters).")");
  2694. call_user_func_array(array($this, $method_name), $parameters);
  2695. }
  2696. }
  2697. }
  2698. }
  2699. }
  2700. }
  2701. /**
  2702. * Validates that a checkbox is clicked.
  2703. * eg. validates_acceptance_of('eula')
  2704. *
  2705. * @param string|array $attribute_names
  2706. * @param string $message
  2707. * @param string $accept
  2708. */
  2709. function validates_acceptance_of($attribute_names, $message = null, $accept = 1) {
  2710. $message = $this->get_error_message_for_validation($message, 'acceptance');
  2711. foreach((array) $attribute_names as $attribute_name) {
  2712. if($this->$attribute_name != $accept) {
  2713. $attribute_human = Inflector::humanize($attribute_name);
  2714. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2715. }
  2716. }
  2717. }
  2718. /**
  2719. * Validates that a field has the same value as its corresponding confirmation field.
  2720. * eg. validates_confirmation_of('password')
  2721. *
  2722. * @param string|array $attribute_names
  2723. * @param string $message
  2724. */
  2725. function validates_confirmation_of($attribute_names, $message = null) {
  2726. $message = $this->get_error_message_for_validation($message, 'confirmation');
  2727. foreach((array) $attribute_names as $attribute_name) {
  2728. $attribute_confirmation = $attribute_name . '_confirmation';
  2729. if($this->$attribute_confirmation != $this->$attribute_name) {
  2730. $attribute_human = Inflector::humanize($attribute_name);
  2731. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2732. }
  2733. }
  2734. }
  2735. /**
  2736. * Validates that specified attributes are NOT in an array of elements.
  2737. * eg. validates_exclusion_of('age, 'in' => array(13, 19))
  2738. *
  2739. * @param string|array $attribute_names
  2740. * @param mixed $in array(1,2,3,4,5) or string 1..5
  2741. * @param string $message
  2742. */
  2743. function validates_exclusion_of($attribute_names, $in = array(), $message = null) {
  2744. $message = $this->get_error_message_for_validation($message, 'exclusion');
  2745. foreach((array) $attribute_names as $attribute_name) {
  2746. if(is_string($in)) {
  2747. list($minimum, $maximum) = explode('..', $in);
  2748. if($this->$attribute_name >= $minimum && $this->$attribute_name <= $maximum) {
  2749. $attribute_human = Inflector::humanize($attribute_name);
  2750. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2751. }
  2752. } elseif(is_array($in)) {
  2753. if(in_array($this->$attribute_name, $in)) {
  2754. $attribute_human = Inflector::humanize($attribute_name);
  2755. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2756. }
  2757. }
  2758. }
  2759. }
  2760. /**
  2761. * Validates that specified attributes matches a regular expression
  2762. * eg. validates_format_of('email', '/^(+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i')
  2763. *
  2764. * @param string|array $attribute_names
  2765. * @param string $regex
  2766. * @param string $message
  2767. */
  2768. function validates_format_of($attribute_names, $regex, $message = null) {
  2769. $message = $this->get_error_message_for_validation($message, 'invalid');
  2770. foreach((array) $attribute_names as $attribute_name) {
  2771. $value = $this->$attribute_name;
  2772. # Was there an error?
  2773. if(!preg_match($regex, $value)) {
  2774. $attribute_human = Inflector::humanize($attribute_name);
  2775. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2776. }
  2777. }
  2778. }
  2779. /**
  2780. * Validates that specified attributes are in an array of elements.
  2781. * eg. validates_inclusion_of('gender', array('m', 'f'))
  2782. *
  2783. * @param string|array $attribute_names
  2784. * @param mixed $in array(1,2,3,4,5) or string 1..5
  2785. * @param string $message
  2786. */
  2787. function validates_inclusion_of($attribute_names, $in = array(), $message = null) {
  2788. $message = $this->get_error_message_for_validation($message, 'inclusion');
  2789. foreach((array) $attribute_names as $attribute_name) {
  2790. if(is_string($in)) {
  2791. list($minimum, $maximum) = explode('..', $in);
  2792. if(!($this->$attribute_name >= $minimum && $this->$attribute_name <= $maximum)) {
  2793. $attribute_human = Inflector::humanize($attribute_name);
  2794. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2795. }
  2796. } elseif(is_array($in)) {
  2797. if(!in_array($this->$attribute_name, $in)) {
  2798. $attribute_human = Inflector::humanize($attribute_name);
  2799. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2800. }
  2801. }
  2802. }
  2803. }
  2804. /**
  2805. * Validates that specified attributes are of some length
  2806. * eg. validates_length_of('password', array('minimum' => 8))
  2807. *
  2808. * @param string|array $attribute_names
  2809. * @param array $options
  2810. */
  2811. function validates_length_of($attribute_names, $options = array(
  2812. 'too_short' => null, 'too_long' => null, 'wrong_length' => null, 'message' => null)) {
  2813. # Convert 'in' to 'minimum' and 'maximum'
  2814. if(isset($options['in'])) {
  2815. list($options['minimum'], $options['maximum']) = explode('..', $options['in']);
  2816. }
  2817. # If 'message' is set see if we need to override other messages
  2818. if(isset($options['message'])) {
  2819. if(!isset($options['too_short'])) $options['too_short'] = $options['message'];
  2820. if(!isset($options['too_long'])) $options['too_long'] = $options['message'];
  2821. if(!isset($options['wrong_length'])) $options['wrong_length'] = $options['message'];
  2822. }
  2823. foreach((array) $attribute_names as $attribute_name) {
  2824. # Attribute string length
  2825. $len = strlen($this->$attribute_name);
  2826. $attribute_human = Inflector::humanize($attribute_name);
  2827. # If you have set the min length option
  2828. if(isset($options['minimum'])) {
  2829. $message = $this->get_error_message_for_validation($options['too_short'], 'too_short', $options['minimum']);
  2830. if($len < $options['minimum']) {
  2831. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2832. }
  2833. }
  2834. # If you have set the max length option
  2835. if(isset($options['maximum'])) {
  2836. $message = $this->get_error_message_for_validation($options['too_long'], 'too_long', $options['maximum']);
  2837. if($len > $options['maximum']) {
  2838. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2839. }
  2840. }
  2841. # If you have set an exact length option
  2842. if(isset($options['is'])) {
  2843. $message = $this->get_error_message_for_validation($options['wrong_length'], 'wrong_length', $options['is']);
  2844. if($len != $options['is']) {
  2845. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2846. }
  2847. }
  2848. }
  2849. }
  2850. /**
  2851. * Validates that specified attributes are numbers
  2852. * eg. validates_numericality_of('value')
  2853. *
  2854. * @param string|array $attribute_names
  2855. * @param string $message
  2856. */
  2857. function validates_numericality_of($attribute_names, $message = null, $only_integer = false, $allow_null = false) {
  2858. foreach((array) $attribute_names as $attribute_name) {
  2859. $value = $this->$attribute_name;
  2860. # Skip validation if you allow null
  2861. if($allow_null && is_null($value)) {
  2862. break;
  2863. }
  2864. if($only_integer) {
  2865. $message = $this->get_error_message_for_validation($message, 'not_an_integer');
  2866. if(!is_integer($value)) {
  2867. $attribute_human = Inflector::humanize($attribute_name);
  2868. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2869. }
  2870. } else {
  2871. $message = $this->get_error_message_for_validation($message, 'not_a_number');
  2872. if(!is_numeric($value)) {
  2873. $attribute_human = Inflector::humanize($attribute_name);
  2874. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2875. }
  2876. }
  2877. }
  2878. }
  2879. /**
  2880. * Validates that specified attributes are not blank
  2881. * eg. validates_presence_of(array('firstname', 'lastname'))
  2882. *
  2883. * @param string|array $attribute_names
  2884. * @param string $message
  2885. */
  2886. function validates_presence_of($attribute_names, $message = null) {
  2887. $message = $this->get_error_message_for_validation($message, 'empty');
  2888. foreach((array) $attribute_names as $attribute_name) {
  2889. if($this->$attribute_name === '' || is_null($this->$attribute_name)) {
  2890. $attribute_human = Inflector::humanize($attribute_name);
  2891. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2892. }
  2893. }
  2894. }
  2895. /**
  2896. * Validates that specified attributes are unique in the model database table
  2897. * eg. validates_uniqueness_of('username')
  2898. *
  2899. * @param string|array $attribute_names
  2900. * @param string $message
  2901. */
  2902. function validates_uniqueness_of($attribute_names, $message = null) {
  2903. $message = $this->get_error_message_for_validation($message, 'taken');
  2904. foreach((array) $attribute_names as $attribute_name) {
  2905. $quoted_value = $this->quote_attribute($attribute_name);
  2906. # Conditions for new and existing record
  2907. if($this->new_record) {
  2908. $conditions = sprintf("%s = %s", $attribute_name, $quoted_value);
  2909. } else {
  2910. $conditions = sprintf("%s = %s AND %s", $attribute_name,
  2911. $quoted_value, $this->get_primary_key_conditions("!="));
  2912. }
  2913. if($this->find_first($conditions)) {
  2914. $attribute_human = Inflector::humanize($attribute_name);
  2915. $this->add_error("{$attribute_human} {$message}", $attribute_name);
  2916. }
  2917. }
  2918. }
  2919. /**
  2920. * Return the error message for a validation function
  2921. *
  2922. * @param string $message
  2923. * @param string $key
  2924. * @param string $value
  2925. * @return string
  2926. */
  2927. private function get_error_message_for_validation($message, $key, $value = null) {
  2928. if(is_null($message)) {
  2929. # Return default error message
  2930. return sprintf($this->default_error_messages[$key], $value);
  2931. } else {
  2932. # Return your custom error message
  2933. return $message;
  2934. }
  2935. }
  2936. /**
  2937. * Test whether argument is a PEAR Error object or a MDB2 Error object.
  2938. *
  2939. * @param object $obj Object to test
  2940. * @return boolean Whether object is one of these two errors
  2941. */
  2942. function is_error($obj) {
  2943. if((PEAR::isError($obj)) || (MDB2::isError($obj))) {
  2944. return true;
  2945. } else {
  2946. return false;
  2947. }
  2948. }
  2949. /**
  2950. * Throw an exception describing an error in this object
  2951. *
  2952. * @throws {@link ActiveRecordError}
  2953. */
  2954. function raise($message) {
  2955. $error_message = "Model Class: ".$this->get_class_name()."<br>";
  2956. $error_message .= "Error Message: ".$message;
  2957. throw new ActiveRecordError($error_message, "ActiveRecord Error", "500");
  2958. }
  2959. /**
  2960. * Add or overwrite description of an error to the list of errors
  2961. * @param string $error Error message text
  2962. * @param string $key Key to associate with the error (in the
  2963. * simple case, column name). If omitted, numeric keys will be
  2964. * assigned starting with 0. If specified and the key already
  2965. * exists in $errors, the old error message will be overwritten
  2966. * with the value of $error.
  2967. * @uses $errors
  2968. */
  2969. function add_error($error, $key = null) {
  2970. if(!is_null($key)) {
  2971. $this->errors[$key] = $error;
  2972. } else {
  2973. $this->errors[] = $error;
  2974. }
  2975. }
  2976. /**
  2977. * Return description of non-fatal errors
  2978. *
  2979. * @uses $errors
  2980. * @param boolean $return_string
  2981. * <ul>
  2982. * <li>true => Concatenate all error descriptions into a string
  2983. * using $seperator between elements and return the
  2984. * string</li>
  2985. * <li>false => Return the error descriptions as an array</li>
  2986. * </ul>
  2987. * @param string $seperator String to concatenate between error
  2988. * descriptions if $return_string == true
  2989. * @return mixed Error description(s), if any
  2990. */
  2991. function get_errors($return_string = false, $seperator = "<br>") {
  2992. if($return_string && count($this->errors) > 0) {
  2993. return implode($seperator, $this->errors);
  2994. } else {
  2995. return $this->errors;
  2996. }
  2997. }
  2998. /**
  2999. * Return errors as a string.
  3000. *
  3001. * Concatenate all error descriptions into a stringusing
  3002. * $seperator between elements and return the string.
  3003. * @param string $seperator String to concatenate between error
  3004. * descriptions
  3005. * @return string Concatenated error description(s), if any
  3006. */
  3007. function get_errors_as_string($seperator = "<br>") {
  3008. return $this->get_errors(true, $seperator);
  3009. }
  3010. /**
  3011. * Log SQL query in development mode
  3012. *
  3013. * If running in development mode, log the query to self::$query_log
  3014. * @param string SQL to be logged
  3015. */
  3016. function log_query($query) {
  3017. if(TRAX_ENV == "development" && $query) {
  3018. self::$query_log[] = $query;
  3019. }
  3020. }
  3021. }
  3022. // -- set Emacs parameters --
  3023. // Local variables:
  3024. // tab-width: 4
  3025. // c-basic-offset: 4
  3026. // c-hanging-comment-ender-p: nil
  3027. // indent-tabs-mode: nil
  3028. // End:
  3029. ?>