Models¶
Overview¶
The Phalcon\Mvc\Model is the M
in MVC. It is a class that connects business objects and database tables, to create a persistent domain model, where logic and data are wrapped into one. It is an implementation of the object-relational mapping (ORM).
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.
The Phalcon\Mvc\Model is the first ORM written in Zephir/C languages for PHP, giving to developers high performance when interacting with databases while is also easy to use.
NOTE
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.
<?php
use MyApp\Models\Invoices;
$invoice = new Invoices();
$invoice->inv_cst_id = 1;
$invoice->inv_status_flag = 1;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 100;
$invoice->inv_created_at = '2019-12-25 01:02:03';
$result = $invoice->save();
if (false === $result) {
echo 'Error saving Invoice: ';
$messages = $invoice->getMessages();
foreach ($messages as $message) {
echo $message . PHP_EOL;
}
} else {
echo 'Record Saved';
}
NOTE
For information on how to create a model please check the Creating Models section
Constants¶
Constant | Value |
---|---|
DIRTY_STATE_DETACHED | 2 |
DIRTY_STATE_PERSISTENT | 0 |
DIRTY_STATE_TRANSIENT | 1 |
OP_CREATE | 1 |
OP_DELETE | 3 |
OP_NONE | 0 |
OP_UPDATE | 2 |
TRANSACTION_INDEX | 'transaction' |
Methods¶
final public function __construct(
mixed $data = null,
DiInterface $container = null,
ManagerInterface $modelsManager = null
)
assign
. Optionally you can pass a DI container and a Models Manager object. If they are not passed, the defaults will be used. Handles method calls when a method is not implemented. Throws Phalcon\Mvc\Model\Exception if the method doesn't exist Handles method calls when a static method is not implemented. Throws Phalcon\Mvc\Model\Exception if the method doesn't exist Magic method to get related records using the relation alias as a property Magic method to check if a property is a valid relation Magic method to assign values to the the model Setups a behavior in a model <?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Behavior\Timestampable;
class Invoices extends Model
{
public function initialize()
{
$this->addBehavior(
new Timestampable(
[
'onCreate' => [
'field' => 'inv_created_at',
'format' => 'Y-m-d H:i:s',
],
]
)
);
}
}
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
use Phalcon\Messages\Message as Message;
class Invoices extends Model
{
public function beforeSave()
{
if (0 === $this->inv_status_flag) {
$message = new Message(
'Sorry, an invoice cannot be unpaid'
);
$this->appendMessage($message);
}
}
}
public function assign(
mixed $data,
array $whiteList = null,
array $dataColumnMap = null
): ModelInterface
data
parameter can be an array or a database row. The whitelist
is an array of model properties that will be updated during the assignment process. Omitted properties will NOT be accepted even if they are included in the array or database row; nevertheless if one of them is required by the model, the data will not be saved and the model will produce an error. The dataColumnMap
is an array that maps columns from the data
to the actual model. This helps when you want to map input from an array such as $_POST
to fields in the database. Assign values to a model from an array
<?php
$invoice->assign(
[
'inv_cst_id' => 1,
'inv_status_flag' => 1,
'inv_title' => 'Invoice for ACME Inc.',
'inv_total' => 100,
'inv_created_at' => '2019-12-25 01:02:03',
]
);
assign
with a database row. - Requires a Column Map
<?php
$invoice->assign(
$row,
null,
[
'inv_cst_id' => 'customerId',
'inv_status_flag' => 'status',
'inv_title' => 'title',
'inv_total' => 'total',
]
);
Update only the inv_status_flag
, inv_title
, inv_total
fields.
By default assign
will use setters if they exist, you can disable it by using ini_set
to directly use properties
<?php
use MyApp\Models\Invoices;
$average = Invoices::average(
[
'column' => 'inv_total',
]
);
echo 'AVG: ', $average, PHP_EOL;
$average = Invoices::average(
[
'inv_cst_id = 1',
'column' => 'inv_total',
]
);
echo 'AVG [Customer: 1] ', $average, PHP_EOL;
public static function cloneResult(
ModelInterface $base,
array $data,
int $dirtyState = 0
): ModelInterface
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::cloneResult(
new Invoices(),
[
'inv_cst_id' => 1,
'inv_status_flag' => 0,
'inv_title' => 'Invoice for ACME Inc. #2',
'inv_total' => 400,
'inv_created_at' => '2019-12-25 01:02:03',
]
);
public static function cloneResultMap(
mixed $base,
array $data,
array $columnMap,
int $dirtyState = 0,
bool $keepSnapshots = null
): ModelInterface
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::cloneResultMap(
new Invoices(),
[
'customerId' => 1,
'status' => 0,
'title' => 'Invoice for ACME Inc. #2',
'total' => 400,
'created' => '2019-12-25 01:02:03',
]
);
public static function cloneResultMapHydrate(
array $data,
array $columnMap,
int $hydrationMode
): mixed
<?php
use MyApp\Models\Invoices;
$average = Invoices::count();
echo 'COUNT: ', $average, PHP_EOL;
$average = Invoices::count(
'inv_cst_id = 1'
);
echo 'COUNT [Customer: 1] ', $average, PHP_EOL;
create()
will throw an exception. It will return true
on success, false
otherwise. <?php
use MyApp\Models\Invoices;
$invoice = new Invoices();
$invoice->assign(
[
'inv_cst_id' => 1,
'inv_status_flag' => 1,
'inv_title' => 'Invoice for ACME Inc.',
'inv_total' => 100,
'inv_created_at' => '2019-12-25 01:02:03',
]
);
$result = $invoice->create();
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::findFirst('inv_id = 4');
$result = $invoice->delete();
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 1,
]
]
);
foreach ($invoices as $invoice) {
$invoice->delete();
}
var_dump()
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::findFirst('inv_id = 4');
var_dump(
$invoice->dump()
);
find()
is flexible enough to accept a variety of parameters to find the data required. You can check the Finding Records section for more information. Query the first record that matches the specified conditions. It will return a resultset or null
if the record was not found. NOTE
findFirst()
no longer returns false
if records were not found.
false
Returns a list of changed values. <?php
use MyApp\Models\Invoices;
$invoice = Invoices::findFirst();
print_r(
$invoice->getChangedFields()
);
// []
$invoice->inv_total = 120;;
$invoice->getChangedFields();
print_r(
$invoice->getChangedFields()
);
// ['inv_total']
DIRTY_STATE_*
constants telling if the record exists in the database or not Returns array of validation messages <?php
use MyApp\Models\Invoices;
$invoice = new Invoices();
$invoice->inv_cst_id = 1;
$invoice->inv_status_flag = 1;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 100;
$invoice->inv_created_at = '2019-12-25 01:02:03';
$result = $invoice->save();
if (false === $result) {
echo 'Error saving Invoice: ';
$messages = $invoice->getMessages();
foreach ($messages as $message) {
echo $message . PHP_EOL;
}
} else {
echo 'Record Saved';
}
NOTE
save()
no longer accepts parameters to set data. You can use assign
instead.
OP_*
class constants Returns the internal old snapshot data Gets the connection used to read data for the model Returns the DependencyInjection connection service name used to read data related the model public function getRelated(
string $alias,
mixed $arguments = null
): Phalcon\Mvc\Model\Resultset\Simple | null
null
NOTE
getRelated()
no longer returns false
if a record was not found on a one to one relationship.
<?php
use MyApp\Models\Customers;
$customer = Customers::findFirst('cst_id = 1');
$invoices = $customer->getRelated('invoices');
true
if the records were previously fetched through the model without any additional parameters. <?php
use MyApp\Models\Customers;
$customer = Customers::findFirst('cst_id = 1');
$invoices = $customer->isRelationshipLoaded('invoices'); // false
$invoices = $customer->getRelated('invoices');
$invoices = $customer->isRelationshipLoaded('invoices'); // true
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::findFirst();
print_r(
$invoice->getChangedFields()
);
// []
$invoice->inv_total = 120;;
$invoice->getChangedFields();
print_r(
$invoice->getChangedFields()
);
// ['inv_total']
$invoice->save();
print_r(
$invoice->getChangedFields()
);
// []
print_r(
$invoice->getUpdatedFields()
);
// ['inv_total']
<?php
use MyApp\Models\Invoices;
$invoice = new Invoices();
$invoice->inv_cst_id = 1;
$invoice->inv_status_flag = 1;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 100;
$invoice->inv_created_at = '2019-12-25 01:02:03';
$result = $invoice->create();
$invoice->inv_total = 120;
$hasChanged = $invoice->hasChanged('inv_title');
// false
$hasChanged = $invoice->hasChanged(
[
'inv_total',
]
);
// true
$hasChanged = $invoice->hasChanged(
[
'inv_title',
'inv_total'
],
true
);
// false
<?php
use MyApp\Models\Invoices;
$id = Invoices::maximum(
[
'column' => 'inv_id',
]
);
echo 'MAX: ', $id, PHP_EOL;
$max = Invoices::maximum(
[
'inv_cst_id = 1',
'column' => 'inv_total',
]
);
echo 'MAX [Customer: 1] ', $max, PHP_EOL;
<?php
use MyApp\Models\Invoices;
$id = Invoices::minimum(
[
'column' => 'inv_id',
]
);
echo 'MIN: ', $id, PHP_EOL;
$max = Invoices::minimum(
[
'inv_cst_id = 1',
'column' => 'inv_total',
]
);
echo 'MIN [Customer: 1] ', $max, PHP_EOL;
true
on success or false
otherwise. <?php
use MyApp\Models\Invoices;
$invoice = new Invoices();
$invoice->inv_cst_id = 1;
$invoice->inv_status_flag = 1;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 100;
$invoice->inv_created_at = '2019-12-25 01:02:03';
$result = $invoice->save();
$invoice = Invoices::findFirst('inv_id = 100');
$invoice->inv_total = 120;
$invoice->save();
NOTE
save()
no longer accepts parameters to set data. You can use assign
instead.
DIRTY_STATE_*
constants Sets a custom events manager Sets the DependencyInjection connection service name used to read data Sets the record's old snapshot data. This method is used internally to set old snapshot data when the model was set up to keep snapshot data Sets the record's snapshot data. This method is used internally to set snapshot data when the model was set up to keep snapshot data Sets a transaction related to the Model instance <?php
use MyApp\Models\Customers;
use MyApp\Models\Invoices;
use Phalcon\Mvc\Model\Transaction\Manager;
use Phalcon\Mvc\Model\Transaction\Failed;
try {
$txManager = new Manager();
$transaction = $txManager->get();
$customer = new Customers();
$customer->setTransaction($transaction);
$customer->cst_name_last = 'Vader';
$customer->cst_name_first = 'Darth';
if (false === $customer->save()) {
$transaction->rollback('Cannot save Customer');
}
$invoice = new Invoices();
$invoice->setTransaction($transaction);
$invoice->inv_cst_id = $customer->cst_id;
$invoice->inv_status_flag = 1;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 100;
$invoice->inv_created_at = '2019-12-25 01:02:03';
if (false === $invoice->save()) {
$transaction->rollback('Cannot save record');
}
$transaction->commit();
} catch (Failed $ex) {
echo 'ERROR: ', $ex->getMessage();
}
public function skipOperation(bool $skip): void
```php
Skips the current operation forcing a success state
```php
public static function sum(
array $parameters = null
): float
<?php
use MyApp\Models\Invoices;
$total = Invoices::sum(
[
'column' => 'inv_total',
]
);
echo 'SUM: ', $total, PHP_EOL;
$total = Invoices::sum(
[
'inv_cst_id = 1',
'column' => 'inv_total',
]
);
echo 'SUM [Customer: 1] ', $total, PHP_EOL;
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::findFirst('inv_id = 4');
print_r(
$invoice->toArray()
);
// [
// 'inv_id' => 4,
// 'inv_cst_id' = $customer->cst_id,
// 'inv_status_flag' = 1,
// 'inv_title' = 'Invoice for ACME Inc.',
// 'inv_total' = 100,
// 'inv_created_at' = '2019-12-25 01:02:03',
// ]
print_r(
$invoice->toArray(
[
'inv_status_flag',
'inv_title',
'inv_total',
]
)
);
// [
// 'inv_status_flag' = 1,
// 'inv_title' = 'Invoice for ACME Inc.',
// 'inv_total' = 100,
// ]
false
otherwise. <?php
use MyApp\Models\Invoices;
$invoice = Invoices::findFirst('inv_id = 4');
$invoice->inv_total = 120;
$invoice->update();
UPDATE
statement <?php
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->allowEmptyStringValues(
[
'inv_created_at',
]
);
}
}
protected function belongsTo(
string | array $fields,
string $referenceModel,
string | array $referencedFields,
array options = null
): Relation
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class InvoicesXProducts extends Model
{
public function initialize()
{
$this->belongsTo(
'ixp_inv_id',
Invoices::class,
'inv_id'
);
}
}
protected function hasMany(
string | array $fields,
string $referenceModel,
string | array $referencedFields,
array options = null
): Relation
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Customers extends Model
{
public function initialize()
{
$this->hasMany(
'cst_id',
Invoices::class,
'inv_cst_id'
);
}
}
protected function hasManyToMany(
string | array $fields,
string $intermediateModel,
string | array $intermediateFields,
string | array $intermediateReferencedFields,
string $referenceModel,
string | array $referencedFields,
array $options = null
): Relation
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->hasManyToMany(
'inv_id',
InvoicesXProducts::class,
'ixp_inv_id',
'ixp_prd_id',
Products::class,
'prd_id'
);
}
}
protected function hasOne(
string | array $fields,
string $referenceModel,
string | array $referencedFields,
array options = null
): Relation
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->hasOne(
'inv_cst_id',
Customers::class,
'cst_id'
);
}
}
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->keepSnapshots(true);
}
}
INSERT
/UPDATE
statement <?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->skipAttributes(
[
'inv_created_at',
]
);
}
}
INSERT
statement <?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->skipAttributesOnCreate(
[
'inv_created_at',
]
);
}
}
UPDATE
statement <?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->skipAttributesOnUpdate(
[
'inv_modified_at',
]
);
}
}
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->useDynamicUpdate(true);
}
}
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
use Phalcon\Validation;
use Phalcon\Validation\Validator\ExclusionIn;
class Invoices extends Model
{
public function validation()
{
$validator = new Validation();
$validator->add(
'inv_status_flag',
new ExclusionIn(
[
'domain' => [
0,
1,
],
]
)
);
return $this->validate($validator);
}
}
Creating Models¶
A model is a class that extends from Phalcon\Mvc\Model. Its class name should be in camel case notation:
By default, the model MyApp\Models\Invoices
will map to the table invoices
. If you want to manually specify another name for the mapped table, you can use the setSource()
method:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->setSource('co_invoices');
}
}
The model Invoices
now maps to co_invoices
table. The initialize()
method helps with setting up this model with a custom behavior i.e. a different table.
The initialize()
method is only called once during the request. This method is 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:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function onConstruct()
{
// ...
}
}
Properties vs. Setters/Getters
NOTE
The model class uses some properties internally for services. The names of those properties are reserved and cannot be used as fields in the database. Please keep that in mind when naming the fields of your tables. If there are collisions, your models will not update properly.
container
, dirtyState
, dirtyRelated
, errorMessages
, modelsManager
, modelsMetaData
, related
, operationMade
, oldSnapshot
, skipped
, snapshot
, transaction
, uniqueKey
, uniqueParams
, uniqueTypes
Models can be implemented with public properties, meaning that each property can be read and updated updated from any part of the code that has instantiated that model class:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public $inv_id;
public $inv_cst_id;
public $inv_status_flag;
public $inv_title;
public $inv_total;
public $inv_created_at;
}
Another implementation is to use getter and setter functions, which control which properties are publicly available for that model.
The benefit of using getters and setters is that the developer can perform transformations and validation checks on the values set or retrieved for the model, which is impossible when using public properties.
Additionally, getters and setters allow for future changes without changing the interface of the model class. So if a field name changes, the only change needed will be in the private property of the model referenced in the relevant getter/setter and nowhere else in the code.
<?php
namespace MyApp\Models;
use InvalidArgumentException;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
protected $inv_id;
protected $inv_cst_id;
protected $inv_status_flag;
protected $inv_title;
protected $inv_total;
protected $inv_created_at;
public function getId(): int
{
return (int) $this->inv_id;
}
public function getCustomerId(): int
{
return (int) $this->inv_cst_id;
}
public function getStatus(): int
{
return (int) $this->inv_status_flag;
}
public function getTitle(): string
{
return (string) $this->inv_title;
}
public function getTotal(): float
{
return (float) $this->inv_total;
}
public function getCreatedAt(): string
{
return (string) $this->inv_created_at;
}
public function setCustomerId(int $customerId): Invoices
{
$this->inv_cst_id = $customerId;
return $this;
}
public function setStatus(int $status): Invoices
{
$this->inv_status_flag = $status;
return $this;
}
public function setTitle(string $title): Invoices
{
$this->inv_title = $title;
return $this;
}
public function setTotal(float $total): Invoices
{
if ($total < 0) {
throw new InvalidArgumentException(
'Incorrect total'
);
}
$this->inv_total = $total;
return $this;
}
public function setCreatedAt(string $date): Invoices
{
$this->inv_created_at = $date;
return $this;
}
}
Public properties provide less complexity in development. However, getters/setters can heavily increase the testability, extensibility and maintainability of applications. You will need to decide which strategy is best for you depending on the needs of the application. The ORM is compatible with both schemes of defining properties.
NOTE
Underscores in property names can be problematic when using getters and setters.
NOTE
When using the getters/setters approach, you will need to define your properties as protected
.
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.).
The ORM expects camel case naming and underscores are commonly removed. It is therefore recommended naming 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.
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 'co_customers' with the records:
mysql> select * from co_customers;
+--------+---------------+----------------+
| cst_id | cst_name_last | cst_name_first |
+--------+---------------+----------------+
| 1 | Vader | Darth |
| 2 | Skywalker | Like |
| 3 | Skywalker | Leia |
+--------+---------------+----------------+
3 rows in set (0.00 sec)
You could find a certain record by its primary key and then print its name:
<?php
use MyApp\Models\Customers;
// cst_id = 3
$customer = Customers::findFirst(3);
// 'Leia'
echo $customer->cst_name_first;
Once the record is in memory, you can make modifications to its data and then save changes:
<?php
use MyApp\Models\Customers;
// cst_id = 3
$customer = Customers::findFirst(3);
$customer->cst_name_last = 'Princess';
$customer->save();
As you can see, there is no need to use raw SQL statements. Phalcon\Mvc\Model provides high database abstraction for web applications, simplifying database operations.
Finding Records¶
Phalcon\Mvc\Model also offers several methods for querying records.
find
¶
The method returns a Phalcon\Mvc\Model\Resultset, Phalcon\Mvc\Model\Resultset\Comples or Phalcon\Mvc\Model\Resultset\Simple collection of records even if the result returned is only one record.
The method accept a variety of parameters to retrieve data:
You can also pass a string with aWHERE
clause. In the above example we are getting the same record, instructing the ORM to give us a record with inv_cst_id = 3
The most flexible syntax is to pass an array with different parameters:
The first parameter of the array (without a key) is treated the same way as the example above (passing a string). The array accepts additional parameters that offer additional options to customize the find operation.findFirst
¶
You could also use the findFirst()
method to get only the first record matching the given criteria:
findFirst
without a parameter will return the first record the ORM finds. Usually this is the first record in the table. Passing a number, will query the underlying model using the primary key matching the number parameter passed. If there is no primary key defined or there is a compound key, you will not get any results. You can also pass a string with a WHERE
clause. In the above example we are getting the same record, instructing the ORM to give us a record with inv_cst_id = 3
NOTE
If primary key of table is not numeric, use condition. See examples below.
$uuid = '5741bfd7-6870-40b7-adf6-cbacb515b9a9';
$invoice = Invoices::findFirst([
'uuid = ?0',
'bind' => [$uuid],
]);
// OR
$uuid = '5741bfd7-6870-40b7-adf6-cbacb515b9a9';
$invoice = Invoices::findFirst([
'uuid = :primary:',
'bind' => ['primary' => $uuid],
]);
NOTE
If you do not use bound parameters in your conditions, PHQL will create a new plan internally, therefore consuming more memory. Using bound parameters is highly recommended!
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::findFirst('uuid = "5741bfd7-6870-40b7-adf6-cbacb515b9a9"');
Parameters¶
NOTE
It is highly recommended to use the array syntax with conditions
and bind
to shield yourself from SQL injections, especially when the criteria comes from user input. For more information check the Binding Parameters` section.
Both find()
and findFirst()
methods accept an associative array specifying the search criteria.
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'inv_cst_id = 3',
'order' => 'inv_total desc'
]
);
You can (and should) use the conditions
and bind
array elements which bind parameters to the query parameters. Using this implementation will ensure that your parameters are bound and thus reducing the possibility of SQL injections:
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
'order' => 'inv_total desc',
]
);
The available query options are:
bind
Bind is used together with conditions
, by replacing placeholders and escaping values thus increasing security
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::findFirst(
[
'conditions' => 'inv_id = :inv_id:',
'bind' => [
'inv_id' => 3,
],
]
);
bindTypes
When binding parameters, you can use this option to define additional casting to the bound parameters increasing even more the security of your query.
<?php
use MyApp\Models\Invoices;
use Phalcon\Mvc\Model\Column;
$invoice = Invoices::findFirst(
[
'conditions' => 'inv_id = :inv_id:',
'bind' => [
'inv_id' => 3,
],
'bindTypes' => [
Column::BIND_PARAM_INT,
],
]
);
cache
Cache the resultset, reducing the continuous access to the relational system.
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
'cache' => [
'key' => 'customer.3',
'lifetime' => 84600,
],
'order' => 'inv_total desc',
]
);
columns
Return specific columns in the model.
NOTE
When using this option an incomplete object is returned, and therefore you cannot call methods such as update()
, getRelated()
etc.
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'columns' => [
'inv_id',
'total' => 'inv_total'
],
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
]
);
The columns array can return the columns directly if only a value has been set for one of the array elements. However if you choose to specify a key, it will be used as an alias for that field. In the above example, the cst_name_first
is aliased as first
.
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.
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
]
);
for_update
With this option, Phalcon\Mvc\Model reads the latest available data, setting exclusive locks on each row it reads
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
'for_update' => true,
]
);
group
Allows to collect data across multiple records and group the results by one or more columns 'group' => 'name, status'
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
'group' => 'inv_status_flag',
]
);
hydration
Sets the hydration strategy to represent each returned record in the result
<?php
use MyApp\Models\Invoices;
use Phalcon\Mvc\Model\Resultset;
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
'hydration' => Resultset::HYDRATE_OBJECTS,
]
);
limit
Limit the results of the query to results to certain range
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
'limit' => 10,
]
);
offset
Offset the results of the query by a certain amount
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
'limit' => 10,
'offset' => 100,
]
);
order
Is used to sort the resultset. Use one or more fields separated by commas.
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
'order' => 'inv_status_flag, inv_total desc',
]
);
shared_lock
With this option, Phalcon\Mvc\Model reads the latest available data, setting shared locks on each row it reads
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
'shared_lock' => true,
]
);
query
¶
If you prefer, there is also available a way to create queries in an object-oriented way, instead of using an array of parameters:
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::query()
->where('inv_cst_id = :cst_id:')
->andWhere('inv_total > :total:')
->bind(
[
'cst_id' => 3,
'total' => 1000,
]
)
->orderBy('inv_status_flag, inv_total desc')
->execute()
;
The static method query()
returns a Phalcon\Mvc\Model\Criteria object that is friendly with IDE auto complete.
All the queries are internally handled as PHQL queries. PHQL is a high-level, object-oriented and SQL-like language. This language offers more features to perform queries such as joining other models, group records, aggregations etc.
findBy*
¶
You can use the findBy<property-name>()
method. This method expands on the find()
method mentioned above. It allows you to quickly perform a select of records 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.
For the following model:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public $inv_cst_id;
public $inv_id;
public $inv_status_flag;
public $inv_title;
public $inv_created_at;
}
We have the properties inv_cst_id
, inv_id
, inv_status_flag
, inv_title
, inv_created_at
. If we want to find all the invoices with inv_total = 100
we can use:
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_total = :total:',
'bind' => [
'total' => 100,
],
]
);
but we can also use:
NOTE
The property names are changed to camel case if they have underscores. inv_total
becomes InvTotal
You can also pass parameters in an array as the second parameter. These parameters are the same as the ones you can pass in the find
method.
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::findByInvTotal(
100,
[
'order' => `inv_cst_id, inv_created_at`
]
);
findFirstBy*
¶
Finally, you can use the findFirstBy<property-name>()
method. This method expands on the findFirst()
method mentioned above. It allows you to quickly perform a select 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.
For the following model:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Guestbook extends Model
{
public $id;
public $email;
public $name;
public $text;
}
We have the properties id
, email
, name
and text
. If we want to find the guest book entry for Darth Vader
we can:
<?php
use MyApp\Models\Guestbook;
$guest = Guestbook::findFirst(
[
'conditions' => 'name = :name:',
'bind' => [
'name' => 'Darth Vader',
],
]
);
but we can also use:
<?php
use MyApp\Models\Guestbook;
$name = 'Darth Vader';
$guest = Guestbook::findFirstByName($name);
NOTE
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, such as seeking, traversing, 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.
Some examples of traversing resultsets are:
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find();
// foreach
foreach ($invoices as $invoice) {
echo $invoice->inv_title, PHP_EOL;
}
// while
$invoices->rewind();
while ($invoices->valid()) {
$invoice = $invoices->current();
echo $invoice->inv_title, PHP_EOL;
$invoices->next();
}
// count
echo count($invoices);
echo $invoices->count();
// seek
$invoices->seek(2);
$invoice = $invoices->current();
// array
$invoice = $invoices[5];
// array - isset
if (true === isset($invoices[3])) {
$invoice = $invoices[3];
}
// First
$invoice = $invoices->getFirst();
// Last
$invoice = $invoices->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
Some database systems do not support scrollable cursors. This forces Phalcon 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.
Storing large query results in memory will consume many resources. You can however instruct Phalcon to fetch data in chunks of rows, thus reducing the need to re-execute the request in many cases. You can achieve that by setting the orm.resultset_prefetch_records
setup value. This can be done either in php.ini
or in the model setup()
. More information about this can be found in the features section.
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.
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find();
file_put_contents(
'invoices.cache',
serialize($invoices)
);
$invoices = unserialize(
file_get_contents('invoices.cache')
);
foreach ($invoices as $invoice) {
echo $invoice->inv_title;
}
Custom Resultsets¶
There are times that the application logic requires additional manipulation of the data as it is retrieved from the database. Previously, we would just extend the model and encapsulate the functionality in a class in the model or a trait, returning back to the caller usually an array of transformed data.
With custom resultsets, you no longer need to do that. The custom resultset will encapsulate the functionality, that otherwise would be in the model, and can be reused by other models, thus keeping the code DRY. This way, the find()
method will no longer return the default Phalcon\Mvc\Model\Resultset, but instead the custom one. Phalcon allows you to do this by using the getResultsetClass()
in your model.
First we need to define the resultset class:
<?php
namespace MyApp\Mvc\Model\Resultset;
use \Phalcon\Mvc\Model\Resultset\Simple;
class Custom extends Simple
{
public function calculate() {
// ....
}
}
In the model, we set the class in the getResultsetClass()
as follows:
<?php
namespace Phalcon\Test\Models\Statistics;
use MyApp\Mvc\Model\Resultset\Custom;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->setSource('co_invoices');
}
public function getResultsetClass()
{
return Custom::class;
}
}
and finally in your code you will have something like this:
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id = :cst_id:',
'bind' => [
'cst_id' => 3,
],
]
);
$calculated = $invoices->calculate();
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:
<?php
$invoices = Invoices::find();
$invoices = $invoices->filter(
function ($invoice) {
if (1 === $invoice->inv_status_flag) {
return $invoice;
}
}
);
The above example will return only the paid invoices from our table (inv_status_flag = 1
);
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.
NOTE
When using integer
placeholders you must prefix them with ?
(?0
, ?1
). When using string
placeholders you must enclose the string in :
(:name:
, :total:
).
Some examples:
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
[
'conditions' => 'inv_title LIKE :title: AND ' .
'inv_total > :total:',
'bind' => [
'title' => '%ACME%',
'total' => 1000,
],
]
);
$invoices = Invoices::find(
[
'conditions' => 'inv_title LIKE ?0 = ?0 AND ' .
'inv_total > ?1',
'bind' => [
0 => '%ACME%',
1 => 1000,
],
]
);
$invoices = Invoices::find(
[
'conditions' => 'inv_title = ?0 AND ' .
'inv_total > :total:',
'bind' => [
0 => '%ACME%',
'total' => 1000,
],
]
);
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 their data type:
<?php
use MyApp\Models\Invoices;
use Phalcon\Db\Column;
$parameters = [
'title' => '%ACME%',
'total' => 1000,
];
$types = [
'title' => Column::BIND_PARAM_STR,
'total' => Column::BIND_PARAM_INT,
];
$invoices = Invoices::find(
[
'conditions' => 'inv_title LIKE :title: AND ' .
'inv_total > :total:',
'bind' => $parameters,
'bindTypes' => $types,
]
);
NOTE
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 strings
You can also bind arrays in the parameters, especially when using the IN
SQL keyword.
NOTE
You need to use a zero based array for arrays without missing elements
<?php
use MyApp\Models\Invoices;
$customerIds = [1, 3, 4]; // $array: [[0] => 1, [1] => 2, [2] => 4]
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id IN ({customerId:array})',
'bind' => [
'customerId' => $customerIds,
],
]
);
unset($customerIds[1]); // $array: [[0] => 1, [2] => 4]
$customerIds = array_values($customerIds); // $array: [[0] => 1, [1] => 4]
$invoices = Invoices::find(
[
'conditions' => 'inv_cst_id IN ({customerId:array})',
'bind' => [
'customerId' => $customerIds,
],
]
);
NOTE
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 e.g. find()
, findFirst()
, etc., you can inject the bound parameters when using the string syntax for the first parameter instead of using the conditions
array element. Also when using findFirstBy*
the parameters are automatically bound.
<?php
use MyApp\Models\Invoices;
$invoices = Invoices::find(
'inv_total > ?0',
'bind' => [
1000,
]
);
$invoices = Invoices::findByInvTotal(1000);
Before/After Fetching¶
There are cases where we need to manipulate the data after it has been fetched from the database so that our model contains what we need in the application layer. As seen in the events document, models act as listeners so we can implement some events as methods in the model.
Such methods include beforeSave
, afterSave
and afterFetch
as shown in our example below. The afterFetch
method will run right after the data populates the model from the database. We can utilize this method to modify or transform the data in the model.
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public $inv_id;
public $inv_cst_id;
public $inv_status_flag;
public $inv_total;
public $status;
public function beforeSave()
{
$this->status = join(',', $this->status);
}
public function afterFetch()
{
$this->status = explode(',', $this->status);
}
public function afterSave()
{
$this->status = explode(',', $this->status);
}
}
explode
it to an array so that it can be used from our application. After that, you can add or remove elements in the array; before the model saves it, implode
will be called to store the array as a string in the database. If you use getters/setters instead of/or together with public properties, you can initialize the field once it is accessed:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public $inv_id;
public $inv_cst_id;
public $inv_status_flag;
public $inv_total;
public $status;
public function getStatus()
{
return explode(',', $this->status);
}
}
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
<?php
$rowcount = Invoices::count();
// inv_cst_id = 3
$rowcount = Invoices::count(
[
'inv_cst_id = ?0',
'bind' => [
3,
],
]
);
We can also use the group
parameter to group our results. The count results appear in the rowcount
property of each object in the collection returned.
<?php
$group = Invoices::count(
[
'group' => 'inv_cst_id',
]
);
foreach ($group as $row) {
echo 'Count: ', $row->rowcount, ' - Customer: ', $row->inv_cst_id;
}
$group = Invoices::count(
[
'group' => 'inv_cst_id',
'order' => 'rowcount',
]
);
SUM
<?php
$total = Invoices::sum(
[
'column' => 'inv_total',
]
);
$total = Invoices::sum(
[
'column' => 'total',
'conditions' => 'inv_cst_id = ?0',
'bind' => [
3
]
]
);
sumatory
property of each object in the collection returned. <?php
$group = Invoices::sum(
[
'column' => 'inv_total',
'group' => 'inv_cst_id',
]
);
foreach ($group as $row) {
echo 'Customer: ', $row->inv_cst_id, ' - Total: ', $row->sumatory;
}
$group = Invoices::sum(
[
'column' => 'inv_total',
'group' => 'inv_cst_id',
'order' => 'sumatory DESC',
]
);
AVERAGE
<?php
$average = Invoices::average(
[
'column' => 'inv_total',
]
);
$average = Invoices::average(
[
'column' => 'inv_total',
'conditions' => 'inv_status_flag = ?0',
'bind' => [
0
]
]
);
MAX
- MIN
<?php
$max = Invoices::maximum(
[
'column' => 'inv_total',
]
);
$max = Invoices::maximum(
[
'column' => 'inv_total',
'conditions' => 'inv_status_flag = ?0',
'bind' => [
0
],
]
);
$min = Invoices::minimum(
[
'column' => 'inv_total',
]
);
$min = Invoices::minimum(
[
'column' => 'inv_total',
'conditions' => 'inv_status_flag = ?0',
'bind' => [
0
],
]
);
Creating - Updating¶
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 created or updated.
The method also executes associated validators, virtual foreign keys and events that are defined in the model:
<?php
use MyApp\Models\Invoices;
$invoice = new Invoices();
$invoice->inv_cst_id = 1;
$invoice->inv_status_flag = 1;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 100;
$invoice->inv_created_at = '2019-12-25 01:02:03';
$result = $invoice->save();
if (false === $result) {
echo 'Error saving Invoice: ';
$messages = $invoice->getMessages();
foreach ($messages as $message) {
echo $message . PHP_EOL;
}
} else {
echo 'Record Saved';
}
You can also use the assign()
method and pass an array of field => value
elements, to avoid assigning each 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:
<?php
use MyApp\Models\Invoices;
$invoice = new Invoices();
$invoice->assign(
[
'inv_cst_id' => 1,
'inv_status_flag' => 1,
'inv_title' => 'Invoice for ACME Inc.',
'inv_total' => 100,
'inv_created_at' => '2019-12-25 01:02:03',
]
);
$result = $invoice->save();
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:
<?php
use MyApp\Models\Invoices;
$invoice = new Invoices();
$invoice->assign($_POST);
$result = $invoice->save();
NOTE
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 assign
to set a whitelist of fields that only must taken into account when doing the mass assignment:
<?php
use MyApp\Models\Invoices;
$invoice = new Invoices();
$invoice->assign(
$_POST,
[
'inv_cst_id',
'inv_status_flag',
'inv_title',
'inv_total',
]
);
$result = $invoice->save();
NOTE
On really busy applications, you can use create
or update
for the respective operations. By using those two methods instead of save, we ensure that data will be saved or not in the database, since those throw exceptions on create
if the record already exists, and on update
if the record does not exist.
<?php
use MyApp\Models\Invoices;
$invoice = new Invoices();
$invoice->inv_id = 1234;
$invoice->inv_cst_id = 1;
$invoice->inv_status_flag = 1;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 100;
$invoice->inv_created_at = '2019-12-25 01:02:03';
$result = $invoice->update();
if (false === $result) {
echo 'Error saving Invoice: ';
$messages = $invoice->getMessages();
foreach ($messages as $message) {
echo $message . PHP_EOL;
}
} else {
echo 'Record Updated';
}
The methods create
and update
also accept an array of values as parameter.
Deleting¶
The delete()
method allows you to delete a record. It returns a boolean signifying success or failure
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::findFirst(
[
'conditions' => 'inv_id = :id:',
'bind' => [
'id' => 4,
]
]
);
if (false !== $invoice) {
if (false === $invoice->delete()) {
$messages = $invoice->getMessages();
foreach ($messages as $message) {
echo $message . PHP_EOL;
}
} else {
echo 'Record Deleted';
}
}
You can also delete many records by traversing a resultset with a foreach
:
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::find(
[
'conditions' => 'inv_cst_id = :id:',
'bind' => [
'id' => 3,
]
]
);
foreach ($invoices as $invoice) {
if (false === $invoice->delete()) {
$messages = $invoice->getMessages();
foreach ($messages as $message) {
echo $message . PHP_EOL;
}
} else {
echo 'Record Deleted';
}
}
NOTE
Check the transactions section on how you can delete all the records in a loop with one operation
Hydration Modes¶
As mentioned earlier, resultsets are collections of complete objects. This means that every returned result is an object, representing a row in the database. These documents can be modified and later on saved to persist the changes in the database.
However, there are times that you will need to get the data in a read only mode, such as in cases of just viewing data. In these cases, it is useful to change the way the records are returned to save resources and increase performance. The strategy used to represent these objects returned in an resultset is called hydration
.
Phalcon offers three ways of hydrating data: - Arrays : Phalcon\Mvc\Model\Resultset::HYDRATE_ARRAYS
- Objects : Phalcon\Mvc\Model\Resultset::HYDRATE_OBJECTS
- Records : Phalcon\Mvc\Model\Resultset::HYDRATE_RECORDS
The default hydration mode is to return records (HYDRATE_RECORDS
). We can easily change the hydration mode to get arrays or objects back. Changing the hydration mode to anything other than HYDRATE_RECORDS
will return back objects (or arrays) that have no connection to the database i.e. we will not be able to perform any operations on those objects such as save()
, create()
, delete()
etc.
<?php
use MyApp\Models\Invoices;
use Phalcon\Mvc\Model\Resultset;
$invoices = Invoices::findFirst(
[
'conditions' => 'inv_id = :id:',
'bind' => [
'id' => 4,
]
]
);
// Array
$invoices->setHydrateMode(
Resultset::HYDRATE_ARRAYS
);
foreach ($invoices as $invoice) {
echo $invoice['inv_total'], PHP_EOL;
}
// \stdClass
$invoices->setHydrateMode(
Resultset::HYDRATE_OBJECTS
);
foreach ($invoices as $invoice) {
echo $invoice->inv_total, PHP_EOL;
}
// Invoices
$invoices->setHydrateMode(
Resultset::HYDRATE_RECORDS
);
foreach ($invoices as $invoice) {
echo $invoice->inv_total, PHP_EOL;
}
Hydration mode can also be passed as a parameter of find
, findFirst
, findFirstBy*
etc.:
<?php
use MyApp\Models\Invoices;
use Phalcon\Mvc\Model\Resultset;
$invoices = Invoices::findFirst(
[
'hydration' => Resultset::HYDRATE_ARRAYS,
'conditions' => 'inv_id = :id:',
'bind' => [
'id' => 4,
],
]
);
foreach ($invoices as $invoice) {
echo $invoice['inv_total'], PHP_EOL;
}
Table Prefixes¶
If you want all your tables to have certain prefix and without setting the source in all models, you can use the Phalcon\Mvc\Model\Manager and the method setModelPrefix()
:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model\Manager;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
}
$manager = new Manager();
$manager->setModelPrefix('co_');
$invoices = new Invoices(null, null, $manager);
echo $invoices->getSource(); // will return co_invoices
Identity Columns¶
Some models may have identity columns. These columns usually are the primary key of the mapped table. Phalcon\Mvc\Model can recognize the identity column omitting it in the generated INSERT
SQL statements, to allow the database system to correctly generate a new value for that field. After creating a new record, the identity field will always be registered with the value generated in the database system for it:
Phalcon\Mvc\Model attempts to recognize the identity column from each table. However, depending on the database system, these columns might be serial columns, such as in the case of PostgreSQL or auto_increment
columns in the case of MySQL.
PostgreSQL uses sequences to generate automatically numeric values for the primary key. Phalcon tries to obtain the generated value from the sequence table_field_seq
, for example: co_invoices_id_seq
. If the sequence name is different, you can always use the getSequenceName()
method method in the model, instructing Phalcon the sequence it needs to use for the primary key:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function getSequenceName()
{
return 'invoices_sequence_name';
}
}
Skipping Columns¶
Depending on how you implement business rules or model rules in your database, certain fields could very well be ignored in database operations. For instance, if we have a inv_created_date
in our model, we can instruct the database system to inject the current timestamp on it:
The code above (for MySQL) instructs the RDBMS to assign the current timestamp on the inv_created_at
field when the record is created. We can therefore omit this field when creating a record. Similarly we might want to ignore some fields when we are updating records.
To achieve this task we can use the skipAttributes
(for any operation), skipAttributesOnCreate
(create) or skipAttributesOnUpdate
(update)
To tell Phalcon\Mvc\Model that always omits some fields in the creation and/or update of records in order to delegate the database system the assignation of the values by a trigger or a default:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->skipAttributes(
[
'inv_total',
'inv_created_at',
]
);
$this->skipAttributesOnCreate(
[
'inv_created_at',
]
);
$this->skipAttributesOnUpdate(
[
'inv_modified_at',
]
);
}
}
If you want to set default values in your model properties (such as the inv_created_at
) you can use the Phalcon\Db\RawValue:
<?php
use MyApp\Models\Invoices;
use Phalcon\Db\RawValue;
$invoice = new Invoices();
$invoice->inv_id = 1234;
$invoice->inv_cst_id = 1;
$invoice->inv_status_flag = 1;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 100;
$invoice->inv_created_at = new RawValue('default');
$invoice->create();
We can also take advantage of the beforeCreate
event in the model to assign the default value there:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
use Phalcon\Db\RawValue;
class Invoices extends Model
{
public function beforeCreate()
{
$this->inv_created_at = new RawValue('default');
}
}
NOTE
Never use a Phalcon\Db\RawValue to assign external data (such as user input) or variable data. The value of these fields is ignored when binding parameters to the query. So it could be used for SQL injection attacks.
Dynamic Updates¶
SQL UPDATE
statements are by default created with every column defined in the model (full all-field SQL update). You can change specific models to make dynamic updates, in this case, just the fields that had changed are used to create the final SQL statement.
In some cases this could improve the performance by reducing the traffic between the application and the database server, especially when the target table has blob/text fields:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->useDynamicUpdate(true);
}
}
Column Mapping¶
The ORM supports an independent column map, which allows the developer to use different column names in the model to the ones in the table. Phalcon will recognize the new column names and will rename them accordingly to match the respective columns in the database. This is a great feature when one needs to rename fields in the database without having to worry about all the queries in the code.
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public $inv_id;
public $inv_cst_id;
public $inv_status_flag;
public $inv_title;
public $inv_total;
public $inv_created_at;
public function columnMap()
{
return [
'inv_id' => 'id',
'inv_cst_id' => 'customerId',
'inv_status_flag' => 'status',
'inv_title' => 'title',
'inv_total' => 'total',
'inv_created_at' => 'createdAt',
];
}
}
NOTE
In the array defined in the column map, the keys are the actual names of the fields in the database, and the values are the virtual fields we can use in your code
Now we can use those virtual fields (or column map) in your code:
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::findFirst(
[
'conditions' => 'inv_id = :id:',
'bind' => [
'id' => 4,
]
]
);
echo $invoice->customerId, PHP_EOL,
$invoice->total, PHP_EOL,
$invoice->createdAt, PHP_EOL;
$invoices = Invoices::find(
[
'order' => 'createdAt DESC',
]
);
foreach ($invoices as $invoice) {
echo $invoice->customerId, PHP_EOL,
$invoice->total, PHP_EOL,
$invoice->createdAt, PHP_EOL;
}
$invoice = new Invoices();
$invoice->customerId = 1;
$invoice->status = 1;
$invoice->title = 'Invoice for ACME Inc.';
$invoice->total = 100;
$invoice->createdAt = '2019-12-25 01:02:03';
$invoice->save();
Considerations
Consider the following when renaming your columns:
- References to attributes in relationships/validators must use the virtual names
- Refer the real column names will result in an exception by the ORM
The independent column map allows you to:
- Write applications using your own conventions
- Eliminate vendor prefixes/suffixes in your code
- Change column names without changes your application code
Record Snapshots¶
Specific models could be set to maintain a record snapshot when they are queried. You can use this feature to implement auditing or just to know what fields have been changed in the model compared to the data in the database.
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->keepSnapshots(true);
}
}
When activating this feature, the application consumes a bit more of memory, to keep track of the original values obtained from the database. In models that have this feature activated, you can check what fields changed as follows:
<?php
use MyApp\Models\Invoices;
$invoice = Invoices::findFirst();
$invoice->inv_total = 120;
var_dump($invoice->getChangedFields()); // ['inv_total']
var_dump($invoice->hasChanged('inv_total')); // true
var_dump($invoice->hasChanged('inv_cst_id')); // false
Snapshots are updated on model creation/update. Using hasUpdated()
and getUpdatedFields()
can be used to check if fields were updated after a create/save/update but it could potentially cause problems to your application if you execute getChangedFields()
in afterUpdate()
, afterSave()
or afterCreate()
.
You can disable this functionality by using:
or if you prefer set this in yourphp.ini
Using this functionality will have the following effect:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public $inv_id;
public $inv_cst_id;
public $inv_status_flag;
public $inv_title;
public $inv_total;
public $inv_created_at;
public function initialize()
{
$this->keepSnapshots(true);
}
}
$invoice = new Invoices();
$invoice->inv_id = 1234;
$invoice->inv_cst_id = 1;
$invoice->inv_status_flag = 1;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 100;
$invoice->inv_created_at = '2019-12-25 01:02:03';
$invoice->create();
var_dump(
$invoice->getChangedFields() // []
);
$invoice->inv_total = 120;
var_dump(
$invoice->getChangedFields() // ['inv_total']
);
$invoice->update();
var_dump(
$invoice->getChangedFields() // []
);
getUpdatedFields()
will properly return updated fields or as mentioned above you can go back to the previous behavior by setting the relevant ini value.
Events¶
As mentioned before Phalcon\Mvc\Model acts as a listener to events. Therefore, all the events that the model is listening to can be implemented as methods in the model itself. You can check the events document for additional information.
The events supported are:
afterCreate
afterDelete
afterFetch
afterSave
afterUpdate
afterValidation
afterValidationOnCreate
afterValidationOnUpdate
beforeDelete
beforeCreate
beforeSave
beforeUpdate
beforeValidation
beforeValidationOnCreate
beforeValidationOnUpdate
notDeleted
notSaved
onValidationFails
prepareSave
validation
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
use Phalcon\Messages\Message as Message;
class Invoices extends Model
{
public function beforeSave()
{
if (0 === $this->inv_status_flag) {
$message = new Message(
'Sorry, an invoice cannot be unpaid'
);
$this->appendMessage($message);
}
}
}
Transactions¶
Transactions are necessary to ensure data integrity, when we need to insert or update data in more than one table during the same operation. Phalcon\Mvc\Model exposes the setTransaction
method that allows you to bind each model to an active transaction.
<?php
use MyApp\Models\Customers;
use MyApp\Models\Invoices;
use Phalcon\Mvc\Model\Transaction\Manager;
use Phalcon\Mvc\Model\Transaction\Failed;
try {
$txManager = new Manager();
$transaction = $txManager->get();
$customer = new Customers();
$customer->setTransaction($transaction);
$customer->cst_name_last = 'Vader';
$customer->cst_name_first = 'Darth';
if (false === $customer->save()) {
$transaction->rollback('Cannot save Customer');
}
$invoice = new Invoices();
$invoice->setTransaction($transaction);
$invoice->inv_cst_id = $customer->cst_id;
$invoice->inv_status_flag = 1;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 100;
$invoice->inv_created_at = '2019-12-25 01:02:03';
if (false === $invoice->save()) {
$transaction->rollback('Cannot save record');
}
$transaction->commit();
} catch (Failed $ex) {
echo 'ERROR: ', $ex->getMessage();
}
You can also include finder results in your transactions or even have multiple transactions running at the same time:
<?php
use MyApp\Models\Customers;
use MyApp\Models\Invoices;
use Phalcon\Mvc\Model\Transaction\Manager;
use Phalcon\Mvc\Model\Transaction\Failed;
try {
$txManager = new Manager();
$transaction = $txManager->get();
$customer = new Customers();
$customer->setTransaction($transaction);
$customer->cst_name_last = 'Vader';
$customer->cst_name_first = 'Darth';
if (false === $customer->save()) {
$transaction->rollback('Cannot save Customer');
}
$average = Invoices::average(
[
Model::TRANSACTION_INDEX => $transaction,
'column' => 'inv_total',
'conditions' => 'inv_cst_id = :customerId:',
'bind' => [
'customerId' => 3,
],
]
);
$invoice = new Invoices();
$invoice->setTransaction($transaction);
$invoice->inv_cst_id = $customer->cst_id;
$invoice->inv_status_flag = 1;
$invoice->inv_title = 'Invoice for ACME Inc.';
$invoice->inv_total = 100 + $average;
$invoice->inv_created_at = '2019-12-25 01:02:03';
if (false === $invoice->save()) {
$transaction->rollback('Cannot save record');
}
$transaction->commit();
} catch (Failed $ex) {
echo 'ERROR: ', $ex->getMessage();
}
Changing Schema¶
If a model is mapped to a table that is located in a different schema than the default, you can use the setSchema()
to point to the correct location:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->setSchema('invoices');
}
}
Multiple Databases¶
Phalcon models by default connect to the same database connection (db
service) that has been defined in the dependency injection container. However, you might need to connect specific models to different connections, which could be connections to different databases.
We can define which model connects to which database in the initialize
method of each model:
<?php
use Phalcon\Di\FactoryDefault;
use Phalcon\Db\Adapter\Pdo\Mysql;
use Phalcon\Db\Adapter\Pdo\PostgreSQL;
$container = new FactoryDefault();
// MySQL
$container->set(
'dbMysql',
function () {
return new Mysql(
[
'host' => 'localhost',
'username' => 'root',
'password' => 'secret',
'dbname' => 'tutorial',
]
);
},
true
);
// PostgreSQL
$container->set(
'dbPostgres',
function () {
return new PostgreSQL(
[
'host' => 'localhost',
'username' => 'postgres',
'password' => '',
'dbname' => 'tutorial',
]
);
}
);
and in the initialize()
method:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->setConnectionService('dbPostgres');
}
}
Additional flexibility is available regarding database connections. You can specify a different connection for read
operations and a different one for write
operations. This is particularly useful when you have memory databases that can be used for read operations and different, more powerful databases that are used for write
operations.
You can set two different connections and utilize each database in each model transparently
<?php
use Phalcon\Di\FactoryDefault;
use Phalcon\Db\Adapter\Pdo\Mysql;
use Phalcon\Db\Adapter\Pdo\PostgreSQL;
$container = new FactoryDefault();
// MySQL - read
$container->set(
'mysqlRead',
function () {
return new Mysql(
[
'host' => '10.0.4.100',
'username' => 'root',
'password' => 'secret',
'dbname' => 'tutorial',
]
);
},
true
);
// MySQL - write
$container->set(
'mysqlWrite',
function () {
return new Mysql(
[
'host' => '10.0.4.200',
'username' => 'root',
'password' => 'secret',
'dbname' => 'tutorial',
]
);
},
true
);
and in the initialize()
method:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function initialize()
{
$this->setReadConnectionService('mysqlRead');
$this->setWriteConnectionService('mysqlWrite');
}
}
The ORM also provides Horizontal Sharding features, by allowing you to implement a shard
selection according to the query conditions:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
/**
* Dynamically selects a shard
*
* @param array $intermediate
* @param array $bindParams
* @param array $bindTypes
*
* @return Phalcon\Db\Adapter\AdapterInterface
*/
public function selectReadConnection(
$intermediate,
$bindParams,
$bindTypes
) {
if (true === isset($intermediate['where'])) {
$conditions = $intermediate['where'];
if ($conditions['left']['name'] === 'id') {
$id = $conditions['right']['value'];
if ($id > 0 && $id < 10000) {
return $this->getDI()->get('dbShard1');
}
if ($id > 10000) {
return $this->getDI()->get('dbShard2');
}
}
}
return $this->getDI()->get('dbShard0');
}
}
In the above example, we are checking the $intermediate
array, which is an array constructed internally in Phalcon, offering the intermediate representation of the query. We check if we have any where
conditions. If not, we just use the default shard dbShard0
.
If conditions have been defined, we are checking if we have the id
as a field in the conditions, and retrieve its value. If the id
is between 0
and 100000
then we use dbShard1
, alternatively dbShard2
.
The selectReadConnection()
method is called every time we need to get data from the database, and returns the correct connection to be used.
Dependency Injection¶
Phalcon\Mvc\Model is tightly bound to the DI container. You can retrieve the container by using the getDI
method. Therefore you have access to all services registered in the DI container.
The following example shows you how you can print any messages generated by an unsuccessful save
operation in the model, and show these messages in the flash messenger. To do this, we use the notSaved
event:
<?php
namespace MyApp\Models;
use Phalcon\Mvc\Model;
class Invoices extends Model
{
public function notSaved()
{
$flash = $this->getDI()->getFlash();
$messages = $this->getMessages();
// Show validation messages
foreach ($messages as $message) {
$flash->error($message);
}
}
}
Model Features¶
The ORM has several options that control specific behaviors globally. You can enable or disable these features by adding specific lines to your php.ini
file or use the setup
static method on the model. You can enable or disable these features temporarily in your code or permanently.
or by using the Model
:
The available options are:
Option | Default | Description |
---|---|---|
caseInsensitiveColumnMap | false | Case insensitive column map |
castLastInsertIdToInt | false | Casts the lastInsertId to an integer |
castOnHydrate | false | Automatic cast to original types on hydration |
columnRenaming | true | Column renaming |
disableAssignSetters | false | Disable setters |
enableImplicitJoins | true | Enable implicit joins |
events | true | Callbacks, hooks and event notifications from all the models |
exceptionOnFailedMetaDataSave | false | Throw an exception when there is a failed meta-data save |
exceptionOnFailedSave | false | Throw an exception when there is a failed save() |
forceCasting | false | Force casting bound parameters to their native types |
ignoreUnknownColumns | false | Ignore unknown columns on the model |
lateStateBinding | false | Late state binding of the Phalcon\Mvc\Model::cloneResultMap() method |
notNullValidations | true | Automatically validate the not null columns present |
phqlLiterals | true | Literals in the PHQL parser |
prefetchRecords | 0 | The number of records to prefetch when getting data from the ORM |
updateSnapshotOnSave | true | Update snapshots on save() |
virtualForeignKeys | true | Virtual foreign keys |
ini
options:
; phalcon.orm.ast_cache = null
; phalcon.orm.cache_level = 3
; phalcon.orm.case_insensitive_column_map = false
; phalcon.orm.cast_last_insert_id_to_int = false
; phalcon.orm.cast_on_hydrate = false
; phalcon.orm.column_renaming = true
; phalcon.orm.disable_assign_setters = false
; phalcon.orm.enable_implicit_joins = true
; phalcon.orm.enable_literals = true
; phalcon.orm.events = true
; phalcon.orm.exception_on_failed_metadata_save = true
; phalcon.orm.exception_on_failed_save = false
; phalcon.orm.force_casting = false
; phalcon.orm.ignore_unknown_columns = false
; phalcon.orm.late_state_binding = false
; phalcon.orm.not_null_validations = true
; phalcon.orm.parser_cache = null,
; phalcon.orm.resultset_prefetch_records = 0
; phalcon.orm.unique_cache_id = 3
; phalcon.orm.update_snapshot_on_save = true
; phalcon.orm.virtual_foreign_keys = true
NOTE
Phalcon\Mvc\Model::assign()
(which is used also when creating/updating/saving model) is always using setters if they exist when have data arguments passed, even when it's required or necessary. This will add some additional overhead to your application. You can change this behavior by adding phalcon.orm.disable_assign_setters = 1
to your ini file, it will just simply use $this->property = value
.
Stand Alone Component¶
You can use Phalcon\Mvc\Model on its own, performing the necessary setup on your own if you wish. The example below demonstrates how you can achieve that.
<?php
use Phalcon\Di;
use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Manager;
use Phalcon\Db\Adapter\Pdo\Sqlite;
use Phalcon\Mvc\Model\Metadata\Memory;
$container = new Di();
$container->set(
'db',
new Sqlite(
[
'dbname' => 'sample.db',
]
)
);
$container->set(
'modelsManager',
new Manager()
);
$container->set(
'modelsMetadata',
new Memory()
);
class Invoices extends Model
{
}
echo Invoices::count();