leafs/mvc-core
A core MVC layer for Leaf PHP applications, offering controllers, views, routing integration, and request/response plumbing to help organize code into a clean, structured architecture. Lightweight and extensible, it adds familiar MVC conventions to the Leaf micro-framework without unnecessary overhe...
leafs/mvc-core is not a package you install to get a feature — it is the runtime engine underneath the Leaf MVC and Leaf API skeletons. It provides the application bootstrap, kernel, base controller/model/middleware classes, and service-provider wiring that those skeletons assume exists. If you're building a Leaf app, you almost certainly should not composer require leafs/mvc-core directly; you get it transitively when you scaffold a project:
composer create-project leafs/mvc my-app # or leafs/api
cd my-app && composer show leafs/mvc-core # confirm the resolved version
Where to look first: public/index.php → whatever bootstrap file it requires (typically App/ or bootstrap/app.php) → the Application/Kernel class in vendor/leafs/mvc-core/src/. That three-hop trace tells you more about this package than any README will, because the package's documentation surface is essentially zero. Read the kernel's handle()/bootstrap() method once and you'll understand the request lifecycle for the rest of the project.
First useful exercise: generate or hand-write a controller, register a route for it, and add one middleware. That exercises the three extension points the core actually cares about (routing glue, controller resolution, middleware pipeline) and confirms your skeleton and core versions agree. Then override a single config value via .env and verify it propagates — this validates the config-loading path, which is where most "why is my app ignoring this setting" confusion originates.
Treat the package as an implementation detail with a stable-ish public seam: the base classes and the bootstrap contract. Everything else is internal.
Keep the bootstrap files thin and untouched. public/index.php and the app bootstrap belong to the skeleton, not to you. When you need to run code at boot — registering bindings, adding global middleware, aliasing a service — put it in a service provider under App/Providers/ and register it in the config's providers list. Editing vendor files here is a guaranteed future merge conflict on every composer update.
Extend the base classes once, then extend your own. Controllers, models, and middleware from core are meant to be subclassed, but subclass them indirectly. Create App\Http\Controller extending the core controller and App\Models\Model extending the core model, and have your concrete classes extend those. When a core minor release changes a protected method signature, you patch one file instead of forty.
Prefer container resolution over facades. Constructor-inject what you need, or resolve through the app container, rather than reaching for static facades in business logic. Facades are convenient in controllers, but they pin you to global state and make unit tests either slow or flaky. The container is the intended seam; use it.
Model the route → middleware → controller flow explicitly. Register middleware in the order you want it to run and be deliberate about whether it's global or route-scoped. Because the middleware pipeline lives in core, ordering bugs manifest as confusing "middleware ran twice" or "auth ran after the controller" behaviour — write the order down in a comment near the registration.
Isolate config from environment. Use env() only inside config files, and read config with the config helper in application code. This keeps you able to cache config later and keeps tests able to override config without touching process environment.
Test through the kernel, not through curl. Boot the application in your test bootstrap and dispatch synthetic requests through the HTTP kernel. You get fast integration coverage of routing and middleware without running a web server, and it's the only realistic way to test code that depends on core's request lifecycle.
Treat leafs/mvc / leafs/api as the product and mvc-core as the engine. Lock the skeleton version, let Composer resolve core, and put all your dependency-review attention on the skeleton's changelog. That's the artefact with a release process aimed at application developers.
Never pin mvc-core independently. The skeleton picks the version it was built against. Adding an explicit require or running composer update leafs/mvc-core in isolation can leave you on a core version the skeleton's bootstrap doesn't understand, and the failure mode is a blank page or a fatal in a vendor file rather than a clear incompatibility error. Pin the skeleton; let core float beneath it.
The bus factor is real. Four stars, zero listed dependents, and no dedicated documentation means you are the support team. Two consequences: (1) always read the vendor source before assuming behaviour, and (2) if you find yourself wanting to hook into a core internal, fork or vendor the package rather than subclassing deeply — a maintained fork is cheaper than fighting an upstream that has one consumer.
Don't build public abstractions on core internals. Because the package exists to serve the maintainer's own skeletons, its internals can and do change between minor versions without a deprecation cycle. Anything in Leaf\Mvc\* that isn't a base class or the bootstrap contract should be treated as private API, even though PHP won't stop you.
Namespace and naming drift. Docs and examples frequently refer to Leaf\* classes while the actual files may live under Leaf\Mvc\*, and helper functions may or may not be autoloaded depending on skeleton version. When a class "doesn't exist," check vendor/composer/autoload_psr4.php and the actual src/ tree before debugging your own code.
Config and env caching will make .env look ignored. If config caching is enabled, editing .env has no effect until the cache is cleared. Symptom: you change APP_ENV, nothing happens, and you spend an hour in the wrong file. Clear the cache first, always.
Exception handling hides errors in production mode. The core's handler is designed to be quiet outside debug mode. Set debug/development environment explicitly in your local .env and verify it took effect early in a project; otherwise you're debugging silence.
Static state leaks between tests. Facades and container singletons persist across test cases unless reset. Add a teardown that reinitialises the application container, or you'll get order-dependent test failures that look like real bugs.
Check the tag, not just the release date. The latest release is dated 2026-10-22 — confirm your skeleton's constraint actually permits it before assuming a feature or fix is present, and check whether the tag points at the same commit as the default branch. With a package this small, release hygiene is the first thing that slips.
Watch for the classic upgrade trap: run composer update for the skeleton, read its changelog, and then run your test suite. Skipping the changelog step is how you discover a core-level breaking change in production instead of in CI.
How can I help you explore Laravel packages today?