devco/model

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

此包的官方仓库似乎已丢失,因此该包已被冻结。

维护者

详细信息

github.com/devco/Model

主页

安装: 1,382

依赖者: 0

建议者: 0

安全: 0

星标: 17

关注者: 18

分支: 9

1.0.1 2018-02-26 05:29 UTC

This package is not auto-updated.

Last update: 2024-01-20 11:39:52 UTC


README

是什么

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

为什么

因为您希望模型由业务需求定义,而不是由数据库需求定义。您还希望控制如何使用后端访问数据,无论是 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'];

过滤器

允许在设置值之前使用任意的callable进行过滤。

$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;

整数

确保值是一个整数。

货币

值被转换为string,并使用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 type匹配。例如@filter from

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

在某些情况下,如果没有找到过滤器,则不会抛出错误。如果您怀疑过滤器没有被应用,您可能需要将一些调试信息插入到过滤器的 __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;

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

验证器允许任何 callable 的东西。此外,当使用注解时,您可以传递类名、函数名以及 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

特此授予任何人获得本软件及其相关文档文件(以下简称“软件”)副本(“软件”)的自由,无限制地处理“软件”,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或出售“软件”的副本,并允许向“软件”提供副本的个人这样做,前提是符合以下条件:

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

本软件按“原样”提供,不提供任何形式的质量保证,无论是明示的还是暗示的,包括但不限于适销性、特定用途适用性和非侵权性保证。在任何情况下,作者或版权所有者不对任何索赔、损害或其他责任负责,无论是基于合同、侵权或其他方式,由软件引起、源于软件或与软件的使用或其他交易有关。