Skip to content

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 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\Db component documentation.

<?php

namespace MyApp\Models;

use Phalcon\Mvc\Model;

class Invoices extends Model
{

}
<?php

use MyApp\Models\Invoices;

/**
 * Create an invoice 
 */
$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
)
Constructs the model object. The method accepts an array of data that are used to populate the object by internally using assign. Optionally you can pass a DI container and a Models Manager object. If they are not passed, the defaults will be used.

public function __call(string $method, array $arguments): mixed
Handles method calls when a method is not implemented. Throws Phalcon\Mvc\Model\Exception if the method doesn't exist

public static function __callStatic(
    string $method, 
    array $arguments
): mixed
Handles method calls when a static method is not implemented. Throws Phalcon\Mvc\Model\Exception if the method doesn't exist

public function __get(string $property)
Magic method to get related records using the relation alias as a property

public function __isset(string $property): bool
Magic method to check if a property is a valid relation

public function __set(string $property, mixed $value)
Magic method to assign values to the model

public function addBehavior(
    BehaviorInterface $behavior
): void
Setup 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',
                    ],
                ]
            )
        );
    }
}

public function appendMessage(
    MessageInterface $message
): ModelInterface
Appends a customized message on the validation process

<?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
Assign data to the model. The 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.

<?php

$invoice->assign(
    $_POST,
    [
        'inv_status_flag',
        'inv_title',
        'inv_total',
    ]
);

By default assign will use setters if they exist, you can disable it by using ini_set to directly use properties

ini_set('phalcon.orm.disable_assign_setters', true);
<?php

$invoice->assign(
    $_POST,
    null,
    [
        'inv_status_flag',
        'inv_title',
        'inv_total',
    ]
);

public static function average(
    mixed $parameters = null
): float
Returns the average value on a column for a result-set of rows matching the specified conditions

<?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
Assigns values to a model from an array returning a new model

<?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
Assign values to a model from an array, returning a new model, using the column map.

<?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
Returns a hydrated result based on the data and the column map

public static function count(
    mixed $parameters = null
): int
Returns a count of how many records match the specified conditions

<?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;

public function create(): bool
Inserts a model in the database. If the record exists in the database, 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();

public function delete(): bool
Deletes a model instance. Returning true on success or false otherwise.

<?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();
}

public function dump(): array
Returns a simple representation of the object that can be used with var_dump()

<?php

use MyApp\Models\Invoices;

$invoice = Invoices::findFirst('inv_id = 4');

var_dump(
    $invoice->dump()
);

public static function find(
    mixed $parameters = null
): ResultsetInterface
Query for a set of records that match the specified conditions. 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.

<?php

use MyApp\Models\Invoices;

$invoice = Invoices::find();

public static function findFirst(
    mixed $parameters = null
): ModelInterface | null
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.

<?php

use MyApp\Models\Invoices;

$invoice = Invoices::findFirst();

public function fireEvent(string $eventName): bool
Fires an event, implicitly calls behaviors, and listeners in the events manager are notified

public function fireEventCancel(string $eventName): bool
Fires an event, implicitly calls behaviors, and listeners in the events manager are notified. This method stops if one of the callbacks/listeners returns false

public function getChangedFields(): array
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']

public function getDirtyState(): int
Returns one of the DIRTY_STATE_* constants telling if the record exists in the database or not

public function getMessages(
    mixed $filter = null
): MessageInterface[]
Returns an 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.

public function getModelsManager(): ManagerInterface
Returns the models manager related to the entity instance

public function getModelsMetaData(): MetaDataInterface
Returns the model's meta-data service related to the entity instance

public function getOperationMade(): int
Returns the type of the latest operation performed by the ORM. Returns one of the OP_* class constants

public function getOldSnapshotData(): array
Returns the internal old snapshot data

final public function getReadConnection(): AdapterInterface
Gets the connection used to read data for the model

final public function getReadConnectionService(): string
Returns the DependencyInjection connection service name used to read data related to the model

public function getRelated(
    string $alias, 
    mixed $arguments = null
): Phalcon\Mvc\Model\Resultset\Simple | null
Returns related records based on defined relations. If the relationship is one to one and no records have been found, it will return 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');

public function isRelationshipLoaded(
    string $relationshipAlias
): bool
Checks if saved related records have already been loaded. Only returns 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

final public function getSchema(): string
Returns schema name where the mapped table is located

public function getSnapshotData(): array
Returns the internal snapshot data

final public function getSource(): string
Returns the table name mapped in the model

public function getUpdatedFields(): array
Returns a list of updated 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']

$invoice->save();

print_r(
    $invoice->getChangedFields()
);
// []

print_r(
    $invoice->getUpdatedFields()
);
// ['inv_total']

final public function getWriteConnection(): AdapterInterface
Gets the connection used to write data to the model

final public function getWriteConnectionService(): string
Returns the DependencyInjection connection service name used to write data related to the model

public function hasChanged(
    string | array $fieldName = null, 
    bool $allFields = false
): bool
Check if a specific attribute has changed. This only works if the model is keeping data snapshots

<?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

public function hasSnapshotData(): bool
Check if the object has internal snapshot data

public function hasUpdated(
    string | array $fieldName = null, 
    bool $allFields = false
): bool
Check if a specific attribute was updated. This only works if the model is keeping data snapshots.

public function jsonSerialize(): array
Serializes the object for json_encode

echo json_encode($invoice);

public static function maximum(
    mixed $parameters = null
): mixed
Returns the maximum value of a column for a result-set of rows that match the specified conditions

<?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;

public static function minimum(
    mixed parameters = null
): mixed 
Returns the minimum value of a column for a result-set of rows that match the specified conditions

<?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;

public static function query(
    DiInterface $container = null
): CriteriaInterface
Create criteria for a specific model

public function readAttribute(
    string $attribute
): mixed | null
Reads an attribute value by its name

echo $invoice->readAttribute('inv_title');

public function refresh(): ModelInterface
Refreshes the model attributes re-querying the record from the database

public function save(): bool
Inserts or updates a model instance. Returning 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.

public function serialize(): string
Serializes the object ignoring connections, services, related objects or static properties

public function unserialize(mixed $data)
Unserializes the object from a serialized string

final public function setConnectionService(
    string $connectionService
): void
Sets the DependencyInjection connection service name

public function setDirtyState(
    int $dirtyState
): ModelInterface | bool
Sets the dirty state of the object using one of the DIRTY_STATE_* constants

public function setEventsManager(
    EventsManagerInterface $eventsManager
)
Sets a custom events manager

final public function setReadConnectionService(
    string $connectionService
): void
Sets the DependencyInjection connection service name used to read data

public function setOldSnapshotData(
    array $data, 
    array $columnMap = null
)
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

public function setSnapshotData(
    array $data, 
    array $columnMap = null
): void
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

public function setTransaction(
    TransactionInterface $transaction
): ModelInterface
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 static function setup(
    array $options
): void
Enables/disables options in the ORM such as events, column renaming, etc.

final public function setWriteConnectionService(
    string $connectionService
): void
Sets the DependencyInjection connection service name used to write data

public function skipOperation(bool $skip): void
Skips the current operation forcing a success state

public static function sum(
    array $parameters = null
): float
Calculates the sum on a column for a result-set of rows that match the specified conditions

<?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;

public function toArray(
    array $columns = null,
    bool $useGetters = true
): array
Returns the instance as an array representation. Accepts an array with column names to include in the result, it will use getters by default.

<?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,
//  ]

toArray uses getters by default, to disable this behavior, set $useGetters to false

<?php

use MyApp\Models\InvoicesGetters;

$invoice = InvoicesGetters::findFirst('inv_id = 4');

print_r(
    $invoice->inv_title
);

// 'Invoice for ACME Inc.'


print_r(
    $invoice->getInvTitle()
);

// 'Invoice for ACME Inc. - Status 1'

print_r(
    $invoice->toArray()
);

//  [
//      'inv_id'          => 4,
//      'inv_cst_id'      = $customer->cst_id,
//      'inv_status_flag' = 1,
//      'inv_title'       = 'Invoice for ACME Inc. - Status 1' ,
//      'inv_total'       = 100,
//      'inv_created_at'  = '2019-12-25 01:02:03',
//  ]

print_r(
    $invoice->toArray(
        null,
        false
    )
);


//  [
//      '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',
//  ]

public function update(): bool
Updates a model instance. If the instance doesn't exist in the persistence it will throw an exception. Returning true on success or false otherwise.

<?php

use MyApp\Models\Invoices;

$invoice = Invoices::findFirst('inv_id = 4');

$invoice->inv_total = 120;

$invoice->update();

NOTE

When retrieving the record with findFirst(), you need to get the full object back (no columns definition) but also retrieve it using the primary key. If not, the ORM will issue an INSERT instead of UPDATE.

public function writeAttribute(
    string $attribute, 
    mixed $value
): void
Writes an attribute value by its name

$invoice->writeAttribute('inv_total', 120);

protected function allowEmptyStringValues(
    array $attributes
): void
Sets a list of attributes that must be skipped from the generated 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
Set up a reverse 1-1 or n-1 relation between two models

<?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
Setup a 1-n relation between two models

<?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
Set up an n-n relation between two models, through an intermediate 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
Setup a 1-1 relation between two models

<?php 

namespace MyApp\Models;

use Phalcon\Mvc\Model;

class Invoices extends Model
{
    public function initialize()
    {
        $this->hasOne(
            'inv_cst_id',
            Customers::class,
            'cst_id'
        );
    }
}

protected function keepSnapshots(
    bool $keepSnapshot
): void
Sets if the model must keep the original record snapshot in memory

<?php

namespace MyApp\Models;

use Phalcon\Mvc\Model;

class Invoices extends Model
{
    public function initialize()
    {
        $this->keepSnapshots(true);
    }
}

final protected function setSchema(
    string $schema
): ModelInterface
Sets schema name where the mapped table is located

final protected function setSource(
    string $source
): ModelInterface
Sets the table name to which the model should be mapped

protected function skipAttributes(array $attributes)
Sets a list of attributes that must be skipped from the generated INSERT/UPDATE statement

<?php

namespace MyApp\Models;

use Phalcon\Mvc\Model;

class Invoices extends Model
{
    public function initialize()
    {
        $this->skipAttributes(
            [
                'inv_created_at',
            ]
        );
    }
}

protected function skipAttributesOnCreate(
    array $attributes
): void
Sets a list of attributes that must be skipped from the generated INSERT statement

<?php

namespace MyApp\Models;

use Phalcon\Mvc\Model;

class Invoices extends Model
{
    public function initialize()
    {
        $this->skipAttributesOnCreate(
            [
                'inv_created_at',
            ]
        );
    }
}

protected function skipAttributesOnUpdate(
    array $attributes
): void
Sets a list of attributes that must be skipped from the generated UPDATE statement

<?php

namespace MyApp\Models;

use Phalcon\Mvc\Model;

class Invoices extends Model
{
    public function initialize()
    {
        $this->skipAttributesOnUpdate(
            [
                'inv_modified_at',
            ]
        );
    }
}

protected function useDynamicUpdate(
    bool dynamicUpdate
): void
Sets if a model must use dynamic update instead of the all-field update

<?php

namespace MyApp\Models;

use Phalcon\Mvc\Model;

class Invoices extends Model
{
    public function initialize()
    {
        $this->useDynamicUpdate(true);
    }
}

protected function validate(
    ValidationInterface $validator
): bool
Executes validators on every validation call

<?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);
    }
}

public function validationHasFailed(): bool
Check whether validation process has generated any messages

Creating Models

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

<?php

namespace MyApp\Models;

use Phalcon\Mvc\Model;

class Invoices extends Model
{

}

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 to 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 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 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.

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\Complex or Phalcon\Mvc\Model\Resultset\Simple collection of records even if the result returned is only one record.

The method accepts a variety of parameters to retrieve data:

<?php

use MyApp\Models\Customers;

$invoice = Invoices::findFirst('inv_id = 3');
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

The most flexible syntax is to pass an array with different parameters:

<?php

use MyApp\Models\Customers;

$invoice = Invoices::findFirst(
    [
        'inv_id = 3',
    ]
);
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:

<?php

use MyApp\Models\Invoices;

$invoice = Invoices::findFirst();
Calling findFirst without a parameter will return the first record the ORM finds. Usually, this is the first record in the table.

<?php

use MyApp\Models\Invoices;

// cst_id = 3
$invoice = Invoices::findFirst(3);
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.

<?php

use MyApp\Models\Invoices;

$invoice = Invoices::findFirst('inv_id = 3');
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 the primary key of the 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 come 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 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 is 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 query 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:

<?php

use MyApp\Models\Invoices;

$invoices = Invoices::findByInvTotal(100);

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\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 when 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 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 by 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 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, therefore it is 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 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);
    }
}
In the above example, we receive a comma-delimited string from the database and 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 of 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
        ]
    ]
);
You can also group results. The count results appear in the 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 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 are only 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 accepts an array of values as parameters.

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 a resultset is called hydration.

Phalcon offers three ways of hydrating data:

Result Mode
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 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 a certain prefix 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:

<?php

$invoice->save();

echo $invoice->inv_id; // 4

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 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:

CREATE TABLE co_invoices (
    // ...
    inv_created_at datetime DEFAULT CURRENT_TIMESTAMP
)

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

NOTE

Dynamic Update is now enabled by default, you can disable it by changing the "phalcon.orm.dynamic_update" in the php.ini or by ini_set.

If the system-wide dynamic update is disabled, the 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 maps) 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 changing 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 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:

<?php

Phalcon\Mvc\Model::setup(
    [
        'updateSnapshotOnSave' => false,
    ]
);
or if you prefer set this in your php.ini

phalcon.orm.update_snapshot_on_save = 0

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(
        array $intermediate, 
        array $bindParams, 
        array $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();

        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 using the setup static method on the model. You can enable or disable these features temporarily in your code or permanently.

phalcon.orm.column_renaming = false
phalcon.orm.events          = false

or by using the Model:

<?php

use Phalcon\Mvc\Model;

Model::setup(
    [
        'columnRenaming' => false,
        'events'         => false,
    ]
);

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.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.resultset_prefetch_records = "0"
; phalcon.orm.unique_cache_id = 3
; phalcon.orm.update_snapshot_on_save = true
; phalcon.orm.virtual_foreign_keys = true
; phalcon.db.escape_identifiers = On
; phalcon.db.force_casting = Off

NOTE

Phalcon\Mvc\Model::assign() (which is used also when creating/updating/saving model) is always using setters if they exist when data arguments are 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.

Integers vs. Strings

If you want to get integer values back from int related database fields, you will need to do the following: - Make sure that the castOnHydrate (or set ini_set('phalcon.orm.cast_on_hydrate', 'on')) is enabled

<?php

use Phalcon\Mvc\Model;

Model::setup(
    [
        'castOnHydrate' => true,
    ]
);
  • Ensure that you are using the mysqlnd driver on your server. You can check that using phpinfo() (pdo_mysql > Client API version)
  • In your database connection provider you need to pass the following options:
[
    PDO::ATTR_EMULATE_PREPARES  => false,
    PDO::ATTR_STRINGIFY_FETCHES => false,
]

Registering the provider should look like this:

<?php

$parameters = [
  // ....
];

/** @var Manager $eventsManager */
$eventsManager = $container->getShared('eventsManager');

$container->setShared(
    'db',
    function () use ($eventsManager, $parameters) {
        $options = [
            'host'     => $parameters['host'] ?? 'localhost',
            'dbname'   => $parameters['dbname'] ?? 'phalcon',
            'username' => $parameters['user'] ?? 'root',
            'password' => $parameters['pass'] ?? 'secret',
            'encoding' => $parameters['encoding'] ?? 'utf8',
            'options'  => [
                PDO::ATTR_EMULATE_PREPARES  => false,
                PDO::ATTR_STRINGIFY_FETCHES => false,
            ]
        ];

        $connection = new Mysql($options);
        $connection->setEventsManager($eventsManager);

        return $connection;
    );
}

Invalid parameter number

In v5.6, the parameters used to instantiate PDO have reverted to the default settings. Therefore, PDO::ATTR_EMULATE_PREPARES defaults to true.

However, if you encounter the following message in your code, when a bound parameter is used more than once in a query:

Error

Invalid parameter number

You can check the PDO parameters and ensure that PDO::ATTR_EMULATE_PREPARES is set to true, when registering your database provider.

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\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();