5

I have two Model classes with a oneToMany relation like this

App\Car
class Car extends Model
{

    public $timestamps = true;

    protected $fillable = [
        'name',
        'price'
    ];

    public function parts ()
    {
        return $this->hasMany('App\Part');
    }

}
App\Part
class Part extends Model
{

    public $timestamps = false;

    protected $fillable = [
        'name',
        'price'
    ];

    public function car ()
    {
        return $this->belongsTo('App\Car');
    }

}

The client makes a POST request with a JSON representing a Car with a nested Array of Parts

{
    "name": "Fiat Punto",
    "price": 15000,
    "parts": [
        {
            "name": "wheel",
            "price": 300
        },
        {
            "name": "engine",
            "price": 5000
        }
    ]
}

Is there a way to save the Car model and create the relations in just one hit?

I tried to do this in my controller but it doesn't work:

...
public function store (Request $request) {
    $input = $request->all();
    Car::create($input);
}
...

PS: I already know how to do the task with a foreach or array_reduce, just wondering if laravel could do that for me


-- Edit --

Here is how i implemented the controller right now:

...
public function store (Request $request) {
    $input = $request->all();
    $car = Car::create($input);
    $parts = array_reduce(
        $input['parts'],
        function ($carry, $item) {
            array_push($carry, new Part($item));
            return $carry;
        },
        []
    );
    $car->parts()->saveMany($parts);
}
...

Any improvement is welcome

L. Catallo
  • 563
  • 4
  • 11
  • What exactly do you expect? You won't to make Laravel automatically create all related objects without writing a line of extra code? I don't think it's possible out of the box – Marcin Nabiałek Jan 14 '16 at 17:38
  • That would be awesome :D - But i am realistic, i'm ok with writing more code, just wondering if there is a laravel way to do the job. I edited the question to add my current implementation. I just want to create and store an array of Parts in the DB – L. Catallo Jan 14 '16 at 17:47
  • 2
    You could do something like this: `$car = Car::create($input); $car->parts()->createMany($input['parts']);` – Thomas Kim Jan 14 '16 at 18:01

2 Answers2

3

I don´t think there is a way of getting around the fact that you need to create each of the App\Part model instances that are defined in the request object. So, at some point you have to iterate over those items in the request object, meaning you have (at least) two options. Those are both described here.

On a side note I think in this case it is better to use a foreach loop for the first option:

foreach ($request->input('parts') as $item) {
    $car->parts()->save(new Part($item));
}

If you'd go for the second option of storing them in an array first, then I think array_map is the more appropriate method to use (because there is no actual "reducing" going on):

$parts = array_map(function($part) {
     return new App\Part($part);
}, $request->input('parts'));

$car->parts()-saveMany($parts);

Edit: As per suggestion from @TomasKim, with only 2 queries - one for the Car-model and another for the Part-models:

$parts = array_map(function($part) {
  return [...$part, 'car_id' => $car->id];
}, $input['parts'])

DB::table('parts')->insert($parts);
David Wickström
  • 397
  • 3
  • 11
  • I'm not sure about this but using the `foreach` option i'd get multiple queries, right? – L. Catallo Jan 14 '16 at 18:12
  • 2
    Both options generate multiple queries. If you want to avoid multiple queries, you'd need to use the query builder. – Thomas Kim Jan 14 '16 at 18:13
  • Yeah, that's true, multiple queries. Didn't know about the createMany method @ThomasKim, found it here: https://github.com/illuminate/database/blob/0a51c1a6cffc01e2aa0ffd73b0dace4fdf1c8040/Eloquent/Relations/HasOneOrMany.php#L302-L337 Does look like it would generate multiple queries too though. – David Wickström Jan 14 '16 at 18:23
3

While there isn't a way to technically do this entirely automatically, you can make the eloquent api for your model act this way by just tweaking the fillable property and adding a method:

class Car extends Model
{

    public $timestamps = true;

    protected $fillable = [
        'name',
        'price',
        'parts',
    ];

    public function parts ()
    {
        return $this->hasMany('App\Part');
    }

    public function setPartsAttribute($parts)
    {
        foreach ($parts as $part) {
            Part::updateOrCreate(
                ['id' => array_get($parts, 'id')],
                array_merge($part, ['car_id' => $this->id])
            );
        }
        // ... delete parts not in the list if you want
    }
}

Notice we just added parts to the fillable property, and then used Eloquent's attribute setting hook set[Field]Attribute() to perform the assignment. This makes it possible to have the Car api look about right:

Car::find(1)->update($carWithParts);

Note: This won't work when creating the Car model since the there is no id. If you want to automate that too, you can queue these updates and then use the model's event hooks to perform this action.

jpschroeder
  • 6,636
  • 2
  • 34
  • 34
  • 1
    I think you have a typo in your foreach loop `foreach ($pars as $part)` I believe you meant `$parts` – WhyAyala Jun 02 '17 at 17:28