staudenmeir/eloquent-json-relations

Many-To-Many Relationship doesnt seem to work with uuid

Subwaytime opened this issue · 33 comments

Hey there,
just tried to use a many to many relationship where both models have a uuid and i cant seem to get it working!

class Auctionhouse extends Model {
	use HasJsonRelationships;

	protected $casts = [
		'itemCollection' => 'json',
	];

	protected $attributes = [
		'itemCollection' => '[]'
	];

	public function soldItems(): BelongsToJson {
		return $this->belongsToJson(Item::class, 'itemCollection[]->id');
	}
}
class Item extends Model {
	use HasJsonRelationships;

	public function soldInAuctionhouse(): HasManyJson {
		return $this->hasManyJson(Auctionhouse::class, 'itemCollection[]->id');
	}
}

Item Id is a uuid via a HasUuid Trait that sets incrementing to false, keyType to 'string' and primaryKey to 'id';

Idea is to add items to the auctionhouse via sync ->

$ac->soldItems()->sync(['077df06d-1c38-4fd6-b141-3544c6ddbc27' => ['price' => '200g']])->save());

Migrations are just using $table->json('itemCollection'); on the Auctionhouse Migration.

Database data looks like this

[{"id":"077df06d-1c38-4fd6-b141-3544c6ddbc27","price":"200g"}]

If i remove the json cast i cant seem to load the soldItems via $ac->soldItems as this either returns an empty Eloquent Collection

Illuminate\Database\Eloquent\Collection {#560 ▼
  #items: []
  #escapeWhenCastingToString: false
}

if the cast for the itemCollection is json/array it will throw this error
array_key_exists(): Argument #2 ($array) must be of type array, null given which is similiar to issue

Hopefully i provided all necessary informations! Let me know if something is missing!

Greetings

Hi @Subwaytime,
Thanks, I can reproduce this. The error occurs when the $attributes property has a default value. I'm working on a fix.

What Laravel version are you using?

Using Laravel 9.13!

Is there a workaround for the time? Because the issue still exists even if i remove $attributes! - This also occurs when setting a simple empty array [] into the json column..

Because the issue still exists even if i remove $attributes!

How are querying the soldItems relationship in this case?

This also occurs when setting a simple empty array [] into the json column..

How are you setting that? What query are you then executing?

Right now i only tried
$ac->soldItems and with/load so nothing special - these didnt work

Added the array just via the mysql itself, so nothing from Laravel

I released a new version that fixes the issue for me. I can't reproduce it without default $attributes so we'll have to see if it works for you.

Unfortunately it still does not work!
I am still getting the array_key_exists error, with the same setup as above, just updated to 1.6.2

What query exactly are you executing? What's the whole stacktrace? Please also share the whole files of both models.

$character = Character::where('id', 'bc8207fe-a640-48d4-b576-8e9f5e1d01f6')->first();
$ac = Auctionhouse::whereBelongsTo($character)->first();
$ac->soldItems

I tried this and i tried adding protected $with = ['soldItems']; into the auctionhouse model and on the query. Both dont work!
Both models look like above on difference is there is a fillable for auctionhouse with character_id and itemCollection.

The Uuid Trait looks like this, but i also tried different methods now for this, to get a work around going for this library..

<?php

declare(strict_types=1);

namespace App\Traits;

use Illuminate\Support\Str;

trait HasUuid {
    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @return bool
     */
    public function getIncrementing() {
        return false;
    }

    /**
     * Returns Model Key.
     *
     * @return string
     */
    public function getKeyName() {
        return 'id';
    }

    /**
     * Create Models with UUID.
     */
    protected static function booted(): void {

        static::creating(function ($model): void {
            $model->keyType = 'string';
            $id = (string) Str::uuid();
            $model->{$model->getKeyName()} = $id;
        });
    }
}

edit: ive only got the library to work if i use only a string on the relation ship like itemCollection
if i use itemCollection[]->id (or something else there like item_id etc) it doesnt work.

edit: stacktrace share via flare https://flareapp.io/share/q5YElxj5#F46

Any update on this? Keen to help if its needed!

What's the result of dd($ac->getAttributes());?

array:5 [▼
  "id" => "470f0a4f-cbaa-46e7-b056-da56079033ed"
  "character_id" => "bc8207fe-a640-48d4-b576-8e9f5e1d01f6"
  "itemCollection" => "[{"item_id": "077df06d-1c38-4fd6-b141-3544c6ddbc27"}]"
  "created_at" => "2022-07-21 17:28:35"
  "updated_at" => "2022-07-21 17:28:35"
]

this is the output

"itemCollection" => "[{"item_id": "077df06d-1c38-4fd6-b141-3544c6ddbc27"}]"

Are the foreign keys inside the JSON objects named item_id or id? In your initial post, it's id:

return $this->belongsToJson(Item::class, 'itemCollection[]->id');

[{"id":"077df06d-1c38-4fd6-b141-3544c6ddbc27","price":"200g"}]

Oh sorry ive changed the model while testing out what the issue could be, the foreign keys are now item_id, which still runs into the same issue as if its just using id. Both fail for me.
Basically it doesnt seem to matter what foreign keys i have defined, only difference is the getAttribute output, the error mentioned above stays the same

I just can't reproduce this. What do you get when you put this in line 201 of vendor/staudenmeir/eloquent-json-relations/src/Relations/BelongsToJson.php?

dd($model->getAttributes(), $parent->getAttributes(), $key, $this->ownerKey);

Hopefully i got this right! My Error still is array_key_exists(): Argument #2 ($array) must be of type array, null given when i call $ac->soldItems

image

My Error still is array_key_exists(): Argument #2 ($array) must be of type array, null given when i call $ac->soldItems

With the dd() call in line 201 or without it?

without it, with the dd call i endup with the image posted above

Please remove the dd() call and put this in line 206 instead:

if (!$record) dd($model->getAttributes(), $parent->getAttributes(), $key, $this->ownerKey);

same result as the picture from above

hopefully thats correct -->

    protected function pivotAttributes(Model $model, Model $parent)
    {
        $key = str_replace('->', '.', $this->key);

        $record = (new BaseCollection($parent->{$this->path}))
            ->filter(function ($value) use ($key, $model) {
                return Arr::get($value, $key) == $model->{$this->ownerKey};
            })->first();

        if (!$record) dd($model->getAttributes(), $parent->getAttributes(), $key, $this->ownerKey);

        return Arr::except($record, $key);
    }

class Auctionhouse extends Model {
class Item extends Model {

Is Model Illuminate\Database\Eloquent\Model or do you have a custom base model?

You are using version v1.6.2 of the package, right?

No both are from the default Laravel Eloquent Model - use Illuminate\Database\Eloquent\Model
Yes i am using 1.6.2

Can you reproduce it on a fresh Laravel installation? I don't have anything left to try, something must be different in your environment.

Will recreate a repo and post its link here!

https://github.com/Subwaytime/json-relation-repro
Hopefully i didnt miss anything - everything lives inside the web routes where i also provided an easy setup with uuids
under traits there is the usual uuid trait i use

note: i also discovered that if i use $item->id on the sync method rather then an actual string array then it works and there is no error! So it might be that the string array just isnt working

Thanks. I can't access the repo, is it private maybe?

Ups sorry, forgot that! Its public now

under traits there is the usual uuid trait i use

Does the Item model use the HasUuid trait in the original application where the error first occurred? The trait is not used in json-relation-repro and that's what causes the issue: When you don't override the $keyType, the UUID is cast to an integer:

$item->id // 77 instead of 077df06d-1c38-4fd6-b141-3544c6ddbc27

It works when you use the trait:

class Item extends Model
{
    use HasFactory, HasJsonRelationships, HasUuid;

Interesting, yeah using the HasUuid Trait in the original Application, just wanted to set specific Uuids for this test repro thats why i removed it.

But why does this
$ac->soldItems()->sync(['077df06d-1c38-4fd6-b141-3544c6ddbc27'])->save();
not work but this does
$ac->soldItems()->sync([$item->id])->save();

It throws back the item from the database.

Using the HasUuid Trait doesnt work on the real application nor on the test one for me if i use the string uuid instead of the $item->id

just wanted to set specific Uuids for this test repro thats why i removed it.

You need to override the $keyType when your primary key is not an integer. Typically, that's done with protected $keyType = 'string';.

but this does
$ac->soldItems()->sync([$item->id])->save();

This "works" because it sets $ac's item_id to 77 and then the relationship compares 77 to 77 internally.

Using the HasUuid Trait doesnt work on the real application nor on the test one for me if i use the string uuid instead of the $item->id

Your test repository doesn't work when you replace use HasFactory, HasJsonRelationships; with use HasFactory, HasJsonRelationships, HasUuid; in Item?

sorry for the late response. I am currently doing more investigations around this, it seems that setting the keyType via a trait doesnt work. Neither via $model->keyType nor via function getKeyType...

Will post here again once i find more information!

I found a workaround, thats seems to fix the issue atleast for now, not sure if its something that could be handled by this library directly 🤔

    public function __construct() {
    parent::__construct();
        $this->setKeyType('string');
    }

Ive added this code to the Uuid Trait, as function getKeyType was still throwing back int for the keyType. Maybe it might be worth adding an error in for the sync method and others, that if the ID is a 36-string, it throws back that the keyType needs to be reflecting that? Not sure tbh, the error array_key_exists(): Argument #2 ($array) must be of type array, null given just might be a bit to general

Did you try protected $keyType = 'string';?

This works if used inside the models, but its quite tedious for multiple models! (or alteast the amount of models were i use uuids) (does not work in the trait)