activecollab/databasestructure

使用PHP定义数据库结构,并让库生成对象类和表

3.2.4 2023-12-05 16:20 UTC

README

Build Status

版本 1.0 待办事项

  • 获取代码覆盖率超过 90%,
  • serialize 方法添加到字段,以便在将字段添加到类型时自动将其添加到序列化列表中,
  • 将所有基类前缀为 Base
  • 将所有管理器和集合后缀分别为 ManagerCollection
  • 检查由关联添加的字段和属性之间的可能冲突,
  • 向 Has Many 和 Has Many Via 关联添加 releaseclear 方法,
  • 添加 ChildInterface,并确保 ParentField 将其添加到包含它的模型中,
  • 关联应自动将连接字段添加到要序列化的字段列表中,
  • 关联级联选项和测试,

字段

布尔字段(以 is_has_had_was_were_have_ 开头)也获得一个简短的获取器。例如,如果字段名称是 is_awesome,构建器将生成两个获取器:getIsAwesome()isAwesome()

密码字段

密码字段是用于存储密码散列的字段。默认情况下,它将 password 设置为字段名称。它与 StringField 类似(使用 VARCHAR 列),但不能有默认值(唉!),并且它没有用于轻松索引的方法(如果您愿意,您仍然可以自己添加索引)。

<?php

namespace MyApp;

use ActiveCollab\DatabaseStructure\Field\Scalar\PasswordField;

new PasswordField(); // Use default name (password).
new PasswordField('psswd_hash'); // Specify field name. 

JSON 字段

JSON 字段将 JSON 字段添加到类型中。它将在读写时自动序列化和反序列化

$this->addType('stats_snapshots')->addFields(
    new JsonField('stats')
);

除了常规的获取器和设置器外,JSON 字段还添加了一个 modify 方法。该方法接收一个回调函数,该函数将使用解码的 JSON 值调用。然后自动将回调函数的结果存储在字段中

$object->modifyStats(
    function ($stats) {
        $stats['something-to-add'] = true;
        unset($stats['something-to-remove']);
        
        return $stats;
    }
);

JSON 字段可以存储很多不同的数据类型,因此您可能无法总是知道将传递给回调函数的类型。在我们的日常使用中,我们注意到数组是存储在 JSON 字段中最常见的数据类型。为了确保您始终获得数组,无论字段中有什么内容,请传递第二个 $force_array 参数

$object->modifyStats(
    function (array $this_will_be_array_for_sure) {
        return $this_will_be_array_for_sure;
    },
    true
);

系统支持从 JSON 字段中提取值。这些值由 MySQL 自动提取,并且可以存储和索引。

添加提取器有两种方式。首先是通过自己构建提取器实例并添加它

$execution_time_extractor = (new FloatValueExtractor('execution_time', '$.exec_time', 0))
    ->storeValue()
    ->addIndex();

$this->addType('stats_snapshots')->addFields(
    new DateField('day'),
    (new JsonField('stats'))
        ->addValueExtractor($execution_time_extractor)
);

第二种是通过调用 extractValue 方法,该方法使用提供的参数构建适当的提取器,配置它并将其添加到字段中。方法参数

  1. field_name - 生成的字段名称,
  2. expression - 用于从 JSON 中提取值的表达式。有关详细信息,请参阅 https://dev.mysqlserver.cn/doc/refman/5.7/en/json-search-functions.html#function_json-extract MySQL 函数,
  3. default_value - 如果 expression 返回 NULL,则使用的值。
  4. extractor_type - 应使用的提取器实现类的名称。默认为 ValueExtractor(字符串值提取器),但也支持 int、float、bool、日期和日期时间值的提取器。
  5. is_stored - 值是否应永久存储,或者是否应为虚拟的(在读取时动态计算)。默认值是存储。
  6. is_indexed - 值是否应被索引。当设置为 TRUE 时,在生成的字段上添加索引。默认为 FALSE

示例

$this->addType('stats_snapshots')->addFields(
    new DateField('day'),
    (new JsonField('stats'))
        ->extractValue('plan_name', '$.plan_name', 'Unknown', ValueExtractor::class, true, true)
        ->extractValue('number_of_active_users', '$.users.num_active', 0, IntValueExtractor::class, true)
        ->extractValue('is_used_on_day', '$.is_used_on_day', null, BoolValueExtractor::class, false),
);

自动为所有生成的字段添加获取器方法。

$snapshot = $pool->getById(StatsSnapshot::class, 1);
print $snapshot->getPlanName() . "\n";
print $snapshot->getNumberOfActiveUsers() . "\n";
print ($snapshot->isUsedOnDay() ? 'yes' : 'no') . "\n";

请注意,生成的字段的值不能直接设置。此代码将引发异常。

$snapshot = $pool->getById(StatsSnapshot::class, 1);
$snapshot->setFieldValue('number_of_active_users', 123);  // Exception!

关联

属于

面向接口编程

“属于”关联支持“面向接口编程”方法。这意味着您可以将其设置为接受(并返回)实现特定接口的实例。

<?php

namespace MyApp;

use ActiveCollab\DatabaseStructure\Association\BelongsToAssociation;

(new BelongsToAssociation('author'))->accepts(AuthorInterface::class);

有多个

<?php

namespace App;

use ActiveCollab\DatabaseStructure\Association\BelongsToAssociation;
use ActiveCollab\DatabaseStructure\Association\HasManyAssociation;
use ActiveCollab\DatabaseStructure\Field\Composite\NameField;
use ActiveCollab\DatabaseStructure\Structure;

class HasManyExampleStructure extends Structure
{
    public function configure(): void
    {
        $this->addType('writers')->addFields(
            (new NameField('name', ''))->required(),
        )->addAssociations(
            new HasManyAssociation('books'),
        );
        
        $this->addType('books')->addFields(
            (new NameField('name', ''))->required(),
        )->addAssociations(
            new BelongsToAssociation('writer'),
        );
    }
}

此关联将为 Writer 模型添加以下方法:

  • getBooksFinder(): FinderInterface - 为此作者准备一个书籍查找实例,所有默认值都已设置(例如排序)。就像使用任何其他查找器一样使用它:通过添加额外条件扩展它,使用它来计数记录、获取所有记录或第一条记录等。
  • getBooks(): ?iterable - 返回属于作者的所有书籍。如果没有找到书籍,则此方法返回 NULL
  • getBookIds(): ?iterable - 返回属于作者的所有书籍 ID 列表。如果没有找到书籍,则此方法返回 NULL
  • countBooks(): int - 返回书籍的总数。

属性

“有多个”关联还为模型添加以下属性:

  • books - 通过提供其实例来设置关联的书籍。这些实例可以持久化到数据库,或它们可以是新实例。如果是新的,则当父作者对象被保存时将保存它们。
  • book_ids - 通过提供其 ID 来设置关联的书籍。
<?php

namespace App;

// Set books using an attribute:
$writer = $pool->produce(Writer::class, [
    'name' => 'Leo Tolstoy',
    'books' => [$book1, $book2, $book3],
]);

// Or, using ID-s:
$writer = $pool->produce(Writer::class, [
    'name' => 'Leo Tolstoy',
    'book_ids' => [1, 2, 3, 4],
]);

面向接口编程

“有多个”关联支持“面向接口编程”方法。这意味着您可以将其设置为接受(并返回)实现特定接口的实例。

示例

<?php

namespace MyApp;

use ActiveCollab\DatabaseStructure\Association\HasManyAssociation;

(new HasManyAssociation('books'))->accepts(BookInterface::class);

有一个

面向接口编程

“有一个”关联支持“面向接口编程”方法。这意味着您可以将其设置为接受(并返回)实现特定接口的实例。

示例

<?php

namespace MyApp;

use ActiveCollab\DatabaseStructure\Association\HasOneAssociation;

(new HasOneAssociation('book'))->accepts(BookInterface::class);

通过

多对多

<?php

namespace App;

use ActiveCollab\DatabaseStructure\Association\HasAndBelongsToManyAssociation;
use ActiveCollab\DatabaseStructure\Field\Composite\NameField;
use ActiveCollab\DatabaseStructure\Structure;

class HasManyExampleStructure extends Structure
{
    public function configure(): void
    {
        $this->addType('writers')->addFields(
            (new NameField('name', ''))->required(),
        )->addAssociations(
            new HasAndBelongsToManyAssociation('books'),
        );

        $this->addType('books')->addFields(
            (new NameField('name', ''))->required(),
        )->addAssociations(
            new HasAndBelongsToManyAssociation('writers'),
        );
    }
}

此关联将为 Writer 模型添加以下方法:

  • getBooksFinder(): FinderInterface - 为此作者准备一个书籍查找实例,所有默认值都已设置(例如排序)。就像使用任何其他查找器一样使用它:通过添加额外条件扩展它,使用它来计数记录、获取所有记录或第一条记录等。
  • getBooks(): ?iterable - 返回属于作者的所有书籍。如果没有找到书籍,则此方法返回 NULL
  • getBookIds(): ?iterable - 返回属于作者的所有书籍 ID 列表。如果没有找到书籍,则此方法返回 NULL
  • countBooks(): int - 返回书籍的总数。
  • &addBooks(...$books): void - 向作者添加一个或多个书籍。
  • &removeBooks(...$books): void - 移除与作者关联的一个或多个书籍。
  • &clearBooks(): void - 清除与作者关联的所有书籍连接(书籍对象不会被删除)。

属性

多对多关联还为模型添加以下属性:

  • books - 通过提供其实例来设置关联的书籍。这些实例可以持久化到数据库,或它们可以是新实例。如果是新的,则当父作者对象被保存时将保存它们。
  • book_ids - 通过提供其 ID 来设置关联的书籍。
<?php

namespace App;

// Set books using an attribute:
$writer = $pool->produce(Writer::class, [
    'name' => 'Leo Tolstoy',
    'books' => [$book1, $book2, $book3],
]);

// Or, using ID-s:
$writer = $pool->produce(Writer::class, [
    'name' => 'Leo Tolstoy',
    'book_ids' => [1, 2, 3, 4],
]);

结构选项

结构对象通过 setConfig() 方法支持通过配置设置配置选项。此方法可以在对象配置期间或创建后调用。

class MyStructure extends Structure
{
    public function configure(): void
    {
        $this->setConfig('option_name', 'value');
    }
}

以下选项可用:

  1. add_permissions - 向对象添加 CRUD 权限检查。更多信息…
  2. base_class_doc_block_properties - 指定要添加到生成的类的 DocBlock 部分的属性数组。更多信息…
  3. base_class_extends - 指定应扩展对象构建的类(默认为 ActiveCollab\DatabaseObject\Object)。

add_permissions

此选项告诉结构自动为其添加到其中的所有类型调用 permissions() 方法。默认情况下,此选项是关闭的,但可以通过将其设置为两个值之一来启用它。

  1. StructureInterface::ADD_PERMISSIVE_PERMISSIONS 启用权限和方法检查权限是否设置,默认返回 true
  2. StructureInterface::ADD_RESTRICTIVE_PERMISSIONS 启用权限和方法检查权限是否设置,默认返回 false

示例

class MyStructure extends Structure
{
    public function configure(): void
    {
        $this->setConfig(‘add_permissions’, StructureInterface::ADD_RESTRICTIVE_PERMISSIONS);
    }
}

base_class_doc_block_properties

一些编辑器会从类的 DocBlock 部分读取 @property,并知道哪些属性可以通过魔法方法访问,它们的类型是什么,并根据这些信息提供各种功能(如代码补全、类型检查等)。使用 base_class_doc_block_properties 来指定要添加到类中的属性列表。配置示例:

class MyStructure extends Structure
{
    public function configure(): void
    {
        $this->setConfig(‘base_class_doc_block_properties’, [
            'jobs' => '\\ActiveCollab\\JobsQueue\\Dispatcher'
        ]);
    }
}

构建内容

<?php

namespace Application\Structure\Namespace\Base;

/**
 * @property \ActiveCollab\JobsQueue\Dispatcher $jobs
 *
 * …
 */
abstract class Token extends \ActiveCollab\DatabaseObject\Entity\Entity
{
}

deprecate_long_bool_field_getter

如果您希望将布尔字段的长 getter 方法标记为已弃用,当存在短 getter 方法(isAwesome()getIsAwesome())时,请将此选项设置为 true。

header_comment

添加一个注释,该注释将被包含在所有自动生成的文件的开头。此选项在您需要在源代码中包含许可信息时很有用。

行为

行为是类型和字段添加到结果对象类的接口和接口实现。这些行为可以执行各种操作:允许您在集合中定位元素,在对象级别存储额外的信息,检查用户权限等。

权限行为

应用后,权限行为会将 ActiveCollab\DatabaseStructure\Behaviour\PermissionsInterface 添加到对象类中,它添加了四个检查给定对象用户权限的方法

  1. canCreate($user)
  2. canView($user)
  3. canEdit($user)
  4. canDelete($user)

所有四个方法仅接受一个参数,并且该参数需要是实现 \ActiveCollab\User\UserInterface 接口的实例。

有两个默认实现可以作为 PermissionsInterface 的实现添加

  1. ActiveCollab\DatabaseStructure\Behaviour\PermissionsInterface\PermissiveImplementation 默认返回 true
  2. ActiveCollab\DatabaseStructure\Behaviour\PermissionsInterface\RestrictiveImplementation 默认返回 false

注意:生成的代码在执行 CRUD 操作之前不会强制执行这些检查。强制应用这些限制的责任在于包含 DatabaseStructure 库的应用程序(例如在 ACL 或控制器层)。

可以将结构配置为自动将权限行为应用于类型(请参阅 add_permissions 结构选项)。当结构自动将权限行为添加到类型时,但您希望为特定类型关闭此功能,只需再次调用 permissions(false) 即可。

class MyStructure extends Structure
{
    public function configure(): void
    {
        $this->setConfig(‘add_permissions’, StructureInterface::ADD_RESTRICTIVE_PERMISSIONS);
        
        $this->addType(‘reverted_elements’)
            ->addFields()
            ->permissions(false);
    }
}

受保护字段行为

此行为向对象添加一个简单的受保护字段列表(可通过 getProtectedFields() 方法访问)。系统的其余部分需要决定如何处理此列表,但最常见的情况是在使用 POST 请求添加对象或使用 PUT 请求更新对象时禁用这些字段的设置。

class MyStructure extends Structure
{
    public function configure(): void
    {
        $this->addType('elements')->protectFields('created_at', 'created_by_id')->unprotectFields('created_by_id'); // will record ['created_at']
    }
}

protectFields 忽略空字段值,并且可以多次调用。

class MyStructure extends Structure
{
    public function configure(): void
    {
        $this->addType('elements')->protectFields('field_1', 'field_2')->protectFields('', '')->protectFields('field_2', 'field_3'); // will only record ['field_1', 'field_2', 'field_3']
    }
}