
r/PHP

Polling API RFC is now in voting phase
This RFC is now in the voting phase. Lowkey, it could have a massive impact on PHP, especially by bringing more modern backend stream handling. It would also replace stream_select() and make async PHP libraries far more viable without relying on extensions like ext-uv to bypass file descriptor limitations and O(N) performance.
I've been optimizing a PHP server written in C, here's what makes it fast
TrueAsync 0.7.0 is shipping very soon, with a thread pool and a few other features. But the most interesting part is probably TrueAsync Server: a high-performance HTTP/1.1, HTTP/2 and HTTP/3 server embedded directly into PHP.
Benchmarks: https://www.http-arena.com/leaderboard/
Everything in one thread
The whole request lifecycle (parse, dispatch, respond) happens on a single thread. Same model as NGINX, Node.js, or Rust's Tokio: one thread owns the connection and the request end to end. There is no handoff between an accept thread and a worker thread, no locks, no context switches.
Why C?
It embeds straight into PHP, links against the OpenSSL already in your build, and uses the de-facto-standard C libraries: nghttp2 (HTTP/2), ngtcp2 plus nghttp3 (HTTP/3), llhttp (the same HTTP/1 parser Node.js uses). It runs on the Zend VM, so server memory and PHP memory share one memory_limit.
Multi-protocol, one port
HTTP/1.1, HTTP/2, WebSocket, SSE and gRPC share a single TCP port and event loop (protocol picked via ALPN or HTTP Upgrade). HTTP/3 runs on the same UDP port and gets advertised via Alt-Svc. One $server->start() serves all of them.
The API is two classes
$server = new HttpServer(
(new HttpServerConfig())->addListener('0.0.0.0', 8080)
);
$server->addHttpHandler(function ($request, $response) {
$response->setStatusCode(200)->setBody('Hello, World!');
});
$server->start();
Each handler runs in its own coroutine (one per request on H1, one per stream on H2/H3). When a handler awaits a DB query, it blocks nothing else.
Streaming is first-class. $res->send($chunk) pushes data straight onto the wire: Transfer-Encoding: chunked on H1, DATA frames on H2/H3, same handler code either way. Great for SSE, big exports, gRPC. There is even $res->sendable() for per-stream backpressure. Request bodies stream too via $req->readBody(), so you can proxy a multi-GB upload without ever holding it in memory.
Where the speed comes from
Not one big trick, just a pile of small ones:
- Pooling everywhere. Body buffers, encoders, streams, connection slots. The allocator barely gets touched on repeat requests.
- Geometric buffer growth. PHP's
smart_strhas a hidden cliff where every grow becomes a syscall whose cost scales with size. On large bodies that ate up to half the request time. - Zero-copy hot paths. Multipart parses in place,
sendfile()for large files. - A static file handler that never enters the PHP VM at all. Pure C state machine, zero-copy
sendfile, built-in MIME table, ETags, range requests, precompressed sidecars. The slowest part of any PHP server is PHP, so for static assets it just skips it.
The whole point is to keep the server invisible relative to the actual workload. It lives between coroutines: while your PHP waits on the database, it is already accepting the next request.
What's next
It's early-stage but you can try it today. WebSocket, gRPC and telemetry are coming over the next few months. The repo is on GitHub under true-async. Happy to answer questions in the comments.
Git: https://github.com/true-async/server
Additional: https://medium.com/@edmond.ht/trueasync-server-e6ed1ae9e8ec
How I patched Playwright level to bypass fingerprinting anti-bot
github.comZealPHP — modernizing the PHP request model with an OpenSwoole runtime
I'm building ZealPHP, an open-source PHP framework on top of OpenSwoole. MIT licensed, alpha but usable.
Not trying to replace Laravel/Symfony. Not another MVC framework experiment. The goal is to modernize the traditional PHP request model itself.
In the classic LAMP / PHP-FPM model, Nginx/Apache forwards the request to PHP, PHP handles it, the process context dies. Simple and reliable — but every "modern" feature your product needs (WebSocket, queues, Redis for shared state, cron, SSE streaming) becomes a separate moving part. Six services, six failure points, six config files.
ZealPHP explores a different model: PHP runs as a long-running OpenSwoole-powered runtime and natively handles HTTP, WebSocket, SSE, sessions, shared memory (OpenSwoole\Table), timers, task workers, and coroutine-based I/O — all in one php app.php.
Mental model I'm aiming for: keep the simplicity PHP devs liked from the LAMP era, give PHP a modern async runtime.
What's in the repo:
- ~117k req/s text, ~106k req/s JSON on 4 workers with full PSR-15 middleware stack (CORS, ETag, sessions, routing). Methodology and reproduction scripts are in
PERF.md— happy to be told where I'm wrong. - Legacy code compatibility:
session_start(),header(),$_GET,echoall work as expected inside coroutines viauopzoverrides. - WordPress runs unmodified on it via a CGI worker (Apache mod_php compat layer). Zero WP code changes. That's the real test for whether the migration story holds.
- Built on OpenSwoole 22.1+, PHP 8.3+
Learn section — a handcrafted step-by-step where you build a real Personal Notes + AI Chat app using ZealPHP, htmx, server-rendered PHP components, sessions, notes CRUD, AI chat, and real-time sync. Trying to teach the framework through a realistic app, not toy examples.
Links:
- Site: https://php.zeal.ninja
- Learn (build a real app): https://php.zeal.ninja/learn
- Migration ladder: https://php.zeal.ninja/migration
- WordPress on ZealPHP: https://php.zeal.ninja/legacy-apps
- Repo: https://github.com/sibidharan/zealphp
What I'd actually like this sub to weigh in on:
- Does the "modernized LAMP request model" framing make sense, or does it muddy the pitch?
- Are the PHP-FPM-vs-OpenSwoole-runtime claims fair, or do they overclaim?
- Does the gradual legacy migration idea feel practical to people who've actually maintained big PHP codebases?
- Is htmx + server-rendered PHP components a sound teaching direction, or am I betting on the wrong horse?
- What would make you trust — or distrust — a long-running PHP app runtime in production?
Honest about where it is: alpha, v0.2.x, APIs may shift before 1.0. Not asking anyone to put it in production tomorrow. Asking whether the architecture and migration approach are sound before I push for v1.0.
Roast welcome.
I built a minimal TUI/GUI PHP version manager for Linux so I could stop typing `update-alternatives`
I got tired of manually juggling update-alternatives between my different side projects. I know Docker exists, but I wanted a native nvm like experience for my personal PHP projects.
So, I built phpvm to scratch my own itch. Would love your feedback!
What it does:
- Auto-switches: Reads
.php-versionorcomposer.jsonand changes your PHP version when youcdinto a directory. - TUI: Type
phpvmfor a fast, interactive terminal menu. - GUI: A system tray icon to check xdebug status and instantly restart FPM.
Released v4.0 stable of a Redis-backed roles & permissions package for Laravel — drop-in API compatible with Spatie
TL;DR: Just released v4.0.0 stable of laravel-permissions-redis, a Redis-backed alternative to spatie/laravel-permission. Same API (hasRole, hasPermissionTo, @permission, middleware), different cache strategy: every user→roles→permissions mapping lives in Redis as a SET, and checks become O(1) SISMEMBER calls. ~10x faster on the median request vs Spatie on identical workloads. Three weeks in production without surfacing a new bug.
Why I built it
Spatie is excellent and remains the default — but in apps with heavy authorization on the hot path I kept hitting the same pattern: every request lazy-loads the authenticated user's roles and permissions (one DB query) and then deserializes the cached permission array and scans it. Fine for most apps. Painful for high-traffic APIs.
I wanted a package where the authorization check itself is a constant-time Redis lookup and where invalidation is surgical, not "drop the entire cache and let the next N requests pay the rebuild cost simultaneously".
How it differs
| Aspect | spatie/laravel-permission | laravel-permissions-redis |
|---|---|---|
| Warm check | Deserialize cached array → scan for match | SISMEMBER (O(1)) |
| Cache scope | Global permission registry | Per-user/per-role SETs |
| Invalidation | forgetCachedPermissions() — drops everything |
Rewarm only affected user/role |
| After invalidation | Next request rebuilds full cache | Cache is already warm |
| Per-request | Hits cache driver every call | In-memory cache, no repeated Redis |
Benchmarks
Numbers from a standalone benchmark app that runs both packages side-by-side. Apple Silicon, PHP 8.4, predis, 5 warm-ups + 30 measurement runs, GC reset between runs, median reported.
Scenario (1 iter = 27 hasPermissionTo + 4 hasRole + 4 batch ops + 2 collections) |
Spatie | This package | Speedup |
|---|---|---|---|
| 1 iteration | 14.27 ms | 1.44 ms | 9.92x |
| 10 iterations | 144.38 ms | 14.39 ms | 10.03x |
| 50 iterations | 730.88 ms | 72.87 ms | 10.03x |
DB queries per request: 4 → 1 (75% reduction).
The ~10x holds because both strategies scale linearly — what differs is the per-iteration constant (4 DB queries vs 1 Redis lookup).
What 4.0 brings
- Permission group metadata preserved in Redis with atomic rebuild (
replacePermissionGroups) - Guard-aware Blade directives and full multi-guard isolation
- Queue-backed cache warming (
WarmUserCacheJob) permissions-redis:migrate-from-spatieartisan command for one-shot data migration- Contract suite running against a real Redis instance (not mocks) — auto-skips when Redis isn't available
- Multi-tenancy (
stancl/tenancyintegration + custom resolvers), UUID/ULID support, Laravel Octane support
PHP 8.3+, Laravel 11/12/13, PHPStan level max, Pest 4, mutation testing via Infection.
When NOT to use it
Honest list, because this matters more than the speedups:
- You don't run Redis. This package requires it. If Redis isn't in your stack, Spatie's database/file-cache approach is the right choice.
- You need
getDirectPermissions()/getPermissionsViaRoles()at runtime. This package merges them by design. - You need Spatie's
teamsfeature. The multi-tenancy here is different — Redis namespace isolation, not team-scoped role assignments. - You're on PHP < 8.3 or Laravel < 11. No backport planned.
- Authorization isn't a bottleneck. If your app is low-traffic, the DB-backed approach is fine — don't add Redis just for this.
Links
- Repo: https://github.com/scabarcas17/laravel-permissions-redis
- Packagist: https://packagist.org/packages/scabarcas/laravel-permissions-redis
- Benchmark app: https://github.com/scabarcas17/laravel-permissions-redis-benchmark
- Migration guide from Spatie: in the repo's
docs/folder
Happy to answer architecture questions or hear what breaks if you try it. Roast welcome.
bare repo git pattern when working with Symfony?
Hi all,
I'm using Laragon for local development and currently learning Symfony. SymfonyCasts has been great so far.
I recently learned about using a bare Git repository with multiple git worktree checkouts, and I'm wondering whether anyone uses this workflow when developing Symfony apps locally.
Normally, if I create a project at:
www/my-project
Laragon automatically gives me a local domain like:
https://my-project.test
But with a bare repo / worktree setup, I imagine the structure would be more like:
www/my-project/
repo.git/
main/
public/
feature-1/
public/
feature-2/
public/
Since Symfony's document root needs to point to each worktree's public/ directory, I'm not sure what the best local setup would be.
Do people usually create a separate Apache virtual host for each worktree, for example:
my-project-main.test -> www/my-project/main/public
my-project-feature-1.test -> www/my-project/feature-1/public
Or is there a better/common way to use Git worktrees with Symfony and Laragon?
I'm mainly trying to avoid constantly switching branches in one working copy, while still being able to test different worktrees easily in the browser.
Thanks!
fastjson 0.3.0: drop-in faster ext/json for PHP, backed by yyjson (6× encode, 2.7× decode, 5× validate)
I maintain fastjson, a native PHP extension that drops in next to ext/json with a namespaced fastjson_* API and the same flag/error model. 0.3.0 just landed.
The problem. ext/json leaves a lot of performance on the table on both encode and decode paths. High-throughput PHP APIs spend a non-trivial fraction of CPU budget in JSON serialization. The two existing escape hatches are uncomfortable: simdjson_php is decode-only and not API-compatible, and rolling a custom validator is fragile.
The shape. fastjson exposes fastjson_encode/decode/validate behind a namespaced API that mirrors ext/json's. Backed by yyjson 0.12.0 (MIT). PHP 8.3 minimum; coexists with ext/json so adoption is opt-in per call site, not a repo-wide flag day.
Numbers. Full simdjson_php canonical 14.8MB corpus, 15 files, i9-13950HX, release builds of both PHP (8.6.0-dev) and fastjson:
- Decode (stdClass): 602 MB/s vs ext/json 227 MB/s = 2.66×
- Decode (assoc array): 628 MB/s vs ext/json 237 MB/s = 2.65×
- Encode: 1,092 MB/s vs ext/json 180 MB/s = 6.06×
- Validate: 1,352 MB/s vs ext/json 265 MB/s = 5.10×
Visual side-by-side (also includes ext/json (https://github.com/php/php-src/pull/17734) SIMD encode and simdjson_php on the same PHP build): https://iliaal.github.io/fastjson/baseline.html
Drop-in mechanics. fastjson_* signatures track ext/json. JSON_* flags and JSON_ERROR_* constants match byte-for-byte; fastjson_last_error mirrors json_last_error. Migration is search-and-replace.
Honest tradeoff. Decode and validate hold the yyjson doc in memory alongside results. Decode peak heap is ~1.7× ext/json's. Validate peak is ~101× ext/json's streaming validator (constant ~80 bytes), already 2.7× better than yyjson's stock read path thanks to a vendored patch. Encode is one-stage (direct write into smart_str), peak ~1.06× ext/json. If you are validate-heavy on huge inputs under tight memory_limit, the memory profile is a real consideration. For most callers the speedup wins.
What 0.3.0 does:
- ~36% speedup on object-heavy decode under JSON_INVALID_UTF8_IGNORE/SUBSTITUTE. A no-alloc UTF-8 validator scans each string and object key first; the sanitizer only runs on byte sequences that actually need replacement. Valid UTF-8 inputs no longer pay a per-string sanitize allocation and copy.
- HEX flag rewrites (JSON_HEX_TAG/AMP/APOS/QUOT) now scan first and skip the rewrite entirely when no candidates exist. ~4× faster on the no-hit case (532 µs to 125 µs on 1k strings), ~13% regression on the all-hit case from the extra scan pass.
- dw_emit_double checks the cheap range bound before calling floor(). Non-integer or out-of-range doubles in number-heavy arrays no longer pay libm per element.
- Two correctness fixes: a use-after-free in fastjson_encode for PHP 8.4 objects whose property has a SET hook but no GET hook (engine's trivial-read fast path returns a borrowed pointer; the previous stash logic freed it), and a 32-bit zend_long overflow on the integer-valued-double shortcut that could emit INT32-saturated garbage for things like fastjson_encode(1e10).
- ext/json parity for integer-valued doubles between 1e15 and 1e17. fastjson_encode(1e16) now emits "10000000000000000" instead of yyjson's "10000000000000000.0", matching json_encode.
Install via PIE:
pie install iliaal/fastjson
Repo: https://github.com/iliaal/fastjson
Open to feedback on flag coverage and edge cases, especially anyone running JSON_INVALID_UTF8_IGNORE or the HEX flags on hot paths.
JetBrains PHPverse 2026 - Bringing the PHP Community Together
lp.jetbrains.comBuilding High Performance Self Healing Process Pool with Hibla Parallel
Hello everyone, I just wanted to share a past article I wrote about this topic. I’d love to hear your thoughts, feedback, or any questions you may have.
Who's hiring/looking
This is a bi-monthly thread aimed to connect PHP companies and developers who are hiring or looking for a job.
Rules
- No recruiters
- Don't share any personal info like email addresses or phone numbers in this thread. Contact each other via DM to get in touch
- If you're hiring: don't just link to an external website, take the time to describe what you're looking for in the thread.
- If you're looking: feel free to share your portfolio, GitHub, … as well. Keep into account the personal information rule, so don't just share your CV and be done with it.
Announcing the Ecosystem Security Team at The PHP Foundation
thephp.foundation#[Reaped]? #[Pooled]?
PHP 8.4 was a game changer for PHP, and not just syntactically. Lazy/ghost objects, and the ability to reset them, is the 1st real opportunity we've had to manage memory in long-running PHP.
Hot take: the more recent focus on functional and generics are just adding to the maintenance burden and moving the focus in the wrong direction.
Fluent method chaining and PHPStan get us most of the way there; the rest is a trade off for very little net gain.
Can we circle back and focus on what's already there -- what PHP8 through 8.4 already gave us?
Some example ideas:
`#[Reaped]` or `#[Pooled]`
Gives class memory back or frees it to be reused.
(ZMM challenges aside)
`#[Stacked]` or `#[Record]`
Stack-allocated objects, and the semantics to go along with it.
__lazy() and/or __ghost()
Provides a lazy/ghost constructor - likely providing helpful args passed in, respectively.
Easier said than done..I'm aware.
It seems the direction of the language is a bit ADD at this point though.
PIE is now finding its footing too.
With some thoughtful leverage and iteration, PHP could really find its own stride.. instead of becoming C#'s slower cousin. The '$' that embodies PHP spirit is slowly becoming '$$$'.
Rant over...thx for reading
Namespaces, interfaces and stutter
Hi all. I wanted to see what the opinion is on the following situation:
Lets say your team targets removing suffixes from interfaces (so no Interface suffix), while also avoiding namespace/class stutter. You then have an example like this:
You have a Payment namespace with a Client interface that drives certain interactions with different payment providers (such as Stripe). Within the Payment namespace you have a Stripe folder that has the payment provider's implementation of the Client interface. To avoid namespace stutter, you end with with this:
namespace App\Payment;
interface Client
namespace App\Payment\Stripe;
use App\Payment\Client as ClientInterface;
class Client implements ClientInterface
So the Stripe client is App\Payment\Stripe\Client and not App\Payment\Stripe\StripeClient to reduce namespace stutter.
To avoid collision you have to now alias the interface, which introduces its own readability issues.
Should namespace stutter just be accepted here, or is there a better way of handling it?
What PHPStan level and why? Your thoughts on the cost-benefit analysis
A simple question: at what level do you use PHPStan, and do you think it’s worth going beyond a certain threshold?
I’d like to understand where the point of diminishing returns lies between the time spent typing/rewriting and the bugs actually prevented.
- Level 5-6: many projects stop there. Is this a good balance, or is it just laziness to go any higher?
- Level 7-8: things start to get tricky with union types and nullables. Does this have a tangible impact on code quality, or does it mainly become a hunt for warnings?
- Level 9-10: the war on mixed types. For those of you at this level, have you actually seen any concrete benefit? Bugs that would have slipped through the net at lower levels? Because on paper it’s stricter, but in practice, is the investment justified?
Do some of you use hybrid strategies (baseline + max level, or strict rules on only part of the code)?
I’m trying to gather a range of feedback to form an objective view. There are no right or wrong answers; I just want to know what you do in real life and why.
Building a JSONPath PHP Extension — Useful?
I’m thinking about building a PHP c extension for JSONPath querying, inspired by zig-jsonpath.
Example:
<?php
/*
|--------------------------------------------------------------------------
| JSONPath examples
|--------------------------------------------------------------------------
|
| JSONPath allows querying nested JSON values using expressions.
| Similar to XPath but for JSON.
|
*/
$json = '{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
}';
/*
|--------------------------------------------------------------------------
| Get all authors
|--------------------------------------------------------------------------
*/
jsonpath_get($json, '$.store.book[*].author');
/*
Result:
[
"Nigel Rees",
"Evelyn Waugh",
"Herman Melville"
]
*/
/*
|--------------------------------------------------------------------------
| Get first book title
|--------------------------------------------------------------------------
*/
jsonpath_get($json, '$.store.book[0].title');
/*
Result:
"Sayings of the Century"
*/
/*
|--------------------------------------------------------------------------
| Recursive search
|--------------------------------------------------------------------------
*/
jsonpath_get($json, '$..price');
/*
Result:
[
8.95,
12.99,
8.99,
19.95
]
*/
/*
|--------------------------------------------------------------------------
| Filter books cheaper than 10
|--------------------------------------------------------------------------
*/
jsonpath_get($json, '$.store.book[?(@.price < 10)]');
/*
Result:
[
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
}
]
*/
/*
|--------------------------------------------------------------------------
| Books with ISBN
|--------------------------------------------------------------------------
*/
jsonpath_get($json, '$.store.book[?(@.isbn)]');
/*
Result:
[
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
}
]
*/
/*
|--------------------------------------------------------------------------
| Get all objects in store
|--------------------------------------------------------------------------
*/
jsonpath_get($json, '$.store.*');
/*
Result:
[
[...books...],
{
"color": "red",
"price": 19.95
}
]
*/
/*
|--------------------------------------------------------------------------
| Array slice
|--------------------------------------------------------------------------
*/
jsonpath_get($json, '$.store.book[:2]');
/*
Result:
First 2 books
*/
/*
|--------------------------------------------------------------------------
| Last book
|--------------------------------------------------------------------------
*/
jsonpath_get($json, '$.store.book[-1]');
/*
Result:
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
}
*/
/*
|--------------------------------------------------------------------------
| Wildcard everything
|--------------------------------------------------------------------------
*/
jsonpath_get($json, '$..*');
/*
Result:
Returns all values recursively
*/
Goals:
Faster than pure PHP
Low memory usage
wildcard/filter support
Maybe query raw JSON without full decoding
Would this be useful for:
1.WordPress
Laravel
API projects
Would love opinions before I start building
Alternatives to Bun now that it is absolute AI slop?
I've been using Bun for my projects for a while now because it's so easy to integrate into NodeJS projects and is incredibly fast and easy. But now that it is AI slop, I feel obligated to switch. Any alternatives?
I would pick Deno but I've noticed it doesn't have the best compatibility with NodeJS. Should I just switch back to NodeJS w/ pnpm or is there some other niche project that isn't AI slop?
EDIT: I recently noticed vlt, but it appears that they are using AI for pull requests as well?