Skip to content

Commit ac281a1

Browse files
committed
added methods loadOneLatest and loadOneOldest
1 parent 26e69e8 commit ac281a1

9 files changed

+312
-1
lines changed

README.md

+18-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
With standard use of Laravel, if you want the sum or find the maximum column value in the related model, you will have two database queries.
1010
With this methods, it all turns into one query to the database and there is no need to load extra data.
1111
It is also possible to sort by related models. And this sorting works with all types of relations.
12-
Added ability to load relations with a limit for each model without multiple queries.
12+
Added ability to load only one latest or oldest related model for each model without multiple queries.
1313
I often use this in my work and I hope it will be useful to you!
1414

1515
## Say thank you
@@ -110,6 +110,23 @@ By default, sorting is by `max` and `desc`, you can choose one of the options `m
110110
$invoices = Invoice::orderByRelation('items:price', 'asc', 'sum')->get();
111111
```
112112

113+
### Load latest or oldest relation
114+
115+
Imagine you want to get a list of 50 accounts, each with 100 items. By default, you will get 5000 positions and select the first ones for each account. PHP smokes nervously on the sidelines.
116+
Wow! Now you can load only one latest or oldest related model:
117+
```php
118+
$invoices = Invoice::all();
119+
$invoices->loadOneLatest('items');
120+
$invoices->loadOneOldest('items');
121+
```
122+
or with conditions
123+
```php
124+
$invoices->loadOneLatest(['items' => function ($query) {
125+
$query->orderBy('id', 'desc')->where('price', '<', 6);
126+
}]);
127+
```
128+
You can use this with relation types hasMany, belongsToMany and hasManyThrough.
129+
113130
### Limit relations
114131

115132
If you want to load related model with limit, simply use the following method:

src/Collection/LaravelSubQueryCollection.php

+38
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Alexmg86\LaravelSubQuery\Collection;
44

5+
use Closure;
56
use Illuminate\Database\Eloquent\Collection;
67
use Illuminate\Support\Arr;
78

@@ -192,4 +193,41 @@ private function getLimits($relations)
192193
}
193194
return ['relations' => $relationsList, 'limits' => $limits];
194195
}
196+
197+
/**
198+
* Load a set of relationship with limit one latest item onto the collection.
199+
*
200+
* @param array|string $relations
201+
* @return $this
202+
*/
203+
public function loadOneLatest($relations)
204+
{
205+
return $this->loadOne($relations);
206+
}
207+
208+
/**
209+
* Load a set of relationship with limit one oldest item onto the collection.
210+
*
211+
* @param array|string $relations
212+
* @return $this
213+
*/
214+
public function loadOneOldest($relations)
215+
{
216+
return $this->loadOne($relations, 'MIN');
217+
}
218+
219+
private function loadOne($relations, $type = 'MAX')
220+
{
221+
if ($this->isNotEmpty()) {
222+
if (is_string($relations)) {
223+
$relations = func_get_args()[0];
224+
}
225+
226+
$query = $this->first()->newQueryWithoutRelationships()->with($relations);
227+
228+
$this->items = $query->eagerLoadRelationsOne($this->items, $type);
229+
}
230+
231+
return $this;
232+
}
195233
}

src/LaravelSubQuery.php

+80
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
namespace Alexmg86\LaravelSubQuery;
44

5+
use Closure;
56
use Illuminate\Database\Eloquent\Builder;
67
use Illuminate\Database\Eloquent\Concerns\QueriesRelationships;
8+
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
9+
use Illuminate\Database\Eloquent\Relations\HasMany;
10+
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
711
use Illuminate\Database\Query\Expression;
812
use Illuminate\Support\Str;
913

@@ -208,4 +212,80 @@ public function setWithAvg($withAvg)
208212
{
209213
return $this->withAvg($withAvg);
210214
}
215+
216+
/**
217+
* Eager load the relationships for the models.
218+
* Overwriting Illuminate\Database\Eloquent\Builder@eagerLoadRelations
219+
*
220+
* @param array $models
221+
* @return array
222+
*/
223+
public function eagerLoadRelationsOne(array $models, string $type)
224+
{
225+
foreach ($this->eagerLoad as $name => $constraints) {
226+
if (strpos($name, '.') === false) {
227+
$models = $this->eagerLoadRelationOne($models, $name, $constraints, $type);
228+
}
229+
}
230+
231+
return $models;
232+
}
233+
234+
/**
235+
* Eagerly load the relationship on a set of models.
236+
* Overwriting Illuminate\Database\Eloquent\Builder@eagerLoadRelation
237+
*
238+
* @param array $models
239+
* @param string $name
240+
* @param \Closure $constraints
241+
* @return array
242+
*/
243+
protected function eagerLoadRelationOne(array $models, $name, Closure $constraints, string $type)
244+
{
245+
$relation = $this->getRelation($name);
246+
247+
$relation->addEagerConstraints($models);
248+
249+
$constraints($relation);
250+
251+
$parseData = $this->parseRelationToKeys($relation, $type);
252+
253+
return $relation->match(
254+
$relation->initRelation($models, $name),
255+
$relation
256+
->whereIn($parseData[0], $parseData[1])
257+
->getEager(),
258+
$name
259+
);
260+
}
261+
262+
/**
263+
* Getting only need ids
264+
* @param object $relation
265+
* @param string $type
266+
* @return array
267+
*/
268+
private function parseRelationToKeys(object $relation, string $type)
269+
{
270+
$relationCopy = clone $relation;
271+
272+
$typeRelation = get_class($relationCopy);
273+
switch (get_class($relationCopy)) {
274+
case HasMany::class:
275+
$maxKey = $asKey = $relation->getLocalKeyName();
276+
$groupby = $relation->getForeignKeyName();
277+
break;
278+
case BelongsToMany::class:
279+
$maxKey = $asKey = $relation->getRelatedKeyName();
280+
$groupby = $relation->getRelated()->getTable() . '.' . $relation->getForeignPivotKeyName();
281+
break;
282+
case HasManyThrough::class:
283+
$asKey = $relation->getSecondLocalKeyName();
284+
$maxKey = $relation->getRelated()->getTable() . '.' . $asKey;
285+
$groupby = $relation->getParent()->getTable() . '.' . $relation->getFirstKeyName();
286+
break;
287+
}
288+
289+
return [$maxKey, $relationCopy->selectRaw("$type($maxKey) as $asKey")->groupby($groupby)->pluck($asKey)];
290+
}
211291
}

src/Traits/LaravelSubQueryTrait.php

+22
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,28 @@ public function loadAvg($relations)
6767
return $this;
6868
}
6969

70+
/**
71+
* Eager load latest relation on the model.
72+
*
73+
* @param array|string $relations
74+
* @return $this
75+
*/
76+
public function loadOneLatest($relations)
77+
{
78+
return $this->newCollection([$this])->loadOneLatest($relations);
79+
}
80+
81+
/**
82+
* Eager load oldest relation on the model.
83+
*
84+
* @param array|string $relations
85+
* @return $this
86+
*/
87+
public function loadOneOldest($relations)
88+
{
89+
return $this->newCollection([$this])->loadOneOldest($relations);
90+
}
91+
7092
public function newEloquentBuilder($builder)
7193
{
7294
$newEloquentBuilder = new LaravelSubQuery($builder);

tests/DatabaseTestCase.php

+29
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ protected function setUp(): void
1414
$this->createInvoicesTable();
1515
$this->createItemsTable();
1616
$this->createGoodsTable();
17+
$this->createCountriesTable();
18+
$this->createCustomersTable();
19+
$this->createPostsTable();
1720
}
1821

1922
protected function getEnvironmentSetUp($app)
@@ -56,4 +59,30 @@ protected function createGoodsTable(): void
5659
$table->integer('price2');
5760
});
5861
}
62+
63+
protected function createCountriesTable(): void
64+
{
65+
Schema::create('countries', function (Blueprint $table) {
66+
$table->id();
67+
$table->string('name');
68+
});
69+
}
70+
71+
protected function createCustomersTable(): void
72+
{
73+
Schema::create('customers', function (Blueprint $table) {
74+
$table->id();
75+
$table->integer('country_id');
76+
$table->string('name');
77+
});
78+
}
79+
80+
protected function createPostsTable(): void
81+
{
82+
Schema::create('posts', function (Blueprint $table) {
83+
$table->id();
84+
$table->integer('user_id');
85+
$table->string('title');
86+
});
87+
}
5988
}

tests/LaravelSubQueryLoadOneTest.php

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace Alexmg86\LaravelSubQuery\Tests;
4+
5+
use Alexmg86\LaravelSubQuery\Facades\LaravelSubQuery;
6+
use Alexmg86\LaravelSubQuery\ServiceProvider;
7+
use Alexmg86\LaravelSubQuery\Tests\Models\Country;
8+
use Alexmg86\LaravelSubQuery\Tests\Models\Customer;
9+
use Alexmg86\LaravelSubQuery\Tests\Models\Post;
10+
11+
class LaravelSubQueryLoadOneTest extends DatabaseTestCase
12+
{
13+
protected function getPackageProviders($app)
14+
{
15+
return [ServiceProvider::class];
16+
}
17+
18+
protected function getPackageAliases($app)
19+
{
20+
return [
21+
'laravel-sub-query' => LaravelSubQuery::class,
22+
];
23+
}
24+
25+
private function createBasic()
26+
{
27+
$countriesNames = ['Russia', 'Spain', 'Mexico'];
28+
foreach ($countriesNames as $countriesName) {
29+
$country = Country::create(['name' => $countriesName]);
30+
31+
$customersNames = ['Ivan', 'Juan', 'Julio'];
32+
foreach ($customersNames as $customersName) {
33+
$customer = Customer::create([
34+
'country_id' => $country->id,
35+
'name' => $customersName
36+
]);
37+
38+
$titles = ['title_1', 'title_2', 'title_3'];
39+
foreach ($titles as $title) {
40+
Post::create([
41+
'user_id' => $customer->id,
42+
'title' => $title
43+
]);
44+
}
45+
}
46+
}
47+
}
48+
49+
public function testLoadOneLatest()
50+
{
51+
$this->createBasic();
52+
53+
$countries = Country::all();
54+
$countries->loadOneLatest('posts');
55+
56+
foreach ($countries as $country) {
57+
$this->assertEquals(count($country->posts), 1);
58+
$this->assertTrue(in_array($country->posts->first()->id, [9, 18, 27]));
59+
$this->assertEquals($country->posts->first()->title, 'title_3');
60+
}
61+
}
62+
63+
public function testLoadOneOldest()
64+
{
65+
$this->createBasic();
66+
67+
$countries = Country::all();
68+
$countries->loadOneOldest('posts');
69+
70+
foreach ($countries as $country) {
71+
$this->assertEquals(count($country->posts), 1);
72+
$this->assertTrue(in_array($country->posts->first()->id, [1, 10, 19]));
73+
$this->assertEquals($country->posts->first()->title, 'title_1');
74+
}
75+
}
76+
}

tests/Models/Country.php

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Alexmg86\LaravelSubQuery\Tests\Models;
4+
5+
use Alexmg86\LaravelSubQuery\Traits\LaravelSubQueryTrait;
6+
use Illuminate\Database\Eloquent\Model;
7+
8+
class Country extends Model
9+
{
10+
use LaravelSubQueryTrait;
11+
12+
public $table = 'countries';
13+
public $timestamps = false;
14+
protected $fillable = ['name'];
15+
16+
public function customers()
17+
{
18+
return $this->hasMany(Customer::class);
19+
}
20+
21+
public function posts()
22+
{
23+
return $this->hasManyThrough(Post::class, Customer::class, 'country_id', 'user_id', 'id', 'id');
24+
}
25+
}

tests/Models/Customer.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Alexmg86\LaravelSubQuery\Tests\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class Customer extends Model
8+
{
9+
public $table = 'customers';
10+
public $timestamps = false;
11+
protected $fillable = ['country_id', 'name'];
12+
}

tests/Models/Post.php

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Alexmg86\LaravelSubQuery\Tests\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class Post extends Model
8+
{
9+
public $table = 'posts';
10+
public $timestamps = false;
11+
protected $fillable = ['user_id', 'title'];
12+
}

0 commit comments

Comments
 (0)