PHP & Laravel Magic: Mastering Web Development
Статистика🔮 Dive into professional web development with PHP and Laravel! Discover: 🛠️ Efficient snippets 📚 Tips and guides 💡 Advanced solutions 🌟 Code optimization methods Subscribe to enhance your skills and master best practices! 🚀
- Последний пост
- 14 июл.
- Последнее чтение
- 13 авг.
- Постов за неделю
- 0
- Всего постов
- 25
- Тип
- открытый
- Язык
- английский
- В каталоге с
- 13 авг.
- 1/24сутки в ленте
- —
- 1/48двое суток
- —
- 1/72трое суток
- —
Оценка по просмотрам недавних постов: пост набирает почти всё за первые сутки.
Посты
🕰️ The silent Laravel scheduler bug — finally fixed in 13.17 You write clean, obvious code: $schedule->command('send-morning-report') ->dailyAt('09:00') ->between('09:00', '10:00') ->timezone('America/New_York'); "Send the morning report between 9 and 10 New York time." Reads one way only. Except it didn't work. The report went out overnight. The reason — between() and unlessBetween() locked the timezone when defined, not when the filter ran. If timezone() came later in the chain, Laravel silently used UTC. New York — 5 hours off. Tokyo — 9. No warning. No error. Job just fires at the wrong time. The only order that worked before: $schedule->command('...') ->timezone('America/New_York') // strictly BEFORE between() ->between('09:00', '10:00'); Method order = logic. Laravel doesn't work that way anywhere else — hundreds of methods chain in any order. That's why the bug slipped past so many codebases. Fixed in 13.17. Timezone is now resolved when the filter evaluates. Both orders behave the same. Do this now → Open routes/console.php or app/Console/Kernel.php → Grep for every ->between(...) and ->unlessBetween(...) → Make sure ->timezone(...) comes before them → Not on 13.17 yet? Reorder the chain, or upgrade One of those bugs where production stays quiet for months. Then the business asks: "why is the digest going out at 3 AM?"
⚙️ `php artisan dev` in Laravel 13.16 — the end of the `composer dev` era Every Laravel dev knows the ritual: open 4 terminals. → php artisan serve → php artisan queue:work → php artisan pail → npm run dev Someone came up with composer dev — a single command running everything via concurrently. Better. But the config lives in composer.json, no typing, adding your own process means editing JSON by hand. 13.16 moves it into PHP. Register in a service provider: use Illuminate\Foundation\Console\DevCommands; DevCommands::artisan('reverb:start')->orange(); DevCommands::register('stripe listen --forward-to ' . config('app.url'))->green(); Then — one command for the whole stack: php artisan dev Everything spins up: server, queue, logs, vite, reverb, stripe listen — whatever else you added. Each process in its own color in the terminal. What you get over `composer dev` → Config lives in PHP, not JSON — you can use config(), env(), environment conditions → Auto-detects your package manager — bun.lock present → runs via bun, pnpm-lock.yaml → pnpm. No more patching npm scripts per teammate → Packages in vendor/ can't shove themselves into your dev stack — explicit opt-in only → Named processes with colors — readable output, not a firehose of interleaved logs One terminal. One color per process. One config in PHP. You can retire composer dev.
🪤 Nested `whereHas` — the slowest construct in Eloquent If the first two traps were about silent bugs, this one just kills the database. You need users with orders for a specific product: User::whereHas('orders', function ($q) { $q->whereHas('items', function ($q2) { $q2->where('product_id', 5); }); })->get(); Clean. Declarative. Sails through code review. Here's what actually hits the DB: SELECT * FROM users WHERE EXISTS ( SELECT * FROM orders WHERE user_id = users.id AND EXISTS ( SELECT * FROM items WHERE order_id = orders.id AND product_id = 5 ) ) Two nested EXISTS. For every users row, MySQL walks orders. For every order — walks items. N × M × K in the worst case. Real numbers: 100k users, a million orders, 10M items — query holds the DB for 30+ seconds. MySQL's planner barely optimizes nested correlated subqueries — it just brute-forces them. Fix — rewrite as JOIN with DISTINCT: User::join('orders', 'orders.user_id', '=', 'users.id') ->join('items', 'items.order_id', '=', 'orders.id') ->where('items.product_id', 5) ->select('users.*') ->distinct() ->get(); Uglier. 10–30× faster. Same dataset — seconds instead of tens of seconds. Rule of thumb → Single whereHas + proper indexes — fine → Nesting 2+ levels deep — rewrite to JOIN → orWhereHas inside — don't even think, go straight to JOIN Series closed. Eloquent is a great tool — as long as you remember there's SQL underneath.
🛡️ 5 lines in `AppServiceProvider` — and `key:generate` can't nuke your production anymore Every Laravel dev has felt this once: hit Enter on a command, and the same second realize you were in the wrong terminal. For most commands, it's a 5-second scare. For php artisan key:generate on production — it's a disaster in under a second. APP_KEY is regenerated. Then, in escalating order: → Every session invalidated — every logged-in user kicked out → Encrypted cookies become unreadable → Signed URLs go invalid → Encrypted DB columns (`encrypted` cast, `Crypt::encrypt`) turn into garbage — no way to decrypt them back Got a .env backup? That's your only lifeline. Without it — the data is gone for good. Artisan::prohibit() has been in Laravel for a while, but it didn't cover these two commands. 13.12.0 fixes that: key:generate and cache:clear can now be locked down. The fix: use Illuminate\Foundation\Console\KeyGenerateCommand; public function boot(): void { if ($this->app->isProduction()) { KeyGenerateCommand::prohibit(); } } Any accidental php artisan key:generate on production now fails with an error and does nothing. Five lines. Cheaper than restoring from a backup.
🪤 `doesntHave` + SoftDeletes — the silent bug you only catch in production You've written this code: User::doesntHave('orders')->get(); "Give me users with no orders" — for re-engagement emails, analytics, cleanup scripts. Looks unambiguous. But if Order uses SoftDeletes, Laravel silently adds orders.deleted_at IS NULL to the inner query: SELECT * FROM users WHERE NOT EXISTS ( SELECT * FROM orders WHERE user_id = users.id AND orders.deleted_at IS NULL ) So here's what actually happens: a user whose orders are all soft-deleted gets pulled in as "has no orders". From the database's view — he's a customer. From Eloquent's view — he isn't. Where this hurts most → A "come back, you haven't ordered yet" email goes to loyal customers whose history you trimmed yourself → A "delete inactive users with no orders" script wipes active ones → Conversion analytics shows phantom numbers The worst part — your dev DB won't show it. No soft-deleted rows there. Tests stay green. The bug lives for months until someone notices by hand. Fix — explicitly include trashed: User::whereDoesntHave('orders', fn($q) => $q->withTrashed() )->get(); Now the logic is honest: a user with no orders at all, including deleted ones. Trap 2 of 3. The last one — cascading EXISTS — is the worst.
💸 One flag in Laravel 13.10 — and your workers stop burning money A typical queue worker runs 24/7. Actually works 3% of the time. The other 97% — idle on your AWS/GCP bill. The old option was --stop-when-empty: queue empty → worker exits. Looks perfect on paper. In practice — jobs arrive in waves with 2–3 second gaps. Worker keeps starting and stopping. Each boot is ~300ms of Laravel bootstrap. Net savings — zero. Sometimes worse. New in 13.10.0: php artisan queue:work --stop-when-empty-for=60 The worker exits only if 60 full seconds pass without a single job. One job arrives — timer resets, worker keeps going. Where it actually pays off → Serverless workers (Cloud Run, Fargate, ECS scale-to-zero) — spin up on spike, shut down clean, stop paying for idle → Autoscaling — keep-alive for 60–120 seconds of silence, then the instance dies → Cron-based workers — boot every N minutes, drain the queue, exit Small flag. Big difference on your infra bill.
🪤 whereHas + withCount: doubling your DB work for nothing Everyone writes this. You probably too: User::whereHas('orders', fn($q) => $q->where('status', 'paid')) ->withCount(['orders' => fn($q) => $q->where('status', 'paid')]) ->get(); "Give me users with paid orders and the count." Reads clean. Here's what the DB actually sees: SELECT *, (SELECT COUNT(*) FROM orders WHERE user_id = users.id AND status = 'paid') as orders_count FROM users WHERE EXISTS ( SELECT * FROM orders WHERE user_id = users.id AND status = 'paid' ) Same subquery. Twice. Same filters. On 50k users and a couple million orders that's +300–500ms per request. On a list endpoint hit hundreds of times a minute — it shows up on your dashboards. Fix — drop `whereHas`. Count first, filter via HAVING: User::withCount(['orders as paid_orders_count' => fn($q) => $q->where('status', 'paid')]) ->having('paid_orders_count', '>', 0) ->get(); One subquery. Same result. Bonus — the count is already there in $user->paid_orders_count. No need to duplicate it in your Blade. This is trap 1 of 3 with whereHas. Next up — soft deletes and cascading EXISTS.
🔐 Laravel 13.9.0 fixes a UX wound you stopped noticing The flow nobody talks about: → user clicks "generate password" in 1Password → your form rejects it: needs a symbol, too short, whatever → user gives up and types qwerty123 Laravel 13.9.0 ships Password::toPasswordRulesString() — it converts your validation rules into the HTML passwordrules attribute (an Apple spec). Safari, 1Password and Bitwarden read it and generate a password that passes your validation on the first try. Wire it up once Declare the policy in AppServiceProvider: Password::defaults(fn () => Password::min(12)->max(64)->mixedCase()->numbers()->symbols() ); Drop it into the input: <input type="password" autocomplete="new-password" passwordrules="{{ Password::defaults()->toPasswordRulesString() }}" /> Output: minlength: 12; maxlength: 64; required: lower; required: upper; required: digit; required: special; Why it matters → One source of truth — server and browser share the policy → Generated passwords pass validation on the first try → Fewer abandoned signups → No JS copies, no duplicated regex on the frontend ⚡ Heads-up: uncompromised() is skipped — it has no equivalent in the spec. Two lines of code. Significantly less user friction.
📎 Laravel PDF 2.6 — Attach PDFs to Emails Without Disk Before, generating PDFs for email looked like this: → Create PDF → Save to disk → Attach to email → Delete file Spatie laravel-pdf 2.6.0 solves this elegantly. Before: $pdf = Pdf::view('pdfs.invoice', ['invoice' => $invoice]) ->save(storage_path('temp/invoice.pdf')); $mail->attach(storage_path('temp/invoice.pdf')); unlink(storage_path('temp/invoice.pdf')); After: $pdf = Pdf::view('pdfs.invoice', ['invoice' => $invoice]) ->name('invoice.pdf'); $mail->attach($pdf); In Notification: public function toMail($notifiable): MailMessage { $pdf = Pdf::view('pdfs.invoice', ['invoice' => $this->invoice]) ->name('invoice.pdf'); return (new MailMessage) ->subject('Your invoice') ->attach($pdf); } In Mailable via attachments(): public function attachments(): array { return [ Pdf::view('pdfs.invoice', ['invoice' => $this->invoice]) ->name('invoice.pdf'), ]; } How it works: → PdfBuilder implements Attachable contract → File generated in memory → MIME type application/pdf added automatically → .pdf extension appended if missing No temp files. No cleanup.
🔍 UniqueConstraintViolationException Now Has Details — Laravel 13.2 Catching UniqueConstraintViolationException, but then what? Before — just a database message. Parse it yourself. Now — structured data. Before: try { User::create(['email' => 'taken@example.com']); } catch (UniqueConstraintViolationException $e) { // Which field? Parse $e->getMessage()... } After: try { User::create(['email' => 'taken@example.com']); } catch (UniqueConstraintViolationException $e) { $e->columns; // ['email'] $e->index; // 'users_email_unique' } What's available per driver: → PostgreSQL: columns ✅ index ✅ → SQLite: columns ✅ index ❌ → MySQL: columns ❌ index ✅ → SQL Server: columns ❌ index ✅ Use cases: → Human-friendly messages: "Email already taken" → Debugging composite unique constraints → Logging conflicts → Automatic duplicate handling No more parsing strings with regex.
🔥 releaseOnSignal — No More Stuck Locks in Laravel 13.2 withoutOverlapping() prevents parallel task execution. But there's a catch: if the process gets killed — the lock stays. The scenario: → Kubernetes does a rolling deploy → Scheduler receives SIGTERM → Task didn't finish in time → Lock remains in cache → Task won't run until TTL expires The fix in Laravel 13.2: Artisan::command('reports:generate', function () { // ... })->withoutOverlapping(releaseOnSignal: true); How it works: → Catches SIGTERM, SIGINT, SIGQUIT → Releases lock before process dies → Task runs immediately on restart Where it matters: → Kubernetes / Docker → Laravel Cloud / Vapor → Any managed infrastructure → Blue-green and rolling deploys Requires pcntl extension (available almost everywhere). One line. Zero stuck tasks.
📐 JSON:API Resources — Native Spec Support in Laravel 13 Building APIs that follow the JSON:API spec? Before: spatie/laravel-json-api or manual boilerplate. Now: built-in. Generate: php artisan make:resource PostResource --json-api Define attributes and relationships: class PostResource extends JsonApiResource { public $attributes = [ 'title', 'body', 'created_at', ]; public $relationships = [ 'author', 'comments', ]; } Return from controller: return $post->toResource(); Response follows the spec automatically: { "data": { "id": "1", "type": "posts", "attributes": { "title": "Hello World" } } } What works out of the box: → Sparse fieldsets: ?fields[posts]=title → Includes: ?include=author,comments → Links and meta → Content-Type: application/vnd.api+json → Nested relationships via dot notation For parsing incoming query params — Spatie Query Builder is still the best choice. But serialization is now native.
🔥 PreventRequestForgery — Smarter CSRF in Laravel 13 VerifyCsrfToken middleware is now PreventRequestForgery. But it's not just a rename. What changed: Origin-aware verification — checks the Origin header alongside tokens. Why it matters: → Tokens can be stolen via XSS → Origin can't be forged — browser controls it → Double protection: token + origin How it works: // Before — token only VerifyCsrfToken::class // Now — token + origin check PreventRequestForgery::class What to do: → Nothing — backward compatible → Old tokens still work → Middleware applies automatically Where it's critical: → Fintech, payment forms → Any data mutations via POST → APIs with cookie-based auth Free upgrade. Stronger protection.
🔥 Laravel AI SDK — First-Party AI Toolkit in Laravel 13 No more third-party packages for basic AI tasks. Laravel 13 ships with an official SDK. Unified API. Any provider. Pure Laravel-way. Image generation: use Laravel\Ai\Image; $image = Image::of('Donut on a kitchen table')->generate(); Text-to-speech: use Laravel\Ai\Audio; $audio = Audio::of('Welcome to Laravel.')->generate(); Embeddings from string: use Illuminate\Support\Str; $embeddings = Str::of('Best wineries in Napa Valley')->toEmbeddings(); AI agents with tool calling: use App\Ai\Agents\SalesCoach; $response = SalesCoach::make()->prompt('Analyze this conversation...'); What's inside: → Text, images, audio, embeddings → Agents with tool calling → Vector store integrations → Provider-agnostic (OpenAI, Anthropic, etc.) Before: openai-php/client + tons of boilerplate. Now: one line, Laravel-way.
🔥 Cache::touch() — Extend TTL Without Fetching the Value Before Laravel 13, extending cache TTL looked like this: $data = Cache::get('report:monthly'); if ($data !== null) { Cache::put('report:monthly', $data, now()->addHours(6)); } The problems: → Fetches data over the network (could be 10MB JSON) → Loads into memory just to re-store it → Race condition between get and put Laravel 13 adds Cache::touch(): Cache::touch('report:monthly', now()->addHours(6)); One line. No fetch. No race condition. Calling conventions: → Seconds: Cache::touch('key', 3600) → Carbon: Cache::touch('key', now()->addDay()) → Forever: Cache::touch('key', null) Use cases: → Extend user sessions on activity → Keep-alive for expensive reports → Any "still valid? extend it" pattern Works on all drivers: Redis, Memcached, Database, File. Drops: March 17, 2026
🔥 JSON Decoding Flags in HTTP Client — More Control Over API Responses Response::json() now accepts custom decoding flags. Small change? Not if you work with real-world APIs. The problem: PHP silently corrupts large integers (ID > 2^53), and json_decode() swallows errors by default. The fix: $response = Http::get('https://api.example.com/data'); // Large integers as strings $data = $response->json(flags: JSON_BIGINT_AS_STRING); // Strict mode + big integers $data = $response->json( flags: JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR ); When this saves you: → Payment APIs with long transaction_id → Snowflake IDs (Discord, Twitter) → Any external API where numbers > PHP_INT_MAX → Debugging — JSON_THROW_ON_ERROR instead of silent null Before: manually call json_decode() on $response->body(). Now: one line. PR #58379
🔥 Arr::onlyValues() & exceptValues() — Value-Based Filtering in Laravel 12.46 Arr::only() and Arr::except() filter by keys. But what if you need to filter by values? Before: array_filter() + in_array() + ugly callbacks. Now: two new helpers. Keep only matching values: $roles = ['admin', 'editor', 'viewer', 'guest']; Arr::onlyValues($roles, ['admin', 'editor']); // [0 => 'admin', 1 => 'editor'] Remove unwanted values: $statuses = ['pending', 'completed', 'failed', 'shipped']; Arr::exceptValues($statuses, ['failed', 'completed']); // [0 => 'pending', 3 => 'shipped'] Strict mode for mixed types: $mixed = [1, '1', 2, '2', 3]; Arr::onlyValues($mixed, [1, 2, 3], strict: true); // [0 => 1, 2 => 2, 4 => 3] — integers only Use cases: → Filtering statuses and roles → Input sanitization via allowlists/denylists → Config arrays and enum-like filtering Small addition. Cleaner code.
🔥 Property Hooks in Laravel 12: RIP getFullNameAttribute() For 10 years, we wrote accessors and prayed our IDE would understand them. PHP 8.4 changed everything. Laravel 12 embraced it. Now the logic lives directly on the property: public string $fullName { get => "{$this->first_name} {$this->last_name}"; set(string $value) => [ $this->first_name, $this->last_name ] = explode(' ', $value, 2); } Why it matters → Native type safety (no more PHPDoc hacks) → Autocomplete just works → Smaller, cleaner models → Less magic = fewer surprises Best use cases → Virtual attributes (full_name, formatted_price) → Data normalization on write → Computed fields without caching overhead Attribute::make() isn’t going anywhere. But for simple transformations — Property Hooks are cleaner and faster. Bottom line It’s 2026. Time to write models like native PHP — not Laravel 5.
🆕 Laravel 12.43: Control Model Visibility Without Loops Ever needed to hide or expose attributes for a whole Eloquent collection — and hated looping through models? Laravel 12.43 adds mergeHidden() and mergeVisible(). $users->mergeHidden(['password', 'remember_token']); Need to expose fields instead? $users->mergeVisible(['email', 'created_at']); These changes are temporary and affect only serialization — perfect right before returning an API response. ✅ One call for the entire collection ✅ No model mutation ✅ Cleaner controllers & resources Bottom line: Less boilerplate, more intent. mergeHidden() and mergeVisible() are small helpers you’ll reuse everywhere.
🚀 Laravel 12.40: New Time Helpers You’ll Actually Want to Use Working with cache timers, token expiration, or scheduling logic? Laravel 12.40 introduces brand-new expressive time helpers from Taylor Otwell — and they make your time-based code way cleaner. ⏱️ More Expressive Carbon Durations use Illuminate\Support\Carbon; // Add 1 year and 5 days Carbon::now()->plus(years: 1, days: 5); // Subtract 4 weeks Carbon::now()->minus(weeks: 4); Named parameters instantly show intent — no more guessing what the “5” means. 🧩 Interval Helpers (CarbonInterval) use function Illuminate\Support\{ seconds, minutes, hours, days, years }; seconds(5); minutes(5); hours(1); days(30); years(1); Perfect for inline usage: use function Illuminate\Support\minutes; Cache::put('username', 'Alex', minutes(5)); Invitation::create([ 'expires_at' => now()->plus(weeks: 1), ]); 💡 Bottom line Your time-related code becomes cleaner, more readable, and far more expressive. Tiny helpers — massive upgrade to maintainability.