lombokclarion / app
LombokClarion application skeleton (framework monorepo + example app).
Requires
- php: >=8.3
- ext-fileinfo: *
- ext-mbstring: *
- ext-openssl: *
- ext-pdo: *
- lombokclarion/auth: *
- lombokclarion/bus: *
- lombokclarion/config: *
- lombokclarion/console: *
- lombokclarion/container: *
- lombokclarion/http: *
- lombokclarion/laravel-flavor: *
- lombokclarion/log: *
- lombokclarion/persistence: *
- lombokclarion/routing: *
- lombokclarion/security: *
- lombokclarion/storage: *
- lombokclarion/validation: *
- lombokclarion/view: *
Requires (Dev)
This package is auto-updated.
Last update: 2026-08-11 07:13:02 UTC
README
A PHP 8.3+ Full Stack Web framework built on the philosophy explicit over magic: edge/serverless-first, domain layer with zero framework imports, containerization-ready.
Core capabilities: DI Container + AOT compilation, Routing + 3 runtime adapters (FPM/Function/Swoole), Persistence (QueryBuilder, Schema, Migrations, Seeding), ActiveRecord (opt-in), Auth (HMAC token, RBAC, Gate/Policy), Validation + i18n (24 languages).
Facades and ActiveRecord exist as opt-in packages (lombokclarion/facades,
lombokclarion/active-record, lombokclarion/laravel-flavor) but are never
loaded by default — no core package depends on them, and the domain-boundary
gate blocks them from app/Domain/**. There is no auto-discovery anywhere:
every binding, route, command, and migration is registered in an explicit
manifest file.
This repo contains working, tested code (377 tests, 0 failures) plus a
small end-to-end example app (a Widget CRUD feature) wiring all packages
together. It is not a drop-in composer create-project package yet,
every piece that exists actually runs, and is covered by tests that
actually run (377 tests, 0 failures, see "Running the tests").
Installing / consuming the packages
The packages are published to Packagist under the lombokclarion/* vendor. Pull
the whole runtime stack in one shot with the metapackage:
composer require lombokclarion/framework
lombokclarion/framework installs the full runtime (including the opt-in magic
packages — requiring it is itself the explicit opt-in choice). It
leaves out the two dev-only packages, which you add to require-dev yourself:
composer require --dev lombokclarion/testing lombokclarion/phpstan-rules
Or install packages individually — and note that requiring one never drags in
another's optional pieces (require lombokclarion/persistence does not pull
ActiveRecord):
composer require lombokclarion/routing lombokclarion/http lombokclarion/persistence
# the "magic" packages, only if you ask for them by name:
composer require lombokclarion/active-record lombokclarion/facades lombokclarion/laravel-flavor
This repository (codinglombok/LombokClarion) is the canonical monorepo where
all development happens; each package is mirrored read-only to its own repo, and
those mirrors are what Packagist watches. Contribute here, not to the mirrors. The
full rationale, the vendor/org mapping (lombokclarion/* on Packagist ↔
codinglombok/* on GitHub), and the release flow live in
docs/PUBLISHING.md.
To hack on the framework itself, clone this monorepo and use it directly — the
packages resolve to each other as path repositories, so composer install
needs no network:
git clone https://github.com/codinglombok/LombokClarion.git cd LombokClarion && composer install php bin/lombokclarion migrate && php bin/lombokclarion optimize php tests/run-all.php
Layout
packages/ 20 packages
container/ LombokClarion\Container — DI container + AOT compiler
http/ LombokClarion\Http — Request/Response/Middleware/ErrorHandler
routing/ LombokClarion\Routing — Router, Kernel, runtime adapters
bus/ LombokClarion\Bus — CommandBus/QueryBus/EventBus
config/ LombokClarion\Config — typed config compiler
persistence/ LombokClarion\Persistence — QueryBuilder/SchemaBuilder/migrations/seeding
view/ LombokClarion\View — Blade-like compiler, auto-escaping
console/ LombokClarion\Console — CLI kernel + 12 built-in commands
security/ LombokClarion\Security — hashing/CSRF/rate-limit/headers/encryption
auth/ LombokClarion\Auth — AuthManager/Gate/Policy/RBAC/TokenIssuer
i18n/ LombokClarion\I18n — Translator + DetectLocale middleware
validation/ LombokClarion\Validation — Validator/FormRequest/Rule (24 locales)
storage/ LombokClarion\Storage — LocalStorage with Storage interface
log/ LombokClarion\Log — Logger/ChannelLogger/StreamHandler/Redactor
active-record/ LombokClarion\ActiveRecord — Model base class + EagerLoader (opt-in magic)
facades/ LombokClarion\Facades — Facade base + Bus/Event/Hash (opt-in magic)
laravel-flavor/ LombokClarion\LaravelFlavor — Auth/DB facade shims (opt-in magic)
testing/ LombokClarion\Testing — HttpTestCase, fakes, ColdStartTest
phpstan-rules/ LombokClarion\PHPStanRules — SQL-injection + domain-boundary PHPStan ext
framework/ (metapackage) — one-require for full runtime stack
app/
Domain/Widget/ entity, repository interface, command+query, handlers
(zero LombokClarion\* imports — enforced by
bin/check-domain-boundary.php)
Http/Controllers/ thin controller, dispatches to CommandBus/QueryBus
Http/Requests/ FormRequest (mass-assignment-proof validation)
Infrastructure/ SqlWidgetRepository (QueryBuilder), migrations,
ServiceFactories (array-callable factories so
`optimize` can compile them)
bootstrap/
services.php every binding, one file, grep-able
routes.php every route + its middleware, one file
console.php every CLI command, one file
migrations.php explicit migration manifest (no directory scanning)
seeders.php explicit seeder manifest
externals.php external library integrations (TableSheet, etc.)
config/config.schema.php typed config schema
public/index.php HTTP entrypoint (FpmAdapter)
bin/lombokclarion CLI entrypoint
bin/check-domain-boundary.php Deptrac-equivalent CI check (see below)
tests/ 377 tests across 27 files, custom zero-dependency harness
Why a custom autoloader instead of Composer?
This environment has no network access to Packagist, so composer install
can't run here. autoload.php at the repo root is a small PSR-4 shim that
maps each package's namespace straight to its src/ folder — good enough
to run and test everything in this sandbox. Every package still ships a
real composer.json with correct autoload.psr-4 blocks; in a normal
environment you'd delete autoload.php and just
composer install && require 'vendor/autoload.php'.
Running the tests
No PHPUnit (same network restriction), so there's a ~90-line
assertion-based harness in tests/harness.php. Run everything:
php tests/run-all.php
Or a single file:
php -r "require 'tests/harness.php'; runTests('tests/ContainerTest.php');"
Trying the example app end-to-end
php bin/lombokclarion migrate # creates storage/database.sqlite php -S localhost:8080 -t public # then curl it, or: php bin/lombokclarion optimize # writes storage/services.compiled.php # and storage/config.compiled.php php bin/check-domain-boundary.php # proves app/Domain/** stays framework-free
curl localhost:8080/api/widgets
curl -X POST localhost:8080/api/widgets -d name=Lamp -d price_cents=1500
# -> 419 CSRF token mismatch (correct: CSRF is required on this route)
Design requirements mapping
| Section | Requirement | Where |
|---|---|---|
| 2.1-2.6 | No auto-discovery, explicit config, magic opt-in only | Core packages never use facades; lombokclarion/facades is opt-in with forbidden-layers: [app/Domain]; every binding is in bootstrap/services.php |
| 3 | Container to Router to Middleware to Container to Controller to Bus to Domain to Repository flow | packages/routing/src/Kernel.php |
| 3 | Domain layer zero framework imports, CI-enforced | app/Domain/Widget/* + bin/check-domain-boundary.php (passing; verified it also fails on a deliberately-introduced violation) |
| 4.1 | Container: explicit binding, no reflection auto-wiring of interfaces | packages/container/src/Container.php |
| 4.2 | Http: Request/Response value objects | packages/http/src/Request.php, Response.php |
| 4.3 | Routing: route table, middleware composition, groups | packages/routing/src/Router.php, Route.php, Kernel.php |
| 4.4 | Bus: Command/Query/EventBus, explicit registration | packages/bus/src/* |
| 4.5 | Config: typed, schema-generated | packages/config/src/ConfigCompiler.php - $config->mail->smtp->host as real typed property access, not config('a.b.c') |
| 4.6 | Kernel + FpmAdapter, FunctionAdapter, SwooleAdapter | packages/routing/src/Adapters/* |
| 4.7 | Persistence: bound-params-only QueryBuilder, SchemaBuilder, migration runner reading an explicit manifest | packages/persistence/src/* |
| 4.8 | View: Blade-like compiler, auto-escaping by default, compiled/cached ahead of time | packages/view/src/* |
| 4.9 | Console: CLI kernel, same container as HTTP | packages/console/src/* |
| 4.10 | Testing: HttpTestCase, fakes, InMemoryRepository, ColdStartTest | packages/testing/src/* |
| 4.11 | Security package: Argon2id, CSRF, rate-limit, security headers, Encrypted<T> |
packages/security/src/* |
| 5 | Cold-start budget, compiled/reflection-free boot, no persistent-process assumptions | ContainerCompiler (AOT reflection) + CompiledContainer (zero reflection) + ColdStartTest; verified the compiled boot path actually serves a request correctly end-to-end |
| 6 | Security requirements (hashing, CSRF, sessions, validation, rate limit, headers, encryption, audit:security) |
packages/security/* + AuditSecurityCommand |
| 7 | SQL/injection hardening, audit:sql, N+1 with(), MySQL non-transactional migrations |
QueryBuilder/Identifier/RawExpression + AuditSqlCommand + SchemaBuilder::migrationsAreTransactionalByDefault() |
| 9 | Domain tests need zero HTTP/DB; HttpTestCase boots the real container |
InMemoryRepository + HttpTestCase |
| 10 | No implicit retry on queued commands | RetryPolicy::none() is the only default; RetriesQueuedCommand is opt-in |
Known, deliberate limitations (documented, not bugs)
ContainerCompilercan't see inside closures. A binding registered as[FactoryClass::class, 'method']is compiled as a direct static call (zero reflection), but if that method internally does$c->get(SomethingElse::class),SomethingElsemust also have its own explicit binding (or be listed inextraRootIds) or the compiled container won't know about it. This bit the example app during development (CreateWidgetHandler/ListWidgetsHandlerare only referenced insideCommandBus::register()calls inside a factory closure) - fixed by listing them inbootstrap/console.php'sOptimizeCommandwiring. This is the correct fix, not a workaround: it's the same "explicit over magic" trade-off applied to compilation.- Runtime-constructed singletons (a
PDOconnection) can't be baked into a static compiled file.ContainerCompiler::compile()takes an$externallyProvidedlist for exactly this - the adapter constructs the connection fresh per request/invocation (per §5, no pooled connection is assumed) and callsCompiledContainer::instance(PDO::class, $pdo)before handling the request. audit:sql/audit:securityare real, working, regex-based heuristics, not a full bundled PHPStan/Psalm ruleset. They catch the concrete cases named in the design spec §6/§7 (string- concatenatedquery()/prepare()calls, unmarked{!! !!}output, missing CSRF/security-headers middleware,APP_DEBUG=truein production) and are wired intobootstrap/console.phptoday.- Config values are resolved once, at
optimizetime, per §5's "never re-parsed per request." There's no runtime env-var re-read in the compiled config - that's intentional, not an oversight.
LombokCSS starter kit (§8, §13) — now implemented
- The real LombokCSS (github.com/codinglombok/LombokCSS) is vendored
self-hosted at
resources/lombokcss/lombok.min.css(MIT, license included) — never CDN-loaded, per §8. - Reality check vs. the design spec: the actual library's vocabulary is
plain component classes (
.btn,.card,.navbar,.table) with--lc-*design tokens anddata-style/data-themeattributes — not thelc-*class prefix ordata-variant/data-elevationattributes the spec guessed. The starter views follow the actual vocabulary, which honors the spec's real rule ("use LombokCSS's own vocabulary exclusively"). - Upstream ships resonant-stark, neo-brutalism, glassmorphism (plus
modern-corporate-flat, semantic-minimalist) but not
quiet-editorial— soresources/lombokcss/quiet-editorial.cssauthors it as a preset extension following upstream's own token-remap pattern from src/themes.css. data-stylecomes fromTheme(validated at boot), fed by theTHEME_STYLEenv var through the typed config — never hardcoded in a layout. All 4 preset values verified rendering end-to-end.lombokclarion optimizenow also publishes assets with content-hashed filenames + a PHP manifest (AssetPublisher); views resolve viaAssetManifest;StaticAssetsMiddlewareserves/assets/*withCache-Control: public, max-age=31536000, immutableunderphp -S(real deployments let the web server/CDN do it).- Fixing this surfaced and fixed a real Kernel bug: global middleware now wraps the routing decision, so security headers/asset serving apply to 404/405 responses too (regression-tested).
What's not here yet (explicitly out of scope for this pass)
These are called out in the design doc but weren't built in this pass - each is additive, not a rework of what exists:
Optional packages (§4.12) — now implemented
lombokclarion/active-record— full Model base class with:create()/find()/update()/delete(),query()withwhere()/orderBy()/limit(),with()eager-loading viaEagerLoader(N+1 safe),$fillablewhitelist (mass-assignment blocked structurally). Relations declared asRelation::hasMany/hasOne/belongsTo.composer.jsoncarriesforbidden-layers: ["app/Domain"].lombokclarion/facades—Facadebase class + concrete facades (Bus,Event,Hash). Requires explicitFacade::setContainer()opt-in — never auto-discovered. Sameforbidden-layersmetadata.app/Domain/**cannot import either package (enforced bybin/check-domain-boundary.php).
Eager-loading / N+1 prevention (§7) — now implemented
EagerLoaderissues oneWHERE INquery per relation instead of N+1 lazy queries. SupportshasMany,hasOne,belongsTo.Relationvalue object:Relation::hasMany('comments', 'post_id').- Wired through
ModelQueryBuilder::with()(ActiveRecord) and directly usable from explicit repositories viaEagerLoader::load(). - 5 dedicated tests covering all relation types + edge cases.
Multi-tenancy (§11) — now implemented
- Tenancy is a request-scoped container binding pattern, not a framework
mode.
ResolveTenantmiddleware is declared per-route/group (never globally); routes without it simply have no tenant — no implicit fallback, no global toggle. - Ships
HeaderTenantResolver(readsX-Tenant-ID); theTenantResolverinterface supports subdomain/path-prefix resolvers as app-specific implementations. TenantAwareConnectionbuilds a per-tenant PDO from a DSN template with{database}placeholder — proven isolated with separate SQLite databases in tests.- Tenant flows through
RequestContext(not statics), so downstream controllers/handlers receive it via typed injection. - 8 tests covering: header resolution, missing/unknown tenant, middleware binding into context, no-tenant public routes, connection factory, isolation.
Queue/Worker (§12) — now implemented
ShouldQueuemarker interface: commands implementing it get serialized and pushed to aQueueStorebyQueuedCommandBusinstead of running inline. Commands without it dispatch immediately (parity: same handler code runs in both paths).- Default: single-attempt, no retry (§10). Opt-in via
RetriesQueuedCommand+RetryPolicy(maxAttempts, backoffSeconds). QueueWorkerpops jobs, deserializes, feeds into the realCommandBus(same handler code path as inline — §12 parity). Failed jobs retry up tomaxAttemptswith optional backoff, then go toQueueStore::fail().- Two
QueueStoreimplementations:InMemoryQueueStore(testing/dev),DatabaseQueueStore(persistent, SQLite/Postgres/MySQL). lombokclarion workCLI command:--queue=name,--loop,--sleep=N.- 8 tests covering: enqueue-vs-inline, worker processing, retry exhaustion, failed-job recording, DB store round-trip.
QueryBuilder joins — now implemented
join()andleftJoin()with fully validatedtable.columnON clauses — identifiers go throughIdentifier::validate()on both sides, no injection surface.groupBy()for aggregate queries.select(),where(),orderBy()now all accept qualifiedtable.columnreferences.- 4 new tests covering inner join, left join (null-match rows present), groupBy aggregate, and invalid-identifier rejection.
audit:sql upgraded: token-based scanner + --explain (§7)
TokenScannerreplaces the regex heuristics with PHP-tokenizer-based (AST-lite) analysis: catches multi-line concatenation, variable interpolation inside query strings, and sprintf feeding query()/prepare()/exec(), while correctly ignoring the same patterns inside comments and plain string literals (regex could do none of that reliably — proven by dedicated tests for each case).audit:sql --explainconnects to the configured DB, runs EXPLAIN (QUERY PLAN / ANALYZE per driver) on populated tables, and flags sequential scans as missing-index candidates. Verified against SQLite's modern "SCAN " plan format.- This is still not a bundled PHPStan/Psalm ruleset (the only remaining gap vs. the spec) — but it is a real static analyzer, not pattern matching, and it is wired into CI-style commands today.
Plugininterface: name() + capabilities() (['bindings','routes','commands'])- register(Container). Registration is always explicit in services.php — no composer-extra scanning, no vendor sweep, no self-registration.
PluginRegistrarenforces an optional capability allow-list, so an app can declare in code "plugins may add bindings but never routes"; violations fail loudly at registration. Duplicate registration throws.- 4 tests: registration, duplicates, allow-list blocking, null-allow-all.
- A bundled PHPStan/Psalm ruleset distributing the audit rules as real extension packages (today: the TokenScanner static analyzer above).
Plugin system (§10) — now implemented
Remaining gap vs. the spec
License
Apache License 2.0 — see LICENSE and NOTICE.
The Apache-2.0 terms cover LombokClarion's own source (packages/, app/,
bootstrap/, bin/, config/, tests/, docs/). Third-party frontend assets
vendored under resources/ (LombokCSS, LombokCharts) keep their own MIT licenses,
recorded in NOTICE and in their LICENSE-* files — they are not relicensed.