Skip to content

Commit

Permalink
feat: add support for create or first (#1809)
Browse files Browse the repository at this point in the history
  • Loading branch information
remcom committed Dec 30, 2023
1 parent b96c1a9 commit 0e54f3f
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/Methods/BuilderHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class BuilderHelper
public const MODEL_RETRIEVAL_METHODS = ['first', 'find', 'findMany', 'findOrFail', 'firstOrFail', 'sole'];

/** @var string[] */
public const MODEL_CREATION_METHODS = ['make', 'create', 'forceCreate', 'findOrNew', 'firstOrNew', 'updateOrCreate', 'firstOrCreate'];
public const MODEL_CREATION_METHODS = ['make', 'create', 'forceCreate', 'findOrNew', 'firstOrNew', 'updateOrCreate', 'firstOrCreate', 'createOrFirst'];

/**
* The methods that should be returned from query builder.
Expand Down
155 changes: 155 additions & 0 deletions stubs/10.0.0/BelongsToMany.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php

namespace Illuminate\Database\Eloquent\Relations;

/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @extends Relation<TRelatedModel>
*/
class BelongsToMany extends Relation
{
/**
* Find a related model by its primary key or return new instance of the related model.
*
* @param mixed $id
* @param array<int, (model-property<TRelatedModel>|'*')>|model-property<TRelatedModel>|'*' $columns
* @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Support\Collection<int, TRelatedModel> : TRelatedModel)
*/
public function findOrNew($id, $columns = ['*']);

/**
* Get the first related model record matching the attributes or instantiate it.
*
* @param array<string, mixed> $attributes
* @return TRelatedModel
*/
public function firstOrNew(array $attributes);

/**
* Get the first related record matching the attributes or create it.
*
* @param array<string, mixed> $attributes
* @param array<mixed> $joining
* @param bool $touch
* @return TRelatedModel
*/
public function firstOrCreate(array $attributes, array $joining = [], $touch = true);

/**
* Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record.
*
* @param array<string, mixed> $attributes
* @param array<mixed> $joining
* @param bool $touch
* @return TRelatedModel
*/
public function createOrFirst(array $attributes, array $joining = [], $touch = true);

/**
* Create or update a related record matching the attributes, and fill it with values.
*
* @param array<string, mixed> $attributes
* @param array<mixed> $values
* @param array<mixed> $joining
* @param bool $touch
* @return TRelatedModel
*/
public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true);

/**
* Find a related model by its primary key.
*
* @param mixed $id
* @param array<int, mixed> $columns
* @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> : TRelatedModel|null)
*/
public function find($id, $columns = ['*']);

/**
* Find multiple related models by their primary keys.
*
* @param \Illuminate\Contracts\Support\Arrayable<array-key, mixed>|int[] $ids
* @param array<int, mixed> $columns
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
*/
public function findMany($ids, $columns = ['*']);

/**
* Find a related model by its primary key or throw an exception.
*
* @param mixed $id
* @param array<int, mixed> $columns
* @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Database\Eloquent\Collection<int, TRelatedModel> : TRelatedModel)
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findOrFail($id, $columns = ['*']);

/**
* Execute the query and get the first result.
*
* @param array<int, mixed> $columns
* @return TRelatedModel|null
*/
public function first($columns = ['*']);

/**
* Execute the query and get the first result or throw an exception.
*
* @param array<int, mixed> $columns
* @return TRelatedModel
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function firstOrFail($columns = ['*']);

/**
* Create a new instance of the related model.
*
* @param array<model-property<TRelatedModel>, mixed> $attributes
* @param mixed[] $joining
* @param bool $touch
* @return TRelatedModel
*/
public function create(array $attributes = [], array $joining = [], $touch = true);

/**
* Get the results of the relationship.
*
* @phpstan-return \Traversable<int, TRelatedModel>
*/
public function getResults();

/**
* Get a paginator for the "select" statement.
*
* @param int|null $perPage
* @param array<int, mixed> $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Pagination\LengthAwarePaginator<TRelatedModel>
*/
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);

/**
* Paginate the given query into a simple paginator.
*
* @param int|null $perPage
* @param array<int, mixed> $columns
* @param string $pageName
* @param int|null $page
* @return \Illuminate\Pagination\Paginator<TRelatedModel>
*/
public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null);

/**
* Paginate the given query into a cursor paginator.
*
* @param int|null $perPage
* @param array<int, mixed> $columns
* @param string $cursorName
* @param string|null $cursor
* @return \Illuminate\Pagination\CursorPaginator<TRelatedModel>
*/
public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null);
}
9 changes: 9 additions & 0 deletions stubs/10.0.0/EloquentBuilder.stub
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ class Builder
*/
public function firstOrCreate(array $attributes, array $values = []);

/**
* Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record.
*
* @param array<model-property<TModelClass>, mixed> $attributes
* @param array<model-property<TModelClass>, mixed> $values
* @phpstan-return TModelClass
*/
public function createOrFirst(array $attributes = [], array $values = []);

/**
* Create or update a record matching the attributes, and fill it with values.
*
Expand Down
95 changes: 95 additions & 0 deletions stubs/10.0.0/HasOneOrMany.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace Illuminate\Database\Eloquent\Relations;

/**
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
* @extends Relation<TRelatedModel>
*/
abstract class HasOneOrMany extends Relation
{
/**
* Create a new has one or many relationship instance.
*
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
* @param TRelatedModel $parent
* @param string $foreignKey
* @param string $localKey
* @return void
*/
public function __construct(\Illuminate\Database\Eloquent\Builder $query, \Illuminate\Database\Eloquent\Model $parent, $foreignKey, $localKey);

/**
* @param array<model-property<TRelatedModel>, mixed> $attributes
* @phpstan-return TRelatedModel
*/
public function make(array $attributes = []);

/**
* Find a model by its primary key or return new instance of the related model.
*
* @param mixed $id
* @param array<int, mixed> $columns
* @return ($id is (\Illuminate\Contracts\Support\Arrayable<array-key, mixed>|array<mixed>) ? \Illuminate\Support\Collection<int, TRelatedModel> : TRelatedModel)
*/
public function findOrNew($id, $columns = ['*']);

/**
* Get the first related model record matching the attributes or instantiate it.
*
* @param array<model-property<TRelatedModel>, mixed> $attributes
* @param array<mixed, mixed> $values
* @return TRelatedModel
*/
public function firstOrNew(array $attributes, array $values = []);

/**
* Get the first related record matching the attributes or create it.
*
* @param array<model-property<TRelatedModel>, mixed> $attributes
* @param array<mixed, mixed> $values
* @return TRelatedModel
*/
public function firstOrCreate(array $attributes, array $values = []);

/**
* Attempt to create the record. If a unique constraint violation occurs, attempt to find the matching record.
*
* @param array<model-property<TRelatedModel>, mixed> $attributes
* @param array<mixed, mixed> $values
* @return TRelatedModel
*/
public function createOrFirst(array $attributes, array $values = []);

/**
* Create or update a related record matching the attributes, and fill it with values.
*
* @param array<model-property<TRelatedModel>, mixed> $attributes
* @param array<array-key, mixed> $values
* @return TRelatedModel
*/
public function updateOrCreate(array $attributes, array $values = []);

/**
* Attach a model instance to the parent model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return TRelatedModel|false
*/
public function save(\Illuminate\Database\Eloquent\Model $model);

/**
* @phpstan-param array<model-property<TRelatedModel>, mixed> $attributes
*
* @phpstan-return TRelatedModel
*/
public function create(array $attributes = []);

/**
* Create a Collection of new instances of the related model.
*
* @param iterable<mixed> $records
* @return \Illuminate\Database\Eloquent\Collection<int, TRelatedModel>
*/
public function createMany(iterable $records);
}
9 changes: 9 additions & 0 deletions tests/Rules/ModelPropertyRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ public function testModelPropertyRuleOnRelation(): void
10,
],
]);

if (version_compare(LARAVEL_VERSION, '10.20.0', '>=')) {
$this->analyse([__DIR__.'/data/model-property-relation-l10-20.php'], [
[
'Property \'foo\' does not exist in App\\Account model.',
4,
]
]);
}
}

public function testModelPropertyRuleOnModel(): void
Expand Down
4 changes: 4 additions & 0 deletions tests/Rules/data/model-property-relation-l10-20.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php

/** @var \App\User $user */
$user->accounts()->createOrFirst(['foo' => 'bar']);
5 changes: 5 additions & 0 deletions tests/Type/GeneralTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__.'/data/custom-eloquent-collection.php');
yield from $this->gatherAssertTypes(__DIR__.'/data/translator.php');

if (version_compare(LARAVEL_VERSION, '10.20.0', '>=')) {
yield from $this->gatherAssertTypes(__DIR__.'/data/model-relations-l10-20.php');
yield from $this->gatherAssertTypes(__DIR__.'/data/model-l10-20.php');
}

//##############################################################################################################

// Console Commands
Expand Down
12 changes: 12 additions & 0 deletions tests/Type/data/model-l10-20.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Model;

use App\User;

use function PHPStan\Testing\assertType;

function testCreateOrFirst()
{
assertType('App\User', User::createOrFirst([]));
}
13 changes: 13 additions & 0 deletions tests/Type/data/model-relations-l10-20.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace ModelRelations;

use App\Account;
use App\User;

use function PHPStan\Testing\assertType;

function test(User $user, \App\Address $address, Account $account, ExtendsModelWithPropertyAnnotations $model, Tag $tag, User|Account $union)
{
assertType('App\Account', $user->accounts()->createOrFirst([]));
}

0 comments on commit 0e54f3f

Please sign in to comment.