Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

mr_reboot's avatar

Saving Deep HasMany Relationship

I have these models:

Alt

Worksheet:

    public function items(): HasMany
    {
        return $this->hasMany(WorksheetItems::class);
    }

WorksheetItems:

    public function worksheet(): BelongsTo
    {
        return $this->belongsTo(Worksheet::class);
    }

    public function months(): HasMany
    {
        return $this->hasMany(WorkSheetItemMonths::class);
    }

WorkSheetItemMonths

    public function worksheet(): BelongsTo
    {
        return $this->belongsTo(WorksheetItems::class);
    }

I am struggling with how to add worksheet_item_months through the Worksheet Model.

I am using code like this to save the items which works great as long as the $worksheet_items array in the below example doesn't include anything about the worksheet_item_months:

$worksheet_items[0] = new WorksheetItems(
            [
                'name'         => 'Some Name',

            ]
        );
$sheet = Worksheet::create($worksheet_data)->items()->saveMany($worksheet_items);

That works.

If $worksheet_items looks like this:

$worksheet_items[0] = new WorksheetItems(
            [
                'name'         => 'Some Name',
                "months"                   => array(
                    new WorksheetItemsMonths(['period' => '2023-05-27', 'amount' => 10.00]),
                    new WorksheetItemsMonths(['period' => '2023-06-27', 'amount' => 10.00])
                )
            ]
        );

It doesn't work. I get the following error:

vendor\laravel\framework\src\Illuminate\Support\Str.php: 890 Array to string conversion

What is the best way to save deep relationships like this? Do I need to create worksheet_item individually? Or did I just screw up the code somewhere?

I greatly appreciate the help!

0 likes
6 replies
LaryAI's avatar
Level 58

To save deep relationships like this, you need to create the WorksheetItems and WorksheetItemsMonths individually and then associate them with each other. Here's an example of how you can do it:

// Create a new Worksheet
$worksheet = new Worksheet([
    'name' => 'Some Worksheet',
]);

// Save the Worksheet
$worksheet->save();

// Create a new WorksheetItem
$worksheetItem = new WorksheetItem([
    'name' => 'Some Name',
]);

// Associate the WorksheetItem with the Worksheet
$worksheet->items()->save($worksheetItem);

// Create new WorksheetItemMonths
$worksheetItemMonth1 = new WorksheetItemMonth([
    'period' => '2023-05-27',
    'amount' => 10.00,
]);

$worksheetItemMonth2 = new WorksheetItemMonth([
    'period' => '2023-06-27',
    'amount' => 10.00,
]);

// Associate the WorksheetItemMonths with the WorksheetItem
$worksheetItem->months()->saveMany([$worksheetItemMonth1, $worksheetItemMonth2]);

In this example, we first create a new Worksheet and save it. Then we create a new WorksheetItem and associate it with the Worksheet using the save method on the items relationship. Finally, we create two new WorksheetItemMonths and associate them with the WorksheetItem using the saveMany method on the months relationship.

Note that you should replace the model class names (Worksheet, WorksheetItem, WorksheetItemMonth) with the actual names of your models.

mr_reboot's avatar

This bot is really cool!

However, having to separate it out like this breaks quite a bit of convenience. Currently Eloquent will manage the relation for me and if it fails at any point the entire transaction is rolled back (at least that appears to be the case in my testing). I pass it an array and all the work is done. Nice and clean.

Our model here can have 100s of Worksheet_Items and hundreds of Worksheet_Items_Months

If I break everything out that way the only way I can think of doing it is to create a new transaction for each entry using loops. And if it fails anywhere in that process the transaction will not be rolled back.

I'm hoping/assuming there is a better way to do this? Similar how you can just pass an array in the single relation way?

kevinbui's avatar
kevinbui
Best Answer
Level 41

First, following the standard naming convention, I suggest using the singular form for model class names with proper camel cases. I rename WorksheetItems to WorksheetItem and WorkSheetItemMonths to WorksheetItemMonth.

First, we don't have to do anything crazy, let's create new objects from top to bottom.

$worksheet = new Worksheet([...]);

$worksheetItem = $worksheet->items()->create(['name' => 'Some Name']);

$worksheetItemMonths = $worksheetItem->saveMany([
    new WorksheetItemMonth(['period' => '2023-05-27', 'amount' => 10.00]),
    new WorksheetItemMonth(['period' => '2023-06-27', 'amount' => 10.00]),
]);

Alternatively, you can recursively save models by using the push method, saving everything at once.

use Illuminate\Database\Eloquent\Collection;

$worksheet = Worksheet::create(...);

items = new Collection([
     new WorksheetItem([...]),
     new WorksheetItem([...]),
]);

$months = new Collection([
    new WorksheetItemMonth(['period' => '2023-05-27', 'amount' => 10.00]),
    new WorksheetItemMonth(['period' => '2023-06-27', 'amount' => 10.00]),
]);

items[0]->setRelation('months', $months);

$worksheet->setRelation('items', $items);

$worksheet->push();

For more about the latter approach, you might want to have a look at the Illuminate\Database\Eloquent\Concerns\HasRelationships trait from the source code.

1 like
mr_reboot's avatar

@kevinbui I really appreciate the suggestion on model names. I assume just need to rename the Model and update the references to it? Then create a migration to rename the tables?

And thank you for your write up on setting the relations. Makes perfect sense. Will try both now and see if I run into any issues. I really like the idea of using the push method.

@dacfabre these models won't be associated with other models. So not really sure how a polymorphic relationship would do anything here?

Please or to participate in this conversation.