Skip to main content
Samar State University · Center for Lifelong Learning ssu.edu.ph
SSU Lifelong Learning LMS

Laravel for Public-Sector Applications

Take-home reference

A concise printable reference of the most important commands, snippets, and gotchas covered in this training. Bring it to class or save it for after-hours review.

Back to training

DICT-ILCDB Laravel — Take-Home Reference

Everything you need at your desk, in one file. Compiled from the daily Pocket Guides + Glossary + FAQ + course map. Last regenerated: 2026-05-06



Course Map — Laravel 11 (4 days)

Visual map: where you are, where you're going, and where every artifact lives.

┌──────────────── DAY 1 — Foundations ────────────────┐
│  1.1 Welcome + Pretest          (30 min)            │
│  1.2 Intro to Laravel 11        (60 min) ──┐        │
│  1.3 Config & .env              (60 min)   │ slides │
│  1.4 Routing & Controllers      (90 min)   │ + work │
│  1.5 Blade Views & Layouts      (90 min) ──┘        │
│  📘 Day-1 Cheatsheet · 📝 Day-1 Quiz · 📤 Assignment │
└─────────────────────────────────────────────────────┘
                     â–¼
┌──────────────── DAY 2 — Data & Forms ───────────────┐
│  2.1 Database Config           (45 min)             │
│  2.2 Migrations & Seeders      (90 min)             │
│  2.3 Eloquent ORM              (120 min)            │
│  2.4 Forms, Requests & Validation (90 min)          │
└─────────────────────────────────────────────────────┘
                     â–¼
┌──────────────── DAY 3 — Auth & APIs ────────────────┐
│  3.1 Sessions & Flash          (45 min)             │
│  3.2 Auth (Breeze)             (90 min)             │
│  3.3 Gates & Policies          (90 min)             │
│  3.4 Security                  (60 min)             │
│  3.5 REST APIs (Sanctum)       (90 min)             │
└─────────────────────────────────────────────────────┘
                     â–¼
┌──────────────── DAY 4 — Ship It ────────────────────┐
│  4.1 File Storage              (75 min)             │
│  4.2 Debugging & Pest Testing  (90 min)             │
│  4.3 Deployment (Forge / VPS)  (120 min)            │
│  4.4 Capstone Brief            (45 min)             │
│  4.5 Closing + Post-test       (45 min)             │
└─────────────────────────────────────────────────────┘
                     â–¼
                 Capstone week

Dependency chain (don't skip ahead!)

Routing ─► Controllers ─► Blade ─► Validation ─► Eloquent ─► Auth ─► Policies ─► API/Sanctum ─► Storage ─► Tests ─► Deploy

Per-session lookup

Session Slides Workshop Cheat Quiz Bridge to CI4
1.1 1.1 — — — —
1.2 1.2 1.2 §1 Q1 CI4 install
1.3 1.3 1.3 §3 Q3 App.php + .env
1.4 1.4 1.4 §4 Q5–Q6 Routes.php
1.5 1.5 1.5 §5 Q7–Q9 View::extend
2.1 2.1 2.1 §1 — Database.php
2.2 2.2 2.2 §2 Q1–Q3 spark migrate
2.3 2.3 2.3 §3 Q4–Q6 CI4 Models
2.4 2.4 2.4 §4 Q7–Q10 $validation
3.1 3.1 3.1 §1 Q1–Q2 session()
3.2 3.2 3.2 §2 Q3–Q5 Roll-your-own
3.3 3.3 3.3 §3 Q6 Filter:role
3.4 3.4 3.4 §4 Q7–Q9 esc/csrf
3.5 3.5 3.5 §5 Q10–Q15 ResourceController
4.1 4.1 4.1 §1 Q1–Q4 UploadedFile
4.2 4.2 4.2 §2 Q5–Q10 PHPUnit/CIUnit
4.3 4.3 4.3 §3 Q11–Q15 spark + Apache
4.4 4.4 — — — —
4.5 4.5 — — — —

When stuck

Symptom First open
Code error troubleshooting.md
Forgot a verb quick-reference-card.md
Forgot a term glossary.md / glossary-tagalog.md
"Why is Laravel different from CI4?" codeigniter-bridge.md
Need to study after training post-graduation-track.md

4-week self-study plan

Week Focus
1 Re-do every workshop solo.
2 Build a tiny Eloquent app from scratch.
3 Add Breeze, policies, and one Pest test.
4 Deploy to a $5 VPS or Laravel Forge demo.

See post-graduation-track.md for the full plan.


Day 1 Pocket Guide — Laravel Foundations

Print this. Carry it. Sticky-tab the parts you re-use most.

§1 — Install + smoke test

composer create-project laravel/laravel myapp
cd myapp
php artisan serve   # http://127.0.0.1:8000

Verify:

  • php artisan --version → 11.x
  • php -v → 8.2.x
  • node -v → 18+

§2 — Anatomy of a Laravel project

app/Http/Controllers/      ← business logic
app/Models/                ← Eloquent classes
routes/web.php             ← browser routes (sessions/CSRF)
routes/api.php             ← stateless API routes
resources/views/           ← Blade templates
config/*.php               ← arrays of settings (do NOT edit .env via config files)
bootstrap/app.php          ← framework boot (NEW in L11; replaces old Kernel)
database/migrations/       ← schema as code
storage/                   ← logs, uploads, caches (writable)
public/                    ← webroot (index.php)

§3 — Config & .env

Rule: code reads config('mail.host'). config files read env('MAIL_HOST'). Never call env() outside of config files. (After config:cache, env() returns null everywhere else.)

.env examples:

APP_NAME="My LGU App"
APP_ENV=local
APP_DEBUG=true
APP_KEY=                            # generate with: php artisan key:generate
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=

Cache once in production:

php artisan config:cache
# After deploy, also:
php artisan route:cache
php artisan view:cache
# To bust them all:
php artisan optimize:clear

§4 — Routing

use App\Http\Controllers\PostController;

Route::get('/',                                fn () => view('welcome'));
Route::get('/posts',  [PostController::class, 'index'])->name('posts.index');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show'); // implicit binding
Route::resource('posts', PostController::class)->middleware('auth');               // 7 routes
Route::middleware('auth')->prefix('admin')->group(function () {
    Route::get('users', [AdminUserController::class, 'index']);
});
php artisan route:list -v   # show every route + middleware + name

Generate URLs:

route('posts.show', $post);     // /posts/42
url('/login');

§5 — Blade

resources/views/layouts/app.blade.php:

<!DOCTYPE html>
<html><head>
  <title>@yield('title', config('app.name'))</title>
  @vite(['resources/css/app.css', 'resources/js/app.js'])
</head><body>
  @include('partials.nav')
  <main>@yield('content')</main>
</body></html>

Child:

@extends('layouts.app')
@section('title', 'Posts')
@section('content')
  @foreach ($posts as $post)
    <h2>{{ $post->title }}</h2>      {{-- escaped --}}
  @endforeach
@endsection

Component:

{{-- resources/views/components/alert.blade.php --}}
<div class="alert alert-{{ $type }}">
  {{ $slot }}
</div>

Use:

<x-alert type="success">Saved!</x-alert>

§6 — Common Blade directives

Directive What
@if / @elseif / @else / @endif Conditional
@foreach / @endforeach Loop
@auth / @endauth Logged in only
@guest / @endguest Not logged in
@can('action', $model) / @endcan Authorization
@error('field') ... @enderror Show validation error
@csrf CSRF token input
@method('PUT') Method-spoof for forms

§7 — Common pitfalls (Day 1)

Symptom Fix
No application encryption key has been specified php artisan key:generate
Vite manifest not found npm install && npm run build (or npm run dev)
View [posts.index] not found File path is resources/views/posts/index.blade.php
Route works but POST 419 You forgot @csrf in the form
Edited .env, no effect php artisan config:clear (you cached config earlier)

§8 — Tinker is your friend

php artisan tinker
> User::count()
> User::first()
> bcrypt('test')
> Str::slug('Hello World')

§… Recipes & One-liners (Day 1)

🛣️ Routing one-liners

// Health check
Route::get('/healthz', fn () => response()->json(['ok' => true]));

// Permanent redirect
Route::redirect('/old', '/new', 301);

// Resource (7 routes auto)
Route::resource('posts', PostController::class);

// API resource (5 routes — no create/edit)
Route::apiResource('posts', PostController::class);

// View-only route (no controller)
Route::view('/about', 'about', ['title' => 'About'])->name('about');

// Multiple verbs
Route::match(['get', 'post'], '/login', LoginController::class);

// Catch-all 404 fallback
Route::fallback(fn () => view('errors.404'));

// Subdomain
Route::domain('{lgu}.app.test')->group(fn () => Route::get('/', $handler));

// Signed URL (tamper-proof)
$url = URL::signedRoute('unsubscribe', ['user' => $user->id]);

// Closure with JSON
Route::get('/now', fn () => response()->json(['t' => now()]));

âš¡ Controller patterns

// Read query with default & ceiling
$page = (int) $request->query('page', 1);
$per  = min(100, (int) $request->query('per_page', 25));

// Detect JSON request
if ($request->wantsJson()) { /* JSON */ }

// Get bearer token
$token = $request->bearerToken();

// Throw 404
abort_if(! $resident, 404);
abort_unless(auth()->user()?->isAdmin(), 403);

// Force download
return response()->download(storage_path('reports/jan.csv'));

// Streamed download (huge file, low memory)
return response()->streamDownload(fn () => /* echo $chunks */, 'big.csv');

🎨 Blade one-liners

{{ $name }}                              {{-- escaped --}}
{!! $html !!}                            {{-- raw HTML — careful! --}}
{{ Str::limit($post->body, 120) }}
{{ number_format($product->price, 2) }}
{{ $resident->created_at->diffForHumans() }}
@class(['btn', 'btn-active' => $isActive])
@style(['color: red' => $hasError])
@selected(old('status') === 'active')
@checked($user->is_admin)
@disabled(! $canSubmit)
@required($field === 'name')
@error('email') <span class="err">{{ $message }}</span> @enderror

🔧 .env recipe collection

# — development
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000
LOG_LEVEL=debug
DB_CONNECTION=sqlite

# — staging
APP_ENV=staging
APP_DEBUG=false
APP_URL=https://staging.lgu.gov.ph
LOG_LEVEL=warning
SESSION_DRIVER=database

# — production
APP_ENV=production
APP_DEBUG=false
APP_URL=https://lgu.gov.ph
LOG_LEVEL=error
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
MAIL_MAILER=ses

✅ End of Day 1 — you should be able to: install Laravel, edit routes, return Blade views, use dd() and Tinker fluently.


Day 2 Pocket Guide — Data & Forms

§1 — Database config

.env:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=secret

Test:

php artisan tinker
> DB::select('select 1+1 as sum')

§2 — Migrations & Seeders

Create a migration:

php artisan make:migration create_posts_table
public function up(): void
{
    Schema::create('posts', function (Blueprint $t) {
        $t->id();
        $t->foreignId('user_id')->constrained()->cascadeOnDelete();
        $t->string('title');
        $t->text('body');
        $t->timestamp('published_at')->nullable();
        $t->softDeletes();
        $t->timestamps();
    });
}

Common column types: id, string($name, $len = 255), text, integer, bigInteger, boolean, decimal($n, 8, 2), json, date, dateTime, timestamp, enum($n, [...]), foreignId(...)->constrained().

Migrate:

php artisan migrate
php artisan migrate:rollback
php artisan migrate:fresh --seed   # drops + re-creates + seeds (DEV ONLY)

Seeder:

// database/seeders/UserSeeder.php
public function run(): void
{
    User::factory()->count(20)->create();
    User::factory()->create(['email' => 'admin@dict.gov.ph', 'role' => 'admin']);
}

DatabaseSeeder calls them:

public function run(): void
{
    $this->call([UserSeeder::class, PostSeeder::class]);
}

Run:

php artisan db:seed
php artisan db:seed --class=UserSeeder

§3 — Eloquent essentials

php artisan make:model Post -mfrc      # model + migration + factory + resource controller
class Post extends Model
{
    use HasFactory, SoftDeletes;

    protected $fillable = ['title', 'body', 'published_at'];

    protected $casts = [
        'published_at' => 'datetime',
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

CRUD:

Post::create(['title' => 'Hi', 'body' => '...', 'user_id' => $userId]);
$post = Post::findOrFail($id);
$post->update(['title' => 'New']);
$post->delete();        // soft delete (uses SoftDeletes trait)
$post->forceDelete();
Post::onlyTrashed()->get();
Post::withTrashed()->get();

Queries:

Post::where('user_id', $u)->latest()->paginate(15);
Post::whereBetween('published_at', [$a, $b])->count();
Post::has('user')->get();
Post::whereHas('user', fn ($q) => $q->where('role', 'admin'))->get();

Eager loading (N+1 killer):

Post::with('user')->get();
Post::with(['user', 'comments.user'])->get();

§4 — Forms & validation

Blade:

<form method="POST" action="{{ route('posts.store') }}">
    @csrf
    <input name="title" value="{{ old('title') }}">
    @error('title') <div class="text-red-500">{{ $message }}</div> @enderror

    <textarea name="body">{{ old('body') }}</textarea>
    @error('body') <div class="text-red-500">{{ $message }}</div> @enderror

    <button>Save</button>
</form>

Inline validation in controller:

public function store(Request $request)
{
    $data = $request->validate([
        'title' => 'required|string|max:255',
        'body'  => 'required|min:10',
    ]);

    auth()->user()->posts()->create($data);

    return redirect()->route('posts.index')->with('success', 'Saved!');
}

Form Request (preferred for non-trivial):

php artisan make:request StorePostRequest
class StorePostRequest extends FormRequest
{
    public function authorize(): bool { return $this->user() !== null; }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'body'  => ['required', 'min:10'],
        ];
    }
}

public function store(StorePostRequest $request)
{
    auth()->user()->posts()->create($request->validated());
    return redirect()->route('posts.index')->with('success', 'Saved!');
}

Show flash:

@if (session('success'))
    <div class="alert">{{ session('success') }}</div>
@endif

§5 — Common Day-2 pitfalls

Symptom Fix
MassAssignmentException Add column to $fillable
Class 'App\Models\Post' not found Wrong namespace; regenerate with make:model
Tests use real DB! Add use RefreshDatabase + use SQLite in-memory in phpunit.xml
Date saves as string Add 'col' => 'datetime' to $casts
Slow page (N+1) Add with('relation')

§… Recipes & One-liners (Day 2)

🔍 Eloquent recipes

// Mass-assignment safe insert
Resident::create($request->validated());

// Bulk insert (no events, fastest)
Resident::insert($rows);          // 1 query

// Upsert (insert or update by unique key)
Resident::upsert($rows, uniqueBy: ['email'], update: ['phone','barangay']);

// Increment / decrement (atomic, no race)
Product::where('id', $id)->increment('stock', 5);
Product::where('id', $id)->decrement('stock', 1);

// Conditional where (clean alternative to if/else)
$residents = Resident::query()
    ->when($q, fn ($qb) => $qb->where('last_name', 'like', "%$q%"))
    ->when($barangay, fn ($qb) => $qb->where('barangay', $barangay))
    ->paginate(15);

// Eager load only specific columns (avoid SELECT *)
Resident::with('household:id,address')->get();

// Lazy load when ALREADY fetched (rare)
$resident->load('household.barangay');

// Chunk large datasets (low memory)
Resident::chunkById(500, function ($chunk) { /* process */ });

// Stream as cursor (lowest memory, no offset)
foreach (Resident::cursor() as $r) { /* process */ }

// Aggregate in one query
Resident::selectRaw('barangay, count(*) as total, avg(YEAR(now())-YEAR(birthdate)) as avg_age')
    ->groupBy('barangay')
    ->get();

🔄 Transactions

DB::transaction(function () use ($data) {
    $h = Household::create($data['household']);
    $h->residents()->createMany($data['residents']);
});

// Manual control
DB::beginTransaction();
try {
    /* … */
    DB::commit();
} catch (\Throwable $e) {
    DB::rollBack();
    throw $e;
}

🏗️ Migration patterns

// Add column (after existing)
Schema::table('users', fn ($t) => $t->string('phone', 20)->nullable()->after('email'));

// Add unique with where clause (PostgreSQL)
DB::statement('CREATE UNIQUE INDEX users_email_active_uniq ON users(email) WHERE deleted_at IS NULL');

// Composite index
$table->index(['barangay', 'purok']);

// FK with explicit names + cascade
$table->foreignId('barangay_id')->constrained()->cascadeOnUpdate()->restrictOnDelete();

// Drop column
Schema::table('users', fn ($t) => $t->dropColumn('middle_name'));

// Modify column type (requires doctrine/dbal)
Schema::table('products', fn ($t) => $t->decimal('price', 12, 2)->change());

// Soft deletes
$table->softDeletes();

// Raw SQL
DB::statement('CREATE FULLTEXT INDEX ft_name ON residents(first_name, last_name)');

✅ Validation rule library

Rule Example
required_if 'spouse'=>'required_if:civil_status,married'
required_with 'voter_id'=>'required_with:is_voter'
unique ignore self Rule::unique('users')->ignore($user->id)
exists 'category_id'=>'exists:categories,id'
regex PH mobile 'regex:/^09\d{9}$/'
between 'age'=>'integer|between:18,120'
date_format 'birthdate'=>'date_format:Y-m-d'
before_or_equal 'birthdate'=>'before_or_equal:today'
mimes 'photo'=>'mimes:jpg,png|max:2048'
dimensions 'photo'=>'dimensions:max_width=2000'
confirmed 'password'=>'confirmed' (needs password_confirmation)
array + * 'tags'=>'array', 'tags.*'=>'string|max:50'

§6 — DB quick tips

  • Use migrate:fresh --seed aggressively in dev. Never in production.
  • Always wrap multi-row writes in DB::transaction(fn () => ...) for safety.
  • For full-text search, use whereLike (L11) or Scout package.

✅ End of Day 2 — you can: design tables, run migrations both ways, write Eloquent CRUD with relations, and validate forms safely.


Day 3 Pocket Guide — Auth, Authz, Security & APIs

§1 — Sessions & flash

session(['cart_id' => 1]);
session('cart_id');                 // read
session()->forget('cart_id');
session()->flush();

// Flash (read-once)
return back()->with('success', 'Updated!');
return redirect('/login')->with('error', 'Login required.');

In Blade:

@if (session('success'))
    <div class="alert">{{ session('success') }}</div>
@endif

config/session.php — drivers: file (default), database (recommended for prod), redis, cookie, array. Default lifetime 120 min.

§2 — Auth (Breeze)

Install once:

composer require laravel/breeze --dev
php artisan breeze:install            # choose Blade option
npm install && npm run dev
php artisan migrate

You now have: /register, /login, /forgot-password, /reset-password, /verify-email, /dashboard, /profile. All views editable in resources/views/auth/ and resources/views/profile/.

Daily API:

auth()->user();         // logged-in user or null
auth()->check();        // bool
auth()->id();
Auth::login($user);
Auth::logout();

Protect routes:

Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'show']);
});

In Blade:

@auth Hello, {{ auth()->user()->name }}! @endauth
@guest <a href="{{ route('login') }}">Login</a> @endguest

Hashing:

Hash::make($plain);
Hash::check($plain, $hashed);

§3 — Authorization: Gates & Policies

Gate (closure, ad-hoc)

In App\Providers\AppServiceProvider::boot():

Gate::define('admin', fn (User $u) => $u->role === 'admin');

Use:

if (Gate::allows('admin')) { ... }
@can('admin') ... @endcan

Policy (class, full CRUD for one Model)

php artisan make:policy PostPolicy --model=Post
class PostPolicy
{
    public function update(User $u, Post $p): bool
    {
        return $u->id === $p->user_id || $u->role === 'admin';
    }
    public function delete(User $u, Post $p): bool
    {
        return $u->id === $p->user_id;
    }
}

Auto-discovered if naming matches (Post → PostPolicy).

In controller:

public function update(Request $r, Post $post)
{
    $this->authorize('update', $post);     // throws 403 if denied
    $post->update($r->validate([...]));
    return redirect()->route('posts.show', $post);
}

In Blade:

@can('update', $post) <a href="...">Edit</a> @endcan

Bypass for super-admin (in boot):

Gate::before(fn (User $u) => $u->is_super ? true : null);

§4 — Security baseline

Threat Defense (built-in) Your job
CSRF web middleware adds VerifyCsrfToken Always emit @csrf in forms
XSS Blade {{ }} auto-escapes Don't use {!! !!} on untrusted data
Mass assignment Throws if not in $fillable Always set $fillable (or $guarded)
SQL injection Eloquent uses bindings Don't concat raw SQL with input
Plain-text passwords Hash::make + bcrypt driver Never log/store password input
Brute force throttle middleware Route::middleware('throttle:5,1') on /login
HTTPS URL::forceScheme('https') Force in production + HSTS header
Sensitive files in git .env is gitignored Don't add it back

Headers (drop into a middleware or bootstrap/app.php):

->withMiddleware(function (Middleware $m) {
    $m->web(append: [
        AddSecurityHeaders::class,
    ]);
});

// app/Http/Middleware/AddSecurityHeaders.php
public function handle($request, Closure $next)
{
    $r = $next($request);
    $r->headers->set('X-Content-Type-Options', 'nosniff');
    $r->headers->set('X-Frame-Options', 'SAMEORIGIN');
    $r->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
    $r->headers->set('Permissions-Policy', 'geolocation=()');
    return $r;
}

§5 — REST API + Sanctum

Install:

composer require laravel/sanctum
php artisan migrate

config/auth.php: 'guards' => ['api' => ['driver' => 'sanctum']] (default in fresh L11).

Routes (routes/api.php):

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $r) => $r->user());
    Route::apiResource('posts', Api\PostController::class);
});

Route::post('/login', [Api\AuthController::class, 'login']);   // public, issues token

Controller:

public function login(Request $r)
{
    $r->validate(['email' => 'required|email', 'password' => 'required']);

    if (! Auth::attempt($r->only('email', 'password'))) {
        return response()->json(['message' => 'Invalid credentials'], 401);
    }

    $token = $r->user()->createToken($r->userAgent() ?? 'token')->plainTextToken;
    return response()->json(compact('token'));
}

public function logout(Request $r)
{
    $r->user()->currentAccessToken()->delete();
    return response()->json(['message' => 'Logged out']);
}

Client header: Authorization: Bearer <token>.

API resource (response shaping):

php artisan make:resource PostResource
public function toArray($r): array
{
    return [
        'id'    => $this->id,
        'title' => $this->title,
        'author'=> $this->whenLoaded('user', fn () => ['id' => $this->user->id, 'name' => $this->user->name]),
        'links' => ['self' => route('posts.show', $this)],
    ];
}

Use:

return PostResource::collection(Post::with('user')->paginate(15));

§6 — Common Day-3 pitfalls

§… Recipes & One-liners (Day 3)

🔐 Auth snippets

// Hash + verify
$hash = Hash::make($plain);
if (Hash::check($plain, $user->password)) { /* ok */ }
if (Hash::needsRehash($user->password)) { $user->update(['password' => Hash::make($plain)]); }

// Login programmatically
Auth::login($user, remember: true);
Auth::loginUsingId($id);
Auth::logout(); request()->session()->invalidate(); request()->session()->regenerateToken();

// Get current user (anywhere)
auth()->user();   $request->user();   Auth::user();

// Require auth in route
Route::middleware('auth')->group(…);
Route::middleware('auth:sanctum')->group(…);   // API

// Throttled login
Route::post('/login', LoginController::class)->middleware('throttle:5,1');

// Password reset trigger
Password::sendResetLink(['email' => $email]);

🔑 Sanctum tokens

// Issue
$token = $user->createToken('mobile-app', ['residents:read', 'residents:write']);
return ['token' => $token->plainTextToken];

// Revoke
$user->tokens()->where('id', $tokenId)->delete();
$user->tokens()->delete();    // logout all devices

// Check abilities
if ($user->tokenCan('residents:write')) { /* allow */ }

// Route gate
Route::middleware(['auth:sanctum', 'abilities:residents:write'])->…

🛡️ Security headers (middleware)

class SecureHeaders {
    public function handle($req, Closure $next) {
        $res = $next($req);
        $res->headers->set('X-Content-Type-Options', 'nosniff');
        $res->headers->set('X-Frame-Options', 'DENY');
        $res->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
        $res->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
        $res->headers->set('Content-Security-Policy',
            "default-src 'self'; img-src 'self' data:; "
           ."style-src 'self' 'unsafe-inline'; script-src 'self'");
        $res->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
        return $res;
    }
}

🌐 API response shapes

// Success
return response()->json(['data' => $row], 200);
return response()->json(['data' => $row], 201);   // created
return response()->noContent();                    // 204

// Errors
throw ValidationException::withMessages(['email' => 'Already taken.']);
abort(404, 'Resident not found.');
abort(429, 'Too many requests.');
abort(403);

// Resource collection
return ResidentResource::collection($residents);

// Paginated
return response()->json([
    'data'  => ResidentResource::collection($paginator),
    'meta'  => [
        'page' => $paginator->currentPage(),
        'per_page' => $paginator->perPage(),
        'total' => $paginator->total(),
        'last_page' => $paginator->lastPage(),
    ],
]);

📝 Audit log helper

// migration: audits(id, user_id, action, subject_type, subject_id, ip, meta json, created_at)
function audit(string $action, $subject = null, array $meta = []): void {
    DB::table('audits')->insert([
        'user_id'      => auth()->id(),
        'action'       => $action,
        'subject_type' => $subject ? get_class($subject) : null,
        'subject_id'   => $subject?->getKey(),
        'ip'           => request()->ip(),
        'meta'         => json_encode($meta),
        'created_at'   => now(),
    ]);
}

⚠️ Common gotchas (extended)

Symptom Fix
419 on every POST Forgot @csrf in form
403 even though I'm logged in Policy returned false — dd($post->user_id, auth()->id())
401 on API even with token Header is Authorization: Bearer <token>; not in body
auth()->user() is null in API Used wrong middleware: should be auth:sanctum not auth
Login session lost across browser tabs Session driver mismatched / cookie domain wrong

✅ End of Day 3 — you can: ship a fully authenticated, authorized, hardened app — both web and API.


Day 4 Pocket Guide — Storage, Tests & Deploy

§1 — File storage

Disks defined in config/filesystems.php. Defaults: local, public, s3.

Save a file:

// from a form upload
$path = $request->file('photo')->store('uploads');                 // storage/app/uploads/<hash>.jpg
$path = $request->file('photo')->store('uploads', 'public');       // storage/app/public/uploads/...
$path = $request->file('photo')->storeAs('uploads', 'avatar.jpg', 'public');

Generate public URL:

$url = Storage::url($path);                  // /storage/uploads/<hash>.jpg
asset('storage/' . $path);                   // alternative

Once per project:

php artisan storage:link

Validate uploads:

$request->validate([
    'photo'      => ['required', 'image', 'mimes:jpg,jpeg,png', 'max:2048'], // 2 MB
    'attachment' => ['required', 'file', 'mimes:pdf', 'max:10240'],          // 10 MB
]);

Read / delete:

$contents = Storage::disk('public')->get($path);
Storage::disk('public')->delete($path);
Storage::disk('public')->exists($path);

S3-compatible (R2 / Spaces):

FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=ap-southeast-1
AWS_BUCKET=my-bucket
AWS_ENDPOINT=https://nyc3.digitaloceanspaces.com
AWS_USE_PATH_STYLE_ENDPOINT=false

§2 — Pest tests

Default in Laravel 11. PHPUnit still works.

Create:

php artisan make:test PostTest --pest                # Feature
php artisan make:test PostTest --pest --unit         # Unit

Feature test:

use function Pest\Laravel\{actingAs, get, post};

it('lets a user create a post', function () {
    $user = User::factory()->create();

    actingAs($user)
        ->post('/posts', ['title' => 'Hello', 'body' => 'World!!'])
        ->assertRedirect('/posts');

    $this->assertDatabaseHas('posts', ['title' => 'Hello', 'user_id' => $user->id]);
});

it('blocks guests from creating posts', function () {
    post('/posts', ['title' => 'X', 'body' => 'Y'])->assertRedirect('/login');
});

Use RefreshDatabase in tests/Pest.php:

uses(Tests\TestCase::class, Illuminate\Foundation\Testing\RefreshDatabase::class)
    ->in('Feature');

In phpunit.xml, point at SQLite memory for fast tests:

<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>

Run:

vendor/bin/pest
vendor/bin/pest --filter PostTest
vendor/bin/pest --coverage             # if Xdebug installed

§3 — Debugging tools

Tool Install Use
dd($x) built-in Stop + dump
dump($x) built-in Dump + continue
Log::info(...) built-in Goes to storage/logs/laravel.log
Tinker built-in php artisan tinker REPL
Telescope composer require laravel/telescope --dev Web UI: requests, queries, jobs, exceptions, mail
Debugbar composer require barryvdh/laravel-debugbar --dev Bottom bar like CI4's
Ray paid app Better dump
php artisan pail built-in (L11) Live tail of logs in terminal

§4 — Pre-deploy checklist

  • APP_ENV=production
  • APP_DEBUG=false
  • APP_KEY set (random, never reused)
  • HTTPS forced + redirect
  • DB credentials in .env, file mode 600
  • composer install --no-dev --optimize-autoloader
  • php artisan migrate --force
  • php artisan optimize (config + route + view + event cache)
  • npm ci && npm run build (or build locally + commit public/build/)
  • storage/ and bootstrap/cache/ writable by web user
  • Logs rotated (logrotate)
  • Backups scheduled (DB dump + storage/)

§5 — Deployment options

Path Cost Best for
Manual VPS (Ubuntu + nginx + php-fpm) $5–10/mo LGU pilot, full control
Laravel Forge $12/mo Forge + VPS Push-to-deploy, zero-downtime
Laravel Vapor usage-based AWS Serverless, autoscaling
Render / Railway / Fly.io usage-based Simple, container-based
Shared hosting (cPanel) $2–5/mo Tiny apps; expect pain

Forge in 5 steps

  1. Connect Forge to your VPS (SSH bootstrap).
  2. Connect your git provider (GitHub).
  3. Create site → choose project from repo.
  4. Set deploy script (defaults usually fine).
  5. Click "Deploy Now" — done.

Manual deploy script

See linux-ssh-appendix.md §"Deploy script".

§6 — Post-deploy smoke test

curl -I https://app.example.com           # 200 + secure headers
curl https://app.example.com/health       # JSON OK

App routes to test:

  • /, /login, /register
  • One protected page (should redirect to login)
  • Submit a form → see flash + DB row
  • Hit one API endpoint with Postman + token

§7 — Common Day-4 pitfalls

§… Recipes & One-liners (Day 4)

📁 File handling

// Validate + store
$path = $request->file('photo')->store('residents', 'public');
$url  = Storage::disk('public')->url($path);

// Custom name
$name = uniqid('res_').'.'.$file->extension();
$path = $file->storeAs('residents', $name, 'public');

// Delete old + store new
Storage::disk('public')->delete($resident->photo);
$resident->photo = $request->file('photo')->store('residents', 'public');

// Stream a private file
return Storage::disk('private')->download($path);

// Temporary signed URL (S3)
$url = Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(15));

// Validate dimensions
'photo' => ['image', 'mimes:jpg,png,webp', 'max:2048',
            'dimensions:min_width=200,max_width=4000']

🧪 Test snippets

use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);

it('lists residents', function () {
    $user = User::factory()->create();
    Resident::factory()->count(3)->create();

    $this->actingAs($user)
        ->get('/residents')
        ->assertOk()
        ->assertSee('Residents');
});

it('rejects non-admin from delete', function () {
    $u = User::factory()->staff()->create();
    $r = Resident::factory()->create();
    $this->actingAs($u)->delete("/residents/{$r->id}")->assertForbidden();
});

it('uploads a photo', function () {
    Storage::fake('public');
    $u = User::factory()->create();
    $file = UploadedFile::fake()->image('a.jpg', 800, 600)->size(500);
    $this->actingAs($u)->post('/residents', […, 'photo' => $file])->assertRedirect();
    Storage::disk('public')->assertExists('residents/'.$file->hashName());
});

// DB assertions
$this->assertDatabaseHas('residents', ['email' => 'x@y.test']);
$this->assertDatabaseCount('residents', 35);
$this->assertSoftDeleted($resident);

// HTTP API tests
$this->withHeaders(['Authorization' => 'Bearer '.$token])
     ->postJson('/api/residents', $data)
     ->assertCreated()
     ->assertJsonStructure(['data' => ['id','name']]);

🚀 Deployment commands

# Production install
composer install --no-dev --optimize-autoloader --no-interaction

# Cache everything
php artisan optimize
# = config:cache + route:cache + view:cache + event:cache

# Clear when something looks stale
php artisan optimize:clear

# Migrations on deploy (production-safe flag)
php artisan migrate --force

# Storage symlink
php artisan storage:link

# Permissions (Linux)
sudo chown -R www-data:www-data storage bootstrap/cache
sudo find storage -type d -exec chmod 775 {} \;
sudo find storage -type f -exec chmod 664 {} \;

# Queue worker (systemd or supervisord)
php artisan queue:work --sleep=3 --tries=3 --max-time=3600

# Schedule (cron)
* * * * * cd /var/www/lgu && php artisan schedule:run >> /dev/null 2>&1

# Backup with spatie/laravel-backup
php artisan backup:run --only-db

# DB dump nightly cron
mysqldump --single-transaction -u $U -p$P $D | gzip > /backups/lgu-$(date +%F).sql.gz
find /backups -name 'lgu-*.sql.gz' -mtime +30 -delete

🌐 Healthcheck route

Route::get('/healthz', function () {
    $checks = [
        'db'      => fn () => DB::connection()->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION),
        'cache'   => fn () => Cache::set('hc', '1', 5) && Cache::get('hc') === '1',
        'storage' => fn () => is_writable(storage_path('framework')),
    ];
    $results = [];
    $ok = true;
    foreach ($checks as $name => $fn) {
        try { $results[$name] = $fn(); } catch (\Throwable $e) { $results[$name] = false; $ok = false; }
    }
    return response()->json([
        'status'  => $ok ? 'ok' : 'degraded',
        'checks'  => $results,
        'version' => config('app.version', 'unknown'),
        'time'    => now()->toIso8601String(),
    ], $ok ? 200 : 503);
});

⚠️ Common gotchas (extended)

Symptom Fix
500 white screen on prod APP_DEBUG=true temporarily, OR check storage/logs/laravel.log
optimize cached old config Run php artisan optimize:clear after every deploy that changes config
Vite build error on server (low RAM) Build locally, commit public/build, skip npm run build on server
storage:link says "already exists" but symlink missing rm public/storage then re-run
Tests pass locally, fail on CI composer install on CI uses different lock; commit composer.lock

✅ End of Day 4 — you can ship: validated uploads, a green test suite, and a live HTTPS deployment.


Quick Reference Card — Laravel 11

Print this. Tape it on your monitor. The 80 commands you'll actually use.

Artisan basics

php artisan serve                         # dev server
php artisan tinker                        # REPL
php artisan list                          # all commands
php artisan route:list -v                 # show routes
php artisan optimize                      # cache config+routes+views
php artisan optimize:clear                # clear all caches
php artisan storage:link                  # public/storage symlink

Make: generators

php artisan make:model Post -mfrc         # model + migration + factory + resource controller
php artisan make:controller PostController --resource
php artisan make:request StorePostRequest
php artisan make:policy PostPolicy --model=Post
php artisan make:middleware EnsureAdmin
php artisan make:migration create_posts_table
php artisan make:factory PostFactory --model=Post
php artisan make:seeder PostSeeder
php artisan make:test PostTest --pest

Migrations / DB

php artisan migrate
php artisan migrate:rollback
php artisan migrate:fresh --seed          # WIPE + re-migrate + seed
php artisan db:seed
php artisan db:seed --class=PostSeeder
php artisan migrate --pretend             # dry-run SQL

Routes

Route::get('/users', [UserController::class, 'index'])->name('users.index');
Route::resource('posts', PostController::class);
Route::middleware('auth')->prefix('admin')->group(fn () => Route::get(...));
Route::apiResource('posts', PostApiController::class);
Route::fallback(fn () => abort(404));
route('users.show', $user);                   // generate URL

Eloquent — daily

User::all();
User::find(1);
User::findOrFail(1);                          // 404 if missing
User::where('active', 1)->get();
User::where('email', $e)->first();
User::create(['name' => 'X', 'email' => 'x@y']);    // mass-assign (needs $fillable)
$user->update(['name' => 'Y']);
$user->delete();
$user->posts;                                  // relation (lazy)
User::with('posts')->get();                    // eager (no N+1)
User::factory()->count(10)->create();

Validation — inline

$data = $request->validate([
    'email'    => ['required', 'email', Rule::unique('users')],
    'password' => 'required|min:8|confirmed',
    'photo'    => 'nullable|image|max:2048',
]);

Blade — daily

{{ $user->name }}                  {{-- escaped --}}
{!! $html !!}                      {{-- raw --}}
@if ($x) ... @elseif ($y) ... @else ... @endif
@foreach ($users as $user) ... @endforeach
@auth ... @endauth
@guest ... @endguest
@can('update', $post) ... @endcan
@error('email') <span>{{ $message }}</span> @enderror
@csrf @method('PUT')
@include('partials.flash')
@extends('layouts.app')
@section('content') ... @endsection
@yield('content')
<x-alert type="success">Saved!</x-alert>

Auth (Breeze)

composer require laravel/breeze --dev
php artisan breeze:install
npm install && npm run dev
php artisan migrate
Auth::user();   auth()->user();
Auth::check();  auth()->check();
Auth::login($user);
Auth::logout();
Hash::make($plain);  Hash::check($plain, $hash);

Authorization

Gate::define('update-post', fn ($u, $p) => $u->id === $p->user_id);
Gate::allows('update-post', $post);
$this->authorize('update', $post);             // throws 403 if denied
@can('update', $post) <a href="...">Edit</a> @endcan

API + Sanctum

composer require laravel/sanctum
php artisan migrate
Route::middleware('auth:sanctum')->get('/user', fn (Request $r) => $r->user());
$token = $user->createToken('mobile')->plainTextToken;     // give to client
$user->tokens()->delete();                                  // revoke all

File storage

$path = $request->file('photo')->store('posts');                 // local
$path = $request->file('photo')->store('posts', 'public');       // public
$url  = Storage::url($path);                                     // /storage/posts/...
Storage::disk('public')->delete($path);

HTTP responses

return response()->json([...], 201);
return redirect('/login');
return back()->withInput()->withErrors(['email' => 'bad']);
return redirect()->route('posts.show', $post)->with('success', 'Saved!');
abort(404);   abort(403, 'Forbidden');

Logging & debug

Log::info('msg', ['ctx' => $x]);
Log::error($exception);
dd($x);            // dump-and-die
dump($x);          // dump-and-keep-going
ray($x);           // if Spatie Ray installed

Pest tests

vendor/bin/pest
vendor/bin/pest --filter=PostTest
it('creates a post', function () {
    $user = User::factory()->create();
    $resp = $this->actingAs($user)->post('/posts', ['title' => 'Hi', 'body' => 'B']);
    $resp->assertRedirect('/posts');
    $this->assertDatabaseHas('posts', ['title' => 'Hi']);
});

Composer / npm

composer install         composer update         composer require x/y
composer dump-autoload   composer require x/y --dev
npm install              npm run dev             npm run build

Deployment one-liner (post-pull)

composer install --no-dev --optimize-autoloader \
  && php artisan migrate --force \
  && php artisan optimize \
  && npm ci && npm run build

Common .env keys

APP_NAME=...           APP_ENV=local|production
APP_KEY=base64:...     APP_DEBUG=true|false
APP_URL=http://localhost
DB_CONNECTION=mysql    DB_HOST=127.0.0.1
DB_DATABASE=...        DB_USERNAME=...     DB_PASSWORD=...
SESSION_DRIVER=database|file|redis
QUEUE_CONNECTION=database|redis|sync
MAIL_MAILER=log|smtp

Folder cheat

app/Http/Controllers/    routes/web.php          routes/api.php
app/Models/              database/migrations/    database/seeders/
resources/views/         resources/css/          resources/js/
storage/logs/            storage/app/            bootstrap/app.php
config/*.php             tests/Feature/          tests/Unit/

Glossary — Laravel 11

~80 Laravel terms, A→Z, with one-line explanations and an example where useful.

A

  • Artisan — Laravel's CLI tool. php artisan <command>. Equivalent to CI4's spark.
  • Auth (facade) — Auth::user(), Auth::login($user), Auth::check().
  • Attribute (Eloquent cast) — typed accessor/mutator: protected function name(): Attribute { ... }.
  • App key — APP_KEY in .env; used to encrypt sessions, cookies. Keep secret.

B

  • Blade — Laravel's template engine. Files end in .blade.php. Syntax: {{ $x }}, @if, @foreach, @extends.
  • Breeze — Official starter kit for auth (login, register, reset, verify). composer require laravel/breeze --dev && php artisan breeze:install.
  • Bootstrap (kernel) — bootstrap/app.php boots the framework (Laravel 11 unified file).

C

  • Cache — Cache::put('k', $v, 60) / Cache::get('k'). Drivers: file, redis, database.
  • Collection — Eloquent results are Illuminate\Support\Collection — chainable like arrays on steroids.
  • Container (service) — Dependency-injection registry. app()->make(X::class).
  • Controller — Class in app/Http/Controllers/; receives Request, returns Response.
  • CSRF — Built-in middleware. Blade form: @csrf directive emits the hidden token.
  • Composer — PHP package manager. composer install, composer require x/y.

D

  • DB facade — Raw query escape hatch. DB::select('SELECT * FROM users WHERE id = ?', [$id]).
  • Dependency injection — Type-hint a class in a constructor or method, the container provides it.
  • dd() — dump-and-die — prints variable + halts. Daily debugging tool.
  • dump() — Like dd() but doesn't halt.
  • .env — Environment file. Loaded by vlucas/phpdotenv. Don't commit.

E

  • Eloquent — Laravel's ORM. User::find(1), User::where('email', $e)->first(). Models in app/Models/.
  • Eager loading — User::with('posts')->get() — avoids N+1.
  • Event — Pub/sub. event(new UserRegistered($user)). Listeners auto-discover.

F

  • Facade — Static-like accessor for a service-container binding (Auth, Cache, DB, Route, Schema).
  • Factory — Generates fake model rows for tests/seeders. User::factory()->count(10)->create().
  • Form Request — Custom validation class, php artisan make:request StoreUserRequest.
  • fillable — Allow-list of mass-assignable attributes on a model.

G

  • Gate — Closure-based authorization. Gate::define('update-post', fn($u, $p) => $u->id === $p->user_id).
  • guard — Auth provider/driver pair (web, api).

H

  • Helper functions — view(), route(), url(), auth(), request(), session(), now(), str(), collect().
  • HTTP client — Http::get('url'), Http::withToken($t)->post('url', $body).

I

  • Illuminate — The internal Laravel namespace.
  • Inertia / Livewire — Optional UI layers (out of scope for this course; mentioned only).

J

  • Jobs / Queues — Background work. dispatch(new SendEmail($user)), processed by php artisan queue:work.

K

  • Kernel — Old name for the HTTP/console request entry point. In Laravel 11, merged into bootstrap/app.php.

L

  • Logging — Log::info('msg'). Channels in config/logging.php. Default: storage/logs/laravel.log.
  • Log::stack(['daily','slack']) — Combine channels.

M

  • Middleware — Code that wraps requests. web, auth, signed, custom. Defined in bootstrap/app.php (L11).
  • Migration — Schema-as-code. php artisan make:migration create_posts_table then php artisan migrate.
  • Model — Eloquent class. php artisan make:model Post -mfrc (also makes migration/factory/resource/controller).
  • make: commands — Family of generators: model, controller, request, policy, factory, seeder, migration, etc.

N

  • Notification — Multi-channel messages: mail, database, slack. $user->notify(new InvoicePaid($invoice)).

O

  • ORM — Object-Relational Mapper. Eloquent.

P

  • Pest — Modern test runner; reads like English. Default in Laravel 11. PHPUnit still ships under the hood.
  • Pint — Opinionated PHP code style fixer. vendor/bin/pint.
  • Policy — Class-based authorization for a model. php artisan make:policy PostPolicy --model=Post.
  • Provider — Service registration class. app/Providers/AppServiceProvider.php.

Q

  • Queue — See Jobs.

R

  • Route — Route::get('/users', UserController::class . '@index') or [UserController::class, 'index']. File: routes/web.php, routes/api.php.
  • Resource controller — RESTful 7 actions. Route::resource('posts', PostController::class).
  • route('name') — Generate URL by route name. Route::get(...)->name('users.show').
  • Request — Inject Request $request into a controller; access input.

S

  • Sanctum — Token-based auth for SPAs and mobile/API. composer require laravel/sanctum.
  • Scout — Search driver (Algolia, Meilisearch). Out of scope.
  • Seeder — Fake-data populator. php artisan db:seed.
  • Session — session(['key' => 'value']), session('key'). Drivers: file, database, redis.
  • storage — Folder. App-writable area. Symlink to public/storage for public files.

T

  • Tinker — REPL. php artisan tinker → run any PHP / Eloquent code interactively.
  • Telescope — Debug-toolbar-on-steroids (logs, queries, requests). Optional install.

U

  • url() — Helper to generate URLs.
  • Update strategy — composer update per minor version; major Laravel bumps follow Upgrade Guide.

V

  • Validation — $request->validate(['email' => 'required|email|unique:users']). Or use Form Requests.
  • Vite — Default asset bundler. npm install && npm run dev for development, npm run build for prod.
  • View — Blade template. return view('posts.index', ['posts' => $posts]).

W

  • withInput() — Redirect back with old form data. back()->withInput().
  • web middleware group — Default group: session, CSRF, errors, etc.

X / Y / Z

  • XSS — Cross-site scripting. {{ $x }} auto-escapes. {!! $x !!} does NOT — only use for trusted HTML.
  • Yield — @yield('content') — Blade placeholder filled by @section('content') in a child view.
  • Zero-downtime deploy — Forge / Envoyer / Deployer atomic releases.

Common abbreviations

  • L11 — Laravel 11.
  • AC — Authorization (gate/policy) Check.
  • FR — Form Request.
  • MFC — Model + Factory + Controller (make:model -mfc).

Glossary — Filipino / Tagalog Companion (Laravel)

English Tagalog Halimbawa
Framework Balangkas o tulay para mas mabilis gumawa ng app. Laravel, CodeIgniter.
Artisan CLI tool ng Laravel. php artisan migrate.
Eloquent ORM ng Laravel. Class na nakikipag-usap sa table. User::find(1).
Blade Template engine — .blade.php files. {{ $name }}.
Migration Bersyon kontrol ng database schema. php artisan make:migration create_posts_table.
Seeder Pang-puno ng pekeng datos. User::factory(10)->create().
Factory Recipe para gumawa ng pekeng row. Para sa testing at seeding.
Route Mapa: URL → controller. Route::get('/users', ...).
Controller Class na sumasagot sa request. app/Http/Controllers/UserController.php.
Middleware Code na tumatakbo bago/pagkatapos ng request. auth, verified, custom.
Form Request Class para sa validation rules. php artisan make:request StoreUserRequest.
Gate Authorization closure. Gate::define(...).
Policy Authorization class para sa isang Model. php artisan make:policy PostPolicy --model=Post.
Sanctum Token-based auth para sa API at SPA. $user->createToken('mobile')->plainTextToken.
Breeze Auth scaffolding (login/register). php artisan breeze:install.
Pest Test runner; mas magaan basahin kaysa PHPUnit. vendor/bin/pest.
Pint Code-style fixer. vendor/bin/pint.
Tinker Interactive PHP/Laravel REPL. php artisan tinker.
Vite Asset bundler para sa CSS/JS. npm run dev, npm run build.
.env Setting file (lokal lang, hindi i-commit). APP_KEY, DB_*, etc.
APP_KEY Encryption key. Wag ipakita kahit kanino. Generate via php artisan key:generate.
storage/ Folder kung saan pwedeng magsulat ang app. Logs, uploads, cache.
public/storage Symlink para publicly accessible. php artisan storage:link.
CSRF Token na pumipigil sa request galing ibang website. @csrf sa Blade form.
XSS Pag-pasok ng <script> ng attacker. Defense: {{ }}. {!! ... !!} ay delikado.
fillable Allowed columns sa mass assignment. Gabay para hindi nahuhulaan ang is_admin=1.
guarded Kabaliktaran ng fillable (block these instead). Gamitin ang isa lang.
Eager loading Pag-load ng relations sa isang query (kontra N+1). User::with('posts')->get().
N+1 Bug kung saan tumatakbo ang isang query per row. Pinakamadalas na perf bug. Solusyon: with().
Helper Function na pwede tawagin kahit saan. route(), view(), now().
Facade Singleton accessor (mukhang static). Cache::get('x'), DB::table(...).
Service Container Talaan ng mga "kayang gawin ng app". Pinagmumulan ng dependency injection.
Service Provider Class na nagre-register ng kakayahan sa container. app/Providers/AppServiceProvider.php.
Job / Queue Trabahong tatakbo sa background. dispatch(new SendEmail(...)).
Notification Multi-channel message (email, DB, Slack). $user->notify(new InvoicePaid(...)).
Production Live na server. APP_ENV=production, walang dd().
Local / Development Sa laptop mo, may toolbar at errors. APP_ENV=local, APP_DEBUG=true.
Forge DICT-style: pinapadali ang deploy ng VPS. Pinangangalagaan ang Nginx, PHP, queues.
Tinker Para sumubok ng code agad. User::count().
Log file Talaarawan ng app — unang tingnan kapag may error. storage/logs/laravel.log.

Mga sikat na sabi:

  • "Kapag nalilito ka, buksan mo ang php artisan tinker."
  • "{{ }} ay safe; {!! !!} ay delikado. Tandaan ang dalawa."
  • "with() ang kaibigan mo laban sa N+1."
  • "Wag mag-deploy kapag Biyernes ng hapon."

Tip: pares-pares mo ito sa glossary.md (English).


FAQ — Laravel 11 Training

40 questions trainees actually ask. Search Ctrl+F.

Setup & install

Q1. Composer is downloading slowly. Try the China/PH-friendly mirror or just composer global config repo.packagist composer https://repo.packagist.org. If on a venue wifi, use the cached USB.

Q2. php artisan says "command not found". You're not in the project root. cd into the folder that has artisan (no extension).

Q3. vite complains "missing public/build". Run npm install && npm run build once. For dev, npm run dev runs Vite in watch mode.

Q4. Why Laragon over XAMPP for Windows? Multi-PHP, easier .env handling, built-in Composer + Node. XAMPP works but you'll fight it.

Q5. Can I use SQLite instead of MySQL during the course? Yes — just DB_CONNECTION=sqlite and create database/database.sqlite. Faster for tests.

Routing & controllers

Q6. web.php vs api.php? Same idea, different middleware groups. web adds session+CSRF, api doesn't (stateless).

Q7. Where do I put the controller? app/Http/Controllers/. Generate with php artisan make:controller PostController --resource.

Q8. How do I see all defined routes? php artisan route:list (add -v for middleware/names).

Q9. Route caching — should I? In production, yes: php artisan route:cache. Run route:clear after every deploy that changes routes.

Blade

Q10. {{ $x }} vs {!! $x !!}? {{ }} HTML-escapes (safe). {!! !!} injects raw — only for HTML you fully control.

Q11. How do I include a partial? @include('partials.flash') — looks for resources/views/partials/flash.blade.php.

Q12. Components vs partials? Components (<x-alert>) are reusable, accept attributes/slots. Partials are dumb includes.

Q13. @csrf not enough — POST still fails. Make sure your form <method> is POST and the route is registered as Route::post(...). Also: clear browser cache.

Eloquent

Q14. User::find(1) returns null but the row exists. Wrong primary key column or wrong table. Check protected $table and $primaryKey.

Q15. MassAssignmentException! Add the column to protected $fillable = [...] on the model.

Q16. N+1 query problem — what is it? Looping over rows + accessing relations triggers a query per row. Fix with with('relation').

Q17. How do I see the SQL Eloquent runs? Two ways: dd($query->toSql()), or in Tinker: DB::enableQueryLog(); /* run */; DB::getQueryLog();. Or use Telescope/Debugbar.

Q18. Soft deletes? Add use SoftDeletes; to the model + $table->softDeletes() to migration. delete() flags deleted_at; forceDelete() removes for real.

Validation

Q19. Validation error — page just refreshes, no error. You forgot to @error('field') in Blade or pass errors with ->withErrors($v).

Q20. Where to put complex validation? Form Request: php artisan make:request StorePostRequest. Move rules + authorization there.

Q21. Custom rule message? Pass messages() method in Form Request, or array in validate(rules, messages).

Auth

Q22. Why install Breeze? Saves you 3 hours. Ships login, register, password reset, email verification — all editable Blade views.

Q23. Breeze vs Jetstream vs Fortify?

  • Breeze: Blade + Tailwind, simple. We use this.
  • Jetstream: Livewire/Inertia + 2FA + teams.
  • Fortify: backend only — bring your own UI.

Q24. How do I add a role column? Migration: $table->string('role')->default('user'). Add to $fillable. Use a Gate or Policy.

Q25. Auth stops working after closing the browser. Default session lifetime is 120 min. Change in config/session.php → 'lifetime' => 1440.

Authorization (Gates / Policies)

Q26. Gate vs Policy — when? Gate = closure, ad-hoc. Policy = class, all CRUD methods for one Model. Use Policy for resources.

Q27. $this->authorize('update', $post) throws 403 when I'm logged in. Either the policy method returned false, or you forgot to register the policy in AuthServiceProvider. (In L11, auto-discovery exists if naming convention is followed.)

Security

Q28. Default Laravel security — what do I get for free? CSRF, XSS auto-escape in Blade, mass-assignment protection (with $fillable), parameterized queries (Eloquent), HTTPS-only cookies (when configured).

Q29. How do I force HTTPS? In App\Providers\AppServiceProvider::boot: URL::forceScheme('https') when app()->isProduction(). Plus a redirect in nginx/Apache.

Q30. Rate-limiting routes? Route::middleware('throttle:60,1')->group(...) — 60 requests/minute. Or define in bootstrap/app.php.

API / Sanctum

Q31. Sanctum vs Passport? Sanctum: simple, SPAs + mobile, opaque tokens. Passport: full OAuth2. We use Sanctum.

Q32. Where to send the token? HTTP header Authorization: Bearer <token>.

Q33. CORS error from React/Vue frontend? Configure config/cors.php (allowed_origins, allowed_methods). Restart npm run dev.

File storage

Q34. Where do uploads go? By default, storage/app/. To make public, use Storage::disk('public')->put(...) and run php artisan storage:link.

Q35. How to validate uploaded image? 'photo' => 'required|image|mimes:jpg,png|max:2048'.

Testing (Pest)

Q36. Pest or PHPUnit? Pest reads better. PHPUnit is still under the hood. Use Pest for new tests.

Q37. Tests use my real DB! Add use RefreshDatabase trait, point DB_DATABASE (in phpunit.xml) to a separate test DB or use :memory: SQLite.

Deployment

Q38. Forge — paid? Yes ($12/month). For free, deploy by SSH yourself; Forge just automates it.

Q39. After deploy, page is blank.

  1. php artisan optimize:clear
  2. Check storage/logs/laravel.log
  3. Verify APP_ENV=production, APP_DEBUG=false, and the webroot is public/.

Q40. npm run build failed on the server (low RAM). Build assets locally and git add -f public/build, deploy with build artifacts. OR NODE_OPTIONS=--max_old_space_size=512.

Add to this file as questions come up. It's the cheapest curriculum upgrade.


CodeIgniter 4 → Laravel 11 Bridge

If you finished the CI4 course, this maps everything you know to its Laravel equivalent. Skim this; you'll feel at home in 30 minutes.

Mental model

CI4 idea Laravel idea
app/, system/, public/ app/, bootstrap/, public/
app/Config/Routes.php routes/web.php, routes/api.php
app/Controllers/ app/Http/Controllers/
app/Models/ (your models) app/Models/ (Eloquent)
app/Views/ resources/views/ (Blade)
app/Filters/ app/Http/Middleware/
writable/ storage/
.env .env (same idea, different keys)

CLI commands

CI4 (spark) Laravel (artisan)
php spark serve php artisan serve
php spark routes php artisan route:list
php spark make:controller X php artisan make:controller X
php spark make:model X php artisan make:model X -mfc
php spark make:migration X php artisan make:migration create_x_table
php spark migrate php artisan migrate
php spark migrate:rollback php artisan migrate:rollback
php spark db:seed UserSeeder php artisan db:seed --class=UserSeeder
php spark cache:clear php artisan cache:clear
php spark phpunit vendor/bin/pest (or phpunit)
php spark help php artisan list

Routing

// CI4
$routes->get('users/(:num)', 'UserController::show/$1');
$routes->group('admin', ['filter' => 'auth'], function ($r) { ... });
// Laravel
Route::get('users/{id}', [UserController::class, 'show']);
Route::middleware('auth')->prefix('admin')->group(function () { ... });

Controllers

// CI4
public function show($id) {
    $user = model(UserModel::class)->find($id);
    return view('users/show', ['user' => $user]);
}
// Laravel
public function show(User $user) {            // route-model binding!
    return view('users.show', compact('user'));
}

Models — biggest mind-shift

CI4 Laravel
model(UserModel::class)->find(1) User::find(1) (static facade)
$model->where('active', 1)->findAll() User::where('active', 1)->get()
$model->insert($data) User::create($data)
$model->update($id, $data) User::where('id', $id)->update($data) or $user->update($data)
$allowedFields = [...] protected $fillable = [...]
$validationRules = [...] (in Model) Form Request class (separate)
useSoftDeletes = true use SoftDeletes; trait
Manual relations (with() you wrote) Built-in hasMany/belongsTo + with() eager-load

Views

// CI4
<?= $this->extend('layouts/default') ?>
<?= $this->section('content') ?>
<h1><?= esc($user->name) ?></h1>
<?= $this->endSection() ?>
{{-- Laravel Blade --}}
@extends('layouts.default')
@section('content')
<h1>{{ $user->name }}</h1>          {{-- auto-escaped --}}
@endsection

Validation

// CI4 (in controller)
$rules = ['email' => 'required|valid_email'];
if (! $this->validate($rules)) {
    return redirect()->back()->withInput()
        ->with('errors', $this->validator->getErrors());
}
// Laravel inline
$validated = $request->validate(['email' => 'required|email']);

// Or Form Request: php artisan make:request StoreUserRequest
public function rules(): array
{
    return ['email' => ['required', 'email', Rule::unique('users')]];
}

Sessions & flash

CI4 Laravel
session()->set('user_id', 1) session(['user_id' => 1])
session('user_id') session('user_id') ✅ same
redirect()->to('/')->with('success', 'OK') redirect('/')->with('success', 'OK')
session()->getFlashdata('success') session('success') (read-once via Blade @if(session('success')))

Auth

CI4 Laravel
Roll your own (password_hash, password_verify, custom filter) composer require laravel/breeze && php artisan breeze:install — done in 5 min
Custom AuthFilter auth middleware (built-in)
session('user_id') checks auth()->user(), Auth::check()

RBAC

CI4 Laravel
Filter:role:admin (custom) Gate::define(...) or php artisan make:policy PostPolicy --model=Post
Manual role check in controller $this->authorize('update', $post)

REST APIs

CI4 Laravel
ResourceController Route::resource('posts', PostController::class)
Custom Bearer filter auth:sanctum middleware after composer require laravel/sanctum
$this->response->setJSON([...]) return response()->json([...]) (or just return $array — auto-JSON)
failNotFound() abort(404)

File uploads

// CI4
$file = $this->request->getFile('photo');
$file->move(WRITEPATH . 'uploads', $file->getRandomName());
// Laravel
$path = $request->file('photo')->store('uploads');               // storage/app/uploads
$path = $request->file('photo')->store('uploads', 'public');     // storage/app/public/uploads

Testing

CI4 Laravel
vendor/bin/phpunit vendor/bin/pest (or PHPUnit)
CIUnitTestCase + DatabaseTestTrait Tests\TestCase + RefreshDatabase
Manual fixture inserts User::factory()->create()

Debugging

CI4 Laravel
Debug toolbar (env=development) Telescope (optional) / Debugbar (community)
log_message('error', ...) Log::error(...)
print_r + die dd($x) (built-in)

Deployment

Same shape, different commands.

CI4 Laravel
composer install --no-dev --optimize-autoloader Same. Plus: php artisan config:cache && route:cache && view:cache
php spark migrate php artisan migrate --force
Webroot → public/ Same.
writable/ perms 775 storage/ and bootstrap/cache/ perms 775
CI_ENVIRONMENT=production in .env APP_ENV=production, APP_DEBUG=false

Things Laravel has that CI4 doesn't (out of the box)

  • Eloquent relationships (hasMany, belongsToMany, eager loading).
  • Queues / Jobs (dispatch(...), workers).
  • Notifications (email + DB + Slack from one class).
  • Gates / Policies (first-class authorization).
  • Sanctum / Passport (token auth packages).
  • Tinker (REPL).
  • Pest / PHPUnit + Factories (User::factory()->count(10)->create()).
  • Vite + Breeze + Tailwind preconfigured.
  • Forge / Vapor (paid hosting deploy automation).
  • Larger ecosystem (1000s of packages, official Filament / Nova admin panels).

Things CI4 has that Laravel doesn't (out of the box)

  • Smaller footprint (~1 MB vs ~30 MB vendor).
  • Lower memory / CPU per request — better on cheap VPS.
  • Spark debug toolbar enabled by default.
  • Built-in ResourceController with predictable failXxx() helpers.

When to pick which (in PH gov context)

Situation Pick
Tiny LGU app, 1-2 admins, $5 VPS CI4
App that may grow (jobs, queues, multi-role) Laravel
Already have CI4 codebase Stay CI4, port slowly.
Hiring market matters (freelancers/contractors) Laravel (deeper PH talent pool)
Audit-friendly minimal stack CI4

Don't pick a religion. Pick a fit.


Linux + SSH Appendix (Laravel deployment)

Same fundamentals as the CI4 appendix. Copy/refresh as needed.

SSH basics

ssh-keygen -t ed25519 -C "you@dict.gov.ph"
ssh-copy-id deploy@vps.example.com
ssh deploy@vps.example.com

~/.ssh/config:

Host laravel-vps
  HostName vps.example.com
  User deploy
  IdentityFile ~/.ssh/id_ed25519

Then: ssh laravel-vps.

Server bootstrap (Ubuntu 22.04)

sudo apt update && sudo apt -y upgrade
sudo apt install -y nginx mysql-server git unzip curl

# PHP 8.2
sudo add-apt-repository ppa:ondrej/php -y && sudo apt update
sudo apt install -y php8.2 php8.2-{cli,fpm,mysql,mbstring,xml,curl,bcmath,zip,intl,gd,tokenizer}

# Composer
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Node 20
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt install -y nodejs

Database

sudo mysql_secure_installation
sudo mysql -e "CREATE DATABASE app; CREATE USER 'app'@'localhost' IDENTIFIED BY 'STRONG'; GRANT ALL ON app.* TO 'app'@'localhost'; FLUSH PRIVILEGES;"

Nginx vhost (Laravel)

server {
    listen 80;
    server_name app.example.com;
    root /var/www/app/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
    }

    location ~ /\.(?!well-known).* { deny all; }
}
sudo nginx -t && sudo systemctl reload nginx

Permissions

sudo chown -R www-data:www-data storage bootstrap/cache
sudo find storage bootstrap/cache -type d -exec chmod 775 {} \;
sudo find storage bootstrap/cache -type f -exec chmod 664 {} \;

HTTPS — Let's Encrypt

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com

Auto-renew is a systemd timer; verify with sudo systemctl list-timers | grep certbot.

Deploy script (deploy.sh)

#!/usr/bin/env bash
set -euo pipefail
cd /var/www/app
git pull --ff-only
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize
npm ci && npm run build
sudo systemctl reload php8.2-fpm
echo "Deployed at $(date)"

Queue worker (systemd unit)

/etc/systemd/system/laravel-worker.service:

[Unit]
Description=Laravel queue worker
After=network.target

[Service]
User=www-data
Group=www-data
Restart=always
ExecStart=/usr/bin/php /var/www/app/artisan queue:work --sleep=3 --tries=3 --max-time=3600

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now laravel-worker
sudo systemctl status laravel-worker

Scheduler (one cron line)

* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

Logs to check first

File What
storage/logs/laravel.log App errors
/var/log/nginx/error.log 502 / FPM upstream issues
/var/log/php8.2-fpm.log FPM crashes
journalctl -u laravel-worker -n 200 Queue worker
tail -f storage/logs/*.log Live app log

Post-Graduation Track — Laravel

12-week guided self-study after the 4-day course. Free.

Week 1 — Re-do everything

  • Re-do every workshop solo, fresh project.
  • Re-take Day 1–4 quizzes, target 90%+.

Week 2 — Build "TaskList"

  • laravel new tasklist
  • Tasks: title, body, due_date, completed_at.
  • Eloquent + Blade + Breeze auth + simple gate (own tasks only).
  • Publish to GitHub.

Week 3 — Add Pest tests

  • Feature test: registered user can CRUD their own tasks.
  • Authorization test: cannot edit someone else's task.
  • Aim for ≥ 70 % coverage.

Week 4 — Deploy

  • $5 VPS (DigitalOcean / Linode / Hetzner) OR Forge demo.
  • HTTPS via Let's Encrypt.
  • Domain (or .app subdomain).

Week 5 — REST API

  • Add routes/api.php with full CRUD using Sanctum.
  • Build Postman collection.
  • Write API integration tests.

Week 6 — Queues + Mail

  • Notification: send email when a task becomes overdue.
  • Schedule job daily at 8 AM.
  • Move email send to a queued job.

Week 7 — File uploads + Storage

  • Allow attaching files to tasks.
  • Use public disk.
  • Validate: image|max:2 MB.
  • Optional: switch to S3-compatible storage (R2/Spaces).

Week 8 — Refactor

  • Move all DB calls into a Service class.
  • Add Form Requests for every store/update.
  • Add policies for every Model.
  • Run Pint: vendor/bin/pint.

Week 9 — Frontend polish (optional but encouraged)

  • Tailwind dark mode.
  • Toast for flash messages.
  • Loading states.

Week 10 — Public release

  • Write a README with screenshots.
  • Deploy a demo with seed data.
  • Announce on a community channel (Slack/Discord/FB group).

Week 11 — Read the source

  • Read Illuminate\Support\Collection source — best-written class in Laravel.
  • Read 1 Eloquent\Model method end-to-end (e.g. find).

Week 12 — Contribute

  • File 1 issue or 1 PR (docs typo counts!) to a Laravel package you use.
  • Help a beginner on r/laravel or Stack Overflow.

Recommended free resources

Recommended paid (worth it once you're earning)

  • Laracasts subscription ($15/mo).
  • Build A SaaS App with Laravel by Andrew Schmelyun.
  • Battle-Ready Laravel by Ash Allen.

Cookies & privacy. We use essential cookies for authentication and (optionally) privacy-friendly, anonymous analytics. Read our notice.