PageRenderTime 69ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://bitbucket.org/trujka/codegrounds
PHP | 2137 lines | 1362 code | 227 blank | 548 comment | 147 complexity | 9157fd5632b54cb3e1008398db8cde4a MD5 | raw file
Possible License(s): MIT, BSD-3-Clause, LGPL-2.1
  1. <?php
  2. /**
  3. * Fuel
  4. *
  5. * Fuel is a fast, lightweight, community driven PHP5 framework.
  6. *
  7. * @package Fuel
  8. * @version 1.6
  9. * @author Fuel Development Team
  10. * @license MIT License
  11. * @copyright 2010 - 2013 Fuel Development Team
  12. * @link http://fuelphp.com
  13. */
  14. namespace Orm;
  15. /**
  16. * Record Not Found Exception
  17. */
  18. class RecordNotFound extends \OutOfBoundsException {}
  19. /**
  20. * Frozen Object Exception
  21. */
  22. class FrozenObject extends \RuntimeException {}
  23. class Model implements \ArrayAccess, \Iterator
  24. {
  25. /* ---------------------------------------------------------------------------
  26. * Static usage
  27. * --------------------------------------------------------------------------- */
  28. /**
  29. * @var string connection to use
  30. */
  31. // protected static $_connection = null;
  32. /**
  33. * @var string write connection to use
  34. */
  35. // protected static $_write_connection = null;
  36. /**
  37. * @var string table name to overwrite assumption
  38. */
  39. // protected static $_table_name;
  40. /**
  41. * @var array array of object properties
  42. */
  43. // protected static $_properties;
  44. /**
  45. * @var array array of views with additional properties
  46. */
  47. // protected static $_views;
  48. /**
  49. * @var array array of observer classes to use
  50. */
  51. // protected static $_observers;
  52. /**
  53. * @var array relationship properties
  54. */
  55. // protected static $_has_one;
  56. // protected static $_belongs_to;
  57. // protected static $_has_many;
  58. // protected static $_many_many;
  59. // protected static $_eav;
  60. /**
  61. * @var array name or names of the primary keys
  62. */
  63. protected static $_primary_key = array('id');
  64. /**
  65. * @var array name or columns that need to be excluded from any to_array() result
  66. */
  67. protected static $_to_array_exclude = array();
  68. /**
  69. * @var array cached tables
  70. */
  71. protected static $_table_names_cached = array();
  72. /**
  73. * @var array cached properties
  74. */
  75. protected static $_properties_cached = array();
  76. /**
  77. * @var array cached properties
  78. */
  79. protected static $_views_cached = array();
  80. /**
  81. * @var string relationships
  82. */
  83. protected static $_relations_cached = array();
  84. /**
  85. * @var array cached observers
  86. */
  87. protected static $_observers_cached = array();
  88. /**
  89. * @var array array of fetched objects
  90. */
  91. protected static $_cached_objects = array();
  92. /**
  93. * @var array array of valid relation types
  94. */
  95. protected static $_valid_relations = array(
  96. 'belongs_to' => 'Orm\\BelongsTo',
  97. 'has_one' => 'Orm\\HasOne',
  98. 'has_many' => 'Orm\\HasMany',
  99. 'many_many' => 'Orm\\ManyMany',
  100. );
  101. public static function forge($data = array(), $new = true, $view = null, $cache = true)
  102. {
  103. return new static($data, $new, $view, $cache);
  104. }
  105. /**
  106. * Fetch the database connection name to use
  107. *
  108. * @param bool if true return the writeable connection (if set)
  109. * @return null|string
  110. */
  111. public static function connection($writeable = false)
  112. {
  113. $class = get_called_class();
  114. if ($writeable and property_exists($class, '_write_connection'))
  115. {
  116. return static::$_write_connection;
  117. }
  118. return property_exists($class, '_connection') ? static::$_connection : null;
  119. }
  120. /**
  121. * Get the table name for this class
  122. *
  123. * @return string
  124. */
  125. public static function table()
  126. {
  127. $class = get_called_class();
  128. // Table name unknown
  129. if ( ! array_key_exists($class, static::$_table_names_cached))
  130. {
  131. // Table name set in Model
  132. if (property_exists($class, '_table_name'))
  133. {
  134. static::$_table_names_cached[$class] = static::$_table_name;
  135. }
  136. else
  137. {
  138. static::$_table_names_cached[$class] = \Inflector::tableize($class);
  139. }
  140. }
  141. return static::$_table_names_cached[$class];
  142. }
  143. /**
  144. * Get a defined condition for this class
  145. *
  146. * @param string type of condition to return
  147. * @return array
  148. */
  149. public static function condition($type = null)
  150. {
  151. $class = get_called_class();
  152. // a specific condition requested?
  153. if (property_exists($class, '_conditions'))
  154. {
  155. if ($type !== null)
  156. {
  157. return isset(static::$_conditions[$type]) ? static::$_conditions[$type] : array();
  158. }
  159. else
  160. {
  161. return static::$_conditions;
  162. }
  163. }
  164. else
  165. {
  166. return array();
  167. }
  168. }
  169. /**
  170. * Attempt to retrieve an earlier loaded object
  171. *
  172. * @param array|Model $obj
  173. * @param null|string $class
  174. * @return Model|false
  175. */
  176. public static function cached_object($obj, $class = null)
  177. {
  178. $class = $class ?: get_called_class();
  179. $id = (is_int($obj) or is_string($obj)) ? (string) $obj : $class::implode_pk($obj);
  180. $result = ( ! empty(static::$_cached_objects[$class][$id])) ? static::$_cached_objects[$class][$id] : false;
  181. return $result;
  182. }
  183. /**
  184. * Get the primary key(s) of this class
  185. *
  186. * @return array
  187. */
  188. public static function primary_key()
  189. {
  190. return static::$_primary_key;
  191. }
  192. /**
  193. * Implode the primary keys within the data into a string
  194. *
  195. * @param array
  196. * @return string
  197. */
  198. public static function implode_pk($data)
  199. {
  200. if (count(static::$_primary_key) == 1)
  201. {
  202. $p = reset(static::$_primary_key);
  203. return (is_object($data)
  204. ? strval($data->{$p})
  205. : (isset($data[$p])
  206. ? strval($data[$p])
  207. : null));
  208. }
  209. $pk = '';
  210. foreach (static::$_primary_key as $p)
  211. {
  212. if (is_null((is_object($data) ? $data->{$p} : (isset($data[$p]) ? $data[$p] : null))))
  213. {
  214. return null;
  215. }
  216. $pk .= '['.(is_object($data) ? $data->{$p} : $data[$p]).']';
  217. }
  218. return $pk;
  219. }
  220. /**
  221. * Get the class's properties
  222. *
  223. * @throws \FuelException Listing columns failed
  224. *
  225. * @return array
  226. */
  227. public static function properties()
  228. {
  229. $class = get_called_class();
  230. // If already determined
  231. if (array_key_exists($class, static::$_properties_cached))
  232. {
  233. return static::$_properties_cached[$class];
  234. }
  235. // Try to grab the properties from the class...
  236. if (property_exists($class, '_properties'))
  237. {
  238. $properties = static::$_properties;
  239. foreach ($properties as $key => $p)
  240. {
  241. if (is_string($p))
  242. {
  243. unset($properties[$key]);
  244. $properties[$p] = array();
  245. }
  246. }
  247. }
  248. // ...if the above failed, run DB query to fetch properties
  249. if (empty($properties))
  250. {
  251. try
  252. {
  253. $properties = \DB::list_columns(static::table(), null, static::connection());
  254. }
  255. catch (\Exception $e)
  256. {
  257. throw new \FuelException('Listing columns failed, you have to set the model properties with a '.
  258. 'static $_properties setting in the model. Original exception: '.$e->getMessage());
  259. }
  260. }
  261. // cache the properties for next usage
  262. static::$_properties_cached[$class] = $properties;
  263. return static::$_properties_cached[$class];
  264. }
  265. /**
  266. * Fetches a property description array, or specific data from it
  267. *
  268. * @param string property or property.key
  269. * @param mixed return value when key not present
  270. * @return mixed
  271. */
  272. public static function property($key, $default = null)
  273. {
  274. $class = get_called_class();
  275. // If already determined
  276. if ( ! array_key_exists($class, static::$_properties_cached))
  277. {
  278. static::properties();
  279. }
  280. return \Arr::get(static::$_properties_cached[$class], $key, $default);
  281. }
  282. /**
  283. * Fetch the model's views
  284. *
  285. * @throws \InvalidArgumentException Database view is defined without columns
  286. *
  287. * @return array
  288. */
  289. public static function views()
  290. {
  291. $class = get_called_class();
  292. if ( ! isset(static::$_views_cached[$class]))
  293. {
  294. static::$_views_cached[$class] = array();
  295. if (property_exists($class, '_views'))
  296. {
  297. $views = $class::$_views;
  298. foreach ($views as $k => $v)
  299. {
  300. if ( ! isset($v['columns']))
  301. {
  302. throw new \InvalidArgumentException('Database view '.$k.' is defined without columns.');
  303. }
  304. $v['columns'] = (array) $v['columns'];
  305. if ( ! isset($v['view']))
  306. {
  307. $v['view'] = $k;
  308. }
  309. static::$_views_cached[$class][$k] = $v;
  310. }
  311. }
  312. }
  313. return static::$_views_cached[$class];
  314. }
  315. /**
  316. * Get the class's relations
  317. *
  318. * @param bool $specific
  319. * @return HasOne|HasMany|ManyMany|Belongsto|HasOne[]|HasMany[]|ManyMany[]|Belongsto[]
  320. */
  321. public static function relations($specific = false)
  322. {
  323. $class = get_called_class();
  324. if ( ! array_key_exists($class, static::$_relations_cached))
  325. {
  326. $relations = array();
  327. foreach (static::$_valid_relations as $rel_name => $rel_class)
  328. {
  329. if (property_exists($class, '_'.$rel_name))
  330. {
  331. foreach (static::${'_'.$rel_name} as $key => $settings)
  332. {
  333. $name = is_string($settings) ? $settings : $key;
  334. $settings = is_array($settings) ? $settings : array();
  335. $relations[$name] = new $rel_class($class, $name, $settings);
  336. }
  337. }
  338. }
  339. static::$_relations_cached[$class] = $relations;
  340. }
  341. if ($specific === false)
  342. {
  343. return static::$_relations_cached[$class];
  344. }
  345. else
  346. {
  347. if ( ! array_key_exists($specific, static::$_relations_cached[$class]))
  348. {
  349. return false;
  350. }
  351. return static::$_relations_cached[$class][$specific];
  352. }
  353. }
  354. /**
  355. * Get the name of the class that defines a relation
  356. *
  357. * @param string
  358. * @return array
  359. */
  360. public static function related_class($relation)
  361. {
  362. $class = get_called_class();
  363. foreach (static::$_valid_relations as $rel_name => $rel_class)
  364. {
  365. if (property_exists($class, '_'.$rel_name))
  366. {
  367. if (isset(static::${'_'.$rel_name}[$relation]))
  368. {
  369. return static::${'_'.$rel_name}[$relation]['model_to'];
  370. }
  371. }
  372. }
  373. return null;
  374. }
  375. /**
  376. * Get the class's observers and what they observe
  377. *
  378. * @param string specific observer to retrieve info of, allows direct param access by using dot notation
  379. * @param mixed default return value when specific key wasn't found
  380. * @return array
  381. */
  382. public static function observers($specific = null, $default = null)
  383. {
  384. $class = get_called_class();
  385. if ( ! array_key_exists($class, static::$_observers_cached))
  386. {
  387. $observers = array();
  388. if (property_exists($class, '_observers'))
  389. {
  390. foreach (static::$_observers as $obs_k => $obs_v)
  391. {
  392. if (is_int($obs_k))
  393. {
  394. $observers[$obs_v] = array();
  395. }
  396. else
  397. {
  398. if (is_string($obs_v) or (is_array($obs_v) and is_int(key($obs_v))))
  399. {
  400. // @TODO deprecated until v1.4
  401. logger(\Fuel::L_WARNING, 'Passing observer events as array is deprecated, they must be
  402. inside another array under a key "events". Check the docs for more info.', __METHOD__);
  403. $observers[$obs_k] = array('events' => (array) $obs_v);
  404. }
  405. else
  406. {
  407. $observers[$obs_k] = $obs_v;
  408. }
  409. }
  410. }
  411. }
  412. static::$_observers_cached[$class] = $observers;
  413. }
  414. if ($specific)
  415. {
  416. return \Arr::get(static::$_observers_cached[$class], $specific, $default);
  417. }
  418. return static::$_observers_cached[$class];
  419. }
  420. /**
  421. * Register an observer
  422. *
  423. * @param string class name of the observer (including namespace)
  424. * @param mixed observer options
  425. *
  426. * @return void
  427. */
  428. public static function register_observer($name, $options = null)
  429. {
  430. $class = get_called_class();
  431. $new_observer = is_null($options) ? array($name) : array($name => $options);
  432. static::$_observers_cached[$class] = static::observers() + $new_observer;
  433. }
  434. /**
  435. * Unregister an observer
  436. *
  437. * @param string class name of the observer (including namespace)
  438. * @return void
  439. */
  440. public static function unregister_observer($name)
  441. {
  442. $class = get_called_class();
  443. foreach (static::observers() as $key => $value)
  444. {
  445. if ((is_array($value) and $key == $name) or $value == $name)
  446. {
  447. unset(static::$_observers_cached[$class][$key]);
  448. }
  449. }
  450. }
  451. /**
  452. * Find one or more entries
  453. *
  454. * @param int|null $id
  455. * @param array $options
  456. *
  457. * @throws \FuelException
  458. *
  459. * @return Model|Model[]
  460. */
  461. public static function find($id = null, array $options = array())
  462. {
  463. // deal with null valued PK's
  464. if (is_null($id))
  465. {
  466. // if no options are present, simply return null. a PK with a null value can exist
  467. return func_num_args() === 2 ? static::query($options) : null;
  468. }
  469. // Return all that match $options array
  470. elseif ($id === 'all')
  471. {
  472. return static::query($options)->get();
  473. }
  474. // Return first or last row that matches $options array
  475. elseif ($id === 'first' or $id === 'last')
  476. {
  477. $query = static::query($options);
  478. foreach(static::primary_key() as $pk)
  479. {
  480. $query->order_by($pk, $id == 'first' ? 'ASC' : 'DESC');
  481. }
  482. return $query->get_one();
  483. }
  484. // Return specific request row by ID
  485. else
  486. {
  487. $cache_pk = $where = array();
  488. $id = (array) $id;
  489. foreach (static::primary_key() as $pk)
  490. {
  491. $where[] = array($pk, '=', current($id));
  492. $cache_pk[$pk] = current($id);
  493. next($id);
  494. }
  495. if (array_key_exists(get_called_class(), static::$_cached_objects)
  496. and array_key_exists(static::implode_pk($cache_pk), static::$_cached_objects[get_called_class()])
  497. and (! isset($options['from_cache']) or $options['from_cache'] == true))
  498. {
  499. return static::$_cached_objects[get_called_class()][static::implode_pk($cache_pk)];
  500. }
  501. array_key_exists('where', $options) and $where = array_merge($options['where'], $where);
  502. $options['where'] = $where;
  503. return static::query($options)->get_one();
  504. }
  505. }
  506. /**
  507. * Creates a new query with optional settings up front
  508. *
  509. * @param array
  510. * @return Query
  511. */
  512. public static function query($options = array())
  513. {
  514. return Query::forge(get_called_class(), array(static::connection(), static::connection(true)), $options);
  515. }
  516. /**
  517. * Count entries, optionally only those matching the $options
  518. *
  519. * @param array
  520. * @return int
  521. */
  522. public static function count(array $options = array())
  523. {
  524. return static::query($options)->count();
  525. }
  526. /**
  527. * Find the maximum
  528. *
  529. * @param mixed
  530. * @param array
  531. * @return bool|int Maximum value or false
  532. */
  533. public static function max($key = null)
  534. {
  535. return static::query()->max($key ?: static::primary_key());
  536. }
  537. /**
  538. * Find the minimum
  539. *
  540. * @param mixed
  541. * @param array
  542. * @return object|array
  543. */
  544. public static function min($key = null)
  545. {
  546. return static::query()->min($key ?: static::primary_key());
  547. }
  548. public static function __callStatic($method, $args)
  549. {
  550. // Start with count_by? Get counting!
  551. if (strpos($method, 'count_by') === 0)
  552. {
  553. $find_type = 'count';
  554. $fields = substr($method, 9);
  555. }
  556. // Otherwise, lets find stuff
  557. elseif (strpos($method, 'find_') === 0)
  558. {
  559. if ($method == 'find_by')
  560. {
  561. $find_type = 'all';
  562. $fields = array_shift($args);
  563. }
  564. else
  565. {
  566. $find_type = strncmp($method, 'find_all_by_', 12) === 0 ? 'all' : (strncmp($method, 'find_by_', 8) === 0 ? 'first' : false);
  567. $fields = $find_type === 'first' ? substr($method, 8) : substr($method, 12);
  568. }
  569. }
  570. // God knows, complain
  571. else
  572. {
  573. throw new \FuelException('Invalid method call. Method '.$method.' does not exist.', 0);
  574. }
  575. $where = $or_where = array();
  576. if (($and_parts = explode('_and_', $fields)))
  577. {
  578. foreach ($and_parts as $and_part)
  579. {
  580. $or_parts = explode('_or_', $and_part);
  581. if (count($or_parts) == 1)
  582. {
  583. $where[] = array($or_parts[0], array_shift($args));
  584. }
  585. else
  586. {
  587. foreach($or_parts as $or_part)
  588. {
  589. $or_where[] = array($or_part, array_shift($args));
  590. }
  591. }
  592. }
  593. }
  594. $options = count($args) > 0 ? array_pop($args) : array();
  595. if ( ! empty($where))
  596. {
  597. if ( ! array_key_exists('where', $options))
  598. {
  599. $options['where'] = $where;
  600. }
  601. else
  602. {
  603. $options['where'] = array_merge($where, $options['where']);
  604. }
  605. }
  606. if ( ! empty($or_where))
  607. {
  608. if ( ! array_key_exists('or_where', $options))
  609. {
  610. $options['or_where'] = $or_where;
  611. }
  612. else
  613. {
  614. $options['or_where'] = array_merge($or_where, $options['or_where']);
  615. }
  616. }
  617. if ($find_type == 'count')
  618. {
  619. return static::count($options);
  620. }
  621. else
  622. {
  623. return static::find($find_type, $options);
  624. }
  625. // min_...($options)
  626. // max_...($options)
  627. }
  628. /* ---------------------------------------------------------------------------
  629. * Object usage
  630. * --------------------------------------------------------------------------- */
  631. /**
  632. * @var bool keeps track of whether it's a new object
  633. */
  634. protected $_is_new = true;
  635. /**
  636. * @var bool keeps to object frozen
  637. */
  638. protected $_frozen = false;
  639. /**
  640. * @var array keeps the current state of the object
  641. */
  642. protected $_data = array();
  643. /**
  644. * @var array storage for custom properties on this object
  645. */
  646. protected $_custom_data = array();
  647. /**
  648. * @var array keeps a copy of the object as it was retrieved from the database
  649. */
  650. protected $_original = array();
  651. /**
  652. * @var array
  653. */
  654. protected $_data_relations = array();
  655. /**
  656. * @var array keeps a copy of the relation ids that were originally retrieved from the database
  657. */
  658. protected $_original_relations = array();
  659. /**
  660. * @var array keeps track of relations that need to be reset before saving the new ones
  661. */
  662. protected $_reset_relations = array();
  663. /**
  664. * @var string view name when used
  665. */
  666. protected $_view;
  667. /**
  668. * Constructor
  669. *
  670. * @param array
  671. * @param bool
  672. */
  673. public function __construct(array $data = array(), $new = true, $view = null, $cache = true)
  674. {
  675. // This is to deal with PHP's native hydration that happens before constructor is called
  676. // for some weird reason, for example using the DB's as_object() function
  677. if( ! empty($this->_data))
  678. {
  679. $this->_original = $this->_data;
  680. $new = false;
  681. }
  682. if ($new)
  683. {
  684. $properties = $this->properties();
  685. foreach ($properties as $prop => $settings)
  686. {
  687. if (array_key_exists($prop, $data))
  688. {
  689. $this->_data[$prop] = $data[$prop];
  690. unset($data[$prop]);
  691. }
  692. elseif (array_key_exists('default', $settings))
  693. {
  694. $this->_data[$prop] = $settings['default'];
  695. }
  696. }
  697. $this->_custom_data = $data;
  698. }
  699. else
  700. {
  701. $this->_update_original($data);
  702. $this->_data = array_merge($this->_data, $data);
  703. if ($view and array_key_exists($view, $this->views()))
  704. {
  705. $this->_view = $view;
  706. }
  707. }
  708. if ($new === false)
  709. {
  710. $cache and static::$_cached_objects[get_class($this)][static::implode_pk($data)] = $this;
  711. $this->_is_new = false;
  712. $this->observe('after_load');
  713. }
  714. else
  715. {
  716. $this->observe('after_create');
  717. }
  718. }
  719. /**
  720. * Update the original setting for this object
  721. *
  722. * @param array|null $original
  723. */
  724. public function _update_original($original = null)
  725. {
  726. $original = is_null($original) ? $this->_data : $original;
  727. $this->_original = array_merge($this->_original, $original);
  728. $this->_update_original_relations();
  729. }
  730. /**
  731. * Update the original relations for this object
  732. */
  733. public function _update_original_relations($relations = null)
  734. {
  735. if (is_null($relations))
  736. {
  737. $this->_original_relations = array();
  738. $relations = $this->_data_relations;
  739. }
  740. else
  741. {
  742. foreach ($relations as $key => $rel)
  743. {
  744. // Unload the just fetched relation from the originals
  745. unset($this->_original_relations[$rel]);
  746. // Unset the numeric key and set the data to update by the relation name
  747. unset($relations[$key]);
  748. $relations[$rel] = $this->_data_relations[$rel];
  749. }
  750. }
  751. foreach ($relations as $rel => $data)
  752. {
  753. if (is_array($data))
  754. {
  755. $this->_original_relations[$rel] = array();
  756. foreach ($data as $obj)
  757. {
  758. $this->_original_relations[$rel][] = $obj ? $obj->implode_pk($obj) : null;
  759. }
  760. }
  761. else
  762. {
  763. $this->_original_relations[$rel] = $data ? $data->implode_pk($data) : null;
  764. }
  765. }
  766. }
  767. /**
  768. * Fetch or set relations on this object
  769. * To be used only after having fetched them from the database!
  770. *
  771. * @param array|bool|null $rels
  772. *
  773. * @throws \FuelException Invalid input for _relate(), should be an array
  774. * @throws FrozenObject No changes allowed
  775. *
  776. * @return void|array
  777. */
  778. public function _relate($rels = false)
  779. {
  780. if ($this->_frozen)
  781. {
  782. throw new FrozenObject('No changes allowed.');
  783. }
  784. if ($rels === false)
  785. {
  786. return $this->_data_relations;
  787. }
  788. elseif (is_array($rels))
  789. {
  790. $this->_data_relations = $rels;
  791. }
  792. else
  793. {
  794. throw new \FuelException('Invalid input for _relate(), should be an array.');
  795. }
  796. }
  797. /**
  798. * Fetch a property or relation
  799. *
  800. * @param string
  801. * @return mixed
  802. */
  803. public function & __get($property)
  804. {
  805. return $this->get($property);
  806. }
  807. /**
  808. * Set a property or relation
  809. *
  810. * @param string
  811. * @param mixed
  812. *
  813. * @return Model
  814. */
  815. public function __set($property, $value)
  816. {
  817. return $this->set($property, $value);
  818. }
  819. /**
  820. * Check whether a property exists, only return true for table columns, relations, eav and custom data
  821. *
  822. * @param string $property
  823. * @return bool
  824. */
  825. public function __isset($property)
  826. {
  827. if (array_key_exists($property, static::properties()))
  828. {
  829. return true;
  830. }
  831. elseif (static::relations($property))
  832. {
  833. return true;
  834. }
  835. elseif ($this->_get_eav($property, true))
  836. {
  837. return true;
  838. }
  839. elseif (array_key_exists($property, $this->_custom_data))
  840. {
  841. return true;
  842. }
  843. return false;
  844. }
  845. /**
  846. * Empty a property, relation or custom data
  847. *
  848. * @param string $property
  849. */
  850. public function __unset($property)
  851. {
  852. if (array_key_exists($property, static::properties()))
  853. {
  854. $this->_data[$property] = null;
  855. }
  856. elseif ($rel = static::relations($property))
  857. {
  858. $this->_reset_relations[$property] = true;
  859. $this->_data_relations[$property] = $rel->singular ? null : array();
  860. }
  861. elseif ($this->_get_eav($property, true, true))
  862. {
  863. // no additional work needed here
  864. }
  865. elseif (array_key_exists($property, $this->_custom_data))
  866. {
  867. unset($this->_custom_data[$property]);
  868. }
  869. }
  870. /**
  871. * Allow for getter, setter and unset methods
  872. *
  873. * @param string $method
  874. * @param array $args
  875. * @return mixed
  876. * @throws \BadMethodCallException
  877. */
  878. public function __call($method, $args)
  879. {
  880. if (substr($method, 0, 4) == 'get_')
  881. {
  882. return $this->get(substr($method, 4));
  883. }
  884. elseif (substr($method, 0, 4) == 'set_')
  885. {
  886. return $this->set(substr($method, 4), reset($args));
  887. }
  888. elseif (substr($method, 0, 6) == 'unset_')
  889. {
  890. return $this->__unset(substr($method, 6));
  891. }
  892. // Throw an exception
  893. throw new \BadMethodCallException('Call to undefined method '.get_class($this).'::'.$method.'()');
  894. }
  895. /**
  896. * Allow object cloning to new object
  897. */
  898. public function __clone()
  899. {
  900. // Reset primary keys
  901. foreach (static::$_primary_key as $pk)
  902. {
  903. $this->_data[$pk] = null;
  904. }
  905. // This is a new object
  906. $this->_is_new = true;
  907. $this->_original = array();
  908. $this->_original_relations = array();
  909. // Cleanup relations
  910. foreach ($this->relations() as $name => $rel)
  911. {
  912. // singular relations (hasone, belongsto) can't be copied, neither can HasMany
  913. if ($rel->singular or $rel instanceof HasMany)
  914. {
  915. unset($this->_data_relations[$name]);
  916. }
  917. }
  918. $this->observe('after_clone');
  919. }
  920. /**
  921. * Get
  922. *
  923. * Gets a property or
  924. * relation from the
  925. * object
  926. *
  927. * @access public
  928. * @param string $property
  929. * @return mixed
  930. */
  931. public function & get($property)
  932. {
  933. if (array_key_exists($property, static::properties()))
  934. {
  935. if ( ! array_key_exists($property, $this->_data))
  936. {
  937. // avoid a notice, we're returning by reference
  938. $var = null;
  939. return $var;
  940. }
  941. return $this->_data[$property];
  942. }
  943. elseif ($rel = static::relations($property))
  944. {
  945. if ( ! array_key_exists($property, $this->_data_relations))
  946. {
  947. if ($this->_frozen)
  948. {
  949. // avoid a notice, we're returning by reference
  950. $var = null;
  951. return $var;
  952. }
  953. $this->_data_relations[$property] = $rel->get($this);
  954. $this->_update_original_relations(array($property));
  955. }
  956. return $this->_data_relations[$property];
  957. }
  958. elseif (($value = $this->_get_eav($property)) !== false)
  959. {
  960. return $value;
  961. }
  962. elseif ($this->_view and in_array($property, static::$_views_cached[get_class($this)][$this->_view]['columns']))
  963. {
  964. return $this->_data[$property];
  965. }
  966. elseif (array_key_exists($property, $this->_custom_data))
  967. {
  968. return $this->_custom_data[$property];
  969. }
  970. else
  971. {
  972. throw new \OutOfBoundsException('Property "'.$property.'" not found for '.get_class($this).'.');
  973. }
  974. }
  975. /**
  976. * Set
  977. *
  978. * Sets a property or
  979. * relation of the
  980. * object
  981. *
  982. * @access public
  983. * @param string|array $property
  984. * @param string $value in case $property is a string
  985. *
  986. * @throws \FuelException Primary key on model cannot be changed
  987. * @throws \InvalidArgumentException You need to pass both a property name and a value to set()
  988. * @throws FrozenObject No changes allowed
  989. *
  990. * @return Model
  991. */
  992. public function set($property, $value = null)
  993. {
  994. if ($this->_frozen)
  995. {
  996. throw new FrozenObject('No changes allowed.');
  997. }
  998. if (is_array($property))
  999. {
  1000. foreach ($property as $p => $v)
  1001. {
  1002. $this->set($p, $v);
  1003. }
  1004. }
  1005. else
  1006. {
  1007. if (func_num_args() < 2)
  1008. {
  1009. throw new \InvalidArgumentException('You need to pass both a property name and a value to set().');
  1010. }
  1011. if (in_array($property, static::primary_key()) and $this->{$property} !== null)
  1012. {
  1013. throw new \FuelException('Primary key on model '.get_class($this).' cannot be changed.');
  1014. }
  1015. if (array_key_exists($property, static::properties()))
  1016. {
  1017. $this->_data[$property] = $value;
  1018. }
  1019. elseif (static::relations($property))
  1020. {
  1021. $this->is_fetched($property) or $this->_reset_relations[$property] = true;
  1022. $this->_data_relations[$property] = $value;
  1023. }
  1024. elseif ( ! $this->_set_eav($property, $value))
  1025. {
  1026. $this->_custom_data[$property] = $value;
  1027. }
  1028. }
  1029. return $this;
  1030. }
  1031. /**
  1032. * Save the object and it's relations, create when necessary
  1033. *
  1034. * @param mixed $cascade
  1035. * null = use default config,
  1036. * bool = force/prevent cascade,
  1037. * array cascades only the relations that are in the array
  1038. *
  1039. * @return bool
  1040. */
  1041. public function save($cascade = null, $use_transaction = false)
  1042. {
  1043. if ($this->frozen())
  1044. {
  1045. return false;
  1046. }
  1047. if ($use_transaction)
  1048. {
  1049. $db = \Database_Connection::instance(static::connection(true));
  1050. $db->start_transaction();
  1051. }
  1052. try
  1053. {
  1054. $this->observe('before_save');
  1055. $this->freeze();
  1056. foreach($this->relations() as $rel_name => $rel)
  1057. {
  1058. if (array_key_exists($rel_name, $this->_reset_relations))
  1059. {
  1060. if (method_exists($rel, 'delete_related'))
  1061. {
  1062. $rel->delete_related($this);
  1063. }
  1064. else
  1065. {
  1066. if (empty($this->_original_relations[$rel_name]))
  1067. {
  1068. $data = $rel->get($this);
  1069. if (is_array($data))
  1070. {
  1071. $this->_original_relations[$rel_name] = array();
  1072. foreach ($data as $obj)
  1073. {
  1074. $this->_original_relations[$rel_name][] = $obj ? $obj->implode_pk($obj) : null;
  1075. }
  1076. }
  1077. else
  1078. {
  1079. $this->_original_relations[$rel_name] = $data ? $data->implode_pk($data) : null;
  1080. }
  1081. }
  1082. }
  1083. unset($this->_reset_relations[$rel_name]);
  1084. }
  1085. if (array_key_exists($rel_name, $this->_data_relations))
  1086. {
  1087. $rel->save($this, $this->{$rel_name},
  1088. array_key_exists($rel_name, $this->_original_relations) ? $this->_original_relations[$rel_name] : null,
  1089. false, is_array($cascade) ? in_array($rel_name, $cascade) : $cascade
  1090. );
  1091. }
  1092. }
  1093. $this->unfreeze();
  1094. // Insert or update
  1095. $return = $this->_is_new ? $this->create() : $this->update();
  1096. $this->freeze();
  1097. foreach($this->relations() as $rel_name => $rel)
  1098. {
  1099. if (array_key_exists($rel_name, $this->_data_relations))
  1100. {
  1101. $rel->save($this, $this->{$rel_name},
  1102. array_key_exists($rel_name, $this->_original_relations) ? $this->_original_relations[$rel_name] : null,
  1103. true, is_array($cascade) ? in_array($rel_name, $cascade) : $cascade
  1104. );
  1105. }
  1106. }
  1107. $this->unfreeze();
  1108. $this->_update_original();
  1109. $this->observe('after_save');
  1110. $use_transaction and $db->commit_transaction();
  1111. }
  1112. catch (\Exception $e)
  1113. {
  1114. $use_transaction and $db->rollback_transaction();
  1115. throw $e;
  1116. }
  1117. return $return;
  1118. }
  1119. /**
  1120. * Save using INSERT
  1121. */
  1122. protected function create()
  1123. {
  1124. // Only allow creation with new object, otherwise: clone first, create later
  1125. if ( ! $this->is_new())
  1126. {
  1127. return false;
  1128. }
  1129. $this->observe('before_insert');
  1130. // Set all current values
  1131. $query = Query::forge(get_called_class(), static::connection(true));
  1132. $primary_key = static::primary_key();
  1133. $properties = array_keys(static::properties());
  1134. foreach ($properties as $p)
  1135. {
  1136. if ( ! (in_array($p, $primary_key) and is_null($this->{$p})))
  1137. {
  1138. $query->set($p, $this->{$p});
  1139. }
  1140. }
  1141. // Insert!
  1142. $id = $query->insert();
  1143. // when there's one PK it might be auto-incremented, get it and set it
  1144. if (count($primary_key) == 1 and $id !== false)
  1145. {
  1146. $pk = reset($primary_key);
  1147. // only set it if it hasn't been set manually
  1148. is_null($this->{$pk}) and $this->{$pk} = $id;
  1149. }
  1150. // update the original properties on creation and cache object for future retrieval in this request
  1151. $this->_is_new = false;
  1152. $this->_original = $this->_data;
  1153. static::$_cached_objects[get_class($this)][static::implode_pk($this)] = $this;
  1154. $this->observe('after_insert');
  1155. return $id !== false;
  1156. }
  1157. /**
  1158. * Save using UPDATE
  1159. */
  1160. protected function update()
  1161. {
  1162. // New objects can't be updated, neither can frozen
  1163. if ($this->is_new())
  1164. {
  1165. return false;
  1166. }
  1167. // Non changed objects don't have to be saved, but return true anyway (no reason to fail)
  1168. if ( ! $this->is_changed(array_keys(static::properties())))
  1169. {
  1170. return true;
  1171. }
  1172. $this->observe('before_update');
  1173. // Create the query and limit to primary key(s)
  1174. $query = Query::forge(get_called_class(), static::connection(true));
  1175. $primary_key = static::primary_key();
  1176. $properties = array_keys(static::properties());
  1177. //Add the primary keys to the where
  1178. $this->add_primary_keys_to_where($query);
  1179. // Set all current values
  1180. foreach ($properties as $p)
  1181. {
  1182. if ( ! in_array($p, $primary_key) )
  1183. {
  1184. if (array_key_exists($p, $this->_original))
  1185. {
  1186. $this->{$p} !== $this->_original[$p] and $query->set($p, isset($this->_data[$p]) ? $this->_data[$p] : null);
  1187. }
  1188. else
  1189. {
  1190. array_key_exists($p, $this->_data) and $query->set($p, $this->_data[$p]);
  1191. }
  1192. }
  1193. }
  1194. // Return false when update fails
  1195. if ( ! $query->update())
  1196. {
  1197. return false;
  1198. }
  1199. // update the original property on success
  1200. $this->observe('after_update');
  1201. return true;
  1202. }
  1203. /**
  1204. * Adds the primary keys in where clauses to the given query.
  1205. *
  1206. * @param Query $query
  1207. */
  1208. protected function add_primary_keys_to_where($query)
  1209. {
  1210. $primary_key = static::primary_key();
  1211. foreach ($primary_key as $pk)
  1212. {
  1213. $query->where($pk, '=', $this->_original[$pk]);
  1214. }
  1215. }
  1216. /**
  1217. * Delete current object
  1218. *
  1219. * @param mixed $cascade
  1220. * null = use default config,
  1221. * bool = force/prevent cascade,
  1222. * array cascades only the relations that are in the array
  1223. * @param bool $use_transaction
  1224. *
  1225. * @throws \Exception
  1226. *
  1227. * @return Model this instance as a new object without primary key(s)
  1228. */
  1229. public function delete($cascade = null, $use_transaction = false)
  1230. {
  1231. // New objects can't be deleted, neither can frozen
  1232. if ($this->is_new() or $this->frozen())
  1233. {
  1234. return false;
  1235. }
  1236. if ($use_transaction)
  1237. {
  1238. $db = \Database_Connection::instance(static::connection(true));
  1239. $db->start_transaction();
  1240. }
  1241. try
  1242. {
  1243. $this->observe('before_delete');
  1244. $this->freeze();
  1245. foreach($this->relations() as $rel_name => $rel)
  1246. {
  1247. $rel->delete($this, $this->{$rel_name}, false, is_array($cascade) ? in_array($rel_name, $cascade) : $cascade);
  1248. }
  1249. $this->unfreeze();
  1250. // Create the query and limit to primary key(s)
  1251. $query = Query::forge(get_called_class(), static::connection(true))->limit(1);
  1252. $primary_key = static::primary_key();
  1253. foreach ($primary_key as $pk)
  1254. {
  1255. $query->where($pk, '=', $this->{$pk});
  1256. }
  1257. // Return success of update operation
  1258. if ( ! $query->delete())
  1259. {
  1260. return false;
  1261. }
  1262. $this->freeze();
  1263. foreach($this->relations() as $rel_name => $rel)
  1264. {
  1265. $should_cascade = is_array($cascade) ? in_array($rel_name, $cascade) : $cascade;
  1266. //Give model subclasses a chance to chip in.
  1267. if ($should_cascade && ! $this->should_cascade_delete($rel))
  1268. {
  1269. //The function returned false so something does not want this relation to be cascade deleted
  1270. $should_cascade = false;
  1271. }
  1272. $rel->delete($this, $this->{$rel_name}, true, $should_cascade);
  1273. }
  1274. $this->unfreeze();
  1275. // Perform cleanup:
  1276. // remove from internal object cache, remove PK's, set to non saved object, remove db original values
  1277. if (array_key_exists(get_called_class(), static::$_cached_objects)
  1278. and array_key_exists(static::implode_pk($this), static::$_cached_objects[get_called_class()]))
  1279. {
  1280. unset(static::$_cached_objects[get_called_class()][static::implode_pk($this)]);
  1281. }
  1282. foreach ($this->primary_key() as $pk)
  1283. {
  1284. unset($this->_data[$pk]);
  1285. }
  1286. // remove original relations too
  1287. foreach($this->relations() as $rel_name => $rel)
  1288. {
  1289. $this->_original_relations[$rel_name] = $rel->singular ? null : array();
  1290. }
  1291. $this->_is_new = true;
  1292. $this->_original = array();
  1293. $this->observe('after_delete');
  1294. $use_transaction and $db->commit_transaction();
  1295. }
  1296. catch (\Exception $e)
  1297. {
  1298. $use_transaction and $db->rollback_transaction();
  1299. throw $e;
  1300. }
  1301. return $this;
  1302. }
  1303. /**
  1304. * Allows subclasses to more easily define if a relation can be cascade deleted or not.
  1305. *
  1306. * @param array $rel
  1307. *
  1308. * @return bool False to stop the relation from being deleted. Works the same as the cascade_delete property
  1309. */
  1310. protected function should_cascade_delete($rel)
  1311. {
  1312. return true;
  1313. }
  1314. /**
  1315. * Reset values to those gotten from the database
  1316. */
  1317. public function reset()
  1318. {
  1319. foreach ($this->_original as $p => $val)
  1320. {
  1321. $this->_data[$p] = $val;
  1322. }
  1323. }
  1324. /**
  1325. * Calls all observers for the current event
  1326. *
  1327. * @param string
  1328. */
  1329. public function observe($event)
  1330. {
  1331. foreach ($this->observers() as $observer => $settings)
  1332. {
  1333. $events = isset($settings['events']) ? $settings['events'] : array();
  1334. if (empty($events) or in_array($event, $events))
  1335. {
  1336. if ( ! class_exists($observer))
  1337. {
  1338. $observer_class = \Inflector::get_namespace($observer).'Observer_'.\Inflector::denamespace($observer);
  1339. if ( ! class_exists($observer_class))
  1340. {
  1341. throw new \UnexpectedValueException($observer);
  1342. }
  1343. // Add the observer with the full classname for next usage
  1344. unset(static::$_observers_cached[$observer]);
  1345. static::$_observers_cached[$observer_class] = $events;
  1346. $observer = $observer_class;
  1347. }
  1348. try
  1349. {
  1350. call_user_func(array($observer, 'orm_notify'), $this, $event);
  1351. }
  1352. catch (\Exception $e)
  1353. {
  1354. // Unfreeze before failing
  1355. $this->unfreeze();
  1356. throw $e;
  1357. }
  1358. }
  1359. }
  1360. }
  1361. /**
  1362. * Compare current state with the retrieved state
  1363. *
  1364. * @param string|array $property
  1365. *
  1366. * @throws \OutOfBoundsException
  1367. *
  1368. * @return bool
  1369. */
  1370. public function is_changed($property = null)
  1371. {
  1372. $properties = static::properties();
  1373. $relations = static::relations();
  1374. $property = (array) $property ?: array_merge(array_keys($properties), array_keys($relations));
  1375. foreach ($property as $p)
  1376. {
  1377. if (isset($properties[$p]))
  1378. {
  1379. if (array_key_exists($p, $this->_original))
  1380. {
  1381. if (array_key_exists('data_type', $properties[$p]) and $properties[$p]['data_type'] == 'int')
  1382. {
  1383. if ($this->{$p} != $this->_original[$p])
  1384. {
  1385. return true;
  1386. }
  1387. }
  1388. elseif ($this->{$p} !== $this->_original[$p])
  1389. {
  1390. return true;
  1391. }
  1392. }
  1393. else
  1394. {
  1395. if (array_key_exists($p, $this->_data))
  1396. {
  1397. return true;
  1398. }
  1399. }
  1400. }
  1401. elseif (isset($relations[$p]))
  1402. {
  1403. if ($relations[$p]->singular)
  1404. {
  1405. if (empty($this->_original_relations[$p]) !== empty($this->_data_relations[$p])
  1406. or ( ! empty($this->_original_relations[$p])
  1407. and $this->_original_relations[$p] !== $this->_data_relations[$p]->implode_pk($this->{$p})))
  1408. {
  1409. return true;
  1410. }
  1411. }
  1412. else
  1413. {
  1414. if (empty($this->_original_relations[$p]))
  1415. {
  1416. if ( ! empty($this->_data_relations[$p]))
  1417. {
  1418. return true;
  1419. }
  1420. continue;
  1421. }
  1422. $orig_rels = $this->_original_relations[$p];
  1423. foreach ($this->{$p} as $rk => $r)
  1424. {
  1425. if ( ! in_array($r->implode_pk($r), $orig_rels))
  1426. {
  1427. return true;
  1428. }
  1429. unset($orig_rels[array_search($rk, $orig_rels)]);
  1430. }
  1431. if ( ! empty($orig_rels))
  1432. {
  1433. return true;
  1434. }
  1435. }
  1436. }
  1437. else
  1438. {
  1439. throw new \OutOfBoundsException('Unknown property or relation: '.$p);
  1440. }
  1441. }
  1442. return false;
  1443. }
  1444. /**
  1445. * Generates an array with keys new & old that contain ONLY the values that differ between the original and
  1446. * the current unsaved model.
  1447. * Note: relations are given as single or array of imploded pks
  1448. *
  1449. * @return array
  1450. */
  1451. public function get_diff()
  1452. {
  1453. $diff = array(0 => array(), 1 => array());
  1454. foreach ($this->_data as $key => $val)
  1455. {
  1456. if ($this->is_changed($key))
  1457. {
  1458. $diff[0][$key] = array_key_exists($key, $this->_original) ? $this->_original[$key] : null;
  1459. $diff[1][$key] = $val;
  1460. }
  1461. }
  1462. foreach ($this->_data_relations as $key => $val)
  1463. {
  1464. $rel = static::relations($key);
  1465. if ($rel->singular)
  1466. {
  1467. $new_pk = null;
  1468. if (empty($this->_original_relations[$key]) !== empty($val)
  1469. or ( ! empty($this->_original_relations[$key]) and ! empty($val)
  1470. and $this->_original_relations[$key] !== $new_pk = $val->implode_pk($val)
  1471. ))
  1472. {
  1473. $diff[0][$key] = isset($this->_original_relations[$key]) ? $this->_original_relations[$key] : null;
  1474. $diff[1][$key] = isset($val) ? $new_pk : null;
  1475. }
  1476. }
  1477. else
  1478. {
  1479. $original_pks = empty($this->_original_relations[$key]) ? array() : $this->_original_relations[$key];
  1480. $new_pks = array();
  1481. if ($val)
  1482. {
  1483. foreach ($val as $v)
  1484. {
  1485. if ( ! in_array(($new_pk = $v->implode_pk($v)), $original_pks))
  1486. {
  1487. $new_pks[] = $new_pk;
  1488. }
  1489. else
  1490. {
  1491. $original_pks = array_diff($original_pks, array($new_pk));
  1492. }
  1493. }
  1494. }
  1495. if ( ! empty($original_pks) or ! empty($new_pks)) {
  1496. $diff[0][$key] = empty($original_pks) ? null : $original_pks;
  1497. $diff[1][$key] = empty($new_pks) ? null : $new_pks;
  1498. }
  1499. }
  1500. }
  1501. return $diff;
  1502. }
  1503. /***
  1504. * Returns whether the given relation is fetched. If no relation is
  1505. *
  1506. * @param string $relation Name of relation
  1507. *
  1508. * @return bool
  1509. */
  1510. public function is_fetched($relation)
  1511. {
  1512. if (static::relations($relation))
  1513. {
  1514. return array_key_exists($relation, $this->_data_relations);
  1515. }
  1516. return false;
  1517. }
  1518. /***
  1519. * Returns whether this is a saved or a new object
  1520. *
  1521. * @return bool
  1522. */
  1523. public function is_new()
  1524. {
  1525. return $this->_is_new;
  1526. }
  1527. /**
  1528. * Check whether the object was frozen
  1529. *
  1530. * @return boolean
  1531. */
  1532. public function frozen()
  1533. {
  1534. return $this->_frozen;
  1535. }
  1536. /**
  1537. * Freeze the object to disallow changing it or saving it
  1538. */
  1539. public function freeze()
  1540. {
  1541. $this->_frozen = true;
  1542. }
  1543. /**
  1544. * Unfreeze the object to allow changing it or saving it again
  1545. */
  1546. public function unfreeze()
  1547. {
  1548. $this->_frozen = false;
  1549. }
  1550. /**
  1551. * Method for use with Fieldset::add_model()
  1552. *
  1553. * @param Fieldset Fieldset instance to add fields to
  1554. * @param array|Model Model instance or array for use to repopulate
  1555. */
  1556. public static function set_form_fields($form, $instance = null)
  1557. {
  1558. Observer_Validation::set_fields($instance instanceof static ? $instance : get_called_class(), $form);
  1559. $instance and $form->populate($instance, true);
  1560. }
  1561. /**
  1562. * Allow populating this object from an array, and any related objects
  1563. *
  1564. * @param array assoc array with named values to store in the object
  1565. *
  1566. * @return Model this instance as a new object without primary key(s)
  1567. */
  1568. public function from_array(array $values)
  1569. {
  1570. foreach($values as $property => $value)
  1571. {
  1572. if (array_key_exists($property, static::properties()) and ! in_array($property, static::primary_key()))
  1573. {
  1574. $this->_data[$property] = $value;
  1575. }
  1576. elseif (array_key_exists($property, static::relations()) and is_array($value))
  1577. {
  1578. $rel = static::relations($property);
  1579. if ( ! isset($this->_data_relations[$property]))
  1580. {
  1581. $this->_data_relations[$property] = $rel->singular ? null : array();
  1582. }
  1583. foreach($value as $id => $data)
  1584. {
  1585. if (is_array($data))
  1586. {
  1587. if (array_key_exists($id, $this->_data_relations[$property]))
  1588. {
  1589. foreach($data as $field => $contents)
  1590. {
  1591. if ($rel->singular)
  1592. {
  1593. $this->_data_relations[$property]->{$field} = $contents;
  1594. }
  1595. else
  1596. {
  1597. $this->_data_relations[$property][$id]->{$field} = $contents;
  1598. }
  1599. }
  1600. }
  1601. else
  1602. {
  1603. if ($rel->singular)
  1604. {
  1605. $this->_data_relations[$property] = call_user_func(static::relations($property)->model_to.'::forge', $data);
  1606. }
  1607. else
  1608. {
  1609. $this->_data_relations[$property][] = call_user_func(static::relations($property)->model_to.'::forge', $data);
  1610. }
  1611. }
  1612. }
  1613. }
  1614. }
  1615. else
  1616. {
  1617. $this->_custom_data[$property] = $value;
  1618. }
  1619. }
  1620. return $this;
  1621. }
  1622. /**
  1623. * Allow converting this object to an array
  1624. *
  1625. * @param bool $custom
  1626. * @param bool $recurse
  1627. *
  1628. * @internal param \Orm\whether $bool or not to include the custom data array
  1629. *
  1630. * @return array
  1631. */
  1632. public function to_array($custom = false, $recurse = false)
  1633. {
  1634. static $references = array();
  1635. $array = array();
  1636. // reset the references array on first call
  1637. $recurse or $references = array();
  1638. // make sure all data is scalar or array
  1639. if ($custom)
  1640. {
  1641. foreach ($this->_custom_data as $key => $val)
  1642. {
  1643. if (is_object($val))
  1644. {
  1645. if (method_exists($val, '__toString'))
  1646. {
  1647. $val = (string) $val;
  1648. }
  1649. else
  1650. {
  1651. $val = get_object_vars($val);
  1652. }
  1653. }
  1654. $array[$key] = $val;
  1655. }
  1656. }
  1657. // make sure all data is scalar or array
  1658. foreach ($this->_data as $key => $val)
  1659. {
  1660. if (is_object($val))
  1661. {
  1662. if (method_exists($val, '__toString'))
  1663. {
  1664. $val = (string) $val;
  1665. }
  1666. else
  1667. {
  1668. $val = get_object_vars($val);
  1669. }
  1670. }
  1671. $array[$key] = $val;
  1672. }
  1673. // convert relations
  1674. foreach ($this->_data_relations as $name => $rel)
  1675. {
  1676. if (is_array($rel))
  1677. {
  1678. $array[$name] = array();
  1679. if ( ! empty($rel))
  1680. {
  1681. foreach ($rel as $id => $r)
  1682. {
  1683. $array[$name][$id] = $r->to_array($custom, true);
  1684. }
  1685. $references[] = get_class($r);
  1686. }
  1687. }
  1688. else
  1689. {
  1690. if ( ! in_array(get_class($rel), $references))
  1691. {
  1692. if (is_null($rel))
  1693. {
  1694. $array[$name] = null;
  1695. }
  1696. else
  1697. {
  1698. $array[$name] = $rel->to_array($custom, true);
  1699. $references[] = get_class($rel);
  1700. }
  1701. }
  1702. }
  1703. }
  1704. // strip any excluded values from the array
  1705. foreach (static::$_to_array_exclude as $key)
  1706. {
  1707. if (array_key_exists($key, $array))
  1708. {
  1709. unset($array[$key]);
  1710. }
  1711. }
  1712. return $array;
  1713. }
  1714. /**
  1715. * Allow converting this object to a real object
  1716. *
  1717. * @return object
  1718. */
  1719. public function to_object()
  1720. {
  1721. return (object) $this->to_array();
  1722. }
  1723. /**
  1724. * EAV attribute getter. Also deals with isset() and unset()
  1725. *
  1726. * @param string $attribute, the attribute value to get
  1727. * @param bool $isset, if true, do an exists check instead of returning the value
  1728. * @param bool $unset, if true, delete the EAV attribute if it exists
  1729. *
  1730. * @throws \OutOfBoundsException if the defined EAV relation does not exist or of the wrong type
  1731. *
  1732. * @return mixed
  1733. */
  1734. protected function _get_eav($attribute, $isset = false, $unset = false)
  1735. {
  1736. // get the current class name
  1737. $class = get_called_class();
  1738. // don't do anything unless we actually have an EAV container
  1739. if (property_exists($class, '_eav'))
  1740. {
  1741. // loop through the defined EAV containers
  1742. foreach (static::$_eav as $rel => $settings)
  1743. {
  1744. // normalize the container definition, could be string or array
  1745. if (is_string($settings))
  1746. {
  1747. $rel = $settings;
  1748. $settings = array();
  1749. }
  1750. // fetch the relation object for this EAV container
  1751. if ( ! $rel = static::relations($rel))
  1752. {
  1753. throw new \OutOfBoundsException('EAV container defines a relation that does not exist in '.get_class($this).'.');
  1754. }
  1755. // EAV containers must be of the "Many type"
  1756. if ($rel instanceOf \Orm\HasOne or $rel instanceOf \Orm\BelongsTo )
  1757. {
  1758. throw new \OutOfBoundsException('EAV containers can only be defined on "HasMany" or "ManyMany" relations in '.get_class($this).'.');
  1759. }
  1760. // determine attribute and value column names
  1761. $attr = isset($settings['attribute']) ? $settings['attribute'] : 'attribute';
  1762. $val = isset($settings['value']) ? $settings['value'] : 'value';
  1763. // see if we have a result
  1764. if ($result = $this->{$rel->name})
  1765. {
  1766. // loop over the resultset
  1767. foreach ($result as $key => $record)
  1768. {
  1769. // check if this is the attribute we need
  1770. if ($record->{$attr} === $attribute)
  1771. {
  1772. if ($unset)
  1773. {
  1774. // delete the related object if we need to unset
  1775. unset($this->{$rel->name}[$key]);
  1776. $record->delete();
  1777. return true;
  1778. }
  1779. else
  1780. {
  1781. // else return its existence or its value
  1782. return $isset ? true : $record->{$val};
  1783. }
  1784. }
  1785. }
  1786. }
  1787. }
  1788. }
  1789. return false;
  1790. }
  1791. /**
  1792. * EAV attribute setter
  1793. *
  1794. * @param string $attribute
  1795. * @param string $value
  1796. *
  1797. * @throws \OutOfBoundsException
  1798. *
  1799. * @return mixed
  1800. */
  1801. protected function _set_eav($attribute, $value)
  1802. {
  1803. // get the current class name
  1804. $class = get_called_class();
  1805. // don't do anything unless we actually have an EAV container
  1806. if (property_exists($class, '_eav'))
  1807. {
  1808. // loop through the defined EAV containers
  1809. foreach (static::$_eav as $rel => $settings)
  1810. {
  1811. // normalize the container definition, could be string or array
  1812. if (is_string($settings))
  1813. {
  1814. $rel = $settings;
  1815. $settings = array();
  1816. }
  1817. // fetch the relation object for this EAV container
  1818. if ( ! $relation = static::relations($rel))
  1819. {
  1820. throw new \OutOfBoundsException('EAV container defines a relation that does not exist in '.get_class($this).'.');
  1821. }
  1822. // EAV containers must be of the "Many type"
  1823. if ($relation instanceOf \Orm\HasOne or $relation instanceOf \Orm\BelongsTo)
  1824. {
  1825. throw new \OutOfBoundsException('EAV containers can only be defined on "HasMany" or "ManyMany" relations in '.get_class($this).'.');
  1826. }
  1827. // determine attribute and value column names
  1828. $attr = isset($settings['attribute']) ? $settings['attribute'] : 'attribute';
  1829. $val = isset($settings['value']) ? $settings['value'] : 'value';
  1830. // loop over the resultset
  1831. foreach ($this->{$relation->name} as $key => $record)
  1832. {
  1833. if ($record->{$attr} === $attribute)
  1834. {
  1835. $record->{$val} = $value;
  1836. return true;
  1837. }
  1838. }
  1839. // not found, we've got outselfs a new attribute, so add it
  1840. if ($rel = static::related_class($rel))
  1841. {
  1842. $this->{$relation->name}[] = $rel::forge(array(
  1843. $attr => $attribute,
  1844. $val => $value,
  1845. ));
  1846. return true;
  1847. }
  1848. }
  1849. }
  1850. return false;
  1851. }
  1852. /***************************************************************************
  1853. * Implementation of ArrayAccess
  1854. **************************************************************************/
  1855. public function offsetSet($offset, $value)
  1856. {
  1857. try
  1858. {
  1859. $this->__set($offset, $value);
  1860. }
  1861. catch (\Exception $e)
  1862. {
  1863. return false;
  1864. }
  1865. }
  1866. public function offsetExists($offset)
  1867. {
  1868. return $this->__isset($offset);
  1869. }
  1870. public function offsetUnset($offset)
  1871. {
  1872. $this->__unset($offset);
  1873. }
  1874. public function offsetGet($offset)
  1875. {
  1876. try
  1877. {
  1878. return $this->__get($offset);
  1879. }
  1880. catch (\Exception $e)
  1881. {
  1882. return false;
  1883. }
  1884. }
  1885. /***************************************************************************
  1886. * Implementation of Iterable
  1887. **************************************************************************/
  1888. protected $_iterable = array();
  1889. public function rewind()
  1890. {
  1891. $this->_iterable = array_merge($this->_custom_data, $this->_data, $this->_data_relations);
  1892. reset($this->_iterable);
  1893. }
  1894. public function current()
  1895. {
  1896. return current($this->_iterable);
  1897. }
  1898. public function key()
  1899. {
  1900. return key($this->_iterable);
  1901. }
  1902. public function next()
  1903. {
  1904. return next($this->_iterable);
  1905. }
  1906. public function valid()
  1907. {
  1908. return key($this->_iterable) !== null;
  1909. }
  1910. }