buffalokiwi/magicgraph

PHP 8 的基于行为对象建模和持久化库

v0.1.24 2021-09-15 16:19 UTC

README

PHP 8 的基于行为对象建模、映射和持久化库

OSL 3.0 许可证

目录

生成的文档

文档正在制作中。

  1. 概述
  2. 安装
  3. 依赖关系
  4. 定义
  5. 入门
    1. Hello Model
    2. 基本数据库和仓库设置
  6. 属性配置
    1. 属性配置数组属性
    2. 属性数据类型
    3. 属性标志
    4. 属性行为
    5. 快速模型
    6. 注解
  7. 仓库
    1. 映射对象工厂
    2. 可保存的映射对象工厂
    3. SQL 仓库
    4. 装饰仓库
    5. 可服务仓库
    6. 组合主键
  8. 事务
    1. 概述
    2. 创建事务
    3. 事务工厂
  9. 模型服务提供商
    1. 可服务模型
    2. 可服务仓库
  10. 关系
    1. 一对一
    2. 一对多
    3. 多对多
    4. 嵌套关系提供者
    5. 编辑和保存的工作方式
  11. 可扩展模型
    1. 属性配置接口
    2. 属性配置实现
    3. 使用多个属性配置
    4. 模型接口
    5. 模型实现
  12. 行为策略
  13. 数据库连接
    1. PDO
    2. MySQL PDO
    3. 连接工厂
  14. 处理货币
  15. 创建 HTML 元素
  16. Magic Graph 设置
  17. 实体-属性-值 (EAV)
  18. 搜索
  19. 扩展 Magic Graph
    1. 配置映射器
  20. 教程

概述

Magic graph 是一个纯 PHP 编写的对象映射和持久化库。Magic Graph 使设计和使用丰富的分层域模型变得容易,这些模型可以结合各种独立设计和测试的行为策略。

目标

这是此项目的原始目标集合

  1. 轻松创建自验证模型
  2. 在运行时动态创建模型
  3. 将模型行为与模型对象分离
  4. 允许第三方扩展模型而无需修改模型对象或子类化

持久化

Magic Graph 持久化使用仓库和工作单元模式。目前 Magic Graph 包括 MySQL/MariaDB 适配器,未来版本中将添加更多适配器。

安装

composer require buffalokiwi/magicgraph

依赖关系

Magic Graph 需要 1 个第三方和 3 个 BuffaloKiwi 库。

  1. BuffaloKiwi/buffalotools_ioc - 服务定位器
  2. BuffaloKiwi/buffalotools_types - 枚举和集合支持
  3. BuffaloKiwi/buffalotools_date - DateTime 工厂/包装器
  4. MoneyPHP/Money - PHP 实现 Fowler 的 Money 模式

定义

什么是模型?

Magic Graph 模型是可扩展和自包含的程序。它们旨在封装与任何单一数据源相关联的所有属性和行为,但模型对如何加载数据或持久化数据没有知识。不必太担心这些组件的工作原理,我们将在未来的章节中介绍。

Magic Graph 模型由 4 个主要组件组成

  1. 属性定义和基本行为
  2. 属性捆绑到属性集中
  3. 模型对象
  4. 行为策略

属性

每个Magic Graph模型的核心都包含一系列属性。与标准的类属性类似,Magic Graph属性具有名称、数据类型和值。与标准类属性不同,Magic Graph属性是第一类对象。它们完全封装与其数据类型相关的所有行为,具有可扩展性、可重用性、自验证性和可配置的行为。

属性集

模型属性被打包成一个名为“属性集”的对象,它是一个基于Set的对象。属性集提供了访问属性对象、元数据、标志、配置数据以及在运行时添加和删除属性的方法。

模型对象

所有模型都必须实现IModel接口。Magic Graph模型实际上是属性集的包装器,它们将集合内的属性作为模型类的公共成员公开。添加getter和setter方法是可选的,但推荐使用。除了提供对属性的访问外,模型还跟踪新创建的或/和编辑的属性,有自己验证方法,并且可以附加额外的行为策略对象。

行为策略

策略是修改模型或属性行为的程序,并实现INamedPropertyBehavior接口。策略在对象构造期间传递给模型,并且模型将调用策略方法。例如,假设您有一个订单对象,您想在客户提交订单后发送收据。可以创建一个策略,在订单成功创建并保存后发送电子邮件。IModel和INamedPropertyBehavior都可以扩展以添加所需的其他事件。

入门

Hello Model

这是在Magic Graph中编写模型的一种方式。随着您阅读文档,我们将逐步转向编写更健壮和可扩展的模型。以下模型示例用于说明模型的内部结构。

现在,让我们看看一些基本的模型创建代码。

在此示例中,使用了以下对象
buffalokiwi\magicgraph\DefaultModel
buffalokiwi\magicgraph\property\DefaultIntegerProperty
buffalokiwi\magicgraph\property\DefaultStringProperty
buffalokiwi\magicgraph\property\PropertyListSet

第一步是决定要包含在模型中的属性的名称和数据类型。在我们的示例中,我们将添加两个属性:一个名为“id”的整数属性和一个名为“name”的字符串属性。我们将使用DefaultIntegerProperty和DefaultStringProperty。要创建模型,每个属性都传递给PropertyListSet构造函数,然后传递给DefaultModel。

$model = new DefaultModel(                //..Create the model
  new PropertyListSet(                    //..Create the property set 
    new DefaultIntegerProperty( 'id' ),   //..Add the id property
    new DefaultStringProperty( 'name' )   //..Add the name property 
));

现在已创建具有两个属性的模型。属性现在作为公共类属性可用。

//..Set the id and name property values 

$model->id = 1;       
$model->name = 'Hello Model';

//..Get the id and property values 
var_dump( $model->id ); //..Outputs: "int 1"
var_dump( $model->name ); //..Outputs: "string 'Hello Model' (length=11)"

现在,如果我们尝试将错误的类型值分配给属性之一会发生什么?会抛出一个异常!以下代码将导致抛出一个ValidationException,消息为:“属性id的值foo必须是整数。得到了字符串。”。

$model->id = 'foo'; //..id is not a string.

模型是自验证的,并且在尝试设置无效值时会立即抛出ValidationException。Magic Graph包含许多默认属性,我们将详细介绍这些属性在验证章节中的验证选项。

基本数据库和仓库设置

那么,如果我们想将此数据持久化到MySQL数据库中怎么办?不深入细节,我们可以创建一个SQL存储库,它同时充当上述定义的模型的对象工厂。

以下对象被使用

buffalokiwi\magicgraph\pdo\IConnectionProperties
定义用于建立数据库连接的连接属性

buffalokiwi\magicgraph\pdo\IDBConnection
定义一个通用的数据库连接

buffalokiwi\magicgraph\pdo\MariaConnectionProperties
MariaDB/MySQL连接属性

buffalokiwi\magicgraph\pdo\MariaDBConnection
为MariaDB/MySQL提供数据库连接和语句辅助库

buffalokiwi\magicgraph\pdo\PDOConnectionFactory
用于创建数据库连接实例的工厂

第一步是创建数据库连接。

$dbFactory = new PDOConnectionFactory(         //..A factory for managing and sharing connection instances 
  new MariaConnectionProperties(  //..Connection properties for MariaDB / MySQL
    'localhost',                  //..Database server host name 
    'root',                       //..User name
    '',                           //..Password
    'testdatabase' ),             //..Database 
  //..This is the factory method, which is used to create database connection instances
  //..The above-defined connection arguments are passed to the closure.
  function( IConnectionProperties $args  ) { 
    //..Return a MariaDB connection 
    return new MariaDBConnection( $args );
  });

下一步是为我们的测试模型创建一个表

CREATE TABLE `inlinetest` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 

最后,我们创建一个InlineSQLRepo的实例,这是一个用于处理模型构建、加载数据和保存数据的仓库。您可能会注意到,我们现在使用PrimaryIntegerProperty而不是IntegerProperty作为id。这是因为仓库需要至少有一个属性被标记为主键,而PrimaryIntegerProperty会自动设置这个标记。

$repo = new InlineSQLRepo( 
  'inlinetest',                       //..Database table name 
  $dbFactory->getConnection(),        //..Database connection
  //..Model properties follows 
  PrimaryIntegerProperty( 'id' ),     //..Primary id property 
  DefaultStringProperty( 'name' ));   //..Optional string property 

现在我们创建并保存!

从我们的新仓库创建一个新模型,如下所示

$model = $repo->create();

我们还可以使用create方法初始化属性

$model = $repo->create(['name' => 'foo']));

设置属性值

$model->name = 'foo';

由于id被定义为主键,我们不想设置该值。仓库将负责为我们分配该值。通过传递给仓库的save()方法来保存模型。

$repo->save( $model );

echo $model->id;  //..Prints 1 

在保存时,仓库首先通过调用模型附加的validate()方法验证模型。然后,在成功保存后,仓库将id(由数据库自动生成)分配给id属性。

假设新创建的记录的id为1,我们可以检索该模型

$model = $repo->get('1');

入门部分展示了使用Magic Graph的最基本方式。虽然这很好,但对除了简单程序之外的东西来说几乎没有用处。接下来的几章将详细介绍如何在大型应用程序中使用Magic Graph。

属性配置

属性配置文件是一种定义属性和特定属性行为的方式,必须实现IPropertyConfig接口。配置对象类似于PHP traits,其中我们定义部分对象。这些对象可以被分配给IModel实例,并定义属性集(在其中使用的属性)和关联模型的行

在下面的示例中,我们将创建一个包含两个属性的示例属性集:"id"和"name"。

id将是一个整型属性,默认值为零,被标记为主键,如果值为非零,则为只读。
name将是一个字符串属性,默认值为空字符串,并被标记为必需。

在这个例子中,使用了以下额外的类和接口

基础属性配置是定义属性配置时使用的基类。它提供了常量、常见的属性配置以及用于处理行为的几个方法。buffalokiwi\magicgraph\property\BasePropertyConfig

IPropertyFlags定义了属性可用的各种标志。此接口可以扩展以添加额外的标志和功能。buffalokiwi\magicgraph\property\IPropertyFlags

IPropertyType定义了可用的属性类型。每个类型通过IConfigMapper接口映射到属性对象。
buffalokiwi\magicgraph\property\IPropertyType

StandardPropertySet 使用默认的 IConfigMapper 和 IPropertyFactory 实现,为创建 IModel 实例时实例化 IPropertySet 提供了便捷的方式。buffalokiwi\magicgraph\property\StandardPropertySet

class SamplePropertyConfig extends BasePropertyConfig
{
  //..Returns an array detailing the properties to add
  protected function createConfig() : array
  {
    //..A map of property name to configuration 
    return [
      //..The Id Property 
      'id' => [
        self::TYPE => IPropertyType::TINTEGER,     //..The data type
        self::FLAGS => [IPropertyFlags::PRIMARY],  //..Flags 
        self::VALUE => 0                           //..Default value 
      ],
        
      'name' => [
        self::TYPE => IPropertyType::TSTRING,
        self::FLAGS => [IPropertyFlags::REQUIRED],
        self::VALUE => ''
      ]        
    ];
  }
}

属性配置对象继承自 BasePropertyConfig 或实现了 IPropertyConfig 接口。派生类只需要实现一个 createConfig() 方法,并返回一个包含零个或多个属性定义的数组。

createConfig() 返回一个属性名到属性配置数据的映射。定义属性配置时,'type'(BasePropertyConfig::TYPE)是唯一必须的属性。

BasePropertyConfig::FLAGS 映射到一个数组,该数组包含来自 IPropertyFlags 的常量。可以提供零个或多个标志,每个标志都会修改属性的验证方式。

可以使用 BasePropertyConfig::VALUE 属性设置默认值,并分配所需的默认值。

创建属性定义后,我们可以将它们分配给属性集,然后分配给模型。可以将多个 IPropertyConfig 实例传递给 StandardPropertySet。

$model = new DefaultModel( new StandardPropertySet( new SamplePropertyConfig()));

BasePropertyConfig 包含一些辅助常量,可用于简化属性配置对象的创建。例如,前面的示例可以重写为

class SamplePropertyConfig extends BasePropertyConfig
{
  //..Returns an array detailing the properties to add
  protected function createConfig() : array
  {
    //..A map of property name to configuration 
    return [
      //..The Id Property 
      'id' => self::FINTEGER_PRIMARY,
      'name' => self::FSTRING_REQUIRED
    ];
  }
}

FINTEGER_PRIMARY 将创建一个整型属性,标记为主键,默认值为零
FSTRING_REQUIRED 将创建一个字符串属性,标记为必需,默认值为空字符串。

属性配置数组属性

BasePropertyConfig 类包含一系列常量,用于在 createConfig() 返回的数组中创建模型属性。某些属性适用于特定数据类型,与其他类型一起使用时将没有效果。

标题

在应用程序级别使用的属性标题/标签。
魔幻图不会为此值读取任何特定目的。

BasePropertyConfig::CAPTION = 'caption'

标识符

某些属性的可选唯一标识符。这只是一个标签,应在应用程序级别使用。魔幻图不会为此值读取任何特定目的。

BasePropertyConfig::ID = 'id'

默认值

默认值。
如果模型构建期间未提供值,或者调用了 IProperty::reset() 方法,属性值将分配为属性配置对象中列出的默认值。

BasePropertyConfig::VALUE = 'value'

设置器回调

当设置属性值时,将按定义顺序调用提供的任何设置器。
配置数组中可以定义单个设置器,但可以通过向属性配置对象构造函数提供属性行为对象添加多个设置器。

设置器回调由 IProperty::setValue() 调用,可用于在验证之前修改传入的值。在设置器链中,前一个设置器的结果用作后续设置器的值参数。

f( IProperty, mixed $value ) : mixed  
BasePropertyConfig::SETTER = 'setter'

获取器回调

当检索属性值时,将按定义顺序调用提供的任何获取器。配置数组中可以定义单个获取器,但可以通过向属性配置对象构造函数提供属性行为对象添加多个获取器。

获取器回调由 IProperty::getValue() 调用,可用于在 getValue() 返回之前修改值。在获取器链中,前一个获取器的结果用作后续获取器的值参数。

f( IProperty, mixed $value ) : mixed   
BasePropertyConfig::GETTER = 'getter'

模型设置器回调

模型设置器与属性设置器相同,但它们在模型级别被调用。模型设置器与属性设置器的区别在于模型设置器可以访问其他属性,而属性设置器则不能。由于完整模型验证仅在保存时调用,因此可以用来在对象内部验证状态,并通过抛出ValidationException来防止任何修改。

  1. 在调用IModel::setValue(或通过IModel::__set()设置值)时,模型设置器的调用顺序与它们定义的顺序相同。
  2. 模型设置器在属性设置器之前和在属性验证之前被调用。
  3. 在链式调用模型设置器时,前一个设置器的结果被用作后续模型设置器的值参数。
f( IModel, IProperty, mixed $value ) : mixed  
BasePropertyConfig::MSETTER = 'msetter'

模型获取器回调

模型获取器与属性获取器相同,但它们在模型级别被调用。模型获取器与属性获取器的区别在于模型获取器可以访问其他属性,而属性获取器则不能。

  1. 在调用IModel::getValue(或通过IModel::__get()获取值)时,模型获取器的调用顺序与它们定义的顺序相同。
  2. 模型获取器在属性获取器之后被调用。
  3. 在链式调用模型获取器时,前一个获取器的结果被用作后续模型获取器的值参数。
f( IModel, IProperty, mixed $value ) : mixed   
BasePropertyConfig::MGETTER = 'mgetter'

属性数据类型

这必须映射到IPropertyType的有效值。有关更多信息,请参阅属性数据类型部分。

BasePropertyConfig::TYPE = "type"  

属性标志

这必须映射到逗号分隔的有效IPropertyFlags值列表。
有关更多信息,请参阅属性标志部分。

BasePropertyConfig::FLAGS = "flags"  

返回对象的属性类名

当使用ObjectProperty的后代支持的属性时,必须使用clazz属性。值应该是一个完全命名空间类名。

BasePropertyConfig::CLAZZ = "clazz"

例如,当属性类型定义为枚举或集合时,clazz将与某个枚举类名相等。

//..Sample enum class
class SampleEnum extends Enum {} 

//..Property configuration
'enum_property' => [
  'type' => 'enum',
  'clazz' => SampleEnum::class
]

初始化回调

当调用IProperty::reset()时,此函数将使用默认值调用。这是在将默认值分配为初始属性值之前修改默认值的方法。init回调返回的新值是新默认值。

f( mixed $defaultValue ) : mixed  
BasePropertyConfig::INIT = "initialize"

最小值/长度

这用于整数和字符串属性,是最小值或最小字符串长度。

BasePropertyConfig::MIN = "min"

最大值/长度

这用于整数和字符串属性,是最大值或最小字符串长度。

BasePropertyConfig::MAX = "max"

验证

验证回调用于在保存之前或当调用IProperty::callback()时验证单个属性值。验证回调在调用支持属性对象验证调用之前被调用,可以返回表示有效性的布尔值,或抛出ValidationException。返回false将自动抛出一个带有适当消息的ValidationException。

[bool is valid] = function( IProperty, [input value] )  
BasePropertyConfig::VALIDATE = "validate"

正则表达式

当使用字符串属性时,可以使用"pattern"属性提供正则表达式,该表达式将在属性验证期间使用。只有匹配提供的模式值才能提交到属性。

BasePropertyConfig::PATTERN = "pattern"

自定义配置数据

一个配置数组。这是特定于实现的,目前仅用于运行时枚举数据类型(IPropertyType::RTEnum)。这可以在您的应用程序中用于任何您想要的内容。

BasePropertyConfig::CONFIG = "config"

嵌入式模型前缀

默认属性集使用的前缀,可以代理对嵌套IModel实例的get/set值调用。例如,假设您有一个客户模型,并希望在其中嵌入地址。您无需复制/粘贴属性或链接客户到地址,只需在客户配置中将名为'address'的属性分配一个前缀,并添加包含地址模型类名的CLAZZ属性。客户模型将随后在客户模型内部嵌入地址模型,并将包含所有地址模型功能。此外,每个地址属性都将表现为客户模型的一个成员,并具有定义的前缀。

BasePropertyConfig::PREFIX = 'prefix'

//..Example configuration entry:
'address' => [
  'type' => IPropertyType::TMODEL,
  'clazz' => Address::class,
  'prefix' => 'address_'
]  

更改事件

在成功设置属性值后,将按照提供的顺序调用更改事件。

f( IProperty, oldValue, newValue ) : void   
BasePropertyConfig::CHANGE = 'onchange'

对于特定的属性,创建一个用作HTML表单输入的htmlproperty\IElement实例。基本上,为属性生成一个HTML输入,并将其作为字符串返回,该字符串可以嵌入到某个模板中。

f( IModel $model, IProperty $property, string $name, string $id, string $value ) : IElement   
BasePropertyConfig::HTMLINPUT = 'htmlinput'

空检查

这是一个可选的回调,可以用来确定一个属性是否可以被视为“空”。提供的函数的结果是空检查的结果。

f( IProperty, value ) : bool  
BasePropertyConfig::IS_EMPTY = 'isempty'

标记

属性的任意标记。
这可以是任何字符串,并且是特定于应用的。默认情况下,Magic Graph 中的任何内容都不会在默认情况下对该值进行操作。

BasePropertyConfig::TAG = 'tag'

属性数据类型

属性数据类型定义定义了属性背后的数据类型对象。所有可用的定义都在buffalokiwi\magicgraph\property\IPropertyType接口中。

以下是随Magic Graph一起提供的内置属性类型列表

布尔值

'bool'属性类型将由IBooleanProperty实例支持。除非指定为null,否则布尔属性将具有默认值false。

IPropertyType::TBOOLEAN = 'bool'

整数

IIntegerProperty支持。

IPropertyType::TINTEGER = 'int'

十进制

IFloatProperty支持。

IPropertyType::TFLOAT = 'float'

字符串

IStringProperty支持。

IPropertyType::TSTRING = 'string'

枚举

IEnumProperty支持。列必须在'clazz'属性中列出实现IEnum接口的类名。有关更多信息,请参阅BuffaloKiwi Types

IPropertyType::TENUM = 'enum'

运行时枚举

IEnumProperty支持。枚举成员通过“config”属性进行配置,并支持RuntimeEnum实例。运行时枚举实例不使用“clazz”属性。有关更多信息,请参阅BuffaloKiwi Types

IPropertyType::TRTENUM = 'rtenum' 

数组

ArrayProperty支持。数组属性主要用于Magic Graph关系提供程序。虽然可以定义用于任意数据的数组属性,但建议创建一个关系或模型服务提供程序来管理数组属性中的数据。
数组属性可以读取“clazz”参数以限制数组成员为指定类型的对象。

IPropertyType::TARRAY = 'array'

集合

设置属性由 ISetProperty 支持,并读取/写入 ISet(或由 "clazz" 属性指定的 ISet 的子类)的实例。更多信息请参阅 BuffaloKiwi Types

IPropertyType::TSET = 'set'

日期/时间

IDateProperty 支持,可用于表示日期和/或时间。这通常与时间戳或 DateTime SQL 列类型一起使用。

IPropertyType::TDATE = 'date'

货币。

IMoneyProperty 支持的属性,包含实现 IMoney 接口的对象。此属性类型需要使用服务定位器,并且已安装 MoneyPHP/Money 依赖项。

IPropertyType::TMONEY = 'money'

IModel

IModelProperty 支持,并包含实现 IModel 接口的对象。模型属性通常由 OneOnePropertyService 管理。

IPropertyType::TMODEL = 'model'

对象

仅接受指定对象类型实例的属性。
建议扩展 ObjectProperty 类以创建处理特定对象类型的属性,而不是使用通用的 ObjectProperty 对象。未来,我可能将 ObjectProperty 标记为抽象,以防止直接实例化。

IPropertyType::TOBJECT = 'object'

属性标志

属性标志是属性的一系列修饰符。可以分配零个或多个标志给任何属性,每个标志都将修改关联模型中使用的验证策略。每个标志都是在 buffalokiwi\magicgraph\property\IPropertyFlags 接口中定义的常量。

不允许插入

此属性永远不允许插入

IPropertyFlags::NO_INSERT = 'noinsert';

不允许更新

此属性永远不允许更新。
这也可以被认为是“只读”。

IPropertyFlags::NO_UPDATE = 'noupdate'

必需

此属性需要一个值

IPropertyFlags::REQUIRED = 'required'

允许为空

属性值可以包含空值

IPropertyFlags::USE_NULL = 'null'

主键

主键(每个属性集一个)

IPropertyFlags::PRIMARY = 'primary'

子配置

魔图不使用此标志,但它在这里,以防某些属性是从某些子/第三方配置中加载的,并且您想对这些属性做些什么。例如,这用于零售货架,用于识别从数据库中存储的配置加载的属性。

IPropertyFlags::SUBCONFIG = 'subconfig'

写入空值

在模型上调用 setValue() 时,如果存储的值不为空,将抛出 ValidationException。

IPropertyFlags::WRITE_EMPTY = 'writeempty'

不允许数组输出

设置此标志以防止在调用 IModel::toArray() 时打印属性。toArray() 用于复制和保存模型,并不是所有属性都应该被读取。例如:属性在读取时连接到某些 api,返回的值不应保存到任何地方。

IPropertyFlags::NO_ARRAY_OUTPUT = 'noarrayoutput'

属性行为

每个属性都有一系列先前在 属性配置数组属性 中定义的回调。在创建从 BasePropertyConfig 衍生的对象的实例时,可以将 INamedPropertyBehavior 的实例传递给构造函数。

本目的在于为对象创建不同的策略。策略是独立的、自包含的、可测试的程序。可以有零个或多个策略附加到属性配置对象上,可以修改相关模型的属性,并可能导致副作用。

例如,假设你希望在开发环境中保存模型时向日志文件中添加调试信息。我们可以创建一个扩展 GenericNamedPropertyBehavior 的类,并重写 getAfterSaveCallback() 方法。

/**
 * Attach this strategy to any model to add a debug log message when the model is saved.
 */
class DebugLogSaveStrategy extends GenericNamedPropertyBehavior
{
  /**
   * The log 
   * @var LoggerInterface
   */
  private LoggerInterface $log;
  
  
  /**
   * @param LoggerInterface $log
   */
  public function __construct( LoggerInterface $log )
  {
    //..Since this is a save event, we simply pass the name of the class as the property name.
    //..Save events are called regardless of the supplied name.
    parent::__construct( static::class );   
    $this->log = $log;
  }
  
  /**
   * Retrieve the after save function  
   * @return Closure|null function 
   */
  public function getAfterSaveCallback() : ?Closure
  {
    return function( IModel $model ) : void {
      //..Get the primary key value from the model
      $priKey = $model->getValue( $model->getPropertySet()->getPrimaryKey()->getName());
      
      //..Add the log message 
      $this->log->debug( 'Model with primary key value: ' . $priKey . ' successfully saved.' );
    };
  }  
}

创建策略后,我们可以通过其属性配置对象将其附加到模型上。

//..Create the property config object and attach the strategy 
$config = new SamplePropertyConfig( new DebugLogSaveStrategy( new LoggerInterfaceImpl()));

//..Create a model using the configuration 
$model = new DefaultModel( new StandardPropertySet( $config ));

当通过某个 IRepository 实例保存模型时,策略中将执行保存后的回调,并添加日志信息。

有几个回调,可以一起使用,通过解耦策略对象来创建丰富的模型。
有关详细信息,请参阅 属性配置数组属性

为单个属性添加行为策略的过程与上述相同,只是我们会公开从 GenericNamedPropertyBehavior 构造函数中传递的 "name" 参数。

/**
 * Attach this strategy to a model to print a log message when a value was set 
 */
class DebugSetterStrategy extends GenericNamedPropertyBehavior
{
  /**
   * The log 
   * @var LoggerInterface
   */
  private LoggerInterface $log;
  
  
  /**
   * @param string $name Property name 
   * @param LoggerInterface $log
   */
  public function __construct( string $name, LoggerInterface $log )
  {
    //..Pass the property name 
    parent::__construct( $name );   
    $this->log = $log;
  }
  
  
  /**
   * Callback used to set a value.
   * This is called prior to IProperty::validate() and the return value will 
   * replace the supplied value.
   * 
   * f( IProperty, value ) : mixed
   * 
   * @return Closure callback
   */
  public function getSetterCallback() : ?Closure
  {
    return function( IProperty $prop, $value ) {
      //..Add the log message
      $this->log->debug( $prop->getName() . ' changed to ' . (string)$value );

      //..Return the unmodified value.
      //..Setters can modify this value if desired
      return $value;
    };
  }  
}

然后使用策略

//..Create the property config object and attach the strategy for the "name" property
$config = new SamplePropertyConfig( new DebugSetterStrategy( 'name', new LoggerInterfaceImpl()));

//..Create a model using the configuration 
$model = new DefaultModel( new StandardPropertySet( $config ));

现在每次设置 "name" 属性时,调试日志将显示 "[属性名称] 更改为 [新值]"

快速模型

快速模型 在你需要创建临时模型或快速创建模拟模型时非常有用。快速模型接受标准配置数组,可以执行标准模型能做的任何事情。在以下示例中,我们创建了一个具有两个属性 "id" 和 "name" 的模型,并向 "name" 属性添加了一些行为。当设置 "name" 属性时,将在传入值后追加 "-bar"。当检索 "name" 属性时,将在输出值后追加 "-baz"。

//..Create a new quick model 
$q = new \buffalokiwi\magicgraph\QuickModel([
  //..Id property, integer, primary key
  'id' => [
    'type' => 'int',
    'flags' => ['primary']
  ],
    
  //..Name property, string 
  'name' => [
    'type' => 'string',
      
    //..Append -bar to the name property value when seting 
    'setter' => function( IProperty $prop, string $value ) : string {
      return $value . '-bar';
    },
            
    //..Append -baz to the name property value when retrieving 
    'getter' => function( IProperty $prop, string $value ) : string {
      return $value . '-baz';
    }
  ]
]);

//..Set the name attribute
$q->name = 'foo';

echo $q->name; //..Outputs "foo-bar-baz"

注解

PHP 8 添加了一个名为属性的新特性。这些令人惊艳的东西让我们可以给属性贴上诸如支持对象类型、默认值、标志等标签。如果你愿意让 Magic Graph 做一些假设,你可以跳过创建属性集和配置数组/文件。一个注解模型可能看起来像这样

use buffalokiwi\magicgraph\AnnotatedModel;
use buffalokiwi\magicgraph\property\annotation\IntegerProperty;
use buffalokiwi\magicgraph\property\annotation\BooleanProperty;
use buffalokiwi\magicgraph\property\annotation\DateProperty;
use buffalokiwi\magicgraph\property\annotation\ArrayProperty;
use buffalokiwi\magicgraph\property\annotation\EnumProperty;
use buffalokiwi\magicgraph\property\annotation\FloatProperty;
use buffalokiwi\magicgraph\property\annotation\SetProperty;
use buffalokiwi\magicgraph\property\annotation\StringProperty;
use buffalokiwi\magicgraph\property\annotation\USDollarProperty;


class Test extends AnnotatedModel
{  
  #[IntegerProperty]
  private int $id;
  
  #[BooleanProperty]
  private bool $b;
  
  #[DateProperty('d', '1/1/2020')]
  private IDateTime $d;  
  
  #[ArrayProperty('a','\stdClass')]
  private array $a;
  
  #[EnumProperty('e','\buffalokiwi\magicgraph\property\EPropertyType','int')]
  private \buffalokiwi\magicgraph\property\EPropertyType $e;
  
  #[FloatProperty]
  private float $f;
  
  #[SetProperty('set','\buffalokiwi\magicgraph\property\SPropertyFlags',['noinsert','noupdate'])]
  private \buffalokiwi\buffalotools\types\ISet $set;
  
  #[USDollarProperty]
  private buffalokiwi\magicgraph\money\IMoney $money;
  
  #[StringProperty]
  private string $str;
  
  public \buffalokiwi\magicgraph\property\IIntegerProperty $pubProp;
  
  public function __construct()
  {
    $this->pubProp = new buffalokiwi\magicgraph\property\DefaultIntegerProperty( 'pubProp', 10 );    

    parent::__construct( new \buffalokiwi\magicgraph\property\QuickPropertySet([
       'name' => [
           'type' => 'string',
           'value' => 'qp string'
       ]
    ]));
  }
}


$a = new Test();

$aa = $a->a;
$aa[] = new \stdClass();
$a->a = $aa;

$a->id = 22;
$a->b = true;
$a->d = '10/10/2020';
$a->f = 1.123;
$a->set->add( 'primary' );
$a->str = 'foo';
$a->e->setValue( 'string' );
$a->pubProp->setValue( 11 );
$a->money = '3.50';

var_dump( $a->toArray(null, true, true));

Outputs:

array (size=11)
  'name' => string 'qp string' (length=9)
  'id' => int 22
  'b' => int 1
  'd' => 
    object(DateTimeImmutable)[644]
      public 'date' => string '2020-10-10 00:00:00.000000' (length=26)
      public 'timezone_type' => int 3
      public 'timezone' => string 'UTC' (length=3)
  'a' => 
    array (size=1)
      0 => 
        object(stdClass)[701]
  'e' => string 'string' (length=6)
  'f' => float 1.123
  'set' => string 'primary,noupdate,noinsert' (length=25)
  'money' => string '3.50' (length=4)
  'str' => string 'foo' (length=3)
  'pubProp' => int 11

上述示例混合了 PHP 属性、配置数组和公共 IProperty 实例。如果扩展 AnnotatedModel 类,三种方式都可以用来创建模型。

在未来版本中,注解包将扩展以包括所有可用的属性配置选项,并配置关系。

仓库

Magic Graph 存储库是实现 Repository 模式 的。
存储库是一个封装了访问某些持久层的逻辑的抽象。类似于集合,存储库提供了创建、保存、删除和检索 IModel 实例的方法。存储库是对象工厂,旨在生产单个对象类型。然而,在 SQL 设置中,存储库可以与数据库中的多个表一起工作以生成单个模型实例。在创建聚合存储库时,你可以选择为每个表创建一个存储库,或者创建一个访问多个表的单一存储库。值得注意的是,存储库可能引用其他引用不同持久层的存储库。

存储库实现了 IRepository 接口。

映射对象工厂

数据映射器将来自某些持久层检索到的数据映射到 IModel 实例,并实现 IModelMapper 接口。在 Magic Graph 中,数据映射器也充当对象工厂。

MappingObjectFactory 通常作为所有仓库的基类,并实现了 IObjectFactory 接口。映射对象工厂负责保存数据映射器和定义某些模型的属性集,并使用这些引用来创建单个 IModel 实现的实例。create() 方法接受持久层提供的原始数据,创建一个新的 IModel 实例,并将提供的数据映射到新创建的模型。

IObjectFactory 实现不应直接访问任何持久层。相反,扩展此接口并定义访问特定类型持久层的抽象。例如,在 Magic Graph 中,有一个用于处理 MySQL 数据库的 SQLRepository。

注意:如果您只想创建模型的对象工厂,可以直接实例化 MappingObjectFactory。

可保存的映射对象工厂

SaveableMappingObjectFactory 是一个扩展 IObjectFactory 的抽象类,实现了 ISaveableObjectFactory 接口,并增加了保存 IModel 实例的能力。Magic Graph 中的所有仓库都扩展了这个类。可保存的映射对象工厂增加了 save() 和 saveAll() 方法,以下概述了仓库保存事件:

  1. 测试提供的模型是否与仓库管理的 IModel 实现匹配。这可以防止保存错误类型的模型。
  2. 调用受保护的 beforeValidate() 方法。这可以用于在扩展仓库中准备模型以进行验证。
  3. 通过调用 IModel::validate() 来验证模型。
  4. 调用受保护的 beforeSave() 方法。这可以用于准备模型以保存。
  5. 调用受保护的 saveModel() 方法。这需要在所有扩展仓库中实现。
  6. 调用受保护的 afterSave() 方法。这可以用于保存后清理任何内容。此方法不应产生副作用。

调用 saveAll() 与 save 方法略有不同。在测试模型类型后,保存过程分为三个部分

  1. 对于每个提供的模型,调用 beforeValidate()、validate() 和 beforeSave()。
  2. 对于每个提供的模型,调用 saveModel()。
  3. 对于每个提供的模型,调用 afterSave()。

SQL 仓库

SQLRepository 是用于处理 MariaDB/MySQL 数据库的 IRepository。SQLRepository 还扩展了 ISQLRepository 接口,该接口为处理 SQL 数据库添加了额外的方法。

在下面的示例中,我们将使用与基本数据库和仓库设置中概述的相同表和数据库连接。

实例化 SQLRepository

$testSQLRepo = new SQLRepository(                            //..Create the SQL Repository
  'inlinetest',                                              //..Table Name
  new DefaultModelMapper( function( IPropertySet $props ) {  //..Create a data mapper 
    return new DefaultModel( $props );                       //..Object factory 
  }, IModel::class ),                                        //..Type of model being returned 
  $dbFactory->getConnection(),                               //..SQL database connection 
  new QuickPropertySet([                                     //..Property set defining properties added to the model 
    //..Id property, integer, primary key      
    'id' => [                                                //.."id" is a property
      'type' => 'int',                                       //..Id is an integer
      'flags' => ['primary']                                 //..Id is the primary key
    ], 

    //..Name property, string 
    'name' => [                                              //.."name" is a property 
      'type' => 'string',                                    //..Name is a string 
    ]
  ])
);

上述设置类似于 InlineSQLRepo,但它允许我们对哪些组件被使用有更细粒度的控制。在这里,我们可以定义使用哪种数据映射器以及如何创建属性集。有关更多信息,请参阅可扩展模型

装饰仓库

Magic Graph 提供了几个代理类,这些类可以用作仓库装饰器的基类。

  1. ObjectFactoryProxy
  2. RepositoryProxy
  3. SaveableMappingObjectFactoryProxy
  4. ServiceableRepository
  5. SQLRepositoryProxy
  6. SQLServiceableRepository

上述列出的代理类都接受一个相关的存储库实例作为构造函数参数,并将方法调用映射到提供的存储库实例。应扩展代理类以向存储库提供附加功能。
ServiceableRepository和SQLServiceableRepository是代理类的实现,提供了进一步扩展存储库功能的方法。这些内容将在下一节中讨论。

我计划在未来的Magic Graph版本中添加更多装饰器,当前版本中包含一个装饰器。

CommonObjectRepo扩展了RepositoryProxy类,用于防止多次数据库查询。每次从存储库检索模型时,它都会在内存中缓存,任何后续的检索调用都将返回模型的缓存版本。

以下是一个使用装饰器的示例。

$testSQLRepo = new CommonObjectRepo( new SQLRepository(      //..Create the SQL Repository and add the caching decorator 
  'inlinetest',                                              //..Table Name
  new DefaultModelMapper( function( IPropertySet $props ) {  //..Create a data mapper 
    return new DefaultModel( $props );                       //..Object factory 
  }, IModel::class ),                                        //..Type of model being returned 
  $dbFactory->getConnection(),                               //..SQL database connection 
  new QuickPropertySet([                                     //..Property set defining properties added to the model 
    //..Id property, integer, primary key      
    'id' => [                                                //.."id" is a property
      'type' => 'int',                                       //..Id is an integer
      'flags' => ['primary']                                 //..Id is the primary key
    ], 

    //..Name property, string 
    'name' => [                                              //.."name" is a property 
      'type' => 'string',                                    //..Name is a string 
    ]
  ])
));

装饰存储库既简单又有趣!

可服务仓库

服务性存储库用于存储库关系,这些内容将在关系部分中讨论。

基本上,服务性存储库接受ITransactionFactory和零个或多个IModelPropertyProvider实例,这些实例用于扩展某些属性的功能。

例如,假设您有一个具有IModel属性B的模型A。默认情况下,模型A的存储库不知道存储库B或模型B的存在。IModelPropertyProvider实例可以在从模型A访问属性时添加用于检索模型B的懒加载方案。此外,模型属性提供者还可以提供在模型A保存时保存模型B编辑的方法。

组合主键

Magic Graph完全支持复合主键,IRepository和ISQLRepository的某些方法将包含可变参数id参数,用于传递多个主键值。复合主键通过以下方式通过IPropertyFlags::PRIMARY属性分配:

[                                                          
  //..Id property, integer, primary key      
  'id' => [                                                //.."id" is a property
    'type' => 'int',                                       //..Id is an integer
    'flags' => ['primary']                                 //..Id is the primary key
  ], 

  'id2' => [                                               //.."id2" is a property
    'type' => 'int',                                       //..Id2 is an integer
    'flags' => ['primary']                                 //..Id2 is the other primary key
  ], 
]

注意:当向存储库方法提供主键值时,它们以定义的顺序接受。我将在未来的版本中创建一种不需要依赖参数顺序的方法。

事务

事务概述

事务代表一些工作单元,通常用于执行针对某些持久层的保存操作。类似于数据库事务,Magic Graph事务将

  1. 在持久层可用时启动事务
  2. 在持久层上执行任意代码
  3. 提交更改
  4. 在失败时回滚更改

事务基于单个接口ITransaction,可以有多种实现用于支持各种持久层。Magic Graph完全支持同时使用多个和不同的持久层。事务可以被视为一个适配器,它执行用于实现所需提交和回滚功能的特定于持久层的命令。

目前,Magic Graph附带一个事务类型:MySQLTransaction

创建事务

任何事务的核心都是要执行代码。在Magic Graph中,使用IRunnable接口来定义事务中要执行的代码。这种类型存在,因为每种持久化类型都需要创建IRunnable的子类。这些类型用于按持久化类型分组事务,并公开在处理事务时可能需要的持久化特定方法。例如,ISQLRunnable用于利用SQL的持久化层。ISQLRunnable添加了一个getConnection()方法,可以用来访问底层数据库连接。

在它的最简单形式中,事务是传递给Transaction对象的函数。当调用Transaction::run()时,将执行提供的函数。值得注意的是,Transaction可以接受多个函数。Transaction::run()将按照接收到的顺序调用提供的每个函数。

//..Get some data, model, etc.
$data = 'This represents a model or some other data being saved';

//..Create a new transaction. and write the contents of $data to a file when Transaction::run() is executed.
$transaction = new buffalokiwi\magicgraph\persist\Transaction( new buffalokiwi\magicgraph\persist\Runnable( function() use($data) {
  file_put_contents( 'persistence.txt', $data );
}));

执行事务

//..Start a new transaction inside of the persistence layer 
$transaction->beginTransaction();

try {
  //..Execute the code 
  $transaction->run();

  //..Commit any changes in the persistence layer 
  $transaction->commit();

} catch( \Exception $e ) {
  //..OH NO!  An Error!
  //..Revert any changes in the persistence layer
  $transaction->rollBack();
}

Magic Graph默认的Transaction对象不连接到任何特定的持久化层,beginTransaction、commit和rollBack的实现不执行任何操作。

由于我们希望方法真正做些事情,以下是如何针对MySQL/MariaDB运行事务的示例。MySQL事务传递一个ISQLRunnable实例。目前,ISQLRunnable只有一个实现,即MySQLRunnable
MySQL Runnable与上一个示例中使用的Transaction对象不同,它添加了一个构造函数参数,该参数接受ISQLRepository的实例。该存储库用于获取buffalokiwi\magicgraph\pdo\IDBConnection实例,该实例用于执行事务。

以下是如何针对SQLRepository实例执行事务的完整示例

//..Create a repository
$testSQLRepo = new SQLRepository(
  'inlinetest',
  new DefaultModelMapper( function( IPropertySet $props ) {
    return new DefaultModel( $props );
  }, IModel::class ),
  $dbFactory->getConnection(),
  new QuickPropertySet([
    //..Id property, integer, primary key
    'id' => [
      'type' => 'int',
      'flags' => ['primary']
    ],

    //..Name property, string 
    'name' => [
      'type' => 'string',
    ]
  ])
);   
   
//..Create a new model and assign some property values
$model = $testSQLRepo->create([]);
$model->name = 'test';
  
//..Create a transaction 
$transaction = new \buffalokiwi\magicgraph\persist\MySQLTransaction(
  new \buffalokiwi\magicgraph\persist\MySQLRunnable(
    $testSQLRepo,
    function() use( $testSQLRepo, $model ) {
      $testSQLRepo->save( $model );
    }
));


//..Start a new transaction inside of the persistence layer
$transaction->beginTransaction();

try {
  //..Execute the code 
  $transaction->run();

  //..Commit any changes in the persistence layer 
  $transaction->commit();

} catch( \Exception $e ) {
  //..OH NO!  An Error!
  //..Revert any changes in the persistence layer
  $transaction->rollBack();
}

虽然针对单个持久化引擎的事务可以直接针对数据库进行编码,但Magic Graph事务抽象提供了一种方法,使我们能够针对多个数据库连接或不同的持久化引擎运行事务。

事务工厂

事务工厂生成ITransaction某些子类的实例。想法是将IRunnable实例的列表传递给ITransactionFactory::createTransactions(),然后事务工厂将按持久化类型(注册的子类)对它们进行分组。
事务的执行方式如下

  1. 为每个ITransaction实例执行开始事务
  2. 为每个ITransaction实例调用run
  3. 为每个ITransaction实例调用commit
  4. 如果在任何时候抛出异常,则对每个ITransaction实例调用rollback,并重新抛出异常。
//..Create a database connection factory for some MySQL database
$dbFactory = new PDOConnectionFactory( 
  new MariaConnectionProperties( 
    'localhost',    //..Host
    'root',         //..User
    '',             //..Pass
    'retailrack' ), //..Database 
  function(IConnectionProperties $args  ) {
    return new MariaDBConnection( $args );
});


//..Create a quick test repository for a table named "inlinetest", with two columns id (int,primary,autoincrement) and name(varchar).
$repo = new InlineSQLRepo( 
  'inlinetest', 
  $dbFactory->getConnection(),
  new PrimaryIntegerProperty( 'id' ),
  new DefaultStringProperty( 'name' )
);

//..Create a new model and set the name property value to "test"
$model = $repo->create([]);
$model->name = 'test';


//..Create a new transaction factory
//..The supplied map is used within the TransactionFactory::createTransactions() method, and will generate ITransaction
//  instances of the appropriate type based on a predefined subclass of IRunnable 
//..Instances passed to TransactionFactory must be ordered so that the most generic IRunnable instances are last.
$tf = new TransactionFactory([
  //..Supplying ISQLRunnable instances will generate instaces of MySQLTransaction
  ISQLRunnable::class => function( IRunnable ...$tasks ) { return new MySQLTransaction( ...$tasks ); },
  //..Supplying instances of IRunnable will generate a Transaction instance
  IRunnable::class => function( IRunnable ...$tasks ) { return new Transaction( ...$tasks ); }
]);

//..Execute a mysql transaction
//..This will use a database transaction to save the model
//..If any exceptions are thrown by the supplied closure, then rollback is called.  Otherwise, commit is called 
//..upon successful completion of the closure
$tf->execute( new MySQLRunnable( $repo, function() use($repo, $model) {
  $repo->save( $model );  
}));

如果在上一个示例中$repo->save()抛出异常,则事务将回滚。如果执行以下代码,则您将看到由于在抛出异常时调用rollback,该行永远不会添加到数据库中。

$tf->execute( new MySQLRunnable( $repo, function() use($repo, $model) {
  $repo->save( $model );  
  throw new \Exception( 'No save for you' );
}));

模型关系提供者

类似于关系数据库中的外键,关系允许我们创建域对象之间的关联。在Magic Graph中,模型(IModel)可以包含零个或多个引用单个或列表关联IModel对象的属性。父模型可以包含IModelProperty和/或ArrayProperty属性,这些属性可以保存引用的模型对象。

例如,以下配置数组包含一个模型属性和一个数组属性。

[
  'one' => [
      'type' => 'model',
      'flags' => ['noinsert','noupdate'],
      'clazz' => \buffalokiwi\magicgraph\DefaultModel::class
  ]

  'many' => [
      'type' => 'array',
      'flags' => ['noinsert','noupdate'],
      'value' => [],
      'clazz' => \buffalokiwi\magicgraph\DefaultModel::class
  ]
]

模型和数组属性都必须包含“clazz”配置属性,该属性必须等于对象或数组中对象的类名。这用于确定在关系提供程序中实例化哪个对象,并确保设置属性值时只接受指定类型的对象。

请注意,这两个属性都带有“noinsert”和“noupdate”标记。这对于模型和数组属性都是必需的,将阻止在插入和更新数据库查询中使用这些属性。如果省略这些值,IModel属性将持久化为IModel::__toString(),ArrayProperty将编码为json。

将值分配给父模型大致如下

  //..Assuming $model was created using the above config and that $ref1 and $ref2 are both instances of DefaultModel

  //..Ok
  $model->one = $ref1;

  //..Throws exception
  $model->one = 'foo';

  //..Multiple models can be added as an array
  $model->many = [$ref1, $ref2];

一旦我们有一些模型或模型数组属性,我们可能希望自动加载和保存这些模型。例如,当我们访问模型属性时,我们可以从数据库中加载模型并返回它。我们还可以在父模型保存时保存对引用模型的任何编辑。这种行为是通过实现IModelPropertyProvider接口来实现的。

IModelPropertyProvider定义了初始化模型、检索值、设置值和持久化值的方法。
必须与支持模型和存储库一起使用模型属性提供程序。

//..A sample child model.  This uses a unique class name instead of QuickModel because IModelProperty will attempt
//  to instantiate an instance of the model when assigning the default value, and quick model is generic.
class ChildModel extends buffalokiwi\magicgraph\QuickModel {
  public function __construct() {
    parent::__construct([
      'name' => [
         'type' => 'string',
         'value' => 'child model'
       ]
    ]);    
  }
}
  
//..The parent model includes a property "child", which is backed by an IModelProperty, and will contain 
//  an instance of ChildModel
$parent = new buffalokiwi\magicgraph\QuickModel([
   'name' => [
       'type' => 'string',
       'value' => 'parent model'
   ],
    
   'child' =>  [
       'type' => 'model',
       'clazz' => ChildModel::class
   ]
]);


//..Models are converted to arrays when using toArray()
var_dump( $parent->toArray( null, true, true ));

Outputs:
array (size=2)
  'name' => string 'parent model' (length=12)
  'child' => 
    array (size=1)
      'name' => string 'child model' (length=11)

可服务模型

Serviceable Model扩展了DefaultModel,并修改了DefaultModel构造函数以接受零个或多个IModelPropertyProvider实例。传递的提供程序与父模型配置中定义的属性相关联,并将处理关联模型(s)的加载和保存。

可服务仓库

Serviceable RepositorySQL Serviceable Repository(用于SQL存储库)是存储库装饰器,它们添加了对IModelPropertyProviders的支持。当在存储库对象工厂中创建模型时,将模型属性提供程序实例传递给ServiceableModel构造函数。此外,当调用存储库保存方法时,将包括模型属性提供程序的保存函数作为保存事务的一部分。

下一节将描述Magic Graph中包含的模型属性提供程序。

关系

有关模型属性和IModelPropertyProvider的信息,请参阅模型服务提供程序

以下表格用于一对一和多对一示例部分

-- Parent Table
create table table1 (
  id int not null primary key auto_increment,
  name varchar(20) not null,
  childid int not null
) engine=innodb;


-- Child / linked table 
create table table2 (
  id int not null auto_increment,
  link_table1 int not null,
  name varchar(20) not null,
  primary key (link_table1, id),
  key id(id)
) engine=innodb;

--Insert the parent model record
insert into table1 (name,childid) values ('Parent',1);

--insert the child model records
insert into table2 (link_table1, name) values(last_insert_id(),'Child 1'),(last_insert_id(),'Child 2');

一对一

OneOnePropertyService提供加载、附加、编辑和保存单个关联模型的能力。

//..When using model property providers / relationships, models MUST extend ServiceableModel.  ServiceableModel 
//  extends DefaultModel, and adds the required functionality for relationships.
//..Table1 Model
class Table1Model extends \buffalokiwi\magicgraph\ServiceableModel {};

//..Table2 Model
class Table2Model extends \buffalokiwi\magicgraph\ServiceableModel {};


//..Create a SQL Database connection 
$dbFactory = new buffalokiwi\magicgraph\pdo\PDOConnectionFactory( //..A factory for managing and sharing connection instances 
  new buffalokiwi\magicgraph\pdo\MariaConnectionProperties(       //..Connection properties for MariaDB / MySQL
    'localhost',                  //..Database server host name 
    'root',                       //..User name
    '',                           //..Password
    'retailrack' ),               //..Database 
  //..This is the factory method, which is used to create database connection instances
  //..The above-defined connection arguments are passed to the closure.
  function( buffalokiwi\magicgraph\pdo\IConnectionProperties $args  ) { 
    //..Return a MariaDB connection 
    return new buffalokiwi\magicgraph\pdo\MariaDBConnection( $args );
  }
);


//..Create the transaction factory
$tFact = new \buffalokiwi\magicgraph\persist\DefaultTransactionFactory();


//..Table2 Repository
//..This must be initialized prior to Table1Repo because Table1Repo depends on Table2Repo
$table2Repo = new buffalokiwi\magicgraph\persist\DefaultSQLRepository(
  'table2', 
  $dbFactory->getConnection(),
  Table2Model::class,
  $table2Properties
);

//..Create properties for Table1Model
$table1Properties = new buffalokiwi\magicgraph\property\QuickPropertySet([
  //..Primary key
  'id' => [
      'type' => 'int',
      'flags' => ['primary']
  ],
    
  //..A name 
  'name' => [
      'type' => 'string'      
  ],
    
   //..Property containing the primary key for a Table2Model 
  'childid' => [
      'type' => 'int',
      'value' => 0
  ],
    
  //..Child model property.
  //..A model from Table2Repository is pulled by the id defined in the "childid" property
  'child' => [
    'type' => 'model',
    'flags' => ['noinsert','noupdate','null'],  //..Since Table2Model requires constructor arguments, we'll pass null here.
    'clazz' => Table2Model::class
  ]
]);


$table1Repo = new \buffalokiwi\magicgraph\persist\DefaultSQLServiceableRepository(
    'table1', //..SQL table name 
    $dbFactory->getConnection(), //..SQL database connection 
    Table1Model::class, //..The Table1Model class name used for the object factory 
    $table1Properties,  //..Properties used to create Table1Model instances
    $tFact, //..Transaction factory used to handle saving across multiple model property providers 
    new \buffalokiwi\magicgraph\OneOnePropertyService( new \buffalokiwi\magicgraph\OneOnePropSvcCfg(
      $table2Repo,
      'childid',
      'child'
)));        

//..Get the only record in table1
$model = $table1Repo->get('1');

//..Print the model contents with related child models 
var_dump( $model->toArray( null, true, true ));


Outputs:
array (size=4)
  'id' => string '1' (length=1)
  'name' => string 'Parent' (length=6)
  'childid' => string '1' (length=1)
  'child' => 
    array (size=3)
      'id' => string '1' (length=1)
      'link_table1' => string '1' (length=1)
      'name' => string 'Child 1' (length=7)

一对多

OneManyPropertyService提供加载、附加、编辑和保存多个关联模型的能力。

//..When using model property providers / relationships, models MUST extend ServiceableModel.  ServiceableModel 
//  extends DefaultModel, and adds the required functionality for relationships.
//..Table1 Model
class Table1Model extends \buffalokiwi\magicgraph\ServiceableModel {};

//..Table2 Model
class Table2Model extends \buffalokiwi\magicgraph\ServiceableModel {};


//..Model properties for Table1
$table1Properties = new buffalokiwi\magicgraph\property\QuickPropertySet([
  'id' => [
      'type' => 'int',
      'flags' => ['primary']
  ],
    
  'name' => [
      'type' => 'string'      
  ],
    
  'children' => [
    'type' => 'array',
    'flags' => ['noinsert','noupdate'],
    'clazz' => Table2Model::class
  ]
]);


//..Model properties for table 2 
$table2Properties = new buffalokiwi\magicgraph\property\QuickPropertySet([
  'id' => [
      'type' => 'int',
      'flags' => ['primary']
  ],
   
  'link_table1' => [
      'type' => 'int',
      'value' => 0
  ],
    
  'name' => [
      'type' => 'string'      
  ]
]);

//..Create a SQL Database connection 
$dbFactory = new buffalokiwi\magicgraph\pdo\PDOConnectionFactory( //..A factory for managing and sharing connection instances 
  new buffalokiwi\magicgraph\pdo\MariaConnectionProperties(       //..Connection properties for MariaDB / MySQL
    'localhost',                  //..Database server host name 
    'root',                       //..User name
    '',                           //..Password
    'retailrack' ),               //..Database 
  //..This is the factory method, which is used to create database connection instances
  //..The above-defined connection arguments are passed to the closure.
  function( buffalokiwi\magicgraph\pdo\IConnectionProperties $args  ) { 
    //..Return a MariaDB connection 
    return new buffalokiwi\magicgraph\pdo\MariaDBConnection( $args );
  }
);


//..Create the transaction factory
$tFact = new \buffalokiwi\magicgraph\persist\DefaultTransactionFactory();


//..Table2 Repository
//..This must be initialized prior to Table1Repo because Table1Repo depends on Table2Repo
$table2Repo = new buffalokiwi\magicgraph\persist\DefaultSQLRepository(
  'table2', 
  $dbFactory->getConnection(),
  Table2Model::class,
  $table2Properties
);

//..Table1 Repository
//..A sql database repository that can include model property providers used for relationships
$table1Repo = new \buffalokiwi\magicgraph\persist\DefaultSQLServiceableRepository( 
    'table1', //..SQL table name 
    $dbFactory->getConnection(), //..SQL database connection 
    Table1Model::class, //..The Table1Model class name used for the object factory 
    $table1Properties,  //..Properties used to create Table1Model instances
    $tFact, //..Transaction factory used to handle saving across multiple model property providers 
    new buffalokiwi\magicgraph\OneManyPropertyService( //..This handles loading and saving related models 
      new buffalokiwi\magicgraph\OneManyPropSvcCfg( //..Configuration 
        $table2Repo,    //..Linked model repository //$parentIdProperty, $arrayProperty, $linkEntityProperty, $idProperty)
        'id',           //..The parent model primary key property name.
        'children',     //..The parent model property name for the array of linked models
        'link_table1',  //..A linked model property that contains the parent id
        'id' )          //..A linked model property containing the unique id of the linked model
));


//..Get the only record in table1
$model = $table1Repo->get('1');

//..Print the model contents with related child models 
var_dump( $model->toArray( null, true, true ));


Outputs:

array (size=3)
  'id' => string '1' (length=1)
  'name' => string 'Parent' (length=6)
  'children' => 
    array (size=2)
      0 => 
        array (size=3)
          'id' => string '1' (length=1)
          'link_table1' => string '1' (length=1)
          'name' => string 'Child 1' (length=7)
      1 => 
        array (size=3)
          'id' => string '2' (length=1)
          'link_table1' => string '1' (length=1)
          'name' => string 'Child 2' (length=7)

多对多

有时我们有很多可以映射到很多其他东西的东西。例如,在电子商务设置中,产品可以映射到多个类别,而类别可以包含多个产品。在这种情况下,我们需要一个交换单来存储这些映射。幸运的是,在Magic Graph中这相当简单。

首先,我们从标准交换单开始。如果我们使用以下表定义,我们可以使用内置的交换单模型。

CREATE TABLE `product_category_link` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `link_parent` int(11),
  `link_target` int(11),
  PRIMARY KEY (`link_parent`,`link_target`),
  KEY `id` (`id`)
) ENGINE=InnoDB 
  1. "id"包含主键
  2. "link_parent"是父模型的ID。例如:产品ID
  3. "link_target" 是目标模型的 ID。例如:分类 ID

现在我们创建两个其他表。一个用于父级,另一个用于目标。为了娱乐,我们将在两个表中都添加一个名称列。

create table `product` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) not null,
  primary key (id)
) ENGINE=InnoDB;


insert into product (name) values ('product1');
insert into product (name) values ('product2');

分类表

create table `product_category` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) not null,
  primary key (id)
) ENGINE=InnoDB;


insert into product_category (name) values ('category1');
insert into product_category (name) values ('category2');

现在我们将 product1 添加到 category1,并将 product2 添加到 category2

insert into product_category_link (link_parent,link_target) values (1,1),(2,2);
//..Define the category model
class CategoryModel extends \buffalokiwi\magicgraph\ServiceableModel {}

//..Create the category model property configuration 
//..In this instance, we are using QuickJunctionPropertyConfig because we want to use this model as a junction table target
//..QuickJunctionPropertyConfig implements IJunctionTargetProperties, which exposes the primary id property name and is used 
//  to generate database queries.
$cProps = new \buffalokiwi\magicgraph\property\QuickPropertySet( new \buffalokiwi\magicgraph\junctionprovider\QuickJunctionPropertyConfig([
    'id' => [
        'type' => 'int',
        'flags' => ['primary']
    ],

    'name' => [
        'type' => 'string'      
    ],

    //..This is the list of products contained within a category
    'products' => [
      'type' => 'array',
      'flags' => ['noinsert','noupdate'],
      'clazz' => ProductModel::class
    ]        
  ], 
  'id' //..Primary key property name used as the junction link target 
));


//..Define the product model 
class ProductModel extends \buffalokiwi\magicgraph\ServiceableModel {}



//..Create the product model property configuration 
$pProps =  new \buffalokiwi\magicgraph\property\QuickPropertySet( new \buffalokiwi\magicgraph\junctionprovider\QuickJunctionPropertyConfig([
    'id' => [
        'type' => 'int',
        'flags' => ['primary']
    ],

    'name' => [
        'type' => 'string'      
    ],

    //..The list of categories containing some product
    'categories' => [
      'type' => 'array',
      'flags' => ['noinsert','noupdate'],
      'clazz' => CategoryModel::class
    ]        
  ],
  'id' //..Primary key property name used as the junction link target 
));


//..Create the transaction factory
$tFact = new \buffalokiwi\magicgraph\persist\DefaultTransactionFactory();
    

//..Create a SQL Database connection 
$dbFactory = new buffalokiwi\magicgraph\pdo\PDOConnectionFactory( //..A factory for managing and sharing connection instances 
  new buffalokiwi\magicgraph\pdo\MariaConnectionProperties(       //..Connection properties for MariaDB / MySQL
    'localhost',                  //..Database server host name 
    'root',                       //..User name
    '',                           //..Password
    'retailrack' ),               //..Database 
  //..This is the factory method, which is used to create database connection instances
  //..The above-defined connection arguments are passed to the closure.
  function( buffalokiwi\magicgraph\pdo\IConnectionProperties $args  ) { 
    //..Return a MariaDB connection 
    return new buffalokiwi\magicgraph\pdo\MariaDBConnection( $args );
  }
);


//..Create the repository for the junction table 
$jRepo = new buffalokiwi\magicgraph\junctionprovider\DefaultMySQLJunctionRepo(
  'product_category_link',
  $dbFactory->getConnection()
);
  

//..Create the product repository 
$pRepo = new buffalokiwi\magicgraph\persist\DefaultSQLServiceableRepository(
  'product',
  $dbFactory->getConnection(),
  ProductModel::class,
  $pProps,
  $tFact
);


//..Create the category repository 
$cRepo = new buffalokiwi\magicgraph\persist\DefaultSQLServiceableRepository(
  'product_category',
  $dbFactory->getConnection(),
  CategoryModel::class,
  $cProps,
  $tFact
);


//..Since we want both models to reference each other, we cannot instantiate the junction providers until
//  both parent and target repositories have been created.
//..There is a handy method for adding these: addModelPropertyProvider()
//
//..If we were only referencing the target models in the parent repository or vice versa, we would have passed the junction
//..model instance directly to the serviceable repository constructor 


//..Add the junction model property provider 
$pRepo->addModelPropertyProvider(
  new buffalokiwi\magicgraph\junctionprovider\MySQLJunctionPropertyService( 
    new buffalokiwi\magicgraph\junctionprovider\JunctionModelPropSvcCfg(
      'id',
      'categories' ),
    $jRepo,
    $cRepo
));


$cRepo->addModelPropertyProvider(
  new buffalokiwi\magicgraph\junctionprovider\MySQLJunctionPropertyService( 
    new buffalokiwi\magicgraph\junctionprovider\JunctionModelPropSvcCfg(
      'id',
      'products' ),
    $jRepo,
    $pRepo
));


//..Get and print the product model 
$p1 = $pRepo->get('1');
var_dump( $p1->toArray(null,true,true));

Outputs:

array (size=3)
  'id' => int 1
  'name' => string 'product1' (length=8)
  'categories' => 
    array (size=1)
      0 => 
        array (size=3)
          'id' => int 1
          'name' => string 'category1' (length=9)
          'products' => 
            array (size=1)
              0 => 
                array (size=3)
                  'id' => int 1
                  'name' => string 'product1' (length=8)
                  'categories' => 
                    array (size=1)
                      ...


//..Get and print the category model
$c1 = $cRepo->get('1');
var_dump( $p1->toArray(null,true,true));


Outputs:

array (size=3)
  'id' => int 1
  'name' => string 'category1' (length=9)
  'products' => 
    array (size=1)
      0 => 
        array (size=3)
          'id' => int 1
          'name' => string 'product1' (length=8)
          'categories' => 
            array (size=1)
              0 => 
                array (size=3)
                  'id' => int 1
                  'name' => string 'category1' (length=9)
                  'products' => 
                    array (size=1)
                      ...

嵌套关系提供者

嵌套是通过使用《关系》章节中概述的相同方法完成的。

正如您在上面的多对多示例中所注意到的,关系提供者可以用来创建一系列嵌套对象。关系提供者可以插入任何模型的任何属性中,这意味着我们可以使用它们来创建一个漂亮的对象树。关系提供者可以用于支持任何模型属性

首先,我们创建 3 个简单的表。在这个例子中,这些表将只包含一个 ID 列。

创建一些表并插入一些值。为了保持简单,我们将使用 ID 为 "1" 的所有内容。

  
create table tablea( id int, primary key(id) ) engine=innodb;
create table tableb( id int, primary key(id) ) engine=innodb;
create table tablec( id int, primary key(id) ) engine=innodb;

insert into tablea values(1);
insert into tableb values(1);
insert into tablec values(1);

接下来,我们为每个表创建一个可用的模型和相应的属性集。我们将假设有一个变量 $dbFactory,它是一个 IConnectionFactory 的实例。还有一个变量 $tfact,它是一个 ITransactionFactory 的实例。这些在之前的章节中已有详细说明。

class Table1Model extends buffalokiwi\magicgraph\ServiceableModel {}
class Table2Model extends buffalokiwi\magicgraph\ServiceableModel {}
class Table3Model extends buffalokiwi\magicgraph\DefaultModel {}

$t1Props = new buffalokiwi\magicgraph\property\QuickPropertySet([
   'id' => [
       'type' => 'int',
       'flags' => ['primary']
   ],
    
   'table2model' => [
       'type' => 'model',
       'clazz' => Table2Model::class,
       'flags' => ['noinsert','noupdate','null'], //..Table2Model requires constructor arguments. Use null here.
   ]
]);


$t2Props = new buffalokiwi\magicgraph\property\QuickPropertySet([
   'id' => [
       'type' => 'int',
       'flags' => ['primary']
   ],
    
   'table3model' => [
       'type' => 'model',
       'clazz' => Table3Model::class,
       'flags' => ['noinsert','noupdate','null'], //..Table3Model requires constructor arguments. Use null here.
   ]
]);

$t3Props = new buffalokiwi\magicgraph\property\QuickPropertySet([
   'id' => [
       'type' => 'int',
       'flags' => ['primary']
   ]
]);

创建模型后,我们需要为每种类型的模型创建一个存储库。为此,我们将使用 DefaultSQLServiceableRepository,它与 ServiceableModel 一起允许我们使用关系提供者。控制位于对象图边缘的模型所在的存储库需要首先创建。例如:TableC,然后 TableB,然后 TableA。

//..There are no relationships in tableC
$t3Repo = new buffalokiwi\magicgraph\persist\DefaultSQLRepository(
  'tablec',
  $dbFactory->getConnection(),
  Table3Model::class,
  $t3Props,
);

$t2Repo = new buffalokiwi\magicgraph\persist\DefaultSQLServiceableRepository(
  'tableb',
  $dbFactory->getConnection(),
  Table2Model::class,
  $t2Props,
  $tfact,
  new buffalokiwi\magicgraph\OneOnePropertyService( new \buffalokiwi\magicgraph\OneOnePropSvcCfg(
    $t3Repo,
    'id',
    'table3model'
)));

$t1Repo = new buffalokiwi\magicgraph\persist\DefaultSQLServiceableRepository(
  'tablea',
  $dbFactory->getConnection(),
  Table1Model::class,
  $t1Props,
  $tfact,
  new buffalokiwi\magicgraph\OneOnePropertyService( new \buffalokiwi\magicgraph\OneOnePropSvcCfg(
    $t2Repo,
    'id',
    'table2model'
)));

最后,我们从 tablea 获取模型,并打印出图。

$model1 = $t1Repo->get("1");

var_dump( $model1->toArray( null, true, true ));

Outputs:

array (size=2)
  'id' => int 1
  'table2model' => 
    array (size=2)
      'id' => int 1
      'table3model' => 
        array (size=1)
          'id' => int 1

任何关系提供者都将与一对一提供者完全相同的方式工作。

编辑和保存的工作方式

正如《可保存映射对象工厂》部分所详细说明的,可以通过调用 ISaveableObjectFactory 的 save 方法将模型保存到某个地方。本节将讨论使用关系提供者进行编辑和保存时的工作方式。

编辑和保存由关系提供者和内置在 ServiceableModel 中的编辑属性跟踪系统共同控制。
当某个可服务存储库保存可服务模型时,调用 save 方法会导致存储库从关系提供者获取保存函数。

关系提供者必须实现 IModelPropertyProvider 接口。IModelPropertyProvider::getSaveFunction 方法将返回一个函数,该函数被传递给 ITransactionFactory,在其中调用保存函数,并持久化相关模型。

《一对一》关系提供者相对简单。任何由 OneOnePropertyService 支持的模型属性将在父模型通过某个存储库保存时自动保存。这将与通过提供者加载的新模型和现有模型一起工作。

一对多(《One to Many》)和多对多(《Many to Many》)关系提供者将管理包含IModel实例的数组属性。这比一对一提供者稍微复杂一些。除了保存编辑后的模型外,一对多和多对多提供者还将管理插入和删除操作。如果将新模型添加到数组属性中,它将被插入到数据库中。如果从数组属性中删除现有模型,它将从数据库中删除。如果在加载相关模型时使用任何过滤器或限制,则删除功能将禁用,并且必须手动通过控制该模型类型的存储库解除模型之间的链接。

当使用嵌套关系提供者时,保存操作将自动级联。对象图中的任何嵌套模型都可以进行编辑,当最顶层的模型被保存时,它将

可扩展模型

我们终于走过了基础概念,耶!

魔法图模型旨在尽可能灵活。正如你肯定已经注意到的,有多种方式可以配置模型,并且每种方式都具有不同级别的可扩展性。例如,可以使用简单的属性注释创建模型,也可以通过使用属性配置对象在运行时创建模型。虽然使用注解属性既快又简单,但它远不如使用属性配置对象可扩展。

通过使用属性配置对象,我们可以

  1. 定义给定模型中存在的属性
  2. 为属性提供运行时类型信息。例如,配置对象可以实现返回属性名称的方法,这可以用于在模型的属性集中查询属性元数据。
  3. 为属性添加额外的元数据
  4. 提供交换用于持久化的属性列表的能力。例如,如果我们想在具有不同属性名称的多个持久化类型之间共享模型,我们可以交换配置对象。
  5. 将简单行为附加到单个属性上。例如:获取、设置、更改等。
  6. 通过如:保存前、保存后、保存时、保存函数等函数扩展保存功能。
  7. 属性配置可以在运行时动态生成,这允许我们实现如EAV之类的模式。
  8. 可以使用多个属性配置对象创建单个模型。这允许开发人员创建模型扩展或将关注点分离到不同的包中。

属性配置接口

有关配置数组定义,请参阅《属性配置》。

属性配置对象必须全部实现《IPropertyConfig》接口。该接口由《IPropertySetFactory》的实现用于创建包含所有相关属性、元数据和行为的《IPropertySet》实例,这些属性、元数据和行为用于《IModel》实例。

目前,IPropertyConfig接口包含四个方法

getConfig(): array

当由IPropertySetFactory调用时,getConfig()会返回属性配置数组。此数组包含所有属性定义、元数据和可选的事件处理器。

getPropertyNames(): array

getPropertyNames()将返回一个字符串列表,包含由此配置对象定义的每个属性名称。

beforeSave( IModel $model ) : void;

在模型被持久化之前,IRepository会调用beforeSave()。这是一个修改模型状态或添加额外验证的机会,在保存之前。当创建beforeSave()处理程序时,IPropertyConfig实现应该遍历配置数组中定义的属性,并调用每个属性级别的beforeSave处理程序。

afterSave( IModel $model ) : void;

在模型被持久化之后,但在commit()之前,IRepository会调用afterSave()。这可以用于保存后的清理、检查保存结果等。当创建afterSave()处理程序时,IPropertyConfig实现应该遍历配置数组中定义的属性,并调用每个属性级别的afterSave处理程序。

属性配置实现

既然我们已经知道了属性配置对象以及配置数组的定义,让我们构建一个完整的实现。Magic Graph提供了一个抽象基类BasePropertyConfig,其中包含常用属性配置的常量,并通过将INamedPropertyBehavior实例传递给构造函数来添加通过行为策略的功能。

让我们创建一个基本的矩形模型。在这个例子中,我们将创建两个类:Rectangle和RectangleProperties。
Rectangle是值对象,RectangleProperties定义了Rectangle值对象中包含的属性。

/**
 * Property configuration for a rectangle value object
 */
class RectangleProperties extends buffalokiwi\magicgraph\property\BasePropertyConfig
{
  /**
   * Height property name 
   */
  const HEIGHT = 'height';
  
  /**
   * Width property name 
   */
  const WIDTH = 'width';
  
  
  /**
   * Returns the property configuration array 
   * @return array 
   */
  protected function createConfig() : array
  {
    return [
      self::HEIGHT => self::FINTEGER_REQUIRED,
      self::WIDTH => self::FINTEGER_REQUIRED
    ];
  }
}


/**
 * Rectangle Value Object
 */
class Rectangle extends buffalokiwi\magicgraph\GenericModel {}

//..Create the rectangle model instance 
$rectangle = new Rectangle( new RectangleProperties());


/**
 * Outputs:
 * array (size=2)
 *   'height' => int 0
 *   'width' => int 0
 */
var_dump( $rectangle->toArray());

/**
 * Throws Exception with message: 
 * "height" property of class "Rectangle" of type "int" is REQUIRED and must not be empty.
 */
$rectangle->validate();

上面的例子相当简单。配置对象定义了两个必需属性:高度和宽度。当模型被实例化时,高度和宽度都是零。这是因为每个属性的默认值是零,默认值将绕过属性验证。当调用IModel::validate()时,这两个属性都会被验证,并将抛出ValidationException

这很好,但是属性应该在设置时验证,对吧?必需属性标志只会在调用IModel::validate()时验证,因此如果我们想在设置属性时进行验证,我们必须添加一个验证回调。

我们可以像这样重写createConfig函数

protected function createConfig() : array
{
  //..Validation callback that will throw an exception when setting an integer property value to zero
  $vInt = fn( buffalokiwi\magicgraph\property\IProperty $prop, int $value ) : bool => !empty( $value );

  return [
    self::HEIGHT => self::FINTEGER_REQUIRED + [self::VALIDATE => $vInt],
    self::WIDTH => self::FINTEGER_REQUIRED + [self::VALIDATE => $vInt]
  ];
}

使用上述更改创建模型后,将高度或宽度设置为零将抛出异常。这不会影响默认属性值零。默认值在创建属性实例时将绕过验证。

//..Set height to zero
$rectangle->height = 0;

//..Throws an exception like:
//  Behavior validation failure in closure: RectangleProperties in file test.php on line 71

如果我们想使矩形表现得像一个正方形怎么办?由于正方形是矩形的特化,我们可以创建一个行为策略,该策略将用于强制执行高度必须等于宽度的规则。当高度被设置时,宽度将自动设置为高度值,反之亦然。

首先,我们想要为RectangleProperties创建一个接口。这将定义两个方法getHeight()和getWidth(),它们将返回高度和宽度的模型属性名称。这个接口将确保只使用矩形与行为策略,并且这也是将属性名称与数据库列名称解耦的好方法。

/**
 * This interface defines a property configuration object for a Rectangle.
 */
interface IRectangleProperties extends \buffalokiwi\magicgraph\property\IPropertyConfig
{
  /**
   * Get the height property name 
   * @return string
   */
  public function getHeight() : string;
  
  
  /**
   * Get the width property name 
   * @return string
   */
  public function getWidth() : string;
}

我们的修改后的RectangleProperties类现在看起来像这样

/**
 * Property configuration for a rectangle value object
 */
class RectangleProperties extends buffalokiwi\magicgraph\property\BasePropertyConfig implements IRectangleProperties
{
  /**
   * Height property name in the database
   */
  const HEIGHT = 'height';
  
  /**
   * Width property name in the database 
   */
  const WIDTH = 'width';
  
  
  /**
   * Get the height property name 
   * @return string
   */
  public function getHeight() : string
  {
    return self::HEIGHT;
  }
  
  
  /**
   * Get the width property name 
   * @return string
   */
  public function getWidth() : string
  {
    return self::WIDTH;
  }
  
  
  /**
   * Returns the property configuration array 
   * @return array 
   */
  protected function createConfig() : array
  {
    //..Zero is no longer allowed
    $vInt = fn( buffalokiwi\magicgraph\property\IProperty $prop, int $value ) : bool => !empty( $value );
    
    return [
      self::HEIGHT => self::FINTEGER_REQUIRED + [self::VALIDATE => $vInt],
      self::WIDTH => self::FINTEGER_REQUIRED + [self::VALIDATE => $vInt]
    ];
  }
}

现在属性配置已经正确配置,我们可以创建一个行为策略,它将使任何使用IRectangleProperties的模型中的高度始终等于宽度。为此,我们将创建一个名为BehaveAsSquare的类,它从GenericNamedPropertyBehavior派生。GenericNamedPropertyBehavior通常用于将行为附加到单个属性。

对于我们的正方形行为,我们想使用模型设置回调(在调用IModel::setValue()设置属性值时调用)。这意味着我们需要将static::class传递给GenericNamedPropertyBehavior构造函数。当属性名与类名相等时,该行为将应用于模型中的所有属性。这将使我们能够为多个属性编写单个处理器。

/**
 * Causes rectangles to behave as squares.
 * This uses the model setter callback to force height and width to always be equal.
 */
class BehaveAsSquare extends buffalokiwi\magicgraph\property\GenericNamedPropertyBehavior
{
  /**
   * Model setter callback 
   * @var \Closure
   */
  private \Closure $mSetter;
  
  
  public function __construct()
  {
    parent::__construct( static::class );
    $this->mSetter = $this->createModelSetterCallback();
  }
  
  
  /**
   * Return the model setter callback
   * @return \Closure|null
   */
  public function getModelSetterCallback(): ?\Closure
  {
    return $this->mSetter;
  }
  
  
  /**
   * Creates the model setter callback.  
   * No need to create this every time the setter is called.
   * @return \Closure
   */
  private function createModelSetterCallback() : \Closure 
  {
    //..This setter is a circular reference, so we want to know if we're already in the closure
    $inClosure = false;
    
    return function( 
      \buffalokiwi\magicgraph\IModel $model, 
      \buffalokiwi\magicgraph\property\IProperty $prop, 
      $value ) use(&$inClosure) : mixed 
    { 
      //..Return if already in closure 
      if ( $inClosure )
        return $value;      
      
      //..Set the state
      $inClosure = true;
      
      //..Get the rectangle property config 
      //..This will throw an exception if rectangleproperties are not used in the model.
      /* @var $props IRectangleProperties */
      $props = $model->getPropertyConfig( IRectangleProperties::class );
      
      //..Set the other dimension 
      switch( $prop->getName())
      {
        case $props->getHeight():
          $model->setValue( $props->getWidth(), $value );
        break;

        case $props->getWidth():
          $model->setValue( $props->getHeight(), $value );
        break;
      }

      try {
        return $value;
      } finally {
        //..Reset state
        $inClosure = false;
      }
    };    
  }
}

为了使矩形表现得像正方形,我们可以这样初始化它:

//..Create the rectangle model instance and make it a square 
$rectangle = new Rectangle( new RectangleProperties( new BehaveAsSquare()));

//..Set one dimension
$rectangle->height = 10;

/**
 * Outputs:
 * array (size=2)
 *   'height' => int 10
 *   'width' => int 10
 */
var_dump( $rectangle->toArray());

我们都知道矩形和正方形是不同的东西,因此我们可以使用相同的属性配置和新的行为策略来创建两个新的模型:矩形和正方形。

/**
 * Rectangle Value Object
 */
class Rectangle extends buffalokiwi\magicgraph\GenericModel 
{
  private IRectangleProperties $props;
         
  public function __construct( \buffalokiwi\magicgraph\property\IPropertyConfig ...$config )
  {
    parent::__construct( ...$config );
    //..Here we ensure that the model is actually a rectangle, and we get the property names.
    $this->props = $this->getPropertyConfig( IRectangleProperties::class );
  }
  

  /**
   * Sets the rectangle dimensions 
   * @param int $height Height 
   * @param int $width Width 
   * @return void
   */      
  public function setDimensions( int $height, int $width ) : void
  {
    $this->setValue( $this->props->getHeight(), $height );
    $this->setValue( $this->props->getWidth(), $width );
  }
  
  
  /**
   * Gets the height 
   * @return int
   */
  public function getHeight() : int
  {
    return $this->getValue( $this->props->getHeight());
  }
  
  
  /**
   * Gets the width 
   * @return int
   */
  public function getWidth() : int
  {
    return $this->getValue( $this->props->getWidth());
  }
}


/**
 * Square value object
 * Height and width are always equal 
 */
class Square extends buffalokiwi\magicgraph\GenericModel 
{
  private IRectangleProperties $props;
         
  public function __construct( \buffalokiwi\magicgraph\property\IPropertyConfig ...$config )
  {
    parent::__construct( ...$config );
    //..Here we ensure that the model is actually a rectangle, and we get the property names.
    $this->props = $this->getPropertyConfig( IRectangleProperties::class );
  }
  

  /**
   * Sets the rectangle dimensions 
   * @param int $height Height 
   * @param int $width Width 
   * @return void
   */      
  public function setDimension( int $heightAndWidth ) : void
  {
    //..Our BehaveAsSquare will handle this 
    //..We could have just as easily set both properties here, but this is an example of how strategies work.
    $this->setValue( $this->props->getHeight(), $heightAndWidth );
  }
  
  
  /**
   * Gets the height 
   * @return int
   */
  public function getHeight() : int
  {
    return $this->getValue( $this->props->getHeight());
  }  
}

最后,我们可以创建一个正方形的实例:

$square = new Square( new RectangleProperties( new BehaveAsSquare()));

$square->setDimension( 10 );

/**
 * Outputs:
 * array (size=2)
 *   'height' => int 10
 *   'width' => int 10
 */
var_dump( $square->toArray());

使用多个属性配置

可以使用多个IPropertyConfig对象来创建单个模型。这是属性配置对象的一个非常有用的特性。一个包可以定义一个模型,而其他包可以通过添加属性和行为来扩展该模型。每个属性配置还可以结合任何内联事件处理器和零个或多个行为策略。这可以看作是一种插件系统。以下是一个示例:

首先,我们创建两个属性配置:

class FooProps extends buffalokiwi\magicgraph\property\BasePropertyConfig
{
  protected function createConfig(): array
  {
    return [
      'foo' => self::FSTRING
    ];
  }
}


class BarProps extends buffalokiwi\magicgraph\property\BasePropertyConfig
{
  protected function createConfig(): array
  {
    return [
      'bar' => self::FSTRING
    ];
  }  
}

现在我们可以将每个配置对象的实例传递给模型(或属性集)构造函数。

//..Create the model instance with both property configuration objects 
$model = new buffalokiwi\magicgraph\GenericModel( new FooProps(), new BarProps());


/**
 * Outputs:
 * array (size=2)
 *   'foo' => string '' (length=0)
 *   'bar' => string '' (length=0)
 */
var_dump( $model->toArray());

模型中会出现来自两个配置的属性。

我们还可以在运行时添加属性。这里是将添加到模型中的第三个配置:

class BazProps extends buffalokiwi\magicgraph\property\BasePropertyConfig
{
  protected function createConfig(): array
  {
    return [
      'baz' => self::FSTRING
    ];
  }  
}

在运行时添加配置对象是通过属性集完成的:

$model->getPropertySet()->addPropertyConfig( new BazProps());

/**
 * Outputs:
 * array (size=2)
 *   'foo' => string '' (length=0)
 *   'bar' => string '' (length=0)
 *   'baz' => string '' (length=0)
 */
var_dump( $model->toArray());

模型接口

在Magic Graph中的所有模型都必须实现IModel接口。该接口本身相当简单直接。

IModel关注几个关键领域,属性、验证、状态、序列化和克隆

  1. 属性
    1. instanceOf() - 测试IPropertyConfig实例是否是或实现了提供的类或接口名称。这用于测试模型是否“属于某种类型”。
    2. getPropertySet() - 获取包含模型中使用的属性的内部IPropertySet实例
    3. getPropertyNameSet() - 获取一个包含属性集中属性名称列表的IBigSet实例。这用于需要模型属性名称的方法。例如:toArray()可以通过提供包含每个所需属性活动位的IBigSet实例来返回有限的属性列表。
    4. getPropertyNameSetByFlags() - 与getPropertyNameSet()相同,并包括通过启用的属性标志进行过滤的能力。
    5. getPropertyConfig() - 获取一个包含用于创建模型属性的属性配置的数组
    6. getIterator() - 获取一个用于迭代模型中包含的非数组和模型属性及其值的迭代器
  2. 验证
    1. validate() - 逐个验证每个属性,第一个测试为无效的属性将抛出ValidationException
    2. validateAll() - 验证模型中的每个属性,并将结果存储在列表中。验证失败的属性作为映射返回。
  3. 状态
    1. getModifiedProperties() - 获取一个包含任何编辑过的属性位的IBigSet实例
    2. getInsertProperties() - 获取一个包含数据库“插入”所需的属性位的IBigSet实例
    3. hasEdits() - 测试自初始化以来是否有任何属性被编辑
  4. 序列化
    1. toArray() - 用于持久化、调试和其他有趣的事情。将IModel实例转换为多维数组。
    2. toObject() - 用于JSON序列化,将IModel转换为对象图。
    3. fromArray() - 用于从持久化层初始化模型。用提供的值填充任何匹配的IModel属性。
    4. jsonSerialize() - 通常调用toObject()。
  5. 克隆
    1. __clone() - IModel 实例是可克隆的。
    2. createCopy() - 相比于 __clone,推荐使用此方法,它可以用于克隆或复制(不包括主键)模型,并使它们成为只读。

模型实现

模型由几个组件组成,一个属性集包含所有各种对象属性实例和模型实现。目前,每个属性集扩展了 DefaultPropertySet,每个模型扩展了 DefaultModel

值得注意的是,DefaultModel 包含了很多功能。不建议直接实现 IModel,而是推荐从 DefaultModel 扩展所有模型。

截至写作时,Magic Graph 包含 7 个 IModel 实现和两个装饰器

  1. DefaultModel - 基础模型。每个模型都应该从这个类扩展。
    1. ServiceableModel - 扩展 DefaultModel 并添加支持关系提供者所需的功能。
    2. AnnotatedModel - 可以使用 php8 中的属性来配置和初始化模型属性。
    3. GenericModel - 使用 IPropertyConfig 快速创建模型的一种方式。
    4. QuickModel - 使用除了属性配置数组之外没有任何内容的快速创建模型的一种方式。
    5. QuickServiceableModel
    6. ProxyModel - 用于装饰 IModel 实例
      1. ReadOnlyModelWrapper - 一个用于 IModel 的装饰器,用于禁用设置属性值
      2. ServiceableModelWrapper - 一个用于 IModel 的装饰器,可以向模型实例添加关系提供者。

快速和通用模型变体更容易实例化,但使用这些模型会阻止您选择属性集、配置映射器和属性工厂。内部,快速和通用模型都使用 DefaultPropertySet、DefaultConfigMapper 和 PropertyFactory 的实例。

行为策略

这已经在 属性行为 部分中详细说明,但由于这可能是在 Magic Graph 中最重要的主题,我们将再次讨论。

行为策略的目标如下

  1. 减少模型的复杂性
  2. 增加编写测试的便利性
  3. 在不扩展或修改模型的情况下引入或替换功能

我们都见过试图做所有事的模型。代码混乱,代码难闻。像将第三方包的代码拼接到模型中,忽略关注点分离,或引用模型应该一无所知的对象等问题。这些问题有许多解决方案,但大多数时候我看到开发者编写一个用于连接几个包的服务。这很好,但仍然紧密耦合了包并增加了复杂性。如果使用存储库,并且在存储库之上有一个单独的服务,开发者应该使用哪个来保存模型?如果编写了不知道服务的代码会发生什么?就会出现混乱。

行为策略试图简化包之间的交互。将策略想象成一个适配器。我们可以编写一个带测试的程序,然后将程序附加到模型上。然后模型和/或存储库将触发事件,策略程序将使用这些事件来更改模型的状态和/或引入副作用。

在这个上下文中,副作用可能不是一件坏事。例如,假设我们有一个电子商务平台,当我们想要在订单打包并准备好发货时生成发货标签。我们可以编写一个策略,该策略监视订单状态,知道如何与某些发货API交互,并在订单状态变为“准备发货”时生成发货标签。这个策略在组合根对象创建期间简单地附加到存储库和模型上。现在我们有一个可独立测试的程序,该程序为订单模型添加了对发货API的支持,而无需修改订单模型、存储库或创建服务层。

行为策略程序基本上是IProperty、IModel和IRepository触发的各种事件的处理器。目前有几个行为接口。

IPropertyBehavior 主要用于属性配置数组,并包含与单个属性相关的几个回调。所有回调都将包含一个IProperty参数,即触发回调的属性。

验证回调

验证回调在调用IProperty::validate()时被调用。

function getValidationCallback() : ?Closure
{
  /**
   * Validate some property value 
   * @param buffalokiwi\magicgraph\property\IProperty $prop Property being validated
   * @param mixed $value Value to validate
   * @return bool is valid
   */
  return function( IProperty $prop, mixed $value ) : bool {
    //..Validate $value 
    return false; //..Not valid, throws an exception 
  };
}

设置器回调

设置器回调在调用IProperty::validate()之前被调用。这个回调的目的是在将其写入后端属性对象之前修改值。将其视为序列化属性值。

function getSetterCallback() : ?Closure
{
  /**
   * Modify a property value prior to being written to the backing property
   * @param buffalokiwi\magicgraph\property\IProperty $prop Property being set 
   * @param mixed $value Value to set 
   * @return mixed modified value 
   */  
  return function( buffalokiwi\magicgraph\property\IProperty $prop, mixed $value ) : mixed {
    //..Ensure that any incoming value is a string, then append 'bar'
    return (string)$value . 'bar';
  };
}

获取器回调

获取器回调在从IProperty::getValue()返回值之前被调用。这是为了在使用它之前修改后端属性对象中存储的值。将其视为反序列化属性值。注意获取器回调中的$context参数。IModel::setValue()包含一个上下文参数,可以在获取器回调中用来设置一些任意上下文/元数据/状态等。

function getGetterCallback() : ?Closure
{
  /**
   * Modify a property value prior to being written to the backing property
   * @param buffalokiwi\magicgraph\property\IProperty $prop Property being set 
   * @param mixed $value Value to set 
   * @param array $context The context 
   * @return mixed modified value 
   */  
  return function( buffalokiwi\magicgraph\property\IProperty $prop, mixed $value, array $context ) : mixed {
    //..Ensure that any incoming value is a string, then append 'bar'
    return (string)$value . 'bar';
  };
}

初始化回调

初始化回调用于修改在将其写入后端对象之前默认值。当调用IProperty::reset()时,将调用此回调。由于此回调不会通过IProperty::validate()运行,因此请小心处理默认值。

function getInitCallback() : ?Closure
{
  /**
   * Modify the default value 
   * @param mixed $value The default value 
   * @return mixed default value 
   */
  return function ( mixed $value ) : mixed {
    return $value;
  };
}

空值回调

空值回调在empty()返回false的情况下很有用,但无论值是什么,都应该被视为空。例如,如果属性是一个表示原语的对象,那么即使对象内部的实际值是空的,empty()也会返回false。

function getIsEmptyCallback() : ?Closure
{
  /**
   * Basic empty check that returns true if the value is empty or the value is equal to the default property value.
   * @param buffalokiwi\magicgraph\property\IProperty $prop Property being tested
   * @param mixed $value The value to test
   * @param mixed $defaultValue The default value for the property. 
   * @return bool is empty 
   */
  return function ( buffalokiwi\magicgraph\property\IProperty $prop, mixed $value, mixed $defaultValue ) : bool {
    return empty( $value ) || $value === $defaultValue;
  };
}

更改回调

当属性值更改时,将触发此回调。值得注意的是,这发生在属性级别,而不是任何模型内部。因此,此事件将无法访问模型中的其他属性。由于此限制,此回调可能用途有限。如果您需要访问模型中的其他属性,请使用模型级别的获取器/设置器回调。

function getOnChangeCallback() : ?Closure
{
  /**
   * @param buffalokiwi\magicgraph\property\IProperty $prop The property being changed
   * @param mixed $oldValue The value prior to the change
   * @param mixed $newValue The value after the change
   */
  return function ( buffalokiwi\magicgraph\property\IProperty $prop, mixed $oldValue, mixed $newValue ) : void {
    //..Do something interesting 
  };
}

HTML属性包回调

HTML输入回调

作为对Magic Graph的一个小乐趣,所有IProperty实例都可以转换为它们的HTML等价物。当使用IElementFactory生成HTML输入时,此回调将用于覆盖元素工厂生成的默认HTML。我们将在[创建HTML元素](#creating-html-elements)章节中进一步介绍此内容。

function getHTMLInputCallback() : ?Closure
{
  /**
   * Convert IProperty to IElement for HTML output
   * @param \buffalokiwi\magicgraph\IModel $model Model property belongs to
   * @param buffalokiwi\magicgraph\property\IProperty $prop Property to convert
   * @param string $name HTML element name attribute value
   * @param string $id HTML element id attribute value 
   * @param mixed $value Property value
   * @return \buffalokiwi\magicgraph\property\htmlproperty\IElement The HTML element 
   */
  return function (
    \buffalokiwi\magicgraph\IModel $model,
    \buffalokiwi\magicgraph\property\IProperty $prop,
    string $name,
    string $id,
    mixed $value ) : \buffalokiwi\magicgraph\property\htmlproperty\IElement {
    return new buffalokiwi\magicgraph\property\htmlproperty\TextAreaElement( $name, $id, $value );
  };
}

模型级别回调

以下回调由IModel实现调用。

转换为数组回调

IModel::toArray()用于持久化和序列化。当将属性值转换为持久化状态时,将调用toArray回调。

function getToArrayCallback() : ?Closure
{
  /**
   * @param \buffalokiwi\magicgraph\IModel $model Model being converted to an array
   * @param buffalokiwi\magicgraph\property\IProperty $prop Property the value belongs to
   * @param mixed $value Value to modify 
   * @return mixed modified value 
   */
  return function( 
    \buffalokiwi\magicgraph\IModel $model, 
    buffalokiwi\magicgraph\property\IProperty $prop, 
    mixed $value ) : mixed {
    //..Return the modified value 
    return $value;
  };
}

模型设置器回调

模型设置器回调与属性设置器回调相同,不同之处在于它增加了对模型的访问,并且由DefaultModel而不是AbstractProperty调用。

function getModelSetterCallback() : ?Closure
{
  /**
   * @param \buffalokiwi\magicgraph\IModel $model The model the property belongs to
   * @param buffalokiwi\magicgraph\property\IProperty $prop The property being set 
   * @param mixed $value The value being written
   * @return mixed The modified value to write to the backing property
   */
  return function( 
    \buffalokiwi\magicgraph\IModel $model, 
    \buffalokiwi\magicgraph\property\IProperty $prop, 
    mixed $value ) : mixed {
    //..Return modified value 
    return $value;
  };
}

模型获取器回调

模型获取器回调与属性获取器回调相同,但增加了对模型的访问,并且由DefaultModel调用,而不是由AbstractProperty调用。

function getModelGetterCallback() : ?Closure
{
  /**
   * @param \buffalokiwi\magicgraph\IModel $model The model the property belongs to
   * @param buffalokiwi\magicgraph\property\IProperty $prop The property being retrieved
   * @param mixed $value The value being retrieved
   * @return mixed The modified value to retrieve
   */
  return function( 
    \buffalokiwi\magicgraph\IModel $model, 
    \buffalokiwi\magicgraph\property\IProperty $prop, 
    mixed $value ) : mixed {
    //..Return modified value 
    return $value;
  };
}

命名属性行为

INamedPropertyBehavior 接口扩展了 IPropertyBehavior,并添加了额外的模型级回调。

以下回调由 ISaveableMappingObjectFactory 实现。

模型验证回调

当调用 IModel::validate() 时,将调用此回调,这是一个验证模型状态的机会。任何验证错误都必须抛出 ValidationException

function getModelValidationCallback() : ?Closure 
{
  /**
   * @param \buffalokiwi\magicgraph\IModel $model The model to validate 
   */
  return function( \buffalokiwi\magicgraph\IModel $model ) : void {
    if ( !$valid )
      throw new \buffalokiwi\magicgraph\ValidationException( 'Model is invalid' );
  };
}

保存前回调

听起来就是这样。当模型被某个 ISaveableMappingObjectFactory 实现保存时,在模型持久化之前调用保存之前的方法。
注意:在默认仓库实现中,保存是事务的一部分,任何抛出的异常都将触发回滚。

function getBeforeSaveCallback() : ?Closure
{
  /**
   * @param \buffalokiwi\magicgraph\IModel $model The model to save
   */
  return function( \buffalokiwi\magicgraph\IModel $model ) : void {
    //..Do something with the model before it's saved
  };
}

保存后回调

这与保存前相同,但发生在模型保存之后。

function getAfterSaveCallback() : ?Closure
{
  /**
   * @param \buffalokiwi\magicgraph\IModel $model The model to save
   */
  return function( \buffalokiwi\magicgraph\IModel $model ) : void {
    //..Do something with the model after it's saved
  };
}

实现 INamedPropertyBehavior 的几种方法

  1. 扩展 buffalokiwi\magicgraph\property\GenericNamedPropertyBehavior
  2. 使用 NamedPropertyBehaviorBuilder 创建匿名策略
  3. 编写自己的实现

扩展 GenericNamedPropertyBehavior 是创建行为策略的首选方法。默认情况下,每个回调都会返回 null。在某个子类中重写任何方法并返回回调闭包。以下示例中,我们将创建一个名为 Test 的模型,并具有属性名称。我们将创建一个策略,当名称设置为 "foo" 时,将名称设置为 "bar",如果名称设置为 "baz",将抛出异常。

/**
 * Property definition for TestModel 
 */
class TestProperties extends buffalokiwi\magicgraph\property\BasePropertyConfig
{
  const NAME = 'name';
  
  public function getName() : string
  {
    return self::NAME;
  }
  
  protected function createConfig() : array
  {
    return [
      self::NAME => self::FSTRING
    ];
  }
}


/**
 * Test model
 */
class TestModel extends \buffalokiwi\magicgraph\GenericModel 
{
  /**
   * Property definitions 
   * @var TestProperties
   */
  private TestProperties $props;
  
  
  public function __construct( \buffalokiwi\magicgraph\property\IPropertyConfig ...$config )
  {
    parent::__construct( ...$config );
    $this->props = $this->getPropertyConfig( TestProperties::class );
  }
  
    
  public function getName() : string
  {
    return $this->getValue( $this->props->getName());
  }
  
  
  public function setName( string $name ) : void
  {
    $this->setValue( $this->props->getName(), $name );
  }
}


/**
 * If the name property equals "foo", it is set to "bar".
 * If theh name property equals "baz", a ValidationException is thrown 
 */
class TestModelBehavior extends buffalokiwi\magicgraph\property\GenericNamedPropertyBehavior
{
  public function getValidateCallback(): ?\Closure
  {
    return function( buffalokiwi\magicgraph\property\IProperty $prop, string $name ) : bool {
      //..If $name equals baz, then an exception is thrown
      return $name != 'baz';        
    };
  }
  
  
  public function getSetterCallback(): ?\Closure
  {
    return function( buffalokiwi\magicgraph\property\IProperty $prop, string $name ) : string {
      //..Returns bar if name equals foo.
      return ( $name == 'foo' ) ? 'bar' : $name;
    };
  }
}



//..Create an instance of test model with the test behavior. 
//..The behavior is wired to the name property.
$model = new TestModel( new TestProperties( new TestModelBehavior( TestProperties::NAME )));

//..Set the name 
$model->setName( 'The name' );

/**
 * Outputs:
 * array (size=1)
 *   'name' => string 'The name' (length=8)
 */
var_dump( $model->toArray());


//..Set the name to "foo"
$model->setName( 'foo' );

/**
 * Outputs:
 * array (size=1)
 *   'name' => string 'bar' (length=3)
 */
var_dump( $model->toArray());

//..Set to baz and an exception will be thrown 
//..Throws: "baz" of type "buffalokiwi\magicgraph\property\StringProperty" is not a valid value for the "name" property.  
//  Check any behavior callbacks, and ensure that the property is set to the correct type.  IPropertyBehavior::getValidateCallback() failed.
//..This will also generate an error "Behavior validation failure in closure: TestProperties in file XXX"
$model->setName( 'baz' );

当使用任何行为回调时,您可以将 IModel 和混合类型替换为任何派生类型。还值得注意的是,如果您想要行为与所有属性一起工作,您可以将 static::class 作为属性名称传递给 GenericNamedPropertyBehavior 的 PropertyBehavior 构造函数。这将仅适用于模型级回调,并且当策略类名与提供的属性名称匹配时,该策略应用于模型中的每个属性。

数据库连接

Magic Graph 在 PHP PDO 库 上提供了一个简单的抽象。首先,让我们概述四个接口,然后我们将介绍 MySQL 实现。

IConnectionProperties

连接属性接口用于定义连接到某些数据库引擎的准则。您会发现一些非常复杂的方法,如 getHost() 和 getDSN()。这里真正令人叹为观止的东西。它拥有数据库连接属性包中您期望的一切。

IConnectionFactory

连接工厂正是其名称所暗示的。此接口可能在不久的将来进行修订。概念是拥有一个创建数据库连接的工厂。在其当前形式中,最好是一个工厂为一种持久性类型提供连接。在未来,此接口将进行修订,以更容易地支持单个工厂中的多个持久性类型。注意:此接口支持单个工厂中的多个类型,但使用起来并不容易。目前,请保持一对一,它运行良好。

IDBConnection

这是一个用于 PHP 中的 PDO 对象的接口,但有一个额外的方法。

executeQuery( string $statement ) : Generator

executeQuery() 是执行不带参数的简单语句的一种简单方法。

IPDOConnection

IPDOConnection 扩展了 IDBConnection 并添加了几个方法,以简化常见查询类型的操作。让我们看看。

删除

删除方法用于删除行。此方法只能通过主键删除,并且支持组合主键。

/**
 * Execute a delete query for a record using a compound key.
 * @param string $table table name
 * @param array $pkPairs primary key to value pairs 
 * @param int $limit limit
 * @return int affected rows
 * @throws InvalidArgumentExcepton if table or col or id are empty or if col
 * contains invalid characters or if limit is not an integer or is less than
 * one
 * @throws DBException if there is a problem executing the query
 */
function delete( string $table, array $pkCols, int $limit = 1 ) : int;

//..Example:

$affectedRows = delete( 'mytable', ['pkcol1' => 'value1', 'pkcol2' => 'value2'], 1 );

//..Generates the statement:
// delete from mytable where pkcol1=? and pkcol2=? limit 1;  

更新

更新会更新某个表中匹配的行。这也通过主键匹配,并且支持组合键。

/**
 * Build an update query using a prepared statement.
 * @param string $table Table name
 * @param array $pkPairs list of [primary key => value] for locating records to update.
 * @param array $pairs Column names and values map
 * @param int $limit Limit to this number
 * @return int the number of affected rows
 * @throws InvalidArgumentException
 * @throws DBException
 */
function update( string $table, array $pkPairs, array $pairs, int $limit = 1 ) : int;

//..Example

$affectedRows = update( 'mytable', ['id' => 1], ['name' => 'foo', 'md5name:md5' => 'foo'], 1 );

//..Generates the statement:
// update mytable set name=?, md5name=md5(?) where id=? limit 1;

可以通过在任意列名后附加 ':func' 来向列添加函数。可以通过这种方式链接多个函数:':func1:func2'

插入

插入与更新类似,但它是插入新记录!哇哦!

/**
 * Build an insert query using a prepared statement.
 * This will work for most queries, but if you need to do something
 * super complicated, write your own sql...
 *
 *
 * @param string $table Table name
 * @param array $pairs Column names and values map
 * @return int last insert id for updates
 * @throws InvalidArgumentException
 * @throws DBException
 */
function insert( string $table, array $pairs ) : string;    

//..Example:
$lastInsertId = insert( 'mytable', ['col1' => 'value1', 'col2:md5' => 'value2'] );

//..generates statement:
// insert into mytable (col1, col2) values(?,md5(?)); 

游标

你是否曾想过遍历某个表中的每一行?你有好运了!

/**
 * Creates a cursor over some result set 
 * @param string $statement Statement 
 * @param type $options Parameters
 * @param type $scroll Enable Scroll 
 * @return Generator Results 
 */
function forwardCursor( string $statement, $options = null, $scroll = false ) : Generator;  

//..Use it like this:

foreach( forwardCursor( 'select * from mytable where col=?', ['foo'] ) as $row )
{
  //..Do something with $row
  //..$row is an associative array containing column names and values.
}

注意:$scroll 参数已被弃用,将在未来的版本中删除。滚动原本允许双向游标移动,但并非所有驱动程序都支持可滚动游标(mysql 不支持)因此不应在通用接口中包含 $scroll。

选择

惊喜!我们也可以选择!将你的语句和绑定传递给选择方法,然后 BAM!你得到结果。

/**
 * Select some stuff from some database
 * @param string $statement sql statement
 * @param type $opt Bindings for prepared statement.  This can be an object or an array 
 */ 
function select( string $statement, $opt = null ) : \Generator;  

//..Use like this:

foreach( select( 'select * from mytable where col=?', ['foo'] ) as $row )
{
  //..Do something with $row
  //..$row is an associative array containing column names and values.
}

//..Generates the statement:
//  select * from mytable where col=?

选择多个结果集

返回多个结果集的查询也完全受支持。这可能是一个返回多个结果集的存储过程或简单地在语句之间添加分号。小心使用此功能。分号可能会做些坏事。

/**
 * Execute a sql statement that has multiple result sets
 * ie: a stored procedure that has multiple selects, or one of those snazzy
 * subquery statements
 * @param string $sql SQL statement to execute
 * @param array $bindings Column bindings 
 * @return Generator array results
 * @throws DBException if there is one
 */
public function multiSelect( string $sql, array $bindings = [] ) : Generator
//..Example:

foreach( multiSelect( 'select * from mytable where id=?; select * from mytable where id=?', [1,2] ) as $rowSet )
{
  //..Each $rowSet entry contains a set of rows to iterate over.
  foreach( $rowSet as $row )
  {
    //..$row is an associative array of column => value 
  }
}

执行

执行一些不带结果集的任意语句。

/**
 * Executes a query with no result set.
 * @param string $statement Statement to execute 
 * @param array $opt Map of bindings 
 * @return int
 */
function execute( string $statement, $opt = null ) : int;

MySQL PDO

Magic Graph 目前包含一个用于 MySQL 的数据库适配器,MariaDBConnection,它扩展了抽象基类 PDOConnection,实现了 IPDOConnection,并添加了必要的驱动程序特定的 sql 语句。这是用于所有 MySQL/MariaDB 事务的 PDO 实现。

连接工厂

连接工厂为使用某些驱动程序生成数据库连接。我敢肯定你已经看到了整个 readme 中的示例,但以防万一,这里有一个

$dbFactory = new buffalokiwi\magicgraph\pdo\PDOConnectionFactory( //..A factory for managing and sharing connection instances 
  new buffalokiwi\magicgraph\pdo\MariaConnectionProperties(       //..Connection properties for MariaDB / MySQL
    'localhost',                  //..Database server host name 
    'root',                       //..User name
    '',                           //..Password
    'fancydatabase' ),            //..Database 
  //..This is the factory method, which is used to create database connection instances
  //..The above-defined connection arguments are passed to the closure.
  function( buffalokiwi\magicgraph\pdo\IConnectionProperties $args  ) { 
    //..Return a MariaDB connection 
    return new buffalokiwi\magicgraph\pdo\MariaDBConnection( $args );
  }
);

想法是使用一些连接属性创建一个工厂,并让这个通用工厂返回正确类型的 PDO 实现。这里没有什么是突破性的。

处理货币

货币是那种不一定总是正常工作的事物。有许多解决货币问题的方法(我们在这里不会讨论),幸运的是,我们有这个很棒的库 MoneyPHP,它基于 Martin Fowler 的货币模式,使用字符串表示货币,最好的部分是货币对象是不可变的。

MoneyPHP 的一个缺点是它没有任何类型的接口。它只是 Money 对象。在 Magic Graph 中,有一个 Money 接口 IMoney,由 MoneyProxy 实现,它接受一个 Money 实例并代理所有调用到底层 Money 对象。这是因为在某个时刻我们可能想用其他库替换 MoneyPHP,而不进行适当的抽象是无法做到的。

由于MoneyPHP处理不同的货币和格式,我们需要一个货币工厂,以便轻松地生成相同货币的货币实例。在Magic Graph中,我们有一个工厂MoneyFactory,它实现了IMoneyFactory

以下是如何设置美元货币工厂的示例

$currencies = new \Money\Currencies\ISOCurrencies();

//..Money formatter 
$intlFmt = new Money\Formatter\IntlMoneyFormatter( 
  new \NumberFormatter( 'en_US', \NumberFormatter::CURRENCY ), 
  $currencies 
);

$decFmt = new Money\Formatter\DecimalMoneyFormatter( $currencies );

//..Money factory 
//..This is used to lock the system down to a certain type of currency, 
// and to provide an abstract wrapper for the underlying money implementation.
$dollarFactory = new \buffalokiwi\magicgraph\money\MoneyFactory( 
  function( string $amount ) use($intlFmt,$decFmt) : buffalokiwi\magicgraph\money\IMoney {
    return new buffalokiwi\magicgraph\money\MoneyProxy( 
      Money::USD( $amount ), 
      $intlFmt, 
      $decFmt );
  }
);   

现在我们可以创建货币并格式化为配置的货币

$treeFiddy = $dollarFactory->getMoney( '3.50' );

/**
 * Outputs:
 * object(buffalokiwi\magicgraph\money\MoneyProxy)[600]
 *  private 'money' => 
 *    object(Money\Money)[601]
 *      private 'amount' => string '350' (length=3)
 *      private 'currency' => 
 *        object(Money\Currency)[602]
 *          private 'code' => string 'USD' (length=3)
 *  private 'formatter' => 
 *    object(Money\Formatter\IntlMoneyFormatter)[595]
 *      private 'formatter' => 
 *        object(NumberFormatter)[596]
 *      private 'currencies' => 
 *        object(Money\Currencies\ISOCurrencies)[594]
 *  private 'decFmt' => 
 *    object(Money\Formatter\DecimalMoneyFormatter)[597]
 *      private 'currencies' => 
 *        object(Money\Currencies\ISOCurrencies)[594]
 */
var_dump( $treeFiddy );

//..Outputs: 3.50
echo (string)$treeFiddy;

//..Outputs: $3.50
echo $treeFiddy->getFormattedAmount();

在Magic Graph中使用货币属性很容易。只需在属性配置数组中使用'money'属性类型。

注意:由于使用了MoneyFactory,配置映射器需要传递服务定位器。这将允许配置映射器在创建属性对象时找到货币工厂(以及其他内容)。有关更多信息,请参阅Magic Graph设置。以下是快速示例

//..Service locator 
$ioc = new buffalokiwi\buffalotools\ioc\IOC();

//..Default Magic Graph Configuration Mapper. 
//..This creates the property objects.
$configMapper = new buffalokiwi\magicgraph\property\DefaultConfigMapper( $ioc );

//..Factory wraps the config mapper and can combine config arrays.  
//  Uses the config mapper to produce properties.
$propertyFactory = new \buffalokiwi\magicgraph\property\PropertyFactory( $configMapper );

//..Use $propertyFactory to create instances of IPropertySet

创建HTML元素

此包不应是Magic Graph的一部分,而应作为单独的扩展发布。然而,该包已经存在,并且已完全集成到属性中,因此它不会消失。

通过使用IElementFactory实现,可以将IProperty实例转换为IElement,最终转换为包含HTML的字符串。

IElementFactory的默认实现是ElementFactory,它接受IElementFactoryComponent列表。IELementFactoryComponent实例用于将IProperty的子类映射到将IProperty实例转换为IElement实例的函数。

有一个名为DefaultComponentMap的默认映射类,可用于快速开始使用ElementFactory。

以下是一个示例

//..Create a simple model with a few properties
$model = new buffalokiwi\magicgraph\QuickModel([
  'numberinput' => ['type' => 'int'],
  'stringinput' => ['type' => 'string'],
  'dateinput' => ['type' => 'date'],
  'boolinput' => ['type' => 'bool'],
  'enuminput' => ['type' => 'rtenum', 'config' => ['test1','test2','test3'], 'value' => 'test1']    
]);


$elementFactory = new buffalokiwi\magicgraph\property\htmlproperty\ElementFactory( ...( new buffalokiwi\magicgraph\property\htmlproperty\DefaultComponentMap())->getMap());

foreach( $model->getPropertySet()->getProperties() as $prop )
{
  echo $elementFactory->createElement( $model, $prop, $prop->getName(), null, (string)$model->getValue( $prop->getName()))->build();
  echo '<br />';
}

上述示例将生成五个HTML输入

<input step="1" type="number" value="0" name="numberinput" id="numberinput" />
<br />
<input type="text" name="stringinput" id="stringinput" />
<br />
<input type="date" name="dateinput" id="dateinput" />
<br />
<label><input type="checkbox" class=" checkbox" name="boolinput" id="boolinput" /><span></span></label>
<br />
<select name="enuminput" id="enuminput" >
  <option value="test1" selected="selected" >Test1</option>
  <option value="test2" >Test2</option>
  <option value="test3" >Test3</option>
</select>
<br />

如果您想为任何自定义属性添加元素工厂组件,或者您想覆盖默认组件,可以将此传递给DefaultComponentMap的构造函数。任何匹配的属性都会在内部覆盖。

例如,虽然这是IStringProperty的默认处理程序,但如果传递给构造函数,则可以覆盖它。

new buffalokiwi\magicgraph\property\htmlproperty\DefaultComponentMap([
  buffalokiwi\magicgraph\property\IStringProperty::class => function( 
    buffalokiwi\magicgraph\property\IStringProperty $prop, 
    string $name, 
    ?string $id, 
    string $value 
  ) : buffalokiwi\magicgraph\property\htmlproperty\IElement {
    $attrs = [];

    if ( $prop->getMin() != -1 )
      $attrs['minlength'] = $prop->getMin();

    if ( $prop->getMax() != -1 )
      $attrs['maxlength'] = $prop->getMax();

    if ( !empty( $prop->getPattern()))
      $attrs['pattern'] = $prop->getPattern();

    if ( $prop->getFlags()->hasVal( \buffalokiwi\magicgraph\property\IPropertyFlags::REQUIRED ))
      $attrs['required'] = 'required';

    if ( $prop->getMax() != -1 && $prop->getMax() > 255 )
      return new buffalokiwi\magicgraph\property\htmlproperty\TextAreaElement( $name, $id, $value, $attrs );
    else
      return new \buffalokiwi\magicgraph\property\htmlproperty\InputElement( 'text', $name, $id ?? '', $value, $attrs );
  }
]);

回调的定义如下

/**
 * Converts IProperty to IElement 
 * @param \buffalokiwi\magicgraph\property\IProperty $prop Property to convert
 * @param string $name property/html form input name 
 * @param string|null $id html element id attribute value 
 * @param string $value Property value as a string 
 * @return buffalokiwi\magicgraph\property\htmlproperty\IElement HTML Element 
 */
function( \buffalokiwi\magicgraph\property\IProperty $prop, string $name, 
  ?string $id, string $value ) : buffalokiwi\magicgraph\property\htmlproperty\IElement;

Magic Graph 设置

Magic Graph被设计为支持组合根模式。想法是每个文件中的“new object”调用都位于单个名为组合根的文件中。虽然Magic Graph对象实例化可能看起来很复杂,但您只需编写一次代码,所有代码最终都会在一个地方。然后,将各种Magic Graph组件的实例作为依赖项注入到其他类中。

以下是magic graph的组合根部分可能的样子。

  1. 创建服务定位器容器。这用于向配置映射器提供各种工厂(如DateFactory和MoneyFactory)。
  2. 创建数据库连接工厂。这将被存储库使用。
  3. 将DateFactory添加到容器中。这用于IDateProperty中。
  4. 将MoneyFactory添加到容器中。这用于IMoneyProperty。
  5. 创建配置映射器。这是一个IConfigMapper的实例,负责根据属性配置数组中列出的类型创建IProperty的实例。
  6. 创建PropertyFactory实例。这使用适当的IConfigMapper创建IPropertySet实例。属性集包含IModel实例使用的属性。
  7. 将 ITransactionFactory 添加到容器中。这将被各种关系提供者和需要跨多个仓库统一保存的实体使用。
  8. 可选地创建 IDBConnection 和 IDBFactory 的局部变量。这可以使编写组合根更加容易,并减少对容器的调用。
/*********************/
/* IoC Container     */
/*********************/

$ioc = new \buffalokiwi\buffalotools\ioc\IOC();


/**********************/
/* Database           */
/**********************/

$ioc->addInterface(buffalokiwi\magicgraph\pdo\IConnectionFactory::class, function() {
  return new \buffalokiwi\magicgraph\pdo\PDOConnectionFactory( 
    new buffalokiwi\magicgraph\pdo\MariaConnectionProperties( 
      'localhost',    //..Host
      'root',         //..User
      '',             //..Pass
      'magicgraph' ), //..Database 
   function(\buffalokiwi\magicgraph\pdo\IConnectionProperties $args  ) {
     return new buffalokiwi\magicgraph\pdo\MariaDBConnection( $args, function(buffalokiwi\magicgraph\pdo\IDBConnection $c ) { $this->closeConnection($c); });
   });                
});


/**********************/
/* Dates              */
/**********************/

$ioc->addInterface( \buffalokiwi\buffalotools\date\IDateFactory::class, function() { 
  return new \buffalokiwi\buffalotools\date\DateFactory();   
});



/*********************/
/* Money Factory     */
/*********************/

$ioc->addInterface( \buffalokiwi\magicgraph\money\IMoneyFactory::class, function() {
  
  $currencies = new Money\Currencies\ISOCurrencies();
  //..Money formatter 
  $intlFmt = new \Money\Formatter\IntlMoneyFormatter( 
    new \NumberFormatter( 'en_US', \NumberFormatter::CURRENCY ), 
    $currencies );

  $decFmt = new \Money\Formatter\DecimalMoneyFormatter( $currencies );

  //..Money factory 
  //..This is used to lock the system down to a certain type of currency, 
  // and to provide an abstract wrapper for the underlying money implementation.
  return new \buffalokiwi\magicgraph\money\MoneyFactory( function( string $amount ) use($intlFmt,$decFmt) : \buffalokiwi\magicgraph\money\IMoney {
    return new \buffalokiwi\magicgraph\money\MoneyProxy( \Money\Money::USD( $amount ), $intlFmt, $decFmt );
  });
});


/*********************/
/* Magic Graph Setup */
/*********************/

//..Converts IPropertyConfig config arrays into properties
//..If creating custom propeties, this must be replaced with a custom implementation.
$configMapper = new buffalokiwi\magicgraph\property\DefaultConfigMapper( $ioc );

//..Factory wraps the config mapper and can combine config arrays.  
//  Uses the config mapper to produce properties.
$propertyFactory = new \buffalokiwi\magicgraph\property\PropertyFactory( $configMapper );

//..The property set factory is required when service providers augment the model configuration.  This is used for 
//  things like the EAV system.
//..The closure is provided with a list of IPropertyConfig instances which are
//  supplied by the various service providers and base config.
$ioc->addInterface( \buffalokiwi\magicgraph\property\IPropertySetFactory::class, function() use ($propertyFactory) {
  return new \buffalokiwi\magicgraph\property\PropertySetFactory(
    $propertyFactory, 
    function(\buffalokiwi\magicgraph\property\IPropertyFactory $factory, \buffalokiwi\magicgraph\property\IPropertyConfig ...$config ) {
      return new DefaultPropertySet( $factory, ...$config );
  });
});

//..Transaction factory is used to handle saving multiple things at one time
$ioc->addInterface( \buffalokiwi\magicgraph\persist\ITransactionFactory::class, function() {
  return new \buffalokiwi\magicgraph\persist\DefaultTransactionFactory();
});

//..I like to set up a few shared variables to use in composition root for the database factory and default connection.
//..Database connection factory 
$db = $ioc->getInstance( \buffalokiwi\magicgraph\pdo\IConnectionFactory::class );    
/* @var $db \buffalokiwi\magicgraph\pdo\IConnectionFactory */

//..Default shared db connection 
$dbc = $db->getConnection();
/* @var $dbc \buffalokiwi\magicgraph\pdo\IDBConnection */

一旦 Magic Graph 初始化完成,您就可以开始将仓库添加到容器中。

以下示例基于此表

create table testtable ( id int auto_increment primary key, name varchar(50)) engine=innodb;
//..Test repository interface
//..We always need a unique name for the service locator
interface ITestRepo extends \buffalokiwi\magicgraph\persist\IRepository {}

//..Test repository implementation 
class TestRepo extends \buffalokiwi\magicgraph\persist\SQLRepository implements ITestRepo {};

//..Test model 
class TestModel extends buffalokiwi\magicgraph\DefaultModel {}


//..Add ITestRepo to the container 
$ioc->addInterface( ITestRepo::class, function() use ($dbc,$propertyFactory) {
  return new TestRepo(
    'testtable',
    new \buffalokiwi\magicgraph\DefaultModelMapper( function( buffalokiwi\magicgraph\property\IPropertySet $props ) {
      return new TestModel( $props );
    }, TestModel::class ),
    $dbc,
    new buffalokiwi\magicgraph\property\DefaultPropertySet( 
      $propertyFactory, 
      new buffalokiwi\magicgraph\property\QuickPropertyConfig([
        'id' => ['type' => 'int', 'flags' => ['primary']], 
        'name' => ['type' => 'string']]))
  );
});


//..And now if we wanted to use this
$testRepo = $ioc->getInstance( ITestRepo::class );
/* @var $testRepo \buffalokiwi\magicgraph\persist\IRepository */

//..Create a new model
$testModel = $testRepo->create();

//..Set the name property
$testModel->name = 'test';

//..Save the model 
$testRepo->save( $testModel );

//..Get the id of the new model
//..Outputs "1" 
echo $testModel->id;

实体属性值

这是 buffalokiwi\magicgraph\eav 包。

搜索

这是 buffalokiwi\magicgraph\search 和 buffalokiwi\magicgraph\eav\search 包。

搜索与关系等一起工作。它已内置于 IRepository 实现,当构造仓库时可以替换查询构建器。