Skip to content

MatthewYeend/Laravel-Best-Practices

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 

Repository files navigation

Laravel Best Practices

Table of Contents

  1. Database Interaction
  2. Middleware
  3. Caching
  4. Events
  5. Logging
  6. Commands
  7. Notifications
  8. API Responses
  9. Blade Templates
  10. Direct querying in Blade files
  11. Using echo in Blade files
  12. Eloquent Relationships
  13. Testing
  14. Direct SQL Queries in Controllers
  15. Database Querying
  16. Validation
  17. Security Concerns
  18. Error Handling
  19. File Uploads
  20. User model
  21. Hardcoding configuration values
  22. Not using Route Model Binding
  23. Hardcoding Dependencies instead of using Dependency Injection
  24. Hardcoding configurations
  25. Mass assignment without guarded fields
  26. Lack of pagination for large datasets
  27. Use config and language files, constants instead of text in the code
  28. Use Constants for Repeated Values
  29. API Rate Limiting
  30. Form Input Sanitazation
  31. Custom Helpers
  32. Avoid Duplicate Queries
  33. Testing Practices
  34. Service Container Binding
  35. Repository Pattern
  36. Using Static Methods
  37. Queue Jobs
  38. Best Practices accepted by community
  39. Laravel Naming Conventions
  40. Interview Questions
    1. Beginner
    2. Intermediate
    3. Expert
    4. General
    5. Authentication and Authorization Questions
    6. Miscellaneous Questions

Database Interaction

Bad

class ProductController extends Controller
{
    public function show()
    {
        $products = DB::table('products')->where('active', 1)->get();
        return view('products.index', ['products' => $products]);
    }
}

Good

use App\Models\Product;

class ProductController extends Controller
{
    public function index()
    {
        $products = Product::active()->get();
        return view('products.index', compact('products'));
    }
}

Bad

class UserController extends Controller
{
    public function index()
    {
        // Query the database directly in the controller
        $users = DB::table('users')->get();

        return view('users.index', compact('users'));
    }

    public function store(Request $request)
    {
        // No validation, direct database insertion
        DB::table('users')->insert([
            'name' => $request->input('name'),
            'email' => $request->input('email'),
            'password' => bcrypt($request->input('password')),
        ]);

        return redirect()->route('users.index');
    }
}

Good

class UserController extends Controller
{
    public function index()
    {
        // Use Eloquent model to fetch users
        $users = User::all();

        return view('users.index', compact('users'));
    }

    public function store(UserRequest $request)
    {
        // Validation logic is handled through a custom Form Request class
        User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => bcrypt($request->password),
        ]);

        return redirect()->route('users.index');
    }
}

Middleware

Bad

public function handle($request, Closure $next)
{
    if (Auth::user()->role !== 'admin') {
        return response('Unauthorized.', 403);
    }

    return $next($request);
}

Good

public function handle($request, Closure $next)
{
    if (!Auth::user() || !Auth::user()->hasRole('admin')) {
        abort(403, 'Unauthorized action.');
    }

    return $next($request);
}

Caching

Bad

$products = DB::table('products')->get();
Cache::put('products', $products, 3600);

Good

$products = Cache::remember('products', 3600, function () {
    return Product::all();
});

Events

Bad

public function store(Request $request)
{
    $user = User::create($request->validated());
    Mail::to($user->email)->send(new WelcomeMail($user));
}

Good

public function store(Request $request)
{
    $user = User::create($request->validated());
    event(new UserRegistered($user));
}

Event Listener:

public function handle(UserRegistered $event)
{
    Mail::to($event->user->email)->send(new WelcomeMail($event->user));
}

Logging

Bad

Log::info('Something went wrong: ' . $e->getMessage());

Good

Log::error('Exception encountered.', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);

Commands

Bad

public function handle()
{
    DB::table('orders')->where('status', 'pending')->delete();
}

Good

public function handle()
{
    Order::pending()->delete();
}

Notifications

Bad

Mail::to($user->email)->send(new ResetPasswordMail($token));

Good

$user->notify(new ResetPasswordNotification($token));

API Responses

Bad

return response()->json(['data' => $data], 200);

Good

return response()->json([
    'status' => 'success',
    'data' => $data,
], 200);

Blade Templates

Bad

@if ($user->role == 'admin')
    <p>Welcome Admin</p>
@endif

Good

@can('viewAdminDashboard', $user)
    <p>Welcome Admin</p>
@endcan

Direct querying in Blade files

Bad

<h1>Users</h1>
@foreach (User::all() as $user)
    <p>{{ $user->name }}</p>
@endforeach

Good

Controller

public function index()
{
    $users = User::all();
    return view('users.index', compact('users'));
}

Blade

<h1>Users</h1>
@foreach ($users as $user)
    <p>{{ $user->name }}</p>
@endforeach

Using echo in Blade files

Bad

<p><?php echo $user->name; ?></p>

Good

<p>{{ $user->name }}</p>

Even better

<p>{{ $user->name ?? 'Guest' }}</p>

Eloquent Relationships

Bad

$comments = DB::table('comments')->where('post_id', $postId)->get();

Good

$comments = $post->comments;

Testing

Bad

public function testExample()
{
    $this->get('/home')->assertStatus(200);
}

Good

public function testHomePageLoadsCorrectly()
{
    $this->get('/home')
        ->assertStatus(200)
        ->assertSee('Welcome')
        ->assertDontSee('Error');
}

Direct SQL Queries in Controllers

Bad

public function index()
{
    $users = DB::select('SELECT * FROM users');
    return response()->json($users);
}

Good

use App\Models\User;

public function index()
{
    $users = User::all();
    return response()->json($users);
}

Database Querying

Bad

// Direct query execution without optimization
$users = DB::table('users')
    ->join('orders', 'users.id', '=', 'orders.user_id')
    ->where('orders.created_at', '>', now()->subMonth())
    ->select('users.name', 'orders.total')
    ->get();

Good

class UserController extends Controller
{
    public function index()
    {
        // Use Eloquent relationships and eager loading
        $users = User::with('orders')->whereHas('orders', function($query) {
            $query->where('created_at', '>', now()->subMonth());
        })->get();

        return view('users.index', compact('users'));
    }
}

Validation

Bad

public function store(Request $request)
{
    // No validation, using raw request data
    $user = new User;
    $user->name = $request->input('name');
    $user->email = $request->input('email');
    $user->password = bcrypt($request->input('password'));
    $user->save();
}

Good

class UserRequest extends FormRequest
{
    public function rules()
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users,email',
            'password' => 'required|string|min:8|confirmed',
        ];
    }
}

Controller

public function store(UserRequest $request)
{
    $user = User::create($request->validated());
}

Security Concerns

Bad

public function login(Request $request)
{
    $user = User::where('email', $request->email)->first();

    if ($user && Hash::check($request->password, $user->password)) {
        Auth::login($user);
        return redirect()->route('dashboard');
    }

    return back()->withErrors(['email' => 'Invalid credentials']);
}

Good

public function login(Request $request)
{
    $credentials = $request->only('email', 'password');

    if (Auth::attempt($credentials)) {
        return redirect()->route('dashboard');
    }

    return back()->withErrors(['email' => 'Invalid credentials']);
}

Error Handling

Bad

public function show($id)
{
    $user = User::find($id);

    if (!$user) {
        return response('User not found', 404);
    }

    return view('users.show', compact('user'));
}

Good

public function show($id)
{
    $user = User::findOrFail($id);

    return view('users.show', compact('user'));
}

File Uploads

Bad

public function upload(Request $request)
{
    $request->file('image')->move('uploads', 'image.jpg');
}

Good

public function upload(Request $request)
{
    $request->validate([
        'image' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048',
    ]);

    $path = $request->file('image')->store('uploads', 'public');

    return response()->json(['path' => $path]);
}

User model

Full name

Bad

public function getFullNameLong(): string
{
    return 'Mr. ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
}

Good

public function getFullNameLong(): string
{
    return $this->title . ' ' . $this->first_name . ' ' . $this->middle_name . ' ' . $this->last_name;
}

Better

public function getFullNameLong(): string
{
    return $this->title . ' ' . ($this->first_name ?? '') . ' ' . ($this->middle_name ?? '') . ' ' . ($this->last_name ?? '');
}

Short name

Bad

public function getFullNameShort(): string
{
    return $this->first_name[0] . '. ' . $this->last_name;
}

Good

public function getFullNameShort(): string
{
    $firstNameInitial = !empty($this->first_name) ? $this->first_name[0] . '.' : '';
    return $firstNameInitial . ' ' . $this->last_name;
}

Hardcoding configuration values

Bad

public function sendEmail()
{
    $to = '[email protected]';
    $subject = 'Hello World';
    mail($to, $subject, 'This is a test email.');
}

Good

use Illuminate\Support\Facades\Mail;

public function sendEmail()
{
    Mail::to(config('mail.default_to_address'))->send(new App\Mail\WelcomeMail());
}

Not using Route Model Binding

Bad

public function show($id)
{
    $user = User::find($id);
    if (!$user) {
        abort(404);
    }
    return view('user.show', compact('user'));
}

Good

public function show(User $user)
{
    return view('user.show', compact('user'));
}

Hardcoding Dependencies instead of using Dependency Injection

Bad

public function sendNotification()
{
    $mailer = new \App\Services\Mailer();
    $mailer->send('Hello World');
}

Good

use App\Services\Mailer;

public function sendNotification(Mailer $mailer)
{
    $mailer->send('Hello World');
}

Hardcoding configurations

Bad

$apiKey = '12345'; // API key hardcoded

Good

$apiKey = config('services.api.key');

Bad

public function uploadFile()
{
    $path = env('UPLOAD_PATH', 'uploads/default');
    Storage::put($path . '/file.txt', 'content');
}

Good

Inside config/filesystems.php

upload_path => env('UPLOADED_PATH', 'uploads/default');

Inside controller

public function uploadFile()
{
    $path = config('filesystems.upload_path');
    Storage::put($path . '/file.txt', 'content');
}

Mass assignment without guarded fields

Bad

public function store(Request $request)
{
    User::create($request->all());
}

Good

Inside User model

protected $fillable = ['name', 'email', 'password'];

Inside the controller

public function store(Request $request)
{
    $data = $request->only(['name', 'email', 'password']);
    $data['password'] = bcrypt(data['password']);

    User::create($data);
}

Lack of pagination for large datasets

Bad

public function index()
{
    $users = User::all();
    return response()->json($users);
}

Good

public function index()
{
    $users = User::paginate(10);
    return response()->json($users);
}

Use config and language files, constants instead of text in the code

Bad

public function isNormal(): bool
{
    return $article->type === 'normal';
}

return back()->with('message', 'Your article has been added!');

Good

public function isNormal()
{
    return $article->type === Article::TYPE_NORMAL;
}

return back()->with('message', __('app.article_added'));

Using Constants for Repeated Values

Bad

if ($user->type === 'admin') {
    // Perform action
}

Good

class User
{
    public const TYPE_ADMIN = 'admin';
    public const TYPE_CUSTOMER = 'customer';
}

Usage

if ($user->type === User::TYPE_ADMIN) {
    // Perform action
}

API Rate Limiting

Bad

Route::get('/api/resource', [ApiController::class, 'index']);

Good

Route::middleware('throttle:60,1')->get('/api/resource', [ApiController::class, 'index']);

Form Input Sanitization

Bad

$input = $request->all();

Good

$input = $request->only(['name', 'email', 'password']);

Custom Helpers

Bad

function calculateAge($birthdate)
{
    return \Carbon\Carbon::parse($birthdate)->age;
}

Good

Create a dedicated helper file:

if (!function_exists('calculateAge')) {
    function calculateAge($birthdate)
    {
        return \Carbon\Carbon::parse($birthdate)->age;
    }
}

Register the helper in composer.json:

"autoload": {
    "files": [
        "app/helpers.php"
    ]
}

Avoid Duplicate Queries

Bad

foreach ($users as $user) {
    $profile = $user->profile; // Triggers N+1 query issue
}

Good

$users = User::with('profile')->get();
foreach ($users as $user) {
    $profile = $user->profile;
}

Testing Practices

Bad

public function testUserCanLogin()
{
    $response = $this->post('/login', ['email' => '[email protected]', 'password' => 'password']);
    $response->assertStatus(200);
}

Good

public function testUserCanLogin()
{
    $response = $this->post('/login', ['email' => '[email protected]', 'password' => 'password']);
    $response->assertStatus(200)
        ->assertJsonStructure(['token']);
}

Service Container Binding

Bad

$userRepo = new EloquentUserRepository();

Good

Bind the repository in a service provider

$this->app->bind(UserRepository::class, EloquentUserRepository::class);

Usage:

$userRepo = app(UserRepository::class);

Repository Pattern

Bad

class UserController extends Controller
{
    public function index()
    {
        $users = User::all();
        return view('users.index', compact('users'));
    }
}

Good

interface UserRepository
{
    public function getAll();
}

Repository Implementation:

class EloquentUserRepository implements UserRepository
{
    public function getAll()
    {
        return User::all();
    }
}

Controller Usage:

class UserController extends Controller
{
    private UserRepository $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function index()
    {
        $users = $this->userRepository->getAll();
        return view('users.index', compact('users'));
    }
}

Using Static Methods

Bad

class UserHelper
{
    public static function isAdmin($user)
    {
        return $user->role === 'admin';
    }
}

Good

class UserHelper
{
    public function isAdmin($user)
    {
        return $user->role === 'admin';
    }
}

Usage:

$userHelper = new UserHelper();
$userHelper->isAdmin($user);

Queue Jobs

Bad

public function sendNotification(Request $request)
{
    Mail::to($request->email)->send(new NotificationMail());
}

Good

NotificationJob::dispatch($request->email);

Job Implementation:

class NotificationJob implements ShouldQueue
{
    public function __construct(public string $email) {}

    public function handle()
    {
        Mail::to($this->email)->send(new NotificationMail());
    }
}

Best Practices accepted by community

Laravel has some built in functionality and community packages can help instead of using 3rd party packages and tools.

Task Standard Tools 3rd Party Tools
Authorization Policies Entrust, Sentinel and other packages
Compiling Assests Laravel Mix, Vite Grunt, Gulp, and other packages
Development Environment Laravel Sail, Homestead Docker
Deployment Laravel Forge Deployer and other solutions
Unit Testing PHPUnit Pest
Browser Testing Laravel Dusk Codeception
DB Eloquent SQL, Doctrine
Templates Blade Twig
Working With Data Laravel Collections Arrays
Form Validation Request classes Validation in controller
Authentication Built In 3rd party packages, your own solution
API authentication Laravel Passport, Laravel Sanctum 3rd party JWT and OAuth packages
Creating an API Built in Dingo API or similar
Working with DB structure Migrations Working directly with the DB
Localisition Built in 3rd party packages
Realtime user interfaces Laravel Echo, Pusher 3rd party packages and working with WebSockets directly
Generating testing data Seeder classes, Model Factories, Faker Creating testing data manually
Task scheduling Laravel Task Scheduler Scripts and 3rd party packages
DB MySQL, PostgreSQL, SQLite, SQL Server MongoDB

Laravel Naming Conventions

To follow PSR standards And, follow naming conventions accepted by the Laravel community:

What How Good Bad
Controller Singular ArticaleController ArticalesController
Route Plural articles/1 article/1
Route Name snake_case with dot notation users.show_active users.show-active, show-active-users
Model Singular User Users
hasOne or belongsTo relationship Singular articleComment articleComments, article_comments
All other relationships Plural articleComments articleComment, article_comments
Table Plural article_comments article_comment, articleComments
Pivot Table Singular model names in alphabetical order article_user users_article, articles_users
Table Column snake_case without model name meta_title MetaTitle, article_meta_title
Model Proprty snake_case $model->created_at $model->createdAt
Foreign Key Singular model name with _id suffix article_id ArticleId, id_article, article_id
Primary Key - id custom_id
Migration - 2017_01_01_000000_create_articles_table 2017_01_01_000000_articles
Method camelCase getAll get_all
Method in resource controller Table store saveArticle
Method in test class camelCase testGuestCannotSeeArticle test_guest_cannot_see_article
Variable camelCase $articlesWithAuthor $articles_with_author
Collection Descriptive, Plural $activeUsers = User::active()->get() $active, $data
Object Descriptive, Singular $activeUser = User::active()->first() $users, $obj
Config and language files index snake_case articles_enabled ArticlesEnabled, articles-enabled
View kabab-case show-filtered.blade.php showFiltered.blade.php, show_filtered.blade.php
Config snake_case google_calendar.php googleCalendar.php, google-calendar.php
Contract (Interface) Adjective or noun AuthenticationInterface Authenticatable, IAuthentication
Trait Adjective Notifiable NotificationTrait
Trait (PSR) Adjective NotifiableTrait Notification
Enum Singular UserType UserTypes, UserTypeEnum
Form Request Singular UpdateUserRequest UpdateUserFormRequest, UserFormRequest, UserRequest
Seeder Singular UserSeeder UsersSeeder
Language File Names Lower case, snake_case user_management.php, order_status.php UserManagement.php, OrderStatus.php
Language Files Lower case, snake_case 'login_failed', 'user' 'LoginFailed', 'User'

Interview Questions

Beginner

  • What is Laravel?
    • Laravel is a PHP framework based on the MVC (Model-View-Controller) architecture, designed to make web development easier and faster by providing built-in features like routing, authentication, session management, and more.
  • What is Composer in Laravel?
    • Composer is a dependancy manager for PHP. It helps manage the libraries and packages required for a Laravel project.
  • What are the benefits of using Laravel?
    • Built-in tools for common tasks like routing, sessiong handling, and authentication.
    • Elegant syntax and expressive ORM (Eloquent).
    • Scalability and maintainability.
    • Robust security features.
  • What are service providers in Laravel?
    • Service providers are the central place to configure and bootstrap your application. Laravel's core services are all bootstrapped through service providers.
  • What is the Artisan CLI tool?
    • Artisan is Laravel's command-line interface that provides various commands to assist developers, such as creating controllers, migrations, and more

Intermediate

  • Explain Eloquent ORM in Laravel.
    • Eloquent is Laravel's built-in ORM, providing an easy-to-use Active Record implementation. It allows developers to interact with the database by defining models and relationships instead of writing raw SQL queries.
  • What are middleware in Laravel?
    • Middleware is a way to filter HTTP requests entering your application. Examples include authentication and logging.
  • What are migrations in Laravel?
    • Migrations are version control for your database. They allow you to modify the database schema programmatically and share the schema across teams.
  • How does routing work in Laravel?
    • Routes in Laravel are defined in routes/web.php for web routes and routes/api.php for API routes. A typical route is defined using:
      Route::get('/path', [Controller::class, 'method']);
  • How do you handle validation in Laravel?
    • Validation can be handled using the validate method in a controller or by creating a Form Request class.
  • What are queues in Laravel?
    • Queues allow you to defer the processing of time-consuming tasks, such as sending emails, or processing large files.
  • What is the differences between require and use in Laravel?
    • require includes files in PHP, whereas use is used to include namespaces or traits.

Expert

  • Explain service container in Laravel.
    • The service container is a powerful tool for managing class dependencies and performing dependency injection.
  • What is a Repository pattern in Laravel?
    • The Repository pattern separates the logic that retrieves data from the database from the business logic. It improves code readability and testability.
  • What is Laravel Event Broadcasting?
    • Broadcasting in Laravel allows you to share events between the server-side and client-side applications, enabling real-time features like notifications.
  • What is the difference between hasOne and belongsTo relationships in Laravel?
    • hasOne defines a one-to-one relationship where the parent model owns the related model. belongsTo defines the inverse relationship where the related model is owned by the parent model.
  • What is a policy in Laravel?
    • Policies are classes that organize authorization logic for a specific model.
  • How do you optimize a Laravel application?
    • Use caching for routes, views, and queries.
    • Optimize the database with proper indexing.
    • Use eager loading to avoid N+1 query problems.
    • Enable query caching.
  • How does Laravel handle error and exception handling?
    • Laravel uses the App\Exceptions\Handler class to handle all exceptions. You can log errors, render specific views, or redirect users.
  • What is the difference between @include, @yield, and @section in Blade?
    • @include includes a partial view.
    • @yield defines a placeholder for a section in a layout.
    • @section defines content for a section in the layout.
  • How can you implement custom helper functions in Laravel?
    • Create a helper file, define functions, and load it via Composer's autoload configuration in composer.json.
  • What are jobs and workers in Laravel?
    • Jobs represent tasks to be processed, and workers are the processes that execute those tasks.
  • What is Laravel Telescope?
    • Telescope is a debugging assistant for Laravel that provides insights into requests, jobs, database queries, and more.
  • How can you implement caching in Laravel?
    • You can use caching drivers like file, database, Redis, or Memcached. Example:
      Cache::put('key', 'value', $seconds);
      Cache::get('key');

General

  • What are facades in Laravel? How do they work?
    • Facades provide a static interface to classes in the service container. They act as a proxy to underlying classes and allow calling methods without needing to instantiate the class. Example: Cache::get('key').
  • What is the difference between public, protected, and private in a Laravel context?
    • public: Methods or properties accessible from anywhere.
    • protected: Accessible only within the class and its subclasses.
    • private: Accessible only within the class where it's declared.
  • What is the use of the boot method in Eloquent models?
    • The boot method is used to observe or hook into Eloquent model events (e.g., creating, updating, deleting) and to set global scopes.
  • What are traits in Laravel?
    • Traits are used to include reusable methods in multiple classes. Example: Using SoftDeletes to enable soft deletion functionality in models.

Authentication and Authorization Questions

  • What is the difference between Auth::attempt() and Auth::login()?
    • Auth::attempt() validates user credentials and logs in the user if valid.
    • Auth::login() directly logs in a user without validating credentials.
  • How does the Laravel Gate work?
    • Gates provide a way to define and authorize user actions at a higher level, like determining if a user can update a post:
      Gate::define('update-post', function ($user, $post) {
          return $user->id === $post->user_id;
      });
  • What is Sanctum in Laravel? How is it different from Passport?
    • Sanctum is a lightweight authentication system for API tokens and SPA authentication. Passport is for full OAuth2 authentication.

Miscellaneous Questions

  • What is a Laravel package? How do you create one?
    • Packages extend Laravel's functionality. To create one:
      • Set up a package directory structure.
      • Define service providers.
      • Publish assets or configurations as needed.
  • What is the event:listen and event:dispatch mechanism?
    • event:listen registers an event listener, while event:dispatch triggers the event. Example:
      Event::listen(UserRegistered::class, SendWelcomeEmail::class);
      event(new UserRegistered($user));
  • What is Laravel Horizon?
    • Horizon is a dashboard tool for monitoring and managing Laravel queues powered by Redis.
  • Explain the $fillable and $guarded properties in Laravel models.
    • $fillable: Specifies fields allowed for mass assignment.
    • $guarded: Specifies fields that are not mass assignable.
  • What is the purpose of broadcastOn() in Laravel events?
    • It defines the channels the event should be broadcast on.
  • What is the difference between session and cache in Laravel?
    • session stores user specific data for the duration of the user's session (e.g. user login info). It typically uses storage like files, database, or cookies
    • cache temporarily stores application data to optimise performance. It uses faster storage systems like Redis or Memchached.
  • What is the use of the dd() function in Laravel?
    • dd() stands for "Dump and Die". It is a debugging function used to dump variable contents and stop script execution.
  • How does Laravel handle APIs?
    • Laravel provides tools for building RESTful APIs:
      • Use routes/api.php for API routes.
      • Return JSON responses:
        return response()->json(['data' => $data]);
      • Use Resource classes for API responses:
        php artisan make:resource UserResource
      • Example in controller:
        return new UserResource(User::find(1));

Releases

No releases published

Sponsor this project

Packages

No packages published