AI 资讯
Building a Multi-Vendor Home Services Marketplace with Laravel: Architecture, Workflows and Key Decisions
Building a Multi-Vendor Home Services Marketplace with Laravel: Architecture, Workflows and Key Decisions Building a home services marketplace looks straightforward until you start mapping the actual workflows. A customer searches for a service, chooses a provider, selects a time slot, enters an address, pays, and receives confirmation. Simple enough. But behind that booking are several systems working together: customers, providers, services, locations, schedules, bookings, payments, invoices, notifications, and administration. For Laravel developers, the real challenge isn't creating another CRUD application. It's designing these components so the marketplace remains maintainable as providers, locations, services, and bookings grow. This article explores some of the most important architecture and development decisions to consider when building a multi-vendor home services marketplace with Laravel. 1. Think of It as Three Connected Applications A useful starting point is to stop thinking about the marketplace as one application. In practice, you're creating experiences for three different types of users: Customers Service Providers Marketplace Administrators Each has different responsibilities and permissions. Customer Experience Customers typically need to: Register and manage their account Select their location Discover services Find available providers View service details Choose an appointment date and time Save service addresses Create bookings Make payments View booking history Access invoices The customer interface should remain simple even if the system behind it is complex. A typical booking flow may look like: Location → Service → Provider → Date & Time → Address → Payment → Confirmation Every unnecessary step increases friction. 2. The Provider Side Is a Different Product The provider dashboard deserves just as much attention as the customer interface. A service professional or company may need to manage: Business profile Services Pricing Service areas
AI 资讯
Building Robust Crypto Data Pipelines in PHP: Introducing the Token Terminal SDK
The cryptocurrency and decentralized finance ecosystems generate an overwhelming amount of data every single day. For developers building financial dashboards, algorithmic trading tools, or market research platforms, accessing clean, standardized, and reliable data is absolutely critical. Token Terminal has established itself as a premier provider of fundamental financial data for the crypto space, offering institutional-grade metrics across various blockchains and decentralized applications 1 . However, integrating complex third-party APIs into enterprise PHP applications often requires writing significant amounts of boilerplate code to handle edge cases, rate limits, and unexpected response structures. To solve this problem and streamline the developer experience, the PHP community now has access to a dedicated solution: the tokenterminal-php SDK. This new open-source package provides a robust, fully-typed, and developer-friendly PHP 8.1+ client for the Token Terminal API v2 2 . Designed with modern PHP standards and framework integration in mind, it abstracts away the complexities of the underlying HTTP transport, allowing developers to focus entirely on building their applications rather than wrestling with API mechanics. The Challenge of Integrating Financial APIs When working with comprehensive financial data APIs like Token Terminal, developers frequently encounter several architectural challenges. First, there is the issue of rate limiting. Token Terminal enforces a strict limit of 1,000 requests per minute 3 . When building data pipelines that ingest historical metrics across hundreds of assets, hitting this limit is practically guaranteed. A naive implementation will simply crash or drop data, requiring manual intervention. Second, the cryptocurrency space moves rapidly. Projects frequently rebrand, merge, or migrate to new smart contracts. The Token Terminal API handles this gracefully by issuing HTTP 308 Permanent Redirects when a requested project ID ha
开发者
Free, Zero-Dependency YouTube Website Embed (Self-Hostable PHP/JS)
Hey DEV community! 👋 If you've ever tried to embed a dynamic YouTube channel feed, a live stream detector, or playlist carousel on a client site, you've probably run into two major issues: Expensive SaaS widgets that slap watermarks on your site unless you pay a monthly fee. Leaking your YouTube API Key directly in the frontend script. To solve this, we built YT Widget —a free, self-hostable, dependency-free JavaScript library that handles YouTube feeds, playlists, channel stats, and live stream status seamlessly. 📦 Where to Get It The project is fully open-source and ready for your production projects: Source Code & Contributions: scott8462 / YT-Widget A free, self-hostable, dependency-free JavaScript library for embedding YouTube feeds, playlists, channel stats, single videos, and live stream status on any website. YT Widget — Free Open-Source YouTube Website Embed A free, self-hostable, dependency-free JavaScript library for embedding YouTube feeds, playlists, channel stats, single videos, and live stream status on any website — just like SociableKIT, but 100% free and open-source. Created and provided free to the developer community by R&S Development . ✨ Features 📺 5 Widget Types feed : Latest channel uploads grid or list live : Auto-detects live broadcasts and embeds the live player — shows a custom Offline Card with recent uploads when offline playlist : Show videos from any YouTube playlist stats : Channel metrics cards (Subscribers, Views, Videos count) single : Responsive single video player with metadata 🔒 Secure PHP Server Proxy ( proxy/ ) : Keep your YouTube API key hidden server-side with built-in CORS, rate limiting, and 5-minute response caching. 🎨 Full Color Customization Light & Dark themes Custom Accent / Button… View on GitHub Alternative Downloads & Mirrors: Download on SourceForge ✨ Core Features 📺 5 Widget Types: Switch layouts instantly ( feed grid/list, live stream detector, playlist fetcher, profile stats , or single responsive video). 🔒 Se
AI 资讯
# Why I’m Rewriting a PHP Extension in C23, Not C++
I forked the DataStax Cassandra driver when it stopped compiling on PHP 8 and most of its maintainers had already moved on. My first instinct was to write the new parts in C++. I built a Zend wrapper class, used RAII throughout, and put smart pointers around zval s—the whole modern setup. It introduced memory bugs that took me days to track down, and I did not get a meaningful benefit in return. So the driver is being rewritten in C23. I want to explain why, because “just use C++; it’s safer” is the reflexive answer. For a PHP extension, I no longer think it is the right one. This is not an argument that C++ is a bad language. In an application where I own the allocator, error model, and object lifetimes, std::vector and std::unique_ptr earn their keep. A PHP extension is different: the Zend Engine owns those rules, and its rules are written in C. The problem is not that C++ cannot call the Zend API. Plenty of extensions do. The problem is impedance: each abstraction has to be taught PHP’s lifetime rules, and the teaching code can become more complicated than the work it was meant to simplify. These are the four places where that cost me real debugging time. PHP owns the allocator PHP has its own memory manager. Request-scoped memory is allocated with functions such as emalloc , ecalloc , and safe_emalloc , then released with efree . Zend tracks that memory and normally reclaims what remains at request shutdown. Persistent allocations use a separate API because they have a different lifetime. Plain malloc and free —and therefore ordinary new and delete —sit outside that request-memory model. The moment I put a std::vector<zval> in an extension, its backing storage uses the C++ allocator unless I replace it. The obvious fix is a custom allocator: template < class T > struct PhpAllocator { using value_type = T ; template < class U > PhpAllocator ( const PhpAllocator < U >& ) noexcept {} PhpAllocator () noexcept = default ; [[ nodiscard ]] T * allocate ( std :: size_t
AI 资讯
I stopped hardcoding locales, and adding a language became a one-line change
Most i18n tutorials stop at "put your strings in a JSON file". That covers labels. It does not cover the thing that actually breaks: URLs. I run a personality test site in five locales (French, English, Brazilian Portuguese, European Portuguese, Spanish). Same site, five language trees, roughly 96 articles each. The first version had the shape every project seems to grow by accident: $prefix = $locale === 'en' ? '/en' : '' ; That line, or a cousin of it, spread across controllers, views and helpers. Every one of them was correct when written and wrong the moment a third locale showed up. The bug it produces is the worst kind: nothing throws, the page renders, and the Spanish version quietly links to French URLs. The rule that fixed it One rule, enforced by a test: no locale literal anywhere outside the config file. Not in controllers, not in views, not in helpers. If code needs to know something about a locale, it asks the config. Adding a locale then becomes: content files, plus one entry in config/locales.php . Nothing structural. 'default' => 'fr' , 'available' => [ 'fr' , 'en' , 'pt-br' , 'pt-pt' , 'es' ], Three helpers cover essentially every call site: locale_prefix () // '' for the default locale, '/es' otherwise locale_url ( '/disc' ) // prefixed, canonical, ready to print locale_slug ( 'groupe' ) // the localized route segment The part nobody warns you about: slugs are data Labels translate. Slugs are a different problem, because a slug is simultaneously a URL, a cache key, an SEO asset and a foreign key into your own content. The decision that saved me: one locale is canonical, always. French, in my case. Every slug in every other language resolves back to a French slug before anything else happens. $slugs = app ( SlugService :: class ); $slugs -> toLocale ( 'quatre-tendances' , 'es' ); // 'cuatro-tendencias' $slugs -> resolveToCanonical ( 'cuatro-tendencias' ); // 'quatre-tendances' Without that pivot you get N-to-N translation tables and, eventually, two
AI 资讯
How We Evolved a Cultural Recommendation Feed From a Weighted SQL Ranker to a Narrative Affinity Model
Building a personalization engine for a multi-format content feed, without machine learning, and the testing process that forced us to rebuild it. TL;DR We run a collaborative cultural curation platform (think: user-submitted recommendations for movies, books, games, music, and long-form posts, all mixed into one feed) on a fairly ordinary PHP + MySQL stack. Over about a year we went through two full generations of the feed ranking algorithm. The first version solved the obvious problem (stop being purely chronological) but quietly failed at real personalization. The second version fixed that by rethinking what "user taste" even means, moving scoring out of SQL and into application code, and adding a layer of post-ranking business rules. This post walks through both generations, why the second one had to happen, and how we actually tested and calibrated a feed ranking system without a data science team or an ML pipeline. No exact weights, table names, or formulas below — just the engineering story. The starting problem: one feed, five content shapes Before personalization is even on the table, a multi-format feed has a normalization problem. Movies, books, games, music, and editorial posts live in different tables, with different columns, different publishing cadences, and engagement numbers on completely different scales. "1,000 likes" on a music post and "1,000 likes" on a book review are not the same signal. So the very first architectural decision — before any ranking logic existed — was building a unification layer that maps every content type into a shared shape (type, author, title, cover, category, engagement counters, timestamp) before any scoring happens. Everything downstream depends on that layer being consistent. Generation 1: a weighted ranker living inside a single SQL query The first real version of the algorithm — internally we called it the hybrid model — had a modest goal: get away from a purely chronological feed without building anything resembl
AI 资讯
Add toast messages in Laravel with Wiretoast
Fire toast notifications in Laravel from PHP, Alpine and plain JavaScript with one notify call, plus positioning, auto-dismiss and grouping, and no CSS framework in your bundle Here is a problem I hit on every project. A Livewire action finishes and I need to tell the user it worked, but the toast library I grabbed assumes Tailwind, or ships its own huge runtime, or only works from JavaScript when half my triggers actually live in PHP. Wiretoast is my answer to that, and this post is the fast path to using it. The problem You want to fire a toast from PHP, from Alpine, and from plain JavaScript with the same call, and you do not want to drag a CSS framework into your bundle to get it. How to install Start with Composer, then wire up the assets. I bundle with Vite, so I import the package CSS and JS into my entry files. // resources/js/app.js import ' @wiretoast/js/wiretoast.js ' ; import ' @wiretoast/css/wiretoast.css ' ; That @wiretoast alias is optional, and you set it up by pointing Vite at the vendor resources folder so the imports stay short. // vite.config.js resolve : { alias : { ' @wiretoast ' : path . resolve ( __dirname , ' vendor/edulazaro/wiretoast/resources ' ), }, }, Then the component goes once into your layout, and on the Vite path it injects no tags of its own. <x-wiretoast /> How to use it The fastest possible win is a one-liner in a Livewire component right after something succeeds. The helper is a component macro named notify , registered for you when Livewire is present. $this -> notify ( 'Profile updated' , 'success' ); Under the hood that dispatches a notify browser event, which is exactly what Alpine fires too. So the same toast from a purely front-end button looks like this. <button @ click= "$dispatch('notify', { message: 'Copied', type: 'info' })" > Copy link </button> The five types you can pass are success , error , warning , info and neutral , and a message can be a plain string or an object with a title and a message when you want a he
AI 资讯
Generate your entire Laravel CRUD stack with one Artisan command
TL;DR — composer require bouda/laravel-make-pattern → php artisan make:pattern Post → 9 consistent files in seconds. DDD-ready, rollback included, every stub is yours to override. The problem I kept running into Every new Laravel project starts the same way. You know the architecture you want: Repository, Service, Controller, some Form Requests, a Resource, a Policy, a test. You've written this stack dozens of times. And every time, you either: Copy-paste from a previous project — and immediately introduce inconsistency between how PostRepository is structured vs CategoryRepository . Write everything from scratch — which is slow and error-prone. Use make:model -a — which gives you the Model, Migration, Factory, Controller, but nothing about repositories, services, or policies wired together. None of these feel like the right answer when you want a clean, layered architecture. So I built laravel-make-pattern . What it does One command: php artisan make:pattern Post Generates 9 files : app/Models/Post.php app/Repositories/Contracts/PostRepositoryInterface.php app/Repositories/PostRepository.php app/Services/PostService.php app/Http/Controllers/PostController.php app/Http/Requests/PostStoreRequest.php app/Http/Requests/PostUpdateRequest.php app/Http/Resources/PostResource.php app/Policies/PostPolicy.php tests/Feature/PostTest.php All consistently named, all using the same conventions, all generated from stubs you own and can override . The generated code Here's what the repository looks like out of the box: <?php namespace App\Repositories ; use App\Models\Post ; use App\Repositories\Contracts\PostRepositoryInterface ; class PostRepository implements PostRepositoryInterface { public function all () { return Post :: all (); } public function find ( string $id ) { return Post :: findOrFail ( $id ); } public function create ( array $data ) { return Post :: create ( $data ); } public function update ( string $id , array $data ) { $model = $this -> find ( $id ); $model -> u
AI 资讯
Turn Claude Code into a Laravel expert with LaraClaude
Claude Code writes PHP in Laravel quite well, but it starts every session as a generalist. It does not know your project has three hundred migrations that should be thirty, that a @foreach two files over is firing an N+1, or that your modals follow one specific pattern. You end up re-explaining the same context constantly. LaraClaude packages that context as slash commands. It is a Claude Code plugin with over thirty Laravel skills, each a /lc: command. Install it once and you have audits, scaffolders and cleanup tools that already know Laravel. Here are the ones I run most. How to install LaraClaude installs through Claude Code's plugin system. Add the marketplace once, then install, so you get updates later: /plugin marketplace add edulazaro/laraclaude /plugin install laraclaude@edulazaro Or grab it directly from GitHub: /plugin install github:edulazaro/laraclaude You need Claude Code and a Laravel project. That is it for most skills; a couple that hit a live database also want Docker. Audit before you change anything Most skills default to a read-only report and only touch files when you add fix , so start by looking. /lc:find-n-plus-one scans your Blade views, Livewire components and controllers for a relationship accessed inside a loop, traces it back to the query that built the collection, and tells you the exact with() to add. /lc:find-n-plus-one /lc:security-audit is the other one I run on any project I inherit. It looks for SQL injection, XSS, mass-assignment and secrets committed to the repo, and like most fixable skills it takes a preview flag before it changes anything. /lc:security-audit # report /lc:security-audit fix --dry-run # preview the fixes /lc:security-audit fix # apply, with confirmation Clean up what has piled up Every long-lived Laravel app accumulates migration cruft: a create followed by twenty add_column and change_column files. /lc:consolidate-migrations groups them by table, classifies each table as safe to merge or not, and folds the A
AI 资讯
Add tags and categories to any model with Laraterms
Sometimes you need tags on a model. The usual answer is a tags table, a pivot, a slug and a belongsToMany , and you write it again in the next project with slightly different columns. Laraterms replaces that with a config entry and a trait, and it comes with the parts you normally bolt on later: hierarchy, per-tenant isolation and translations. This is the simple path first, then the two features you reach for next. How to install One package, its config and two migrations. composer require edulazaro/laraterms php artisan vendor:publish --tag = laraterms-config php artisan vendor:publish --tag = laraterms-migrations php artisan migrate Step 1: define a taxonomy A taxonomy is a kind of label, declared in config/laraterms.php . Start with a flat tags taxonomy; the file already ships one you can keep. 'taxonomies' => [ 'tags' => [ 'hierarchical' => false , 'max_terms_per_model' => null , 'scope' => 'tenant' , ], ], Step 2: tag a model Add the HasTerms trait and the model can hold terms. Attaching is find-or-create: pass a label, and the term is created the first time and reused afterwards. use EduLazaro\Laraterms\Concerns\HasTerms ; class Post extends Model { use HasTerms ; } $post -> attachTerm ( 'Laravel' , 'tags' ); $post -> attachTerms ([ 'Laravel' , 'PHP' ], 'tags' ); $post -> syncTerms ([ 'Laravel' , 'Vue' ], 'tags' ); // replace the tag set $post -> termsIn ( 'tags' ); // read them back Filtering by tag is a query scope, so it composes with the rest of your query. Post :: whereHasTerm ( 'laravel' , 'tags' ) -> get (); Post :: whereHasAllTerms ([ 'laravel' , 'tutorial' ], 'tags' ) -> get (); Hierarchical categories Set hierarchical => true on a taxonomy and its terms form a tree. Read the whole tree in one query, and walk a term's ancestry. 'categories' => [ 'hierarchical' => true , 'max_terms_per_model' => 1 , 'scope' => 'tenant' , ], use EduLazaro\Laraterms\Support\TermTree ; $tree = TermTree :: for ( 'categories' ); // roots with children, one query $term -> b
AI 资讯
This Week In PHP Internals | Aug 05, 2026
Hello world, it's Wednesday, August 5, 2026, and here's what happened This Week in PHP Internals. 13 stories this week, so let's get into it. But first, This week's episode is brought to you by Tideways . When a request is slow in production, Tideways takes you from symptom to root cause in minutes, with profiling, tracing, and monitoring built specifically for PHP. It installs in 5 minutes, there's no credit card required, and it's hosted in Germany. Start your free trial at tideways.com . This week's top story: the mass deprecation vote for PHP 8.6 is in its final week. All 35 ballots close Monday , August 10, and Gina P. Banyard posted the 1-week reminder so nobody gets caught out. Most of the 35 are passing comfortably. The interesting ones are the holdouts. list() is now deadlocked at 21 to 21 — a flat tie, nowhere near the 2/3 it needs. Reserving let stands at 22 to 11, which is exactly two-thirds — a single vote in either column decides it. The dechunk filter sits at 17 to 15 — still well short. The gettext _() alias is failing at 9 to 20, and reserving in , out , and inout is failing at 7 to 20, with 12 abstentions. Everything else you'd recognize from the list — the object-parameter cleanups, the is_double() family, spl_classes() — is cruising toward the finish. The thread itself turned into a corrections desk this week. Calvin Buckley relayed a note from Nora, who isn't on the list, pointing out: "The text for the metaphone deprecation isn't fully right. It lists \"linguistics\" as a replacement package, but that one actually uses php-src's metaphone internally too." Weilin Du, who proposed that item, conceded the docs point while standing by the idea, writing: "My point in deprecating it is to stop using ancient metaphone algo as a whole." Voters seem unbothered — metaphone stands at 19 to 6, with 15 abstentions. Rowan Tommins raised a bigger flag on reserving is : it would collide with Hamcrest, the test assertion framework, whose PHP port has 500 millio
AI 资讯
Add Livewire modals in Laravel with Wiremodal
Wiremodal is a framework-agnostic modal package for Laravel, which allows to handle modals, so you don't have co configure them in all your projects. It ships a few Livewire-side helpers that make exactly this pleasant. This post is the Livewire integration end to end: opening and closing from PHP, delivering a payload on open, the one trap to avoid, and the optional form panel for when a modal happens to be a form. How to install Pull the package in and get the assets onto the page. composer require edulazaro/wiremodal php artisan vendor:publish --tag = wiremodal-assets The service provider auto-registers and there is no config file. Point your layout at the published files: <link rel="stylesheet" href="{{ asset('vendor/wiremodal/css/wiremodal.css') }}"> <script src="{{ asset('vendor/wiremodal/js/wiremodal.js') }}" defer></script> If you bundle with Vite, skip the publish and import straight from the vendor directory instead, so a package update flows through without re-publishing anything: /* resources/css/app.css */ @import "../../vendor/edulazaro/wiremodal/resources/css/wiremodal.css" ; // resources/js/app.js import ' ../../vendor/edulazaro/wiremodal/resources/js/wiremodal.js ' ; Opening and closing from Livewire Define the modal once with the <x-wiremodal> component, give it a name , and fill the body and footer slots. Here is a delete confirmation: <x-wiremodal name="confirm-delete" title="Delete record?" size="sm"> <x-slot:body> <p>This action cannot be undone.</p> </x-slot:body> <x-slot:footer> <button type="button" data-wm-dismiss>Cancel</button> <button type="button" wire:click="destroy">Delete</button> </x-slot:footer> </x-wiremodal> The Cancel button carries data-wm-dismiss , and any element with that attribute closes the modal it sits in, so you never write a cancel handler. To open and close from the component itself, use the macros the package registers on every Livewire component: public function confirmDelete (): void { $this -> openModal ( 'confirm
AI 资讯
Your Laravel Models Aren’t the Problem. Hidden Workflows Are.
Most Laravel applications do not become difficult to maintain because somebody made one obviously terrible architectural decision. They get there through dozens of small decisions that looked completely reasonable at the time. A controller grows beyond a comfortable size, so part of its logic moves into an Eloquent model. Later, the application needs to send a notification after an order ships. Then inventory must be checked through an external API. Accounting requests an ERP integration, and marketing wants loyalty points awarded after dispatch. Each change adds only a few lines. Since the Order model is already available wherever the feature is being implemented, adding one more method feels natural: $order -> ship (); It is concise, expressive, and pleasantly object-oriented. Six months later, however, that innocent method may be updating several tables, calling two APIs, dispatching events, sending notifications, and deciding whether the entire operation is allowed. The model has quietly become the place where the application runs its business workflows. That is the real problem. It is not that the model is “fat.” It is that persistence, domain decisions, and application orchestration have been mixed together until nobody can change one without understanding all three. Business Logic in Eloquent Models Is Not Automatically Wrong The common advice to remove all business logic from Laravel models goes too far. Eloquent follows the Active Record pattern. Martin Fowler describes Active Record as an object that represents a database row, encapsulates database access, and adds domain logic related to that data. In other words, an object containing both state and behavior is not an architectural mistake by itself. A model should be allowed to answer questions about itself: class Order extends Model { protected function casts (): array { return [ 'paid_at' => 'datetime' , 'expires_at' => 'datetime' , ]; } public function isPaid (): bool { return $this -> paid_at !== nul
AI 资讯
When "select all" checkboxes don't actually select anything — verifying after `check()`, not just trusting it
WordPress's plugin and theme update screens both have a "select all" checkbox. Calling check() on it with Playwright succeeds — no error, no exception. But look at the individual checkboxes afterward, and sometimes none of them are actually checked. Note: Playwright's check() ticks a checkbox. The click itself can succeed even if the page's JavaScript handler never fires, leaving what the form actually submits out of sync with what the screen visually shows. What actually happens The "select all" checkbox is usually wired up with a JavaScript handler: clicking it is supposed to check every individual checkbox underneath it. Playwright's check(force=True) can force the DOM state of that one checkbox — but that only changes that checkbox's own state . It doesn't guarantee the JavaScript handler that's supposed to propagate the change to the individual checkboxes actually fires. # Looks like it worked, but the individual checkboxes are still unchecked select_all . first . check ( force = True ) page . click ( ' input[type= " submit " ][name= " upgrade " ] ' ) Clicking the update button submits whatever the form's actual state is — which is "nothing checked." Nothing updates. No error is thrown, so on the surface it looks like the run completed normally. The fix — verify right after checking, every time Right after checking "select all," confirm that the individual checkboxes underneath are actually checked. If they aren't, fall back to checking each one individually. sel_all_sel = ' input[type= " checkbox " ][id^= " plugins-select-all " ] ' select_all = plugin_form . locator ( sel_all_sel ) if select_all . count () > 0 : select_all . first . check ( force = True ) page . wait_for_timeout ( 500 ) # Verification step — confirm checkboxes are actually checked chk_sel_check = ' input[type= " checkbox " ][name= " checked[] " ]:checked ' any_checked = plugin_form . locator ( chk_sel_check ). count () > 0 if not any_checked : # Select-all had no effect; switch to individual s
开发者
Atomic Money: Making a PHP/MySQL Wallet Safe Under Concurrency
The lost-update bug that quietly corrupts homegrown wallet balances — and the five disciplines we used across PayWithToken to make money movement correct under concurrency. There is a bug that lives in a large share of the world's homegrown wallet systems. It doesn't throw an error. It doesn't show up in tests. It surfaces months later as a balance that is quietly, inexplicably wrong — and in a payments system, a wrong balance is either a customer who has lost money or a company that has given it away. This is the story of that bug, why the "obvious" wallet code causes it, and the handful of disciplines we used across PayWithToken to make money movement correct under concurrency. The bug: lost updates Here is wallet code almost everyone writes first. Credit a user's balance: // DON'T do this $row = $db->query("SELECT balance FROM users WHERE id = $id")->fetch(); $new = $row['balance'] + $amount; $db->exec("UPDATE users SET balance = $new WHERE id = $id"); Read the balance, add to it in PHP, write it back. It works perfectly — until two things happen at the same time. Picture a wallet at ₦1,000. Two credits of ₦500 arrive simultaneously — say a bank webhook and the user tapping "confirm" on their phone: Request A reads balance = 1000. Request B reads balance = 1000 (A hasn't written yet). A computes 1500, writes 1500. B computes 1500, writes 1500. Two credits landed; the balance rose by ₦500. ₦500 vanished. This is a lost update, and it is a race condition, which means it is invisible until you have real concurrent traffic — exactly when you can least afford it. The debit version of the same bug lets a balance go negative or double-spends a token. Fix #1: let the database do the arithmetic The read-modify-write happened in PHP, across three round trips, with a gap where another request could interleave. The fix is to make the update a single atomic statement and let the database's row lock serialise it: // DO this — one atomic statement $db->prepare("UPDATE users SET
AI 资讯
When `update-core.php` version scraping goes wrong — telling a plugin version number apart from WordPress core
On hosting without SSH access, a common pattern is to open update-core.php (the WordPress update screen) with Playwright and read "what version is WordPress core currently running" straight off the page text. In one deployment, the recorded core version came back as something like 1.7.11 — a number that has never existed for WordPress core. Note: update-core.php is the WordPress admin's "Updates" page, listing pending updates for core, plugins, themes, and translations all on one screen. What was actually happening That page doesn't only show the core version string — it's packed with version numbers belonging to pending plugin and translation updates too. A line like "Update Plugin X to 1.7.11" is typical. # First implementation — grabs the first N.N.N-shaped number on the page match = re . search ( r ' \d+\.\d+(?:\.\d+)? ' , page_text ) version = match . group ( 0 ) if match else None This naive regex grabs whatever N.N.N -shaped number appears first on the page. Because of how the page is laid out, a plugin's pending update can render above the core version message, so 1.7.11 (a plugin's version) ended up recorded as the WordPress core version. Why this is hard to catch The bug doesn't throw an exception — the regex matches successfully, just on the wrong value. Nothing about it looks broken until someone notices the report shows a version that WordPress core has never shipped (there's no 1.x series for core). The fix — a three-stage guard Prefer a dedicated selector first — look for specific DOM locations where core's update message actually renders, like p.response > strong or #wp-version-message strong Fall back to keyword-anchored regex — if no selector matches, only accept a number immediately following the words "WordPress," "バージョン," or "Version" Validate plausibility as a final check — whichever path produced a value, run it through a function that checks the major version number falls within 4–9 def is_plausible_wp_core_version ( ver : str ) -> bool : m =
AI 资讯
Building Laravel NATS: A Modern, Production-Ready NATS Integration for Laravel
Building Laravel NATS: A Modern, Production-Ready NATS Integration for Laravel When building distributed systems, one of the biggest challenges is enabling services to communicate reliably without creating tight coupling. Laravel has excellent support for queues, events, broadcasting, and jobs, but when it comes to NATS , the ecosystem has been relatively limited. That's exactly why I built Laravel NATS . Instead of being just another wrapper around an existing PHP client, Laravel NATS aims to provide a Laravel-first developer experience while exposing the full power of NATS for modern event-driven architectures. In this article I'll explain: Why I built Laravel NATS Why you should consider NATS How Laravel NATS works Features that make it production ready Code examples Real-world use cases What makes this package different from existing solutions What is NATS? NATS is a lightweight, high-performance messaging system designed for cloud-native applications. Unlike traditional queues, NATS focuses on: Extremely low latency High throughput Simple publish/subscribe messaging Request/Reply APIs JetStream persistence Horizontal scalability Instead of applications calling each other directly: Order Service │ ▼ Notification Service Applications publish events: Order Service │ ▼ NATS Server │ │ ▼ ▼ Email Analytics Every service becomes independent. Why Laravel Needed a Better NATS Package Most existing packages expose the underlying PHP client almost directly. That means developers still have to understand: client lifecycle connections serialization subscriptions queue consumers JetStream APIs Laravel developers expect something different. We are used to APIs like: Cache :: put (); Queue :: push (); Event :: dispatch (); The goal of Laravel NATS was to make NATS feel just as natural. Installing Laravel NATS Installation is straightforward. composer require zaeem2396/laravel-nats php artisan vendor:publish --tag = nats-config Then configure your environment: NATS_HOST=127.0.0
AI 资讯
GDPR cookie consent in Laravel with Wirecookies
Ship a compliant cookie banner in Laravel and actually gate analytics and marketing scripts on the user's choice, using the wirecookies-saved event and a plain localStorage object as the consent gate. Wirecookies is a Laravel package which handles the cookies consent for you. It gives you a consent banner and a preferences modal from a single Blade tag, and, more usefully, it hands you a plain localStorage object and a browser event you can use as the gate for your analytics and marketing scripts. This article is built around that gate, not around how the banner looks. One thing to get out of the way first, because it will bite you otherwise: Wirecookies ships no JavaScript of its own and uses wiremodal's JS to open the preferences modal. If you skip the wiremodal import in the install steps, the banner still shows and Accept all / Reject all still work, but the Configure button and the floating re-open button silently do nothing, with no error in the console. Do the JS step. How to install Pull the package in with Composer. The service provider is auto-discovered, so there is nothing to register. composer require edulazaro/wirecookies Wirecookies depends on edulazaro/wiremodal , which Composer pulls in for you. Now import the stylesheet in resources/css/app.css , after a wire* base (wiremodal or wiretoast) that defines the theme tokens. /* resources/css/app.css */ @import '../../vendor/edulazaro/wiremodal/resources/css/wiremodal.css' ; @import '../../vendor/edulazaro/wirecookies/resources/css/wirecookies.css' ; Then bundle wiremodal's JS. This is the step that makes the Configure and re-open buttons work, so do not skip it. // resources/js/app.js import ' ../../vendor/edulazaro/wiremodal/resources/js/wiremodal.js ' ; How to use it Drop the single Blade component once, near the end of your layout. <x-wirecookies :policy-url="route('cookies')" /> First-time visitors get a bottom banner after a short delay. When they choose Accept all, Reject all, or save from the Con
AI 资讯
60 AI-written WordPress plugins, and JavaScript escaping that is safe by accident
This post has two halves: what "escape your output" actually means once the obvious answer stops working, and then a study of whether current AI assistants get that harder version right. It continues a series on what coding assistants actually produce when a non-expert asks them for WordPress code. Start with a line that passes review and still leaves a hole: echo '<a href="' . esc_html ( $url ) . '">Visit</a>' ; There is an escaping function wrapped around the value. A grep for esc_ finds it. A quick review passes it. Now set $url to javascript:alert(document.cookie) . The link still runs when the visitor clicks it. esc_html() escapes characters that matter in HTML text, like < and > . It does nothing about a dangerous URL scheme. The code is escaped. It is escaped for the wrong context. From the outside, correct escaping and wrong-context escaping look identical. A URL is a different context and needs its own escaper. That escaper is esc_url() , and it returns an empty string for a javascript: URL. Which escaper goes where Escaping is context-dependent. "Sanitize on input, escape on output" is the rule the first post of this series covers, but "escape on output" hides a second decision: which escaper. The answer depends on where the value lands on the page. Where the value lands Escaper Visible text between tags ( <p>HERE</p> ) esc_html() Inside an attribute ( title="HERE" ) esc_attr() A URL slot ( href="HERE" , src="HERE" ) esc_url() , which also enforces a safe scheme Inside a tag attribute that holds JavaScript ( onclick="greet('HERE')" ) esc_js() , scoped to this slot by core's own documentation Any value handed to JavaScript inside a <script> block ( var data = HERE ) wp_json_encode() , which writes its own quotes; add JSON_HEX_TAG when the value is untrusted HTML you want to keep, like a formatted post body wp_kses() with an explicit allow-list Six functions, one job each. The mistake is almost never "forgot to escape". It is "escaped for the wrong context",
AI 资讯
Your A/B test has three goals and they disagree. Now what?
Every A/B testing tutorial ends the same way: run the test, wait for significance, ship the winner. Then you run a real test and variant B converts 12% better on newsletter signups, brings in 4% less revenue per visitor, and bounce is flat. Nothing is significant except the signups. Ship it? I spent an embarrassing amount of time on this question while building an A/B engine, and most of what I read online didn't help, because most of it assumes one metric. This post is what I ended up with. It's not novel — the statistics are decades old — but I couldn't find it written down in one place with working code, so here it is. Why the p-value doesn't answer the question you're asking Two problems, and the second one is the bad one. Multiple comparisons. Three metrics at α = 0.05 means roughly a 14% chance of at least one false positive if nothing is actually different. Bonferroni fixes this, but now you need α = 0.017 per metric and your test needs to run three times as long. On a site doing 300 conversions a month that's not a fix, it's a refusal. The p-value is answering a different question. It tells you the probability of your data assuming no difference exists. What you actually want to know is: if I ship B, how much do I expect to lose if I'm wrong? Those are not the same question and no amount of Bonferroni turns one into the other. There's also the peeking problem — everyone checks the dashboard daily and stops when it goes green, which quietly inflates the false positive rate well past whatever α you wrote down. I'll come back to that, because Bayesian methods do not magically solve it, whatever you may have read. Posterior first, decision second For a conversion rate, the Beta-Binomial conjugate pair gives you the posterior in one line. With a uniform prior, after c conversions out of n visitors: p | data ~ Beta ( 1 + c , 1 + n - c ) That's it. No closed-form comparison between two Betas that's worth implementing, so sample. PHP has no Beta sampler in core, and