IT大叔 互联网开发技术信息资源聚合

模型代表了应用程序中的信息(数据)和处理数据的规则。模型主要用于管理与相应数据库表进行交互的规则。 大多数情况中,在应用程序中,数据库中每个表将对应一个模型。 应用程序中的大部分业务逻辑都将集中在模型里。

Phalcon\Mvc\Model 是 Phalcon 应用程序中所有模型的基类。它保证了数据库的独立性,基本的 CURD 操作, 高级的查询功能,多表关联等功能。 Phalcon\Mvc\Model 不需要直接使用 SQL 语句,因为它的转换方法,会动态的调用相应的数据库引擎进行处理。

模型是数据库的高级抽象层。如果您想进行低层次的数据库操作,您可以查看 Phalcon\Db 组件文档。

模型是一个继承自 Phalcon\Mvc\Model 的一个类。时它的类名必须符合驼峰命名法:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{

}
如果使用 PHP 5.4/5.5 建议在模型中预先定义好所有的列,这样可以减少模型内存的开销以及内存分配。

默认情况下,模型 “Store\Toys\Robots” 对应的是数据库表 “robots”, 如果想映射到其他数据库表,可以使用 setSource() 方法:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

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

模型 Robots 现在映射到了 “toys_robots” 表。initialize() 方法可以帮助在模型中建立自定义行为,例如指定不同的数据库表。

initialize() 方法在请求期间仅会被调用一次,目的是为应用中所有该模型的实例进行初始化。如果需要为每一个实例在创建的时候单独进行初始化, 可以使用 onConstruct() 事件:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

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

模型可以通过公共属性的方式实现,意味着模型的所有属性在实例化该模型的地方可以无限制的读取和更新。

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public $price;
}

通过使用 getters/setters 方法,可以控制哪些属性可以公开访问,并且对属性值执行不同的形式的转换,同时可以保存在模型中的数据添加相应的验证规则。

<?php

namespace Store\Toys;

use InvalidArgumentException;
use Phalcon\Mvc\Model;

class Robots extends Model
{
    protected $id;

    protected $name;

    protected $price;

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

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

        $this->name = $name;
    }

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

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

        $this->price = $price;
    }

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

公共属性的方式可以在开发中降低复杂度。而 getters/setters 的实现方式可以显著的增强应用的可测试性、扩展性和可维护性。 开发人员可以自己决定哪一种策略更加适合自己开发的应用。ORM同时兼容这两种方法。

把字段设置为自由属性可以用过 getterssetters魔术方法访问属性

如果你在字段名中使用了下划线,那么你必须使用驼峰命名法去代替下划线来访问属性,. (例如: $model->getPropertyName 代替 $model->getProperty_name, $model->findByPropertyName 代替 $model->findByProperty_name, etc.).在模型名中也是同样的,例如,数据库名node_body对应的模型文件名和类名应该是NodeBody而不是Node_body。如果你不想遵循这些规则,那么您可以通过列映射来正确让模型访问数据库

每个模型的实例对应一条数据表中的记录。可以方便的通过读取对象的属性来访问相应的数据。比如, 一个表 “robots” 有如下数据:

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

你可以通过主键找到某一条记录并且打印它的名称:

<?php

use Store\Toys\Robots;

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

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

一旦记录被加载到内存中之后,你可以修改它的数据并保存所做的修改:

<?php

use Store\Toys\Robots;

$robot = Robots::findFirst(3);

$robot->name = "RoboCop";

$robot->save();

如上所示,不需要写任何SQL语句。Phalcon\Mvc\Model 为web应用提供了高层数据库抽象。

Phalcon\Mvc\Model 为数据查询提供了多种方法。下面的例子将演示如何从一个模型中查找一条或者多条记录:

<?php

use Store\Toys\Robots;

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

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

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

// Get first 100 virtual robots ordered by name
$robots = Robots::find(
    [
        "type = 'virtual'",
        "order" => "name",
        "limit" => 100,
    ]
);
foreach ($robots as $robot) {
   echo $robot->name, "\n";
}
如果需要通过外部数据(比如用户输入)或变量来查询记录,则必须要用`Binding Parameters`(绑定参数)的方式来防止SQL注入.

你可以使用 findFirst() 方法获取第一条符合查询条件的结果:

<?php

use Store\Toys\Robots;

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

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

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

find()findFirst() 方法都接受关联数组作为查询条件:

<?php

use Store\Toys\Robots;

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

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

可用的查询选项如下:

如果你愿意,除了使用数组作为查询参数外,还可以通过一种面向对象的方式来创建查询:

<?php

use Store\Toys\Robots;

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

静态方法 query() 返回一个对IDE自动完成友好的 Phalcon\Mvc\Model\Criteria 对象。

所有查询在内部都以 PHQL 查询的方式处理。PHQL是一个高层的、面向对象的类SQL语言。通过PHQL语言你可以使用更多的比如join其他模型、定义分组、添加聚集等特性。

最后,还有一个 findFirstBy<property-name>() 方法。这个方法扩展了前面提及的 findFirst() 方法。它允许您利用方法名中的属性名称,通过将要搜索的该字段的内容作为参数传给它,来快速从一个表执行检索操作。

还是用上面用过的 Robots 模型来举例说明:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public $price;
}

我们这里有3个属性:$id, $name$price。因此,我们以想要查询第一个名称为 ‘Terminator’ 的记录为例,可以这样写:

<?php

use Store\Toys\Robots;

$name = "Terminator";

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

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

请注意我们在方法调用中用的是 ‘Name’,并向它传递了变量 $name$name 的值就是我们想要找的记录的名称。另外注意,当我们的查询找到了符合的记录后,这个记录的其他属性也都是可用的。

findFirst() 方法直接返回一个被调用对象的实例(如果有结果返回的话),而 find() 方法返回一个 Phalcon\Mvc\Model\Resultset\Simple 对象。这个对象也封装进了所有结果集的功能,比如遍历、查找特定的记录、统计等等。

这些对象比一般数组功能更强大。最大的特点是 Phalcon\Mvc\Model\Resultset 每时每刻只有一个结果在内存中。这对操作大数据量时的内存管理相当有帮助。

<?php

use Store\Toys\Robots;

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

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

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

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

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

    $robots->next();
}

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

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

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

$robot = $robots->current();

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

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

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

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

Phalcon 的结果集模拟了可滚动的游标,你可以通过位置,或者内部指针去访问任何一条特定的记录。注意有一些数据库系统不支持滚动游标,这就使得查询会被重复执行, 以便回放光标到最开始的位置,然后获得相应的记录。类似地,如果多次遍历结果集,那么必须执行相同的查询次数。

将大数据量的查询结果存储在内存会消耗很多资源,正因为如此,分成每32行一块从数据库中获得结果集,以减少重复执行查询请求的次数,在一些情况下也节省内存。

注意结果集可以序列化后保存在一个后端缓存里面。 Phalcon\Cache 可以用来实现这个。但是,序列化数据会导致 Phalcon\Mvc\Model 将从数据库检索到的所有数据以一个数组的方式保存,因此在这样执行的地方会消耗更多的内存。

<?php

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

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

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

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

过滤数据最有效的方法是设置一些查询条件,数据库会利用表的索引快速返回数据。Phalcon 额外的允许你通过任何数据库不支持的方式过滤数据。

<?php

$customers = Customers::find();

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

Phalcon\Mvc\Model 中也支持绑定参数。即使使用绑定参数对性能有一点很小的影响,还是强烈建议您使用这种方法,以消除代码受SQL注入攻击的可能性。 绑定参数支持字符串和整数占位符。实现方法如下:

<?php

use Store\Toys\Robots;

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

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

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

如果是数字占位符,则必须把它们定义成整型(如1或者2)。若是定义为字符串型(如”1”或者”2”),则这个占位符不会被替换。

使用PDO_的方式会自动转义字符串。它依赖于字符集编码,因此建议在连接参数或者数据库配置中设置正确的字符集编码。 若是设置错误的字符集编码,在存储数据或检索数据时,可能会出现乱码。

另外你可以设置参数的“bindTypes”,这允许你根据数据类型来定义参数应该如何绑定:

<?php

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

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

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

// Query robots binding parameters with string placeholders
$robots = Robots::find(
    [
        "name = :name: AND year = :year:",
        "bind"      => $parameters,
        "bindTypes" => $types,
    ]
);
默认的参数绑定类型是 Phalcon\Db\Column::BIND_PARAM_STR , 若所有字段都是string类型,则不用特意去设置参数的“bindTypes”.

如果你的绑定参数是array数组,那么数组索引必须从数字0开始:

<?php

use Store\Toys\Robots;

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

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

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

$robots = Robots::find(
    [
        'letter IN ({letter:array})',
        'bind' => [
            'letter' => $array
        ]
    ]
);
参数绑定的方式适用于所有与查询相关的方法,如 find() , findFirst() 等等, 同时也适用于与计算相关的方法,如 count(), sum(), average() 等等.

若使用如下方式,phalcon也会自动为你进行参数绑定:

<?php

use Store\Toys\Robots;

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

// Implicit query using bound parameters(隐式的参数绑定)
$robots = Robots::findByName("Ultron");

有时从数据库中获取了一条记录之后,在被应用程序使用之前,需要对数据进行初始化。 你可以在模型中实现”afterFetch”方法,在模型实例化之后会执行这个方法,并将数据分配给它:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public $status;

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

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

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

如果使用getters/setters方法代替公共属性的取/赋值,你能在它被调用时,对成员属性进行初始化:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public $status;

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

有四种关系类型:1对1,1对多,多对1,多对多。关系可以是单向或者双向的,每个关系可以是简单的(一个1对1的模型)也可以是复杂的(1组模型)。 通过定义模型关系,我们可以定义虚拟外键,并懂进行外键约束,更快更简单的去获得关联记录,通过统一的方法去管理、访问关系记录。

只需一方对另一方负责,可以理解为sql中的leftJoin或者rightJoin

模型双方都必须实现双方的约定,可以依照sql中的fullJoin理解

在Phalcon, 模型关系必须在initialize()模型初始化函数中声明. 声明模型关系的方法有 belongsTo(), hasOne(), hasMany() , hasManyToMany()这些方法用来定义一个或者多个字段与另一个模型中字段的对应关系。需要有三个参数,本模型字段(local fields)、关联模型(referenced model)、关联模型的关联字段(referenced fields)

Method Description
hasMany 定义 1-n 关系
hasOne 定义 1-1 关系
belongsTo 定义 n-1 关系
hasManyToMany 定义 n-n 关系

下面的数据库结构为我们定义了在传统SQL模式下三个表的外键约束和关联状态。

CREATE TABLE `robots` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(70) NOT NULL,
    `type` varchar(32) NOT NULL,
    `year` int(11) NOT NULL,
    PRIMARY KEY (`id`)
);

CREATE TABLE `robots_parts` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `robots_id` int(10) NOT NULL,
    `parts_id` int(10) NOT NULL,
    `created_at` DATE NOT NULL,
    PRIMARY KEY (`id`),
    KEY `robots_id` (`robots_id`),
    KEY `parts_id` (`parts_id`)
);

CREATE TABLE `parts` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(70) NOT NULL,
    PRIMARY KEY (`id`)
);
  • The model “Robots” has many “RobotsParts”.
  • The model “Parts” has many “RobotsParts”.
  • The model “RobotsParts” belongs to both “Robots” and “Parts” models as a many-to-one relation.
  • The model “Robots” has a relation many-to-many to “Parts” through “RobotsParts”.

Check the EER diagram to understand better the relations:

/file/png/2016/09/26/eer-1.png

The models with their relations could be implemented as follows:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany(
            "id",
            "RobotsParts",
            "robots_id"
        );
    }
}
<?php

use Phalcon\Mvc\Model;

class Parts extends Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany(
            "id",
            "RobotsParts",
            "parts_id"
        );
    }
}
<?php

use Phalcon\Mvc\Model;

class RobotsParts extends Model
{
    public $id;

    public $robots_id;

    public $parts_id;

    public function initialize()
    {
        $this->belongsTo(
            "robots_id",
            "Store\\Toys\\Robots",
            "id"
        );

        $this->belongsTo(
            "parts_id",
            "Parts",
            "id"
        );
    }
}

The first parameter indicates the field of the local model used in the relationship; the second indicates the name of the referenced model and the third the field name in the referenced model. You could also use arrays to define multiple fields in the relationship.

Many to many relationships require 3 models and define the attributes involved in the relationship:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasManyToMany(
            "id",
            "RobotsParts",
            "robots_id", "parts_id",
            "Parts",
            "id"
        );
    }
}

When explicitly defining the relationships between models, it is easy to find related records for a particular record.

<?php

use Store\Toys\Robots;

$robot = Robots::findFirst(2);

foreach ($robot->robotsParts as $robotPart) {
    echo $robotPart->parts->name, "\n";
}

Phalcon uses the magic methods __set/__get/__call to store or retrieve related data using relationships.

By accessing an attribute with the same name as the relationship will retrieve all its related record(s).

<?php

use Store\Toys\Robots;

$robot = Robots::findFirst();

// All the related records in RobotsParts
$robotsParts = $robot->robotsParts;

Also, you can use a magic getter:

<?php

use Store\Toys\Robots;

$robot = Robots::findFirst();

// All the related records in RobotsParts
$robotsParts = $robot->getRobotsParts();

// Passing parameters
$robotsParts = $robot->getRobotsParts(
    [
        "limit" => 5,
    ]
);

If the called method has a “get” prefix Phalcon\Mvc\Model will return a findFirst()/find() result. The following example compares retrieving related results with using magic methods and without:

<?php

use Store\Toys\Robots;

$robot = Robots::findFirst(2);

// Robots model has a 1-n (hasMany)
// relationship to RobotsParts then
$robotsParts = $robot->robotsParts;

// Only parts that match conditions
$robotsParts = $robot->getRobotsParts("created_at = '2015-03-15'");

// Or using bound parameters
$robotsParts = $robot->getRobotsParts(
    [
        "created_at = :date:",
        "bind" => [
            "date" => "2015-03-15"
        ]
    ]
);

$robotPart = RobotsParts::findFirst(1);

// RobotsParts model has a n-1 (belongsTo)
// relationship to RobotsParts then
$robot = $robotPart->robots;

Getting related records manually:

<?php

use Store\Toys\Robots;

$robot = Robots::findFirst(2);

// Robots model has a 1-n (hasMany)
// relationship to RobotsParts, then
$robotsParts = RobotsParts::find(
    "robots_id = '" . $robot->id . "'"
);

// Only parts that match conditions
$robotsParts = RobotsParts::find(
    "robots_id = '" . $robot->id . "' AND created_at = '2015-03-15'"
);

$robotPart = RobotsParts::findFirst(1);

// RobotsParts model has a n-1 (belongsTo)
// relationship to RobotsParts then
$robot = Robots::findFirst(
    "id = '" . $robotPart->robots_id . "'"
);

The prefix “get” is used to find()/findFirst() related records. Depending on the type of relation it will use find() or findFirst():

Type Description Implicit Method
Belongs-To Returns a model instance of the related record directly findFirst
Has-One Returns a model instance of the related record directly findFirst
Has-Many Returns a collection of model instances of the referenced model find
Has-Many-to-Many Returns a collection of model instances of the referenced model, it implicitly does ‘inner joins’ with the involved models (complex query)

You can also use the “count” prefix to return an integer denoting the count of the related records:

<?php

use Store\Toys\Robots;

$robot = Robots::findFirst(2);

echo "The robot has ", $robot->countRobotsParts(), " parts\n";

To explain better how aliases work, let’s check the following example:

The “robots_similar” table has the function to define what robots are similar to others:

mysql> desc robots_similar;
+-------------------+------------------+------+-----+---------+----------------+
| Field             | Type             | Null | Key | Default | Extra          |
+-------------------+------------------+------+-----+---------+----------------+
| id                | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| robots_id         | int(10) unsigned | NO   | MUL | NULL    |                |
| similar_robots_id | int(10) unsigned | NO   |     | NULL    |                |
+-------------------+------------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

Both “robots_id” and “similar_robots_id” have a relation to the model Robots:

/file/png/2016/09/26/eer-2.png

A model that maps this table and its relationships is the following:

<?php

class RobotsSimilar extends Phalcon\Mvc\Model
{
    public function initialize()
    {
        $this->belongsTo(
            "robots_id",
            "Store\\Toys\\Robots",
            "id"
        );

        $this->belongsTo(
            "similar_robots_id",
            "Store\\Toys\\Robots",
            "id"
        );
    }
}

Since both relations point to the same model (Robots), obtain the records related to the relationship could not be clear:

<?php

$robotsSimilar = RobotsSimilar::findFirst();

// Returns the related record based on the column (robots_id)
// Also as is a belongsTo it's only returning one record
// but the name 'getRobots' seems to imply that return more than one
$robot = $robotsSimilar->getRobots();

// but, how to get the related record based on the column (similar_robots_id)
// if both relationships have the same name?

The aliases allow us to rename both relationships to solve these problems:

<?php

use Phalcon\Mvc\Model;

class RobotsSimilar extends Model
{
    public function initialize()
    {
        $this->belongsTo(
            "robots_id",
            "Store\\Toys\\Robots",
            "id",
            [
                "alias" => "Robot",
            ]
        );

        $this->belongsTo(
            "similar_robots_id",
            "Store\\Toys\\Robots",
            "id",
            [
                "alias" => "SimilarRobot",
            ]
        );
    }
}

With the aliasing we can get the related records easily:

<?php

$robotsSimilar = RobotsSimilar::findFirst();

// Returns the related record based on the column (robots_id)
$robot = $robotsSimilar->getRobot();
$robot = $robotsSimilar->robot;

// Returns the related record based on the column (similar_robots_id)
$similarRobot = $robotsSimilar->getSimilarRobot();
$similarRobot = $robotsSimilar->similarRobot;

在使用模型关系的使用,大部分IDE的智能提示并不能更好的去判断模型关系中使用魔术方法返回的值的类型是否合法,我们可以通过重新魔术方法来让IDE更好的去工作,当然,这不是必须的。

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany(
            "id",
            "RobotsParts",
            "robots_id"
        );
    }

    /**
     * Return the related "robots parts"
     *
     * @return \RobotsParts[]
     */
    public function getRobotsParts($parameters = null)
    {
        return $this->getRelated("RobotsParts", $parameters);
    }
}

By default, relationships do not act like database foreign keys, that is, if you try to insert/update a value without having a valid value in the referenced model, Phalcon will not produce a validation message. You can modify this behavior by adding a fourth parameter when defining a relationship.

The RobotsPart model can be changed to demonstrate this feature:

<?php

use Phalcon\Mvc\Model;

class RobotsParts extends Model
{
    public $id;

    public $robots_id;

    public $parts_id;

    public function initialize()
    {
        $this->belongsTo(
            "robots_id",
            "Store\\Toys\\Robots",
            "id",
            [
                "foreignKey" => true
            ]
        );

        $this->belongsTo(
            "parts_id",
            "Parts",
            "id",
            [
                "foreignKey" => [
                    "message" => "The part_id does not exist on the Parts model"
                ]
            ]
        );
    }
}

If you alter a belongsTo() relationship to act as foreign key, it will validate that the values inserted/updated on those fields have a valid value on the referenced model. Similarly, if a hasMany()/hasOne() is altered it will validate that the records cannot be deleted if that record is used on a referenced model.

<?php

use Phalcon\Mvc\Model;

class Parts extends Model
{
    public function initialize()
    {
        $this->hasMany(
            "id",
            "RobotsParts",
            "parts_id",
            [
                "foreignKey" => [
                    "message" => "The part cannot be deleted because other robots are using it",
                ]
            ]
        );
    }
}

A virtual foreign key can be set up to allow null values as follows:

<?php

use Phalcon\Mvc\Model;

class RobotsParts extends Model
{
    public $id;

    public $robots_id;

    public $parts_id;

    public function initialize()
    {
        $this->belongsTo(
            "parts_id",
            "Parts",
            "id",
            [
                "foreignKey" => [
                    "allowNulls" => true,
                    "message"    => "The part_id does not exist on the Parts model",
                ]
            ]
        );
    }
}

Relationships that act as virtual foreign keys by default restrict the creation/update/deletion of records to maintain the integrity of data:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;
use Phalcon\Mvc\Model\Relation;

class Robots extends Model
{
    public $id;

    public $name;

    public function initialize()
    {
        $this->hasMany(
            "id",
            "Parts",
            "robots_id",
            [
                "foreignKey" => [
                    "action" => Relation::ACTION_CASCADE,
                ]
            ]
        );
    }
}

The above code set up to delete all the referenced records (parts) if the master record (robot) is deleted.

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

Count examples:

<?php

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

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

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

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

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

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

Sum examples:

<?php

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

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

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

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

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

Average examples:

<?php

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

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

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

Max/Min examples:

<?php

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

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

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

As mentioned above, resultsets are collections of complete objects, this means that every returned result is an object representing a row in the database. These objects can be modified and saved again to persistence:

<?php

use Store\Toys\Robots;

$robots = Robots::find();

// Manipulating a resultset of complete objects
foreach ($robots as $robot) {
    $robot->year = 2000;

    $robot->save();
}

Sometimes records are obtained only to be presented to a user in read-only mode, in these cases it may be useful to change the way in which records are represented to facilitate their handling. The strategy used to represent objects returned in a resultset is called ‘hydration mode’:

<?php

use Phalcon\Mvc\Model\Resultset;
use Store\Toys\Robots;

$robots = Robots::find();

// Return every robot as an array
$robots->setHydrateMode(
    Resultset::HYDRATE_ARRAYS
);

foreach ($robots as $robot) {
    echo $robot["year"], PHP_EOL;
}

// Return every robot as a stdClass
$robots->setHydrateMode(
    Resultset::HYDRATE_OBJECTS
);

foreach ($robots as $robot) {
    echo $robot->year, PHP_EOL;
}

// Return every robot as a Robots instance
$robots->setHydrateMode(
    Resultset::HYDRATE_RECORDS
);

foreach ($robots as $robot) {
    echo $robot->year, PHP_EOL;
}

Hydration mode can also be passed as a parameter of ‘find’:

<?php

use Phalcon\Mvc\Model\Resultset;
use Store\Toys\Robots;

$robots = Robots::find(
    [
        "hydration" => Resultset::HYDRATE_ARRAYS,
    ]
);

foreach ($robots as $robot) {
    echo $robot["year"], PHP_EOL;
}

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

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

<?php

use Store\Toys\Robots;

$robot = new Robots();

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

if ($robot->save() === false) {
    echo "Umh, We can't store robots right now: \n";

    $messages = $robot->getMessages();

    foreach ($messages as $message) {
        echo $message, "\n";
    }
} else {
    echo "Great, a new robot was saved successfully!";
}

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

<?php

use Store\Toys\Robots;

$robot = new Robots();

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

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

<?php

use Store\Toys\Robots;

$robot = new Robots();

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

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

<?php

use Store\Toys\Robots;

$robot = new Robots();

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

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

<?php

use Store\Toys\Robots;

$robot = new Robots();

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

// This record only must be created
if ($robot->create() === false) {
    echo "Umh, We can't store robots right now: \n";

    $messages = $robot->getMessages();

    foreach ($messages as $message) {
        echo $message, "\n";
    }
} else {
    echo "Great, a new robot was created successfully!";
}

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

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 SQL INSERT, so the database system can generate an auto-generated value for it. Always after creating a record, the identity field will be registered with the value generated in the database system for it:

<?php

$robot->save();

echo "The generated id is: ", $robot->id;

Phalcon\Mvc\Model is able to recognize the identity column. Depending on the database system, those columns may be serial columns like in PostgreSQL or auto_increment columns in the case of MySQL.

PostgreSQL uses sequences to generate auto-numeric values, by default, Phalcon tries to obtain the generated value from the sequence “table_field_seq”, for example: robots_id_seq, if that sequence has a different name, the getSequenceName() method needs to be implemented:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public function getSequenceName()
    {
        return "robots_sequence_name";
    }
}

Magic properties can be used to store a records and its related properties:

<?php

// Create an artist
$artist = new Artists();

$artist->name    = "Shinichi Osawa";
$artist->country = "Japan";

// Create an album
$album = new Albums();

$album->name   = "The One";
$album->artist = $artist; // Assign the artist
$album->year   = 2008;

// Save both records
$album->save();

Saving a record and its related records in a has-many relation:

<?php

// Get an existing artist
$artist = Artists::findFirst(
    "name = 'Shinichi Osawa'"
);

// Create an album
$album = new Albums();

$album->name   = "The One";
$album->artist = $artist;

$songs = [];

// Create a first song
$songs[0]           = new Songs();
$songs[0]->name     = "Star Guitar";
$songs[0]->duration = "5:54";

// Create a second song
$songs[1]           = new Songs();
$songs[1]->name     = "Last Days";
$songs[1]->duration = "4:29";

// Assign the songs array
$album->songs = $songs;

// Save the album + its songs
$album->save();

Saving the album and the artist at the same time implicitly makes use of a transaction so if anything goes wrong with saving the related records, the parent will not be saved either. Messages are passed back to the user for information regarding any errors.

Note: Adding related entities by overloading the following methods is not possible:

  • Phalcon\Mvc\Model::beforeSave()
  • Phalcon\Mvc\Model::beforeCreate()
  • Phalcon\Mvc\Model::beforeUpdate()

You need to overload Phalcon\Mvc\Model::save() for this to work from within a model.

Phalcon\Mvc\Model has a messaging subsystem that provides a flexible way to output or store the validation messages generated during the insert/update processes.

Each message consists of an instance of the class Phalcon\Mvc\Model\Message. The set of messages generated can be retrieved with the getMessages() method. Each message provides extended information like the field name that generated the message or the message type:

<?php

if ($robot->save() === false) {
    $messages = $robot->getMessages();

    foreach ($messages as $message) {
        echo "Message: ", $message->getMessage();
        echo "Field: ", $message->getField();
        echo "Type: ", $message->getType();
    }
}

Phalcon\Mvc\Model can generate the following types of validation messages:

Type Description
PresenceOf Generated when a field with a non-null attribute on the database is trying to insert/update a null value
ConstraintViolation Generated when a field part of a virtual foreign key is trying to insert/update a value that doesn’t exist in the referenced model
InvalidValue Generated when a validator failed because of an invalid value
InvalidCreateAttempt Produced when a record is attempted to be created but it already exists
InvalidUpdateAttempt Produced when a record is attempted to be updated but it doesn’t exist

The getMessages() method can be overridden in a model to replace/translate the default messages generated automatically by the ORM:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public function getMessages()
    {
        $messages = [];

        foreach (parent::getMessages() as $message) {
            switch ($message->getType()) {
                case "InvalidCreateAttempt":
                    $messages[] = "The record cannot be created because it already exists";
                    break;

                case "InvalidUpdateAttempt":
                    $messages[] = "The record cannot be updated because it doesn't exist";
                    break;

                case "PresenceOf":
                    $messages[] = "The field " . $message->getField() . " is mandatory";
                    break;
            }
        }

        return $messages;
    }
}

Models allow you to implement events that will be thrown when performing an insert/update/delete. They help define business rules for a certain model. The following are the events supported by Phalcon\Mvc\Model and their order of execution:

Operation Name Can stop operation? Explanation
Inserting/Updating beforeValidation YES Is executed before the fields are validated for not nulls/empty strings or foreign keys
Inserting beforeValidationOnCreate YES Is executed before the fields are validated for not nulls/empty strings or foreign keys when an insertion operation is being made
Updating beforeValidationOnUpdate YES Is executed before the fields are validated for not nulls/empty strings or foreign keys when an updating operation is being made
Inserting/Updating onValidationFails YES (already stopped) Is executed after an integrity validator fails
Inserting afterValidationOnCreate YES Is executed after the fields are validated for not nulls/empty strings or foreign keys when an insertion operation is being made
Updating afterValidationOnUpdate YES Is executed after the fields are validated for not nulls/empty strings or foreign keys when an updating operation is being made
Inserting/Updating afterValidation YES Is executed after the fields are validated for not nulls/empty strings or foreign keys
Inserting/Updating beforeSave YES Runs before the required operation over the database system
Updating beforeUpdate YES Runs before the required operation over the database system only when an updating operation is being made
Inserting beforeCreate YES Runs before the required operation over the database system only when an inserting operation is being made
Updating afterUpdate NO Runs after the required operation over the database system only when an updating operation is being made
Inserting afterCreate NO Runs after the required operation over the database system only when an inserting operation is being made
Inserting/Updating afterSave NO Runs after the required operation over the database system

The easier way to make a model react to events is implement a method with the same name of the event in the model’s class:

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public function beforeValidationOnCreate()
    {
        echo "This is executed before creating a Robot!";
    }
}

Events can be useful to assign values before performing an operation, for example:

<?php

use Phalcon\Mvc\Model;

class Products extends Model
{
    public function beforeCreate()
    {
        // Set the creation date
        $this->created_at = date("Y-m-d H:i:s");
    }

    public function beforeUpdate()
    {
        // Set the modification date
        $this->modified_in = date("Y-m-d H:i:s");
    }
}

Additionally, this component is integrated with Phalcon\Events\Manager, this means we can create listeners that run when an event is triggered.

<?php

namespace Store\Toys;

use Phalcon\Mvc\Model;
use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;

class Robots extends Model
{
    public function initialize()
    {
        $eventsManager = new EventsManager();

        // Attach an anonymous function as a listener for "model" events
        $eventsManager->attach(
            "model:beforeSave",
            function (Event $event, $robot) {
                if ($robot->name == "Scooby Doo") {
                    echo "Scooby Doo isn't a robot!";

                    return false;
                }

                return true;
            }
        );

        // Attach the events manager to the event
        $this->setEventsManager($eventsManager);
    }
}

In the example given above, the Events Manager only acts as a bridge between an object and a listener (the anonymous function). Events will be fired to the listener when ‘robots’ are saved:

<?php

use Store\Toys\Robots;

$robot = new Robots();

$robot->name = "Scooby Doo";
$robot->year = 1969;

$robot->save();

If we want all objects created in our application use the same EventsManager, then we need to assign it to the Models Manager:

<?php

use Phalcon\Events\Event;
use Phalcon\Events\Manager as EventsManager;

// Registering the modelsManager service
$di->setShared(
    "modelsManager",
    function () {
        $eventsManager = new EventsManager();

        // Attach an anonymous function as a listener for "model" events
        $eventsManager->attach(
            "model:beforeSave",
            function (Event $event, $model) {
                // Catch events produced by the Robots model
                if (get_class($model) === "Store\\Toys\\Robots") {
                    if ($model->name === "Scooby Doo") {
                        echo "Scooby Doo isn't a robot!";

                        return false;
                    }
                }

                return true;
            }
        );

        // Setting a default EventsManager
        $modelsManager = new ModelsManager();

        $modelsManager->setEventsManager($eventsManager);

        return $modelsManager;
    }
);

If a listener returns false that will stop the operation that is executing currently.

When an insert, update or delete is executed, the model verifies if there are any methods with the names of the events listed in the table above.

0人赞过 0条评论
查看发帖规则和积分规则

    还没有评论,快来抢占沙发吧~