MEP 75. Mochi and PHP package bridge
| Field | Value |
|---|---|
| MEP | 75 |
| Title | Mochi and PHP package bridge |
| Author | Mochi core |
| Status | Draft |
| Type | Standards Track |
| Created | 2026-05-29 22:11 (GMT+7) |
| Depends | MEP-1 (Grammar, for the import php extension), MEP-2 (AST, for the import node), MEP-4 (Type System), MEP-13 (ADTs and Match, for nullable and union type translation), MEP-55 (PHP transpiler, for the lowering pipeline, the PHP runtime library, and the TargetPhpLibrary addition), MEP-57 (Mochi module and package system, for mochi.toml, mochi.lock, the content-addressed object store, the capability declaration model, and the publish flow infrastructure) |
| Research | /docs/research/0075/ |
| Tracking | /docs/implementation/0075/ |
Abstract
Mochi today (May 2026, after MEP-55's PHP transpiler landed all 18 phases and MEP-57's source-level package system reached Draft) has ten code-emitting targets and the beginnings of a source-level package management story. MEP-55 enables Mochi programs to compile to PHP 8.4 and publish the resulting Composer package to Packagist, but only for Mochi-native programs: user-written Mochi programs cannot pull in arbitrary PHP packages from the 400,000+ packages on Packagist, and a Mochi package author cannot publish their work as a Composer library that downstream PHP users composer require. The Composer/Packagist ecosystem, which serves WordPress (~43% of the web by CMS deployment in 2026 Q1), Laravel, Symfony, Drupal, and the entire PHP application tier, remains off-limits to Mochi authors in both directions.
MEP-75 specifies the bidirectional PHP Composer bridge: Mochi packages can consume any Packagist-hosted Composer package via import php "vendor/package@^semver" as alias with no user-written FFI boilerplate, and Mochi packages can publish to Packagist as Composer libraries via mochi pkg publish --to=packagist. The bridge is the sixth source-language interop story Mochi ships (after Go, Python, TypeScript, Rust, and the MEP-74 Go module bridge), and the first one where the target language has no native OIDC trusted publishing path, no machine-readable API surface format equivalent to rustdoc JSON or go/types, and no C-ABI layer between the bridged packages (because both sides are PHP, the bridge is PHP-to-PHP).
The proposal builds on MEP-57's manifest/lockfile/capability infrastructure and MEP-55's PHP emit pipeline, and adds a new self-contained component under package3/php/. The Mochi grammar gains php as a valid <lang> token in the existing FFI-import production (no other surface-syntax changes). The Mochi build pipeline gains one new target (TargetPhpLibrary in MEP-55's build driver). The lockfile gains one new repeated table ([[php-package]]). No existing transpiler MEP needs structural change.
The system is anchored on seven load-bearing decisions, each justified in the companion research notes:
-
PHP Reflection API plus PHPDoc plus Psalm stubs as the canonical machine-readable PHP package surface. PHP has no rustdoc-JSON equivalent. The bridge invokes a Go-side PHP reflection CLI (
php reflect.php <package-path>) that runs PHP's built-inReflectionClass,ReflectionFunction, andReflectionTypeAPIs to walk the package's classes, interfaces, and functions and emit a JSON surface document. PHPDoc annotations (@param,@return,@var) and Psalm stub files augment the runtime Reflection API output for types that PHP's native reflection cannot express (generics, array shapes, union type narrowing). PHPStan and PHP-Parser are rejected as the primary surface source. See 04-packagist-ingest §1, 05-type-mapping §1, 02-design-philosophy §1. -
Closed PHP-to-Mochi type translation table with explicit SkipReport for out-of-table cases. The table covers
int↔int,float↔float,string↔string,bool↔bool,?T↔T|nil,array(shape-heuristic tolist<T>ormap<K,V>), named class to opaque handle or record (if all fields are typed and PHP 8.4 readonly), named interface to protocol handle,void↔unit,never↔panic,callable↔closure. Out-of-table cases (mixed,object, untypedarray,self,static,parent, intersection typesA&B, union typesint|stringwith more than two members, first-class callables beyond theClosurecase) are skipped with aSkipReport. See 05-type-mapping, 02-design-philosophy §2. -
import php "vendor/package@^semver" as aliasas the sole surface keyword, with<semver>resolved throughmochi.lock. The grammar gainsphpas a<lang>alternative in the existingimport <lang> "<spec>" as <alias>production. The spec is<vendor>/<package>or<vendor>/<package>@<semver-req>. Resolution proceeds through the Packagist v2 sparse API, content-addressed dist cache, PHP reflection CLI, and[[php-package]]lockfile entry. See 01-language-surface §1. -
TargetPhpLibrary emission with PSR-4
src/layout as the only viable Composer library layout. MEP-55 today emits executable PHP programs (TargetPhpSource, TargetPhpRun, TargetPhpPhar, TargetPhpFrankenPHP, TargetPhpRoadRunner). MEP-75 addsTargetPhpLibrarywhich emits a PSR-4-compliantsrc/tree, acomposer.jsonwith package metadata frommochi.toml, and aREADME.mdandLICENSE. PSR-4 is the only autoloading standard Packagist requires for published packages; PSR-0 is deprecated. See 09-psr-autoloading, 02-design-philosophy §4. -
Packagist API token plus GPG-signed git tag plus Sigstore attestation as the publish path, with a documented gap for missing OIDC trusted publishing. Packagist (as of May 2026) has no OIDC trusted publishing equivalent to crates.io RFC #3724, npm Trusted Publishing, or PyPI PEP 740. The MEP-75 publish flow: tag a git commit with a GPG-signed semver tag, push the tag, ping the Packagist Update API with an API token, and attach a Sigstore
actions/attest-build-provenance@v1OIDC attestation on the dist zip (reusing MEP-55 Phase 18's trust chain). The GitHub App integration Packagist ships can serve as a partial CI-bound mitigation. Full OIDC is on the Packagist roadmap but has no GA date. See 06-composer-publish-flow, 07-packagist-trusted-publishing-gap, 02-design-philosophy §5. -
PHP-only bridge scope: no C wrapper, no cross-target. Unlike MEP-73 (Rust) and MEP-74 (Go) which generate extern "C" wrapper crates that link into any MEP-53/MEP-54 target, the PHP bridge targets only the MEP-55 PHP output pipeline. PHP packages can only be called from PHP programs (there is no PHP C ABI that other targets can link); the wrapper is PHP source calling the vendor package via Composer's autoload, injected into the MEP-55 build sandbox via
vendor/directory materialisation. No changes to MEP-53, MEP-54, or MEP-45 are needed. See 02-design-philosophy §6. -
Async bridge via ReactPHP/RevoltPHP event loop as an opt-in behind a capability flag, not the default. PHP has no native async runtime in the standard library; MEP-55 Phase 11 lowers Mochi async to synchronous wrappers. MEP-75 provides an optional
[php.async] = truepath that injectsreact/event-loop(orrevolt/event-loop) into the vendor sandbox, enabling non-blocking I/O for packages that target ReactPHP. The async bridge is a separate opt-in because most Packagist packages are synchronous and the ReactPHP/RevoltPHP overhead is non-trivial. See 08-async-bridge, 02-design-philosophy §7.
The gate for each delivery phase is empirical: the bridge must successfully ingest the 24-package fixture corpus, generate a translatable surface for every public item the closed type-table covers, emit a SkipReport for every out-of-table item, produce Mochi extern fn declarations that parse cleanly, and build the resulting PHP program via MEP-55's existing php main.php path with zero additional flags. A separate publish gate exercises TargetPhpLibrary against an in-tree mock Packagist server, asserts the emitted composer.json parses cleanly, and asserts the Sigstore attestation verifies.
Motivation
Mochi today (May 2026) integrates with foreign ecosystems through five FFI surfaces: Go, Python, TypeScript, Rust (MEP-73), and Go modules (MEP-74). MEP-55 added PHP as a compile target but not as a consumable ecosystem. PHP is the world's largest server-side web language by deployed instance count, and Packagist is the fourth-largest package registry by total package count (400,000+ as of May 2026). The MEP-75 motivation:
-
The PHP/Packagist ecosystem is the primary distribution channel for the WordPress, Laravel, Symfony, and Drupal tiers. WordPress powers roughly 43% of websites by CMS deployment (W3Techs, 2026 Q1). Every WordPress plugin author, every Laravel developer, every Symfony component maintainer uses Composer. A Mochi program that targets the PHP ecosystem cannot currently depend on
guzzlehttp/guzzle,symfony/console,doctrine/orm,ramsey/uuid, or 400,000+ other packages. -
MEP-55 already ships the PHP output pipeline; MEP-75 closes the bidirectional gap. MEP-55 taught Mochi to emit PHP and package it as a Composer library. What MEP-55 did not ship is the reverse: consuming Packagist packages in Mochi source. MEP-75 is the natural complement to MEP-55, completing the bidirectional story.
-
Packagist is the fourth-largest registry without OIDC trusted publishing; Mochi must acknowledge and mitigate the gap. npm, PyPI, Maven Central, and crates.io all reached OIDC trusted publishing by late 2025. Packagist has not. A 2026 package system targeting Packagist must either pretend the gap does not exist or document it honestly and provide the best available mitigation (GPG-signed tags + Sigstore attestation + GitHub App integration). MEP-75 takes the honest path.
-
The PHP Reflection API is a credible machine-readable surface source, despite having no rustdoc-JSON equivalent. PHP 8.4 ships a mature
Reflection*family (ReflectionClass,ReflectionFunction,ReflectionNamedType,ReflectionUnionType,ReflectionIntersectionType,ReflectionEnum). PHPDoc annotations and Psalm stub files fill the gaps (array shapes, generics, return-type narrowing). The combination is the standard tool chain for PHP static analysis (PHPStan, Psalm, Intelephense all use it). The bridge reuses this well-established surface rather than inventing a new one. -
The zero-boilerplate promise applies to PHP exactly as it does to Rust and Go.
import php "guzzlehttp/guzzle@^7.8" as guzzleis the same shape asimport rust "reqwest@^0.12"andimport go "github.com/spf13/cobra@^v1.8". The Mochi user does not need to know what acomposer.jsonis, what a PSR-4 namespace is, what a PHPDoc@paramis, what PHP's nullable syntax is, what a Packagist Update API token is, or what RevoltPHP's event loop is. They writeimport php "..." as ...and the bridge handles it. -
The PHP type system's challenges (mixed, untyped array, union types, nullable, never, fibers) are well-defined once translated through a closed table with explicit refusals. PHP 8.4 ships the most type-rich PHP version to date: readonly class properties, asymmetric visibility, first-class callable syntax, typed class constants, and PHP 8.1 fibers. The closed table handles the common cases; the SkipReport surfaces the rest for the user to hand-override.
-
PSR-4 autoloading is the only viable layout for a published Composer library; MEP-75 locks in that choice. PSR-0 (the old class-to-path convention) is deprecated. PSR-4 is the standard Packagist requires. The
TargetPhpLibraryemitter writes asrc/tree with PSR-4 namespace-to-path mapping and the matchingcomposer.jsonautoload section. This is the correct, future-proof choice with no viable alternative.
Specification
This section is normative. Sub-notes under /docs/research/0075/ are informative.
1. Pipeline overview
MEP-75 introduces a per-import PHP dependency resolution layer that sits between the Mochi parser (after MEP-57 has resolved mochi.toml) and the MEP-55 build driver:
mochi.toml [php-dependencies]
| pkgmanifest.Parse + pkgsolver.Solve (MEP-57)
v
resolved PHP dep tree (vendor/package + version + Packagist dist URL)
| package3/php/packagist.Fetch (Packagist v2 sparse API)
v
dist zips in ~/.cache/mochi/php-deps/<sha256-hex>/
| package3/php/reflect.Run (php reflect.php <pkg-path> → JSON)
v
ReflectionSurface JSON document per package
| package3/php/typemap.Translate (closed PHP-to-Mochi table)
v
TranslatedSurface + SkipReport per package
| package3/php/externemit.Emit (Mochi extern fn / extern type)
v
synthesised .mochi shim file per package
| package3/php/glue.Emit (PHP-side use + forwarding stubs)
v
vendor/ injection into MEP-55 build sandbox
| MEP-55 Driver.Build (TargetPhpSource / TargetPhpLibrary / ...)
v
PHP program or Composer library
The bridge does not invoke composer install at reflection time. The only PHP toolchain invocation is the reflection CLI script (php reflect.php). The PHP glue stubs are emitted as source by the Go bridge and placed in the MEP-55 build sandbox's vendor/ directory before the MEP-55 driver runs composer install.
2. Manifest extension: [php-dependencies] and [php]
The MEP-57 mochi.toml gains two new optional top-level tables:
[php-dependencies]
"guzzlehttp/guzzle" = "^7.8"
"symfony/console" = { version = "^7.0", suggest = ["psr/log"] }
"ramsey/uuid" = "^4.7"
"my-local-package" = { path = "../my-package" }
[php]
php-version = "8.4"
monomorphise = []
build-tags = []
[php.async]
enabled = false
event-loop = "react"
[php.publish]
packagist-name = "my-vendor/my-package"
packagist-description = "A useful Mochi-compiled PHP library."
license = "MIT"
homepage = "https://github.com/example/my-mochi-lib"
keywords = ["mochi", "php"]
psr4-namespace = "MyVendor\\MyPackage\\"
[php.capabilities]
net = false
fs = false
db = false
[php-dependencies] uses Composer's require grammar: a bare string is a version constraint; a table form admits version, suggest, path. The bridge passes these constraints to the Packagist v2 sparse client and the Composer version solver.
The [php] table holds Mochi-specific knobs:
php-version: the PHP version floor the bridge targets. Default"8.4"(MEP-55's floor)."8.1"is the minimum supported value (PHP 8.1 introduced fibers and readonly properties; below 8.1 the reflection surface is materially different).monomorphise: reserved for future generic-PHP extensions; empty in v1 (PHP's generics are PHPDoc-only and not runtime-observable).build-tags: future extension; empty in v1.
The [php.async] table holds async bridge knobs:
enabled: whether to inject the ReactPHP/RevoltPHP event-loop dependency into the build sandbox. Defaultfalse.event-loop:"react"(injectsreact/event-loop ^3.0) or"revolt"(injectsrevolt/event-loop ^1.0). Default"react".
The [php.publish] table holds producer knobs:
packagist-name: the Composer package name (vendor/package). REQUIRED formochi pkg publish --to=packagist.psr4-namespace: the PHP root namespace for the emitted PSR-4 tree. Defaults to a PascalCase transformation of the package name.license,homepage,keywords: mirrored intocomposer.json.
The [php.capabilities] table holds PHP-bridge-specific capability flags:
net: the dep graph contains packages that open network sockets (GuzzleHTTP, Symfony HTTP Client).fs: the dep graph reads or writes files.db: the dep graph opens database connections (Doctrine ORM, PDO).
3. Lockfile extension: [[php-package]]
The MEP-57 mochi.lock gains one new repeated table:
[[php-package]]
name = "guzzlehttp/guzzle"
version = "7.8.1"
source = { kind = "packagist", url = "https://packagist.org" }
dist-url = "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440..."
dist-sha256 = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
reflection-sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
capabilities-declared = ["net"]
dependencies = ["guzzlehttp/promises@^2.0", "guzzlehttp/psr7@^2.7", "psr/http-client@^1.0"]
php-version-constraint = "^8.1"
dist-sha256 is the SHA-256 of the downloaded dist zip. The bridge verifies this at fetch time; a mismatch is a hard error.
reflection-sha256 is the SHA-256 of the JSON surface document the reflection CLI emitted. A drift here at mochi pkg lock --check time is a hard error (the upstream package silently changed its public surface).
capabilities-declared is the capability set the manifest declared at lock time. A capability addition at --check time triggers an explicit re-acknowledgement.
dependencies is the resolved transitive dependency tree.
php-version-constraint records the require.php constraint from the package's composer.json, used to verify compatibility with the user's [php] php-version setting.
4. Surface syntax: import php "..."
The Mochi grammar's existing FFI-import production:
ImportStmt := "import" Lang? StringLit "as" Ident ("auto")?
Lang := "go" | "python" | "typescript" | "rust" | "php"
gains php as a Lang alternative. The string literal is one of:
<vendor>/<package>: bare name. Resolves through[php-dependencies]constraint plusmochi.lock.<vendor>/<package>@<semver-req>: explicit version constraint (^7.8,~7.8.0,>=7.0 <8.0,7.8.1).<vendor>/<package>@path+<relative>: path source for local development.
Example surface:
import php "guzzlehttp/guzzle@^7.8" as guzzle
import php "ramsey/uuid" as uuid
import php "symfony/console@^7.0" as console
fn fetch_json(url: string): string {
let client = guzzle.new_client()
let response = client.get(url)
return response.get_body().get_contents()
}
fn make_uuid(): string {
return uuid.uuid4().to_string()
}
The <alias> introduces a Mochi namespace; symbol resolution looks up <alias>.<item> and binds against the synthesised extern fn declaration the bridge generated for <vendor>/<package>. Item names from PHP are PascalCase (class names) and camelCase (method names); the bridge lowercases class constructors to new_<class> and passes method names through with camelCase preserved (Mochi identifiers are case-insensitive by convention).
5. CLI surface
The Mochi CLI gains the following additions under the existing mochi pkg subcommand:
mochi pkg add php <vendor>/<package>[@<semver>]: adds an entry to[php-dependencies]and runsmochi pkg lock.mochi pkg lock: extended to walk[php-dependencies], query the Packagist v2 sparse API, fetch each dist zip into the content-addressed cache, run the reflection CLI, synthesise shims, and write[[php-package]]entries.mochi pkg lock --check: verifiesdist-sha256,reflection-sha256, andcapabilities-declaredfor every[[php-package]]entry.mochi pkg publish --to=packagist [--dry-run]: builds the package as a Composer library viaTargetPhpLibrary, GPG-signs the git tag, pings the Packagist Update API, and attaches a Sigstore attestation on the dist zip.--dry-runskips upload but exercises the signing flow.mochi pkg sync php: regenerates the PHP shims and extern declarations from the existingmochi.lockwithout re-resolving versions.
6. Build orchestration
When a Mochi program contains one or more import php "..." declarations, the MEP-55 build driver gains the following extensions:
-
Before invoking
php main.php, the driver invokespackage3/php/Bridge.PrepareVendor(workdir, mochiLock)which:- For each
[[php-package]]inmochi.lock, materialises the package source from the content-addressed cache into<workdir>/vendor/<vendor>/<package>/. - Writes a
<workdir>/vendor/autoload.phpgenerated by the bridge's autoload emitter (not by invokingcomposer installat runtime; the autoload map is pre-computed from the PSR-4 maps recorded in each package'scomposer.json). - Injects the PHP glue stubs into
<workdir>/vendor/<vendor>/<package>/MochiGlue/.
- For each
-
The MEP-55 emit pass treats each
import php "<spec>" as <alias>as a Mochiimport "./php_shims/<vendor>_<package>/shim.mochi" as <alias>shim. The shim file contains theextern fncorpus the bridge emitted. -
The PHP glue stubs (
MochiGlue/) are plain PHP files thatusethe vendor class and forward method calls. Noext-ffiis required; the glue stubs are loaded via Composer's PSR-4 autoloader alongside the vendor package. -
The driver's existing
php main.phpinvocation (orphp main.phar,frankenphp, etc.) picks up thevendor/autoload.phpand the glue stubs automatically. -
The driver invalidates the cache when any
[[php-package]]reflection-sha256changes.
7. PHP reflection CLI
The reflection CLI is a Go-managed PHP script (package3/php/reflect/reflect.php) invoked as:
php reflect.php <package-path> [--php-version=8.4] [--include-docblocks=true]
The script uses PHP's built-in Reflection API to walk the package tree:
foreach ($classNames as $className) {
$rc = new ReflectionClass($className);
$surface[] = extractClass($rc); // includes properties, methods, constants
}
foreach ($funcNames as $funcName) {
$rf = new ReflectionFunction($funcName);
$surface[] = extractFunction($rf);
}
It supplements the runtime Reflection API with PHPDoc parsing (via a minimal inline parser; no heavy PHPDocumentor dependency) to recover array shapes, generic type hints, and narrowed return types. The output is a JSON document with one entry per public item.
The Go bridge invokes the CLI via exec.Command("php", "reflect.php", packagePath) and parses the stdout JSON. The Go binary verifies the SHA-256 of the output and writes it to [[php-package]] reflection-sha256.
8. Packagist v2 sparse API
The bridge fetches package metadata from the Packagist v2 sparse API endpoint:
GET https://packagist.org/p2/<vendor>/<package>.json
The response is a JSON document with a packages object containing one array per version, each entry including version, dist (URL + type + shasum), require, autoload, description, license, and php. The bridge:
- Selects the version satisfying the manifest constraint using PubGrub-derived semver resolution (MEP-57's solver, extended for Composer's
^and~operators). - Downloads the
dist.urlzip into~/.cache/mochi/php-deps/<sha256-hex>/. - Verifies
sha256(downloaded-zip) == dist.shasumfrom the Packagist response. - Extracts the package source tree.
The Packagist v2 endpoint also has a provider-includes index (/packages.json) for full-catalog scanning (used during mochi pkg lock for transitive dependency resolution) and a metadata-changes stream for mirror synchronisation.
9. TargetPhpLibrary
TargetPhpLibrary is a new MEP-55 build target that lowers a Mochi package to a publish-ready Composer library. Where TargetPhpSource emits a single main.php, the library target emits:
src/
<PascalVendor>/
<PascalPackage>/
<ClassName>.php (one file per exported type)
functions.php (exported free functions)
composer.json (PSR-4 autoload + metadata from mochi.toml)
README.md (from mochi.toml [package].readme or generated)
LICENSE (SPDX expression from mochi.toml)
The composer.json is synthesised from mochi.toml:
{
"name": "<php.publish.packagist-name>",
"description": "<php.publish.packagist-description>",
"type": "library",
"license": "<php.publish.license>",
"require": {
"php": "^8.4",
"mochi/runtime": "^<version>"
},
"autoload": {
"psr-4": {
"<psr4-namespace>": "src/"
}
}
}
The driver gates TargetPhpLibrary on Driver.LibraryMode = true plus target == TargetPhpLibrary.
10. Packagist publish flow
mochi pkg publish --to=packagist
- Builds the library via
TargetPhpLibrary. - Validates
composer.jsonschema andphp -lchecks all emitted PHP files. - Tags the git commit with a semver tag (e.g.,
v1.2.3) using GPG signing:git tag -s -u <key-id> v1.2.3. - Pushes the tag to the canonical git remote.
- Produces a dist zip of the
src/+composer.json+README.md+LICENSEtree. - Attaches a Sigstore
actions/attest-build-provenance@v1OIDC attestation on the dist zip (reusing MEP-55 Phase 18'sTRUST.mdtrust chain). - Pings the Packagist Update API:
POST https://packagist.org/api/update-package?username=<user>&apiToken=<token>{ "repository": { "url": "https://github.com/example/my-mochi-lib" } }
- Records the dist zip SHA-256 and Sigstore attestation reference in the build provenance.
The Packagist GitHub App integration (where registered, the App auto-triggers on tag push without a manual API ping) can substitute for step 7 in CI environments. See 07-packagist-trusted-publishing-gap for the OIDC gap discussion.
Phases
See /docs/implementation/0075/ for the per-phase tracking matrix. Fifteen phases cover skeleton (0), Packagist v2 sparse client (1), Composer dist fetcher and content-addressed cache (2), PHP reflection CLI (3), PHP-to-Mochi type mapping table (4), Mochi extern emitter (5), import php grammar wiring and MEP-55 build orchestration (6), autoload.php and composer install integration (7), mochi.lock integration (8), TargetPhpLibrary emit (9), Packagist publish flow (10), interface and abstract class bridge (11), async PHP bridge via ReactPHP/RevoltPHP (12), phar distribution path (13), and full 24-package fixture corpus gate plus mochi.lock round-trip (14).
A phase is LANDED only when its gate is green against the curated 24-package fixture corpus.
Target matrix
| Phase | PHP 8.4.0 (ubuntu-24.04) | PHP 8.4 latest (ubuntu-24.04) | PHP 8.5 (allow-failure) |
|---|---|---|---|
| 0. skeleton | NOT STARTED | n/a | n/a |
| 1. Packagist v2 sparse client | NOT STARTED | n/a | n/a |
| 2. dist fetcher + cache | NOT STARTED | required | n/a |
| 3. PHP reflection CLI | NOT STARTED | required | allow-failure |
| 4. type-mapping table | NOT STARTED | required | allow-failure |
| 5. extern emitter | NOT STARTED | required | allow-failure |
| 6. import php grammar + MEP-55 orchestration | NOT STARTED | required | allow-failure |
| 7. autoload + composer install | NOT STARTED | required | allow-failure |
| 8. mochi.lock integration | NOT STARTED | required | allow-failure |
| 9. TargetPhpLibrary emit | NOT STARTED | required | allow-failure |
| 10. Packagist publish flow | NOT STARTED | n/a (publish is host-only) | n/a |
| 11. interface/abstract class bridge | NOT STARTED | required | allow-failure |
| 12. async bridge (ReactPHP/RevoltPHP) | NOT STARTED | required | allow-failure |
| 13. phar distribution | NOT STARTED | required | allow-failure |
| 14. 24-package fixture corpus + lock round-trip | NOT STARTED | required | allow-failure |
A phase marked n/a for a target is intentional.
Alternatives considered
-
Use PHPStan's reflection layer instead of the PHP Reflection API. PHPStan ships a powerful static-analysis reflection that understands PHPDoc generics, array shapes, and conditional return types. Rejected for the primary path: PHPStan is a dev-only dependency and requires the full package's source plus its dev dependencies to run. The PHP Reflection API is available wherever
phpis installed. PHPStan's output is used as an augmentation layer (via Psalm stubs) when native reflection is insufficient, but is not the primary bridge. -
Use PHP-Parser (nikic/php-parser) instead of the Reflection API. PHP-Parser gives a full AST of any PHP source file without executing it. Rejected: PHP-Parser does not resolve types across files; it sees the raw AST before any class-loading happens. The Reflection API runs post-loading and has resolved types. PHP-Parser could work for simple packages but fails on packages that use PHP's type coercion to specialise return types based on arguments; the Reflection API handles these cases by observing the declared type at load time.
-
Require Psalm stubs as the mandatory surface format. Psalm stubs (
.phpstubfiles) are the most precise machine-readable PHP API description available; they include generics, array shapes, and conditional return types. Rejected as the mandatory format: the vast majority of Packagist packages do not ship Psalm stubs. The bridge reads Psalm stubs when present (augmenting the Reflection API output) but falls back to PHPDoc + Reflection API for packages without stubs. -
Use phpDocumentor's Reflection component as the ingest engine. phpDocumentor ships a Reflection library that parses PHP source and PHPDoc in a single pass. Rejected: it is a heavy Composer dependency (~40MB installed), it requires the full PHP toolchain in the Go bridge's runtime, and it does not run on PHP-less build hosts. The bridge's reflection CLI is a self-contained PHP script (~200 lines) with zero Composer dependencies.
-
Generate extern "C" wrappers (like MEP-73 and MEP-74). PHP has no stable C ABI that other language targets can link against. The
ext-ffiPHP extension allows PHP to call into C, but not the reverse. Rejected: PHP packages can only be called from PHP programs; the bridge is PHP-to-PHP by construction. -
Use phpize + extension authoring for deep integration. A PHP extension authored in C could expose a Mochi runtime to PHP applications. Rejected for v1: extension authoring requires C, a PHP build environment, and platform-specific compilation. This violates the zero-boilerplate promise and is unnecessary because both sides are PHP.
-
Use
amphp/ampinstead ofreact/event-loopfor the async bridge. amphp/amp (the Amp framework, which ships the RevoltPHP scheduler) and ReactPHP use the same underlying RevoltPHP event loop sincerevolt/event-loopwas extracted as a shared dependency in 2022. Rejected as a distinct alternative: both are valid; the bridge supports both via[php.async] event-loop = "revolt"(for amphp/Revolt) and"react"(for ReactPHP). The default is"react"because ReactPHP has wider ecosystem adoption. -
Require OIDC trusted publishing for Packagist and block publish until Packagist ships it. Rejected: this would make the MEP-75 publish direction unavailable indefinitely. The GPG-signed git tag plus Sigstore attestation workaround provides a credible supply-chain story in the interim and will be replaced by native OIDC when Packagist ships it.
-
Use
composer requireandcomposer installat bridge time instead of a content-addressed cache. Rejected:composer installresolves the full dependency graph at install time, is network-dependent, and does not integrate with the MEP-57 lockfile model. The content-addressed cache stores immutable dist zips keyed by SHA-256 and is reproducible across machines. The bridge pre-computes thevendor/autoload.phpfrom the pinned lockfile, socomposer installis only needed for the full publish gate. -
Emit PSR-0 layouts for backward compatibility with old PHP projects. Rejected: PSR-0 is deprecated. Packagist still accepts PSR-0 packages but does not recommend them. The
TargetPhpLibrarytarget is a modern, forward-looking emitter; PSR-4 is the only viable choice. -
Translate PHP's
mixedtype asanyin Mochi. Rejected: Mochi has noanytype analogous to TypeScript'sany.mixedwould require either a special Mochi type (a language-level change) or a silent skip. The bridge takes the skip path with a SkipReport entry, consistent with the MEP-73 approach todyn TraitandBox<dyn Future>. -
Support PHP 8.0 and earlier in the bridge. Rejected: PHP 8.0 lacks readonly properties, first-class callable syntax, and union types in the Reflection API. PHP 8.1 (December 2021) is the minimum that gives the bridge a coherent type surface. The
[php] php-versionfloor is"8.1".
Risks
-
PHP Reflection API is not sufficient for all type information. PHP's native
ReflectionTypecannot express array shapes (list<int>vsarray<string, mixed>), generics (PHPDoc@template), or narrowed conditional return types. The bridge relies on PHPDoc and Psalm stubs to fill these gaps, but not all packages provide either. Mitigation: the SkipReport documents every item where type information is insufficient; users hand-writeextern fnoverrides for critical items. -
Packagist dist zip URLs are not stable across time. Packagist's
dist.urlfor a given package version may change (GitHub tarball URLs change format after repository renames or migrations). The bridge recordsdist-sha256in the lockfile; if the URL changes but the content is the same, a re-fetch with verification succeeds. If the content changes, the--checkfails and the user must explicitly re-lock. -
No OIDC trusted publishing on Packagist. The publish direction carries a known gap: Packagist API tokens are long-lived and stored in CI secrets. Mitigation: the GitHub App integration (auto-webhook on tag push) reduces the attack surface; GPG-signed tags provide signing provenance; Sigstore attestation provides a transparency-log entry. The gap is documented in 07-packagist-trusted-publishing-gap and will be closed when Packagist ships OIDC.
-
PHP version spread in the wild (8.0-8.4 in production). Many Packagist packages declare
php: >=7.4orphp: >=8.0in theirrequireconstraints. The bridge targets PHP 8.4 (MEP-55's floor) but imports packages with lower floors. Mitigation: the bridge recordsphp-version-constraintfrom the package'scomposer.json; a package that requiresphp: ^7.4runs on PHP 8.4 without incident but the bridge warns when the package's floor is below MEP-55's floor. -
PSR-4 namespace collisions across multiple imported packages. Two packages that share a root namespace (
App\) would produce a collision in the emitted autoload map. Mitigation: the bridge prefixes the glue namespace withMochiGlue\<VendorPascal>\<PackagePascal>\to isolate the generated forwarding stubs. The user's own code in thesrc/tree uses the namespace declared in[php.publish] psr4-namespace. -
Composer version solver vs PubGrub divergence. Composer's version solver (based on Dart's PubGrub but with Composer-specific extensions for
^and~) may resolve to a different version than MEP-57's PubGrub solver. Mitigation: the bridge delegates transitive PHP dep resolution to Composer's embedded solver by runningcomposer update --no-install --dry-run --format=jsonin a temp workspace; MEP-57's solver records the result but does not second-guess it. -
ReactPHP vs RevoltPHP ecosystem fragmentation. The PHP async ecosystem split between ReactPHP and RevoltPHP in 2022 (RevoltPHP extracted as a shared scheduler; ReactPHP uses it internally but also ships its own event-loop facade). Mitigation: the bridge supports both via the
[php.async] event-loopsetting; the default"react"covers the majority of packages that target ReactPHP. -
Reflection CLI PHP version pinning. The reflection CLI (
reflect.php) usesReflectionUnionType(PHP 8.0) andReflectionIntersectionType(PHP 8.1). Running the CLI under PHP < 8.1 fails with a fatal error. Mitigation: the bridge checks the PHP version before invoking the CLI and emits a clear diagnostic if the host PHP is too old. -
TargetPhpLibrary PHP 8.1 minimum. The emitter uses
readonly(PHP 8.1),enum(PHP 8.1), first-class callables (PHP 8.1), andreadonly class(PHP 8.2). A published library with these constructs cannot be consumed by PHP < 8.1 users. Mitigation: thecomposer.jsonemitted byTargetPhpLibraryalways includes"php": "^8.4"as a constraint, matching MEP-55's floor and alerting users trying to install on older PHP. -
composer installnetwork dependency in CI. Phase 7 gates on a fullcomposer installintegration test; this requires network access to Packagist in CI. Mitigation: the fixture corpus uses a local mirror of the 24 packages (pre-fetched and content-addressed in the test fixture directory); CI does not need live Packagist access after the fixtures are committed. -
Packagist Update API rate limiting. The Packagist Update API accepts one POST per repository per push. Mitigation: the bridge always pings only once per
mochi pkg publishinvocation; CI workflows use the GitHub App integration where available, eliminating the API ping entirely. -
PHP fibers and the async bridge interaction. PHP 8.1 fibers are cooperatively scheduled and do not interoperate directly with ReactPHP's event loop in all packages. Mitigation: the bridge documents the interaction in 08-async-bridge and defers fiber-native async support to a sub-phase beyond Phase 12.
Acknowledgements
This MEP builds on MEP-55 (PHP transpiler) for the ptree IR, the PHP emit pipeline, the PHP runtime library, the Composer packaging, and the Phase 18 GPG + Sigstore trust chain; on MEP-57 (Mochi module and package system) for the mochi.toml manifest, the mochi.lock lockfile, the SHA-256 content-addressed object store, and the capability declaration scheme; on MEP-73 (Mochi + Rust package bridge) and MEP-74 (Mochi + Go package bridge) for the bidirectional-bridge spec template, the closed-type-table philosophy, the synthesised-shim analogue, the SkipReport pattern, and the capability-monotonicity rule; on the PHP Reflection API and the PHP team's reflection extension for the surface extraction foundation; on PHPStan and Psalm for the static-analysis type model the bridge augments reflection with; on nikic/php-parser for prior art on PHP source-level type extraction; on Packagist and its maintainers for the v2 sparse API and the Update API; on the Composer project (Jordi Boggiano and Nils Adermann) for the PSR-4 autoloading convention, the composer.json metadata schema, and the version solver; on PSR-4 (PHP-FIG) for the autoloading standard the emitter targets; on ReactPHP and RevoltPHP for the async event-loop model the optional bridge wraps; on the Sigstore project and actions/attest-build-provenance@v1 for the attestation infrastructure the publish flow reuses; on the GPG / OpenPGP ecosystem for the git-tag signing story; on the Packagist GitHub App for the partial CI-bound publish mitigation; and on the broader PHP supply-chain initiative (Packagist ownership policies, 2FA enforcement, and the roadmap OIDC item) for the gap documentation and roadmap the bridge builds toward.