ext-php-rs 0.15.10

Bindings for the Zend API to build PHP extensions natively in Rust.
Documentation
# Embedded PHP Execution

Extensions sometimes need to execute PHP code at runtime for setup tasks
like registering autoloaders, defining helper classes, or configuring error
handlers. The `php_eval` module lets you embed `.php` files into your
extension binary at compile time and execute them from Rust.

## Why Not `eval`?

This module uses `zend_compile_string` + `zend_execute` instead of
`zend_eval_string` because:

- `zend_eval_string` triggers security scanner false positives (eval-like
  semantics)
- Some hardened PHP builds disable eval-like functionality
- The embedded code is static bytes in the binary -- there is no injection risk

## Basic Usage

### 1. Write your PHP file

Create a normal `.php` file with full IDE support (syntax highlighting,
linting, static analysis):

```php
<?php
// php/setup.php

spl_autoload_register(function (string $class): void {
    $prefix = 'Acme\\Encryption\\';
    if (str_starts_with($class, $prefix)) {
        $relative = substr($class, strlen($prefix));
        $file = __DIR__ . '/src/' . str_replace('\\', '/', $relative) . '.php';
        if (file_exists($file)) {
            require $file;
        }
    }
});

function acme_encrypt_version(): string {
    return '1.0.0';
}
```

### 2. Embed and execute from Rust

Use `include_bytes!` or `include_str!` to embed the file at compile time,
then call `php_eval::execute()` from whatever lifecycle hook fits your target
SAPI. The function accepts any type that implements `AsRef<[u8]>`:

```rust,ignore
use ext_php_rs::prelude::*;
use ext_php_rs::php_eval;

// Both forms work:
const SETUP: &[u8] = include_bytes!("../php/setup.php");
// const SETUP: &str = include_str!("../php/setup.php");

unsafe extern "C" fn on_request_start(
    _type: i32,
    _module_number: i32,
) -> i32 {
    if let Err(e) = php_eval::execute(SETUP) {
        eprintln!("Failed to run embedded PHP setup: {:?}", e);
    }
    0
}

#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
    module.request_startup_function(on_request_start)
}
```

## API Reference

### `php_eval::execute(code: impl AsRef<[u8]>) -> Result<(), PhpEvalError>`

Compiles and executes the given PHP source within the running PHP engine.

**Arguments:**

- `code` -- Raw PHP source, typically from `include_bytes!` or `include_str!`.
  Any type implementing `AsRef<[u8]>` is accepted (`&[u8]`, `&str`,
  `String`, `Vec<u8>`, etc.).

**Returns:**

- `Ok(())` on success.
- `Err(PhpEvalError::MissingOpenTag)` if the code does not start with
  `<?php` (case-insensitive).
- `Err(PhpEvalError::CompilationFailed)` if PHP cannot compile the code
  (syntax error).
- `Err(PhpEvalError::ExecutionFailed)` if the code throws an unhandled
  exception.
- `Err(PhpEvalError::Bailout)` if a PHP fatal error occurs.

### Input handling

The code **must** start with a `<?php` opening tag (case-insensitive).
The tag is stripped before compilation.

| Input | Handling |
|-------|----------|
| `<?php` opening tag | **Required** (case-insensitive), stripped before compilation |
| UTF-8 BOM (`0xEF 0xBB 0xBF`) | Stripped before compilation |
| Empty after tag (e.g. `<?php`) | Returns `Ok(())` immediately |
| No `<?php` tag (including empty input) | Returns `Err(MissingOpenTag)` |

## Lifecycle Hooks

The module does not prescribe *when* to run embedded PHP. The SAPI landscape
is fragmented -- FrankenPHP in worker mode does not trigger RINIT per request,
for example. Choose the hook that fits your target:

| Hook | Use case |
|------|----------|
| RINIT (`request_startup_function`) | Per-request setup (classic php-fpm / mod_php) |
| MINIT (`startup_function`) | One-time global setup |
| Custom SAPI callback | Worker-mode runtimes (FrankenPHP, RoadRunner) |

## Error Handling

Errors during embedded PHP execution should not crash the host process.
The recommended pattern is to log and continue:

```rust,ignore
if let Err(e) = php_eval::execute(SETUP_CODE) {
    match e {
        PhpEvalError::MissingOpenTag => {
            eprintln!("embedded PHP missing <?php open tag");
        }
        PhpEvalError::CompilationFailed => {
            eprintln!("embedded PHP syntax error");
        }
        PhpEvalError::ExecutionFailed => {
            eprintln!("embedded PHP threw an exception");
        }
        PhpEvalError::Bailout => {
            eprintln!("embedded PHP fatal error");
        }
    }
}
```

## How It Works

1. **Build time**: `include_bytes!` embeds the `.php` file contents into the
   extension binary as a `&[u8]` constant.

2. **Runtime**: `php_eval::execute()` strips the `<?php` tag and BOM, then
   calls two C wrapper functions in `wrapper.c`:
   - `ext_php_rs_zend_compile_string` -- compiles the source into an op_array.
     On PHP 8.2+ it passes `ZEND_COMPILE_POSITION_AFTER_OPEN_TAG` so the
     scanner starts directly in PHP mode. On PHP 8.1 the two-argument form
     is used.
   - `ext_php_rs_zend_execute` -- executes the op_array, sets the execution
     scope, then cleans up static vars and frees the op_array.

3. **Safety**: The entire execution is wrapped in `try_catch` to catch PHP
   bailouts (longjmp) without unwinding the Rust stack. Error reporting is
   suppressed during execution and restored afterward.