PageRenderTime 35ms CodeModel.GetById 15ms app.highlight 15ms RepoModel.GetById 1ms app.codeStats 0ms

/framework/core/db/DbObject.php

http://zoop.googlecode.com/
PHP | 781 lines | 379 code | 88 blank | 314 comment | 42 complexity | cb9a094a3728fb4fd191f88fc0f9afa7 MD5 | raw file
  1<?php
  2class DbObject extends Object implements Iterator
  3{
  4	/**
  5	 * The assigned name of the table (defaults to the class name translated by the getDefaultTableName
  6	 *
  7	 * @var string
  8	 */
  9	protected $tableName;
 10
 11	/**
 12	 * The field name(s) of the primary key in an array
 13	 *
 14	 * @var array
 15	 */
 16	protected $primaryKey;
 17	protected $keyAssignedBy;
 18	private $missingKeyFields;
 19	private $bound;
 20	private $persisted;
 21	private $scalars;
 22	protected $relationships;
 23
 24	const keyAssignedBy_db = 1;
 25	const keyAssignedBy_dev = 2;
 26	const keyAssignedBy_auto = 3;
 27
 28	/**
 29	 * This is the constructor.  (honest, I swear)
 30	 *
 31	 * Some things to know about the defaults defined by this constructor:
 32	 * Default primary key: id
 33	 * Default keyAssignedBy: keyAssignedBy_db
 34	 *
 35	 * If keyAssignedBy is keyAssignedBy_db the primary_key array can contain no more than one field
 36	 *
 37	 * @param mixed $init Initial value for the primary key field (if this is supplied there can be only one field in the primary key)
 38	 */
 39	function __construct($init = NULL)
 40	{
 41		//	set up some sensible defaults
 42		$this->primaryKey = array('id');
 43		$this->tableName = $this->getDefaultTableName();
 44		$this->bound = false;
 45		$this->keyAssignedBy = self::keyAssignedBy_db;
 46		$this->scalars = array();
 47		$this->relationships = array();
 48		$this->persisted = NULL;
 49		
 50		$this->init($init);
 51
 52		$this->missingKeyFields = count($this->primaryKey);
 53		if($this->keyAssignedBy == self::keyAssignedBy_db && count($this->primaryKey) != 1)
 54			trigger_error("in order for 'keyAssignedBy_db' to work you must have a single primary key field");
 55
 56		if(is_array($init))
 57		{
 58			$this->assignScalars($init);
 59		}
 60		else if($init === NULL)
 61		{
 62			return;
 63		}
 64		else
 65		{
 66			assert(count($this->primaryKey) == 1);
 67			$this->assignScalars(array($this->primaryKey[0] => $init));
 68		}
 69	}
 70
 71	/**
 72	 * This is a second stage constructor that should be overridden in individual database objects to initialize the instance
 73	 *
 74	 */
 75	protected function init()
 76	{
 77		//	override this function to setup relationships without having to handle the constructor chaining
 78	}
 79
 80	/**
 81	 * Returns the name of the table based on the name of the current class.  If the class is called "personObject" the tablename will default to person_object.
 82	 *
 83	 * @return string
 84	 */
 85	private function getDefaultTableName()
 86	{
 87		$name = get_class($this);
 88
 89		//      if there are any capitals after the firstone insert and underscore
 90		$name = $name[0] . preg_replace('/[A-Z]/', '_$0', substr($name, 1));
 91
 92		//      lowercase everything and return it
 93		return strtolower($name);
 94	}
 95
 96	/**
 97	 * Returns the name of the table associated with the db object
 98	 *
 99	 * @return string
100	 */
101	public function getTableName()
102	{
103		return $this->tableName;
104	}
105
106	/**
107	 * Returns the value of the primary key of the record that this object is associated with.  An error 
108	 * will be thrown if there is more than one primary key.
109	 *
110	 * @return mixed
111	 */
112	public function getId()
113	{
114		assert(count($this->primaryKey) == 1);
115		return $this->scalars[$this->primaryKey[0]];
116	}
117
118	/**
119	 * Returns the field name(s) in the primary key in an array
120	 *
121	 * @return array of field names
122	 */
123	public function getPrimaryKey()
124	{
125		return $this->primaryKey;
126	}
127
128	/**
129	 * Returns true if this DbObject class is set to use primary keys generated by the database
130	 *
131	 * @return boolean True if the database automatically generates primary keys
132	 */
133	public function primaryKeyAssignedByDb()
134	{
135		return $this->keyAssignedBy == self::keyAssignedBy_db ? true : false;
136	}
137
138	/**
139	 * Returns a DbTable object with scheme information for the associated table (if supported by your database)
140	 *
141	 * @return DbTable DbTable object
142	 */
143	static public function _getTableSchema($className)
144	{
145		$object = new $className();
146		return $object->getSchema();
147	}
148	
149	/**
150	 * Returns a DbTable object with scheme information for the associated table (if supported by your database)
151	 *
152	 * @return DbTable DbTable object
153	 */
154	public function getSchema()
155	{
156		return new DbTable(self::_getConnection(get_class($this)), $this->tableName);
157	}
158	
159	/**
160	 * Alias for getSchema
161	 *
162	 * @return DbTable DbTable object
163	 */
164	public function getTable()
165	{
166		return $this->getSchema();
167	}
168
169	/**
170	 * Loops through all the fields in the associated table and ensures a default NULL value for each of them in the class
171	 * This feature only works with DbConnection objects that have full schema support implemented
172	 */
173	public function forceFields()
174	{
175		if($this->bound)
176			$this->loadScalars();
177		else
178		{
179			foreach($this->getSchema()->fields as $thisField)
180				$this->scalars[$thisField->name] = NULL;
181		}
182	}
183
184	/**
185	 * Serializes all column names and values and returns them in a string of the format "<DbObject class>: <field> => <value> <field> => <value> ..."
186	 *
187	 * @return string string
188	 */
189	public function getString()
190	{
191		$s = '';
192		$this->loadScalars();
193		foreach($this->scalars as $field => $value)
194			$s .= " $field => $value";
195		return get_class($this) . ':' . $s;
196	}
197
198	/**
199	 * Returns the connection associated with the DbObject
200	 *
201	 * @return DbConnection DbConnection object
202	 */
203	public function getDb()
204	{
205		return self::_getConnection(get_class($this));
206	}
207
208	//
209	//	the scalar handlers
210	//
211	//	rewrite them and make them handle primary keys with different names or more than one field
212	//
213
214	/**
215	 * Returns a $field => $value array containing all the fields and their values
216	 *
217	 * @return assoc_array assoc_array containing all fields and values
218	 */
219	public function getFields()
220	{
221		return $this->scalars;
222	}
223
224	/**
225	 * Accepts a $field => $value array containing fields and their values to be set in this DbObject
226	 *
227	 * @param assoc_array $data $field => $value associative array with data to be stored in this object
228	 */
229	public function setFields($data)
230	{
231		$this->assignScalars($data);
232	}
233
234	/**
235	 * Returns the value of the specified field
236	 *
237	 * @param string $field Field to retreive
238	 * @return string String value of the field
239	 */
240	public function getField($field)
241	{
242		return $this->getScalar($field);
243	}
244
245	/**
246	 * Returns the value of the specified field
247	 *
248	 * @param string $field Field to retreive
249	 * @return string String value of the field
250	 */
251	private function getScalar($field)
252	{
253		if(!isset($this->scalars[$field]))
254		{
255			if(!$this->bound)
256			{
257				/* TODO: Handle "getScalar" calls to unbound DbObject instances
258				Different possibilities on how to handle this situation.  Maybe we could use some flags.
259				1. check the metadata.  (alwaysCheckMeta)
260					1. if its there then (useDummyDefaults requires alwaysCheckMeta)
261						1. return the default value
262						2. return NULL
263					2. if its not there
264						1. throw and error
265				2.	dont check the metadata (useDummyNulls requires !alwaysCheckMeta)
266					1.	return null
267					2.	throw an error
268
269				trigger_error("the field: $field is not present in memory and this object is not yet bound to a database row");
270				*/
271
272				return NULL;
273			}
274
275			$this->loadScalars();
276		}
277		
278		if(!array_key_exists($field, $this->scalars))
279			trigger_error("the field $field is present neither in memory nor in the cooresponding database table");
280
281		return $this->scalars[$field];
282	}
283
284	/**
285	 * Assigns a value to the specified field
286	 *
287	 * @param string $field Field to change the value of
288	 * @param mixed $value Value to assign to the specified field
289	 */
290	private function setScalar($field, $value)
291	{
292		$data[$field] = $value;
293		$this->assignScalars($data);
294	}
295
296	/*
297	private function setScalars($data)
298	{
299		foreach($data as $field => $value)
300		{
301			$this->scalars[$field] = $value;
302		}
303	}
304	*/
305
306	/**
307	 * Accepts a $field => $value array containing fields and their values to be set in this DbObject
308	 *
309	 * @param assoc_array $data $field => $value associative array with data to be stored in this object
310	 */
311	private function assignScalars($data)
312	{
313		foreach($data as $member => $value)
314		{
315			if(!isset($this->scalars[$member]) && in_array($member, $this->primaryKey))
316			{
317				$this->missingKeyFields--;
318				if($this->missingKeyFields == 0)
319					$this->bound = 1;
320			}
321
322			$this->scalars[$member] = $value;
323		}
324	}
325
326	/**
327	 * Loads values into the fields of the DbObject
328	 *
329	 */
330	private function loadScalars()
331	{
332		assert($this->bound);
333		$row = $this->fetchPersisted();
334		$this->assignPersisted($row);
335	}
336
337	private function assignPersisted($row)
338	{
339		//	if they manually set a field don't write over it just because they loaded one scalar
340		foreach($row as $field => $value)
341		{
342			if(!isset($this->scalars[$field]))
343				$this->scalars[$field] = $value;
344		}
345	}
346
347	/**
348	 * Retrieves field values from the database using primary key as lookup fields
349	 *
350	 * @return unknown
351	 */
352	private function fetchPersisted()
353	{
354		$wheres = array();
355		$whereValues = array();
356		foreach($this->primaryKey as $keyField)
357		{
358			$wheres[] = ":fld_$keyField:identifier = :$keyField";
359			$whereValues["fld_$keyField"] = $keyField;
360			$whereValues[$keyField] = $this->scalars[$keyField];
361		}
362		$whereClause = implode(' and ', $wheres);
363		$row = self::_getConnection(get_class($this))->fetchRow("select * from $this->tableName where $whereClause", $whereValues);
364		if($row)
365			$this->persisted = true;
366		else
367			$this->persisted = false;
368		return $row;
369	}
370
371	/**
372	 * Returns true if this DbObject is (and can be) saved in the database
373	 *
374	 * @return boolean True if the DbObject is/can be saved in the DB
375	 */
376	private function _persisted()
377	{
378		if(!$this->bound)
379			return false;
380
381		if($this->keyAssignedBy == self::keyAssignedBy_db)
382			return true;
383		else
384		{
385			$row = $this->fetchPersisted();
386			if($row)
387			{
388				//	we might as well save the results
389				$this->assignPersisted($row);
390				return true;
391			}
392
393			return false;
394		}
395	}
396
397	/**
398	 * Returns true if this DbObject is (and can be) saved in the database
399	 *
400	 * @return boolean True if the DbObject is/can be saved in the DB
401	 */
402	public function persisted()
403	{
404		if($this->persisted !== NULL)
405			return $this->persisted;
406		else
407			return $this->persisted = $this->_persisted();
408	}
409
410	/**
411	 * Saves the record in memory
412	 *
413	 */
414	public function save()
415	{
416		if(!$this->bound)
417		{
418			if($this->keyAssignedBy == self::keyAssignedBy_db)
419				$this->setScalar($this->primaryKey[0], self::_getConnection(get_class($this))->insertArray($this->tableName, $this->scalars));
420			else
421				trigger_error("you must define all foreign key fields in order by save this object");
422		}
423		else
424		{
425			if($this->keyAssignedBy == self::keyAssignedBy_db)
426			{
427				$updateInfo = DbConnection::generateUpdateInfo($this->tableName, $this->getKeyConditions(), $this->scalars);
428				self::_getConnection(get_class($this))->updateRow($updateInfo['sql'], $updateInfo['params']);
429			}
430			else
431			{
432				if(!$this->persisted())
433					self::_getConnection(get_class($this))->insertArray($this->tableName, $this->scalars, false);
434				else
435				{
436					$updateInfo = DbConnection::generateUpdateInfo($this->tableName, $this->getKeyConditions(), $this->scalars);
437					self::_getConnection(get_class($this))->updateRow($updateInfo['sql'], $updateInfo['params']);
438				}
439			}
440		}
441	}
442
443	/**
444	 * Returns an array containing all primary key fields that have a value assigned to them in this DbObject instance
445	 *
446	 * @return array of fields
447	 */
448	private function getKeyConditions()
449	{
450		assert($this->bound);
451		return array_intersect_key($this->scalars, array_flip($this->primaryKey));
452	}
453
454	/**
455	 * Deletes the record from the database, deletes all fields and values from memory, and unbinds the DbObject
456	 *
457	 */
458	public function destroy()
459	{
460		//	have a way to destroy any existing vector fields or refuse to continue (destroy_r)
461		$deleteInfo = DbConnection::generateDeleteInfo($this->tableName, $this->getKeyConditions());
462		self::_getConnection(get_class($this))->deleteRow($deleteInfo['sql'], $deleteInfo['params']);
463		$this->bound = false;
464		$this->scalars = array();
465		$this->persisted = false;
466	}
467
468	//
469	//	end of scalar handlers
470	//
471
472
473	//
474	//	vector handlers
475	//
476
477	private function addRelationship($name, $relationship)
478	{
479		$this->relationships[$name] = $relationship;
480	}
481
482	private function hasRelationship($name)
483	{
484		return isset($this->relationships[$name]) ? true : false;
485	}
486
487	private function getRelationshipInfo($name)
488	{
489		return $this->relationships[$name]->getInfo();
490	}
491
492	protected function hasMany($name, $params = array())
493	{
494		if(isset($params['through']) && $params['through'])
495			$this->addRelationship($name, new DbRelationshipHasManyThrough($name, $params, $this));
496		else
497			$this->addRelationship($name, new DbRelationshipHasMany($name, $params, $this));
498	}
499
500	protected function hasOne($name, $params = array())
501	{
502		$this->addRelationship($name, new DbRelationshipHasOne($name, $params, $this));
503	}
504
505	protected function belongsTo($name, $params = array())
506	{
507		$this->addRelationship($name, new DbRelationshipBelongsTo($name, $params, $this));
508	}
509
510	protected function fieldOptions($name, $params = array())
511	{
512		$this->addRelationship($name, new DbRelationshipOptions($name, $params, $this));
513	}
514	
515	public function getFieldOptions($field)
516	{
517		foreach($this->relationships as $thisRelationship)
518			if($thisRelationship instanceof DbRelationshipOptions && $thisRelationship->isTiedToField($field))
519				return $thisRelationship;
520		
521		return false;
522	}
523
524	//
525	//	end vector handlers
526	//
527
528	//
529	//	begin magic functions
530	//
531
532	/**
533	 * Automatic getter: maps unknown variables to database fields
534	 *
535	 * @param string $varname Name of the database field to get the value of
536	 * @return mixed Value of the given database field
537	 */
538	public function __get($varname)
539	{
540		// check on inherited getters, setters, and mixins from Object
541		if(parent::__isset($varname))
542			return parent::__get($varname);
543		
544		if($this->hasRelationship($varname))
545			return $this->getRelationshipInfo($varname);
546
547		return $this->getScalar($varname);
548	}
549
550	/**
551	 * Automatic setter: maps unknown variables to database fields
552	 *
553	 * @param string $varname Name of the database field to set the value of  
554	 * @param mixed $value New value for the given database field
555	 */
556	function __set($varname, $value)
557	{
558		$this->setScalar($varname, $value);
559	}
560
561	//
562	//	end magic functions
563	//
564
565	//
566	//	begin iterator functions
567	//
568
569	/**
570	 * Resets the internal pointer to the first column
571	 *
572	 */
573	public function rewind()
574	{
575		reset($this->scalars);
576	}
577
578	/**
579	 * Returns the value of the column that the internal pointer is at
580	 *
581	 * @return mixed
582	 */
583	public function current()
584	{
585		$var = current($this->scalars);
586		return $var;
587	}
588
589	/**
590	 * returns the name of the column the internal pointer is at
591	 *
592	 * @return string Column Name
593	 */
594	public function key()
595	{
596		$var = key($this->scalars);
597		return $var;
598	}
599
600	/**
601	 * Moves the internal pointer to the next column and returns the value of that column
602	 *
603	 * @return mixed Value of the next column
604	 */
605	public function next()
606	{
607		$var = next($this->scalars);
608		return $var;
609	}
610
611	/**
612	 * Returns true if this DbObject is successfully bound to a row in the database
613	 *
614	 * @return boolean True if the object is bound to a row in the database
615	 */
616	public function valid()
617	{
618		$var = $this->current() !== false;
619		return $var;
620	}
621
622	//
623	//	end iterator functions
624	//
625
626
627	//
628	//	static methods
629	//
630
631	/**
632	 * Returns the name of the default connection to be used with this DbObject (override in child class to default to a different connection)
633	 *
634	 * @param string $className Name of the DbObject to get the default connection name for
635	 * @return string Name of the default database connection for this object
636	 */
637	static private function _getConnectionName($className)
638	{
639		return 'default';
640	}
641
642	/**
643	 * Static method to return the database connection associated with a given DbObject
644	 *
645	 * @param string $className Name of the DbObject to retreive the default connection of
646	 * @return DbConnection object
647	 */
648	static private function _getConnection($className)
649	{
650		return DbModule::getConnection(call_user_func(array($className, '_getConnectionName'), $className));
651	}
652
653	/**
654	 * Returns the name of the SQL table based on the name of the DbObject class
655	 *
656	 * @param string $className
657	 * @return string Name of the SQL table to link to
658	 */
659	static public function _getTableName($className)
660	{
661		//	work around lack of "late static binding"
662		$dummy = new $className();
663		return $dummy->getTableName();
664	}
665
666	/**
667	 * Static method creates a new DbObject by name.  There must be a class that inherits from DbObject of the name "className" for this to work.
668	 *
669	 * @param string $className Name of the DbObject class to use
670	 * @param array $values Associative array of $fieldName => $value to store in the table
671	 * @return DbObject
672	 */
673	static public function _create($className, $values)
674	{
675		$object = new $className($values);
676		$object->save();
677		return $object;
678	}
679
680	/**
681	 * Inserts a row into the table associated with the given DbObject class
682	 *
683	 * @param string $className Name of the DbObject class
684	 * @param assoc_array $values $field => $value array to be inserted into the database (must contain all required fields or a SQL error will be generated)
685	 */
686	static public function _insert($className, $values)
687	{
688		self::_getConnection($className)->insertArray(self::_getTableName($className), $values, false);
689	}
690
691	/**
692	 * Returns an array of DbObjects each representing a row in the database returned by the given SQL statement
693	 *
694	 * @param string $className Name of the DbObject class to retreive
695	 * @param string $sql SQL select statement to use to search
696	 * @param assoc_array $params $param => $value array with parameters to be substituted into the SQL statement
697	 * @return array of DbObject
698	 */
699	static public function _findBySql($className, $sql, $params)
700	{
701		$res = self::_getConnection($className)->query($sql, $params);
702
703		if(!$res->valid())
704			return array();
705
706		$objects = array();
707		for($row = $res->current(); $res->valid(); $row = $res->next())
708		{
709			$objects[] = new $className($row);
710		}
711
712		return $objects;
713	}
714
715	/**
716	 * Returns an array of DbObjects each representing a row in the database returned by selecting on the DbObject specified with the given WHERE clause
717	 *
718	 * @param string $className Name of the DbObject class to retreive
719	 * @param string $where SQL "where" clause (minus the "where " at the beginning) to select on
720	 * @param assoc_array $params $param => $value array with parameters to be substituted into the WHERE clause
721	 * @return array of DbObject
722	 */
723	static public function _findByWhere($className, $where, $params)
724	{
725		$tableName = DbObject::_getTableName($className);
726		return self::_findBySql($className, "select * from $tableName where $where", $params);
727	}
728
729	/**
730	 * Searches the database and returns an array of DbObjects each representing a record in the resultset
731	 *
732	 * @param string $className Name of the DbObject class to retreive
733	 * @param assoc_array $conditions $field => $value array of conditions to search on (currently only "=" operator is supported)
734	 * @param assoc_array $params $param => $value array of parameters to be passed to generateSelectInfo
735	 * @return array of DbObject (s)
736	 */
737	
738	static public function _find($className, $conditions = NULL, $params = NULL)
739	{
740		$tableName = DbObject::_getTableName($className);
741		$selectInfo = self::_getConnection($className)->generateSelectInfo($tableName, '*', $conditions, $params);
742		return self::_findBySql($className, $selectInfo['sql'], $selectInfo['params']);
743	}
744
745	/**
746	 * Retrieve one object from the database and map it to an object.  Throws an error if more than one row is returned.
747	 *
748	 * @param string $className The name of the class corresponding to the table in the database
749	 * @param array $conditions Key value pair for the fields you want to look up
750	 * @return DbObject
751	 */
752	static public function _findOne($className, $conditions = NULL)
753	{
754		$a = DbObject::_find($className, $conditions);
755		if(!$a)
756			return false;
757
758		assert(is_array($a));
759		assert(count($a) == 1);
760
761		return current($a);
762	}
763
764	/**
765	 * Retrieves a DbObject from the given table with a row in it, creating the row if neccesary
766	 *
767	 * @param string $className name of the DbObject class to return an instance of
768	 * @param assoc_array $conditions $field => $value for generating the where clause to
769	 * @return DbObject
770	 */
771	static public function _getOne($className, $conditions = NULL, $default = NULL)
772	{
773		$tableName = DbObject::_getTableName($className);
774		$row = self::_getConnection($className)->selsertRow($tableName, "*", $conditions, $default);
775		return new $className($row);
776	}
777
778	//
779	//	end static methods
780	//
781}