ODM (Object-Document Mapper) ============================ In addition to its ability to :doc:`map tables ` in relational databases, Phalcon can map documents from NoSQL databases. The ODM offers a CRUD functionality, events, validations among other services. Due to the absence of SQL queries and planners, NoSQL databases can see real improvements in performance using the Phalcon approach. Additionally, there are no SQL building reducing the possibility of SQL injections. The following NoSQL databases are supported: +------------+----------------------------------------------------------------------+ | Name | Description | +============+======================================================================+ | MongoDB_ | MongoDB is a scalable, high-performance, open source NoSQL database. | +------------+----------------------------------------------------------------------+ モデルの作成 --------------- A model is a class that extends from :doc:`Phalcon\\Mvc\\Collection <../api/Phalcon_Mvc_Collection>`. It must be placed in the models directory. A model file must contain a single class; its class name should be in camel case notation: .. code-block:: php setSource("the_robots"); } } Understanding Documents To Objects ---------------------------------- Every instance of a model represents a document in the collection. You can easily access collection data by reading object properties. For example, for a collection "robots" with the documents: .. code-block:: bash $ mongo test MongoDB shell version: 1.8.2 connecting to: test > db.robots.find() { "_id" : ObjectId("508735512d42b8c3d15ec4e1"), "name" : "Astro Boy", "year" : 1952, "type" : "mechanical" } { "_id" : ObjectId("5087358f2d42b8c3d15ec4e2"), "name" : "Bender", "year" : 1999, "type" : "mechanical" } { "_id" : ObjectId("508735d32d42b8c3d15ec4e3"), "name" : "Wall-E", "year" : 2008 } > 名前空間内のモデル -------------------- Namespaces can be used to avoid class name collision. In this case it is necessary to indicate the name of the related collection using the :code:`setSource()` method: .. code-block:: php setSource("robots"); } } You could find a certain document by its ID and then print its name: .. code-block:: php name; Once the record is in memory, you can make modifications to its data and then save changes: .. code-block:: php "Astro Boy", ] ] ); $robot->name = "Voltron"; $robot->save(); 接続設定 -------------------- Connections are retrieved from the services container. By default, Phalcon tries to find the connection in a service called "mongo": .. code-block:: php set( "mongo", function () { $mongo = new MongoClient(); return $mongo->selectDB("store"); }, true ); // Connecting to a domain socket, falling back to localhost connection $di->set( "mongo", function () { $mongo = new MongoClient( "mongodb:///tmp/mongodb-27017.sock,localhost:27017" ); return $mongo->selectDB("store"); }, true ); ドキュメントの検索 ------------------ As :doc:`Phalcon\\Mvc\\Collection <../api/Phalcon_Mvc_Collection>` relies on the Mongo PHP extension you have the same facilities to query documents and convert them transparently to model instances: .. code-block:: php "mechanical", ] ] ); echo "There are ", count($robots), "\n"; // Get and print mechanical robots ordered by name upward $robots = Robots::find( [ [ "type" => "mechanical", ], "sort" => [ "name" => 1, ], ] ); foreach ($robots as $robot) { echo $robot->name, "\n"; } // Get first 100 mechanical robots ordered by name $robots = Robots::find( [ [ "type" => "mechanical", ], "sort" => [ "name" => 1, ], "limit" => 100, ] ); foreach ($robots as $robot) { echo $robot->name, "\n"; } You could also use the :code:`findFirst()` method to get only the first record matching the given criteria: .. code-block:: php name, "\n"; // What's the first mechanical robot in robots collection? $robot = Robots::findFirst( [ [ "type" => "mechanical", ] ] ); echo "The first mechanical robot name is ", $robot->name, "\n"; Both :code:`find()` and :code:`findFirst()` methods accept an associative array specifying the search criteria: .. code-block:: php [ "type" => "mechanical", "year" => "1999", ], ] ); // All virtual robots ordered by name downward $robots = Robots::find( [ "conditions" => [ "type" => "virtual", ], "sort" => [ "name" => -1, ], ] ); The available query options are: +--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------+ | Parameter | Description | Example | +====================+==============================================================================================================================================================================================+======================================================+ | :code:`conditions` | Search conditions for the find operation. Is used to extract only those records that fulfill a specified criterion. By default Phalcon_model assumes the first parameter are the conditions. | :code:`"conditions" => array('$gt' => 1990)` | +--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------+ | :code:`fields` | Returns specific columns instead of the full fields in the collection. When using this option an incomplete object is returned | :code:`"fields" => array('name' => true)` | +--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------+ | :code:`sort` | It's used to sort the resultset. Use one or more fields as each element in the array, 1 means ordering upwards, -1 downward | :code:`"sort" => array("name" => -1, "status" => 1)` | +--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------+ | :code:`limit` | Limit the results of the query to results to certain range | :code:`"limit" => 10` | +--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------+ | :code:`skip` | Skips a number of results | :code:`"skip" => 50` | +--------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------+ If you have experience with SQL databases, you may want to check the `SQL to Mongo Mapping Chart`_. 集計 ------------ A model can return calculations using `aggregation framework`_ provided by Mongo. The aggregated values are calculate without having to use MapReduce. With this option is easy perform tasks such as totaling or averaging field values: .. code-block:: php [ "category" => 1, ], ], [ "\$group" => [ "_id" => [ "category" => "\$category" ], "id" => [ "\$max" => "\$_id", ], ], ], ] ); レコードの作成、更新 ------------------------- The :code:`Phalcon\Mvc\Collection::save()` method allows you to create/update documents according to whether they already exist in the collection associated with a model. The :code:`save()` method is called internally by the create and update methods of :doc:`Phalcon\\Mvc\\Collection <../api/Phalcon_Mvc_Collection>`. Also the method executes associated validators and events that are defined in the model: .. code-block:: php type = "mechanical"; $robot->name = "Astro Boy"; $robot->year = 1952; if ($robot->save() === false) { echo "Umh, We can't store robots right now: \n"; $messages = $robot->getMessages(); foreach ($messages as $message) { echo $message, "\n"; } } else { echo "Great, a new robot was saved successfully!"; } The "_id" property is automatically updated with the MongoId_ object created by the driver: .. code-block:: php save(); echo "The generated id is: ", $robot->getId(); バリデーション・メッセージ ^^^^^^^^^^^^^^^^^^^^^^^^^^ :doc:`Phalcon\\Mvc\\Collection <../api/Phalcon_Mvc_Collection>` has a messaging subsystem that provides a flexible way to output or store the validation messages generated during the insert/update processes. Each message consists of an instance of the class :doc:`Phalcon\\Mvc\\Model\\Message <../api/Phalcon_Mvc_Model_Message>`. The set of messages generated can be retrieved with the method getMessages(). Each message provides extended information like the field name that generated the message or the message type: .. code-block:: php save() === false) { $messages = $robot->getMessages(); foreach ($messages as $message) { echo "Message: ", $message->getMessage(); echo "Field: ", $message->getField(); echo "Type: ", $message->getType(); } } バリデーション・イベントとイベント・マネージャ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Models allow you to implement events that will be thrown when performing an insert or update. They help define business rules for a certain model. The following are the events supported by :doc:`Phalcon\\Mvc\\Collection <../api/Phalcon_Mvc_Collection>` and their order of execution: +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Operation | Name | Can stop operation? | Explanation | +====================+==================================+=======================+====================================================================================================================+ | Inserting/Updating | :code:`beforeValidation` | YES | Is executed before the validation process and the final insert/update to the database | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Inserting | :code:`beforeValidationOnCreate` | YES | Is executed before the validation process only when an insertion operation is being made | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Updating | :code:`beforeValidationOnUpdate` | YES | Is executed before the fields are validated for not nulls or foreign keys when an updating operation is being made | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Inserting/Updating | :code:`onValidationFails` | YES (already stopped) | Is executed before the validation process only when an insertion operation is being made | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Inserting | :code:`afterValidationOnCreate` | YES | Is executed after the validation process when an insertion operation is being made | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Updating | :code:`afterValidationOnUpdate` | YES | Is executed after the validation process when an updating operation is being made | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Inserting/Updating | :code:`afterValidation` | YES | Is executed after the validation process | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Inserting/Updating | :code:`beforeSave` | YES | Runs before the required operation over the database system | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Updating | :code:`beforeUpdate` | YES | Runs before the required operation over the database system only when an updating operation is being made | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Inserting | :code:`beforeCreate` | YES | Runs before the required operation over the database system only when an inserting operation is being made | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Updating | :code:`afterUpdate` | NO | Runs after the required operation over the database system only when an updating operation is being made | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Inserting | :code:`afterCreate` | NO | Runs after the required operation over the database system only when an inserting operation is being made | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ | Inserting/Updating | :code:`afterSave` | NO | Runs after the required operation over the database system | +--------------------+----------------------------------+-----------------------+--------------------------------------------------------------------------------------------------------------------+ To make a model to react to an event, we must to implement a method with the same name of the event: .. code-block:: php created_at = date("Y-m-d H:i:s"); } public function beforeUpdate() { // Set the modification date $this->modified_in = date("Y-m-d H:i:s"); } } Additionally, this component is integrated with :doc:`Phalcon\\Events\\Manager `, this means we can create listeners that run when an event is triggered. .. code-block:: php attach( "collection:beforeSave", function (Event $event, $robot) { if ($robot->name === "Scooby Doo") { echo "Scooby Doo isn't a robot!"; return false; } return true; } ); $robot = new Robots(); $robot->setEventsManager($eventsManager); $robot->name = "Scooby Doo"; $robot->year = 1969; $robot->save(); In the example given above the EventsManager only acted as a bridge between an object and a listener (the anonymous function). If we want all objects created in our application use the same EventsManager, then we need to assign this to the Models Manager: .. code-block:: php set( "collectionManager", function () { $eventsManager = new EventsManager(); // Attach an anonymous function as a listener for "model" events $eventsManager->attach( "collection:beforeSave", function (Event $event, $model) { if (get_class($model) === "Robots") { if ($model->name === "Scooby Doo") { echo "Scooby Doo isn't a robot!"; return false; } } return true; } ); // Setting a default EventsManager $modelsManager = new CollectionManager(); $modelsManager->setEventsManager($eventsManager); return $modelsManager; }, true ); ビジネス・ルールの実装 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When an insert, update or delete is executed, the model verifies if there are any methods with the names of the events listed in the table above. We recommend that validation methods are declared protected to prevent that business logic implementation from being exposed publicly. The following example implements an event that validates the year cannot be smaller than 0 on update or insert: .. code-block:: php year < 0) { echo "Year cannot be smaller than zero!"; return false; } } } Some events return false as an indication to stop the current operation. If an event doesn't return anything, :doc:`Phalcon\\Mvc\\Collection <../api/Phalcon_Mvc_Collection>` will assume a true value. データ整合性の検証 ^^^^^^^^^^^^^^^^^^^^^^^^^ :doc:`Phalcon\\Mvc\\Collection <../api/Phalcon_Mvc_Collection>` provides several events to validate data and implement business rules. The special "validation" event allows us to call built-in validators over the record. Phalcon exposes a few built-in validators that can be used at this stage of validation. The following example shows how to use it: .. code-block:: php validate( new InclusionIn( [ "field" => "type", "message" => "Type must be: mechanical or virtual", "domain" => [ "Mechanical", "Virtual", ], ] ) ); $this->validate( new Numericality( [ "field" => "price", "message" => "Price must be numeric", ] ) ); return $this->validationHasFailed() !== true; } } The example given above performs a validation using the built-in validator "InclusionIn". It checks the value of the field "type" in a domain list. If the value is not included in the method, then the validator will fail and return false. The following built-in validators are available: +-------------------------------------------------------------------------------------------------------+------------------------------------------------------------------+ | Name | Explanation | +=======================================================================================================+==================================================================+ | :doc:`Phalcon\\Mvc\\Model\\Validator\\Email <../api/Phalcon_Mvc_Model_Validator_Email>` | Validates that field contains a valid email format | +-------------------------------------------------------------------------------------------------------+------------------------------------------------------------------+ | :doc:`Phalcon\\Mvc\\Model\\Validator\\Exclusionin <../api/Phalcon_Mvc_Model_Validator_Exclusionin>` | Validates that a value is not within a list of possible values | +-------------------------------------------------------------------------------------------------------+------------------------------------------------------------------+ | :doc:`Phalcon\\Mvc\\Model\\Validator\\Inclusionin <../api/Phalcon_Mvc_Model_Validator_Inclusionin>` | Validates that a value is within a list of possible values | +-------------------------------------------------------------------------------------------------------+------------------------------------------------------------------+ | :doc:`Phalcon\\Mvc\\Model\\Validator\\Numericality <../api/Phalcon_Mvc_Model_Validator_Numericality>` | Validates that a field has a numeric format | +-------------------------------------------------------------------------------------------------------+------------------------------------------------------------------+ | :doc:`Phalcon\\Mvc\\Model\\Validator\\Regex <../api/Phalcon_Mvc_Model_Validator_Regex>` | Validates that the value of a field matches a regular expression | +-------------------------------------------------------------------------------------------------------+------------------------------------------------------------------+ | :doc:`Phalcon\\Mvc\\Model\\Validator\\StringLength <../api/Phalcon_Mvc_Model_Validator_StringLength>` | Validates the length of a string | +-------------------------------------------------------------------------------------------------------+------------------------------------------------------------------+ In addition to the built-in validators, you can create your own validators: .. code-block:: php getOption("field"); $value = $model->$field; $filtered = filter_var($value, FILTER_VALIDATE_URL); if (!$filtered) { $this->appendMessage( "The URL is invalid", $field, "UrlValidator" ); return false; } return true; } } Adding the validator to a model: .. code-block:: php validate( new UrlValidator( [ "field" => "url", ] ) ); if ($this->validationHasFailed() === true) { return false; } } } The idea of creating validators is to make them reusable across several models. A validator can also be as simple as: .. code-block:: php type === "Old") { $message = new ModelMessage( "Sorry, old robots are not allowed anymore", "type", "MyType" ); $this->appendMessage($message); return false; } return true; } } レコードの削除 ---------------- The :code:`Phalcon\Mvc\Collection::delete()` method allows you to delete a document. You can use it as follows: .. code-block:: php delete() === false) { echo "Sorry, we can't delete the robot right now: \n"; $messages = $robot->getMessages(); foreach ($messages as $message) { echo $message, "\n"; } } else { echo "The robot was deleted successfully!"; } } You can also delete many documents by traversing a resultset with a :code:`foreach` loop: .. code-block:: php "mechanical", ] ] ); foreach ($robots as $robot) { if ($robot->delete() === false) { echo "Sorry, we can't delete the robot right now: \n"; $messages = $robot->getMessages(); foreach ($messages as $message) { echo $message, "\n"; } } else { echo "The robot was deleted successfully!"; } } The following events are available to define custom business rules that can be executed when a delete operation is performed: +-----------+----------------------+---------------------+------------------------------------------+ | Operation | Name | Can stop operation? | Explanation | +===========+======================+=====================+==========================================+ | Deleting | :code:`beforeDelete` | YES | Runs before the delete operation is made | +-----------+----------------------+---------------------+------------------------------------------+ | Deleting | :code:`afterDelete` | NO | Runs after the delete operation was made | +-----------+----------------------+---------------------+------------------------------------------+ バリデーション失敗のイベント ---------------------------- Another type of events is available when the data validation process finds any inconsistency: +--------------------------+---------------------------+--------------------------------------------------------------------+ | Operation | Name | Explanation | +==========================+===========================+====================================================================+ | Insert or Update | :code:`notSave` | Triggered when the insert/update operation fails for any reason | +--------------------------+---------------------------+--------------------------------------------------------------------+ | Insert, Delete or Update | :code:`onValidationFails` | Triggered when any data manipulation operation fails | +--------------------------+---------------------------+--------------------------------------------------------------------+ 暗黙のID VS. ユーザー指定プライマリキー --------------------------------------- By default :doc:`Phalcon\\Mvc\\Collection <../api/Phalcon_Mvc_Collection>` assumes that the :code:`_id` attribute is automatically generated using MongoIds_. If a model uses custom primary keys this behavior can be overridden: .. code-block:: php useImplicitObjectIds(false); } } 複数のデータベースの設定 -------------------------- In Phalcon, all models can belong to the same database connection or have an individual one. Actually, when :doc:`Phalcon\\Mvc\\Collection <../api/Phalcon_Mvc_Collection>` needs to connect to the database it requests the "mongo" service in the application's services container. You can overwrite this service setting it in the initialize method: .. code-block:: php set( "mongo1", function () { $mongo = new MongoClient( "mongodb://scott:nekhen@192.168.1.100" ); return $mongo->selectDB("management"); }, true ); // This service returns a mongo database at localhost $di->set( "mongo2", function () { $mongo = new MongoClient( "mongodb://localhost" ); return $mongo->selectDB("invoicing"); }, true ); Then, in the :code:`initialize()` method, we define the connection service for the model: .. code-block:: php setConnectionService("mongo1"); } } モデルにサービスを注入する ------------------------------ You may be required to access the application services within a model, the following example explains how to do that: .. code-block:: php getDI()->getShared("flash"); $messages = $this->getMessages(); // Show validation messages foreach ($messages as $message) { $flash->error( (string) $message ); } } } The "notSave" event is triggered whenever a "creating" or "updating" action fails. We're flashing the validation messages obtaining the "flash" service from the DI container. By doing this, we don't have to print messages after each saving. .. _MongoDB: http://www.mongodb.org/ .. _MongoId: http://www.php.net/manual/en/class.mongoid.php .. _MongoIds: http://www.php.net/manual/en/class.mongoid.php .. _`SQL to Mongo Mapping Chart`: http://www.php.net/manual/en/mongo.sqltomongo.php .. _`aggregation framework`: http://docs.mongodb.org/manual/applications/aggregation/