18 KiB
18 KiB
| 1 | No | Category | Guideline | Description | Do | Don't | Code Good | Code Bad | Severity | Docs URL |
|---|---|---|---|---|---|---|---|---|---|---|
| 2 | 1 | Blade Templates | Use Blade components for reusable UI | Extract repeated markup into named Blade components | Use x-* components with @props for all reusable UI | Duplicate HTML blocks across views | <x-card :title="$title">{{ $slot }}</x-card> | @include('card' ['title' => $title]) | High | https://laravel.com/docs/blade#components |
| 3 | 2 | Blade Templates | Use layouts with @extends and @section | Define one master layout and extend it per page | @extends layout with named @section blocks | Duplicate header/footer HTML in every view | @extends('layouts.app') @section('content') | Full HTML in every view file | High | https://laravel.com/docs/blade#layouts-using-template-inheritance |
| 4 | 3 | Blade Templates | Use @props for component type-safety | Declare accepted props inside components with @props | @props with defaults to document component API | Pass arbitrary variables without declaration | @props(['title' => '' 'variant' => 'primary']) | No @props declaration in component | Medium | https://laravel.com/docs/blade#component-data-and-attributes |
| 5 | 4 | Blade Templates | Use conditional CSS classes with @class | Build class strings conditionally without ternary noise | @class directive for conditional class binding | String concatenation or nested ternaries | @class(['btn' 'btn-primary' => $primary 'btn-disabled' => $disabled]) | class="btn {{ $primary ? 'btn-primary' : '' }}" | Medium | https://laravel.com/docs/blade#conditional-classes-and-styles |
| 6 | 5 | Blade Templates | Use named slots for flexible layouts | Named slots let callers inject content into specific regions | @slot('header') and $slot for flexible component APIs | Hard-code all sub-sections inside components | <x-modal><x-slot:header>Title</x-slot>Body</x-modal> | <x-modal title="Title">Body with no slot control</x-modal> | Medium | https://laravel.com/docs/blade#slots |
| 7 | 6 | Blade Templates | Use Blade directives instead of raw PHP | Blade directives are readable and IDE-supported | @if @foreach @forelse @empty instead of <?php ?> | Raw PHP tags inside Blade templates | @forelse($items as $item) ... @empty <p>None</p> @endforelse | <?php foreach($items as $item): ?> | High | https://laravel.com/docs/blade#blade-directives |
| 8 | 7 | Blade Templates | Escape output with {{ }} | Use double curly braces for XSS-safe output | {{ }} for all user-supplied or dynamic text | {!! !!} for untrusted data | {{ $user->name }} | {!! $user->name !!} | High | https://laravel.com/docs/blade#displaying-data |
| 9 | 8 | Blade Templates | Use @vite for asset loading | Vite integration handles cache busting and HMR automatically | @vite(['resources/css/app.css' 'resources/js/app.js']) | Manual script/link tags with hardcoded paths | @vite(['resources/css/app.css' 'resources/js/app.js']) | <link href="/css/app.css?v=123"> | High | https://laravel.com/docs/vite |
| 10 | 9 | Livewire | Bind inputs with wire:model | Two-way data binding keeps component state in sync | wire:model for all form inputs managed by Livewire | Manual JavaScript listeners syncing to component | <input wire:model="email"> | <input @change="$wire.email = $event.target.value"> | High | https://livewire.laravel.com/docs/properties |
| 11 | 10 | Livewire | Use wire:model.live for real-time validation | Validate on input rather than only on submit | wire:model.live + #[Validate] for instant feedback | Only validate on form submit | <input wire:model.live="email"> with #[Validate('email')] | <input wire:model="email"> with validate() on submit only | Medium | https://livewire.laravel.com/docs/validation |
| 12 | 11 | Livewire | Use wire:click for actions | Bind UI events to component methods cleanly | wire:click for buttons and interactive elements | JavaScript fetch calls replicating Livewire actions | <button wire:click="save">Save</button> | <button onclick="fetch('/save')">Save</button> | High | https://livewire.laravel.com/docs/actions |
| 13 | 12 | Livewire | Use lifecycle hooks appropriately | mount() for init; updated() for reactive side effects | mount() for initialization updatedFoo() for property changes | Heavy logic in render() or __construct() | public function mount(): void { $this->items = Item::all(); } | public function render(): View { $this->items = Item::all(); } | Medium | https://livewire.laravel.com/docs/lifecycle-hooks |
| 14 | 13 | Livewire | Use lazy loading for heavy components | Defer render of expensive components until visible | wire:init or lazy attribute on components | Load all Livewire components on page load | <livewire:analytics-chart lazy /> | <livewire:analytics-chart /> with heavy DB queries on mount | Medium | https://livewire.laravel.com/docs/lazy |
| 15 | 14 | Livewire | Integrate Alpine.js for local UI state | Use Alpine.js for UI-only state that doesn't need server round-trips | x-data / x-show / x-transition for tooltips dropdowns | Livewire server calls for purely visual toggle state | <div x-data="{ open: false }"><button @click="open = !open"> | <button wire:click="toggleDropdown"> for a local dropdown | Medium | https://livewire.laravel.com/docs/alpine |
| 16 | 15 | Livewire | Use wire:loading for feedback | Always indicate to users when a server action is in progress | wire:loading.attr="disabled" and wire:loading elements | Provide no feedback while Livewire request is in flight | <button wire:click="save" wire:loading.attr="disabled">Save</button> | <button wire:click="save">Save</button> with no loading state | High | https://livewire.laravel.com/docs/wire-loading |
| 17 | 16 | Livewire | Handle file uploads with WithFileUploads | Livewire's trait manages chunked upload and temp storage | WithFileUploads trait + wire:model for file inputs | Manual multipart form submissions for Livewire pages | use WithFileUploads; public $photo; <input wire:model="photo" type="file"> | <form action="/upload" method="POST" enctype="multipart/form-data"> | Medium | https://livewire.laravel.com/docs/uploads |
| 18 | 17 | Inertia.js | Use Inertia page components as route endpoints | Each page is a Vue/React component rendered server-side via Inertia::render() | Inertia::render('Dashboard' ['data' => $data]) in controllers | Return JSON and fetch from JavaScript | return Inertia::render('Users/Index' ['users' => $users]); | return response()->json($users); with client-side fetch | High | https://inertiajs.com/responses |
| 19 | 18 | Inertia.js | Share global data via HandleInertiaRequests | Middleware share() provides auth user and flash to every page | Share auth/flash in HandleInertiaRequests middleware | Pass auth to every Inertia::render() call | public function share(Request $r): array { return ['auth' => ['user' => $r->user()]]; } | Inertia::render('Page' ['auth' => auth()->user()]) every controller | High | https://inertiajs.com/shared-data |
| 20 | 19 | Inertia.js | Use <Link> for client-side navigation | Inertia Link intercepts clicks for SPA-like transitions | <Link href="/dashboard"> instead of <a href> | Regular <a> tags for internal navigation | <Link href={route('dashboard')}>Dashboard</Link> | <a href="/dashboard">Dashboard</a> | High | https://inertiajs.com/links |
| 21 | 20 | Inertia.js | Use useForm for form state and submission | Inertia's useForm manages progress errors and transforms | useForm for all page-level forms, form.post() for submit | Axios/fetch for form submissions on Inertia pages | const form = useForm({ name: '' }); form.post('/users'); | axios.post('/users', { name }); | High | https://inertiajs.com/forms |
| 22 | 21 | Inertia.js | Use persistent layouts to preserve state | Wrap pages in a persistent layout so header/sidebar don't remount | layout property on page component for persistent UI | Re-render full layout on every page visit | MyPage.layout = (page) => <AppLayout>{page}</AppLayout> | No layout — full page reload feel on navigation | Medium | https://inertiajs.com/pages#persistent-layouts |
| 23 | 22 | Inertia.js | Enable SSR for public pages | Server-side rendering improves SEO and first paint | Enable Inertia SSR for marketing and public pages | Client-only rendering for all pages including public | php artisan inertia:start-ssr with @inertiaHead | No SSR on pages requiring good SEO | Medium | https://inertiajs.com/server-side-rendering |
| 24 | 23 | Styling | Set up Tailwind CSS via Vite | Use Vite + tailwindcss plugin for fast HMR and optimized builds | Install tailwindcss @tailwindcss/vite and configure vite.config.js | Laravel Mix or manual PostCSS pipeline for new projects | plugins: [tailwindcss()] in vite.config.js + @import 'tailwindcss' in app.css | Laravel Mix with require('tailwindcss') in webpack | High | https://tailwindcss.com/docs/installation/framework-guides |
| 25 | 24 | Styling | Purge unused styles via content config | Tailwind scans Blade and JS files to tree-shake unused classes | content: ['./resources/views/**/*.blade.php', './resources/js/**/*.{js,vue}'] | No content config — ship all 3MB of CSS | content: ['./resources/**/*.blade.php', './resources/**/*.js'] | content: [] | High | https://tailwindcss.com/docs/content-configuration |
| 26 | 25 | Styling | Use dark mode class strategy | class-based dark mode integrates with server-rendered preference | darkMode: 'class' with a toggle that sets class on <html> | Media query only — no user override possible | darkMode: 'class'; document.documentElement.classList.toggle('dark') | darkMode: 'media' — no programmatic control | Medium | https://tailwindcss.com/docs/dark-mode |
| 27 | 26 | Styling | Use @apply sparingly in component CSS | Extract only truly repeated multi-class patterns | @apply for BEM base classes shared across many components | @apply for every single element — defeats Tailwind's purpose | @apply flex items-center gap-2 (shared button base) | @apply text-sm for a single use | Low | https://tailwindcss.com/docs/functions-and-directives#apply |
| 28 | 27 | Styling | Configure custom design tokens in CSS | Define brand colors spacing fonts as CSS variables consumed by Tailwind | Custom @theme tokens matched to brand guidelines | Magic color hex codes scattered across Blade templates | @theme { --color-brand: oklch(0.6 0.2 250); } | bg-[#1a2b3c] inline throughout templates | Medium | https://tailwindcss.com/docs/theme |
| 29 | 28 | Components | Use anonymous Blade components for UI primitives | Blade files in resources/views/components/ auto-register as x-* components | Anonymous components for buttons alerts badges cards | Blade @includes for anything reusable | <x-badge variant="success">Active</x-badge> | @include('partials.badge' ['variant' => 'success']) | Medium | https://laravel.com/docs/blade#anonymous-components |
| 30 | 29 | Components | Use class-based components for complex logic | PHP class components can inject services and pre-process data | app/View/Components/ class when component needs PHP logic | Blade @php blocks for business logic inside templates | class AlertComponent { public function __construct(public string $type) {} } | @php $color = $type === 'error' ? 'red' : 'green'; @endphp | Medium | https://laravel.com/docs/blade#components |
| 31 | 30 | Components | Forward extra attributes with $attributes | Pass through HTML attributes like class id aria to root element | $attributes->merge() on root element of components | Ignore caller-provided HTML attributes silently | <div {{ $attributes->merge(['class' => 'btn']) }}> | <div class="btn"> — drops extra class/id from caller | High | https://laravel.com/docs/blade#component-attributes |
| 32 | 31 | Components | Separate variant logic from templates | Keep variant/size/color logic in a PHP class or helper not in Blade | Variant class or match() expression in component class | Long @if chains for variants inside Blade templates | public function classes(): string { return match($this->variant) { 'primary' => 'bg-blue-600', } } | @if($variant === 'primary') bg-blue-600 @elseif($variant === 'secondary')... | Medium | https://laravel.com/docs/blade#components |
| 33 | 32 | Components | Provide default slot content | Use {{ $slot ?? '' }} or named slot defaults so components are usable empty | Default content in slots for optional regions | Require every slot to be filled — throws errors on empty usage | {{ $icon ?? '' }} in component Blade file | {{ $icon }} — fatal if caller omits slot | Low | https://laravel.com/docs/blade#slots |
| 34 | 33 | Components | Use component namespacing for packages | Prefix third-party or module components to avoid collisions | Register custom prefix via Blade::componentNamespace() | Mix first-party and package component names with no prefix | Blade::componentNamespace('Modules\\Shop\\Views' 'shop'); <x-shop::product-card /> | <x-product-card /> colliding with first-party card | Low | https://laravel.com/docs/blade#manually-registering-components |
| 35 | 34 | Forms | Validate with Form Request classes | Move validation rules out of controllers into dedicated FormRequest classes | php artisan make:request and define rules() + authorize() | Inline validate() in controller actions | class StorePostRequest extends FormRequest { public function rules() { return ['title' => 'required|max:255']; } } | public function store(Request $r) { $r->validate(['title' => 'required']); } | High | https://laravel.com/docs/validation#form-request-validation |
| 36 | 35 | Forms | Preserve old input on validation failure | Use old() to repopulate form fields after server-side error redirect | old('field') as default value on all form inputs | Empty form fields when validation fails | <input name="email" value="{{ old('email') }}"> | <input name="email"> | High | https://laravel.com/docs/validation#repopulating-forms |
| 37 | 36 | Forms | Display validation errors with @error | Use the @error directive for inline field-level error messages | @error('field') to show per-field messages | Dump $errors->all() in one block at top of form | @error('email') <p class="text-red-500">{{ $message }}</p> @enderror | @foreach($errors->all() as $e) {{ $e }} @endforeach | Medium | https://laravel.com/docs/validation#quick-displaying-the-validation-errors |
| 38 | 37 | Forms | Use CSRF token on all forms | CSRF protection is enabled by default — include @csrf in every form | @csrf in every POST/PUT/PATCH/DELETE form | Disable VerifyCsrfToken middleware for convenience | <form method="POST">@csrf ... | <form method="POST"> without @csrf | High | https://laravel.com/docs/csrf |
| 39 | 38 | Forms | Use method spoofing for PUT/PATCH/DELETE | HTML forms only support GET/POST — use @method for REST actions | @method('PUT') inside form for update/delete routes | Route::post for all mutations including updates | <form method="POST">@csrf @method('PUT') | <form method="POST" action="/users/update"> | Medium | https://laravel.com/docs/routing#form-method-spoofing |
| 40 | 39 | Forms | Display flash messages consistently | Flash success/error in controller; read in layout with session() | session('status') in layout for global flash display | Re-query DB or pass flash from every controller individually | @if(session('success')) <div class="alert">{{ session('success') }}</div> @endif | if($user) return back()->with(['user' => $user]); | Medium | https://laravel.com/docs/session#flash-data |
| 41 | 40 | Performance | Eager load relationships to prevent N+1 | Always eager load related models used in views with with() | with() in queries before passing collections to views | Lazy-load relations inside Blade loops | User::with('posts' 'avatar')->get() | User::all() then @foreach $user->posts in Blade | High | https://laravel.com/docs/eloquent-relationships#eager-loading |
| 42 | 41 | Performance | Cache rendered Blade fragments | Use cache() helper to wrap expensive rendered partials | cache() around slow partials that change infrequently | Re-render identical content on every request | @php echo cache()->remember('sidebar' 3600 fn() => view('sidebar')->render()); @endphp | {{ view('sidebar')->render() }} on every page load | Medium | https://laravel.com/docs/cache |
| 43 | 42 | Performance | Paginate large data sets | Always paginate collections in list views | ->paginate() or ->simplePaginate() with {{ $items->links() }} | ->get() for large tables in views | User::paginate(20) with <x-pagination :links="$users" /> | User::all() passed to Blade | High | https://laravel.com/docs/pagination |
| 44 | 43 | Performance | Queue slow background tasks | Offload emails notifications and heavy processing to queues | Dispatch jobs for anything taking >200ms | Block HTTP request with slow operations | ProcessImage::dispatch($file); return back(); | Storage::put(); Mail::send(); Image::resize(); in controller | High | https://laravel.com/docs/queues |
| 45 | 44 | Performance | Use route model binding | Laravel resolves models automatically — avoids manual find() | Type-hint model in controller method | Manual User::findOrFail($id) in every method | public function show(User $user): View { return view('users.show' compact('user')); } | public function show($id) { $user = User::findOrFail($id); } | Medium | https://laravel.com/docs/routing#route-model-binding |
| 46 | 45 | Performance | Enable HTTP response caching for static content | Cache control headers for pages that rarely change | Cache-Control headers via middleware for public pages | No caching — serve every response fresh | response()->view('home')->header('Cache-Control', 'public, max-age=3600') | No cache headers on marketing pages | Medium | https://laravel.com/docs/responses#response-headers |
| 47 | 46 | Security | Escape all output in Blade | {{ }} auto-escapes HTML — never use {!! !!} on user data | {{ }} for all untrusted or dynamic content | {!! !!} for user-controlled strings | {{ $comment->body }} | {!! $comment->body !!} | High | https://laravel.com/docs/blade#displaying-data |
| 48 | 47 | Security | Protect routes with Gate and Policy | Use policies for authorization — never inline permission checks in views | @can / Gate::allows() for UI visibility; policy()->authorize() for actions | Hardcode role checks inline across templates | @can('update' $post) <a href="{{ route('posts.edit' $post) }}">Edit</a> @endcan | @if(auth()->user()->role === 'admin') <a href="/edit"> | High | https://laravel.com/docs/authorization#policies |
| 49 | 48 | Security | Validate and authorize file uploads | Check MIME type size and store outside public root | Store in storage/app/private + validate mimes and max | Store raw upload in public/ without validation | 'avatar' => ['required' 'image' 'mimes:jpg,png' 'max:2048'] | 'avatar' => 'required' with no MIME or size check | High | https://laravel.com/docs/filesystem#file-uploads |
| 50 | 49 | Security | Use signed URLs for temporary links | Generate expiring URLs for private downloads or email confirmations | URL::signedRoute() or temporarySignedRoute() | Expose sequential IDs in download URLs without auth | URL::temporarySignedRoute('file.download' now()->addMinutes(30) ['file' => $id]) | route('file.download' $id) with no expiry or signature | High | https://laravel.com/docs/urls#signed-urls |
| 51 | 50 | Security | Set a strict Content Security Policy | CSP headers prevent XSS injection of external scripts | spatie/laravel-csp or custom middleware to emit CSP header | No CSP — browser runs any injected script | Header: Content-Security-Policy: default-src 'self'; script-src 'self' | No Content-Security-Policy header on responses | Medium | https://laravel.com/docs/middleware |