treshugart/model

Model 是一个简单、轻量级且易于使用的领域驱动实体框架。

该软件包的规范仓库似乎已消失,因此该软件包已被冻结。

维护者

详细信息

github.com/treshugart/Model

主页

1.0.1 2018-02-26 05:29 UTC

This package is not auto-updated.

Last update: 2024-01-20 10:11:36 UTC


README

什么是 Model

Model 是使用 PHP 5.4.x 编写的简单、轻量级且易于使用的领域驱动实体框架。

为什么选择 Model

因为您希望模型由业务需求定义,而不是数据库需求。您还希望控制后端如何访问数据,无论是 Zend、Doctrine、Propel 还是简单的 PDO 或 MongoDB。它甚至让您自由使用其他数据源和库;实际上没有任何限制。

抽象理论

由于您不受特定后端的限制,您可以自由选择如何结构实体和仓库,而不必基于它们将如何存储或检索。当您结构实体时,您应该只从领域角度考虑如何使用它们,而不是它们将如何在后端中存储。映射器可用于在后台和业务实体之间导入和导出。

实体

要创建实体,您实际上只需要扩展基实体类

<?php

namespace Model\Entity;

class Content extends Entity
{

}

配置

实体允许您为实体指定一个 init() 方法来放置设置实体的代码,或者您可以使用 PHPDoc 标签通过注释来告诉实体您希望它如何设置。

值对象

值对象用于定义属性。默认情况下,在 Model\Vo 命名空间下提供了一系列值对象,但您也可以通过实现 Model\Vo\VoInterface 或扩展 Model\Vo\VoAbstractModel\Vo\Generic 来创建自己的值对象。对于大多数情况,只需扩展通用值对象并重写您需要的任何方法就足够了。它扩展了 VoAbstract 值对象,并在标准设置、获取、检查和删除之上提供了验证功能。

您可以通过两种方式将值对象应用于实体。首先,您可以在 init() 方法中简单地使用 setVo 方法。

public function init()
{
    $this->setVo('name', new Model\Vo\Generic);
}

其次,并且可能是更常见的方式,您可以通过注释您想要它作用的属性来应用值对象。

/**
 * @vo Model\Vo\Generic
 */
public $name = 'Default Value';

如果您这样做,则必须将该属性设置为公共的。这样,它可以被取消设置,并且魔术方法可以使用值对象来接管。默认值成为值对象的初始值。

某些值对象需要传递给它们的参数。如果这不需要是动态的,您可以在注释中指定它们。就像指定方法参数列表一样指定它们。

/**
 * @vo Model\Vo\VoSet 'Model\Vo\Date', ['format' => 'Y-m-d H:i:s']
 */
public $updated = 'now';

布尔型

值被转换为布尔值并保持为布尔值。所有假值或真值都转换为 falsetrue

日期

值通过 DateTime 对象传递并使用它进行操作。构造函数接受一个配置对象,因此您可以配置输出格式和时区(如果需要)。

$entity->setVo('updated', new Model\Vo\Date([
    'format'   => 'Y-m-d H:i:s',
    'timezone' => 'Australia/Sydney'
]));

$entity->date = 'yesterday 1 hour ago';

// 2012-10-12 16:10:55
$entity->date;

枚举

期望一个值数组以限制设置的值。

$entity->setVo('gender', new Model\Vo\Enum([
    'male',
    'female'
]));

$entity->gender = 'male';

EnumSet

期望一个值数组以限制传入的数组。

$entity->setVo('guitars', new Model\Vo\EnumSet([
    'Cordoba',
    'Gibson',
    'Ibanez',
    'Parker',
    'Seagull'
]));

$entity->guitars = ['Ibanez', 'Parker'];

过滤器

允许使用任意可调用函数在设置值之前进行过滤。

$entity->setVo('forename', new Model\Vo\Filter(function($forename) {
    return ucfirst($forename);
}));

$entity->forename = 'trey';

// Trey
$entity->forename;

浮点数

将任何值转换为浮点数。

泛型

一个基本的VO,它传递任何值。通常,这用于

HasMany

允许与给定实体集的实体之间的一对多关系。

$entity->setVo('hobbies', new Model\Vo\HasMany('Moden\Entity\Hobby'));

$entity->hobbies = [[
    'name' => 'Music',
    'type' => 'art'
], [
    'name' => 'Golf',
    'type' => 'sport'
]];

// Music
$entity->hobbies->first()->name;

HasOne

允许与另一个实体之间的一对一关系。

$entity->setVo('address', new Model\Vo\HasOne('Model\Entity\Address'));

$entity->address = [
    'number'   => '1',
    'street'   => 'Ice Street',
    'city'     => 'Presenton',
    'country'  => 'North Pole',
    'postcode' => '12345'
];

// North Pole
$entity->address->country;

整数

确保值是整数。

货币

值被转换为字符串,并使用number_format()将字符串格式化为两位小数。

集合

表示任意值的集合或数组。

字符串

确保传入的值始终是字符串。

UniqueId

扩展通用值对象,并将值初始化为唯一的字符串。

VoSet

表示值对象的数组。例如,您可能需要一个日期对象的数组。

$entity->setVo('edits', new Model\Vo\VoSet('Model\Vo\Date', [
    'format'   => 'd/M/Y',
    'timeozne' => 'Australia/Sydney'
]));

过滤器

过滤器是简单的类,可以分配给其他类或方法,在数据被过滤器分配访问之前拦截数据。过滤器提供了一种运行预定义代码来转换数据或执行其他任务的方法,这些任务是多个类或方法共享的。

过滤器用于在导入或导出期间拦截数据(当调用实体的to()或from()方法时)。过滤器主要用于将数据从一种格式转换为另一种格式。在处理旧实体数据时通常需要转换。

过滤器的__invoke()方法签名如下所示

    public function __invoke(array $data)

所有定义的过滤器都位于app/main/src/Model/Filter

过滤器使用

定义过滤器

过滤器通过实现__invoke()魔术方法的一个简单类来定义。类的名称应与数据源或目的地匹配,例如db表示数据库。过滤器的位置应具体到过滤器处理的数据方向,即tofrom

e.g. Your file path for a database filter for translating database data to entity data may look something like this:
 app/main/src/Filter/From/Billing/CodeGroup/Db.php

过滤器上的invoke方法应接受一个参数,一个数组,并返回一个参数,也是一个数组。返回的数组是过滤后的数据。

使用过滤器

要使用过滤器与实体一起使用,您可以使用docTag参数@filter将其附加到类或方法。要使用的模式将匹配以下内容

@filter from <filter type i.e. db> using <class name i.e. Model\Filter\From\Billing\CodeGroup\Db>

例如:/** * 表示账单项目代码。 * * @filter from db using Model\Filter\From\Billing\CodeGroup\Db */ class CodeGroup extends Entity ...

在实例化实体时使用过滤器应类似于以下内容

    $found = ServiceContainer::main()->db->find
        ->in('billing.code_groups')
        ->where('icg_uid', $id)
        ->one();

    $entity = new Entity\Billing\CodeGroup($found, 'db');

注意CodeGroup($found, 'db')的第二个参数,这个参数需要与docTag中的filter类型匹配。例如@filter from

注意!并非所有错误情况都会导致错误

在某些情况下,如果未找到过滤器,则不会抛出错误。如果您怀疑过滤器没有被应用,您可能需要在过滤器的__invoke()方法中插入一些调试信息,以查看是否调用该方法。如果invoke方法没有被调用,那么可能是您的$entity = new Entity\Billing\CodeGroup($found, 'db');过滤器参数名称不正确或您的@filter from db using Model\Filter\From\Billing\CodeGroup\Db参数不正确。

验证器

验证器用于验证实体的状态。验证器唯一的要求是它必须是可调用的(is_callable())。验证实体有两种方式。您可以将验证器附加到实体本身或直接附加到值对象。

将它们附加到您的实体上很方便,当您需要根据不同的属性值来验证实体的状态时。您可能有一个值依赖于另一个值。

$entity->addValidator('That's malarkey! The content ":title" cannot be created before it is updated.', function($entity) {
    return $entity->created <= $entity->updated;
});

您还可以使用 @validator 标签将其应用到实体上

/**
 * @validator Model\Validator\EnsureCreatedIsBeforeUpdated The content ":title" cannot be created before it is updated.
 */
class Content extends Model\Entity\Entity
{

}

将它们附加到您的值对象上非常适合当您正在对某种类型的值进行非常具体的验证,例如创建日期是否为有效日期。

$entity->getVo('title')->addValidator('":title" is an invalid content title.', function($vo) {
    return (new Zend\Validator\Alnum)->isValid($vo->get());
});

或者您可以使用注解

/**
 * @var Model\Vo\String
 *
 * @validator Zend\Validator\NotEmpty The user's name must not be empty.
 */
public $name;

当使用注解对属性进行操作时,您必须确保在 @validator 标签之前放置 @var 值对象定义标签,否则验证器的配置器会抱怨,因为它需要值对象来应用验证器。

验证器允许任何可调用的东西。此外,在注解使用时,您可以传递类名、函数名以及 Zend Framework 1.x 和 2.x 验证器类名。

通过特质使用行为

您会注意到 Timestampable 特质的用法。这个特质不包括在库中,但是它展示了您如何使用特质将功能混合到您的实体中。我们假设特质具有以下定义

<?php

namespace Model\Behavior;

trait Timestampable
{
    /**
     * @var Model\Vo\Datetime
     *
     * @validator Zend\Validator\Date The created date ":created" is not valid.
     */
    public $created;

    /**
     * @var Model\Vo\Datetime
     *
     * @validator Zend\Validator\Date The last updated date ":updated" is not valid.
     */
    public $updated;
}

关系

如前所述,关系是通过使用 HasOneHasMany 值对象定义的

<?php

namespace Model\Entity;

class Content extends Entity
{
    /**
     * @var Model\Vo\HasOne 'Model\Entity\Content\User'
     */
    public $user;

    /**
     * The the past modifications of the entity.
     *
     * @var Model\Vo\HasMany 'Model\Entity\Content\Modification'
     */
    public $modifications;
}

通过添加关系,您确保如果指定的属性被设置或访问,它将是指定类的一个实例。

<?php

use Entity\Content;

$entity = new Content;

// instance of Model\Entity\Content\User
$user = $entity->user;

// instance of Model\EntitySet containing instances of Model\Entity\Content\Modification
$modifications = $entity->modifications;

这意味着如果您将这些属性之一设置为一个数组,它将确保创建指定关系的实例并用指定的数组数据填充。

$entity->user = array('name' => 'Me');

您甚至可以传递任何可遍历的项

$user       = new stdClass;
$user->name = 'Me';

// applying a stdClass
$entity->user = $user;

// entity sets work the same way
$entity->modifications = array(
    array('name' => 'Me'),
    new stdClass,
);

验证实体

当您需要验证实体时,您有两个选择。首先,您可以简单地验证实体并获取其错误信息。

if ($errors = $entity->validate()) {
    // do some error handling
}

然而,当您的根实体无效时,您可能希望停止执行并在某处捕获它。assert() 方法允许您做到这一点。

// validate and throw an exception if it's not valid
$entity->assert('Some errors occured.', 1000);

断言将抛出一个特殊类型的异常,该异常是 Model\Validator\ValidatorException 的一个实例。这个异常类允许您获取在整个实体验证过程中捕获的每个错误信息。这包括根实体及其所有子实体的所有实体验证器和值对象验证器。

这允许您在代码中的某个地方捕获它。

use Model\Validator\ValidatorException;
use Exception;

try {
    // do something like dispatch your application
} catch (ValidatorException $e) {
    // handle validation errors
} catch (Exception $e) {
    // fatal exception
}

您可以使用异常中的方法以任何方式处理它

<?php

use Model\Validator\ValidatorException;

// allows a main message
$exception = new ValidatorException('The following errors happened:');

// allows you to add messages
$exception->addMessage('my first message');
$exception->addMessages([
    'my second message',
    'my third message'
]);

// implements IteratorAggregate
foreach ($exception as $message) {
    ...
}

// The following errors happened:
//
// - my first message
// - my second message
// - my third message
//
// [stack trace goes here]
echo $exception;

// or you can just throw it
throw $exception;

存储库

编写存储库相对简单

<?php

namespace Model\Repository;
use Model\Entity;

class Content
{
    public function getById($id)
    {
        ...
    }

    public function getByTitle($title)
    {
        ...
    }

    public function save(Entity\Content $content)
    {
        ...
    }
}

缓存

存储库方法自动启用缓存。方法名称和参数用于生成一个唯一键,用于将返回值发布到您选择的任何后端。下次调用该方法时,如果其生命周期尚未过期,则从缓存中提取它,而不是再次运行该方法。

为了使缓存过程自动进行,我们必须通过 __call() 代理方法。这意味着方法必须标记为 protected

Model\Cache 命名空间下提供了多个不同的后端。

Memcache

这可能是最受欢迎且易于使用的选项。在底层使用PHP Memcache PECL 扩展,因此请确保已经安装。

$repository->setCacheDriver('Memcache', new Model\Cache\Memcache([
    'servers' => [[
        'host' => 'localhost',
        'port' => 11211
    ]]
]));

Mongo

MongoDB正变得越来越受欢迎。它的速度与memcache相当,灵活的结构使其成为缓存的良好解决方案。使用PHP Mongo PECL 扩展

$repository->setCacheDriver('Mongo', new Model\Cache\Mongo([
    'db'         => 'cache',
    'collection' => 'cache',
    'dsn'        => null,
    'lifetime'   => null,
    'options'    => []
]));

Php

PHP缓存驱动器简单地将在当前脚本的生存周期内将值存储在内存中。目前,生存周期值被忽略。

$repository->setCacheDriver('PHP', new Model\Cache\Php);

在示例中,缓存驱动器在init()方法中对实体进行应用并赋予一个名称。现在,可以使用注释从其他存储库方法中引用这些驱动器。注释的语法灵活,就像你正在写一个句子一样。

/**
 * @cache Using Memcache for 1 hour.
 */
protected function getById($id)
{

}

这会告诉存储库,将结果存储在Memcache中1小时。一小时后,该方法将再次执行,循环继续。

如果你想永久存储缓存中的项目,只需省略关于生存周期的部分。

/**
 * @cache Using PHP.
 */
protected function getById($id)
{

}

你也可以使用Chain驱动器来组合这些。

$chain = new Model\Cache\Chain;
$chain->add(new Model\Cache\Php);
$chain->add(new Model\Cache\Memcache);
$repository->setCacheDriver('Php and Memcache', $chain);

驱动器按应用顺序使用,因此它首先会在PHP中查找,然后是Memcache中的结果。在持久化到缓存时,也会按此顺序持久化到所有驱动器。

然后你会像以前一样使用它。

/**
 * @cache Using PHP and Memcache for 1 hour.
 */
protected function getById($id)
{

}

性能

作为PHP开发者,我们必须面对的一个严峻现实是ORMs,虽然种类繁多,但本质上很慢。如果你正在管理大量数据集(1000+实体),你应该小心使用,如果你期望有良好的性能。人们经常在Propel、Doctrine等之间讨论性能。你可以挑剔到极致,但如果你懒惰且没有正确设计解决方案,所有这些工具之间的性能差异都不会有多大。

注释

在注释中已尽量减少必要的解析。任何类或方法的文档注释通常只解析一次,并且配置该实体或存储库所需的信息将保留在内存中,以便下次配置相同类型的实体或存储库。

实时注释正在生产环境中成功使用,几乎没有性能影响。当然,如果你想做一些愚蠢的基准测试,那就去做吧。

缓存

缓存非常有用。有时它是真正必要的,有时它只是糟糕设计的临时解决方案。请自行决定使用。

后台进程

如果你正在对大型数据集执行CRUD操作,运行后台进程或使用作业队列来处理它可能是一个好主意。这意味着你可以快速返回结果给用户,同时仍然在大数据集上执行操作,而不牺牲ORM的便利性。让我们面对现实,ORM是方便的,并且真的可以帮助你管理代码。为什么我们要牺牲它呢?

处理更少的数据

在许多情况下,我们只是在做错。我们不需要一次性向用户显示1000个项目,或者在他们等待时更新1000条记录。很多时候,程序员是问题,而不是工具。

许可

版权(c)2005-2013 Trey Shugart

本软件及其相关文档文件(以下简称“软件”)的使用权在此免费授予任何人,允许其在不受限制的情况下使用软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件的副本,并允许将软件提供给他人进行上述行为,但须遵守以下条件:

上述版权声明和本许可声明应包含在软件的所有副本或主要部分中。

软件按“原样”提供,不提供任何形式的质量保证,无论是明示的、暗示的还是其他的,包括但不限于适销性、适用于特定目的和不侵犯他人知识产权的保证。在任何情况下,作者或版权所有者不对任何索赔、损害或其他责任承担责任,无论这些责任是基于合同、侵权或其他原因,这些责任源自、因之产生或与软件或软件的使用或其他行为有关。