Courses

How to Structure Laravel 11 Projects

Validation to Form Request

The first goal of our course is to discuss where to store the code inside our /app folder.

Storing everything in the Controller is typical, but if it becomes too long, it's a good practice to separate the code into various places. But where exactly?

So, lesson by lesson, we will look at different options.

Spoiler alert: at the end of the day, it's your personal preference where to move the code. You MAY choose any option listed in this course. My goal here is to show you the possibilities.


Initial Controller code

Here's the Controller method code, which we will try to make shorter:

public function store(Request $request)
{
$this->authorize('user_create');
 
$userData = $request->validate([
'name' => 'required',
'email' => 'required|unique:users',
'password' => 'required',
]);
 
$userData['start_at'] = Carbon::createFromFormat('m/d/Y', $request->start_at)->format('Y-m-d');
$userData['password'] = bcrypt($request->password);
 
$user = User::create($userData);
$user->roles()->sync($request->input('roles', []));
 
Project::create([
'user_id' => $user->id,
'name' => 'Demo project 1',
]);
Category::create([
'user_id' => $user->id,
'name' => 'Demo category 1',
]);
Category::create([
'user_id' => $user->id,
'name' => 'Demo category 2',
]);
 
MonthlyReport::where('month', now()->format('Y-m'))->increment('users_count');
$user->sendEmailVerificationNotification();
 
$admins = User::where('is_admin', 1)->get();
Notification::send($admins, new AdminNewUserNotification($user));
 
return response()->json([
'result' => 'success',
'data' => $user,
], 200);
}

It's a pretty big method, right? Now, let's walk through the options to shorten it.

In this first lesson, let's start with the validation and authorization by moving them into Form Request.


Validation to Form Request

In this example, validation is simple, with three fields, but in real life, you could have 10+ fields.

Actually, we have two parts of the validation:

  • permissions
  • input data validation

Both of them MAY be moved to the Form Request class.

Let's start by creating a Form Request:

php artisan make:request StoreUserRequest

Now we have the app/Http/Requests/StoreUserRequest.php file, which has two methods inside:

  • authorize() for permissions
  • rules() for data validation

app/Http/Requests/StoreUserRequest.php:

class StoreUserRequest extends FormRequest
{
public function authorize()
{
return Gate::allows('user_create');
}
 
public function rules()
{
return [
'name' => 'required',
'email' => 'required|unique:users',
'password' => 'required',
];
}
}

Then, instead of the default Request class in the Controller, we need to inject our StoreUserRequest, and validated data can be accessed using the validated() method from the Request.

Now, the Controller will look like this:

public function store(StoreUserRequest $request)
{
$userData = $request->validated();
 
$userData['start_at'] = Carbon::createFromFormat('m/d/Y', $request->start_at)->format('Y-m-d');
$userData['password'] = bcrypt($request->password);
 
$user = User::create($userData);
$user->roles()->sync($request->input('roles', []));
 
// ... the rest of the method
}

So, this is our first goal: the few first lines of the Controller shortened, with validation logic moved into a separate layer.

Now, in addition to restructuring our Controller, in every lesson, I will try to show a few examples of the same Class from open-source projects.


Form Request: Open-Source Examples

Example Project 1. akaunting/akaunting

Let's check another example from an open-source project akaunting/akaunting.

In this example, the Form Request has additional checks to set if the field is required or nullable. Also, some validation messages are overwritten.

Also, notice the location and the filename of the FormRequest. You're free to store it wherever you want, and the file name of the Form Request doesn't necessarily have to end with ...Request. Here, it's just called Item.

app/Http/Requests/Common/Item.php

use App\Abstracts\Http\FormRequest;
use Illuminate\Support\Str;
 
class Item extends FormRequest
{
public function rules()
{
$picture = $sale_price = $purchase_price = 'nullable';
 
if ($this->files->get('picture')) {
$picture = 'mimes:' . config('filesystems.mimes')
. '|between:0,' . config('filesystems.max_size') * 1024
. '|dimensions:max_width=' . config('filesystems.max_width') . ',max_height=' . config('filesystems.max_height');
}
 
if ($this->request->get('sale_information') == 'true') {
$sale_price = 'required';
}
 
if ($this->request->get('purchase_information') == 'true') {
$purchase_price = 'required';
}
 
return [
'type' => 'required|string|in:product,service',
'name' => 'required|string',
'sale_price' => $sale_price . '|regex:/^(?=.*?[0-9])[0-9.,]+$/',
'purchase_price' => $purchase_price . '|regex:/^(?=.*?[0-9])[0-9.,]+$/',
'tax_ids' => 'nullable|array',
'category_id' => 'nullable|integer',
'enabled' => 'integer|boolean',
'picture' => $picture,
];
}
 
public function messages()
{
$picture_dimensions = trans('validation.custom.invalid_dimension', [
'attribute' => Str::lower(trans_choice('general.pictures', 1)),
'width' => config('filesystems.max_width'),
'height' => config('filesystems.max_height'),
]);
 
return [
'picture.dimensions' => $picture_dimensions,
];
}
}

How messy would the Controller look with such additional code, right? But now only the Form Request is injected into the Controller method, and the request is used where needed.

app/Http/Controllers/Api/Common/Items.php:

use App\Abstracts\Http\ApiController;
use App\Http\Requests\Common\Item as Request;
use App\Http\Resources\Common\Item as Resource;
use App\Jobs\Common\CreateItem;
use App\Jobs\Common\DeleteItem;
use App\Jobs\Common\UpdateItem;
use App\Models\Common\Item;
 
class Items extends ApiController
{
// ...
 
public function store(Request $request) // Don't confuse it with Laravel default Request
{
$item = $this->dispatch(new CreateItem($request));
 
return $this->created(route('api.items.show', $item->id), new Resource($item));
}
 
// ...
}

Notice: Personally, I disagree with using an alias of a general Request here. Some developers may not even notice that there's a Form Request class and confuse it with the default Request from Laravel. But hey, this is another proof that you can structure/name the classes however you want.


Example Project 2. crater-invoice/crate

The second example is from an open-source project crater-invoice/crate. There are a few validation rules for updating customer profile.

In the Form Request, two additional methods are used when creating a relationship record. So, you may create extra logic inside of that Form Request class.

app/Http/Requests/Customer/CustomerProfileRequest.php:

class CustomerProfileRequest extends FormRequest
{
public function authorize()
{
return true;
}
 
public function rules()
{
return [
'name' => [
'nullable',
],
'password' => [
'nullable',
'min:8',
],
'email' => [
'nullable',
'email',
Rule::unique('customers')->where('company_id', $this->header('company'))->ignore(Auth::id(), 'id'),
],
'billing.name' => [
'nullable',
],
'billing.address_street_1' => [
'nullable',
],
'billing.address_street_2' => [
'nullable',
],
'billing.city' => [
'nullable',
],
'billing.state' => [
'nullable',
],
'billing.country_id' => [
'nullable',
],
'billing.zip' => [
'nullable',
],
'billing.phone' => [
'nullable',
],
'billing.fax' => [
'nullable',
],
'shipping.name' => [
'nullable',
],
'shipping.address_street_1' => [
'nullable',
],
'shipping.address_street_2' => [
'nullable',
],
'shipping.city' => [
'nullable',
],
'shipping.state' => [
'nullable',
],
'shipping.country_id' => [
'nullable',
],
'shipping.zip' => [
'nullable',
],
'shipping.phone' => [
'nullable',
],
'shipping.fax' => [
'nullable',
],
'customer_avatar' => [
'nullable',
'file',
'mimes:gif,jpg,png',
'max:20000'
]
];
}
 
public function getShippingAddress()
{
return collect($this->shipping)
->merge([
'type' => Address::SHIPPING_TYPE
])
->toArray();
}
 
public function getBillingAddress()
{
return collect($this->billing)
->merge([
'type' => Address::BILLING_TYPE
])
->toArray();
}
}

In the Controller, values from the request are obtained using respective methods.

app/Http/Controllers/V1/Customer/General/ProfileController.php:

class ProfileController extends Controller
{
public function updateProfile(Company $company, CustomerProfileRequest $request)
{
$customer = Auth::guard('customer')->user();
 
$customer->update($request->validated());
 
if (isset($request->is_customer_avatar_removed) && (bool) $request->is_customer_avatar_removed) {
$customer->clearMediaCollection('customer_avatar');
}
if ($customer && $request->hasFile('customer_avatar')) {
$customer->clearMediaCollection('customer_avatar');
 
$customer->addMediaFromRequest('customer_avatar')
->toMediaCollection('customer_avatar');
}
 
if ($request->billing !== null) {
$customer->shippingAddress()->delete();
$customer->addresses()->create($request->getShippingAddress());
}
 
if ($request->shipping !== null) {
$customer->billingAddress()->delete();
$customer->addresses()->create($request->getBillingAddress());
}
 
return new CustomerResource($customer);
}
 
// ...
}

So yeah, these are examples of moving the validation logic from Controller to Form Request. Let's move on to moving other parts!

avatar

With examples from open source projects very cool, thank you!

👍 4