jasny/entity

此包已被弃用,不再维护。未建议替代包。

状态实体对象表示

v0.2.0 2019-05-01 01:32 UTC

This package is auto-updated.

Last update: 2022-03-22 22:29:02 UTC


README

Build Status Scrutinizer Code Quality Code Coverage Packagist Stable Version Packagist License

实体是您希望在数据库或其他数据存储中表示的“事物”。它可以是您博客上的新文章、您的论坛中的用户或您权限管理系统中的权限。

实体对象的属性是持久数据的表示。

安装

composer require jasny/entity

使用

namespace App;

use Jasny\Entity\IdentifiableEntityInterface;
use Jasny\Entity\IdentifiableEntityTraits;

/**
 * A user in our system
 */
class User extends AbstractIdentifiableEntity
{
    /** @var string */
    public $id;
    
    /** @var string */
    public $name;
    
    /** @var string */
    public $email;
    
    /** @var string */
    public $password_hash;
}

一个快速而简单的脚本可以创建并输出一个 User 实体的 JSON,如下所示

use App\User;
use Jasny\Entity\Event;

$data = $db->prepare("SELECT * FROM user WHERE id = ?")->execute($id)->fetch(PDO::FETCH_ASSOC);
$user = User::fromData($data);

$user->addEventListener(function(Event\ToJson $event): void {
    $data = $event->getPayload();
    unset($data['password_hash']);

    $event->setPaypload($data);
});

header('Content-Type: application\json');
echo json_serialize($user);

在这个例子中,您也可以直接将数据序列化为 JSON。这层抽象可以帮助添加应用层的抽象,而不仅仅是简单的脚本。

文档

基本实体

Entity 接口定义了所有实体需要实现的方法。要实现实体,您可以扩展 AbstractBasicEntity 基类。或者,您可以使用此库的 特性

建议将属性定义为公共的,但仅使用它们来获取值,而不是设置值。对于设置值,请使用 set() 方法。这不是在运行时强制执行的,但可能会被静态代码分析器(如 PHPStan)检查。

class Color extends AbstractBasicEntity
{
    /** @var int */
    public $red;
    
    /** @var int */
    public $green;
    
    /** @var int */
    public $blue;
}

可识别实体

如果一个实体有一个唯一标识符,则类应该实现 IdentifiableEntity

class User extends AbstractIdentifiableEntity
{
    /** @var int */
    public $id;
    
    /** @var string */
    public $name;
    
    /** @var string */
    public $email;
    
    /** @var string */
    public $password_hash;
}

假设您正在使用属性 id 作为代理键。如果您正在使用不同的属性,请确保重写静态方法 getIdProperty()

class Invoice extends AbstractIdentifiableEntity
{
    /** @var string */
    public $number;
    
    // ...
    
    protected static function getIdProperty(): string
    {
        return 'number';
    }
}

动态实体

默认情况下,实体应该只具有由类指定的属性。如果 set() 方法遇到任何与类中定义的属性不对应的值,它将忽略这些值。如果由于某些原因添加了额外的属性,则 toAssoc()jsonSerialize() 方法也将忽略类中未定义的属性。

在某些情况下,实体可能是动态的;它可以在运行时添加属性。某些数据存储,如 MongoDB,具有动态模式,这不需要事先定义,以支持此功能。

为了表示实体可能具有动态属性,它应该实现 DynamicEntity 接口。

class User extends AbstractIdentifiableEntity implements DynamicEntity
{
    // ...
}

新实体

使用 new 关键字保留用于创建新实体。

$user = new User(); // This represents a new user in the system

如果您设置了新实体的标识(id 属性),它将覆盖它或抛出重复 id 错误,具体取决于数据存储实现。但是,它将不会(或者应该不会)更新现有记录。

isNew() 方法将告诉它是一个新用户还是从数据加载的。

现有实体

当获取实体的数据时,使用静态的fromData()方法来创建实体。

$data = $db->prepare("SELECT * FROM user WHERE id = ?")->execute($id)->fetch(PDO::FETCH_ASSOC);
$user = User::fromData($data);

fromData()方法在调用构造函数之前设置实体对象的属性。

var_export

__set_state()方法设置为fromData()的别名,允许实体通过var_export进行序列化并存储为PHP脚本。其他库,如Jasny Typecast,也依赖于__set_state()方法。

设置值

set()方法是一个辅助函数,用于从数组中设置所有属性。

$foo = new Foo();
$foo->set('answer', 42);
$foo->set(['red' => 10, 'green' => 20, 'blue' => 30]);

它可以用作流畅接口

$adventure = (new Adventure)
  ->set('destination', 'unknown')
  ->set('duration', '1 year')
  ->go();

set()方法触发2个事件:BeforeSetAfterSet

相同实体

使用返回布尔值的is()方法检查两个实体是否相同。如果对象是相同的对象,该方法始终返回true(类似于===)。

对于可识别的对象,如果实体类和id值相同,该方法也将返回true。其他属性的值不予考虑。

转换为关联数组

使用toAssoc()方法将实体转换为关联数组。默认情况下,此方法将返回所有公共属性的值。

$data = $user->toAssoc();

可用的ToAssoc事件可以修改此方法的输出。该库附带ToAssocRecursive事件监听器,它还将子实体转换为关联数组。

$user->addEventListener(function(Event\ToAssoc $event): void {
    $assoc = $event->getPayload();
    
    if (isset($assoc['password'])) {
        $assoc['password_hashed'] = password_hash($assoc['password'], PASSWORD_DEFAULT);
        unset($assoc['password_hashed']);
    }
    
    $event->setPayload($assoc);
});

toAssoc()方法可以在多个地方使用。不建议创建事件监听器来处理特定用例。相反,为该特定用例创建一个新类型的事件。

转换为JSON

实体必须实现JsonSerializable接口,这意味着它们可以通过json_encode()转换为JSON。默认情况下,结果是包含实体所有公共属性的对象。

可以在实体类中重写jsonSerialize方法。或者,可以使用ToJson事件在序列化为JSON字符串之前修改结果。

$user->addEventListener(function(Event\ToJson $event): void {
    $assoc = $event->getPayload();
    unset($assoc['password_hashed']);
    
    $event->setPayload($assoc);
});

该库包含JsonCast事件监听器,它将DateTime对象转换为日期字符串,并将任何实现JsonSerializable的(子)对象转换为JSON。

持久化实体

此库包含将实体保存到持久存储(如数据库)的方法。

建议实现数据网关服务(而不是采用Active Record模式)。

class UserGateway
{
    /** @var \PDO */
    protected $db;

    public function __construct(\PDO $db)
    {
        $this->db = $db;
    }

    public function load(string $id): User
    {
        $data = $db->prepare("SELECT * FROM user WHERE id = ?")->execute($id)->fetch(PDO::FETCH_ASSOC);
        
        if ($data === null) {
            throw new RuntimeException("User `$id` not found");
        }
        
        return User::fromData($data);
    }
    
    public function save(User $user): void
    {
        $data = $user->toAssoc();
        
        $columns = join(', ', array_keys($data));             // "id, name, email, password_hash"
        $placeholders = ':' . join(', :', array_keys($data)); // ":id, :name, :email, :password_hash"
        
        $db->prepare("REPLACE INTO users ($columns) VALUES ($placeholders)")->execute();
        
        $user->markAsPersisted();
    }
}

示例始终执行REPLACE查询,但您可以在isNew()返回false的情况下执行UPDATE查询。

实体保存后,网关应调用 markAsPersisted 方法,这将触发事件并将实体标记为不再是新的(对于 isNew())。

如果您使用的是自动生成的标识符,应在调用 marktAsPersisted() 之前从数据库层检索它并直接设置 id 属性。

事件

实体可能通过一个 PSR-14 兼容 的事件调度器支持事件。这为不同的服务提供了额外的抽象,并且在某些情况下非常重要

在您可以添加事件监听器之前,您需要注册一个事件调度器。实体不会自己创建一个。

use Jasny\Entity\Event;
use Jasny\Entity\EventListener\JsonCast;
use Jasny\EventDispatcher\EventDispatcher;

$listener = (new ListenerProvider)
    ->withListener(function(Event\Serialize $event): void {
        $assoc = $event->getPayload();
        
        if (isset($assoc['password'])) {
            $assoc['password_hashed'] = password_hash($assoc['password'], PASSWORD_DEFAULT);
            unset($assoc['password_hashed']);
        }
        
        $event->setPayload($assoc);
    })
    ->withListener(new JsonCast());
    
$dispatcher = new EventDispatcher($listener);

$user = new User;
$user->setEventDispatcher($dispatcher);

通常,事件调度器是由网关添加到实体的。这意味着在创建新实体时也应使用网关。

要向现有实体添加事件监听器,请使用实体的 addEventListener() 方法。

$user->addEventListener(function(Event\ToJson $event): void {
    $assoc = $event->getPayload();
    unset($assoc['password_hashed']);
    
    $event->setPayload($assoc);
});

请注意,由于添加事件监听器不是由 PSR-14 标准定义的,因此 addEventListener() 方法仅与 Jasny Event Dispatcher 一起工作。

dispatchEvent() 方法接受一个事件并将其调度到监听器。它将返回传递的事件对象,该对象可能被事件监听器修改。

$event = $user->dispatchEvent(new CustomEvent($user, $someData));

事件对象

事件可以是任何对象。事件监听器根据对象类进行筛选。

此库的事件类将 $entity$payload 作为构造函数参数。使用 getEntity() 方法将返回发出事件的实体。您可以使用 getPayload() 获取负载,并使用 setPayload() 更新它。修改后的事件将传递给后续监听器,并由触发事件的函数使用。

实体事件

库具有以下事件

  • BeforeSet - 由 set() 调用。修改负载将影响设置的值。此方法可用于将值转换为正确的类型或过滤掉不允许手动更改的属性。
  • AfterSet - 由 set() 调用,在更新实体对象之后。修改负载没有任何影响。
  • Persisted - 由 markAsPersisted() 调用,这反过来又应在实体保存到持久存储(如数据库)时调用。
  • ToAssoc - 由 toAssoc() 调用。修改负载将影响此方法的返回值。
  • ToJson - 由 jsonSerialize() 调用。修改负载将影响此方法的返回值。

事件监听器

  • ToAssocRecursive - 递归遍历所有属性,也将子实体转换为关联数组。
  • JsonCast - 递归遍历所有属性,将 DateTime 对象转换为日期/时间字符串,并获取 JsonSerializable 对象的 JSON 数据。