PageRenderTime 59ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/fuel/packages/orm/classes/model.php

https://bitbucket.org/arkross/venus
PHP | 1582 lines | 992 code | 182 blank | 408 comment | 96 complexity | 2bb3517858336759f56b6b458f7ab4ca MD5 | raw file
Possible License(s): MIT, BSD-3-Clause
  1. <?php
  2. /**
  3. * Fuel is a fast, lightweight, community driven PHP5 framework.
  4. *
  5. * @package Fuel
  6. * @version 1.0
  7. * @author Fuel Development Team
  8. * @license MIT License
  9. * @copyright 2010 - 2011 Fuel Development Team
  10. * @link http://fuelphp.com
  11. */
  12. namespace Orm;
  13. /**
  14. * Record Not Found Exception
  15. */
  16. class RecordNotFound extends \OutOfBoundsException {}
  17. /**
  18. * Frozen Object Exception
  19. */
  20. class FrozenObject extends \RuntimeException {}
  21. class Model implements \ArrayAccess, \Iterator {
  22. /* ---------------------------------------------------------------------------
  23. * Static usage
  24. * --------------------------------------------------------------------------- */
  25. /**
  26. * @var string connection to use
  27. */
  28. // protected static $_connection = null;
  29. /**
  30. * @var string table name to overwrite assumption
  31. */
  32. // protected static $_table_name;
  33. /**
  34. * @var array array of object properties
  35. */
  36. // protected static $_properties;
  37. /**
  38. * @var array array of views with additional properties
  39. */
  40. // protected static $_views;
  41. /**
  42. * @var array array of observer classes to use
  43. */
  44. // protected static $_observers;
  45. /**
  46. * @var array relationship properties
  47. */
  48. // protected static $_has_one;
  49. // protected static $_belongs_to;
  50. // protected static $_has_many;
  51. // protected static $_many_many;
  52. /**
  53. * @var array name or names of the primary keys
  54. */
  55. protected static $_primary_key = array('id');
  56. /**
  57. * @var array cached tables
  58. */
  59. protected static $_table_names_cached = array();
  60. /**
  61. * @var array cached properties
  62. */
  63. protected static $_properties_cached = array();
  64. /**
  65. * @var array cached properties
  66. */
  67. protected static $_views_cached = array();
  68. /**
  69. * @var string relationships
  70. */
  71. protected static $_relations_cached = array();
  72. /**
  73. * @var array cached observers
  74. */
  75. protected static $_observers_cached = array();
  76. /**
  77. * @var array array of fetched objects
  78. */
  79. protected static $_cached_objects = array();
  80. /**
  81. * @var array array of valid relation types
  82. */
  83. protected static $_valid_relations = array(
  84. 'belongs_to' => 'Orm\\BelongsTo',
  85. 'has_one' => 'Orm\\HasOne',
  86. 'has_many' => 'Orm\\HasMany',
  87. 'many_many' => 'Orm\\ManyMany',
  88. );
  89. /**
  90. * This method is deprecated...use forge() instead.
  91. *
  92. * @deprecated until 1.2
  93. */
  94. public static function factory($data = array(), $new = true)
  95. {
  96. logger(\Fuel::L_WARNING, 'This method is deprecated. Please use a forge() instead.', __METHOD__);
  97. return static::forge($data, $new);
  98. }
  99. public static function forge($data = array(), $new = true, $view = null)
  100. {
  101. return new static($data, $new, $view);
  102. }
  103. /**
  104. * Fetch the database connection name to use
  105. *
  106. * @return null|string
  107. */
  108. public static function connection()
  109. {
  110. $class = get_called_class();
  111. return property_exists($class, '_connection') ? static::$_connection : null;
  112. }
  113. /**
  114. * Get the table name for this class
  115. *
  116. * @return string
  117. */
  118. public static function table()
  119. {
  120. $class = get_called_class();
  121. // Table name unknown
  122. if ( ! array_key_exists($class, static::$_table_names_cached))
  123. {
  124. // Table name set in Model
  125. if (property_exists($class, '_table_name'))
  126. {
  127. static::$_table_names_cached[$class] = static::$_table_name;
  128. }
  129. else
  130. {
  131. static::$_table_names_cached[$class] = \Inflector::tableize($class);
  132. }
  133. }
  134. return static::$_table_names_cached[$class];
  135. }
  136. /**
  137. * Attempt to retrieve an earlier loaded object
  138. *
  139. * @param array|Model $obj
  140. * @param null|string $class
  141. * @return Model|false
  142. */
  143. public static function cached_object($obj, $class = null)
  144. {
  145. $class = $class ?: get_called_class();
  146. $id = (is_int($obj) or is_string($obj)) ? (string) $obj : $class::implode_pk($obj);
  147. $result = ( ! empty(static::$_cached_objects[$class][$id])) ? static::$_cached_objects[$class][$id] : false;
  148. return $result;
  149. }
  150. /**
  151. * Get the primary key(s) of this class
  152. *
  153. * @return array
  154. */
  155. public static function primary_key()
  156. {
  157. return static::$_primary_key;
  158. }
  159. /**
  160. * Implode the primary keys within the data into a string
  161. *
  162. * @param array
  163. * @return string
  164. */
  165. public static function implode_pk($data)
  166. {
  167. if (count(static::$_primary_key) == 1)
  168. {
  169. $p = reset(static::$_primary_key);
  170. return (is_object($data)
  171. ? strval($data->{$p})
  172. : (isset($data[$p])
  173. ? strval($data[$p])
  174. : null));
  175. }
  176. $pk = '';
  177. foreach (static::$_primary_key as $p)
  178. {
  179. if (is_null((is_object($data) ? $data->{$p} : (isset($data[$p]) ? $data[$p] : null))))
  180. {
  181. return null;
  182. }
  183. $pk .= '['.(is_object($data) ? $data->{$p} : $data[$p]).']';
  184. }
  185. return $pk;
  186. }
  187. /**
  188. * Get the class's properties
  189. *
  190. * @return array
  191. */
  192. public static function properties()
  193. {
  194. $class = get_called_class();
  195. // If already determined
  196. if (array_key_exists($class, static::$_properties_cached))
  197. {
  198. return static::$_properties_cached[$class];
  199. }
  200. // Try to grab the properties from the class...
  201. if (property_exists($class, '_properties'))
  202. {
  203. $properties = static::$_properties;
  204. foreach ($properties as $key => $p)
  205. {
  206. if (is_string($p))
  207. {
  208. unset($properties[$key]);
  209. $properties[$p] = array();
  210. }
  211. }
  212. }
  213. // ...if the above failed, run DB query to fetch properties
  214. if (empty($properties))
  215. {
  216. try
  217. {
  218. $properties = \DB::list_columns(static::table(), null, static::connection());
  219. }
  220. catch (\Exception $e)
  221. {
  222. throw new \FuelException('Listing columns failed, you have to set the model properties with a '.
  223. 'static $_properties setting in the model. Original exception: '.$e->getMessage());
  224. }
  225. }
  226. // cache the properties for next usage
  227. static::$_properties_cached[$class] = $properties;
  228. return static::$_properties_cached[$class];
  229. }
  230. /**
  231. * Fetches a property description array, or specific data from it
  232. *
  233. * @param string property or property.key
  234. * @param mixed return value when key not present
  235. * @return mixed
  236. */
  237. public static function property($key, $default = null)
  238. {
  239. $class = get_called_class();
  240. // If already determined
  241. if ( ! array_key_exists($class, static::$_properties_cached))
  242. {
  243. static::properties();
  244. }
  245. return \Arr::get(static::$_properties_cached[$class], $key, $default);
  246. }
  247. /**
  248. * Fetch the model's views
  249. *
  250. * @return array
  251. */
  252. public static function views()
  253. {
  254. $class = get_called_class();
  255. if ( ! isset(static::$_views_cached[$class]))
  256. {
  257. static::$_views_cached[$class] = array();
  258. if (property_exists($class, '_views'))
  259. {
  260. $views = $class::$_views;
  261. foreach ($views as $k => $v)
  262. {
  263. if ( ! isset($v['columns']))
  264. {
  265. throw new \InvalidArgumentException('Database view '.$k.' is defined without columns.');
  266. }
  267. $v['columns'] = (array) $v['columns'];
  268. if ( ! isset($v['view']))
  269. {
  270. $v['view'] = $k;
  271. }
  272. static::$_views_cached[$class][$k] = $v;
  273. }
  274. }
  275. }
  276. return static::$_views_cached[$class];
  277. }
  278. /**
  279. * Get the class's relations
  280. *
  281. * @param string
  282. * @return array
  283. */
  284. public static function relations($specific = false)
  285. {
  286. $class = get_called_class();
  287. if ( ! array_key_exists($class, static::$_relations_cached))
  288. {
  289. $relations = array();
  290. foreach (static::$_valid_relations as $rel_name => $rel_class)
  291. {
  292. if (property_exists($class, '_'.$rel_name))
  293. {
  294. foreach (static::${'_'.$rel_name} as $key => $settings)
  295. {
  296. $name = is_string($settings) ? $settings : $key;
  297. $settings = is_array($settings) ? $settings : array();
  298. $relations[$name] = new $rel_class($class, $name, $settings);
  299. }
  300. }
  301. }
  302. static::$_relations_cached[$class] = $relations;
  303. }
  304. if ($specific === false)
  305. {
  306. return static::$_relations_cached[$class];
  307. }
  308. else
  309. {
  310. if ( ! array_key_exists($specific, static::$_relations_cached[$class]))
  311. {
  312. return false;
  313. }
  314. return static::$_relations_cached[$class][$specific];
  315. }
  316. }
  317. /**
  318. * Get the class's observers and what they observe
  319. *
  320. * @param string specific observer to retrieve info of, allows direct param access by using dot notation
  321. * @param mixed default return value when specific key wasn't found
  322. * @return array
  323. */
  324. public static function observers($specific = null, $default = null)
  325. {
  326. $class = get_called_class();
  327. if ( ! array_key_exists($class, static::$_observers_cached))
  328. {
  329. $observers = array();
  330. if (property_exists($class, '_observers'))
  331. {
  332. foreach (static::$_observers as $obs_k => $obs_v)
  333. {
  334. if (is_int($obs_k))
  335. {
  336. $observers[$obs_v] = array();
  337. }
  338. else
  339. {
  340. if (is_string($obs_v) or (is_array($obs_v) and is_int(key($obs_v))))
  341. {
  342. // @TODO deprecated until v1.2
  343. logger(\Fuel::L_WARNING, 'Passing observer events as array is deprecated, they must be
  344. inside another array under a key "events". Check the docs for more info.', __METHOD__);
  345. $observers[$obs_k] = array('events' => (array) $obs_v);
  346. }
  347. else
  348. {
  349. $observers[$obs_k] = $obs_v;
  350. }
  351. }
  352. }
  353. }
  354. static::$_observers_cached[$class] = $observers;
  355. }
  356. if ($specific)
  357. {
  358. return \Arr::get(static::$_observers_cached[$class], $specific, $default);
  359. }
  360. return static::$_observers_cached[$class];
  361. }
  362. /**
  363. * Find one or more entries
  364. *
  365. * @param mixed
  366. * @param array
  367. * @return object|array
  368. */
  369. public static function find($id = null, array $options = array())
  370. {
  371. // Return Query object
  372. if (is_null($id))
  373. {
  374. return static::query($options);
  375. }
  376. // Return all that match $options array
  377. elseif ($id == 'all')
  378. {
  379. return static::query($options)->get();
  380. }
  381. // Return first or last row that matches $options array
  382. elseif ($id == 'first' or $id == 'last')
  383. {
  384. $query = static::query($options);
  385. foreach(static::primary_key() as $pk)
  386. {
  387. $query->order_by($pk, $id == 'first' ? 'ASC' : 'DESC');
  388. }
  389. return $query->get_one();
  390. }
  391. // Return specific request row by ID
  392. else
  393. {
  394. $cache_pk = $where = array();
  395. $id = (array) $id;
  396. foreach (static::primary_key() as $pk)
  397. {
  398. $where[] = array($pk, '=', current($id));
  399. $cache_pk[$pk] = current($id);
  400. next($id);
  401. }
  402. if (array_key_exists(get_called_class(), static::$_cached_objects)
  403. and array_key_exists(static::implode_pk($cache_pk), static::$_cached_objects[get_called_class()]))
  404. {
  405. return static::$_cached_objects[get_called_class()][static::implode_pk($cache_pk)];
  406. }
  407. array_key_exists('where', $options) and $where = array_merge($options['where'], $where);
  408. $options['where'] = $where;
  409. return static::query($options)->get_one();
  410. }
  411. }
  412. /**
  413. * Creates a new query with optional settings up front
  414. *
  415. * @param array
  416. * @return Query
  417. */
  418. public static function query($options = array())
  419. {
  420. return Query::forge(get_called_class(), static::connection(), $options);
  421. }
  422. /**
  423. * Count entries, optionally only those matching the $options
  424. *
  425. * @param array
  426. * @return int
  427. */
  428. public static function count(array $options = array())
  429. {
  430. return Query::forge(get_called_class(), static::connection(), $options)->count();
  431. }
  432. /**
  433. * Find the maximum
  434. *
  435. * @param mixed
  436. * @param array
  437. * @return object|array
  438. */
  439. public static function max($key = null)
  440. {
  441. return Query::forge(get_called_class(), static::connection())->max($key ?: static::primary_key());
  442. }
  443. /**
  444. * Find the minimum
  445. *
  446. * @param mixed
  447. * @param array
  448. * @return object|array
  449. */
  450. public static function min($key = null)
  451. {
  452. return Query::forge(get_called_class(), static::connection())->min($key ?: static::primary_key());
  453. }
  454. public static function __callStatic($method, $args)
  455. {
  456. // Start with count_by? Get counting!
  457. if (strpos($method, 'count_by') === 0)
  458. {
  459. $find_type = 'count';
  460. $fields = substr($method, 9);
  461. }
  462. // Otherwise, lets find stuff
  463. elseif (strpos($method, 'find_') === 0)
  464. {
  465. $find_type = strncmp($method, 'find_all_by_', 12) === 0 ? 'all' : (strncmp($method, 'find_by_', 8) === 0 ? 'first' : false);
  466. $fields = $find_type === 'first' ? substr($method, 8) : substr($method, 12);
  467. }
  468. // God knows, complain
  469. else
  470. {
  471. throw new \FuelException('Invalid method call. Method '.$method.' does not exist.', 0);
  472. }
  473. $where = $or_where = array();
  474. if (($and_parts = explode('_and_', $fields)))
  475. {
  476. foreach ($and_parts as $and_part)
  477. {
  478. $or_parts = explode('_or_', $and_part);
  479. if (count($or_parts) == 1)
  480. {
  481. $where[] = array($or_parts[0] => array_shift($args));
  482. }
  483. else
  484. {
  485. foreach($or_parts as $or_part)
  486. {
  487. $or_where[] = array($or_part => array_shift($args));
  488. }
  489. }
  490. }
  491. }
  492. $options = count($args) > 0 ? array_pop($args) : array();
  493. if ( ! array_key_exists('where', $options))
  494. {
  495. $options['where'] = $where;
  496. }
  497. else
  498. {
  499. $options['where'] = array_merge($where, $options['where']);
  500. }
  501. if ( ! array_key_exists('or_where', $options))
  502. {
  503. $options['or_where'] = $or_where;
  504. }
  505. else
  506. {
  507. $options['or_where'] = array_merge($or_where, $options['or_where']);
  508. }
  509. if ($find_type == 'count')
  510. {
  511. return static::count($options);
  512. }
  513. else
  514. {
  515. return static::find($find_type, $options);
  516. }
  517. // min_...($options)
  518. // max_...($options)
  519. }
  520. /* ---------------------------------------------------------------------------
  521. * Object usage
  522. * --------------------------------------------------------------------------- */
  523. /**
  524. * @var bool keeps track of whether it's a new object
  525. */
  526. private $_is_new = true;
  527. /**
  528. * @var bool keeps to object frozen
  529. */
  530. private $_frozen = false;
  531. /**
  532. * @var array keeps the current state of the object
  533. */
  534. private $_data = array();
  535. /**
  536. * @var array keeps a copy of the object as it was retrieved from the database
  537. */
  538. private $_original = array();
  539. /**
  540. * @var array
  541. */
  542. private $_data_relations = array();
  543. /**
  544. * @var array keeps a copy of the relation ids that were originally retrieved from the database
  545. */
  546. private $_original_relations = array();
  547. /**
  548. * @var string view name when used
  549. */
  550. private $_view;
  551. /**
  552. * Constructor
  553. *
  554. * @param array
  555. * @param bool
  556. */
  557. public function __construct(array $data = array(), $new = true, $view = null)
  558. {
  559. // This is to deal with PHP's native hydration from that happens before constructor is called
  560. // for example using the DB's as_object() function
  561. if( ! empty($this->_data))
  562. {
  563. $this->_original = $this->_data;
  564. $new = false;
  565. }
  566. if ($new)
  567. {
  568. $properties = $this->properties();
  569. foreach ($properties as $prop => $settings)
  570. {
  571. if (array_key_exists($prop, $data))
  572. {
  573. $this->_data[$prop] = $data[$prop];
  574. }
  575. elseif (array_key_exists('default', $settings))
  576. {
  577. $this->_data[$prop] = $settings['default'];
  578. }
  579. }
  580. }
  581. else
  582. {
  583. $this->_update_original($data);
  584. $this->_data = array_merge($this->_data, $data);
  585. if ($view and array_key_exists($view, $this->views()))
  586. {
  587. $this->_view = $view;
  588. }
  589. }
  590. if ($new === false)
  591. {
  592. static::$_cached_objects[get_class($this)][static::implode_pk($data)] = $this;
  593. $this->_is_new = false;
  594. $this->observe('after_load');
  595. }
  596. else
  597. {
  598. $this->observe('after_create');
  599. }
  600. }
  601. /**
  602. * Update the original setting for this object
  603. *
  604. * @param array|null $original
  605. */
  606. public function _update_original($original = null)
  607. {
  608. $original = is_null($original) ? $this->_data : $original;
  609. $this->_original = array_merge($this->_original, $original);
  610. $this->_update_original_relations();
  611. }
  612. /**
  613. * Update the original relations for this object
  614. */
  615. public function _update_original_relations($relations = null)
  616. {
  617. if (is_null($relations))
  618. {
  619. $this->_original_relations = array();
  620. $relations = $this->_data_relations;
  621. }
  622. else
  623. {
  624. foreach ($relations as $key => $rel)
  625. {
  626. // Unload the just fetched relation from the originals
  627. unset($this->_original_relations[$rel]);
  628. // Unset the numeric key and set the data to update by the relation name
  629. unset($relations[$key]);
  630. $relations[$rel] = $this->_data_relations[$rel];
  631. }
  632. }
  633. foreach ($relations as $rel => $data)
  634. {
  635. if (is_array($data))
  636. {
  637. foreach ($data as $obj)
  638. {
  639. $this->_original_relations[$rel][] = $obj ? $obj->implode_pk($obj) : null;
  640. }
  641. }
  642. else
  643. {
  644. $this->_original_relations[$rel] = $data ? $data->implode_pk($data) : null;
  645. }
  646. }
  647. }
  648. /**
  649. * Fetch or set relations on this object
  650. * To be used only after having fetched them from the database!
  651. *
  652. * @param array|null $rels
  653. * @return void|array
  654. */
  655. public function _relate($rels = false)
  656. {
  657. if ($this->_frozen)
  658. {
  659. throw new FrozenObject('No changes allowed.');
  660. }
  661. if ($rels === false)
  662. {
  663. return $this->_data_relations;
  664. }
  665. elseif (is_array($rels))
  666. {
  667. $this->_data_relations = $rels;
  668. }
  669. else
  670. {
  671. throw new \FuelException('Invalid input for _relate(), should be an array.');
  672. }
  673. }
  674. /**
  675. * Fetch a property or relation
  676. *
  677. * @param string
  678. * @return mixed
  679. */
  680. public function & __get($property)
  681. {
  682. return $this->get($property);
  683. }
  684. /**
  685. * Set a property or relation
  686. *
  687. * @param string
  688. * @param mixed
  689. */
  690. public function __set($property, $value)
  691. {
  692. return $this->set($property, $value);
  693. }
  694. /**
  695. * Check whether a property exists, only return true for table columns and relations
  696. *
  697. * @param string $property
  698. * @return bool
  699. */
  700. public function __isset($property)
  701. {
  702. if (array_key_exists($property, static::properties()))
  703. {
  704. return true;
  705. }
  706. elseif (static::relations($property))
  707. {
  708. return true;
  709. }
  710. return false;
  711. }
  712. /**
  713. * Empty a property or relation
  714. *
  715. * @param string $property
  716. */
  717. public function __unset($property)
  718. {
  719. if (array_key_exists($property, static::properties()))
  720. {
  721. $this->_data[$property] = null;
  722. }
  723. elseif ($rel = static::relations($property))
  724. {
  725. $this->_data_relations[$property] = $rel->singular ? null : array();
  726. }
  727. }
  728. /**
  729. * Get
  730. *
  731. * Gets a property or
  732. * relation from the
  733. * object
  734. *
  735. * @access public
  736. * @param string $property
  737. * @return mixed
  738. */
  739. public function & get($property)
  740. {
  741. if (array_key_exists($property, static::properties()))
  742. {
  743. if ( ! array_key_exists($property, $this->_data))
  744. {
  745. $this->_data[$property] = null;
  746. }
  747. return $this->_data[$property];
  748. }
  749. elseif ($rel = static::relations($property))
  750. {
  751. if ( ! array_key_exists($property, $this->_data_relations))
  752. {
  753. $this->_data_relations[$property] = $rel->get($this);
  754. $this->_update_original_relations(array($property));
  755. }
  756. return $this->_data_relations[$property];
  757. }
  758. elseif ($this->_view and in_array($property, static::$_views_cached[get_class($this)][$this->_view]['columns']))
  759. {
  760. return $this->_data[$property];
  761. }
  762. else
  763. {
  764. throw new \OutOfBoundsException('Property "'.$property.'" not found for '.get_called_class().'.');
  765. }
  766. }
  767. /**
  768. * Set
  769. *
  770. * Sets a property or
  771. * relation of the
  772. * object
  773. *
  774. * @access public
  775. * @param string $property
  776. * @param string $value
  777. * @return Orm\Model
  778. */
  779. public function set($property, $value)
  780. {
  781. if ($this->_frozen)
  782. {
  783. throw new FrozenObject('No changes allowed.');
  784. }
  785. if (in_array($property, static::primary_key()) and $this->{$property} !== null)
  786. {
  787. throw new \FuelException('Primary key cannot be changed.');
  788. }
  789. if (array_key_exists($property, static::properties()))
  790. {
  791. $this->_data[$property] = $value;
  792. }
  793. elseif (static::relations($property))
  794. {
  795. $this->_data_relations[$property] = $value;
  796. }
  797. else
  798. {
  799. throw new \OutOfBoundsException('Property "'.$property.'" not found for '.get_called_class().'.');
  800. }
  801. return $this;
  802. }
  803. /**
  804. * Values
  805. *
  806. * Short way of setting the values
  807. * for the object as opposed to setting
  808. * each one individually
  809. *
  810. * @access public
  811. * @param array $values
  812. * @return Orm\Model
  813. */
  814. public function values(Array $data)
  815. {
  816. foreach ($data as $property => $value)
  817. {
  818. $this->set($property, $value);
  819. }
  820. return $this;
  821. }
  822. /**
  823. * Save the object and it's relations, create when necessary
  824. *
  825. * @param mixed $cascade
  826. * null = use default config,
  827. * bool = force/prevent cascade,
  828. * array cascades only the relations that are in the array
  829. */
  830. public function save($cascade = null, $use_transaction = false)
  831. {
  832. if ($this->frozen())
  833. {
  834. return false;
  835. }
  836. if ($use_transaction)
  837. {
  838. $db = \Database_Connection::instance(static::connection());
  839. $db->start_transaction();
  840. }
  841. try
  842. {
  843. $this->observe('before_save');
  844. $this->freeze();
  845. foreach($this->relations() as $rel_name => $rel)
  846. {
  847. if (array_key_exists($rel_name, $this->_data_relations))
  848. {
  849. $rel->save($this, $this->{$rel_name},
  850. array_key_exists($rel_name, $this->_original_relations) ? $this->_original_relations[$rel_name] : null,
  851. false, is_array($cascade) ? in_array($rel_name, $cascade) : $cascade
  852. );
  853. }
  854. }
  855. $this->unfreeze();
  856. // Insert or update
  857. $return = $this->_is_new ? $this->create() : $this->update();
  858. $this->freeze();
  859. foreach($this->relations() as $rel_name => $rel)
  860. {
  861. if (array_key_exists($rel_name, $this->_data_relations))
  862. {
  863. $rel->save($this, $this->{$rel_name},
  864. array_key_exists($rel_name, $this->_original_relations) ? $this->_original_relations[$rel_name] : null,
  865. true, is_array($cascade) ? in_array($rel_name, $cascade) : $cascade
  866. );
  867. }
  868. }
  869. $this->unfreeze();
  870. $this->_update_original();
  871. $this->observe('after_save');
  872. $use_transaction and $db->commit_transaction();
  873. }
  874. catch (\Exception $e)
  875. {
  876. $use_transaction and $db->rollback_transaction();
  877. throw $e;
  878. }
  879. return $return;
  880. }
  881. /**
  882. * Save using INSERT
  883. */
  884. protected function create()
  885. {
  886. // Only allow creation with new object, otherwise: clone first, create later
  887. if ( ! $this->is_new())
  888. {
  889. return false;
  890. }
  891. $this->observe('before_insert');
  892. // Set all current values
  893. $query = Query::forge(get_called_class(), static::connection());
  894. $primary_key = static::primary_key();
  895. $properties = array_keys(static::properties());
  896. foreach ($properties as $p)
  897. {
  898. if ( ! (in_array($p, $primary_key) and is_null($this->{$p})))
  899. {
  900. $query->set($p, $this->{$p});
  901. }
  902. }
  903. // Insert!
  904. $id = $query->insert();
  905. // when there's one PK it might be auto-incremented, get it and set it
  906. if (count($primary_key) == 1 and $id !== false)
  907. {
  908. $pk = reset($primary_key);
  909. if ($this->{$pk} === null)
  910. {
  911. $this->{$pk} = $id;
  912. }
  913. }
  914. // update the original properties on creation and cache object for future retrieval in this request
  915. $this->_is_new = false;
  916. static::$_cached_objects[get_class($this)][static::implode_pk($this)] = $this;
  917. $this->observe('after_insert');
  918. return $id !== false;
  919. }
  920. /**
  921. * Save using UPDATE
  922. */
  923. protected function update()
  924. {
  925. // New objects can't be updated, neither can frozen
  926. if ($this->is_new())
  927. {
  928. return false;
  929. }
  930. // Non changed objects don't have to be saved, but return true anyway (no reason to fail)
  931. if ( ! $this->is_changed(array_keys(static::properties())))
  932. {
  933. return true;
  934. }
  935. $this->observe('before_update');
  936. // Create the query and limit to primary key(s)
  937. $query = Query::forge(get_called_class(), static::connection())->limit(1);
  938. $primary_key = static::primary_key();
  939. $properties = array_keys(static::properties());
  940. foreach ($primary_key as $pk)
  941. {
  942. $query->where($pk, '=', $this->_data[$pk]);
  943. }
  944. // Set all current values
  945. foreach ($properties as $p)
  946. {
  947. if ( ! in_array($p, $primary_key))
  948. {
  949. $query->set($p, isset($this->_data[$p]) ? $this->_data[$p] : null);
  950. }
  951. }
  952. // Return false when update fails
  953. if ( ! $query->update())
  954. {
  955. return false;
  956. }
  957. // update the original property on success
  958. $this->observe('after_update');
  959. return true;
  960. }
  961. /**
  962. * Delete current object
  963. *
  964. * @param mixed $cascade
  965. * null = use default config,
  966. * bool = force/prevent cascade,
  967. * array cascades only the relations that are in the array
  968. * @return Model this instance as a new object without primary key(s)
  969. */
  970. public function delete($cascade = null, $use_transaction = false)
  971. {
  972. // New objects can't be deleted, neither can frozen
  973. if ($this->is_new() or $this->frozen())
  974. {
  975. return false;
  976. }
  977. if ($use_transaction)
  978. {
  979. $db = \Database_Connection::instance(static::connection());
  980. $db->start_transaction();
  981. }
  982. try
  983. {
  984. $this->observe('before_delete');
  985. $this->freeze();
  986. foreach($this->relations() as $rel_name => $rel)
  987. {
  988. $rel->delete($this, $this->{$rel_name}, false, is_array($cascade) ? in_array($rel_name, $cascade) : $cascade);
  989. }
  990. $this->unfreeze();
  991. // Create the query and limit to primary key(s)
  992. $query = Query::forge(get_called_class(), static::connection())->limit(1);
  993. $primary_key = static::primary_key();
  994. foreach ($primary_key as $pk)
  995. {
  996. $query->where($pk, '=', $this->{$pk});
  997. }
  998. // Return success of update operation
  999. if ( ! $query->delete())
  1000. {
  1001. return false;
  1002. }
  1003. $this->freeze();
  1004. foreach($this->relations() as $rel_name => $rel)
  1005. {
  1006. $rel->delete($this, $this->{$rel_name}, true, is_array($cascade) ? in_array($rel_name, $cascade) : $cascade);
  1007. }
  1008. $this->unfreeze();
  1009. // Perform cleanup:
  1010. // remove from internal object cache, remove PK's, set to non saved object, remove db original values
  1011. if (array_key_exists(get_called_class(), static::$_cached_objects)
  1012. and array_key_exists(static::implode_pk($this), static::$_cached_objects[get_called_class()]))
  1013. {
  1014. unset(static::$_cached_objects[get_called_class()][static::implode_pk($this)]);
  1015. }
  1016. foreach ($this->primary_key() as $pk)
  1017. {
  1018. unset($this->_data[$pk]);
  1019. }
  1020. $this->_is_new = true;
  1021. $this->_original = array();
  1022. $this->observe('after_delete');
  1023. $use_transaction and $db->commit_transaction();
  1024. }
  1025. catch (\Exception $e)
  1026. {
  1027. $use_transaction and $db->rollback_transaction();
  1028. throw $e;
  1029. }
  1030. return $this;
  1031. }
  1032. /**
  1033. * Reset values to those gotten from the database
  1034. */
  1035. public function reset()
  1036. {
  1037. foreach ($this->_original as $p => $val)
  1038. {
  1039. $this->_data[$p] = $val;
  1040. }
  1041. }
  1042. /**
  1043. * Calls all observers for the current event
  1044. *
  1045. * @param string
  1046. */
  1047. public function observe($event)
  1048. {
  1049. foreach ($this->observers() as $observer => $settings)
  1050. {
  1051. $events = isset($settings['events']) ? $settings['events'] : array();
  1052. if (empty($events) or in_array($event, $events))
  1053. {
  1054. if ( ! class_exists($observer))
  1055. {
  1056. $observer_class = \Inflector::get_namespace($observer).'Observer_'.\Inflector::denamespace($observer);
  1057. if ( ! class_exists($observer_class))
  1058. {
  1059. throw new \UnexpectedValueException($observer);
  1060. }
  1061. // Add the observer with the full classname for next usage
  1062. unset(static::$_observers_cached[$observer]);
  1063. static::$_observers_cached[$observer_class] = $events;
  1064. $observer = $observer_class;
  1065. }
  1066. try
  1067. {
  1068. call_user_func(array($observer, 'orm_notify'), $this, $event);
  1069. }
  1070. catch (\Exception $e)
  1071. {
  1072. // Unfreeze before failing
  1073. $this->unfreeze();
  1074. throw $e;
  1075. }
  1076. }
  1077. }
  1078. }
  1079. /**
  1080. * Compare current state with the retrieved state
  1081. *
  1082. * @param string|array $property
  1083. * @return bool
  1084. */
  1085. public function is_changed($property = null)
  1086. {
  1087. $properties = static::properties();
  1088. $relations = static::relations();
  1089. $property = (array) $property ?: array_merge(array_keys($properties), array_keys($relations));
  1090. foreach ($property as $p)
  1091. {
  1092. if (isset($properties[$p]))
  1093. {
  1094. if ( ! isset($this->_original[$p]) or $this->{$p} !== $this->_original[$p])
  1095. {
  1096. return true;
  1097. }
  1098. }
  1099. elseif (isset($relations[$p]))
  1100. {
  1101. if ($relations[$p]->singular)
  1102. {
  1103. if (empty($this->_original_relations[$p]) !== empty($this->_data_relations[$p])
  1104. or ( ! empty($this->_original_relations[$p])
  1105. and $this->_original_relations[$p] !== $this->_data_relations[$p]->implode_pk($this->{$p})))
  1106. {
  1107. return true;
  1108. }
  1109. }
  1110. else
  1111. {
  1112. if (empty($this->_original_relations[$p]))
  1113. {
  1114. if ( ! empty($this->_data_relations[$p]))
  1115. {
  1116. return true;
  1117. }
  1118. continue;
  1119. }
  1120. $orig_rels = $this->_original_relations[$p];
  1121. foreach ($this->{$p} as $rk => $r)
  1122. {
  1123. if ( ! in_array($r->implode_pk($r), $orig_rels))
  1124. {
  1125. return true;
  1126. }
  1127. unset($orig_rels[array_search($rk, $orig_rels)]);
  1128. }
  1129. if ( ! empty($orig_rels))
  1130. {
  1131. return true;
  1132. }
  1133. }
  1134. }
  1135. else
  1136. {
  1137. throw new \OutOfBoundsException('Unknown property or relation: '.$p);
  1138. }
  1139. }
  1140. return false;
  1141. }
  1142. /**
  1143. * Generates an array with keys new & old that contain ONLY the values that differ between the original and
  1144. * the current unsaved model.
  1145. * Note: relations are given as single or array of imploded pks
  1146. *
  1147. * @return array
  1148. */
  1149. public function get_diff()
  1150. {
  1151. $diff = array(0 => array(), 1 => array());
  1152. foreach ($this->_data as $key => $val)
  1153. {
  1154. if ($this->is_changed($key))
  1155. {
  1156. $diff[0][$key] = $this->_original[$key];
  1157. $diff[1][$key] = $val;
  1158. }
  1159. }
  1160. foreach ($this->_data_relations as $key => $val)
  1161. {
  1162. $rel = static::relations($key);
  1163. if ($rel->singular)
  1164. {
  1165. if ((($new_pk = $val->implode_pk($val)) and ! isset($this->_original_relations[$key]))
  1166. or $new_pk != $this->_original_relations[$key])
  1167. {
  1168. $diff[0][$key] = isset($this->_original_relations[$key]) ? $this->_original_relations[$key] : null;
  1169. $diff[1][$key] = $new_pk;
  1170. }
  1171. }
  1172. else
  1173. {
  1174. $original_pks = $this->_original_relations[$key];
  1175. foreach ($val as $v)
  1176. {
  1177. if ( ! in_array(($new_pk = $v->implode_pk($v)), $original_pks))
  1178. {
  1179. $diff[0][$key] = null;
  1180. $diff[1][$key] = $new_pk;
  1181. }
  1182. else
  1183. {
  1184. $original_pks = array_diff($original_pks, array($new_pk));
  1185. }
  1186. $diff[0][$key] += $original_pks;
  1187. }
  1188. }
  1189. }
  1190. return $diff;
  1191. }
  1192. /***
  1193. * Returns whether this is a saved or a new object
  1194. *
  1195. * @return bool
  1196. */
  1197. public function is_new()
  1198. {
  1199. return $this->_is_new;
  1200. }
  1201. /**
  1202. * Check whether the object was frozen
  1203. *
  1204. * @return boolean
  1205. */
  1206. public function frozen()
  1207. {
  1208. return $this->_frozen;
  1209. }
  1210. /**
  1211. * Freeze the object to disallow changing it or saving it
  1212. */
  1213. public function freeze()
  1214. {
  1215. $this->_frozen = true;
  1216. }
  1217. /**
  1218. * Unfreeze the object to allow changing it or saving it again
  1219. */
  1220. public function unfreeze()
  1221. {
  1222. $this->_frozen = false;
  1223. }
  1224. /**
  1225. * Allow object cloning to new object
  1226. */
  1227. public function __clone()
  1228. {
  1229. // Reset primary keys
  1230. foreach (static::$_primary_key as $pk)
  1231. {
  1232. $this->_data[$pk] = null;
  1233. }
  1234. // This is a new object
  1235. $this->_is_new = true;
  1236. $this->_original = array();
  1237. $this->_original_relations = array();
  1238. // Cleanup relations
  1239. foreach ($this->relations() as $name => $rel)
  1240. {
  1241. // singular relations (hasone, belongsto) can't be copied, neither can HasMany
  1242. if ($rel->singular or $rel instanceof HasMany)
  1243. {
  1244. unset($this->_data_relations[$name]);
  1245. }
  1246. }
  1247. $this->observe('after_clone');
  1248. }
  1249. /**
  1250. * Method for use with Fieldset::add_model()
  1251. *
  1252. * @param Fieldset Fieldset instance to add fields to
  1253. * @param array|Model Model instance or array for use to repopulate
  1254. */
  1255. public static function set_form_fields($form, $instance = null)
  1256. {
  1257. Observer_Validation::set_fields($instance instanceof static ? $instance : get_called_class(), $form);
  1258. $instance and $form->repopulate($instance);
  1259. }
  1260. /**
  1261. * Implementation of ArrayAccess
  1262. */
  1263. public function offsetSet($offset, $value)
  1264. {
  1265. try
  1266. {
  1267. $this->__set($offset, $value);
  1268. }
  1269. catch (\Exception $e)
  1270. {
  1271. return false;
  1272. }
  1273. }
  1274. public function offsetExists($offset)
  1275. {
  1276. return $this->__isset($offset);
  1277. }
  1278. public function offsetUnset($offset)
  1279. {
  1280. $this->__unset($offset);
  1281. }
  1282. public function offsetGet($offset)
  1283. {
  1284. try
  1285. {
  1286. return $this->__get($offset);
  1287. }
  1288. catch (\Exception $e)
  1289. {
  1290. return false;
  1291. }
  1292. }
  1293. /**
  1294. * Implementation of Iterable
  1295. */
  1296. protected $_iterable = array();
  1297. public function rewind()
  1298. {
  1299. $this->_iterable = array_merge($this->_data, $this->_data_relations);
  1300. reset($this->_iterable);
  1301. }
  1302. public function current()
  1303. {
  1304. return current($this->_iterable);
  1305. }
  1306. public function key()
  1307. {
  1308. return key($this->_iterable);
  1309. }
  1310. public function next()
  1311. {
  1312. return next($this->_iterable);
  1313. }
  1314. public function valid()
  1315. {
  1316. return key($this->_iterable) !== null;
  1317. }
  1318. /**
  1319. * Allow populating this object from an array
  1320. *
  1321. * @return array
  1322. */
  1323. public function from_array(array $values)
  1324. {
  1325. foreach($values as $property => $value)
  1326. {
  1327. if (array_key_exists($property, static::properties()) and ! in_array($property, static::primary_key()))
  1328. {
  1329. $this->_data[$property] = $value;
  1330. }
  1331. }
  1332. }
  1333. /**
  1334. * Allow converting this object to an array
  1335. *
  1336. * @return array
  1337. */
  1338. public function to_array()
  1339. {
  1340. $array = array();
  1341. // make sure all data is scalar or array
  1342. foreach ($this->_data as $key => $val)
  1343. {
  1344. if (is_object($val))
  1345. {
  1346. if (method_exists($val, '__toString'))
  1347. {
  1348. $val = (string) $val;
  1349. }
  1350. else
  1351. {
  1352. $val = get_object_vars($val);
  1353. }
  1354. }
  1355. $array[$key] = $val;
  1356. }
  1357. // convert relations
  1358. foreach ($this->_data_relations as $name => $rel)
  1359. {
  1360. if (is_array($rel))
  1361. {
  1362. $array[$name] = array();
  1363. foreach ($rel as $id => $r)
  1364. {
  1365. $array[$name][$id] = $r->to_array();
  1366. }
  1367. }
  1368. else
  1369. {
  1370. $array[$name] = is_null($rel) ? null : $rel->to_array();
  1371. }
  1372. }
  1373. return $array;
  1374. }
  1375. /**
  1376. * Allow for getter, setter and unset methods
  1377. *
  1378. * @param string $method
  1379. * @param array $args
  1380. * @return mixed
  1381. * @throws \BadMethodCallException
  1382. */
  1383. public function __call($method, $args)
  1384. {
  1385. if (substr($method, 0, 4) == 'get_')
  1386. {
  1387. return $this->get(substr($method, 4));
  1388. }
  1389. elseif (substr($method, 0, 4) == 'set_')
  1390. {
  1391. return $this->set(substr($method, 4), reset($args));
  1392. }
  1393. elseif (substr($method, 0, 6) == 'unset_')
  1394. {
  1395. return $this->__unset(substr($method, 6));
  1396. }
  1397. // Throw an exception
  1398. throw new \BadMethodCallException('Call to undefined method '.get_class($this).'::'.$method.'()');
  1399. }
  1400. }