vlucas / spot2
基于 Doctrine DBAL 的简单 DataMapper
Requires
- php: >=5.4.0
- doctrine/dbal: ^2.5.4
- sabre/event: ~2.0
- vlucas/valitron: ~1.1
Requires (Dev)
- phpunit/phpunit: ^4.8
README
Spot v2.x 是基于 Doctrine DBAL 构建,并针对 PHP 5.4+。
Spot 的目标是成为一个轻量级的 DataMapper 代替品,它清晰、高效、简单,并且不使用注解或代理类。
在您的项目中使用 Spot
Spot 是一个独立的 ORM,可以在任何项目中使用。按照以下说明在项目中设置 Spot。
使用 Composer 安装
composer require vlucas/spot2
连接到数据库
Spot\Locator
对象是访问 spot 的主要点,您需要在需要运行查询或处理实体的地方访问它。它负责加载映射器和管理配置。要创建定位器,您需要一个 Spot\Config
对象。
Spot\Config
对象存储并引用按名称命名的数据库连接。创建一个新的 Spot\Config
实例,并添加具有 DSN 字符串的数据库连接,以便 Spot 可以建立数据库连接,然后创建您的定位器对象
$cfg = new \Spot\Config(); // MySQL $cfg->addConnection('mysql', 'mysql://user:password@localhost/database_name'); // Sqlite $cfg->addConnection('sqlite', 'sqlite://path/to/database.sqlite'); $spot = new \Spot\Locator($cfg);
如果您喜欢,也可以使用 DBAL 兼容的配置数组 代替 DSN 字符串
$cfg->addConnection('mysql', [ 'dbname' => 'mydb', 'user' => 'user', 'password' => 'secret', 'host' => 'localhost', 'driver' => 'pdo_mysql', ]);
访问定位器
由于您需要在任何使用数据库的地方都能访问您的映射器,大多数人会创建一个辅助方法来创建一个映射器实例一次,然后再次需要时返回相同的实例。这样的辅助方法可能看起来像这样
function spot() { static $spot; if($spot === null) { $spot = new \Spot\Locator(); $spot->config()->addConnection('test_mysql', 'mysql://user:password@localhost/database_name'); } return $spot; }
如果您使用的是一个带有依赖注入容器或服务的框架,您将想使用它,以便 Spot\Locator
对象在您需要的地方都可以在您的应用程序中访问。
获取映射器
由于 Spot 遵循 DataMapper 设计模式,您将需要一个映射器实例来处理对象实体和数据库表。您可以通过向 Spot\Locator
对象的 mapper
方法提供完全限定的实体命名空间 + 类名来从 Spot\Locator
对象获取映射器实例
$postMapper = $spot->mapper('Entity\Post');
映射器只与一种实体类型一起工作,因此您将为每个要处理的实体类需要一个映射器(例如,要保存 Entity\Post,您需要一个适当的映射器,要保存 Entity\Comment,您需要一个评论映射器,而不是相同的帖子映射器。Spot 将自动通过其对应的映射器加载和处理关系)。
注意:除非您需要自定义查找方法或其他自定义逻辑,否则您不需要为每个实体创建映射器。如果您想要的实体没有特定映射器,Spot 将为您加载通用的映射器并返回它。
创建实体
实体类可以按照您在项目结构中设置的任何名称和命名空间来命名。对于以下示例,实体将仅使用 Entity
命名空间进行前缀,以便轻松符合 psr-0 自动加载规范。
namespace Entity; use Spot\EntityInterface as Entity; use Spot\MapperInterface as Mapper; class Post extends \Spot\Entity { protected static $table = 'posts'; public static function fields() { return [ 'id' => ['type' => 'integer', 'autoincrement' => true, 'primary' => true], 'title' => ['type' => 'string', 'required' => true], 'body' => ['type' => 'text', 'required' => true], 'status' => ['type' => 'integer', 'default' => 0, 'index' => true], 'author_id' => ['type' => 'integer', 'required' => true], 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] ]; } public static function relations(Mapper $mapper, Entity $entity) { return [ 'tags' => $mapper->hasManyThrough($entity, 'Entity\Tag', 'Entity\PostTag', 'tag_id', 'post_id'), 'comments' => $mapper->hasMany($entity, 'Entity\Post\Comment', 'post_id')->order(['date_created' => 'ASC']), 'author' => $mapper->belongsTo($entity, 'Entity\Author', 'author_id') ]; } }
使用自定义映射器
尽管您不需要为每个实体创建映射器,但有时如果您有很多自定义查找方法或想要一个更好的地方来包含构建所需的所有查询的逻辑,创建一个映射器会很有用。
只需在实体中指定完整的映射器类名即可
namespace Entity; class Post extends \Spot\Entity { protected static $mapper = 'Entity\Mapper\Post'; // ... snip ... }
然后创建您的映射器
namespace Entity\Mapper; use Spot\Mapper; class Post extends Mapper { /** * Get 10 most recent posts for display on the sidebar * * @return \Spot\Query */ public function mostRecentPostsForSidebar() { return $this->where(['status' => 'active']) ->order(['date_created' => 'DESC']) ->limit(10); } }
然后,当您像平时一样加载映射器时,Spot 会看到您定义的自定义 Entity\Post::$mapper
,并加载它而不是通用的映射器,这使得您能够调用您自定义的方法
$mapper = $spot->mapper('Entity\Post'); $sidebarPosts = $mapper->mostRecentPostsForSidebar();
字段类型
由于 Spot v2.x 是基于 DBAL 构建的,所以所有 DBAL 类型 都在 Spot 中使用,并得到全面支持
整数类型
smallint
integer
bigint
十进制类型
decimal
float
字符串类型
string
text
guid
二进制字符串类型
binary
blob
布尔/位类型
boolean
日期和时间类型
date
datetime
datetimetz
time
数组类型
array
- PHP 序列化/反序列化simple_array
- PHP implode/explodejson_array
- json_encode/json_decode
对象类型
object
- PHP 序列化/反序列化
请仔细阅读 Doctrine DBAL 类型参考页面 以获取更多信息、类型和跨数据库支持。某些类型可能在不同数据库上的存储方式不同,这取决于数据库供应商的支持和其他因素。
注册自定义字段类型
如果您想注册自己的自定义字段类型,并在 get/set 上实现自定义功能,请查看 DBAL 参考页面上的自定义映射类型。
由于 Spot 使用 DBAL 作为内部组件,因此您无需对自定义类型进行任何额外更改即可使其与 Spot 一起工作。
迁移/创建和更新表
Spot 提供了一种对实体运行迁移的方法,它将根据当前实体的 fields
定义自动创建和修改表。
$mapper = $spot->mapper('Entity\Post'); $mapper->migrate();
现在您的数据库中应该有一个名为 posts
的表,其中包含您在 Post
实体中描述的所有字段。
注意:请注意,迁移不支持重命名列,因为 spot 无法知道您将哪个列重命名为什么 - Spot 将看到一个需要创建的新列,以及一个不再存在的列需要删除。这可能导致在自动迁移过程中数据丢失。
查找器(映射器)
最常用的主要查找器是 all
,用于返回实体集合,以及 first
或 get
,用于返回匹配条件的一个单独实体。
all()
查找所有实体并返回一个包含已加载 Spot\Entity
对象的 Spot\Entity\Collection
。
where([条件])
查找所有匹配给定条件的实体并返回一个包含已加载 Spot\Entity
对象的 Spot\Entity\Collection
。
// Where can be called directly from the mapper $posts = $mapper->where(['status' => 1]); // Or chained using the returned `Spot\Query` object - results identical to above $posts = $mapper->all()->where(['status' => 1]); // Or more explicitly using using `select`, which always returns a `Spot\Query` object $posts = $mapper->select()->where(['status' => 1]);
由于返回了一个 Spot\Query
对象,因此可以以任何方式或顺序链接条件和其他语句。查询将在迭代或 count
时惰性执行,或者通过在链的末尾调用 execute()
来手动执行。
first([条件])
查找并返回一个匹配标准的单个 Spot\Entity
对象。
$post = $mapper->first(['title' => "Test Post"]);
或者 first
可以在之前使用 all
的查询上使用,以获取仅匹配的第一个记录。
$post = $mapper->all(['title' => "Test Post"])->first();
调用 first
将始终立即执行查询,并返回一个已加载的实体对象或布尔值 false
。
条件查询
# All posts with a 'published' status, descending by date_created $posts = $mapper->all() ->where(['status' => 'published']) ->order(['date_created' => 'DESC']); # All posts that are not published $posts = $mapper->all() ->where(['status <>' => 'published']) # All posts created before 3 days ago $posts = $mapper->all() ->where(['date_created <' => new \DateTime('-3 days')]); # Posts with 'id' of 1, 2, 5, 12, or 15 - Array value = automatic "IN" clause $posts = $mapper->all() ->where(['id' => [1, 2, 5, 12, 15]]);
连接
Spot 的查询构建器目前尚未启用连接。Doctine DBAL 查询构建器完全支持它们,因此它们可能在将来启用。
自定义查询
虽然像 Spot 这样的 ORM 非常易于使用,但如果您需要进行复杂查询,最好使用您熟悉的 SQL 来编写自定义查询。
Spot提供了一个query
方法,允许您运行自定义SQL,并将结果加载到一组普通实体对象中。这样,您就可以像使用内置的查找方法一样轻松地运行自定义SQL查询,而且您不需要做任何特殊处理。
使用自定义SQL
$posts = $mapper->query("SELECT * FROM posts WHERE id = 1");
使用查询参数
$posts = $mapper->query("SELECT * FROM posts WHERE id = ?", [1]);
使用命名占位符
$posts = $mapper->query("SELECT * FROM posts WHERE id = :id", ['id' => 1]);
注意:Spot将从您运行的查询中加载目标实体上的所有返回列。因此,如果您执行了JOIN或获取了比目标实体通常具有更多的数据,它将仅加载到目标实体上,并且不会尝试将数据映射到其他实体或根据定义的仅字段过滤它。
关系
关系是方便地从另一个已加载的实体对象访问相关、父级和子级实体的方法。一个例子可能是使用$post->comments
查询与当前$post
对象相关的所有评论。
实时查询对象
所有关系都返回为扩展Spot\Relation\RelationAbstract
的关系类实例。这个类内部持有Spot\Query
对象,并允许您在它上面链式执行自己的查询修改,以便您可以对关系执行自定义操作,例如排序、添加更多查询条件等。
$mapper->hasMany($entity, 'Entity\Comment', 'post_id') ->where(['status' => 'active']) ->order(['date_created' => 'ASC']);
所有这些查询修改都保存在队列中,并在关系实际执行时(在count
或foreach
迭代时,或在显式调用execute
时)运行。
预加载
默认情况下,所有关系类型都是延迟加载的,可以使用with
方法预加载以解决N+1查询问题。
$posts = $posts->all()->with('comments');
可以使用数组预加载多个关系
$posts = $posts->all()->with(['comments', 'tags']);
关系类型
实体关系类型有
HasOne
BelongsTo
HasMany
HasManyThrough
HasOne
HasOne是一种关系,其中相关对象有一个字段指向当前对象——一个例子可能是User
有一个Profile
。
方法
$mapper->hasOne(Entity $entity, $foreignEntity, $foreignKey)
$entity
- 当前实体实例$foreignEntity
- 您要加载的实体名称$foreignKey
- 在$foreignEntity
上与当前实体主键匹配的字段名
示例
namespace Entity; use Spot\EntityInterface as Entity; use Spot\MapperInterface as Mapper; class User extends \Spot\Entity { protected static $table = 'users'; public static function fields() { return [ 'id' => ['type' => 'integer', 'autoincrement' => true, 'primary' => true], 'username' => ['type' => 'string', 'required' => true], 'email' => ['type' => 'string', 'required' => true], 'status' => ['type' => 'integer', 'default' => 0, 'index' => true], 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] ]; } public static function relations(Mapper $mapper, Entity $entity) { return [ 'profile' => $mapper->hasOne($entity, 'Entity\User\Profile', 'user_id') ]; } }
在这个场景中,Entity\User\Profile
实体有一个名为user_id
的字段,其值为Entity\User
的id
字段。请注意,此实体上不存在此关系的字段,而是相关实体。
BelongsTo
BelongsTo是一种关系,其中当前对象有一个字段指向相关对象——一个例子可能是Post
属于User
。
方法
$mapper->belongsTo(Entity $entity, $foreignEntity, $localKey)
$entity
- 当前实体实例$foreignEntity
- 您要加载的实体名称$localKey
- 当前实体上与$foreignEntity
(您要加载的)主键匹配的字段名
示例
namespace Entity; use Spot\EntityInterface as Entity; use Spot\MapperInterface as Mapper; class Post extends \Spot\Entity { protected static $table = 'posts'; public static function fields() { return [ 'id' => ['type' => 'integer', 'autoincrement' => true, 'primary' => true], 'user_id' => ['type' => 'integer', 'required' => true], 'title' => ['type' => 'string', 'required' => true], 'body' => ['type' => 'text', 'required' => true], 'status' => ['type' => 'integer', 'default' => 0, 'index' => true], 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] ]; } public static function relations(Mapper $mapper, Entity $entity) { return [ 'user' => $mapper->belongsTo($entity, 'Entity\User', 'user_id') ]; } }
在这个场景中,Entity\Post
实体有一个名为user_id
的字段,其值为Entity\User
的id
字段。请注意,此实体上存在此关系的字段,但相关实体上不存在。
HasMany
HasMany用于单个记录与多个其他记录相关的情况——一个例子可能是Post
有许多Comments
。
方法
$mapper->hasMany(Entity $entity, $entityName, $foreignKey, $localValue = null)
$entity
- 当前实体实例$entityName
- 您要加载的实体集合的名称$foreignKey
- 在$entityName
上与当前实体主键匹配的字段名
示例
我们首先在我们的Post
对象上添加一个comments
关系
namespace Entity; use Spot\EntityInterface as Entity; use Spot\MapperInterface as Mapper; class Post extends Spot\Entity { protected static $table = 'posts'; public static function fields() { return [ 'id' => ['type' => 'integer', 'autoincrement' => true, 'primary' => true], 'title' => ['type' => 'string', 'required' => true], 'body' => ['type' => 'text', 'required' => true], 'status' => ['type' => 'integer', 'default' => 0, 'index' => true], 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] ]; } public static function relations(Mapper $mapper, Entity $entity) { return [ 'comments' => $mapper->hasMany($entity, 'Entity\Comment', 'post_id')->order(['date_created' => 'ASC']), ]; } }
并添加一个带有'belongsTo'关系的Entity\Post\Comment
对象回到帖子
namespace Entity; class Comment extends \Spot\Entity { // ... snip ... public static function relations(Mapper $mapper, Entity $entity) { return [ 'post' => $mapper->belongsTo($entity, 'Entity\Post', 'post_id') ]; } }
HasManyThrough
HasManyThrough 用于多对多关系。一个很好的例子是标签。一篇文章可以有多个标签,一个标签也可以有多个文章。这种关系比其他关系稍微复杂一些,因为 HasManyThrough 需要一个连接表和映射器。
方法
$mapper->hasManyThrough(Entity $entity, string $hasManyEntity, string $throughEntity, string $selectField, string $whereField)
$entity
- 当前实体实例$hasManyEntity
- 这是您想要收集的目标实体。在这种情况下,我们想要一个Entity\Tag
对象的集合。$throughEntity
- 我们正在通过它来获取我们想要的实体的名称 - 在这种情况下,Entity\PostTag
。$selectField
-$throughEntity
上的字段名称,该字段将通过$hasManyEntity
的主键来选择记录。$whereField
-$throughEntity
上的字段名称,用于通过当前实体的主键来选择记录(我们有一个文章,所以这将是对Entity\PostTag->post_id
字段的引用)。
示例
我们需要向我们的 Post
实体添加 tags
关系,并指定关系的两边的查询条件。
namespace Entity; use Spot\EntityInterface as Entity; use Spot\MapperInterface as Mapper; class Post extends Spot\Entity { protected static $table = 'posts'; public static function fields() { return [ 'id' => ['type' => 'integer', 'autoincrement' => true, 'primary' => true], 'title' => ['type' => 'string', 'required' => true], 'body' => ['type' => 'text', 'required' => true], 'status' => ['type' => 'integer', 'default' => 0, 'index' => true], 'date_created' => ['type' => 'datetime', 'value' => new \DateTime()] ]; } public static function relations(Mapper $mapper, Entity $entity) { return [ 'tags' => $mapper->hasManyThrough($entity, 'Entity\Tag', 'Entity\PostTag', 'tag_id', 'post_id'), ]; }
说明
我们想要的结果是一个 Entity\Tag
对象的集合,其中 id 等于 post_tags.tag_id
列。我们通过遍历 Entity\PostTags
实体,使用当前加载的文章 id 与 post_tags.post_id
匹配来获取这个结果。