pnoexz / spot3
Spot2 的分支:基于 Doctrine DBAL 构建的简单 DataMapper
Requires
- php: >=7.1
- ext-json: *
- ext-pdo: *
- doctrine/dbal: ^2.5.4
- sabre/event: ~2.0
- vlucas/valitron: ~1.1
Requires (Dev)
- phpunit/phpunit: ^4.8
This package is auto-updated.
Last update: 2024-09-23 06:06:24 UTC
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);
如果您更喜欢使用 DSN 字符串,也可以使用 DBAL 兼容的配置数组
$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
方法获取映射器实例
$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查询构建器完全支持它们,因此它们可能在将来启用。
自定义查询
虽然ORM如Spot使用起来非常方便,但如果您需要执行复杂查询,最好还是使用您熟悉的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
字段的字段)。
示例
我们需要将 tags
关系添加到我们的 Post
实体中,并指定关系的两边的查询条件。
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
来获取这个结果。