vlucas/spot2

基于 Doctrine DBAL 的简单 DataMapper

v2.2.1 2018-10-09 10:05 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);

如果您喜欢,也可以使用 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/explode
  • json_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,用于返回实体集合,以及 firstget,用于返回匹配条件的一个单独实体。

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']);

所有这些查询修改都保存在队列中,并在关系实际执行时(在countforeach迭代时,或在显式调用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\Userid字段。请注意,此实体上不存在此关系的字段,而是相关实体

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\Userid字段。请注意,此实体上存在此关系的字段,但相关实体上不存在

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 匹配来获取这个结果。