Skip to content

Commit 5196f8f

Browse files
authored
Merge pull request #157 from caugner/firstOrCreate-firstOrNew
Ensure firstOrNew/Create can be called without parameters in Laravel 8+
2 parents 04dd39b + 02f4b50 commit 5196f8f

8 files changed

+256
-66
lines changed

stubs/6/EloquentBuilder.stubphp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent;
4+
/**
5+
* @template-covariant TModel of \Illuminate\Database\Eloquent\Model
6+
* @property-read HigherOrderBuilderProxy $orWhere
7+
*
8+
* @mixin \Illuminate\Database\Query\Builder
9+
*/
10+
class Builder
11+
{
12+
/**
13+
* @param array $attributes
14+
* @param array $values
15+
* @return TModel
16+
*/
17+
public function firstOrNew(array $attributes, array $values = []) { }
18+
19+
/**
20+
* @param array $attributes
21+
* @param array $values
22+
* @return TModel
23+
*/
24+
public function firstOrCreate(array $attributes, array $values = []) { }
25+
}

stubs/6/HasOneOrMany.stubphp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent\Relations;
4+
5+
/**
6+
* @template TRelatedModel of Model
7+
* @template-extends Relation<TRelatedModel>
8+
* @mixin \Illuminate\Database\Eloquent\Builder<TRelatedModel>
9+
*/
10+
abstract class HasOneOrMany extends Relation
11+
{
12+
/**
13+
* @param array $attributes
14+
* @param array $values
15+
* @return \Illuminate\Database\Eloquent\Model
16+
* @psalm-return TRelatedModel
17+
*/
18+
public function firstOrNew(array $attributes, array $values = []) { }
19+
20+
/**
21+
* @param array $attributes
22+
* @param array $values
23+
* @return \Illuminate\Database\Eloquent\Model
24+
* @psalm-return TRelatedModel
25+
*/
26+
public function firstOrCreate(array $attributes, array $values = []) { }
27+
}

stubs/8/EloquentBuilder.stubphp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent;
4+
5+
/**
6+
* @template-covariant TModel of \Illuminate\Database\Eloquent\Model
7+
* @property-read HigherOrderBuilderProxy $orWhere
8+
*
9+
* @mixin \Illuminate\Database\Query\Builder
10+
*/
11+
class Builder
12+
{
13+
/**
14+
* @param array $attributes
15+
* @param array $values
16+
* @return TModel
17+
*/
18+
public function firstOrNew(array $attributes = [], array $values = []) { }
19+
20+
/**
21+
* @param array $attributes
22+
* @param array $values
23+
* @return TModel
24+
*/
25+
public function firstOrCreate(array $attributes = [], array $values = []) { }
26+
}

stubs/8/HasOneOrMany.stubphp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Illuminate\Database\Eloquent\Relations;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Builder;
7+
8+
/**
9+
* @template TRelatedModel of Model
10+
* @template-extends Relation<TRelatedModel>
11+
* @mixin \Illuminate\Database\Eloquent\Builder<TRelatedModel>
12+
*/
13+
abstract class HasOneOrMany extends Relation
14+
{
15+
/**
16+
* @param array $attributes
17+
* @param array $values
18+
* @return \Illuminate\Database\Eloquent\Model
19+
* @psalm-return TRelatedModel
20+
*/
21+
public function firstOrNew(array $attributes = [], array $values = []) { }
22+
23+
/**
24+
* @param array $attributes
25+
* @param array $values
26+
* @return \Illuminate\Database\Eloquent\Model
27+
* @psalm-return TRelatedModel
28+
*/
29+
public function firstOrCreate(array $attributes = [], array $values = []) { }
30+
}

stubs/EloquentBuilder.stubphp

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -136,20 +136,6 @@ class Builder
136136
*/
137137
public function findOrNew($id, $columns = ['*']) { }
138138

139-
/**
140-
* @param array $attributes
141-
* @param array $values
142-
* @return TModel
143-
*/
144-
public function firstOrNew(array $attributes, array $values = []) { }
145-
146-
/**
147-
* @param array $attributes
148-
* @param array $values
149-
* @return TModel
150-
*/
151-
public function firstOrCreate(array $attributes, array $values = []) { }
152-
153139
/**
154140
* @param array $attributes
155141
* @param array $values

stubs/HasOneOrMany.stubphp

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ namespace Illuminate\Database\Eloquent\Relations;
44

55
use Illuminate\Database\Eloquent\Model;
66
use Illuminate\Database\Eloquent\Builder;
7-
use Illuminate\Database\Eloquent\Collection;
87

98
/**
109
* @template TRelatedModel of Model
@@ -46,22 +45,6 @@ abstract class HasOneOrMany extends Relation
4645
*/
4746
public function findOrNew($id, $columns = ['*']) { }
4847

49-
/**
50-
* @param array $attributes
51-
* @param array $values
52-
* @return \Illuminate\Database\Eloquent\Model
53-
* @psalm-return TRelatedModel
54-
*/
55-
public function firstOrNew(array $attributes, array $values = []) { }
56-
57-
/**
58-
* @param array $attributes
59-
* @param array $values
60-
* @return \Illuminate\Database\Eloquent\Model
61-
* @psalm-return TRelatedModel
62-
*/
63-
public function firstOrCreate(array $attributes, array $values = []) { }
64-
6548
/**
6649
* @param array $attributes
6750
* @param array $values

tests/acceptance/EloquentBuilderTypes.feature

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,73 +15,78 @@ Feature: Eloquent Builder types
1515
</plugins>
1616
</psalm>
1717
"""
18+
And I have the following code preamble
19+
"""
20+
<?php declare(strict_types=1);
21+
namespace Tests\Psalm\LaravelPlugin\Sandbox;
22+
23+
use Illuminate\Database\Eloquent\Builder;
24+
use Illuminate\Database\Eloquent\Collection;
25+
use Tests\Psalm\LaravelPlugin\Models\User;
26+
"""
1827

1928
Scenario: Models can call eloquent query builder instance methods
2029
Given I have the following code
2130
"""
22-
<?php declare(strict_types=1);
23-
24-
use Tests\Psalm\LaravelPlugin\Models\User;
25-
2631
final class UserRepository
2732
{
2833
2934
/**
30-
* @return \Illuminate\Database\Eloquent\Builder<User>
35+
* @return Builder<User>
3136
*/
32-
public function getNewQuery(): \Illuminate\Database\Eloquent\Builder
37+
public function getNewQuery(): Builder
3338
{
3439
return User::query();
3540
}
3641
3742
/**
38-
* @return \Illuminate\Database\Eloquent\Builder<User>
43+
* @return Builder<User>
3944
*/
40-
public function getNewModelQuery(): \Illuminate\Database\Eloquent\Builder
45+
public function getNewModelQuery(): Builder
4146
{
4247
return (new User())->newModelQuery();
4348
}
4449
4550
/**
46-
* @param \Illuminate\Database\Eloquent\Builder<User> $builder
51+
* @param Builder<User> $builder
4752
*/
48-
public function firstOrFailFromBuilderInstance(\Illuminate\Database\Eloquent\Builder $builder): User {
53+
public function firstOrFailFromBuilderInstance(Builder $builder): User {
4954
return $builder->firstOrFail();
5055
}
5156
5257
/**
53-
* @param \Illuminate\Database\Eloquent\Builder<User> $builder
58+
* @param Builder<User> $builder
5459
*/
55-
public function findOrFailFromBuilderInstance(\Illuminate\Database\Eloquent\Builder $builder): User {
60+
public function findOrFailFromBuilderInstance(Builder $builder): User {
5661
return $builder->findOrFail(1);
5762
}
5863
5964
/**
60-
* @param \Illuminate\Database\Eloquent\Builder<User> $builder
61-
* @return \Illuminate\Database\Eloquent\Collection<User>
65+
* @param Builder<User> $builder
66+
* @return Collection<User>
6267
*/
63-
public function findMultipleOrFailFromBuilderInstance(\Illuminate\Database\Eloquent\Builder $builder): \Illuminate\Database\Eloquent\Collection {
68+
public function findMultipleOrFailFromBuilderInstance(Builder $builder): Collection {
6469
return $builder->findOrFail([1, 2]);
6570
}
6671
6772
/**
68-
* @param \Illuminate\Database\Eloquent\Builder<User> $builder
73+
* @param Builder<User> $builder
6974
*/
70-
public function findOne(\Illuminate\Database\Eloquent\Builder $builder): ?User {
75+
public function findOne(Builder $builder): ?User {
7176
return $builder->find(1);
7277
}
7378
7479
/**
75-
* @param \Illuminate\Database\Eloquent\Builder<User> $builder
80+
* @param Builder<User> $builder
7681
*/
77-
public function findViaArray(\Illuminate\Database\Eloquent\Builder $builder): \Illuminate\Database\Eloquent\Collection {
82+
public function findViaArray(Builder $builder): Collection {
7883
return $builder->find([1]);
7984
}
8085
8186
/**
82-
* @return \Illuminate\Database\Eloquent\Builder<User>
87+
* @return Builder<User>
8388
*/
84-
public function getWhereBuilderViaInstance(array $attributes): \Illuminate\Database\Eloquent\Builder {
89+
public function getWhereBuilderViaInstance(array $attributes): Builder {
8590
return (new User())->where($attributes);
8691
}
8792
}
@@ -90,10 +95,16 @@ Feature: Eloquent Builder types
9095
Then I see no errors
9196

9297
Scenario: can call static methods on model
93-
Given I have the following code
98+
Given I have the following code preamble
9499
"""
95100
<?php declare(strict_types=1);
96101
102+
use Illuminate\Database\Eloquent\Builder;
103+
use Illuminate\Database\Eloquent\Collection;
104+
"""
105+
And I have the following code
106+
"""
107+
97108
final class User extends \Illuminate\Database\Eloquent\Model {
98109
protected $table = 'users';
99110
};
@@ -102,17 +113,17 @@ Feature: Eloquent Builder types
102113
{
103114
104115
/**
105-
* @return \Illuminate\Database\Eloquent\Builder<User>
116+
* @return Builder<User>
106117
*/
107-
public function getWhereBuilderViaStatic(array $attributes): \Illuminate\Database\Eloquent\Builder
118+
public function getWhereBuilderViaStatic(array $attributes): Builder
108119
{
109120
return User::where($attributes);
110121
}
111122
112123
/**
113-
* @psalm-return \Illuminate\Database\Eloquent\Collection<User>
124+
* @psalm-return Collection<User>
114125
*/
115-
public function getWhereViaStatic(array $attributes): \Illuminate\Database\Eloquent\Collection
126+
public function getWhereViaStatic(array $attributes): Collection
116127
{
117128
return User::where($attributes)->get();
118129
}
@@ -122,20 +133,25 @@ Feature: Eloquent Builder types
122133
Then I see no errors
123134

124135
Scenario:
125-
Given I have the following code
136+
Given I have the following code preamble
126137
"""
127138
<?php declare(strict_types=1);
128139
129-
final class User extends \Illuminate\Database\Eloquent\Model {
140+
use Illuminate\Database\Eloquent\Builder;
141+
use Illuminate\Database\Eloquent\Model;
142+
"""
143+
And I have the following code
144+
"""
145+
final class User extends Model {
130146
protected $table = 'users';
131147
};
132148
133149
final class UserRepository
134150
{
135151
/**
136-
* @return \Illuminate\Database\Eloquent\Builder<User>
152+
* @return Builder<User>
137153
*/
138-
public function test_failure(): \Illuminate\Database\Eloquent\Builder
154+
public function test_failure(): Builder
139155
{
140156
return User::fakeQueryMethodThatDoesntExist();
141157
}
@@ -150,11 +166,6 @@ Feature: Eloquent Builder types
150166
Scenario: can call methods on underlying query builder
151167
Given I have the following code
152168
"""
153-
<?php declare(strict_types=1);
154-
155-
use Tests\Psalm\LaravelPlugin\Models\User;
156-
use \Illuminate\Database\Eloquent\Builder;
157-
158169
/**
159170
* @psalm-param Builder<User> $builder
160171
* @psalm-return Builder<User>
@@ -165,3 +176,52 @@ Feature: Eloquent Builder types
165176
"""
166177
When I run Psalm
167178
Then I see no errors
179+
180+
Scenario: cannot call firstOrNew and firstOrCreate without parameters in Laravel 6.x
181+
Given I have the "laravel/framework" package satisfying the "6.*"
182+
And I have the following code
183+
"""
184+
/**
185+
* @psalm-param Builder<User> $builder
186+
* @psalm-return User
187+
*/
188+
function test_firstOrCreate(Builder $builder): User {
189+
return $builder->firstOrCreate();
190+
}
191+
192+
/**
193+
* @psalm-param Builder<User> $builder
194+
* @psalm-return User
195+
*/
196+
function test_firstOrNew(Builder $builder): User {
197+
return $builder->firstOrNew();
198+
}
199+
"""
200+
When I run Psalm
201+
Then I see these errors
202+
| Type | Message |
203+
| TooFewArguments | Too few arguments for method Illuminate\Database\Eloquent\Builder::firstorcreate saw 0 |
204+
| TooFewArguments | Too few arguments for method Illuminate\Database\Eloquent\Builder::firstornew saw 0 |
205+
206+
Scenario: can call firstOrNew and firstOrCreate without parameters in Laravel 8.x
207+
Given I have the "laravel/framework" package satisfying the ">= 8.0"
208+
And I have the following code
209+
"""
210+
/**
211+
* @psalm-param Builder<User> $builder
212+
* @psalm-return User
213+
*/
214+
function test_firstOrCreate(Builder $builder): User {
215+
return $builder->firstOrCreate();
216+
}
217+
218+
/**
219+
* @psalm-param Builder<User> $builder
220+
* @psalm-return User
221+
*/
222+
function test_firstOrNew(Builder $builder): User {
223+
return $builder->firstOrNew();
224+
}
225+
"""
226+
When I run Psalm
227+
Then I see no errors

0 commit comments

Comments
 (0)