Skip to content

Commit 3d7a0a4

Browse files
committed
Add HasOneJson relation
1 parent d871de1 commit 3d7a0a4

File tree

11 files changed

+406
-7
lines changed

11 files changed

+406
-7
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Use this command if you are in PowerShell on Windows (e.g. in VS Code):
5151
- [Many-To-Many Relationships](#many-to-many-relationships)
5252
- [Array of IDs](#array-of-ids)
5353
- [Array of Objects](#array-of-objects)
54+
- [HasOneJson](#hasonejson)
5455
- [Composite Keys](#composite-keys)
5556
- [Query Performance](#query-performance)
5657
- [Has-Many-Through Relationships](#has-many-through-relationships)
@@ -226,6 +227,23 @@ $user->roles()->toggle([2 => ['active' => true], 3])->save();
226227

227228
**Limitations:** On SQLite and SQL Server, these relationships only work partially.
228229

230+
#### HasOneJson
231+
232+
Define a `HasOneJson relationship if you only want to retrieve a single related instance:
233+
234+
```php
235+
class Role extends Model
236+
{
237+
use \Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
238+
239+
public function latestUser(): \Staudenmeir\EloquentJsonRelations\Relations\HasOneJson
240+
{
241+
return $this->hasOneJson(User::class, 'options->roles[]->role_id')
242+
->latest();
243+
}
244+
}
245+
```
246+
229247
#### Composite Keys
230248

231249
If multiple columns need to match, you can define a composite key.

src/HasJsonRelationships.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use RuntimeException;
1616
use Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson;
1717
use Staudenmeir\EloquentJsonRelations\Relations\HasManyJson;
18+
use Staudenmeir\EloquentJsonRelations\Relations\HasOneJson;
1819
use Staudenmeir\EloquentJsonRelations\Relations\Postgres\BelongsTo as BelongsToPostgres;
1920
use Staudenmeir\EloquentJsonRelations\Relations\Postgres\HasMany as HasManyPostgres;
2021
use Staudenmeir\EloquentJsonRelations\Relations\Postgres\HasManyThrough as HasManyThroughPostgres;
@@ -307,6 +308,52 @@ protected function newHasManyJson(Builder $query, Model $parent, $foreignKey, $l
307308
return new HasManyJson($query, $parent, $foreignKey, $localKey);
308309
}
309310

311+
/**
312+
* Define a one-to-one JSON relationship.
313+
*
314+
* @param string $related
315+
* @param string|array $foreignKey
316+
* @param string|array|null $localKey
317+
* @return \Staudenmeir\EloquentJsonRelations\Relations\HasOneJson
318+
*/
319+
public function hasOneJson(string $related, string|array $foreignKey, string|array|null $localKey = null): HasOneJson
320+
{
321+
/** @var \Illuminate\Database\Eloquent\Model $instance */
322+
$instance = $this->newRelatedInstance($related);
323+
324+
if (is_array($foreignKey)) {
325+
$foreignKey = array_map(
326+
fn (string $key) => "{$instance->getTable()}.$key",
327+
$foreignKey
328+
);
329+
} else {
330+
$foreignKey = "{$instance->getTable()}.$foreignKey";
331+
}
332+
333+
$localKey = $localKey ?: $this->getKeyName();
334+
335+
return $this->newHasOneJson(
336+
$instance->newQuery(),
337+
$this,
338+
$foreignKey,
339+
$localKey
340+
);
341+
}
342+
343+
/**
344+
* Instantiate a new HasOneJson relationship.
345+
*
346+
* @param \Illuminate\Database\Eloquent\Builder $query
347+
* @param \Illuminate\Database\Eloquent\Model $parent
348+
* @param string|array $foreignKey
349+
* @param string|array $localKey
350+
* @return \Staudenmeir\EloquentJsonRelations\Relations\HasOneJson
351+
*/
352+
protected function newHasOneJson(Builder $query, Model $parent, string|array $foreignKey, string|array $localKey): HasOneJson
353+
{
354+
return new HasOneJson($query, $parent, $foreignKey, $localKey);
355+
}
356+
310357
/**
311358
* Define has-many-through JSON relationship.
312359
*

src/Relations/HasManyJson.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ protected function parentKeyToArray($parentKey)
170170
protected function matchOneOrMany(array $models, Collection $results, $relation, $type)
171171
{
172172
if ($this->hasCompositeKey()) {
173-
$this->matchWithCompositeKey($models, $results, $relation);
173+
$this->matchWithCompositeKey($models, $results, $relation, 'many');
174174
} else {
175175
parent::matchOneOrMany($models, $results, $relation, $type);
176176
}

src/Relations/HasOneJson.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace Staudenmeir\EloquentJsonRelations\Relations;
4+
5+
use Illuminate\Database\Eloquent\Collection;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels;
8+
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
9+
10+
class HasOneJson extends HasManyJson
11+
{
12+
use SupportsDefaultModels;
13+
14+
/** @inheritDoc */
15+
public function getResults()
16+
{
17+
if (is_null($this->getParentKey())) {
18+
return $this->getDefaultFor($this->parent);
19+
}
20+
21+
return $this->first() ?: $this->getDefaultFor($this->parent);
22+
}
23+
24+
/** @inheritDoc */
25+
public function initRelation(array $models, $relation)
26+
{
27+
foreach ($models as $model) {
28+
$model->setRelation($relation, $this->getDefaultFor($model));
29+
}
30+
31+
return $models;
32+
}
33+
34+
/** @inheritDoc */
35+
public function match(array $models, Collection $results, $relation)
36+
{
37+
return $this->matchOne($models, $results, $relation);
38+
}
39+
40+
/** @inheritDoc */
41+
public function matchOne(array $models, Collection $results, $relation)
42+
{
43+
if ($this->hasCompositeKey()) {
44+
$this->matchWithCompositeKey($models, $results, $relation, 'one');
45+
} else {
46+
HasOneOrMany::matchOneOrMany($models, $results, $relation, 'one');
47+
}
48+
49+
if ($this->key) {
50+
foreach ($models as $model) {
51+
$model->setRelation(
52+
$relation,
53+
$this->hydratePivotRelation(
54+
new Collection(
55+
array_filter([$model->$relation])
56+
),
57+
$model,
58+
fn (Model $model) => $model->{$this->getPathName()}
59+
)->first()
60+
);
61+
}
62+
}
63+
64+
return $models;
65+
}
66+
67+
/** @inheritDoc */
68+
public function newRelatedInstanceFor(Model $parent)
69+
{
70+
return $this->related->newInstance();
71+
}
72+
}

src/Relations/Traits/CompositeKeys/SupportsHasManyJsonCompositeKeys.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,10 @@ function (Builder $query) use ($key) {
8888
* @param array $models
8989
* @param \Illuminate\Database\Eloquent\Collection $results
9090
* @param string $relation
91+
* @param string $type
9192
* @return array
9293
*/
93-
protected function matchWithCompositeKey(array $models, Collection $results, string $relation): array
94+
protected function matchWithCompositeKey(array $models, Collection $results, string $relation, string $type): array
9495
{
9596
$dictionary = $this->buildDictionaryWithCompositeKey($results);
9697

@@ -105,7 +106,7 @@ protected function matchWithCompositeKey(array $models, Collection $results, str
105106
if (isset($dictionary[$key])) {
106107
$model->setRelation(
107108
$relation,
108-
$this->getRelationValue($dictionary, $key, 'many')
109+
$this->getRelationValue($dictionary, $key, $type)
109110
);
110111
}
111112
}

src/Relations/Traits/IsJsonRelation.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ trait IsJsonRelation
3636
* @param \Illuminate\Database\Eloquent\Collection $models
3737
* @param \Illuminate\Database\Eloquent\Model $parent
3838
* @param callable $callback
39-
* @return void
39+
* @return \Illuminate\Database\Eloquent\Collection
4040
*/
41-
public function hydratePivotRelation(Collection $models, Model $parent, callable $callback)
41+
public function hydratePivotRelation(Collection $models, Model $parent, callable $callback): Collection
4242
{
4343
foreach ($models as $i => $model) {
4444
$clone = clone $model;
@@ -48,6 +48,8 @@ public function hydratePivotRelation(Collection $models, Model $parent, callable
4848
$this->pivotRelation($clone, $parent, $callback)
4949
);
5050
}
51+
52+
return $models;
5153
}
5254

5355
/**
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace Tests\CompositeKeys;
4+
5+
use Illuminate\Database\Capsule\Manager as DB;
6+
use Tests\Models\Task;
7+
use Tests\TestCase;
8+
9+
class HasOneJsonTest extends TestCase
10+
{
11+
public function testLazyLoading()
12+
{
13+
$employee = Task::find(101)->employee;
14+
15+
$this->assertEquals(121, $employee->id);
16+
}
17+
18+
public function testLazyLoadingWithObjects()
19+
{
20+
if (in_array($this->connection, ['sqlite', 'sqlsrv'])) {
21+
$this->markTestSkipped();
22+
}
23+
24+
$employee = Task::find(101)->employeeWithObjects;
25+
26+
$this->assertEquals(121, $employee->id);
27+
$this->assertEquals(['work_stream' => ['active' => true]], $employee->pivot->getAttributes());
28+
}
29+
30+
public function testEmptyLazyLoading()
31+
{
32+
DB::enableQueryLog();
33+
34+
$employee = (new Task())->employee;
35+
36+
$this->assertNull($employee);
37+
$this->assertEmpty(DB::getQueryLog());
38+
}
39+
40+
public function testEagerLoading()
41+
{
42+
$tasks = Task::with('employee')->get();
43+
44+
$this->assertEquals(121, $tasks[0]->employee->id);
45+
$this->assertEquals(122, $tasks[1]->employee->id);
46+
$this->assertNull($tasks[5]->employee);
47+
}
48+
49+
public function testEagerLoadingWithObjects()
50+
{
51+
if (in_array($this->connection, ['sqlite', 'sqlsrv'])) {
52+
$this->markTestSkipped();
53+
}
54+
55+
$tasks = Task::with('employeeWithObjects')->get();
56+
57+
$this->assertEquals(121, $tasks[0]->employeeWithObjects->id);
58+
$this->assertEquals(122, $tasks[1]->employeeWithObjects->id);
59+
$this->assertNull($tasks[5]->employeeWithObjects);
60+
$this->assertEquals(['work_stream' => ['active' => true]], $tasks[0]->employeeWithObjects->pivot->getAttributes());
61+
}
62+
63+
public function testLazyEagerLoading()
64+
{
65+
$tasks = Task::all()->load('employee');
66+
67+
$this->assertEquals(121, $tasks[0]->employee->id);
68+
$this->assertEquals(122, $tasks[1]->employee->id);
69+
$this->assertNull($tasks[5]->employee);
70+
}
71+
72+
public function testLazyEagerLoadingWithObjects()
73+
{
74+
if (in_array($this->connection, ['sqlite', 'sqlsrv'])) {
75+
$this->markTestSkipped();
76+
}
77+
78+
$tasks = Task::all()->load('employeeWithObjects');
79+
80+
$this->assertEquals(121, $tasks[0]->employeeWithObjects->id);
81+
$this->assertEquals(122, $tasks[1]->employeeWithObjects->id);
82+
$this->assertNull($tasks[5]->employeeWithObjects);
83+
$this->assertEquals(['work_stream' => ['active' => true]], $tasks[0]->employeeWithObjects->pivot->getAttributes());
84+
}
85+
}

tests/HasManyJsonTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ public function testEmptyLazyLoading()
4141
{
4242
DB::enableQueryLog();
4343

44-
$roles = (new Role())->users;
44+
$users = (new Role())->users;
4545

46-
$this->assertInstanceOf(Collection::class, $roles);
46+
$this->assertInstanceOf(Collection::class, $users);
4747
$this->assertEmpty(DB::getQueryLog());
4848
}
4949

0 commit comments

Comments
 (0)