VPHP Value Layers
May 18, 2026 ยท View on GitHub
This document explains how VPHP value types relate to each other.
Use it when choosing between:
ZValRequestBorrowedZBox/RequestOwnedZBox/PersistentOwnedZBoxPhpValue/PhpInt/PhpString/PhpArray/PhpObject/ ...PhpArg/PhpArgInputRequestScope/FrameScope
One Value, Several Questions
A PHP value crosses several boundaries before application code should touch it. Each layer answers a different question:
flowchart TD
ZEND["Zend value<br/>C.zval / zend_object / zend_array"] --> ZVAL["ZVal<br/>low-level Zend handle"]
ZVAL --> ZBOX["ZBox lifecycle layer"]
ZBOX --> SEM["PHP semantic wrappers"]
SEM --> ARG["Argument / call ergonomics"]
ARG --> SCOPE["Scope helpers"]
ZBOX --> RB["RequestBorrowedZBox"]
ZBOX --> RO["RequestOwnedZBox"]
ZBOX --> PO["PersistentOwnedZBox"]
SEM --> PV["PhpValue"]
SEM --> PS["PhpInt / PhpString / PhpBool / PhpDouble / PhpNull"]
SEM --> PC["PhpArray / PhpObject / PhpCallable / PhpClosure / PhpResource"]
ARG --> IN["PhpArg / PhpArgs"]
ARG --> FN["PhpArgInput"]
SCOPE --> REQ["RequestScope"]
SCOPE --> FRAME["FrameScope"]
| Layer | Main types | Question answered |
|---|---|---|
| Zend | C.zval, zend_object, zend_array | What does the PHP engine store? |
| Low-level handle | ZVal | How does V touch the Zend value? |
| Lifecycle | RequestBorrowedZBox, RequestOwnedZBox, PersistentOwnedZBox | How long can this value live, and who releases it? |
| PHP semantics | PhpValue, PhpInt, PhpArray, PhpObject, ... | What PHP type does this value mean? |
| Arguments | PhpArg, PhpArgInput | Does this value carry PHP input metadata, or is it accepted as a PHP call argument? |
| Scopes | RequestScope, FrameScope | Where are temporary request-owned values released? |
The important rule: do not use a lower layer when a higher layer expresses the intent clearly.
ZVal
ZVal is the bridge-level escape hatch over a Zend value.
Use it for:
- C / Zend bridge helpers
- raw interop code with explicit
_zvalAPIs - implementing semantic wrappers
- places where PHP key type or low-level zval state must be inspected directly
Avoid it in extension application code when a semantic wrapper can say the same thing:
// Prefer this in application code.
fn greet(name vphp.PhpString) string {
return 'Hello ${name.value()}'
}
// Keep this style in bridge-level code.
fn greet_raw(name vphp.ZVal) string {
return 'Hello ${name.to_string()}'
}
*ZBox
ZBox types add lifecycle and ownership to a Zend value.
| Type | Meaning | Typical source | Release rule |
|---|---|---|---|
RequestBorrowedZBox | Borrowed view of a request value | PHP arguments, borrowed wrappers | Do not release |
RequestOwnedZBox | Temporary value owned in the current request | PHP call result, RequestOwnedZBox.new_*, adopted zval | Release once or hand to a scope |
PersistentOwnedZBox | Long-lived detached data or retained handle | app state, route metadata, cached callables | Owner must release |
Request-owned values are for the current request or call flow. Persistent-owned values are for long-lived storage. Converting from request to persistent is an ownership/lifecycle decision, not a PHP type decision.
Common constructors:
borrowed := vphp.RequestBorrowedZBox.of(z)
owned := vphp.RequestOwnedZBox.of(z)
stored := vphp.PersistentOwnedZBox.of(z)
When the input kind is known, prefer the explicit persistent constructor:
stored_data := vphp.PersistentOwnedZBox.of_data(dyn)
stored_object := obj.retain()
stored_callable := callable.retain()
In extension-facing and framework-facing code, prefer value.retain() when the
intent is "keep this PHP value beyond the current request frame". Lower-level
to_persistent_owned() / PersistentOwnedZBox.* APIs remain lifecycle storage
tools for vphp internals and ownership-sensitive boundaries.
For detached runtime storage data, use:
stored := vphp.DynValue.persistent_owned_zbox(value)
value may be:
DynValueRetainedObjectfor internal object-retention plumbingRetainedCallablefor internal callable-retention plumbing
PHP Semantic Wrappers
Semantic wrappers describe PHP type intent.
| Wrapper | Meaning |
|---|---|
PhpValue | mixed PHP value |
PhpNull, PhpBool, PhpInt, PhpDouble, PhpString | scalar/null PHP values |
PhpArray | PHP array |
PhpObject | PHP object |
PhpCallable, PhpClosure | callable / closure semantics |
PhpResource | PHP resource |
PhpIterator, PhpIterable, PhpReference, PhpThrowable, PhpEnumCase | specialized PHP semantics |
PhpIterator means a PHP object that implements Traversable; PhpIterable
means the PHP iterable shape: PhpArray | PhpIterator.
These wrappers are built on top of PhpValueZBox, so they can carry borrowed,
request-owned, or persistent-owned storage while presenting a PHP type API.
Examples:
value := vphp.PhpValue.from_zval(raw)
name := value.require_string()!
arr := vphp.PhpArray.empty()
arr.string('name', 'codex')
obj := vphp.PhpObject.borrowed(raw_obj)
title := obj.method[vphp.PhpString]('title')!
Use with_result(...) / with_method_result(...) for complex temporary return
values so the lifecycle stays inside the callback:
count := vphp.PhpFunction.named('array_filter').with_result[vphp.PhpArray, int](
fn (filtered vphp.PhpArray) int {
return filtered.count()
}, items)!
Use call[T] / method[T] for copied scalar wrappers:
length := vphp.PhpFunction.named('strlen').call[vphp.PhpInt](vphp.PhpString.of('codex'))!
PhpArg And PhpArgInput
These names separate the normalized argument object from the broad input shape accepted by PHP call helpers.
PhpArg
PhpArg means: "a PHP argument value, optionally with input metadata".
It carries:
value PhpValue- optional
PhpArgMetawithindex,name, andattributes
PhpAttribute is intentionally generic. PhpArgMeta.attributes is only one
mount point; class, function, method, property, or return metadata can reuse the
same []PhpAttribute model when those runtime meta wrappers grow attributes.
PhpAttribute.target records the current mount point. ctx.args_with_meta(...)
normalizes missing attribute targets to .parameter.
Use it when the function needs dynamic access to the original PHP argument list:
arg := ctx.arg_at(0)
name := arg.name()
value := arg.string_value() or { vphp.PhpString.empty() }
Context exposes two collection helpers:
args := ctx.args()
args_with_decl := ctx.args_with_meta([
vphp.PhpArgMeta{
index: 0
name: 'query'
attributes: [
vphp.PhpAttribute.named('FromQuery').string('q'),
vphp.PhpAttribute.named('Range').int(1).int(100),
]
},
])
ctx.args() reads the actual PHP arguments only. ctx.args_with_meta(...)
attaches declaration metadata supplied by generated glue or by a hand-written
Context function.
Parameter attributes are available through PhpArg:
query := args_with_decl.at(0)
if from_query := query.attr('FromQuery') {
source := from_query.items[0].value
}
if query.has_attr('Range') {
// inspect query.attr('Range')!.items
}
Normally, exported V functions should prefer typed parameters instead:
@[php_function]
fn hello(name vphp.PhpString, count int) string {
return '${name.value()}:${count}'
}
PhpArgInput
PhpArgInput means: "a semantic value shape accepted when V calls a PHP
function, method, or callable".
It is a sum type over semantic wrappers, plus PhpArg for pass-through cases:
pub type PhpArgInput = PhpArray
| PhpArg
| PhpBool
| PhpCallable
| PhpClosure
| PhpDouble
| PhpEnumCase
| PhpInt
| PhpIterable
| PhpIterator
| PhpNull
| PhpObject
| PhpReference
| PhpResource
| PhpScalar
| PhpString
| PhpThrowable
| PhpValue
That lets V call PHP with semantic values directly:
result := vphp.PhpFunction.named('sprintf').call[vphp.PhpString](
vphp.PhpString.of('hello %s'),
vphp.PhpString.of('codex'),
)!
Rule of thumb:
| Direction | Type |
|---|---|
| PHP -> V argument metadata | PhpArg / PhpArgs |
| V -> PHP call argument input | PhpArgInput |
| Already typed V-exported function parameter | PhpString, PhpObject, int, string, etc. |
Scopes
RequestScope
RequestScope manages request-owned values for a request/call boundary.
Compiler-generated glue uses:
mut vphp_scope := vphp.PhpScope.once()
defer {
vphp_scope.close()
}
Use PhpScope.request() when a larger request-level window is needed. Use
PhpScope.once() for one call boundary.
Most extension code should not need to call low-level autorelease functions directly.
FrameScope
FrameScope is for building temporary PHP call arguments inside one V frame.
It owns the request-owned boxes it creates and releases them all at once:
mut frame := vphp.PhpScope.frame()
defer {
frame.release()
}
args := [
frame.string('codex'),
frame.int(42),
]
mut result := vphp.PhpFunction.named('handler').invoke(...args)
Use FrameScope.args_from_persistent_owned(...) when long-lived stored values
need to be passed into a request-time PHP callable:
mut frame := vphp.PhpScope.frame()
defer {
frame.release()
}
args := frame.args_from_persistent_owned(stored_args)
mut result := handler.invoke(...args)
FrameScope is not persistent storage. It is a short-lived conversion and
cleanup helper.
Common Flows
PHP Calls V
flowchart LR
PHP["PHP call"] --> CTX["Context / ZExData"]
CTX --> INARGS["PhpArgs / PhpArg"]
INARGS --> SEM["Typed params<br/>PhpString / PhpObject / int / string"]
SEM --> V["V function body"]
Supported parameter styles:
| Style | Example | Use when |
|---|---|---|
Context | fn handle(ctx vphp.Context) | Need raw execute data or manual arg access |
| V scalars | fn add(a int, b string) | Simple scalar parameters |
| PHP semantic wrappers | fn run(argv vphp.PhpIterable) | Want PHP type semantics |
| optional params | fn value(default ?vphp.PhpValue) | Missing argument matters |
@[params] struct | fn create(p CreateParams) | Named/default argument groups |
Do not combine Context with PhpArgs as public parameters. PhpArgs is
derived from the context inside glue through ctx.args() or
ctx.args_with_meta(...).
V Calls PHP
flowchart LR
V["V code"] --> SEM["PhpValue / PhpString / PhpObject"]
SEM --> ARG["PhpArgInput"]
ARG --> CALL["PhpFunction / PhpObject / PhpClass"]
CALL --> RESULT["RequestOwnedZBox or with_result callback"]
Prefer semantic call APIs:
upper := vphp.PhpFunction.named('strtoupper').call[vphp.PhpString](
vphp.PhpString.of('codex'),
)!
sent := conn.with_method_result[vphp.PhpValue, bool]('send',
fn (result vphp.PhpValue) bool {
return result.is_valid()
}, vphp.PhpString.of('payload'))!
Use _zval APIs only at bridge boundaries or when the raw zval behavior is the
point of the code.
Store For Later
flowchart LR
NOW["Request-time value"] --> CHECK["Known kind?"]
CHECK --> DATA["Detached data<br/>DynValue"]
CHECK --> OBJ["PhpObject.retain()"]
CHECK --> CALL["PhpCallable.retain()"]
DATA --> PZ["PersistentOwnedZBox"]
OBJ --> PZ
CALL --> PZ
Use:
stored := value.retain()
or, at a lower-level lifecycle boundary where detached runtime storage is the actual target:
stored := vphp.DynValue.persistent_owned_zbox(ref.clone())
The owner of a retained semantic value or PersistentOwnedZBox must have a
matching release() path.
Naming Rules
Current naming direction:
| Pattern | Meaning |
|---|---|
from_zval(...) | Build a semantic wrapper view from an existing ZVal |
from_request_owned_zbox(...) | Build a semantic wrapper from RequestOwnedZBox |
from_persistent_owned_zbox(...) | Build a semantic wrapper from PersistentOwnedZBox |
from_persistent_zval(...) | Preserve a persistent zval payload inside a semantic wrapper |
to_zval() | Expose the underlying zval view |
zval() | Read the underlying ZVal from a PHP input argument |
zbox() | Borrow a PHP input argument as RequestBorrowedZBox |
to_request_owned() | Materialize an owned request box |
persistent_owned_zbox(...) | Produce PersistentOwnedZBox |
invoke(...) / call_method(...) / call_static(...) | PHP call result as a semantic PhpValue |
with_result(...) | Borrow a result inside a callback |
_zval suffix | Low-level escape hatch |
Names should reveal both type semantics and lifecycle semantics. If a helper
returns PersistentOwnedZBox, its name should say zbox, not just box.
Remaining Edges To Clean Up
The model is now coherent, but a few edges are still worth tightening:
with_request_zval(...)still exists in vphp internals. That is reasonable at the lifecycle boundary, but new application code should prefer semantic helpers such aswith_request_value,with_request_array, orwith_request_object.- Generated glue still has to touch
Contextand raw argument slots. That is a compiler boundary detail; public extension signatures should continue moving toward typed scalar/semantic wrappers and@[params]structs.