Overview

Packages

  • Rdo

Classes

  • Horde_Rdo
  • Horde_Rdo_Base
  • Horde_Rdo_Exception
  • Horde_Rdo_Iterator
  • Horde_Rdo_List
  • Horde_Rdo_Mapper
  • Horde_Rdo_Query
  • Horde_Rdo_Query_Literal
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Rdo Mapper base class.
  4:  *
  5:  * @category Horde
  6:  * @package  Rdo
  7:  */
  8: 
  9: /**
 10:  * Rdo Mapper class. Controls mapping of entity obects (instances of
 11:  * Horde_Rdo_Base) from and to Horde_Db_Adapters.
 12:  *
 13:  * Public properties:
 14:  *   $adapter - Horde_Db_Adapter that stores this Mapper's objects.
 15:  *
 16:  *   $inflector - The Horde_Support_Inflector this mapper uses to singularize
 17:  *   and pluralize PHP class, database table, and database field/key names.
 18:  *
 19:  *   $table - The Horde_Db_Adapter_Base_TableDefinition object describing
 20:  *   the main table of this entity.
 21:  *
 22:  * @category Horde
 23:  * @package  Rdo
 24:  */
 25: abstract class Horde_Rdo_Mapper implements Countable
 26: {
 27:     /**
 28:      * If this is true and fields named created_at and updated_at are present,
 29:      * Rdo will automatically set creation and last updated timestamps.
 30:      * Timestamps are always GMT for portability.
 31:      *
 32:      * @var boolean
 33:      */
 34:     protected $_setTimestamps = true;
 35: 
 36:     /**
 37:      * What class should this Mapper create for objects? Defaults to the Mapper
 38:      * subclass' name minus "Mapper". So if the Rdo_Mapper subclass is
 39:      * UserMapper, it will default to trying to create User objects.
 40:      *
 41:      * @var string
 42:      */
 43:     protected $_classname;
 44: 
 45:     /**
 46:      * The definition of the database table (or view, etc.) that holds this
 47:      * Mapper's objects.
 48:      *
 49:      * @var Horde_Db_Adapter_Base_TableDefinition
 50:      */
 51:     protected $_tableDefinition;
 52: 
 53:     /**
 54:      * Fields that should only be read from the database when they are
 55:      * accessed.
 56:      *
 57:      * @var array
 58:      */
 59:     protected $_lazyFields = array();
 60: 
 61:     /**
 62:      * Relationships for this entity.
 63:      *
 64:      * @var array
 65:      */
 66:     protected $_relationships = array();
 67: 
 68:     /**
 69:      * Relationships that should only be read from the database when
 70:      * they are accessed.
 71:      *
 72:      * @var array
 73:      */
 74:     protected $_lazyRelationships = array();
 75: 
 76:     /**
 77:      * Default sorting rule to use for all queries made with this mapper. This
 78:      * is a SQL ORDER BY fragment (without 'ORDER BY').
 79:      *
 80:      * @var string
 81:      */
 82:     protected $_defaultSort;
 83: 
 84:     public function __construct(Horde_Db_Adapter $adapter)
 85:     {
 86:         $this->adapter = $adapter;
 87:     }
 88: 
 89:     /**
 90:      * Provide read-only, on-demand access to several properties. This
 91:      * method will only be called for properties that aren't already
 92:      * present; once a property is fetched once it is cached and
 93:      * returned directly on any subsequent access.
 94:      *
 95:      * These properties are available:
 96:      *
 97:      * adapter: The Horde_Db_Adapter this mapper is using to talk to
 98:      * the database.
 99:      *
100:      * inflector: The Horde_Support_Inflector this Mapper uses to singularize
101:      * and pluralize PHP class, database table, and database field/key names.
102:      *
103:      * table: The database table or view that this Mapper manages.
104:      *
105:      * tableDefinition: The Horde_Db_Adapter_Base_TableDefinition object describing
106:      * the table or view this Mapper manages.
107:      *
108:      * fields: Array of all field names that are loaded up front
109:      * (eager loading) from the table.
110:      *
111:      * lazyFields: Array of fields that are only loaded when accessed.
112:      *
113:      * relationships: Array of relationships to other Mappers.
114:      *
115:      * lazyRelationships: Array of relationships to other Mappers which
116:      * are only loaded when accessed.
117:      *
118:      * @param string $key Property name to fetch
119:      *
120:      * @return mixed Value of $key
121:      */
122:     public function __get($key)
123:     {
124:         switch ($key) {
125:         case 'inflector':
126:             $this->inflector = new Horde_Support_Inflector();
127:             return $this->inflector;
128: 
129:         case 'primaryKey':
130:             $this->primaryKey = (string)$this->tableDefinition->getPrimaryKey();
131:             return $this->primaryKey;
132: 
133:         case 'table':
134:             $this->table = !empty($this->_table) ? $this->_table : $this->mapperToTable();
135:             return $this->table;
136: 
137:         case 'tableDefinition':
138:             $this->tableDefinition = $this->adapter->table($this->table);
139:             return $this->tableDefinition;
140: 
141:         case 'fields':
142:             $this->fields = array_diff($this->tableDefinition->getColumnNames(), $this->_lazyFields);
143:             return $this->fields;
144: 
145:         case 'lazyFields':
146:         case 'relationships':
147:         case 'lazyRelationships':
148:         case 'defaultSort':
149:             return $this->{'_' . $key};
150:         }
151: 
152:         return null;
153:     }
154: 
155:     /**
156:      * Create an instance of $this->_classname from a set of data.
157:      *
158:      * @param array $fields Field names/default values for the new object.
159:      *
160:      * @see $_classname
161:      *
162:      * @return Horde_Rdo_Base An instance of $this->_classname with $fields
163:      * as initial data.
164:      */
165:     public function map($fields = array())
166:     {
167:         // Guess a classname if one isn't explicitly set.
168:         if (!$this->_classname) {
169:             $this->_classname = $this->mapperToEntity();
170:             if (!$this->_classname) {
171:                 throw new Horde_Rdo_Exception('Unable to find an entity class (extending Horde_Rdo_Base) for ' . get_class($this));
172:             }
173:         }
174: 
175:         $o = new $this->_classname();
176:         $o->setMapper($this);
177: 
178:         $this->mapFields($o, $fields);
179: 
180:         if (is_callable(array($o, 'afterMap'))) {
181:             $o->afterMap();
182:         }
183: 
184:         return $o;
185:     }
186: 
187:     /**
188:      * Update an instance of $this->_classname from a set of data.
189:      *
190:      * @param Horde_Rdo_Base $object The object to update
191:      * @param array $fields Field names/default values for the object
192:      */
193:     public function mapFields($object, $fields = array())
194:     {
195:         $relationships = array();
196:         foreach ($fields as $fieldName => &$fieldValue) {
197:             if (strpos($fieldName, '@') !== false) {
198:                 list($rel, $field) = explode('@', $fieldName, 2);
199:                 $relationships[$rel][$field] = $fieldValue;
200:                 unset($fields[$fieldName]);
201:             }
202:             if (isset($this->fields[$fieldName])) {
203:                 $fieldName = $this->fields[$fieldName];
204:             }
205:             $column = $this->tableDefinition->getColumn($fieldName);
206:             if ($column) {
207:                 $fieldValue = $column->typeCast($fieldValue);
208:             }
209:         }
210: 
211:         $object->setFields($fields);
212: 
213:         if (count($relationships)) {
214:             foreach ($this->relationships as $relationship => $rel) {
215:                 if (isset($rel['mapper'])) {
216:                     // @TODO - should be getting this instance from somewhere
217:                     // else external, and not passing the adapter along
218:                     // automatically.
219:                     $m = new $rel['mapper']($this->adapter);
220:                 } else {
221:                     $m = $this->tableToMapper($relationship);
222:                     if (is_null($m)) {
223:                         // @TODO Throw an exception?
224:                         continue;
225:                     }
226:                 }
227: 
228:                 if (isset($relationships[$m->table])) {
229:                     $object->$relationship = $m->map($relationships[$m->table]);
230:                 }
231:             }
232:         }
233:     }
234: 
235:     /**
236:      * Transform a table name to a mapper class name.
237:      *
238:      * @param string $table The database table name to look up.
239:      *
240:      * @return Horde_Rdo_Mapper A new Mapper instance if it exists, else null.
241:      */
242:     public function tableToMapper($table)
243:     {
244:         if (class_exists(($class = ucwords($table) . 'Mapper'))) {
245:             return new $class;
246:         }
247:         return null;
248:     }
249: 
250:     /**
251:      * Transform this mapper's class name to a database table name.
252:      *
253:      * @return string The database table name.
254:      */
255:     public function mapperToTable()
256:     {
257:         return $this->inflector->pluralize(strtolower(str_replace('Mapper', '', get_class($this))));
258:     }
259: 
260:     /**
261:      * Transform this mapper's class name to an entity class name.
262:      *
263:      * @return string A Horde_Rdo_Base concrete class name if the class exists, else null.
264:      */
265:     public function mapperToEntity()
266:     {
267:         $class = str_replace('Mapper', '', get_class($this));
268:         if (class_exists($class)) {
269:             return $class;
270:         }
271:         return null;
272:     }
273: 
274:     /**
275:      * Count objects that match $query.
276:      *
277:      * @param mixed $query The query to count matches of.
278:      *
279:      * @return integer All objects matching $query.
280:      */
281:     public function count($query = null)
282:     {
283:         $query = Horde_Rdo_Query::create($query, $this);
284:         $query->setFields('COUNT(*)')
285:               ->clearSort();
286:         list($sql, $bindParams) = $query->getQuery();
287:         return $this->adapter->selectValue($sql, $bindParams);
288:     }
289: 
290:     /**
291:      * Check if at least one object matches $query.
292:      *
293:      * @param mixed $query Either a primary key, an array of keys
294:      *                     => values, or a Horde_Rdo_Query object.
295:      *
296:      * @return boolean True or false.
297:      */
298:     public function exists($query)
299:     {
300:         $query = Horde_Rdo_Query::create($query, $this);
301:         $query->setFields(1)
302:               ->clearSort();
303:         list($sql, $bindParams) = $query->getQuery();
304:         return (bool)$this->adapter->selectValue($sql, $bindParams);
305:     }
306: 
307:     /**
308:      * Create a new object in the backend with $fields as initial values.
309:      *
310:      * @param array $fields Array of field names => initial values.
311:      *
312:      * @return Horde_Rdo_Base The newly created object.
313:      */
314:     public function create($fields)
315:     {
316:         // If configured to record creation and update times, set them
317:         // here. We set updated_at to the initial creation time so it's
318:         // always set.
319:         if ($this->_setTimestamps) {
320:             $time = gmmktime();
321:             $fields['created_at'] = $time;
322:             $fields['updated_at'] = $time;
323:         }
324: 
325:         // Filter out any extra fields.
326:         $fields = array_intersect_key($fields, array_flip($this->tableDefinition->getColumnNames()));
327: 
328:         if (!$fields) {
329:             throw new Horde_Rdo_Exception('create() requires at least one field value.');
330:         }
331: 
332:         $sql = 'INSERT INTO ' . $this->adapter->quoteTableName($this->table);
333:         $keys = array();
334:         $placeholders = array();
335:         $bindParams = array();
336:         foreach ($fields as $field => $value) {
337:             $keys[] = $this->adapter->quoteColumnName($field);
338:             $placeholders[] = '?';
339:             $bindParams[] = $value;
340:         }
341:         $sql .= ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $placeholders) . ')';
342: 
343:         $id = $this->adapter->insert($sql, $bindParams);
344: 
345:         return $this->map(array_merge($fields, array($this->primaryKey => $id)));
346:     }
347: 
348:     /**
349:      * Updates a record in the backend. $object can be either a
350:      * primary key or an Rdo object. If $object is an Rdo instance
351:      * then $fields will be ignored as values will be pulled from the
352:      * object.
353:      *
354:      * @param string|Rdo $object The Rdo instance or unique id to update.
355:      * @param array $fields If passing a unique id, the array of field properties
356:      *                      to set for $object.
357:      *
358:      * @return integer Number of objects updated.
359:      */
360:     public function update($object, $fields = null)
361:     {
362:         if ($object instanceof Horde_Rdo_Base) {
363:             $key = $this->primaryKey;
364:             $id = $object->$key;
365:             $fields = iterator_to_array($object);
366: 
367:             if (!$id) {
368:                 // Object doesn't exist yet; create it instead.
369:                 $o = $this->create($fields);
370:                 $this->mapFields($object, iterator_to_array($o));
371:                 return 1;
372:             }
373:         } else {
374:             $id = $object;
375:         }
376: 
377:         // If configured to record update time, set it here.
378:         if ($this->_setTimestamps) {
379:             $fields['updated_at'] = gmmktime();
380:         }
381: 
382:         // Filter out any extra fields.
383:         $fields = array_intersect_key($fields, array_flip($this->tableDefinition->getColumnNames()));
384: 
385:         if (!$fields) {
386:             // Nothing to change.
387:             return 0;
388:         }
389: 
390:         $sql = 'UPDATE ' . $this->adapter->quoteTableName($this->table) . ' SET';
391:         $bindParams = array();
392:         foreach ($fields as $field => $value) {
393:             $sql .= ' ' . $this->adapter->quoteColumnName($field) . ' = ?,';
394:             $bindParams[] = $value;
395:         }
396:         $sql = substr($sql, 0, -1) . ' WHERE ' . $this->primaryKey . ' = ?';
397:         $bindParams[] = $id;
398: 
399:         return $this->adapter->update($sql, $bindParams);
400:     }
401: 
402:     /**
403:      * Deletes a record from the backend. $object can be either a
404:      * primary key, an Rdo_Query object, or an Rdo object.
405:      *
406:      * @param string|Horde_Rdo_Base|Horde_Rdo_Query $object The Rdo object,
407:      * Horde_Rdo_Query, or unique id to delete.
408:      *
409:      * @return integer Number of objects deleted.
410:      */
411:     public function delete($object)
412:     {
413:         if ($object instanceof Horde_Rdo_Base) {
414:             $key = $this->primaryKey;
415:             $id = $object->$key;
416:             $query = array($key => $id);
417:         } elseif ($object instanceof Horde_Rdo_Query) {
418:             $query = $object;
419:         } else {
420:             $key = $this->primaryKey;
421:             $query = array($key => $object);
422:         }
423: 
424:         $query = Horde_Rdo_Query::create($query, $this);
425: 
426:         $clauses = array();
427:         $bindParams = array();
428:         foreach ($query->tests as $test) {
429:             $clauses[] = $this->adapter->quoteColumnName($test['field']) . ' ' . $test['test'] . ' ?';
430:             $bindParams[] = $test['value'];
431:         }
432:         if (!$clauses) {
433:             throw new Horde_Rdo_Exception('Refusing to delete the entire table.');
434:         }
435: 
436:         $sql = 'DELETE FROM ' . $this->adapter->quoteTableName($this->table) .
437:                ' WHERE ' . implode(' ' . $query->conjunction . ' ', $clauses);
438: 
439:         return $this->adapter->delete($sql, $bindParams);
440:     }
441: 
442:     /**
443:      * find() can be called in several ways.
444:      *
445:      * Primary key mode: pass find() a numerically indexed array of primary
446:      * keys, and it will return a list of the objects that correspond to those
447:      * keys.
448:      *
449:      * If you pass find() no arguments, all objects of this type will be
450:      * returned.
451:      *
452:      * If you pass find() an associative array, it will be turned into a
453:      * Horde_Rdo_Query object.
454:      *
455:      * If you pass find() a Horde_Rdo_Query, it will return a list of all
456:      * objects matching that query.
457:      */
458:     public function find($arg = null)
459:     {
460:         if (is_null($arg)) {
461:             $query = null;
462:         } elseif (is_array($arg)) {
463:             if (!count($arg)) {
464:                 throw new Horde_Rdo_Exception('No criteria found');
465:             }
466: 
467:             if (is_numeric(key($arg))) {
468:                 // Numerically indexed arrays are assumed to be an array of
469:                 // primary keys.
470:                 $query = new Horde_Rdo_Query();
471:                 $query->combineWith('OR');
472:                 foreach ($argv[0] as $id) {
473:                     $query->addTest($this->primaryKey, '=', $id);
474:                 }
475:             } else {
476:                 $query = $arg;
477:             }
478:         } else {
479:             $query = $arg;
480:         }
481: 
482:         // Build a full Query object.
483:         $query = Horde_Rdo_Query::create($query, $this);
484:         return new Horde_Rdo_List($query);
485:     }
486: 
487:     /**
488:      * findOne can be called in several ways.
489:      *
490:      * Primary key mode: pass find() a single primary key, and it will return a
491:      * single object matching that primary key.
492:      *
493:      * If you pass findOne() no arguments, the first object of this type will be
494:      * returned.
495:      *
496:      * If you pass findOne() an associative array, it will be turned into a
497:      * Horde_Rdo_Query object.
498:      *
499:      * If you pass findOne() a Horde_Rdo_Query, it will return the first object
500:      * matching that query.
501:      */
502:     public function findOne($arg = null)
503:     {
504:         if (is_null($arg)) {
505:             $query = null;
506:         } elseif (is_scalar($arg)) {
507:             $query = array($this->primaryKey => $arg);
508:         } else {
509:             $query = $arg;
510:         }
511: 
512:         // Build a full Query object, and limit it to one result.
513:         $query = Horde_Rdo_Query::create($query, $this);
514:         $query->limit(1);
515: 
516:         $list = new Horde_Rdo_List($query);
517:         return $list->current();
518:     }
519: 
520:     /**
521:      * Set a default sort rule for all queries done with this Mapper.
522:      *
523:      * @param string $sort SQL sort fragment, such as 'updated DESC'
524:      */
525:     public function sortBy($sort)
526:     {
527:         $this->_defaultSort = $sort;
528:         return $this;
529:     }
530: }
531: 
API documentation generated by ApiGen