From abaa2194ab21b0f6037d286e6a409d608e2d1014 Mon Sep 17 00:00:00 2001 From: Albert Chen Date: Tue, 30 Sep 2025 12:42:21 +0800 Subject: [PATCH 01/14] Support options `batch` for `migrate:rollback`. (#7531) --- src/Commands/Migrations/RollbackCommand.php | 2 ++ src/Migrations/DatabaseMigrationRepository.php | 15 +++++++++++++++ src/Migrations/MigrationRepositoryInterface.php | 8 ++++++++ src/Migrations/Migrator.php | 4 ++++ tests/DatabaseMigrationRepositoryTest.php | 15 +++++++++++++++ tests/DatabaseMigrationRollbackCommandTest.php | 17 ++++++++++++++--- 6 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/Commands/Migrations/RollbackCommand.php b/src/Commands/Migrations/RollbackCommand.php index 6d93cf6..31ff6b5 100755 --- a/src/Commands/Migrations/RollbackCommand.php +++ b/src/Commands/Migrations/RollbackCommand.php @@ -45,6 +45,7 @@ public function handle() [ 'pretend' => $this->input->getOption('pretend'), 'step' => (int) $this->input->getOption('step'), + 'batch' => (int) $this->input->getOption('batch'), ] ); } @@ -63,6 +64,7 @@ protected function getOptions() ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], ['pretend', null, InputOption::VALUE_NONE, 'Dump the SQL queries that would be run'], ['step', null, InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted'], + ['batch', null, InputOption::VALUE_OPTIONAL, 'The batch of migrations (identified by their batch number) to be reverted'], ]; } } diff --git a/src/Migrations/DatabaseMigrationRepository.php b/src/Migrations/DatabaseMigrationRepository.php index 8453453..ea8a85b 100755 --- a/src/Migrations/DatabaseMigrationRepository.php +++ b/src/Migrations/DatabaseMigrationRepository.php @@ -88,6 +88,21 @@ public function getMigrationBatches() ->pluck('batch', 'migration')->all(); } + /** + * Get the list of the migrations by batch number. + * + * @param int $batch + * @return array + */ + public function getMigrationsByBatch($batch) + { + return $this->table() + ->where('batch', $batch) + ->orderBy('migration', 'desc') + ->get() + ->all(); + } + /** * Log that a migration was run. * diff --git a/src/Migrations/MigrationRepositoryInterface.php b/src/Migrations/MigrationRepositoryInterface.php index db06bf9..0154f19 100755 --- a/src/Migrations/MigrationRepositoryInterface.php +++ b/src/Migrations/MigrationRepositoryInterface.php @@ -43,6 +43,14 @@ public function getLast(); */ public function getMigrationBatches(); + /** + * Get the list of the migrations by batch number. + * + * @param int $batch + * @return array + */ + public function getMigrationsByBatch($batch); + /** * Log that a migration was run. * diff --git a/src/Migrations/Migrator.php b/src/Migrations/Migrator.php index ec1a479..8557470 100755 --- a/src/Migrations/Migrator.php +++ b/src/Migrations/Migrator.php @@ -355,6 +355,10 @@ protected function getMigrationsForRollback(array $options): array return $this->repository->getMigrations($steps); } + if (($batch = $options['batch'] ?? 0) > 0) { + return $this->repository->getMigrationsByBatch($batch); + } + return $this->repository->getLast(); } diff --git a/tests/DatabaseMigrationRepositoryTest.php b/tests/DatabaseMigrationRepositoryTest.php index afe2e56..1e54197 100755 --- a/tests/DatabaseMigrationRepositoryTest.php +++ b/tests/DatabaseMigrationRepositoryTest.php @@ -139,6 +139,21 @@ public function testCreateRepositoryCreatesProperDatabaseTable() $repo->createRepository(); } + public function testGetMigrationsByBatchReturnsCorrectMigrations() + { + $repo = $this->getRepository(); + $query = Mockery::mock(Builder::class); + $connectionMock = Mockery::mock(Connection::class); + $repo->getConnectionResolver()->shouldReceive('connection')->with(null)->andReturn($connectionMock); + $repo->getConnection()->shouldReceive('table')->once()->with('migrations')->andReturn($query); + $query->shouldReceive('where')->once()->with('batch', '5')->andReturn($query); + $query->shouldReceive('orderBy')->once()->with('migration', 'desc')->andReturn($query); + $query->shouldReceive('get')->once()->andReturn(new Collection(['migration2', 'migration1'])); + $query->shouldReceive('useWritePdo')->once()->andReturn($query); + + $this->assertEquals(['migration2', 'migration1'], $repo->getMigrationsByBatch('5')); + } + protected function getRepository() { return new DatabaseMigrationRepository(Mockery::mock(ConnectionResolverInterface::class), 'migrations'); diff --git a/tests/DatabaseMigrationRollbackCommandTest.php b/tests/DatabaseMigrationRollbackCommandTest.php index b6adb47..34e8a86 100755 --- a/tests/DatabaseMigrationRollbackCommandTest.php +++ b/tests/DatabaseMigrationRollbackCommandTest.php @@ -49,7 +49,7 @@ public function testRollbackCommandCallsMigratorWithProperArguments() $migrator->shouldReceive('paths')->once()->andReturn([]); $migrator->shouldReceive('setConnection')->once()->with('default'); $migrator->shouldReceive('setOutput')->once()->andReturn($migrator); - $migrator->shouldReceive('rollback')->once()->with([BASE_PATH . DIRECTORY_SEPARATOR . 'migrations'], ['pretend' => false, 'step' => 0]); + $migrator->shouldReceive('rollback')->once()->with([BASE_PATH . DIRECTORY_SEPARATOR . 'migrations'], ['pretend' => false, 'step' => 0, 'batch' => 0]); $this->runCommand($command); } @@ -60,11 +60,22 @@ public function testRollbackCommandCallsMigratorWithStepOption() $migrator->shouldReceive('paths')->once()->andReturn([]); $migrator->shouldReceive('setConnection')->once()->with('default'); $migrator->shouldReceive('setOutput')->once()->andReturn($migrator); - $migrator->shouldReceive('rollback')->once()->with([BASE_PATH . DIRECTORY_SEPARATOR . 'migrations'], ['pretend' => false, 'step' => 2]); + $migrator->shouldReceive('rollback')->once()->with([BASE_PATH . DIRECTORY_SEPARATOR . 'migrations'], ['pretend' => false, 'step' => 2, 'batch' => 0]); $this->runCommand($command, ['--step' => 2]); } + public function testRollbackCommandCallsMigratorWithBatchOption() + { + $command = new RollbackCommand($migrator = Mockery::mock(Migrator::class)); + $migrator->shouldReceive('paths')->once()->andReturn([]); + $migrator->shouldReceive('setConnection')->once()->with('default'); + $migrator->shouldReceive('setOutput')->once()->andReturn($migrator); + $migrator->shouldReceive('rollback')->once()->with([BASE_PATH . DIRECTORY_SEPARATOR . 'migrations'], ['pretend' => false, 'step' => 0, 'batch' => 2]); + + $this->runCommand($command, ['--batch' => 2]); + } + public function testRollbackCommandCanBePretended() { $command = new RollbackCommand($migrator = Mockery::mock(Migrator::class)); @@ -82,7 +93,7 @@ public function testRollbackCommandCanBePretendedWithStepOption() $migrator->shouldReceive('paths')->once()->andReturn([]); $migrator->shouldReceive('setConnection')->once()->with('foo'); $migrator->shouldReceive('setOutput')->once()->andReturn($migrator); - $migrator->shouldReceive('rollback')->once()->with([BASE_PATH . DIRECTORY_SEPARATOR . 'migrations'], ['pretend' => true, 'step' => 2]); + $migrator->shouldReceive('rollback')->once()->with([BASE_PATH . DIRECTORY_SEPARATOR . 'migrations'], ['pretend' => true, 'step' => 2, 'batch' => 0]); $this->runCommand($command, ['--pretend' => true, '--database' => 'foo', '--step' => 2]); } From 839650401f4da12fe970049a18d8f47f5d4ef64e Mon Sep 17 00:00:00 2001 From: Sasaya Date: Tue, 28 Oct 2025 20:54:39 +0800 Subject: [PATCH 02/14] Optimized the PHPDoc for `hyperf/database`. (#7511) --- src/Concerns/BuildsQueries.php | 21 +- src/Model/Builder.php | 82 +++--- src/Model/Collection.php | 51 ++-- src/Model/Concerns/HasRelationships.php | 138 ++++++--- src/Model/Concerns/QueriesRelationships.php | 124 ++++++-- src/Model/Model.php | 24 +- src/Model/ModelNotFoundException.php | 4 + src/Model/Relations/BelongsTo.php | 14 +- src/Model/Relations/BelongsToMany.php | 47 ++-- .../Concerns/InteractsWithPivotTable.php | 4 +- src/Model/Relations/HasMany.php | 6 + src/Model/Relations/HasManyThrough.php | 28 +- src/Model/Relations/HasOne.php | 6 + src/Model/Relations/HasOneOrMany.php | 28 +- src/Model/Relations/HasOneThrough.php | 11 +- src/Model/Relations/MorphMany.php | 6 + src/Model/Relations/MorphOne.php | 8 +- src/Model/Relations/MorphOneOrMany.php | 7 + src/Model/Relations/MorphTo.php | 16 +- src/Model/Relations/MorphToMany.php | 8 + src/Model/Relations/Relation.php | 20 +- src/Model/SoftDeletes.php | 9 +- src/Query/Builder.php | 22 +- tests/types/Autoload.php | 26 ++ tests/types/Model/Builder.php | 245 ++++++++++++++++ tests/types/Model/Collection.php | 187 ++++++++++++ tests/types/Model/Model.php | 77 +++++ tests/types/Model/ModelNotFoundException.php | 25 ++ tests/types/Model/Relations.php | 265 ++++++++++++++++++ tests/types/Query/Builder.php | 66 +++++ 30 files changed, 1357 insertions(+), 218 deletions(-) create mode 100644 tests/types/Autoload.php create mode 100644 tests/types/Model/Builder.php create mode 100644 tests/types/Model/Collection.php create mode 100644 tests/types/Model/Model.php create mode 100644 tests/types/Model/ModelNotFoundException.php create mode 100644 tests/types/Model/Relations.php create mode 100644 tests/types/Query/Builder.php diff --git a/src/Concerns/BuildsQueries.php b/src/Concerns/BuildsQueries.php index ee90712..daf05c1 100644 --- a/src/Concerns/BuildsQueries.php +++ b/src/Concerns/BuildsQueries.php @@ -23,7 +23,6 @@ use Hyperf\Database\Exception\RecordsNotFoundException; use Hyperf\Database\Model\Builder; use Hyperf\Database\Model\Collection; -use Hyperf\Database\Model\Model; use Hyperf\Database\Query\Expression; use Hyperf\Paginator\Contract\CursorPaginator as CursorPaginatorContract; use Hyperf\Paginator\Cursor; @@ -36,7 +35,7 @@ use function Hyperf\Collection\data_get; /** - * @template TValue + * @template TValue of object * @mixin Builder * @mixin \Hyperf\Database\Query\Builder */ @@ -48,6 +47,7 @@ trait BuildsQueries * Chunk the results of the query. * * @param int $count + * @param callable(BaseCollection, int): mixed $callback * @return bool */ public function chunk($count, callable $callback) @@ -85,6 +85,8 @@ public function chunk($count, callable $callback) /** * Run a map over each item while chunking. + * + * @param callable(TValue): mixed $callback */ public function chunkMap(callable $callback, int $count = 1000): BaseCollection { @@ -102,6 +104,7 @@ public function chunkMap(callable $callback, int $count = 1000): BaseCollection /** * Execute a callback over each item while chunking. * + * @param callable(TValue, int): mixed $callback * @param int $count * @return bool */ @@ -118,6 +121,8 @@ public function each(callable $callback, $count = 1000) /** * Query lazily, by chunks of the given size. + * + * @return LazyCollection */ public function lazy(int $chunkSize = 1000): LazyCollection { @@ -146,6 +151,8 @@ public function lazy(int $chunkSize = 1000): LazyCollection /** * Query lazily, by chunking the results of a query by comparing IDs. + * + * @return LazyCollection */ public function lazyById(int $chunkSize = 1000, ?string $column = null, ?string $alias = null): LazyCollection { @@ -154,6 +161,8 @@ public function lazyById(int $chunkSize = 1000, ?string $column = null, ?string /** * Query lazily, by chunking the results of a query by comparing IDs in descending order. + * + * @return LazyCollection */ public function lazyByIdDesc(int $chunkSize = 1000, ?string $column = null, ?string $alias = null): LazyCollection { @@ -164,7 +173,7 @@ public function lazyByIdDesc(int $chunkSize = 1000, ?string $column = null, ?str * Execute the query and get the first result. * * @param array $columns - * @return null|Model|object|static + * @return null|TValue */ public function first($columns = ['*']) { @@ -173,6 +182,8 @@ public function first($columns = ['*']) /** * Execute a callback over each item while chunking by ID. + * + * @param callable(TValue, int): mixed $callback */ public function eachById(callable $callback, int $count = 1000, ?string $column = null, ?string $alias = null): bool { @@ -188,6 +199,8 @@ public function eachById(callable $callback, int $count = 1000, ?string $column /** * Chunk the results of a query by comparing IDs in a given order. + * + * @param callable(BaseCollection, int): mixed $callback */ public function orderedChunkById(int $count, callable $callback, ?string $column = null, ?string $alias = null, bool $descending = false): bool { @@ -240,6 +253,8 @@ public function orderedChunkById(int $count, callable $callback, ?string $column /** * Chunk the results of a query by comparing IDs in descending order. + * + * @param callable(BaseCollection, int): mixed $callback */ public function chunkByIdDesc(int $count, callable $callback, ?string $column = null, ?string $alias = null): bool { diff --git a/src/Model/Builder.php b/src/Model/Builder.php index 0bdda32..55332ff 100755 --- a/src/Model/Builder.php +++ b/src/Model/Builder.php @@ -43,6 +43,10 @@ /** * @template TModel of Model + * + * @method bool chunk(int $count, callable(ModelCollection, int): (bool|void) $callback) + * @method bool chunkByIdDesc(int $count, callable(ModelCollection, int): (bool|void) $callback, null|string $column = null, null|string $alias = null) + * * @mixin \Hyperf\Database\Query\Builder */ class Builder @@ -71,22 +75,16 @@ class Builder /** * The relationships that should be eager loaded. - * - * @var array */ protected $eagerLoad = []; /** * All of the globally registered builder macros. - * - * @var array */ protected static $macros = []; /** * All of the locally registered builder macros. - * - * @var array */ protected $localMacros = []; @@ -99,8 +97,6 @@ class Builder /** * The methods that should be returned from query builder. - * - * @var array */ protected $passthru = [ 'insert', 'insertGetId', 'getBindings', 'toSql', 'toRawSql', 'insertOrIgnore', @@ -110,15 +106,11 @@ class Builder /** * Applied global scopes. - * - * @var array */ protected $scopes = []; /** * Removed global scopes. - * - * @var array */ protected $removedScopes = []; @@ -227,7 +219,7 @@ public function clone() /** * Create and return an un-saved model instance. * - * @return Model + * @return TModel */ public function make(array $attributes = []) { @@ -373,7 +365,7 @@ public function where($column, $operator = null, $value = null, $boolean = 'and' * @param array|Closure|string $column * @param null|mixed $operator * @param null|mixed $value - * @return Builder|static + * @return $this */ public function orWhere($column, $operator = null, $value = null) { @@ -423,7 +415,7 @@ public function oldest($column = null) /** * Create a collection of models from plain arrays. * - * @return ModelCollection + * @return ModelCollection */ public function hydrate(array $items) { @@ -439,7 +431,7 @@ public function hydrate(array $items) * * @param string $query * @param array $bindings - * @return ModelCollection + * @return ModelCollection */ public function fromQuery($query, $bindings = []) { @@ -451,9 +443,9 @@ public function fromQuery($query, $bindings = []) /** * Find a model by its primary key. * + * @param mixed $id * @param array $columns - * @param array|int|string $id - * @return null|Model|ModelCollection|static|static[] + * @return ($id is array ? ModelCollection : null|TModel) */ public function find($id, $columns = ['*']) { @@ -469,7 +461,7 @@ public function find($id, $columns = ['*']) * * @param array|Arrayable $ids * @param array $columns - * @return ModelCollection + * @return ModelCollection */ public function findMany($ids, $columns = ['*']) { @@ -485,7 +477,7 @@ public function findMany($ids, $columns = ['*']) * * @param array $columns * @param mixed $id - * @return Model|ModelCollection|static|static[] + * @return ($id is array ? ModelCollection : TModel) * @throws ModelNotFoundException */ public function findOrFail($id, $columns = ['*']) @@ -513,11 +505,7 @@ public function findOrFail($id, $columns = ['*']) * * @param (Closure(): TValue)|list|string $columns * @param null|(Closure(): TValue) $callback - * @return ( - * $id is (Arrayable|array) - * ? Collection - * : TModel|TValue - * ) + * @return ($id is (array|Arrayable) ? ModelCollection : TModel|TValue) */ public function findOr(mixed $id, array|Closure|string $columns = ['*'], ?Closure $callback = null): mixed { @@ -539,7 +527,7 @@ public function findOr(mixed $id, array|Closure|string $columns = ['*'], ?Closur * * @param array $columns * @param mixed $id - * @return Model|static + * @return ($id is array ? ModelCollection : TModel) */ public function findOrNew($id, $columns = ['*']) { @@ -553,7 +541,7 @@ public function findOrNew($id, $columns = ['*']) /** * Get the first record matching the attributes or instantiate it. * - * @return Model|static + * @return TModel */ public function firstOrNew(array $attributes, array $values = []) { @@ -567,7 +555,7 @@ public function firstOrNew(array $attributes, array $values = []) /** * Get the first record matching the attributes. If the record is not found, create it. * - * @return Model|static + * @return TModel */ public function firstOrCreate(array $attributes, array $values = []) { @@ -581,7 +569,7 @@ public function firstOrCreate(array $attributes, array $values = []) /** * Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record. * - * @return Model|static + * @return TModel */ public function createOrFirst(array $attributes = [], array $values = []) { @@ -595,7 +583,7 @@ public function createOrFirst(array $attributes = [], array $values = []) /** * Create or update a record matching the attributes, and fill it with values. * - * @return Model|static + * @return TModel */ public function updateOrCreate(array $attributes, array $values = []) { @@ -620,7 +608,7 @@ public function incrementOrCreate(array $attributes, string $column = 'count', f * Execute the query and get the first result or throw an exception. * * @param array $columns - * @return Model|static + * @return TModel * @throws ModelNotFoundException */ public function firstOrFail($columns = ['*']) @@ -635,8 +623,10 @@ public function firstOrFail($columns = ['*']) /** * Execute the query and get the first result or call a callback. * - * @param array|Closure $columns - * @return mixed|Model|static + * @template TValue + * @param array|(Closure(): TValue) $columns + * @param null|(Closure(): TValue) $callback + * @return ($columns is (Closure(): TValue) ? TModel|TValue : ($callback is null ? null|TModel : TModel|TValue)) */ public function firstOr($columns = ['*'], ?Closure $callback = null) { @@ -680,7 +670,7 @@ public function valueOrFail(Expression|string $column): mixed * Execute the query as a "select" statement. * * @param array $columns - * @return ModelCollection|static[] + * @return ModelCollection */ public function get($columns = ['*']) { @@ -700,7 +690,7 @@ public function get($columns = ['*']) * Get the hydrated models without eager loading. * * @param array $columns - * @return Model[]|static[] + * @return array */ public function getModels($columns = ['*']) { @@ -712,7 +702,8 @@ public function getModels($columns = ['*']) /** * Eager load the relationships for the models. * - * @return array + * @param array $models + * @return array */ public function eagerLoadRelations(array $models) { @@ -732,7 +723,7 @@ public function eagerLoadRelations(array $models) * Get the relation instance for the given relation name. * * @param string $name - * @return Relation + * @return Relation */ public function getRelation($name) { @@ -762,7 +753,7 @@ public function getRelation($name) /** * Get a generator for the given query. * - * @return Generator + * @return Generator */ public function cursor() { @@ -775,6 +766,7 @@ public function cursor() * Chunk the results of a query by comparing numeric IDs. * * @param int $count + * @param callable(ModelCollection, int): mixed $callback * @param null|string $column * @param null|string $alias * @return bool @@ -825,7 +817,7 @@ public function chunkById($count, callable $callback, $column = null, $alias = n * * @param string $column * @param null|string $key - * @return Collection + * @return Collection */ public function pluck($column, $key = null) { @@ -905,7 +897,7 @@ public function cursorPaginate(?int $perPage = null, array|string $columns = ['* /** * Save a new model and return the instance. * - * @return $this|Model + * @return TModel */ public function create(array $attributes = []) { @@ -917,7 +909,7 @@ public function create(array $attributes = []) /** * Save a new model and return the instance. Allow mass-assignment. * - * @return $this|Model + * @return TModel */ public function forceCreate(array $attributes) { @@ -1134,7 +1126,7 @@ public function without($relations) * Create a new instance of the model being queried. * * @param array $attributes - * @return Model|static + * @return TModel */ public function newModelInstance($attributes = []) { @@ -1211,7 +1203,7 @@ public function setEagerLoads(array $eagerLoad) /** * Get the model instance being queried. * - * @return Model|static + * @return TModel */ public function getModel() { @@ -1221,7 +1213,9 @@ public function getModel() /** * Set a model instance for the model being queried. * - * @return $this + * @template TNewModel of \Hyperf\Database\Model\Model + * @param TNewModel $model + * @return Builder */ public function setModel(Model $model) { diff --git a/src/Model/Collection.php b/src/Model/Collection.php index 2581124..a352820 100755 --- a/src/Model/Collection.php +++ b/src/Model/Collection.php @@ -18,6 +18,7 @@ use Hyperf\Contract\Arrayable; use Hyperf\Contract\CompressInterface; use Hyperf\Contract\UnCompressInterface; +use Hyperf\Database\Model\Relations\Relation; use Hyperf\Stringable\Str; use RuntimeException; @@ -39,7 +40,7 @@ class Collection extends BaseCollection implements CompressInterface * * @param mixed $key * @param TFindDefault $default - * @return static|TFindDefault|TModel + * @return ($key is array ? static : TFindDefault|TModel) */ public function find($key, $default = null) { @@ -67,7 +68,7 @@ public function find($key, $default = null) /** * Find a model in the collection by key or throw an exception. * - * @return TModel + * @return ($key is array ? static : TModel) * * @throws ModelNotFoundException */ @@ -98,7 +99,7 @@ public function findOrFail(mixed $key) /** * Load a set of relationships onto the collection. * - * @param array|string $relations + * @param array): mixed)|string>|string $relations * @return $this */ public function load($relations) @@ -119,7 +120,8 @@ public function load($relations) /** * Load a set of aggregations over relationship's column onto the collection. * - * @param array|string $relations + * @param array): mixed)|string>|string $relations + * @return $this */ public function loadAggregate(array|string $relations, string $column, ?string $function = null): static { @@ -153,9 +155,10 @@ public function loadAggregate(array|string $relations, string $column, ?string $ /** * Load a set of relationship's max column values onto the collection. * - * @param array|string $relations + * @param array): mixed)|string>|string $relations + * @return $this */ - public function loadMax(array $relations, string $column): static + public function loadMax(array|string $relations, string $column): static { return $this->loadAggregate($relations, $column, 'max'); } @@ -163,9 +166,10 @@ public function loadMax(array $relations, string $column): static /** * Load a set of relationship's min column values onto the collection. * - * @param array|string $relations + * @param array): mixed)|string>|string $relations + * @return $this */ - public function loadMin(array $relations, string $column): static + public function loadMin(array|string $relations, string $column): static { return $this->loadAggregate($relations, $column, 'min'); } @@ -173,7 +177,8 @@ public function loadMin(array $relations, string $column): static /** * Load a set of relationship's column summations onto the collection. * - * @param array|string $relations + * @param array): mixed)|string>|string $relations + * @return $this */ public function loadSum(array|string $relations, string $column): static { @@ -183,7 +188,8 @@ public function loadSum(array|string $relations, string $column): static /** * Load a set of relationship's average column values onto the collection. * - * @param array|string $relations + * @param array): mixed)|string>|string $relations + * @return $this */ public function loadAvg(array|string $relations, string $column): static { @@ -193,7 +199,7 @@ public function loadAvg(array|string $relations, string $column): static /** * Load a set of relationship counts onto the collection. * - * @param array|string $relations + * @param array): mixed)|string>|string $relations * @return $this */ public function loadCount($relations) @@ -225,7 +231,7 @@ public function loadCount($relations) /** * Load a set of relationships onto the collection if they are not already eager loaded. * - * @param array|string $relations + * @param array): mixed)|string>|string $relations * @return $this */ public function loadMissing($relations) @@ -261,7 +267,7 @@ public function loadMissing($relations) * Load a set of relationships onto the mixed relationship collection. * * @param string $relation - * @param array|string $relations + * @param array): mixed)|string>|string $relations * @return $this */ public function loadMorph($relation, $relations) @@ -285,7 +291,7 @@ public function loadMorph($relation, $relations) /** * Load a set of relationship counts onto the mixed relationship collection. * - * @param array $relations + * @param array): mixed)|string>|string $relations * @return $this */ public function loadMorphCount(string $relation, array $relations) @@ -338,7 +344,7 @@ public function contains($key, $operator = null, $value = null): bool /** * Get the array of primary keys. * - * @return array + * @return array */ public function modelKeys() { @@ -351,7 +357,6 @@ public function modelKeys() * Merge the collection with the given items. * * @param iterable $items - * @return static */ public function merge($items): static { @@ -370,7 +375,7 @@ public function merge($items): static * @template TMapValue * * @param callable(TModel, TKey): TMapValue $callback - * @return BaseCollection|static + * @return (TMapValue is Model ? static : BaseCollection) */ public function map(callable $callback): Enumerable { @@ -385,7 +390,7 @@ public function map(callable $callback): Enumerable * Reload a fresh model instance from the database for all the entities. * * @param array|string $with - * @return static + * @return static */ public function fresh($with = []) { @@ -411,7 +416,6 @@ public function fresh($with = []) * Diff the collection with the given items. * * @param iterable $items - * @return static */ public function diff($items): static { @@ -432,7 +436,6 @@ public function diff($items): static * Intersect the collection with the given items. * * @param iterable $items - * @return static */ public function intersect(mixed $items): static { @@ -452,7 +455,7 @@ public function intersect(mixed $items): static /** * Return only unique items from the collection. * - * @param null|(callable(TModel, TKey): bool)|string $key + * @param null|(callable(TModel, int): mixed)|string $key * @return static */ public function unique(mixed $key = null, bool $strict = false): static @@ -485,7 +488,7 @@ public function only($keys): static * Returns only the columns from the collection with the specified keys. * * @param null|array|TKey $keys - * @return static + * @return BaseCollection */ public function columns($keys) { @@ -608,7 +611,7 @@ public function getDictionary($items = null) * Get an array with the values of a given key. * * @param array|string $value - * @return BaseCollection + * @return BaseCollection */ public function pluck(array|string $value, ?string $key = null): Enumerable { @@ -618,7 +621,7 @@ public function pluck(array|string $value, ?string $key = null): Enumerable /** * Get the keys of the collection items. * - * @return BaseCollection + * @return BaseCollection */ public function keys(): Enumerable { diff --git a/src/Model/Concerns/HasRelationships.php b/src/Model/Concerns/HasRelationships.php index ee2512e..315693e 100644 --- a/src/Model/Concerns/HasRelationships.php +++ b/src/Model/Concerns/HasRelationships.php @@ -26,6 +26,7 @@ use Hyperf\Database\Model\Relations\HasOneThrough; use Hyperf\Database\Model\Relations\MorphMany; use Hyperf\Database\Model\Relations\MorphOne; +use Hyperf\Database\Model\Relations\MorphPivot; use Hyperf\Database\Model\Relations\MorphTo; use Hyperf\Database\Model\Relations\MorphToMany; use Hyperf\Database\Model\Relations\Pivot; @@ -100,10 +101,12 @@ public static function resolveRelationUsing($name, Closure $callback) /** * Define a one-to-one relationship. * - * @param string $related + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param class-string $related * @param string $foreignKey * @param string $localKey - * @return HasOne + * @return HasOne */ public function hasOne($related, $foreignKey = null, $localKey = null) { @@ -119,13 +122,16 @@ public function hasOne($related, $foreignKey = null, $localKey = null) /** * Define a has-one-through relationship. * - * @param string $related - * @param string $through + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TThroughModel of \Hyperf\Database\Model\Model + * + * @param class-string $related + * @param class-string $through * @param null|string $firstKey * @param null|string $secondKey * @param null|string $localKey * @param null|string $secondLocalKey - * @return HasOneThrough + * @return HasOneThrough */ public function hasOneThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null) { @@ -149,12 +155,14 @@ public function hasOneThrough($related, $through, $firstKey = null, $secondKey = /** * Define a polymorphic one-to-one relationship. * - * @param string $related + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param class-string $related * @param string $name * @param string $type * @param string $id * @param string $localKey - * @return MorphOne + * @return MorphOne */ public function morphOne($related, $name, $type = null, $id = null, $localKey = null) { @@ -172,11 +180,13 @@ public function morphOne($related, $name, $type = null, $id = null, $localKey = /** * Define an inverse one-to-one or many relationship. * - * @param string $related + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param class-string $related * @param string $foreignKey * @param string $ownerKey * @param string $relation - * @return BelongsTo + * @return BelongsTo */ public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null) { @@ -217,7 +227,7 @@ public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relat * @param string $type * @param string $id * @param string $ownerKey - * @return MorphTo + * @return MorphTo */ public function morphTo($name = null, $type = null, $id = null, $ownerKey = null) { @@ -254,10 +264,12 @@ public static function getActualClassNameForMorph($class) /** * Define a one-to-many relationship. * - * @param string $related + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param class-string $related * @param string $foreignKey * @param string $localKey - * @return HasMany + * @return HasMany */ public function hasMany($related, $foreignKey = null, $localKey = null) { @@ -278,13 +290,16 @@ public function hasMany($related, $foreignKey = null, $localKey = null) /** * Define a has-many-through relationship. * - * @param string $related - * @param string $through + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TThroughModel of \Hyperf\Database\Model\Model + * + * @param class-string $related + * @param class-string $through * @param null|string $firstKey * @param null|string $secondKey * @param null|string $localKey * @param null|string $secondLocalKey - * @return HasManyThrough + * @return HasManyThrough */ public function hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null) { @@ -308,12 +323,14 @@ public function hasManyThrough($related, $through, $firstKey = null, $secondKey /** * Define a polymorphic one-to-many relationship. * - * @param string $related + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param class-string $related * @param string $name * @param string $type * @param string $id * @param string $localKey - * @return MorphMany + * @return MorphMany */ public function morphMany($related, $name, $type = null, $id = null, $localKey = null) { @@ -334,14 +351,17 @@ public function morphMany($related, $name, $type = null, $id = null, $localKey = /** * Define a many-to-many relationship. * - * @param string $related + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param class-string $related * @param string $table * @param string $foreignPivotKey * @param string $relatedPivotKey * @param string $parentKey * @param string $relatedKey * @param string $relation - * @return BelongsToMany + * + * @return BelongsToMany */ public function belongsToMany( $related, @@ -390,7 +410,9 @@ public function belongsToMany( /** * Define a polymorphic many-to-many relationship. * - * @param string $related + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param class-string $related * @param string $name * @param string $table * @param string $foreignPivotKey @@ -398,7 +420,7 @@ public function belongsToMany( * @param string $parentKey * @param string $relatedKey * @param bool $inverse - * @return MorphToMany + * @return MorphToMany */ public function morphToMany( $related, @@ -449,14 +471,16 @@ public function morphToMany( /** * Define a polymorphic, inverse many-to-many relationship. * - * @param string $related + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param class-string $related * @param string $name * @param string $table * @param string $foreignPivotKey * @param string $relatedPivotKey * @param string $parentKey * @param string $relatedKey - * @return MorphToMany + * @return MorphToMany */ public function morphedByMany( $related, @@ -684,11 +708,18 @@ protected function newHasOne(Builder $query, Model $parent, $foreignKey, $localK /** * Instantiate a new HasOneThrough relationship. * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TThroughModel of \Hyperf\Database\Model\Model + * @template TParentModel of \Hyperf\Database\Model\Model + * + * @param Builder $query + * @param TParentModel $farParent + * @param TThroughModel $throughParent * @param string $firstKey * @param string $secondKey * @param string $localKey * @param string $secondLocalKey - * @return HasOneThrough + * @return HasOneThrough */ protected function newHasOneThrough(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey) { @@ -698,10 +729,13 @@ protected function newHasOneThrough(Builder $query, Model $farParent, Model $thr /** * Instantiate a new MorphOne relationship. * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param Builder $query * @param string $type * @param string $id * @param string $localKey - * @return MorphOne + * @return MorphOne */ protected function newMorphOne(Builder $query, Model $parent, $type, $id, $localKey) { @@ -711,10 +745,13 @@ protected function newMorphOne(Builder $query, Model $parent, $type, $id, $local /** * Instantiate a new BelongsTo relationship. * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param Builder $query * @param string $foreignKey * @param string $ownerKey * @param string $relation - * @return BelongsTo + * @return BelongsTo */ protected function newBelongsTo(Builder $query, Model $child, $foreignKey, $ownerKey, $relation) { @@ -728,7 +765,7 @@ protected function newBelongsTo(Builder $query, Model $child, $foreignKey, $owne * @param string $type * @param string $id * @param string $ownerKey - * @return MorphTo + * @return MorphTo */ protected function morphEagerTo($name, $type, $id, $ownerKey) { @@ -750,7 +787,7 @@ protected function morphEagerTo($name, $type, $id, $ownerKey) * @param string $type * @param string $id * @param string $ownerKey - * @return MorphTo + * @return MorphTo */ protected function morphInstanceTo($target, $name, $type, $id, $ownerKey) { @@ -771,11 +808,16 @@ protected function morphInstanceTo($target, $name, $type, $id, $ownerKey) /** * Instantiate a new MorphTo relationship. * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @param Builder $query + * @param TDeclaringModel $parent * @param string $foreignKey * @param string $ownerKey * @param string $type * @param string $relation - * @return MorphTo + * @return MorphTo */ protected function newMorphTo(Builder $query, Model $parent, $foreignKey, $ownerKey, $type, $relation) { @@ -797,9 +839,14 @@ protected function guessBelongsToRelation() /** * Instantiate a new HasMany relationship. * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @param Builder $query + * @param TDeclaringModel $parent * @param string $foreignKey * @param string $localKey - * @return HasMany + * @return HasMany */ protected function newHasMany(Builder $query, Model $parent, $foreignKey, $localKey) { @@ -809,11 +856,18 @@ protected function newHasMany(Builder $query, Model $parent, $foreignKey, $local /** * Instantiate a new HasManyThrough relationship. * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TIntermediateModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @param Builder $query + * @param TDeclaringModel $farParent + * @param TIntermediateModel $throughParent * @param string $firstKey * @param string $secondKey * @param string $localKey * @param string $secondLocalKey - * @return HasManyThrough + * @return HasManyThrough */ protected function newHasManyThrough(Builder $query, Model $farParent, Model $throughParent, $firstKey, $secondKey, $localKey, $secondLocalKey) { @@ -823,10 +877,15 @@ protected function newHasManyThrough(Builder $query, Model $farParent, Model $th /** * Instantiate a new MorphMany relationship. * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @param Builder $query + * @param TDeclaringModel $parent * @param string $type * @param string $id * @param string $localKey - * @return MorphMany + * @return MorphMany */ protected function newMorphMany(Builder $query, Model $parent, $type, $id, $localKey) { @@ -836,13 +895,19 @@ protected function newMorphMany(Builder $query, Model $parent, $type, $id, $loca /** * Instantiate a new BelongsToMany relationship. * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @param Builder $query + * @param TDeclaringModel $parent * @param string $table * @param string $foreignPivotKey * @param string $relatedPivotKey * @param string $parentKey * @param string $relatedKey * @param string $relationName - * @return BelongsToMany + * + * @return BelongsToMany */ protected function newBelongsToMany( Builder $query, @@ -860,6 +925,11 @@ protected function newBelongsToMany( /** * Instantiate a new MorphToMany relationship. * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @param Builder $query + * @param TDeclaringModel $parent * @param string $name * @param string $table * @param string $foreignPivotKey @@ -868,7 +938,7 @@ protected function newBelongsToMany( * @param string $relatedKey * @param string $relationName * @param bool $inverse - * @return MorphToMany + * @return MorphToMany */ protected function newMorphToMany( Builder $query, diff --git a/src/Model/Concerns/QueriesRelationships.php b/src/Model/Concerns/QueriesRelationships.php index 0cbe557..50b9c27 100644 --- a/src/Model/Concerns/QueriesRelationships.php +++ b/src/Model/Concerns/QueriesRelationships.php @@ -27,11 +27,14 @@ trait QueriesRelationships /** * Add a relationship count / exists condition to the query. * - * @param Relation|string $relation + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param \Hyperf\Database\Model\Relations\Relation|string $relation * @param string $operator * @param int $count * @param string $boolean - * @return Builder|static + * @param null|(Closure(Builder): void) $callback + * @return static */ public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null) { @@ -78,10 +81,10 @@ public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ? /** * Add a relationship count / exists condition to the query with an "or". * - * @param string $relation + * @param Relation|string $relation * @param string $operator * @param int $count - * @return Builder|static + * @return static */ public function orHas($relation, $operator = '>=', $count = 1) { @@ -91,9 +94,11 @@ public function orHas($relation, $operator = '>=', $count = 1) /** * Add a relationship count / exists condition to the query. * - * @param string $relation + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @param \Hyperf\Database\Model\Relations\Relation|string $relation * @param string $boolean - * @return Builder|static + * @param null|(Closure(Builder): void) $callback + * @return static */ public function doesntHave($relation, $boolean = 'and', ?Closure $callback = null) { @@ -103,8 +108,8 @@ public function doesntHave($relation, $boolean = 'and', ?Closure $callback = nul /** * Add a relationship count / exists condition to the query with an "or". * - * @param string $relation - * @return Builder|static + * @param Relation|string $relation + * @return static */ public function orDoesntHave($relation) { @@ -114,10 +119,13 @@ public function orDoesntHave($relation) /** * Add a relationship count / exists condition to the query with where clauses. * - * @param string $relation + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param \Hyperf\Database\Model\Relations\Relation|string $relation + * @param null|(Closure(Builder): void) $callback * @param string $operator * @param int $count - * @return Builder|static + * @return static */ public function whereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1) { @@ -128,6 +136,9 @@ public function whereHas($relation, ?Closure $callback = null, $operator = '>=', * Add a relationship count / exists condition to the query with where clauses. * * Also load the relationship with same condition. + * + * @param null|(Closure(\Hyperf\Database\Model\Builder<*>|\Hyperf\Database\Model\Relations\Relation<*, *, *>): void) $callback + * @return static */ public function withWhereHas(string $relation, ?Closure $callback = null, string $operator = '>=', int $count = 1): Builder|static { @@ -138,10 +149,12 @@ public function withWhereHas(string $relation, ?Closure $callback = null, string /** * Add a relationship count / exists condition to the query with where clauses and an "or". * - * @param string $relation + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @param \Hyperf\Database\Model\Relations\Relation|string $relation + * @param null|(Closure(Builder): void) $callback * @param string $operator * @param int $count - * @return Builder|static + * @return static */ public function orWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1) { @@ -151,8 +164,10 @@ public function orWhereHas($relation, ?Closure $callback = null, $operator = '>= /** * Add a relationship count / exists condition to the query with where clauses. * - * @param string $relation - * @return Builder|static + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @param \Hyperf\Database\Model\Relations\Relation|string $relation + * @param null|(Closure(Builder): void) $callback + * @return static */ public function whereDoesntHave($relation, ?Closure $callback = null) { @@ -162,8 +177,10 @@ public function whereDoesntHave($relation, ?Closure $callback = null) /** * Add a relationship count / exists condition to the query with where clauses and an "or". * - * @param string $relation - * @return Builder|static + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @param \Hyperf\Database\Model\Relations\Relation|string $relation + * @param null|(Closure(Builder): void) $callback + * @return static */ public function orWhereDoesntHave($relation, ?Closure $callback = null) { @@ -332,11 +349,14 @@ public function mergeConstraintsFrom(Builder $from) /** * Add a polymorphic relationship count / exists condition to the query with where clauses. * - * @param string $relation + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param MorphTo|string $relation * @param array|string $types + * @param null|(Closure(Builder, string): void) $callback * @param string $operator * @param int $count - * @return $this + * @return static */ public function whereHasMorph($relation, $types, ?Closure $callback = null, $operator = '>=', $count = 1) { @@ -346,10 +366,14 @@ public function whereHasMorph($relation, $types, ?Closure $callback = null, $ope /** * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or". * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param MorphTo|string $relation * @param array|string $types - * @return $this + * @param null|(Closure(Builder, string): void) $callback + * @return static */ - public function orWhereHasMorph(string $relation, $types, ?Closure $callback = null, string $operator = '>=', int $count = 1) + public function orWhereHasMorph(MorphTo|string $relation, $types, ?Closure $callback = null, string $operator = '>=', int $count = 1) { return $this->hasMorph($relation, $types, $operator, $count, 'or', $callback); } @@ -357,10 +381,14 @@ public function orWhereHasMorph(string $relation, $types, ?Closure $callback = n /** * Add a polymorphic relationship count / exists condition to the query with where clauses. * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param MorphTo|string $relation * @param array|string $types - * @return $this + * @param null|(Closure(Builder, string): void) $callback + * @return static */ - public function whereDoesntHaveMorph(string $relation, $types, ?Closure $callback = null) + public function whereDoesntHaveMorph(MorphTo|string $relation, $types, ?Closure $callback = null) { return $this->doesntHaveMorph($relation, $types, 'and', $callback); } @@ -368,18 +396,28 @@ public function whereDoesntHaveMorph(string $relation, $types, ?Closure $callbac /** * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or". * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param MorphTo|string $relation * @param array|string $types - * @return $this + * @param null|(Closure(Builder, string): void) $callback + * @return static */ - public function orWhereDoesntHaveMorph(string $relation, $types, ?Closure $callback = null) + public function orWhereDoesntHaveMorph(MorphTo|string $relation, $types, ?Closure $callback = null) { return $this->doesntHaveMorph($relation, $types, 'or', $callback); } /** * Add a basic where clause to a relationship query. + * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param Relation|string $relation + * @param array|(Closure(Builder): mixed)|Expression|string $column + * @return static */ - public function whereRelation(string $relation, array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null): Builder|static + public function whereRelation(Relation|string $relation, array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null): Builder|static { return $this->whereHas($relation, function ($query) use ($column, $operator, $value) { if ($column instanceof Closure) { @@ -392,8 +430,11 @@ public function whereRelation(string $relation, array|Closure|Expression|string /** * Add an "or where" clause to a relationship query. + * + * @param Relation<*, *, *>|string $relation + * @return static */ - public function orWhereRelation(string $relation, array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null): Builder|static + public function orWhereRelation(Relation|string $relation, array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null): Builder|static { return $this->orWhereHas($relation, function ($query) use ($column, $operator, $value) { if ($column instanceof Closure) { @@ -406,6 +447,9 @@ public function orWhereRelation(string $relation, array|Closure|Expression|strin /** * Add a polymorphic relationship condition to the query with a where clause. + * + * @param MorphTo<*, *>|string $relation + * @return static */ public function whereMorphRelation(MorphTo|string $relation, array|string $types, array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null): Builder|static { @@ -416,6 +460,9 @@ public function whereMorphRelation(MorphTo|string $relation, array|string $types /** * Add a polymorphic relationship condition to the query with an "or where" clause. + * + * @param MorphTo<*, *>|string $relation + * @return static */ public function orWhereMorphRelation(MorphTo|string $relation, array|string $types, array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null): Builder|static { @@ -427,16 +474,21 @@ public function orWhereMorphRelation(MorphTo|string $relation, array|string $typ /** * Add a polymorphic relationship count / exists condition to the query. * - * @param string $relation + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param MorphTo|string $relation * @param array|string $types * @param string $operator * @param int $count * @param string $boolean - * @return $this + * @param null|(Closure(Builder, string): void) $callback + * @return static */ public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null) { - $relation = $this->getRelationWithoutConstraints($relation); + if (is_string($relation)) { + $relation = $this->getRelationWithoutConstraints($relation); + } $types = (array) $types; @@ -468,6 +520,9 @@ public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boole /** * Add a polymorphic relationship count / exists condition to the query with an "or". + * + * @param MorphTo<*, *>|string $relation + * @return static */ public function orHasMorph(MorphTo|string $relation, array|string $types, string $operator = '>=', int $count = 1): Builder|static { @@ -477,17 +532,24 @@ public function orHasMorph(MorphTo|string $relation, array|string $types, string /** * Add a polymorphic relationship count / exists condition to the query. * + * @template TRelatedModel of \Hyperf\Database\Model\Model + * + * @param MorphTo|string $relation * @param array|string $types * @param string $boolean - * @return $this + * @param null|(Closure(Builder, string): void) $callback + * @return static */ - public function doesntHaveMorph(string $relation, $types, $boolean = 'and', ?Closure $callback = null) + public function doesntHaveMorph(MorphTo|string $relation, $types, $boolean = 'and', ?Closure $callback = null) { return $this->hasMorph($relation, $types, '<', 1, $boolean, $callback); } /** * Add a polymorphic relationship count / exists condition to the query with an "or". + * + * @param MorphTo<*, *>|string $relation + * @return static */ public function orDoesntHaveMorph(MorphTo|string $relation, array|string $types): Builder|static { diff --git a/src/Model/Model.php b/src/Model/Model.php index 822b3bc..1034632 100644 --- a/src/Model/Model.php +++ b/src/Model/Model.php @@ -390,7 +390,7 @@ public function newFromBuilder($attributes = [], $connection = null) * Begin querying the model on a given connection. * * @param null|string $connection - * @return Builder + * @return Builder */ public static function on($connection = null) { @@ -407,7 +407,7 @@ public static function on($connection = null) /** * Begin querying the model on the write connection. * - * @return QueryBuilder + * @return Builder */ public static function onWriteConnection() { @@ -418,7 +418,7 @@ public static function onWriteConnection() * Get all of the models from the database. * * @param array|mixed $columns - * @return Collection|static[] + * @return Collection */ public static function all($columns = ['*']) { @@ -429,7 +429,7 @@ public static function all($columns = ['*']) * Begin querying a model with eager loading. * * @param array|string $relations - * @return Builder|static + * @return Builder */ public static function with($relations) { @@ -754,7 +754,7 @@ public function forceDelete() /** * Begin querying the model. * - * @return Builder + * @return Builder */ public static function query() { @@ -764,7 +764,7 @@ public static function query() /** * Get a new query builder for the model's table. * - * @return Builder + * @return Builder */ public function newQuery() { @@ -774,7 +774,7 @@ public function newQuery() /** * Get a new query builder that doesn't have any global scopes or eager loading. * - * @return Builder|static + * @return Builder */ public function newModelQuery() { @@ -784,7 +784,7 @@ public function newModelQuery() /** * Get a new query builder with no relationships loaded. * - * @return Builder + * @return Builder */ public function newQueryWithoutRelationships() { @@ -809,7 +809,7 @@ public function registerGlobalScopes($builder) /** * Get a new query builder that doesn't have any global scopes. * - * @return Builder|static + * @return Builder */ public function newQueryWithoutScopes() { @@ -820,7 +820,7 @@ public function newQueryWithoutScopes() * Get a new query instance without a given scope. * * @param Scope|string $scope - * @return Builder + * @return Builder */ public function newQueryWithoutScope($scope) { @@ -831,7 +831,7 @@ public function newQueryWithoutScope($scope) * Get a new query to restore one or more models by their queueable IDs. * * @param array|int $ids - * @return Builder + * @return Builder */ public function newQueryForRestoration($ids) { @@ -853,7 +853,7 @@ public function newModelBuilder($query) /** * Create a new Model Collection instance. * - * @return Collection + * @return Collection */ public function newCollection(array $models = []) { diff --git a/src/Model/ModelNotFoundException.php b/src/Model/ModelNotFoundException.php index 6491493..e4f56d2 100755 --- a/src/Model/ModelNotFoundException.php +++ b/src/Model/ModelNotFoundException.php @@ -56,6 +56,8 @@ public function setModel(string $model, $ids = []) /** * Get the affected Model model. + * + * @return null|class-string */ public function getModel(): ?string { @@ -64,6 +66,8 @@ public function getModel(): ?string /** * Get the affected Model model IDs. + * + * @return array */ public function getIds(): array { diff --git a/src/Model/Relations/BelongsTo.php b/src/Model/Relations/BelongsTo.php index 6951f60..67c6c6a 100755 --- a/src/Model/Relations/BelongsTo.php +++ b/src/Model/Relations/BelongsTo.php @@ -17,6 +17,12 @@ use Hyperf\Database\Model\Model; use Hyperf\Database\Model\Relations\Concerns\SupportsDefaultModels; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @extends Relation + */ class BelongsTo extends Relation { use SupportsDefaultModels; @@ -172,8 +178,8 @@ public function update(array $attributes) /** * Associate the model instance to the given parent. * - * @param int|Model|string $model - * @return Model + * @param int|string|TRelatedModel $model + * @return TDeclaringModel */ public function associate($model) { @@ -193,7 +199,7 @@ public function associate($model) /** * Dissociate previously associated model from the given parent. * - * @return Model + * @return TDeclaringModel */ public function dissociate() { @@ -245,7 +251,7 @@ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder /** * Get the child of the relationship. * - * @return Model + * @return TDeclaringModel */ public function getChild() { diff --git a/src/Model/Relations/BelongsToMany.php b/src/Model/Relations/BelongsToMany.php index f704dbb..6b53b0c 100755 --- a/src/Model/Relations/BelongsToMany.php +++ b/src/Model/Relations/BelongsToMany.php @@ -27,6 +27,14 @@ use function Hyperf\Support\class_basename; use function Hyperf\Tappable\tap; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * @template TPivotModel of \Hyperf\Database\Model\Relations\Pivot = \Hyperf\Database\Model\Relations\Pivot + * @template TAccessor of string = 'pivot' + * + * @extends Relation> + */ class BelongsToMany extends Relation { use Concerns\InteractsWithPivotTable; @@ -362,9 +370,9 @@ public function orWherePivotIn($column, $values) /** * Find a related model by its primary key or return new instance of the related model. * - * @param array $columns * @param mixed $id - * @return \Hyperf\Collection\Collection|Model + * @param array $columns + * @return ($id is array ? Collection : object{pivot: Pivot}&TRelatedModel) */ public function findOrNew($id, $columns = ['*']) { @@ -378,7 +386,7 @@ public function findOrNew($id, $columns = ['*']) /** * Get the first related model record matching the attributes or instantiate it. * - * @return Model + * @return object{pivot: Pivot}&TRelatedModel */ public function firstOrNew(array $attributes) { @@ -393,7 +401,7 @@ public function firstOrNew(array $attributes) * Get the first related record matching the attributes. If the record is not found, create it. * * @param bool $touch - * @return Model + * @return object{pivot: Pivot}&TRelatedModel */ public function firstOrCreate(array $attributes, array $joining = [], $touch = true) { @@ -408,7 +416,7 @@ public function firstOrCreate(array $attributes, array $joining = [], $touch = t * Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record. * * @param bool $touch - * @return Model + * @return object{pivot: Pivot}&TRelatedModel */ public function createOrFirst(array $attributes = [], array $values = [], array $joining = [], $touch = true) { @@ -431,7 +439,7 @@ public function createOrFirst(array $attributes = [], array $values = [], array * Create or update a related record matching the attributes, and fill it with values. * * @param bool $touch - * @return Model + * @return object{pivot: Pivot}&TRelatedModel */ public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true) { @@ -449,9 +457,9 @@ public function updateOrCreate(array $attributes, array $values = [], array $joi /** * Find a related model by its primary key. * - * @param array $columns * @param mixed $id - * @return null|Collection|Model + * @param array $columns + * @return ($id is array ? Collection : null|(object{pivot: Pivot}&TRelatedModel)) */ public function find($id, $columns = ['*']) { @@ -465,9 +473,9 @@ public function find($id, $columns = ['*']) /** * Find multiple related models by their primary keys. * - * @param array $columns * @param mixed $ids - * @return Collection + * @param array $columns + * @return Collection */ public function findMany($ids, $columns = ['*']) { @@ -480,9 +488,9 @@ public function findMany($ids, $columns = ['*']) /** * Find a related model by its primary key or throw an exception. * - * @param array $columns * @param mixed $id - * @return Collection|Model + * @param array $columns + * @return ($id is array ? Collection : object{pivot: Pivot}&TRelatedModel) * @throws ModelNotFoundException */ public function findOrFail($id, $columns = ['*']) @@ -504,6 +512,7 @@ public function findOrFail($id, $columns = ['*']) * Execute the query and get the first result. * * @param array $columns + * @return null|(object{pivot: Pivot}&TRelatedModel) */ public function first($columns = ['*']) { @@ -516,7 +525,7 @@ public function first($columns = ['*']) * Execute the query and get the first result or throw an exception. * * @param array $columns - * @return Model|static + * @return object{pivot: Pivot}&TRelatedModel * @throws ModelNotFoundException */ public function firstOrFail($columns = ['*']) @@ -540,7 +549,6 @@ public function getResults() * Execute the query as a "select" statement. * * @param array $columns - * @return Collection */ public function get($columns = ['*']) { @@ -679,8 +687,9 @@ public function allRelatedIds() /** * Save a new model and attach it to the parent model. * + * @param TRelatedModel $model * @param bool $touch - * @return Model + * @return object{pivot: Pivot}&TRelatedModel */ public function save(Model $model, array $pivotAttributes = [], $touch = true) { @@ -694,8 +703,8 @@ public function save(Model $model, array $pivotAttributes = [], $touch = true) /** * Save an array of new models and attach them to the parent model. * - * @param array|\Hyperf\Collection\Collection $models - * @return array + * @param iterable $models + * @return ($models is array ? array : Collection) */ public function saveMany($models, array $pivotAttributes = []) { @@ -712,7 +721,7 @@ public function saveMany($models, array $pivotAttributes = []) * Create a new instance of the related model. * * @param bool $touch - * @return Model + * @return object{pivot: Pivot}&TRelatedModel */ public function create(array $attributes = [], array $joining = [], $touch = true) { @@ -731,7 +740,7 @@ public function create(array $attributes = [], array $joining = [], $touch = tru /** * Create an array of new instances of the related models. * - * @return array + * @return array */ public function createMany(array $records, array $joinings = []) { diff --git a/src/Model/Relations/Concerns/InteractsWithPivotTable.php b/src/Model/Relations/Concerns/InteractsWithPivotTable.php index fe4f947..22c7f26 100644 --- a/src/Model/Relations/Concerns/InteractsWithPivotTable.php +++ b/src/Model/Relations/Concerns/InteractsWithPivotTable.php @@ -79,7 +79,7 @@ public function toggle($ids, $touch = true) * Sync the intermediate tables with a list of IDs without detaching. * * @param array|BaseCollection|Model $ids - * @return array + * @return array{attached: (int|string)[], detached: (int|string)[], updated: (int|string)[]} */ public function syncWithoutDetaching($ids) { @@ -91,7 +91,7 @@ public function syncWithoutDetaching($ids) * * @param array|BaseCollection|Model $ids * @param bool $detaching - * @return array + * @return array{attached: (int|string)[], detached: (int|string)[], updated: (int|string)[]} */ public function sync($ids, $detaching = true) { diff --git a/src/Model/Relations/HasMany.php b/src/Model/Relations/HasMany.php index 344b043..036cfb1 100755 --- a/src/Model/Relations/HasMany.php +++ b/src/Model/Relations/HasMany.php @@ -14,6 +14,12 @@ use Hyperf\Database\Model\Collection; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @extends HasOneOrMany> + */ class HasMany extends HasOneOrMany { /** diff --git a/src/Model/Relations/HasManyThrough.php b/src/Model/Relations/HasManyThrough.php index 6845c0d..0702548 100644 --- a/src/Model/Relations/HasManyThrough.php +++ b/src/Model/Relations/HasManyThrough.php @@ -23,6 +23,13 @@ use function Hyperf\Support\class_uses_recursive; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TIntermediateModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @extends Relation> + */ class HasManyThrough extends Relation { /** @@ -184,7 +191,7 @@ public function match(array $models, Collection $results, $relation) /** * Get the first related model record matching the attributes or instantiate it. * - * @return Model + * @return TRelatedModel */ public function firstOrNew(array $attributes) { @@ -198,7 +205,7 @@ public function firstOrNew(array $attributes) /** * Create or update a related record matching the attributes, and fill it with values. * - * @return Model + * @return TRelatedModel */ public function updateOrCreate(array $attributes, array $values = []) { @@ -213,6 +220,7 @@ public function updateOrCreate(array $attributes, array $values = []) * Execute the query and get the first related model. * * @param array $columns + * @return null|TRelatedModel */ public function first($columns = ['*']) { @@ -225,7 +233,7 @@ public function first($columns = ['*']) * Execute the query and get the first result or throw an exception. * * @param array $columns - * @return Model|static + * @return TRelatedModel * @throws ModelNotFoundException */ public function firstOrFail($columns = ['*']) @@ -240,9 +248,9 @@ public function firstOrFail($columns = ['*']) /** * Find a related model by its primary key. * - * @param array $columns * @param mixed $id - * @return null|Collection|Model + * @param array $columns + * @return ($id is array ? Collection : null|TRelatedModel) */ public function find($id, $columns = ['*']) { @@ -260,9 +268,9 @@ public function find($id, $columns = ['*']) /** * Find multiple related models by their primary keys. * - * @param array $columns * @param mixed $ids - * @return Collection + * @param array $columns + * @return Collection */ public function findMany($ids, $columns = ['*']) { @@ -279,9 +287,9 @@ public function findMany($ids, $columns = ['*']) /** * Find a related model by its primary key or throw an exception. * - * @param array $columns * @param mixed $id - * @return Collection|Model + * @param array $columns + * @return ($id is array ? Collection : TRelatedModel) * @throws ModelNotFoundException */ public function findOrFail($id, $columns = ['*']) @@ -311,7 +319,7 @@ public function getResults() * Execute the query as a "select" statement. * * @param array $columns - * @return Collection + * @return Collection */ public function get($columns = ['*']) { diff --git a/src/Model/Relations/HasOne.php b/src/Model/Relations/HasOne.php index 9387c65..d703583 100755 --- a/src/Model/Relations/HasOne.php +++ b/src/Model/Relations/HasOne.php @@ -16,6 +16,12 @@ use Hyperf\Database\Model\Model; use Hyperf\Database\Model\Relations\Concerns\SupportsDefaultModels; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @extends HasOneOrMany + */ class HasOne extends HasOneOrMany { use SupportsDefaultModels; diff --git a/src/Model/Relations/HasOneOrMany.php b/src/Model/Relations/HasOneOrMany.php index ab20393..09252b6 100755 --- a/src/Model/Relations/HasOneOrMany.php +++ b/src/Model/Relations/HasOneOrMany.php @@ -20,6 +20,13 @@ use function Hyperf\Tappable\tap; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * @template TResult + * + * @extends Relation + */ abstract class HasOneOrMany extends Relation { /** @@ -60,7 +67,7 @@ public function __construct(Builder $query, Model $parent, $foreignKey, $localKe /** * Create and return an un-saved instance of the related model. * - * @return Model + * @return TRelatedModel */ public function make(array $attributes = []) { @@ -119,9 +126,9 @@ public function matchMany(array $models, Collection $results, $relation) /** * Find a model by its primary key or return new instance of the related model. * - * @param array $columns * @param mixed $id - * @return \Hyperf\Collection\Collection|Model + * @param array $columns + * @return TRelatedModel */ public function findOrNew($id, $columns = ['*']) { @@ -137,7 +144,7 @@ public function findOrNew($id, $columns = ['*']) /** * Get the first related model record matching the attributes or instantiate it. * - * @return Model + * @return TRelatedModel */ public function firstOrNew(array $attributes, array $values = []) { @@ -153,7 +160,7 @@ public function firstOrNew(array $attributes, array $values = []) /** * Get the first related record matching the attributes. If the record is not found, create it. * - * @return Model + * @return TRelatedModel */ public function firstOrCreate(array $attributes, array $values = []) { @@ -167,7 +174,7 @@ public function firstOrCreate(array $attributes, array $values = []) /** * Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record. * - * @return Model + * @return TRelatedModel */ public function createOrFirst(array $attributes = [], array $values = []) { @@ -181,7 +188,7 @@ public function createOrFirst(array $attributes = [], array $values = []) /** * Create or update a related record matching the attributes, and fill it with values. * - * @return Model + * @return TRelatedModel */ public function updateOrCreate(array $attributes, array $values = []) { @@ -195,7 +202,8 @@ public function updateOrCreate(array $attributes, array $values = []) /** * Attach a model instance to the parent model. * - * @return false|Model + * @param TRelatedModel $model + * @return false|TRelatedModel */ public function save(Model $model) { @@ -222,7 +230,7 @@ public function saveMany($models) /** * Create a new instance of the related model. * - * @return Model + * @return TRelatedModel */ public function create(array $attributes = []) { @@ -236,7 +244,7 @@ public function create(array $attributes = []) /** * Create a Collection of new instances of the related model. * - * @return Collection + * @return Collection */ public function createMany(array $records) { diff --git a/src/Model/Relations/HasOneThrough.php b/src/Model/Relations/HasOneThrough.php index 6423a07..6290aa5 100644 --- a/src/Model/Relations/HasOneThrough.php +++ b/src/Model/Relations/HasOneThrough.php @@ -16,12 +16,21 @@ use Hyperf\Database\Model\Model; use Hyperf\Database\Model\Relations\Concerns\SupportsDefaultModels; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TIntermediateModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @extends HasManyThrough + */ class HasOneThrough extends HasManyThrough { use SupportsDefaultModels; /** * Get the results of the relationship. + * + * @return null|TRelatedModel */ public function getResults() { @@ -72,7 +81,7 @@ public function match(array $models, Collection $results, $relation) /** * Make a new related instance for the given model. * - * @return Model + * @return TRelatedModel */ public function newRelatedInstanceFor(Model $parent) { diff --git a/src/Model/Relations/MorphMany.php b/src/Model/Relations/MorphMany.php index 63e64ee..49c5143 100755 --- a/src/Model/Relations/MorphMany.php +++ b/src/Model/Relations/MorphMany.php @@ -14,6 +14,12 @@ use Hyperf\Database\Model\Collection; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @extends MorphOneOrMany> + */ class MorphMany extends MorphOneOrMany { /** diff --git a/src/Model/Relations/MorphOne.php b/src/Model/Relations/MorphOne.php index d7ab77b..d1a9750 100755 --- a/src/Model/Relations/MorphOne.php +++ b/src/Model/Relations/MorphOne.php @@ -16,6 +16,12 @@ use Hyperf\Database\Model\Model; use Hyperf\Database\Model\Relations\Concerns\SupportsDefaultModels; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @extends MorphOneOrMany + */ class MorphOne extends MorphOneOrMany { use SupportsDefaultModels; @@ -57,7 +63,7 @@ public function match(array $models, Collection $results, $relation) /** * Make a new related instance for the given model. * - * @return Model + * @return TRelatedModel */ public function newRelatedInstanceFor(Model $parent) { diff --git a/src/Model/Relations/MorphOneOrMany.php b/src/Model/Relations/MorphOneOrMany.php index 33939b8..6286c18 100755 --- a/src/Model/Relations/MorphOneOrMany.php +++ b/src/Model/Relations/MorphOneOrMany.php @@ -17,6 +17,13 @@ use function Hyperf\Collection\last; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TParentModel of \Hyperf\Database\Model\Model + * @template TResult + * + * @extends HasOneOrMany + */ abstract class MorphOneOrMany extends HasOneOrMany { /** diff --git a/src/Model/Relations/MorphTo.php b/src/Model/Relations/MorphTo.php index a924b6c..0269453 100644 --- a/src/Model/Relations/MorphTo.php +++ b/src/Model/Relations/MorphTo.php @@ -20,6 +20,12 @@ use function Hyperf\Collection\collect; use function Hyperf\Collection\head; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * + * @extends BelongsTo + */ class MorphTo extends BelongsTo { /** @@ -120,6 +126,8 @@ public function getResults() * Get the results of the relationship. * * Called via eager load method of Model query builder. + * + * @return Collection */ public function getEager() { @@ -134,7 +142,7 @@ public function getEager() * Create a new model instance by type. * * @param string $type - * @return Model + * @return TRelatedModel */ public function createModelByType($type) { @@ -157,8 +165,8 @@ public function match(array $models, Collection $results, $relation) /** * Associate the model instance to the given parent. * - * @param Model $model - * @return Model + * @param null|TRelatedModel $model + * @return TDeclaringModel */ public function associate($model) { @@ -178,7 +186,7 @@ public function associate($model) /** * Dissociate previously associated model from the given parent. * - * @return Model + * @return TDeclaringModel */ public function dissociate() { diff --git a/src/Model/Relations/MorphToMany.php b/src/Model/Relations/MorphToMany.php index c360385..519fa95 100644 --- a/src/Model/Relations/MorphToMany.php +++ b/src/Model/Relations/MorphToMany.php @@ -18,6 +18,14 @@ use function Hyperf\Collection\collect; +/** + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TDeclaringModel of \Hyperf\Database\Model\Model + * @template TPivotModel of \Hyperf\Database\Model\Relations\Pivot = \Hyperf\Database\Model\Relations\MorphPivot + * @template TAccessor of string = 'pivot' + * + * @extends BelongsToMany + */ class MorphToMany extends BelongsToMany { /** diff --git a/src/Model/Relations/Relation.php b/src/Model/Relations/Relation.php index 21fede8..03fd328 100755 --- a/src/Model/Relations/Relation.php +++ b/src/Model/Relations/Relation.php @@ -25,7 +25,11 @@ use function Hyperf\Collection\last; /** - * @mixin \Hyperf\Database\Model\Builder + * @template TRelatedModel of \Hyperf\Database\Model\Model + * @template TParentModel of \Hyperf\Database\Model\Model + * @template TResult + * + * @mixin \Hyperf\Database\Model\Builder */ abstract class Relation { @@ -109,6 +113,10 @@ public function __clone() /** * Run a callback with constraints disabled on the relation. + * + * @template TCallbackReturn + * @param Closure(): TCallbackReturn $callback + * @return TCallbackReturn */ public static function noConstraints(Closure $callback) { @@ -154,13 +162,15 @@ abstract public function match(array $models, Collection $results, $relation); /** * Get the results of the relationship. + * + * @return TResult */ abstract public function getResults(); /** * Get the relationship for eager loading. * - * @return Collection + * @return Collection */ public function getEager() { @@ -171,7 +181,7 @@ public function getEager() * Execute the query as a "select" statement. * * @param array $columns - * @return Collection + * @return Collection */ public function get($columns = ['*']) { @@ -256,7 +266,7 @@ public function getBaseQuery() /** * Get the parent model of the relation. * - * @return Model + * @return TParentModel */ public function getParent() { @@ -276,7 +286,7 @@ public function getQualifiedParentKeyName() /** * Get the related model of the relation. * - * @return Model + * @return TRelatedModel */ public function getRelated() { diff --git a/src/Model/SoftDeletes.php b/src/Model/SoftDeletes.php index 0d8c6e9..407ed54 100644 --- a/src/Model/SoftDeletes.php +++ b/src/Model/SoftDeletes.php @@ -17,10 +17,11 @@ use function Hyperf\Tappable\tap; /** - * @method static static|\Hyperf\Database\Model\Builder|\Hyperf\Database\Query\Builder restoreOrCreate(array $attributes = [], array $values = []) - * @method static static|\Hyperf\Database\Model\Builder|\Hyperf\Database\Query\Builder withTrashed(bool $withTrashed = true) - * @method static static|\Hyperf\Database\Model\Builder|\Hyperf\Database\Query\Builder onlyTrashed() - * @method static static|\Hyperf\Database\Model\Builder|\Hyperf\Database\Query\Builder withoutTrashed() + * @method static \Hyperf\Database\Model\Builder withTrashed(bool $withTrashed = true) + * @method static \Hyperf\Database\Model\Builder onlyTrashed() + * @method static \Hyperf\Database\Model\Builder withoutTrashed() + * @method static static restoreOrCreate(array $attributes = [], array $values = []) + * @method static static createOrRestore(array $attributes = [], array $values = []) */ trait SoftDeletes { diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 1c798e6..c069e7e 100755 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -299,7 +299,7 @@ public function select($columns = ['*']) /** * Add a subselect expression to the query. * - * @param Builder|Closure|string $query + * @param Builder|Closure|ModelBuilder|string $query * @param string $as * @return Builder|static * @throws InvalidArgumentException @@ -331,7 +331,7 @@ public function selectRaw($expression, array $bindings = []) /** * Makes "from" fetch from a subquery. * - * @param Builder|Closure|string $query + * @param Builder|Closure|ModelBuilder|string $query * @param string $as * @return Builder|static * @throws InvalidArgumentException @@ -519,7 +519,7 @@ public function joinWhere($table, $first, $operator, $second, $type = 'inner') /** * Add a subquery join clause to the query. * - * @param Builder|Closure|string $query + * @param Builder|Closure|ModelBuilder|string $query * @param string $as * @param Closure|string $first * @param null|string $operator @@ -595,7 +595,7 @@ public function leftJoinWhere($table, $first, $operator, $second) /** * Add a subquery left join to the query. * - * @param Builder|Closure|string $query + * @param Builder|Closure|ModelBuilder|string $query * @param string $as * @param Closure|string $first * @param null|string $operator @@ -638,7 +638,7 @@ public function rightJoinWhere($table, $first, $operator, $second) /** * Add a subquery right join to the query. * - * @param Builder|Closure|string $query + * @param Builder|Closure|ModelBuilder|string $query * @param string $as * @param Closure|string $first * @param null|string $operator @@ -1996,7 +1996,7 @@ public function orderBy(mixed $column, string $direction = 'asc'): static /** * Add a descending "order by" clause to the query. * - * @param string $column + * @param Closure|Expression|ModelBuilder|static|string $column * @return $this */ public function orderByDesc($column) @@ -2181,7 +2181,7 @@ public function reorder(mixed $column = null, string $direction = 'asc'): static /** * Add a union statement to the query. * - * @param Builder|Closure $query + * @param Builder|Closure|ModelBuilder $query * @param bool $all * @return Builder|static */ @@ -2201,7 +2201,7 @@ public function union($query, $all = false) /** * Add a union all statement to the query. * - * @param Builder|Closure $query + * @param Builder|Closure|ModelBuilder $query * @return Builder|static */ public function unionAll($query) @@ -2276,7 +2276,7 @@ public function toRawSql() * * @param mixed $id * @param array $columns - * @return mixed|static + * @return null|object */ public function find($id, $columns = ['*']) { @@ -2299,6 +2299,7 @@ public function value($column) * Execute the query as a "select" statement. * * @param array|string $columns + * @return Collection */ public function get($columns = ['*']): Collection { @@ -2386,6 +2387,7 @@ public function cursor() * Chunk the results of a query by comparing numeric IDs. * * @param int $count + * @param callable(Collection, int): mixed $callback * @param string $column * @param null|string $alias * @return bool @@ -2681,7 +2683,7 @@ public function insertGetId(array $values, $sequence = null) /** * Insert new records into the table using a subquery. * - * @param Builder|Closure|string $query + * @param Builder|Closure|ModelBuilder|string $query * @return bool */ public function insertUsing(array $columns, $query) diff --git a/tests/types/Autoload.php b/tests/types/Autoload.php new file mode 100644 index 0000000..e74b63f --- /dev/null +++ b/tests/types/Autoload.php @@ -0,0 +1,26 @@ + $query */ +function test( + Builder $query, + User $user, + Post $post, + ChildPost $childPost, + Comment $comment +): void { + assertType('Hyperf\Database\Model\Builder', $query->where('id', 1)); + assertType('Hyperf\Database\Model\Builder', $query->orWhere('name', 'John')); + assertType('Hyperf\Database\Model\Builder', $query->with('relation')); + assertType('Hyperf\Database\Model\Builder', $query->with(['relation' => ['foo' => fn ($q) => $q]])); + assertType('Hyperf\Database\Model\Builder', $query->with(['relation' => function ($query) { + // assertType('Hyperf\Database\Model\Relations\Relation<*,*,*>', $query); + }])); + assertType('Hyperf\Database\Model\Builder', $query->without('relation')); + assertType('array', $query->getModels()); + assertType('array', $query->eagerLoadRelations([])); + assertType('Hyperf\Database\Model\Collection', $query->get()); + assertType('Hyperf\Database\Model\Collection', $query->hydrate([])); + assertType('Hyperf\Database\Model\Collection', $query->fromQuery('foo', [])); + assertType('Hyperf\Database\Model\Collection', $query->findMany([1, 2, 3])); + assertType('Hyperf\Database\Model\Collection', $query->findOrFail([1])); + assertType('Hyperf\Database\Model\Collection', $query->findOrNew([1])); + assertType('Hyperf\Database\Model\Collection', $query->find([1])); + assertType('Hyperf\Database\Model\Collection', $query->findOr([1], callback: fn () => 42)); + assertType('Hyperf\Types\Builder\User', $query->findOrFail(1)); + assertType('Hyperf\Types\Builder\User|null', $query->find(1)); + assertType('Hyperf\Types\Builder\User|int', $query->findOr(1, fn () => 42)); + assertType('Hyperf\Types\Builder\User|int', $query->findOr(1, callback: fn () => 42)); + assertType('Hyperf\Types\Builder\User|null', $query->first()); + assertType('Hyperf\Types\Builder\User|int', $query->firstOr(fn () => 42)); + assertType('Hyperf\Types\Builder\User|int', $query->firstOr(callback: fn () => 42)); + assertType('Hyperf\Types\Builder\User', $query->firstOrNew(['id' => 1])); + assertType('Hyperf\Types\Builder\User', $query->findOrNew(1)); + assertType('Hyperf\Types\Builder\User', $query->firstOrCreate(['id' => 1])); + assertType('Hyperf\Types\Builder\User', $query->createOrFirst(['id' => 1])); + assertType('Hyperf\Types\Builder\User', $query->create(['name' => 'John'])); + assertType('Hyperf\Types\Builder\User', $query->forceCreate(['name' => 'John'])); + assertType('Hyperf\Types\Builder\User', $query->getModel()); + assertType('Hyperf\Types\Builder\User', $query->make(['name' => 'John'])); + assertType('Hyperf\Types\Builder\User', $query->forceCreate(['name' => 'John'])); + assertType('Hyperf\Types\Builder\User', $query->updateOrCreate(['id' => 1], ['name' => 'John'])); + assertType('Hyperf\Types\Builder\User', $query->firstOrFail()); + assertType('Hyperf\Types\Builder\User', $query->sole()); + assertType('Generator', $query->cursor()); + assertType('Hyperf\Collection\LazyCollection', $query->lazy()); + assertType('Hyperf\Collection\LazyCollection', $query->lazyById()); + assertType('Hyperf\Collection\LazyCollection', $query->lazyByIdDesc()); + assertType('Hyperf\Collection\Collection<(int|string), mixed>', $query->pluck('foo')); + assertType('Hyperf\Database\Model\Relations\Relation', $query->getRelation('foo')); + assertType('Hyperf\Database\Model\Builder', $query->setModel(new Post())); + + assertType('Hyperf\Database\Model\Builder', $query->has('foo', callback: function ($query) { + assertType('Hyperf\Database\Model\Builder', $query); + })); + assertType('Hyperf\Database\Model\Builder', $query->has($user->posts(), callback: function ($query) { + assertType('Hyperf\Database\Model\Builder', $query); + })); + assertType('Hyperf\Database\Model\Builder', $query->orHas($user->posts())); + assertType('Hyperf\Database\Model\Builder', $query->doesntHave($user->posts(), callback: function ($query) { + assertType('Hyperf\Database\Model\Builder', $query); + })); + assertType('Hyperf\Database\Model\Builder', $query->orDoesntHave($user->posts())); + assertType('Hyperf\Database\Model\Builder', $query->whereHas($user->posts(), function ($query) { + assertType('Hyperf\Database\Model\Builder', $query); + })); + assertType('Hyperf\Database\Model\Builder', $query->withWhereHas('posts', function ($query) { + assertType('Hyperf\Database\Model\Builder<*>|Hyperf\Database\Model\Relations\Relation<*, *, *>', $query); + })); + assertType('Hyperf\Database\Model\Builder', $query->orWhereHas($user->posts(), function ($query) { + assertType('Hyperf\Database\Model\Builder', $query); + })); + assertType('Hyperf\Database\Model\Builder', $query->whereDoesntHave($user->posts(), function ($query) { + assertType('Hyperf\Database\Model\Builder', $query); + })); + assertType('Hyperf\Database\Model\Builder', $query->orWhereDoesntHave($user->posts(), function ($query) { + assertType('Hyperf\Database\Model\Builder', $query); + })); + assertType('Hyperf\Database\Model\Builder', $query->hasMorph($post->taggable(), 'taggable', callback: function ($query, $type) { + assertType('Hyperf\Database\Model\Builder', $query); + assertType('string', $type); + })); + assertType('Hyperf\Database\Model\Builder', $query->orHasMorph($post->taggable(), 'taggable')); + assertType('Hyperf\Database\Model\Builder', $query->doesntHaveMorph($post->taggable(), 'taggable', callback: function ($query, $type) { + assertType('Hyperf\Database\Model\Builder', $query); + assertType('string', $type); + })); + assertType('Hyperf\Database\Model\Builder', $query->orDoesntHaveMorph($post->taggable(), 'taggable')); + assertType('Hyperf\Database\Model\Builder', $query->whereHasMorph($post->taggable(), 'taggable', function ($query, $type) { + assertType('Hyperf\Database\Model\Builder', $query); + assertType('string', $type); + })); + assertType('Hyperf\Database\Model\Builder', $query->orWhereHasMorph($post->taggable(), 'taggable', function ($query, $type) { + assertType('Hyperf\Database\Model\Builder', $query); + assertType('string', $type); + })); + assertType('Hyperf\Database\Model\Builder', $query->whereDoesntHaveMorph($post->taggable(), 'taggable', function ($query, $type) { + assertType('Hyperf\Database\Model\Builder', $query); + assertType('string', $type); + })); + assertType('Hyperf\Database\Model\Builder', $query->orWhereDoesntHaveMorph($post->taggable(), 'taggable', function ($query, $type) { + assertType('Hyperf\Database\Model\Builder', $query); + assertType('string', $type); + })); + assertType('Hyperf\Database\Model\Builder', $query->whereRelation($user->posts(), 'id', 1)); + assertType('Hyperf\Database\Model\Builder', $query->orWhereRelation($user->posts(), 'id', 1)); + assertType('Hyperf\Database\Model\Builder', $query->whereMorphRelation($post->taggable(), 'taggable', 'id', 1)); + assertType('Hyperf\Database\Model\Builder', $query->orWhereMorphRelation($post->taggable(), 'taggable', 'id', 1)); + + $query->chunk(1, function ($users, $page) { + assertType('Hyperf\Database\Model\Collection', $users); + assertType('int', $page); + }); + $query->chunkById(1, function ($users, $page) { + assertType('Hyperf\Database\Model\Collection', $users); + assertType('int', $page); + }); + $query->chunkMap(function ($users) { + assertType('Hyperf\Types\Builder\User', $users); + }); + $query->chunkByIdDesc(1, function ($users, $page) { + assertType('Hyperf\Database\Model\Collection', $users); + assertType('int', $page); + }); + $query->each(function ($users, $page) { + assertType('Hyperf\Types\Builder\User', $users); + assertType('int', $page); + }); + $query->eachById(function ($users, $page) { + assertType('Hyperf\Types\Builder\User', $users); + assertType('int', $page); + }); + + assertType('Hyperf\Database\Model\Builder', Post::query()); + assertType('Hyperf\Database\Model\Builder', Post::on()); + assertType('Hyperf\Database\Model\Builder', Post::onWriteConnection()); + assertType('Hyperf\Database\Model\Builder', Post::with([])); + assertType('Hyperf\Database\Model\Builder', $post->newQuery()); + assertType('Hyperf\Database\Model\Builder', $post->newModelQuery()); + assertType('Hyperf\Database\Model\Builder', $post->newQueryWithoutRelationships()); + assertType('Hyperf\Database\Model\Builder', $post->newQueryWithoutScopes()); + assertType('Hyperf\Database\Model\Builder', $post->newQueryWithoutScope('foo')); + assertType('Hyperf\Database\Model\Builder', $post->newQueryForRestoration(1)); + assertType('Hyperf\Database\Model\Builder', $post->newQuery()->where('foo', 'bar')); + assertType('Hyperf\Types\Builder\Post', $post->newQuery()->create(['name' => 'John'])); + + assertType('Hyperf\Database\Model\Builder', ChildPost::query()); + assertType('Hyperf\Database\Model\Builder', ChildPost::on()); + assertType('Hyperf\Database\Model\Builder', ChildPost::onWriteConnection()); + assertType('Hyperf\Database\Model\Builder', ChildPost::with([])); + assertType('Hyperf\Database\Model\Builder', $childPost->newQuery()); + assertType('Hyperf\Database\Model\Builder', $childPost->newModelQuery()); + assertType('Hyperf\Database\Model\Builder', $childPost->newQueryWithoutRelationships()); + assertType('Hyperf\Database\Model\Builder', $childPost->newQueryWithoutScopes()); + assertType('Hyperf\Database\Model\Builder', $childPost->newQueryWithoutScope('foo')); + assertType('Hyperf\Database\Model\Builder', $childPost->newQueryForRestoration(1)); + assertType('Hyperf\Database\Model\Builder', $childPost->newQuery()->where('foo', 'bar')); + assertType('Hyperf\Types\Builder\ChildPost', $childPost->newQuery()->create(['name' => 'John'])); + + assertType('Hyperf\Database\Model\Builder', Comment::query()); + assertType('Hyperf\Database\Model\Builder', Comment::on()); + assertType('Hyperf\Database\Model\Builder', Comment::onWriteConnection()); + assertType('Hyperf\Database\Model\Builder', Comment::with([])); + assertType('Hyperf\Database\Model\Builder', $comment->newQuery()); + assertType('Hyperf\Database\Model\Builder', $comment->newModelQuery()); + assertType('Hyperf\Database\Model\Builder', $comment->newQueryWithoutRelationships()); + assertType('Hyperf\Database\Model\Builder', $comment->newQueryWithoutScopes()); + assertType('Hyperf\Database\Model\Builder', $comment->newQueryWithoutScope('foo')); + assertType('Hyperf\Database\Model\Builder', $comment->newQueryForRestoration(1)); + assertType('Hyperf\Database\Model\Builder', $comment->newQuery()->where('foo', 'bar')); + assertType('Hyperf\Types\Builder\Comment', $comment->newQuery()->create(['name' => 'John'])); +} + +class User extends Model +{ + /** @return HasMany */ + public function posts(): HasMany + { + return $this->hasMany(Post::class); + } +} + +class Post extends Model +{ + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** @return MorphTo */ + public function taggable(): MorphTo + { + return $this->morphTo(); + } +} + +class ChildPost extends Post +{ +} + +class Comment extends Model +{ +} + +/** + * @template TModel of \Hyperf\Database\Model\Model + * + * @extends \Hyperf\Database\Model\Builder + */ +class CommonBuilder extends Builder +{ + public function foo(): static + { + return $this->where('foo', 'bar'); + } +} + +/** @extends CommonBuilder */ +class CommentBuilder extends CommonBuilder +{ +} diff --git a/tests/types/Model/Collection.php b/tests/types/Model/Collection.php new file mode 100644 index 0000000..8716589 --- /dev/null +++ b/tests/types/Model/Collection.php @@ -0,0 +1,187 @@ +', $collection); + +assertType('User|null', $collection->find(1)); +assertType('string|User', $collection->find(1, 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->find([1])); + +assertType('Hyperf\Database\Model\Collection', $collection->load('string')); +assertType('Hyperf\Database\Model\Collection', $collection->load(['string'])); +assertType('Hyperf\Database\Model\Collection', $collection->load(['string' => ['foo' => fn ($q) => $q]])); +assertType('Hyperf\Database\Model\Collection', $collection->load(['string' => function ($query) { + // assertType('Hyperf\Database\Model\Relations\Relation<*,*,*>', $query); +}])); + +assertType('Hyperf\Database\Model\Collection', $collection->loadAggregate('string', 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadAggregate(['string'], 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadAggregate(['string' => ['foo' => fn ($q) => $q]], 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadAggregate(['string'], 'string', 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadAggregate(['string' => function ($query) { + // assertType('Hyperf\Database\Model\Relations\Relation<*,*,*>', $query); +}], 'string', 'string')); + +assertType('Hyperf\Database\Model\Collection', $collection->loadCount('string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadCount(['string'])); +assertType('Hyperf\Database\Model\Collection', $collection->loadCount(['string' => ['foo' => fn ($q) => $q]])); +assertType('Hyperf\Database\Model\Collection', $collection->loadCount(['string' => function ($query) { + // assertType('Hyperf\Database\Model\Relations\Relation<*,*,*>', $query); +}])); + +assertType('Hyperf\Database\Model\Collection', $collection->loadMax('string', 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadMax(['string'], 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadMax(['string' => ['foo' => fn ($q) => $q]], 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadMax(['string' => function ($query) { + // assertType('Hyperf\Database\Model\Relations\Relation<*,*,*>', $query); +}], 'string')); + +assertType('Hyperf\Database\Model\Collection', $collection->loadMin('string', 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadMin(['string'], 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadMin(['string' => ['foo' => fn ($q) => $q]], 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadMin(['string' => function ($query) { + // assertType('Hyperf\Database\Model\Relations\Relation<*,*,*>', $query); +}], 'string')); + +assertType('Hyperf\Database\Model\Collection', $collection->loadSum('string', 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadSum(['string'], 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadSum(['string' => ['foo' => fn ($q) => $q]], 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadSum(['string' => function ($query) { + // assertType('Hyperf\Database\Model\Relations\Relation<*,*,*>', $query); +}], 'string')); + +assertType('Hyperf\Database\Model\Collection', $collection->loadAvg('string', 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadAvg(['string'], 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadAvg(['string' => ['foo' => fn ($q) => $q]], 'string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadAvg(['string' => function ($query) { + // assertType('Hyperf\Database\Model\Relations\Relation<*,*,*>', $query); +}], 'string')); + +assertType('Hyperf\Database\Model\Collection', $collection->loadMissing('string')); +assertType('Hyperf\Database\Model\Collection', $collection->loadMissing(['string'])); +assertType('Hyperf\Database\Model\Collection', $collection->loadMissing(['string' => ['foo' => fn ($q) => $q]])); +assertType('Hyperf\Database\Model\Collection', $collection->loadMissing(['string' => function ($query) { + // assertType('Hyperf\Database\Model\Relations\Relation<*,*,*>', $query); +}])); + +assertType('Hyperf\Database\Model\Collection', $collection->loadMorph('string', ['string'])); +assertType('Hyperf\Database\Model\Collection', $collection->loadMorph('string', ['string' => ['foo' => fn ($q) => $q]])); +assertType('Hyperf\Database\Model\Collection', $collection->loadMorph('string', ['string' => function ($query) { + // assertType('Hyperf\Database\Model\Relations\Relation<*,*,*>', $query); +}])); + +assertType('Hyperf\Database\Model\Collection', $collection->loadMorphCount('string', ['string'])); +assertType('Hyperf\Database\Model\Collection', $collection->loadMorphCount('string', ['string' => ['foo' => fn ($q) => $q]])); +assertType('Hyperf\Database\Model\Collection', $collection->loadMorphCount('string', ['string' => function ($query) { + // assertType('Hyperf\Database\Model\Relations\Relation<*,*,*>', $query); +}])); + +assertType('bool', $collection->contains(function ($user) { + assertType('User', $user); + + return true; +})); +assertType('bool', $collection->contains('string', '=', 'string')); + +asserttype('array', $collection->modelKeys()); + +assertType('Hyperf\Database\Model\Collection', $collection->merge($collection)); +assertType('Hyperf\Database\Model\Collection', $collection->merge([new User()])); + +assertType( + 'Hyperf\Database\Model\Collection', + $collection->map(function ($user, $int) { + assertType('User', $user); + assertType('int', $int); + + return new User(); + }) +); +assertType( + 'Hyperf\Collection\Collection', + $collection->map(function ($user, $int) { + assertType('User', $user); + assertType('int', $int); + + return 'string'; + }) +); + +assertType( + 'Hyperf\Database\Model\Collection', + $collection->fresh() +); +assertType( + 'Hyperf\Database\Model\Collection', + $collection->fresh('string') +); +assertType( + 'Hyperf\Database\Model\Collection', + $collection->fresh(['string']) +); + +assertType('Hyperf\Database\Model\Collection', $collection->diff($collection)); +assertType('Hyperf\Database\Model\Collection', $collection->diff([new User()])); + +assertType('Hyperf\Database\Model\Collection', $collection->intersect($collection)); +assertType('Hyperf\Database\Model\Collection', $collection->intersect([new User()])); + +assertType('Hyperf\Database\Model\Collection', $collection->unique()); +assertType('Hyperf\Database\Model\Collection', $collection->unique(function ($user, $int) { + assertType('User', $user); + assertType('int', $int); + + return $user->getTable(); +})); +assertType('Hyperf\Database\Model\Collection', $collection->unique('string')); + +assertType('Hyperf\Database\Model\Collection', $collection->only(null)); +assertType('Hyperf\Database\Model\Collection', $collection->only(['string'])); + +assertType('Hyperf\Database\Model\Collection', $collection->except(null)); +assertType('Hyperf\Database\Model\Collection', $collection->except(['string'])); + +assertType('Hyperf\Database\Model\Collection', $collection->makeHidden('string')); +assertType('Hyperf\Database\Model\Collection', $collection->makeHidden(['string'])); + +assertType('Hyperf\Database\Model\Collection', $collection->makeVisible('string')); +assertType('Hyperf\Database\Model\Collection', $collection->makeVisible(['string'])); + +assertType('Hyperf\Database\Model\Collection', $collection->append('string')); +assertType('Hyperf\Database\Model\Collection', $collection->append(['string'])); + +assertType('Hyperf\Database\Model\Collection', $collection->unique()); +assertType('Hyperf\Database\Model\Collection', $collection->uniqueStrict()); + +assertType('array', $collection->getDictionary()); +assertType('array', $collection->getDictionary($collection)); +assertType('array', $collection->getDictionary([new User()])); + +assertType('Hyperf\Collection\Collection<(int|string), mixed>', $collection->pluck('string')); +assertType('Hyperf\Collection\Collection<(int|string), mixed>', $collection->pluck(['string'])); + +assertType('Hyperf\Collection\Collection', $collection->keys()); + +assertType('Hyperf\Collection\Collection>', $collection->zip([1])); +assertType('Hyperf\Collection\Collection>', $collection->zip(['string'])); + +assertType('Hyperf\Collection\Collection', $collection->collapse()); + +assertType('Hyperf\Collection\Collection', $collection->flatten()); +assertType('Hyperf\Collection\Collection', $collection->flatten(4)); + +assertType('Hyperf\Collection\Collection', $collection->flip()); + +assertType('Hyperf\Collection\Collection', $collection->pad(2, 0)); +assertType('Hyperf\Collection\Collection', $collection->pad(2, 'string')); diff --git a/tests/types/Model/Model.php b/tests/types/Model/Model.php new file mode 100644 index 0000000..de4b751 --- /dev/null +++ b/tests/types/Model/Model.php @@ -0,0 +1,77 @@ +', User::query()); + assertType('Hyperf\Database\Model\Builder', $user->newQuery()); + assertType('Hyperf\Database\Model\Builder', $user->withTrashed()); + assertType('Hyperf\Database\Model\Builder', $user->onlyTrashed()); + assertType('Hyperf\Database\Model\Builder', $user->withoutTrashed()); + + assertType('Hyperf\Database\Model\Collection<(int|string), User>', $user->newCollection([new User()])); + assertType('Hyperf\Types\Model\Comments', $comment->newCollection([new Comment()])); + assertType('Hyperf\Database\Model\Collection<(int|string), Hyperf\Types\Model\Post>', $post->newCollection(['foo' => new Post()])); + assertType('Hyperf\Database\Model\Collection<(int|string), Hyperf\Types\Model\Article>', $article->newCollection([new Article()])); + assertType('Hyperf\Types\Model\Comments', $comment->newCollection([new Comment()])); + + assertType('bool|null', $user->restore()); +} + +class Post extends Model +{ + protected static string $collectionClass = Posts::class; +} + +/** + * @template TKey of array-key + * @template TModel of Post + * + * @extends Collection */ +class Posts extends Collection +{ +} + +final class Comment extends Model +{ + /** @param array $models */ + public function newCollection(array $models = []): Comments + { + return new Comments($models); + } +} + +/** @extends Collection */ +final class Comments extends Collection +{ +} + +class Article extends Model +{ +} + +/** + * @template TKey of array-key + * @template TModel of Article + * + * @extends Collection */ +class Articles extends Collection +{ +} diff --git a/tests/types/Model/ModelNotFoundException.php b/tests/types/Model/ModelNotFoundException.php new file mode 100644 index 0000000..eaf5e5d --- /dev/null +++ b/tests/types/Model/ModelNotFoundException.php @@ -0,0 +1,25 @@ + $exception */ +$exception = new ModelNotFoundException(); + +assertType('array', $exception->getIds()); +assertType('class-string|null', $exception->getModel()); + +$exception->setModel(User::class, 1); +$exception->setModel(User::class, [1]); +$exception->setModel(User::class, '1'); +$exception->setModel(User::class, ['1']); diff --git a/tests/types/Model/Relations.php b/tests/types/Model/Relations.php new file mode 100644 index 0000000..55908ce --- /dev/null +++ b/tests/types/Model/Relations.php @@ -0,0 +1,265 @@ +', $user->address()); + assertType('Hyperf\Types\Relations\Address|null', $user->address()->getResults()); + assertType('Hyperf\Database\Model\Collection', $user->address()->get()); + assertType('Hyperf\Types\Relations\Address', $user->address()->make()); + assertType('Hyperf\Types\Relations\Address', $user->address()->create()); + assertType('Hyperf\Database\Model\Relations\HasOne', $child->address()); + assertType('Hyperf\Types\Relations\Address', $child->address()->make()); + assertType('Hyperf\Types\Relations\Address', $child->address()->create([])); + assertType('Hyperf\Types\Relations\Address', $child->address()->getRelated()); + assertType('Hyperf\Types\Relations\ChildUser', $child->address()->getParent()); + + assertType('Hyperf\Database\Model\Relations\HasMany', $user->posts()); + assertType('Hyperf\Database\Model\Collection', $user->posts()->getResults()); + assertType('Hyperf\Database\Model\Collection', $user->posts()->createMany([])); + assertType('Hyperf\Types\Relations\Post', $user->posts()->make()); + assertType('Hyperf\Types\Relations\Post', $user->posts()->create()); + assertType('Hyperf\Types\Relations\Post|false', $user->posts()->save(new Post())); + + assertType("Hyperf\\Database\\Model\\Relations\\BelongsToMany", $user->roles()); + assertType('Hyperf\Database\Model\Collection', $user->roles()->getResults()); + assertType('Hyperf\Database\Model\Collection', $user->roles()->find([1])); + assertType('Hyperf\Database\Model\Collection', $user->roles()->findMany([1, 2, 3])); + assertType('Hyperf\Database\Model\Collection', $user->roles()->findOrNew([1])); + assertType('Hyperf\Database\Model\Collection', $user->roles()->findOrFail([1])); + assertType('Hyperf\Types\Relations\Role&object{pivot: Hyperf\Database\Model\Relations\Pivot}', $user->roles()->findOrNew(1)); + assertType('Hyperf\Types\Relations\Role&object{pivot: Hyperf\Database\Model\Relations\Pivot}', $user->roles()->findOrFail(1)); + assertType('(Hyperf\Types\Relations\Role&object{pivot: Hyperf\Database\Model\Relations\Pivot})|null', $user->roles()->find(1)); + assertType('(Hyperf\Types\Relations\Role&object{pivot: Hyperf\Database\Model\Relations\Pivot})|null', $user->roles()->first()); + assertType('Hyperf\Types\Relations\Role&object{pivot: Hyperf\Database\Model\Relations\Pivot}', $user->roles()->firstOrNew([])); + assertType('Hyperf\Types\Relations\Role&object{pivot: Hyperf\Database\Model\Relations\Pivot}', $user->roles()->firstOrFail()); + assertType('Hyperf\Types\Relations\Role&object{pivot: Hyperf\Database\Model\Relations\Pivot}', $user->roles()->firstOrCreate([])); + assertType('Hyperf\Types\Relations\Role&object{pivot: Hyperf\Database\Model\Relations\Pivot}', $user->roles()->create()); + assertType('Hyperf\Types\Relations\Role&object{pivot: Hyperf\Database\Model\Relations\Pivot}', $user->roles()->updateOrCreate([])); + assertType('Hyperf\Types\Relations\Role&object{pivot: Hyperf\Database\Model\Relations\Pivot}', $user->roles()->save(new Role())); + $roles = $user->roles()->getResults(); + assertType('Hyperf\Database\Model\Collection', $user->roles()->saveMany($roles)); + assertType('array', $user->roles()->saveMany($roles->all())); + assertType('array', $user->roles()->createMany($roles->all())); + assertType('array{attached: array, detached: array, updated: array}', $user->roles()->sync($roles)); + assertType('array{attached: array, detached: array, updated: array}', $user->roles()->syncWithoutDetaching($roles)); + + assertType('Hyperf\Database\Model\Relations\HasOneThrough', $user->car()); + assertType('Hyperf\Types\Relations\Car|null', $user->car()->getResults()); + assertType('Hyperf\Database\Model\Collection', $user->car()->find([1])); + assertType('Hyperf\Types\Relations\Car|null', $user->car()->find(1)); + assertType('Hyperf\Types\Relations\Car|null', $user->car()->first()); + + assertType('Hyperf\Database\Model\Relations\HasManyThrough', $user->parts()); + assertType('Hyperf\Database\Model\Collection', $user->parts()->getResults()); + + assertType('Hyperf\Database\Model\Relations\BelongsTo', $post->user()); + assertType('Hyperf\Types\Relations\User|null', $post->user()->getResults()); + assertType('Hyperf\Types\Relations\User', $post->user()->make()); + assertType('Hyperf\Types\Relations\User', $post->user()->create()); + assertType('Hyperf\Types\Relations\Post', $post->user()->associate(new User())); + assertType('Hyperf\Types\Relations\Post', $post->user()->dissociate()); + assertType('Hyperf\Types\Relations\Post', $post->user()->getChild()); + + assertType('Hyperf\Database\Model\Relations\MorphOne', $post->image()); + assertType('Hyperf\Types\Relations\Image|null', $post->image()->getResults()); + assertType('Hyperf\Types\Relations\Image', $post->image()->forceCreate([])); + + assertType('Hyperf\Database\Model\Relations\MorphMany', $post->comments()); + assertType('Hyperf\Database\Model\Collection', $post->comments()->getResults()); + + assertType('Hyperf\Database\Model\Relations\MorphTo', $comment->commentable()); + assertType('Hyperf\Database\Model\Model|null', $comment->commentable()->getResults()); + assertType('Hyperf\Database\Model\Collection', $comment->commentable()->getEager()); + assertType('Hyperf\Database\Model\Model', $comment->commentable()->createModelByType('foo')); + assertType('Hyperf\Types\Relations\Comment', $comment->commentable()->associate(new Post())); + assertType('Hyperf\Types\Relations\Comment', $comment->commentable()->dissociate()); + + assertType("Hyperf\\Database\\Model\\Relations\\MorphToMany", $post->tags()); + assertType('Hyperf\Database\Model\Collection', $post->tags()->getResults()); + + assertType('int', Relation::noConstraints(fn () => 42)); +} + +class User extends Model +{ + /** @return HasOne */ + public function address(): HasOne + { + $hasOne = $this->hasOne(Address::class); + assertType('Hyperf\Database\Model\Relations\HasOne', $hasOne); + + return $hasOne; + } + + /** @return HasMany */ + public function posts(): HasMany + { + $hasMany = $this->hasMany(Post::class); + assertType('Hyperf\Database\Model\Relations\HasMany', $hasMany); + + return $hasMany; + } + + /** @return BelongsToMany */ + public function roles(): BelongsToMany + { + $belongsToMany = $this->belongsToMany(Role::class); + assertType('Hyperf\Database\Model\Relations\BelongsToMany', $belongsToMany); + + return $belongsToMany; + } + + /** @return HasOne */ + public function mechanic(): HasOne + { + return $this->hasOne(Mechanic::class); + } + + /** @return HasMany */ + public function mechanics(): HasMany + { + return $this->hasMany(Mechanic::class); + } + + /** @return HasOneThrough */ + public function car(): HasOneThrough + { + $hasOneThrough = $this->hasOneThrough(Car::class, Mechanic::class); + assertType('Hyperf\Database\Model\Relations\HasOneThrough', $hasOneThrough); + + return $hasOneThrough; + } + + /** @return HasManyThrough */ + public function parts(): HasManyThrough + { + $hasManyThrough = $this->hasManyThrough(Part::class, Mechanic::class); + assertType('Hyperf\Database\Model\Relations\HasManyThrough', $hasManyThrough); + + return $hasManyThrough; + } +} + +class Post extends Model +{ + /** @return BelongsTo */ + public function user(): BelongsTo + { + $belongsTo = $this->belongsTo(User::class); + assertType('Hyperf\Database\Model\Relations\BelongsTo', $belongsTo); + + return $belongsTo; + } + + /** @return MorphOne */ + public function image(): MorphOne + { + $morphOne = $this->morphOne(Image::class, 'imageable'); + assertType('Hyperf\Database\Model\Relations\MorphOne', $morphOne); + + return $morphOne; + } + + /** @return MorphMany */ + public function comments(): MorphMany + { + $morphMany = $this->morphMany(Comment::class, 'commentable'); + assertType('Hyperf\Database\Model\Relations\MorphMany', $morphMany); + + return $morphMany; + } + + /** @return MorphToMany */ + public function tags(): MorphToMany + { + $morphToMany = $this->morphedByMany(Tag::class, 'taggable'); + assertType('Hyperf\Database\Model\Relations\MorphToMany', $morphToMany); + + return $morphToMany; + } +} + +class Comment extends Model +{ + /** @return MorphTo */ + public function commentable(): MorphTo + { + $morphTo = $this->morphTo(); + assertType('Hyperf\Database\Model\Relations\MorphTo', $morphTo); + + return $morphTo; + } +} + +class Tag extends Model +{ + /** @return MorphToMany */ + public function posts(): MorphToMany + { + $morphToMany = $this->morphToMany(Post::class, 'taggable'); + assertType('Hyperf\Database\Model\Relations\MorphToMany', $morphToMany); + + return $morphToMany; + } +} + +class Mechanic extends Model +{ + /** @return HasOne */ + public function car(): HasOne + { + return $this->hasOne(Car::class); + } + + /** @return HasMany */ + public function parts(): HasMany + { + return $this->hasMany(Part::class); + } +} + +class ChildUser extends User +{ +} +class Address extends Model +{ +} +class Role extends Model +{ +} +class Car extends Model +{ +} +class Part extends Model +{ +} +class Image extends Model +{ +} diff --git a/tests/types/Query/Builder.php b/tests/types/Query/Builder.php new file mode 100644 index 0000000..3b7569f --- /dev/null +++ b/tests/types/Query/Builder.php @@ -0,0 +1,66 @@ + $userQuery */ +function test(Builder $query, EloquentBuilder $userQuery): void +{ + assertType('object|null', $query->first()); + assertType('object|null', $query->find(1)); + assertType('Hyperf\Database\Query\Builder', $query->selectSub($userQuery, 'alias')); + assertType('Hyperf\Database\Query\Builder', $query->fromSub($userQuery, 'alias')); + assertType('Hyperf\Database\Query\Builder', $query->joinSub($userQuery, 'alias', 'foo')); + assertType('Hyperf\Database\Query\Builder', $query->joinLateral($userQuery, 'alias')); + assertType('Hyperf\Database\Query\Builder', $query->leftJoinLateral($userQuery, 'alias')); + assertType('Hyperf\Database\Query\Builder', $query->leftJoinSub($userQuery, 'alias', 'foo')); + assertType('Hyperf\Database\Query\Builder', $query->rightJoinSub($userQuery, 'alias', 'foo')); + assertType('Hyperf\Database\Query\Builder', $query->orderBy($userQuery)); + assertType('Hyperf\Database\Query\Builder', $query->orderByDesc($userQuery)); + assertType('Hyperf\Database\Query\Builder', $query->union($userQuery)); + assertType('Hyperf\Database\Query\Builder', $query->unionAll($userQuery)); + assertType('bool', $query->insertUsing([], $userQuery)); + assertType('int', $query->insertOrIgnoreUsing([], $userQuery)); + assertType('Hyperf\Collection\LazyCollection', $query->lazy()); + assertType('Hyperf\Collection\LazyCollection', $query->lazyById()); + assertType('Hyperf\Collection\LazyCollection', $query->lazyByIdDesc()); + + $query->chunk(1, function ($users, $page) { + assertType('Hyperf\Collection\Collection', $users); + assertType('int', $page); + }); + $query->chunkById(1, function ($users, $page) { + assertType('Hyperf\Collection\Collection', $users); + assertType('int', $page); + }); + $query->chunkMap(function ($users) { + assertType('object', $users); + }); + $query->chunkByIdDesc(1, function ($users, $page) { + assertType('Hyperf\Collection\Collection', $users); + assertType('int', $page); + }); + $query->each(function ($users, $page) { + assertType('object', $users); + assertType('int', $page); + }); + $query->eachById(function ($users, $page) { + assertType('object', $users); + assertType('int', $page); + }); +} From 37dd365197bd7e344ffb6d25036f8313060ce278 Mon Sep 17 00:00:00 2001 From: Deeka Wong Date: Wed, 29 Oct 2025 14:02:53 +0800 Subject: [PATCH 03/14] feature: Added PHPStan type analysis for `hyperf/collection` (#7578) --- {tests/types => types}/Autoload.php | 0 {tests/types => types}/Model/Builder.php | 0 {tests/types => types}/Model/Collection.php | 0 {tests/types => types}/Model/Model.php | 0 {tests/types => types}/Model/ModelNotFoundException.php | 0 {tests/types => types}/Model/Relations.php | 7 ++++++- {tests/types => types}/Query/Builder.php | 0 7 files changed, 6 insertions(+), 1 deletion(-) rename {tests/types => types}/Autoload.php (100%) rename {tests/types => types}/Model/Builder.php (100%) rename {tests/types => types}/Model/Collection.php (100%) rename {tests/types => types}/Model/Model.php (100%) rename {tests/types => types}/Model/ModelNotFoundException.php (100%) rename {tests/types => types}/Model/Relations.php (98%) rename {tests/types => types}/Query/Builder.php (100%) diff --git a/tests/types/Autoload.php b/types/Autoload.php similarity index 100% rename from tests/types/Autoload.php rename to types/Autoload.php diff --git a/tests/types/Model/Builder.php b/types/Model/Builder.php similarity index 100% rename from tests/types/Model/Builder.php rename to types/Model/Builder.php diff --git a/tests/types/Model/Collection.php b/types/Model/Collection.php similarity index 100% rename from tests/types/Model/Collection.php rename to types/Model/Collection.php diff --git a/tests/types/Model/Model.php b/types/Model/Model.php similarity index 100% rename from tests/types/Model/Model.php rename to types/Model/Model.php diff --git a/tests/types/Model/ModelNotFoundException.php b/types/Model/ModelNotFoundException.php similarity index 100% rename from tests/types/Model/ModelNotFoundException.php rename to types/Model/ModelNotFoundException.php diff --git a/tests/types/Model/Relations.php b/types/Model/Relations.php similarity index 98% rename from tests/types/Model/Relations.php rename to types/Model/Relations.php index 55908ce..cc1c45c 100644 --- a/tests/types/Model/Relations.php +++ b/types/Model/Relations.php @@ -104,7 +104,12 @@ function test(User $user, Post $post, Comment $comment, ChildUser $child): void assertType("Hyperf\\Database\\Model\\Relations\\MorphToMany", $post->tags()); assertType('Hyperf\Database\Model\Collection', $post->tags()->getResults()); - assertType('int', Relation::noConstraints(fn () => 42)); + $value = 42; + /** @var int $value */ + $value = $value; + assertType('int', Relation::noConstraints(function () use ($value): int { + return $value; + })); } class User extends Model diff --git a/tests/types/Query/Builder.php b/types/Query/Builder.php similarity index 100% rename from tests/types/Query/Builder.php rename to types/Query/Builder.php From a8d292078e6f2a48a52cf5a76dc007b83edb08db Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:44:44 +0800 Subject: [PATCH 04/14] Add `/types export-ignore` to all `.gitattributes` in `src/*` (#7581) --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 27b765f..f11271b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ /tests export-ignore /.github export-ignore +/types export-ignore From d54974856b70f3e05a83f4530ce06dad51e0ccb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=93=AD=E6=98=95?= <715557344@qq.com> Date: Mon, 1 Dec 2025 11:37:59 +0800 Subject: [PATCH 05/14] Fixed test cases (#7643) --- src/Model/Model.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Model/Model.php b/src/Model/Model.php index 1034632..742b9e0 100644 --- a/src/Model/Model.php +++ b/src/Model/Model.php @@ -29,6 +29,7 @@ use JsonSerializable; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\StoppableEventInterface; +use Stringable; use Throwable; use function Hyperf\Collection\collect; @@ -40,7 +41,7 @@ /** * @mixin ModelIDE */ -abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable, CompressInterface +abstract class Model implements Stringable, ArrayAccess, Arrayable, Jsonable, JsonSerializable, CompressInterface { use Concerns\HasAttributes; use Concerns\HasEvents; From b1edc7af2c209483850c2d920627e18136c5bb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=93=AD=E6=98=95?= <715557344@qq.com> Date: Mon, 1 Dec 2025 18:06:18 +0800 Subject: [PATCH 06/14] Optimized phpdoc for `Closure():TReturn`. (#7650) --- src/Model/Relations/Relation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Relations/Relation.php b/src/Model/Relations/Relation.php index 03fd328..08107dd 100755 --- a/src/Model/Relations/Relation.php +++ b/src/Model/Relations/Relation.php @@ -115,7 +115,7 @@ public function __clone() * Run a callback with constraints disabled on the relation. * * @template TCallbackReturn - * @param Closure(): TCallbackReturn $callback + * @param (Closure(): TCallbackReturn) $callback * @return TCallbackReturn */ public static function noConstraints(Closure $callback) From 223df68ac99e95076f3dab174cd0c32c4769be6b Mon Sep 17 00:00:00 2001 From: "Mr.tang" Date: Tue, 2 Dec 2025 22:30:56 +0800 Subject: [PATCH 07/14] phpstrom Code prompt optimization (#7579) --- src/Model/Relations/Relation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Relations/Relation.php b/src/Model/Relations/Relation.php index 08107dd..16ff7a8 100755 --- a/src/Model/Relations/Relation.php +++ b/src/Model/Relations/Relation.php @@ -115,7 +115,7 @@ public function __clone() * Run a callback with constraints disabled on the relation. * * @template TCallbackReturn - * @param (Closure(): TCallbackReturn) $callback + * @param Closure():TCallbackReturn $callback * @return TCallbackReturn */ public static function noConstraints(Closure $callback) From dc6cfae2edcee277a6eecffcdfa83014593f8899 Mon Sep 17 00:00:00 2001 From: Deeka Wong Date: Thu, 4 Dec 2025 11:05:16 +0800 Subject: [PATCH 08/14] feat: update HasUuids to use UUIDv7 instead of ordered UUID (#7644) --- src/Model/Concerns/HasUuids.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Concerns/HasUuids.php b/src/Model/Concerns/HasUuids.php index 3bd3603..5d27dbe 100644 --- a/src/Model/Concerns/HasUuids.php +++ b/src/Model/Concerns/HasUuids.php @@ -23,7 +23,7 @@ trait HasUuids */ public function newUniqueId() { - return (string) Str::orderedUuid(); + return (string) Str::uuidv7(); } /** From 2e5040a6d2ff5a18111365bc9bc79d6575bf6999 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:33:06 +1030 Subject: [PATCH 09/14] Added methods `cascadeOnDelete()`, `restrictOnDelete()`, `noActionOnDelete()`, `restrictOnUpdate()`, `nullOnUpdate()`, `noActionOnUpdate()` for `ForeignKeyDefinition`. (#7693) --- src/Schema/ForeignKeyDefinition.php | 48 +++++++++++++++++++++++ tests/MySqlSchemaGrammarTest.php | 60 +++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/src/Schema/ForeignKeyDefinition.php b/src/Schema/ForeignKeyDefinition.php index 19fc325..d2c6630 100644 --- a/src/Schema/ForeignKeyDefinition.php +++ b/src/Schema/ForeignKeyDefinition.php @@ -32,6 +32,46 @@ public function cascadeOnUpdate(): static return $this->onUpdate('cascade'); } + /** + * Indicate that updates should be restricted. + */ + public function restrictOnUpdate(): static + { + return $this->onUpdate('restrict'); + } + + /** + * Indicate that updates should set the foreign key value to null. + */ + public function nullOnUpdate(): static + { + return $this->onUpdate('set null'); + } + + /** + * Indicate that updates should have "no action". + */ + public function noActionOnUpdate(): static + { + return $this->onUpdate('no action'); + } + + /** + * Indicate that deletes should cascade. + */ + public function cascadeOnDelete(): static + { + return $this->onDelete('cascade'); + } + + /** + * Indicate that deletes should be restricted. + */ + public function restrictOnDelete(): static + { + return $this->onDelete('restrict'); + } + /** * Indicate that deletes should set the foreign key value to null. */ @@ -39,4 +79,12 @@ public function nullOnDelete(): static { return $this->onDelete('set null'); } + + /** + * Indicate that deletes should have "no action". + */ + public function noActionOnDelete(): static + { + return $this->onDelete('no action'); + } } diff --git a/tests/MySqlSchemaGrammarTest.php b/tests/MySqlSchemaGrammarTest.php index 33c9cfc..b20779f 100755 --- a/tests/MySqlSchemaGrammarTest.php +++ b/tests/MySqlSchemaGrammarTest.php @@ -546,6 +546,66 @@ public function testAddingForeignKey() $this->assertSame('alter table `users` add constraint `users_foo_id_foreign` foreign key (`foo_id`) references `orders` (`id`)', $statements[0]); } + public function testAddingForeignKeyWithCascadeOnDelete() + { + $blueprint = new Blueprint('users'); + $blueprint->foreign('foo_id')->references('id')->on('orders')->cascadeOnDelete(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add constraint `users_foo_id_foreign` foreign key (`foo_id`) references `orders` (`id`) on delete cascade', $statements[0]); + } + + public function testAddingForeignKeyWithRestrictOnDelete() + { + $blueprint = new Blueprint('users'); + $blueprint->foreign('foo_id')->references('id')->on('orders')->restrictOnDelete(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add constraint `users_foo_id_foreign` foreign key (`foo_id`) references `orders` (`id`) on delete restrict', $statements[0]); + } + + public function testAddingForeignKeyWithNoActionOnDelete() + { + $blueprint = new Blueprint('users'); + $blueprint->foreign('foo_id')->references('id')->on('orders')->noActionOnDelete(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add constraint `users_foo_id_foreign` foreign key (`foo_id`) references `orders` (`id`) on delete no action', $statements[0]); + } + + public function testAddingForeignKeyWithRestrictOnUpdate() + { + $blueprint = new Blueprint('users'); + $blueprint->foreign('foo_id')->references('id')->on('orders')->restrictOnUpdate(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add constraint `users_foo_id_foreign` foreign key (`foo_id`) references `orders` (`id`) on update restrict', $statements[0]); + } + + public function testAddingForeignKeyWithNullOnUpdate() + { + $blueprint = new Blueprint('users'); + $blueprint->foreign('foo_id')->references('id')->on('orders')->nullOnUpdate(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add constraint `users_foo_id_foreign` foreign key (`foo_id`) references `orders` (`id`) on update set null', $statements[0]); + } + + public function testAddingForeignKeyWithNoActionOnUpdate() + { + $blueprint = new Blueprint('users'); + $blueprint->foreign('foo_id')->references('id')->on('orders')->noActionOnUpdate(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add constraint `users_foo_id_foreign` foreign key (`foo_id`) references `orders` (`id`) on update no action', $statements[0]); + } + public function testAddingIncrementingID() { $blueprint = new Blueprint('users'); From c9d3cb65869c0e8050f53454acc79464d4e2a6db Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 25 Jan 2026 12:09:41 +1030 Subject: [PATCH 10/14] feat: Port `whereJsonContainsKey` methods + `CompilesJsonPaths` from Laravel (#7699) --- src/Concerns/CompilesJsonPaths.php | 76 +++++++++++++++++++++++++++ src/Query/Builder.php | 36 +++++++++++++ src/Query/Grammars/Grammar.php | 48 ++++++++--------- src/Query/Grammars/MySqlGrammar.php | 10 ++++ src/Schema/Grammars/Grammar.php | 3 ++ tests/QueryBuilderTest.php | 80 +++++++++++++++++++++++++++++ 6 files changed, 226 insertions(+), 27 deletions(-) create mode 100644 src/Concerns/CompilesJsonPaths.php diff --git a/src/Concerns/CompilesJsonPaths.php b/src/Concerns/CompilesJsonPaths.php new file mode 100644 index 0000000..7d89b00 --- /dev/null +++ b/src/Concerns/CompilesJsonPaths.php @@ -0,0 +1,76 @@ +', $column, 2); + + $field = $this->wrap($parts[0]); + + $path = count($parts) > 1 ? ', ' . $this->wrapJsonPath($parts[1], '->') : ''; + + return [$field, $path]; + } + + /** + * Wrap the given JSON path. + * + * @param string $value + * @param string $delimiter + * @return string + */ + protected function wrapJsonPath($value, $delimiter = '->') + { + $value = preg_replace("/([\\\\]+)?\\'/", "''", $value); + + $jsonPath = collect(explode($delimiter, $value)) + ->map(fn ($segment) => $this->wrapJsonPathSegment($segment)) + ->join('.'); + + return "'$" . (str_starts_with($jsonPath, '[') ? '' : '.') . $jsonPath . "'"; + } + + /** + * Wrap the given JSON path segment. + * + * @param string $segment + * @return string + */ + protected function wrapJsonPathSegment($segment) + { + if (preg_match('/(\[[^\]]+\])+$/', $segment, $parts)) { + $key = Str::beforeLast($segment, $parts[0]); + + if (! empty($key)) { + return '"' . $key . '"' . $parts[0]; + } + + return $parts[0]; + } + + return '"' . $segment . '"'; + } +} diff --git a/src/Query/Builder.php b/src/Query/Builder.php index c069e7e..6e61138 100755 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -1560,6 +1560,42 @@ public function orWhereJsonDoesntOverlap(string $column, mixed $value): static return $this->whereJsonDoesntOverlap($column, $value, 'or'); } + /** + * Add a clause that determines if a JSON path exists to the query. + */ + public function whereJsonContainsKey(string $column, string $boolean = 'and', bool $not = false): static + { + $type = 'JsonContainsKey'; + + $this->wheres[] = compact('type', 'column', 'boolean', 'not'); + + return $this; + } + + /** + * Add an "or" clause that determines if a JSON path exists to the query. + */ + public function orWhereJsonContainsKey(string $column): static + { + return $this->whereJsonContainsKey($column, 'or'); + } + + /** + * Add a clause that determines if a JSON path does not exist to the query. + */ + public function whereJsonDoesntContainKey(string $column, string $boolean = 'and'): static + { + return $this->whereJsonContainsKey($column, $boolean, true); + } + + /** + * Add an "or" clause that determines if a JSON path does not exist to the query. + */ + public function orWhereJsonDoesntContainKey(string $column): static + { + return $this->whereJsonDoesntContainKey($column, 'or'); + } + /** * Add an "where Bit Functions and Operators" clause to the query. */ diff --git a/src/Query/Grammars/Grammar.php b/src/Query/Grammars/Grammar.php index a98a897..6364ca8 100755 --- a/src/Query/Grammars/Grammar.php +++ b/src/Query/Grammars/Grammar.php @@ -13,6 +13,7 @@ namespace Hyperf\Database\Query\Grammars; use Hyperf\Collection\Arr; +use Hyperf\Database\Concerns\CompilesJsonPaths; use Hyperf\Database\Grammar as BaseGrammar; use Hyperf\Database\Query\Builder; use Hyperf\Database\Query\Expression; @@ -27,6 +28,8 @@ class Grammar extends BaseGrammar { + use CompilesJsonPaths; + /** * The grammar specific operators. */ @@ -398,6 +401,24 @@ protected function compileJsonOverlaps(string $column, string $value): string throw new RuntimeException('This database engine does not support JSON overlaps operations.'); } + /** + * Compile a "where JSON contains key" clause. + */ + protected function whereJsonContainsKey(Builder $query, array $where): string + { + $not = $where['not'] ? 'not ' : ''; + + return $not . $this->compileJsonContainsKey($where['column']); + } + + /** + * Compile a "JSON contains key" statement into SQL. + */ + protected function compileJsonContainsKey(string $column): string + { + throw new RuntimeException('This database engine does not support JSON contains key operations.'); + } + /** * Compile the components necessary for a select clause. */ @@ -1108,33 +1129,6 @@ protected function wrapJsonSelector($value): string throw new RuntimeException('This database engine does not support JSON operations.'); } - /** - * Split the given JSON selector into the field and the optional path and wrap them separately. - * - * @param string $column - */ - protected function wrapJsonFieldAndPath($column): array - { - $parts = explode('->', $column, 2); - - $field = $this->wrap($parts[0]); - - $path = count($parts) > 1 ? ', ' . $this->wrapJsonPath($parts[1], '->') : ''; - - return [$field, $path]; - } - - /** - * Wrap the given JSON path. - * - * @param string $value - * @param string $delimiter - */ - protected function wrapJsonPath($value, $delimiter = '->'): string - { - return '\'$."' . str_replace($delimiter, '"."', $value) . '"\''; - } - /** * Determine if the given string is a JSON selector. * diff --git a/src/Query/Grammars/MySqlGrammar.php b/src/Query/Grammars/MySqlGrammar.php index fe929c4..55ed879 100755 --- a/src/Query/Grammars/MySqlGrammar.php +++ b/src/Query/Grammars/MySqlGrammar.php @@ -246,6 +246,16 @@ protected function compileJsonOverlaps(string $column, string $value): string return 'json_overlaps(' . $field . ', ' . $value . $path . ')'; } + /** + * Compile a "JSON contains key" statement into SQL. + */ + protected function compileJsonContainsKey(string $column): string + { + [$field, $path] = $this->wrapJsonFieldAndPath($column); + + return 'ifnull(json_contains_path(' . $field . ', \'one\'' . $path . '), 0)'; + } + /** * Compile a "JSON length" statement into SQL. * diff --git a/src/Schema/Grammars/Grammar.php b/src/Schema/Grammars/Grammar.php index 94cc90c..622e7a0 100755 --- a/src/Schema/Grammars/Grammar.php +++ b/src/Schema/Grammars/Grammar.php @@ -14,6 +14,7 @@ use Doctrine\DBAL\Schema\AbstractSchemaManager as SchemaManager; use Doctrine\DBAL\Schema\TableDiff; +use Hyperf\Database\Concerns\CompilesJsonPaths; use Hyperf\Database\Connection; use Hyperf\Database\Grammar as BaseGrammar; use Hyperf\Database\Query\Expression; @@ -25,6 +26,8 @@ abstract class Grammar extends BaseGrammar { + use CompilesJsonPaths; + /** * If this Grammar supports schema changes wrapped in a transaction. */ diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 9b39799..ed310f6 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -2153,6 +2153,29 @@ public function testMySqlUpdateWrappingNestedJson() ]); } + public function testMySqlUpdateWrappingJsonPathArrayIndex() + { + $grammar = new MySqlGrammar(); + $processor = Mockery::mock(Processor::class); + + $connection = $this->createMock(ConnectionInterface::class); + $connection->expects($this->once()) + ->method('update') + ->with( + 'update `users` set `options` = json_set(`options`, \'$[1]."2fa"\', false), `meta` = json_set(`meta`, \'$."tags"[0][2]\', ?) where `active` = ?', + [ + 'large', + 1, + ] + ); + + $builder = new Builder($connection, $grammar, $processor); + $builder->from('users')->where('active', 1)->update([ + 'options->[1]->2fa' => false, + 'meta->tags[0][2]' => 'large', + ]); + } + public function testMySqlUpdateWithJsonPreparesBindingsCorrectly() { $grammar = new MySqlGrammar(); @@ -2257,6 +2280,29 @@ public function testMySqlWrappingJson() $this->assertEquals('select * from `users` where json_unquote(json_extract(`items`, \'$."price"."in_usd"\')) = ? and json_unquote(json_extract(`items`, \'$."age"\')) = ?', $builder->toSql()); } + public function testJsonPathEscaping() + { + $expectedWithJsonEscaped = <<<'SQL' +select json_unquote(json_extract(`json`, '$."''))#"')) +SQL; + + $builder = $this->getMySqlBuilder(); + $builder->select("json->'))#"); + $this->assertEquals($expectedWithJsonEscaped, $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->select("json->\\'))#"); + $this->assertEquals($expectedWithJsonEscaped, $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->select("json->\\'))#"); + $this->assertEquals($expectedWithJsonEscaped, $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->select("json->\\\\'))#"); + $this->assertEquals($expectedWithJsonEscaped, $builder->toSql()); + } + public function testMySqlSoundsLikeOperator() { $builder = $this->getMySqlBuilder(); @@ -2971,6 +3017,40 @@ public function testWhereJsonDoesntContainMySql() $this->assertEquals([1], $builder->getBindings()); } + public function testWhereJsonContainsKeyMySql() + { + $builder = $this->getMySqlBuilder(); + $builder->select('*')->from('users')->whereJsonContainsKey('users.options->languages'); + $this->assertSame('select * from `users` where ifnull(json_contains_path(`users`.`options`, \'one\', \'$."languages"\'), 0)', $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->select('*')->from('users')->whereJsonContainsKey('options->language->primary'); + $this->assertSame('select * from `users` where ifnull(json_contains_path(`options`, \'one\', \'$."language"."primary"\'), 0)', $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->select('*')->from('users')->where('id', '=', 1)->orWhereJsonContainsKey('options->languages'); + $this->assertSame('select * from `users` where `id` = ? or ifnull(json_contains_path(`options`, \'one\', \'$."languages"\'), 0)', $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->select('*')->from('users')->whereJsonContainsKey('options->languages[0][1]'); + $this->assertSame('select * from `users` where ifnull(json_contains_path(`options`, \'one\', \'$."languages"[0][1]\'), 0)', $builder->toSql()); + } + + public function testWhereJsonDoesntContainKeyMySql() + { + $builder = $this->getMySqlBuilder(); + $builder->select('*')->from('users')->whereJsonDoesntContainKey('options->languages'); + $this->assertSame('select * from `users` where not ifnull(json_contains_path(`options`, \'one\', \'$."languages"\'), 0)', $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->select('*')->from('users')->where('id', '=', 1)->orWhereJsonDoesntContainKey('options->languages'); + $this->assertSame('select * from `users` where `id` = ? or not ifnull(json_contains_path(`options`, \'one\', \'$."languages"\'), 0)', $builder->toSql()); + + $builder = $this->getMySqlBuilder(); + $builder->select('*')->from('users')->whereJsonDoesntContainKey('options->languages[0][1]'); + $this->assertSame('select * from `users` where not ifnull(json_contains_path(`options`, \'one\', \'$."languages"[0][1]\'), 0)', $builder->toSql()); + } + public function testWhereJsonLengthMySql() { $builder = $this->getMySqlBuilder(); From 02ffa406ccaef50b7538ec5f0ba80a9890a98f13 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:28:53 +1030 Subject: [PATCH 11/14] Add Laravel's `withoutForeignKeyConstraints` method to Schema Builder (#7684) --- src/Schema/Builder.php | 14 ++++++++++++++ src/Schema/Schema.php | 1 + 2 files changed, 15 insertions(+) diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index 7603feb..c7bfc43 100755 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -353,6 +353,20 @@ public function disableForeignKeyConstraints(): bool ); } + /** + * Disable foreign key constraints during the execution of a callback. + */ + public function withoutForeignKeyConstraints(Closure $callback): mixed + { + $this->disableForeignKeyConstraints(); + + try { + return $callback(); + } finally { + $this->enableForeignKeyConstraints(); + } + } + /** * Get the foreign keys for a given table. */ diff --git a/src/Schema/Schema.php b/src/Schema/Schema.php index 55cded1..fe301ee 100644 --- a/src/Schema/Schema.php +++ b/src/Schema/Schema.php @@ -44,6 +44,7 @@ * @method static void rename(string $from, string $to) * @method static bool enableForeignKeyConstraints() * @method static bool disableForeignKeyConstraints() + * @method static mixed withoutForeignKeyConstraints(\Closure $callback) * @method static \Hyperf\Database\Connection getConnection() * @method static Builder setConnection(\Hyperf\Database\Connection $connection) * @method static void blueprintResolver(\Closure $resolver) From 1cd7d6e801c4c6141542e74a704622b1acf0d3a0 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 25 Jan 2026 13:33:10 +1030 Subject: [PATCH 12/14] fix(database): correct return type for Blueprint::foreign() (#7682) --- src/Schema/Blueprint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Schema/Blueprint.php b/src/Schema/Blueprint.php index 484ce6d..cb54c3b 100755 --- a/src/Schema/Blueprint.php +++ b/src/Schema/Blueprint.php @@ -481,7 +481,7 @@ public function spatialIndex($columns, $name = null) * * @param array|string $columns * @param string $name - * @return Fluent|ForeignKeyDefinition + * @return ForeignKeyDefinition */ public function foreign($columns, $name = null) { From 24dff99db6bc6f74975f6fd7953ab0338aca4193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=93=AD=E6=98=95?= <715557344@qq.com> Date: Fri, 30 Jan 2026 18:17:09 +0800 Subject: [PATCH 13/14] Fixed bug that `eachById` cannot work for `Hyperf\Database\Query\Builder`. (#7705) --- src/Concerns/BuildsQueries.php | 2 +- tests/ModelRealBuilderTest.php | 20 ++++++++++++++++++++ tests/Stubs/ContainerStub.php | 2 ++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Concerns/BuildsQueries.php b/src/Concerns/BuildsQueries.php index daf05c1..e7ad0e2 100644 --- a/src/Concerns/BuildsQueries.php +++ b/src/Concerns/BuildsQueries.php @@ -187,7 +187,7 @@ public function first($columns = ['*']) */ public function eachById(callable $callback, int $count = 1000, ?string $column = null, ?string $alias = null): bool { - return $this->chunkById($count, function (Collection $results) use ($callback) { + return $this->chunkById($count, function (BaseCollection $results) use ($callback) { foreach ($results as $value) { if ($callback($value) === false) { return false; diff --git a/tests/ModelRealBuilderTest.php b/tests/ModelRealBuilderTest.php index c9209b4..66c2cbc 100644 --- a/tests/ModelRealBuilderTest.php +++ b/tests/ModelRealBuilderTest.php @@ -67,6 +67,7 @@ use PHPUnit\Framework\TestCase; use Psr\EventDispatcher\EventDispatcherInterface; use RuntimeException; +use stdClass; /** * @internal @@ -98,6 +99,25 @@ protected function tearDown(): void Mockery::close(); } + public function testEachById() + { + $this->getContainer(); + + $count = User::query()->count(); + + $i = 0; + User::query()->eachById(function (User $user) use (&$i) { + ++$i; + }); + $this->assertSame($i, $count); + + $i = 0; + Db::table('user')->eachById(function (stdClass $user) use (&$i) { + ++$i; + }, 100, 'id'); + $this->assertSame($i, $count); + } + public function testPivot() { $this->getContainer(); diff --git a/tests/Stubs/ContainerStub.php b/tests/Stubs/ContainerStub.php index f212fb4..ce9d458 100644 --- a/tests/Stubs/ContainerStub.php +++ b/tests/Stubs/ContainerStub.php @@ -19,6 +19,7 @@ use Hyperf\Database\ConnectionResolverInterface; use Hyperf\Database\Connectors\ConnectionFactory; use Hyperf\Database\Connectors\MySqlConnector; +use Hyperf\DbConnection\Db; use Hyperf\Di\Container; use Mockery; use Psr\EventDispatcher\EventDispatcherInterface; @@ -35,6 +36,7 @@ public static function getContainer($callback = null) $container->shouldReceive('has')->with(StdoutLoggerInterface::class)->andReturnFalse(); $container->shouldReceive('has')->with(EventDispatcherInterface::class)->andReturnFalse(); $container->shouldReceive('get')->with('db.connector.mysql')->andReturn(new MySqlConnector()); + $container->shouldReceive('get')->with(Db::class)->andReturn(new Db($container)); $connector = new ConnectionFactory($container); $dbConfig = [ From 2476ecb0172d7ea5e88ae3704069f0085f56134b Mon Sep 17 00:00:00 2001 From: Adhik Joshi Date: Sun, 1 Feb 2026 13:24:40 +0530 Subject: [PATCH 14/14] Added casts `AsCollection` for `hyperf/database`. (#7607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 李铭昕 <715557344@qq.com> --- src/Model/Casts/AsCollection.php | 95 ++++++++++++++++++++++++ src/Model/Casts/Json.php | 89 ++++++++++++++++++++++ tests/DatabaseModelCustomCastingTest.php | 43 +++++++++++ 3 files changed, 227 insertions(+) create mode 100644 src/Model/Casts/AsCollection.php create mode 100644 src/Model/Casts/Json.php diff --git a/src/Model/Casts/AsCollection.php b/src/Model/Casts/AsCollection.php new file mode 100644 index 0000000..37f65e5 --- /dev/null +++ b/src/Model/Casts/AsCollection.php @@ -0,0 +1,95 @@ +collectionClass) ? Collection::class : $this->collectionClass; + + if (! is_a($collectionClass, Collection::class, true)) { + throw new InvalidArgumentException('The provided class must extend [' . Collection::class . '].'); + } + + if (! is_array($data)) { + return null; + } + + $instance = new $collectionClass($data); + + if (empty($this->parseCallback)) { + return $instance; + } + + $parseCallback = Str::parseCallback($this->parseCallback); + if (is_callable($parseCallback)) { + return $instance->map($parseCallback); + } + + return $instance->mapInto($parseCallback[0]); + } + + public function set($model, $key, $value, $attributes) + { + return [$key => Json::encode($value)]; + } + + /** + * Specify the type of object each item in the collection should be mapped to. + * + * @param array{class-string, string}|class-string $map + * @return string + */ + public static function of($map) + { + return static::using('', $map); + } + + /** + * Specify the collection type for the cast. + * + * @param class-string $class + * @param array{class-string, string}|class-string $map + * @return string + */ + public static function using($class, $map = null) + { + if ( + is_array($map) + && count($map) === 2 + && is_string($map[0]) + && is_string($map[1]) + && is_callable($map) + ) { + $map = $map[0] . '@' . $map[1]; + } + + return static::class . ':' . implode(',', [$class, $map]); + } +} diff --git a/src/Model/Casts/Json.php b/src/Model/Casts/Json.php new file mode 100644 index 0000000..781dd13 --- /dev/null +++ b/src/Model/Casts/Json.php @@ -0,0 +1,89 @@ +assertEquals($data, $unserialized->config->toArray()); $this->assertEquals(json_encode($data), $unserialized->getAttributes()['config']); } + + public function testCastsAsCollection() + { + $m = new TestModelWithCustomCast(); + $m->fill([ + 'as_collection' => [['id' => 1, 'name' => 'hyperf'], ['id' => 2, 'name' => 'swoole']], + 'as_collection2' => [['id' => 3, 'name' => 'hyperf'], ['id' => 4, 'name' => 'swoole']], + ]); + + /** @var Collection $collection */ + $collection = $m->as_collection; + + $this->assertSame(1, $collection->first()['id']); + $this->assertSame(2, $collection->last()['id']); + + /** @var Collection $collection */ + $collection = $m->as_collection2; + + $this->assertSame(3, $collection->first()->id); + $this->assertSame(4, $collection->last()->id); + } } class TestModelWithCustomCast extends Model @@ -415,7 +437,28 @@ class TestModelWithCustomCast extends Model 'value_object_caster_with_caster_instance' => ValueObjectWithCasterInstance::class, 'cast_using' => CastUsing::class, 'invalid_caster' => InvalidCaster::class, + 'as_collection' => AsCollection::class, + 'as_collection2' => AsCollection::class, ]; + + public function getCasts(): array + { + $casts = parent::getCasts(); + $casts['as_collection2'] = AsCollection::using(Collection::class, [TestModelValue::class, 'from']); + return $casts; + } +} + +class TestModelValue +{ + public function __construct(public int $id, public string $name) + { + } + + public static function from(array $item) + { + return new TestModelValue($item['id'], $item['name']); + } } class TestModelWithArrayObjectCast extends Model