AI 资讯
Migrating Laravel to Symfony Without Rewriting Your Domain
Book: Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools Me: xgabriel.com | GitHub You've been handed the ticket everyone dreads: move the app off Laravel and onto Symfony. Maybe the team standardized on Symfony. Maybe a Messenger-and-Doctrine shop acquired you. Maybe someone decided Eloquent's global scopes had cost enough Friday nights. The estimate comes back in quarters. Someone quotes the phrase "big rewrite" and the room goes quiet, because everyone remembers the last big rewrite. Here is the question that decides whether this is a quarter or a year: how much of your business logic imports the framework? If your PlaceOrder logic reaches into request() , calls Order::create() on an Eloquent model, and opens a transaction with DB::transaction() , then the framework is the application and you are rewriting the application. If your business rules sit in plain PHP classes that never heard of Laravel, then the migration is an adapter swap, and adapter swaps ship one route at a time. What the framework actually owns Strip a typical Laravel app down and you find three categories of code. The first is your domain and use cases: the rules about what an order is, what placing one means, when it can be cancelled. This is the part the business pays for. It should not import a framework at all. The second is glue that translates the outside world into calls on that logic: controllers, form requests, Eloquent models, queue jobs, service providers. This is framework-specific by definition. The third is infrastructure your code talks to through interfaces: the database, the queue, the mailer, the payment gateway. A framework migration only touches the second category. The trouble is that most Laravel apps let the first and
AI 资讯
Eloquent Events vs Domain Events: Why the Framework Hook Isn't Enough
Book: Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools Me: xgabriel.com | GitHub You wire a listener to Eloquent's saved event on the Order model. When an order is saved, send the confirmation email. It works in the demo. Then a support ticket lands: a customer got two confirmation emails for one purchase, and another got a refund receipt for an order that was never refunded. You dig in. The double email came from a background job that touched updated_at on the order to bump a cache. The bogus receipt came from an admin editing the shipping address, which saved the model, which fired saved , which ran a listener that assumed "saved means the order changed state." None of that was the customer's intent. All of it was persistence. That's the whole problem in one sentence. saved tells you a row hit the database. It does not tell you what happened in your business. What Eloquent events actually fire on Eloquent dispatches creating , created , updating , updated , saving , saved , deleting , deleted , and a few more. Every one of them is tied to a persistence operation on a single model. They fire because you called save() , update() , create() , or delete() , not because a business rule was satisfied. Here is the shape most teams start with: <?php namespace App\Models ; use Illuminate\Database\Eloquent\Model ; class Order extends Model { protected static function booted (): void { static :: updated ( function ( Order $order ): void { // "the order changed, email the customer" OrderMailer :: confirmation ( $order ); }); } } The listener assumes updated means "something the customer cares about changed." It doesn't. updated fires for any dirty column: a cached counter, a nightly touch() , an admin fixing a typo in t
AI 资讯
Laravel Nightwatch: First-Party APM and What It Actually Replaces
Book: Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools Me: xgabriel.com | GitHub You already run three tools that half-cover this job. Pulse gives you a live wall on a local route. Datadog runs an agent and prices on host and usage volume, so the bill scales with your infrastructure. Sentry catches the exceptions after they already hurt someone. And none of them can tell you the one thing you actually asked: the checkout request that took 900ms at 14:03 dispatched a job, that job ran a query, and the query is what timed out. Laravel Nightwatch reached general availability in 2025 as the framework's own APM, aimed straight at that gap. It is worth knowing exactly what it captures, what it charges, and where its knowledge of your app stops and yours begins. What Nightwatch actually is Two moving parts. A Composer package inside your app, and a separate agent process that ships the data. composer require laravel/nightwatch The package writes events to a local socket. The agent listens on 127.0.0.1:2407 , batches what it receives, and sends it to Nightwatch's cloud. Because the agent runs outside your request cycle, the request thread is not blocked waiting on a network call to a telemetry backend. Laravel puts the added cost at under 3ms per request ; take that as a starting figure and measure your own before you trust it. # environment token per app + environment NIGHTWATCH_TOKEN = your-env-token # start the collector (keep it running under a # process monitor: Forge daemon, Vapor, supervisor) php artisan nightwatch:agent # confirm it is alive and receiving php artisan nightwatch:status One detail that bites people: the agent has to be running for anything to arrive. In local dev you start it by hand. In product
AI 资讯
Laravel Precognition: Live Validation That Reuses Your Backend Rules
Book: Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools Me: xgabriel.com | GitHub You have two copies of the same rules. One lives in a StoreUserRequest on the server. The other lives in a Zod schema, or a Yup object, or a pile of required attributes, on the front end. They started identical. Then someone bumped the password minimum from 8 to 12 on the backend and forgot the client. Now the form says the password is fine, the user clicks submit, and a 422 bounces back with an error the UI never predicted. That drift is the whole reason live client-side validation is annoying to maintain. You are keeping two rulesets in sync by hand, and the sync breaks quietly. Laravel Precognition removes the second copy. The front end asks the server "would this pass?" before the user submits, and the server answers using the exact same validation rules the real request will run. What a precognitive request actually is A precognitive request is a normal HTTP request to your real endpoint, tagged with a Precognition: true header. Laravel sees the header, runs the route's middleware and validation, and then stops before your controller does any real work. It never writes a row. It never sends an email. It runs the rules and returns the verdict. Success comes back as 204 No Content with a Precognition-Success: true header. Failure comes back as a normal 422 with the same JSON error bag your form submit would produce. Same rules, same messages, same field names. There is no second schema to drift. The lifecycle is worth holding in your head: Front end sends the form state to the real URL with Precognition: true . Middleware runs. FormRequest validation runs. Laravel short-circuits: your controller body never executes. Response is
AI 资讯
Integrating Claude/OpenAI API into a Laravel App: A Practical Guide
After 12+ years of building PHP applications, I recently added AI-powered features to a production Laravel dashboard — automatic report summaries generated from raw analytics data. What surprised me wasn't how hard it was. It was how little good PHP-focused content exists on this topic. Almost every LLM tutorial assumes you're writing Python. So here's the guide I wish I had: integrating the Claude API and OpenAI API into a Laravel app, with a clean architecture you can actually ship to production. What we'll build: a ReportSummaryService that takes raw data and returns a human-readable summary — with a driver pattern so you can switch between Claude and OpenAI with one config change. Step 1: Get Your API Keys Claude: Sign up at the Claude Console , generate a key under Account Settings. OpenAI: Get a key from the OpenAI Platform . Add them to your .env : AI_PROVIDER=claude ANTHROPIC_API_KEY=sk-ant-xxxxx ANTHROPIC_MODEL=claude-sonnet-4-6 OPENAI_API_KEY=sk-xxxxx OPENAI_MODEL=gpt-5-mini ⚠️ Never hardcode API keys. Never commit them. If you've ever pushed a key to Git, rotate it immediately. (You know this. I'm saying it anyway.) Now register them in config/services.php — this is the Laravel way, so you can use config() everywhere and benefit from config caching: 'anthropic' => [ 'key' => env ( 'ANTHROPIC_API_KEY' ), 'model' => env ( 'ANTHROPIC_MODEL' , 'claude-sonnet-4-6' ), ], 'openai' => [ 'key' => env ( 'OPENAI_API_KEY' ), 'model' => env ( 'OPENAI_MODEL' , 'gpt-5-mini' ), ], 'ai' => [ 'provider' => env ( 'AI_PROVIDER' , 'claude' ), ], Step 2: Understand the Two APIs (They're 95% Similar) Both are simple REST APIs. You POST JSON, you get JSON back. Claude (Messages API): POST https://api.anthropic.com/v1/messages Headers: x-api-key: YOUR_KEY anthropic-version: 2023-06-01 content-type: application/json OpenAI (Chat Completions API): POST https://api.openai.com/v1/chat/completions Headers: Authorization: Bearer YOUR_KEY content-type: application/json The key differenc
AI 资讯
Stop Writing the Same Laravel Boilerplate: Generate a Complete Module with One Artisan Command
Stop Writing the Same Laravel Boilerplate: Generate a Complete Module with One Artisan Command Every Laravel developer has experienced this. You start implementing a new feature and immediately create the same files you've created dozens of times before: Model Migration Repository Service Form Request API Resource Policy Filter Status Enum Feature Tests Unit Tests Swagger/OpenAPI annotations The process is repetitive, time-consuming, and easy to get wrong. The Problem While Laravel provides excellent generators, building a production-ready API module still requires running many Artisan commands and wiring everything together manually. For large projects following Repository and Service Layer architectures, this becomes even more repetitive. The Solution I built Laravel Base , an open-source package that generates an entire production-ready module from a single command. php artisan make:module Product The generated module includes: ✅ Model ✅ Migration ✅ Repository Pattern ✅ Service Layer ✅ Form Requests ✅ API Resources ✅ Filters & Pagination ✅ Policies ✅ Status Enums ✅ Swagger/OpenAPI annotations ✅ Feature Tests ✅ Unit Tests Modern Development Experience The package is actively maintained and includes: Laravel 10–13 support PHP 8.1–8.4 compatibility GitHub Actions CI PHPStan static analysis Laravel Pint code style Automated releases Repository automation Why I Built It After working on multiple Laravel projects, I noticed I was spending too much time generating the same project structure instead of focusing on business logic. I wanted a tool that lets developers start implementing features immediately rather than setting up folders and classes. Feedback Welcome Laravel Base is open source, and I'd love to hear your thoughts. GitHub Repository: https://github.com/MuhammedMSalama/LaravelBase Packagist: https://packagist.org/packages/muhammedsalama/laravel-base The package was recently featured by Laravel News, and I'm continuing to improve it based on community feedbac
AI 资讯
Resolve the tenant from the user, not the request
TL;DR A multi-tenant app was resolving the active tenant from the request (subdomain/header) instead of the authenticated user . That makes the client the source of truth for "which tenant am I" — the wrong place for it. Fix: derive the tenant from the user's organization membership, enforce it in middleware, and fail closed. One test locks the behaviour. The bug, in one sentence The request was telling the app which tenant to load, and the app believed it. In a multi-tenant SaaS, every query is implicitly scoped: "give me this tenant's dashboards." If the tenant ID comes from something the client controls — a subdomain, a header, a route param — then the scoping is only as trustworthy as the client. That's a leak waiting to happen. Where the trust should live Think of it like a building pass. The request is someone saying "I'm here for floor 9." The membership record is the pass that says which floors you're actually allowed on. You check the pass, not the claim. Before After Source of truth request (subdomain / header) user's organization membership Who decides the tenant the client the server Failure mode user can land in a tenant they don't belong to resolution fails closed Testable? hard — depends on request shape yes — depends on the user The shape of the fix Resolve the tenant from the authenticated user's organization, in one middleware, before anything tenant-scoped runs: final class SetTenantContext { public function handle ( Request $request , Closure $next ): Response { $org = $request -> user () ?-> currentOrganization (); // No org, no tenant context. Fail closed, never guess. abort_if ( $org === null , 403 , 'No organization context.' ); Tenancy :: setCurrent ( $org -> tenant ); // server-derived, not request-derived return $next ( $request ); } } The key line isn't the setCurrent() — it's that the value comes from $request->user() , not from $request . The user is authenticated; the subdomain is not. request ──> [auth] ──> [SetTenantContext] ──> tena
AI 资讯
When a KPI reads 163 billion instead of 819
TL;DR A metrics engine had two query paths — a SQL push-down for big datasets, an in-memory aggregator for small ones. They drifted. The push-down path bound a metric parameter but never added it to the WHERE . With several metric series in one dataset, every query summed across all of them. A KPI that should read 819 read 163,667,603,769 . Fix: put the metric_key predicate in the shared base WHERE so every compile path inherits it, and regression-test both paths assert it. The setup: two paths, one contract A lot of analytics layers compute the same number two ways. For a big dataset you push the aggregation down to the database. For a small one — a preview, a draft dashboard — you pull the rows and aggregate in memory. Faster path, correct path. Both are supposed to return the same value. That's the contract. The dataset stores rows keyed by a metric_key , because one dataset can hold several series at once — say a plain row count and a count-distinct. Each series lives in the same table, told apart only by its key. The bug: a bound param is not a filter The in-memory aggregator filtered by metric_key correctly. The SQL compiler bound a metric parameter into the query... and never referenced it in the WHERE . With a single series in the dataset, it worked by accident — there was nothing else to sum. Add a second series and the math quietly breaks: the query sums across every series. In this case the second series stored hashed values around 1.9 billion each, so the KPI ballooned from 819 to 163 billion. Before After metric value bound, unused bound WHERE predicate (none on metric) metric_key = {metric:String} 1 series in dataset correct (by luck) correct N series in dataset sums across all isolated The lesson is small and easy to miss: binding a parameter only makes the value available — it does nothing until a predicate references it. When one path already returns sane-looking numbers, nobody goes looking. The real fix is parity, not a patch You could bolt the pr
开发者
Dev Log: 2026-06-29
TL;DR Two threads today: an organization layer on top of an existing multi-tenant app, and driver-based password-reset backends in an identity portal. Both came down to the same idea — put the source of truth in the right place, then test it. Multi-tenant app: an organization layer above tenancy The product already had tenancy. What it lacked was a human-friendly layer on top: organizations users actually belong to, can switch between, and manage. What landed: Area Change Org switcher A sidebar switcher to move between organizations you belong to Management Create/update org, invitations, ownership transfer Tenancy Resolve the active tenant from the user's org — closed a leak UI Dark-mode pass + responsive fixes across the org views Dashboards Richer per-widget configuration from the UI The standout is the tenancy fix: the active tenant was being resolved from the request instead of the authenticated user. I pulled that into its own focused post — "Resolve the tenant from the user, not the request." Short version: if a value scopes data, it can't come from something the client controls. Identity portal: make the reset backends swappable The password-reset flow needed to support more than one backend, and let an admin decide the order they run in. Classic case for a driver-based abstraction — a contract plus interchangeable drivers, picked at runtime from config. interface PasswordResetBackend { public function reset ( User $user , string $password ): void ; public function name (): string ; } Two optional backends came back as drivers behind that contract, and the run order is now admin-reorderable instead of hard-coded. Adding a third backend later is a new class + a config line — no touching the flow itself. The other half of the day was unglamorous but necessary: the test suite had drifted — stale tests for removed features, and env leakage between tests (one test's state bleeding into the next). Fixed the leakage, deleted the dead tests, and the suite is honest
AI 资讯
Dev Log: 2026-06-28
TL;DR Centred a sidebar brand mark in the collapsed rail (open-source starter kit) — pure CSS, no JS. A CRM app got a "daily cockpit" dashboard (hot leads + overdue follow-ups) plus a full favicon/PWA icon set. An analytics product's ingest pipeline learned to handle messy uploads — files with no date column and no numeric measure — and a nasty metrics bug got squashed. A spread day across three repos. Quick tour. Centring a collapsed sidebar logo (CSS only) Kickoff , my open-source Laravel starter kit, had a small visual snag: when the sidebar collapses to a narrow rail, the header switches to a column — but the brand mark sat off-centre. The content area is ~72px, yet the logo kept its width and a leftover space-x margin, nudging it left of the nav icons. No JavaScript needed. Make the logo and toggle full-width, centre their content, and zero the leftover child margins when collapsed: [ data-flux-sidebar ][ data-collapsed ] .sidebar-header .app-logo { width : 100% ; justify-content : center ; padding-inline : 0 ; } /* kill the leftover space-x margin pushing it off-centre */ [ data-flux-sidebar ][ data-collapsed ] .sidebar-header .app-logo > * { margin : 0 ; } Lesson: when a flex container changes direction, old horizontal margins don't disappear — they just push things in the new axis. Tag the element, scope the override to the collapsed state, done. A CRM "daily cockpit" A CRM app I work on got a dashboard rebuild: instead of a generic landing screen, the first thing you see is what needs action today — hot leads and overdue follow-ups. The cockpit framing matters more than the widgets: surface the work, don't make people hunt for it. Also shipped a full favicon/PWA icon set and a branded responsive landing page, with feature tests so the brand pass didn't quietly break routing. Ingest that survives real-world files The bigger chunk of the day went into an analytics/dashboard product's ingest pipeline. Real uploads are messy, so the pipeline now copes with the
AI 资讯
Seu código de validação de CPF tá gritando por socorro (e você nem percebeu)
Deixa eu adivinhar. Você tá com um projeto Laravel rodando, tem uns 5, 10, talvez 15 formulários que recebem CPF. Cadastro de cliente, cadastro de fornecedor, atualização de perfil, checkout, área administrativa… e em cada um desses lugares tem aquela mesma lógica de validação de CPF. Copiada. Colada. Com pequenas variações. E tá tudo bem. Até o dia em que o cliente pede pra mudar uma regra. Ou um bug aparece em um formulário e funciona normal no outro. Aí você abre o projeto, dá um Ctrl+Shift+F procurando "cpf" e… surpresa: tem oito lugares diferentes com a mesma validação. Com mensagens de erro escritas de oito jeitos. Uma delas até com erro de digitação. Já passou por isso? Então senta que essa conversa é pra você. O crime acontecendo em câmera lenta Olha esse cenário aqui, que eu garanto que você já viu (ou escreveu): // app/Http/Requests/StoreClienteRequest.php public function rules () { return [ 'cpf' => [ 'required' , function ( $attribute , $value , $fail ) { $cpf = preg_replace ( '/[^0-9]/' , '' , $value ); if ( strlen ( $cpf ) !== 11 ) { $fail ( 'CPF inválido.' ); return ; } // ... mais 20 linhas do algoritmo }], ]; } E aí, três dias depois, no outro Form Request: // app/Http/Requests/StoreFornecedorRequest.php public function rules () { return [ 'cpf' => [ 'required' , function ( $attribute , $value , $fail ) { $cpf = preg_replace ( '/[^0-9]/' , '' , $value ); if ( strlen ( $cpf ) !== 11 ) { $fail ( 'O CPF informado não é válido!' ); // mensagem diferente, claro return ; } // ... mais 20 linhas quase iguais, mas não exatamente }], ]; } Multiplica isso por 8 telas. Agora imagina o seu "eu do futuro" tentando manter isso. Dá pra sentir a dor daqui. DRY: a sigla que vai salvar seu projeto (e sua sanidade) DRY significa Don't Repeat Yourself . Em bom português: não se repita, caramba. A ideia é simples: cada pedaço de conhecimento (uma regra de negócio, um cálculo, uma validação) deve existir em um único lugar no seu sistema. Se precisar mudar, você muda em u
AI 资讯
Laravel API Development in Morocco: Architecture Guide 2026
Laravel API Development in Morocco: Architecture Guide 2026 Laravel remains the #1 PHP framework for API development in 2026 Laravel remains the #1 PHP framework for API development in 2026, and Morocco has become a hub for quality Laravel freelancers and teams. Here is the complete guide to building production-grade APIs with Laravel, based on 40+ projects shipped. Why Laravel for APIs in 2026 Eloquent ORM — most expressive DB layer in any framework Sanctum for SPA/mobile auth (simpler than Passport for most cases) Scout for Meilisearch / Algolia / Elastic full-text search Queues with Horizon for background jobs Octane for performance (Swoole / RoadRunner) Deep ecosystem : Telescope, Pulse, Forge, Vapor REST vs GraphQL — What to Choose Criteria REST GraphQL Learning curve Low Medium-high Caching Easy (HTTP) Complex Over-fetching Common Solved Mobile bandwidth Higher Optimized Best for Public APIs, simple CRUD Complex dashboards, mobile apps My default : REST with Laravel API Resources unless the client has clear GraphQL-specific needs (mobile app with variable fields, highly nested data). Standard Laravel API Architecture app/ ├── Http/ │ ├── Controllers/Api/V1/ │ ├── Requests/ (FormRequest for validation) │ └── Resources/ (API Resources for shaping output) ├── Models/ ├── Services/ (business logic) ├── Repositories/ (optional, if complex queries) ├── Jobs/ └── Events/ Key architectural decisions Versioning via URL (/api/v1/users) not headers — simpler FormRequest for validation (never validate in controller) API Resources for every response (shape, transforms, conditionals) Services layer when controllers exceed 100 lines Dedicated DTOs for complex payloads (spatie/laravel-data) Authentication — Sanctum Setup SPA on same domain : cookie-based, CSRF protected Mobile app / 3rd party : personal access tokens Revocation endpoint for logout Token abilities for granular permissions Rate Limiting & Security RateLimiter facade — per user, per IP, per endpoint CORS : use c
开发者
Laravel From Version 5 To Today: The Framework Grew Up With Us
Have you ever opened an old Laravel 5 project and felt like you were walking into a house where every...
AI 资讯
Giving an AI agent the keys without giving it the building: RBAC + org-scoped MCP tools in Laravel
Exposing your app to an AI agent over MCP is basically handing someone a master keyring and trusting them to only open the doors they're supposed to. That trust is a bug waiting to happen. This week I wired up a batch of MCP tools over a multi-tenant Laravel app, and the whole exercise was really about one question: how do I let an agent drive the app without letting it drive someone else's data? Here's the thing about MCP tools — each one is an endpoint. An agent calls list_events , publish_event , check_in_participant , and your server runs code on the caller's behalf. The moment you have more than one tenant, every single tool needs to answer two questions before it does anything: are you allowed to do this , and are you allowed to do it *here *. Authorization and scope. Skip either and you've built a confused deputy. The trap: ambient scope doesn't exist under token auth In a normal web request, multi-tenancy is comfortable. You've got a logged-in user, a global scope on the model that quietly appends where organization_id = ? , and you mostly forget it's there. Everything Just Works because there's an ambient "current organization" sitting in the session. MCP tools don't have that. The caller authenticates with a token, there's no session, no middleware stack that set up a current-tenant context. If you lean on a global OrganizationScope that reads "the current org" from somewhere, it reads nothing — and a query you assumed was fenced returns every tenant's rows. That's the kind of bug that doesn't throw an error; it just silently leaks. So the rule I settled on: under token auth, never rely on ambient scope. Filter explicitly, every time, in one place. That "one place" is a small trait every event-scoped tool pulls in: trait ResolvesOrgEvents { protected function resolveOrgEvent ( Authenticatable $user , string $uuid ): ?Event { if ( empty ( $user -> organization_id )) { return null ; } return Event :: query () -> withOrganization ( $user -> organization_id )
AI 资讯
Dev Log: 2026-06-23 — Query Cleanups, Real Health Checks, Safer MCP Tools, and Password-Reset Plumbing
A wide day rather than a deep one — four separate threads across a few projects, each with a lesson worth keeping. I'll teach the patterns and keep the specifics generic. The through-line: make the system honest about what it's actually doing — which queries it fires, whether a service is really up, what a tool will do when you call it twice, and in what order a password change should land. The performance thread got big enough that I split it into its own focused post; here's the short version plus the three other threads. Thread 1 — Stop paying for queries you don't use A sustained sweep through an app (and the package behind it) hunting wasted database work. The highlights: Arm an N+1 detector in dev only. A query detector wired in behind an environment check turns invisible lazy-loads into a visible to-do list. Never in production — it's a developer aid, not a runtime guard. Unused eager loads are N+1s in disguise. Index screens love to with(['creator', 'approver']) for columns a redesign later removed. Not a loop, but the same disease: queries you hydrate and throw away. Delete the eager loads with no consumer in the view. Memoize per-request constants. A default-connection resolver and a sidebar unread count were both recomputed on every call. ??= once, reuse for the rest of the request. Collapse a dashboard's stat queries. ~20 count() calls became one grouped query per table, wrapped in a short-lived cache. A dashboard can tolerate being a few seconds stale; trade live-to-the-second for cheap. The meta-lesson: performance at this layer is mostly removal , and you lock it in with a Pest query-count assertion so nobody quietly re-adds an N+1 six months later. Full write-up in the focused post. Thread 2 — Health checks that actually check Here's a trap I keep seeing in "is it up?" tooling: the check verifies the record exists, or that a config row is present, and calls it green. That's not a health check — that's a config check. The service can be configured per
AI 资讯
A Day of Performance Hardening: Hunting N+1s and Killing Wasted Queries in Laravel
Performance work has a reputation for being glamorous — the heroic "we cut latency by 80%" story. Most days it's not that. Most days it's a janitorial pass: you go looking for the queries you're firing without realizing it, and you quietly delete them. That was today. One sustained sweep across an app and the package that backs it, chasing the same theme everywhere: stop asking the database for things you don't use. Let me walk through the patterns, because they generalize to any Laravel app of a certain age. First, make the invisible visible You can't fix N+1s you can't see. The first move was wiring up an N+1 detector in the local/dev environment only — beyondcode/laravel-query-detector . It hooks into the request lifecycle, watches your Eloquent relationship loads, and screams (in the console, or as an exception if you want it strict) when it spots the classic loop-and-lazy-load pattern. The "dev-only" part matters. You never want a query detector running in production — it adds overhead and it's a developer aid, not a runtime guard. So it goes in behind an environment check, registered only when the app isn't in production: public function register (): void { if ( $this -> app -> environment ( 'local' , 'testing' )) { $this -> app -> register ( \BeyondCode\QueryDetector\QueryDetectorServiceProvider :: class ); } } Think of it like a smoke detector you only arm while you're cooking. It's noisy by design — that's the point. The noise is a to-do list. Eager loads you don't actually use are just N+1s wearing a disguise Here's the counterintuitive one. We're all trained to fix N+1s by adding with() . But the opposite bug is just as common and almost never gets caught: you eager-load a relationship, and then... never touch it in the view. Index screens are the worst offenders. Someone builds a listing, eager-loads creator and approver so the table can show names, then a redesign drops those columns — but the with(['creator', 'approver']) stays. Now every page load hyd
AI 资讯
Dev Log: 2026-06-22 — Configurable Schedulers, Load-Test Toolkits, and an MCP Server
Some days the work spreads across a few projects instead of landing as one big feature. Today was that — three distinct threads, each with a lesson worth keeping. I'll keep things generic and teach the pattern rather than the project, but the through-line is the same: move things that were hardcoded or ephemeral into something you can configure, repeat, and trust. Thread 1 — Make scheduled tasks configurable instead of code-only If you've run a Laravel app for any length of time, you know the scheduler lives in code: routes/console.php or the kernel, a wall of ->daily() , ->everyFiveMinutes() , ->cron(...) . That's fine until the day an operator — not a developer — needs to change when something runs. Then you're shipping a deploy just to nudge a cron expression. Silly. Today's work pulled scheduler configuration into a settings-backed UI. The pattern is worth stealing: instead of the schedule being a literal in code, the code reads its cadence from a settings store, and there's an admin screen to edit it. // Instead of a hardcoded cadence... $schedule -> command ( 'subscriptions:reconcile' ) -> daily (); // ...read it from settings, with a sane default baked in. $schedule -> command ( 'subscriptions:reconcile' ) -> cron ( $this -> schedulerSettings -> reconcileCron ?? '0 2 * * *' ); Two things made this clean. First, a SchedulerSettings object (Spatie's settings pattern) so the values are typed, cached, and migratable — not loose rows you Setting::get('...') by string key. Second, grouping the more user-facing schedules behind their own modal rather than dumping every cron in one giant form. A subscription-related schedule belongs next to subscriptions; a platform schedule belongs in admin. Same data, but organized by who needs to touch it . The edge case to watch: a UI-editable cron is a foot-gun if you let people type nonsense. Validate the expression on save, and always keep a default so a blank setting can never silently disable a job. Thread 2 — A load-testing
开发者
bcrypt and Laravel: 72 Bytes, Not 72 Characters
I expected bcrypt to silently drop characters past 72. I did not expect it to bake in half an emoji. That's what happens with a specific password combination I tested. The original password still works. But strip the emoji (a password manager, a different keyboard, a Unicode normalizer) and you're locked out. Your Laravel validator passed it as valid the whole time. The 72-Byte Rule bcrypt has a hard input limit of 72 bytes. Not characters - bytes. When you call password_hash($password, PASSWORD_BCRYPT) , PHP silently truncates anything past byte 72. Most developers know this in theory. But for ASCII-only apps, it never bites. 72 ASCII characters is already a very long password, and the silent clip is harmless in practice. The trouble starts with multi-byte scripts. How Many Characters Fit? Character set Bytes per char Effective bcrypt limit ASCII 1 72 chars Cyrillic 2 36 chars CJK (Chinese, Japanese, Korean, common block) 3 24 chars Emoji 4 18 chars Past the byte limit, a longer password adds no security at all. A 200-character Cyrillic password hashes identically to its own first 36 characters. Byte 73 and beyond simply do not exist from bcrypt's point of view. So "longer always produces a stronger bcrypt hash" is not true. A Cyrillic user with a 37-character password gets silently truncated at char 36. The hash is still consistent. The user logs in fine, but any variation past character 36 doesn't matter to bcrypt. Annoying from a security standpoint, but it does not break login. The Split-Byte Trap The 72-byte limit cuts at a byte boundary, not a character boundary. If a multi-byte character falls on that cut, bcrypt bakes in an incomplete UTF-8 sequence. // 35 Cyrillic chars = 70 bytes, emoji = 4 bytes, total = 74 bytes $password = str_repeat ( 'А' , 35 ) . '🔑' ; $hash = password_hash ( $password , PASSWORD_BCRYPT ); password_verify ( $password , $hash ); // ? password_verify ( str_repeat ( 'А' , 35 ), $hash ); // ? password_verify ( str_repeat ( 'А' , 36 ), $h
AI 资讯
Extending Filament exports with Laravel Excel
Filament's export action is great. It's quick to set up, supports queued exports, includes column mapping, handles notifications, and keeps a history of generated files through the Export model. For most use cases, it's exactly what you need. But I recently ran into a limitation that the native export couldn't solve. When XLSX isn't really Excel I was exporting financial data or measurements from a Filament table. The export worked. The file downloaded. Excel opened it without any issue. The problem was that every amount was exported as text instead of a real numeric value. For an accountant, that creates several problems immediately: Excel formulas such as =SUM() don't work correctly Selecting a range of cells doesn't display totals in Excel's status bar Conditional formatting based on numeric values becomes unreliable Additional manual cleanup is required before the file can be used Technically the export contained the data. Practically, it wasn't usable. The root cause is simple: Filament's export system is designed around CSV-style exports. That's perfect for many scenarios, but it doesn't expose the full spreadsheet capabilities offered by PhpSpreadsheet and Laravel Excel . On top of that, I also had a second, completely different requirement: a yearly report with one worksheet per month, merged headers, borders, conditional formatting, and custom layouts. Not a table dump but a report. Why not just use Laravel Excel directly? Laravel Excel already solves all of these problems. It's built on PhpSpreadsheet and provides complete control over cell types, number formats, formulas, styling, and multiple worksheets. The obvious solution would have been to abandon Filament's export action entirely and build custom exports from scratch. But that means losing everything Filament already provides: Export modal and options form Column mapping UI Queue handling Progress notifications Download links Export model history I didn't want to rebuild all of that. I simply wanted
AI 资讯
The Tips Behind API Artisan: Building Laravel APIs Developers Actually Want to Use
I have just finished writing API Artisan: A Guide to Building APIs with Laravel , and I am giving it away for free. Before you commit to 300-odd pages, let me give you the short version: the tips, patterns, and small decisions that separate an API that technically works from one that developers are genuinely happy to depend on. None of this needs more hardware, a different framework, or a bigger team. It needs you to point your attention at the right things. These are the ones I keep coming back to. Start by measuring the right thing Ask most teams how they know their API is good and you get a single question back: does it work? Can I hit this endpoint and get a response? That question is necessary, and it is nowhere near enough. The question I want you to ask instead is whether your API is liveable with. Can a developer read your docs, understand your auth model, make a successful request, and handle an error without contacting support, trawling a forum, or guessing what a status code is trying to tell them? The gap between "works" and "liveable with" never shows up in a sprint retro, but it shows up everywhere else: in support volume, in integration timelines that overrun, and in the quiet moment a developer decides to build around your API rather than with it. Everything else in the book hangs off one mindset shift: an API is a product. It has users. Treat it as an implementation detail and it will behave like one. It will change without warning when your internals change, and it will be inconsistent because different people wrote different parts on different days with different conventions. Write the contract before the code The natural way to build an endpoint is to write the handler, return some data, and document it afterwards if there is time. It feels efficient, and in the short term it is. The problem is what it produces: a contract that was never designed, only discovered. Let me show you the trap, because I have watched it catch good developers. You have