Skip to content
All posts
LaravelArchitecture

Best use of Skinny Controller Fat Model in Laravel

January 30, 2021·Read on Medium·
Man vector created by upklyak — www.freepik.com

Bloating codes in single place would be devastating. It would be tons of duplication and unreadable codes which give your code maintainer a headache 😅.

So, i would cover how i go with Slim Controller and Fat Model

Skinny Controller basically means move all business logic, database logic and non response related logic to somewhere else and leave the controller clean and neat.

Fat Model basically means put only database related logic in the model instead of controller and make it as reusable method.

Don’t get me wrong. Fat Model doesn’t actually fat. I would prefer less fat than bloated model

There are some approach to implement this

Move Business Login In Repository

As mention in a lot of articles, repository pattern works as a bridge between models and controllers. If you don’t want to cluttered your model and controller with tons of business logic, using repository should be good enough. For more info, you can refer here

Most of repository pattern use interface to create methods. But i prefer to create a class as a base class. Why? i don’t want to repeat same query all over again to all repository. Here is sample of my base class

BaseRepository.php View on GitHub
<?php

namespace App\Models\Repository;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

class BaseRepository
{
    protected $model;

    public function __construct(Model $model)
    {
        $this->model = $model;
    }

    public function getQuery()
    {
        return $this->model->query();
    }

    public function factory(): \Illuminate\Database\Eloquent\Factories\Factory
    {
        return $this->model::factory();
    }

    public function getDbQuery()
    {
        return DB::connection($this->model->getConnectionName())->table($this->model->getTable());
    }

    public function first()
    {
        return $this->getQuery()->first();
    }

    public function all()
    {
        return $this->getQuery()->get();
    }

    public function count()
    {
        return $this->getQuery()->count();
    }

    public function paginate($limit = 10)
    {
        return $this->getQuery()->paginate($limit);
    }

    public function find($id, $withTrash = false)
    {
        if ($withTrash) {
            return $this->getQuery()->withTrashed()->find($id);
        }

        return $this->getQuery()->find($id);
    }

    public function where($column, $id, $first = false)
    {
        $query = $this->getQuery()->where($column, $id);

        return ($first) ? $query->first() : $query->get();
    }

    public function create(array $request)
    {
        return $this->getQuery()->create($request);
    }

    public function with($relation)
    {
        return $this->getQuery()->with($relation);
    }

    public function update($id, array $request, $withTrash = false)
    {
        if ($withTrash) {
            $app = $this->getQuery()->withTrashed()->find($id);
        } else {
            $app = $this->getQuery()->find($id);
        }

        $app->update($request);

        return $app;
    }
  
    public function delete($id)
    {
        return $this->getQuery()->find($id)->delete();
    }
}

From this BaseRepository class, i only need to pass a Model in constructor. Let’s create UserRepository

UserRepository.php View on GitHub
<?php
...
...
   
class UserRepository extends BaseRepository
{
   public function __construct(User $model)
   {
       parent::__construct($model);
   } 
   public function getAllUserWithAgeFive()
   {
        return $this->where('age', 5);
   }  
}

and since Laravel already have Automatic Injection, in Controller, we just put at constructor.

UserController.php View on GitHub
<?php
....
....

class UserController extends Controller 
{
    protected $repo; 
    public function __construct(UserRepository $repository)
    {
         $this->repo = $repository; 
    } 
    public function index()
    {
         return $this->repo->getAllUserWithAgeFive();
         // return $this->repo->where('age', 5);
         // return $this->repo->paginate(10);
         // return $this->repo->count();
    }
    public function store(Request $request)
    {
          $input = $request->only(['username', 'full_name']
          $this->repo->create($input);
 
    }
}

As we can see, UserRepository is clean from basic query logic. So, you can focus to put all business logic in it.

Make use of Route Model Binding

Use route model binding as necessary because it uses dependency injection to automatically find the model instance from the route.

It will turn from this

public function index(Request $request, $id)
{
$user = User::find($id);
$user->update($request->all());
}

into this

public function index(Request $request, User $user)
{
$user->update($request->all());
}

“I have a lot of logic to filter. That’s why i cant use route model binding”. Don’t worry. You can custom your own route binding by using Route::bind and place it in RouteServiceProvider

Route::bind('user', function($value) {
return User::query()->active()->findOrFail($value);
});

Laravel itself provide many features out of the box. All you need to do is try and error 😜. Please read Laravel doc about route model binding. They already explained.

Make best use of Model

There is tons of benefits can be use in the Model. Let’s take a look what we can benefit from. Many thing you can do in model like scoping, observers, events, mass assignment, traits, mutators, etc..

Query Builder

Usually we’ll see developers use like this

DB::table('users')->get();

Don’t recommended it. Instead write like this

create a trait name DBQuery

DBQuery.php View on GitHub
<?php

trait DBQuery {
   public static function useQuery()
   {
         return DB::connection((new self)->getConnectionName())
            ->table((new self)->getTable());
      
         //or return self::query()->getQuery(); 
   }
}

How to use?

Mode::useQuery()->get();

Or if you don’t want to use custom trait, you can directly use like example below.

Mode::query()->getQuery()->get();

Scopes

You can avoid using same query logic by using Scope. Scope have 2 types — Global and Local scope. One of example use global scope is Soft Delete where it automatically append you query.

Lets see how local scope works

There is very much repetition query there. Let’s scoping it,

<?php

class User extends Model
{
    ...
    ...
 
    public function scopeIsActive($query)
    {
         $query->where('active', true);
    }
    public function scopeIsInActive($query)
    {
         $query->where('active', false);
    }
    public function scopeWithAgeMoreThan($query, $age)
    {
         $query->where('age', '>' ,$age);
    }
    public function scopeInCountry($query, $country)
    {
         $query->where('country', $country);
    }
}

With this in place, we need to rewrite the queries to use scopes.

Its nice right? Much more human readable and cleaner. You can chain it with other query and also can use with eager load.

Observer

Observer will decouple your CRUD logic without messy your controller. How it works? The observer will listen every changes of your Model and automatically fired to the registered event.

“Eloquent models dispatch several events, allowing you to hook into the following moments in a model’s lifecycle: retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored, and replicating.”

It’s very recommended to use especially in big project. Here is the example:

Let’s say before/after you create or update, you want to execute several code.

<?php

public function store(Request $request)
{
    $input = $request->only(['title', 'description'];
    $input['uuid'] = Str::random(20);
    $input['title_slug'] = Str::slug($input['title']);
 
    $post = Post::create($input);
    event(new NotifyEveryone($post));    
    
    return redirect()->back()->withMessage('Create Success');
}

But after we use Observer, the controller will look like this

AfterStore.php View on GitHub
<?php

public function store(Request $request)
{
    $input = $request->only(['title', 'description']);
    $post = Post::create($input);  
    
    return redirect()->back()->withMessage('Create Success');
}

Where do all the logic go? It’s the observer do the rest of your after commit logic.

PostObserver.php View on GitHub
<?php

class PostObserver
{
     public function creating(Post $post)
     {
         $post->uuid = Str::random(20);
         $post->title_slug = Str::slug($post->title);
     }
     public function created(Post $post)
     {
         event(new NotifyEveryone($post));
     } 
}

Mass Assignment

Mass assignment means save all input with one line. For example, its turn from this

$post = new Post;
$post->title = "hello";
$post->description = "Here i am";
...
...
...
$post->save();

into this

$post = Post::create($request->all());

But there is vulnerability in this method and you should not use $request->all(). Why? Usually developers will use mass assignment and fill all the related fillable in their mode. IF your fillable variables contain sensitive information, attackers can modify your current data.

Instead use $request->only() / $request->except() / $request->validated(). These method will filter out non-required fields from modifying unintended data

Append Attribute

Append works as adding attributes to the serialized JSON while you do not have a corresponding column in your database.

For example, you have a user and there is status which in database refer as 1,2,3. When you print out the json, you want to translate the 1,2,3 value into more readable words.

into this

How? Add the attribute name to the appends property of your model.

<?php

class User extends Model {
 
   protected $appends = ['status_name'];
   ... 
   public function getStatusNameAttribute()
   {
       switch($this->attributes['status']) {
            case 1:
                return "Active";
            ...
            ... 
       }
   } 
}

Make use of FormRequest

In Laravel, there is a class called FormRequest that mainly used for request validation. But in bigger picture,FormRequest come in handy when validating complex logics ex: user permission, request input filtering, custom message, data manipulation, etc..

You can simple create by artisan command, lets say create request class named as UserFormRequest

php artisan make:request UserFormRequest

First we look authorize() method. This is where the part you should do validation related to user. No need to do redundant checking in Controller if you can do here.

public function authorize()
{
return $this->user()->can('create user');
}

Next, look into rules() method. This is where you put all your rules. Don’t cluttered your rules with long logic. Use php artisan make:rule instead.

return [
'full_name' => ['required'],
'username' => ['required', new UsernameRule]
];

Usually, in controller, most of us used $request->all() to create or update. Some of us used $request->only([‘username’, ‘full_name’, 'email', 'age']. Why don’t we tidy a little more by making a new method named allowed() in UserFormRequest

public function allowed()
{
return $request->only([‘username’, ‘full_name’, 'email', 'age'];
}

You can create any logic in FormRequest to make it as reusable. Let’s say i give another sample

public function ageMoreThanFive()
{
return $this->age > 5;
}

So, the UserFormRequest will looks like this

UserFormRequest.php View on GitHub
<?php


class UserFormRequest extends FormRequest
{
      public function authorize()
      {
           return $this->user()->can('create user');
      }
  
      public function rules()
      {
          return [
              'full_name' => ['required'], 
              'username' => ['required', new UsernameRule]
          ]
      }
  
      public function allowed()
      {
          return $this->only(['username', 'full_name', 'email', 'age'])
      }
}

As full usage sample in controller will be like this

UserFormController.php View on GitHub
<?php

public function store(UserFormRequest $request)
{
      if ($request->ageMoreThanFive()) {
         $this->repo->create($request->allowed());
      }
      ...
      ...  
}

Hope this tips and tricks tutorial will be helpful for you. 😁

Thanks for your time~~

Found this helpful?

If this article saved you time or solved a problem, consider supporting — it helps keep the writing going.

Originally published on Medium.

View on Medium