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.xphp -v→ 8.2.xnode -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 --seedaggressively in dev. Never in production. - Always wrap multi-row writes in
DB::transaction(fn () => ...)for safety. - For full-text search, use
whereLike(L11) orScoutpackage.
✅ 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_KEYset (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 + commitpublic/build/) -
storage/andbootstrap/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
- Connect Forge to your VPS (SSH bootstrap).
- Connect your git provider (GitHub).
- Create site → choose project from repo.
- Set deploy script (defaults usually fine).
- 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'sspark. - Auth (facade) —
Auth::user(),Auth::login($user),Auth::check(). - Attribute (Eloquent cast) — typed accessor/mutator:
protected function name(): Attribute { ... }. - App key —
APP_KEYin.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.phpboots 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:
@csrfdirective 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()— Likedd()but doesn't halt..env— Environment file. Loaded byvlucas/phpdotenv. Don't commit.
E
- Eloquent — Laravel's ORM.
User::find(1),User::where('email', $e)->first(). Models inapp/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 byphp 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 inconfig/logging.php. Default:storage/logs/laravel.log. Log::stack(['daily','slack'])— Combine channels.
M
- Middleware — Code that wraps requests.
web,auth,signed, custom. Defined inbootstrap/app.php(L11). - Migration — Schema-as-code.
php artisan make:migration create_posts_tablethenphp 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 $requestinto 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 topublic/storagefor 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 updateper 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 devfor development,npm run buildfor prod. - View — Blade template.
return view('posts.index', ['posts' => $posts]).
W
withInput()— Redirect back with old form data.back()->withInput().webmiddleware 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.
php artisan optimize:clear- Check
storage/logs/laravel.log - Verify
APP_ENV=production,APP_DEBUG=false, and the webroot ispublic/.
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
ResourceControllerwith predictablefailXxx()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
.appsubdomain).
Week 5 — REST API
- Add
routes/api.phpwith 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
publicdisk. - 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\Collectionsource — best-written class in Laravel. - Read 1
Eloquent\Modelmethod 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
- laravel.com/docs/11.x — read it sequentially, once.
- Laracasts — first lesson of every series is free.
- Laravel News.
- Laravel Daily YouTube.
- Spatie blog.
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.