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();
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
$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
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
, andreplicating
.ā
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
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 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.
{
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~~
