Skip to content

实体

Testing Is Documentation

tests/Database/Ddd/EntityTest.php

实体是整个系统最为核心的基本单位,实体封装了一些常用的功能。

Uses

php
<?php

use Leevel\Database\Condition;
use Leevel\Database\Ddd\EntityCollection;
use Leevel\Database\Ddd\EntityIdentifyConditionException;
use Leevel\Database\Ddd\Meta;
use Leevel\Database\Ddd\Select;
use Leevel\Database\DuplicateKeyException;
use Leevel\Di\Container;
use Leevel\Kernel\Utils\Api;
use Leevel\Validate\Validator;
use Tests\Database\DatabaseTestCase as TestCase;
use Tests\Database\Ddd\Entity\CompositeId;
use Tests\Database\Ddd\Entity\DemoToArrayShowPropNullRelationEntity;
use Tests\Database\Ddd\Entity\DemoUnique;
use Tests\Database\Ddd\Entity\DemoUniqueNew;
use Tests\Database\Ddd\Entity\DemoVersion;
use Tests\Database\Ddd\Entity\DemoVirtualEntity;
use Tests\Database\Ddd\Entity\EntityWithEnum;
use Tests\Database\Ddd\Entity\EntityWithEnum2;
use Tests\Database\Ddd\Entity\EntityWithEnumButClassNotFound;
use Tests\Database\Ddd\Entity\EntityWithEnumValidator;
use Tests\Database\Ddd\Entity\EntityWithEnumValidator2;
use Tests\Database\Ddd\Entity\EntityWithEnumValidator3;
use Tests\Database\Ddd\Entity\EntityWithoutAnyField;
use Tests\Database\Ddd\Entity\EntityWithoutPrimaryKey;
use Tests\Database\Ddd\Entity\EntityWithoutPrimaryKeyNullInArray;
use Tests\Database\Ddd\Entity\PostNew;
use Tests\Database\Ddd\Entity\PostNew2;
use Tests\Database\Ddd\Entity\PostNew3;
use Tests\Database\Ddd\Entity\PostNew4;
use Tests\Database\Ddd\Entity\PostNew5;
use Tests\Database\Ddd\Entity\PostNew6;
use Tests\Database\Ddd\Entity\PostNew7;
use Tests\Database\Ddd\Entity\PostNew8;
use Tests\Database\Ddd\Entity\Relation\Post;
use Tests\Database\Ddd\Entity\Relation\PostForReplace;
use Tests\Database\Ddd\Entity\StatusEnum;
use Tests\Database\Ddd\Entity\WithoutPrimarykey;
use Tests\Database\Ddd\Entity\WithoutPrimarykeyAndAllAreKey;

withProps 批量设置属性数据

php
public function testWithProps(): void
{
    $entity = new Post();
    $entity->withProps([
        'title' => 'foo',
        'summary' => 'bar',
    ]);

    self::assertSame('foo', $entity->title);
    self::assertSame('bar', $entity->summary);
    self::assertSame(['title', 'summary'], $entity->changed());
}

description 获取枚举值对应的描述

fixture 定义

Tests\Database\Ddd\Entity\EntityWithEnum

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class EntityWithEnum extends Entity
{
    public const string TABLE = 'entity_with_enum';

    public const string ID = 'id';

    public const string AUTO = 'id';

    #[Struct([
        self::READONLY => true,
    ])]
    protected ?int $id = null;

    #[Struct([
    ])]
    protected ?string $title = null;

    #[Struct([
        self::ENUM_CLASS => StatusEnum::class,
    ])]
    protected ?int $status = null;
}
php
public function testEntityWithEnumDescription(): void
{
    $this->initI18n();

    $entity = new EntityWithEnum([
        'title' => 'foo',
        'status' => 1,
    ]);

    self::assertSame('foo', $entity->title);
    self::assertSame(1, $entity->status);

    $data = <<<'eot'
        {
            "title": "foo"
        }
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            $entity
                ->only(['title'])
                ->toArray()
        )
    );

    $data = <<<'eot'
        {
            "title": "foo",
            "status": 1,
            "status_enum": "启用"
        }
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            $entity->toArray(),
            2
        )
    );

    self::assertSame('启用', StatusEnum::description('1'));
    self::assertSame('禁用', StatusEnum::description('0'));

    $data = <<<'eot'
        {
            "value": {
                "DISABLE": 0,
                "ENABLE": 1
            },
            "description": {
                "DISABLE": "禁用",
                "ENABLE": "启用"
            }
        }
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            StatusEnum::descriptions(),
            3
        )
    );
}

多枚举值

fixture 定义

Tests\Database\Ddd\Entity\EntityWithEnum2

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class EntityWithEnum2 extends Entity
{
    public const string TABLE = 'entity_with_enum';

    public const string ID = 'id';

    public const string AUTO = 'id';

    #[Struct([
        self::READONLY => true,
    ])]
    protected ?int $id = null;

    #[Struct([
    ])]
    protected ?string $title = null;

    #[Struct([
        self::ENUM_CLASS => StatusEnum::class,
    ])]
    protected ?string $status = null;
}
php
public function testEntityWithEnumDescription2(): void
{
    $this->initI18n();

    $entity = new EntityWithEnum2([
        'title' => 'foo',
        'status' => '0,1',
    ]);

    $data = <<<'eot'
        {
            "title": "foo",
            "status": "0,1",
            "status_enum": "禁用,启用"
        }
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            $entity->toArray(),
            2
        )
    );
}

虚拟属性保存丢弃

fixture 定义

Tests\Database\Ddd\Entity\PostNew7

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class PostNew7 extends Entity
{
    public const string TABLE = 'post';

    public const string ID = 'id';

    public const string AUTO = 'id';

    public const string DELETE_AT = 'delete_at';

    #[Struct([
        self::READONLY => true,
        self::COLUMN_NAME => 'ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
        ],
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_NAME => '标题',
        self::COLUMN_STRUCT => [
            'type' => 'varchar',
            'default' => '',
            'length' => 64,
        ],
    ])]
    protected ?string $title = null;

    #[Struct([
        self::COLUMN_NAME => '用户ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
            'default' => 0,
        ],
    ])]
    protected ?int $userId = null;

    #[Struct([
        self::COLUMN_NAME => '文章摘要',
        self::COLUMN_STRUCT => [
            'type' => 'varchar',
            'default' => '',
            'length' => 200,
        ],
    ])]
    protected ?string $summary = null;

    #[Struct([
        self::COLUMN_NAME => '创建时间',
        self::COLUMN_STRUCT => [
            'type' => 'datetime',
            'default' => 'CURRENT_TIMESTAMP',
        ],
    ])]
    protected ?string $createAt = null;

    #[Struct([
        self::CREATE_FILL => 0,
        self::COLUMN_NAME => '删除时间',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
            'default' => 0,
        ],
    ])]
    protected ?int $deleteAt = null;

    #[Struct([
        self::COLUMN_NAME => '内容',
        self::COLUMN_STRUCT => [
            'type' => 'mediumtext',
            'default' => null,
        ],
        self::VIRTUAL_COLUMN => true,
    ])]
    protected ?string $content = null;
}
php
public function testVirtualColumnData(): void
{
    $entity = new PostNew7([
        'title' => 'foo',
        'content' => 'content',
    ]);
    $result = $entity->save()->flush();
    self::assertSame(1, $result);
}

columnValidators 返回字段验证规则(默认场景)

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;
use Leevel\Validate\IValidator;

class EntityWithEnumValidator extends Entity
{
    public const string TABLE = 'entity_with_enum';

    public const string ID = 'id';

    public const string AUTO = 'id';

    #[Struct([
        self::READONLY => true,
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_VALIDATOR => [
            self::VALIDATOR_SCENES => [
                'required',
                'max_length:30',
            ],
            'store' => null,
            ':update' => [
                IValidator::OPTIONAL,
            ],
            'update_new' => [
                'required',
                'max_length:10',
            ],
        ],
    ])]
    protected ?string $title = null;

    #[Struct([
        self::ENUM_CLASS => StatusEnum::class,
    ])]
    protected ?int $status = null;
}
php
public function testColumnValidators(): void
{
    [$validatorRules, $validatorMessages] = EntityWithEnumValidator::columnValidators(EntityWithEnumValidator::VALIDATOR_SCENES);

    $data = <<<'eot'

"title": [
    "required",
    "max_length:30"
],
"status": [
    [
        "in",
        [
            0,
            1
        ]
    ],
    "configal"
]



    self::assertSame(
        $data,
        $this->varJson(
            $validatorRules
        )
    );

    $data = <<<'eot'



    self::assertSame(
        $data,
        $this->varJson(
            $validatorMessages
        )
    );

    $data = <<<'eot'



    self::assertSame(
        $data,
        $this->varJson(
            $validatorMessages
        )
    );
}

columnValidators 返回字段验证规则(继承场景)

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;
use Leevel\Validate\IValidator;

class EntityWithEnumValidator extends Entity
{
    public const string TABLE = 'entity_with_enum';

    public const string ID = 'id';

    public const string AUTO = 'id';

    #[Struct([
        self::READONLY => true,
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_VALIDATOR => [
            self::VALIDATOR_SCENES => [
                'required',
                'max_length:30',
            ],
            'store' => null,
            ':update' => [
                IValidator::OPTIONAL,
            ],
            'update_new' => [
                'required',
                'max_length:10',
            ],
        ],
    ])]
    protected ?string $title = null;

    #[Struct([
        self::ENUM_CLASS => StatusEnum::class,
    ])]
    protected ?int $status = null;
}

继承场景用法:

php
 'store' => null
php
public function testColumnValidators2(): void
{
    [$validatorRules, $validatorMessages] = EntityWithEnumValidator::columnValidators('store');

    $data = <<<'eot'

"title": [
    "required",
    "max_length:30"
],
"status": [
    [
        "in",
        [
            0,
            1
        ]
    ],
    "configal"
]



    self::assertSame(
        $data,
        $this->varJson(
            $validatorRules
        )
    );

    $data = <<<'eot'



    self::assertSame(
        $data,
        $this->varJson(
            $validatorMessages
        )
    );
}

columnValidators 返回字段验证规则(合并场景)

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;
use Leevel\Validate\IValidator;

class EntityWithEnumValidator extends Entity
{
    public const string TABLE = 'entity_with_enum';

    public const string ID = 'id';

    public const string AUTO = 'id';

    #[Struct([
        self::READONLY => true,
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_VALIDATOR => [
            self::VALIDATOR_SCENES => [
                'required',
                'max_length:30',
            ],
            'store' => null,
            ':update' => [
                IValidator::OPTIONAL,
            ],
            'update_new' => [
                'required',
                'max_length:10',
            ],
        ],
    ])]
    protected ?string $title = null;

    #[Struct([
        self::ENUM_CLASS => StatusEnum::class,
    ])]
    protected ?int $status = null;
}

继承场景用法:

php
':update' => [
    \Leevel\Validate\IValidator::OPTIONAL,
],
php
public function testColumnValidators3(): void
{
    [$validatorRules, $validatorMessages] = EntityWithEnumValidator::columnValidators('update');

    $data = <<<'eot'

"title": [
    "required",
    "max_length:30",
    "configal"
],
"status": [
    [
        "in",
        [
            0,
            1
        ]
    ],
    "configal"
]



    self::assertSame(
        $data,
        $this->varJson(
            $validatorRules
        )
    );

    $data = <<<'eot'



    self::assertSame(
        $data,
        $this->varJson(
            $validatorMessages
        )
    );
}

columnValidators 返回字段验证规则(替换场景)

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;
use Leevel\Validate\IValidator;

class EntityWithEnumValidator extends Entity
{
    public const string TABLE = 'entity_with_enum';

    public const string ID = 'id';

    public const string AUTO = 'id';

    #[Struct([
        self::READONLY => true,
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_VALIDATOR => [
            self::VALIDATOR_SCENES => [
                'required',
                'max_length:30',
            ],
            'store' => null,
            ':update' => [
                IValidator::OPTIONAL,
            ],
            'update_new' => [
                'required',
                'max_length:10',
            ],
        ],
    ])]
    protected ?string $title = null;

    #[Struct([
        self::ENUM_CLASS => StatusEnum::class,
    ])]
    protected ?int $status = null;
}

继承场景用法:

php
'update_new' => [
    'required',
    'max_length:10',
],
php
public function testColumnValidators4(): void
{
    [$validatorRules, $validatorMessages] = EntityWithEnumValidator::columnValidators('update_new');

    $data = <<<'eot'

"title": [
    "required",
    "max_length:10"
],
"status": [
    [
        "in",
        [
            0,
            1
        ]
    ],
    "configal"
]



    self::assertSame(
        $data,
        $this->varJson(
            $validatorRules
        )
    );

    $data = <<<'eot'



    self::assertSame(
        $data,
        $this->varJson(
            $validatorMessages
        )
    );
}

columnValidators 返回字段验证规则(指定字段)

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;
use Leevel\Validate\IValidator;

class EntityWithEnumValidator extends Entity
{
    public const string TABLE = 'entity_with_enum';

    public const string ID = 'id';

    public const string AUTO = 'id';

    #[Struct([
        self::READONLY => true,
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_VALIDATOR => [
            self::VALIDATOR_SCENES => [
                'required',
                'max_length:30',
            ],
            'store' => null,
            ':update' => [
                IValidator::OPTIONAL,
            ],
            'update_new' => [
                'required',
                'max_length:10',
            ],
        ],
    ])]
    protected ?string $title = null;

    #[Struct([
        self::ENUM_CLASS => StatusEnum::class,
    ])]
    protected ?int $status = null;
}
php
public function testColumnValidators5(): void
{
    [$validatorRules, $validatorMessages] = EntityWithEnumValidator::columnValidators('update_new', ['title']);

    $data = <<<'eot'

"title": [
    "required",
    "max_length:10"
]



    self::assertSame(
        $data,
        $this->varJson(
            $validatorRules
        )
    );

    $data = <<<'eot'



    self::assertSame(
        $data,
        $this->varJson(
            $validatorMessages
        )
    );
}

columnValidators 返回字段验证规则(支持自定义消息)

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;
use Leevel\Validate\IValidator;

class EntityWithEnumValidator2 extends Entity
{
    public const string TABLE = 'entity_with_enum';

    public const string ID = 'id';

    public const string AUTO = 'id';

    #[Struct([
        self::READONLY => true,
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_VALIDATOR => [
            self::VALIDATOR_SCENES => [
                'required',
                'max_length:30',
            ],
            'store' => null,
            ':update' => [
                IValidator::OPTIONAL,
            ],
            'update_new' => [
                'required',
                'max_length:10',
            ],
        ],
        self::VALIDATOR_MESSAGES => [
            'required' => '{field} 不能为空 new',
            'max_length' => '{field} 不满足最大长度 {rule} new',
        ],
    ])]
    protected ?string $title = null;

    #[Struct([
        self::ENUM_CLASS => StatusEnum::class,
    ])]
    protected ?int $status = null;
}
php
public function testColumnValidators6(): void
{
    [$validatorRules, $validatorMessages] = EntityWithEnumValidator2::columnValidators('update_new', ['title']);

    $data = <<<'eot'

"title": [
    "required",
    "max_length:10"
]



    self::assertSame(
        $data,
        $this->varJson(
            $validatorRules
        )
    );

    $data = <<<'eot'

"title": {
    "required": "{field} 不能为空 new",
    "max_length": "{field} 不满足最大长度 {rule} new"
}



    self::assertSame(
        $data,
        $this->varJson(
            $validatorMessages
        )
    );
}

hasChanged 检测属性是否已经改变

php
public function testHasChanged(): void
{
    $entity = new Post();
    self::assertFalse($entity->hasChanged('title'));
    $entity->title = 'change';
    self::assertTrue($entity->hasChanged('title'));
}

addChanged 添加指定属性为已改变

php
public function testAddChanged(): void
{
    $entity = new Post();
    $data = <<<'eot'
        []
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            $entity->changed()
        )
    );

    $entity->addChanged(['user_id', 'title']);

    $data = <<<'eot'
        [
            "user_id",
            "title"
        ]
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            $entity->changed(),
            1
        )
    );
}

deleteChanged 删除已改变属性

php
public function testDeleteChanged(): void
{
    $entity = new Post();
    $data = <<<'eot'
        []
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            $entity->changed()
        )
    );

    $entity->addChanged(['user_id', 'title']);

    $data = <<<'eot'
        [
            "user_id",
            "title"
        ]
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            $entity->changed(),
            1,
        )
    );

    $entity->deleteChanged(['user_id']);

    $data = <<<'eot'
        [
            "title"
        ]
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            $entity->changed(),
            2,
        )
    );
}

clearChanged 清空已改变属性

php
public function testClearChanged(): void
{
    $entity = new Post();
    $data = <<<'eot'
        []
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            $entity->changed()
        )
    );

    $entity->addChanged(['user_id', 'title']);

    $data = <<<'eot'
        [
            "user_id",
            "title"
        ]
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            $entity->changed(),
            1,
        )
    );

    $entity->clearChanged(['user_id']);

    $data = <<<'eot'
        []
        eot;

    self::assertSame(
        $data,
        $this->varJson(
            $entity->changed(),
            2,
        )
    );
}

idCondition 获取查询主键条件

php
public function testIdCondition(): void
{
    $entity = new Post(['id' => 5]);
    self::assertSame(['id' => 5], $entity->idCondition());
}

实体属性数组访问 ArrayAccess.offsetExists 支持

php
public function testArrayAccessOffsetExists(): void
{
    $entity = new Post(['id' => 5, 'title' => 'hello']);
    self::assertTrue(isset($entity['title']));
    self::assertFalse(isset($entity['user_id']));
}

实体属性数组访问 ArrayAccess.offsetSet 支持

php
public function testArrayAccessOffsetSet(): void
{
    $entity = new Post(['id' => 5]);
    self::assertFalse(isset($entity['title']));
    self::assertNull($entity->title);
    $entity['title'] = 'world';
    self::assertTrue(isset($entity['title']));
    self::assertSame('world', $entity->title);
}

实体属性数组访问 ArrayAccess.offsetGet 支持

php
public function testArrayAccessOffsetGet(): void
{
    $entity = new Post(['id' => 5]);
    self::assertNull($entity['title']);
    $entity['title'] = 'world';
    self::assertSame('world', $entity['title']);
}

实体属性数组访问 ArrayAccess.offsetUnset 支持

php
public function testArrayAccessOffsetUnset(): void
{
    $entity = new Post(['id' => 5]);
    self::assertNull($entity['title']);
    $entity['title'] = 'world';
    self::assertSame('world', $entity['title']);
    unset($entity['title']);
    self::assertNull($entity['title']);
}

实体属性访问魔术方法 __isset 支持

php
public function testMagicIsset(): void
{
    $entity = new Post(['id' => 5, 'title' => 'hello']);
    self::assertTrue(isset($entity->title));
    self::assertFalse(isset($entity->userId));
}

实体属性访问魔术方法 __set 支持

php
public function testMagicSet(): void
{
    $entity = new Post(['id' => 5]);
    self::assertFalse(isset($entity->title));
    self::assertNull($entity->title);
    $entity->title = 'world';
    self::assertTrue(isset($entity->title));
    self::assertSame('world', $entity->title);
}

实体属性访问魔术方法 __get 支持

php
public function testMagicGet(): void
{
    $entity = new Post(['id' => 5]);
    self::assertNull($entity->title);
    $entity->title = 'world';
    self::assertSame('world', $entity->title);
}

实体属性访问魔术方法 __unset 支持

php
public function testMagicUnset(): void
{
    $entity = new Post(['id' => 5]);
    self::assertNull($entity->title);
    $entity->title = 'world';
    self::assertSame('world', $entity->title);
    $entity->title = null;
    self::assertNull($entity->title);
}

setter 设置属性值

php
public function testCallSetter(): void
{
    $entity = new Post(['id' => 5]);
    self::assertNull($entity->title);
    self::assertNull($entity->userId);
    $entity->setTitle('hello');
    $entity->setUserId(5);
    self::assertSame('hello', $entity->title);
    self::assertSame(5, $entity->userId);
}

getter 获取属性值

php
public function testCallGetter(): void
{
    $entity = new Post(['id' => 5]);
    self::assertNull($entity->getTitle());
    self::assertNull($entity->getUserId());
    $entity->setTitle('hello');
    $entity->setUserId(5);
    self::assertSame('hello', $entity->getTitle());
    self::assertSame(5, $entity->getUserId());
}

find 获取实体查询对象

php
public function testStaticFind(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('post')
            ->insert([
                'title' => 'hello world',
                'user_id' => 1,
                'summary' => 'post summary',
                'delete_at' => 0,
            ])
    );

    $post = Post::find()->where('id', 1)->findOne();
    self::assertSame('hello world', $post->title);
    self::assertSame(1, $post->userId);
    self::assertSame('post summary', $post->summary);
}

connectSandbox 数据库连接沙盒

php
public function testConnectSandbox(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('post')
            ->insert([
                'title' => 'hello world',
                'user_id' => 1,
                'summary' => 'post summary',
                'delete_at' => 0,
            ])
    );

    $post = Post::connectSandbox('password_right', static function () {
        return Post::find()->where('id', 1)->findOne();
    });

    self::assertSame('hello world', $post->title);
    self::assertSame(1, $post->userId);
    self::assertSame('post summary', $post->summary);
}

newed 确定对象是否对应数据库中的一条记录

php
public function testNewed(): void
{
    $entity = new Post();
    self::assertTrue($entity->newed());

    $entity = new Post(['id' => 5]);
    self::assertTrue($entity->newed());

    $entity = new Post(['id' => 5], true);
    self::assertFalse($entity->newed());
}

withNewed 设置确定对象是否对应数据库中的一条记录

php
public function testWithNewed(): void
{
    $entity = new Post();
    self::assertTrue($entity->newed());
    $entity->withNewed(false);
    self::assertFalse($entity->newed());

    $entity = new Post(['id' => 5]);
    self::assertTrue($entity->newed());
    $entity->withNewed(false);
    self::assertFalse($entity->newed());

    $entity = new Post(['id' => 5], true);
    self::assertFalse($entity->newed());
    $entity->withNewed(true);
    self::assertTrue($entity->newed());
}

original 获取原始数据

php
public function testOriginal(): void
{
    $entity = new Post();
    self::assertSame([], $entity->original());

    $entity = new Post($data = [
        'title' => 'hello',
        'summary' => 'world',
        'foo' => 'bar',
    ], false, true);
    self::assertSame($data, $entity->original());
    self::assertSame('hello', $entity->title);
    self::assertSame('world', $entity->summary);
}

id 获取主键值

php
public function testId(): void
{
    $entity = new Post();
    self::assertFalse($entity->id());

    $entity = new Post(['id' => 5]);
    self::assertSame(['id' => 5], $entity->id());
}

id 获取复合主键值

fixture 定义

Tests\Database\Ddd\Entity\CompositeId

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class CompositeId extends Entity
{
    public const string TABLE = 'composite_id';

    public const array ID = ['id1', 'id2'];

    public const ?string AUTO = null;

    #[Struct([
    ])]
    protected ?int $id1 = null;

    #[Struct([
    ])]
    protected ?int $id2 = null;

    #[Struct([
    ])]
    protected ?string $name = null;
}
php
public function testCompositeId(): void
{
    $entity = new CompositeId();
    self::assertFalse($entity->id(false));
    self::assertFalse($entity->id());

    $entity = new CompositeId(['id1' => 5]);
    self::assertFalse($entity->id(false));
    self::assertFalse($entity->id());

    $entity = new CompositeId(['id1' => 5, 'id2' => 8]);
    self::assertSame(['id1' => 5, 'id2' => 8], $entity->id(false));
    self::assertSame(['id1' => 5, 'id2' => 8], $entity->id());
}

refresh 从数据库重新读取当前对象的属性

php
public function testRefresh(): void
{
    $post1 = new Post();
    $post1->create()->flush();
    $this->assertInstanceof(Post::class, $post1);
    self::assertSame(1, $post1->id);
    self::assertNull($post1->userId);
    self::assertNull($post1->title);
    self::assertNull($post1->summary);
    self::assertNull($post1->delete_at);

    $post1->refresh();
    self::assertSame(1, $post1->id);
    self::assertSame(0, $post1->userId);
    self::assertSame('', $post1->title);
    self::assertSame('', $post1->summary);
    self::assertSame(0, $post1->delete_at);
}

refresh 从数据库重新读取当前对象的属性支持复合主键

php
public function testRefreshWithCompositeId(): void
{
    $entity = new CompositeId(['id1' => 1, 'id2' => 3]);
    $entity->create()->flush();
    $this->assertInstanceof(CompositeId::class, $entity);
    self::assertSame(1, $entity->id1);
    self::assertSame(3, $entity->id2);
    self::assertNull($entity->name);

    $entity->refresh();
    self::assertSame(1, $entity->id1);
    self::assertSame(3, $entity->id2);
    self::assertSame('', $entity->name);
}

构造器支持忽略未定义属性

$ignoreUndefinedProp 用于数据库添加了字段,但是我们的实体并没有更新字段,查询得到的实体对象将会忽略掉新增的字段而不报错。

php
public function testIgnoreUndefinedProp(): void
{
    $entity = new Post(['undefined_prop' => 5], true, true);
    self::assertSame([], $entity->toArray());
}

字段支持格式化

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class PostNew2 extends Entity
{
    public const string TABLE = 'post';

    public const string ID = 'id';

    public const string AUTO = 'id';

    public const string DELETE_AT = 'delete_at';

    #[Struct([
        self::READONLY => true,
        self::COLUMN_NAME => 'ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
        ],
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_NAME => '标题',
        self::COLUMN_STRUCT => [
            'type' => 'varchar',
            'default' => '',
            'length' => 64,
            'format' => [self::class, 'titleFormat'],
            'format_raw' => true,
        ],
    ])]
    protected ?string $title = null;

    #[Struct([
        self::COLUMN_NAME => '用户ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
            'default' => 0,
            'format' => null,
            'format_raw' => true,
        ],
    ])]
    protected ?int $userId = null;

    #[Struct([
        self::COLUMN_NAME => '文章摘要',
        self::COLUMN_STRUCT => [
            'type' => 'varchar',
            'default' => '',
            'length' => 200,
            'format' => 'not_found',
            'format_raw' => true,
        ],
    ])]
    protected ?string $summary = null;

    #[Struct([
        self::COLUMN_NAME => '创建时间',
        self::COLUMN_STRUCT => [
            'type' => 'datetime',
            'default' => 'CURRENT_TIMESTAMP',
        ],
    ])]
    protected ?string $createAt = null;

    #[Struct([
        self::CREATE_FILL => 0,
        self::COLUMN_NAME => '删除时间',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
            'default' => 0,
        ],
    ])]
    protected ?int $deleteAt = null;

    public static function titleFormat(string $v): string
    {
        return $v.'_new';
    }
}
php
public function test6(): void
{
    $entity = new PostNew2(['title' => 'foo']);
    self::assertSame([
        'title_format_raw' => 'foo',
        'title' => 'foo_new',
    ], $entity->toArray());
}

字符串支持默认值函数

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class PostNew3 extends Entity
{
    public const string TABLE = 'post';

    public const string ID = 'id';

    public const string AUTO = 'id';

    public const string DELETE_AT = 'delete_at';

    #[Struct([
        self::READONLY => true,
        self::COLUMN_NAME => 'ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
        ],
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_NAME => '标题',
        self::COLUMN_STRUCT => [
            'type' => 'varchar',
            'default' => '',
            'length' => 64,
        ],
    ])]
    protected ?string $title = null;

    #[Struct([
        self::COLUMN_NAME => '用户ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
            'default' => 0,
        ],
    ])]
    protected ?int $userId = null;

    #[Struct([
        self::COLUMN_NAME => '文章摘要',
        self::COLUMN_STRUCT => [
            'type' => 'varchar',
            'default' => '',
            'length' => 200,
        ],
    ])]
    protected ?string $summary = null;

    #[Struct([
        self::COLUMN_NAME => '创建时间',
        self::COLUMN_STRUCT => [
            'type' => 'datetime',
            'default' => 'CURRENT_TIMESTAMP',
        ],
    ])]
    protected ?string $createAt = null;

    #[Struct([
        self::CREATE_FILL => 0,
        self::COLUMN_NAME => '删除时间',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
            'default' => 0,
        ],
    ])]
    protected ?int $deleteAt = null;

    protected function titleDefaultValue(): string
    {
        return 'hello world';
    }
}
php
public function test9(): void
{
    $entity = new PostNew3();
    self::assertSame([
        'title' => 'hello world',
    ], $entity->toArray());
}

浮点数填充数据

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class PostNew4 extends Entity
{
    public const string TABLE = 'post';

    public const string ID = 'id';

    public const string AUTO = 'id';

    public const string DELETE_AT = 'delete_at';

    #[Struct([
        self::READONLY => true,
        self::COLUMN_NAME => 'ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
        ],
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_NAME => '用户ID',
        self::COLUMN_STRUCT => [
            'type' => 'float',
            'default' => 0,
        ],
    ])]
    protected ?float $userId = null;
}
php
public function test10(): void
{
    $entity = new PostNew4([
        'user_id' => '1.3',
    ]);
    self::assertSame([
        'user_id' => 1.3,
    ], $entity->toArray());
}

自定义转换优先

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class PostNew8 extends Entity
{
    public const string TABLE = 'post';

    public const string ID = 'id';

    public const string AUTO = 'id';

    public const string DELETE_AT = 'delete_at';

    #[Struct([
        self::READONLY => true,
        self::COLUMN_NAME => 'ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
        ],
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_NAME => '用户ID',
        self::COLUMN_STRUCT => [
            'type' => 'float',
            'default' => 0,
        ],
    ])]
    protected ?float $userId = null;

    protected function userIdTransformValue(mixed $value): float
    {
        return (float) $value + 5;
    }
}
php
public function test15(): void
{
    $entity = new PostNew8([
        'user_id' => '1.3',
    ]);
    self::assertSame([
        'user_id' => 6.3,
    ], $entity->toArray());
}

默认格式化

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class PostNew6 extends Entity
{
    public const string TABLE = 'post';

    public const string ID = 'id';

    public const string AUTO = 'id';

    public const string DELETE_AT = 'delete_at';

    #[Struct([
        self::READONLY => true,
        self::COLUMN_NAME => 'ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
        ],
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_NAME => '标题',
        self::COLUMN_STRUCT => [
            'type' => 'varchar',
            'default' => '',
            'length' => 64,
        ],
    ])]
    protected ?string $title = null;

    #[Struct([
        self::COLUMN_NAME => '用户ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
            'default' => 0,
        ],
    ])]
    protected ?int $userId = null;

    #[Struct([
        self::COLUMN_NAME => '文章摘要',
        self::COLUMN_STRUCT => [
            'type' => 'varchar',
            'default' => '',
            'length' => 200,
        ],
    ])]
    protected ?string $summary = null;

    #[Struct([
        self::COLUMN_NAME => '创建时间',
        self::COLUMN_STRUCT => [
            'type' => 'datetime',
            'default' => 'CURRENT_TIMESTAMP',
        ],
    ])]
    protected ?string $createAt = null;

    #[Struct([
        self::CREATE_FILL => 0,
        self::COLUMN_NAME => '删除时间',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
            'default' => 0,
        ],
    ])]
    protected ?int $deleteAt = null;

    protected function titleFormatValue(string $title): string
    {
        return $title.'_new';
    }
}
php
public function test12(): void
{
    $entity = new PostNew6([
        'title' => 'hello',
    ]);
    self::assertSame([
        'title' => 'hello_new',
    ], $entity->toArray());
}

update 更新数据带上版本号

可以用于并发控制,例如商品库存,客户余额等。

fixture 定义

Tests\Database\Ddd\Entity\DemoVersion

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class DemoVersion extends Entity
{
    public const string TABLE = 'test_version';

    public const string ID = 'id';

    public const string AUTO = 'id';

    public const string VERSION = 'version';

    #[Struct([
        self::READONLY => true,
    ])]
    protected ?int $id = null;

    #[Struct([
    ])]
    protected ?string $name = null;

    #[Struct([
    ])]
    protected ?string $availableNumber = null;

    #[Struct([
    ])]
    protected ?string $realNumber = null;

    #[Struct([
    ])]
    protected ?int $version = null;

    protected bool $enabledVersionFramework = true;
}
php
public function testUpdateWithVersion(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('test_version')
            ->insert([
                'name' => 'xiaoniuge',
            ])
    );

    $testVersion = DemoVersion::select()->findEntity(1);

    $this->assertInstanceof(DemoVersion::class, $testVersion);
    self::assertSame(1, $testVersion->id);
    self::assertSame('xiaoniuge', $testVersion->name);
    self::assertSame('0.0000', $testVersion->availableNumber);
    self::assertSame('0.0000', $testVersion->realNumber);

    $condition = [
        'available_number' => $testVersion->availableNumber,
        'real_number' => $testVersion->realNumber,
    ];
    $testVersion->name = 'aniu';
    $testVersion->availableNumber = Condition::raw('[available_number]+1');
    $testVersion->realNumber = Condition::raw('[real_number]+3');
    self::assertSame(
        1,
        $testVersion
            ->condition($condition)
            ->update()
            ->flush()
    );
    self::assertSame('SQL: [493] UPDATE `test_version` SET `test_version`.`name` = :named_param_name,`test_version`.`available_number` = `test_version`.`available_number`+1,`test_version`.`real_number` = `test_version`.`real_number`+3,`test_version`.`version` = `test_version`.`version`+1 WHERE `test_version`.`available_number` = :test_version_available_number AND `test_version`.`real_number` = :test_version_real_number AND `test_version`.`id` = :test_version_id AND `test_version`.`version` = :test_version_version LIMIT 1 | Params:  5 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [30] :test_version_available_number | paramno=1 | name=[30] ":test_version_available_number" | is_param=1 | param_type=2 | Key: Name: [25] :test_version_real_number | paramno=2 | name=[25] ":test_version_real_number" | is_param=1 | param_type=2 | Key: Name: [16] :test_version_id | paramno=3 | name=[16] ":test_version_id" | is_param=1 | param_type=1 | Key: Name: [21] :test_version_version | paramno=4 | name=[21] ":test_version_version" | is_param=1 | param_type=1 (UPDATE `test_version` SET `test_version`.`name` = \'aniu\',`test_version`.`available_number` = `test_version`.`available_number`+1,`test_version`.`real_number` = `test_version`.`real_number`+3,`test_version`.`version` = `test_version`.`version`+1 WHERE `test_version`.`available_number` = \'0.0000\' AND `test_version`.`real_number` = \'0.0000\' AND `test_version`.`id` = 1 AND `test_version`.`version` = 0 LIMIT 1)', $testVersion->select()->getLastSql());

    $testVersion->name = 'hello';
    self::assertSame(1, $testVersion->update()->flush());
    self::assertSame('SQL: [227] UPDATE `test_version` SET `test_version`.`version` = `test_version`.`version`+1,`test_version`.`name` = :named_param_name WHERE `test_version`.`id` = :test_version_id AND `test_version`.`version` = :test_version_version LIMIT 1 | Params:  3 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [16] :test_version_id | paramno=1 | name=[16] ":test_version_id" | is_param=1 | param_type=1 | Key: Name: [21] :test_version_version | paramno=2 | name=[21] ":test_version_version" | is_param=1 | param_type=1 (UPDATE `test_version` SET `test_version`.`version` = `test_version`.`version`+1,`test_version`.`name` = \'hello\' WHERE `test_version`.`id` = 1 AND `test_version`.`version` = 1 LIMIT 1)', $testVersion->select()->getLastSql());
}

update 更新数据不含版本数据则不会带上版本号

version 对应的字段无数据,将会忽略版本号。

php
public function testUpdateNoVersionDataWithoutVersion(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('test_version')
            ->insert([
                'name' => 'xiaoniuge',
            ])
    );

    $testVersion = DemoVersion::select()
        ->findEntity(1, ['id,name,available_number,real_number'])
    ;

    $this->assertInstanceof(DemoVersion::class, $testVersion);
    self::assertSame(1, $testVersion->id);
    self::assertNull($testVersion->version);
    self::assertSame('xiaoniuge', $testVersion->name);
    self::assertSame('0.0000', $testVersion->availableNumber);
    self::assertSame('0.0000', $testVersion->realNumber);

    $condition = [
        'available_number' => $testVersion->availableNumber,
        'real_number' => $testVersion->realNumber,
    ];
    $testVersion->name = 'aniu';
    $testVersion->availableNumber = Condition::raw('[available_number]+1');
    $testVersion->realNumber = Condition::raw('[real_number]+3');
    self::assertSame(
        1,
        $testVersion
            ->condition($condition)
            ->update()
            ->flush()
    );
    self::assertSame('SQL: [386] UPDATE `test_version` SET `test_version`.`name` = :named_param_name,`test_version`.`available_number` = `test_version`.`available_number`+1,`test_version`.`real_number` = `test_version`.`real_number`+3 WHERE `test_version`.`available_number` = :test_version_available_number AND `test_version`.`real_number` = :test_version_real_number AND `test_version`.`id` = :test_version_id LIMIT 1 | Params:  4 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [30] :test_version_available_number | paramno=1 | name=[30] ":test_version_available_number" | is_param=1 | param_type=2 | Key: Name: [25] :test_version_real_number | paramno=2 | name=[25] ":test_version_real_number" | is_param=1 | param_type=2 | Key: Name: [16] :test_version_id | paramno=3 | name=[16] ":test_version_id" | is_param=1 | param_type=1 (UPDATE `test_version` SET `test_version`.`name` = \'aniu\',`test_version`.`available_number` = `test_version`.`available_number`+1,`test_version`.`real_number` = `test_version`.`real_number`+3 WHERE `test_version`.`available_number` = \'0.0000\' AND `test_version`.`real_number` = \'0.0000\' AND `test_version`.`id` = 1 LIMIT 1)', $testVersion->select()->getLastSql());

    $testVersion->name = 'hello';
    self::assertSame(1, $testVersion->update()->flush());
    self::assertSame('SQL: [120] UPDATE `test_version` SET `test_version`.`name` = :named_param_name WHERE `test_version`.`id` = :test_version_id LIMIT 1 | Params:  2 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [16] :test_version_id | paramno=1 | name=[16] ":test_version_id" | is_param=1 | param_type=1 (UPDATE `test_version` SET `test_version`.`name` = \'hello\' WHERE `test_version`.`id` = 1 LIMIT 1)', $testVersion->select()->getLastSql());
}

version.condition 设置是否启用乐观锁版本字段配合设置扩展查询条件

php
public function testUpdateWithVersionAndWithCondition(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('test_version')
            ->insert([
                'name' => 'xiaoniuge',
            ])
    );

    $testVersion = DemoVersion::select()->findEntity(1);

    $this->assertInstanceof(DemoVersion::class, $testVersion);
    self::assertSame(1, $testVersion->id);
    self::assertSame('xiaoniuge', $testVersion->name);
    self::assertSame('0.0000', $testVersion->availableNumber);
    self::assertSame('0.0000', $testVersion->realNumber);

    $testVersion->name = 'aniu';
    $testVersion->availableNumber = Condition::raw('[available_number]+1');
    $testVersion->realNumber = Condition::raw('[real_number]+3');
    self::assertSame(1, $testVersion->version(true)->update()->flush());
    self::assertSame('SQL: [361] UPDATE `test_version` SET `test_version`.`name` = :named_param_name,`test_version`.`available_number` = `test_version`.`available_number`+1,`test_version`.`real_number` = `test_version`.`real_number`+3,`test_version`.`version` = `test_version`.`version`+1 WHERE `test_version`.`id` = :test_version_id AND `test_version`.`version` = :test_version_version LIMIT 1 | Params:  3 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [16] :test_version_id | paramno=1 | name=[16] ":test_version_id" | is_param=1 | param_type=1 | Key: Name: [21] :test_version_version | paramno=2 | name=[21] ":test_version_version" | is_param=1 | param_type=1 (UPDATE `test_version` SET `test_version`.`name` = \'aniu\',`test_version`.`available_number` = `test_version`.`available_number`+1,`test_version`.`real_number` = `test_version`.`real_number`+3,`test_version`.`version` = `test_version`.`version`+1 WHERE `test_version`.`id` = 1 AND `test_version`.`version` = 0 LIMIT 1)', $testVersion->select()->getLastSql());

    $testVersion->refresh();
    $condition = ['available_number' => $testVersion->availableNumber];
    $testVersion->name = 'hello';
    $testVersion->availableNumber = Condition::raw('[available_number]+8');
    self::assertSame(1, $testVersion->condition($condition)->update()->flush());
    self::assertSame('SQL: [370] UPDATE `test_version` SET `test_version`.`version` = `test_version`.`version`+1,`test_version`.`name` = :named_param_name,`test_version`.`available_number` = `test_version`.`available_number`+8 WHERE `test_version`.`available_number` = :test_version_available_number AND `test_version`.`id` = :test_version_id AND `test_version`.`version` = :test_version_version LIMIT 1 | Params:  4 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [30] :test_version_available_number | paramno=1 | name=[30] ":test_version_available_number" | is_param=1 | param_type=2 | Key: Name: [16] :test_version_id | paramno=2 | name=[16] ":test_version_id" | is_param=1 | param_type=1 | Key: Name: [21] :test_version_version | paramno=3 | name=[21] ":test_version_version" | is_param=1 | param_type=1 (UPDATE `test_version` SET `test_version`.`version` = `test_version`.`version`+1,`test_version`.`name` = \'hello\',`test_version`.`available_number` = `test_version`.`available_number`+8 WHERE `test_version`.`available_number` = \'1.0000\' AND `test_version`.`id` = 1 AND `test_version`.`version` = 1 LIMIT 1)', $testVersion->select()->getLastSql());
}

version 设置是否启用乐观锁版本字段支持取消

php
public function testUpdateWithVersionAndWithoutVersionCondition(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('test_version')
            ->insert([
                'name' => 'xiaoniuge',
            ])
    );

    $testVersion = DemoVersion::select()->findEntity(1);

    $this->assertInstanceof(DemoVersion::class, $testVersion);
    self::assertSame(1, $testVersion->id);
    self::assertSame('xiaoniuge', $testVersion->name);
    self::assertSame('0.0000', $testVersion->availableNumber);
    self::assertSame('0.0000', $testVersion->realNumber);

    $testVersion->name = 'aniu';
    $testVersion->availableNumber = Condition::raw('[available_number]+1');
    $testVersion->realNumber = Condition::raw('[real_number]+3');
    self::assertSame(1, $testVersion->version(false)->update()->flush());
    self::assertSame('SQL: [254] UPDATE `test_version` SET `test_version`.`name` = :named_param_name,`test_version`.`available_number` = `test_version`.`available_number`+1,`test_version`.`real_number` = `test_version`.`real_number`+3 WHERE `test_version`.`id` = :test_version_id LIMIT 1 | Params:  2 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [16] :test_version_id | paramno=1 | name=[16] ":test_version_id" | is_param=1 | param_type=1 (UPDATE `test_version` SET `test_version`.`name` = \'aniu\',`test_version`.`available_number` = `test_version`.`available_number`+1,`test_version`.`real_number` = `test_version`.`real_number`+3 WHERE `test_version`.`id` = 1 LIMIT 1)', $testVersion->select()->getLastSql());

    $testVersion->name = 'hello';
    self::assertSame(1, $testVersion->update()->flush());
    self::assertSame('SQL: [120] UPDATE `test_version` SET `test_version`.`name` = :named_param_name WHERE `test_version`.`id` = :test_version_id LIMIT 1 | Params:  2 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [16] :test_version_id | paramno=1 | name=[16] ":test_version_id" | is_param=1 | param_type=1 (UPDATE `test_version` SET `test_version`.`name` = \'hello\' WHERE `test_version`.`id` = 1 LIMIT 1)', $testVersion->select()->getLastSql());
}

condition 设置扩展查询条件支持直接设置版本查询条件

php
public function testUpdateWithCondition(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('test_version')
            ->insert([
                'name' => 'xiaoniuge',
            ])
    );

    $testVersion = DemoVersion::select()->findEntity(1);

    $this->assertInstanceof(DemoVersion::class, $testVersion);
    self::assertSame(1, $testVersion->id);
    self::assertSame('xiaoniuge', $testVersion->name);
    self::assertSame('0.0000', $testVersion->availableNumber);
    self::assertSame('0.0000', $testVersion->realNumber);

    $testVersion->name = 'aniu';
    $testVersion->availableNumber = Condition::raw('[available_number]+1');
    $testVersion->realNumber = Condition::raw('[real_number]+3');
    self::assertSame(1, $testVersion->version(true)->update()->flush());
    self::assertSame('SQL: [361] UPDATE `test_version` SET `test_version`.`name` = :named_param_name,`test_version`.`available_number` = `test_version`.`available_number`+1,`test_version`.`real_number` = `test_version`.`real_number`+3,`test_version`.`version` = `test_version`.`version`+1 WHERE `test_version`.`id` = :test_version_id AND `test_version`.`version` = :test_version_version LIMIT 1 | Params:  3 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [16] :test_version_id | paramno=1 | name=[16] ":test_version_id" | is_param=1 | param_type=1 | Key: Name: [21] :test_version_version | paramno=2 | name=[21] ":test_version_version" | is_param=1 | param_type=1 (UPDATE `test_version` SET `test_version`.`name` = \'aniu\',`test_version`.`available_number` = `test_version`.`available_number`+1,`test_version`.`real_number` = `test_version`.`real_number`+3,`test_version`.`version` = `test_version`.`version`+1 WHERE `test_version`.`id` = 1 AND `test_version`.`version` = 0 LIMIT 1)', $testVersion->select()->getLastSql());

    $testVersion->refresh();
    $condition = ['available_number' => $testVersion->availableNumber, DemoVersion::VERSION => 9999];
    $testVersion->name = 'hello';
    $testVersion->availableNumber = Condition::raw('[available_number]+8');
    self::assertSame(0, $testVersion->condition($condition)->update()->flush());
    self::assertSame('SQL: [370] UPDATE `test_version` SET `test_version`.`version` = `test_version`.`version`+1,`test_version`.`name` = :named_param_name,`test_version`.`available_number` = `test_version`.`available_number`+8 WHERE `test_version`.`available_number` = :test_version_available_number AND `test_version`.`version` = :test_version_version AND `test_version`.`id` = :test_version_id LIMIT 1 | Params:  4 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [30] :test_version_available_number | paramno=1 | name=[30] ":test_version_available_number" | is_param=1 | param_type=2 | Key: Name: [21] :test_version_version | paramno=2 | name=[21] ":test_version_version" | is_param=1 | param_type=1 | Key: Name: [16] :test_version_id | paramno=3 | name=[16] ":test_version_id" | is_param=1 | param_type=1 (UPDATE `test_version` SET `test_version`.`version` = `test_version`.`version`+1,`test_version`.`name` = \'hello\',`test_version`.`available_number` = `test_version`.`available_number`+8 WHERE `test_version`.`available_number` = \'1.0000\' AND `test_version`.`version` = 9999 AND `test_version`.`id` = 1 LIMIT 1)', $testVersion->select()->getLastSql());
}

实体设置虚拟主键可以解决没有主键的表数据更新问题

fixture 定义

without_primarykey

sql
CREATE TABLE `without_primarykey` (
    `goods_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '商品 ID',
    `description` varchar(255) NOT NULL DEFAULT '' COMMENT '商品描述',
    `name` varchar(100) NOT NULL DEFAULT '' COMMENT '商品名称'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='没有主键的表';

Tests\Database\Ddd\Entity\WithoutPrimarykey

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class WithoutPrimarykey extends Entity
{
    public const string TABLE = 'without_primarykey';

    public const string ID = 'goods_id';

    public const ?string AUTO = null;

    #[Struct([
        self::READONLY => true,
    ])]
    protected ?int $goodsId = null;

    #[Struct([
    ])]
    protected ?string $description = null;

    #[Struct([
    ])]
    protected ?string $name = null;
}
php
public function testUpdateWithoutPrimarykey(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        0,
        $connect
            ->table('without_primarykey')
            ->insert([
                'goods_id' => 1,
                'description' => 'hello',
            ])
    );

    $withoutPrimarykey = WithoutPrimarykey::select()->findEntity(1);
    self::assertSame(['goods_id'], WithoutPrimarykey::primaryKey());

    $this->assertInstanceof(WithoutPrimarykey::class, $withoutPrimarykey);
    self::assertSame(1, $withoutPrimarykey->goodsId);
    self::assertSame('hello', $withoutPrimarykey->description);

    $withoutPrimarykey->description = 'world';
    self::assertSame(1, $withoutPrimarykey->update()->flush());
    self::assertSame('SQL: [170] UPDATE `without_primarykey` SET `without_primarykey`.`description` = :named_param_description WHERE `without_primarykey`.`goods_id` = :without_primarykey_goods_id LIMIT 1 | Params:  2 | Key: Name: [24] :named_param_description | paramno=0 | name=[24] ":named_param_description" | is_param=1 | param_type=2 | Key: Name: [28] :without_primarykey_goods_id | paramno=1 | name=[28] ":without_primarykey_goods_id" | is_param=1 | param_type=1 (UPDATE `without_primarykey` SET `without_primarykey`.`description` = \'world\' WHERE `without_primarykey`.`goods_id` = 1 LIMIT 1)', $withoutPrimarykey->select()->getLastSql());
}

实体未设置主键所有非关联字段将变为虚拟主键

fixture 定义

without_primarykey

sql
CREATE TABLE `without_primarykey` (
    `goods_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '商品 ID',
    `description` varchar(255) NOT NULL DEFAULT '' COMMENT '商品描述',
    `name` varchar(100) NOT NULL DEFAULT '' COMMENT '商品名称'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='没有主键的表';

Tests\Database\Ddd\Entity\WithoutPrimarykeyAndAllAreKey

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class WithoutPrimarykeyAndAllAreKey extends Entity
{
    public const string TABLE = 'without_primarykey';

    public const null ID = null;

    public const null AUTO = null;

    #[Struct([
        self::READONLY => true,
    ])]
    protected ?int $goodsId = null;

    #[Struct([
    ])]
    protected ?string $description = null;

    #[Struct([
    ])]
    protected ?string $name = null;
}
php
public function testUpdateWithoutPrimaryKeyAndAllAreKey(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        0,
        $connect
            ->table('without_primarykey')
            ->insert([
                'goods_id' => 1,
                'description' => 'hello',
                'name' => 'world',
            ])
    );

    $withoutPrimarykey = WithoutPrimarykeyAndAllAreKey::select()->findOne();
    self::assertSame(['goods_id', 'description', 'name'], WithoutPrimarykeyAndAllAreKey::primaryKey());

    $this->assertInstanceof(WithoutPrimarykeyAndAllAreKey::class, $withoutPrimarykey);
    self::assertSame(1, $withoutPrimarykey->goodsId);
    self::assertSame('hello', $withoutPrimarykey->description);
    self::assertSame('world', $withoutPrimarykey->name);

    $withoutPrimarykey->description = 'my';
    $withoutPrimarykey->name = 'php';
    self::assertSame(1, $withoutPrimarykey->update()->flush());
    self::assertSame('SQL: [350] UPDATE `without_primarykey` SET `without_primarykey`.`description` = :named_param_description,`without_primarykey`.`name` = :named_param_name WHERE `without_primarykey`.`goods_id` = :without_primarykey_goods_id AND `without_primarykey`.`description` = :without_primarykey_description AND `without_primarykey`.`name` = :without_primarykey_name LIMIT 1 | Params:  5 | Key: Name: [24] :named_param_description | paramno=0 | name=[24] ":named_param_description" | is_param=1 | param_type=2 | Key: Name: [17] :named_param_name | paramno=1 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [28] :without_primarykey_goods_id | paramno=2 | name=[28] ":without_primarykey_goods_id" | is_param=1 | param_type=1 | Key: Name: [31] :without_primarykey_description | paramno=3 | name=[31] ":without_primarykey_description" | is_param=1 | param_type=2 | Key: Name: [24] :without_primarykey_name | paramno=4 | name=[24] ":without_primarykey_name" | is_param=1 | param_type=2 (UPDATE `without_primarykey` SET `without_primarykey`.`description` = \'my\',`without_primarykey`.`name` = \'php\' WHERE `without_primarykey`.`goods_id` = 1 AND `without_primarykey`.`description` = \'hello\' AND `without_primarykey`.`name` = \'world\' LIMIT 1)', $withoutPrimarykey->select()->getLastSql());

    $withoutPrimarykey->name = 'new name';
    self::assertSame(1, $withoutPrimarykey->update()->flush());
    self::assertSame('SQL: [288] UPDATE `without_primarykey` SET `without_primarykey`.`name` = :named_param_name WHERE `without_primarykey`.`goods_id` = :without_primarykey_goods_id AND `without_primarykey`.`description` = :without_primarykey_description AND `without_primarykey`.`name` = :without_primarykey_name LIMIT 1 | Params:  4 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [28] :without_primarykey_goods_id | paramno=1 | name=[28] ":without_primarykey_goods_id" | is_param=1 | param_type=1 | Key: Name: [31] :without_primarykey_description | paramno=2 | name=[31] ":without_primarykey_description" | is_param=1 | param_type=2 | Key: Name: [24] :without_primarykey_name | paramno=3 | name=[24] ":without_primarykey_name" | is_param=1 | param_type=2 (UPDATE `without_primarykey` SET `without_primarykey`.`name` = \'new name\' WHERE `without_primarykey`.`goods_id` = 1 AND `without_primarykey`.`description` = \'my\' AND `without_primarykey`.`name` = \'php\' LIMIT 1)', $withoutPrimarykey->select()->getLastSql());

    $withoutPrimarykey->name = 'new and new';
    $withoutPrimarykey->update();
    $withoutPrimarykey->name = 'new and new2';
    self::assertSame(1, $withoutPrimarykey->update()->flush());
    self::assertSame('SQL: [288] UPDATE `without_primarykey` SET `without_primarykey`.`name` = :named_param_name WHERE `without_primarykey`.`goods_id` = :without_primarykey_goods_id AND `without_primarykey`.`description` = :without_primarykey_description AND `without_primarykey`.`name` = :without_primarykey_name LIMIT 1 | Params:  4 | Key: Name: [17] :named_param_name | paramno=0 | name=[17] ":named_param_name" | is_param=1 | param_type=2 | Key: Name: [28] :without_primarykey_goods_id | paramno=1 | name=[28] ":without_primarykey_goods_id" | is_param=1 | param_type=1 | Key: Name: [31] :without_primarykey_description | paramno=2 | name=[31] ":without_primarykey_description" | is_param=1 | param_type=2 | Key: Name: [24] :without_primarykey_name | paramno=3 | name=[24] ":without_primarykey_name" | is_param=1 | param_type=2 (UPDATE `without_primarykey` SET `without_primarykey`.`name` = \'new and new2\' WHERE `without_primarykey`.`goods_id` = 1 AND `without_primarykey`.`description` = \'my\' AND `without_primarykey`.`name` = \'new name\' LIMIT 1)', $withoutPrimarykey->select()->getLastSql());
}

__clone 实体克隆

复制的实体没有主键值,保存数据时将会在数据库新增一条记录。

php
public function testEntityClone(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('post')
            ->insert([
                'title' => 'hello world',
                'user_id' => 1,
                'summary' => 'post summary',
                'delete_at' => 0,
            ])
    );

    $post = Post::find()->where('id', 1)->findOne();
    self::assertSame(1, $post->id);
    self::assertSame('hello world', $post->title);
    self::assertSame(1, $post->userId);
    self::assertSame('post summary', $post->summary);

    $postClone = clone $post;
    self::assertNull($postClone->id);
    self::assertSame('hello world', $postClone->title);
    self::assertSame(1, $postClone->userId);
    self::assertSame('post summary', $postClone->summary);

    $post->title = 'world';
    self::assertSame('hello world', $postClone->title);
    $postClone->title = 'goods';
    self::assertSame('world', $post->title);
}

make 创建实例

php
public function testEntityMake(): void
{
    $post = Post::make([
        'title' => 'hello world',
        'user_id' => 1,
        'summary' => 'post summary',
        'delete_at' => 0,
    ]);

    self::assertTrue($post->newed());
    self::assertNull($post->id);
    self::assertSame('hello world', $post->title);
    self::assertSame(1, $post->userId);
    self::assertSame('post summary', $post->summary);
}

createAssign 新增批量赋值

php
public function testEntityCreateAssign(): void
{
    $post = Post::createAssign([
        'title' => 'hello world',
        'user_id' => 1,
        'summary' => 'post summary',
        'delete_at' => 0,
    ]);

    self::assertTrue($post->newed());
    self::assertNull($post->id);
    self::assertSame('hello world', $post->title);
    self::assertSame(1, $post->userId);
    self::assertSame('post summary', $post->summary);
}

updateAssign 更新批量赋值

php
public function testEntityUpdateAssign(): void
{
    $post = Post::updateAssign([
        'id' => 1,
        'title' => 'hello world',
        'user_id' => 1,
        'summary' => 'post summary',
        'delete_at' => 0,
    ]);

    self::assertFalse($post->newed());
    self::assertSame(1, $post->id);
    self::assertSame('hello world', $post->title);
    self::assertSame(1, $post->userId);
    self::assertSame('post summary', $post->summary);
}

addGlobalScope 添加全局作用域

php
public function testAddGlobalScope(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('post')
            ->insert([
                'title' => 'hello world',
                'user_id' => 1,
                'summary' => 'post summary',
                'delete_at' => 0,
            ])
    );

    Post::addGlobalScope('hello', static function (Select $select): void {
        $select->where('id', 5);
    });
    Post::select()->findAll();
    $sql = Post::select()->getLastSql();
    self::assertSame($sql, 'SQL: [97] SELECT `post`.* FROM `post` WHERE `post`.`delete_at` = :post_delete_at AND `post`.`id` = :post_id | Params:  2 | Key: Name: [15] :post_delete_at | paramno=0 | name=[15] ":post_delete_at" | is_param=1 | param_type=1 | Key: Name: [8] :post_id | paramno=1 | name=[8] ":post_id" | is_param=1 | param_type=1 (SELECT `post`.* FROM `post` WHERE `post`.`delete_at` = 0 AND `post`.`id` = 5)');
}

withoutGlobalScope 不带指定全局作用域查询

php
public function testWithoutGlobalScope(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('post')
            ->insert([
                'title' => 'hello world',
                'user_id' => 1,
                'summary' => 'post summary',
                'delete_at' => 0,
            ])
    );

    Post::addGlobalScope('hello', static function (Select $select): void {
        $select->where('id', 5);
    });
    Post::withoutGlobalScope(['hello'])->findAll();
    $sql = Post::select()->getLastSql();
    self::assertSame($sql, 'SQL: [70] SELECT `post`.* FROM `post` WHERE `post`.`delete_at` = :post_delete_at | Params:  1 | Key: Name: [15] :post_delete_at | paramno=0 | name=[15] ":post_delete_at" | is_param=1 | param_type=1 (SELECT `post`.* FROM `post` WHERE `post`.`delete_at` = 0)');
}

findEntity 通过主键或条件查找实体

php
public function testFindEntity(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('post')
            ->insert([
                'title' => 'hello world',
                'user_id' => 1,
                'summary' => 'post summary',
                'delete_at' => 0,
            ])
    );

    $post = Post::findEntity(1);
    self::assertSame(1, $post->id);
    self::assertSame('hello world', $post->title);
    self::assertSame('post summary', $post->summary);
}

findMany 通过主键或条件查找多个实体

php
public function testFindMany(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('post')
            ->insert([
                'title' => 'hello world',
                'user_id' => 1,
                'summary' => 'post summary',
                'delete_at' => 0,
            ])
    );

    $collection = Post::findMany([1]);
    self::assertInstanceOf(EntityCollection::class, $collection);
    $post = $collection[0];
    self::assertSame(1, $post->id);
    self::assertSame('hello world', $post->title);
    self::assertSame('post summary', $post->summary);
}

findOrFail 通过主键或条件查找实体,未找到则抛出异常

php
public function testFindOrFail(): void
{
    $connect = $this->createDatabaseConnect();

    self::assertSame(
        1,
        $connect
            ->table('post')
            ->insert([
                'title' => 'hello world',
                'user_id' => 1,
                'summary' => 'post summary',
                'delete_at' => 0,
            ])
    );

    $post = Post::findOrFail(1);
    self::assertSame(1, $post->id);
    self::assertSame('hello world', $post->title);
    self::assertSame('post summary', $post->summary);
}

meta 返回实体类的元对象(虚拟实体)

php
public function testMeta1(): void
{
    $this->expectException(\RuntimeException::class);
    $this->expectExceptionMessage('The virtual entity does not support select');

    DemoVirtualEntity::meta();
}

delete 删除实体(虚拟实体)

php
public function testVirtualDelete(): void
{
    $demo = new DemoVirtualEntity(['id' => 1]);
    $demo->delete();
    self::assertSame(0, $demo->flush());
}

create 新增实体(虚拟实体)

php
public function testVirtualCreate(): void
{
    $demo = new DemoVirtualEntity(['id' => 1]);
    $demo->create();
    self::assertSame(1, $demo->flush());
}

update 更新实体(虚拟实体)

php
public function testVirtualUpdate(): void
{
    $demo = new DemoVirtualEntity(['id' => 1, 'name' => 'new']);
    $demo->update();
    self::assertSame(0, $demo->flush());
}

select 查询实体(虚拟实体)

php
public function testVirtualSelect(): void
{
    $this->expectException(\RuntimeException::class);
    $this->expectExceptionMessage(
        'The virtual entity does not support select.'
    );

    DemoVirtualEntity::select();
}

columnNames 回所有字段名字

php
public function testColumnNames(): void
{
    $this->initI18n();

    $data = <<<'eot'

"id": "ID",
"title": "标题",
"user_id": "用户ID",
"summary": "文章摘要",
"create_at": "创建时间",
"delete_at": "删除时间"



    self::assertSame(
        $data,
        $this->varJson(
            Post::columnNames(),
        )
    );
}

columnName 返回字段名字

php
public function testColumnName(): void
{
    $this->initI18n();

    self::assertSame('用户ID', Post::columnName('user_id'));
}

构造器初始化数据自动转换

fixture 定义

php
namespace Tests\Database\Ddd\Entity;

use Leevel\Database\Ddd\Entity;
use Leevel\Database\Ddd\Struct;

class PostNew extends Entity
{
    public const string TABLE = 'post';

    public const string ID = 'id';

    public const string AUTO = 'id';

    public const string DELETE_AT = 'delete_at';

    #[Struct([
        self::READONLY => true,
        self::COLUMN_NAME => 'ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
        ],
    ])]
    protected ?int $id = null;

    #[Struct([
        self::COLUMN_NAME => '标题',
        self::COLUMN_STRUCT => [
            'type' => 'varchar',
            'default' => '',
            'length' => 64,
        ],
    ])]
    protected ?string $title = null;

    #[Struct([
        self::COLUMN_NAME => '用户ID',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
            'default' => 0,
        ],
    ])]
    protected ?int $userId = null;

    #[Struct([
        self::COLUMN_NAME => '文章摘要',
        self::COLUMN_STRUCT => [
            'type' => 'varchar',
            'default' => '',
            'length' => 200,
        ],
    ])]
    protected ?string $summary = null;

    #[Struct([
        self::COLUMN_NAME => '创建时间',
        self::COLUMN_STRUCT => [
            'type' => 'datetime',
            'default' => 'CURRENT_TIMESTAMP',
        ],
    ])]
    protected ?string $createAt = null;

    #[Struct([
        self::CREATE_FILL => 0,
        self::COLUMN_NAME => '删除时间',
        self::COLUMN_STRUCT => [
            'type' => 'bigint',
            'default' => 0,
        ],
    ])]
    protected ?int $deleteAt = null;
}
php
public function test3(): void
{
    $entity = new PostNew(['title' => 'foo']);
    $entity->create()->flush();
}