完全重构的、PSR兼容的Paris版本:一个用于PHP5的轻量级Active Record实现,基于Idiorm构建。

v2.0.3 2012-10-25 05:03 UTC

This package is not auto-updated.

Last update: 2024-09-28 14:06:50 UTC


README

http://j4mie.github.com/idiormandparis/

PHP5的轻量级Active Record实现。

基于 Idiorm 构建。

在PHP 5.2.0+上测试 - 可能可以通过PDO和正确的数据库驱动在早期版本上运行。

BSD许可 下发布。

特性

  • 极简单的配置。
  • 暴露了Idiorm的流畅查询API的全部功能。
  • 支持关联。
  • 简单的机制来封装过滤器方法中的常见查询。
  • 基于 PDO 构建。
  • 始终使用预定义语句来保护免受SQL注入攻击。
  • 数据库无关。目前支持SQLite和MySQL。可能支持其他数据库,请尝试一下!

变更日志

1.0.0 - 发布于2010-12-01

  • 初始版本

1.1.0 - 发布于2011-01-24

  • 添加 is_dirty 方法

1.1.1 - 发布于2011-01-30

  • 修复错误的测试,请参阅问题 #12

2.0.0 - 发布于2012-09-19

2.0.1 - 发布于2012-10-24

  • 修复了两个调用错误Idiorm函数的错别字

2.0.2 - 发布于2012-10-25

  • 添加了更多测试以提高代码覆盖率,并修复了沿途发现的少量错误。

理念

Paris与Idiorm一样,采用“少即是多”的理念构建。

让我们看看一些代码

设置

Paris需要Idiorm。将Idiorm和Paris安装到项目目录的某个位置,并require它们。

require_once 'your/path/to/idiorm.php';
require_once 'your/path/to/paris.php';

然后,您需要告诉Idiorm如何连接到您的数据库。有关如何操作的完整详细信息,请参阅Idiorm的文档

简要地说,您需要将数据源名称连接字符串传递给ORM类的configure方法。

ORM::configure('sqlite:./example.db');

您可能还需要传递用户名和密码到您的数据库驱动程序,使用usernamepassword配置选项。例如,如果您正在使用MySQL

ORM::configure('mysql:host=localhost;dbname=my_database');
ORM::configure('username', 'database_user');
ORM::configure('password', 'top_secret');

模型类

您应该为应用程序中的每个实体创建一个模型类。例如,如果您正在构建需要用户的应用程序,您应该创建一个User类。您的模型类应该扩展基本Model

class User extends Model {
}

Paris负责创建您模型类的实例,并将来自数据库的数据填充到其中。然后,您可以在该类中以公共方法的形式添加行为,这些方法实现您的应用程序逻辑。这种数据和行为的组合是Active Record模式的精髓。

数据库表

您的User类应该有一个相应的user表,用于存储其数据。

默认情况下,Paris假定您的类名采用CapWords样式,您的表名采用lowercase_with_underscores样式。它将自动在这两种样式之间转换。例如,如果您的类名为CarTyre,Paris将查找名为car_tyre的表。

要覆盖此默认行为,请在您的类中添加一个名为$_tablepublic static属性。

class User extends Model {
    public static $_table = 'my_user_table';
}

ID列

Paris要求您的数据库表具有唯一的主键列。默认情况下,Paris将使用名为id的列。要覆盖此默认行为,请在您的类中添加一个名为$_id_columnpublic static属性。

class User extends Model {
    public static $_id_column = 'my_id_column';
}

注意 - Paris有自己的默认ID列名称机制,不遵守Idiorm配置中指定的列名称。

查询

查询允许您从数据库中选择数据并填充模型类的实例。查询从一个对基本Model类的静态工厂方法的调用开始,该方法接受一个参数:您希望用于查询的模型类的名称。然后,该工厂方法用作方法链的起点,这使您能够完全访问Idiorm的流畅查询API。请参阅Idiorm文档了解此API的详细信息。

例如

$users = Model::factory('User')
    ->where('name', 'Fred')
    ->where_gte('age', 20)
    ->find_many();

您还可以使用Idiorm提供的相同快捷方式,通过主键ID查找记录。

$user = Model::factory('User')->find_one($id);

使用Idiorm和Paris进行查询之间的唯一区别如下

  1. 您不需要调用for_table方法来指定要使用的数据库表。Paris将自动根据类名(或如果存在,则根据$_table静态属性)提供此信息。

  2. find_onefind_many方法将返回您的模型子类的实例,而不是基ORM类。与Idiorm一样,find_one将返回单个实例或false(如果没有行与您的查询匹配),而find_many将返回实例数组,如果没有任何行与查询匹配,则该数组可能为空。

  3. 自定义过滤,请参阅下一节。

您还可以获取查询返回的行数的计数。此方法的行为与Idiorm的count方法完全相同

$count = Model::factory('User')->where_lt('age', 20)->count();

关联

Paris提供了一个简单的API,用于模型之间的一对一、一对多和多对多关系(关联)。它采用与许多其他ORM不同的方法,这些ORM使用关联数组在模型类中添加关于关系的配置元数据。这些数组通常很深很复杂,因此很容易出错。

相反,Paris将查询关系视为一种行为,并提供一组辅助方法来帮助生成此类查询。这些辅助方法应从您的模型类的方法中调用,这些方法的命名描述了关系。这些方法返回ORM实例(而不是实际的Model实例),因此,如果需要,可以在运行之前修改和添加关系查询。

摘要

以下列表总结了Paris提供的关联,并解释了哪个辅助方法支持每种关联类型

一对一

在基本模型中使用has_one,在关联模型中使用belongs_to

一对多

在基本模型中使用has_many,在关联模型中使用belongs_to

多对多

在基本模型和关联模型中都使用has_many_through

下面将详细介绍每个关联辅助方法。

一对一

一对一关系使用has_one方法实现。例如,假设我们有一个User模型。每个用户只有一个Profile,因此user表应该与profile表相关联。为了能够找到特定用户的配置文件,我们应该在User类中添加一个名为profile的方法(注意,这里的方法名是任意的,但应该描述关系)。此方法调用Paris提供的受保护的has_one方法,传入相关对象的类名。此profile方法应该返回一个ORM实例,可用于(可选)进一步筛选。

class Profile extends Model {
}

class User extends Model {
    public function profile() {
        return $this->has_one('Profile');
    }
}

此方法的API如下

// Select a particular user from the database
$user = Model::factory('User')->find_one($user_id);

// Find the profile associated with the user
$profile = $user->profile()->find_one();

默认情况下,Paris假定相关表的关联键列与当前(基)表同名,并在其后追加_id。在上面的例子中,Paris将在Profile类所用的表中查找名为user_id的外键列。要覆盖此行为,请在has_one调用中添加第二个参数,传入要使用的列名。

多对一

一对多关系使用has_many方法实现。例如,假设我们有一个User模型。每个用户有多个Post对象。因此user表应该与post表相关联。为了能够找到特定用户的帖子,我们应该在User类中添加一个名为posts的方法(注意,这里的方法名是任意的,但应该描述关系)。此方法调用Paris提供的受保护的has_many方法,传入相关对象的类名。直接传递模型类名,而不是复数形式。此posts方法应该返回一个ORM实例,可用于(可选)进一步筛选。

class Post extends Model {
}

class User extends Model {
    public function posts() {
        return $this->has_many('Post'); // Note we use the model name literally - not a pluralised version
    }
}

此方法的API如下

// Select a particular user from the database
$user = Model::factory('User')->find_one($user_id);

// Find the posts associated with the user
$posts = $user->posts()->find_many();

默认情况下,Paris假定相关表的关联键列与当前(基)表同名,并在其后追加_id。在上面的例子中,Paris将在Post类所用的表中查找名为user_id的外键列。要覆盖此行为,请在has_many调用中添加第二个参数,传入要使用的列名。

属于

has_onehas_many的“另一边”是belongs_to。此方法调用与这些方法具有相同的参数,但假定外键位于当前(基)表中,而不是相关表中。

class Profile extends Model {
    public function user() {
        return $this->belongs_to('User');
    }
}

class User extends Model {
}

此方法的API如下

// Select a particular profile from the database
$profile = Model::factory('Profile')->find_one($profile_id);

// Find the user associated with the profile
$user = $profile->user()->find_one();

同样,Paris假定当前(基)表的外键与相关表同名,并在其后追加_id。在上面的例子中,Paris将查找名为user_id的列。要覆盖此行为,将第二个参数传递给belongs_to方法,指定要使用的当前(基)表列名。

多对多通过

多对多关系使用has_many_through方法实现。此方法只有一个必需参数:相关模型的名称。提供更多参数允许我们覆盖方法的默认行为。

例如,假设我们有一个Book模型。每本Book可能有几个Author对象,并且每个Author可能写了多本Books。为了能够找到特定书籍的作者,我们首先创建一个中间模型。此模型的名称应由两个相关类的名称按字母顺序连接而成。在这种情况下,我们的类名是AuthorBook,所以中间模型应命名为AuthorBook

然后,我们应该在Book类中添加一个名为authors的方法(注意,这里的方法名是任意的,但应该描述关系)。此方法调用Paris提供的受保护的方法has_many_through,传入相关对象的类名。直接传入模型类名,而不是复数形式authors方法应该返回一个ORM实例,以便(可选)进一步筛选。

class Author extends Model {
    public function books() {
        return $this->has_many_through('Book');
    }
}

class Book extends Model {
    public function authors() {
        return $this->has_many_through('Author');
    }
}

class AuthorBook extends Model {
}

此方法的API如下

// Select a particular book from the database
$book = Model::factory('Book')->find_one($book_id);

// Find the authors associated with the book
$authors = $book->authors()->find_many();

// Get the first author
$first_author = $authors[0];

// Find all the books written by this author
$first_author_books = $first_author->books()->find_many();
覆盖默认设置

has_many_through方法最多接受四个参数,这允许我们逐步覆盖该方法做出的默认假设。

第一个参数:关联模型名称 - 这是必需的,应该是我们希望在关联中选择的模型名称。

第二个参数:中间模型名称 - 这是可选的,默认为两个关联模型名称,按字母顺序排序并连接。

第三个参数:基于中间表的表的自定义键 - 这是可选的,默认为在基本表名称后附加_id

第四个参数:基于中间表上的关联表的自定义键 - 这是可选的,默认为在关联表名称后附加_id

筛选器

通常希望创建可重用的查询,可以用于提取特定数据子集,而不必重复大量代码。Paris通过提供名为filter的方法来实现这一点,该方法可以在现有Idiorm查询API的查询中串联使用。筛选器方法接受当前模型子类上的一个公共静态方法名称作为参数。在调用filter的链中,将调用提供的方法,并将ORM对象作为第一个参数传递。该方法应在调用一个或多个查询方法后返回ORM对象。如果需要,可以继续方法链。

用示例来说明这一点最容易。想象一个应用程序,其中用户可以被分配一个角色,该角色控制他们对某些功能部件的访问。在这种情况下,您可能经常希望检索具有角色'admin'的用户列表。为此,请将一个名为(例如)admins的静态方法添加到您的模型类中

class User extends Model {
    public static function admins($orm) {
        return $orm->where('role', 'admin');
    }
}

您可以在查询中使用此筛选器

$admin_users = Model::factory('User')->filter('admins')->find_many();

您也可以像通常一样将其与其他方法串联

$young_admins = Model::factory('User')
                    ->filter('admins')
                    ->where_lt('age', 18)
                    ->find_many();

带有参数的筛选器

您还可以向自定义筛选器传递参数。传递给filter方法(在要应用的筛选器名称之后)的任何其他参数都将作为额外参数(在ORM实例之后)传递到您的自定义筛选器中。

例如,假设您希望将上述角色筛选器(请参阅上面)泛化,以便您可以检索具有任何角色的用户。您可以将角色名称传递给筛选器作为参数

class User extends Model {
    public static function has_role($orm, $role) {
        return $orm->where('role', $role);
    }
}

$admin_users = Model::factory('User')->filter('has_role', 'admin')->find_many();
$guest_users = Model::factory('User')->filter('has_role', 'guest')->find_many();

这些示例可能看起来很简单(使用filter('has_role', 'admin')可以像使用where('role', 'admin')一样轻松实现),但请记住,筛选器可以包含任意复杂的代码 - 添加raw_where子句或完整的raw_query调用以执行连接等。筛选器提供了一种强大的机制,可以在模型查询API中隐藏复杂性。

从对象中获取数据、更新和插入数据

您的查询返回的模型实例现在表现得就像它们是Idiorm原始ORM类的实例一样。

您可以访问数据

$user = Model::factory('User')->find_one($id);
echo $user->name;

更新数据并保存实例

$user = Model::factory('User')->find_one($id);
$user->name = 'Paris';
$user->save();

要创建一个新的(空)实例,请使用create方法

$user = Model::factory('User')->create();
$user->name = 'Paris';
$user->save();

要检查属性是否在对象创建(或上次保存)后已更改,请调用is_dirty方法

$name_has_changed = $person->is_dirty('name'); // Returns true or false

当然,因为这些对象是您的基模型类的实例,因此您也可以调用您在它们上定义的方法

class User extends Model {
    public function full_name() {
        return $this->first_name . ' ' . $this->last_name;
    }
}

$user = Model::factory('User')->find_one($id);
echo $user->full_name();

要删除与您的模型实例关联的数据库行,请调用其delete方法

$user = Model::factory('User')->find_one($id);
$user->delete();

您还可以使用as_array方法获取由模型子类实例包裹的所有数据。这将返回一个关联数组,将列名(键)映射到它们的值。

as_array方法将列名作为可选参数。如果提供了这些参数中的一个或多个,则只返回匹配的列名。

class Person extends Model {
}

$person = Model::factory('Person')->create();

$person->first_name = 'Fred';
$person->surname = 'Bloggs';
$person->age = 50;

// Returns array('first_name' => 'Fred', 'surname' => 'Bloggs', 'age' => 50)
$data = $person->as_array();

// Returns array('first_name' => 'Fred', 'age' => 50)
$data = $person->as_array('first_name', 'age');

关于验证的说明

通常认为将数据验证集中在一个地方是一个好主意,而且一个好的地方是在您的模型类内部进行。这比与表单处理代码一起处理验证要好。将验证代码放在模型中意味着如果您将来扩展应用程序以通过替代路径更新模型(例如,通过REST API而不是表单),则可以重用相同的验证代码。

尽管如此,Paris没有提供任何内置的验证支持。这是因为验证可能相当复杂,并且通常是特定于应用程序的。Paris故意对自己的实际数据相当无知——它只是执行查询,并让您负责确保模型中的数据是有效和正确的。向Paris添加完整的验证框架可能需要比Paris本身更多的代码!

然而,有几种简单的方法可以添加验证到您的模型中,而不需要Paris的帮助。您可以覆盖save()方法,检查数据是否有效,在失败时返回false,或者在成功时调用parent::save()。您可以为基类Model创建自己的子类并添加自己的通用验证方法。或者,您可以编写自己的外部验证框架,将模型实例传递给它以进行检查。选择最适合您需求的方案。

配置

Paris本身提供的唯一配置选项是模型类上的$_table$_id_column静态属性。要配置数据库连接,您应通过ORM::configure方法使用Idiorm的配置系统。有关完整详细信息,请参阅Idiorm的文档

事务

Paris(或Idiorm)没有提供任何处理事务的额外方法,但使用PDO的内置方法非常简单

// Start a transaction
ORM::get_db()->beginTransaction();

// Commit a transaction
ORM::get_db()->commit();

// Roll back a transaction
ORM::get_db()->rollBack();

有关更多详细信息,请参阅PDO事务的文档

查询日志记录

Idiorm可以记录它执行的所有查询。要启用查询日志记录,将logging选项设置为true(默认为false)。

ORM::configure('logging', true);

当启用查询日志记录时,您可以使用两个静态方法来访问日志。ORM::get_last_query()返回最近执行的查询。ORM::get_query_log()返回一个包含所有执行的查询的数组。