middag-io / framework
MIDDAG PHP Framework — DDD kernel, container, Inertia adapter, router, form engine, and domain abstractions
Requires
- php: ^8.2
- ext-curl: *
- ext-intl: *
- ext-json: *
- ext-mbstring: *
- ext-pdo: *
- middag-io/ui: ^1.0
- monolog/monolog: ^3.0
- nyholm/psr7: ^1.8
- psr/clock: ^1.0
- psr/container: ^2.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0
- psr/http-server-middleware: ^1.0
- psr/log: ^3.0
- psr/simple-cache: ^3.0
- symfony/cache: ^7.0 || ^8.0
- symfony/clock: ^7.0 || ^8.0
- symfony/dependency-injection: ^7.0 || ^8.0
- symfony/deprecation-contracts: ^3.7
- symfony/event-dispatcher-contracts: ^3.0
- symfony/http-client: ^7.0 || ^8.0
- symfony/http-foundation: ^7.0 || ^8.0
- symfony/http-kernel: ^7.0 || ^8.0
- symfony/messenger: ^7.0 || ^8.0
- symfony/property-access: ^7.0 || ^8.0
- symfony/property-info: ^7.0 || ^8.0
- symfony/psr-http-message-bridge: ^7.0 || ^8.0
- symfony/routing: ^7.0 || ^8.0
- symfony/serializer: ^7.0 || ^8.0
- symfony/uid: ^7.0 || ^8.0
- symfony/validator: ^7.0 || ^8.0
Requires (Dev)
- doctrine/dbal: ^4.0
- fakerphp/faker: ^1.23
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
- rector/rector: ^2.0
- zircote/swagger-php: ^4.0
Suggests
- doctrine/dbal: Standalone DB via DBAL (^4.0)
- illuminate/database: Standalone DB via Eloquent (^10.0)
- league/flysystem: Flysystem-backed FilesystemInterface impl for cloud/abstract storage (^3.0)
- middag-io/moodle: Moodle host adapter (^0.5)
- middag-io/wordpress: WordPress host adapter (^0.5)
- sentry/sentry: Error reporting via ErrorReporterInterface (^4.0)
- symfony/amqp-messenger: AMQP/RabbitMQ async transport for the command worker (^7.0 || ^8.0)
- symfony/console: CLI worker for async commands and scheduled-command runner (^7.0 || ^8.0)
- symfony/doctrine-messenger: Persistent DB-backed async transport / outbox for the command worker (^7.0 || ^8.0)
- symfony/lock: Single-run locking for the command worker and scheduled commands (^7.0 || ^8.0)
- symfony/rate-limiter: Rate limiting / throttling via Symfony's rate-limiter component (^7.0 || ^8.0)
- symfony/redis-messenger: Redis async transport for the command worker (^7.0 || ^8.0)
- symfony/scheduler: Standalone recurring scheduling for #[Schedule] commands via the messenger bus (^7.0 || ^8.0)
This package is auto-updated.
Last update: 2026-06-04 16:14:07 UTC
README
middag-io/framework
Symfony-grade DDD architecture that runs inside a Moodle or WordPress plugin, or standalone.
Documentation · Where it runs · Professional services · GitHub
About
middag-io/framework is a platform-agnostic PHP framework for building business domains that do not depend on their host. You write controllers, forms, queries and services against contracts, then run the exact same code inside a Moodle plugin, a WordPress plugin, or as a standalone app by swapping a thin adapter.
Your domain should not know, or care, where it runs.
Status: alpha (the public API may still change). License: Apache-2.0.
Why middag-io/framework?
Plugin code couples business logic straight to the host: $DB, $CFG and mform on Moodle, $wpdb on WordPress. That coupling is exactly what makes a plugin impossible to test without booting a full host, impossible to reuse on another platform, and fragile on every upgrade. This framework breaks the coupling, and gives you back four things:
- Testable — exercise your domain against an in-memory database, with no host running. The suite proves it.
- Portable — write the domain once; host it on Moodle, WordPress, or nothing at all, by changing one adapter.
- Maintainable — the domain outlives platform upgrades and migrations, because it never depended on the platform.
- Familiar — one coherent API over Symfony, PSR, Monolog and Inertia, not a new dialect to learn.
Built on giants
We did not reinvent the wheel. Under the hood it is Symfony (dependency injection, HTTP kernel, routing, Messenger, Validator), plain PSR contracts, Monolog and Inertia, wrapped in a single host-agnostic API. Less of our code to carry bugs, battle-tested foundations underneath.
Installation
Requires PHP ^8.2 and the curl, intl, json, mbstring and pdo extensions. Runs standalone or behind a host adapter.
composer require middag-io/framework
Quick start
A controller that loads your domain and renders an Inertia page: attribute routing, a domain service injected by the container, and not a single host API in sight.
use Middag\Framework\Http\Controller\AbstractController; use Middag\Framework\Http\Inertia\InertiaAdapter; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; final class CourseController extends AbstractController { #[Route('/courses', name: 'courses')] public function index(CourseCatalog $catalog): Response { // CourseCatalog is your own domain service, injected by the container. // No $DB, no $CFG, no host — just your code, described in PHP. return InertiaAdapter::render('Courses/Index', [ 'courses' => $catalog->published(), ]); } }
CourseCatalog is your service and index() stays plain PHP, so the exact same controller runs standalone, inside Moodle, or inside WordPress, by swapping the adapter.
And the proof of decoupling: the query layer runs with no host at all, over a plain PDO connection.
use Middag\Framework\Database\PdoConnectionAdapter; use Middag\Framework\Persistence\Query\QueryBuilder; $conn = new PdoConnectionAdapter(new PDO('sqlite::memory:')); // or a mysql:/pgsql: DSN // The QueryBuilder is immutable. ::on() binds a connection, so the terminals execute. $courses = QueryBuilder::on($conn, 'courses') ->where('published', true) // equality shortcut ->orderBy('title') ->get(); // array<int, array<string, mixed>>
What's inside
Generic plumbing, organised by concern (each concern owns its own Contract/):
| Concern | What it does |
|---|---|
| Kernel | Symfony DI container booted from a BootstrapInterface, service auto-discovery, AbstractModule, AbstractFacade, and a per-instance WordPress-style HookManager. |
| Http | PSR-15 HttpKernel, StandaloneKernel, attribute routing (#[Route]), declarative auth (#[Auth]), per-route middleware (#[Middleware]), AbstractController / AbstractApiController, and AbstractFormRequest. |
| Inertia | Full Inertia v3 protocol: lazy (optional), deferred (defer), merge (merge / deepMerge), partial reloads and asset versioning. |
| Form | AbstractForm, FormValidator, a set of field types (text, select, date, file, and more), and an Inertia renderer. |
| Persistence | An immutable QueryBuilder, an active-record Model, AbstractRepository / AbstractMapper, and Page pagination. |
| Database | ConnectionAdapterInterface with a default PdoConnectionAdapter, a SQL dialect layer, and a schema builder with migrations. |
| Bus | A command bus over Symfony Messenger (sync, plus async by transport routing), an InMemoryTransport, and a CommandWorker. |
| Logging | A Monolog-backed LoggerFactory per channel, with a RotatingStreamHandler. |
| Shared / Exception | DTOs, enums and utilities, plus a typed exception hierarchy mapped to HTTP status codes. |
Full API reference, guides and examples live at docs.middag.dev.
Where it runs
| Target | How |
|---|---|
| Standalone | Implement Middag\Framework\Kernel\Contract\BootstrapInterface, build the container with ContainerFactory::build(), and serve through StandaloneKernel. |
| Moodle | The OSS middag-io/moodle adapter (MoodleBootstrap plus the bridge contracts). |
| WordPress | The OSS middag-io/wordpress adapter (in build-out). |
An adapter is a thin layer that implements the bridge contracts (BootstrapInterface, ConfigResolverInterface, ConnectionAdapterInterface, HostEventBridgeInterface, UserContextResolverInterface, TranslatorInterface, FormRendererInterface). The framework ships a default OSS implementation of each, so it runs standalone with no adapter at all.
Open source and MIDDAG
The framework, the Moodle and WordPress adapters, and middag-io/ui are open source under Apache-2.0: the generic plumbing, free, forever.
The governed domain infrastructure that is genuinely hard to get right — reliable event delivery, async jobs with retry and audit, an EAV query engine, multi-tenancy, and licensing — is MIDDAG's proprietary product. It is built on top of this OSS and is opt-in, for when a domain outgrows the basics. The framework never imports the proprietary layer; the dependency only ever points downward.
Professional services
Adopting the framework, building a custom adapter, or migrating a legacy plugin to a testable domain? MIDDAG offers consulting and custom development. Reach us at email@middag.io or visit middag.io for more.
Development
composer test # PHPUnit composer check # PHPStan (level 6) + PHP-CS-Fixer + Rector, all dry-run composer fix # auto-fix style and Rector (composer fix:all re-settles formatting afterwards)
Git hooks are wired automatically on install; the commit-msg hook enforces Conventional Commits, and releases are cut by release-please.
Contributing
See CONTRIBUTING.md for the workflow, coding standards and quality pipeline. Please also read the CODE_OF_CONDUCT.md. Found a security issue? Follow SECURITY.md.
License
Licensed under the Apache License, Version 2.0. See LICENSE. Copyright 2026 MIDDAG.