PageRenderTime 26ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/framework/Rdo/lib/Horde/Rdo/Base.php

https://github.com/imr/horde
PHP | 449 lines | 200 code | 39 blank | 210 comment | 33 complexity | 2922f131843146fb29c130c48884ebe1 MD5 | raw file
  1. <?php
  2. /**
  3. * @category Horde
  4. * @package Rdo
  5. */
  6. /**
  7. * Horde_Rdo_Base abstract class (Rampage Data Objects). Entity
  8. * classes extend this baseline.
  9. *
  10. * @category Horde
  11. * @package Rdo
  12. */
  13. abstract class Horde_Rdo_Base implements IteratorAggregate, ArrayAccess
  14. {
  15. /**
  16. * The Horde_Rdo_Mapper instance associated with this Rdo object. The
  17. * Mapper takes care of all backend access.
  18. *
  19. * @see Horde_Rdo_Mapper
  20. * @var Horde_Rdo_Mapper
  21. */
  22. protected $_mapper;
  23. /**
  24. * This object's fields.
  25. *
  26. * @var array
  27. */
  28. protected $_fields = array();
  29. /**
  30. * Constructor. Can be called directly by a programmer, or is
  31. * called in Horde_Rdo_Mapper::map(). Takes an associative array
  32. * of initial object values.
  33. *
  34. * @param array $fields Initial values for the new object.
  35. *
  36. * @see Horde_Rdo_Mapper::map()
  37. */
  38. public function __construct($fields = array())
  39. {
  40. $this->setFields($fields);
  41. }
  42. /**
  43. * When Rdo objects are cloned, unset the unique id that
  44. * identifies them so that they can be modified and saved to the
  45. * backend as new objects. If you don't really want a new object,
  46. * don't clone.
  47. */
  48. public function __clone()
  49. {
  50. // @TODO Support composite primary keys
  51. unset($this->{$this->getMapper()->primaryKey});
  52. // @TODO What about associated objects?
  53. }
  54. /**
  55. * Fetch fields that haven't yet been loaded. Lazy-loaded fields
  56. * and lazy-loaded relationships are handled this way. Once a
  57. * field is retrieved, it is cached in the $_fields array so it
  58. * doesn't need to be fetched again.
  59. *
  60. * @param string $field The name of the field to access.
  61. *
  62. * @return mixed The value of $field or null.
  63. */
  64. public function __get($field)
  65. {
  66. // Honor any explicit getters.
  67. $fieldMethod = 'get' . ucfirst($field);
  68. // If an Rdo_Base subclass has a __call() method, is_callable
  69. // returns true on every method name, so use method_exists
  70. // instead.
  71. if (method_exists($this, $fieldMethod)) {
  72. return call_user_func(array($this, $fieldMethod));
  73. }
  74. if (isset($this->_fields[$field])) {
  75. return $this->_fields[$field];
  76. }
  77. $mapper = $this->getMapper();
  78. // Look for lazy fields first, then relationships.
  79. if (in_array($field, $mapper->lazyFields)) {
  80. // @TODO Support composite primary keys
  81. $query = new Horde_Rdo_Query($mapper);
  82. $query->setFields($field)
  83. ->addTest($mapper->primaryKey, '=', $this->{$mapper->primaryKey});
  84. list($sql, $params) = $query->getQuery();
  85. $this->_fields[$field] = $mapper->adapter->selectValue($sql, $params);;
  86. return $this->_fields[$field];
  87. } elseif (isset($mapper->lazyRelationships[$field])) {
  88. $rel = $mapper->lazyRelationships[$field];
  89. } else {
  90. return null;
  91. }
  92. // Try to find the Mapper class for the object the
  93. // relationship is with, and fail if we can't.
  94. if (isset($rel['mapper'])) {
  95. if ($mapper->factory) {
  96. $m = $mapper->factory->create($rel['mapper']);
  97. } else {
  98. // @TODO - should be getting this instance from somewhere
  99. // else external, and not passing the adapter along
  100. // automatically.
  101. $m = new $rel['mapper']($mapper->adapter);
  102. }
  103. } else {
  104. $m = $mapper->tableToMapper($field);
  105. if (is_null($m)) {
  106. return null;
  107. }
  108. }
  109. // Based on the kind of relationship, fetch the appropriate
  110. // objects and fill the cache.
  111. switch ($rel['type']) {
  112. case Horde_Rdo::ONE_TO_ONE:
  113. case Horde_Rdo::MANY_TO_ONE:
  114. if (isset($rel['query'])) {
  115. $query = $this->_fillPlaceholders($rel['query']);
  116. $this->_fields[$field] = $m->findOne($query);
  117. } elseif (!empty($this->{$rel['foreignKey']})) {
  118. $this->_fields[$field] = $m->findOne($this->{$rel['foreignKey']});
  119. if (empty($this->_fields[$field])) {
  120. throw new Horde_Rdo_Exception('The referenced object with key ' . $this->{$rel['foreignKey']} . ' does not exist. Your data is inconsistent');
  121. }
  122. } else {
  123. $this->_fields[$field] = null;
  124. }
  125. break;
  126. case Horde_Rdo::ONE_TO_MANY:
  127. $this->_fields[$field] = $m->find(array($rel['foreignKey'] => $this->{$rel['foreignKey']}));
  128. break;
  129. case Horde_Rdo::MANY_TO_MANY:
  130. $key = $mapper->primaryKey;
  131. $query = new Horde_Rdo_Query();
  132. $on = isset($rel['on']) ? $rel['on'] : $m->primaryKey;
  133. $query->addRelationship($field, array('mapper' => $mapper,
  134. 'table' => $rel['through'],
  135. 'type' => Horde_Rdo::MANY_TO_MANY,
  136. 'query' => array("$m->table.$on" => new Horde_Rdo_Query_Literal($rel['through'] . '.' . $on), $key => $this->$key)));
  137. $this->_fields[$field] = $m->find($query);
  138. break;
  139. }
  140. return $this->_fields[$field];
  141. }
  142. /**
  143. * Implements getter for ArrayAccess interface.
  144. *
  145. * @see __get()
  146. */
  147. public function offsetGet($field)
  148. {
  149. return $this->__get($field);
  150. }
  151. /**
  152. * Set a field's value.
  153. *
  154. * @param string $field The field to set
  155. * @param mixed $value The field's new value
  156. */
  157. public function __set($field, $value)
  158. {
  159. // Honor any explicit setters.
  160. $fieldMethod = 'set' . ucfirst($field);
  161. // If an Rdo_Base subclass has a __call() method, is_callable
  162. // returns true on every method name, so use method_exists
  163. // instead.
  164. if (method_exists($this, $fieldMethod)) {
  165. return call_user_func(array($this, $fieldMethod), $value);
  166. }
  167. $this->_fields[$field] = $value;
  168. }
  169. /**
  170. * Implements setter for ArrayAccess interface.
  171. *
  172. * @see __set()
  173. */
  174. public function offsetSet($field, $value)
  175. {
  176. $this->__set($field, $value);
  177. }
  178. /**
  179. * Allow using isset($rdo->foo) to check for field or
  180. * relationship presence.
  181. *
  182. * @param string $field The field name to check existence of.
  183. */
  184. public function __isset($field)
  185. {
  186. $m = $this->getMapper();
  187. return isset($this->_fields[$field])
  188. || isset($m->fields[$field])
  189. || isset($m->lazyFields[$field])
  190. || isset($m->relationships[$field])
  191. || isset($m->lazyRelationships[$field]);
  192. }
  193. /**
  194. * Implements isset() for ArrayAccess interface.
  195. *
  196. * @see __isset()
  197. */
  198. public function offsetExists($field)
  199. {
  200. return $this->__isset($field);
  201. }
  202. /**
  203. * Allow using unset($rdo->foo) to unset a basic
  204. * field. Relationships cannot be unset in this way.
  205. *
  206. * @param string $field The field name to unset.
  207. */
  208. public function __unset($field)
  209. {
  210. // @TODO Should unsetting a MANY_TO_MANY relationship remove
  211. // the relationship?
  212. unset($this->_fields[$field]);
  213. }
  214. /**
  215. * Implements unset() for ArrayAccess interface.
  216. *
  217. * @see __unset()
  218. */
  219. public function offsetUnset($field)
  220. {
  221. $this->__unset($field);
  222. }
  223. /**
  224. * Set field values for the object
  225. *
  226. * @param array $fields Initial values for the new object.
  227. *
  228. * @see Horde_Rdo_Mapper::map()
  229. */
  230. public function setFields($fields = array())
  231. {
  232. $this->_fields = $fields;
  233. }
  234. /**
  235. * Implement the IteratorAggregate interface. Looping over an Rdo
  236. * object goes through each property of the object in turn.
  237. *
  238. * @return Horde_Rdo_Iterator The Iterator instance.
  239. */
  240. public function getIterator()
  241. {
  242. return new Horde_Rdo_Iterator($this);
  243. }
  244. /**
  245. * Get a Mapper instance that can be used to manage this
  246. * object. The Mapper instance can come from a few places:
  247. *
  248. * - If the class <RdoClassName>Mapper exists, it will be used
  249. * automatically.
  250. *
  251. * - Any Rdo instance created with Horde_Rdo_Mapper::map() will have a
  252. * $mapper object set automatically.
  253. *
  254. * - Subclasses can override getMapper() to return the correct
  255. * mapper object.
  256. *
  257. * - The programmer can call $rdoObject->setMapper($mapper) to provide a
  258. * mapper object.
  259. *
  260. * A Horde_Rdo_Exception will be thrown if none of these
  261. * conditions are met.
  262. *
  263. * @return Horde_Rdo_Mapper The Mapper instance managing this object.
  264. */
  265. public function getMapper()
  266. {
  267. if (!$this->_mapper) {
  268. $class = get_class($this) . 'Mapper';
  269. if (class_exists($class)) {
  270. $this->_mapper = new $class();
  271. } else {
  272. throw new Horde_Rdo_Exception('No Horde_Rdo_Mapper object found. Override getMapper() or define the ' . get_class($this) . 'Mapper class.');
  273. }
  274. }
  275. return $this->_mapper;
  276. }
  277. /**
  278. * Associate this Rdo object with the Mapper instance that will
  279. * manage it. Called automatically by Horde_Rdo_Mapper:map().
  280. *
  281. * @param Horde_Rdo_Mapper $mapper The Mapper to manage this Rdo object.
  282. *
  283. * @see Horde_Rdo_Mapper::map()
  284. */
  285. public function setMapper($mapper)
  286. {
  287. $this->_mapper = $mapper;
  288. }
  289. /**
  290. * Adds a relation to one of the relationships defined in the mapper.
  291. *
  292. * - For one-to-one relations, simply updates the relation field.
  293. * - For one-to-many relations, updates the related object's relation field.
  294. * - For many-to-many relations, adds an entry in the "through" table.
  295. * - Performs a no-op if the peer is already related.
  296. *
  297. * This is a proxy to the mapper's addRelation() method.
  298. *
  299. * @param string $relationship The relationship key in the mapper.
  300. * @param Horde_Rdo_Base $peer The object to add the relation.
  301. *
  302. * @throws Horde_Rdo_Exception
  303. */
  304. public function addRelation($relationship, Horde_Rdo_Base $peer)
  305. {
  306. $this->mapper->addRelation($relationship, $this, $peer);
  307. }
  308. /**
  309. * Checks whether a relation to a peer is defined through one of the
  310. * relationships in the mapper.
  311. *
  312. * @param string $relationship The relationship key in the mapper.
  313. * @param Horde_Rdo_Base $peer The object to check for the relation.
  314. * If this is null, check if there is any peer
  315. * for this relation.
  316. *
  317. * @return boolean True if related.
  318. * @throws Horde_Rdo_Exception
  319. */
  320. public function hasRelation($relationship, Horde_Rdo_Base $peer = null)
  321. {
  322. $mapper = $this->getMapper();
  323. if (isset($mapper->relationships[$relationship])) {
  324. $rel = $mapper->relationships[$relationship];
  325. } elseif (isset($mapper->lazyRelationships[$relationship])) {
  326. $rel = $mapper->lazyRelationships[$relationship];
  327. } else {
  328. throw new Horde_Rdo_Exception('The requested relation is not defined in the mapper');
  329. }
  330. $result = $this->$relationship;
  331. switch ($rel['type']) {
  332. case Horde_Rdo::ONE_TO_ONE:
  333. case Horde_Rdo::MANY_TO_ONE:
  334. if (empty($peer) || empty($result)) {
  335. return (bool) $result;
  336. }
  337. $key = $result->mapper->primaryKey;
  338. return $result->$key == $peer->$key;
  339. case Horde_Rdo::ONE_TO_MANY:
  340. case Horde_Rdo::MANY_TO_MANY:
  341. if (empty($peer)) {
  342. return (bool) count($result);
  343. }
  344. $key = $peer->mapper->primaryKey;
  345. foreach ($result as $item) {
  346. if ($item->$key == $peer->$key) {
  347. return true;
  348. }
  349. }
  350. break;
  351. }
  352. return false;
  353. }
  354. /**
  355. * Removes a relation to one of the relationships defined in the mapper.
  356. *
  357. * - For one-to-one and one-to-many relations, simply sets the relation
  358. * field to 0.
  359. * - For many-to-many, either deletes all relations to this object or just
  360. * the relation to a given peer object.
  361. * - Performs a no-op if the peer is already unrelated.
  362. *
  363. * This is a proxy to the mapper's removeRelation method.
  364. *
  365. * @param string $relationship The relationship key in the mapper
  366. * @param Horde_Rdo_Base $peer The object to remove from the relation
  367. * @return integer The number of relations affected
  368. * @throws Horde_Rdo_Exception
  369. */
  370. public function removeRelation($relationship, Horde_Rdo_Base $peer = null)
  371. {
  372. return $this->mapper->removeRelation($relationship, $this, $peer);
  373. }
  374. /**
  375. * Save any changes to the backend.
  376. *
  377. * @return boolean Success.
  378. */
  379. public function save()
  380. {
  381. return $this->getMapper()->update($this) == 1;
  382. }
  383. /**
  384. * Delete this object from the backend.
  385. *
  386. * @return boolean Success or failure.
  387. */
  388. public function delete()
  389. {
  390. return $this->getMapper()->delete($this) == 1;
  391. }
  392. /**
  393. * Take a query array and replace @field@ placeholders with values
  394. * from this object.
  395. *
  396. * @param array $query The query to process placeholders on.
  397. *
  398. * @return array The query with placeholders filled in.
  399. */
  400. protected function _fillPlaceholders($query)
  401. {
  402. foreach (array_keys($query) as $field) {
  403. $value = $query[$field];
  404. if (preg_match('/^@(.*)@$/', $value, $matches)) {
  405. $query[$field] = $this->{$matches[1]};
  406. }
  407. }
  408. return $query;
  409. }
  410. }