Best use of Skinny Controller Fat Model in Laravel

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

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

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.

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();

Please don’t!. Instead write like this

create a trait name DBQuery

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

Usage

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

Or without trait

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

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

$users = User::where('active', true)->where('age', '>' , 27)->where('country', 'MY')->get();$users = User::where('active', true)->where('age', '>' , 20)->where('country', 'US')->get();$users = User::where('active', true)->where('age', '>' , 15)->where('country', 'UK')->get();

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

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.

$users = User::isActive()->withAgeMoreThan(27)->inCountry('MY')->get();$users = User::isInActive()->withAgeMoreThan(20)->inCountry('US')->get();$users = User::isActive()->withAgeMoreThan(15)->inCountry('UK')->get();

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

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.

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

public function store(Request $request)
{
$input = $request->only([‘title’, ‘description’]);
$post = Post::create($input);

return redirect()->back()->withMessage('Create Success');
}

All the logic go to the Observer

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 means save all input with one line. For example, its turn from this

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

to this

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

But there is vulnerability in this method and you should not use $request->all(). Instead use $request->only() / $request->except() / $request->validated()

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.

{
name: "John Doe",
age: 20,
address: "1234 Street",
status: 1,
}

to

{
name: "John Doe",
age: 20,
address: "1234 Street",
status: 1,
status_name: "Active"
}

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

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, lets say 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 $this->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;
}

As full sample in controller will be like this

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~~

Software Engineer at Teratotech.com

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store