Description
API Platform version(s) affected: 4.0.16
Description
On Laravel we use camelCase naming when the Model's name is composed. On relations, we also use that notation.
The relations uisng camelCase are ignored and the data from the paylod is not sent to the Processor.
How to reproduce
Having 2 Models:
GrandFather
namespace App\Models;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[ApiResource()]
class GrandFather extends Model
{
protected $table = 'grand_fathers';
protected $primaryKey = 'id_grand_father';
protected $fillable = ['name','grandSons'];
#[ApiProperty(genId: false, identifier: true)]
private ?int $id_grand_father;
private ?string $name = null;
private ?Collection $grandSons = null;
/**
* @return HasMany
*/
public function grandSons(): HasMany
{
return $this->hasMany(GrandSon::class,'grand_father_id','id_grand_father');
}
}
GrandSon
namespace App\Models;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[ApiResource()]
class GrandSon extends Model
{
protected $table = 'grand_sons';
protected $primaryKey = 'id_grand_son';
protected $fillable = ['name','grandFather','grand_father_id','grandfather'];
#[ApiProperty(genId: false, identifier: true)]
private ?int $id_grand_son;
private ?string $name = null;
private ?GrandFather $grandFather = null;
/**
* @return BelongsTo
*/
public function grandFather(): BelongsTo
{
return $this->belongsTo(GrandFather::class,'grand_father_id','id_grand_father');
}
}
Created using following migrations:
GrandFather
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('grand_fathers', function (Blueprint $table) {
$table->increments('id_grand_father');
$table->string('name');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('grand_fathers');
}
};
GrandSon
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('grand_sons', function (Blueprint $table) {
$table->increments('id_grand_son');
$table->string('name');
$table->unsignedInteger('grand_father_id')->nullable();
$table->timestamps();
$table->foreign('grand_father_id')->references('id_grand_father')->on('grand_fathers');
});
}
public function down(): void
{
Schema::dropIfExists('grand_sons');
}
};
Using POST
on GrandSon with the following Payload:
{
"name": "GrandSon Name",
"grandFather": {
"name": "GrandFather Name"
}
}
so the whole call is:
curl -X 'POST' \
'http://localhost:8008/api/grand_sons' \
-H 'accept: application/ld+json' \
-H 'Content-Type: application/ld+json' \
-d '{
"name": "GrandSon Name",
"grandFather": {
"name": "GrandFather Name"
}
}'
It creates the GrandSon with no realtion at all (so grand_father_id
is NULL.
In order to investigate, if I Print the $body
variable on ApiPlatformController
just before the processor uses process
function on Line 98 I get:
{"name":"GrandSon Name"}
So the Relation is not just not persisted but the data is totally ignored and it is not sent to the Processor.
If then I try to switch the relation namig to all lowercase so on Model GrandSon
:
...
private ?GrandFather $grandfather = null;
/**
* @return BelongsTo
*/
public function grandfather(): BelongsTo
{
return $this->belongsTo(GrandFather::class,'grand_father_id','id_grand_father');
}
...
and I use the Following Payload:
{
"name": "GrandSon Name",
"grandfather": {
"name": "GrandFather Name"
}
}
Then I receive the expected error 500 "Nested documents for attribute \"grandfather\" are not allowed. Use IRIs instead."
so the Relation is processed (The error is a whole different story discussed on: #6882)
On the other hand, I trid setting
#[SerializedName('grand_father')]
on the realation which works but then you are forced to use grand_father
parameter on the Payload.
This is specially annoying for all those projects which have several Models and Relations already defined on the usual way and want to start using ApiPlatform.
Possible Solution
Not using SnakeCaseToCamelCaseNameConverter to denormalize relations.
I think the problem is using SnakeCaseToCamelCaseNameConverter to denormalize Relations.
This denormalization works on actual attributes so camelCase attributes defined on Laravel Models are converted to snake_case as this is how the fileds are named on DB but fails on relations.
Example: GrandFather relation is denormalized as grand_father