craigivemy's avatar

Cleanest way to save model and relationships

Hi. I am in the process of building a front end Angular app that consumes the API that I have / am building in Laravel.

I am wondering the best way to take an object sent from the front end to my api, including relationships (as arrays of nested objects) and saving.

Rather than manually typing out something like:

    1:    $task->owner = $request->input('owner');
        2:    $task->client = $request->input('client');
        3:    $task->campaign_id = $request->input('campaign_id');
        4:    $task->title = $request->input('title');
        5:    $task->notes = $request->input('notes');
        6:    $task->viewed = $request->input('viewed');
        7:    $task->priority = $request->input('priority');
        8:    $task->progress = $request->input('progress');
        9:    $task->start_date = $request->input('start_date');
        10: $task->end_date = $request->input('end_date');
        11: $task->departments = $request->input('departments')

I appreciate the fill() method can be used, before save() - but does this handle any relationships?

Owner to task is one to many, so is client to task, whereas department to task is a many to many relationship. What would people recommend doing here if fill() doesn't include relationships (I find it very hard to understand how it possible could) - extract the relationships and handle them separately and use fill() for the single values?

0 likes
3 replies
EliasSoares's avatar

I usually create some helper methods on my controller that do all the logic. On our last project, I extracted all this logic out for a Service class that extends this base ApiService abstract class:

<?php
/**
 * Copyright (c) 2016 IGET Serviços em comunicação digital LTDA - All rights Reserved
 * Unauthorized copying of this file, via any medium is strictly prohibited
 * Proprietary and confidential
 */

namespace App\Services\Contracts;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;

abstract class ApiService
{
    /**
     * @return array
     */
    abstract public function getRelationships();

    /**
     * @return \Illuminate\Database\Eloquent\Model
     */
    abstract public function getModel();

    /**
     * @param array $data
     * @return \Illuminate\Database\Eloquent\Model
     * @throws \Exception
     */
    public function create(array $data)
    {
        \DB::beginTransaction();
        try {
            if (isset($data['relations'])) {
                $relations = $data['relations'];
                unset($data['relations']);
            }

            $created_model = $this->getModel()->create($data);
            if (isset($relations)) {
                $this->updateRelationships($created_model, $relations);
            }

            \DB::commit();

            return $created_model;
        } catch (\Exception $e) {
            \DB::rollback();
            throw $e;
        }
    }

    /**
     * @param array $data
     * @param int   $model_id
     * @return \Illuminate\Database\Eloquent\Model
     * @throws \Exception
     */
    public function update(array $data, $model_id)
    {
        \DB::beginTransaction();
        try {
            $model = $this->getModel()->find($model_id);

            if (isset($data['relations'])) {
                $relations = $data['relations'];
                unset($data['relations']);
            }

            $model->update($data);

            if (isset($relations)) {
                $this->updateRelationships($model, $relations);
            }

            \DB::commit();

            return $model;
        } catch (\Exception $e) {
            \DB::rollback();
            throw $e;
        }
    }

    /**
     * @param \Illuminate\Database\Eloquent\Model $model
     * @param array $data
     */
    private function updateRelationships(Model $model, array $data)
    {
        foreach ($this->getRelationships() as $relationship_name) {
            if (isset($data[$relationship_name])) {
                $relationship_type = get_class($model->$relationship_name());
                switch ($relationship_type) {
                    case BelongsToMany::class:
                        $this->syncBelongsToManyRelationship($model, $relationship_name, $data[$relationship_name]);
                        break;
                    case MorphMany::class:
                    case HasMany::class:
                        $this->syncHasManyRelationship($model, $relationship_name, $data[$relationship_name]);
                        break;
                    default:
                        break;
                }
                unset($data[$relationship_name]);
            }
        }
    }

    /**
     * @param \Illuminate\Database\Eloquent\Model $model
     * @param $relationship_name
     * @param array $data
     */
    private function syncHasManyRelationship(Model $model, $relationship_name, array $data)
    {
        $present_ids = [];
        foreach ($data as $related) {
            $conditions = [
                'id' => array_key_exists('id', $related) ? $related['id'] : null
            ];

            $present_ids[] = $model->$relationship_name()->updateOrCreate($conditions, $related)->id;
        }

        $model->$relationship_name()->whereNotIn('id', $present_ids)->delete();
    }

    /**
     * @param \Illuminate\Database\Eloquent\Model  $model
     * @param string $relationship_name
     * @param array  $data
     * @return mixed
     */
    private function syncBelongsToManyRelationship(Model $model, $relationship_name, array $data)
    {
        return $model->$relationship_name()->sync($data);
    }
}

Here you can see an example service implementation of a model that have a BelongsToMany contacts relationship:

<?php
/**
 * Copyright (c) 2016 IGET Serviços em comunicação digital LTDA - All rights Reserved
 * Unauthorized copying of this file, via any medium is strictly prohibited
 * Proprietary and confidential
 */

namespace App\Services;

use App\Models\Client;
use App\Services\Contracts\ApiService;

class ClientService extends ApiService
{
    /**
     * @return Client
     */
    public function getModel()
    {
        return new Client();
    }

    /**
     * @return array
     */
    public function getRelationships()
    {
        return [
            'contacts',
        ];
    }
}

This code assumes that you will pass model relationship data on a relations key at your request payload.

Then on your controller, you should only do:

    public function store(ClientRequest $request)
    {
        $created_client     = $this->clientService->create($request->all());

    return "your response";
    } 

I know that it feels complicated, but if you give a try, you will see that this speed up your development process and make your controllers very clean!

Feel free to use this class. It only implements BelongsMany, MorphMany and HasMany relationships.

4 likes
craigivemy's avatar

@EliasSoares much appreciated. I was wondering if there were any inbuilt methods / techniques but it seems as though a solution such as yours would be the best option - it certainly looks very clean and I am keen to abstract as much from my controllers as possible. Thanks!

Blizz's avatar

If you need no more than just to save model and its relations there's a push()-method on the Model class that does exactly that. Transaction to be provided by yourself. It just saves everything according to the regular rules (so assuming it's dirty)

1 like

Please or to participate in this conversation.