TActiveRecord::__call()   C
last analyzed

Complexity

Conditions 16
Paths 29

Size

Total Lines 29
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 18.8084

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 16
eloc 24
c 2
b 0
f 0
nc 29
nop 2
dl 0
loc 29
ccs 14
cts 18
cp 0.7778
crap 18.8084
rs 5.5666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * TActiveRecord class file.
5
 *
6
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
7
 * @link https://github.com/pradosoft/prado
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 */
10
11
namespace Prado\Data\ActiveRecord;
12
13
use Prado\Data\TDbConnection;
14
use Prado\Data\Common\TDbTableInfo;
15
use Prado\Data\ActiveRecord\Exceptions\TActiveRecordException;
16
use Prado\Data\ActiveRecord\Relations\TActiveRecordRelationContext;
17
use Prado\Data\DataGateway\TSqlCriteria;
18
use Prado\Prado;
19
use Prado\TPropertyValue;
20
use ReflectionClass;
21
22
/**
23
 * Base class for active records.
24
 *
25
 * An active record creates an object that wraps a row in a database table
26
 * or view, encapsulates the database access, and adds domain logic on that data.
27
 *
28
 * Active record objects are stateful, this is main difference between the
29
 * TActiveRecord implementation and the TTableGateway implementation.
30
 *
31
 * The essence of an Active Record is an object model of the
32
 * domain (e.g. products, items) that incorporates both behavior and
33
 * data in which the classes match very closely the record structure of an
34
 * underlying database. Each Active Record is responsible for saving and
35
 * loading to the database and also for any domain logic that acts on the data.
36
 *
37
 * The Active Record provides methods that do the following:
38
 *  1. Construct an instance of the Active Record from a SQL result set row.
39
 *  2. Construct a new instance for later insertion into the table.
40
 *  3. Finder methods to wrap commonly used SQL queries and return Active Record objects.
41
 *  4. Update the database and insert into it the data in the Active Record.
42
 *
43
 * Example:
44
 * ```php
45
 * class UserRecord extends TActiveRecord
46
 * {
47
 *     const TABLE='users'; //optional table name.
48
 *
49
 *     public $username; //corresponds to the fieldname in the table
50
 *     public $email;
51
 *
52
 *     //returns active record finder instance
53
 *     public static function finder($className=__CLASS__)
54
 *     {
55
 *         return parent::finder($className);
56
 *     }
57
 * }
58
 *
59
 * //create a connection and give it to the ActiveRecord manager.
60
 * $dsn = 'pgsql:host=localhost;dbname=test';
61
 * $conn = new TDbConnection($dsn, 'dbuser','dbpass');
62
 * TActiveRecordManager::getInstance()->setDbConnection($conn);
63
 *
64
 * //load the user record with username (primary key) 'admin'.
65
 * $user = UserRecord::finder()->findByPk('admin');
66
 * $user->email = '[email protected]';
67
 * $user->save(); //update the 'admin' record.
68
 * ```
69
 *
70
 * Since v3.1.1, TActiveRecord starts to support column mapping. The physical
71
 * column names (defined in database) can be mapped to logical column names
72
 * (defined in active classes as public properties.) To use this feature, declare
73
 * a static class variable COLUMN_MAPPING like the following:
74
 * ```php
75
 * class UserRecord extends TActiveRecord
76
 * {
77
 *     const TABLE='users';
78
 *     public static $COLUMN_MAPPING=array
79
 *     (
80
 *         'user_id'=>'username',
81
 *         'email_address'=>'email',
82
 *     );
83
 *     public $username;
84
 *     public $email;
85
 * }
86
 * ```
87
 * In the above, the 'users' table consists of 'user_id' and 'email_address' columns,
88
 * while the UserRecord class declares 'username' and 'email' properties.
89
 * By using column mapping, we can regularize the naming convention of column names
90
 * in active record.
91
 *
92
 * Since v3.1.2, TActiveRecord enhanced its support to access of foreign objects.
93
 * By declaring a public static variable RELATIONS like the following, one can access
94
 * the corresponding foreign objects easily:
95
 * ```php
96
 * class UserRecord extends TActiveRecord
97
 * {
98
 *     const TABLE='users';
99
 *     public static $RELATIONS=array
100
 *     (
101
 *         'department'=>array(self::BELONGS_TO, 'DepartmentRecord', 'department_id'),
102
 *         'contacts'=>array(self::HAS_MANY, 'ContactRecord', 'user_id'),
103
 *     );
104
 * }
105
 * ```
106
 * In the above, the users table is related with departments table (represented by
107
 * DepartmentRecord) and contacts table (represented by ContactRecord). Now, given a UserRecord
108
 * instance $user, one can access its department and contacts simply by: $user->department and
109
 * $user->contacts. No explicit data fetching is needed. Internally, the foreign objects are
110
 * fetched in a lazy way, which avoids unnecessary overhead if the foreign objects are not accessed
111
 * at all.
112
 *
113
 * Since v3.1.2, new events OnInsert, OnUpdate and OnDelete are available.
114
 * The event OnInsert, OnUpdate and OnDelete methods are executed before
115
 * inserting, updating, and deleting the current record, respectively. You may override
116
 * these methods; a TActiveRecordChangeEventParameter parameter is passed to these methods.
117
 * The property {@see \Prado\Data\ActiveRecord\TActiveRecordChangeEventParameter::setIsValid IsValid} of the parameter
118
 * can be set to false to prevent the change action to be executed. This can be used,
119
 * for example, to validate the record before the action is executed. For example,
120
 * in the following the password property is hashed before a new record is inserted.
121
 * ```php
122
 * class UserRecord extends TActiveRecord
123
 * {
124
 *      function OnInsert($param)
125
 *      {
126
 *          //parent method should be called to raise the event
127
 *          parent::OnInsert($param);
128
 *          $this->nounce = md5(time());
129
 *          $this->password = md5($this->password.$this->nounce);
130
 *      }
131
 * }
132
 * ```
133
 *
134
 * Since v3.1.3 you can also define a method that returns the table name.
135
 * ```php
136
 * class UserRecord extends TActiveRecord
137
 * {
138
 *     public function table()
139
 *     {
140
 *          return 'users';
141
 *     }
142
 *
143
 * }
144
 * ```
145
 *
146
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
147
 * @since 3.1
148
 */
149
abstract class TActiveRecord extends \Prado\TComponent
150
{
151
	public const BELONGS_TO = 'BELONGS_TO';
152
	public const HAS_ONE = 'HAS_ONE';
153
	public const HAS_MANY = 'HAS_MANY';
154
	public const MANY_TO_MANY = 'MANY_TO_MANY';
155
156
	public const STATE_NEW = 0;
157
	public const STATE_LOADED = 1;
158
	public const STATE_DELETED = 2;
159
160
	/**
161
	 * @var int 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 = [];
175
	private static $_columnMapping = [];
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 = [];
185
	private static $_relations = [];
186
187
	/**
188
	 * @var array holding relation objects for non explicitly defined properties
189
	 */
190
	protected $_relationsObjs = [];
191
192
	/**
193
	 * @var TDbConnection database connection object.
194
	 */
195
	protected $_connection; // use protected so that serialization is fine
196
197
198
	/**
199
	 * Defaults to 'null'
200
	 *
201
	 * @var null|string|TActiveRecordInvalidFinderResult
202
	 * @since 3.1.5
203
	 */
204
	protected $_invalidFinderResult; // use protected so that serialization is fine
205
206
	/**
207
	 * Prevent __call() method creating __sleep() when serializing.
208
	 */
209
	public function __sleep()
210
	{
211
		return array_diff(parent::__sleep(), ["\0*\0_connection"]);
212
	}
213
214
	/**
215
	 * Prevent __call() method creating __wakeup() when unserializing.
216
	 */
217
	public function __wakeup()
218
	{
219
		$this->setupColumnMapping();
220
		$this->setupRelations();
221
		parent::__wakeup();
222
	}
223
224
	/**
225
	 * Create a new instance of an active record with given $data. The record
226
	 * can be saved to the database specified by the $connection object.
227 31
	 *
228
	 * @param array $data optional name value pair record data.
229 31
	 * @param null|TDbConnection $connection optional database connection this object record use.
230
	 */
231
	public function __construct($data = [], $connection = null)
232 31
	{
233 31
		if ($connection !== null) {
234 31
			$this->setDbConnection($connection);
235 29
		}
236
		$this->setupColumnMapping();
237 31
		$this->setupRelations();
238
		if (!empty($data)) { //$data may be an object
239
			$this->copyFrom($data);
240
		}
241
		parent::__construct();
242
	}
243
244
	/**
245
	 * Magic method for reading properties.
246
	 * This method is overriden to provide read access to the foreign objects via
247 1
	 * the key names declared in the RELATIONS array.
248
	 * @param string $name property name
249 1
	 * @return mixed property value.
250
	 * @since 3.1.2
251
	 */
252
	public function __get($name)
253 1
	{
254
		if ($this->hasRecordRelation($name) && !$this->canGetProperty($name)) {
255
			$this->fetchResultsFor($name);
256
			return $this->_relationsObjs[$name];
257
		}
258
		return parent::__get($name);
259
	}
260
261
	/**
262
	 * Magic method for writing properties.
263
	 * This method is overriden to provide write access to the foreign objects via
264 1
	 * the key names declared in the RELATIONS array.
265
	 * @param string $name property name
266 1
	 * @param mixed $value property value.
267
	 * @since 3.1.2
268
	 */
269 1
	public function __set($name, $value)
270
	{
271 1
		if ($this->hasRecordRelation($name) && !$this->canSetProperty($name)) {
272
			$this->_relationsObjs[$name] = $value;
273
		} else {
274
			parent::__set($name, $value);
275
		}
276 31
	}
277
278 31
	/**
279 31
	 * @since 3.1.1
280 9
	 */
281 9
	private function setupColumnMapping()
282
	{
283 31
		$className = $this::class;
284
		if (!isset(self::$_columnMapping[$className])) {
285
			$class = new ReflectionClass($className);
286
			self::$_columnMapping[$className] = $class->getStaticPropertyValue('COLUMN_MAPPING');
287
		}
288 31
	}
289
290 31
	/**
291 31
	 * @since 3.1.2
292 9
	 */
293 9
	private function setupRelations()
294 9
	{
295 3
		$className = $this::class;
296
		if (!isset(self::$_relations[$className])) {
297 9
			$class = new ReflectionClass($className);
298
			$relations = [];
299 31
			foreach ($class->getStaticPropertyValue('RELATIONS') as $key => $value) {
300
				$relations[strtolower($key)] = [$key, $value];
301
			}
302
			self::$_relations[$className] = $relations;
303
		}
304
	}
305
306 29
	/**
307
	 * Copies data from an array or another object.
308 29
	 * @param mixed $data
309
	 * @throws TActiveRecordException if data is not array or not object.
310
	 */
311 29
	public function copyFrom($data)
312
	{
313
		if (is_object($data)) {
314 29
			$data = get_object_vars($data);
315 29
		}
316
		if (!is_array($data)) {
317 29
			throw new TActiveRecordException('ar_data_invalid', $this::class);
318
		}
319
		foreach ($data as $name => $value) {
320 6
			$this->setColumnValue($name, $value);
321
		}
322 6
	}
323 6
324
325 6
	public static function getActiveDbConnection()
326
	{
327
		if (($db = self::getRecordManager()->getDbConnection()) !== null) {
328
			$db->setActive(true);
329
		}
330
		return $db;
331
	}
332
333 31
	/**
334
	 * Gets the current Db connection, the connection object is obtained from
335 31
	 * the TActiveRecordManager if connection is currently null.
336 6
	 * @return \Prado\Data\TDbConnection current db connection for this object.
337
	 */
338 31
	public function getDbConnection()
339
	{
340
		if ($this->_connection === null) {
341
			$this->_connection = self::getActiveDbConnection();
342
		}
343
		return $this->_connection;
344
	}
345
346
	/**
347
	 * @param \Prado\Data\TDbConnection $connection db connection object for this record.
348
	 */
349
	public function setDbConnection($connection)
350
	{
351
		$this->_connection = $connection;
352
	}
353
354
	/**
355
	 * @return TDbTableInfo the meta information of the table associated with this AR class.
356
	 */
357
	public function getRecordTableInfo()
358
	{
359
		return $this->getRecordGateway()->getRecordTableInfo($this);
360
	}
361
362
	/**
363
	 * Compare two records using their primary key values (all column values if
364
	 * table does not defined primary keys). The default uses simple == for
365
	 * comparison of their values. Set $strict=true for identity comparison (===).
366
	 * @param TActiveRecord $record another record to compare with.
367
	 * @param bool $strict true to perform strict identity comparison
368
	 * @return bool true if $record equals, false otherwise.
369
	 */
370
	public function equals(TActiveRecord $record, $strict = false)
371
	{
372
		if ($record === null || $this::class !== $record::class) {
373
			return false;
374
		}
375
		$tableInfo = $this->getRecordTableInfo();
376
		$pks = $tableInfo->getPrimaryKeys();
377
		$properties = count($pks) > 0 ? $pks : $tableInfo->getColumns()->getKeys();
378
		$equals = true;
379
		foreach ($properties as $prop) {
380
			if ($strict) {
381
				$equals = $equals && $this->getColumnValue($prop) === $record->getColumnValue($prop);
382
			} else {
383
				$equals = $equals && $this->getColumnValue($prop) == $record->getColumnValue($prop);
384
			}
385
			if (!$equals) {
386
				return false;
387
			}
388
		}
389
		return $equals;
390
	}
391
392
	/**
393
	 * Returns the instance of a active record finder for a particular class.
394
	 * The finder objects are static instances for each ActiveRecord class.
395
	 * This means that event handlers bound to these finder instances are class wide.
396 40
	 * Create a new instance of the ActiveRecord class if you wish to bound the
397
	 * event handlers to object instance.
398 40
	 * @param string $className active record class name.
399 40
	 * @return TActiveRecord active record finder instance.
400 11
	 */
401 11
	public static function finder($className = __CLASS__)
402
	{
403 40
		static $finders = [];
404
		if (!isset($finders[$className])) {
405
			$f = Prado::createComponent($className);
406
			$finders[$className] = $f;
407
		}
408
		return $finders[$className];
409
	}
410
411 6
	/**
412
	 * Gets the record manager for this object, the default is to call
413 6
	 * TActiveRecordManager::getInstance().
414
	 * @return TActiveRecordManager default active record manager.
415
	 */
416
	public static function getRecordManager()
417
	{
418
		return TActiveRecordManager::getInstance();
419 39
	}
420
421 39
	/**
422
	 * @return TActiveRecordGateway record table gateway.
423
	 */
424
	public function getRecordGateway()
425
	{
426
		return TActiveRecordManager::getInstance()->getRecordGateway();
427
	}
428 2
429
	/**
430 2
	 * Saves the current record to the database, insert or update is automatically determined.
431 2
	 * @return bool true if record was saved successfully, false otherwise.
432 2
	 */
433 2
	public function save()
434 2
	{
435 2
		$gateway = $this->getRecordGateway();
436 2
		$param = new TActiveRecordChangeEventParameter();
437
		if ($this->_recordState === self::STATE_NEW) {
438 2
			$this->onInsert($param);
439 2
			if ($param->getIsValid() && $gateway->insert($this)) {
440 2
				$this->_recordState = self::STATE_LOADED;
441 2
				return true;
442
			}
443
		} elseif ($this->_recordState === self::STATE_LOADED) {
444
			$this->onUpdate($param);
445
			if ($param->getIsValid() && $gateway->update($this)) {
446
				return true;
447
			}
448
		} else {
449
			throw new TActiveRecordException('ar_save_invalid', $this::class);
450
		}
451
452
		return false;
453
	}
454
455 2
	/**
456
	 * Deletes the current record from the database. Once deleted, this object
457 2
	 * can not be saved again in the same instance.
458 2
	 * @return bool true if the record was deleted successfully, false otherwise.
459 2
	 */
460 2
	public function delete()
461 2
	{
462 2
		if ($this->_recordState === self::STATE_LOADED) {
463 2
			$gateway = $this->getRecordGateway();
464
			$param = new TActiveRecordChangeEventParameter();
465
			$this->onDelete($param);
466
			if ($param->getIsValid() && $gateway->delete($this)) {
467
				$this->_recordState = self::STATE_DELETED;
468
				return true;
469
			}
470
		} else {
471
			throw new TActiveRecordException('ar_delete_invalid', $this::class);
472
		}
473
474
		return false;
475
	}
476
477
	/**
478
	 * Delete records by primary key. Usage:
479
	 *
480
	 * ```php
481
	 * $finder->deleteByPk($primaryKey); //delete 1 record
482
	 * $finder->deleteByPk($key1,$key2,...); //delete multiple records
483
	 * $finder->deleteByPk(array($key1,$key2,...)); //delete multiple records
484
	 * ```
485
	 *
486
	 * For composite primary keys (determined from the table definitions):
487
	 * ```php
488
	 * $finder->deleteByPk(array($key1,$key2)); //delete 1 record
489
	 *
490
	 * //delete multiple records
491
	 * $finder->deleteByPk(array($key1,$key2), array($key3,$key4),...);
492
	 *
493
	 * //delete multiple records
494
	 * $finder->deleteByPk(array( array($key1,$key2), array($key3,$key4), .. ));
495 2
	 * ```
496
	 *
497 2
	 * @param mixed $keys primary key values.
498 2
	 * @return int number of records deleted.
499
	 */
500 2
	public function deleteByPk($keys)
501
	{
502
		if (func_num_args() > 1) {
503
			$keys = func_get_args();
504
		}
505
		return $this->getRecordGateway()->deleteRecordsByPk($this, (array) $keys);
506
	}
507
508
	/**
509
	 * Alias for deleteByPk()
510
	 * @param mixed $keys
511
	 */
512
	public function deleteAllByPks($keys)
513
	{
514
		if (func_num_args() > 1) {
515
			$keys = func_get_args();
516
		}
517
		return $this->deleteByPk($keys);
518
	}
519
	/**
520 1
	 * Delete multiple records using a criteria.
521
	 * @param string|TActiveRecordCriteria $criteria SQL condition or criteria object.
522 1
	 * @param mixed $parameters parameter values.
523 1
	 * @return int number of records deleted.
524 1
	 */
525
	public function deleteAll($criteria = null, $parameters = [])
526
	{
527
		$args = func_num_args() > 1 ? array_slice(func_get_args(), 1) : null;
528
		$criteria = $this->getRecordCriteria($criteria, $parameters, $args);
529
		return $this->getRecordGateway()->deleteRecordsByCriteria($this, $criteria);
530
	}
531
532
	/**
533 31
	 * Populates a new record with the query result.
534
	 * This is a wrapper of {@see createRecord}.
535 31
	 * @param array $data name value pair of record data
536
	 * @return TActiveRecord object record, null if data is empty.
537
	 */
538
	protected function populateObject($data)
539
	{
540
		return self::createRecord($this::class, $data);
541
	}
542
543 14
	/**
544
	 * @param \Prado\Data\TDbDataReader $reader data reader
545 14
	 * @return array the AR objects populated by the query result
546 14
	 * @since 3.1.2
547 14
	 */
548
	protected function populateObjects($reader)
549 14
	{
550
		$result = [];
551
		foreach ($reader as $data) {
552
			$result[] = $this->populateObject($data);
553
		}
554
		return $result;
555
	}
556
557
	/**
558
	 * Create an AR instance specified by the AR class name and initial data.
559
	 * If the initial data is empty, the AR object will not be created and null will be returned.
560
	 * (You should use the "new" operator to create the AR instance in that case.)
561 31
	 * @param string $type the AR class name
562
	 * @param array $data initial data to be populated into the AR object.
563 31
	 * @return null|TActiveRecord the initialized AR object. Null if the initial data is empty.
564 2
	 * @since 3.1.2
565
	 */
566 29
	public static function createRecord($type, $data)
567 29
	{
568 29
		if (empty($data)) {
569
			return null;
570
		}
571
		$record = new $type($data);
572
		$record->_recordState = self::STATE_LOADED;
573
		return $record;
574
	}
575
576
	/**
577
	 * Find one single record that matches the criteria.
578
	 *
579
	 * Usage:
580
	 * ```php
581
	 * $finder->find('username = :name AND password = :pass',
582
	 * 					array(':name'=>$name, ':pass'=>$pass));
583
	 * $finder->find('username = ? AND password = ?', array($name, $pass));
584
	 * $finder->find('username = ? AND password = ?', $name, $pass);
585
	 * //$criteria is of TActiveRecordCriteria
586
	 * $finder->find($criteria); //the 2nd parameter for find() is ignored.
587
	 * ```
588 10
	 *
589
	 * @param string|TActiveRecordCriteria $criteria SQL condition or criteria object.
590 10
	 * @param mixed $parameters parameter values.
591 10
	 * @return TActiveRecord matching record object. Null if no result is found.
592 10
	 */
593 10
	public function find($criteria, $parameters = [])
594 9
	{
595
		$args = func_num_args() > 1 ? array_slice(func_get_args(), 1) : null;
596
		$criteria = $this->getRecordCriteria($criteria, $parameters, $args);
597
		$criteria->setLimit(1);
598
		$data = $this->getRecordGateway()->findRecordsByCriteria($this, $criteria);
599
		return $this->populateObject($data);
600
	}
601
602
	/**
603
	 * Same as find() but returns an array of objects.
604 9
	 *
605
	 * @param string|TActiveRecordCriteria $criteria SQL condition or criteria object.
606 9
	 * @param mixed $parameters parameter values.
607 9
	 * @return array matching record objects. Empty array if no result is found.
608 4
	 */
609
	public function findAll($criteria = null, $parameters = [])
610 9
	{
611 9
		$args = func_num_args() > 1 ? array_slice(func_get_args(), 1) : null;
612
		if ($criteria !== null) {
613
			$criteria = $this->getRecordCriteria($criteria, $parameters, $args);
614
		}
615
		$result = $this->getRecordGateway()->findRecordsByCriteria($this, $criteria, true);
616
		return $this->populateObjects($result);
617
	}
618
619
	/**
620
	 * Find one record using only the primary key or composite primary keys. Usage:
621
	 *
622
	 * ```php
623
	 * $finder->findByPk($primaryKey);
624
	 * $finder->findByPk($key1, $key2, ...);
625
	 * $finder->findByPk(array($key1,$key2,...));
626 9
	 * ```
627
	 *
628 9
	 * @param mixed $keys primary keys
629
	 * @return null|TActiveRecord Null if no result is found.
630
	 */
631 9
	public function findByPk($keys)
632
	{
633
		if ($keys === null) {
634 9
			return null;
635 9
		}
636
		if (func_num_args() > 1) {
637
			$keys = func_get_args();
638
		}
639
		$data = $this->getRecordGateway()->findRecordByPK($this, $keys);
640
		return $this->populateObject($data);
641
	}
642
643
	/**
644
	 * Find multiple records matching a list of primary or composite keys.
645
	 *
646
	 * For scalar primary keys:
647
	 * ```php
648
	 * $finder->findAllByPk($key1, $key2, ...);
649
	 * $finder->findAllByPk(array($key1, $key2, ...));
650
	 * ```
651
	 *
652
	 * For composite keys:
653
	 * ```php
654
	 * $finder->findAllByPk(array($key1, $key2), array($key3, $key4), ...);
655 3
	 * $finder->findAllByPk(array(array($key1, $key2), array($key3, $key4), ...));
656
	 * ```
657 3
	 * @param mixed $keys primary keys
658 3
	 * @return array matching ActiveRecords. Empty array is returned if no result is found.
659
	 */
660 3
	public function findAllByPks($keys)
661 3
	{
662
		if (func_num_args() > 1) {
663
			$keys = func_get_args();
664
		}
665
		$result = $this->getRecordGateway()->findRecordsByPks($this, (array) $keys);
666
		return $this->populateObjects($result);
0 ignored issues
show
Bug introduced by
$result of type array is incompatible with the type Prado\Data\TDbDataReader expected by parameter $reader of Prado\Data\ActiveRecord\...cord::populateObjects(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

666
		return $this->populateObjects(/** @scrutinizer ignore-type */ $result);
Loading history...
667
	}
668
669
	/**
670
	 * Find records using full SQL, returns corresponding record object.
671
	 * The names of the column retrieved must be defined in your Active Record
672 1
	 * class.
673
	 * @param string $sql select SQL
674 1
	 * @param array $parameters
675 1
	 * @return TActiveRecord null if no result is returned.
676 1
	 */
677 1
	public function findBySql($sql, $parameters = [])
678 1
	{
679
		$args = func_num_args() > 1 ? array_slice(func_get_args(), 1) : null;
680
		$criteria = $this->getRecordCriteria($sql, $parameters, $args);
681
		$criteria->setLimit(1);
682
		$data = $this->getRecordGateway()->findRecordBySql($this, $criteria);
683
		return $this->populateObject($data);
684
	}
685
686
	/**
687
	 * Find records using full SQL, returns corresponding record object.
688
	 * The names of the column retrieved must be defined in your Active Record
689
	 * class.
690
	 * @param string $sql select SQL
691
	 * @param array $parameters
692
	 * @return array matching active records. Empty array is returned if no result is found.
693
	 */
694
	public function findAllBySql($sql, $parameters = [])
695
	{
696
		$args = func_num_args() > 1 ? array_slice(func_get_args(), 1) : null;
697
		$criteria = $this->getRecordCriteria($sql, $parameters, $args);
698
		$result = $this->getRecordGateway()->findRecordsBySql($this, $criteria);
699
		return $this->populateObjects($result);
700
	}
701
702
	/**
703
	 * Fetches records using the sql clause "(fields) IN (values)", where
704
	 * fields is an array of column names and values is an array of values that
705
	 * the columns must have.
706
	 *
707
	 * This method is to be used by the relationship handler.
708
	 *
709 7
	 * @param TActiveRecordCriteria $criteria additional criteria
710
	 * @param array $fields field names to match with "(fields) IN (values)" sql clause.
711 7
	 * @param array $values matching field values.
712 7
	 * @return array matching active records. Empty array is returned if no result is found.
713
	 */
714
	public function findAllByIndex($criteria, $fields, $values)
715
	{
716
		$result = $this->getRecordGateway()->findRecordsByIndex($this, $criteria, $fields, $values);
717
		return $this->populateObjects($result);
718
	}
719
720
	/**
721 3
	 * Find the number of records.
722
	 * @param string|TActiveRecordCriteria $criteria SQL condition or criteria object.
723 3
	 * @param mixed $parameters parameter values.
724 3
	 * @return int number of records.
725 2
	 */
726
	public function count($criteria = null, $parameters = [])
727 3
	{
728
		$args = func_num_args() > 1 ? array_slice(func_get_args(), 1) : null;
729
		if ($criteria !== null) {
730
			$criteria = $this->getRecordCriteria($criteria, $parameters, $args);
731
		}
732
		return $this->getRecordGateway()->countRecords($this, $criteria);
733
	}
734
735
	/**
736
	 * Returns the active record relationship handler for $RELATION with key
737 8
	 * value equal to the $property value.
738
	 * @param string $name relationship/property name corresponding to keys in $RELATION array.
739 8
	 * @param array $args method call arguments.
740 8
	 * @return null|\Prado\Data\ActiveRecord\Relations\TActiveRecordRelation null if the context or the handler doesn't exist
741 8
	 */
742
	protected function getRelationHandler($name, $args = [])
743
	{
744
		if (($context = $this->createRelationContext($name)) !== null) {
745
			$criteria = $this->getRecordCriteria(count($args) > 0 ? $args[0] : null, array_slice($args, 1));
746
			return $context->getRelationHandler($criteria);
747
		} else {
748
			return null;
749
		}
750
	}
751
752
	/**
753
	 * Gets a static copy of the relationship context for given property (a key
754
	 * in $RELATIONS), returns null if invalid relationship. Keeps a null
755
	 * reference to all invalid relations called.
756 8
	 * @param string $name relationship/property name corresponding to keys in $RELATION array.
757
	 * @return null|TActiveRecordRelationContext object containing information on
758 8
	 * the active record relationships for given property, null if invalid relationship
759 8
	 * @since 3.1.2
760 8
	 */
761
	protected function createRelationContext($name)
762
	{
763
		if (($definition = $this->getRecordRelation($name)) !== null) {
0 ignored issues
show
introduced by
The condition $definition = $this->get...elation($name) !== null is always true.
Loading history...
764
			[$property, $relation] = $definition;
765
			return new TActiveRecordRelationContext($this, $property, $relation);
766
		} else {
767
			return null;
768
		}
769
	}
770
771
	/**
772
	 * Tries to load the relationship results for the given property. The $property
773
	 * value should correspond to an entry key in the $RELATION array.
774
	 * This method can be used to lazy load relationships.
775
	 * ```php
776
	 * class TeamRecord extends TActiveRecord
777
	 * {
778
	 *     ...
779
	 *
780
	 *     private $_players;
781
	 *     public static $RELATION=array
782
	 *     (
783
	 *         'players' => array(self::HAS_MANY, 'PlayerRecord'),
784
	 *     );
785
	 *
786
	 *     public function setPlayers($array)
787
	 *     {
788
	 *         $this->_players=$array;
789
	 *     }
790
	 *
791
	 *     public function getPlayers()
792
	 *     {
793
	 *         if($this->_players===null)
794
	 *             $this->fetchResultsFor('players');
795
	 *         return $this->_players;
796
	 *     }
797
	 * }
798
	 * Usage example:
799
	 * $team = TeamRecord::finder()->findByPk(1);
800
	 * var_dump($team->players); //uses lazy load to fetch 'players' relation
801
	 * ```
802
	 * @param string $property relationship/property name corresponding to keys in $RELATION array.
803
	 * @return bool true if relationship exists, false otherwise.
804
	 * @since 3.1.2
805
	 */
806
	protected function fetchResultsFor($property)
807
	{
808
		if (($context = $this->createRelationContext($property)) !== null) {
809
			return $context->getRelationHandler()->fetchResultsInto($this);
810
		} else {
811
			return false;
812
		}
813
	}
814
815
	/**
816
	 * Dynamic find method using parts of method name as search criteria.
817
	 * Method name starting with "findBy" only returns 1 record.
818
	 * Method name starting with "findAllBy" returns 0 or more records.
819
	 * Method name starting with "deleteBy" deletes records by the trail criteria.
820
	 * The condition is taken as part of the method name after "findBy", "findAllBy"
821
	 * or "deleteBy".
822
	 *
823
	 * The following are equivalent:
824
	 * ```php
825
	 * $finder->findByName($name)
826
	 * $finder->find('Name = ?', $name);
827
	 * ```
828
	 * ```php
829
	 * $finder->findByUsernameAndPassword($name,$pass); // OR may be used
830
	 * $finder->findBy_Username_And_Password($name,$pass); // _OR_ may be used
831
	 * $finder->find('Username = ? AND Password = ?', $name, $pass);
832
	 * ```
833
	 * ```php
834
	 * $finder->findAllByAge($age);
835
	 * $finder->findAll('Age = ?', $age);
836
	 * ```
837
	 * ```php
838
	 * $finder->deleteAll('Name = ?', $name);
839
	 * $finder->deleteByName($name);
840
	 * ```
841 38
	 * @param mixed $method
842
	 * @param mixed $args
843 38
	 * @return mixed single record if method name starts with "findBy", 0 or more records
844 38
	 * if method name starts with "findAllBy"
845 8
	 */
846 8
	public function __call($method, $args)
847 38
	{
848 4
		$delete = false;
849 37
		if (strncasecmp($method, 'with', 4) === 0) {
850 1
			$property = $method[4] === '_' ? substr($method, 5) : substr($method, 4);
851 37
			return $this->getRelationHandler($property, $args);
852 1
		} elseif ($findOne = strncasecmp($method, 'findby', 6) === 0) {
853 37
			$condition = $method[6] === '_' ? substr($method, 7) : substr($method, 6);
854
		} elseif (strncasecmp($method, 'findallby', 9) === 0) {
855 37
			$condition = $method[9] === '_' ? substr($method, 10) : substr($method, 9);
856 37
		} elseif ($delete = strncasecmp($method, 'deleteby', 8) === 0) {
857
			$condition = $method[8] === '_' ? substr($method, 9) : substr($method, 8);
858
		} elseif ($delete = strncasecmp($method, 'deleteallby', 11) === 0) {
859
			$condition = $method[11] === '_' ? substr($method, 12) : substr($method, 11);
860
		} elseif (strncasecmp($method, 'dy', 2) === 0 || strncasecmp($method, 'fx', 2) === 0) {
861
			return parent::__call($method, $args);
862
		} else {
863
			if ($this->getInvalidFinderResult() == TActiveRecordInvalidFinderResult::Exception) {
0 ignored issues
show
introduced by
The condition $this->getInvalidFinderR...FinderResult::Exception is always false.
Loading history...
864
				throw new TActiveRecordException('ar_invalid_finder_method', $method);
865 6
			} else {
866 5
				return null;
867 1
			}
868
		}
869 4
870
		$criteria = $this->getRecordGateway()->getCommand($this)->createCriteriaFromString($method, $condition, $args);
871
		if ($delete) {
872
			return $this->deleteAll($criteria);
873
		} else {
874
			return $findOne ? $this->find($criteria) : $this->findAll($criteria);
875
		}
876
	}
877
878
	/**
879
	 * @return TActiveRecordInvalidFinderResult Defaults to '{@see \Prado\Data\ActiveRecord\TActiveRecordInvalidFinderResult::Null Null}'.
880
	 * @see TActiveRecordManager::getInvalidFinderResult
881
	 * @since 3.1.5
882
	 */
883
	public function getInvalidFinderResult()
884
	{
885
		if ($this->_invalidFinderResult !== null) {
886
			return $this->_invalidFinderResult;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_invalidFinderResult also could return the type string which is incompatible with the documented return type Prado\Data\ActiveRecord\...cordInvalidFinderResult.
Loading history...
887
		}
888
889
		return self::getRecordManager()->getInvalidFinderResult();
890
	}
891
892
	/**
893
	 * Define the way an active record finder react if an invalid magic-finder invoked
894
	 *
895
	 * @param null|TActiveRecordInvalidFinderResult $value
896
	 * @see TActiveRecordManager::setInvalidFinderResult
897
	 * @since 3.1.5
898
	 */
899
	public function setInvalidFinderResult($value)
900
	{
901
		if ($value === null) {
902
			$this->_invalidFinderResult = null;
903
		} else {
904
			$this->_invalidFinderResult = TPropertyValue::ensureEnum($value, \Prado\Data\ActiveRecord\TActiveRecordInvalidFinderResult::class);
905
		}
906
	}
907
908
	/**
909
	 * Create a new TSqlCriteria object from a string $criteria. The $args
910
	 * are additional parameters and are used in place of the $parameters
911
	 * if $parameters is not an array and $args is an arrary.
912 23
	 * @param string|TSqlCriteria $criteria sql criteria
913
	 * @param mixed $parameters parameters passed by the user.
914 23
	 * @param array $args additional parameters obtained from function_get_args().
915 12
	 * @return TSqlCriteria criteria object.
916 12
	 */
917 14
	protected function getRecordCriteria($criteria, $parameters, $args = [])
918 7
	{
919
		if (is_string($criteria)) {
920 7
			$useArgs = !is_array($parameters) && is_array($args);
921
			return new TActiveRecordCriteria($criteria, $useArgs ? $args : $parameters);
922
		} elseif ($criteria instanceof TSqlCriteria) {
0 ignored issues
show
introduced by
$criteria is always a sub-type of Prado\Data\DataGateway\TSqlCriteria.
Loading history...
923
			return $criteria;
924
		} else {
925
			return new TActiveRecordCriteria();
926
		}
927
		//throw new TActiveRecordException('ar_invalid_criteria');
928
	}
929
930
	/**
931
	 * Raised when a command is prepared and parameter binding is completed.
932
	 * The parameter object is TDataGatewayEventParameter of which the
933
	 * {@see \Prado\Data\DataGateway\TDataGatewayEventParameter::getCommand Command} property can be
934
	 * inspected to obtain the sql query to be executed.
935
	 *
936 37
	 * Note well that the finder objects obtained from ActiveRecord::finder()
937
	 * method are static objects. This means that the event handlers are
938 37
	 * bound to a static finder object and not to each distinct active record object.
939 37
	 * @param \Prado\Data\DataGateway\TDataGatewayEventParameter $param
940
	 */
941
	public function onCreateCommand($param)
942
	{
943
		$this->raiseEvent('OnCreateCommand', $this, $param);
944
	}
945
946
	/**
947
	 * Raised when a command is executed and the result from the database was returned.
948
	 * The parameter object is TDataGatewayResultEventParameter of which the
949
	 * {@see \Prado\Data\DataGateway\TDataGatewayEventParameter::getResult Result} property contains
950
	 * the data return from the database. The data returned can be changed
951
	 * by setting the {@see \Prado\Data\DataGateway\TDataGatewayEventParameter::setResult Result} property.
952
	 *
953 35
	 * Note well that the finder objects obtained from ActiveRecord::finder()
954
	 * method are static objects. This means that the event handlers are
955 35
	 * bound to a static finder object and not to each distinct active record object.
956 35
	 * @param \Prado\Data\DataGateway\TDataGatewayResultEventParameter $param
957
	 */
958
	public function onExecuteCommand($param)
959
	{
960
		$this->raiseEvent('OnExecuteCommand', $this, $param);
961
	}
962
963 2
	/**
964
	 * Raised before the record attempt to insert its data into the database.
965 2
	 * To prevent the insert operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
966 2
	 * @param TActiveRecordChangeEventParameter $param event parameter to be passed to the event handlers
967
	 */
968
	public function onInsert($param)
969
	{
970
		$this->raiseEvent('OnInsert', $this, $param);
971
	}
972
973 2
	/**
974
	 * Raised before the record attempt to delete its data from the database.
975 2
	 * To prevent the delete operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
976 2
	 * @param TActiveRecordChangeEventParameter $param event parameter to be passed to the event handlers
977
	 */
978
	public function onDelete($param)
979
	{
980
		$this->raiseEvent('OnDelete', $this, $param);
981
	}
982
983 2
	/**
984
	 * Raised before the record attempt to update its data in the database.
985 2
	 * To prevent the update operation, set the TActiveRecordChangeEventParameter::IsValid parameter to false.
986 2
	 * @param TActiveRecordChangeEventParameter $param event parameter to be passed to the event handlers
987
	 */
988
	public function onUpdate($param)
989
	{
990
		$this->raiseEvent('OnUpdate', $this, $param);
991
	}
992
993
	/**
994
	 * Retrieves the column value according to column name.
995 10
	 * This method is used internally.
996
	 * @param string $columnName the column name (as defined in database schema)
997 10
	 * @return mixed the corresponding column value
998 10
	 * @since 3.1.1
999
	 */
1000
	public function getColumnValue($columnName)
1001 10
	{
1002
		$className = $this::class;
1003
		if (isset(self::$_columnMapping[$className][$columnName])) {
1004
			$columnName = self::$_columnMapping[$className][$columnName];
1005
		}
1006
		return $this->$columnName;
1007
	}
1008
1009
	/**
1010
	 * Sets the column value according to column name.
1011 29
	 * This method is used internally.
1012
	 * @param string $columnName the column name (as defined in database schema)
1013 29
	 * @param mixed $value the corresponding column value
1014 29
	 * @since 3.1.1
1015
	 */
1016
	public function setColumnValue($columnName, $value)
1017 29
	{
1018 29
		$className = $this::class;
1019
		if (isset(self::$_columnMapping[$className][$columnName])) {
1020
			$columnName = self::$_columnMapping[$className][$columnName];
1021
		}
1022
		$this->$columnName = $value;
1023
	}
1024
1025 8
	/**
1026
	 * @param string $property relation property name
1027 8
	 * @return array relation definition for the specified property
1028 8
	 * @since 3.1.2
1029 8
	 */
1030
	public function getRecordRelation($property)
1031
	{
1032
		$className = $this::class;
1033
		$property = strtolower($property);
1034
		return self::$_relations[$className][$property] ?? null;
1035
	}
1036
1037
	/**
1038
	 * @return array all relation definitions declared in the AR class
1039
	 * @since 3.1.2
1040
	 */
1041
	public function getRecordRelations()
1042
	{
1043
		return self::$_relations[$this::class];
1044
	}
1045
1046 1
	/**
1047
	 * @param string $property AR property name
1048 1
	 * @return bool whether a relation is declared for the specified AR property
1049
	 * @since 3.1.2
1050
	 */
1051
	public function hasRecordRelation($property)
1052
	{
1053
		return isset(self::$_relations[$this::class][strtolower($property)]);
1054
	}
1055
1056
	/**
1057
	 * Return record data as array
1058
	 * @return array of column name and column values
1059
	 * @since 3.2.4
1060
	 */
1061
	public function toArray()
1062
	{
1063
		$result = [];
1064
		foreach ($this->getRecordTableInfo()->getLowerCaseColumnNames() as $columnName) {
1065
			$result[$columnName] = $this->getColumnValue($columnName);
1066
		}
1067
1068
		return $result;
1069
	}
1070
1071
	/**
1072
	 * Return record data as JSON
1073
	 * @return false|string json
1074
	 * @since 3.2.4
1075
	 */
1076
	public function toJSON()
1077
	{
1078
		return json_encode($this->toArray());
1079
	}
1080
}
1081