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