MVP: write generics, compile, and deploy#1
Merged
Conversation
Introduce the xphp-lang/xphp-symfony-bundle: write .xphp generics in a Symfony app, compile them to vanilla PHP during the normal build, and autoload the generated classes. Bundle: - XphpBundle (AbstractBundle) with source/target/cache, hash_length, compile_on_warmup and register_runtime_autoloader config. - XphpCompiler drives xphp's public ApplicationConsole `compile` facade in-process. - xphp:compile console command for on-demand compilation. - XphpCacheWarmer hooks compilation into cache:warmup; mandatory and fail-loud so a compile error aborts the build instead of shipping missing/stale generated PHP. - GeneratedClassLoader: runtime SPL autoloader for XPHP\Generated\*. Demo (demo/): console-only Symfony app consuming the bundle via a path repository. .xphp and plain .php coexist in src/; an app:demo command calls the compiled Catalog and prints the monomorphized Box<Plastic> class. Verified end to end: composer install, xphp:compile, app:demo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Let service-like generics participate in Symfony's container. A new `xphp.services.bindings` config declares which instantiations become services; the bundle resolves each to the transpiler's monomorphized class (via the public Registry::generatedFqn) and registers it, with per-binding autowire/autoconfigure/public/shared/tags over inheritable _defaults. - GenericServiceRegistrar builds TypeRefs from explicit template/args (nested supported) and creates the Definition + optional alias. - Registration happens in XphpBundle::loadExtension so autoconfigure flows through the normal passes; gated on non-empty bindings. - XphpCompiler gains specialization "roots": monomorphization is usage-driven, so a config-only generic (never `new`-ed in source) is bridged with a synthetic, cleaned-up instantiation. Stopgap until xphp can accept roots directly. - Resolve %kernel.project_dir% etc. against the parameter bag before touching the filesystem at build time. Demo: a cache-aside CachedFinder<T> (injecting CacheInterface + a source service) consumed by FindCommand.xphp — an .xphp command that takes CachedFinder<User> as a constructor argument and injects the specialization directly. The generic implements no interface (a T-generic can't honestly implement a User-specific one); only .xphp can name a generic, so the consumer is .xphp. Verified: #[AsCommand] and use statements survive compilation, the ctor type rewrites to the hash, and autowiring resolves it with its real deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously only the two unit-tested files were in Infection's scope (84% covered MSI over a small slice). Bring the whole bundle under test and mutation coverage. Tests added: - XphpBundleTest: configure() defaults + bounds, loadExtension param/ service wiring, the DI binding registration (service id, di_roots, resolved-path compile, source resource), build-time autoloader registration, and boot(). - XphpCompilerTest: compile output, synthetic-roots generation + cleanup, missing-source and transpiler-error paths. - XphpCacheWarmerTest, CompileCommandTest: enabled/disabled/missing-source and success/failure paths. - TempProjectTrait: throwaway .xphp project fixtures. Source refinements (remove behavior-invisible mutants instead of masking): - GeneratedClassLoader: track registered roots as a list (the boolean marker value was never read). - GenericServiceRegistrar: drop redundant ltrim() — Registry::generatedFqn and TypeRef::canonical already strip leading backslashes. - XphpCompiler: always buffer; setCatchExceptions(true) so any transpiler error becomes a uniform CompilationFailedException (also testable); drop the unused output-streaming param. - CompileCommand: drop the unreachable empty-summary branch. infection.json5 documents the remaining ignores — all provably invisible: mkdir() permission bits, the redundant explicit setCatchExceptions(true), the synthetic file's exact byte layout, the (string) casts on resolveValue(), and the empty-roots cleanup guard. Covered MSI 100% (was 84%); Makefile gate raised to --min-msi=100. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GitHub Actions workflow runs `make test` (PHPUnit) and `make test/mutation` (Infection, --min-msi=100) on PHP 8.4 with PCOV, triggered on pull requests targeting main and on pushes to main (PR merges). Calling the Makefile targets keeps a single source of truth for how the suite runs locally and in CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a parallel `demo` job that installs the demo (which consumes the bundle via a path repository) and runs both console commands, asserting their output: app:find proves the generic-service DI path (autowired CachedFinder<User>, cache-aside), app:demo proves the value-type path (monomorphized Box<Plastic>). A non-zero exit or missing output fails the build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's in it
Bundle (
src/)XphpBundle(AbstractBundle) -- config tree (source/target/cache/hash_length/compile_on_warmup/register_runtime_autoloader/services)and the wiring that turns declared generics into DI services.
XphpCompiler-- drives the transpiler's public console facade in-process;uniform
CompilationFailedExceptionon any failure.xphp:compilecommand and a mandatory, fail-loud cache warmer, socache:warmup(run by every prod build) produces the generated PHP -- and acompile error aborts the build rather than shipping stale output.
GeneratedClassLoader-- runtime SPL autoloader for theXPHP\Generated\*specializations (no
composer dump-autoloadneeded in dev).GenericServiceRegistrar-- resolves each declared binding to themonomorphized class via the transpiler's own
Registry::generatedFqnandregisters it, honoring per-binding
autowire/autoconfigure/public/shared/tagsover inheritable_defaults.Opt-in DI for generics
Declare instantiations under
xphp.services.bindings; the bundle compiles + registersthem at container-build time (so they're reflectable for autowiring/autoconfigure):
Because only
.xphpcan name a generic, the consumer of aFoo<Bar>is itself.xphp(a service, command, or controller) and injects the specialization directly.Demo (
demo/)A console-only Symfony app consuming the bundle via a Composer path repository, showing
both patterns side by side:
app:demo-- value-type generic:Box<Plastic>monomorphized and called fromcompiled code.
app:find-- generic service via DI: an.xphpcommand autowires aCachedFinder<User>(whose ownCacheInterface+ source-service deps areautowired), demonstrating cache-aside.
Notes / follow-ups
composer.lockis committed (CI resolves latest compatible deps).lazy/bindper-binding knobs deferred; the schema extends to them trivially.How to try it locally