Completed
Branch master (4dc390)
by Fabio
30:44
created

TActiveRecord   F

Complexity

Total Complexity 125

Size/Duplication

Total Lines 905
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14
Metric Value
wmc 125
lcom 1
cbo 14
dl 0
loc 905
rs 1.5999

51 Methods

Rating   Name   Duplication   Size   Complexity  
A __sleep() 0 4 1
A __wakeup() 0 5 1
A __construct() 0 9 1
A __get() 0 9 3
A __set() 0 7 3
A setupColumnMapping() 0 9 2
A setupRelations() 0 12 3
A copyFrom() 0 9 4
A getActiveDbConnection() 0 6 2
A getDbConnection() 0 6 2
A setDbConnection() 0 4 1
A getRecordTableInfo() 0 4 1
B equals() 0 19 9
A finder() 0 10 2
A getRecordManager() 0 4 1
A getRecordGateway() 0 4 1
C save() 0 24 7
A delete() 0 18 4
A deleteByPk() 0 6 2
A deleteAllByPks() 0 6 2
A deleteAll() 0 6 2
A populateObject() 0 4 1
A populateObjects() 0 7 2
A createRecord() 0 8 2
A find() 0 8 2
A findAll() 0 8 3
A findByPk() 0 9 3
A findAllByPks() 0 7 2
A findBySql() 0 8 2
A findAllBySql() 0 7 2
A findAllByIndex() 0 5 1
A count() 0 7 3
A getRelationHandler() 0 10 3
A createRelationContext() 0 10 2
A fetchResultsFor() 0 7 2
C __call() 0 30 14
A getInvalidFinderResult() 0 7 2
A setInvalidFinderResult() 0 7 2
B getRecordCriteria() 0 13 5
A onCreateCommand() 0 4 1
A onExecuteCommand() 0 4 1
A onInsert() 0 4 1
A onDelete() 0 4 1
A onUpdate() 0 4 1
A getColumnValue() 0 7 2
A setColumnValue() 0 7 2
A getRecordRelation() 0 6 2
A getRecordRelations() 0 4 1
A hasRecordRelation() 0 4 1
A toArray() 0 8 2
A toJSON() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like TActiveRecord often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TActiveRecord, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * TActiveRecord, TActiveRecordEventParameter, TActiveRecordInvalidFinderResult class file.
4
 *
5
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
6
 * @link https://github.com/pradosoft/prado
7
 * @copyright Copyright &copy; 2005-2015 The PRADO Group
8
 * @license https://github.com/pradosoft/prado/blob/master/COPYRIGHT
9
 * @package System.Data.ActiveRecord
10
 */
11
12
/**
13
 * Load record manager, criteria and relations.
14
 */
15
Prado::using('System.Data.ActiveRecord.TActiveRecordManager');
16
Prado::using('System.Data.ActiveRecord.TActiveRecordCriteria');
17
Prado::using('System.Data.ActiveRecord.Relations.TActiveRecordRelationContext');
18
19
/**
20
 * Base class for active records.
21
 *
22
 * An active record creates an object that wraps a row in a database table
23
 * or view, encapsulates the database access, and adds domain logic on that data.
24
 *
25
 * Active record objects are stateful, this is main difference between the
26
 * TActiveRecord implementation and the TTableGateway implementation.
27
 *
28
 * The essence of an Active Record is an object model of the
29
 * domain (e.g. products, items) that incorporates both behavior and
30
 * data in which the classes match very closely the record structure of an
31
 * underlying database. Each Active Record is responsible for saving and
32
 * loading to the database and also for any domain logic that acts on the data.
33
 *
34
 * The Active Record provides methods that do the following:
35
 *  1. Construct an instance of the Active Record from a SQL result set row.
36
 *  2. Construct a new instance for later insertion into the table.
37
 *  3. Finder methods to wrap commonly used SQL queries and return Active Record objects.
38
 *  4. Update the database and insert into it the data in the Active Record.
39
 *
40
 * Example:
41
 * <code>
42
 * class UserRecord extends TActiveRecord
43
 * {
44
 *     const TABLE='users'; //optional table name.
45
 *
46
 *     public $username; //corresponds to the fieldname in the table
47
 *     public $email;
48
 *
49
 *     //returns active record finder instance
50
 *     public static function finder($className=__CLASS__)
51
 *     {
52
 *         return parent::finder($className);
53
 *     }
54
 * }
55
 *
56
 * //create a connection and give it to the ActiveRecord manager.
57
 * $dsn = 'pgsql:host=localhost;dbname=test';
58
 * $conn = new TDbConnection($dsn, 'dbuser','dbpass');
59
 * TActiveRecordManager::getInstance()->setDbConnection($conn);
60
 *
61
 * //load the user record with username (primary key) 'admin'.
62
 * $user = UserRecord::finder()->findByPk('admin');
63
 * $user->email = '[email protected]';
64
 * $user->save(); //update the 'admin' record.
65
 * </code>
66
 *
67
 * Since v3.1.1, TActiveRecord starts to support column mapping. The physical
68
 * column names (defined in database) can be mapped to logical column names
69
 * (defined in active classes as public properties.) To use this feature, declare
70
 * a static class variable COLUMN_MAPPING like the following:
71
 * <code>
72
 * class UserRecord extends TActiveRecord
73
 * {
74
 *     const TABLE='users';
75
 *     public static $COLUMN_MAPPING=array
76
 *     (
77
 *         'user_id'=>'username',
78
 *         'email_address'=>'email',
79
 *     );
80
 *     public $username;
81
 *     public $email;
82
 * }
83
 * </code>
84
 * In the above, the 'users' table consists of 'user_id' and 'email_address' columns,
85
 * while the UserRecord class declares 'username' and 'email' properties.
86
 * By using column mapping, we can regularize the naming convention of column names
87
 * in active record.
88
 *
89
 * Since v3.1.2, TActiveRecord enhanced its support to access of foreign objects.
90
 * By declaring a public static variable RELATIONS like the following, one can access
91
 * the corresponding foreign objects easily:
92
 * <code>
93
 * class UserRecord extends TActiveRecord
94
 * {
95
 *     const TABLE='users';
96
 *     public static $RELATIONS=array
97
 *     (
98
 *         'department'=>array(self::BELONGS_TO, 'DepartmentRecord', 'department_id'),
99
 *         'contacts'=>array(self::HAS_MANY, 'ContactRecord', 'user_id'),
100
 *     );
101
 * }
102
 * </code>
103
 * In the above, the users table is related with departments table (represented by
104
 * DepartmentRecord) and contacts table (represented by ContactRecord). Now, given a UserRecord
105
 * instance $user, one can access its department and contacts simply by: $user->department and
106
 * $user->contacts. No explicit data fetching is needed. Internally, the foreign objects are
107
 * fetched in a lazy way, which avoids unnecessary overhead if the foreign objects are not accessed
108
 * at all.
109
 *
110
 * Since v3.1.2, new events OnInsert, OnUpdate and OnDelete are available.
111
 * The event OnInsert, OnUpdate and OnDelete methods are executed before
112
 * inserting, updating, and deleting the current record, respectively. You may override
113
 * these methods; a TActiveRecordChangeEventParameter parameter is passed to these methods.
114
 * The property {@link TActiveRecordChangeEventParameter::setIsValid IsValid} of the parameter
115
 * can be set to false to prevent the change action to be executed. This can be used,
116
 * for example, to validate the record before the action is executed. For example,
117
 * in the following the password property is hashed before a new record is inserted.
118
 * <code>
119
 * class UserRecord extends TActiveRecord
120
 * {
121
 *      function OnInsert($param)
122
 *      {
123
 *          //parent method should be called to raise the event
124
 *          parent::OnInsert($param);
125
 *          $this->nounce = md5(time());
126
 *          $this->password = md5($this->password.$this->nounce);
127
 *      }
128
 * }
129
 * </code>
130
 *
131
 * Since v3.1.3 you can also define a method that returns the table name.
132
 * <code>
133
 * class UserRecord extends TActiveRecord
134
 * {
135
 *     public function table()
136
 *     {
137
 *          return 'users';
138
 *     }
139
 *
140
 * }
141
 * </code>
142
 *
143
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
144
 * @package System.Data.ActiveRecord
145
 * @since 3.1
146
 */
147
abstract class TActiveRecord extends TComponent
148
{
149
	const BELONGS_TO='BELONGS_TO';
150
	const HAS_ONE='HAS_ONE';
151
	const HAS_MANY='HAS_MANY';
152
	const MANY_TO_MANY='MANY_TO_MANY';
153
154
	const STATE_NEW=0;
155
	const STATE_LOADED=1;
156
	const STATE_DELETED=2;
157
158
	/**
159
	 * @var integer record state: 0 = new, 1 = loaded, 2 = deleted.
160
	 * @since 3.1.2
161
	 */
162
	protected $_recordState=0; // use protected so that serialization is fine
163
164
	/**
165
	 * This static variable defines the column mapping.
166
	 * The keys are physical column names as defined in database,
167
	 * and the values are logical column names as defined as public variable/property names
168
	 * for the corresponding active record class.
169
	 * @var array column mapping. Keys: physical column names, values: logical column names.
170
	 * @since 3.1.1
171
	 */
172
	public static $COLUMN_MAPPING=array();
173
	private static $_columnMapping=array();
174
175
	/**
176
	 * This static variable defines the relationships.
177
	 * The keys are public variable/property names defined in the AR class.
178
	 * Each value is an array, e.g. array(self::HAS_MANY, 'PlayerRecord').
179
	 * @var array relationship.
180
	 * @since 3.1.1
181
	 */
182
	public static $RELATIONS=array();
183
	private static $_relations=array();
184
185
	/**
186
	 * @var TDbConnection database connection object.
187
	 */
188
	protected $_connection; // use protected so that serialization is fine
189
190
191
	/**
192
	 * Defaults to 'null'
193
	 *
194
	 * @var TActiveRecordInvalidFinderResult
195
	 * @since 3.1.5
196
	 */
197
	protected $_invalidFinderResult = null; // use protected so that serialization is fine
198
199
	/**
200
	 * Prevent __call() method creating __sleep() when serializing.
201
	 */
202
	public function __sleep()
203
	{
204
		return array_diff(parent::__sleep(),array("\0*\0_connection"));
205
	}
206
207
	/**
208
	 * Prevent __call() method creating __wakeup() when unserializing.
209
	 */
210
	public function __wakeup()
211
	{
212
		$this->setupColumnMapping();
213
		$this->setupRelations();
214
	}
215
216
	/**
217
	 * Create a new instance of an active record with given $data. The record
218
	 * can be saved to the database specified by the $connection object.
219
	 *
220
	 * @param array optional name value pair record data.
221
	 * @param TDbConnection optional database connection this object record use.
222
	 */
223
	public function __construct($data=array(), $connection=null)
224
	{
225
		if($connection!==null)
226
			$this->setDbConnection($connection);
227
		$this->setupColumnMapping();
228
		$this->setupRelations();
229
		if(!empty($data)) //$data may be an object
230
			$this->copyFrom($data);
231
	}
232
233
	/**
234
	 * Magic method for reading properties.
235
	 * This method is overriden to provide read access to the foreign objects via
236
	 * the key names declared in the RELATIONS array.
237
	 * @param string property name
238
	 * @return mixed property value.
239
	 * @since 3.1.2
240
	 */
241
	public function __get($name)
242
	{
243
		if($this->hasRecordRelation($name) && !$this->canGetProperty($name))
244
		{
245
			$this->fetchResultsFor($name);
246
			return $this->$name;
247
		}
248
		return parent::__get($name);
249
	}
250
251
	/**
252
	 * Magic method for writing properties.
253
	 * This method is overriden to provide write access to the foreign objects via
254
	 * the key names declared in the RELATIONS array.
255
	 * @param string property name
256
	 * @param mixed property value.
257
	 * @since 3.1.2
258
	 */
259
	public function __set($name,$value)
260
	{
261
		if($this->hasRecordRelation($name) && !$this->canSetProperty($name))
262
			$this->$name=$value;
263
		else
264
			parent::__set($name,$value);
265
	}
266
267
	/**
268
	 * @since 3.1.1
269
	 */
270
	private function setupColumnMapping()
271
	{
272
		$className=get_class($this);
273
		if(!isset(self::$_columnMapping[$className]))
274
		{
275
			$class=new ReflectionClass($className);
276
			self::$_columnMapping[$className]=$class->getStaticPropertyValue('COLUMN_MAPPING');
277
		}
278
	}
279
280
	/**
281
	 * @since 3.1.2
282
	 */
283
	private function setupRelations()
284
	{
285
		$className=get_class($this);
286
		if(!isset(self::$_relations[$className]))
287
		{
288
			$class=new ReflectionClass($className);
289
			$relations=array();
290
			foreach($class->getStaticPropertyValue('RELATIONS') as $key=>$value)
291
				$relations[strtolower($key)]=array($key,$value);
292
			self::$_relations[$className]=$relations;
293
		}
294
	}
295
296
	/**
297
	 * Copies data from an array or another object.
298
	 * @throws TActiveRecordException if data is not array or not object.
299
	 */
300
	public function copyFrom($data)
301
	{
302
		if(is_object($data))
303
			$data=get_object_vars($data);
304
		if(!is_array($data))
305
			throw new TActiveRecordException('ar_data_invalid', get_class($this));
306
		foreach($data as $name=>$value)
307
			$this->setColumnValue($name,$value);
308
	}
309
310
311
	public static function getActiveDbConnection()
312
	{
313
		if(($db=self::getRecordManager()->getDbConnection())!==null)
314
			$db->setActive(true);
315
		return $db;
316
	}
317
318
	/**
319
	 * Gets the current Db connection, the connection object is obtained from
320
	 * the TActiveRecordManager if connection is currently null.
321
	 * @return TDbConnection current db connection for this object.
322
	 */
323
	public function getDbConnection()
324
	{
325
		if($this->_connection===null)
326
			$this->_connection=self::getActiveDbConnection();
327
		return $this->_connection;
328
	}
329
330
	/**
331
	 * @param TDbConnection db connection object for this record.
332
	 */
333
	public function setDbConnection($connection)
334
	{
335
		$this->_connection=$connection;
336
	}
337
338
	/**
339
	 * @return TDbTableInfo the meta information of the table associated with this AR class.
340
	 */
341
	public function getRecordTableInfo()
342
	{
343
		return $this->getRecordGateway()->getRecordTableInfo($this);
344
	}
345
346
	/**
347
	 * Compare two records using their primary key values (all column values if
348
	 * table does not defined primary keys). The default uses simple == for
349
	 * comparison of their values. Set $strict=true for identity comparison (===).
350
	 * @param TActiveRecord another record to compare with.
351
	 * @param boolean true to perform strict identity comparison
352
	 * @return boolean true if $record equals, false otherwise.
353
	 */
354
	public function equals(TActiveRecord $record, $strict=false)
355
	{
356
		if($record===null || get_class($this)!==get_class($record))
357
			return false;
358
		$tableInfo = $this->getRecordTableInfo();
359
		$pks = $tableInfo->getPrimaryKeys();
360
		$properties = count($pks) > 0 ? $pks : $tableInfo->getColumns()->getKeys();
361
		$equals=true;
362
		foreach($properties as $prop)
363
		{
364
			if($strict)
365
				$equals = $equals && $this->getColumnValue($prop) === $record->getColumnValue($prop);
366
			else
367
				$equals = $equals && $this->getColumnValue($prop) == $record->getColumnValue($prop);
368
			if(!$equals)
369
				return false;
370
		}
371
		return $equals;
372
	}
373
374
	/**
375
	 * Returns the instance of a active record finder for a particular class.
376
	 * The finder objects are static instances for each ActiveRecord class.
377
	 * This means that event handlers bound to these finder instances are class wide.
378
	 * Create a new instance of the ActiveRecord class if you wish to bound the
379
	 * event handlers to object instance.
380
	 * @param string active record class name.
381
	 * @return TActiveRecord active record finder instance.
382
	 */
383
	public static function finder($className=__CLASS__)
384
	{
385
		static $finders = array();
386
		if(!isset($finders[$className]))
387
		{
388
			$f = Prado::createComponent($className);
389
			$finders[$className]=$f;
390
		}
391
		return $finders[$className];
392
	}
393
394
	/**
395
	 * Gets the record manager for this object, the default is to call
396
	 * TActiveRecordManager::getInstance().
397
	 * @return TActiveRecordManager default active record manager.
398
	 */
399
	public static function getRecordManager()
400
	{
401
		return TActiveRecordManager::getInstance();
402
	}
403
404
	/**
405
	 * @return TActiveRecordGateway record table gateway.
406
	 */
407
	public function getRecordGateway()
408
	{
409
		return TActiveRecordManager::getInstance()->getRecordGateway();
410
	}
411
412
	/**
413
	 * Saves the current record to the database, insert or update is automatically determined.
414
	 * @return boolean true if record was saved successfully, false otherwise.
415
	 */
416
	public function save()
417
	{
418
		$gateway = $this->getRecordGateway();
419
		$param = new TActiveRecordChangeEventParameter();
420
		if($this->_recordState===self::STATE_NEW)
421
		{
422
			$this->onInsert($param);
423
			if($param->getIsValid() && $gateway->insert($this))
424
			{
425
				$this->_recordState = self::STATE_LOADED;
426
				return true;
427
			}
428
		}
429
		else if($this->_recordState===self::STATE_LOADED)
430
		{
431
			$this->onUpdate($param);
432
			if($param->getIsValid() && $gateway->update($this))
433
				return true;
434
		}
435
		else
436
			throw new TActiveRecordException('ar_save_invalid', get_class($this));
437
438
		return false;
439
	}
440
441
	/**
442
	 * Deletes the current record from the database. Once deleted, this object
443
	 * can not be saved again in the same instance.
444
	 * @return boolean true if the record was deleted successfully, false otherwise.
445
	 */
446
	public function delete()
447
	{
448
		if($this->_recordState===self::STATE_LOADED)
449
		{
450
			$gateway = $this->getRecordGateway();
451
			$param = new TActiveRecordChangeEventParameter();
452
			$this->onDelete($param);
453
			if($param->getIsValid() && $gateway->delete($this))
454
			{
455
				$this->_recordState=self::STATE_DELETED;
456
				return true;
457
			}
458
		}
459
		else
460
			throw new TActiveRecordException('ar_delete_invalid', get_class($this));
461
462
		return false;
463
	}
464
465
	/**
466
	 * Delete records by primary key. Usage:
467
	 *
468
	 * <code>
469
	 * $finder->deleteByPk($primaryKey); //delete 1 record
470
	 * $finder->deleteByPk($key1,$key2,...); //delete multiple records
471
	 * $finder->deleteByPk(array($key1,$key2,...)); //delete multiple records
472
	 * </code>
473
	 *
474
	 * For composite primary keys (determined from the table definitions):
475
	 * <code>
476
	 * $finder->deleteByPk(array($key1,$key2)); //delete 1 record
477
	 *
478
	 * //delete multiple records
479
	 * $finder->deleteByPk(array($key1,$key2), array($key3,$key4),...);
480
	 *
481
	 * //delete multiple records
482
	 * $finder->deleteByPk(array( array($key1,$key2), array($key3,$key4), .. ));
483
	 * </code>
484
	 *
485
	 * @param mixed primary key values.
486
	 * @return int number of records deleted.
487
	 */
488
	public function deleteByPk($keys)
489
	{
490
		if(func_num_args() > 1)
491
			$keys = func_get_args();
492
		return $this->getRecordGateway()->deleteRecordsByPk($this,(array)$keys);
493
	}
494
495
	/**
496
	 * Alias for deleteByPk()
497
	 */
498
	public function deleteAllByPks($keys)
499
	{
500
		if(func_num_args() > 1)
501
			$keys = func_get_args();
502
		return $this->deleteByPk($keys);
503
	}
504
	/**
505
	 * Delete multiple records using a criteria.
506
	 * @param string|TActiveRecordCriteria SQL condition or criteria object.
507
	 * @param mixed parameter values.
508
	 * @return int number of records deleted.
509
	 */
510
	public function deleteAll($criteria=null, $parameters=array())
511
	{
512
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
513
		$criteria = $this->getRecordCriteria($criteria,$parameters, $args);
0 ignored issues
show
Bug introduced by
It seems like $args defined by func_num_args() > 1 ? ar...c_get_args(), 1) : null on line 512 can also be of type null; however, TActiveRecord::getRecordCriteria() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
514
		return $this->getRecordGateway()->deleteRecordsByCriteria($this, $criteria);
515
	}
516
517
	/**
518
	 * Populates a new record with the query result.
519
	 * This is a wrapper of {@link createRecord}.
520
	 * @param array name value pair of record data
521
	 * @return TActiveRecord object record, null if data is empty.
522
	 */
523
	protected function populateObject($data)
524
	{
525
		return self::createRecord(get_class($this), $data);
526
	}
527
528
	/**
529
	 * @param TDbDataReader data reader
530
	 * @return array the AR objects populated by the query result
531
	 * @since 3.1.2
532
	 */
533
	protected function populateObjects($reader)
534
	{
535
		$result=array();
536
		foreach($reader as $data)
537
			$result[] = $this->populateObject($data);
538
		return $result;
539
	}
540
541
	/**
542
	 * Create an AR instance specified by the AR class name and initial data.
543
	 * If the initial data is empty, the AR object will not be created and null will be returned.
544
	 * (You should use the "new" operator to create the AR instance in that case.)
545
	 * @param string the AR class name
546
	 * @param array initial data to be populated into the AR object.
547
	 * @return TActiveRecord the initialized AR object. Null if the initial data is empty.
548
	 * @since 3.1.2
549
	 */
550
	public static function createRecord($type, $data)
551
	{
552
		if(empty($data))
553
			return null;
554
		$record=new $type($data);
555
		$record->_recordState=self::STATE_LOADED;
556
		return $record;
557
	}
558
559
	/**
560
	 * Find one single record that matches the criteria.
561
	 *
562
	 * Usage:
563
	 * <code>
564
	 * $finder->find('username = :name AND password = :pass',
565
	 * 					array(':name'=>$name, ':pass'=>$pass));
566
	 * $finder->find('username = ? AND password = ?', array($name, $pass));
567
	 * $finder->find('username = ? AND password = ?', $name, $pass);
568
	 * //$criteria is of TActiveRecordCriteria
569
	 * $finder->find($criteria); //the 2nd parameter for find() is ignored.
570
	 * </code>
571
	 *
572
	 * @param string|TActiveRecordCriteria SQL condition or criteria object.
573
	 * @param mixed parameter values.
574
	 * @return TActiveRecord matching record object. Null if no result is found.
575
	 */
576
	public function find($criteria,$parameters=array())
577
	{
578
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
579
		$criteria = $this->getRecordCriteria($criteria,$parameters, $args);
0 ignored issues
show
Bug introduced by
It seems like $args defined by func_num_args() > 1 ? ar...c_get_args(), 1) : null on line 578 can also be of type null; however, TActiveRecord::getRecordCriteria() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
580
		$criteria->setLimit(1);
581
		$data = $this->getRecordGateway()->findRecordsByCriteria($this,$criteria);
582
		return $this->populateObject($data);
583
	}
584
585
	/**
586
	 * Same as find() but returns an array of objects.
587
	 *
588
	 * @param string|TActiveRecordCriteria SQL condition or criteria object.
589
	 * @param mixed parameter values.
590
	 * @return array matching record objects. Empty array if no result is found.
591
	 */
592
	public function findAll($criteria=null,$parameters=array())
593
	{
594
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
595
		if($criteria!==null)
596
			$criteria = $this->getRecordCriteria($criteria,$parameters, $args);
0 ignored issues
show
Bug introduced by
It seems like $args defined by func_num_args() > 1 ? ar...c_get_args(), 1) : null on line 594 can also be of type null; however, TActiveRecord::getRecordCriteria() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
597
		$result = $this->getRecordGateway()->findRecordsByCriteria($this,$criteria,true);
598
		return $this->populateObjects($result);
599
	}
600
601
	/**
602
	 * Find one record using only the primary key or composite primary keys. Usage:
603
	 *
604
	 * <code>
605
	 * $finder->findByPk($primaryKey);
606
	 * $finder->findByPk($key1, $key2, ...);
607
	 * $finder->findByPk(array($key1,$key2,...));
608
	 * </code>
609
	 *
610
	 * @param mixed primary keys
611
	 * @return TActiveRecord. Null if no result is found.
0 ignored issues
show
Documentation introduced by
The doc-type TActiveRecord. could not be parsed: Unknown type name "TActiveRecord." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
612
	 */
613
	public function findByPk($keys)
614
	{
615
		if($keys === null)
616
			return null;
617
		if(func_num_args() > 1)
618
			$keys = func_get_args();
619
		$data = $this->getRecordGateway()->findRecordByPK($this,$keys);
620
		return $this->populateObject($data);
621
	}
622
623
	/**
624
	 * Find multiple records matching a list of primary or composite keys.
625
	 *
626
	 * For scalar primary keys:
627
	 * <code>
628
	 * $finder->findAllByPk($key1, $key2, ...);
629
	 * $finder->findAllByPk(array($key1, $key2, ...));
630
	 * </code>
631
	 *
632
	 * For composite keys:
633
	 * <code>
634
	 * $finder->findAllByPk(array($key1, $key2), array($key3, $key4), ...);
635
	 * $finder->findAllByPk(array(array($key1, $key2), array($key3, $key4), ...));
636
	 * </code>
637
	 * @param mixed primary keys
638
	 * @return array matching ActiveRecords. Empty array is returned if no result is found.
639
	 */
640
	public function findAllByPks($keys)
641
	{
642
		if(func_num_args() > 1)
643
			$keys = func_get_args();
644
		$result = $this->getRecordGateway()->findRecordsByPks($this,(array)$keys);
645
		return $this->populateObjects($result);
646
	}
647
648
	/**
649
	 * Find records using full SQL, returns corresponding record object.
650
	 * The names of the column retrieved must be defined in your Active Record
651
	 * class.
652
	 * @param string select SQL
653
	 * @param array $parameters
654
	 * @return TActiveRecord, null if no result is returned.
0 ignored issues
show
Documentation introduced by
The doc-type TActiveRecord, could not be parsed: Expected "|" or "end of type", but got "," at position 13. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
655
	 */
656
	public function findBySql($sql,$parameters=array())
657
	{
658
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
659
		$criteria = $this->getRecordCriteria($sql,$parameters, $args);
0 ignored issues
show
Bug introduced by
It seems like $args defined by func_num_args() > 1 ? ar...c_get_args(), 1) : null on line 658 can also be of type null; however, TActiveRecord::getRecordCriteria() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
660
		$criteria->setLimit(1);
661
		$data = $this->getRecordGateway()->findRecordBySql($this,$criteria);
662
		return $this->populateObject($data);
663
	}
664
665
	/**
666
	 * Find records using full SQL, returns corresponding record object.
667
	 * The names of the column retrieved must be defined in your Active Record
668
	 * class.
669
	 * @param string select SQL
670
	 * @param array $parameters
671
	 * @return array matching active records. Empty array is returned if no result is found.
672
	 */
673
	public function findAllBySql($sql,$parameters=array())
674
	{
675
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
676
		$criteria = $this->getRecordCriteria($sql,$parameters, $args);
0 ignored issues
show
Bug introduced by
It seems like $args defined by func_num_args() > 1 ? ar...c_get_args(), 1) : null on line 675 can also be of type null; however, TActiveRecord::getRecordCriteria() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
677
		$result = $this->getRecordGateway()->findRecordsBySql($this,$criteria);
678
		return $this->populateObjects($result);
679
	}
680
681
	/**
682
	 * Fetches records using the sql clause "(fields) IN (values)", where
683
	 * fields is an array of column names and values is an array of values that
684
	 * the columns must have.
685
	 *
686
	 * This method is to be used by the relationship handler.
687
	 *
688
	 * @param TActiveRecordCriteria additional criteria
689
	 * @param array field names to match with "(fields) IN (values)" sql clause.
690
	 * @param array matching field values.
691
	 * @return array matching active records. Empty array is returned if no result is found.
692
	 */
693
	public function findAllByIndex($criteria,$fields,$values)
694
	{
695
		$result = $this->getRecordGateway()->findRecordsByIndex($this,$criteria,$fields,$values);
696
		return $this->populateObjects($result);
697
	}
698
699
	/**
700
	 * Find the number of records.
701
	 * @param string|TActiveRecordCriteria SQL condition or criteria object.
702
	 * @param mixed parameter values.
703
	 * @return int number of records.
704
	 */
705
	public function count($criteria=null,$parameters=array())
706
	{
707
		$args = func_num_args() > 1 ? array_slice(func_get_args(),1) : null;
708
		if($criteria!==null)
709
			$criteria = $this->getRecordCriteria($criteria,$parameters, $args);
0 ignored issues
show
Bug introduced by
It seems like $args defined by func_num_args() > 1 ? ar...c_get_args(), 1) : null on line 707 can also be of type null; however, TActiveRecord::getRecordCriteria() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
710
		return $this->getRecordGateway()->countRecords($this,$criteria);
711
	}
712
713
	/**
714
	 * Returns the active record relationship handler for $RELATION with key
715
	 * value equal to the $property value.
716
	 * @param string relationship/property name corresponding to keys in $RELATION array.
717
	 * @param array method call arguments.
718
	 * @return TActiveRecordRelation, null if the context or the handler doesn't exist
0 ignored issues
show
Documentation introduced by
The doc-type TActiveRecordRelation, could not be parsed: Expected "|" or "end of type", but got "," at position 21. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
719
	 */
720
	protected function getRelationHandler($name,$args=array())
721
	{
722
		if(($context=$this->createRelationContext($name)) !== null)
723
		{
724
			$criteria = $this->getRecordCriteria(count($args)>0 ? $args[0] : null, array_slice($args,1));
725
			return $context->getRelationHandler($criteria);
726
		}
727
		else
728
			return null;
729
	}
730
731
	/**
732
	 * Gets a static copy of the relationship context for given property (a key
733
	 * in $RELATIONS), returns null if invalid relationship. Keeps a null
734
	 * reference to all invalid relations called.
735
	 * @param string relationship/property name corresponding to keys in $RELATION array.
736
	 * @return TActiveRecordRelationContext object containing information on
737
	 * the active record relationships for given property, null if invalid relationship
738
	 * @since 3.1.2
739
	 */
740
	protected function createRelationContext($name)
741
	{
742
		if(($definition=$this->getRecordRelation($name))!==null)
743
		{
744
			list($property, $relation) = $definition;
745
			return new TActiveRecordRelationContext($this,$property,$relation);
746
		}
747
		else
748
			return null;
749
	}
750
751
	/**
752
	 * Tries to load the relationship results for the given property. The $property
753
	 * value should correspond to an entry key in the $RELATION array.
754
	 * This method can be used to lazy load relationships.
755
	 * <code>
756
	 * class TeamRecord extends TActiveRecord
757
	 * {
758
	 *     ...
759
	 *
760
	 *     private $_players;
761
	 *     public static $RELATION=array
762
	 *     (
763
	 *         'players' => array(self::HAS_MANY, 'PlayerRecord'),
764
	 *     );
765
	 *
766
	 *     public function setPlayers($array)
767
	 *     {
768
	 *         $this->_players=$array;
769
	 *     }
770
	 *
771
	 *     public function getPlayers()
772
	 *     {
773
	 *         if($this->_players===null)
774
	 *             $this->fetchResultsFor('players');
775
	 *         return $this->_players;
776
	 *     }
777
	 * }
778
	 * Usage example:
779
	 * $team = TeamRecord::finder()->findByPk(1);
780
	 * var_dump($team->players); //uses lazy load to fetch 'players' relation
781
	 * </code>
782
	 * @param string relationship/property name corresponding to keys in $RELATION array.
783
	 * @return boolean true if relationship exists, false otherwise.
784
	 * @since 3.1.2
785
	 */
786
	protected function fetchResultsFor($property)
787
	{
788
		if( ($context=$this->createRelationContext($property)) !== null)
789
			return $context->getRelationHandler()->fetchResultsInto($this);
790
		else
791
			return false;
792
	}
793
794
	/**
795
	 * Dynamic find method using parts of method name as search criteria.
796
	 * Method name starting with "findBy" only returns 1 record.
797
	 * Method name starting with "findAllBy" returns 0 or more records.
798
	 * Method name starting with "deleteBy" deletes records by the trail criteria.
799
	 * The condition is taken as part of the method name after "findBy", "findAllBy"
800
	 * or "deleteBy".
801
	 *
802
	 * The following are equivalent:
803
	 * <code>
804
	 * $finder->findByName($name)
805
	 * $finder->find('Name = ?', $name);
806
	 * </code>
807
	 * <code>
808
	 * $finder->findByUsernameAndPassword($name,$pass); // OR may be used
809
	 * $finder->findBy_Username_And_Password($name,$pass); // _OR_ may be used
810
	 * $finder->find('Username = ? AND Password = ?', $name, $pass);
811
	 * </code>
812
	 * <code>
813
	 * $finder->findAllByAge($age);
814
	 * $finder->findAll('Age = ?', $age);
815
	 * </code>
816
	 * <code>
817
	 * $finder->deleteAll('Name = ?', $name);
818
	 * $finder->deleteByName($name);
819
	 * </code>
820
	 * @return mixed single record if method name starts with "findBy", 0 or more records
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use TActiveRecordHasMany|TAc...ger|TActiveRecord|array.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
821
	 * if method name starts with "findAllBy"
822
	 */
823
	public function __call($method,$args)
824
	{
825
		$delete =false;
826
		if(strncasecmp($method,'with',4)===0)
827
		{
828
			$property= $method[4]==='_' ? substr($method,5) : substr($method,4);
829
			return $this->getRelationHandler($property, $args);
830
		}
831
		else if($findOne=strncasecmp($method,'findby',6)===0)
832
			$condition = $method[6]==='_' ? substr($method,7) : substr($method,6);
833
		else if(strncasecmp($method,'findallby',9)===0)
834
			$condition = $method[9]==='_' ? substr($method,10) : substr($method,9);
835
		else if($delete=strncasecmp($method,'deleteby',8)===0)
836
			$condition = $method[8]==='_' ? substr($method,9) : substr($method,8);
837
		else if($delete=strncasecmp($method,'deleteallby',11)===0)
838
			$condition = $method[11]==='_' ? substr($method,12) : substr($method,11);
839
		else
840
		{
841
			if($this->getInvalidFinderResult() == TActiveRecordInvalidFinderResult::Exception)
842
				throw new TActiveRecordException('ar_invalid_finder_method',$method);
843
			else
844
				return null;
845
		}
846
847
		$criteria = $this->getRecordGateway()->getCommand($this)->createCriteriaFromString($method, $condition, $args);
848
		if($delete)
849
			return $this->deleteAll($criteria);
850
		else
851
			return $findOne ? $this->find($criteria) : $this->findAll($criteria);
852
	}
853
854
	/**
855
	 * @return TActiveRecordInvalidFinderResult Defaults to '{@link TActiveRecordInvalidFinderResult::Null Null}'.
856
	 * @see TActiveRecordManager::getInvalidFinderResult
857
	 * @since 3.1.5
858
	 */
859
	public function getInvalidFinderResult()
860
	{
861
		if($this->_invalidFinderResult !== null)
862
			return $this->_invalidFinderResult;
863
864
		return self::getRecordManager()->getInvalidFinderResult();
865
	}
866
867
	/**
868
	 * Define the way an active record finder react if an invalid magic-finder invoked
869
	 *
870
	 * @param TActiveRecordInvalidFinderResult|null
871
	 * @see TActiveRecordManager::setInvalidFinderResult
872
	 * @since 3.1.5
873
	 */
874
	public function setInvalidFinderResult($value)
875
	{
876
		if($value === null)
877
			$this->_invalidFinderResult = null;
878
		else
879
			$this->_invalidFinderResult = TPropertyValue::ensureEnum($value, 'TActiveRecordInvalidFinderResult');
0 ignored issues
show
Documentation Bug introduced by
It seems like \TPropertyValue::ensureE...rdInvalidFinderResult') of type string is incompatible with the declared type object<TActiveRecordInvalidFinderResult> of property $_invalidFinderResult.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
880
	}
881
882
	/**
883
	 * Create a new TSqlCriteria object from a string $criteria. The $args
884
	 * are additional parameters and are used in place of the $parameters
885
	 * if $parameters is not an array and $args is an arrary.
886
	 * @param string|TSqlCriteria sql criteria
887
	 * @param mixed parameters passed by the user.
888
	 * @param array additional parameters obtained from function_get_args().
889
	 * @return TSqlCriteria criteria object.
890
	 */
891
	protected function getRecordCriteria($criteria, $parameters, $args=array())
892
	{
893
		if(is_string($criteria))
894
		{
895
			$useArgs = !is_array($parameters) && is_array($args);
896
			return new TActiveRecordCriteria($criteria,$useArgs ? $args : $parameters);
897
		}
898
		else if($criteria instanceof TSqlCriteria)
899
			return $criteria;
900
		else
901
			return new TActiveRecordCriteria();
902
			//throw new TActiveRecordException('ar_invalid_criteria');
903
	}
904
905
	/**
906
	 * Raised when a command is prepared and parameter binding is completed.
907
	 * The parameter object is TDataGatewayEventParameter of which the
908
	 * {@link TDataGatewayEventParameter::getCommand Command} property can be
909
	 * inspected to obtain the sql query to be executed.
910
	 *
911
	 * Note well that the finder objects obtained from ActiveRecord::finder()
912
	 * method are static objects. This means that the event handlers are
913
	 * bound to a static finder object and not to each distinct active record object.
914
	 * @param TDataGatewayEventParameter
915
	 */
916
	public function onCreateCommand($param)
917
	{
918
		$this->raiseEvent('OnCreateCommand', $this, $param);
919
	}
920
921
	/**
922
	 * Raised when a command is executed and the result from the database was returned.
923
	 * The parameter object is TDataGatewayResultEventParameter of which the
924
	 * {@link TDataGatewayEventParameter::getResult Result} property contains
925
	 * the data return from the database. The data returned can be changed
926
	 * by setting the {@link TDataGatewayEventParameter::setResult Result} property.
927
	 *
928
	 * Note well that the finder objects obtained from ActiveRecord::finder()
929
	 * method are static objects. This means that the event handlers are
930
	 * bound to a static finder object and not to each distinct active record object.
931
	 * @param TDataGatewayResultEventParameter
932
	 */
933
	public function onExecuteCommand($param)
934
	{
935
		$this->raiseEvent('OnExecuteCommand', $this, $param);
936
	}
937
938
	/**
939
	 * Raised before the record attempt to insert its data into the database.
940
	 * To prevent the insert operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
941
	 * @param TActiveRecordChangeEventParameter event parameter to be passed to the event handlers
942
	 */
943
	public function onInsert($param)
944
	{
945
		$this->raiseEvent('OnInsert', $this, $param);
946
	}
947
948
	/**
949
	 * Raised before the record attempt to delete its data from the database.
950
	 * To prevent the delete operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
951
	 * @param TActiveRecordChangeEventParameter event parameter to be passed to the event handlers
952
	 */
953
	public function onDelete($param)
954
	{
955
		$this->raiseEvent('OnDelete', $this, $param);
956
	}
957
958
	/**
959
	 * Raised before the record attempt to update its data in the database.
960
	 * To prevent the update operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
961
	 * @param TActiveRecordChangeEventParameter event parameter to be passed to the event handlers
962
	 */
963
	public function onUpdate($param)
964
	{
965
		$this->raiseEvent('OnUpdate', $this, $param);
966
	}
967
968
	/**
969
	 * Retrieves the column value according to column name.
970
	 * This method is used internally.
971
	 * @param string the column name (as defined in database schema)
972
	 * @return mixed the corresponding column value
973
	 * @since 3.1.1
974
	 */
975
	public function getColumnValue($columnName)
976
	{
977
		$className=get_class($this);
978
		if(isset(self::$_columnMapping[$className][$columnName]))
979
			$columnName=self::$_columnMapping[$className][$columnName];
980
		return $this->$columnName;
981
	}
982
983
	/**
984
	 * Sets the column value according to column name.
985
	 * This method is used internally.
986
	 * @param string the column name (as defined in database schema)
987
	 * @param mixed the corresponding column value
988
	 * @since 3.1.1
989
	 */
990
	public function setColumnValue($columnName,$value)
991
	{
992
		$className=get_class($this);
993
		if(isset(self::$_columnMapping[$className][$columnName]))
994
			$columnName=self::$_columnMapping[$className][$columnName];
995
		$this->$columnName=$value;
996
	}
997
998
	/**
999
	 * @param string relation property name
1000
	 * @return array relation definition for the specified property
1001
	 * @since 3.1.2
1002
	 */
1003
	public function getRecordRelation($property)
1004
	{
1005
		$className=get_class($this);
1006
		$property=strtolower($property);
1007
		return isset(self::$_relations[$className][$property])?self::$_relations[$className][$property]:null;
1008
	}
1009
1010
	/**
1011
	 * @return array all relation definitions declared in the AR class
1012
	 * @since 3.1.2
1013
	 */
1014
	public function getRecordRelations()
1015
	{
1016
		return self::$_relations[get_class($this)];
1017
	}
1018
1019
	/**
1020
	 * @param string AR property name
1021
	 * @return boolean whether a relation is declared for the specified AR property
1022
	 * @since 3.1.2
1023
	 */
1024
	public function hasRecordRelation($property)
1025
	{
1026
		return isset(self::$_relations[get_class($this)][strtolower($property)]);
1027
	}
1028
	
1029
        /**
1030
        * Return record data as array
1031
        * @return array of column name and column values
1032
        * @since 3.2.4
1033
        */
1034
        public function toArray(){
1035
        	$result=array();
1036
        	foreach($this->getRecordTableInfo()->getLowerCaseColumnNames() as $columnName){
1037
            		$result[$columnName]=$this->getColumnValue($columnName);
1038
           	}
1039
        
1040
        	return $result;
1041
    	}
1042
    
1043
    	/**
1044
     	* Return record data as JSON
1045
     	* @return JSON
1046
     	* @since 3.2.4
1047
     	*/
1048
    	public function toJSON(){
1049
        	return json_encode($this->toArray());
1050
    	}
1051
}
1052
1053
/**
1054
 * TActiveRecordChangeEventParameter class
1055
 *
1056
 * TActiveRecordChangeEventParameter encapsulates the parameter data for
1057
 * ActiveRecord change commit events that are broadcasted. The following change events
1058
 * may be raise: {@link TActiveRecord::OnInsert}, {@link TActiveRecord::OnUpdate} and
1059
 * {@link TActiveRecord::OnDelete}. The {@link setIsValid IsValid} parameter can
1060
 * be set to false to prevent the requested change event to be performed.
1061
 *
1062
 * @author Wei Zhuo<[email protected]>
1063
 * @package System.Data.ActiveRecord
1064
 * @since 3.1.2
1065
 */
1066
class TActiveRecordChangeEventParameter extends TEventParameter
1067
{
1068
	private $_isValid=true;
1069
1070
	/**
1071
	 * @return boolean whether the event should be performed.
1072
	 */
1073
	public function getIsValid()
1074
	{
1075
		return $this->_isValid;
1076
	}
1077
1078
	/**
1079
	 * @param boolean set to false to prevent the event.
1080
	 */
1081
	public function setIsValid($value)
1082
	{
1083
		$this->_isValid = TPropertyValue::ensureBoolean($value);
1084
	}
1085
}
1086
1087
/**
1088
 * TActiveRecordInvalidFinderResult class.
1089
 * TActiveRecordInvalidFinderResult defines the enumerable type for possible results
1090
 * if an invalid {@link TActiveRecord::__call magic-finder} invoked.
1091
 *
1092
 * The following enumerable values are defined:
1093
 * - Null: return null (default)
1094
 * - Exception: throws a TActiveRecordException
1095
 *
1096
 * @author Yves Berkholz <[email protected]>
1097
 * @package System.Data.ActiveRecord
1098
 * @see TActiveRecordManager::setInvalidFinderResult
1099
 * @see TActiveRecordConfig::setInvalidFinderResult
1100
 * @see TActiveRecord::setInvalidFinderResult
1101
 * @since 3.1.5
1102
 */
1103
class TActiveRecordInvalidFinderResult extends TEnumerable
1104
{
1105
	const Null = 'Null';
1106
	const Exception = 'Exception';
1107
}
1108