PageRenderTime 68ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

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

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