1
2/*
3 +------------------------------------------------------------------------+
4 | Phalcon Framework                                                      |
5 +------------------------------------------------------------------------+
6 | Copyright (c) 2011-2017 Phalcon Team (https://phalconphp.com)          |
7 +------------------------------------------------------------------------+
8 | This source file is subject to the New BSD License that is bundled     |
9 | with this package in the file LICENSE.txt.                             |
10 |                                                                        |
11 | If you did not receive a copy of the license and are unable to         |
12 | obtain it through the world-wide-web, please send an email             |
13 | to license@phalconphp.com so we can send you a copy immediately.       |
14 +------------------------------------------------------------------------+
15 | Authors: Andres Gutierrez <andres@phalconphp.com>                      |
16 |          Eduar Carvajal <eduar@phalconphp.com>                         |
17 |          Jakob Oberhummer <cphalcon@chilimatic.com>                    |
18 +------------------------------------------------------------------------+
19 */
20
21namespace Phalcon\Mvc;
22
23use Phalcon\Di;
24use Phalcon\Db\Column;
25use Phalcon\Db\RawValue;
26use Phalcon\DiInterface;
27use Phalcon\Mvc\Model\Message;
28use Phalcon\Mvc\Model\ResultInterface;
29use Phalcon\Di\InjectionAwareInterface;
30use Phalcon\Mvc\Model\ManagerInterface;
31use Phalcon\Mvc\Model\MetaDataInterface;
32use Phalcon\Mvc\Model\Criteria;
33use Phalcon\Db\AdapterInterface;
34use Phalcon\Db\DialectInterface;
35use Phalcon\Mvc\Model\CriteriaInterface;
36use Phalcon\Mvc\Model\TransactionInterface;
37use Phalcon\Mvc\Model\Resultset;
38use Phalcon\Mvc\Model\ResultsetInterface;
39use Phalcon\Mvc\Model\Query;
40use Phalcon\Mvc\Model\Query\Builder;
41use Phalcon\Mvc\Model\Relation;
42use Phalcon\Mvc\Model\RelationInterface;
43use Phalcon\Mvc\Model\BehaviorInterface;
44use Phalcon\Mvc\Model\Exception;
45use Phalcon\Mvc\Model\MessageInterface;
46use Phalcon\Mvc\Model\Message;
47use Phalcon\ValidationInterface;
48use Phalcon\Mvc\Model\ValidationFailed;
49use Phalcon\Events\ManagerInterface as EventsManagerInterface;
50
51/**
52 * Phalcon\Mvc\Model
53 *
54 * Phalcon\Mvc\Model connects business objects and database tables to create
55 * a persistable domain model where logic and data are presented in one wrapping.
56 * It‘s an implementation of the object-relational mapping (ORM).
57 *
58 * A model represents the information (data) of the application and the rules to manipulate that data.
59 * Models are primarily used for managing the rules of interaction with a corresponding database table.
60 * In most cases, each table in your database will correspond to one model in your application.
61 * The bulk of your application's business logic will be concentrated in the models.
62 *
63 * Phalcon\Mvc\Model is the first ORM written in Zephir/C languages for PHP, giving to developers high performance
64 * when interacting with databases while is also easy to use.
65 *
66 * <code>
67 * $robot = new Robots();
68 *
69 * $robot->type = "mechanical";
70 * $robot->name = "Astro Boy";
71 * $robot->year = 1952;
72 *
73 * if ($robot->save() === false) {
74 *     echo "Umh, We can store robots: ";
75 *
76 *     $messages = $robot->getMessages();
77 *
78 *     foreach ($messages as $message) {
79 *         echo $message;
80 *     }
81 * } else {
82 *     echo "Great, a new robot was saved successfully!";
83 * }
84 * </code>
85 */
86abstract class Model implements EntityInterface, ModelInterface, ResultInterface, InjectionAwareInterface, \Serializable, \JsonSerializable
87{
88	protected _dependencyInjector;
89
90	protected _modelsManager;
91
92	protected _modelsMetaData;
93
94	protected _errorMessages;
95
96	protected _operationMade = 0;
97
98	protected _dirtyState = 1;
99
100	protected _transaction { get };
101
102	protected _uniqueKey;
103
104	protected _uniqueParams;
105
106	protected _uniqueTypes;
107
108	protected _skipped;
109
110	protected _related;
111
112	protected _snapshot;
113
114	protected _oldSnapshot;
115
116	const TRANSACTION_INDEX = "transaction";
117
118	const OP_NONE = 0;
119
120	const OP_CREATE = 1;
121
122	const OP_UPDATE = 2;
123
124	const OP_DELETE = 3;
125
126	const DIRTY_STATE_PERSISTENT = 0;
127
128	const DIRTY_STATE_TRANSIENT = 1;
129
130	const DIRTY_STATE_DETACHED = 2;
131
132	/**
133	 * Phalcon\Mvc\Model constructor
134	 */
135	public final function __construct(var data = null, <DiInterface> dependencyInjector = null, <ManagerInterface> modelsManager = null)
136	{
137		let this->_oldSnapshot = [];
138
139		/**
140		 * We use a default DI if the user doesn't define one
141		 */
142		if typeof dependencyInjector != "object" {
143			let dependencyInjector = Di::getDefault();
144		}
145
146		if typeof dependencyInjector != "object" {
147			throw new Exception("A dependency injector container is required to obtain the services related to the ORM");
148		}
149
150		let this->_dependencyInjector = dependencyInjector;
151
152		/**
153		 * Inject the manager service from the DI
154		 */
155		if typeof modelsManager != "object" {
156			let modelsManager = <ManagerInterface> dependencyInjector->getShared("modelsManager");
157			if typeof modelsManager != "object" {
158				throw new Exception("The injected service 'modelsManager' is not valid");
159			}
160		}
161
162		/**
163		 * Update the models-manager
164		 */
165		let this->_modelsManager = modelsManager;
166
167		/**
168		 * The manager always initializes the object
169		 */
170		modelsManager->initialize(this);
171
172		/**
173		 * This allows the developer to execute initialization stuff every time an instance is created
174		 */
175		if method_exists(this, "onConstruct") {
176			this->{"onConstruct"}(data);
177		}
178
179		if typeof data == "array" {
180			this->assign(data);
181		}
182	}
183
184	/**
185	 * Sets the dependency injection container
186	 */
187	public function setDI(<DiInterface> dependencyInjector)
188	{
189		let this->_dependencyInjector = dependencyInjector;
190	}
191
192	/**
193	 * Returns the dependency injection container
194	 */
195	public function getDI() -> <DiInterface>
196	{
197		return this->_dependencyInjector;
198	}
199
200	/**
201	 * Sets a custom events manager
202	 */
203	protected function setEventsManager(<EventsManagerInterface> eventsManager)
204	{
205		this->_modelsManager->setCustomEventsManager(this, eventsManager);
206	}
207
208	/**
209	 * Returns the custom events manager
210	 */
211	protected function getEventsManager() -> <EventsManagerInterface>
212	{
213		return this->_modelsManager->getCustomEventsManager(this);
214	}
215
216	/**
217	 * Returns the models meta-data service related to the entity instance
218	 */
219	public function getModelsMetaData() -> <MetaDataInterface>
220	{
221		var metaData, dependencyInjector;
222
223		let metaData = this->_modelsMetaData;
224		if typeof metaData != "object" {
225
226			let dependencyInjector = <DiInterface> this->_dependencyInjector;
227
228			/**
229			 * Obtain the models-metadata service from the DI
230			 */
231			let metaData = <MetaDataInterface> dependencyInjector->getShared("modelsMetadata");
232			if typeof metaData != "object" {
233				throw new Exception("The injected service 'modelsMetadata' is not valid");
234			}
235
236			/**
237			 * Update the models-metadata property
238			 */
239			let this->_modelsMetaData = metaData;
240		}
241		return metaData;
242	}
243
244	/**
245	 * Returns the models manager related to the entity instance
246	 */
247	public function getModelsManager() -> <ManagerInterface>
248	{
249		return this->_modelsManager;
250	}
251
252	/**
253	 * Sets a transaction related to the Model instance
254	 *
255	 *<code>
256	 * use Phalcon\Mvc\Model\Transaction\Manager as TxManager;
257	 * use Phalcon\Mvc\Model\Transaction\Failed as TxFailed;
258	 *
259	 * try {
260	 *     $txManager = new TxManager();
261	 *
262	 *     $transaction = $txManager->get();
263	 *
264	 *     $robot = new Robots();
265	 *
266	 *     $robot->setTransaction($transaction);
267	 *
268	 *     $robot->name       = "WALL·E";
269	 *     $robot->created_at = date("Y-m-d");
270	 *
271	 *     if ($robot->save() === false) {
272	 *         $transaction->rollback("Can't save robot");
273	 *     }
274	 *
275	 *     $robotPart = new RobotParts();
276	 *
277	 *     $robotPart->setTransaction($transaction);
278	 *
279	 *     $robotPart->type = "head";
280	 *
281	 *     if ($robotPart->save() === false) {
282	 *         $transaction->rollback("Robot part cannot be saved");
283	 *     }
284	 *
285	 *     $transaction->commit();
286	 * } catch (TxFailed $e) {
287	 *     echo "Failed, reason: ", $e->getMessage();
288	 * }
289	 *</code>
290	 */
291	public function setTransaction(<TransactionInterface> transaction) -> <ModelInterface>
292	{
293		let this->_transaction = transaction;
294		return this;
295	}
296
297	/**
298	 * Sets the table name to which model should be mapped
299	 */
300	protected function setSource(string! source) -> <Model>
301	{
302		(<ManagerInterface> this->_modelsManager)->setModelSource(this, source);
303		return this;
304	}
305
306	/**
307	 * Returns the table name mapped in the model
308	 */
309	public function getSource() -> string
310	{
311		return (<ManagerInterface> this->_modelsManager)->getModelSource(this);
312	}
313
314	/**
315	 * Sets schema name where the mapped table is located
316	 */
317	protected function setSchema(string! schema) -> <Model>
318	{
319		return (<ManagerInterface> this->_modelsManager)->setModelSchema(this, schema);
320	}
321
322	/**
323	 * Returns schema name where the mapped table is located
324	 */
325	public function getSchema() -> string
326	{
327		return (<ManagerInterface> this->_modelsManager)->getModelSchema(this);
328	}
329
330	/**
331	 * Sets the DependencyInjection connection service name
332	 */
333	public function setConnectionService(string! connectionService) -> <Model>
334	{
335		(<ManagerInterface> this->_modelsManager)->setConnectionService(this, connectionService);
336		return this;
337	}
338
339	/**
340	 * Sets the DependencyInjection connection service name used to read data
341	 */
342	public function setReadConnectionService(string! connectionService) -> <Model>
343	{
344		(<ManagerInterface> this->_modelsManager)->setReadConnectionService(this, connectionService);
345		return this;
346	}
347
348	/**
349	 * Sets the DependencyInjection connection service name used to write data
350	 */
351	public function setWriteConnectionService(string! connectionService) -> <Model>
352	{
353		return (<ManagerInterface> this->_modelsManager)->setWriteConnectionService(this, connectionService);
354	}
355
356	/**
357	 * Returns the DependencyInjection connection service name used to read data related the model
358	 */
359	public function getReadConnectionService() -> string
360	{
361		return (<ManagerInterface> this->_modelsManager)->getReadConnectionService(this);
362	}
363
364	/**
365	 * Returns the DependencyInjection connection service name used to write data related to the model
366	 */
367	public function getWriteConnectionService() -> string
368	{
369		return (<ManagerInterface> this->_modelsManager)->getWriteConnectionService(this);
370	}
371
372	/**
373	 * Sets the dirty state of the object using one of the DIRTY_STATE_* constants
374	 */
375	public function setDirtyState(int dirtyState) -> <ModelInterface>
376	{
377		let this->_dirtyState = dirtyState;
378		return this;
379	}
380
381	/**
382	 * Returns one of the DIRTY_STATE_* constants telling if the record exists in the database or not
383	 */
384	public function getDirtyState() -> int
385	{
386		return this->_dirtyState;
387	}
388
389	/**
390	 * Gets the connection used to read data for the model
391	 */
392	public function getReadConnection() -> <AdapterInterface>
393	{
394		var transaction;
395
396		let transaction = <TransactionInterface> this->_transaction;
397		if typeof transaction == "object" {
398			return transaction->getConnection();
399		}
400
401		return (<ManagerInterface> this->_modelsManager)->getReadConnection(this);
402	}
403
404	/**
405	 * Gets the connection used to write data to the model
406	 */
407	public function getWriteConnection() -> <AdapterInterface>
408	{
409		var transaction;
410
411		let transaction = <TransactionInterface> this->_transaction;
412		if typeof transaction == "object" {
413			return transaction->getConnection();
414		}
415
416		return (<ManagerInterface> this->_modelsManager)->getWriteConnection(this);
417	}
418
419	/**
420	 * Assigns values to a model from an array
421	 *
422	 * <code>
423	 * $robot->assign(
424	 *     [
425	 *         "type" => "mechanical",
426	 *         "name" => "Astro Boy",
427	 *         "year" => 1952,
428	 *     ]
429	 * );
430	 *
431	 * // Assign by db row, column map needed
432	 * $robot->assign(
433	 *     $dbRow,
434	 *     [
435	 *         "db_type" => "type",
436	 *         "db_name" => "name",
437	 *         "db_year" => "year",
438	 *     ]
439	 * );
440	 *
441	 * // Allow assign only name and year
442	 * $robot->assign(
443	 *     $_POST,
444	 *     null,
445	 *     [
446	 *         "name",
447	 *         "year",
448	 *     ]
449	 * );
450	 *
451	 * // By default assign method will use setters if exist, you can disable it by using ini_set to directly use properties
452	 *
453	 * ini_set("phalcon.orm.disable_assign_setters", true);
454	 *
455	 * $robot->assign(
456	 *     $_POST,
457	 *     null,
458	 *     [
459	 *         "name",
460	 *         "year",
461	 *     ]
462	 * );
463	 * </code>
464	 *
465	 * @param array data
466	 * @param array dataColumnMap array to transform keys of data to another
467	 * @param array whiteList
468	 * @return \Phalcon\Mvc\Model
469	 */
470	public function assign(array! data, var dataColumnMap = null, var whiteList = null) -> <Model>
471	{
472		var key, keyMapped, value, attribute, attributeField, metaData, columnMap, dataMapped, disableAssignSetters;
473
474		let disableAssignSetters = globals_get("orm.disable_assign_setters");
475
476		// apply column map for data, if exist
477		if typeof dataColumnMap == "array" {
478			let dataMapped = [];
479			for key, value in data {
480				if fetch keyMapped, dataColumnMap[key] {
481					let dataMapped[keyMapped] = value;
482				}
483			}
484		} else {
485			let dataMapped = data;
486		}
487
488		if count(dataMapped) == 0 {
489			return this;
490		}
491
492		let metaData = this->getModelsMetaData();
493
494		if globals_get("orm.column_renaming") {
495			let columnMap = metaData->getColumnMap(this);
496		} else {
497			let columnMap = null;
498		}
499
500		for attribute in metaData->getAttributes(this) {
501
502			// Check if we need to rename the field
503			if typeof columnMap == "array" {
504				if !fetch attributeField, columnMap[attribute] {
505					if !globals_get("orm.ignore_unknown_columns") {
506						throw new Exception("Column '" . attribute. "' doesn\'t make part of the column map");
507					} else {
508						continue;
509					}
510				}
511			} else {
512				let attributeField = attribute;
513			}
514
515			// The value in the array passed
516			// Check if we there is data for the field
517			if fetch value, dataMapped[attributeField] {
518
519				// If white-list exists check if the attribute is on that list
520				if typeof whiteList == "array" {
521					if !in_array(attributeField, whiteList) {
522						continue;
523					}
524				}
525
526				// Try to find a possible getter
527				if disableAssignSetters || !this->_possibleSetter(attributeField, value) {
528					let this->{attributeField} = value;
529				}
530			}
531		}
532
533		return this;
534	}
535
536	/**
537	 * Assigns values to a model from an array, returning a new model.
538	 *
539	 *<code>
540	 * $robot = \Phalcon\Mvc\Model::cloneResultMap(
541	 *     new Robots(),
542	 *     [
543	 *         "type" => "mechanical",
544	 *         "name" => "Astro Boy",
545	 *         "year" => 1952,
546	 *     ]
547	 * );
548	 *</code>
549	 *
550	 * @param \Phalcon\Mvc\ModelInterface|\Phalcon\Mvc\Model\Row base
551	 * @param array data
552	 * @param array columnMap
553	 * @param int dirtyState
554	 * @param boolean keepSnapshots
555	 */
556	public static function cloneResultMap(var base, array! data, var columnMap, int dirtyState = 0, boolean keepSnapshots = null) -> <Model>
557	{
558		var instance, attribute, key, value, castValue, attributeName;
559
560		let instance = clone base;
561
562		// Change the dirty state to persistent
563		instance->setDirtyState(dirtyState);
564
565		for key, value in data {
566
567			if typeof key == "string" {
568
569				// Only string keys in the data are valid
570				if typeof columnMap != "array" {
571					let instance->{key} = value;
572					continue;
573				}
574
575				// Every field must be part of the column map
576				if !fetch attribute, columnMap[key] {
577					if !globals_get("orm.ignore_unknown_columns") {
578						throw new Exception("Column '" . key . "' doesn't make part of the column map");
579					} else {
580						continue;
581					}
582				}
583
584				if typeof attribute != "array" {
585					let instance->{attribute} = value;
586					continue;
587				}
588
589				if value != "" && value !== null {
590					switch attribute[1] {
591
592						case Column::TYPE_INTEGER:
593							let castValue = intval(value, 10);
594							break;
595
596						case Column::TYPE_DOUBLE:
597						case Column::TYPE_DECIMAL:
598						case Column::TYPE_FLOAT:
599							let castValue = doubleval(value);
600							break;
601
602						case Column::TYPE_BOOLEAN:
603							let castValue = (boolean) value;
604							break;
605
606						default:
607							let castValue = value;
608							break;
609					}
610				} else {
611					switch attribute[1] {
612
613						case Column::TYPE_INTEGER:
614						case Column::TYPE_DOUBLE:
615						case Column::TYPE_DECIMAL:
616						case Column::TYPE_FLOAT:
617						case Column::TYPE_BOOLEAN:
618							let castValue = null;
619							break;
620
621						default:
622							let castValue = value;
623							break;
624					}
625				}
626
627				let attributeName = attribute[0],
628					instance->{attributeName} = castValue;
629			}
630		}
631
632		/**
633		 * Models that keep snapshots store the original data in t
634		 */
635		if keepSnapshots {
636			instance->setSnapshotData(data, columnMap);
637			instance->setOldSnapshotData(data, columnMap);
638		}
639
640		/**
641		 * Call afterFetch, this allows the developer to execute actions after a record is fetched from the database
642		 */
643		if method_exists(instance, "fireEvent") {
644			instance->{"fireEvent"}("afterFetch");
645		}
646
647		return instance;
648	}
649
650	/**
651	 * Returns an hydrated result based on the data and the column map
652	 *
653	 * @param array data
654	 * @param array columnMap
655	 * @param int hydrationMode
656	 * @return mixed
657	 */
658	public static function cloneResultMapHydrate(array! data, var columnMap, int hydrationMode)
659	{
660		var hydrateArray, hydrateObject, key, value, attribute, attributeName;
661
662		/**
663		 * If there is no column map and the hydration mode is arrays return the data as it is
664		 */
665		if typeof columnMap != "array" {
666			if hydrationMode == Resultset::HYDRATE_ARRAYS {
667				return data;
668			}
669		}
670
671		/**
672		 * Create the destination object according to the hydration mode
673		 */
674		if hydrationMode == Resultset::HYDRATE_ARRAYS {
675			let hydrateArray = [];
676		} else {
677			let hydrateObject = new \stdclass();
678		}
679
680		for key, value in data {
681			if typeof key != "string" {
682				continue;
683			}
684
685			if typeof columnMap == "array" {
686
687				/**
688				 * Every field must be part of the column map
689				 */
690				if !fetch attribute, columnMap[key] {
691					if !globals_get("orm.ignore_unknown_columns") {
692						throw new Exception("Column '" . key . "' doesn't make part of the column map");
693					} else {
694						continue;
695					}
696				}
697
698				/**
699				 * Attribute can store info about his type
700				 */
701				if (typeof attribute == "array") {
702					let attributeName = attribute[0];
703				} else {
704					let attributeName = attribute;
705				}
706
707				if hydrationMode == Resultset::HYDRATE_ARRAYS {
708					let hydrateArray[attributeName] = value;
709				} else {
710					let hydrateObject->{attributeName} = value;
711				}
712			} else {
713				if hydrationMode == Resultset::HYDRATE_ARRAYS {
714					let hydrateArray[key] = value;
715				} else {
716					let hydrateObject->{key} = value;
717				}
718			}
719		}
720
721		if hydrationMode == Resultset::HYDRATE_ARRAYS {
722			return hydrateArray;
723		}
724
725		return hydrateObject;
726	}
727
728	/**
729	 * Assigns values to a model from an array returning a new model
730	 *
731	 *<code>
732	 * $robot = Phalcon\Mvc\Model::cloneResult(
733	 *     new Robots(),
734	 *     [
735	 *         "type" => "mechanical",
736	 *         "name" => "Astro Boy",
737	 *         "year" => 1952,
738	 *     ]
739	 * );
740	 *</code>
741	 *
742	 * @param \Phalcon\Mvc\ModelInterface $base
743	 * @param array data
744	 * @param int dirtyState
745	 * @return \Phalcon\Mvc\ModelInterface
746	 */
747	public static function cloneResult(<ModelInterface> base, array! data, int dirtyState = 0)
748	{
749		var instance, key, value;
750
751		/**
752		 * Clone the base record
753		 */
754		let instance = clone base;
755
756		/**
757		 * Mark the object as persistent
758		 */
759		instance->setDirtyState(dirtyState);
760
761		for key, value in data {
762			if typeof key != "string" {
763				throw new Exception("Invalid key in array data provided to dumpResult()");
764			}
765			let instance->{key} = value;
766		}
767
768		/**
769		 * Call afterFetch, this allows the developer to execute actions after a record is fetched from the database
770		 */
771		(<ModelInterface> instance)->fireEvent("afterFetch");
772
773		return instance;
774	}
775
776	/**
777	 * Query for a set of records that match the specified conditions
778	 *
779	 * <code>
780	 * // How many robots are there?
781	 * $robots = Robots::find();
782	 *
783	 * echo "There are ", count($robots), "\n";
784	 *
785	 * // How many mechanical robots are there?
786	 * $robots = Robots::find(
787	 *     "type = 'mechanical'"
788	 * );
789	 *
790	 * echo "There are ", count($robots), "\n";
791	 *
792	 * // Get and print virtual robots ordered by name
793	 * $robots = Robots::find(
794	 *     [
795	 *         "type = 'virtual'",
796	 *         "order" => "name",
797	 *     ]
798	 * );
799	 *
800	 * foreach ($robots as $robot) {
801	 *	 echo $robot->name, "\n";
802	 * }
803	 *
804	 * // Get first 100 virtual robots ordered by name
805	 * $robots = Robots::find(
806	 *     [
807	 *         "type = 'virtual'",
808	 *         "order" => "name",
809	 *         "limit" => 100,
810	 *     ]
811	 * );
812	 *
813	 * foreach ($robots as $robot) {
814	 *	 echo $robot->name, "\n";
815	 * }
816	 *
817	 * // encapsulate find it into an running transaction esp. useful for application unit-tests
818	 * // or complex business logic where we wanna control which transactions are used.
819	 *
820	 * $myTransaction = new Transaction(\Phalcon\Di::getDefault());
821	 * $myTransaction->begin();
822	 * $newRobot = new Robot();
823	 * $newRobot->setTransaction($myTransaction);
824	 * $newRobot->save(['name' => 'test', 'type' => 'mechanical', 'year' => 1944]);
825	 *
826	 * $resultInsideTransaction = Robot::find(['name' => 'test', Model::TRANSACTION_INDEX => $myTransaction]);
827	 * $resultOutsideTransaction = Robot::find(['name' => 'test']);
828	 *
829	 * foreach ($setInsideTransaction as $robot) {
830	 *     echo $robot->name, "\n";
831	 * }
832	 *
833	 * foreach ($setOutsideTransaction as $robot) {
834	 *     echo $robot->name, "\n";
835	 * }
836	 *
837	 * // reverts all not commited changes
838	 * $myTransaction->rollback();
839	 *
840	 * // creating two different transactions
841	 * $myTransaction1 = new Transaction(\Phalcon\Di::getDefault());
842	 * $myTransaction1->begin();
843	 * $myTransaction2 = new Transaction(\Phalcon\Di::getDefault());
844	 * $myTransaction2->begin();
845	 *
846	 *  // add a new robots
847	 * $firstNewRobot = new Robot();
848	 * $firstNewRobot->setTransaction($myTransaction1);
849	 * $firstNewRobot->save(['name' => 'first-transaction-robot', 'type' => 'mechanical', 'year' => 1944]);
850	 *
851	 * $secondNewRobot = new Robot();
852	 * $secondNewRobot->setTransaction($myTransaction2);
853	 * $secondNewRobot->save(['name' => 'second-transaction-robot', 'type' => 'fictional', 'year' => 1984]);
854	 *
855	 * // this transaction will find the robot.
856	 * $resultInFirstTransaction = Robot::find(['name' => 'first-transaction-robot', Model::TRANSACTION_INDEX => $myTransaction1]);
857	 * // this transaction won't find the robot.
858	 * $resultInSecondTransaction = Robot::find(['name' => 'first-transaction-robot', Model::TRANSACTION_INDEX => $myTransaction2]);
859	 * // this transaction won't find the robot.
860	 * $resultOutsideAnyExplicitTransaction = Robot::find(['name' => 'first-transaction-robot']);
861	 *
862	 * // this transaction won't find the robot.
863	 * $resultInFirstTransaction = Robot::find(['name' => 'second-transaction-robot', Model::TRANSACTION_INDEX => $myTransaction2]);
864	 * // this transaction will find the robot.
865	 * $resultInSecondTransaction = Robot::find(['name' => 'second-transaction-robot', Model::TRANSACTION_INDEX => $myTransaction1]);
866	 * // this transaction won't find the robot.
867	 * $resultOutsideAnyExplicitTransaction = Robot::find(['name' => 'second-transaction-robot']);
868	 *
869	 * $transaction1->rollback();
870	 * $transaction2->rollback();
871	 * </code>
872	 */
873	public static function find(var parameters = null) -> <ResultsetInterface>
874	{
875		var params, query, resultset, hydration;
876
877		if typeof parameters != "array" {
878			let params = [];
879			if parameters !== null {
880				let params[] = parameters;
881			}
882		} else {
883			let params = parameters;
884		}
885
886		let query = static::getPreparedQuery(params);
887
888		/**
889		 * Execute the query passing the bind-params and casting-types
890		 */
891		let resultset = query->execute();
892
893		/**
894		 * Define an hydration mode
895		 */
896		if typeof resultset == "object" {
897			if fetch hydration, params["hydration"] {
898				resultset->setHydrateMode(hydration);
899			}
900		}
901
902		return resultset;
903	}
904
905	/**
906	 * Query the first record that matches the specified conditions
907	 *
908	 * <code>
909	 * // What's the first robot in robots table?
910	 * $robot = Robots::findFirst();
911	 *
912	 * echo "The robot name is ", $robot->name;
913	 *
914	 * // What's the first mechanical robot in robots table?
915	 * $robot = Robots::findFirst(
916	 *	 "type = 'mechanical'"
917	 * );
918	 *
919	 * echo "The first mechanical robot name is ", $robot->name;
920	 *
921	 * // Get first virtual robot ordered by name
922	 * $robot = Robots::findFirst(
923	 *     [
924	 *         "type = 'virtual'",
925	 *         "order" => "name",
926	 *     ]
927	 * );
928	 *
929	 * echo "The first virtual robot name is ", $robot->name;
930	 *
931	 * // behaviour with transaction
932	 * $myTransaction = new Transaction(\Phalcon\Di::getDefault());
933	 * $myTransaction->begin();
934	 * $newRobot = new Robot();
935	 * $newRobot->setTransaction($myTransaction);
936	 * $newRobot->save(['name' => 'test', 'type' => 'mechanical', 'year' => 1944]);
937	 *
938	 * $findsARobot = Robot::findFirst(['name' => 'test', Model::TRANSACTION_INDEX => $myTransaction]);
939	 * $doesNotFindARobot = Robot::findFirst(['name' => 'test']);
940	 *
941	 * var_dump($findARobot);
942	 * var_dump($doesNotFindARobot);
943	 *
944	 * $transaction->commit();
945	 * $doesFindTheRobotNow = Robot::findFirst(['name' => 'test']);
946	 * </code>
947	 */
948	public static function findFirst(var parameters = null) -> <Model>
949	{
950		var params, query;
951
952		if typeof parameters != "array" {
953			let params = [];
954			if parameters !== null {
955				let params[] = parameters;
956			}
957		} else {
958			let params = parameters;
959		}
960
961		let query = static::getPreparedQuery(params, 1);
962
963		/**
964		 * Return only the first row
965		 */
966		query->setUniqueRow(true);
967
968		/**
969		 * Execute the query passing the bind-params and casting-types
970		 */
971		return query->execute();
972	}
973
974
975	/**
976	 * shared prepare query logic for find and findFirst method
977	 */
978	private static function getPreparedQuery(var params, var limit = null) -> <Query> {
979		var builder, bindParams, bindTypes, transaction, cache, manager, query, dependencyInjector;
980
981		let dependencyInjector = Di::getDefault();
982		let manager = <ManagerInterface> dependencyInjector->getShared("modelsManager");
983
984		/**
985		 * Builds a query with the passed parameters
986		 */
987		let builder = manager->createBuilder(params);
988		builder->from(get_called_class());
989
990		if limit != null {
991			builder->limit(limit);
992		}
993
994		let query = builder->getQuery();
995
996		/**
997		 * Check for bind parameters
998		 */
999		if fetch bindParams, params["bind"] {
1000			if typeof bindParams == "array" {
1001				query->setBindParams(bindParams, true);
1002			}
1003
1004			if fetch bindTypes, params["bindTypes"] {
1005				if typeof bindTypes == "array" {
1006					query->setBindTypes(bindTypes, true);
1007				}
1008			}
1009		}
1010
1011		if fetch transaction, params[self::TRANSACTION_INDEX] {
1012			if transaction instanceof TransactionInterface {
1013				query->setTransaction(transaction);
1014			}
1015		}
1016
1017		/**
1018		 * Pass the cache options to the query
1019		 */
1020		if fetch cache, params["cache"] {
1021			query->cache(cache);
1022		}
1023
1024		return query;
1025	}
1026	/**
1027	 * Create a criteria for a specific model
1028	 */
1029	public static function query(<DiInterface> dependencyInjector = null) -> <Criteria>
1030	{
1031		var criteria;
1032
1033		/**
1034		 * Use the global dependency injector if there is no one defined
1035		 */
1036		if typeof dependencyInjector != "object" {
1037			let dependencyInjector = Di::getDefault();
1038		}
1039
1040		/**
1041		 * Gets Criteria instance from DI container
1042		 */
1043		if dependencyInjector instanceof DiInterface {
1044			let criteria = <CriteriaInterface> dependencyInjector->get("Phalcon\\Mvc\\Model\\Criteria");
1045		} else {
1046			let criteria = new Criteria();
1047			criteria->setDI(dependencyInjector);
1048		}
1049
1050		criteria->setModelName(get_called_class());
1051
1052		return criteria;
1053	}
1054
1055	/**
1056	 * Checks whether the current record already exists
1057	 *
1058	 * @param \Phalcon\Mvc\Model\MetaDataInterface metaData
1059	 * @param \Phalcon\Db\AdapterInterface connection
1060	 * @param string|array table
1061	 * @return boolean
1062	 */
1063	protected function _exists(<MetaDataInterface> metaData, <AdapterInterface> connection, var table = null) -> boolean
1064	{
1065		int numberEmpty, numberPrimary;
1066		var uniqueParams, uniqueTypes, uniqueKey, columnMap, primaryKeys,
1067			wherePk, field, attributeField, value, bindDataTypes,
1068			joinWhere, num, type, schema, source;
1069
1070		let uniqueParams = null,
1071			uniqueTypes = null;
1072
1073		/**
1074		 * Builds a unique primary key condition
1075		 */
1076		let uniqueKey = this->_uniqueKey;
1077		if uniqueKey === null {
1078
1079			let primaryKeys = metaData->getPrimaryKeyAttributes(this),
1080				bindDataTypes = metaData->getBindTypes(this);
1081
1082			let numberPrimary = count(primaryKeys);
1083			if !numberPrimary {
1084				return false;
1085			}
1086
1087			/**
1088			 * Check if column renaming is globally activated
1089			 */
1090			if globals_get("orm.column_renaming") {
1091				let columnMap = metaData->getColumnMap(this);
1092			} else {
1093				let columnMap = null;
1094			}
1095
1096			let numberEmpty = 0,
1097				wherePk = [],
1098				uniqueParams = [],
1099				uniqueTypes = [];
1100
1101			/**
1102			 * We need to create a primary key based on the current data
1103			 */
1104			for field in primaryKeys {
1105
1106				if typeof columnMap == "array" {
1107					if !fetch attributeField, columnMap[field] {
1108						throw new Exception("Column '" . field . "' isn't part of the column map");
1109					}
1110				} else {
1111					let attributeField = field;
1112				}
1113
1114				/**
1115				 * If the primary key attribute is set append it to the conditions
1116				 */
1117				let value = null;
1118				if fetch value, this->{attributeField} {
1119
1120					/**
1121					 * We count how many fields are empty, if all fields are empty we don't perform an 'exist' check
1122					 */
1123					if value === null || value === "" {
1124						let numberEmpty++;
1125					}
1126					let uniqueParams[] = value;
1127
1128				} else {
1129					let uniqueParams[] = null,
1130						numberEmpty++;
1131				}
1132
1133				if !fetch type, bindDataTypes[field] {
1134					throw new Exception("Column '" . field . "' isn't part of the table columns");
1135				}
1136
1137				let uniqueTypes[] = type,
1138					wherePk[] = connection->escapeIdentifier(field) . " = ?";
1139			}
1140
1141			/**
1142			 * There are no primary key fields defined, assume the record does not exist
1143			 */
1144			if numberPrimary == numberEmpty {
1145				return false;
1146			}
1147
1148			let joinWhere = join(" AND ", wherePk);
1149
1150			/**
1151			 * The unique key is composed of 3 parts _uniqueKey, uniqueParams, uniqueTypes
1152			 */
1153			let this->_uniqueKey = joinWhere,
1154				this->_uniqueParams = uniqueParams,
1155				this->_uniqueTypes = uniqueTypes,
1156				uniqueKey = joinWhere;
1157		}
1158
1159		/**
1160		 * If we already know if the record exists we don't check it
1161		 */
1162		if !this->_dirtyState {
1163			return true;
1164		}
1165
1166		if uniqueKey === null {
1167			let uniqueKey = this->_uniqueKey;
1168		}
1169
1170		if uniqueParams === null {
1171			let uniqueParams = this->_uniqueParams;
1172		}
1173
1174		if uniqueTypes === null {
1175			let uniqueTypes = this->_uniqueTypes;
1176		}
1177
1178		let schema = this->getSchema(), source = this->getSource();
1179		if schema {
1180			let table = [schema, source];
1181		} else {
1182			let table = source;
1183		}
1184
1185		/**
1186		 * Here we use a single COUNT(*) without PHQL to make the execution faster
1187		 */
1188		let num = connection->fetchOne(
1189			"SELECT COUNT(*) \"rowcount\" FROM " . connection->escapeIdentifier(table) . " WHERE " . uniqueKey,
1190			null,
1191			uniqueParams,
1192			uniqueTypes
1193		);
1194		if num["rowcount"] {
1195			let this->_dirtyState = self::DIRTY_STATE_PERSISTENT;
1196			return true;
1197		} else {
1198			let this->_dirtyState = self::DIRTY_STATE_TRANSIENT;
1199		}
1200
1201		return false;
1202	}
1203
1204	/**
1205	 * Generate a PHQL SELECT statement for an aggregate
1206	 *
1207	 * @param string function
1208	 * @param string alias
1209	 * @param array parameters
1210	 * @return \Phalcon\Mvc\Model\ResultsetInterface
1211	 */
1212	protected static function _groupResult(string! functionName, string! alias, var parameters) -> <ResultsetInterface>
1213	{
1214		var params, distinctColumn, groupColumn, columns,
1215			bindParams, bindTypes, resultset, cache, firstRow, groupColumns,
1216			builder, query, dependencyInjector, manager;
1217
1218		let dependencyInjector = Di::getDefault();
1219		let manager = <ManagerInterface> dependencyInjector->getShared("modelsManager");
1220
1221		if typeof parameters != "array" {
1222			let params = [];
1223			if parameters !== null {
1224				let params[] = parameters;
1225			}
1226		} else {
1227			let params = parameters;
1228		}
1229
1230		if !fetch groupColumn, params["column"] {
1231			let groupColumn = "*";
1232		}
1233
1234		/**
1235		 * Builds the columns to query according to the received parameters
1236		 */
1237		if fetch distinctColumn, params["distinct"] {
1238			let columns = functionName . "(DISTINCT " . distinctColumn . ") AS " . alias;
1239		} else {
1240			if fetch groupColumns, params["group"] {
1241				let columns = groupColumns . ", " . functionName . "(" . groupColumn . ") AS " . alias;
1242			} else {
1243				let columns = functionName . "(" . groupColumn . ") AS " . alias;
1244			}
1245		}
1246
1247		/**
1248		 * Builds a query with the passed parameters
1249		 */
1250		let builder = manager->createBuilder(params);
1251		builder->columns(columns);
1252		builder->from(get_called_class());
1253
1254		let query = builder->getQuery();
1255
1256		/**
1257		 * Check for bind parameters
1258		 */
1259		let bindParams = null, bindTypes = null;
1260		if fetch bindParams, params["bind"] {
1261			fetch bindTypes, params["bindTypes"];
1262		}
1263
1264		/**
1265		 * Pass the cache options to the query
1266		 */
1267		if fetch cache, params["cache"] {
1268			query->cache(cache);
1269		}
1270
1271		/**
1272		 * Execute the query
1273		 */
1274		let resultset = query->execute(bindParams, bindTypes);
1275
1276		/**
1277		 * Return the full resultset if the query is grouped
1278		 */
1279		if isset params["group"] {
1280			return resultset;
1281		}
1282
1283		/**
1284		 * Return only the value in the first result
1285		 */
1286		let firstRow = resultset->getFirst();
1287		return firstRow->{alias};
1288	}
1289
1290	/**
1291	 * Counts how many records match the specified conditions
1292	 *
1293	 * <code>
1294	 * // How many robots are there?
1295	 * $number = Robots::count();
1296	 *
1297	 * echo "There are ", $number, "\n";
1298	 *
1299	 * // How many mechanical robots are there?
1300	 * $number = Robots::count("type = 'mechanical'");
1301	 *
1302	 * echo "There are ", $number, " mechanical robots\n";
1303	 * </code>
1304	 *
1305	 * @param array parameters
1306	 * @return mixed
1307	 */
1308	public static function count(var parameters = null)
1309	{
1310		var result;
1311
1312		let result = self::_groupResult("COUNT", "rowcount", parameters);
1313		if typeof result == "string" {
1314			return (int) result;
1315		}
1316		return result;
1317	}
1318
1319	/**
1320	 * Calculates the sum on a column for a result-set of rows that match the specified conditions
1321	 *
1322	 * <code>
1323	 * // How much are all robots?
1324	 * $sum = Robots::sum(
1325	 *     [
1326	 *         "column" => "price",
1327	 *     ]
1328	 * );
1329	 *
1330	 * echo "The total price of robots is ", $sum, "\n";
1331	 *
1332	 * // How much are mechanical robots?
1333	 * $sum = Robots::sum(
1334	 *     [
1335	 *         "type = 'mechanical'",
1336	 *         "column" => "price",
1337	 *     ]
1338	 * );
1339	 *
1340	 * echo "The total price of mechanical robots is  ", $sum, "\n";
1341	 * </code>
1342	 *
1343	 * @param array parameters
1344	 * @return mixed
1345	 */
1346	public static function sum(var parameters = null)
1347	{
1348		return self::_groupResult("SUM", "sumatory", parameters);
1349	}
1350
1351	/**
1352	 * Returns the maximum value of a column for a result-set of rows that match the specified conditions
1353	 *
1354	 * <code>
1355	 * // What is the maximum robot id?
1356	 * $id = Robots::maximum(
1357	 *     [
1358	 *         "column" => "id",
1359	 *     ]
1360	 * );
1361	 *
1362	 * echo "The maximum robot id is: ", $id, "\n";
1363	 *
1364	 * // What is the maximum id of mechanical robots?
1365	 * $sum = Robots::maximum(
1366	 *     [
1367	 *         "type = 'mechanical'",
1368	 *         "column" => "id",
1369	 *     ]
1370	 * );
1371	 *
1372	 * echo "The maximum robot id of mechanical robots is ", $id, "\n";
1373	 * </code>
1374	 *
1375	 * @param array parameters
1376	 * @return mixed
1377	 */
1378	public static function maximum(var parameters = null)
1379	{
1380		return self::_groupResult("MAX", "maximum", parameters);
1381	}
1382
1383	/**
1384	 * Returns the minimum value of a column for a result-set of rows that match the specified conditions
1385	 *
1386	 * <code>
1387	 * // What is the minimum robot id?
1388	 * $id = Robots::minimum(
1389	 *     [
1390	 *         "column" => "id",
1391	 *     ]
1392	 * );
1393	 *
1394	 * echo "The minimum robot id is: ", $id;
1395	 *
1396	 * // What is the minimum id of mechanical robots?
1397	 * $sum = Robots::minimum(
1398	 *     [
1399	 *         "type = 'mechanical'",
1400	 *         "column" => "id",
1401	 *     ]
1402	 * );
1403	 *
1404	 * echo "The minimum robot id of mechanical robots is ", $id;
1405	 * </code>
1406	 *
1407	 * @param array parameters
1408	 * @return mixed
1409	 */
1410	public static function minimum(parameters = null)
1411	{
1412		return self::_groupResult("MIN", "minimum", parameters);
1413	}
1414
1415	/**
1416	 * Returns the average value on a column for a result-set of rows matching the specified conditions
1417	 *
1418	 * <code>
1419	 * // What's the average price of robots?
1420	 * $average = Robots::average(
1421	 *     [
1422	 *         "column" => "price",
1423	 *     ]
1424	 * );
1425	 *
1426	 * echo "The average price is ", $average, "\n";
1427	 *
1428	 * // What's the average price of mechanical robots?
1429	 * $average = Robots::average(
1430	 *     [
1431	 *         "type = 'mechanical'",
1432	 *         "column" => "price",
1433	 *     ]
1434	 * );
1435	 *
1436	 * echo "The average price of mechanical robots is ", $average, "\n";
1437	 * </code>
1438	 *
1439	 * @param array parameters
1440	 * @return double
1441	 */
1442	public static function average(var parameters = null)
1443	{
1444		return self::_groupResult("AVG", "average", parameters);
1445	}
1446
1447	/**
1448	 * Fires an event, implicitly calls behaviors and listeners in the events manager are notified
1449	 */
1450	public function fireEvent(string! eventName) -> boolean
1451	{
1452		/**
1453		 * Check if there is a method with the same name of the event
1454		 */
1455		if method_exists(this, eventName) {
1456			this->{eventName}();
1457		}
1458
1459		/**
1460		 * Send a notification to the events manager
1461		 */
1462		return (<ManagerInterface> this->_modelsManager)->notifyEvent(eventName, this);
1463	}
1464
1465	/**
1466	 * Fires an event, implicitly calls behaviors and listeners in the events manager are notified
1467	 * This method stops if one of the callbacks/listeners returns boolean false
1468	 */
1469	public function fireEventCancel(string! eventName) -> boolean
1470	{
1471		/**
1472		 * Check if there is a method with the same name of the event
1473		 */
1474		if method_exists(this, eventName) {
1475			if this->{eventName}() === false {
1476				return false;
1477			}
1478		}
1479
1480		/**
1481		 * Send a notification to the events manager
1482		 */
1483		if (<ManagerInterface> this->_modelsManager)->notifyEvent(eventName, this) === false {
1484			return false;
1485		}
1486
1487		return true;
1488	}
1489
1490	/**
1491	 * Cancel the current operation
1492	 */
1493	protected function _cancelOperation()
1494	{
1495		if this->_operationMade == self::OP_DELETE {
1496			this->fireEvent("notDeleted");
1497		} else {
1498			this->fireEvent("notSaved");
1499		}
1500	}
1501
1502	/**
1503	 * Appends a customized message on the validation process
1504	 *
1505	 * <code>
1506	 * use Phalcon\Mvc\Model;
1507	 * use Phalcon\Mvc\Model\Message as Message;
1508	 *
1509	 * class Robots extends Model
1510	 * {
1511	 *     public function beforeSave()
1512	 *     {
1513	 *         if ($this->name === "Peter") {
1514	 *             $message = new Message(
1515	 *                 "Sorry, but a robot cannot be named Peter"
1516	 *             );
1517	 *
1518	 *             $this->appendMessage($message);
1519	 *         }
1520	 *     }
1521	 * }
1522	 * </code>
1523	 */
1524	public function appendMessage(<MessageInterface> message) -> <Model>
1525	{
1526		let this->_errorMessages[] = message;
1527		return this;
1528	}
1529
1530	/**
1531	 * Executes validators on every validation call
1532	 *
1533	 *<code>
1534	 * use Phalcon\Mvc\Model;
1535	 * use Phalcon\Validation;
1536	 * use Phalcon\Validation\Validator\ExclusionIn;
1537	 *
1538	 * class Subscriptors extends Model
1539	 * {
1540	 *     public function validation()
1541	 *     {
1542	 *         $validator = new Validation();
1543	 *
1544	 *         $validator->add(
1545	 *             "status",
1546	 *             new ExclusionIn(
1547	 *                 [
1548	 *                     "domain" => [
1549	 *                         "A",
1550	 *                         "I",
1551	 *                     ],
1552	 *                 ]
1553	 *             )
1554	 *         );
1555	 *
1556	 *         return $this->validate($validator);
1557	 *     }
1558	 * }
1559	 *</code>
1560	 */
1561	protected function validate(<ValidationInterface> validator) -> boolean
1562	{
1563		var messages, message;
1564
1565		let messages = validator->validate(null, this);
1566
1567		// Call the validation, if it returns not the boolean
1568		// we append the messages to the current object
1569		if typeof messages == "boolean" {
1570			return messages;
1571		}
1572
1573		for message in iterator(messages) {
1574			this->appendMessage(
1575				new Message(
1576					message->getMessage(),
1577					message->getField(),
1578					message->getType(),
1579					null,
1580					message->getCode()
1581				)
1582			);
1583		}
1584
1585		// If there is a message, it returns false otherwise true
1586		return !count(messages);
1587	}
1588
1589	/**
1590	 * Check whether validation process has generated any messages
1591	 *
1592	 *<code>
1593	 * use Phalcon\Mvc\Model;
1594	 * use Phalcon\Validation;
1595	 * use Phalcon\Validation\Validator\ExclusionIn;
1596	 *
1597	 * class Subscriptors extends Model
1598	 * {
1599	 *     public function validation()
1600	 *     {
1601	 *         $validator = new Validation();
1602	 *
1603	 *         $validator->validate(
1604	 *             "status",
1605	 *             new ExclusionIn(
1606	 *                 [
1607	 *                     "domain" => [
1608	 *                         "A",
1609	 *                         "I",
1610	 *                     ],
1611	 *                 ]
1612	 *             )
1613	 *         );
1614	 *
1615	 *         return $this->validate($validator);
1616	 *     }
1617	 * }
1618	 *</code>
1619	 */
1620	public function validationHasFailed() -> boolean
1621	{
1622		var errorMessages;
1623		let errorMessages = this->_errorMessages;
1624		if typeof errorMessages == "array" {
1625			return count(errorMessages) > 0;
1626		}
1627		return false;
1628	}
1629
1630	/**
1631	 * Returns array of validation messages
1632	 *
1633	 *<code>
1634	 * $robot = new Robots();
1635	 *
1636	 * $robot->type = "mechanical";
1637	 * $robot->name = "Astro Boy";
1638	 * $robot->year = 1952;
1639	 *
1640	 * if ($robot->save() === false) {
1641	 *     echo "Umh, We can't store robots right now ";
1642	 *
1643	 *     $messages = $robot->getMessages();
1644	 *
1645	 *     foreach ($messages as $message) {
1646	 *         echo $message;
1647	 *     }
1648	 * } else {
1649	 *     echo "Great, a new robot was saved successfully!";
1650	 * }
1651	 * </code>
1652	 */
1653	public function getMessages(var filter = null) -> <MessageInterface[]>
1654	{
1655		var filtered, message;
1656
1657		if typeof filter == "string" && !empty filter {
1658			let filtered = [];
1659			for message in this->_errorMessages {
1660				if message->getField() == filter {
1661					let filtered[] = message;
1662				}
1663			}
1664			return filtered;
1665		}
1666
1667		return this->_errorMessages;
1668	}
1669
1670	/**
1671	 * Reads "belongs to" relations and check the virtual foreign keys when inserting or updating records
1672	 * to verify that inserted/updated values are present in the related entity
1673	 */
1674	protected final function _checkForeignKeysRestrict() -> boolean
1675	{
1676		var manager, belongsTo, foreignKey, relation, conditions,
1677			position, bindParams, extraConditions, message, fields,
1678			referencedFields, field, referencedModel, value, allowNulls;
1679		int action, numberNull;
1680		boolean error, validateWithNulls;
1681
1682		/**
1683		 * Get the models manager
1684		 */
1685		let manager = <ManagerInterface> this->_modelsManager;
1686
1687		/**
1688		 * We check if some of the belongsTo relations act as virtual foreign key
1689		 */
1690		let belongsTo = manager->getBelongsTo(this);
1691
1692		let error = false;
1693		for relation in belongsTo {
1694
1695			let validateWithNulls = false;
1696			let foreignKey = relation->getForeignKey();
1697			if foreignKey === false {
1698				continue;
1699			}
1700
1701			/**
1702			 * By default action is restrict
1703			 */
1704			let action = Relation::ACTION_RESTRICT;
1705
1706			/**
1707			 * Try to find a different action in the foreign key's options
1708			 */
1709			if typeof foreignKey == "array" {
1710				if isset foreignKey["action"] {
1711					let action = (int) foreignKey["action"];
1712				}
1713			}
1714
1715			/**
1716			 * Check only if the operation is restrict
1717			 */
1718			if action != Relation::ACTION_RESTRICT {
1719				continue;
1720			}
1721
1722			/**
1723			 * Load the referenced model if needed
1724			 */
1725			let referencedModel = manager->load(relation->getReferencedModel());
1726
1727			/**
1728			 * Since relations can have multiple columns or a single one, we need to build a condition for each of these cases
1729			 */
1730			let conditions = [], bindParams = [];
1731
1732			let numberNull = 0,
1733				fields = relation->getFields(),
1734				referencedFields = relation->getReferencedFields();
1735
1736			if typeof fields == "array" {
1737				/**
1738				 * Create a compound condition
1739				 */
1740				for position, field in fields {
1741					fetch value, this->{field};
1742					let conditions[] = "[" . referencedFields[position] . "] = ?" . position,
1743						bindParams[] = value;
1744					if typeof value == "null" {
1745						let numberNull++;
1746					}
1747				}
1748
1749				let validateWithNulls = numberNull == count(fields);
1750
1751			} else {
1752
1753				fetch value, this->{fields};
1754				let conditions[] = "[" . referencedFields . "] = ?0",
1755					bindParams[] = value;
1756
1757				if typeof value == "null" {
1758					let validateWithNulls = true;
1759				}
1760			}
1761
1762			/**
1763			 * Check if the virtual foreign key has extra conditions
1764			 */
1765			if fetch extraConditions, foreignKey["conditions"] {
1766				let conditions[] = extraConditions;
1767			}
1768
1769			/**
1770			 * Check if the relation definition allows nulls
1771			 */
1772			if validateWithNulls {
1773				if fetch allowNulls, foreignKey["allowNulls"] {
1774					let validateWithNulls = (boolean) allowNulls;
1775				} else {
1776					let validateWithNulls = false;
1777				}
1778			}
1779
1780			/**
1781			 * We don't trust the actual values in the object and pass the values using bound parameters
1782			 * Let's make the checking
1783			 */
1784			if !validateWithNulls && !referencedModel->count([join(" AND ", conditions), "bind": bindParams]) {
1785
1786				/**
1787				 * Get the user message or produce a new one
1788				 */
1789				if !fetch message, foreignKey["message"] {
1790					if typeof fields == "array" {
1791						let message = "Value of fields \"" . join(", ", fields) . "\" does not exist on referenced table";
1792					} else {
1793						let message = "Value of field \"" . fields . "\" does not exist on referenced table";
1794					}
1795				}
1796
1797				/**
1798				 * Create a message
1799				 */
1800				this->appendMessage(new Message(message, fields, "ConstraintViolation"));
1801				let error = true;
1802				break;
1803			}
1804		}
1805
1806		/**
1807		 * Call 'onValidationFails' if the validation fails
1808		 */
1809		if error === true {
1810			if globals_get("orm.events") {
1811				this->fireEvent("onValidationFails");
1812				this->_cancelOperation();
1813			}
1814			return false;
1815		}
1816
1817		return true;
1818	}
1819
1820	/**
1821	 * Reads both "hasMany" and "hasOne" relations and checks the virtual foreign keys (cascade) when deleting records
1822	 */
1823	protected final function _checkForeignKeysReverseCascade() -> boolean
1824	{
1825		var manager, relations, relation, foreignKey,
1826			resultset, conditions, bindParams, referencedModel,
1827			referencedFields, fields, field, position, value,
1828			extraConditions;
1829		int action;
1830
1831		/**
1832		 * Get the models manager
1833		 */
1834		let manager = <ManagerInterface> this->_modelsManager;
1835
1836		/**
1837		 * We check if some of the hasOne/hasMany relations is a foreign key
1838		 */
1839		let relations = manager->getHasOneAndHasMany(this);
1840
1841		for relation in relations {
1842
1843			/**
1844			 * Check if the relation has a virtual foreign key
1845			 */
1846			let foreignKey = relation->getForeignKey();
1847			if foreignKey === false {
1848				continue;
1849			}
1850
1851			/**
1852			 * By default action is restrict
1853			 */
1854			let action = Relation::NO_ACTION;
1855
1856			/**
1857			 * Try to find a different action in the foreign key's options
1858			 */
1859			if typeof foreignKey == "array" {
1860				if isset foreignKey["action"] {
1861					let action = (int) foreignKey["action"];
1862				}
1863			}
1864
1865			/**
1866			 * Check only if the operation is restrict
1867			 */
1868			if action != Relation::ACTION_CASCADE {
1869				continue;
1870			}
1871
1872			/**
1873			 * Load a plain instance from the models manager
1874			 */
1875			let referencedModel = manager->load(relation->getReferencedModel());
1876
1877			let fields = relation->getFields(),
1878				referencedFields = relation->getReferencedFields();
1879
1880			/**
1881			 * Create the checking conditions. A relation can has many fields or a single one
1882			 */
1883			let conditions = [], bindParams = [];
1884
1885			if typeof fields == "array" {
1886				for position, field in fields {
1887					fetch value, this->{field};
1888					let conditions[] = "[". referencedFields[position] . "] = ?" . position,
1889						bindParams[] = value;
1890				}
1891			} else {
1892				fetch value, this->{fields};
1893				let conditions[] = "[" . referencedFields . "] = ?0",
1894					bindParams[] = value;
1895			}
1896
1897			/**
1898			 * Check if the virtual foreign key has extra conditions
1899			 */
1900			if fetch extraConditions, foreignKey["conditions"] {
1901				let conditions[] = extraConditions;
1902			}
1903
1904			/**
1905			 * We don't trust the actual values in the object and then we're passing the values using bound parameters
1906			 * Let's make the checking
1907			 */
1908			let resultset = referencedModel->find([
1909				join(" AND ", conditions),
1910				"bind": bindParams
1911			]);
1912
1913			/**
1914			 * Delete the resultset
1915			 * Stop the operation if needed
1916			 */
1917			if resultset->delete() === false {
1918				return false;
1919			}
1920		}
1921
1922		return true;
1923	}
1924
1925	/**
1926	 * Reads both "hasMany" and "hasOne" relations and checks the virtual foreign keys (restrict) when deleting records
1927	 */
1928	protected final function _checkForeignKeysReverseRestrict() -> boolean
1929	{
1930		boolean error;
1931		var manager, relations, foreignKey, relation,
1932			relationClass, referencedModel, fields, referencedFields,
1933			conditions, bindParams,position, field,
1934			value, extraConditions, message;
1935		int action;
1936
1937		/**
1938		 * Get the models manager
1939		 */
1940		let manager = <ManagerInterface> this->_modelsManager;
1941
1942		/**
1943		 * We check if some of the hasOne/hasMany relations is a foreign key
1944		 */
1945		let relations = manager->getHasOneAndHasMany(this);
1946
1947		let error = false;
1948		for relation in relations {
1949
1950			/**
1951			 * Check if the relation has a virtual foreign key
1952			 */
1953			let foreignKey = relation->getForeignKey();
1954			if foreignKey === false {
1955				continue;
1956			}
1957
1958			/**
1959			 * By default action is restrict
1960			 */
1961			let action = Relation::ACTION_RESTRICT;
1962
1963			/**
1964			 * Try to find a different action in the foreign key's options
1965			 */
1966			if typeof foreignKey == "array" {
1967				if isset foreignKey["action"] {
1968					let action = (int) foreignKey["action"];
1969				}
1970			}
1971
1972			/**
1973			 * Check only if the operation is restrict
1974			 */
1975			if action != Relation::ACTION_RESTRICT {
1976				continue;
1977			}
1978
1979			let relationClass = relation->getReferencedModel();
1980
1981			/**
1982			 * Load a plain instance from the models manager
1983			 */
1984			let referencedModel = manager->load(relationClass);
1985
1986			let fields = relation->getFields(),
1987				referencedFields = relation->getReferencedFields();
1988
1989			/**
1990			 * Create the checking conditions. A relation can has many fields or a single one
1991			 */
1992			let conditions = [], bindParams = [];
1993
1994			if typeof fields == "array" {
1995
1996				for position, field in fields {
1997					fetch value, this->{field};
1998					let conditions[] = "[" . referencedFields[position] . "] = ?" . position,
1999						bindParams[] = value;
2000				}
2001
2002			} else {
2003				fetch value, this->{fields};
2004				let conditions[] = "[" . referencedFields . "] = ?0",
2005					bindParams[] = value;
2006			}
2007
2008			/**
2009			 * Check if the virtual foreign key has extra conditions
2010			 */
2011			if fetch extraConditions, foreignKey["conditions"] {
2012				let conditions[] = extraConditions;
2013			}
2014
2015			/**
2016			 * We don't trust the actual values in the object and then we're passing the values using bound parameters
2017			 * Let's make the checking
2018			 */
2019			if referencedModel->count([join(" AND ", conditions), "bind": bindParams]) {
2020
2021				/**
2022				 * Create a new message
2023				 */
2024				if !fetch message, foreignKey["message"] {
2025					let message = "Record is referenced by model " . relationClass;
2026				}
2027
2028				/**
2029				 * Create a message
2030				 */
2031				this->appendMessage(new Message(message, fields, "ConstraintViolation"));
2032				let error = true;
2033				break;
2034			}
2035		}
2036
2037		/**
2038		 * Call validation fails event
2039		 */
2040		if error === true {
2041			if globals_get("orm.events") {
2042				this->fireEvent("onValidationFails");
2043				this->_cancelOperation();
2044			}
2045			return false;
2046		}
2047
2048		return true;
2049	}
2050
2051	/**
2052	 * Executes internal hooks before save a record
2053	 */
2054	protected function _preSave(<MetaDataInterface> metaData, boolean exists, var identityField) -> boolean
2055	{
2056		var notNull, columnMap, dataTypeNumeric, automaticAttributes, defaultValues,
2057			field, attributeField, value, emptyStringValues;
2058		boolean error, isNull;
2059
2060		/**
2061		 * Run Validation Callbacks Before
2062		 */
2063		if globals_get("orm.events") {
2064
2065			/**
2066			 * Call the beforeValidation
2067			 */
2068			if this->fireEventCancel("beforeValidation") === false {
2069				return false;
2070			}
2071
2072			/**
2073			 * Call the specific beforeValidation event for the current action
2074			 */
2075			if !exists {
2076				if this->fireEventCancel("beforeValidationOnCreate") === false {
2077					return false;
2078				}
2079			} else {
2080				if this->fireEventCancel("beforeValidationOnUpdate") === false {
2081					return false;
2082				}
2083			}
2084		}
2085
2086		/**
2087		 * Check for Virtual foreign keys
2088		 */
2089		if globals_get("orm.virtual_foreign_keys") {
2090			if this->_checkForeignKeysRestrict() === false {
2091				return false;
2092			}
2093		}
2094
2095		/**
2096		 * Columns marked as not null are automatically validated by the ORM
2097		 */
2098		if globals_get("orm.not_null_validations") {
2099
2100			let notNull = metaData->getNotNullAttributes(this);
2101			if typeof notNull == "array" {
2102
2103				/**
2104				 * Gets the fields that are numeric, these are validated in a different way
2105				 */
2106				let dataTypeNumeric = metaData->getDataTypesNumeric(this);
2107
2108				if globals_get("orm.column_renaming") {
2109					let columnMap = metaData->getColumnMap(this);
2110				} else {
2111					let columnMap = null;
2112				}
2113
2114				/**
2115				 * Get fields that must be omitted from the SQL generation
2116				 */
2117				if exists {
2118					let automaticAttributes = metaData->getAutomaticUpdateAttributes(this);
2119				} else {
2120					let automaticAttributes = metaData->getAutomaticCreateAttributes(this);
2121				}
2122
2123				let defaultValues = metaData->getDefaultValues(this);
2124
2125				/**
2126				 * Get string attributes that allow empty strings as defaults
2127				 */
2128				let emptyStringValues = metaData->getEmptyStringAttributes(this);
2129
2130				let error = false;
2131				for field in notNull {
2132
2133					/**
2134					 * We don't check fields that must be omitted
2135					 */
2136					if !isset automaticAttributes[field] {
2137
2138						let isNull = false;
2139
2140						if typeof columnMap == "array" {
2141							if !fetch attributeField, columnMap[field] {
2142								throw new Exception("Column '" . field . "' isn't part of the column map");
2143							}
2144						} else {
2145							let attributeField = field;
2146						}
2147
2148						/**
2149						 * Field is null when: 1) is not set, 2) is numeric but
2150						 * its value is not numeric, 3) is null or 4) is empty string
2151						 * Read the attribute from the this_ptr using the real or renamed name
2152						 */
2153						if fetch value, this->{attributeField} {
2154
2155							/**
2156							 * Objects are never treated as null, numeric fields must be numeric to be accepted as not null
2157							 */
2158							if typeof value != "object" {
2159								if !isset dataTypeNumeric[field] {
2160									if isset emptyStringValues[field] {
2161										if value === null {
2162											let isNull = true;
2163										}
2164									} else {
2165										if value === null || (value === "" && (!isset defaultValues[field] || value !== defaultValues[field])) {
2166											let isNull = true;
2167										}
2168									}
2169								} else {
2170									if !is_numeric(value) {
2171										let isNull = true;
2172									}
2173								}
2174							}
2175
2176						} else {
2177							let isNull = true;
2178						}
2179
2180						if isNull === true {
2181
2182							if !exists {
2183								/**
2184								 * The identity field can be null
2185								 */
2186								if field == identityField {
2187									continue;
2188								}
2189
2190								/**
2191								 * The field have default value can be null
2192								 */
2193								if isset defaultValues[field] {
2194									continue;
2195								}
2196							}
2197
2198							/**
2199							 * An implicit PresenceOf message is created
2200							 */
2201							let this->_errorMessages[] = new Message(attributeField . " is required", attributeField, "PresenceOf"),
2202								error = true;
2203						}
2204					}
2205				}
2206
2207				if error === true {
2208					if globals_get("orm.events") {
2209						this->fireEvent("onValidationFails");
2210						this->_cancelOperation();
2211					}
2212					return false;
2213				}
2214			}
2215		}
2216
2217		/**
2218		 * Call the main validation event
2219		 */
2220		if this->fireEventCancel("validation") === false {
2221			if globals_get("orm.events") {
2222				this->fireEvent("onValidationFails");
2223			}
2224			return false;
2225		}
2226
2227		/**
2228		 * Run Validation
2229		 */
2230		if globals_get("orm.events") {
2231
2232			/**
2233			 * Run Validation Callbacks After
2234			 */
2235			if !exists {
2236				if this->fireEventCancel("afterValidationOnCreate") === false {
2237					return false;
2238				}
2239			} else {
2240				if this->fireEventCancel("afterValidationOnUpdate") === false {
2241					return false;
2242				}
2243			}
2244
2245			if this->fireEventCancel("afterValidation") === false {
2246				return false;
2247			}
2248
2249			/**
2250			 * Run Before Callbacks
2251			 */
2252			if this->fireEventCancel("beforeSave") === false {
2253				return false;
2254			}
2255
2256			let this->_skipped = false;
2257
2258			/**
2259			 * The operation can be skipped here
2260			 */
2261			if exists {
2262				if this->fireEventCancel("beforeUpdate") === false {
2263					return false;
2264				}
2265			} else {
2266				if this->fireEventCancel("beforeCreate") === false {
2267					return false;
2268				}
2269			}
2270
2271			/**
2272			 * Always return true if the operation is skipped
2273			 */
2274			if this->_skipped === true {
2275				return true;
2276			}
2277
2278		}
2279
2280		return true;
2281	}
2282
2283	/**
2284	 * Executes internal events after save a record
2285	 */
2286	protected function _postSave(boolean success, boolean exists) -> boolean
2287	{
2288		if success === true {
2289			if exists {
2290				this->fireEvent("afterUpdate");
2291			} else {
2292				this->fireEvent("afterCreate");
2293			}
2294		}
2295
2296		return success;
2297	}
2298
2299	/**
2300	 * Sends a pre-build INSERT SQL statement to the relational database system
2301	 *
2302	 * @param \Phalcon\Mvc\Model\MetaDataInterface metaData
2303	 * @param \Phalcon\Db\AdapterInterface connection
2304	 * @param string|array table
2305	 * @param boolean|string identityField
2306	 * @return boolean
2307	 */
2308	protected function _doLowInsert(<MetaDataInterface> metaData, <AdapterInterface> connection,
2309		table, identityField) -> boolean
2310	{
2311		var bindSkip, fields, values, bindTypes, attributes, bindDataTypes, automaticAttributes,
2312			field, columnMap, value, attributeField, success, bindType,
2313			defaultValue, sequenceName, defaultValues, source, schema, snapshot, lastInsertedId, manager;
2314		boolean useExplicitIdentity;
2315
2316		let bindSkip = Column::BIND_SKIP;
2317		let manager = <ManagerInterface> this->_modelsManager;
2318
2319		let fields = [],
2320			values = [],
2321			snapshot = [],
2322			bindTypes = [];
2323
2324		let attributes = metaData->getAttributes(this),
2325			bindDataTypes = metaData->getBindTypes(this),
2326			automaticAttributes = metaData->getAutomaticCreateAttributes(this),
2327			defaultValues = metaData->getDefaultValues(this);
2328
2329		if globals_get("orm.column_renaming") {
2330			let columnMap = metaData->getColumnMap(this);
2331		} else {
2332			let columnMap = null;
2333		}
2334
2335		/**
2336		 * All fields in the model makes part or the INSERT
2337		 */
2338		for field in attributes {
2339
2340			if !isset automaticAttributes[field] {
2341
2342				/**
2343				 * Check if the model has a column map
2344				 */
2345				if typeof columnMap == "array" {
2346					if !fetch attributeField, columnMap[field] {
2347						throw new Exception("Column '" . field . "' isn't part of the column map");
2348					}
2349				} else {
2350					let attributeField = field;
2351				}
2352
2353				/**
2354				 * Check every attribute in the model except identity field
2355				 */
2356				if field != identityField {
2357
2358					/**
2359					 * This isset checks that the property be defined in the model
2360					 */
2361					if fetch value, this->{attributeField} {
2362
2363						if value === null && isset defaultValues[field] {
2364							let snapshot[attributeField] = null;
2365							let value = connection->getDefaultValue();
2366						} else {
2367							let snapshot[attributeField] = value;
2368						}
2369
2370						/**
2371						 * Every column must have a bind data type defined
2372						 */
2373						if !fetch bindType, bindDataTypes[field] {
2374							throw new Exception("Column '" . field . "' have not defined a bind data type");
2375						}
2376
2377						let fields[] = field, values[] = value, bindTypes[] = bindType;
2378					} else {
2379
2380						if isset defaultValues[field] {
2381							let values[] = connection->getDefaultValue();
2382							/**
2383							 * This is default value so we set null, keep in mind it's value in database!
2384							 */
2385							let snapshot[attributeField] = null;
2386						} else {
2387							let values[] = value;
2388							let snapshot[attributeField] = value;
2389						}
2390
2391						let fields[] = field, bindTypes[] = bindSkip;
2392					}
2393				}
2394			}
2395		}
2396
2397		/**
2398		 * If there is an identity field we add it using "null" or "default"
2399		 */
2400		if identityField !== false {
2401
2402			let defaultValue = connection->getDefaultIdValue();
2403
2404			/**
2405			 * Not all the database systems require an explicit value for identity columns
2406			 */
2407			let useExplicitIdentity = (boolean) connection->useExplicitIdValue();
2408			if useExplicitIdentity {
2409				let fields[] = identityField;
2410			}
2411
2412			/**
2413			 * Check if the model has a column map
2414			 */
2415			if typeof columnMap == "array" {
2416				if !fetch attributeField, columnMap[identityField] {
2417					throw new Exception("Identity column '" . identityField . "' isn't part of the column map");
2418				}
2419			} else {
2420				let attributeField = identityField;
2421			}
2422
2423			/**
2424			 * Check if the developer set an explicit value for the column
2425			 */
2426			if fetch value, this->{attributeField} {
2427
2428				if value === null || value === "" {
2429					if useExplicitIdentity {
2430						let values[] = defaultValue, bindTypes[] = bindSkip;
2431					}
2432				} else {
2433
2434					/**
2435					 * Add the explicit value to the field list if the user has defined a value for it
2436					 */
2437					if !useExplicitIdentity {
2438						let fields[] = identityField;
2439					}
2440
2441					/**
2442					 * The field is valid we look for a bind value (normally int)
2443					 */
2444					if !fetch bindType, bindDataTypes[identityField] {
2445						throw new Exception("Identity column '" . identityField . "' isn\'t part of the table columns");
2446					}
2447
2448					let values[] = value, bindTypes[] = bindType;
2449				}
2450			} else {
2451				if useExplicitIdentity {
2452					let values[] = defaultValue, bindTypes[] = bindSkip;
2453				}
2454			}
2455		}
2456
2457		/**
2458		 * The low level insert is performed
2459		 */
2460		let success = connection->insert(table, values, fields, bindTypes);
2461		if success && identityField !== false {
2462
2463			/**
2464			 * We check if the model have sequences
2465			 */
2466			let sequenceName = null;
2467			if connection->supportSequences() === true {
2468				if method_exists(this, "getSequenceName") {
2469					let sequenceName = this->{"getSequenceName"}();
2470				} else {
2471
2472					let source = this->getSource(),
2473						schema = this->getSchema();
2474
2475					if empty schema {
2476						let sequenceName = source . "_" . identityField . "_seq";
2477					} else {
2478						let sequenceName = schema . "." . source . "_" . identityField . "_seq";
2479					}
2480				}
2481			}
2482
2483			/**
2484			 * Recover the last "insert id" and assign it to the object
2485			 */
2486			let lastInsertedId = connection->lastInsertId(sequenceName);
2487
2488			let this->{attributeField} = lastInsertedId;
2489			let snapshot[attributeField] = lastInsertedId;
2490
2491			/**
2492			 * Since the primary key was modified, we delete the _uniqueParams
2493			 * to force any future update to re-build the primary key
2494			 */
2495			let this->_uniqueParams = null;
2496		}
2497
2498		if success && manager->isKeepingSnapshots(this) && globals_get("orm.update_snapshot_on_save") {
2499			let this->_snapshot = snapshot;
2500		}
2501
2502
2503		return success;
2504	}
2505
2506	/**
2507	 * Sends a pre-build UPDATE SQL statement to the relational database system
2508	 *
2509	 * @param \Phalcon\Mvc\Model\MetaDataInterface metaData
2510	 * @param \Phalcon\Db\AdapterInterface connection
2511	 * @param string|array table
2512	 * @return boolean
2513	 */
2514	 protected function _doLowUpdate(<MetaDataInterface> metaData, <AdapterInterface> connection, var table) -> boolean
2515 	{
2516 		var bindSkip, fields, values, dataType, dataTypes, bindTypes, manager, bindDataTypes, field,
2517 			automaticAttributes, snapshotValue, uniqueKey, uniqueParams, uniqueTypes,
2518 			snapshot, nonPrimary, columnMap, attributeField, value, primaryKeys, bindType, newSnapshot, success;
2519 		boolean useDynamicUpdate, changed;
2520
2521 		let bindSkip = Column::BIND_SKIP,
2522 			fields = [],
2523 			values = [],
2524 			bindTypes = [],
2525 			newSnapshot = [],
2526 			manager = <ManagerInterface> this->_modelsManager;
2527
2528 		/**
2529 		 * Check if the model must use dynamic update
2530 		 */
2531 		let useDynamicUpdate = (boolean) manager->isUsingDynamicUpdate(this);
2532
2533		let snapshot = this->_snapshot;
2534
2535 		if useDynamicUpdate {
2536 			if typeof snapshot != "array" {
2537 				let useDynamicUpdate = false;
2538 			}
2539 		}
2540
2541 		let dataTypes = metaData->getDataTypes(this),
2542			 bindDataTypes = metaData->getBindTypes(this),
2543 			nonPrimary = metaData->getNonPrimaryKeyAttributes(this),
2544 			automaticAttributes = metaData->getAutomaticUpdateAttributes(this);
2545
2546 		if globals_get("orm.column_renaming") {
2547 			let columnMap = metaData->getColumnMap(this);
2548 		} else {
2549 			let columnMap = null;
2550 		}
2551
2552 		/**
2553 		 * We only make the update based on the non-primary attributes, values in primary key attributes are ignored
2554 		 */
2555 		for field in nonPrimary {
2556
2557 			if !isset automaticAttributes[field] {
2558
2559 				/**
2560 				 * Check a bind type for field to update
2561 				 */
2562 				if !fetch bindType, bindDataTypes[field] {
2563 					throw new Exception("Column '" . field . "' have not defined a bind data type");
2564 				}
2565
2566 				/**
2567 				 * Check if the model has a column map
2568 				 */
2569 				if typeof columnMap == "array" {
2570 					if !fetch attributeField, columnMap[field] {
2571 						throw new Exception("Column '" . field . "' isn't part of the column map");
2572 					}
2573 				} else {
2574 					let attributeField = field;
2575 				}
2576
2577 				/**
2578 				 * Get the field's value
2579 				 * If a field isn't set we pass a null value
2580 				 */
2581 				if fetch value, this->{attributeField} {
2582
2583 					/**
2584 					 * When dynamic update is not used we pass every field to the update
2585 					 */
2586 					if !useDynamicUpdate {
2587 						let fields[] = field, values[] = value;
2588 						let bindTypes[] = bindType;
2589 					} else {
2590
2591 						/**
2592 						 * If the field is not part of the snapshot we add them as changed
2593 						 */
2594 						if !fetch snapshotValue, snapshot[attributeField] {
2595 							let changed = true;
2596 						} else {
2597 							/**
2598 							 * See https://github.com/phalcon/cphalcon/issues/3247
2599 							 * Take a TEXT column with value '4' and replace it by
2600 							 * the value '4.0'. For PHP '4' and '4.0' are the same.
2601 							 * We can't use simple comparison...
2602 							 *
2603 							 * We must use the type of snapshotValue.
2604 							 */
2605 							if value === null {
2606 								let changed = snapshotValue !== null;
2607 							} else {
2608 								if snapshotValue === null {
2609 									let changed = true;
2610 								} else {
2611
2612									 if !fetch dataType, dataTypes[field] {
2613										 throw new Exception("Column '" . field . "' have not defined a data type");
2614									 }
2615
2616 									switch dataType {
2617
2618 										case Column::TYPE_BOOLEAN:
2619 											let changed = (boolean) snapshotValue !== (boolean) value;
2620 											break;
2621
2622 										case Column::TYPE_DECIMAL:
2623 										case Column::TYPE_FLOAT:
2624 											let changed = floatval(snapshotValue) !== floatval(value);
2625 											break;
2626
2627 										case Column::TYPE_INTEGER:
2628 										case Column::TYPE_DATE:
2629 										case Column::TYPE_VARCHAR:
2630 										case Column::TYPE_DATETIME:
2631 										case Column::TYPE_CHAR:
2632 										case Column::TYPE_TEXT:
2633 										case Column::TYPE_VARCHAR:
2634 										case Column::TYPE_BIGINTEGER:
2635 											let changed = (string) snapshotValue !== (string) value;
2636 											break;
2637
2638 										/**
2639 										 * Any other type is not really supported...
2640 										 */
2641 										default:
2642 											let changed = value != snapshotValue;
2643 									}
2644 								}
2645 							}
2646 						}
2647
2648 						/**
2649 						 * Only changed values are added to the SQL Update
2650 						 */
2651 						if changed {
2652 							let fields[] = field, values[] = value;
2653 							let bindTypes[] = bindType;
2654 						}
2655 					}
2656					let newSnapshot[attributeField] = value;
2657
2658 				} else {
2659 				    let newSnapshot[attributeField] = null;
2660 					let fields[] = field, values[] = null, bindTypes[] = bindSkip;
2661 				}
2662 			}
2663 		}
2664
2665 		/**
2666 		 * If there is no fields to update we return true
2667 		 */
2668 		if !count(fields) {
2669 			if useDynamicUpdate {
2670 				let this->_oldSnapshot = snapshot;
2671 			}
2672 			return true;
2673 		}
2674
2675 		let uniqueKey = this->_uniqueKey,
2676 			uniqueParams = this->_uniqueParams,
2677 			uniqueTypes = this->_uniqueTypes;
2678
2679 		/**
2680 		 * When unique params is null we need to rebuild the bind params
2681 		 */
2682 		if typeof uniqueParams != "array" {
2683
2684 			let primaryKeys = metaData->getPrimaryKeyAttributes(this);
2685
2686 			/**
2687 			 * We can't create dynamic SQL without a primary key
2688 			 */
2689 			if !count(primaryKeys) {
2690 				throw new Exception("A primary key must be defined in the model in order to perform the operation");
2691 			}
2692
2693 			let uniqueParams = [];
2694 			for field in primaryKeys {
2695
2696 				/**
2697 				 * Check if the model has a column map
2698 				 */
2699 				if typeof columnMap == "array" {
2700 					if !fetch attributeField, columnMap[field] {
2701 						throw new Exception("Column '" . field . "' isn't part of the column map");
2702 					}
2703 				} else {
2704 					let attributeField = field;
2705 				}
2706
2707 				if fetch value, this->{attributeField} {
2708 				    let newSnapshot[attributeField] = value;
2709 					let uniqueParams[] = value;
2710 				} else {
2711 				    let newSnapshot[attributeField] = null;
2712 					let uniqueParams[] = null;
2713 				}
2714 			}
2715 		}
2716
2717 		/**
2718 		 * We build the conditions as an array
2719 		 * Perform the low level update
2720 		 */
2721 		let success = connection->update(table, fields, values, [
2722 			"conditions" : uniqueKey,
2723 			"bind"	     : uniqueParams,
2724 			"bindTypes"  : uniqueTypes
2725 		], bindTypes);
2726
2727 		if success && manager->isKeepingSnapshots(this) && globals_get("orm.update_snapshot_on_save") {
2728			if typeof snapshot == "array" {
2729				let this->_oldSnapshot = snapshot;
2730				let this->_snapshot = array_merge(snapshot, newSnapshot);
2731			} else {
2732				let this->_oldSnapshot = [];
2733				let this->_snapshot = newSnapshot;
2734			}
2735		}
2736
2737 		return success;
2738 	}
2739
2740	/**
2741	 * Saves related records that must be stored prior to save the master record
2742	 *
2743	 * @param \Phalcon\Db\AdapterInterface connection
2744	 * @param \Phalcon\Mvc\ModelInterface[] related
2745	 * @return boolean
2746	 */
2747	protected function _preSaveRelatedRecords(<AdapterInterface> connection, related) -> boolean
2748	{
2749		var className, manager, type, relation, columns, referencedFields,
2750			referencedModel, message, nesting, name, record;
2751
2752		let nesting = false;
2753
2754		/**
2755		 * Start an implicit transaction
2756		 */
2757		connection->begin(nesting);
2758
2759		let className = get_class(this),
2760			manager = <ManagerInterface> this->getModelsManager();
2761
2762		for name, record in related {
2763
2764			/**
2765			 * Try to get a relation with the same name
2766			 */
2767			let relation = <RelationInterface> manager->getRelationByAlias(className, name);
2768			if typeof relation == "object" {
2769
2770				/**
2771				 * Get the relation type
2772				 */
2773				let type = relation->getType();
2774
2775				/**
2776				 * Only belongsTo are stored before save the master record
2777				 */
2778				if type == Relation::BELONGS_TO {
2779
2780					if typeof record != "object" {
2781						connection->rollback(nesting);
2782						throw new Exception("Only objects can be stored as part of belongs-to relations");
2783					}
2784
2785					let columns = relation->getFields(),
2786						referencedModel = relation->getReferencedModel(),
2787						referencedFields = relation->getReferencedFields();
2788
2789					if typeof columns == "array" {
2790						connection->rollback(nesting);
2791						throw new Exception("Not implemented");
2792					}
2793
2794					/**
2795					 * If dynamic update is enabled, saving the record must not take any action
2796					 */
2797					if !record->save() {
2798
2799						/**
2800						 * Get the validation messages generated by the referenced model
2801						 */
2802						for message in record->getMessages() {
2803
2804							/**
2805							 * Set the related model
2806							 */
2807							if typeof message == "object" {
2808								message->setModel(record);
2809							}
2810
2811							/**
2812							 * Appends the messages to the current model
2813							 */
2814							this->appendMessage(message);
2815						}
2816
2817						/**
2818						 * Rollback the implicit transaction
2819						 */
2820						connection->rollback(nesting);
2821						return false;
2822					}
2823
2824					/**
2825					 * Read the attribute from the referenced model and assigns it to the current model
2826					 * Assign it to the model
2827					 */
2828					let this->{columns} = record->readAttribute(referencedFields);
2829				}
2830			}
2831		}
2832
2833		return true;
2834	}
2835
2836	/**
2837	 * Save the related records assigned in the has-one/has-many relations
2838	 *
2839	 * @param  Phalcon\Db\AdapterInterface connection
2840	 * @param  Phalcon\Mvc\ModelInterface[] related
2841	 * @return boolean
2842	 */
2843	protected function _postSaveRelatedRecords(<AdapterInterface> connection, related) -> boolean
2844	{
2845		var nesting, className, manager, relation, name, record, message,
2846			columns, referencedModel, referencedFields, relatedRecords, value,
2847			recordAfter, intermediateModel, intermediateFields, intermediateValue,
2848			intermediateModelName, intermediateReferencedFields;
2849		boolean isThrough;
2850
2851		let nesting = false,
2852			className = get_class(this),
2853			manager = <ManagerInterface> this->getModelsManager();
2854
2855		for name, record in related {
2856
2857			/**
2858			 * Try to get a relation with the same name
2859			 */
2860			let relation = <RelationInterface> manager->getRelationByAlias(className, name);
2861			if typeof relation == "object" {
2862
2863				/**
2864				 * Discard belongsTo relations
2865				 */
2866				if relation->getType() == Relation::BELONGS_TO {
2867					continue;
2868				}
2869
2870				if typeof record != "object" && typeof record != "array" {
2871					connection->rollback(nesting);
2872					throw new Exception("Only objects/arrays can be stored as part of has-many/has-one/has-many-to-many relations");
2873				}
2874
2875				let columns = relation->getFields(),
2876					referencedModel = relation->getReferencedModel(),
2877					referencedFields = relation->getReferencedFields();
2878
2879				if typeof columns == "array" {
2880					connection->rollback(nesting);
2881					throw new Exception("Not implemented");
2882				}
2883
2884				/**
2885				 * Create an implicit array for has-many/has-one records
2886				 */
2887				if typeof record == "object" {
2888					let relatedRecords = [record];
2889				} else {
2890					let relatedRecords = record;
2891				}
2892
2893				if !fetch value, this->{columns} {
2894					connection->rollback(nesting);
2895					throw new Exception("The column '" . columns . "' needs to be present in the model");
2896				}
2897
2898				/**
2899				 * Get the value of the field from the current model
2900				 * Check if the relation is a has-many-to-many
2901				 */
2902				let isThrough = (boolean) relation->isThrough();
2903
2904				/**
2905				 * Get the rest of intermediate model info
2906				 */
2907				if isThrough {
2908					let intermediateModelName = relation->getIntermediateModel(),
2909						intermediateFields = relation->getIntermediateFields(),
2910						intermediateReferencedFields = relation->getIntermediateReferencedFields();
2911				}
2912
2913				for recordAfter in relatedRecords {
2914
2915					/**
2916					 * For non has-many-to-many relations just assign the local value in the referenced model
2917					 */
2918					if !isThrough {
2919
2920						/**
2921						 * Assign the value to the
2922						 */
2923						recordAfter->writeAttribute(referencedFields, value);
2924					}
2925
2926					/**
2927					 * Save the record and get messages
2928					 */
2929					if !recordAfter->save() {
2930
2931						/**
2932						 * Get the validation messages generated by the referenced model
2933						 */
2934						for message in recordAfter->getMessages() {
2935
2936							/**
2937							 * Set the related model
2938							 */
2939							if typeof message == "object" {
2940								message->setModel(record);
2941							}
2942
2943							/**
2944							 * Appends the messages to the current model
2945							 */
2946							this->appendMessage(message);
2947						}
2948
2949						/**
2950						 * Rollback the implicit transaction
2951						 */
2952						connection->rollback(nesting);
2953						return false;
2954					}
2955
2956					if isThrough {
2957
2958						/**
2959						 * Create a new instance of the intermediate model
2960						 */
2961						let intermediateModel = manager->load(intermediateModelName, true);
2962
2963						/**
2964						 * Write value in the intermediate model
2965						 */
2966						intermediateModel->writeAttribute(intermediateFields, value);
2967
2968						/**
2969						 * Get the value from the referenced model
2970						 */
2971						let intermediateValue = recordAfter->readAttribute(referencedFields);
2972
2973						/**
2974						 * Write the intermediate value in the intermediate model
2975						 */
2976						intermediateModel->writeAttribute(intermediateReferencedFields, intermediateValue);
2977
2978						/**
2979						 * Save the record and get messages
2980						 */
2981						if !intermediateModel->save() {
2982
2983							/**
2984							 * Get the validation messages generated by the referenced model
2985							 */
2986							for message in intermediateModel->getMessages() {
2987
2988								/**
2989								 * Set the related model
2990								 */
2991								if typeof message == "object" {
2992									message->setModel(record);
2993								}
2994
2995								/**
2996								 * Appends the messages to the current model
2997								 */
2998								this->appendMessage(message);
2999							}
3000
3001							/**
3002							 * Rollback the implicit transaction
3003							 */
3004							connection->rollback(nesting);
3005							return false;
3006						}
3007					}
3008
3009				}
3010			} else {
3011				if typeof record != "array" {
3012					connection->rollback(nesting);
3013
3014					throw new Exception(
3015						"There are no defined relations for the model '" . className . "' using alias '" . name . "'"
3016					);
3017				}
3018			}
3019		}
3020
3021		/**
3022		 * Commit the implicit transaction
3023		 */
3024		connection->commit(nesting);
3025		return true;
3026	}
3027
3028	/**
3029	 * Inserts or updates a model instance. Returning true on success or false otherwise.
3030	 *
3031	 *<code>
3032	 * // Creating a new robot
3033	 * $robot = new Robots();
3034	 *
3035	 * $robot->type = "mechanical";
3036	 * $robot->name = "Astro Boy";
3037	 * $robot->year = 1952;
3038	 *
3039	 * $robot->save();
3040	 *
3041	 * // Updating a robot name
3042	 * $robot = Robots::findFirst("id = 100");
3043	 *
3044	 * $robot->name = "Biomass";
3045	 *
3046	 * $robot->save();
3047	 *</code>
3048	 *
3049	 * @param array data
3050	 * @param array whiteList
3051	 * @return boolean
3052	 */
3053	public function save(var data = null, var whiteList = null) -> boolean
3054	{
3055		var metaData, related, schema, writeConnection, readConnection,
3056			source, table, identityField, exists, success;
3057
3058		let metaData = this->getModelsMetaData();
3059
3060		if typeof data == "array" && count(data) > 0 {
3061			this->assign(data, null, whiteList);
3062		}
3063
3064		/**
3065		 * Create/Get the current database connection
3066		 */
3067		let writeConnection = this->getWriteConnection();
3068
3069		/**
3070		 * Fire the start event
3071		 */
3072		this->fireEvent("prepareSave");
3073
3074		/**
3075		 * Save related records in belongsTo relationships
3076		 */
3077		let related = this->_related;
3078		if typeof related == "array" {
3079			if this->_preSaveRelatedRecords(writeConnection, related) === false {
3080				return false;
3081			}
3082		}
3083
3084		let schema = this->getSchema(),
3085			source = this->getSource();
3086
3087		if schema {
3088			let table = [schema, source];
3089		} else {
3090			let table = source;
3091		}
3092
3093		/**
3094		 * Create/Get the current database connection
3095		 */
3096		let readConnection = this->getReadConnection();
3097
3098		/**
3099		 * We need to check if the record exists
3100		 */
3101		let exists = this->_exists(metaData, readConnection, table);
3102
3103		if exists {
3104			let this->_operationMade = self::OP_UPDATE;
3105		} else {
3106			let this->_operationMade = self::OP_CREATE;
3107		}
3108
3109		/**
3110		 * Clean the messages
3111		 */
3112		let this->_errorMessages = [];
3113
3114		/**
3115		 * Query the identity field
3116		 */
3117		let identityField = metaData->getIdentityField(this);
3118
3119		/**
3120		 * _preSave() makes all the validations
3121		 */
3122		if this->_preSave(metaData, exists, identityField) === false {
3123
3124			/**
3125			 * Rollback the current transaction if there was validation errors
3126			 */
3127			if typeof related == "array" {
3128				writeConnection->rollback(false);
3129			}
3130
3131			/**
3132			 * Throw exceptions on failed saves?
3133			 */
3134			if globals_get("orm.exception_on_failed_save") {
3135				/**
3136				 * Launch a Phalcon\Mvc\Model\ValidationFailed to notify that the save failed
3137				 */
3138				throw new ValidationFailed(this, this->getMessages());
3139			}
3140
3141			return false;
3142		}
3143
3144		/**
3145		 * Depending if the record exists we do an update or an insert operation
3146		 */
3147		if exists {
3148			let success = this->_doLowUpdate(metaData, writeConnection, table);
3149		} else {
3150			let success = this->_doLowInsert(metaData, writeConnection, table, identityField);
3151		}
3152
3153		/**
3154		 * Change the dirty state to persistent
3155		 */
3156		if success {
3157			let this->_dirtyState = self::DIRTY_STATE_PERSISTENT;
3158		}
3159
3160		if typeof related == "array" {
3161
3162			/**
3163			 * Rollbacks the implicit transaction if the master save has failed
3164			 */
3165			if success === false {
3166				writeConnection->rollback(false);
3167			} else {
3168				/**
3169				 * Save the post-related records
3170				 */
3171				let success = this->_postSaveRelatedRecords(writeConnection, related);
3172			}
3173		}
3174
3175		/**
3176		 * _postSave() invokes after* events if the operation was successful
3177		 */
3178		if globals_get("orm.events") {
3179			let success = this->_postSave(success, exists);
3180		}
3181
3182		if success === false {
3183			this->_cancelOperation();
3184		} else {
3185			this->fireEvent("afterSave");
3186		}
3187
3188		return success;
3189	}
3190
3191	/**
3192	 * Inserts a model instance. If the instance already exists in the persistence it will throw an exception
3193	 * Returning true on success or false otherwise.
3194	 *
3195	 *<code>
3196	 * // Creating a new robot
3197	 * $robot = new Robots();
3198	 *
3199	 * $robot->type = "mechanical";
3200	 * $robot->name = "Astro Boy";
3201	 * $robot->year = 1952;
3202	 *
3203	 * $robot->create();
3204	 *
3205	 * // Passing an array to create
3206	 * $robot = new Robots();
3207	 *
3208	 * $robot->create(
3209	 *     [
3210	 *         "type" => "mechanical",
3211	 *         "name" => "Astro Boy",
3212	 *         "year" => 1952,
3213	 *     ]
3214	 * );
3215	 *</code>
3216	 */
3217	public function create(var data = null, var whiteList = null) -> boolean
3218	{
3219		var metaData;
3220
3221		let metaData = this->getModelsMetaData();
3222
3223		/**
3224		 * Get the current connection
3225		 * If the record already exists we must throw an exception
3226		 */
3227		if this->_exists(metaData, this->getReadConnection()) {
3228			let this->_errorMessages = [
3229				new Message("Record cannot be created because it already exists", null, "InvalidCreateAttempt")
3230			];
3231			return false;
3232		}
3233
3234		/**
3235		 * Using save() anyways
3236		 */
3237		return this->save(data, whiteList);
3238	}
3239
3240	/**
3241	 * Updates a model instance. If the instance doesn't exist in the persistence it will throw an exception
3242	 * Returning true on success or false otherwise.
3243	 *
3244	 *<code>
3245	 * // Updating a robot name
3246	 * $robot = Robots::findFirst("id = 100");
3247	 *
3248	 * $robot->name = "Biomass";
3249	 *
3250	 * $robot->update();
3251	 *</code>
3252	 */
3253	public function update(var data = null, var whiteList = null) -> boolean
3254	{
3255		var metaData;
3256
3257		/**
3258		 * We don't check if the record exists if the record is already checked
3259		 */
3260		if this->_dirtyState {
3261
3262			let metaData = this->getModelsMetaData();
3263
3264			if !this->_exists(metaData, this->getReadConnection()) {
3265				let this->_errorMessages = [
3266					new Message(
3267						"Record cannot be updated because it does not exist",
3268						null,
3269						"InvalidUpdateAttempt"
3270					)
3271				];
3272
3273				return false;
3274			}
3275		}
3276
3277		/**
3278		 * Call save() anyways
3279		 */
3280		return this->save(data, whiteList);
3281	}
3282
3283	/**
3284	 * Deletes a model instance. Returning true on success or false otherwise.
3285	 *
3286	 * <code>
3287	 * $robot = Robots::findFirst("id=100");
3288	 *
3289	 * $robot->delete();
3290	 *
3291	 * $robots = Robots::find("type = 'mechanical'");
3292	 *
3293	 * foreach ($robots as $robot) {
3294	 *     $robot->delete();
3295	 * }
3296	 * </code>
3297	 */
3298	public function delete() -> boolean
3299	{
3300		var metaData, writeConnection, values, bindTypes, primaryKeys,
3301			bindDataTypes, columnMap, attributeField, conditions, primaryKey,
3302			bindType, value, schema, source, table, success;
3303
3304		let metaData = this->getModelsMetaData(),
3305			writeConnection = this->getWriteConnection();
3306
3307		/**
3308		 * Operation made is OP_DELETE
3309		 */
3310		let this->_operationMade = self::OP_DELETE,
3311			this->_errorMessages = [];
3312
3313		/**
3314		 * Check if deleting the record violates a virtual foreign key
3315		 */
3316		if globals_get("orm.virtual_foreign_keys") {
3317			if this->_checkForeignKeysReverseRestrict() === false {
3318				return false;
3319			}
3320		}
3321
3322		let values = [],
3323			bindTypes = [],
3324			conditions = [];
3325
3326		let primaryKeys = metaData->getPrimaryKeyAttributes(this),
3327			bindDataTypes = metaData->getBindTypes(this);
3328
3329		if globals_get("orm.column_renaming") {
3330			let columnMap = metaData->getColumnMap(this);
3331		} else {
3332			let columnMap = null;
3333		}
3334
3335		/**
3336		 * We can't create dynamic SQL without a primary key
3337		 */
3338		if !count(primaryKeys) {
3339			throw new Exception("A primary key must be defined in the model in order to perform the operation");
3340		}
3341
3342		/**
3343		 * Create a condition from the primary keys
3344		 */
3345		for primaryKey in primaryKeys {
3346
3347			/**
3348			 * Every column part of the primary key must be in the bind data types
3349			 */
3350			if !fetch bindType, bindDataTypes[primaryKey] {
3351				throw new Exception("Column '" . primaryKey . "' have not defined a bind data type");
3352			}
3353
3354			/**
3355			 * Take the column values based on the column map if any
3356			 */
3357			if typeof columnMap == "array" {
3358				if !fetch attributeField, columnMap[primaryKey] {
3359					throw new Exception("Column '" . primaryKey . "' isn't part of the column map");
3360				}
3361			} else {
3362				let attributeField = primaryKey;
3363			}
3364
3365			/**
3366			 * If the attribute is currently set in the object add it to the conditions
3367			 */
3368			if !fetch value, this->{attributeField} {
3369				throw new Exception(
3370					"Cannot delete the record because the primary key attribute: '" . attributeField . "' wasn't set"
3371				);
3372			}
3373
3374			/**
3375			 * Escape the column identifier
3376			 */
3377			let values[] = value,
3378				conditions[] = writeConnection->escapeIdentifier(primaryKey) . " = ?",
3379				bindTypes[] = bindType;
3380		}
3381
3382		if globals_get("orm.events") {
3383
3384			let this->_skipped = false;
3385
3386			/**
3387			 * Fire the beforeDelete event
3388			 */
3389			if this->fireEventCancel("beforeDelete") === false {
3390				return false;
3391			} else {
3392				/**
3393				 * The operation can be skipped
3394				 */
3395				if this->_skipped === true {
3396					return true;
3397				}
3398			}
3399		}
3400
3401		let schema = this->getSchema(),
3402			source = this->getSource();
3403
3404		if schema {
3405			let table = [schema, source];
3406		} else {
3407			let table = source;
3408		}
3409
3410		/**
3411		 * Join the conditions in the array using an AND operator
3412		 * Do the deletion
3413		 */
3414		let success = writeConnection->delete(table, join(" AND ", conditions), values, bindTypes);
3415
3416		/**
3417		 * Check if there is virtual foreign keys with cascade action
3418		 */
3419		if globals_get("orm.virtual_foreign_keys") {
3420			if this->_checkForeignKeysReverseCascade() === false {
3421				return false;
3422			}
3423		}
3424
3425		if globals_get("orm.events") {
3426			if success {
3427				this->fireEvent("afterDelete");
3428			}
3429		}
3430
3431		/**
3432		 * Force perform the record existence checking again
3433		 */
3434		let this->_dirtyState = self::DIRTY_STATE_DETACHED;
3435
3436		return success;
3437	}
3438
3439	/**
3440	 * Returns the type of the latest operation performed by the ORM
3441	 * Returns one of the OP_* class constants
3442	 */
3443	public function getOperationMade() -> int
3444	{
3445		return this->_operationMade;
3446	}
3447
3448	/**
3449	 * Refreshes the model attributes re-querying the record from the database
3450	 */
3451	public function refresh() -> <Model>
3452	{
3453		var metaData, readConnection, schema, source, table,
3454			uniqueKey, tables, uniqueParams, dialect, row, fields, attribute, manager, columnMap;
3455
3456		if this->_dirtyState != self::DIRTY_STATE_PERSISTENT {
3457			throw new Exception("The record cannot be refreshed because it does not exist or is deleted");
3458		}
3459
3460		let metaData = this->getModelsMetaData(),
3461			readConnection = this->getReadConnection(),
3462			manager = <ManagerInterface> this->_modelsManager;
3463
3464		let schema = this->getSchema(),
3465			source = this->getSource();
3466
3467		if schema {
3468			let table = [schema, source];
3469		} else {
3470			let table = source;
3471		}
3472
3473		let uniqueKey = this->_uniqueKey;
3474		if !uniqueKey {
3475
3476			/**
3477			 * We need to check if the record exists
3478			 */
3479			if !this->_exists(metaData, readConnection, table) {
3480				throw new Exception("The record cannot be refreshed because it does not exist or is deleted");
3481			}
3482
3483			let uniqueKey = this->_uniqueKey;
3484		}
3485
3486		let uniqueParams = this->_uniqueParams;
3487		if typeof uniqueParams != "array" {
3488			throw new Exception("The record cannot be refreshed because it does not exist or is deleted");
3489		}
3490
3491		/**
3492		 * We only refresh the attributes in the model's metadata
3493		 */
3494		let fields = [];
3495		for attribute in metaData->getAttributes(this) {
3496			let fields[] = [attribute];
3497		}
3498
3499		/**
3500		 * We directly build the SELECT to save resources
3501		 */
3502		let dialect = readConnection->getDialect(),
3503			tables = dialect->select([
3504				"columns": fields,
3505				"tables":  readConnection->escapeIdentifier(table),
3506				"where":   uniqueKey
3507			]),
3508			row = readConnection->fetchOne(tables, \Phalcon\Db::FETCH_ASSOC, uniqueParams, this->_uniqueTypes);
3509
3510		/**
3511		 * Get a column map if any
3512		 * Assign the resulting array to the this object
3513		 */
3514		if typeof row == "array" {
3515			let columnMap = metaData->getColumnMap(this);
3516			this->assign(row, columnMap);
3517			if manager->isKeepingSnapshots(this) {
3518				this->setSnapshotData(row, columnMap);
3519				this->setOldSnapshotData(row, columnMap);
3520			}
3521		}
3522
3523		this->fireEvent("afterFetch");
3524
3525		return this;
3526	}
3527
3528	/**
3529	 * Skips the current operation forcing a success state
3530	 */
3531	public function skipOperation(boolean skip)
3532	{
3533		let this->_skipped = skip;
3534	}
3535
3536	/**
3537	 * Reads an attribute value by its name
3538	 *
3539	 * <code>
3540	 * echo $robot->readAttribute("name");
3541	 * </code>
3542	 */
3543	public function readAttribute(string! attribute)
3544	{
3545		if !isset this->{attribute} {
3546			return null;
3547		}
3548
3549		return this->{attribute};
3550	}
3551
3552	/**
3553	 * Writes an attribute value by its name
3554	 *
3555	 *<code>
3556	 * $robot->writeAttribute("name", "Rosey");
3557	 *</code>
3558	 */
3559	public function writeAttribute(string! attribute, var value)
3560	{
3561		let this->{attribute} = value;
3562	}
3563
3564	/**
3565	 * Sets a list of attributes that must be skipped from the
3566	 * generated INSERT/UPDATE statement
3567	 *
3568	 *<code>
3569	 *
3570	 * class Robots extends \Phalcon\Mvc\Model
3571	 * {
3572	 *     public function initialize()
3573	 *     {
3574	 *         $this->skipAttributes(
3575	 *             [
3576	 *                 "price",
3577	 *             ]
3578	 *         );
3579	 *     }
3580	 * }
3581	 *</code>
3582	 */
3583	protected function skipAttributes(array! attributes)
3584	{
3585		this->skipAttributesOnCreate(attributes);
3586		this->skipAttributesOnUpdate(attributes);
3587	}
3588
3589	/**
3590	 * Sets a list of attributes that must be skipped from the
3591	 * generated INSERT statement
3592	 *
3593	 *<code>
3594	 *
3595	 * class Robots extends \Phalcon\Mvc\Model
3596	 * {
3597	 *     public function initialize()
3598	 *     {
3599	 *         $this->skipAttributesOnCreate(
3600	 *             [
3601	 *                 "created_at",
3602	 *             ]
3603	 *         );
3604	 *     }
3605	 * }
3606	 *</code>
3607	 */
3608	protected function skipAttributesOnCreate(array! attributes) -> void
3609	{
3610		var keysAttributes, attribute;
3611
3612		let keysAttributes = [];
3613		for attribute in attributes {
3614			let keysAttributes[attribute] = null;
3615		}
3616
3617		this->getModelsMetaData()->setAutomaticCreateAttributes(this, keysAttributes);
3618	}
3619
3620	/**
3621	 * Sets a list of attributes that must be skipped from the
3622	 * generated UPDATE statement
3623	 *
3624	 *<code>
3625	 *
3626	 * class Robots extends \Phalcon\Mvc\Model
3627	 * {
3628	 *     public function initialize()
3629	 *     {
3630	 *         $this->skipAttributesOnUpdate(
3631	 *             [
3632	 *                 "modified_in",
3633	 *             ]
3634	 *         );
3635	 *     }
3636	 * }
3637	 *</code>
3638	 */
3639	protected function skipAttributesOnUpdate(array! attributes) -> void
3640	{
3641		var keysAttributes, attribute;
3642
3643		let keysAttributes = [];
3644		for attribute in attributes {
3645			let keysAttributes[attribute] = null;
3646		}
3647
3648		this->getModelsMetaData()->setAutomaticUpdateAttributes(this, keysAttributes);
3649	}
3650
3651	/**
3652	 * Sets a list of attributes that must be skipped from the
3653	 * generated UPDATE statement
3654	 *
3655	 *<code>
3656	 *
3657	 * class Robots extends \Phalcon\Mvc\Model
3658	 * {
3659	 *     public function initialize()
3660	 *     {
3661	 *         $this->allowEmptyStringValues(
3662	 *             [
3663	 *                 "name",
3664	 *             ]
3665	 *         );
3666	 *     }
3667	 * }
3668	 *</code>
3669	 */
3670	protected function allowEmptyStringValues(array! attributes) -> void
3671	{
3672		var keysAttributes, attribute;
3673
3674		let keysAttributes = [];
3675		for attribute in attributes {
3676			let keysAttributes[attribute] = true;
3677		}
3678
3679		this->getModelsMetaData()->setEmptyStringAttributes(this, keysAttributes);
3680	}
3681
3682	/**
3683	 * Setup a 1-1 relation between two models
3684	 *
3685	 *<code>
3686	 *
3687	 * class Robots extends \Phalcon\Mvc\Model
3688	 * {
3689	 *     public function initialize()
3690	 *     {
3691	 *         $this->hasOne("id", "RobotsDescription", "robots_id");
3692	 *     }
3693	 * }
3694	 *</code>
3695	 */
3696	protected function hasOne(var fields, string! referenceModel, var referencedFields, options = null) -> <Relation>
3697	{
3698		return (<ManagerInterface> this->_modelsManager)->addHasOne(this, fields, referenceModel, referencedFields, options);
3699	}
3700
3701	/**
3702	 * Setup a reverse 1-1 or n-1 relation between two models
3703	 *
3704	 *<code>
3705	 *
3706	 * class RobotsParts extends \Phalcon\Mvc\Model
3707	 * {
3708	 *     public function initialize()
3709	 *     {
3710	 *         $this->belongsTo("robots_id", "Robots", "id");
3711	 *     }
3712	 * }
3713	 *</code>
3714	 */
3715	protected function belongsTo(var fields, string! referenceModel, var referencedFields, options = null) -> <Relation>
3716	{
3717		return (<ManagerInterface> this->_modelsManager)->addBelongsTo(
3718			this,
3719			fields,
3720			referenceModel,
3721			referencedFields,
3722			options
3723		);
3724	}
3725
3726	/**
3727	 * Setup a 1-n relation between two models
3728	 *
3729	 *<code>
3730	 *
3731	 * class Robots extends \Phalcon\Mvc\Model
3732	 * {
3733	 *     public function initialize()
3734	 *     {
3735	 *         $this->hasMany("id", "RobotsParts", "robots_id");
3736	 *     }
3737	 * }
3738	 *</code>
3739	 */
3740	protected function hasMany(var fields, string! referenceModel, var referencedFields, options = null) -> <Relation>
3741	{
3742		return (<ManagerInterface> this->_modelsManager)->addHasMany(
3743			this,
3744			fields,
3745			referenceModel,
3746			referencedFields,
3747			options
3748		);
3749	}
3750
3751	/**
3752	 * Setup an n-n relation between two models, through an intermediate relation
3753	 *
3754	 *<code>
3755	 *
3756	 * class Robots extends \Phalcon\Mvc\Model
3757	 * {
3758	 *     public function initialize()
3759	 *     {
3760	 *         // Setup a many-to-many relation to Parts through RobotsParts
3761	 *         $this->hasManyToMany(
3762	 *             "id",
3763	 *             "RobotsParts",
3764	 *             "robots_id",
3765	 *             "parts_id",
3766	 *             "Parts",
3767	 *             "id",
3768	 *         );
3769	 *     }
3770	 * }
3771	 *</code>
3772	 *
3773	 * @param	string|array fields
3774	 * @param	string intermediateModel
3775	 * @param	string|array intermediateFields
3776	 * @param	string|array intermediateReferencedFields
3777	 * @param	string referencedModel
3778	 * @param   string|array referencedFields
3779	 * @param   array options
3780	 * @return  Phalcon\Mvc\Model\Relation
3781	 */
3782	protected function hasManyToMany(var fields, string! intermediateModel, var intermediateFields, var intermediateReferencedFields,
3783		string! referenceModel, var referencedFields, options = null) -> <Relation>
3784	{
3785		return (<ManagerInterface> this->_modelsManager)->addHasManyToMany(
3786			this,
3787			fields,
3788			intermediateModel,
3789			intermediateFields,
3790			intermediateReferencedFields,
3791			referenceModel,
3792			referencedFields,
3793			options
3794		);
3795	}
3796
3797	/**
3798	 * Setups a behavior in a model
3799	 *
3800	 *<code>
3801	 *
3802	 * use Phalcon\Mvc\Model;
3803	 * use Phalcon\Mvc\Model\Behavior\Timestampable;
3804	 *
3805	 * class Robots extends Model
3806	 * {
3807	 *     public function initialize()
3808	 *     {
3809	 *         $this->addBehavior(
3810	 *             new Timestampable(
3811	 *                [
3812	 *                    "onCreate" => [
3813	 *                         "field"  => "created_at",
3814	 *                         "format" => "Y-m-d",
3815	 * 	                   ],
3816	 *                 ]
3817	 *             )
3818	 *         );
3819	 *     }
3820	 * }
3821	 *</code>
3822	 */
3823	public function addBehavior(<BehaviorInterface> behavior) -> void
3824	{
3825		(<ManagerInterface> this->_modelsManager)->addBehavior(this, behavior);
3826	}
3827
3828	/**
3829	 * Sets if the model must keep the original record snapshot in memory
3830	 *
3831	 *<code>
3832	 *
3833	 * use Phalcon\Mvc\Model;
3834	 *
3835	 * class Robots extends Model
3836	 * {
3837	 *     public function initialize()
3838	 *     {
3839	 *         $this->keepSnapshots(true);
3840	 *     }
3841	 * }
3842	 *</code>
3843	 */
3844	protected function keepSnapshots(boolean keepSnapshot) -> void
3845	{
3846		(<ManagerInterface> this->_modelsManager)->keepSnapshots(this, keepSnapshot);
3847	}
3848
3849	/**
3850	 * Sets the record's snapshot data.
3851	 * This method is used internally to set snapshot data when the model was set up to keep snapshot data
3852	 *
3853	 * @param array data
3854	 * @param array columnMap
3855	 */
3856	public function setSnapshotData(array! data, columnMap = null)
3857	{
3858		var key, value, snapshot, attribute;
3859
3860		/**
3861		 * Build the snapshot based on a column map
3862		 */
3863		if typeof columnMap == "array" {
3864
3865			let snapshot = [];
3866			for key, value in data {
3867
3868				/**
3869				 * Use only strings
3870				 */
3871				if typeof key != "string" {
3872					continue;
3873				}
3874
3875				/**
3876				 * Every field must be part of the column map
3877				 */
3878				if !fetch attribute, columnMap[key] {
3879					if !globals_get("orm.ignore_unknown_columns") {
3880						throw new Exception("Column '" . key . "' doesn't make part of the column map");
3881					} else {
3882						continue;
3883					}
3884				}
3885
3886				if typeof attribute == "array" {
3887					if !fetch attribute, attribute[0] {
3888						if !globals_get("orm.ignore_unknown_columns") {
3889							throw new Exception("Column '" . key . "' doesn't make part of the column map");
3890						} else {
3891							continue;
3892						}
3893					}
3894				}
3895
3896				let snapshot[attribute] = value;
3897			}
3898		} else {
3899			let snapshot = data;
3900		}
3901
3902		if typeof this->_snapshot == "array" {
3903			let this->_oldSnapshot = this->_snapshot;
3904		}
3905
3906		let this->_snapshot = snapshot;
3907	}
3908
3909	/**
3910	 * Sets the record's old snapshot data.
3911	 * This method is used internally to set old snapshot data when the model was set up to keep snapshot data
3912	 *
3913	 * @param array data
3914	 * @param array columnMap
3915	 */
3916	public function setOldSnapshotData(array! data, columnMap = null)
3917	{
3918		var key, value, snapshot, attribute;
3919		/**
3920		 * Build the snapshot based on a column map
3921		 */
3922		if typeof columnMap == "array" {
3923			let snapshot = [];
3924			for key, value in data {
3925				/**
3926				 * Use only strings
3927				 */
3928				if typeof key != "string" {
3929					continue;
3930				}
3931				/**
3932				 * Every field must be part of the column map
3933				 */
3934				if !fetch attribute, columnMap[key] {
3935					if !globals_get("orm.ignore_unknown_columns") {
3936						throw new Exception("Column '" . key . "' doesn't make part of the column map");
3937					} else {
3938						continue;
3939					}
3940				}
3941				if typeof attribute == "array" {
3942					if !fetch attribute, attribute[0] {
3943						if !globals_get("orm.ignore_unknown_columns") {
3944							throw new Exception("Column '" . key . "' doesn't make part of the column map");
3945						} else {
3946							continue;
3947						}
3948					}
3949				}
3950				let snapshot[attribute] = value;
3951			}
3952		} else {
3953			let snapshot = data;
3954		}
3955
3956		let this->_oldSnapshot = snapshot;
3957	}
3958
3959	/**
3960	 * Checks if the object has internal snapshot data
3961	 */
3962	public function hasSnapshotData() -> boolean
3963	{
3964		var snapshot;
3965		let snapshot = this->_snapshot;
3966
3967		return typeof snapshot == "array";
3968	}
3969
3970	/**
3971	 * Returns the internal snapshot data
3972	 */
3973	public function getSnapshotData() -> array
3974	{
3975		return this->_snapshot;
3976	}
3977
3978	/**
3979	 * Returns the internal old snapshot data
3980	 */
3981	public function getOldSnapshotData() -> array
3982	{
3983		return this->_oldSnapshot;
3984	}
3985
3986	/**
3987	 * Check if a specific attribute has changed
3988	 * This only works if the model is keeping data snapshots
3989	 *
3990	 *<code>
3991	 * $robot = new Robots();
3992	 *
3993	 * $robot->type = "mechanical";
3994	 * $robot->name = "Astro Boy";
3995	 * $robot->year = 1952;
3996	 *
3997	 * $robot->create();
3998	 *
3999	 * $robot->type = "hydraulic";
4000	 *
4001	 * $hasChanged = $robot->hasChanged("type"); // returns true
4002	 * $hasChanged = $robot->hasChanged(["type", "name"]); // returns true
4003	 * $hasChanged = $robot->hasChanged(["type", "name"], true); // returns false
4004	 *</code>
4005	 *
4006	 * @param string|array fieldName
4007	 * @param boolean allFields
4008	 */
4009	public function hasChanged(var fieldName = null, boolean allFields = false) -> boolean
4010	{
4011		var changedFields;
4012
4013		let changedFields = this->getChangedFields();
4014
4015		/**
4016		 * If a field was specified we only check it
4017		 */
4018		if typeof fieldName == "string" {
4019			return in_array(fieldName, changedFields);
4020		} elseif typeof fieldName == "array" {
4021		    if allFields {
4022		        return array_intersect(fieldName, changedFields) == fieldName;
4023		    }
4024
4025		    return count(array_intersect(fieldName, changedFields)) > 0;
4026		}
4027
4028		return count(changedFields) > 0;
4029	}
4030
4031	/**
4032	 * Check if a specific attribute was updated
4033	 * This only works if the model is keeping data snapshots
4034	 *
4035	 * @param string|array fieldName
4036	 */
4037	public function hasUpdated(var fieldName = null, boolean allFields = false) -> boolean
4038	{
4039		var updatedFields;
4040
4041		let updatedFields = this->getUpdatedFields();
4042
4043		/**
4044		 * If a field was specified we only check it
4045		 */
4046		if typeof fieldName == "string" {
4047			return in_array(fieldName, updatedFields);
4048		} elseif typeof fieldName == "array" {
4049			if allFields {
4050				return array_intersect(fieldName, updatedFields) == fieldName;
4051			}
4052
4053			return count(array_intersect(fieldName, updatedFields)) > 0;
4054		}
4055
4056		return count(updatedFields) > 0;
4057	}
4058
4059	/**
4060	 * Returns a list of changed values.
4061	 *
4062	 * <code>
4063	 * $robots = Robots::findFirst();
4064	 * print_r($robots->getChangedFields()); // []
4065	 *
4066	 * $robots->deleted = 'Y';
4067	 *
4068	 * $robots->getChangedFields();
4069	 * print_r($robots->getChangedFields()); // ["deleted"]
4070	 * </code>
4071	 */
4072	public function getChangedFields() -> array
4073	{
4074		var metaData, changed, name, snapshot,
4075			columnMap, allAttributes, value;
4076
4077		let snapshot = this->_snapshot;
4078		if typeof snapshot != "array" {
4079			throw new Exception("The record doesn't have a valid data snapshot");
4080		}
4081
4082		/**
4083		 * Return the models meta-data
4084		 */
4085		let metaData = this->getModelsMetaData();
4086
4087		/**
4088		 * The reversed column map is an array if the model has a column map
4089		 */
4090		let columnMap = metaData->getReverseColumnMap(this);
4091
4092		/**
4093		 * Data types are field indexed
4094		 */
4095		if typeof columnMap != "array" {
4096			let allAttributes = metaData->getDataTypes(this);
4097		} else {
4098			let allAttributes = columnMap;
4099		}
4100
4101		/**
4102		 * Check every attribute in the model
4103		 */
4104		let changed = [];
4105
4106		for name, _ in allAttributes {
4107			/**
4108			 * If some attribute is not present in the snapshot, we assume the record as changed
4109			 */
4110			if !isset snapshot[name] {
4111				let changed[] = name;
4112				continue;
4113			}
4114
4115			/**
4116			 * If some attribute is not present in the model, we assume the record as changed
4117			 */
4118			if !fetch value, this->{name} {
4119				let changed[] = name;
4120				continue;
4121			}
4122
4123			/**
4124			 * Check if the field has changed
4125			 */
4126			if value !== snapshot[name] {
4127				let changed[] = name;
4128				continue;
4129			}
4130		}
4131
4132		return changed;
4133	}
4134
4135	/**
4136	 * Returns a list of updated values.
4137	 *
4138	 * <code>
4139	 * $robots = Robots::findFirst();
4140	 * print_r($robots->getChangedFields()); // []
4141	 *
4142	 * $robots->deleted = 'Y';
4143	 *
4144	 * $robots->getChangedFields();
4145	 * print_r($robots->getChangedFields()); // ["deleted"]
4146	 * $robots->save();
4147	 * print_r($robots->getChangedFields()); // []
4148	 * print_r($robots->getUpdatedFields()); // ["deleted"]
4149	 * </code>
4150	 */
4151	public function getUpdatedFields()
4152	{
4153		var updated, name, snapshot,
4154			oldSnapshot, value;
4155
4156		let snapshot = this->_snapshot;
4157		let oldSnapshot = this->_oldSnapshot;
4158
4159		if !globals_get("orm.update_snapshot_on_save") {
4160			throw new Exception("Update snapshot on save must be enabled for this method to work properly");
4161		}
4162
4163		if typeof snapshot != "array" {
4164			throw new Exception("The record doesn't have a valid data snapshot");
4165		}
4166
4167		/**
4168		 * Dirty state must be DIRTY_PERSISTENT to make the checking
4169		 */
4170		if this->_dirtyState != self::DIRTY_STATE_PERSISTENT {
4171			throw new Exception("Change checking cannot be performed because the object has not been persisted or is deleted");
4172		}
4173
4174		let updated = [];
4175
4176		for name, value in snapshot {
4177			/**
4178			 * If some attribute is not present in the oldSnapshot, we assume the record as changed
4179			 */
4180			if !isset oldSnapshot[name] {
4181				let updated[] = name;
4182				continue;
4183			}
4184
4185			if value !== oldSnapshot[name] {
4186				let updated[] = name;
4187				continue;
4188			}
4189		}
4190
4191		return updated;
4192	}
4193
4194	/**
4195	 * Sets if a model must use dynamic update instead of the all-field update
4196	 *
4197	 *<code>
4198	 *
4199	 * use Phalcon\Mvc\Model;
4200	 *
4201	 * class Robots extends Model
4202	 * {
4203	 *     public function initialize()
4204	 *     {
4205	 *         $this->useDynamicUpdate(true);
4206	 *     }
4207	 * }
4208	 *</code>
4209	 */
4210	protected function useDynamicUpdate(boolean dynamicUpdate) -> void
4211	{
4212		(<ManagerInterface> this->_modelsManager)->useDynamicUpdate(this, dynamicUpdate);
4213	}
4214
4215	/**
4216	 * Returns related records based on defined relations
4217	 *
4218	 * @param string alias
4219	 * @param array arguments
4220	 * @return \Phalcon\Mvc\Model\ResultsetInterface
4221	 */
4222	public function getRelated(string alias, arguments = null) -> <ResultsetInterface>
4223	{
4224		var relation, className, manager;
4225
4226		/**
4227		 * Query the relation by alias
4228		 */
4229		let className = get_class(this),
4230			manager = <ManagerInterface> this->_modelsManager,
4231			relation = <RelationInterface> manager->getRelationByAlias(className, alias);
4232		if typeof relation != "object" {
4233			throw new Exception("There is no defined relations for the model '" . className . "' using alias '" . alias . "'");
4234		}
4235
4236		/**
4237		 * Call the 'getRelationRecords' in the models manager
4238		 */
4239		return manager->getRelationRecords(relation, null, this, arguments);
4240	}
4241
4242	/**
4243	 * Returns related records defined relations depending on the method name
4244	 *
4245	 * @param string modelName
4246	 * @param string method
4247	 * @param array arguments
4248	 * @return mixed
4249	 */
4250	protected function _getRelatedRecords(string! modelName, string! method, var arguments)
4251	{
4252		var manager, relation, queryMethod, extraArgs;
4253
4254		let manager = <ManagerInterface> this->_modelsManager;
4255
4256		let relation = false,
4257			queryMethod = null;
4258
4259		/**
4260		 * Calling find/findFirst if the method starts with "get"
4261		 */
4262		if starts_with(method, "get") {
4263			let relation = <RelationInterface> manager->getRelationByAlias(modelName, substr(method, 3));
4264		}
4265
4266		/**
4267		 * Calling count if the method starts with "count"
4268		 */
4269		elseif starts_with(method, "count") {
4270			let queryMethod = "count",
4271				relation = <RelationInterface> manager->getRelationByAlias(modelName, substr(method, 5));
4272		}
4273
4274		/**
4275		 * If the relation was found perform the query via the models manager
4276		 */
4277		if typeof relation != "object" {
4278			return null;
4279		}
4280
4281		fetch extraArgs, arguments[0];
4282
4283		return manager->getRelationRecords(
4284			relation,
4285			queryMethod,
4286			this,
4287			extraArgs
4288		);
4289	}
4290
4291	/**
4292	 * Try to check if the query must invoke a finder
4293	 *
4294	 * @param  string method
4295	 * @param  array arguments
4296	 * @return \Phalcon\Mvc\ModelInterface[]|\Phalcon\Mvc\ModelInterface|boolean
4297	 */
4298	protected final static function _invokeFinder(method, arguments)
4299	{
4300		var extraMethod, type, modelName, value, model,
4301			attributes, field, extraMethodFirst, metaData;
4302
4303		let extraMethod = null;
4304
4305		/**
4306		 * Check if the method starts with "findFirst"
4307		 */
4308		if starts_with(method, "findFirstBy") {
4309			let type = "findFirst",
4310				extraMethod = substr(method, 11);
4311		}
4312
4313		/**
4314		 * Check if the method starts with "find"
4315		 */
4316		elseif starts_with(method, "findBy") {
4317			let type = "find",
4318				extraMethod = substr(method, 6);
4319		}
4320
4321		/**
4322		 * Check if the method starts with "count"
4323		 */
4324		elseif starts_with(method, "countBy") {
4325			let type = "count",
4326				extraMethod = substr(method, 7);
4327		}
4328
4329		/**
4330		 * The called class is the model
4331		 */
4332		let modelName = get_called_class();
4333
4334		if !extraMethod {
4335			return null;
4336		}
4337
4338		if !fetch value, arguments[0] {
4339			throw new Exception("The static method '" . method . "' requires one argument");
4340		}
4341
4342		let model = new {modelName}(),
4343			metaData = model->getModelsMetaData();
4344
4345		/**
4346		 * Get the attributes
4347		 */
4348		let attributes = metaData->getReverseColumnMap(model);
4349		if typeof attributes != "array" {
4350			let attributes = metaData->getDataTypes(model);
4351		}
4352
4353		/**
4354		 * Check if the extra-method is an attribute
4355		 */
4356		if isset attributes[extraMethod] {
4357			let field = extraMethod;
4358		} else {
4359
4360			/**
4361			 * Lowercase the first letter of the extra-method
4362			 */
4363			let extraMethodFirst = lcfirst(extraMethod);
4364			if isset attributes[extraMethodFirst] {
4365				let field = extraMethodFirst;
4366			} else {
4367
4368				/**
4369				 * Get the possible real method name
4370				 */
4371				let field = uncamelize(extraMethod);
4372				if !isset attributes[field] {
4373					throw new Exception("Cannot resolve attribute '" . extraMethod . "' in the model");
4374				}
4375			}
4376		}
4377
4378		/**
4379		 * Execute the query
4380		 */
4381		return {modelName}::{type}([
4382			"conditions": "[" . field . "] = ?0",
4383			"bind"		: [value]
4384		]);
4385	}
4386
4387	/**
4388	 * Handles method calls when a method is not implemented
4389	 *
4390	 * @param	string method
4391	 * @param	array arguments
4392	 * @return	mixed
4393	 */
4394	public function __call(string method, arguments)
4395	{
4396		var modelName, status, records;
4397
4398		let records = self::_invokeFinder(method, arguments);
4399		if records !== null {
4400			return records;
4401		}
4402
4403		let modelName = get_class(this);
4404
4405		/**
4406		 * Check if there is a default action using the magic getter
4407		 */
4408		let records = this->_getRelatedRecords(modelName, method, arguments);
4409		if records !== null {
4410			return records;
4411		}
4412
4413		/**
4414		 * Try to find a replacement for the missing method in a behavior/listener
4415		 */
4416		let status = (<ManagerInterface> this->_modelsManager)->missingMethod(this, method, arguments);
4417		if status !== null {
4418			return status;
4419		}
4420
4421		/**
4422		 * The method doesn't exist throw an exception
4423		 */
4424		throw new Exception("The method '" . method . "' doesn't exist on model '" . modelName . "'");
4425	}
4426
4427	/**
4428	 * Handles method calls when a static method is not implemented
4429	 *
4430	 * @param	string method
4431	 * @param	array arguments
4432	 * @return	mixed
4433	 */
4434	public static function __callStatic(string method, arguments)
4435	{
4436		var records;
4437
4438		let records = self::_invokeFinder(method, arguments);
4439		if records === null {
4440			throw new Exception("The static method '" . method . "' doesn't exist");
4441		}
4442
4443		return records;
4444	}
4445
4446	/**
4447	 * Magic method to assign values to the the model
4448	 *
4449	 * @param string property
4450	 * @param mixed value
4451	 */
4452	public function __set(string property, value)
4453	{
4454		var lowerProperty, related, modelName, manager, lowerKey,
4455			relation, referencedModel, key, item, dirtyState;
4456
4457		/**
4458		 * Values are probably relationships if they are objects
4459		 */
4460		if typeof value == "object" {
4461			if value instanceof ModelInterface {
4462				let dirtyState = this->_dirtyState;
4463				if (value->getDirtyState() != dirtyState) {
4464					let dirtyState = self::DIRTY_STATE_TRANSIENT;
4465				}
4466				let lowerProperty = strtolower(property),
4467					this->{lowerProperty} = value,
4468					this->_related[lowerProperty] = value,
4469					this->_dirtyState = dirtyState;
4470				return value;
4471			}
4472		}
4473
4474		/**
4475		 * Check if the value is an array
4476		 */
4477		if typeof value == "array" {
4478
4479			let lowerProperty = strtolower(property),
4480				modelName = get_class(this),
4481				manager = this->getModelsManager();
4482
4483			let related = [];
4484			for key, item in value {
4485				if typeof item == "object" {
4486					if item instanceof ModelInterface {
4487						let related[] = item;
4488					}
4489				} else {
4490					let lowerKey = strtolower(key),
4491						this->{lowerKey} = item,
4492						relation = <RelationInterface> manager->getRelationByAlias(modelName, lowerProperty);
4493					if typeof relation == "object" {
4494						let referencedModel = manager->load(relation->getReferencedModel());
4495						referencedModel->writeAttribute(lowerKey, item);
4496					}
4497				}
4498			}
4499
4500			if count(related) > 0 {
4501				let this->_related[lowerProperty] = related,
4502					this->_dirtyState = self::DIRTY_STATE_TRANSIENT;
4503			}
4504
4505			return value;
4506		}
4507
4508		// Use possible setter.
4509		if this->_possibleSetter(property, value) {
4510			return value;
4511		}
4512
4513		// Throw an exception if there is an attempt to set a non-public property.
4514		if property_exists(this, property) {
4515			let manager = this->getModelsManager();
4516			if !manager->isVisibleModelProperty(this, property) {
4517				throw new Exception("Property '" . property . "' does not have a setter.");
4518			}
4519		}
4520
4521		let this->{property} = value;
4522
4523		return value;
4524	}
4525
4526	/**
4527	 * Check for, and attempt to use, possible setter.
4528	 *
4529	 * @param string property
4530	 * @param mixed value
4531	 * @return string
4532	 */
4533	protected final function _possibleSetter(string property, value)
4534	{
4535		var possibleSetter;
4536
4537		let possibleSetter = "set" . camelize(property);
4538		if method_exists(this, possibleSetter) {
4539			this->{possibleSetter}(value);
4540			return true;
4541		}
4542		return false;
4543	}
4544
4545	/**
4546	 * Magic method to get related records using the relation alias as a property
4547	 *
4548	 * @param string property
4549	 * @return \Phalcon\Mvc\Model\Resultset|Phalcon\Mvc\Model
4550	 */
4551	public function __get(string! property)
4552	{
4553		var modelName, manager, lowerProperty, relation, result, method;
4554
4555		let modelName = get_class(this),
4556			manager = this->getModelsManager(),
4557			lowerProperty = strtolower(property);
4558
4559		/**
4560		 * Check if the property is a relationship
4561		 */
4562		let relation = <RelationInterface> manager->getRelationByAlias(modelName, lowerProperty);
4563		if typeof relation == "object" {
4564
4565			/*
4566			 Not fetch a relation if it is on CamelCase
4567			 */
4568			if isset this->{lowerProperty} && typeof this->{lowerProperty} == "object" {
4569				return this->{lowerProperty};
4570			}
4571			/**
4572			 * Get the related records
4573			 */
4574			let result = manager->getRelationRecords(relation, null, this, null);
4575
4576			/**
4577			 * Assign the result to the object
4578			 */
4579			if typeof result == "object" {
4580
4581				/**
4582				 * We assign the result to the instance avoiding future queries
4583				 */
4584				let this->{lowerProperty} = result;
4585
4586				/**
4587				 * For belongs-to relations we store the object in the related bag
4588				 */
4589				if result instanceof ModelInterface {
4590					let this->_related[lowerProperty] = result;
4591				}
4592			}
4593
4594			return result;
4595		}
4596
4597		/**
4598		 * Check if the property has getters
4599		 */
4600		let method = "get" . camelize(property);
4601
4602		if method_exists(this, method) {
4603			return this->{method}();
4604		}
4605
4606		/**
4607		 * A notice is shown if the property is not defined and it isn't a relationship
4608		 */
4609		trigger_error("Access to undefined property " . modelName . "::" . property);
4610		return null;
4611	}
4612
4613	/**
4614	 * Magic method to check if a property is a valid relation
4615	 */
4616	public function __isset(string! property) -> boolean
4617	{
4618		var modelName, manager, relation;
4619
4620		let modelName = get_class(this),
4621			manager = <ManagerInterface> this->getModelsManager();
4622
4623		/**
4624		 * Check if the property is a relationship
4625		 */
4626		let relation = <RelationInterface> manager->getRelationByAlias(modelName, property);
4627		return typeof relation == "object";
4628	}
4629
4630	/**
4631	 * Serializes the object ignoring connections, services, related objects or static properties
4632	 */
4633	public function serialize() -> string
4634	{
4635		/**
4636		 * Use the standard serialize function to serialize the array data
4637		 */
4638		var attributes, snapshot, manager;
4639
4640		let attributes = this->toArray(),
4641		    manager = <ManagerInterface> this->getModelsManager();
4642
4643		if manager->isKeepingSnapshots(this) {
4644			let snapshot = this->_snapshot;
4645			/**
4646			 * If attributes is not the same as snapshot then save snapshot too
4647			 */
4648			if snapshot != null && attributes != snapshot {
4649				return serialize(["_attributes": attributes, "_snapshot": snapshot]);
4650			}
4651		}
4652
4653		return serialize(attributes);
4654	}
4655
4656	/**
4657	 * Unserializes the object from a serialized string
4658	 */
4659	public function unserialize(var data)
4660	{
4661		var attributes, dependencyInjector, manager, key, value, snapshot;
4662
4663		let attributes = unserialize(data);
4664		if typeof attributes == "array" {
4665
4666			/**
4667			 * Obtain the default DI
4668			 */
4669			let dependencyInjector = Di::getDefault();
4670			if typeof dependencyInjector != "object" {
4671				throw new Exception("A dependency injector container is required to obtain the services related to the ORM");
4672			}
4673
4674			/**
4675			 * Update the dependency injector
4676			 */
4677			let this->_dependencyInjector = dependencyInjector;
4678
4679			/**
4680			 * Gets the default modelsManager service
4681			 */
4682			let manager = <ManagerInterface> dependencyInjector->getShared("modelsManager");
4683			if typeof manager != "object" {
4684				throw new Exception("The injected service 'modelsManager' is not valid");
4685			}
4686
4687			/**
4688			 * Update the models manager
4689			 */
4690			let this->_modelsManager = manager;
4691
4692			/**
4693			 * Try to initialize the model
4694			 */
4695			manager->initialize(this);
4696			if manager->isKeepingSnapshots(this) {
4697				if fetch snapshot, attributes["_snapshot"] {
4698					let this->_snapshot = snapshot;
4699					let attributes = attributes["_attributes"];
4700				}
4701				else {
4702					let this->_snapshot = attributes;
4703				}
4704			}
4705
4706			/**
4707			 * Update the objects attributes
4708			 */
4709			for key, value in attributes {
4710				let this->{key} = value;
4711			}
4712		}
4713	}
4714
4715	/**
4716	 * Returns a simple representation of the object that can be used with var_dump
4717	 *
4718	 *<code>
4719	 * var_dump(
4720	 *     $robot->dump()
4721	 * );
4722	 *</code>
4723	 */
4724	public function dump() -> array
4725	{
4726		return get_object_vars(this);
4727	}
4728
4729	/**
4730	 * Returns the instance as an array representation
4731	 *
4732	 *<code>
4733	 * print_r(
4734	 *     $robot->toArray()
4735	 * );
4736	 *</code>
4737	 *
4738	 * @param array $columns
4739	 * @return array
4740	 */
4741	public function toArray(columns = null) -> array
4742	{
4743		var data, metaData, columnMap, attribute,
4744			attributeField, value;
4745
4746		let data = [],
4747			metaData = this->getModelsMetaData(),
4748			columnMap = metaData->getColumnMap(this);
4749
4750		for attribute in metaData->getAttributes(this) {
4751
4752			/**
4753			 * Check if the columns must be renamed
4754			 */
4755			if typeof columnMap == "array" {
4756				if !fetch attributeField, columnMap[attribute] {
4757					if !globals_get("orm.ignore_unknown_columns") {
4758						throw new Exception("Column '" . attribute . "' doesn't make part of the column map");
4759					} else {
4760						continue;
4761					}
4762				}
4763			} else {
4764				let attributeField = attribute;
4765			}
4766
4767			if typeof columns == "array" {
4768				if !in_array(attributeField, columns) {
4769					continue;
4770				}
4771			}
4772
4773			if fetch value, this->{attributeField} {
4774				let data[attributeField] = value;
4775			} else {
4776				let data[attributeField] = null;
4777			}
4778		}
4779
4780		return data;
4781	}
4782
4783	/**
4784	* Serializes the object for json_encode
4785	*
4786	*<code>
4787	* echo json_encode($robot);
4788	*</code>
4789	*
4790	* @return array
4791	*/
4792	public function jsonSerialize() -> array
4793	{
4794		return this->toArray();
4795	}
4796
4797	/**
4798	 * Enables/disables options in the ORM
4799	 */
4800	public static function setup(array! options) -> void
4801	{
4802		var disableEvents, columnRenaming, notNullValidations,
4803			exceptionOnFailedSave, phqlLiterals, virtualForeignKeys,
4804			lateStateBinding, castOnHydrate, ignoreUnknownColumns,
4805			updateSnapshotOnSave, disableAssignSetters;
4806
4807		/**
4808		 * Enables/Disables globally the internal events
4809		 */
4810		if fetch disableEvents, options["events"] {
4811			globals_set("orm.events", disableEvents);
4812		}
4813
4814		/**
4815		 * Enables/Disables virtual foreign keys
4816		 */
4817		if fetch virtualForeignKeys, options["virtualForeignKeys"] {
4818			globals_set("orm.virtual_foreign_keys", virtualForeignKeys);
4819		}
4820
4821		/**
4822		 * Enables/Disables column renaming
4823		 */
4824		if fetch columnRenaming, options["columnRenaming"] {
4825			globals_set("orm.column_renaming", columnRenaming);
4826		}
4827
4828		/**
4829		 * Enables/Disables automatic not null validation
4830		 */
4831		if fetch notNullValidations, options["notNullValidations"] {
4832			globals_set("orm.not_null_validations", notNullValidations);
4833		}
4834
4835		/**
4836		 * Enables/Disables throws an exception if the saving process fails
4837		 */
4838		if fetch exceptionOnFailedSave, options["exceptionOnFailedSave"] {
4839			globals_set("orm.exception_on_failed_save", exceptionOnFailedSave);
4840		}
4841
4842		/**
4843		 * Enables/Disables literals in PHQL this improves the security of applications
4844		 */
4845		if fetch phqlLiterals, options["phqlLiterals"] {
4846			globals_set("orm.enable_literals", phqlLiterals);
4847		}
4848
4849		/**
4850		 * Enables/Disables late state binding on model hydration
4851		 */
4852		if fetch lateStateBinding, options["lateStateBinding"] {
4853			globals_set("orm.late_state_binding", lateStateBinding);
4854		}
4855
4856		/**
4857		 * Enables/Disables automatic cast to original types on hydration
4858		 */
4859		if fetch castOnHydrate, options["castOnHydrate"] {
4860			globals_set("orm.cast_on_hydrate", castOnHydrate);
4861		}
4862
4863		/**
4864		 * Allows to ignore unknown columns when hydrating objects
4865		 */
4866		if fetch ignoreUnknownColumns, options["ignoreUnknownColumns"] {
4867			globals_set("orm.ignore_unknown_columns", ignoreUnknownColumns);
4868		}
4869
4870		if fetch updateSnapshotOnSave, options["updateSnapshotOnSave"] {
4871			globals_set("orm.update_snapshot_on_save", updateSnapshotOnSave);
4872		}
4873
4874		if fetch disableAssignSetters, options["disableAssignSetters"] {
4875		    globals_set("orm.disable_assign_setters", disableAssignSetters);
4876		}
4877	}
4878
4879	/**
4880	 * Reset a model instance data
4881	 */
4882	public function reset()
4883	{
4884		let this->_uniqueParams = null;
4885		let this->_snapshot = null;
4886	}
4887}
4888