Mencoba Merampingkan Kode Pada Controller Laravel

Published on
-
10 mins read
Authors

Pengantar

Controller merupakan salah satu bagian yang mempunyai peran besar dalam sebuah proyek berbasiskan MVC (Model View Controller). Controller biasanya digunakan secara efektif untuk mengambil permintaan pengguna (request), melakukan beberapa jenis logic, dan kemudian juga mengembalikan respon (response) yang didapat dari View.

Jika kita pernah mengerjakan proyek yang cukup besar, kita akan melihat bahwa akan memiliki banyak controller dan dapat menjadi berantakan dengan cepat tanpa kita sadari. Pada tulisan kali ini, kita akan melihat bagaimana kita dapat membersihkan atau merampingkan controller yang mulai membesar/membengkak (bloated) pada Laravel.

Masalah Dengan Pembengkakan Controller (A Bloated Controller)

Controller yang membengkak atau kode yang ada di dalamnya terlalu banyak akan menyebabkan beberapa masalah bagi para pengembang (developer). Setidaknya yang kita dapatkan adalah sebagai berikut:

  1. Kesulitan untuk melacak bagian kode atau fungsi tertentu

Jika kita ingin mengerjakan dibagian kode tertentu pada controller yang mulai membengkak, kita mungkin perlu meluangkan waktu untuk melacak, dimana method itu sebenarnya berada.

Saat menggunakan controller yang ramping dan dipisahkan secara logis atau kegunaannya masing-masing, paling tidak ini akan membantu kita untuk lebih mudah mencari kode yang akan kita kerjakan.

  1. Sulit untuk menemukan lokasi bug yang tepat

Ketika kita menangani authorization, validation, business logic, dan pembuatan respon pada satu tempat, akan sulit untuk menentukan dengan tepat lokasi bug.

  1. Sulit untuk menulis tes yang lebih kompleks pada kode kita

Terkadang sulit untuk menulis pengujian kode kita yang terlalu kompleks yang memiliki banyak baris dan melakukan banyak hal berbeda. Merampingkan kode membuat pengujian lebih mudah.

Controller yang Bengkak (A Bloated Controller)

Pada kesempatan kali ini, kita coba dengan menggunakan contoh studi kasus pada UserController

app\Http\Controllers\UserController.php
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function store(Request $request): RedirectResponse
{
$this->authorize('create', User::class);
$request->validate([
'name' => 'string|required|max:50',
'email' => 'email|required|unique:users',
'password' => 'string|required|confirmed',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => $request->password,
]);
$user->generateAvatar();
$this->dispatch(RegisterUserToNewsletter::class);
return redirect(route('users.index'));
}
public function unsubscribe(User $user): RedirectResponse
{
$user->unsubscribeFromNewsletter();
return redirect(route('users.index'));
}
}

1. Pindah Validation dan Authorization ke dalam Form Requests

Salah satu hal pertama yang dapat kita lakukan adalah memindahkan validasi dan otorisasi yang ada pada controller ke dalam form request. Kita bisa menggunakan perintah artisan berikut untuk membuat form request baru.

php artisan make:request StoreUserRequest

Perintah di atas akan membuat class baru yaitu StoreUserRequest.php yang terdapat pada folder app/Http/Requests. Form Request yang telah kita buat akan terlihat seperti berikut

app\Http\Requests\StoreUserRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

Kita dapat menggunakan authorize() method untuk menentukan apakah pengguna harus diizinkan untuk melakukan permintaan. Method ini harus mengembalikan berupa keluaran true jika diizinkan atau false jika tidak diizinkan.

Kita juga bisa menggunakan rules() method untuk menentukan aturan validasi apapun yang harus dijalankan pada body permintaan.

Kedua method ini akan berjalan secara otomatis sebelum kita berhasil menjalankan kode apapun di dalam controller tanpa perlu memanggil salah satu secara manual.

app\Http\Requests\StoreUserRequest.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return Gate::allows('create', User::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'name' => 'string|required|max:50',
'email' => 'email|required|unique:users',
'password' => 'string|required|confirmed',
];
}
}

Dan controller kita sekarang akan terlihat seperti berikut

app\Http\Controllers\UserController.php
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreUserRequest;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
class UserController extends Controller
{
public function store(StoreUserRequest $request): RedirectResponse
{
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => $request->password,
]);
$user->generateAvatar();
$this->dispatch(RegisterUserToNewsletter::class);
return redirect(route('users.index'));
}
public function unsubscribe(User $user): RedirectResponse
{
$user->unsubscribeFromNewsletter();
return redirect(route('users.index'));
}
}

2. Pindah Logika yang Umum ke dalam Action atau Services

Cara lain yang dapat kita gunakan untuk merampingkan store() method adalah kita dapat memindahkan business logic ke action class atau service class yang terpisah.

Dalam kasus penggunaan ini, kita dapat melihat bahwa fungsi utama store() method adalah untuk membuat pengguna baru, membuat avatar, dan kemudian mengirimkan antrian tugas pendaftaran pengguna ke newsletter/buletin e-mail.

Menurut pendapat saya, action lebih cocok untuk contoh saat ini daripada service. Dikarenakan action lebih prefer untuk tugas yang kecil yang hanya melakukan hal tertentu daripada menggunakan service.

Sedangkan untuk kode yang lebih besar yang berpotensi menjadi ratusan baris dan melakukan banyak hal, akan lebih cocok menggunakan service. Tapi kembali lagi, ini hanya soal kecocokan, alih-alih dalam sebuah tim ketika kita bekerja bersama rekan. Tidak ada standar yang mengikat terkait hal ini.

<?php
class StoreUserAction
{
public function execute(Request $request): void
{
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => $request->password,
]);
$user->generateAvatar();
$this->dispatch(RegisterUserToNewsletter::class);
}
}

Sekarang kita dapat mengubah controller kita untuk menggunakan action class yang sudah kita buat seperti diatas

app\Http\Controllers\UserController.php
<?php
class UserController extends Controller
{
public function store(StoreUserRequest $request, StoreUserAction $storeUserAction): RedirectResponse
{
$storeUserAction->execute($request);
return redirect(route('users.index'));
}
public function unsubscribe(User $user): RedirectResponse
{
$user->unsubscribeFromNewsletter();
return redirect(route('users.index'));
}
}

Seperti yang kita lihat, kita sekarang dapat mengeluarkan business logic dari method controller ke dalam sebuah action class. Ini berguna, karena controller pada dasarnya adalah "penyambung" antara request dan response kita.

Jadi sekarang kita telah mencoba mengurangi beban kognitif untuk memahami apa yang dilakukan oleh method dengan memisahkan kode secara logic.

Misalnya, ketika kita ingin memerikan otorisasi ataupun validasi, kita tahu bahwa yang perlu dicek adalah form request. Jika kita ingin memeriksa apa yang sedang dilakukan oleh permintaan data, kita dapat memeriksa action class yang sudah kita buat.

Manfaat lainnya yang dapat kita terima yaitu untuk mengabstraksi kode ke dalam class yang terpisah, ini dapat membuat pengujian jauh lebih mudah dan lebih cepat.

3. Menggunakan DTOs (Data Transfer Objects) dengan Actions

Manfaat besar lainnya dari mengekstraksi business logic kita ke dalam action class dan service class adalah kita sekarang dapat menggunakan logika tersebut di tempat yang berbeda tanpa perlu duplikasi kode kita lagi.

Sebagai contoh, anggaplah kita memiliki UserController yang menangani permintaan web dan Api/UserController yang menangani permintaan API. Kita dapat asumsikan bahwa store() method pada kedua controller tersebut sama.

Tapi apa yang akan kita lakukan jika permintaan API kita tidak menggunakan field email misalnya, melainkan menggunakan field email_address ? Kita tidak akan dapat meneruskan (passing) permintaan objek ke StoreUserAction class karena akan mengekspektasikan permintaan objek yang memiliki field email.

Untuk mengatasi masalah ini, kita dapat menggunakan DTO. Ini merupakan cara yang sangat berguna untuk memisahkan data dan meneruskan ke kode sistem kita tanpa terikat dengan apapun (dalam hal ini adalah request).

Untuk menggunakan DTO pada proyek kita, kita akan menggunakan package dari Spatie yaitu spatie/data-transfer-object dengan menggunakan perintah artisan sebagai berikut

composer require spatie/data-transfer-object

Sekarang setelah paket terinstal, kita buat folder bernama DataTransferObjects didalam folder App dan membuat class baru bernama StoreUserDTO.php. Kita juga perlu memastikan bahwa DTO kita meng-extends class Spatie\DataTransferObject\DataTransferObject. Kemudian kita dapat mendefinisikan tiga field sebagai berikut:

app\DataTransferObjects\StoreUserDTO.php
<?php
namespace App\DataTransferObjects;
use Spatie\DataTransferObject\DataTransferObject;
class StoreUserDTO extends DataTransferObject
{
public string $name;
public string $email;
public string $password;
}

Sekarang setelah kita melakukan ini, kita dapat menambahkan method baru ke form request kita yaitu StoreUserRequest yang sebelumnya untuk membuat dan mengembalikan StoreUserDTO class seperti berikut

app\Http\Requests\StoreUserRequest.php
<?php
namespace App\Http\Requests;
use App\DataTransferObjects\StoreUserDTO;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return Gate::allows('create', User::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'name' => 'string|required|max:50',
'email' => 'email|required|unique:users',
'password' => 'string|required|confirmed',
];
}
/**
* Build and return a DTO.
*
* @return StoreUserDTO
*/
public function toDTO(): StoreUserDTO
{
return new StoreUserDTO(
name: $this->name,
email: $this->email,
password: $this->password,
);
}
}

Sekarang kita dapat memperbarui controller kita untuk dapat meneruskan (passing) DTO ke dalam action class

app\Http\Controllers\UserController.php
<?php
class UserController extends Controller
{
public function store(StoreUserRequest $request, StoreUserAction $storeUserAction): RedirectResponse
{
$storeUserAction->execute($request->toDTO());
return redirect(route('users.index'));
}
public function unsubscribe(User $user): RedirectResponse
{
$user->unsubscribeFromNewsletter();
return redirect(route('users.index'));
}
}

Terakhir, kita perlu memperbarui action method kita untuk menerima DTO sebagai argumen menggantikan request object

<?php
class StoreUserAction
{
public function execute(StoreUserDTO $storeUserDTO): void
{
$user = User::create([
'name' => $storeUserDTO->name,
'email' => $storeUserDTO->email,
'password' => $storeUserDTO->password,
]);
$user->generateAvatar();
$this->dispatch(RegisterUserToNewsletter::class);
}
}

Hasil dari melakukan semua ini, sekarang kita telah sepenuhnya memisahkan action dari request object. Yang artinya kita dapat menggunakan kembali action ini dibanyak tempat seluruh sistem tanpa terikat pada struktur permintaan tertentu.

4. Menggunakan Resource Controller atau Single-Use Controller

Cara (yang mungkin) terbaik untuk menjaga kerampingan controller adalah dengan memastikan bahwa controller tersebut adalah "resource controller" atau "single-use controller". Sebelum melangkah lebih jauh dan mencoba memperbarui controller kita, mari kita sejenak memperdalam apa maksud dari kedua istilah ini.

Sebuah "resource controller" adalah controller yang menyediakan fungsionalitas berbasis di sekitar resource tertentu. Jadi, dalam kasus kita, resource kita adalah User dan kita ingin dapat melakukan operasi CRUD (Create, Read, Update, Delete) pada model ini.

"resource controller" biasanya berisi method index(), create(), store(), show(), edit(), update(), dan destroy(). Itupun kita tidak harus menyertakan semua method ini. Dengan menggunakan jenis controller ini, kita dapat menggunakan routing RESTful kita sendiri.

Sedangkan "single-use controller" adalah controller yang hanya memiliki satu public method yaitu __invoke(). Ini sangat berguna ketika kita memiliki controller yang tidak sesuai dengan salah satu metode RESTful yang dimiliki oleh "resource controller".

Jadi, mari kita coba buat controller baru dengan menggunakan perintah artisan sebagai berikut:

php artisan make:controller UnsubscribeUserController -i

Perhatikan bagaimana kita passing -i ke perintah sehingga controller baru kita akan menjadi "single-use controller" yang dapat dipanggil. Kita akan memiliki controller yang terlihat seperti berikut:

app\Http\Controllers\UnsubscribeUserController.php
<?php
class UnsubscribeUserController extends Controller
{
public function __invoke(Request $request)
{
//
}
}

Sekarang kita dapat memindahkan kode method kita dan menghapus method unsubscribe pada controller kita yang sebelumnya.

app\Http\Controllers\UnsubscribeUserController.php
<?php
class UnsubscribeUserController extends Controller
{
public function __invoke(Request $request): RedirectResponse
{
$request->user->unsubscribeFromNewsletter();
return redirect(route('users.index'));
}
}

Penutup

Semoga pada kesempatan kali ini, kita dapat mendapat wawasan baru tentang berbagi jenis hal mengenai controller pada Laravel agar tetap terlihat clean. Dan perlu diingat bahwa teknik yang ada di sini hanyalah pendapat pribadi.

Karena diluar sana ada pengembang (developer) lain yang akan menggunakan pendekatan lain terkait controller pada Laravel. Bagian terpentingnya yaitu konsisten dan menggunakan pendekatan yang sesuai dengan alur kerja kita sendiri, alih-alih apabila kita bekerja bersama tim, pastikan kita tidak membuat pendekatan sendiri agar kerja tim tetap bisa solid. Patuhi sesuai dengan keadaan alur kerja pada tim kita.

Teruslah membangun hal-hal yang luar biasa!

Sekian dan Semoga bermanfaat. Sampai Jumpa.

Referensi: