General Laravel Standards
Prefer Constructor/Method Injection
// Good - Constructor injection
final class TicketService
{
public function __construct (
private TicketRepository $ tickets ,
private NotificationService $ notifications ,
) {}
public function close (Ticket $ ticket ): void
{
$ this ->tickets ->close ($ ticket );
$ this ->notifications ->send ($ ticket ->user , new TicketClosed ($ ticket ));
}
}
// Good - Method injection (controllers, commands)
public function __invoke (
StoreTicketRequest $ request ,
CreateTicket $ createTicket ,
): RedirectResponse {
$ ticket = $ createTicket ->execute (TicketData::from ($ request ));
return redirect ()->route ('tickets.show ' , $ ticket );
}
Use app() When Injection Isn't Possible
// Acceptable - When DI isn't available
final class ConvertFeetToInches
{
public function execute (float $ length , Measurement $ from = Measurement::Feet): float
{
if ($ from === Measurement::Meters) {
$ length = app (ConvertMetersToFeet::class)->execute ($ length );
}
return $ length * 12 ;
}
}
// Acceptable - In service providers
$ this ->app ->bind (PaymentGateway::class, StripeGateway::class);
Avoid Injecting the Container
// Bad - Injecting container
public function __construct (
private Application $ app ,
) {}
public function process (): void
{
$ service = $ this ->app ->make (SomeService::class);
}
// Good - Inject what you need
public function __construct (
private SomeService $ service ,
) {}
// Good - Helper functions
session ('cart ' );
config ('app.name ' );
cache ('key ' );
auth ()->user ();
request ()->input ('name ' );
redirect ()->route ('home ' );
response ()->json ($ data );
view ('welcome ' );
now ();
today ();
// Avoid - Longer alternatives
Session::get ('cart ' );
Config::get ('app.name ' );
Cache::get ('key ' );
Auth::user ();
Request::input ('name ' );
Use Facades for Fluent APIs
// Good - Facade for chained methods
Cache::tags (['users ' , 'profiles ' ])->put ($ key , $ value , $ ttl );
Log::channel ('slack ' )->critical ('System down ' );
Storage::disk ('s3 ' )->put ($ path , $ contents );
// Good - Facade for static-style calls
DB ::transaction (fn () => $ this ->process ());
Route::middleware ('auth ' )->group (fn () => . .. );
Operation
Use
Simple get/set
Helper (cache(), session(), config())
Chained methods
Facade (Cache::tags()->put())
Channel/disk selection
Facade (Log::channel(), Storage::disk())
Transactions
Facade (DB::transaction())
Laravel Helpers vs PHP Functions
// Good - Laravel Str helpers
use Illuminate \Support \Str ;
Str::contains ($ haystack , $ needle );
Str::startsWith ($ string , $ prefix );
Str::endsWith ($ string , $ suffix );
Str::before ($ string , $ delimiter );
Str::after ($ string , $ delimiter );
Str::slug ($ title );
Str::uuid ();
Str::random (32 );
Str::limit ($ text , 100 );
Str::plural ($ word );
Str::camel ($ string );
Str::snake ($ string );
Str::kebab ($ string );
Str::studly ($ string );
// Avoid - PHP equivalents
str_contains ($ haystack , $ needle );
str_starts_with ($ string , $ prefix );
substr ($ string , 0 , strpos ($ string , $ delimiter ));
use Illuminate \Support \Str ;
$ slug = Str::of ($ title )
->trim ()
->lower ()
->replace (' ' , '- ' )
->slug ();
$ excerpt = Str::of ($ content )
->stripTags ()
->limit (200 )
->toString ();
// Good - Laravel Arr helpers
use Illuminate \Support \Arr ;
Arr::get ($ array , 'user.name ' , 'default ' );
Arr::has ($ array , 'user.email ' );
Arr::first ($ array , fn ($ value ) => $ value > 10 );
Arr::last ($ array );
Arr::only ($ array , ['name ' , 'email ' ]);
Arr::except ($ array , ['password ' ]);
Arr::pluck ($ array , 'name ' );
Arr::where ($ array , fn ($ value , $ key ) => $ value > 0 );
Arr::flatten ($ array );
Arr::dot ($ array );
Arr::undot ($ array );
// Good - data_get for nested access
$ name = data_get ($ response , 'data.user.profile.name ' );
$ names = data_get ($ users , '*.name ' );
// Good - Use collections
$ names = collect ($ users )
->filter (fn ($ user ) => $ user ['is_active ' ])
->map (fn ($ user ) => $ user ['name ' ])
->sort ()
->values ();
// Avoid - Array functions
$ active = array_filter ($ users , fn ($ user ) => $ user ['is_active ' ]);
$ names = array_map (fn ($ user ) => $ user ['name ' ], $ active );
sort ($ names );
$ names = array_values ($ names );
// Good - Helper function
$ name = config ('app.name ' );
$ timeout = config ('services.api.timeout ' , 30 );
// Good - Get entire config array
$ mail = config ('mail ' );
// Set config at runtime (testing)
config (['app.debug ' => true ]);
// Good - Access env only in config files
// config/services.php
return [
'stripe ' => [
'key ' => env ('STRIPE_KEY ' ),
'secret ' => env ('STRIPE_SECRET ' ),
],
];
// Then use config() everywhere else
$ key = config ('services.stripe.key ' );
// Bad - Using env() outside config
$ key = env ('STRIPE_KEY ' ); // Returns null when config is cached
// config/services.php
return [
'api ' => [
'timeout ' => (int ) env ('API_TIMEOUT ' , 30 ),
'verify_ssl ' => (bool ) env ('API_VERIFY_SSL ' , true ),
'base_url ' => env ('API_BASE_URL ' , 'https://api.example.com ' ),
],
];
use Illuminate \Support \Facades \Log ;
// Debug information
Log::debug ('User login attempt ' , ['email ' => $ email ]);
// General information
Log::info ('Order placed ' , ['order_id ' => $ order ->id ]);
// Warnings
Log::warning ('Payment retry ' , ['attempt ' => $ attempt ]);
// Errors
Log::error ('Payment failed ' , [
'order_id ' => $ order ->id ,
'error ' => $ exception ->getMessage (),
]);
// Critical (immediate attention)
Log::critical ('Database connection lost ' );
// Good - Structured context
Log::info ('User registered ' , [
'user_id ' => $ user ->id ,
'email ' => $ user ->email ,
'plan ' => $ user ->plan ,
]);
// Bad - String interpolation
Log::info ("User {$ user ->id } registered with email {$ user ->email }" );
Log::channel ('slack ' )->critical ('Production error ' , ['exception ' => $ e ]);
Log::channel ('daily ' )->info ('Daily report generated ' );
Log::stack (['daily ' , 'slack ' ])->error ('Critical failure ' );
use Illuminate \Support \Facades \Cache ;
// Get with default
$ value = Cache::get ('key ' , 'default ' );
$ value = cache ('key ' , 'default ' );
// Get or compute
$ users = Cache::remember ('users ' , now ()->addHours (1 ), function () {
return User::all ();
});
// Store
Cache::put ('key ' , $ value , now ()->addMinutes (10 ));
cache (['key ' => $ value ], now ()->addMinutes (10 ));
// Forever
Cache::forever ('key ' , $ value );
// Delete
Cache::forget ('key ' );
// Check existence
if (Cache::has ('key ' )) {
// ...
}
// Store with tags
Cache::tags (['users ' , 'profiles ' ])->put ('user.1 ' , $ user , $ ttl );
// Retrieve tagged
$ user = Cache::tags (['users ' , 'profiles ' ])->get ('user.1 ' );
// Flush by tag
Cache::tags ('users ' )->flush ();
$ lock = Cache::lock ('processing-order- ' .$ order ->id , 10 );
if ($ lock ->get ()) {
try {
$ this ->processOrder ($ order );
} finally {
$ lock ->release ();
}
}
// Or with callback
Cache::lock ('processing ' )->get (function () {
// Lock acquired, auto-released after callback
});
// Using event helper
event (new OrderPlaced ($ order ));
// Using Event facade
Event::dispatch (new OrderPlaced ($ order ));
// From model (if using $dispatchesEvents)
$ order ->save (); // Fires OrderCreated automatically
<?php
declare (strict_types=1 );
namespace App \Events ;
use App \Models \Order ;
use Illuminate \Foundation \Events \Dispatchable ;
use Illuminate \Queue \SerializesModels ;
final class OrderPlaced
{
use Dispatchable;
use SerializesModels;
public function __construct (
public Order $ order ,
) {}
}
<?php
declare (strict_types=1 );
namespace App \Listeners ;
use App \Events \OrderPlaced ;
use App \Notifications \OrderConfirmation ;
final class SendOrderConfirmation
{
public function handle (OrderPlaced $ event ): void
{
$ event ->order ->user ->notify (new OrderConfirmation ($ event ->order ));
}
}
Register in EventServiceProvider
protected $ listen = [
OrderPlaced::class => [
SendOrderConfirmation::class,
UpdateInventory::class,
NotifyWarehouse::class,
],
];
use App \Jobs \ProcessOrder ;
// Dispatch to default queue
ProcessOrder::dispatch ($ order );
// Dispatch with delay
ProcessOrder::dispatch ($ order )->delay (now ()->addMinutes (5 ));
// Dispatch to specific queue
ProcessOrder::dispatch ($ order )->onQueue ('orders ' );
// Dispatch to specific connection
ProcessOrder::dispatch ($ order )->onConnection ('redis ' );
// Chain jobs
Bus::chain ([
new ProcessPayment ($ order ),
new UpdateInventory ($ order ),
new SendConfirmation ($ order ),
])->dispatch ();
<?php
declare (strict_types=1 );
namespace App \Jobs ;
use App \Models \Order ;
use App \Services \PaymentService ;
use Illuminate \Bus \Queueable ;
use Illuminate \Contracts \Queue \ShouldQueue ;
use Illuminate \Foundation \Bus \Dispatchable ;
use Illuminate \Queue \InteractsWithQueue ;
use Illuminate \Queue \SerializesModels ;
final class ProcessOrder implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public int $ tries = 3 ;
public int $ backoff = 60 ;
public int $ timeout = 120 ;
public function __construct (
public Order $ order ,
) {}
public function handle (PaymentService $ payment ): void
{
$ payment ->process ($ this ->order );
}
public function failed (Throwable $ exception ): void
{
Log::error ('Order processing failed ' , [
'order_id ' => $ this ->order ->id ,
'error ' => $ exception ->getMessage (),
]);
}
}
use Illuminate \Contracts \Queue \ShouldBeUnique ;
final class ProcessOrder implements ShouldQueue, ShouldBeUnique
{
public function uniqueId (): string
{
return (string ) $ this ->order ->id ;
}
public function uniqueFor (): int
{
return 60 ; // Seconds
}
}
// To a single user
$ user ->notify (new OrderShipped ($ order ));
// To multiple users
Notification::send ($ users , new OrderShipped ($ order ));
// On-demand (no user model)
Notification::route ('mail ' , 'admin@example.com ' )
->route ('slack ' , '#alerts ' )
->notify (new SystemAlert ($ message ));
<?php
declare (strict_types=1 );
namespace App \Notifications ;
use App \Models \Order ;
use Illuminate \Bus \Queueable ;
use Illuminate \Contracts \Queue \ShouldQueue ;
use Illuminate \Notifications \Messages \MailMessage ;
use Illuminate \Notifications \Notification ;
final class OrderShipped extends Notification implements ShouldQueue
{
use Queueable;
public function __construct (
public Order $ order ,
) {}
public function via (object $ notifiable ): array
{
return ['mail ' , 'database ' ];
}
public function toMail (object $ notifiable ): MailMessage
{
return (new MailMessage )
->subject (__ ('notifications.order_shipped.subject ' ))
->line (__ ('notifications.order_shipped.line1 ' , ['order ' => $ this ->order ->number ]))
->action (__ ('notifications.order_shipped.action ' ), route ('orders.show ' , $ this ->order ))
->line (__ ('notifications.order_shipped.line2 ' ));
}
public function toArray (object $ notifiable ): array
{
return [
'order_id ' => $ this ->order ->id ,
'message ' => __ ('notifications.order_shipped.message ' ),
];
}
}
When to Create a Service Provider
Binding interfaces to implementations
Registering event listeners
Configuring third-party packages
Bootstrapping application services
<?php
declare (strict_types=1 );
namespace App \Providers ;
use App \Contracts \PaymentGateway ;
use App \Services \StripeGateway ;
use Illuminate \Support \ServiceProvider ;
final class PaymentServiceProvider extends ServiceProvider
{
public function register (): void
{
$ this ->app ->bind (PaymentGateway::class, StripeGateway::class);
$ this ->app ->singleton (StripeClient::class, function ($ app ) {
return new StripeClient (config ('services.stripe.secret ' ));
});
}
public function boot (): void
{
// Bootstrap services after all providers registered
}
}
// bootstrap/app.php
->withExceptions (function (Exceptions $ exceptions ): void {
// Don't report certain exceptions
$ exceptions ->dontReport ([
ValidationException::class,
AuthenticationException::class,
]);
// Custom reporting
$ exceptions ->report (function (PaymentFailedException $ e ): void {
Log::channel ('payments ' )->error ($ e ->getMessage (), [
'order_id ' => $ e ->order ->id ,
]);
});
// Custom rendering
$ exceptions ->render (function (NotFoundHttpException $ e , Request $ request ) {
if ($ request ->expectsJson ()) {
return response ()->json (['message ' => 'Not found ' ], 404 );
}
});
})
<?php
declare (strict_types=1 );
namespace App \Exceptions ;
use Exception ;
final class InsufficientFundsException extends Exception
{
public function __construct (
public readonly float $ available ,
public readonly float $ required ,
) {
parent ::__construct ("Insufficient funds: {$ available } available, {$ required } required " );
}
public function render (): Response
{
return response ()->json ([
'message ' => $ this ->getMessage (),
'available ' => $ this ->available ,
'required ' => $ this ->required ,
], 422 );
}
public function report (): void
{
Log::warning ('Insufficient funds ' , [
'available ' => $ this ->available ,
'required ' => $ this ->required ,
]);
}
}
use Illuminate \Support \Facades \Storage ;
// Store file
Storage::put ('file.txt ' , $ contents );
Storage::disk ('s3 ' )->put ('file.txt ' , $ contents );
// Store uploaded file
$ path = $ request ->file ('avatar ' )->store ('avatars ' , 's3 ' );
// Get file
$ contents = Storage::get ('file.txt ' );
$ exists = Storage::exists ('file.txt ' );
// Delete
Storage::delete ('file.txt ' );
Storage::delete (['file1.txt ' , 'file2.txt ' ]);
// URLs
$ url = Storage::url ('file.txt ' );
$ temporaryUrl = Storage::temporaryUrl ('file.txt ' , now ()->addMinutes (5 ));
Use Laravel's Built-in Features
// Good - Use Carbon
$ date = now ()->addDays (7 );
$ formatted = $ date ->format ('Y-m-d ' );
$ diff = $ date ->diffForHumans ();
// Good - Use collections
$ filtered = collect ($ items )->filter (...)->map (...);
// Good - Use validation
$ validated = $ request ->validate ([...]);
// Good - Use policies
$ this ->authorize ('update ' , $ ticket );
// Bad - Business logic in routes
Route::post ('/orders ' , function (Request $ request ) {
// 50 lines of order processing...
});
// Bad - Raw queries when Eloquent works
DB ::select ('SELECT * FROM users WHERE active = 1 ' );
// Bad - Manual file includes
require_once 'helpers.php ' ;
// Bad - Global functions
function formatPrice ($ price ) { . .. }