Skip to content

Commit 6f15be5

Browse files
authored
fix(laravel): eloquent BelongsTo linking (api-platform#7075)
fixes api-platform#6930
1 parent dfca9bf commit 6f15be5

File tree

7 files changed

+270
-11
lines changed

7 files changed

+270
-11
lines changed

src/Laravel/Eloquent/State/LinksHandler.php

+22-11
Original file line numberDiff line numberDiff line change
@@ -102,20 +102,31 @@ private function buildQuery(Builder $builder, Link $link, mixed $identifier): Bu
102102

103103
if ($from = $link->getFromProperty()) {
104104
$relation = $this->application->make($link->getFromClass());
105-
106-
if (!method_exists($relation->{$from}(), 'getQualifiedForeignKeyName') && method_exists($relation->{$from}(), 'getQualifiedForeignPivotKeyName')) {
107-
/** @var \Illuminate\Database\Eloquent\Relations\BelongsToMany<Model, Model> $relation */
108-
/** @var \Illuminate\Database\Eloquent\Relations\BelongsToMany<Model, Model> $relation_query */
109-
$relation_query = $relation->{$from}();
110-
111-
return $builder->getModel()->join(
112-
$relation_query->getTable(), $relation->{$from}()->getQualifiedRelatedPivotKeyName(), $builder->getModel()->getQualifiedKeyName())
113-
->where($relation->{$from}()->getQualifiedForeignPivotKeyName(),
114-
$identifier)
105+
$relationQuery = $relation->{$from}();
106+
if (!method_exists($relationQuery, 'getQualifiedForeignKeyName') && method_exists($relationQuery, 'getQualifiedForeignPivotKeyName')) {
107+
return $builder->getModel()
108+
->join(
109+
$relationQuery->getTable(), // @phpstan-ignore-line
110+
$relationQuery->getQualifiedRelatedPivotKeyName(), // @phpstan-ignore-line
111+
$builder->getModel()->getQualifiedKeyName()
112+
)
113+
->where(
114+
$relationQuery->getQualifiedForeignPivotKeyName(), // @phpstan-ignore-line
115+
$identifier
116+
)
115117
->select($builder->getModel()->getTable().'.*');
116118
}
117119

118-
return $builder->getModel()->where($relation->{$from}()->getQualifiedForeignKeyName(), $identifier);
120+
if (method_exists($relationQuery, 'dissociate')) {
121+
return $builder->getModel()
122+
->join(
123+
$relationQuery->getParent()->getTable(), // @phpstan-ignore-line
124+
$relationQuery->getParent()->getQualifiedKeyName(), // @phpstan-ignore-line
125+
$identifier
126+
);
127+
}
128+
129+
return $builder->getModel()->where($relationQuery->getQualifiedForeignKeyName(), $identifier);
119130
}
120131

121132
return $builder->where($builder->getModel()->qualifyColumn($link->getIdentifiers()[0]), $identifier);

src/Laravel/Tests/EloquentTest.php

+11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Orchestra\Testbench\TestCase;
2121
use Workbench\Database\Factories\AuthorFactory;
2222
use Workbench\Database\Factories\BookFactory;
23+
use Workbench\Database\Factories\GrandSonFactory;
2324
use Workbench\Database\Factories\WithAccessorFactory;
2425

2526
class EloquentTest extends TestCase
@@ -431,4 +432,14 @@ public function testBooleanFilter(): void
431432
$this->assertEquals($res->getStatusCode(), 200);
432433
$this->assertEquals($res->json()['totalItems'], 0);
433434
}
435+
436+
public function testBelongsTo(): void
437+
{
438+
GrandSonFactory::new()->count(1)->create();
439+
440+
$res = $this->get('/api/grand_sons/1/grand_father', ['Accept' => ['application/ld+json']]);
441+
$json = $res->json();
442+
$this->assertEquals($json['@id'], '/api/grand_sons/1/grand_father');
443+
$this->assertEquals($json['sons'][0], '/api/grand_sons/1');
444+
}
434445
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Workbench\App\Models;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\ApiResource;
18+
use ApiPlatform\Metadata\Get;
19+
use ApiPlatform\Metadata\Link;
20+
use Illuminate\Database\Eloquent\Collection;
21+
use Illuminate\Database\Eloquent\Model;
22+
use Illuminate\Database\Eloquent\Relations\HasMany;
23+
24+
#[ApiResource]
25+
#[ApiResource(
26+
uriTemplate: '/grand_sons/{id_grand_son}/grand_father',
27+
uriVariables: [
28+
'id_grand_son' => new Link(
29+
fromClass: GrandSon::class,
30+
fromProperty: 'grandfather',
31+
identifiers: ['id_grand_son']
32+
),
33+
],
34+
operations: [new Get()]
35+
)]
36+
#[ApiProperty(identifier: true, property: 'id_grand_father')]
37+
class GrandFather extends Model
38+
{
39+
protected $table = 'grand_fathers';
40+
protected $primaryKey = 'id_grand_father';
41+
protected $fillable = ['name', 'sons'];
42+
43+
#[ApiProperty(genId: false, identifier: true)]
44+
private ?int $id_grand_father;
45+
46+
private ?string $name = null;
47+
48+
private ?Collection $sons = null;
49+
50+
public function sons(): HasMany
51+
{
52+
return $this->hasMany(GrandSon::class, 'grand_father_id', 'id_grand_father');
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Workbench\App\Models;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\ApiResource;
18+
use ApiPlatform\Metadata\GetCollection;
19+
use ApiPlatform\Metadata\Link;
20+
use Illuminate\Database\Eloquent\Model;
21+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
22+
23+
#[ApiResource]
24+
#[ApiResource(
25+
uriTemplate: '/grand_fathers/{id_grand_father}/grand_sons',
26+
uriVariables: [
27+
'id_grand_father' => new Link(
28+
fromClass: GrandFather::class,
29+
fromProperty: 'sons',
30+
identifiers: ['id_grand_father']
31+
),
32+
],
33+
operations: [new GetCollection()]
34+
)]
35+
#[ApiProperty(identifier: true, property: 'id_grand_son')]
36+
class GrandSon extends Model
37+
{
38+
protected $table = 'grand_sons';
39+
protected $primaryKey = 'id_grand_son';
40+
protected $fillable = ['name', 'grand_father_id', 'grandfather'];
41+
42+
#[ApiProperty(genId: false, identifier: true)]
43+
private ?int $id_grand_son;
44+
45+
private ?string $name = null;
46+
47+
private ?GrandFather $grandfather = null;
48+
49+
public function grandfather(): BelongsTo
50+
{
51+
return $this->belongsTo(GrandFather::class, 'grand_father_id', 'id_grand_father');
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Workbench\Database\Factories;
15+
16+
use Illuminate\Database\Eloquent\Factories\Factory;
17+
use Workbench\App\Models\GrandFather;
18+
19+
/**
20+
* @template TModel of \Workbench\App\Models\Author
21+
*
22+
* @extends \Illuminate\Database\Eloquent\Factories\Factory<TModel>
23+
*/
24+
class GrandFatherFactory extends Factory
25+
{
26+
/**
27+
* The name of the factory's corresponding model.
28+
*
29+
* @var class-string<TModel>
30+
*/
31+
protected $model = GrandFather::class;
32+
33+
/**
34+
* Define the model's default state.
35+
*
36+
* @return array<string, mixed>
37+
*/
38+
public function definition(): array
39+
{
40+
return [
41+
'name' => fake()->name(),
42+
];
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Workbench\Database\Factories;
15+
16+
use Illuminate\Database\Eloquent\Factories\Factory;
17+
use Workbench\App\Models\GrandSon;
18+
19+
/**
20+
* @template TModel of \Workbench\App\Models\Author
21+
*
22+
* @extends \Illuminate\Database\Eloquent\Factories\Factory<TModel>
23+
*/
24+
class GrandSonFactory extends Factory
25+
{
26+
/**
27+
* The name of the factory's corresponding model.
28+
*
29+
* @var class-string<TModel>
30+
*/
31+
protected $model = GrandSon::class;
32+
33+
/**
34+
* Define the model's default state.
35+
*
36+
* @return array<string, mixed>
37+
*/
38+
public function definition(): array
39+
{
40+
return [
41+
'grand_father_id' => GrandFatherFactory::new(),
42+
'name' => fake()->name(),
43+
];
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
use Illuminate\Database\Migrations\Migration;
15+
use Illuminate\Database\Schema\Blueprint;
16+
use Illuminate\Support\Facades\Schema;
17+
18+
return new class extends Migration {
19+
public function up(): void
20+
{
21+
Schema::create('grand_fathers', function (Blueprint $table): void {
22+
$table->increments('id_grand_father');
23+
$table->string('name');
24+
$table->timestamps();
25+
});
26+
27+
Schema::create('grand_sons', function (Blueprint $table): void {
28+
$table->increments('id_grand_son');
29+
$table->string('name');
30+
$table->unsignedInteger('grand_father_id')->nullable();
31+
$table->timestamps();
32+
$table->foreign('grand_father_id')->references('id_grand_father')->on('grand_fathers');
33+
});
34+
}
35+
36+
public function down(): void
37+
{
38+
Schema::dropIfExists('grand_fathers');
39+
Schema::dropIfExists('grand_sons');
40+
}
41+
};

0 commit comments

Comments
 (0)