Skip to content

[Laravel] camelCase named Relations are ignored as they are denormalized as snake_case #6927

Open
@llei4

Description

@llei4

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions