Artisan CLI
php artisan listList all available commandsphp artisan help <command>Show help for a commandphp artisan serveStart local development serverphp artisan make:model Post -mfscMake model with migration, factory, seeder, controllerphp artisan make:controller PostController --resourceMake resourceful controllerphp artisan make:migration create_posts_tableCreate a new migrationphp artisan make:middleware EnsureVerifiedCreate middleware classphp artisan make:job ProcessPodcastCreate a queueable jobphp artisan make:event UserRegisteredCreate an event classphp artisan make:listener SendWelcomeEmail --event=UserRegisteredCreate a listenerphp artisan make:command SendEmailsCreate an Artisan commandphp artisan tinkerInteractive REPL in app contextphp artisan cache:clearFlush the application cachephp artisan config:cacheCache config for productionphp artisan route:listList all registered routesphp artisan queue:workProcess jobs from the queuephp artisan schedule:runRun scheduled commands (called by cron)Routing
Route::get("/posts", [PostController::class, "index"])Basic GET routeRoute::post("/posts", [PostController::class, "store"])POST routeRoute::resource("posts", PostController::class)Register all resourceful routesRoute::apiResource("posts", PostController::class)Resource routes without create/edit viewsRoute::get("/posts/{post}", ...)->name("posts.show")Named routeroute("posts.show", $post)Generate URL for named routeRoute::get("/users/{id}", ...)->where("id", "[0-9]+")Route parameter constraintRoute::middleware(["auth", "verified"])->group(fn() => ...)Apply middleware to groupRoute::prefix("admin")->group(fn() => ...)Add URL prefix to groupRoute::redirect("/old", "/new", 301)Redirect routeRoute::fallback(fn() => view("404"))Fallback for unmatched routesEloquent ORM
Post::all()Retrieve all recordsPost::find(1)Find by primary key (null if not found)Post::findOrFail(1)Find or throw ModelNotFoundExceptionPost::where("active", true)->get()Query with constraintPost::where("views", ">", 100)->orderBy("created_at", "desc")->get()Chained query constraintsPost::first()Get first matching recordPost::firstOrCreate(["email" => $email], ["name" => $name])Find or create with extra attributesPost::updateOrCreate(["slug" => $slug], $data)Update or create by attributesPost::create($data)Create and persist new model (mass assignable)$post->update($data)Update existing model$post->delete()Delete a modelPost::destroy([1, 2, 3])Delete by primary keysPost::with("user", "tags")->get()Eager load relationshipsPost::withCount("comments")->get()Load relationship countPost::paginate(15)Paginate resultsPost::chunk(200, fn($posts) => ...)Process large results in chunksPost::whereHas("comments", fn($q) => $q->where("approved", true))Filter by relationship existencePost::doesntHave("comments")->get()Filter records with no related modelsPost::latest()->limit(5)->get()Get 5 most recentPost::select("id", "title")->get()Select specific columnsPost::whereBetween("views", [100, 1000])->get()Between constraintPost::whereIn("status", ["draft", "review"])->get()Where in listPost::whereNull("published_at")->get()Where column is NULLPost::withTrashed()->find(1)Include soft-deleted recordsPost::onlyTrashed()->get()Only soft-deleted records$post->restore()Restore a soft-deleted recordPost::withSum("items", "price")->get()Load aggregate sum from relationshipPost::scopePublished($query) { return $query->where("status", "published"); }Define a local query scopePost::published()->latest()->get()Use local scope in queryRelationships
public function user(): BelongsTo { return $this->belongsTo(User::class); }Belongs to (post belongs to user)public function posts(): HasMany { return $this->hasMany(Post::class); }Has many (user has many posts)public function profile(): HasOne { return $this->hasOne(Profile::class); }Has onepublic function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); }Many-to-many with pivot tablepublic function latestPost(): HasOne { return $this->hasOne(Post::class)->latestOfMany(); }Has one of many$post->userAccess related model (auto eager-loads)$user->posts()->where("active", true)->get()Query through relationship$post->tags()->attach($tagId)Attach pivot record$post->tags()->detach($tagId)Detach pivot record$post->tags()->sync([1, 2, 3])Sync pivot IDs (detaches missing)$post->tags()->syncWithoutDetaching([4, 5])Add pivot IDs without removing existing$post->tags()->attach($tagId, ["order" => 1])Attach with extra pivot datapublic function posts(): HasMany { return $this->hasMany(Post::class)->orderBy("created_at", "desc"); }Relationship with default orderingpublic function activeComments(): HasMany { return $this->hasMany(Comment::class)->where("approved", true); }Constrained relationshippublic function country(): HasOneThrough { return $this->hasOneThrough(Country::class, User::class); }Has one throughpublic function posts(): HasManyThrough { return $this->hasManyThrough(Post::class, User::class); }Has many throughMigrations
php artisan migrateRun pending migrationsphp artisan migrate:rollbackRoll back last batchphp artisan migrate:fresh --seedDrop all tables and re-run with seeders$table->id()Auto-incrementing BIGINT primary key$table->string("title")VARCHAR(255)$table->text("body")TEXT column$table->boolean("active")->default(true)BOOLEAN with default$table->foreignId("user_id")->constrained()->cascadeOnDelete()Foreign key with constraint$table->timestamps()created_at and updated_at columns$table->softDeletes()Add deleted_at for soft deletes$table->index(["user_id", "created_at"])Composite index$table->unique("email")Unique constraint$table->unique(["user_id", "post_id"])Composite unique constraint$table->json("meta")JSON column$table->decimal("price", total: 8, places: 2)DECIMAL with precision$table->enum("status", ["draft", "published", "archived"])ENUM column$table->unsignedBigInteger("views")->default(0)Unsigned BIGINT with default$table->string("title")->after("id")Add column after another$table->string("title")->change()Modify existing column (in alter migration)$table->renameColumn("old", "new")Rename a column$table->dropColumn("old_field")Drop a columnSchema::dropIfExists("posts")Drop table in down() methodBlade Templates
{{ $variable }}Echo escaped output{!! $html !!}Echo unescaped output (use with caution)@if ($condition) ... @elseif (...) ... @else ... @endifConditional@foreach ($items as $item) ... @endforeachLoop over collection@forelse ($items as $item) ... @empty ... @endforelseLoop with empty fallback@for ($i = 0; $i < 10; $i++) ... @endforFor loop$loop->index / $loop->first / $loop->last / $loop->countLoop variable properties@extends("layouts.app")Extend a layout@section("content") ... @endsectionDefine a named section@yield("content")Render a section in the layout@include("partials.nav")Include a sub-view@component("components.alert", ["type" => "error"])Include a component with data@csrfCSRF hidden input field (required in forms)@method("PUT")Spoof HTTP method in form@auth ... @endauth / @guest ... @endguestAuth conditional blocks@can("update", $post) ... @endcanPolicy authorization check@push("scripts") <script>...</script> @endpushPush content to a stack@stack("scripts")Render a stack in layoutAuth & Authorization
Auth::user()Get the authenticated userAuth::id()Get authenticated user IDAuth::check()Check if user is authenticatedAuth::attempt(["email" => $email, "password" => $password])Attempt loginAuth::logout()Log out the userauth()->user()Helper equivalent of Auth::user()$request->user()Get user from requestGate::define("update-post", fn($user, $post) => $user->id === $post->user_id)Define a GateGate::allows("update-post", $post)Check a Gate$this->authorize("update", $post)Authorize in controller (throws 403)Request & Validation
$request->input("name")Get input value$request->input("name", "default")Get input with default$request->all()Get all input as array$request->only(["name", "email"])Get subset of input$request->except(["password"])Get all input except keys$request->has("name")Check if input key exists$request->file("avatar")Get uploaded file$request->validate(["title" => "required|max:255", "email" => "required|email|unique:users"])Inline validation (throws on failure)"required|string|min:3|max:255"Common rule string format"nullable|image|mimes:jpg,png|max:2048"File validation rulesphp artisan make:request StorePostRequestCreate a Form Request classpublic function rules(): array { return ["title" => "required"]; }Rules method in Form RequestCollections
collect([1, 2, 3])Create a collection$col->map(fn($item) => $item * 2)Transform each item$col->filter(fn($item) => $item > 1)Filter items (keeps keys)$col->reject(fn($item) => $item > 1)Inverse of filter$col->each(fn($item) => ...)Iterate without transforming$col->pluck("name")Extract a single field from each item$col->keyBy("id")Key collection by field value$col->groupBy("status")Group items by field value$col->first() / ->last()Get first or last item$col->count() / ->sum("price") / ->avg("score")Aggregate methods$col->sortBy("name") / ->sortByDesc("created_at")Sort collection$col->unique("email")Remove duplicates by field$col->values()Reset keys to 0-indexed$col->toArray() / ->toJson()Convert to array or JSONQueues & Jobs
dispatch(new ProcessPodcast($podcast))Dispatch a jobProcessPodcast::dispatch($podcast)Static dispatch helperProcessPodcast::dispatch($podcast)->delay(now()->addMinutes(10))Delay job executionProcessPodcast::dispatch($podcast)->onQueue("high")Dispatch to specific queueBus::chain([new A, new B, new C])->dispatch()Chain jobs sequentiallyBus::batch([...])->dispatch()Dispatch a batch of jobspublic $tries = 3;Max retry attempts on job classpublic $backoff = [1, 5, 10];Retry backoff in secondsphp artisan queue:work --queue=high,defaultWorker processing priority queuesphp artisan queue:failedList failed jobsphp artisan queue:retry allRetry all failed jobsCache, Config & Helpers
Cache::get("key", "default")Get cached valueCache::put("key", $value, now()->addHours(1))Store in cache with expiryCache::remember("key", 3600, fn() => DB::table("users")->get())Get or store if missingCache::forget("key")Remove a cache entryCache::flush()Clear all cacheconfig("app.name")Read config valueconfig(["app.name" => "My App"])Set config at runtimeenv("APP_KEY")Read environment variableurl("/path")Generate absolute URLasset("css/app.css")Generate URL for public assetnow() / today() / Carbon::parse("2025-01-01")Date helpersstr("hello world")->title()->slug()Fluent string helpersStr::uuid() / Str::random(32)Generate UUID or random stringHTTP Client
Http::get("https://api.example.com/users")GET requestHttp::post("https://api.example.com/users", ["name" => "Alice"])POST with JSON bodyHttp::put("https://api.example.com/users/1", $data)PUT requestHttp::delete("https://api.example.com/users/1")DELETE requestHttp::get(url)->json()Get response as arrayHttp::get(url)->body()Get raw response stringHttp::get(url)->status()Get HTTP status codeHttp::get(url)->successful()True if 2xx responseHttp::get(url)->throw()Throw exception on 4xx/5xxHttp::withToken($token)->get(url)Bearer token authHttp::withBasicAuth($user, $pass)->get(url)Basic authHttp::withHeaders(["X-Custom" => "value"])->get(url)Custom headersHttp::timeout(30)->get(url)Set request timeout in secondsHttp::retry(3, 100)->get(url)Retry up to 3 times with 100ms delayHttp::withQueryParameters(["page" => 2])->get(url)Append query string paramsHttp::asForm()->post(url, $data)Send as form-encoded instead of JSONHttp::fake(["example.com/*" => Http::response(["ok" => true], 200)])Fake responses in testsStorage & Files
Storage::put("file.txt", $contents)Write file to default diskStorage::get("file.txt")Read file contentsStorage::exists("file.txt")Check if file existsStorage::delete("file.txt")Delete a fileStorage::url("images/photo.jpg")Get public URL for fileStorage::temporaryUrl("file.txt", now()->addMinutes(5))Temporary signed URL (S3 etc.)Storage::disk("s3")->put("file.txt", $contents)Use a specific diskStorage::disk("local")->files("uploads/")List files in directoryStorage::makeDirectory("uploads/2025")Create directoryStorage::copy("old.txt", "new.txt")Copy a fileStorage::move("old.txt", "new.txt")Move / rename a fileStorage::size("file.txt")Get file size in bytes$request->file("avatar")->store("avatars")Store uploaded file, auto-named$request->file("avatar")->storeAs("avatars", "user-1.jpg")Store uploaded file with explicit name$request->file("avatar")->store("avatars", "s3")Store uploaded file on specific diskTask Scheduling
$schedule->command("emails:send")->daily()Run Artisan command daily at midnight$schedule->command("report")->dailyAt("08:00")Run daily at specific time$schedule->command("sync")->hourly()Run every hour$schedule->command("sync")->everyFiveMinutes()Run every 5 minutes$schedule->command("sync")->everyMinute()Run every minute$schedule->command("report")->weekly()->mondays()->at("09:00")Weekly on Mondays at 9am$schedule->command("backup")->monthly()Run once a month$schedule->command("sync")->cron("0 */6 * * *")Custom cron expression$schedule->job(new ProcessPodcast)->daily()Schedule a queued job$schedule->call(fn() => Cache::flush())->nightly()Schedule a closure->withoutOverlapping()Prevent running if previous instance still active->runInBackground()Run in background, not blocking other tasks->onOneServer()Only run on one server when using multiple workers->environments(["production"])Only run in specified environments->when(fn() => Feature::active("sync"))Conditional execution->sendOutputTo(storage_path("logs/schedule.log"))Write output to file* * * * * php /var/www/html/artisan schedule:run >> /dev/null 2>&1Cron entry to drive the schedulerDB Facade & Transactions
DB::table("users")->get()Query builder - get all rowsDB::table("users")->where("active", 1)->first()Query builder with constraintDB::table("users")->insert(["name" => "Alice", "email" => "a@b.com"])Insert a rowDB::table("users")->where("id", 1)->update(["name" => "Bob"])Update rowsDB::table("users")->where("id", 1)->delete()Delete rowsDB::select("SELECT * FROM users WHERE id = ?", [1])Raw SELECT queryDB::statement("ALTER TABLE users ADD COLUMN bio TEXT")Run arbitrary SQL statementDB::transaction(function () { ... })Wrap operations in a transaction (auto rollback on exception)DB::beginTransaction(); DB::commit(); DB::rollBack();Manual transaction controlDB::transaction(function () { ... }, attempts: 3)Retry transaction on deadlockDB::listen(fn($query) => logger($query->sql))Listen to all executed queriesDB::getQueryLog()Get log of executed queries (requires enableQueryLog())Logging
Log::info("User logged in", ["id" => $user->id])Log info with context arrayLog::warning("Disk space low")Log a warningLog::error("Payment failed", ["error" => $e->getMessage()])Log an error with contextLog::debug("Query result", ["rows" => $rows])Log debug - filtered out in productionLog::critical("Database unreachable")Log critical - triggers alertslogger("Something happened")Helper shorthand for Log::debug()logger()->error("Oops", ["trace" => $e])Helper with level methodLog::channel("slack")->error("Alert!")Log to a specific channelLog::stack(["daily", "slack"])->info("Deployed")Log to multiple channels at onceLog::withContext(["request_id" => $id])Add persistent context to all subsequent log entriesSanctum (API Tokens)
php artisan install:apiInstall Sanctum (Laravel 11+, adds api.php routes)$user->createToken('name')Create personal access token$user->createToken('name', ['read'])Create token with abilities$token->accessTokenThe plaintext token (only available at creation)$request->user()->tokenCan('read')Check token ability in request$user->tokens()->delete()Revoke all user tokens$user->currentAccessToken()->delete()Revoke current tokenHasApiTokens trait on User modelRequired to use Sanctum token methodsPest Testing
it('does something', fn() => ...)Functional test styletest('name', fn() => ...)Alternative syntaxexpect($val)->toBe($expected)Fluent assertion (strict equality)expect($val)->toEqual($expected)Deep equality assertionexpect($val)->toBeTrue()Assert strictly trueexpect($val)->toBeFalse()Assert strictly falseexpect($val)->toBeNull()Assert nullexpect($val)->toContain($needle)String or array containsexpect($val)->toMatchArray([...])Array subset matchexpect($val)->toHaveKey('key')Array/object has keyexpect($fn)->toThrow(Exception::class)Assert throws exceptionexpect($val)->not->toBe($x)Negate any expectationbeforeEach(fn() => ...)Setup hook before each testafterEach(fn() => ...)Teardown hook after each testdataset('name', [...])Shared dataset for parameterized testsuses(RefreshDatabase::class)Apply Laravel trait to test fileCollections Pipeline
collect([...])->map(fn($x) => $x * 2)Transform each item->filter(fn($x) => $x > 2)Keep items matching the callback->reject(fn($x) => $x > 2)Remove items matching the callback->reduce(fn($carry, $item) => $carry + $item, 0)Reduce collection to a single value->each(fn($item) => ...)Iterate without transforming->tap(fn($col) => ...)Inspect collection without interrupting chain->when($condition, fn($col) => ...)Conditional pipeline step->unless($condition, fn($col) => ...)Inverse conditional step->pipe(fn($col) => ...)Pass collection to callback, return result->flatMap(fn($x) => [...])Map then flatten one level->groupBy('key')Group items by key or callback->sortBy('key')Sort by key or callback->unique('key')Remove duplicates by key->values()Re-index keys sequentially->keyBy('id')Re-key collection by field value->pluck('name')Extract a field from each item->chunk(3)Split into chunks of N items->flatten()Flatten nested arrays->zip([1,2,3])Merge with another array element-by-element->combine(['a','b'])Use collection as keys, argument as values->mapWithKeys(fn($item) => [$item->id => $item->name])Remap with custom keys->first(fn($x) => $x > 2)First item matching callback->last(fn($x) => $x > 2)Last item matching callback->contains('key', 'value')Check if collection has item matching key/value->count()Number of items->sum('amount')Sum a field across all items->avg('score')Average of a field->min('price') / ->max('price')Min or max of a fieldHelper Methods
$query->when($cond, fn($q) => ...)Conditional method chaining on Eloquent Builder$query->tap(fn($q) => ...)Inspect query without interrupting chain$query->unless($cond, fn($q) => ...)Inverse conditional on query$query->withCount('relation')Add COUNT sub-query as attribute$query->withSum('relation', 'col')Add SUM sub-query as attribute$query->withAvg('relation', 'col')Add AVG sub-query as attribute$query->withMax('relation', 'col')Add MAX sub-query as attribute$query->withMin('relation', 'col')Add MIN sub-query as attribute$query->withExists('relation')Add EXISTS check as boolean attributeStr::of('hello world')Start a fluent string chain->upper() / ->lower() / ->title()Case conversion (fluent)->trim() / ->ltrim() / ->rtrim()Trim whitespace (fluent)->slug('-') / ->snake() / ->camel()Case/slug conversion (fluent)->contains('hello')Check string contains->startsWith('he') / ->endsWith('d')Prefix/suffix check->replace('old', 'new')Replace substring->split('/\s+/')Split string to collection->words(10, '...')Truncate to N words->limit(50, '...')Truncate to N charactersArr::get($array, 'a.b.c', $default)Dot-notation array access with defaultArr::set($array, 'a.b', $val)Dot-notation setArr::has($array, 'a.b')Dot-notation existence checkArr::only($array, ['a','b'])Keep only specified keysArr::except($array, ['a','b'])Remove specified keysArr::pluck($array, 'name')Extract field from each itemArr::flatten($array, $depth)Flatten nested array to given depthArr::wrap($val)Wrap scalar in array (no-op for arrays)