diff --git a/tests/orm/DbJsonFieldsTest.php b/tests/orm/DbJsonFieldsTest.php index 10765974..e869199d 100644 --- a/tests/orm/DbJsonFieldsTest.php +++ b/tests/orm/DbJsonFieldsTest.php @@ -53,13 +53,13 @@ public function setUp(): void public function testJsonFieldMemberNotExists() { $data = Db::table(self::$table)->where('extend->weight', null)->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.weight', null)->count()); + $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend->weight', null)->count()); $data = Db::table(self::$table)->where('extend->amount', null)->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.amount', null)->count()); + $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend->amount', null)->count()); $data = Db::table(self::$table)->where('extend->pack', null)->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.pack', null)->count()); + $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend->pack', null)->count()); } /** @@ -68,10 +68,10 @@ public function testJsonFieldMemberNotExists() public function testJsonFieldMemberNotExistsOrNull() { $data = Db::table(self::$table)->where('extend->brand', null)->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.brand', null)->count()); + $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend->brand', null)->count()); $data = Db::table(self::$table)->where('extend->standard', null)->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.standard', null)->count()); + $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend->standard', null)->count()); } /** @@ -80,13 +80,13 @@ public function testJsonFieldMemberNotExistsOrNull() public function testJsonFieldMemberEqual() { $data = Db::table(self::$table)->where('extend->brand', 'TP8')->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.brand', 'TP8')->count()); + $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend->brand', 'TP8')->count()); $data = Db::table(self::$table)->where('extend->standard', '大')->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.standard', '大')->count()); + $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend->standard', '大')->count()); $data = Db::table(self::$table)->where('extend->type', '清洁')->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.type', '清洁')->count()); + $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend->type', '清洁')->count()); } /** @@ -95,12 +95,12 @@ public function testJsonFieldMemberEqual() public function testJsonFieldMemberNotEqual() { $data = Db::table(self::$table)->where('extend->brand', '<>', 'TP8')->whereNull('extend->brand', "or")->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.brand', '<>', 'TP8')->count()); + $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend->brand', '<>', 'TP8')->count()); $data = Db::table(self::$table)->where('extend->standard', '<>', '大')->whereNull('extend->standard', "or")->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.standard', '<>', '大')->count()); + $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend->standard', '<>', '大')->count()); $data = Db::table(self::$table)->where('extend->type', '<>', '清洁')->whereNull('extend->type', "or")->select(); - $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend.type', '<>', '清洁')->count()); + $this->assertSame($data->count(), self::$testGoodsDataCollect->where('extend->type', '<>', '清洁')->count()); } } diff --git a/tests/orm/ModelAccessorTest.php b/tests/orm/ModelAccessorTest.php new file mode 100644 index 00000000..0817b659 --- /dev/null +++ b/tests/orm/ModelAccessorTest.php @@ -0,0 +1,247 @@ + 1, 'name' => 'test1', 'price' => '99.99', 'status' => 1, 'extra' => 'info1'], + ['id' => 2, 'name' => 'test2', 'price' => '199.99', 'status' => 0, 'extra' => 'info2'], + ]; + } + + public function testBasicAccessor() + { + $model = new class extends Model { + protected $table = 'test_accessor_model'; + + // 定义name字段获取器 + public function getNameAttr($value) + { + return strtoupper($value); + } + + // 定义status字段获取器 + public function getStatusTextAttr($value, $data) + { + $status = [0 => '禁用', 1 => '启用']; + return $status[$data['status']]; + } + }; + + $data = ['name' => 'test3', 'price' => 299.99, 'status' => 1]; + $result = $model::create($data); + + $this->assertEquals('TEST3', $result->name); + $this->assertEquals('启用', $result->status_text); + } + + public function testBasicMutator() + { + $model = new class extends Model { + protected $table = 'test_accessor_model'; + + // 定义name字段修改器 + public function setNameAttr($value) + { + return ucfirst($value); + } + + // 定义price字段修改器 + public function setPriceAttr($value) + { + return number_format($value, 2, '.', ''); + } + }; + + $data = ['name' => 'test4', 'price' => 399]; + $result = $model::create($data); + + $this->assertEquals('Test4', $result->name); + $this->assertEquals('399.00', $result->price); + } + + public function testCombinedAccessorMutator() + { + $model = new class extends Model { + protected $table = 'test_accessor_model'; + + // 定义extra字段的组合获取器和修改器 + public function getExtraAttr($value) + { + return json_decode($value, true); + } + + public function setExtraAttr($value) + { + return json_encode($value); + } + }; + + $extraData = ['key1' => 'value1', 'key2' => 'value2']; + $data = ['name' => 'test5', 'extra' => $extraData]; + $result = $model::create($data); + + $this->assertIsArray($result->extra); + $this->assertEquals($extraData, $result->extra); + + // 测试数据库中实际存储的值 + $rawData = Db::table('test_accessor_model')->where('id', $result->id)->find(); + $this->assertJson($rawData['extra']); + } + + public function testJsonSerialization() + { + $model = new class extends Model { + protected $table = 'test_accessor_model'; + + // 定义price字段获取器,在序列化时转换为整数分 + public function getPriceCentAttr($value, $data) + { + return intval($data['price'] * 100); + } + + // 定义追加的属性 + protected $append = ['price_cent']; + }; + + $data = ['name' => 'test6', 'price' => '599.99']; + $result = $model::create($data); + + $jsonData = json_decode(json_encode($result), true); + $this->assertEquals(59999, $jsonData['price_cent']); + } + + public function testBasicSearcher() + { + $model = new class extends Model { + protected $table = 'test_accessor_model'; + + // 定义name字段搜索器 + public function searchNameAttr($query, $value) + { + $query->where('name', 'like', '%' . $value . '%'); + } + + // 定义status字段搜索器 + public function searchStatusAttr($query, $value) + { + $query->where('status', '=', $value); + } + }; + + // 插入测试数据 + $model::create(['name' => 'test_search1', 'price' => '99.99', 'status' => 1]); + $model::create(['name' => 'test_search2', 'price' => '199.99', 'status' => 0]); + $model::create(['name' => 'other_name', 'price' => '299.99', 'status' => 1]); + + // 测试name搜索器 + $result = $model::withSearch(['name'], ['name' => 'test'])->select(); + $this->assertEquals(2, count($result)); + + // 测试status搜索器 + $result = $model::withSearch(['status'], ['status' => 1])->select(); + $this->assertEquals(2, count($result)); + } + + public function testSearcherWithParams() + { + $model = new class extends Model { + protected $table = 'test_accessor_model'; + + // 定义带参数的price搜索器 + public function searchPriceAttr($query, $value, $data) + { + if (isset($data['min_price']) && isset($data['max_price'])) { + $query->whereBetween('price', [$data['min_price'], $data['max_price']]); + } + } + }; + + // 插入测试数据 + $model::create(['name' => 'product1', 'price' => '50.00', 'status' => 1]); + $model::create(['name' => 'product2', 'price' => '150.00', 'status' => 1]); + $model::create(['name' => 'product3', 'price' => '250.00', 'status' => 1]); + + // 测试价格范围搜索 + $result = $model::withSearch(['price'], [ + 'min_price' => 100, + 'max_price' => 200 + ])->select(); + + $this->assertEquals(1, count($result)); + $this->assertEquals('150.00', $result[0]->price); + } + + public function testCombinedSearcher() + { + $model = new class extends Model { + protected $table = 'test_accessor_model'; + + // 定义组合搜索器 + public function searchComplexAttr($query, $value, $data) + { + if (!empty($data['keyword'])) { + $query->where('name', 'like', '%' . $data['keyword'] . '%'); + } + + if (isset($data['status'])) { + $query->where('status', '=', $data['status']); + } + + if (isset($data['min_price'])) { + $query->where('price', '>=', $data['min_price']); + } + } + }; + + // 插入测试数据 + $model::create(['name' => 'test_item1', 'price' => '100.00', 'status' => 1]); + $model::create(['name' => 'test_item2', 'price' => '200.00', 'status' => 0]); + $model::create(['name' => 'other_item', 'price' => '150.00', 'status' => 1]); + + // 测试组合搜索 + $result = $model::withSearch(['complex'], [ + 'keyword' => 'test', + 'status' => 1, + 'min_price' => 150 + ])->select(); + + $this->assertEquals(0, count($result)); + + $result = $model::withSearch(['complex'], [ + 'keyword' => 'test', + 'status' => 1, + 'min_price' => 50 + ])->select(); + + $this->assertEquals(1, count($result)); + $this->assertEquals('test_item1', $result[0]->name); + } +} \ No newline at end of file diff --git a/tests/orm/ModelCacheTest.php b/tests/orm/ModelCacheTest.php new file mode 100644 index 00000000..c246461b --- /dev/null +++ b/tests/orm/ModelCacheTest.php @@ -0,0 +1,163 @@ + 1, 'name' => 'test1', 'status' => 1], + ['id' => 2, 'name' => 'test2', 'status' => 0], + ['id' => 3, 'name' => 'test3', 'status' => 1], + ]; + Db::table('test_cache_model')->insertAll(self::$testData); + } + + public function testBasicCache() + { + $model = new class extends Model { + protected $table = 'test_cache_model'; + protected $pk = 'id'; + }; + + // 测试单条数据缓存 + $result1 = $model::cache(true)->find(1); + $this->assertEquals('test1', $result1->name); + + // 通过模型更新数据,验证缓存是否自动更新 + $result1->setCache(true)->save(['name' => 'modified']); + + // 验证缓存是否已自动更新 + $result2 = $model::cache(true)->find(1); + $this->assertEquals('modified', $result2->name); + + // 验证数据库中的实际值 + $dbResult = Db::table('test_cache_model')->where('id', 1)->find(); + $this->assertEquals('modified', $dbResult['name']); + } + + public function testCacheTag() + { + $model = new class extends Model { + protected $table = 'test_cache_model'; + protected $pk = 'id'; + }; + + // 使用标签缓存数据 + $result1 = $model::cache(true, 'test_tag')->select(); + $this->assertCount(3, $result1); + + // 添加新数据 + $model::create(['name' => 'test4', 'status' => 1]); + + // 验证缓存数据 + $result2 = $model::cache(true, 'test_tag')->select(); + $this->assertCount(3, $result2); + } + + public function testCacheWithQuery() + { + $model = new class extends Model { + protected $table = 'test_cache_model'; + protected $pk = 'id'; + }; + + // 测试复杂查询缓存 + $result1 = $model::cache(true) + ->where('status', 1) + ->order('id', 'desc') + ->select(); + $this->assertCount(2, $result1); + + // 添加新的状态为1的数据 + $model::create(['name' => 'test4', 'status' => 1]); + + // 验证缓存数据 + $result2 = $model::cache(true) + ->where('status', 1) + ->order('id', 'desc') + ->select(); + $this->assertCount(2, $result2); + } + + public function testCacheTime() + { + $model = new class extends Model { + protected $table = 'test_cache_model'; + protected $pk = 'id'; + }; + + // 设置缓存时间为1秒 + $result1 = $model::cache(1)->find(1); + $this->assertEquals('test1', $result1->name); + + // 修改数据 + Db::table('test_cache_model')->where('id', 1)->update(['name' => 'modified']); + + // 立即查询,应该返回缓存数据 + $result2 = $model::cache(1)->find(1); + $this->assertEquals('test1', $result2->name); + + // 等待缓存过期 + sleep(2); + + // 再次查询,应该返回新数据 + $result3 = $model::cache(1)->find(1); + $this->assertEquals('modified', $result3->name); + } + + public function testCacheKey() + { + $model = new class extends Model { + protected $table = 'test_cache_model'; + protected $pk = 'id'; + }; + + // 使用自定义缓存标识查询数据 + $result1 = $model::cache('custom_key_1')->where('status', 1)->select(); + $this->assertCount(2, $result1); + + // 使用相同的缓存标识,即使查询条件不同也应该返回缓存的数据 + $result2 = $model::cache('custom_key_1')->where('status', 0)->select(); + $this->assertCount(2, $result2); + + // 使用不同的缓存标识应该返回新的查询结果 + $result3 = $model::cache('custom_key_2')->where('status', 0)->select(); + $this->assertCount(1, $result3); + + // 添加新数据后,使用原缓存标识查询应该返回缓存数据 + $model::create(['name' => 'test4', 'status' => 1]); + $result4 = $model::cache('custom_key_1')->where('status', 1)->select(); + $this->assertCount(2, $result4); + + // 清除指定标识的缓存后,查询应该返回最新数据 + $model::getCache()->delete('custom_key_1'); + $result5 = $model::cache('custom_key_1')->where('status', 1)->select(); + $this->assertCount(3, $result5); + } +} \ No newline at end of file diff --git a/tests/orm/ModelEventTest.php b/tests/orm/ModelEventTest.php new file mode 100644 index 00000000..218c779e --- /dev/null +++ b/tests/orm/ModelEventTest.php @@ -0,0 +1,318 @@ + 1, 'name' => 'test1', 'status' => 1], + ['id' => 2, 'name' => 'test2', 'status' => 0], + ]; + } + + public function testInsertEvents() + { + $beforeInsertCalled = false; + $afterInsertCalled = false; + + $model = new class extends Model { + protected $table = 'test_event_model'; + protected $autoWriteTimestamp = true; + + public function onBeforeInsert($model) + { + global $beforeInsertCalled; + $beforeInsertCalled = true; + // 在插入前修改数据 + $model->name = 'modified_' . $model->name; + } + + public function onAfterInsert($model) + { + global $afterInsertCalled; + $afterInsertCalled = true; + } + }; + + $data = ['name' => 'test3', 'status' => 1]; + $result = $model::create($data); + + $this->assertTrue($beforeInsertCalled, 'before_insert event not triggered'); + $this->assertTrue($afterInsertCalled, 'after_insert event not triggered'); + $this->assertEquals('modified_test3', $result->name); + } + + public function testUpdateEvents() + { + $beforeUpdateCalled = false; + $afterUpdateCalled = false; + + $model = new class extends Model { + protected $table = 'test_event_model'; + protected $autoWriteTimestamp = true; + + public function onBeforeUpdate($model) + { + global $beforeUpdateCalled; + $beforeUpdateCalled = true; + // 在更新前修改数据 + $model->name = 'updated_' . $model->name; + } + + public function onAfterUpdate($model) + { + global $afterUpdateCalled; + $afterUpdateCalled = true; + } + }; + + // 先创建一条记录 + $record = $model::create(['name' => 'test4', 'status' => 1]); + + // 更新记录 + $record->name = 'new_name'; + $record->save(); + + $this->assertTrue($beforeUpdateCalled, 'before_update event not triggered'); + $this->assertTrue($afterUpdateCalled, 'after_update event not triggered'); + $this->assertEquals('updated_new_name', $record->name); + } + + public function testDeleteEvents() + { + $beforeDeleteCalled = false; + $afterDeleteCalled = false; + + $model = new class extends Model { + protected $table = 'test_event_model'; + protected $autoWriteTimestamp = true; + + public function onBeforeDelete($model) + { + global $beforeDeleteCalled; + $beforeDeleteCalled = true; + // 可以在删除前执行一些验证 + if ($model->status === 0) { + return false; // 阻止删除 + } + } + + public function onAfterDelete($model) + { + global $afterDeleteCalled; + $afterDeleteCalled = true; + } + }; + + // 创建两条记录 + $record1 = $model::create(['name' => 'test5', 'status' => 1]); + $record2 = $model::create(['name' => 'test6', 'status' => 0]); + + // 尝试删除状态为1的记录 + $record1->delete(); + $this->assertTrue($beforeDeleteCalled, 'before_delete event not triggered'); + $this->assertTrue($afterDeleteCalled, 'after_delete event not triggered'); + + // 重置标志 + $beforeDeleteCalled = false; + $afterDeleteCalled = false; + + // 尝试删除状态为0的记录 + $record2->delete(); + $this->assertTrue($beforeDeleteCalled, 'before_delete event not triggered'); + $this->assertFalse($afterDeleteCalled, 'after_delete event should not be triggered'); + + // 验证记录2仍然存在 + $this->assertNotNull($model::find($record2->id)); + } + + public function testWriteEvents() + { + $beforeWriteCalled = false; + $afterWriteCalled = false; + + $model = new class extends Model { + protected $table = 'test_event_model'; + protected $autoWriteTimestamp = true; + + public function onBeforeWrite($model) + { + global $beforeWriteCalled; + $beforeWriteCalled = true; + // 在写入前修改数据 + $model->name = 'write_' . $model->name; + } + + public function onAfterWrite($model) + { + global $afterWriteCalled; + $afterWriteCalled = true; + } + }; + + // 测试插入时的写入事件 + $record = $model::create(['name' => 'test7', 'status' => 1]); + $this->assertTrue($beforeWriteCalled, 'before_write event not triggered on insert'); + $this->assertTrue($afterWriteCalled, 'after_write event not triggered on insert'); + $this->assertEquals('write_test7', $record->name); + + // 重置标志 + $beforeWriteCalled = false; + $afterWriteCalled = false; + + // 测试更新时的写入事件 + $record->name = 'test8'; + $record->save(); + $this->assertTrue($beforeWriteCalled, 'before_write event not triggered on update'); + $this->assertTrue($afterWriteCalled, 'after_write event not triggered on update'); + $this->assertEquals('write_test8', $record->name); + } + + public function testModelObserver() + { + $observer = new class { + public $beforeInsertCalled = false; + public $afterInsertCalled = false; + public $beforeUpdateCalled = false; + public $afterUpdateCalled = false; + public $beforeDeleteCalled = false; + public $afterDeleteCalled = false; + public $beforeWriteCalled = false; + public $afterWriteCalled = false; + + public function onBeforeInsert($model) + { + $this->beforeInsertCalled = true; + $model->name = 'observer_' . $model->name; + } + + public function onAfterInsert($model) + { + $this->afterInsertCalled = true; + } + + public function onBeforeUpdate($model) + { + $this->beforeUpdateCalled = true; + $model->name = 'observer_updated_' . $model->name; + } + + public function onAfterUpdate($model) + { + $this->afterUpdateCalled = true; + } + + public function onBeforeDelete($model) + { + $this->beforeDeleteCalled = true; + if ($model->status === 0) { + return false; + } + } + + public function onAfterDelete($model) + { + $this->afterDeleteCalled = true; + } + + public function onBeforeWrite($model) + { + $this->beforeWriteCalled = true; + } + + public function onAfterWrite($model) + { + $this->afterWriteCalled = true; + } + }; + + $model = new class extends Model { + protected $table = 'test_event_model'; + protected $autoWriteTimestamp = true; + protected $eventObserver; + + public function __construct(array $data = []) + { + global $observer; + $this->eventObserver = $observer; + parent::__construct($data); + } + }; + + // 测试插入事件 + $data = ['name' => 'test9', 'status' => 1]; + $result = $model::create($data); + + $this->assertTrue($observer->beforeInsertCalled, 'observer before_insert event not triggered'); + $this->assertTrue($observer->afterInsertCalled, 'observer after_insert event not triggered'); + $this->assertTrue($observer->beforeWriteCalled, 'observer before_write event not triggered'); + $this->assertTrue($observer->afterWriteCalled, 'observer after_write event not triggered'); + $this->assertEquals('observer_test9', $result->name); + + // 重置标志 + $observer->beforeWriteCalled = false; + $observer->afterWriteCalled = false; + + // 测试更新事件 + $result->name = 'test10'; + $result->save(); + + $this->assertTrue($observer->beforeUpdateCalled, 'observer before_update event not triggered'); + $this->assertTrue($observer->afterUpdateCalled, 'observer after_update event not triggered'); + $this->assertTrue($observer->beforeWriteCalled, 'observer before_write event not triggered'); + $this->assertTrue($observer->afterWriteCalled, 'observer after_write event not triggered'); + $this->assertEquals('observer_updated_test10', $result->name); + + // 测试删除事件 + // 创建两条记录用于测试 + $record1 = $model::create(['name' => 'test11', 'status' => 1]); + $record2 = $model::create(['name' => 'test12', 'status' => 0]); + + // 重置标志 + $observer->beforeDeleteCalled = false; + $observer->afterDeleteCalled = false; + + // 尝试删除状态为1的记录 + $record1->delete(); + $this->assertTrue($observer->beforeDeleteCalled, 'observer before_delete event not triggered'); + $this->assertTrue($observer->afterDeleteCalled, 'observer after_delete event not triggered'); + + // 重置标志 + $observer->beforeDeleteCalled = false; + $observer->afterDeleteCalled = false; + + // 尝试删除状态为0的记录 + $record2->delete(); + $this->assertTrue($observer->beforeDeleteCalled, 'observer before_delete event not triggered'); + $this->assertFalse($observer->afterDeleteCalled, 'observer after_delete event should not be triggered'); + + // 验证记录2仍然存在 + $this->assertNotNull($model::find($record2->id)); + } +} \ No newline at end of file diff --git a/tests/orm/ModelFieldMappingTest.php b/tests/orm/ModelFieldMappingTest.php new file mode 100644 index 00000000..6e51d855 --- /dev/null +++ b/tests/orm/ModelFieldMappingTest.php @@ -0,0 +1,120 @@ + 1, 'user_name' => 'user1', 'user_age' => 25, 'is_active' => 1, 'user_info' => json_encode(['city' => 'beijing']), 'create_at' => '2023-01-01 10:00:00'], + ['id' => 2, 'user_name' => 'user2', 'user_age' => 30, 'is_active' => 0, 'user_info' => json_encode(['city' => 'shanghai']), 'create_at' => '2023-01-02 11:00:00'], + ]; + Db::table('test_field_mapping')->insertAll(self::$testData); + } + + public function testBasicFieldMapping() + { + $model = new class extends Model { + protected $table = 'test_field_mapping'; + protected $jsonAssoc = true; + + // 定义字段映射,仅包含字段名称映射 + protected $mapping = [ + 'name' => 'user_name', + 'age' => 'user_age', + 'active' => 'is_active', + 'info' => 'user_info', + 'createdAt' => 'create_at', + ]; + }; + + // 测试读取映射字段 + $item = $model::find(1); + $this->assertEquals('user1', $item->name); + $this->assertEquals(25, $item->age); + $this->assertEquals(1, $item->active); + $this->assertEquals(['city' => 'beijing'], $item->info); + $this->assertEquals('2023-01-01 10:00:00', $item->createdAt); + + // 测试使用映射字段写入 + $newItem = $model::create([ + 'name' => 'user3', + 'age' => 35, + 'active' => 1, + 'info' => ['city' => 'guangzhou'], + 'createdAt' => '2023-01-03 12:00:00', + ]); + + // 验证数据库中的实际字段 + $dbItem = Db::table('test_field_mapping')->where('id', $newItem->id)->find(); + $this->assertEquals('user3', $dbItem['user_name']); + $this->assertEquals(35, $dbItem['user_age']); + $this->assertEquals(1, $dbItem['is_active']); + $this->assertEquals(['city' => 'guangzhou'], json_decode($dbItem['user_info'], true)); + $this->assertEquals('2023-01-03 12:00:00', $dbItem['create_at']); + } + + public function testCustomTypeMapping() + { + $model = new class extends Model { + protected $table = 'test_field_mapping'; + + // 定义自定义类型转换 + protected $type = [ + 'user_age' => 'integer', + 'is_active' => 'boolean', + 'user_info' => 'array', + 'create_at' => 'datetime', + ]; + + // 自定义获取器 + public function getUserAgeAttr($value) + { + return $value . '岁'; + } + + // 自定义修改器 + public function setUserAgeAttr($value) + { + return intval($value); + } + }; + + // 测试自定义类型转换和获取器 + $item = $model::find(1); + $this->assertEquals('25岁', $item->user_age); + + // 测试自定义修改器 + $item->user_age = '30岁'; + $item->save(); + + $dbItem = Db::table('test_field_mapping')->where('id', 1)->find(); + $this->assertEquals(30, $dbItem['user_age']); + } +} \ No newline at end of file diff --git a/tests/orm/ModelFieldTypeTest.php b/tests/orm/ModelFieldTypeTest.php index c4ff55d8..ec33f5fb 100644 --- a/tests/orm/ModelFieldTypeTest.php +++ b/tests/orm/ModelFieldTypeTest.php @@ -7,12 +7,17 @@ use tests\stubs\FieldTypeModel; use tests\stubs\TestFieldJsonDTO; use tests\stubs\TestFieldPhpDTO; +use tests\stubs\UserStatus; use think\facade\Db; +use think\model\type\Date; +use think\model\type\DateTime; class ModelFieldTypeTest extends TestCase { + protected static $testData; + public static function setUpBeforeClass(): void - { + { Db::execute('DROP TABLE IF EXISTS `test_field_type`;'); Db::execute( << 1, 't_json' => '???Invalid', @@ -83,4 +97,183 @@ public function testFieldReadInvalid() $this->assertNull($model->t_json); $this->assertNull($model->t_php); } + + public function testEnumTypeConversion() + { + $model = new class extends \think\Model { + protected $table = 'test_field_type'; + protected $type = [ + 'status' => UserStatus::class, + ]; + }; + + // 测试写入时的枚举类型转换 + $testData = [ + 'status' => UserStatus::Active, + ]; + $result = $model::create($testData); + $this->assertInstanceOf(UserStatus::class, $result->status); + $this->assertEquals(UserStatus::Active, $result->status); + + // 测试数据库实际存储值 + $dbResult = Db::table('test_field_type')->where('id', $result->id)->find(); + $this->assertEquals('active', $dbResult['status']); + + // 测试从数据库读取时的枚举类型转换 + $model = $model::find($result->id); + $this->assertInstanceOf(UserStatus::class, $model->status); + $this->assertEquals(UserStatus::Active, $model->status); + + // 测试更新枚举类型 + $model->status = UserStatus::Inactive; + $model->save(); + $dbResult = Db::table('test_field_type')->where('id', $result->id)->find(); + $this->assertEquals('inactive', $dbResult['status']); + + // 测试无效的枚举值 + $model = new $model(['status' => 'invalid_status']); + $this->assertNull($model->status); + } + + public function testBasicTypeConversion() + { + $model = new class extends \think\Model { + protected $table = 'test_field_type'; + protected $type = [ + 'int_field' => 'integer', + 'float_field' => 'float', + 'bool_field' => 'boolean', + 'string_field' => 'string', + 'array_field' => 'array', + 'object_field' => 'object', + 'date_field' => 'date', + 'datetime_field' => 'datetime', + 'timestamp_field' => 'timestamp', + ]; + }; + + $testData = [ + 'int_field' => '123', + 'float_field' => '123.45', + 'bool_field' => '1', + 'string_field' => 123, + 'array_field' => ['a' => 1, 'b' => 2], + 'object_field' => ['name' => 'test', 'value' => 100], + 'date_field' => '2023-12-25', + 'datetime_field' => '2023-12-25 12:34:56', + 'timestamp_field' => '2023-12-25 12:34:56', + ]; + + // 测试写入时的类型转换 + $result = $model::create($testData); + $array = $result->toArray(); + $this->assertIsInt($result->int_field); + $this->assertEquals(123, $result->int_field); + + $this->assertIsFloat($result->float_field); + $this->assertEquals(123.45, $result->float_field); + + $this->assertIsBool($result->bool_field); + $this->assertTrue($result->bool_field); + + $this->assertIsString($result->string_field); + $this->assertEquals('123', $result->string_field); + + $this->assertIsArray($result->array_field); + $this->assertEquals(['a' => 1, 'b' => 2], $result->array_field); + + $this->assertIsObject($result->object_field); + $this->assertEquals('test', $result->object_field->name); + + $this->assertInstanceOf(Date::class, $result->date_field); + $this->assertEquals('2023-12-25', $result->date_field->format('Y-m-d')); + $this->assertEquals('2023-12-25', $array['date_field']); + + $this->assertInstanceOf(DateTime::class, $result->datetime_field); + $this->assertEquals('2023-12-25 12:34:56', $result->datetime_field->format('Y-m-d H:i:s')); + $this->assertEquals('2023-12-25 12:34:56', $array['datetime_field']); + + $this->assertInstanceOf(DateTime::class, $result->timestamp_field); + $this->assertEquals('2023-12-25 12:34:56', $result->timestamp_field->format('Y-m-d H:i:s')); + $this->assertEquals('2023-12-25 12:34:56', $array['timestamp_field']); + + // 测试数据库实际存储值 + $dbResult = Db::table('test_field_type')->where('id', $result->id)->find(); + $this->assertEquals(123, $dbResult['int_field']); + $this->assertEquals(123.45, $dbResult['float_field']); + $this->assertEquals(1, $dbResult['bool_field']); + $this->assertEquals('123', $dbResult['string_field']); + $this->assertEquals(['a' => 1, 'b' => 2], json_decode($dbResult['array_field'], true)); + $this->assertEquals(['name' => 'test', 'value' => 100], json_decode($dbResult['object_field'], true)); + $this->assertEquals('2023-12-25', $dbResult['date_field']); + $this->assertEquals('2023-12-25 12:34:56', $dbResult['datetime_field']); + $this->assertEquals('2023-12-25 12:34:56', $dbResult['timestamp_field']); + } + + public function testModelOutput() + { + $model = new class extends \think\Model { + protected $table = 'test_field_type'; + protected $type = [ + 'int_field' => 'integer', + 'float_field' => 'float', + 'bool_field' => 'boolean', + 'array_field' => 'array', + 'object_field' => 'object', + 'date_field' => 'date', + ]; + + // 定义获取器 + public function getFullNameAttr() + { + return 'test_' . $this->string_field; + } + }; + + $testData = [ + 'int_field' => 123, + 'float_field' => 123.45, + 'bool_field' => true, + 'string_field' => 'test', + 'array_field' => ['a' => 1, 'b' => 2], + 'object_field' => ['name' => 'test'], + 'date_field' => '2023-12-25', + ]; + + $result = $model::create($testData); + + // 测试toArray输出 + $array = $result->toArray(); + $this->assertIsArray($array); + $this->assertEquals($testData['int_field'], $array['int_field']); + $this->assertEquals($testData['float_field'], $array['float_field']); + $this->assertEquals($testData['bool_field'], $array['bool_field']); + $this->assertEquals($testData['string_field'], $array['string_field']); + $this->assertEquals($testData['array_field'], $array['array_field']); + + // 测试toJson输出 + $json = $result->toJson(); + $this->assertJson($json); + $decodedJson = json_decode($json, true); + $this->assertEquals($array, $decodedJson); + + // 测试hidden属性 + $result->hidden(['int_field', 'float_field']); + $hiddenArray = $result->toArray(); + $this->assertArrayNotHasKey('int_field', $hiddenArray); + $this->assertArrayNotHasKey('float_field', $hiddenArray); + + // 测试visible属性 + $result->visible(['int_field', 'string_field']); + $visibleArray = $result->toArray(); + $this->assertCount(2, $visibleArray); + $this->assertArrayHasKey('int_field', $visibleArray); + $this->assertArrayHasKey('string_field', $visibleArray); + + // 测试append属性 + $result->append(['full_name']); + $appendArray = $result->toArray(); + $this->assertArrayHasKey('full_name', $appendArray); + $this->assertEquals('test_' . $testData['string_field'], $appendArray['full_name']); + } } diff --git a/tests/orm/ModelHasManyThroughTest.php b/tests/orm/ModelHasManyThroughTest.php new file mode 100644 index 00000000..9e0a1cda --- /dev/null +++ b/tests/orm/ModelHasManyThroughTest.php @@ -0,0 +1,168 @@ + 'China' + ]); + + $country2 = CountryModel::create([ + 'name' => 'USA' + ]); + + $author1 = ThroughAuthorModel::create([ + 'country_id' => $country1->id, + 'name' => 'author1', + 'email' => 'author1@example.com' + ]); + + $author2 = ThroughAuthorModel::create([ + 'country_id' => $country1->id, + 'name' => 'author2', + 'email' => 'author2@example.com' + ]); + + $author3 = ThroughAuthorModel::create([ + 'country_id' => $country2->id, + 'name' => 'author3', + 'email' => 'author3@example.com' + ]); + + ThroughPostModel::create([ + 'author_id' => $author1->id, + 'title' => 'Post1', + 'content' => 'Content1' + ]); + + ThroughPostModel::create([ + 'author_id' => $author1->id, + 'title' => 'Post2', + 'content' => 'Content2' + ]); + + ThroughPostModel::create([ + 'author_id' => $author2->id, + 'title' => 'Post3', + 'content' => 'Content3' + ]); + + ThroughPostModel::create([ + 'author_id' => $author3->id, + 'title' => 'Post4', + 'content' => 'Content4' + ]); + } + + public function testHasManyThrough() + { + // 测试关联获取 + $country = CountryModel::find(1); + $this->assertNotNull($country); + + $posts = $country->posts; + $this->assertCount(3, $posts); + $this->assertEquals('Post1', $posts[0]->title); + + // 测试预加载 + $country = CountryModel::with(['posts'])->find(1); + $this->assertTrue($country->isRelationLoaded('posts')); + $this->assertCount(3, $country->posts); + + // 测试关联统计 + $country = CountryModel::withCount('posts')->find(1); + $this->assertEquals(3, $country->posts_count); + + // 测试条件查询 + $posts = $country->posts()->where('test_through_post.title', 'like', '%1%')->select(); + $this->assertCount(1, $posts); + $this->assertEquals('Post1', $posts[0]->title); + + // 测试排序 + $posts = $country->posts()->order('test_through_post.title', 'desc')->select(); + $this->assertEquals('Post3', $posts[0]->title); + + // 测试字段查询 + $posts = $country->posts()->field('test_through_post.title')->select(); + $this->assertArrayNotHasKey('content', $posts[0]->toArray()); + } +} + +class CountryModel extends Model +{ + protected $table = 'test_country'; + protected $autoWriteTimestamp = true; + + public function posts() + { + return $this->hasManyThrough( + ThroughPostModel::class, + ThroughAuthorModel::class, + 'country_id', + 'author_id', + 'id', + 'id' + ); + } +} + +class ThroughAuthorModel extends Model +{ + protected $table = 'test_through_author'; + protected $autoWriteTimestamp = true; +} + +class ThroughPostModel extends Model +{ + protected $table = 'test_through_post'; + protected $autoWriteTimestamp = true; +} \ No newline at end of file diff --git a/tests/orm/ModelHasOneThroughTest.php b/tests/orm/ModelHasOneThroughTest.php new file mode 100644 index 00000000..89a62ce9 --- /dev/null +++ b/tests/orm/ModelHasOneThroughTest.php @@ -0,0 +1,140 @@ + 'user1' + ]); + + $user2 = UserThroughModel::create([ + 'name' => 'user2' + ]); + + $profile1 = ProfileThroughModel::create([ + 'email' => 'user1@example.com', + 'nickname' => 'nickname1' + ]); + + $profile2 = ProfileThroughModel::create([ + 'email' => 'user2@example.com', + 'nickname' => 'nickname2' + ]); + + AccountThroughModel::create([ + 'user_id' => $user1->id, + 'profile_id' => $profile1->id, + 'account' => 'account1' + ]); + + AccountThroughModel::create([ + 'user_id' => $user2->id, + 'profile_id' => $profile2->id, + 'account' => 'account2' + ]); + } + + public function testHasOneThrough() + { + // 测试关联获取 + $user = UserThroughModel::find(1); + $this->assertNotNull($user); + + $profile = $user->profile; + $this->assertNotNull($profile); + $this->assertEquals('user1@example.com', $profile->email); + $this->assertEquals('nickname1', $profile->nickname); + + // 测试预加载 + $user = UserThroughModel::with(['profile'])->find(1); + $this->assertTrue($user->isRelationLoaded('profile')); + $this->assertEquals('user1@example.com', $user->profile->email); + + // 测试关联查询条件 + $user = UserThroughModel::hasWhere('profile', ['nickname' => 'nickname1'])->find(); + $this->assertNotNull($user); + $this->assertEquals('user1', $user->name); + + // 测试关联统计 + $user = UserThroughModel::withCount('profile')->find(1); + $this->assertEquals(1, $user->profile_count); + } +} + +class UserThroughModel extends Model +{ + protected $table = 'test_user_through'; + protected $autoWriteTimestamp = true; + + public function profile() + { + return $this->hasOneThrough( + ProfileThroughModel::class, + AccountThroughModel::class, + 'user_id', + 'id', + 'id', + 'profile_id' + ); + } +} + +class AccountThroughModel extends Model +{ + protected $table = 'test_account_through'; + protected $autoWriteTimestamp = true; +} + +class ProfileThroughModel extends Model +{ + protected $table = 'test_profile_through'; + protected $autoWriteTimestamp = true; +} \ No newline at end of file diff --git a/tests/orm/ModelJsonFieldTest.php b/tests/orm/ModelJsonFieldTest.php new file mode 100644 index 00000000..7c460fa3 --- /dev/null +++ b/tests/orm/ModelJsonFieldTest.php @@ -0,0 +1,182 @@ + 1, + 'name' => 'test1', + 'info' => json_encode(['age' => 18, 'city' => 'beijing']), + 'tags' => json_encode(['php', 'mysql']), + ], + [ + 'id' => 2, + 'name' => 'test2', + 'info' => json_encode(['age' => 20, 'city' => 'shanghai']), + 'tags' => json_encode(['java', 'redis']), + ], + ]; + } + + public function testJsonField() + { + $model = new class extends Model { + protected $table = 'test_json_model'; + protected $autoWriteTimestamp = true; + }; + + // 测试JSON字段写入 + $data = [ + 'name' => 'test3', + 'info' => ['age' => 25, 'city' => 'guangzhou'], + 'tags' => ['python', 'mongodb'], + ]; + $result = $model::create($data); + + $this->assertInstanceOf(Model::class, $result); + $this->assertNotEmpty($result->id); + $this->assertEquals($data['name'], $result->name); + $this->assertEquals($data['info']['age'], $result->info->age); + $this->assertEquals($data['tags'], $result->tags); + + // 测试JSON字段查询 + $found = $model::where('info->age', 25)->find(); + $this->assertNotNull($found); + $this->assertEquals($data['info']['age'], $found->info->age); + + // 测试JSON数组字段 + $withTag = $model::where('tags', 'like', '%python%')->find(); + $this->assertNotNull($withTag); + $this->assertContains('python', $withTag->tags); + + // 测试JSON字段更新 + $updateData = ['info->age' => 26]; + $result = $model::where('id', $found->id)->update($updateData); + $this->assertTrue($result > 0); + + $updated = $model::find($found->id); + $this->assertEquals(26, $updated->info->age); + } + + public function testJsonArrayOperations() + { + Db::table('test_json_model')->insertAll(self::$testData); + + $model = new class extends Model { + protected $table = 'test_json_model'; + protected $jsonAssoc = true; + }; + + // 测试whereJsonContains方法 - 简单值 + $result = $model::whereJsonContains('tags', 'php')->find(); + $this->assertNotNull($result); + $this->assertEquals(1, $result->id); + $this->assertContains('php', $result->tags); + + // 测试whereJsonContains方法 - 不存在的值 + $result = $model::whereJsonContains('tags', 'python')->find(); + $this->assertNull($result); + + // 测试whereJsonContains方法 - 对象值 + $result = $model::whereJsonContains('info', ['city' => 'beijing'])->find(); + $this->assertNotNull($result); + $this->assertEquals(1, $result->id); + $this->assertEquals('beijing', $result->info['city']); + + // 测试whereJsonContains方法 - 多个条件 + $result = $model::whereJsonContains('info', ['age' => 20]) + ->whereJsonContains('tags', 'redis') + ->find(); + $this->assertNotNull($result); + $this->assertEquals(2, $result->id); + $this->assertEquals(20, $result->info['age']); + $this->assertContains('redis', $result->tags); + + // 测试whereJsonContains方法 - 无效的JSON字段 + $result = $model::whereJsonContains('name', 'test1')->find(); + $this->assertNull($result); + + // 测试whereJsonContains方法 - 空值 + $result = $model::whereJsonContains('tags', null)->find(); + $this->assertNull($result); + + // 测试whereJsonContains方法 - 复杂对象 + $model::create([ + 'name' => 'test3', + 'info' => ['address' => ['city' => 'guangzhou', 'street' => 'test']], + 'tags' => ['vue', 'react'] + ]); + + $result = $model::whereJsonContains('info', ['address' => ['city' => 'guangzhou']])->find(); + $this->assertNotNull($result); + $this->assertEquals('test3', $result->name); + $this->assertEquals('guangzhou', $result->info['address']['city']); + + $model = new class extends Model { + protected $table = 'test_json_model'; + }; + + // 测试JSON数组追加 + $record = $model::find(1); + $tags = $record->tags; + $tags[] = 'nginx'; + $record->tags = $tags; + $record->save(); + + $updated = $model::find(1); + $this->assertContains('nginx', $updated->tags); + + // 测试JSON数组条件查询 + $results = $model::where('tags', 'like', '%mysql%')->select(); + $this->assertCount(1, $results); + $this->assertContains('mysql', $results[0]->tags); + } + + public function testJsonFieldValidation() + { + $model = new class extends Model { + protected $table = 'test_json_model'; + protected $jsonAssoc = true; // 设置JSON反序列化为数组 + }; + + // 测试无效JSON数据处理 + $data = [ + 'name' => 'test4', + 'info' => 'invalid json', + 'tags' => ['valid', 'array'], + ]; + + $result = $model::create($data); + $this->assertIsArray($result->info); // 应该被转换为空数组 + $this->assertIsArray($result->tags); + $this->assertEquals($data['tags'], $result->tags); + } +} \ No newline at end of file diff --git a/tests/orm/ModelManyToManyTest.php b/tests/orm/ModelManyToManyTest.php new file mode 100644 index 00000000..e1152041 --- /dev/null +++ b/tests/orm/ModelManyToManyTest.php @@ -0,0 +1,192 @@ + 'student1', + 'email' => 'student1@example.com' + ]); + + $student2 = StudentModel::create([ + 'name' => 'student2', + 'email' => 'student2@example.com' + ]); + + $course1 = CourseModel::create([ + 'title' => 'Math', + 'credit' => 3 + ]); + + $course2 = CourseModel::create([ + 'title' => 'English', + 'credit' => 2 + ]); + + $course3 = CourseModel::create([ + 'title' => 'Physics', + 'credit' => 4 + ]); + + // 建立关联关系 + $student1->courses()->attach($course1->id, ['score' => 85.5]); + $student1->courses()->attach($course2->id, ['score' => 92.0]); + $student2->courses()->attach($course2->id, ['score' => 88.5]); + $student2->courses()->attach($course3->id, ['score' => 90.0]); + } + + public function testManyToManySync() + { + // 测试基本同步功能 + $student = StudentModel::find(1); + $result = $student->courses()->sync([2, 3]); // 同步为English和Physics课程 + + $this->assertTrue($result['attached'] === [3]); // 新增Physics + $this->assertTrue($result['detached'] === [1]); // 移除Math + $this->assertTrue($result['updated'] === [2]); // 保持English + + $courses = $student->courses()->select(); + $this->assertCount(2, $courses); + $this->assertEquals(['English', 'Physics'], $courses->column('title')); + + // 测试带额外数据的同步 + $syncData = [ + 2 => ['score' => 95.0], // 更新English成绩 + 3 => ['score' => 88.0] // 更新Physics成绩 + ]; + $result = $student->courses()->sync($syncData); + + $this->assertTrue($result['updated'] === [2, 3]); // 更新了两门课的成绩 + + $courses = $student->courses()->select(); + foreach ($courses as $course) { + if ($course->title === 'English') { + $this->assertEquals(95.0, $course->pivot->score); + } elseif ($course->title === 'Physics') { + $this->assertEquals(88.0, $course->pivot->score); + } + } + + // 测试清空后重新同步 + $result = $student->courses()->sync([]); + $this->assertTrue($result['detached'] === [2, 3]); // 移除所有课程 + $this->assertCount(0, $student->courses()->select()); + + // 测试同步单个ID + $result = $student->courses()->sync(1, ['score' => 91.0]); + $this->assertTrue($result['attached'] === [1]); + + $course = $student->courses()->find(); + $this->assertEquals('Math', $course->title); + $this->assertEquals(91.0, $course->pivot->score); + } + + public function testManyToManyRelation() + { + // 测试关联获取 + $student = StudentModel::find(1); + $this->assertNotNull($student); + + $courses = $student->courses; + $this->assertCount(2, $courses); + $this->assertEquals('Math', $courses[0]->title); + + // 测试预加载 + $student = StudentModel::with(['courses'])->find(1); + $this->assertTrue($student->isRelationLoaded('courses')); + $this->assertCount(2, $student->courses); + + // 测试中间表数据 + $student = StudentModel::find(1); + $course = $student->courses()->where('test_course.title', 'Math')->find(); + $this->assertEquals(85.5, $course->pivot->score); + + // 测试关联统计 + $student = StudentModel::withCount('courses')->find(1); + $this->assertEquals(2, $student->courses_count); + + // 测试新增关联 + $student = StudentModel::find(2); + $result = $student->courses()->attach(1, ['score' => 87.5]); + $this->assertTrue($result); + + // 测试解除关联 + $result = $student->courses()->detach(1); + $this->assertTrue($result); + } +} + +class StudentModel extends Model +{ + protected $table = 'test_student'; + protected $autoWriteTimestamp = true; + + public function courses() + { + return $this->belongsToMany(CourseModel::class, 'test_student_course', 'course_id', 'student_id') + ->withPivot(['score']) + ->withTimestamp(); + } +} + +class CourseModel extends Model +{ + protected $table = 'test_course'; + protected $autoWriteTimestamp = true; + + public function students() + { + return $this->belongsToMany(StudentModel::class, 'test_student_course', 'student_id', 'course_id') + ->withPivot(['score']) + ->withTimestamp(); + } +} \ No newline at end of file diff --git a/tests/orm/ModelMorphTest.php b/tests/orm/ModelMorphTest.php new file mode 100644 index 00000000..a5759630 --- /dev/null +++ b/tests/orm/ModelMorphTest.php @@ -0,0 +1,199 @@ + 'Test Post', + 'content' => 'Post content' + ]); + + $video = VideoModel::create([ + 'title' => 'Test Video', + 'url' => 'https://example.com/video.mp4', + 'duration' => 300 + ]); + + // 创建评论数据 + CommentModel::create([ + 'content' => 'Comment on post', + 'morphable_type' => PostModel::class, + 'morphable_id' => $post->id, + 'user_id' => 1 + ]); + + CommentModel::create([ + 'content' => 'Another comment on post', + 'morphable_type' => PostModel::class, + 'morphable_id' => $post->id, + 'user_id' => 2 + ]); + + CommentModel::create([ + 'content' => 'Comment on video', + 'morphable_type' => VideoModel::class, + 'morphable_id' => $video->id, + 'user_id' => 1 + ]); + } + + public function testMorphOne() + { + $post = PostModel::find(1); + $this->assertNotNull($post); + + // 测试获取最新的一条评论 + $latestComment = $post->latestComment; + $this->assertNotNull($latestComment); + $this->assertEquals('Another comment on post', $latestComment->content); + + // 测试预加载 + $post = PostModel::with(['latestComment'])->find(1); + $this->assertTrue($post->isRelationLoaded('latestComment')); + $this->assertNotNull($post->latestComment); + } + + public function testMorphMany() + { + $post = PostModel::find(1); + $this->assertNotNull($post); + + // 测试获取所有评论 + $comments = $post->comments; + $this->assertCount(2, $comments); + + // 测试预加载 + $post = PostModel::with(['comments'])->find(1); + $this->assertTrue($post->isRelationLoaded('comments')); + $this->assertCount(2, $post->comments); + + // 测试关联统计 + $post = PostModel::withCount('comments')->find(1); + $this->assertEquals(2, $post->comments_count); + + // 测试新增关联 + $result = $post->comments()->save([ + 'content' => 'New comment on post', + 'user_id' => 3 + ]); + $this->assertNotNull($result); + $this->assertEquals(3, $post->comments()->count()); + + // 测试视频评论 + $video = VideoModel::find(1); + $this->assertNotNull($video); + $this->assertCount(1, $video->comments); + } + + public function testMorphTo() + { + $comment = CommentModel::find(1); + $this->assertNotNull($comment); + + // 测试获取关联的内容 + $commentable = $comment->commentable; + $this->assertInstanceOf(PostModel::class, $commentable); + $this->assertEquals('Test Post', $commentable->title); + + // 测试预加载 + $comment = CommentModel::with(['commentable'])->find(3); + $this->assertTrue($comment->isRelationLoaded('commentable')); + $this->assertInstanceOf(VideoModel::class, $comment->commentable); + $this->assertEquals('Test Video', $comment->commentable->title); + } +} + +class PostModel extends Model +{ + protected $table = 'test_post'; + protected $autoWriteTimestamp = true; + + public function comments() + { + return $this->morphMany(CommentModel::class, 'morphable'); + } + + public function latestComment() + { + return $this->morphOne(CommentModel::class, 'morphable') + ->order('id', 'desc'); + } +} + +class VideoModel extends Model +{ + protected $table = 'test_video'; + protected $autoWriteTimestamp = true; + + public function comments() + { + return $this->morphMany(CommentModel::class, 'morphable'); + } + + public function latestComment() + { + return $this->morphOne(CommentModel::class, 'morphable') + ->order('id', 'desc'); + } +} + +class CommentModel extends Model +{ + protected $table = 'test_comment'; + protected $autoWriteTimestamp = true; + + public function commentable() + { + return $this->morphTo('morphable'); + } +} \ No newline at end of file diff --git a/tests/orm/ModelOneToManyTest.php b/tests/orm/ModelOneToManyTest.php new file mode 100644 index 00000000..179f9eae --- /dev/null +++ b/tests/orm/ModelOneToManyTest.php @@ -0,0 +1,137 @@ + 'author1', + 'email' => 'author1@example.com' + ]); + + $author2 = AuthorModel::create([ + 'name' => 'author2', + 'email' => 'author2@example.com' + ]); + + // 为作者1创建文章 + PostModel::create([ + 'author_id' => $author1->id, + 'title' => 'Post 1 by author1', + 'content' => 'Content of post 1', + 'status' => 1 + ]); + + PostModel::create([ + 'author_id' => $author1->id, + 'title' => 'Post 2 by author1', + 'content' => 'Content of post 2', + 'status' => 1 + ]); + + // 为作者2创建文章 + PostModel::create([ + 'author_id' => $author2->id, + 'title' => 'Post 1 by author2', + 'content' => 'Content of post 1', + 'status' => 0 + ]); + } + + public function testHasManyRelation() + { + // 测试关联获取 + $author = AuthorModel::find(1); + $this->assertNotNull($author); + + $posts = $author->posts; + $this->assertCount(2, $posts); + $this->assertEquals('Post 1 by author1', $posts[0]->title); + + // 测试预加载 + $author = AuthorModel::with(['posts'])->find(1); + $this->assertTrue($author->isRelationLoaded('posts')); + $this->assertCount(2, $author->posts); + + // 测试关联条件 + $author = AuthorModel::with(['posts' => function($query) { + $query->where('status', 1); + }])->find(2); + $this->assertCount(0, $author->posts); + + // 测试关联统计 + $author = AuthorModel::withCount('posts')->find(1); + $this->assertEquals(2, $author->posts_count); + + // 测试关联写入 + $author = AuthorModel::find(2); + $result = $author->posts()->save([ + 'title' => 'New post by author2', + 'content' => 'New content', + 'status' => 1 + ]); + $this->assertNotNull($result); + $this->assertEquals($author->id, $result->author_id); + } +} + +class AuthorModel extends Model +{ + protected $table = 'test_author'; + protected $autoWriteTimestamp = true; + + public function posts() + { + return $this->hasMany(PostModel::class, 'author_id', 'id'); + } +} + +class PostModel extends Model +{ + protected $table = 'test_post'; + protected $autoWriteTimestamp = true; + + public function author() + { + return $this->belongsTo(AuthorModel::class, 'author_id', 'id'); + } +} \ No newline at end of file diff --git a/tests/orm/ModelOneToOneTest.php b/tests/orm/ModelOneToOneTest.php index 19ba6974..b8cb7366 100644 --- a/tests/orm/ModelOneToOneTest.php +++ b/tests/orm/ModelOneToOneTest.php @@ -55,7 +55,7 @@ public function testBindAttr() $userID = $user->id; // 预载入时绑定 - $user = UserModel::with('profile')->find($userID); + $user = UserModel::with(['profile'])->find($userID); $this->assertEquals( [$userID, $email, $nickname], [$user->id, $user->email, $user->new_name] @@ -72,5 +72,107 @@ public function testBindAttr() [$user->id, $user->email, $user->nick_name, $user->true_name] ); } + /** + * 测试基础关联查询 + */ + public function testBasicRelation() + { + $user = new UserModel(); + $user->account = 'thinkphp'; + $user->save(); + $profile = new ProfileModel(); + $profile->email = 'test@thinkphp.cn'; + $profile->nickname = 'test'; + $profile->uid = $user->id; + $profile->save(); + + // 测试hasOne关联 + $user = UserModel::find($user->id); + $this->assertNotNull($user->profile); + $this->assertEquals('test@thinkphp.cn', $user->profile->email); + + // 测试belongsTo关联 + $profile = ProfileModel::find($profile->id); + $this->assertNotNull($profile->user); + $this->assertEquals('thinkphp', $profile->user->account); + } + + /** + * 测试预加载查询 + */ + public function testEagerLoading() + { + // 创建测试数据 + $user1 = new UserModel(['account' => 'user1']); + $user1->save(); + $user2 = new UserModel(['account' => 'user2']); + $user2->save(); + + $profile1 = new ProfileModel([ + 'uid' => $user1->id, + 'email' => 'user1@thinkphp.cn', + 'nickname' => 'nickname1' + ]); + $profile1->save(); + + $profile2 = new ProfileModel([ + 'uid' => $user2->id, + 'email' => 'user2@thinkphp.cn', + 'nickname' => 'nickname2' + ]); + $profile2->save(); + + // 测试with预加载 + $users = UserModel::with(['profile'])->select(); + $this->assertCount(2, $users); + $this->assertEquals('user1@thinkphp.cn', $users[0]->profile->email); + $this->assertEquals('user2@thinkphp.cn', $users[1]->profile->email); + + // 测试预加载条件 + $users = UserModel::with(['profile' => function($query) { + $query->where('nickname', 'nickname1'); + }])->select(); + $this->assertNotNull($users[0]->profile); + $this->assertNull($users[1]->profile); + } + + /** + * 测试关联数据的新增和更新 + */ + public function testRelationSave() + { + // 测试关联新增 + $user = new UserModel(); + $user->account = 'newuser'; + $user->profile = ['email' => 'new@thinkphp.cn', 'nickname' => 'newnick']; + $user->together(['profile'])->save(); + + $this->assertNotNull($user->profile); + $this->assertEquals('new@thinkphp.cn', $user->profile->email); + + // 测试关联更新 + $user->profile->email = 'updated@thinkphp.cn'; + $user->together(['profile'])->save(); + + $profile = ProfileModel::find($user->profile->id); + $this->assertEquals('updated@thinkphp.cn', $profile->email); + } + + /** + * 测试关联删除 + */ + public function testRelationDelete() + { + $user = new UserModel(); + $user->account = 'deletetest'; + $user->profile = ['email' => 'delete@thinkphp.cn', 'nickname' => 'deletenick']; + $user->together(['profile'])->save(); + + $profileId = $user->profile->id; + $user->delete(); + + // 验证关联数据是否被删除 + $this->assertNull(ProfileModel::find($profileId)); + } } diff --git a/tests/orm/ModelSoftDeleteTest.php b/tests/orm/ModelSoftDeleteTest.php new file mode 100644 index 00000000..1cf1efcb --- /dev/null +++ b/tests/orm/ModelSoftDeleteTest.php @@ -0,0 +1,155 @@ + 1, 'name' => 'item1', 'status' => 1], + ['id' => 2, 'name' => 'item2', 'status' => 0], + ['id' => 3, 'name' => 'item3', 'status' => 1], + ]; + Db::table('test_soft_delete')->insertAll(self::$testData); + } + + public function testBasicSoftDelete() + { + $model = new class extends Model { + protected $table = 'test_soft_delete'; + use \think\model\concern\SoftDelete; + protected $deleteTime = 'delete_time'; + }; + + // 测试软删除 + $item = $model::find(1); + $this->assertNotNull($item); + $item->delete(); + + // 验证软删除后无法通过普通查询获取 + $deletedItem = $model::find(1); + $this->assertNull($deletedItem); + + // 验证软删除字段已设置 + $rawData = Db::table('test_soft_delete')->where('id', 1)->find(); + $this->assertNotNull($rawData['delete_time']); + } + + public function testSoftDeleteQuery() + { + $model = new class extends Model { + protected $table = 'test_soft_delete'; + use \think\model\concern\SoftDelete; + protected $deleteTime = 'delete_time'; + }; + + // 软删除一些数据 + $model::find(1)->delete(); + $model::find(2)->delete(); + + // 测试默认查询不包含软删除数据 + $list = $model::select(); + $this->assertEquals(1, count($list)); + + // 测试包含软删除数据的查询 + $listWithTrashed = $model::withTrashed()->select(); + $this->assertEquals(3, count($listWithTrashed)); + + // 测试仅查询软删除数据 + $trashedOnly = $model::onlyTrashed()->select(); + $this->assertEquals(2, count($trashedOnly)); + } + + public function testSoftDeleteRestore() + { + $model = new class extends Model { + protected $table = 'test_soft_delete'; + use \think\model\concern\SoftDelete; + protected $deleteTime = 'delete_time'; + }; + + // 软删除一条数据 + $item = $model::find(1); + $item->delete(); + + // 恢复软删除的数据 + $trashedItem = $model::onlyTrashed()->find(1); + $this->assertNotNull($trashedItem); + $trashedItem->restore(); + + // 验证恢复后可以正常查询 + $restoredItem = $model::find(1); + $this->assertNotNull($restoredItem); + $this->assertNull($restoredItem->delete_time); + } + + public function testSoftDeleteScope() + { + $model = new class extends Model { + protected $table = 'test_soft_delete'; + use \think\model\concern\SoftDelete; + protected $deleteTime = 'delete_time'; + + public function scopeActive($query) + { + return $query->where('status', 1); + } + }; + + // 软删除一条激活状态的数据 + $model::find(1)->delete(); + + // 测试查询作用域和软删除的结合 + $activeItems = $model::active()->select(); + $this->assertEquals(1, count($activeItems)); + + // 测试包含软删除数据的作用域查询 + $allActiveItems = $model::withTrashed()->active()->select(); + $this->assertEquals(2, count($allActiveItems)); + } + + public function testBatchSoftDelete() + { + $model = new class extends Model { + protected $table = 'test_soft_delete'; + use \think\model\concern\SoftDelete; + protected $deleteTime = 'delete_time'; + }; + + // 批量软删除 + $model::where('status', 1)->delete(); + + // 验证软删除结果 + $remainingItems = $model::select(); + $this->assertEquals(1, count($remainingItems)); + $this->assertEquals(0, $remainingItems[0]->status); + + // 验证软删除数据仍在数据库中 + $allItems = $model::withTrashed()->select(); + $this->assertEquals(3, count($allItems)); + } +} \ No newline at end of file diff --git a/tests/orm/ModelTest.php b/tests/orm/ModelTest.php new file mode 100644 index 00000000..8ffa5e42 --- /dev/null +++ b/tests/orm/ModelTest.php @@ -0,0 +1,252 @@ + 1, 'name' => 'test1', 'status' => 1], + ['id' => 2, 'name' => 'test2', 'status' => 0], + ['id' => 3, 'name' => 'test3', 'status' => 1], + ]; + } + + public function testCreate() + { + $model = new class extends Model + { + protected $table = 'test_model'; + protected $autoWriteTimestamp = true; + }; + + $data = ['name' => 'test4', 'status' => 1]; + $result = $model::create($data); + + $this->assertInstanceOf(Model::class, $result); + $this->assertNotEmpty($result->id); + $this->assertEquals($data['name'], $result->name); + $this->assertEquals($data['status'], $result->status); + $this->assertNotEmpty($result->create_time); + } + + public function testUpdate() + { + Db::table('test_model')->insertAll(self::$testData); + + $model = new class extends Model + { + protected $table = 'test_model'; + protected $autoWriteTimestamp = true; + }; + + $updateData = ['name' => 'updated', 'status' => 0]; + $result = $model::update($updateData, ['id' => 1]); + + $this->assertInstanceOf(Model::class, $result); + $this->assertEquals(1, $result->id); + $this->assertEquals($updateData['name'], $result->name); + $this->assertEquals($updateData['status'], $result->status); + $this->assertNotEmpty($result->update_time); + } + + public function testDelete() + { + Db::table('test_model')->insertAll(self::$testData); + + $model = new class extends Model + { + protected $table = 'test_model'; + }; + + $result = $model::destroy(1); + $this->assertTrue($result); + + $count = Db::table('test_model')->where('id', 1)->count(); + $this->assertEquals(0, $count); + } + + public function testChangeDetection() + { + $model = new class extends Model + { + protected $table = 'test_model'; + protected $autoWriteTimestamp = true; + }; + + // 测试新建模型时的变更检测 + $data = ['name' => 'test5', 'status' => 1]; + $model = new $model($data); + + // 测试更新模型时的变更检测 + $model->name = 'updated'; + $this->assertTrue($model->isChange('name')); + $this->assertFalse($model->isChange('status')); + $this->assertEquals(['name' => 'updated'], $model->getChangedData()); + + // 测试多字段变更 + $model->status = 0; + $this->assertTrue($model->isChange('name')); + $this->assertTrue($model->isChange('status')); + $this->assertEquals(['name' => 'updated', 'status' => 0], $model->getChangedData()); + $model->save(); + + // 测试设置相同的值不会触发变更 + $model->name = 'updated'; + $this->assertFalse($model->isChange('name')); + $this->assertEquals([], $model->getChangedData()); + } + + public function testSave() + { + $model = new class extends Model + { + protected $table = 'test_model'; + protected $autoWriteTimestamp = true; + }; + + $data = ['name' => 'test5', 'status' => 1]; + $model = new $model($data); + $result = $model->save(); + + $this->assertTrue($result); + $this->assertNotEmpty($model->id); + $this->assertEquals($data['name'], $model->name); + $this->assertEquals($data['status'], $model->status); + $this->assertNotEmpty($model->create_time); + } + + public function testEvent() + { + $eventCalled = false; + + $model = new class extends Model + { + protected $table = 'test_model'; + + public function onBeforeInsert() + { + global $eventCalled; + $eventCalled = true; + } + }; + + $model::create(['name' => 'test6', 'status' => 1]); + $this->assertTrue($eventCalled); + } + + public function testGlobalScope() + { + Db::table('test_model')->insertAll(self::$testData); + + $model = new class extends Model + { + protected $table = 'test_model'; + protected $globalScope = ['status']; + + public function scopeStatus($query) + { + return $query->where('status', 1); + } + }; + + $result = $model::select(); + $this->assertCount(2, $result); + foreach ($result as $item) { + $this->assertEquals(1, $item->status); + } + } + + public function testLocalScope() + { + Db::table('test_model')->insertAll(self::$testData); + + $model = new class extends Model + { + protected $table = 'test_model'; + + public function scopeActive($query) + { + return $query->where('status', 1); + } + + public function scopeNameLike($query, $name) + { + return $query->where('name', 'like', "%{$name}%"); + } + }; + + // 测试基本查询范围 + $result = $model::active()->select(); + $this->assertCount(2, $result); + foreach ($result as $item) { + $this->assertEquals(1, $item->status); + } + + // 测试带参数的查询范围 + $result = $model::nameLike('test1')->select(); + $this->assertCount(1, $result); + $this->assertEquals('test1', $result[0]->name); + + // 测试组合查询范围 + $result = $model::active()->nameLike('test')->select(); + $this->assertCount(2, $result); + foreach ($result as $item) { + $this->assertEquals(1, $item->status); + $this->assertStringContainsString('test', $item->name); + } + } + + public function testRemoveScope() + { + Db::table('test_model')->insertAll(self::$testData); + + $model = new class extends Model + { + protected $table = 'test_model'; + protected $globalScope = ['status']; + + public function scopeStatus($query) + { + return $query->where('status', 1); + } + }; + + // 测试移除全局查询范围 + $result = $model::withoutGlobalScope()->select(); + $this->assertCount(3, $result); + + // 测试指定移除某个全局查询范围 + $result = $model::withoutGlobalScope(['status'])->select(); + $this->assertCount(3, $result); + + // 测试移除多个全局查询范围 + $result = $model::withoutGlobalScope(['status', 'other'])->select(); + $this->assertCount(3, $result); + } +} diff --git a/tests/orm/ModelViewTest.php b/tests/orm/ModelViewTest.php new file mode 100644 index 00000000..8cecd8d0 --- /dev/null +++ b/tests/orm/ModelViewTest.php @@ -0,0 +1,165 @@ + '张三', 'status' => 1, 'create_time' => '2023-01-01 12:00:00'], + ['name' => '李四', 'status' => 1, 'create_time' => '2023-01-02 12:00:00'], + ]; + Db::table('test_user_view')->insertAll($users); + + $orders = [ + ['user_id' => 1, 'order_no' => 'ORDER001', 'amount' => 100.00, 'create_time' => '2023-01-01 12:00:00'], + ['user_id' => 1, 'order_no' => 'ORDER002', 'amount' => 200.00, 'create_time' => '2023-01-01 13:00:00'], + ['user_id' => 2, 'order_no' => 'ORDER003', 'amount' => 300.00, 'create_time' => '2023-01-02 12:00:00'], + ]; + Db::table('test_order_view')->insertAll($orders); + } + + public function testBasicView() + { + // 定义基本视图模型 + class UserOrderView extends View + { + public function query($query) + { + $query->view('test_user_view', 'id as user_id,name,status') + ->view('test_order_view', 'order_no,amount,create_time', 'test_user_view.id=test_order_view.user_id'); + } + } + + // 测试基本查询 + $model = new UserOrderView; + $result = $model->query()->select()->toArray(); + $this->assertCount(3, $result); + $this->assertEquals('张三', $result[0]['name']); + $this->assertEquals('ORDER001', $result[0]['order_no']); + $this->assertEquals(100.00, $result[0]['amount']); + $this->assertEquals('2023-01-01 12:00:00', $result[0]['create_time']); + } + + public function testViewWithCondition() + { + // 定义带条件的视图模型 + class UserOrderConditionView extends View + { + public function query($query) + { + $query->view('test_user_view', 'id as user_id,name,status,create_time') + ->view('test_order_view', 'order_no,amount', 'test_user_view.id=test_order_view.user_id'); + } + } + + // 测试条件查询 + $model = new UserOrderConditionView; + $result = $model + ->where('test_user_view.status', 1) + ->where('test_order_view.amount', '>', 100) + ->order('test_order_view.amount', 'desc') + ->select() + ->toArray(); + + $this->assertCount(2, $result); + $this->assertEquals(300.00, $result[0]['amount']); + $this->assertEquals('李四', $result[0]['name']); + $this->assertEquals('2023-01-02 12:00:00', $result[0]['create_time']); + } + + public function testViewWithAggregate() + { + // 定义聚合视图模型 + class UserOrderAggregateView extends View + { + public function query($query) + { + $query->view('test_user_view', 'id as user_id,name,create_time') + ->view('test_order_view', 'COUNT(id) as order_count,SUM(amount) as total_amount,MIN(create_time) as first_order_time', 'test_user_view.id=test_order_view.user_id') + ->group('test_user_view.id'); + } + } + + // 测试聚合查询 + $model = new UserOrderAggregateView; + $result = $model->select()->toArray(); + $this->assertCount(2, $result); + + // 验证张三的订单统计 + $this->assertEquals('张三', $result[0]['name']); + $this->assertEquals(2, $result[0]['order_count']); + $this->assertEquals(300.00, $result[0]['total_amount']); + $this->assertEquals('2023-01-01 12:00:00', $result[0]['first_order_time']); + + // 验证李四的订单统计 + $this->assertEquals('李四', $result[1]['name']); + $this->assertEquals(1, $result[1]['order_count']); + $this->assertEquals(300.00, $result[1]['total_amount']); + $this->assertEquals('2023-01-02 12:00:00', $result[1]['first_order_time']); + } + + public function testViewWithJoinType() + { + // 定义带连接类型的视图模型 + class UserOrderJoinTypeView extends View + { + public function query($query) + { + $query->view('test_user_view', 'id as user_id,name,status') + ->view('test_order_view', 'order_no,amount', 'test_user_view.id=test_order_view.user_id', 'RIGHT'); + } + } + + // 测试右连接查询 + $model = new UserOrderJoinTypeView; + $result = $model->select()->toArray(); + $this->assertCount(3, $result); + + // 验证连接结果 + $orderAmounts = array_column($result, 'amount'); + sort($orderAmounts); + $this->assertEquals([100.00, 200.00, 300.00], $orderAmounts); + } +} \ No newline at end of file diff --git a/tests/orm/ModelVirtualTest.php b/tests/orm/ModelVirtualTest.php new file mode 100644 index 00000000..dc09955d --- /dev/null +++ b/tests/orm/ModelVirtualTest.php @@ -0,0 +1,57 @@ + 'test', 'age' => 18]; + $this->assertTrue($model->save($data)); + $this->assertEquals($data, $model->getData()); + + // 测试更新数据 + $updateData = ['age' => 20]; + $this->assertTrue($model->save($updateData)); + $this->assertEquals(20, $model->getData('age')); + + // 测试删除数据 + $this->assertTrue($model->delete()); + $this->assertEmpty($model->getData()); + } + + public function testVirtualModelCreate() + { + // 定义虚拟模型 + $virtualModelClass = new class extends Virtual + { + protected $pk = 'id'; + protected $data = []; + }; + + // 测试create方法创建实例 + $data = ['name' => 'virtual', 'age' => 25]; + $model = $virtualModelClass::create($data); + + // 验证创建的实例 + $this->assertInstanceOf(Virtual::class, $model); + $this->assertEquals($data, $model->getData()); + $this->assertEquals('virtual', $model->getData('name')); + $this->assertEquals(25, $model->getData('age')); + } +} diff --git a/tests/stubs/UserStatus.php b/tests/stubs/UserStatus.php new file mode 100644 index 00000000..2479f2fa --- /dev/null +++ b/tests/stubs/UserStatus.php @@ -0,0 +1,11 @@ +