Working with Models

A model represents the information (data) of the application and the rules to manipulate that data. Models are primarily used for managing the rules of interaction with a corresponding database table. In most cases, each table in your database will correspond to one model in your application. The bulk of your application’s business logic will be concentrated in the models.

Phalcon\Mvc\Model is the base for all models in a Phalcon application. It provides database independence, basic CRUD functionality, advanced finding capabilities, and the ability to relate models to one another, among other services. Phalcon\Mvc\Model avoids the need of having to use SQL statements because it translates methods dynamically to the respective database engine operations.

Models are intended to work with the database on a high layer of abstraction. If you need to work with databases at a lower level check out the Phalcon\Db component documentation.

Creating Models

A model is a class that extends from Phalcon\Mvc\Model. Its class name should be in camel case notation:

namespace Store\Toys;

use Phalcon\Mvc\Model;

class RobotParts extends Model
{

}
If you’re using PHP 5.4/5.5 it is recommended you declare each column that makes part of the model in order to save memory and reduce the memory allocation.

By default, the model “Store\Toys\RobotParts” will refer to the table “robot_parts”. If you want to manually specify another name for the mapping table, you can use the setSource() method:

namespace Store\Toys;

use Phalcon\Mvc\Model;

class RobotParts extends Model
{
    public function initialize()
    {
        $this->setSource("toys_robot_parts");
    }
}

The model RobotParts now maps to “toys_robot_parts” table. The initialize() method aids in setting up the model with a custom behavior i.e. a different table.

The initialize() method is only called once during the request, it’s intended to perform initializations that apply for all instances of the model created within the application. If you want to perform initialization tasks for every instance created you can use the onConstruct() method:

namespace Store\Toys;

use Phalcon\Mvc\Model;

class RobotParts extends Model
{
    public function onConstruct()
    {
        // ...
    }
}

Public properties vs. Setters/Getters

Models can be implemented with properties of public scope, meaning that each property can be read/updated from any part of the code that has instantiated that model class without any restrictions:

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public $price;
}

By using getters and setters you can control which properties are visible publicly perform various transformations to the data (which would be impossible otherwise) and also add validation rules to the data stored in the object:

namespace Store\Toys;

use InvalidArgumentException;
use Phalcon\Mvc\Model;

class Robots extends Model
{
    protected $id;

    protected $name;

    protected $price;

    public function getId()
    {
        return $this->id;
    }

    public function setName($name)
    {
        // The name is too short?
        if (strlen($name) < 10) {
            throw new InvalidArgumentException(
                "The name is too short"
            );
        }

        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setPrice($price)
    {
        // Negative prices aren't allowed
        if ($price < 0) {
            throw new InvalidArgumentException(
                "Price can't be negative"
            );
        }

        $this->price = $price;
    }

    public function getPrice()
    {
        // Convert the value to double before be used
        return (double) $this->price;
    }
}

Public properties provide less complexity in development. However getters/setters can heavily increase the testability, extensibility and maintainability of applications. Developers can decide which strategy is more appropriate for the application they are creating. The ORM is compatible with both schemes of defining properties.

Underscores in property names can be problematic when using getters and setters.

If you use underscores in your property names, you must still use camel case in your getter/setter declarations for use with magic methods. (e.g. $model->getPropertyName instead of $model->getProperty_name, $model->findByPropertyName instead of $model->findByProperty_name, etc.). As much of the system expects camel case, and underscores are commonly removed, it is recommended to name your properties in the manner shown throughout the documentation. You can use a column map (as described above) to ensure proper mapping of your properties to their database counterparts.

Understanding Records To Objects

Every instance of a model represents a row in the table. You can easily access record data by reading object properties. For example, for a table “robots” with the records:

mysql> select * from robots;
+----+------------+------------+------+
| id | name       | type       | year |
+----+------------+------------+------+
|  1 | Robotina   | mechanical | 1972 |
|  2 | Astro Boy  | mechanical | 1952 |
|  3 | Terminator | cyborg     | 2029 |
+----+------------+------------+------+
3 rows in set (0.00 sec)

You could find a certain record by its primary key and then print its name:

use Store\Toys\Robots;

// Find record with id = 3
$robot = Robots::findFirst(3);

// Prints "Terminator"
echo $robot->name;

Once the record is in memory, you can make modifications to its data and then save changes:

use Store\Toys\Robots;

$robot = Robots::findFirst(3);

$robot->name = "RoboCop";

$robot->save();

As you can see, there is no need to use raw SQL statements. Phalcon\Mvc\Model provides high database abstraction for web applications.

Finding Records

Phalcon\Mvc\Model also offers several methods for querying records. The following examples will show you how to query one or more records from a model:

use Store\Toys\Robots;

// How many robots are there?
$robots = Robots::find();
echo "There are ", count($robots), "\n";

// How many mechanical robots are there?
$robots = Robots::find("type = 'mechanical'");
echo "There are ", count($robots), "\n";

// Get and print virtual robots ordered by name
$robots = Robots::find(
    [
        "type = 'virtual'",
        "order" => "name",
    ]
);
foreach ($robots as $robot) {
    echo $robot->name, "\n";
}

// Get first 100 virtual robots ordered by name
$robots = Robots::find(
    [
        "type = 'virtual'",
        "order" => "name",
        "limit" => 100,
    ]
);
foreach ($robots as $robot) {
   echo $robot->name, "\n";
}
If you want find record by external data (such as user input) or variable data you must use Binding Parameters.

You could also use the findFirst() method to get only the first record matching the given criteria:

use Store\Toys\Robots;

// What's the first robot in robots table?
$robot = Robots::findFirst();
echo "The robot name is ", $robot->name, "\n";

// What's the first mechanical robot in robots table?
$robot = Robots::findFirst("type = 'mechanical'");
echo "The first mechanical robot name is ", $robot->name, "\n";

// Get first virtual robot ordered by name
$robot = Robots::findFirst(
    [
        "type = 'virtual'",
        "order" => "name",
    ]
);
echo "The first virtual robot name is ", $robot->name, "\n";

Both find() and findFirst() methods accept an associative array specifying the search criteria:

use Store\Toys\Robots;

$robot = Robots::findFirst(
    [
        "type = 'virtual'",
        "order" => "name DESC",
        "limit" => 30,
    ]
);

$robots = Robots::find(
    [
        "conditions" => "type = ?1",
        "bind"       => [
            1 => "virtual",
        ]
    ]
);

The available query options are:

Parameter Description Example
conditions Search conditions for the find operation. Is used to extract only those records that fulfill a specified criterion. By default Phalcon\Mvc\Model assumes the first parameter are the conditions. "conditions" => "name LIKE 'steve%'"
columns Return specific columns instead of the full columns in the model. When using this option an incomplete object is returned "columns" => "id, name"
bind Bind is used together with options, by replacing placeholders and escaping values thus increasing security "bind" => ["status" => "A", "type" => "some-time"]
bindTypes When binding parameters, you can use this parameter to define additional casting to the bound parameters increasing even more the security "bindTypes" => [Column::BIND_PARAM_STR, Column::BIND_PARAM_INT]
order Is used to sort the resultset. Use one or more fields separated by commas. "order" => "name DESC, status"
limit Limit the results of the query to results to certain range "limit" => 10
offset Offset the results of the query by a certain amount "offset" => 5
group Allows to collect data across multiple records and group the results by one or more columns "group" => "name, status"
for_update With this option, Phalcon\Mvc\Model reads the latest available data, setting exclusive locks on each row it reads "for_update" => true
shared_lock With this option, Phalcon\Mvc\Model reads the latest available data, setting shared locks on each row it reads "shared_lock" => true
cache Cache the resultset, reducing the continuous access to the relational system "cache" => ["lifetime" => 3600, "key" => "my-find-key"]
hydration Sets the hydration strategy to represent each returned record in the result "hydration" => Resultset::HYDRATE_OBJECTS

If you prefer, there is also available a way to create queries in an object-oriented way, instead of using an array of parameters:

use Store\Toys\Robots;

$robots = Robots::query()
    ->where("type = :type:")
    ->andWhere("year < 2000")
    ->bind(["type" => "mechanical"])
    ->order("name")
    ->execute();

The static method query() returns a Phalcon\Mvc\Model\Criteria object that is friendly with IDE autocompleters.

All the queries are internally handled as PHQL queries. PHQL is a high-level, object-oriented and SQL-like language. This language provide you more features to perform queries like joining other models, define groupings, add aggregations etc.

Lastly, there is the findFirstBy<property-name>() method. This method expands on the findFirst() method mentioned earlier. It allows you to quickly perform a retrieval from a table by using the property name in the method itself and passing it a parameter that contains the data you want to search for in that column. An example is in order, so taking our Robots model mentioned earlier:

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public $price;
}

We have three properties to work with here: $id, $name and $price. So, let’s say you want to retrieve the first record in the table with the name ‘Terminator’. This could be written like:

use Store\Toys\Robots;

$name = "Terminator";

$robot = Robots::findFirstByName($name);

if ($robot) {
    echo "The first robot with the name " . $name . " cost " . $robot->price . ".";
} else {
    echo "There were no robots found in our table with the name " . $name . ".";
}

Notice that we used ‘Name’ in the method call and passed the variable $name to it, which contains the name we are looking for in our table. Notice also that when we find a match with our query, all the other properties are available to us as well.

Model Resultsets

While findFirst() returns directly an instance of the called class (when there is data to be returned), the find() method returns a Phalcon\Mvc\Model\Resultset\Simple. This is an object that encapsulates all the functionality a resultset has like traversing, seeking specific records, counting, etc.

These objects are more powerful than standard arrays. One of the greatest features of the Phalcon\Mvc\Model\Resultset is that at any time there is only one record in memory. This greatly helps in memory management especially when working with large amounts of data.

use Store\Toys\Robots;

// Get all robots
$robots = Robots::find();

// Traversing with a foreach
foreach ($robots as $robot) {
    echo $robot->name, "\n";
}

// Traversing with a while
$robots->rewind();

while ($robots->valid()) {
    $robot = $robots->current();

    echo $robot->name, "\n";

    $robots->next();
}

// Count the resultset
echo count($robots);

// Alternative way to count the resultset
echo $robots->count();

// Move the internal cursor to the third robot
$robots->seek(2);

$robot = $robots->current();

// Access a robot by its position in the resultset
$robot = $robots[5];

// Check if there is a record in certain position
if (isset($robots[3])) {
   $robot = $robots[3];
}

// Get the first record in the resultset
$robot = $robots->getFirst();

// Get the last record
$robot = $robots->getLast();

Phalcon’s resultsets emulate scrollable cursors, you can get any row just by accessing its position, or seeking the internal pointer to a specific position. Note that some database systems don’t support scrollable cursors, this forces to re-execute the query in order to rewind the cursor to the beginning and obtain the record at the requested position. Similarly, if a resultset is traversed several times, the query must be executed the same number of times.

As storing large query results in memory could consume many resources, resultsets are obtained from the database in chunks of 32 rows - reducing the need to re-execute the request in several cases.

Note that resultsets can be serialized and stored in a cache backend. Phalcon\Cache can help with that task. However, serializing data causes Phalcon\Mvc\Model to retrieve all the data from the database in an array, thus consuming more memory while this process takes place.

// Query all records from model parts
$parts = Parts::find();

// Store the resultset into a file
file_put_contents(
    "cache.txt",
    serialize($parts)
);

// Get parts from file
$parts = unserialize(
    file_get_contents("cache.txt")
);

// Traverse the parts
foreach ($parts as $part) {
    echo $part->id;
}

Filtering Resultsets

The most efficient way to filter data is setting some search criteria, databases will use indexes set on tables to return data faster. Phalcon additionally allows you to filter the data using PHP using any resource that is not available in the database:

$customers = Customers::find();

$customers = $customers->filter(
    function ($customer) {
        // Return only customers with a valid e-mail
        if (filter_var($customer->email, FILTER_VALIDATE_EMAIL)) {
            return $customer;
        }
    }
);

Binding Parameters

Bound parameters are also supported in Phalcon\Mvc\Model. You are encouraged to use this methodology so as to eliminate the possibility of your code being subject to SQL injection attacks. Both string and integer placeholders are supported. Binding parameters can simply be achieved as follows:

use Store\Toys\Robots;

// Query robots binding parameters with string placeholders
// Parameters whose keys are the same as placeholders
$robots = Robots::find(
    [
        "name = :name: AND type = :type:",
        "bind" => [
            "name" => "Robotina",
            "type" => "maid",
        ],
    ]
);

// Query robots binding parameters with integer placeholders
$robots = Robots::find(
    [
        "name = ?1 AND type = ?2",
        "bind" => [
            1 => "Robotina",
            2 => "maid",
        ],
    ]
);

// Query robots binding parameters with both string and integer placeholders
// Parameters whose keys are the same as placeholders
$robots = Robots::find(
    [
        "name = :name: AND type = ?1",
        "bind" => [
            "name" => "Robotina",
            1      => "maid",
        ],
    ]
);

When using numeric placeholders, you will need to define them as integers i.e. 1 or 2. In this case “1” or “2” are considered strings and not numbers, so the placeholder could not be successfully replaced.

Strings are automatically escaped using PDO. This function takes into account the connection charset, so its recommended to define the correct charset in the connection parameters or in the database configuration, as a wrong charset will produce undesired effects when storing or retrieving data.

Additionally you can set the parameter “bindTypes”, this allows defining how the parameters should be bound according to its data type:

use Phalcon\Db\Column;
use Store\Toys\Robots;

// Bind parameters
$parameters = [
    "name" => "Robotina",
    "year" => 2008,
];

// Casting Types
$types = [
    "name" => Column::BIND_PARAM_STR,
    "year" => Column::BIND_PARAM_INT,
];

// Query robots binding parameters with string placeholders
$robots = Robots::find(
    [
        "name = :name: AND year = :year:",
        "bind"      => $parameters,
        "bindTypes" => $types,
    ]
);
Since the default bind-type is Phalcon\Db\Column::BIND_PARAM_STR, there is no need to specify the “bindTypes” parameter if all of the columns are of that type.

If you bind arrays in bound parameters, keep in mind, that keys must be numbered from zero:

use Store\Toys\Robots;

$array = ["a","b","c"]; // $array: [[0] => "a", [1] => "b", [2] => "c"]

unset($array[1]); // $array: [[0] => "a", [2] => "c"]

// Now we have to renumber the keys
$array = array_values($array); // $array: [[0] => "a", [1] => "c"]

$robots = Robots::find(
    [
        'letter IN ({letter:array})',
        'bind' => [
            'letter' => $array
        ]
    ]
);
Bound parameters are available for all query methods such as find() and findFirst() but also the calculation methods like count(), sum(), average() etc.

If you’re using “finders”, bound parameters are automatically used for you:

use Store\Toys\Robots;

// Explicit query using bound parameters
$robots = Robots::find(
    [
        "name = ?0",
        "bind" => [
            "Ultron",
        ],
    ]
);

// Implicit query using bound parameters
$robots = Robots::findByName("Ultron");

Initializing/Preparing fetched records

May be the case that after obtaining a record from the database is necessary to initialise the data before being used by the rest of the application. You can implement the afterFetch() method in a model, this event will be executed just after create the instance and assign the data to it:

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public $status;

    public function beforeSave()
    {
        // Convert the array into a string
        $this->status = join(",", $this->status);
    }

    public function afterFetch()
    {
        // Convert the string to an array
        $this->status = explode(",", $this->status);
    }

    public function afterSave()
    {
        // Convert the string to an array
        $this->status = explode(",", $this->status);
    }
}

If you use getters/setters instead of/or together with public properties, you can initialize the field once it is accessed:

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public $status;

    public function getStatus()
    {
        return explode(",", $this->status);
    }
}

Generating Calculations

Calculations (or aggregations) are helpers for commonly used functions of database systems such as COUNT, SUM, MAX, MIN or AVG. Phalcon\Mvc\Model allows to use these functions directly from the exposed methods.

Count examples:

// How many employees are?
$rowcount = Employees::count();

// How many different areas are assigned to employees?
$rowcount = Employees::count(
    [
        "distinct" => "area",
    ]
);

// How many employees are in the Testing area?
$rowcount = Employees::count(
    "area = 'Testing'"
);

// Count employees grouping results by their area
$group = Employees::count(
    [
        "group" => "area",
    ]
);
foreach ($group as $row) {
   echo "There are ", $row->rowcount, " in ", $row->area;
}

// Count employees grouping by their area and ordering the result by count
$group = Employees::count(
    [
        "group" => "area",
        "order" => "rowcount",
    ]
);

// Avoid SQL injections using bound parameters
$group = Employees::count(
    [
        "type > ?0",
        "bind" => [
            $type
        ],
    ]
);

Sum examples:

// How much are the salaries of all employees?
$total = Employees::sum(
    [
        "column" => "salary",
    ]
);

// How much are the salaries of all employees in the Sales area?
$total = Employees::sum(
    [
        "column"     => "salary",
        "conditions" => "area = 'Sales'",
    ]
);

// Generate a grouping of the salaries of each area
$group = Employees::sum(
    [
        "column" => "salary",
        "group"  => "area",
    ]
);
foreach ($group as $row) {
   echo "The sum of salaries of the ", $row->area, " is ", $row->sumatory;
}

// Generate a grouping of the salaries of each area ordering
// salaries from higher to lower
$group = Employees::sum(
    [
        "column" => "salary",
        "group"  => "area",
        "order"  => "sumatory DESC",
    ]
);

// Avoid SQL injections using bound parameters
$group = Employees::sum(
    [
        "conditions" => "area > ?0",
        "bind"       => [
            $area
        ],
    ]
);

Average examples:

// What is the average salary for all employees?
$average = Employees::average(
    [
        "column" => "salary",
    ]
);

// What is the average salary for the Sales's area employees?
$average = Employees::average(
    [
        "column"     => "salary",
        "conditions" => "area = 'Sales'",
    ]
);

// Avoid SQL injections using bound parameters
$average = Employees::average(
    [
        "column"     => "age",
        "conditions" => "area > ?0",
        "bind"       => [
            $area
        ],
    ]
);

Max/Min examples:

// What is the oldest age of all employees?
$age = Employees::maximum(
    [
        "column" => "age",
    ]
);

// What is the oldest of employees from the Sales area?
$age = Employees::maximum(
    [
        "column"     => "age",
        "conditions" => "area = 'Sales'",
    ]
);

// What is the lowest salary of all employees?
$salary = Employees::minimum(
    [
        "column" => "salary",
    ]
);

Creating/Updating Records

The Phalcon\Mvc\Model::save() method allows you to create/update records according to whether they already exist in the table associated with a model. The save method is called internally by the create and update methods of Phalcon\Mvc\Model. For this to work as expected it is necessary to have properly defined a primary key in the entity to determine whether a record should be updated or created.

Also the method executes associated validators, virtual foreign keys and events that are defined in the model:

use Store\Toys\Robots;

$robot = new Robots();

$robot->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!";
}

An array could be passed to “save” to avoid assign every column manually. Phalcon\Mvc\Model will check if there are setters implemented for the columns passed in the array giving priority to them instead of assign directly the values of the attributes:

use Store\Toys\Robots;

$robot = new Robots();

$robot->save(
    [
        "type" => "mechanical",
        "name" => "Astro Boy",
        "year" => 1952,
    ]
);

Values assigned directly or via the array of attributes are escaped/sanitized according to the related attribute data type. So you can pass an insecure array without worrying about possible SQL injections:

use Store\Toys\Robots;

$robot = new Robots();

$robot->save($_POST);
Without precautions mass assignment could allow attackers to set any database column’s value. Only use this feature if you want to permit a user to insert/update every column in the model, even if those fields are not in the submitted form.

You can set an additional parameter in ‘save’ to set a whitelist of fields that only must taken into account when doing the mass assignment:

use Store\Toys\Robots;

$robot = new Robots();

$robot->save(
    $_POST,
    [
        "name",
        "type",
    ]
);

Create/Update with Confidence

When an application has a lot of competition, we could be expecting create a record but it is actually updated. This could happen if we use Phalcon\Mvc\Model::save() to persist the records in the database. If we want to be absolutely sure that a record is created or updated, we can change the save() call with create() or update():

use Store\Toys\Robots;

$robot = new Robots();

$robot->type = "mechanical";
$robot->name = "Astro Boy";
$robot->year = 1952;

// This record only must be created
if ($robot->create() === 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 created successfully!";
}

These methods “create” and “update” also accept an array of values as parameter.

Deleting Records

The Phalcon\Mvc\Model::delete() method allows to delete a record. You can use it as follows:

use Store\Toys\Robots;

$robot = Robots::findFirst(11);

if ($robot !== false) {
    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!";
    }
}

You can also delete many records by traversing a resultset with a foreach:

use Store\Toys\Robots;

$robots = Robots::find(
    "type = '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 beforeDelete YES Runs before the delete operation is made
Deleting afterDelete NO Runs after the delete operation was made

With the above events can also define business rules in the models:

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public function beforeDelete()
    {
        if ($this->status === "A") {
            echo "The robot is active, it can't be deleted";

            return false;
        }

        return true;
    }
}
doc_Phalcon
2016-10-16 09:57:36
Comments
Leave a Comment

Please login to continue.