ornament/core

PHP7 ORM工具包,核心包

0.16.5 2022-12-21 13:36 UTC

README

PHP7 ORM工具包,核心包

ORM是一个变幻莫测的生物。许多库(例如Propel、Doctrine、Eloquent等)假设你的数据库应该与你的模型相对应。这根本不是事实;模型包含业务逻辑,可能、可能不或部分地引用数据库表、NoSQL数据库、平面文件、外部API或任何东西(ORM中的“R”实际上应该代表“资源”,而不是“关系”)。重点是:模型不应该关心,而且不应该通过它们的名称进行“常规”映射。(一个常见的例子是多语言页面的模型,其中数据可能存储在一个page表中,以及一个用于特定语言数据的page_i18n表。)

此外,使用广泛的和/或复杂的配置文件真的很糟糕。(XML?这已经是2020年了,人们!)

Ornament的设计目标是

  • 使使用纯PHP类作为模型变得超级简单;
  • 推广将模型作为“愚蠢”的数据容器使用;
  • 鼓励将存储逻辑卸载到辅助类(“存储库”)中;
  • 通过简单的插件机制使模型可扩展。

安装

$ composer require ornament/core

你可能还希望使用ornament/*家族的辅助包。

基本用法

Ornament模型(如果你习惯于Doctrine的术语,则是“实体”)实际上不过就是纯PHP类;不需要扩展任何类型的基对象(因为你可能想在你的框架中这样做)。

Ornament是一个工具包,因此它提供了一些可以useTrait和一些辅助装饰类来扩展你的模型行为,使其超出普通范围。

最基本实现如下

<?php

use Ornament\Core\Model;

class MyModel
{
    // The generic Model trait that bootstraps this class as an Ornament model;
    // it contains core functionality.
    use Model;

    /**
     * All protected properties on a model are considered read-only.
     */
    protected int $id;

    /**
     * Public properties are read/write. To auto-decorate during setting, use
     * the `Model::set()` method.
     */
    public string $name;

    /**
     * Private properties are just that: private. They're left alone.
     */
    private string $password;
}

// Assuming $source is a handle to a data source (in this case, a PDO
// statement):
$model = MyModel::fromIterable($source->fetch(PDO::FETCH_ASSOC));
echo $model->id; // 1
echo $model->name; // Marijn
echo $model->password; // Error: private property.
$model->name = 'Linus'; // Ok; public property.
$model->id = 2; // Error: read-only property.

PHP将处理内置类型转换,而Ornament将处理更复杂的转换和装饰,这样你也可以将类用作装饰器(有关更多信息,请参阅以下内容)。

上述示例还没有做很多事情,只是公开了只读的受保护id属性。然而,请注意,Ornament模型还会阻止修改未在类定义中明确设置的属性;尝试设置任何未在类定义中明确设置的属性将抛出一个类似于PHP内部错误的Error

注释和装饰模型

直到你开始装饰你的模型,Ornament才真正有用。这通过在具有实现Ornament\Core\DecoratorInterface的类名的属性上指定类型提示来完成。

获取虚拟属性的getters

Ornament模型支持“虚拟属性”的概念(按定义,它们是只读的)。

虚拟属性的例子是一个具有firstnamelastname属性的模型,以及一个获取fullname的getter。要将方法标记为getter,请使用#[\Ornament\Core\Getter("property")]属性。

<?php

class MyModel
{
    // ...

    #[\Ornament\Core\Getter("fullname")]
    protected function exampleGetter() : string
    {
        return $this->firstname.' '.$this->lastname;
    }
}

getter的名称和可见性通常不重要;最佳实践是将它们标记为protected,以便它们不能从外部调用,并为它们提供一个合理描述性的名称(在上面的示例中,getFullname会更好)。

装饰类

从版本0.16开始,Ornament支持三种类型的装饰类:简单的支持枚举、仅接收值的类,以及实现Ornament\Core\DecoratorInterface的装饰器。通常,你的装饰器将扩展Ornament\Core\Decorator基类,但你也可以使用类似Carbon\Carbon的东西。

首先,使用枚举的示例

<?php

enum MyEnum : int
{
    case cool = 1;
    case stuff = 2;
}

class MyModel
{
    // ...

    public MyEnum $example;
}

// This now fails, since 3 is not in the enum:
$model = MyModel::fromIterable(['example' => 3]);

如果一个枚举装饰器标记为可为空,Ornament将使用tryFrom,上述示例就不会抛出错误,而是将$model->example设置为null

构造函数的第一个参数是原始值。如果装饰器扩展了Ornament的核心装饰器类,则第二个参数是被装饰属性的ReflectionProperty,自定义装饰器可以使用它来提取配置属性。最后,你可以添加多个Ornament\Core\Construct属性来指定额外的参数。在Carbon的早期示例中,这可以指定时区,例如。

建议装饰类还支持一个__toString方法,以便可以无缝地将装饰过的属性传递给存储引擎。

PHP、PDO和fetchObject

PDO的fetchObject和相关方法试图通过在调用构造函数之前根据检索的数据库列注入属性来表现得聪明。PHP 7.4不喜欢这样,因为装饰过的属性将是错误类型!

因此,现在被认为最好的做法是使用PDO::FETCH_ASSOC并将结果通过Model::fromIterable(对于fetch)或Model::fromIterableCollection(对于fetchAll)来传递。

例如。

<?php

// ...
return MyModel::fromIterable($stmt->fetch(PDO::FETCH_ASSOC));

Ornament <0.14版本没有这个限制,因为它们专门与fetchObject一起工作;在PHP 7.4上这不再可能,所以我们强烈建议您升级到0.15或更高版本。

自定义对象实例化

Ornament提供一个构造函数,该构造函数期望注入到模型中的数据键/值对。有时这并不是你想要的;也许你正在扩展一个基类,该基类期望将每个属性指定为构造函数的参数(或任何其他,例如Laravel的fill方法)。

可以使用initTransformer静态方法覆盖默认行为,传递一个回调,该回调以可迭代的$data作为其唯一参数,并必须返回构建的对象。

<?php

MyModel::initTransformer(function (iterable $data) : MyModel {
    return new MyModel($data['id'], $data['password']);
});

这些转换器是针对每个类的。如果你需要为所有你的模型使用它,你应该让它们扩展一个基类,并在该基类上调用initTransformer

加载和持久化模型

这是你的工作。等等,什么?是的,Ornament与存储引擎无关。你可以使用RDBMS,与JSON API接口,或者将你的东西存储在Excel文件中,我们都不关心。我们相信你不应该将你的模型绑定到你的存储引擎。

我们个人的偏好是使用“仓库”来处理这个问题。当然,你可以自己创建一个基类模型,该模型实现了save()delete()方法或任何其他方法。

有状态的模型

话虽如此,你并不完全孤身一人。模型可以使用Ornament\Core\State特质来公开一些便利方法。

  • isDirty():模型自上次实例化以来是否已更改?
  • isModified(string $property):具体检查某个属性是否已更改。
  • isPristine()isDirty的反义词。
  • markPristine():手动将模型标记为原始,例如在存储后。基本上,这会将初始状态重置为当前状态。

所有这些方法都是公开的。你可以在你的存储逻辑中使用它们来确定如何进行操作(例如,如果模型isPristine(),则跳过昂贵的UPDATE操作)。

防止属性被装饰

如果你的模型不仅仅是“简单”的数据存储,那么可能有一些属性你明确地希望被装饰。请注意,任何私有或静态属性都已经被保留。

为了明确告诉Ornament跳过对公共或受保护属性的装饰,请向它添加属性Ornament\Core\NoDecoration