Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

@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.

## Essential Commands

### Development
```bash
# Build release version
npm run build

# Build debug version (faster compilation, includes debug symbols)
npm run build:debug

# Run all tests
npm test

# Run specific test file
npx ava __test__/headers.spec.mjs

# Lint JavaScript code
npm run lint

# Create universal binary (macOS)
npm run universal

# Version bump
npm run version
```

### Rust-specific builds
```bash
# Build with proper rpath for linking libphp
RUSTFLAGS="-C link-args=-Wl,-rpath,\$ORIGIN" npm run build

# Run Rust tests
cargo test

# Run binary directly
cargo run
```

## Architecture

### Multi-language Structure
- **Rust** (`/src`): Single-crate implementation
- PHP embedding and SAPI implementation
- NAPI bindings exposing Rust to Node.js (when `napi-support` feature is enabled)
- Binary target for standalone testing (`src/main.rs`)
- Library target for NAPI usage (`src/lib.rs`)
- **JavaScript**: Node.js API layer (`index.js`, `index.d.ts`)
- **PHP**: Embedded runtime via libphp.{so,dylib}

### Key Components

1. **PHP Class** (`index.js`): Main entry point for creating PHP environments
- Manages rewriter rules for URL routing
- Handles request/response lifecycle
- Supports both sync and async request handling

2. **Request/Response Model**: Web standards-compatible implementation
- `Request` class with headers, body, method
- `Response` class with status, headers, body
- `Headers` class with case-insensitive header handling

3. **Rewriter System**: Apache mod_rewrite-like functionality
- Conditional rules with regex patterns
- Environment variable support
- Rule chaining with [L], [R], [C] flags

4. **SAPI Implementation**: Custom PHP SAPI in Rust
- Direct Zend API usage for performance
- Thread-safe with TSRM support
- Reusable PHP environments across requests

## Critical Development Notes

1. **System Dependencies Required**:
- Linux: `libssl-dev libcurl4-openssl-dev libxml2-dev libsqlite3-dev libonig-dev re2c libpq5`
- macOS: `openssl@3 curl sqlite libxml2 oniguruma postgresql@16`

2. **PHP Runtime**: Must have `libphp.so` (Linux) or `libphp.dylib` (macOS) in project root

3. **Testing**: AVA framework with 3-minute timeout due to PHP startup overhead

4. **Type Definitions**: `index.d.ts` is auto-generated by NAPI-RS - do not edit manually

5. **Platform Support**: x64 Linux, x64/arm64 macOS (pre-built binaries in `/npm`)

6. **Recent Architecture Changes**:
- Consolidated from multi-crate workspace to single crate named `php`
- NAPI support is now feature-gated with `napi-support` feature
- Binary target supports both library (`rlib`) and dynamic library (`cdylib`) outputs

## Common Tasks

### Adding New NAPI Functions
1. Implement in Rust under `src/` with `#[cfg(feature = "napi-support")]`
2. Use `#[napi]` attributes for exposed functions/classes
3. Run `npm run build` to regenerate TypeScript definitions

### Modifying Request/Response Handling
- Core logic in `src/sapi.rs` and `src/embed.rs`
- JavaScript wrapper in `index.js`
- Request/response types from `http-handler` crate

### Debugging PHP Issues
- Check INTERNALS.md for PHP embedding details
- Use `npm run build:debug` for debug symbols
- PHP superglobals set via `SG(...)` macro in Rust code

### Working with Rewriter Rules
The rewriter system supports Apache mod_rewrite-like functionality:
- Create rules with conditions (header, host, method, path, query)
- Apply rewriters (header, href, method, path, query, status)
- Use flags like [L] (last), [R] (redirect), [C] (chain)
- Example: `new Rewriter([{ conditions: [{type: 'path', args: ['^/old/(.*)$']}], rewriters: [{type: 'path', args: ['^/old/(.*)$', '/new/$1']}] }])`

## Project Files Reference

- `index.js`: Main JavaScript API, exports PHP, Request, Response, Headers, Rewriter classes
- `src/lib.rs`: Library entry point, exports core types and NAPI bindings
- `src/main.rs`: Binary entry point for standalone testing
- `src/embed.rs`: Core `Embed` type for handling PHP requests
- `src/sapi.rs`: PHP SAPI implementation (low-level PHP integration)
- `src/runtime.rs`: NAPI runtime implementation (when `napi-support` feature enabled)
- `__test__/*.spec.mjs`: Test files for each component
55 changes: 48 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,51 @@
[workspace]
resolver = "2"
members = [
"crates/lang_handler",
"crates/php",
"crates/php_node"
]
[package]
edition = "2021"
name = "php"
version = "1.4.0"
authors = ["Platformatic Inc. <[email protected]> (https://platformatic.dev)"]
license = "MIT"
repository = "https://github.com/platformatic/php-node"

[features]
default = []
napi-support = ["dep:napi", "dep:napi-derive", "dep:napi-build", "http-handler/napi-support", "http-rewriter/napi-support"]

[lib]
name = "php"
crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"

[[bin]]
name = "php-main"
path = "src/main.rs"

[dependencies]
async-trait = "0.1.88"
bytes = "1.10.1"
hostname = "0.4.1"
ext-php-rs = { version = "0.14.0", features = ["embed"] }
http-handler = { git = "https://github.com/platformatic/http-handler.git" }
# http-handler = { path = "../http-handler" }
http-rewriter = { git = "https://github.com/platformatic/http-rewriter.git" }
# http-rewriter = { path = "../http-rewriter" }
libc = "0.2.171"
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "3", default-features = false, features = ["napi4"], optional = true }
napi-derive = { version = "3", optional = true }
once_cell = "1.21.0"
tokio = { version = "1.45", features = ["rt", "macros", "rt-multi-thread"] }
regex = "1.0"

[dev-dependencies]
tokio-test = "0.4"

[build-dependencies]
autotools = "0.2"
bindgen = "0.69.4"
cc = "1.1.7"
downloader = "0.2.8"
file-mode = "0.1.2"
napi-build = { version = "2.2.1", optional = true }

# [profile.release]
# lto = true
Expand Down
9 changes: 5 additions & 4 deletions __test__/rewriter.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import test from 'ava'
import { Request, Rewriter } from '../index.js'

const docroot = import.meta.dirname
const filename = import.meta.filename.replace(docroot, '')

test('existence condition', (t) => {
const req = new Request({
method: 'GET',
url: 'http://example.com/util.mjs',
url: `http://example.com${filename}`,
headers: {
TEST: ['foo']
}
Expand All @@ -16,7 +17,7 @@ test('existence condition', (t) => {
const rewriter = new Rewriter([
{
conditions: [
{ type: 'exists' }
{ type: 'exists', args: [] }
],
rewriters: [
{
Expand Down Expand Up @@ -196,7 +197,7 @@ test('header rewriting', (t) => {
test('href rewriting', (t) => {
const rewriter = new Rewriter([{
rewriters: [
{ type: 'href', args: [ '^(.*)$', '/index.php?route=${1}' ] }
{ type: 'href', args: [ '^(.*)$', '/index.php?route=$1' ] }
]
}])

Expand All @@ -213,7 +214,7 @@ test('href rewriting', (t) => {
test('method rewriting', (t) => {
const rewriter = new Rewriter([{
rewriters: [
{ type: 'method', args: ['GET', 'POST'] }
{ type: 'method', args: ['POST'] }
]
}])

Expand Down
22 changes: 22 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use std::env;

#[cfg(feature = "napi-support")]
extern crate napi_build;

fn main() {
#[cfg(feature = "napi-support")]
napi_build::setup();

// Check for manual PHP_RPATH override, otherwise try LD_PRELOAD_PATH,
// and finally fallback to hard-coded /usr/local/lib path.
//
// PHP_RPATH may also be $ORIGIN to instruct the build to search for libphp
// in the same directory as the *.node bindings file.
let php_rpath = env::var("PHP_RPATH")
.or_else(|_| env::var("LD_PRELOAD_PATH"))
.unwrap_or("/usr/local/lib".to_string());

println!("cargo:rustc-link-search={php_rpath}");
println!("cargo:rustc-link-lib=dylib=php");
println!("cargo:rustc-link-arg=-Wl,-rpath,{php_rpath}");
}
22 changes: 0 additions & 22 deletions crates/lang_handler/Cargo.toml

This file was deleted.

7 changes: 0 additions & 7 deletions crates/lang_handler/build.rs

This file was deleted.

61 changes: 0 additions & 61 deletions crates/lang_handler/src/handler.rs

This file was deleted.

Loading
Loading