Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
a04e146
Parse (int) $x = expr as a cast of the assignment, like php
blacktrs Nov 3, 2026
ed6696c
Infer the type of a static slot's array-union default so it lowers to…
blacktrs Nov 3, 2026
6de67db
Forget literal-element drops registered inside a ternary or ?? arm: t…
blacktrs Nov 3, 2026
4f8dbc4
Register define()/const before classes are built, so a static propert…
blacktrs Nov 3, 2026
855090a
Extract a dynamic method call whose receiver is not a local: the inli…
blacktrs Nov 3, 2026
e1ee2b7
Enums are UnitEnum/BackedEnum for instanceof (typed and erased operan…
blacktrs Nov 3, 2026
e851960
Repros for conditional class reflection file name (#566) and two-enum…
blacktrs Nov 3, 2026
8b4672d
Build hoisted classes before any walker is synthesized from the class…
blacktrs Nov 4, 2026
a225e88
Define the syslog constants (LOG_*): host-invariant, Monolog's Syslog…
blacktrs Nov 4, 2026
28d3cfb
A trait hoisted out of a guarded if is known to reflection and trait_…
blacktrs Nov 4, 2026
b9fa663
Attribute argument factories lower in the declaring class's scope, so…
blacktrs Nov 4, 2026
bdcbcd9
is_a/is_subclass_of with allow_string on an erased local subject pick…
blacktrs Nov 4, 2026
87cabc9
An erased method call is sized by a holder that takes the arguments b…
blacktrs Nov 4, 2026
c90a1ef
A dynamic-call thunk gives its result a count of its own when the cal…
blacktrs Nov 4, 2026
8db11db
A function that returns its own bare-array parameter hands back a cou…
blacktrs Nov 4, 2026
10810e6
getParentClass of a reified generic specialization skips its own orig…
blacktrs Nov 5, 2026
02ebd37
A lazy compile unit whose declaring statement holds a return (php-par…
blacktrs Nov 5, 2026
2669ecd
Add the Serializable interface to the prelude: Symfony reflects on it…
blacktrs Nov 5, 2026
0ca6af5
Runtime-string is_a/is_subclass_of: include interfaces as ancestors i…
blacktrs Nov 5, 2026
ccfe720
is_a/is_subclass_of: erased local subject with a runtime target name …
blacktrs Nov 5, 2026
7ca5763
class_exists: Closure and Generator are engine classes (ClassExistenc…
blacktrs Nov 5, 2026
ba9959a
defined()/constant(): enum cases are class constants (Monolog Logger:…
blacktrs Nov 5, 2026
a437b7d
Trait-provided property defaults of an ancestor are initialised on su…
blacktrs Nov 6, 2026
d6af9a0
Reflection: constructor trampolines for prelude classes so newInstanc…
blacktrs Nov 6, 2026
ee050bf
array_first/array_last: callable twins so a call by runtime name reso…
blacktrs Nov 6, 2026
3c3c284
Static method called through an object receiver: evaluate the receive…
blacktrs Nov 6, 2026
df57722
Nested element write through WeakMap/ArrayObject/ArrayIterator ($m[$k…
blacktrs Nov 6, 2026
9ab06fd
Named arguments that match no fixed parameter are collected by the va…
blacktrs Nov 6, 2026
ed9d7e7
Named arguments on a parameter-typed receiver resolve the method sign…
blacktrs Nov 6, 2026
9e4f02d
unset($prop[$numericString]) normalises the key like the local-array …
blacktrs Nov 6, 2026
827f286
eval(): a no-op that answers null instead of an undefined-function trap
blacktrs Nov 6, 2026
1777238
Assignment to an element of an erased array property keeps its value …
blacktrs Nov 6, 2026
18c8ce7
isset($map[$k]->prop) on a missing element is false, not an error on …
blacktrs Nov 6, 2026
29da564
Method signature lookup sees trait-provided methods: named arguments …
blacktrs Nov 6, 2026
f4e61ff
instanceof guard on an object local: the narrowing to another class i…
blacktrs Nov 6, 2026
e592c39
Nullsafe short-circuits the whole chain: $a?->b()->c() skips ->c() wh…
blacktrs Nov 6, 2026
98f1d78
repro: private property shadowing (parent and child private of the sa…
blacktrs Nov 6, 2026
77a2c5f
A redeclared private is its own slot: the parent's is renamed in the …
blacktrs Nov 6, 2026
4a0319e
$map[$k]->p ?? $d on a missing element is the default, not an error o…
blacktrs Nov 6, 2026
c572e4c
DateTime, DateTimeImmutable, DateTimeZone: __serialize, __unserialize…
blacktrs Nov 6, 2026
c6402be
A dynamically invoked closure reading func_get_args() takes its surpl…
blacktrs Nov 7, 2026
c93c523
$o->p = &$v binds a typed property, and a stored by-ref array shares …
blacktrs Nov 7, 2026
637d857
A stored-reference element counts as addressable only when its box ca…
blacktrs Nov 7, 2026
007ab5d
Enum::from/tryFrom coerce an erased argument to the backing type and …
blacktrs Nov 7, 2026
748b5f8
A virtual call whose bodies return different classes is typed by what…
blacktrs Nov 7, 2026
39ace71
A local holding an all-object union is boxed before a merge with a ce…
blacktrs Nov 7, 2026
c7906bc
preg_replace_callback and preg_replace_callback_array honour PREG_OFF…
blacktrs Nov 7, 2026
bd7b015
A stored by-ref param retypes every holder of the method name, and an…
blacktrs Nov 7, 2026
5660e74
An enum returned through a virtual call whose other body returns a sc…
blacktrs Nov 7, 2026
eaa3e7a
A method call lowered without its signature packs a variadic once inf…
blacktrs Nov 7, 2026
db91fbb
An erased receiver hands a cell lvalue by address even beside a typed…
blacktrs Nov 7, 2026
8691392
A bare array property stored through a key not provably an int is no …
blacktrs Nov 7, 2026
3ce5306
DOMDocument::saveHTML (symfony/ux-icons)
blacktrs Nov 7, 2026
697cb3f
[...$v] drains a Traversable held in a cell or an object union, and r…
blacktrs Nov 7, 2026
19da64d
fopen's x, x+, c and c+ modes (Symfony's cache adapters write through…
blacktrs Nov 7, 2026
b06620e
ReflectionProperty sees a readonly slot uninitialized and writes it o…
blacktrs Nov 7, 2026
e5512e1
$a[$k] = $a stores the array as it was (Twig's $context['_parent'] = …
blacktrs Nov 7, 2026
ada2d41
isset behind a property of a missing element sees through a spilled base
blacktrs Nov 7, 2026
4bae304
$o->$name on an erased receiver reads the bag by class (a bagless obj…
blacktrs Nov 7, 2026
a27483c
is_iterable / is_countable answer for objects: Twig's ensureTraversab…
blacktrs Nov 7, 2026
68ffe43
$o->$name obeys visibility: isset and ?? answer unset and a read or w…
blacktrs Nov 7, 2026
a1a6764
A generator owns a bare array param: a literal the caller dropped was…
blacktrs Nov 7, 2026
868015d
A yielding foreach over an erased base walks iterators too: only arra…
blacktrs Nov 7, 2026
3449edf
A callable return of a cell takes a count and a callable compares equ…
blacktrs Nov 7, 2026
452df00
get_object_vars answers by the calling class's visibility and the obj…
blacktrs Nov 7, 2026
771fbaa
Lazy ghosts: newLazyGhost runs its initializer on the first read, wri…
blacktrs Nov 7, 2026
d79de30
isset on a lazy ghost starts its initializer before the slot is read
blacktrs Nov 7, 2026
50f67c1
Lazy gates only on the classes a ghost's slots are read through: the …
blacktrs Nov 7, 2026
6d51c0b
Dom\HTMLDocument::createFromString: a tokenizer and tree builder for …
blacktrs Nov 7, 2026
56a2926
http_build_query walks an object by its public properties: a service …
blacktrs Nov 8, 2026
fe12ec0
debug_backtrace gives each method frame its object (DEBUG_BACKTRACE_P…
blacktrs Nov 8, 2026
3cec623
ReflectionMethod::getEndLine reads the line of the closing brace the …
blacktrs Nov 8, 2026
6129057
Each backtrace frame names its call site's file: functions carry the …
blacktrs Nov 8, 2026
f53069f
Trace records take the call site's file as a push argument: a file gl…
blacktrs Nov 8, 2026
607e4b6
A static local's initial value goes to the heap, never the frame aren…
blacktrs Nov 8, 2026
d3089d0
Promote the private-property isset repro: $o->$name now obeys visibility
blacktrs Nov 8, 2026
1cd5a27
A failed fopen reports its warning to the installed error handler, an…
blacktrs Nov 8, 2026
528c6b1
examples/symfony-demo: the files that adapt symfony/demo to Manticore…
blacktrs Nov 8, 2026
ba57426
debug_backtrace releases the frames array it hands to the object fill…
blacktrs Nov 8, 2026
df2af82
Erased-base element store as an expression: release the fresh value t…
blacktrs Nov 8, 2026
7cc3942
syslog_constants_param_default: restore the class line the rebase dro…
blacktrs Nov 8, 2026
656cbbc
A builtin's result type wins over a mixed stdlib twin: array_first ov…
blacktrs Nov 8, 2026
0bae78d
stdlib_zlib_inflate_stream: no read length on the FDICT cases — zlib …
blacktrs Nov 8, 2026
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
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Third-party code is the quality bar. Each target has a runnable exit criterion.
| Symfony framework app (`examples/symfony-app`) | [x] builds and serves | routes answer 200/404 like php; cold `var/` first request included |
| Laravel app (`examples/laravel-app`) | [x] builds and serves | web and JSON routes, Eloquent/SQLite CRUD, sessions and precompiled Blade views are served; stable over repeated request rounds; output not yet diffed against php |
| php-cs-fixer (`@PSR12`, `@Symfony`) | [~] runs natively with a parallel worker pool; `@Symfony` output matches php | whole rule corpus matches php; speed competitive with native fixers |
| symfony-demo (full framework, ~79k functions) | [~] builds and runs to completion | no missing-function traps; pages answer like php |
| symfony-demo (full framework, ~79k functions) | [x] every page byte-identical to php (search: no php reference) | `examples/symfony-demo`; lazy entities, Twig, UX components; macOS arm64 checked, Linux not yet |
| HTTP server benchmark workload | [~] 64k req/s on one process, 124k on eight workers | competitive with established PHP servers |
| A Laravel-class application | [ ] | needs MySQL/PostgreSQL drivers, `openssl`, XML (see §2) |
| Composer | [ ] | `composer install` on a real project |
Expand Down
6 changes: 5 additions & 1 deletion docs/design/reference-cells.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Status: **implemented (as of 2026-09-14).** `[&$a]`, `[&$this->p]`,
`[&$a[$k]]`, `[$v, &$v]`, `$a[$k] = &$v`, cell-alias ownership, `unset` breaking the
binding and a mutated `mixed` by-ref param all match php; the deepclone witness is
byte-identical. Still open: a `PropertyAccess` as the TARGET of `=&` (a copy), the
byte-identical. Still open: the
static-property source, `R:` in `serialize` and the var_dump `&` marker. `$a = &$b` on two locals
binds through a shared box (`LocalSlots::aliasBoxNames`), and `...$xs` into a by-ref
parameter passes references (`LowerFns::lowerRefSpread`) since 2026-10-25.
Expand Down Expand Up @@ -290,3 +290,7 @@ Still open as well: #353's dynamic property name through a first-class
callable (`$f($o->$n)`) — no static name to promote; it needs a name-dispatched
`byRefAddrOf` for `DynProp_` plus promoting every declared property of the
receiver class, a cost to measure before paying it.

## `$o->p = &$v` on a typed property

A slot bound by `=&` is a cell whatever its declared type (`InferScans::findRefBoundProps` → `scanRefCellProps`), so an `array` or `?int` property holds `cell(REF, box)` like an untyped one. A by-ref array param the body keeps a reference to rides a cell too, and so does an array property handed to one; a capture does not, it binds an address already. A buffer an element write reallocates goes back through the box (`vecWriteBack`): the binding stays, and the count moved with the buffer, so nothing is dropped. An element argument marked `ArrayAccess_::$refStored` makes its base a cell channel, the same promotion `[&$a[$k]]` makes.
2 changes: 1 addition & 1 deletion docs/differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ weakening a member's visibility. Method-signature compatibility (LSP) is not che
| Feature | Behaviour |
|---|---|
| `require` / `include` of a file that is not in the build and is not a pure-data `return` file (including code written at run time) | there is no interpreter, so it cannot run. `require` / `require_once` throw `Error: Failed opening required '<path>'`, as php 8.5 does for a missing file. `include` / `include_once` evaluate to `false` and do not throw (`@include` and `include ... or` idioms keep working; there is no warning channel). `compile` and `analyze` warn (`include.unresolved`) when the path is a constant outside the compile set |
| `eval()` | not supported. The build prints `undefined-function traps (1): eval`; at run time `Call to undefined function eval()` |
| `eval()` | not run: there is no interpreter, so the call answers `null` and declares nothing. Code that needs what the evaluated string defines must have it in the build |
| variable variables (`$$n = 5`, `${'n'}`) | parse error |
| `get_defined_vars()` | not defined (trap at run time) |
| `goto` into a loop body | unsupported (forward/backward `goto` works) |
Expand Down
1 change: 1 addition & 0 deletions docs/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ executable that imports its dependencies' `.sig` and links their `.o`).
| `stdlib` | *optional* — set `false` to opt OUT of the always-on stdlib runtime. Only the self-contained compiler (which embeds `src/Runtime`) needs this. |
| `composer` | *optional* — `true` also compiles the Composer view of the project (see below); the object form `{ "vendor": false }` narrows it |
| `extensions` | *optional* — names of native extensions to bind and link. Every name must exist in the manifest's top-level `extensions` map, or the build fails |
| `lazy` | *optional* — class-name prefixes `ReflectionClass::newLazyGhost` may make ghosts of (e.g. `["App\\Entity\\"]` for Doctrine). Each class listed, and its ancestors, test an init bit on every property read; omit it and every user class does, if the program mentions `newLazyGhost` at all. A ghost of a class outside the list throws `ReflectionException` |

### Library fields

Expand Down
41 changes: 41 additions & 0 deletions examples/symfony-demo/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration

###> symfony/framework-bundle ###
APP_ENV=prod
APP_SECRET=
APP_SHARE_DIR=var/share
###< symfony/framework-bundle ###

###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
DATABASE_URL="sqlite:///%kernel.project_dir%/data/database.sqlite"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ###

###> symfony/mailer ###
MAILER_DSN=null://null
###< symfony/mailer ###

###> symfony/routing ###
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
DEFAULT_URI=http://localhost
###< symfony/routing ###
19 changes: 19 additions & 0 deletions examples/symfony-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Only the files that adapt symfony/demo to Manticore live in this repository;
# setup.sh fetches the rest of the application from upstream.
/*
!/.gitignore
!/README.md
!/setup.sh
!/manticore.json
!/.env
!/bin/
/bin/*
!/bin/warm-twig.php
!/bin/warm-assets.php
!/compat/
!/public/
/public/*
!/public/index.php
!/src/
/src/*
!/src/Kernel.php
107 changes: 107 additions & 0 deletions examples/symfony-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# symfony/demo

The [Symfony Demo application](https://github.com/symfony/demo) — Symfony 8.1, Doctrine
ORM 3.6 over SQLite, Twig, Symfony UX (Twig/Live components, Stimulus, icons), security,
translations in 30 locales — compiled by Manticore into one native binary with its own
HTTP server. No php runs when the binary serves a page.

This directory holds only what adapts the application to Manticore. `setup.sh` fetches
the rest from upstream at a pinned commit.

## Status

Every page below, requested one after another from a single long-lived process, is
byte-identical to what php 8.5 renders from the same tree (render timestamps and the
port aside):

| Page | |
|---|---|
| `/` | home |
| `/en/blog/`, `/en/blog/page/2`, `/uk/blog/` | post list, pagination, another locale |
| `/en/blog/posts/{slug}` | a post with its comments (Doctrine lazy entities) |
| `/en/blog/rss.xml` | feed |
| `/en/login` | login form |
| `/en/admin/post/` | `302` to the login form when signed out |

`/en/blog/search` renders natively; php itself fails on that page under the harness used
for the comparison, so it has no reference yet. Checked on macOS arm64; Linux is the
target platform but has not run this example yet.

## Requirements

- **Manticore built from this repository** (`bin/build` at the repository root). The
example needs compiler features that are newer than any release.
- **php 8.4+ CLI** with `intl`, `pdo_sqlite`, `mbstring`, `ctype`, `iconv`, `dom`,
`tokenizer` — for the build-time steps only (below), never at run time.
- **Composer 2**, **git**, **rsync**, and network access for the first `setup.sh`.
- **Several GB of free RAM and about 20 minutes** for the build: the application compiles
to ~79k functions. The `-O 0` binary is ~900 MB.

## Build and run

```bash
cd examples/symfony-demo
./setup.sh
../../bin/manticore build -O 0 manticore.json
APP_SECRET=change-me MANTICORE_FIBER_STACK=8388608 PORT=8000 ./bin/app
open http://127.0.0.1:8000/
```

Run the binary from this directory: it reads `config/`, `.env`, `data/database.sqlite`
and serves `public/` from the directory it was built in. Sign in with the demo accounts
from upstream's README (`jane_admin` / `kitten`).

| Variable | Default | Meaning |
|---|---|---|
| `APP_SECRET` | — | **required**, any non-empty string (Symfony's CSRF and remember-me secret) |
| `MANTICORE_FIBER_STACK` | 1 MiB | stack of the fiber each request runs in; Symfony and Twig at `-O 0` need **8 MiB** (`8388608`), less overflows the stack on the deeper pages |
| `PORT` | `8181` | TCP port on `127.0.0.1` |
| `WORKERS` | `1` | worker processes forked under a supervisor; `0` serves in this one process (use it under a debugger) |
| `APP_ENV` | `prod` | the only environment set up here: `--no-dev` leaves out the profiler and the debug bundles |
| `DEMO_RAISE` | — | `1` lets an exception escape the kernel to the server's error log instead of rendering Symfony's error page |

## Why the build-time steps run under php

A compiled binary runs no `eval` and no `include` of a file that was not compiled in. The
parts of Symfony that generate PHP at run time therefore run once, under php, before
`manticore build`, and their output is compiled into the binary (`src` in
`manticore.json`):

- **Twig templates** (`bin/warm-twig.php` → `var/cache/prod/twig`). Twig turns each
template into a PHP class on first use; the binary carries all of them.
- **Translation catalogues** (same script → `var/cache/prod/translations`). Symfony caches
each catalogue as a PHP file that it `require`s.
- **Asset map** (`bin/warm-assets.php` → `public/assets`), after `importmap:install` and
`sass:build`. The binary serves the compiled assets as static files.

So **after changing a template, a translation or an asset, run `./setup.sh` again and
rebuild**. Changes to `config/*.yaml` and `.env` need a restart only: the container is
built when the process starts.

## What differs from upstream

- `public/index.php` is the program entry: it loads `.env` itself (the Symfony Runtime
component does not run), boots the kernel **once** before the server starts, and serves
every request through `$kernel->handle()` from Manticore's `Http\Server`, including the
static files under `public/`.
- `src/Kernel.php` registers its bundles in code instead of `require`-ing
`config/bundles.php` (which `setup.sh` removes), keeps the compiled container in memory
instead of dumping it to `var/cache`, and builds lazy *services* eagerly:
`ReflectionClass::newLazyProxy` is not supported.
- `manticore.json` compiles `src/`, the packages of `composer.lock`, the bundles' PHP
config files and the warmed caches. `"lazy": ["App\\Entity\\"]` names the classes
Doctrine may create as lazy ghosts (`ReflectionClass::newLazyGhost`): only those pay
for the check on every property read.
- `compat/` stands in for two things a binary cannot load: `Composer\InstalledVersions`
(its data file) and `symfony/polyfill-deepclone` (`Closure::bind` over private state).
- `.env` sets `APP_ENV=prod`.

## Troubleshooting

- **A page crashes the process** (`exit 139`): check `MANTICORE_FIBER_STACK` first.
- **A page renders an old template or misses a translation**: the warmed caches are
compiled in; rerun `./setup.sh` and rebuild.
- **`newLazyGhost`: "is not lazy in this build"**: the class is missing from `"lazy"` in
`manticore.json`.
- **The build was interrupted**: an interrupted build leaves `bin/app` unchanged; just run
it again.
27 changes: 27 additions & 0 deletions examples/symfony-demo/bin/warm-assets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
// Run under php BEFORE `manticore build`: php bin/warm-assets.php "$PWD" (writes public/assets, which the binary serves as static files).
// Dev tool (runs under Zend). The icon cache warmer is left out: it is a cache, and it fails under php on this vendor set.
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Dotenv\Dotenv;
$root = $argv[1];
require_once $root . '/vendor/autoload.php';
(new Dotenv())->bootEnv($root . '/.env');
putenv('APP_SECRET=warm'); $_SERVER['APP_SECRET'] = $_ENV['APP_SECRET'] = 'warm';
class AssetKernel extends Kernel {
public function getProjectDir(): string { return $GLOBALS['root']; }
public function getCacheDir(): string { return $GLOBALS['root'] . '/var/cache/assets'; }
protected function build(ContainerBuilder $c): void {
parent::build($c);
$c->addCompilerPass(new class implements CompilerPassInterface {
public function process(ContainerBuilder $c): void { $c->removeDefinition('.ux_icons.event_listener.warm_icon_cache_on_asset_compile'); }
});
}
}
$app = new Application(new AssetKernel('prod', false));
$app->setAutoExit(false);
exit($app->run(new ArrayInput(['command' => 'asset-map:compile']), new ConsoleOutput()));
34 changes: 34 additions & 0 deletions examples/symfony-demo/bin/warm-twig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
// Run under php BEFORE `manticore build`: php bin/warm-twig.php "$PWD" (writes var/cache/prod/twig, listed in manticore.json src).
// Dev tool (runs under Zend): compile every Twig template into the cache dir the binary will include as units.
use App\Kernel;
use Symfony\Component\Dotenv\Dotenv;
use Twig\Loader\FilesystemLoader;
$root = $argv[1];
require_once $root . '/vendor/autoload.php';
(new Dotenv())->bootEnv($root . '/.env');
putenv('APP_SECRET=warm'); $_SERVER['APP_SECRET'] = $_ENV['APP_SECRET'] = 'warm';
class WarmKernel extends Kernel { public function getProjectDir(): string { return $GLOBALS["root"]; } protected function build(\Symfony\Component\DependencyInjection\ContainerBuilder $c): void { parent::build($c); $c->addCompilerPass(new class implements \Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface { public function process(\Symfony\Component\DependencyInjection\ContainerBuilder $c): void { $c->getDefinition("twig")->setPublic(true); } }); } }
$kernel = new WarmKernel("prod", false);
$kernel->boot();
$twig = $kernel->getContainer()->get('twig');
$loader = $twig->getLoader();
$n = 0;
foreach ($loader->getNamespaces() as $ns) {
foreach ($loader->getPaths($ns) as $dir) {
if (!is_dir($dir)) { continue; }
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS));
foreach ($it as $f) {
if (!str_ends_with($f->getFilename(), '.twig')) { continue; }
$rel = ltrim(substr($f->getPathname(), strlen($dir)), '/');
$name = $ns === FilesystemLoader::MAIN_NAMESPACE ? $rel : '@' . $ns . '/' . $rel;
try { $twig->load($name); $n++; } catch (\Throwable $e) { fwrite(STDERR, "skip $name: " . substr($e->getMessage(), 0, 100) . "\n"); }
}
}
}
// Translation catalogues are `new MessageCatalogue(...)` scripts: a binary cannot read one back, it has to be a unit.
$translator = $kernel->getContainer()->get('translator');
// The default locale first: as another locale's fallback it would load in memory and never reach the cache.
$translator->getCatalogue($translator->getLocale());
foreach ($kernel->getContainer()->getParameter('kernel.enabled_locales') as $locale) { $translator->getCatalogue($locale); }
echo "warmed $n templates; cache=", $kernel->getCacheDir(), "\n";
49 changes: 49 additions & 0 deletions examples/symfony-demo/compat/InstalledVersions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Composer;

/**
* Stand-in for Composer's runtime API inside a compiled binary: there is no vendor/composer/installed.php
* to `include` at run time, and every package the program uses is compiled in, so a package counts as
* installed (as a non-dev requirement) exactly when one of its classes is present.
*/
final class InstalledVersions
{
public static function isInstalled(string $packageName, bool $includeDevRequirements = true): bool
{
return $packageName !== 'symfony/symfony';
}

/**
* @return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage(): array
{
return [
'name' => 'app/symfony-app',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => null,
'type' => 'project',
'install_path' => '',
'aliases' => [],
'dev' => true,
];
}

public static function getInstallPath(string $packageName): ?string
{
$path = \dirname(__DIR__) . '/vendor/' . $packageName;
return \is_dir($path) ? $path : null;
}

public static function getVersion(string $packageName): ?string
{
return null;
}

public static function getPrettyVersion(string $packageName): ?string
{
return null;
}
}
36 changes: 36 additions & 0 deletions examples/symfony-demo/compat/deepclone.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

/**
* symfony/polyfill-deepclone rebuilds object graphs through Closure::bind() and reflection hacks that a compiled binary
* does not carry. A deep clone is exactly serialize() + unserialize(), so the payload carries the serialized string, plus
* the few keys symfony/var-exporter reads to clone an object AS another class (`prepared`, `classes`, `objectMeta`).
*/

function deepclone_to_array(mixed $value, ?array $allowed_classes = null, bool $allow_named_closures = false): array
{
$payload = ['serialized' => serialize($value)];
if (is_object($value)) {
$payload['prepared'] = 0;
$payload['classes'] = [get_class($value)];
$payload['objectMeta'] = 1;
}
return $payload;
}

function deepclone_from_array(array $data, ?array $allowed_classes = null, bool $allow_named_closures = false): mixed
{
$s = $data['serialized'];
$classes = $data['classes'] ?? null;
$meta = $data['objectMeta'] ?? null;
$root = $data['prepared'] ?? -1;
if (is_array($classes) && is_array($meta) && $root >= 0 && isset($meta[$root])) {
$entry = $meta[$root];
$idx = is_array($entry) ? $entry[0] : $entry;
$target = $classes[$idx] ?? $classes[0];
$orig = $classes[0];
if ($target !== $orig && str_starts_with($s, 'O:' . strlen($orig) . ':"' . $orig . '":')) {
$s = 'O:' . strlen($target) . ':"' . $target . '":' . substr($s, strlen('O:' . strlen($orig) . ':"' . $orig . '":'));
}
}
return unserialize($s, ['allowed_classes' => $allowed_classes ?? true]);
}
Loading
Loading