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