今日已更新 240 条资讯 | 累计 23823 条内容
关于我们

标签:#php

找到 105 篇相关文章

AI 资讯

When a password sync is 'partly done', it's a bug: converging on one canonical flow

TL;DR Two apps each had their own password-change/reset logic, plus config toggles to enable/disable backends. That combination quietly allowed partial syncs. Fix: one shared engine, one canonical order , backends mandatory (no config-disable), and every attempt written to an audit log. Lesson: for a write that spans several systems, "configurable steps" is a footgun. Make the flow fixed and make failure loud. The setup A user changes their password. Behind the scenes that single password has to land in several systems — a directory, an external database, and the app's own store. Two separate apps were doing this, each with slightly different code, and each with config flags like sync_oracle => true|false to turn backends on and off. Sounds flexible. It's actually a trap. Why configurable backends are a footgun TL;DR: a password that updates 2 of 3 systems is worse than one that updates none — because now the systems disagree and nobody gets an error. The moment a backend is optionally skippable, "skipped" and "failed" blur together. Someone flips a flag in one environment, forgets it in another, and now prod and staging run different flows. Debugging a login failure means first reverse-engineering which steps actually ran. Before After Each app had its own reset logic One shared engine, both apps call it Backends toggle via config Backends are mandatory, always run Order implicit / differed per app One canonical order: New directory → external DB → local app "Did it sync?" answered by guessing Every attempt logged with per-service status The canonical order The order isn't cosmetic. It runs most-authoritative-first, so if a downstream step fails you haven't already told the user their new password works. change/reset request | v [ New Directory ] --ok--> [ External DB ] --ok--> [ Local App store ] | | | fail fail fail | | | +----> stop, record per-service status, surface the failure Same engine, same order, both apps. A change API and a reset flow are just two entr

2026-07-09 原文 →
AI 资讯

A pending-plugin-count badge on the 🔌 button — reusing the dashboard cache instead of doubling state

A client asked: " After I run a cross-site update check, can each site show — right in the site list — how many plugin updates are still pending? " Visually the answer was obvious: a small red badge on the top-right of the 🔌 plugins button, like an unread-notification count. Easy to specify. The harder question was where the data comes from . We could have added a fresh API endpoint and a new cache to hold "pending count per site." But doing that would have doubled state management , and we already had a cache that knew this. We routed through the existing one. Here's the reasoning behind that decision. Reuse the dashboard cache as the data source The cross-site updates dashboard (the one we wrote about in killing the 24.5-second silence with a cache-first design ) already kept each site's pending plugins in a localStorage-backed state called _updatesDashState . Its shape: _updatesDashState = { sites : [ { site_id : " abc... " , plugins : [ {...}, {...}, {...} ] }, { site_id : " def... " , plugins : [ ... ] }, ], total_pending_count : 12 , loadedAt : 1748600000000 , } Look up by site_id , take plugins.length , and you have the badge's number. No new API, no new cache. The data that powers the cross-site dashboard is also the data that powers the site-list badge. The win of not adding state is quiet but real: When a maintenance run invalidates _updatesDashState , the badge disappears automatically (no sync code to write) The TTL (originally 7 days; later extended to 30 days with partial invalidation ) inherits from the existing design The badge and the underlying count can't drift — there's no second copy to fall out of step There's always a temptation to spin up a new endpoint for a new UI element. The rule we settled on: if the existing state answers it, don't add more. Attaching the badge — consolidate into helpers Both the list view and grid view need the same badge on the 🔌 button, so the logic lives in helpers. function _getPendingPluginCountForSite ( siteId )

2026-07-08 原文 →
AI 资讯

Dev Log: 2026-07-07

TL;DR Collapsed a billing model from à-la-carte features to plans-only, in four safe phases. Unified authorization across web, API, and MCP so all three obey one permission layer. Fixed a legacy Oracle password-sync writing to the wrong column. Four repos moved today. Here's the thread that ties most of them together: one source of truth beats two. Billing: plans-only Spent most of the day migrating a SaaS off per-feature à-la-carte subscriptions and onto plans-only entitlements. The interesting part isn't the model — it's doing it without a billing outage: seed plans, switch reads to plans, backfill every org, then delete the old machinery. Expand/contract, four deployable phases. Full write-up in the focused post. One permission layer for three surfaces An ops tool exposed the same actions three ways — web UI, API, and an MCP server for agent access. The bug: each surface checked authorization slightly differently, so an MCP tool could allow something the web UI blocked. The fix was to make the MCP tools gate on the same permission layer as everything else, so: web ─┐ API ─┼─► one permission check ─► allow / deny MCP ─┘ TL;DR: web ≡ API ≡ MCP — three doors, one lock. Also added a dedicated support-engineer role scoped for debugging without handing over the keys to everything, plus identity/diagnostics/SLA read tools so an agent can answer "why didn't this notification send?" without shell access. Before After Each surface authorizes its own way Single permission check, shared MCP tool could out-permission the UI MCP bound to the same guard No debug-scoped role support_engineer role, read-only diagnostics Legacy Oracle password sync Smaller but sharp: a password reset was writing to the wrong Oracle column and also touching a date_modified field it had no business updating. Routed the student reset to the correct password column and dropped the stray write. Lesson with legacy schemas — the column that looks right and the column the app actually reads from are not a

2026-07-08 原文 →
AI 资讯

Killing à-la-carte: migrating a feature-gating model to plans-only

TL;DR Moved a SaaS from à-la-carte feature subscriptions (pay per feature) to plans-only (pay for a tier, get its features). Did it in four phases so nothing broke mid-flight: seed plans → gate on plans → migrate orgs → delete the old machinery. Lesson: model migrations are safest as expand → migrate → contract , not a big-bang swap. The problem The old billing let an org subscribe to individual features à la carte. Flexible on paper, painful in practice: entitlement logic had two sources of truth (per-feature subscriptions and an implicit plan), and every gate had to check both. Time to collapse it into one model — you buy a plan , the plan carries the features. The trap with this kind of change is the temptation to rip out the old columns and ship. Do that and every in-flight subscription, every gate check, and every webhook that still speaks the old language breaks at once. The phased plan I ran it as four ordered migrations. Each phase is deployable on its own and leaves the app working. Phase What it does Why this order F1 Seed Plans into the prerequisite chain New model must exist before anything reads it F2 Gate features on the plan, not the feature-sub Reads switch over while writes still dual-run F3 Deploy op migrates existing orgs onto a plan Backfill — nobody left on the old model F4 Remove the à-la-carte machinery Contract — safe only after F3 This is the expand/contract pattern applied to a domain model, not just a schema. Expand (F1) adds the new thing alongside the old. Migrate (F2–F3) moves reads then data. Contract (F4) deletes the old thing once nothing points at it. F1 seed F2 gate on plan F3 backfill orgs F4 drop features ┌────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ plans │ --> │ reads: plan │ -> │ every org on │ -> │ delete a-la- │ │ exist │ │ writes: both │ │ a real plan │ │ carte code │ └────────┘ └──────────────┘ └──────────────┘ └──────────────┘ safe safe safe safe Gating on the plan The gate collapses to a single question

2026-07-08 原文 →
AI 资讯

Syncing a wholesaler's API into WooCommerce without overselling or melting the server

A common WooCommerce brief looks like this: the store does not own its inventory. A distributor does. The shop is a storefront on top of a wholesaler whose catalog, stock levels, and prices change daily, exposed through some REST or XML web service. The job is to make the store reflect the supplier's reality automatically, and to never sell something the supplier cannot ship. We shipped exactly this for an automotive-parts store recently (client and supplier stay anonymous). Tens of thousands of indexes, a wholesaler REST API, and a hard requirement: no manual catalog work, and no orders for parts that are not actually in stock. Here is what the architecture looks like and the traps worth knowing before you start. The store is a view, the wholesaler is the source of truth The first mental shift is that WooCommerce is not the system of record for products. The distributor is. WooCommerce is a cache with a checkout attached. Once you accept that, the design falls out: a sync layer pulls from the supplier and writes into WooCommerce on a schedule, and you treat the WooCommerce product data as derived, not authored. The integration answers three questions, and you should answer them explicitly before writing code: What syncs - catalog, attributes, media, stock, price. Which direction - here it is one-way (supplier to store); orders stay in WooCommerce. How often - split it. Stock and price are cheap and change constantly, so poll them frequently. Full catalog and media are expensive, so refresh them rarely. Map fields declaratively, or you will rewrite it every month The supplier describes a product its way (its own index, EAN, attribute names, HTML description blobs, image URLs). WooCommerce wants its way (product, attributes, variations, media library). The bridge between them is a field map, and the single best decision we made was keeping that map declarative - a data structure, not a pile of if statements. When the wholesaler adds a new attribute, you extend the ma

2026-07-07 原文 →
AI 资讯

The Session ID That Wouldn't Stop Changing

I was implementing a feature where the session container would track a lastActivity timestamp, updated on every authenticated request. Standard stuff. I wrote it, tested it locally with curl, and noticed something odd: I kept getting a new Set-Cookie header value on every response. Not occasionally. On every single one. A week later I was sending a pull request to mezzio/mezzio-session-cache . The Setup: Two Backends, One Session Our system had a constraint: two backend applications, written in different languages, sharing a single user session. One was the main PHP/Mezzio app. The other was a service in a different stack that needed to read from, and update the lastActivity timestamp on, the same session container. There are a few ways to make polyglot session sharing work. We landed on a shared cache backend (Redis) with a well-defined session structure. Both apps could read and write through their own libraries, as long as they agreed on the storage format and the cookie name. The session ID was the contract. That contract is the part that quietly broke. A Missing Escape Hatch My first instinct was the usual list of suspects. Was something calling regenerateId() in a middleware I didn't know about? Was there a logout being triggered somehow? Was a misconfigured cache layer evicting and recreating sessions? After a bit of digging through the call stack, I ended up in the library itself. And there it was: CacheSessionPersistence was regenerating the session ID whenever the session data changed . Not on login. Not on privilege escalation. On every write . That's when the real question hit me: why on earth would a library do that by default? Reading Code Before Changing It When you find behavior that surprises you in someone else's code, the wrong move is to immediately label it broken. The right move is to assume the maintainers had a reason, and find out what it was. The reason, in this case, is session fixation . Session fixation is a class of attack where an atta

2026-07-07 原文 →
AI 资讯

A "days since last maintenance" badge — color-coding staleness across many sites

When you maintain a number of WordPress sites, showing the "last maintenance date" in the site list is the obvious move. A column of dates like 2026-05-21 . But in actual use, that alone falls short. A client put it well: "Besides the last maintenance date, it'd help to also show how many days have passed . And it'd be even better if the color changed at 15 / 30 / 60 days so I can see the risk level ." This post walks through that step — from "absolute date" to "relative elapsed days + color" — including the small design details. Why a date alone isn't enough An absolute date like 2026-05-21 is precise, but it pushes the "difference from today" calculation onto the user's head . Fine for five sites; as the managed set grows, reading "which ones are getting neglected" off a column of dates gets hard. The point of a maintenance inventory is to grasp which sites need attention at a glance. If so, what you should surface is less the absolute date and more the relative quantity — " how many days since the last maintenance " — and ideally let color convey "how many days until it's risky." The client's request landed exactly on this "absolute → relative + risk" shift. Four-tier color coding We went with four tiers by elapsed days. A small badge like (15 days ago) sits right after the last-maintenance date, and the color changes by threshold. Elapsed tier color meaning 0–14 days fresh green recently maintained, fine 15–29 days normal gray standard 30–59 days warn amber needs attention 60+ days danger red needs action green → gray → amber → red — just scrolling the list, "lots of red here" or "a cluster of sites I haven't touched lately" jumps out visually. The badge also gets a hover tooltip ("N days since last maintenance") to back up the number's meaning. Consolidate into helper functions The display logic is called from multiple places (list view, grid view), so scattering inline day calculations would be a DRY violation. We consolidated into a set of helpers. // Returns

2026-07-07 原文 →
AI 资讯

Supracorona Login Gate: Simple Access Control for WordPress Sites

For internal websites, client portals, development environments, and WordPress projects that should not be publicly accessible. Not every private WordPress website needs a complete membership system. Sometimes there is no need to manage subscriptions, membership plans, payments, complex user roles, or dozens of content-access rules. Sometimes the requirement is much simpler: The website should only be accessible to logged-in users. That is the reason I created Supracorona Login Gate , a lightweight WordPress plugin that places a simple access gate in front of a website. When the plugin is enabled, logged-out visitors cannot browse protected site content. Instead, they are redirected to the standard WordPress login page or to a custom page selected by the site administrator. The plugin is now published on WordPress.org: Supracorona Login Gate The problem the plugin solves Many WordPress projects are not intended to be publicly accessible at every stage of their lifecycle. They may be: development or staging websites; internal company or organization websites; client portals; private knowledge bases; documentation websites intended only for team members; projects being prepared for launch; demo websites available only to selected users; websites temporarily closed during migration or reconstruction. WordPress provides privacy controls for individual posts, but that is not the same as restricting access to the entire website. Installing a full membership plugin is possible, but it is often far more than the project requires. Such systems may introduce additional database tables, large settings panels, custom profiles, login forms, subscription management, and complex rule engines that will never be used. Supracorona Login Gate has a much narrower responsibility. It is not intended to become a complete membership platform. It is intended to answer one clear question: Is the current visitor logged in? When the answer is yes, the website behaves normally. When the answer

2026-07-06 原文 →
AI 资讯

Dev Log: 2026-07-04

TL;DR Two Laravel backends started serving Flutter apps on the same day — an events platform (auth, orders, offline check-in) and a helpdesk product (ops mode for agents). gatherhub-web moved to plans-only pricing with a comparison matrix driven by one data file. A hardening pass: payment-safe queues, gateway reconciliation, one heavyweight dependency dropped. Two mobile APIs in one day Coincidence, but a useful one: two products I'm building both needed their Laravel backends to serve mobile apps this week. The events platform got the full foundation — token auth (login/refresh/logout/me), participant orders, mobile payment with status polling, push-device registration, and an offline-first staff check-in flow. That last one is the interesting bit; I wrote it up as its own post. The helpdesk product went the other way: its API was client-only, and today it became role-aware. The same endpoints now serve ops agents working tickets from their phones, with abilities deciding what each role sees. One API surface, two personas, no duplicated /admin routes. The lesson that repeated in both: API Resources are the contract. The moment a mobile dev consumes your endpoint, every field you accidentally leak becomes a field you can't remove. Plans-only pricing (public) gatherhub-web , the Next.js marketing site, dropped à-la-carte feature pricing for three plans and gained a plan comparison matrix. Everything renders from a single plans.ts — the matrix, the pricing cards, the enterprise page — so the marketing site can't drift from what's actually sold. A pricing page is a contract too; it deserves a single source of truth as much as your API does. Hardening pass Change Why Bulk email blasts isolated to their own queue one big send must never delay a payment webhook Reconciliation command for stuck pending orders webhooks fail silently; polling the gateway is the safety net maatwebsite/excel → spatie/simple-excel for exports streams rows instead of building sheets in memory, s

2026-07-05 原文 →
AI 资讯

Quieting PHP 8.2+ deprecated noise from older WP-CLI — three layers to keep JSON parse clean

Our multi-site maintenance tool fires wp plugin list --format=json against the sites it manages. One day, against a specific shared host (Xserver in Japan), this call started failing — and the failure mode was unusually subtle. Both the SSH connection test and the WP-CLI path test ( wp --version ) came back green. Users saw "all diagnostics pass, but the actual operation fails," a frustrating asymmetry. Tracing it back, the root cause was PHP Deprecated warnings emitted by older WP-CLI (2.x) under PHP 8.2+ leaking into the JSON output. This post walks through the three-layer defense we used to structurally absorb the noise without losing real failures. What was happening — Deprecated warnings on stdout The raw output on a problem host looked like this: PHP Deprecated: Creation of dynamic property WP_CLI\Dispatcher\CompositeCommand::$longdesc is deprecated in phar:///usr/bin/wp/vendor/wp-cli/wp-cli/php/... [ {"name":"akismet","status":"active","update":"none", ...}, ... ] Since PHP 8.2, assigning to a dynamic property on a class without #[\AllowDynamicProperties] emits a Deprecated warning. Xserver's /usr/bin/wp (an older WP-CLI 2.x) leans on dynamic properties internally, so running it on PHP 8.2+ produces a steady stream of those warnings. Note: PHP 8.2's dynamic-property deprecation is a healthy direction for the language. But during the transition, you get many libraries that "warn but still work" — WP-CLI was one of them. The actual problem is the host's php.ini : depending on display_errors , those warnings end up on stdout instead of stderr . Calling wp plugin list --format=json returns stdout containing both the warnings and the JSON, and json_decode() fails on the mixed input. Why diagnostics stayed green but operations failed The frustrating asymmetry came from how each test was checking the output: SSH connection test : runs echo ok — passes as long as ok appears somewhere in stdout, extra lines are fine WP-CLI path test : runs wp --version — passes as lon

2026-07-05 原文 →
AI 资讯

LLM Provider Fallback in PHP: Automatic Failover in Neuron AI Router

When I published the first article about the Neuron AI Router , I expected questions about routing rules. Which rule to use for structured output, how to write a custom one, how the round robin behaves under load. Some of those questions arrived, but the most frequent one was different, and it wasn't really about routing at all. It was about failure. What happens to my agent when the provider goes down? It is a fair question, and if you are new to building AI applications it deserves a proper answer before we look at any code. Here is the short version. The new fallback strategy in Neuron AI Router lets you define an ordered list of LLM providers for your PHP agent. When an inference call fails with a transient error, such as a rate limit, a timeout, or an overloaded server, the same request is automatically retried on the next provider in the list. The failover is transparent: the agent never knows it happened, and the conversation continues without losing state. The rest of this article explains why this problem exists, why the usual solutions fall short, and how to configure it. Why LLM providers fail in production An LLM provider is an external service you talk to over HTTP. Every time your agent thinks, it is making a network call to a machine you don’t control, operated by a company that is currently serving millions of other requests. These services fail in very ordinary ways. You hit a rate limit because your traffic spiked. The provider returns an “overloaded” error because their traffic spiked. A request times out. A deployment on their side causes a few minutes of elevated error rates. None of this means you did something wrong, and none of it is rare. If you keep an agent in production long enough, you will see all of these. In a classic web application, a failed call to a third party API is usually a corner of the system. You log it, maybe retry it in a queue, and the rest of the page still works. In an agent based application the inference call is not

2026-07-03 原文 →
AI 资讯

Laravel Middleware Execution Order Explained: Why Your Middleware Runs in the Wrong Order

Laravel middleware can be perfectly written and still behave unexpectedly. You may notice authentication running too late, permission checks failing, tenant initialization not working, logging middleware missing important data, or custom middleware executing in an order you didn't expect. In many cases, the middleware code itself is not the problem. The real issue is middleware execution order. Understanding how Laravel executes middleware is critical when building secure and scalable applications because every request passes through multiple layers before reaching your controller. Common Symptoms You may encounter problems such as: Authenticated users being treated as guests Permission middleware failing unexpectedly Tenant information not being available Request logging missing user details Rate limiting triggering before authentication Redirect loops after login Middleware appearing to be ignored completely These issues are often caused by middleware running in the wrong sequence. How Laravel Processes a Request A typical Laravel request follows this flow: Browser ↓ Global Middleware ↓ Middleware Group (Web/API) ↓ Route Middleware ↓ Controller ↓ Response ↓ Browser Each middleware layer can inspect, modify, allow, or block the request before it reaches your application logic. Because of this, execution order matters. Example Problem #1 Suppose you have two middleware: Authenticate User Log User Activity Your logging middleware expects an authenticated user. $user = auth()->user(); However, the log always shows null. Why? Because the logging middleware executes before authentication. The solution is ensuring authentication middleware runs first so user information is available when logging occurs. Example Problem #2 Multi-tenant applications often initialize tenant information through middleware. TenantMiddleware If another middleware accesses the database before tenant initialization, queries may use the wrong database connection. This can lead to: Incorrect data

2026-07-03 原文 →
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

2026-07-03 原文 →
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

2026-07-03 原文 →
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

2026-07-03 原文 →
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

2026-07-03 原文 →
AI 资讯

Building a typed CMS for business data with PHP, OpenAPI, and MCP

In my previous article, I introduced the NeNe series: a family of small, self-hosted business tools for teams operating in Japan. Previous article: https://dev.to/hideyukimori/i-am-building-self-hosted-business-tools-for-small-teams-in-japan-4i26 This article is a deeper look at one of those tools: NeNe Records . Repository: https://github.com/hideyukiMORI/nene-records NeNe Records is an API-first typed CMS and flexible entity platform built on NENE2 , my small PHP framework for AI-readable business APIs. It is not trying to be a WordPress clone. It is not trying to run WordPress plugins or themes. The goal is different: manage business data and public content through typed schemas, documented APIs, and clear AI tool boundaries. Why not just use WordPress? WordPress is useful. It has proven that a generic content model can support blogs, pages, shops, media, and many small business workflows. But when I think about business data, a few problems keep coming back: untyped metadata plugin-specific data shapes hooks and filters that can change behavior from many places API contracts that depend heavily on plugins AI integrations that may not know where the real application boundary is The postmeta model is flexible, but it often becomes stringly typed storage. That is fine for many websites. But for business data, I usually want the API to know more: this field is text this field is an enum this field is an image this field is a relation this field is required this record belongs to this organization this operation requires this role That is the space where NeNe Records lives. Typed records instead of metadata chaos The core model is simple: Entity Type -> Field Definition -> Record An Entity Type defines what kind of data exists. Examples: posts pages products events internal documents A Field Definition describes the shape of a field: text markdown html blocks int enum bool datetime image file relation A Record is the actual data. The important part is that the schema

2026-07-02 原文 →
AI 资讯

Bimaaji: agent-safe mutations for Waaseyaa

Ahnii! If you let an AI agent modify your application, the agent needs more than a text editor. Raw str_replace on a PHP file passes a lot of tests and still breaks things an hour later in production, because the tool has no idea what the file actually represents. Bimaaji is the Waaseyaa package that gives agents a structured path from "I want to add a field to this entity" to a reviewable patch that a community's sovereignty rules have already vetted. This post walks through what shipped in waaseyaa/bimaaji and why each piece exists. Prerequisites: familiarity with Waaseyaa's package layout, PHP 8.4+, and the idea that an application has more state than the filesystem (routes, entities, introspection metadata). Why not just let the agent edit files The failure mode you want to avoid: an agent reads a prompt like "add a published_at field to the Post entity," does a reasonable-looking edit to Post.php , and leaves the rest of the app inconsistent. The migration is missing. The JSON:API resource doesn't expose the field. The admin panel still doesn't know it exists. The sovereignty profile that was supposed to block the change on a local-only deployment never got consulted. Each of those is a different subsystem. A good agent can write a correct edit to any one of them. What a filesystem-level tool cannot do is ensure the edit is coordinated across all of them and is allowed under the community's posture. Bimaaji separates that problem into three stages: introspect, propose, patch. The pipeline The package description (from packages/bimaaji/composer.json ) spells it out: application graph introspection and agent-safe mutation for Waaseyaa. The flow is: Introspection → ApplicationGraph → MutationRequest → Validator → PatchGenerator → PatchSet An agent reads the graph, submits a structured mutation request, a validator checks it against sovereignty rules, and the patch generator returns reviewable diffs. Nothing touches the filesystem until a human (or a higher-level w

2026-07-02 原文 →
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

2026-07-02 原文 →
AI 资讯

Maintaining WordPress sites behind HTTP Basic auth — Playwright, urllib, and encrypted credentials

It's pretty common to throw a layer of HTTP Basic auth on a WordPress site: a staging environment before launch, an internal test instance only employees should see, or any environment that wants an extra gate before the WordPress login screen itself. From a maintenance-tool point of view, this setup creates a peculiar "half-working, half-broken" asymmetry. The SSH/WP-CLI side runs fine. But everything HTTP-based — visual checks, thumbnail generation, browser-based fallback updates — hits 401 and dies. This post walks through how we resolved that asymmetry. What was breaking — two parallel paths, both blocked A maintenance tool actually touches a Basic-auth-protected site through two distinct paths: Playwright path : visual checks, thumbnail capture, browser fallback updates when SSH isn't available. browser.new_context() → navigation → screenshot urllib path : HTTP status checks (pre/post-update 200/5xx/4xx monitoring, rollback decisions) With no credentials, both paths see a 401 Unauthorized from the protected site. The Playwright symptom is the obvious one: the screenshot you save is the browser's "authentication required" dialog. The thumbnail grid fills with dark auth-prompt images, and you start wondering whether anything actually works. The urllib symptom is much worse — it silently breaks rollback decisions . A 401 baseline followed by another 401 after the update looks like "nothing changed = healthy." Real failures can hide behind that match, and the rollback that should have fired never does. The design — consolidate credential extraction into one helper When the same credentials need to flow through multiple code paths, picking them out of the site dict separately at each call site invites format-mismatch and missed-update bugs. So the first thing we did was build a small core/basic_auth_utils.py module that owns every form of credential extraction . # core/basic_auth_utils.py def get_basic_auth_tuple ( site ): """ Return (user, password), or None if not

2026-07-01 原文 →