18 KiB
18 KiB
| 1 | No | Category | Guideline | Description | Do | Don't | Code Good | Code Bad | Severity | Docs URL | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2 | 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 | |
| 3 | 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 | |
| 4 | 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 | |
| 5 | 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 | |
| 6 | 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 | |
| 7 | 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 | |
| 8 | 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 | |
| 9 | 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 | |
| 10 | 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 | |
| 11 | 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 | |
| 12 | 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 | |
| 13 | 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 | |
| 14 | 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 |
| 15 | 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 | |
| 16 | 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 | |
| 17 | 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 | |
| 18 | 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 | |
| 19 | 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 | |
| 20 | 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 | |
| 21 | 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 | |
| 22 | 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 | |
| 23 | 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 | |
| 24 | 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 | |
| 25 | 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 | |
| 26 | 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 | |
| 27 | 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 |
| 28 | 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 | |
| 29 | 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 | |
| 30 | 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 | |
| 31 | 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 |