Subversion-Projekte lars-tiefland.prado

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/**
3
 * TActiveRecord and TActiveRecordEventParameter class file.
4
 *
5
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
6
 * @link http://www.pradosoft.com/
7
 * @copyright Copyright &copy; 2005-2008 PradoSoft
8
 * @license http://www.pradosoft.com/license/
9
 * @version $Id: TActiveRecord.php 2598 2009-01-07 07:28:22Z christophe.boulain $
10
 * @package System.Data.ActiveRecord
11
 */
12
 
13
/**
14
 * Load record manager, criteria and relations.
15
 */
16
Prado::using('System.Data.ActiveRecord.TActiveRecordManager');
17
Prado::using('System.Data.ActiveRecord.TActiveRecordCriteria');
18
Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext');
19
 
20
/**
21
 * Base class for active records.
22
 *
23
 * An active record creates an object that wraps a row in a database table
24
 * or view, encapsulates the database access, and adds domain logic on that data.
25
 *
26
 * Active record objects are stateful, this is main difference between the
27
 * TActiveRecord implementation and the TTableGateway implementation.
28
 *
29
 * The essence of an Active Record is an object model of the
30
 * domain (e.g. products, items) that incorporates both behavior and
31
 * data in which the classes match very closely the record structure of an
32
 * underlying database. Each Active Record is responsible for saving and
33
 * loading to the database and also for any domain logic that acts on the data.
34
 *
35
 * The Active Record provides methods that do the following:
36
 *  1. Construct an instance of the Active Record from a SQL result set row.
37
 *  2. Construct a new instance for later insertion into the table.
38
 *  3. Finder methods to wrap commonly used SQL queries and return Active Record objects.
39
 *  4. Update the database and insert into it the data in the Active Record.
40
 *
41
 * Example:
42
 * <code>
43
 * class UserRecord extends TActiveRecord
44
 * {
45
 *     const TABLE='users'; //optional table name.
46
 *
47
 *     public $username; //corresponds to the fieldname in the table
48
 *     public $email;
49
 *
50
 *     //returns active record finder instance
51
 *     public static function finder($className=__CLASS__)
52
 *     {
53
 *         return parent::finder($className);
54
 *     }
55
 * }
56
 *
57
 * //create a connection and give it to the ActiveRecord manager.
58
 * $dsn = 'pgsql:host=localhost;dbname=test';
59
 * $conn = new TDbConnection($dsn, 'dbuser','dbpass');
60
 * TActiveRecordManager::getInstance()->setDbConnection($conn);
61
 *
62
 * //load the user record with username (primary key) 'admin'.
63
 * $user = UserRecord::finder()->findByPk('admin');
64
 * $user->email = 'admin@example.org';
65
 * $user->save(); //update the 'admin' record.
66
 * </code>
67
 *
68
 * Since v3.1.1, TActiveRecord starts to support column mapping. The physical
69
 * column names (defined in database) can be mapped to logical column names
70
 * (defined in active classes as public properties.) To use this feature, declare
71
 * a static class variable COLUMN_MAPPING like the following:
72
 * <code>
73
 * class UserRecord extends TActiveRecord
74
 * {
75
 *     const TABLE='users';
76
 *     public static $COLUMN_MAPPING=array
77
 *     (
78
 *         'user_id'=>'username',
79
 *         'email_address'=>'email',
80
 *     );
81
 *     public $username;
82
 *     public $email;
83
 * }
84
 * </code>
85
 * In the above, the 'users' table consists of 'user_id' and 'email_address' columns,
86
 * while the UserRecord class declares 'username' and 'email' properties.
87
 * By using column mapping, we can regularize the naming convention of column names
88
 * in active record.
89
 *
90
 * Since v3.1.2, TActiveRecord enhanced its support to access of foreign objects.
91
 * By declaring a public static variable RELATIONS like the following, one can access
92
 * the corresponding foreign objects easily:
93
 * <code>
94
 * class UserRecord extends TActiveRecord
95
 * {
96
 *     const TABLE='users';
97
 *     public static $RELATIONS=array
98
 *     (
99
 *         'department'=>array(self::BELONGS_TO, 'DepartmentRecord', 'department_id'),
100
 *         'contacts'=>array(self::HAS_MANY, 'ContactRecord', 'user_id'),
101
 *     );
102
 * }
103
 * </code>
104
 * In the above, the users table is related with departments table (represented by
105
 * DepartmentRecord) and contacts table (represented by ContactRecord). Now, given a UserRecord
106
 * instance $user, one can access its department and contacts simply by: $user->department and
107
 * $user->contacts. No explicit data fetching is needed. Internally, the foreign objects are
108
 * fetched in a lazy way, which avoids unnecessary overhead if the foreign objects are not accessed
109
 * at all.
110
 *
111
 * Since v3.1.2, new events OnInsert, OnUpdate and OnDelete are available.
112
 * The event OnInsert, OnUpdate and OnDelete methods are executed before
113
 * inserting, updating, and deleting the current record, respectively. You may override
114
 * these methods; a TActiveRecordChangeEventParameter parameter is passed to these methods.
115
 * The property {@link TActiveRecordChangeEventParameter::setIsValid IsValid} of the parameter
116
 * can be set to false to prevent the change action to be executed. This can be used,
117
 * for example, to validate the record before the action is executed. For example,
118
 * in the following the password property is hashed before a new record is inserted.
119
 * <code>
120
 * class UserRecord extends TActiveRecord
121
 * {
122
 *      function OnInsert($param)
123
 *      {
124
 *          //parent method should be called to raise the event
125
 *          parent::OnInsert($param);
126
 *          $this->nounce = md5(time());
127
 *          $this->password = md5($this->password.$this->nounce);
128
 *      }
129
 * }
130
 * </code>
131
 *
132
 * Since v3.1.3 you can also define a method that returns the table name.
133
 * <code>
134
 * class UserRecord extends TActiveRecord
135
 * {
136
 *     public function table()
137
 *     {
138
 *          return 'users';
139
 *     }
140
 *
141
 * }
142
 * </code>
143
 *
144
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
145
 * @version $Id: TActiveRecord.php 2598 2009-01-07 07:28:22Z christophe.boulain $
146
 * @package System.Data.ActiveRecord
147
 * @since 3.1
148
 */
149
abstract class TActiveRecord extends TComponent
150
{
151
	const BELONGS_TO='BELONGS_TO';
152
	const HAS_ONE='HAS_ONE';
153
	const HAS_MANY='HAS_MANY';
154
	const MANY_TO_MANY='MANY_TO_MANY';
155
 
156
	const STATE_NEW=0;
157
	const STATE_LOADED=1;
158
	const STATE_DELETED=2;
159
 
160
	/**
161
	 * @var integer record state: 0 = new, 1 = loaded, 2 = deleted.
162
	 * @since 3.1.2
163
	 */
164
	protected $_recordState=0; // use protected so that serialization is fine
165
 
166
	/**
167
	 * This static variable defines the column mapping.
168
	 * The keys are physical column names as defined in database,
169
	 * and the values are logical column names as defined as public variable/property names
170
	 * for the corresponding active record class.
171
	 * @var array column mapping. Keys: physical column names, values: logical column names.
172
	 * @since 3.1.1
173
	 */
174
	public static $COLUMN_MAPPING=array();
175
	private static $_columnMapping=array();
176
 
177
	/**
178
	 * This static variable defines the relationships.
179
	 * The keys are public variable/property names defined in the AR class.
180
	 * Each value is an array, e.g. array(self::HAS_MANY, 'PlayerRecord').
181
	 * @var array relationship.
182
	 * @since 3.1.1
183
	 */
184
	public static $RELATIONS=array();
185
	private static $_relations=array();
186
 
187
	/**
188
	 * @var TDbConnection database connection object.
189
	 */
190
	protected $_connection; // use protected so that serialization is fine
191
 
192
	/**
193
	 * Prevent __call() method creating __sleep() when serializing.
194
	 */
195
	public function __sleep()
196
	{
197
		$this->_connection=null;
198
		return array_keys(get_object_vars($this));
199
	}
200
 
201
	/**
202
	 * Prevent __call() method creating __wakeup() when unserializing.
203
	 */
204
	public function __wakeup()
205
	{
206
		$this->setupColumnMapping();
207
		$this->setupRelations();
208
	}
209
 
210
	/**
211
	 * Create a new instance of an active record with given $data. The record
212
	 * can be saved to the database specified by the $connection object.
213
	 *
214
	 * @param array optional name value pair record data.
215
	 * @param TDbConnection optional database connection this object record use.
216
	 */
217
	public function __construct($data=array(), $connection=null)
218
	{
219
		if($connection!==null)
220
			$this->setDbConnection($connection);
221
		$this->setupColumnMapping();
222
		$this->setupRelations();
223
		if(!empty($data)) //$data may be an object
224
			$this->copyFrom($data);
225
	}
226
 
227
	/**
228
	 * Magic method for reading properties.
229
	 * This method is overriden to provide read access to the foreign objects via
230
	 * the key names declared in the RELATIONS array.
231
	 * @param string property name
232
	 * @return mixed property value.
233
	 * @since 3.1.2
234
	 */
235
	public function __get($name)
236
	{
237
		if($this->hasRecordRelation($name) && !$this->canGetProperty($name))
238
		{
239
			$this->fetchResultsFor($name);
240
			return $this->$name;
241
		}
242
		return parent::__get($name);
243
	}
244
 
245
	/**
246
	 * Magic method for writing properties.
247
	 * This method is overriden to provide write access to the foreign objects via
248
	 * the key names declared in the RELATIONS array.
249
	 * @param string property name
250
	 * @param mixed property value.
251
	 * @since 3.1.2
252
	 */
253
	public function __set($name,$value)
254
	{
255
		if($this->hasRecordRelation($name) && !$this->canSetProperty($name))
256
			$this->$name=$value;
257
		else
258
			parent::__set($name,$value);
259
	}
260
 
261
	/**
262
	 * @since 3.1.1
263
	 */
264
	private function setupColumnMapping()
265
	{
266
		$className=get_class($this);
267
		if(!isset(self::$_columnMapping[$className]))
268
		{
269
			$class=new ReflectionClass($className);
270
			self::$_columnMapping[$className]=$class->getStaticPropertyValue('COLUMN_MAPPING');
271
		}
272
	}
273
 
274
	/**
275
	 * @since 3.1.2
276
	 */
277
	private function setupRelations()
278
	{
279
		$className=get_class($this);
280
		if(!isset(self::$_relations[$className]))
281
		{
282
			$class=new ReflectionClass($className);
283
			$relations=array();
284
			foreach($class->getStaticPropertyValue('RELATIONS') as $key=>$value)
285
				$relations[strtolower($key)]=array($key,$value);
286
			self::$_relations[$className]=$relations;
287
		}
288
	}
289
 
290
	/**
291
	 * Copies data from an array or another object.
292
	 * @throws TActiveRecordException if data is not array or not object.
293
	 */
294
	public function copyFrom($data)
295
	{
296
		if(is_object($data))
297
			$data=get_object_vars($data);
298
		if(!is_array($data))
299
			throw new TActiveRecordException('ar_data_invalid', get_class($this));
300
		foreach($data as $name=>$value)
301
			$this->setColumnValue($name,$value);
302
	}
303
 
304
 
305
	public static function getActiveDbConnection()
306
	{
307
		if(($db=self::getRecordManager()->getDbConnection())!==null)
308
			$db->setActive(true);
309
		return $db;
310
	}
311
 
312
	/**
313
	 * Gets the current Db connection, the connection object is obtained from
314
	 * the TActiveRecordManager if connection is currently null.
315
	 * @return TDbConnection current db connection for this object.
316
	 */
317
	public function getDbConnection()
318
	{
319
		if($this->_connection===null)
320
			$this->_connection=self::getActiveDbConnection();
321
		return $this->_connection;
322
	}
323
 
324
	/**
325
	 * @param TDbConnection db connection object for this record.
326
	 */
327
	public function setDbConnection($connection)
328
	{
329
		$this->_connection=$connection;
330
	}
331
 
332
	/**
333
	 * @return TDbTableInfo the meta information of the table associated with this AR class.
334
	 */
335
	public function getRecordTableInfo()
336
	{
337
		return $this->getRecordGateway()->getRecordTableInfo($this);
338
	}
339
 
340
	/**
341
	 * Compare two records using their primary key values (all column values if
342
	 * table does not defined primary keys). The default uses simple == for
343
	 * comparison of their values. Set $strict=true for identity comparison (===).
344
	 * @param TActiveRecord another record to compare with.
345
	 * @param boolean true to perform strict identity comparison
346
	 * @return boolean true if $record equals, false otherwise.
347
	 */
348
	public function equals(TActiveRecord $record, $strict=false)
349
	{
350
		if($record===null || get_class($this)!==get_class($record))
351
			return false;
352
		$tableInfo = $this->getRecordTableInfo();
353
		$pks = $tableInfo->getPrimaryKeys();
354
		$properties = count($pks) > 0 ? $pks : $tableInfo->getColumns()->getKeys();
355
		$equals=true;
356
		foreach($properties as $prop)
357
		{
358
			if($strict)
359
				$equals = $equals && $this->getColumnValue($prop) === $record->getColumnValue($prop);
360
			else
361
				$equals = $equals && $this->getColumnValue($prop) == $record->getColumnValue($prop);
362
			if(!$equals)
363
				return false;
364
		}
365
		return $equals;
366
	}
367
 
368
	/**
369
	 * Returns the instance of a active record finder for a particular class.
370
	 * The finder objects are static instances for each ActiveRecord class.
371
	 * This means that event handlers bound to these finder instances are class wide.
372
	 * Create a new instance of the ActiveRecord class if you wish to bound the
373
	 * event handlers to object instance.
374
	 * @param string active record class name.
375
	 * @return TActiveRecord active record finder instance.
376
	 */
377
	public static function finder($className=__CLASS__)
378
	{
379
		static $finders = array();
380
		if(!isset($finders[$className]))
381
		{
382
			$f = Prado::createComponent($className);
383
			$finders[$className]=$f;
384
		}
385
		return $finders[$className];
386
	}
387
 
388
	/**
389
	 * Gets the record manager for this object, the default is to call
390
	 * TActiveRecordManager::getInstance().
391
	 * @return TActiveRecordManager default active record manager.
392
	 */
393
	public static function getRecordManager()
394
	{
395
		return TActiveRecordManager::getInstance();
396
	}
397
 
398
	/**
399
	 * @return TActiveRecordGateway record table gateway.
400
	 */
401
	public function getRecordGateway()
402
	{
403
		return TActiveRecordManager::getInstance()->getRecordGateway();
404
	}
405
 
406
	/**
407
	 * Saves the current record to the database, insert or update is automatically determined.
408
	 * @return boolean true if record was saved successfully, false otherwise.
409
	 */
410
	public function save()
411
	{
412
		$gateway = $this->getRecordGateway();
413
		$param = new TActiveRecordChangeEventParameter();
414
		if($this->_recordState===self::STATE_NEW)
415
		{
416
			$this->onInsert($param);
417
			if($param->getIsValid() && $gateway->insert($this))
418
			{
419
				$this->_recordState = self::STATE_LOADED;
420
				return true;
421
			}
422
		}
423
		else if($this->_recordState===self::STATE_LOADED)
424
		{
425
			$this->onUpdate($param);
426
			if($param->getIsValid() && $gateway->update($this))
427
				return true;
428
		}
429
		else
430
			throw new TActiveRecordException('ar_save_invalid', get_class($this));
431
 
432
		return false;
433
	}
434
 
435
	/**
436
	 * Deletes the current record from the database. Once deleted, this object
437
	 * can not be saved again in the same instance.
438
	 * @return boolean true if the record was deleted successfully, false otherwise.
439
	 */
440
	public function delete()
441
	{
442
		if($this->_recordState===self::STATE_LOADED)
443
		{
444
			$gateway = $this->getRecordGateway();
445
			$param = new TActiveRecordChangeEventParameter();
446
			$this->onDelete($param);
447
			if($param->getIsValid() && $gateway->delete($this))
448
			{
449
				$this->_recordState=self::STATE_DELETED;
450
				return true;
451
			}
452
		}
453
		else
454
			throw new TActiveRecordException('ar_delete_invalid', get_class($this));
455
 
456
		return false;
457
	}
458
 
459
	/**
460
	 * Delete records by primary key. Usage:
461
	 *
462
	 * <code>
463
	 * $finder->deleteByPk($primaryKey); //delete 1 record
464
	 * $finder->deleteByPk($key1,$key2,...); //delete multiple records
465
	 * $finder->deleteByPk(array($key1,$key2,...)); //delete multiple records
466
	 * </code>
467
	 *
468
	 * For composite primary keys (determined from the table definitions):
469
	 * <code>
470
	 * $finder->deleteByPk(array($key1,$key2)); //delete 1 record
471
	 *
472
	 * //delete multiple records
473
	 * $finder->deleteByPk(array($key1,$key2), array($key3,$key4),...);
474
	 *
475
	 * //delete multiple records
476
	 * $finder->deleteByPk(array( array($key1,$key2), array($key3,$key4), .. ));
477
	 * </code>
478
	 *
479
	 * @param mixed primary key values.
480
	 * @return int number of records deleted.
481
	 */
482
	public function deleteByPk($keys)
483
	{
484
		if(func_num_args() > 1)
485
			$keys = func_get_args();
486
		return $this->getRecordGateway()->deleteRecordsByPk($this,(array)$keys);
487
	}
488
 
489
	/**
490
	 * Alias for deleteByPk()
491
	 */
492
	public function deleteAllByPks($keys)
493
	{
494
		if(func_num_args() > 1)
495
			$keys = func_get_args();
496
		return $this->deleteByPk($keys);
497
	}
498
	/**
499
	 * Delete multiple records using a criteria.
500
	 * @param string|TActiveRecordCriteria SQL condition or criteria object.
501
	 * @param mixed parameter values.
502
	 * @return int number of records deleted.
503
	 */
504
	public function deleteAll($criteria=null, $parameters=array())
505
	{
506
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
507
		$criteria = $this->getRecordCriteria($criteria,$parameters, $args);
508
		return $this->getRecordGateway()->deleteRecordsByCriteria($this, $criteria);
509
	}
510
 
511
	/**
512
	 * Populates a new record with the query result.
513
	 * This is a wrapper of {@link createRecord}.
514
	 * @param array name value pair of record data
515
	 * @return TActiveRecord object record, null if data is empty.
516
	 */
517
	protected function populateObject($data)
518
	{
519
		return self::createRecord(get_class($this), $data);
520
	}
521
 
522
	/**
523
	 * @param TDbDataReader data reader
524
	 * @return array the AR objects populated by the query result
525
	 * @since 3.1.2
526
	 */
527
	protected function populateObjects($reader)
528
	{
529
		$result=array();
530
		foreach($reader as $data)
531
			$result[] = $this->populateObject($data);
532
		return $result;
533
	}
534
 
535
	/**
536
	 * Create an AR instance specified by the AR class name and initial data.
537
	 * If the initial data is empty, the AR object will not be created and null will be returned.
538
	 * (You should use the "new" operator to create the AR instance in that case.)
539
	 * @param string the AR class name
540
	 * @param array initial data to be populated into the AR object.
541
	 * @return TActiveRecord the initialized AR object. Null if the initial data is empty.
542
	 * @since 3.1.2
543
	 */
544
	public static function createRecord($type, $data)
545
	{
546
		if(empty($data))
547
			return null;
548
		$record=new $type($data);
549
		$record->_recordState=self::STATE_LOADED;
550
		return $record;
551
	}
552
 
553
	/**
554
	 * Find one single record that matches the criteria.
555
	 *
556
	 * Usage:
557
	 * <code>
558
	 * $finder->find('username = :name AND password = :pass',
559
	 * 					array(':name'=>$name, ':pass'=>$pass));
560
	 * $finder->find('username = ? AND password = ?', array($name, $pass));
561
	 * $finder->find('username = ? AND password = ?', $name, $pass);
562
	 * //$criteria is of TActiveRecordCriteria
563
	 * $finder->find($criteria); //the 2nd parameter for find() is ignored.
564
	 * </code>
565
	 *
566
	 * @param string|TActiveRecordCriteria SQL condition or criteria object.
567
	 * @param mixed parameter values.
568
	 * @return TActiveRecord matching record object. Null if no result is found.
569
	 */
570
	public function find($criteria,$parameters=array())
571
	{
572
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
573
		$criteria = $this->getRecordCriteria($criteria,$parameters, $args);
574
		$criteria->setLimit(1);
575
		$data = $this->getRecordGateway()->findRecordsByCriteria($this,$criteria);
576
		return $this->populateObject($data);
577
	}
578
 
579
	/**
580
	 * Same as find() but returns an array of objects.
581
	 *
582
	 * @param string|TActiveRecordCriteria SQL condition or criteria object.
583
	 * @param mixed parameter values.
584
	 * @return array matching record objects. Empty array if no result is found.
585
	 */
586
	public function findAll($criteria=null,$parameters=array())
587
	{
588
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
589
		if($criteria!==null)
590
			$criteria = $this->getRecordCriteria($criteria,$parameters, $args);
591
		$result = $this->getRecordGateway()->findRecordsByCriteria($this,$criteria,true);
592
		return $this->populateObjects($result);
593
	}
594
 
595
	/**
596
	 * Find one record using only the primary key or composite primary keys. Usage:
597
	 *
598
	 * <code>
599
	 * $finder->findByPk($primaryKey);
600
	 * $finder->findByPk($key1, $key2, ...);
601
	 * $finder->findByPk(array($key1,$key2,...));
602
	 * </code>
603
	 *
604
	 * @param mixed primary keys
605
	 * @return TActiveRecord. Null if no result is found.
606
	 */
607
	public function findByPk($keys)
608
	{
609
		if(func_num_args() > 1)
610
			$keys = func_get_args();
611
		$data = $this->getRecordGateway()->findRecordByPK($this,$keys);
612
		return $this->populateObject($data);
613
	}
614
 
615
	/**
616
	 * Find multiple records matching a list of primary or composite keys.
617
	 *
618
	 * For scalar primary keys:
619
	 * <code>
620
	 * $finder->findAllByPk($key1, $key2, ...);
621
	 * $finder->findAllByPk(array($key1, $key2, ...));
622
	 * </code>
623
	 *
624
	 * For composite keys:
625
	 * <code>
626
	 * $finder->findAllByPk(array($key1, $key2), array($key3, $key4), ...);
627
	 * $finder->findAllByPk(array(array($key1, $key2), array($key3, $key4), ...));
628
	 * </code>
629
	 * @param mixed primary keys
630
	 * @return array matching ActiveRecords. Empty array is returned if no result is found.
631
	 */
632
	public function findAllByPks($keys)
633
	{
634
		if(func_num_args() > 1)
635
			$keys = func_get_args();
636
		$result = $this->getRecordGateway()->findRecordsByPks($this,(array)$keys);
637
		return $this->populateObjects($result);
638
	}
639
 
640
	/**
641
	 * Find records using full SQL, returns corresponding record object.
642
	 * The names of the column retrieved must be defined in your Active Record
643
	 * class.
644
	 * @param string select SQL
645
	 * @param array $parameters
646
	 * @return TActiveRecord, null if no result is returned.
647
	 */
648
	public function findBySql($sql,$parameters=array())
649
	{
650
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
651
		$criteria = $this->getRecordCriteria($sql,$parameters, $args);
652
		$criteria->setLimit(1);
653
		$data = $this->getRecordGateway()->findRecordBySql($this,$criteria);
654
		return $this->populateObject($data);
655
	}
656
 
657
	/**
658
	 * Find records using full SQL, returns corresponding record object.
659
	 * The names of the column retrieved must be defined in your Active Record
660
	 * class.
661
	 * @param string select SQL
662
	 * @param array $parameters
663
	 * @return array matching active records. Empty array is returned if no result is found.
664
	 */
665
	public function findAllBySql($sql,$parameters=array())
666
	{
667
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
668
		$criteria = $this->getRecordCriteria($sql,$parameters, $args);
669
		$result = $this->getRecordGateway()->findRecordsBySql($this,$criteria);
670
		return $this->populateObjects($result);
671
	}
672
 
673
	/**
674
	 * Fetches records using the sql clause "(fields) IN (values)", where
675
	 * fields is an array of column names and values is an array of values that
676
	 * the columns must have.
677
	 *
678
	 * This method is to be used by the relationship handler.
679
	 *
680
	 * @param TActiveRecordCriteria additional criteria
681
	 * @param array field names to match with "(fields) IN (values)" sql clause.
682
	 * @param array matching field values.
683
	 * @return array matching active records. Empty array is returned if no result is found.
684
	 */
685
	public function findAllByIndex($criteria,$fields,$values)
686
	{
687
		$result = $this->getRecordGateway()->findRecordsByIndex($this,$criteria,$fields,$values);
688
		return $this->populateObjects($result);
689
	}
690
 
691
	/**
692
	 * Find the number of records.
693
	 * @param string|TActiveRecordCriteria SQL condition or criteria object.
694
	 * @param mixed parameter values.
695
	 * @return int number of records.
696
	 */
697
	public function count($criteria=null,$parameters=array())
698
	{
699
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
700
		if($criteria!==null)
701
			$criteria = $this->getRecordCriteria($criteria,$parameters, $args);
702
		return $this->getRecordGateway()->countRecords($this,$criteria);
703
	}
704
 
705
	/**
706
	 * Returns the active record relationship handler for $RELATION with key
707
	 * value equal to the $property value.
708
	 * @param string relationship/property name corresponding to keys in $RELATION array.
709
	 * @param array method call arguments.
710
	 * @return TActiveRecordRelation, null if the context or the handler doesn't exist
711
	 */
712
	protected function getRelationHandler($name,$args=array())
713
	{
714
		if(($context=$this->createRelationContext($name)) !== null)
715
		{
716
			$criteria = $this->getRecordCriteria(count($args)>0 ? $args[0] : null, array_slice($args,1));
717
			return $context->getRelationHandler($criteria);
718
		}
719
		else
720
			return null;
721
	}
722
 
723
	/**
724
	 * Gets a static copy of the relationship context for given property (a key
725
	 * in $RELATIONS), returns null if invalid relationship. Keeps a null
726
	 * reference to all invalid relations called.
727
	 * @param string relationship/property name corresponding to keys in $RELATION array.
728
	 * @return TActiveRecordRelationContext object containing information on
729
	 * the active record relationships for given property, null if invalid relationship
730
	 * @since 3.1.2
731
	 */
732
	protected function createRelationContext($name)
733
	{
734
		if(($definition=$this->getRecordRelation($name))!==null)
735
		{
736
			list($property, $relation) = $definition;
737
			return new TActiveRecordRelationContext($this,$property,$relation);
738
		}
739
		else
740
			return null;
741
	}
742
 
743
	/**
744
	 * Tries to load the relationship results for the given property. The $property
745
	 * value should correspond to an entry key in the $RELATION array.
746
	 * This method can be used to lazy load relationships.
747
	 * <code>
748
	 * class TeamRecord extends TActiveRecord
749
	 * {
750
	 *     ...
751
	 *
752
	 *     private $_players;
753
	 *     public static $RELATION=array
754
	 *     (
755
	 *         'players' => array(self::HAS_MANY, 'PlayerRecord'),
756
	 *     );
757
	 *
758
	 *     public function setPlayers($array)
759
	 *     {
760
	 *         $this->_players=$array;
761
	 *     }
762
	 *
763
	 *     public function getPlayers()
764
	 *     {
765
	 *         if($this->_players===null)
766
	 *             $this->fetchResultsFor('players');
767
	 *         return $this->_players;
768
	 *     }
769
	 * }
770
	 * Usage example:
771
	 * $team = TeamRecord::finder()->findByPk(1);
772
	 * var_dump($team->players); //uses lazy load to fetch 'players' relation
773
	 * </code>
774
	 * @param string relationship/property name corresponding to keys in $RELATION array.
775
	 * @return boolean true if relationship exists, false otherwise.
776
	 * @since 3.1.2
777
	 */
778
	protected function fetchResultsFor($property)
779
	{
780
		if( ($context=$this->createRelationContext($property)) !== null)
781
			return $context->getRelationHandler()->fetchResultsInto($this);
782
		else
783
			return false;
784
	}
785
 
786
	/**
787
	 * Dynamic find method using parts of method name as search criteria.
788
	 * Method name starting with "findBy" only returns 1 record.
789
	 * Method name starting with "findAllBy" returns 0 or more records.
790
	 * Method name starting with "deleteBy" deletes records by the trail criteria.
791
	 * The condition is taken as part of the method name after "findBy", "findAllBy"
792
	 * or "deleteBy".
793
	 *
794
	 * The following are equivalent:
795
	 * <code>
796
	 * $finder->findByName($name)
797
	 * $finder->find('Name = ?', $name);
798
	 * </code>
799
	 * <code>
800
	 * $finder->findByUsernameAndPassword($name,$pass); // OR may be used
801
	 * $finder->findBy_Username_And_Password($name,$pass); // _OR_ may be used
802
	 * $finder->find('Username = ? AND Password = ?', $name, $pass);
803
	 * </code>
804
	 * <code>
805
	 * $finder->findAllByAge($age);
806
	 * $finder->findAll('Age = ?', $age);
807
	 * </code>
808
	 * <code>
809
	 * $finder->deleteAll('Name = ?', $name);
810
	 * $finder->deleteByName($name);
811
	 * </code>
812
	 * @return mixed single record if method name starts with "findBy", 0 or more records
813
	 * if method name starts with "findAllBy"
814
	 */
815
	public function __call($method,$args)
816
	{
817
		$delete =false;
818
		if(strncasecmp($method,'with',4)===0)
819
		{
820
			$property= $method[4]==='_' ? substr($method,5) : substr($method,4);
821
			return $this->getRelationHandler($property, $args);
822
		}
823
		else if($findOne=strncasecmp($method,'findby',6)===0)
824
			$condition = $method[6]==='_' ? substr($method,7) : substr($method,6);
825
		else if(strncasecmp($method,'findallby',9)===0)
826
			$condition = $method[9]==='_' ? substr($method,10) : substr($method,9);
827
		else if($delete=strncasecmp($method,'deleteby',8)===0)
828
			$condition = $method[8]==='_' ? substr($method,9) : substr($method,8);
829
		else if($delete=strncasecmp($method,'deleteallby',11)===0)
830
			$condition = $method[11]==='_' ? substr($method,12) : substr($method,11);
831
		else
832
			return null;//throw new TActiveRecordException('ar_invalid_finder_method',$method);
833
 
834
		$criteria = $this->getRecordGateway()->getCommand($this)->createCriteriaFromString($method, $condition, $args);
835
		if($delete)
836
			return $this->deleteAll($criteria);
837
		else
838
			return $findOne ? $this->find($criteria) : $this->findAll($criteria);
839
	}
840
 
841
	/**
842
	 * Create a new TSqlCriteria object from a string $criteria. The $args
843
	 * are additional parameters and are used in place of the $parameters
844
	 * if $parameters is not an array and $args is an arrary.
845
	 * @param string|TSqlCriteria sql criteria
846
	 * @param mixed parameters passed by the user.
847
	 * @param array additional parameters obtained from function_get_args().
848
	 * @return TSqlCriteria criteria object.
849
	 */
850
	protected function getRecordCriteria($criteria, $parameters, $args=array())
851
	{
852
		if(is_string($criteria))
853
		{
854
			$useArgs = !is_array($parameters) && is_array($args);
855
			return new TActiveRecordCriteria($criteria,$useArgs ? $args : $parameters);
856
		}
857
		else if($criteria instanceof TSqlCriteria)
858
			return $criteria;
859
		else
860
			return new TActiveRecordCriteria();
861
			//throw new TActiveRecordException('ar_invalid_criteria');
862
	}
863
 
864
	/**
865
	 * Raised when a command is prepared and parameter binding is completed.
866
	 * The parameter object is TDataGatewayEventParameter of which the
867
	 * {@link TDataGatewayEventParameter::getCommand Command} property can be
868
	 * inspected to obtain the sql query to be executed.
869
	 *
870
	 * Note well that the finder objects obtained from ActiveRecord::finder()
871
	 * method are static objects. This means that the event handlers are
872
	 * bound to a static finder object and not to each distinct active record object.
873
	 * @param TDataGatewayEventParameter
874
	 */
875
	public function onCreateCommand($param)
876
	{
877
		$this->raiseEvent('OnCreateCommand', $this, $param);
878
	}
879
 
880
	/**
881
	 * Raised when a command is executed and the result from the database was returned.
882
	 * The parameter object is TDataGatewayResultEventParameter of which the
883
	 * {@link TDataGatewayEventParameter::getResult Result} property contains
884
	 * the data return from the database. The data returned can be changed
885
	 * by setting the {@link TDataGatewayEventParameter::setResult Result} property.
886
	 *
887
	 * Note well that the finder objects obtained from ActiveRecord::finder()
888
	 * method are static objects. This means that the event handlers are
889
	 * bound to a static finder object and not to each distinct active record object.
890
	 * @param TDataGatewayResultEventParameter
891
	 */
892
	public function onExecuteCommand($param)
893
	{
894
		$this->raiseEvent('OnExecuteCommand', $this, $param);
895
	}
896
 
897
	/**
898
	 * Raised before the record attempt to insert its data into the database.
899
	 * To prevent the insert operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
900
	 * @param TActiveRecordChangeEventParameter event parameter to be passed to the event handlers
901
	 */
902
	public function onInsert($param)
903
	{
904
		$this->raiseEvent('OnInsert', $this, $param);
905
	}
906
 
907
	/**
908
	 * Raised before the record attempt to delete its data from the database.
909
	 * To prevent the delete operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
910
	 * @param TActiveRecordChangeEventParameter event parameter to be passed to the event handlers
911
	 */
912
	public function onDelete($param)
913
	{
914
		$this->raiseEvent('OnDelete', $this, $param);
915
	}
916
 
917
	/**
918
	 * Raised before the record attempt to update its data in the database.
919
	 * To prevent the update operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
920
	 * @param TActiveRecordChangeEventParameter event parameter to be passed to the event handlers
921
	 */
922
	public function onUpdate($param)
923
	{
924
		$this->raiseEvent('OnUpdate', $this, $param);
925
	}
926
 
927
	/**
928
	 * Retrieves the column value according to column name.
929
	 * This method is used internally.
930
	 * @param string the column name (as defined in database schema)
931
	 * @return mixed the corresponding column value
932
	 * @since 3.1.1
933
	 */
934
	public function getColumnValue($columnName)
935
	{
936
		$className=get_class($this);
937
		if(isset(self::$_columnMapping[$className][$columnName]))
938
			$columnName=self::$_columnMapping[$className][$columnName];
939
		return $this->$columnName;
940
	}
941
 
942
	/**
943
	 * Sets the column value according to column name.
944
	 * This method is used internally.
945
	 * @param string the column name (as defined in database schema)
946
	 * @param mixed the corresponding column value
947
	 * @since 3.1.1
948
	 */
949
	public function setColumnValue($columnName,$value)
950
	{
951
		$className=get_class($this);
952
		if(isset(self::$_columnMapping[$className][$columnName]))
953
			$columnName=self::$_columnMapping[$className][$columnName];
954
		$this->$columnName=$value;
955
	}
956
 
957
	/**
958
	 * @param string relation property name
959
	 * @return array relation definition for the specified property
960
	 * @since 3.1.2
961
	 */
962
	public function getRecordRelation($property)
963
	{
964
		$className=get_class($this);
965
		$property=strtolower($property);
966
		return isset(self::$_relations[$className][$property])?self::$_relations[$className][$property]:null;
967
	}
968
 
969
	/**
970
	 * @return array all relation definitions declared in the AR class
971
	 * @since 3.1.2
972
	 */
973
	public function getRecordRelations()
974
	{
975
		return self::$_relations[get_class($this)];
976
	}
977
 
978
	/**
979
	 * @param string AR property name
980
	 * @return boolean whether a relation is declared for the specified AR property
981
	 * @since 3.1.2
982
	 */
983
	public function hasRecordRelation($property)
984
	{
985
		return isset(self::$_relations[get_class($this)][strtolower($property)]);
986
	}
987
}
988
 
989
/**
990
 * TActiveRecordChangeEventParameter class
991
 *
992
 * TActiveRecordChangeEventParameter encapsulates the parameter data for
993
 * ActiveRecord change commit events that are broadcasted. The following change events
994
 * may be raise: {@link TActiveRecord::OnInsert}, {@link TActiveRecord::OnUpdate} and
995
 * {@link TActiveRecord::OnDelete}. The {@link setIsValid IsValid} parameter can
996
 * be set to false to prevent the requested change event to be performed.
997
 *
998
 * @author Wei Zhuo<weizhuo@gmail.com>
999
 * @version $Id: TActiveRecord.php 2598 2009-01-07 07:28:22Z christophe.boulain $
1000
 * @package System.Data.ActiveRecord
1001
 * @since 3.1.2
1002
 */
1003
class TActiveRecordChangeEventParameter extends TEventParameter
1004
{
1005
	private $_isValid=true;
1006
 
1007
	/**
1008
	 * @return boolean whether the event should be performed.
1009
	 */
1010
	public function getIsValid()
1011
	{
1012
		return $this->_isValid;
1013
	}
1014
 
1015
	/**
1016
	 * @param boolean set to false to prevent the event.
1017
	 */
1018
	public function setIsValid($value)
1019
	{
1020
		$this->_isValid = TPropertyValue::ensureBoolean($value);
1021
	}
1022
}
1023