TActiveRecord::__sleep()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
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