Skip to content

Commit 6038900

Browse files
committed
Migrate to http-handler and http-rewriter
1 parent 61249d9 commit 6038900

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1588
-5090
lines changed

CLAUDE.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
@platformatic/php-node is a Node.js native addon that embeds PHP within the same process as Node.js, enabling seamless communication without network overhead. It's built with Rust using NAPI-RS for safe and performant bindings.
8+
9+
## Essential Commands
10+
11+
### Development
12+
```bash
13+
# Build release version
14+
npm run build
15+
16+
# Build debug version (faster compilation, includes debug symbols)
17+
npm run build:debug
18+
19+
# Run all tests
20+
npm test
21+
22+
# Run specific test file
23+
npx ava __test__/headers.spec.mjs
24+
25+
# Lint JavaScript code
26+
npm run lint
27+
28+
# Create universal binary (macOS)
29+
npm run universal
30+
31+
# Version bump
32+
npm run version
33+
```
34+
35+
### Rust-specific builds
36+
```bash
37+
# Build with proper rpath for linking libphp
38+
RUSTFLAGS="-C link-args=-Wl,-rpath,\$ORIGIN" npm run build
39+
40+
# Build specific crate
41+
cargo build --manifest-path crates/php_node/Cargo.toml --release
42+
```
43+
44+
## Architecture
45+
46+
### Multi-language Structure
47+
- **Rust** (`/crates`): Core implementation using Cargo workspace
48+
- `lang_handler`: Generic language handler abstractions
49+
- `php`: PHP embedding and SAPI implementation
50+
- `php_node`: NAPI bindings exposing Rust to Node.js
51+
- **JavaScript**: Node.js API layer (`index.js`, `index.d.ts`)
52+
- **PHP**: Embedded runtime via libphp.{so,dylib}
53+
54+
### Key Components
55+
56+
1. **PHP Class** (`index.js`): Main entry point for creating PHP environments
57+
- Manages rewriter rules for URL routing
58+
- Handles request/response lifecycle
59+
- Supports both sync and async request handling
60+
61+
2. **Request/Response Model**: Web standards-compatible implementation
62+
- `Request` class with headers, body, method
63+
- `Response` class with status, headers, body
64+
- `Headers` class with case-insensitive header handling
65+
66+
3. **Rewriter System**: Apache mod_rewrite-like functionality
67+
- Conditional rules with regex patterns
68+
- Environment variable support
69+
- Rule chaining with [L], [R], [C] flags
70+
71+
4. **SAPI Implementation**: Custom PHP SAPI in Rust
72+
- Direct Zend API usage for performance
73+
- Thread-safe with TSRM support
74+
- Reusable PHP environments across requests
75+
76+
## Critical Development Notes
77+
78+
1. **System Dependencies Required**:
79+
- Linux: `libssl-dev libcurl4-openssl-dev libxml2-dev libsqlite3-dev libonig-dev re2c libpq5`
80+
- macOS: `openssl@3 curl sqlite libxml2 oniguruma postgresql@16`
81+
82+
2. **PHP Runtime**: Must have `libphp.so` (Linux) or `libphp.dylib` (macOS) in project root
83+
84+
3. **Testing**: AVA framework with 3-minute timeout due to PHP startup overhead
85+
86+
4. **Type Definitions**: `index.d.ts` is auto-generated by NAPI-RS - do not edit manually
87+
88+
5. **Platform Support**: x64 Linux, x64/arm64 macOS (pre-built binaries in `/npm`)
89+
90+
6. **Recent Architecture Changes**:
91+
- `lang_handler` crate no longer uses `napi` features directly (removed from dependencies)
92+
- `php` crate uses custom fork of ext-php-rs from platformatic GitHub org
93+
94+
## Common Tasks
95+
96+
### Adding New NAPI Functions
97+
1. Implement in Rust under `crates/php_node/src/`
98+
2. Use `#[napi]` attributes for exposed functions/classes
99+
3. Run `npm run build` to regenerate TypeScript definitions
100+
101+
### Modifying Request/Response Handling
102+
- Core logic in `crates/php/src/sapi.rs`
103+
- JavaScript wrapper in `index.js`
104+
- Headers handling in `crates/php_node/src/headers.rs`
105+
106+
### Debugging PHP Issues
107+
- Check INTERNALS.md for PHP embedding details
108+
- Use `npm run build:debug` for debug symbols
109+
- PHP superglobals set via `SG(...)` macro in Rust code
110+
111+
### Working with Rewriter Rules
112+
The rewriter system supports Apache mod_rewrite-like functionality:
113+
- Create rules with conditions (header, host, method, path, query)
114+
- Apply rewriters (header, href, method, path, query, status)
115+
- Use flags like [L] (last), [R] (redirect), [C] (chain)
116+
- Example: `new Rewriter([{ conditions: [{type: 'path', args: ['^/old/(.*)$']}], rewriters: [{type: 'path', args: ['^/old/(.*)$', '/new/$1']}] }])`
117+
118+
## Project Files Reference
119+
120+
- `index.js`: Main JavaScript API, exports PHP, Request, Response, Headers, Rewriter classes
121+
- `crates/php_node/src/lib.rs`: NAPI bindings entry point
122+
- `crates/php/src/sapi.rs`: PHP SAPI implementation (core request handling)
123+
- `crates/lang_handler/src/`: Generic language handler abstractions (request/response/rewriter)
124+
- `__test__/*.spec.mjs`: Test files for each component

Cargo.toml

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,47 @@
1-
[workspace]
2-
resolver = "2"
3-
members = [
4-
"crates/lang_handler",
5-
"crates/php",
6-
"crates/php_node"
7-
]
1+
[package]
2+
edition = "2021"
3+
name = "php-node"
4+
version = "1.4.0"
5+
authors = ["Platformatic Inc. <[email protected]> (https://platformatic.dev)"]
6+
license = "MIT"
7+
repository = "https://github.com/platformatic/php-node"
8+
9+
[features]
10+
default = []
11+
napi-support = ["dep:napi", "dep:napi-derive", "dep:napi-build", "http-handler/napi-support", "http-rewriter/napi-support"]
12+
13+
[lib]
14+
name = "php_node"
15+
crate-type = ["cdylib"]
16+
path = "src/lib.rs"
17+
18+
[dependencies]
19+
async-trait = "0.1.88"
20+
bytes = "1.10.1"
21+
hostname = "0.4.1"
22+
ext-php-rs = { git = "https://github.com/platformatic/ext-php-rs.git" }
23+
http-handler = { git = "https://github.com/platformatic/http-handler.git", branch = "avoid-copying-inner-request" }
24+
# http-handler = { path = "../http-handler" }
25+
http-rewriter = { git = "https://github.com/platformatic/http-rewriter.git", branch = "more-flexible-url-path-rewriting" }
26+
# http-rewriter = { path = "../http-rewriter" }
27+
libc = "0.2.171"
28+
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
29+
napi = { version = "3", default-features = false, features = ["napi4"], optional = true }
30+
napi-derive = { version = "3", optional = true }
31+
once_cell = "1.21.0"
32+
tokio = { version = "1.45", features = ["rt", "macros", "rt-multi-thread"] }
33+
regex = "1.0"
34+
35+
[dev-dependencies]
36+
tokio-test = "0.4"
37+
38+
[build-dependencies]
39+
autotools = "0.2"
40+
bindgen = "0.69.4"
41+
cc = "1.1.7"
42+
downloader = "0.2.8"
43+
file-mode = "0.1.2"
44+
napi-build = { version = "2.2.1", optional = true }
845

946
# [profile.release]
1047
# lto = true

__test__/headers.spec.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,20 @@ test('only last set is used for get', (t) => {
1919
const headers = new Headers()
2020
headers.set('Content-Type', 'application/json')
2121
headers.add('Content-Type', 'text/html')
22-
t.is(headers.size, 1)
22+
t.is(headers.size, 2)
2323
t.assert(headers.has('Content-Type'))
24-
t.is(headers.get('Content-Type'), 'text/html')
24+
t.is(headers.get('Content-Type'), 'application/json')
2525
})
2626

2727
test('adding a header with multiple values works and stores to a single entry', (t) => {
2828
const headers = new Headers()
2929
headers.add('Accept', 'application/json')
3030
headers.add('Accept', 'text/html')
31-
t.is(headers.size, 1)
31+
t.is(headers.size, 2)
3232
t.assert(headers.has('Accept'))
3333
t.deepEqual(headers.getAll('Accept'), ['application/json', 'text/html'])
3434
t.deepEqual(headers.getLine('Accept'), 'application/json,text/html')
35-
t.deepEqual(headers.get('Accept'), 'text/html')
35+
t.deepEqual(headers.get('Accept'), 'application/json')
3636
})
3737

3838
test('deleting a header adjusts size and removes value', (t) => {

__test__/request.spec.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ test('full construction', (t) => {
3333
t.assert(req.body instanceof Buffer)
3434
t.is(req.body.toString('utf8'), 'Hello, from Node.js!')
3535
t.assert(req.headers instanceof Headers)
36-
t.is(req.headers.size, 3)
36+
t.is(req.headers.size, 4)
3737
t.is(req.headers.get('Content-Type'), 'application/json')
3838
t.deepEqual(req.headers.getAll('Accept'), ['application/json', 'text/html'])
3939
t.is(req.headers.get('X-Custom-Header'), 'CustomValue')
@@ -58,7 +58,7 @@ test('construction with headers instance', (t) => {
5858
t.assert(req.body instanceof Buffer)
5959
t.is(req.body.toString('utf8'), 'Hello, from Node.js!')
6060
t.assert(req.headers instanceof Headers)
61-
t.is(req.headers.size, 3)
61+
t.is(req.headers.size, 4)
6262
t.is(req.headers.get('Content-Type'), 'application/json')
6363
t.deepEqual(req.headers.getAll('Accept'), ['application/json', 'text/html'])
6464
t.is(req.headers.get('X-Custom-Header'), 'CustomValue')

__test__/rewriter.spec.mjs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import test from 'ava'
33
import { Request, Rewriter } from '../index.js'
44

55
const docroot = import.meta.dirname
6+
const filename = import.meta.filename.replace(docroot, '')
67

78
test('existence condition', (t) => {
89
const req = new Request({
10+
docroot,
911
method: 'GET',
10-
url: 'http://example.com/util.mjs',
12+
url: `http://example.com${filename}`,
1113
headers: {
1214
TEST: ['foo']
1315
}
@@ -16,7 +18,7 @@ test('existence condition', (t) => {
1618
const rewriter = new Rewriter([
1719
{
1820
conditions: [
19-
{ type: 'exists' }
21+
{ type: 'exists', args: [] }
2022
],
2123
rewriters: [
2224
{
@@ -27,11 +29,12 @@ test('existence condition', (t) => {
2729
}
2830
])
2931

30-
t.is(rewriter.rewrite(req, docroot).url, 'http://example.com/404')
32+
t.is(rewriter.rewrite(req).url, 'http://example.com/404')
3133
})
3234

3335
test('non-existence condition', (t) => {
3436
const req = new Request({
37+
docroot,
3538
method: 'GET',
3639
url: 'http://example.com/index.php',
3740
headers: {
@@ -53,7 +56,7 @@ test('non-existence condition', (t) => {
5356
}
5457
])
5558

56-
t.is(rewriter.rewrite(req, docroot).url, 'http://example.com/404')
59+
t.is(rewriter.rewrite(req).url, 'http://example.com/404')
5760
})
5861

5962
test('condition groups - AND', (t) => {
@@ -196,7 +199,7 @@ test('header rewriting', (t) => {
196199
test('href rewriting', (t) => {
197200
const rewriter = new Rewriter([{
198201
rewriters: [
199-
{ type: 'href', args: [ '^(.*)$', '/index.php?route=${1}' ] }
202+
{ type: 'href', args: [ '^http://example.com(.*)$', '/index.php?route=${1}' ] }
200203
]
201204
}])
202205

@@ -213,7 +216,7 @@ test('href rewriting', (t) => {
213216
test('method rewriting', (t) => {
214217
const rewriter = new Rewriter([{
215218
rewriters: [
216-
{ type: 'method', args: ['GET', 'POST'] }
219+
{ type: 'method', args: ['POST'] }
217220
]
218221
}])
219222

build.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
use std::env;
2+
3+
#[cfg(feature = "napi-support")]
4+
extern crate napi_build;
5+
6+
fn main() {
7+
#[cfg(feature = "napi-support")]
8+
napi_build::setup();
9+
10+
// Check for manual PHP_RPATH override, otherwise try LD_PRELOAD_PATH,
11+
// and finally fallback to hard-coded /usr/local/lib path.
12+
//
13+
// PHP_RPATH may also be $ORIGIN to instruct the build to search for libphp
14+
// in the same directory as the *.node bindings file.
15+
let php_rpath = env::var("PHP_RPATH")
16+
.or_else(|_| env::var("LD_PRELOAD_PATH"))
17+
.unwrap_or("/usr/local/lib".to_string());
18+
19+
println!("cargo:rustc-link-search={php_rpath}");
20+
println!("cargo:rustc-link-lib=dylib=php");
21+
println!("cargo:rustc-link-arg=-Wl,-rpath,{php_rpath}");
22+
}

crates/lang_handler/Cargo.toml

Lines changed: 0 additions & 22 deletions
This file was deleted.

crates/lang_handler/build.rs

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)