gnugat/micro-framework-bundle
A lightweight Symfony bundle for bootstrapping micro-frameworks and small PHP applications. It provides a minimal kernel and configuration so you can get routing and services running quickly, ideal for prototypes, simple APIs, and tiny services built on Symfony components.
gnugat/micro-framework-bundle is not a framework. It is a ~200-line bundle that wires the bare Symfony request→response loop: EventDispatcher, Router, HttpKernel, ControllerResolver, ArgumentResolver, and the RouterListener. That's it. You are trading symfony/framework-bundle for a container you can read in one sitting.
Requirements (v0.8.0). PHP 8.0+ and Symfony 6.x. The 0.8.0 release added Symfony 6 / PHP 8 support and dropped Symfony 5 and PHP 7 — so ^0.7 is the last line that runs on Symfony 5, and nothing here runs on Symfony 7 yet. Check composer.json before you start; on Symfony 7 expect to edit constraints or fork.
Install
composer require gnugat/micro-framework-bundle
Do not require symfony/framework-bundle alongside it. If a transitive dependency demands it, Composer will fail — the usual (dishonest) workaround is a replace entry in your root composer.json, which silences the resolver but does not give you the services anything expects. There is no Flex recipe; you register the bundle by hand.
Minimal kernel
<?php
// src/Kernel.php
namespace App;
use Gnugat\MicroFrameworkBundle\MicroFrameworkBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
public function registerBundles(): iterable
{
return [new MicroFrameworkBundle()];
}
public function registerContainerConfiguration(LoaderInterface $loader): void
{
$loader->load($this->getProjectDir().'/config/services.yaml');
}
}
Front controller
<?php
// public/index.php
use App\Kernel;
use Symfony\Component\HttpFoundation\Request;
require dirname(__DIR__).'/vendor/autoload.php';
$kernel = new Kernel($_SERVER['APP_ENV'] ?? 'prod', (bool) ($_SERVER['APP_DEBUG'] ?? false));
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
You can swap this for symfony/runtime (require dirname(__DIR__).'/vendor/autoload_runtime.php'; return fn (array $context) => new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);) if you want the standard-skeleton ergonomics — the bundle doesn't care, but nothing configures it for you either.
Services and routes
# config/services.yaml
services:
_defaults: { autowire: true, autoconfigure: true, public: false }
App\:
resource: '../src/*'
exclude: '../src/Kernel.php'
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
# config/routes.yaml
health:
path: /health
controller: App\Controller\HealthController
final class HealthController
{
public function __invoke(): JsonResponse
{
return new JsonResponse(['ok' => true]);
}
}
Where to look first. Read vendor/gnugat/micro-framework-bundle/Resources/config/services.xml and DependencyInjection/MicroFrameworkExtension.php. That is the entire package. Whatever service ID you don't see there, you don't have, and you must register yourself.
First use case. A single-purpose internal API: a webhook receiver, a health/readiness endpoint, a queue consumer's HTTP face, a Lambda/FaaS handler, or a sidecar service. Anything where boot time and container size matter more than batteries.
Everything is a service, nothing is magic. There is no AbstractController, so no $this->json(), $this->render(), or $this->redirectToRoute(). Return new JsonResponse(...), new Response(...), or new RedirectResponse($urlGenerator->generate('name')) directly. Inject what you need through the constructor — this is closer to Laravel's invokable controllers than to its resource controllers. On PHP 8 you can and should use constructor property promotion and readonly-style discipline (readonly properties themselves need 8.1, which you may have even though the package floor is 8.0).
Routing. YAML (or PHP array) loaders work out of the box. Attribute routing does not: #[Route] is resolved by loader classes that FrameworkBundle normally registers. Symfony 6 ships a perfectly good AttributeDirectoryLoader, but the wiring is still yours. Register it and point it at src/Controller, or accept YAML and move on. YAML is the low-friction path here.
Middleware is kernel.request / kernel.response. This is the mental translation from Laravel: there is no middleware array and no HandleMiddleware pipeline. Use event subscribers, ordered by priority. A request ID / CORS / API-key check looks like:
final class ApiKeyListener implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
// must run after RouterListener (priority 32) so routing is resolved
return [KernelEvents::REQUEST => ['onRequest', 20]];
}
public function onRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
if ($event->getRequest()->headers->get('X-Api-Key') !== '%env(API_KEY)%') {
$event->setResponse(new JsonResponse(['error' => 'forbidden'], 403));
}
}
}
Autoconfigure will tag it kernel.event_subscriber — provided the bundle registers RegisterListenersPass. Grep for it; if it's absent, add a compiler pass in your kernel or tag kernel.event_listener manually and register the pass yourself.
Adding capabilities à la carte. The pattern is: install the library, not the bundle, and define the service.
services:
Doctrine\ORM\EntityManagerInterface:
factory: ['Doctrine\ORM\EntityManager', 'create']
arguments:
- '%env(DATABASE_URL)%'
- '%kernel.cache_dir%/doctrine'
The same shape applies to Twig (Twig\Environment + FilesystemLoader), Validator (Validation::createValidatorBuilder()), Serializer, and PSR-3 logging. You are writing ten lines of YAML per library instead of inheriting a bundle's config tree — that is the whole trade.
Testing. No KernelBrowser (that lives in FrameworkBundle). Either drive the kernel directly:
$kernel = new Kernel('test', true);
$response = $kernel->handle(Request::create('/health'));
self::assertSame(200, $response->getStatusCode());
or add symfony/browser-kit and Symfony\Component\HttpKernel\HttpKernelBrowser, which only needs the kernel itself. PHPUnit 9 is what the package's own suite uses; nothing constrains yours.
Environment config. Use %env(VAR)% parameters plus getenv(). If you want .env files, composer require symfony/dotenv and call (new Dotenv())->bootEnv(...) in the front controller. Nothing loads them for you.
DoctrineBundle, TwigBundle, SecurityBundle, and similar declare a dependency on symfony/framework-bundle and will refuse to install or explode at runtime. Plan on wiring the underlying libraries, not their bundles. If you need more than two of those, this package is the wrong choice.bin/console, no debug:router, no cache:clear, no make: anything. Write throwaway scripts, or add symfony/console and register commands against the kernel by hand. Debugging a route usually means $router->getRouteCollection() in a scratch script.ErrorHandler/ErrorListener are not registered). Add a kernel.exception listener early, before you deploy anything.controller.service_arguments, kernel.event_subscriber, kernel.event_listener, and routing.loader are all inert unless the corresponding pass (RegisterControllerArgumentLocatorsPass, RegisterListenersPass, ...) is registered. Confirm against MicroFrameworkExtension and add what's missing in a Kernel::build() override. This is unchanged by 0.8.0 — the Symfony 6 bump widened the version constraint, it did not widen the feature surface.var/cache/{env}. On read-only filesystems (containers, Lambda), warm it during build: instantiate the kernel with the target env and debug=false and call $kernel->boot() in a build script, or ship a writable cache dir. Symfony 6's cache layout is the same shape; the warmup command (cache:warmup) is still absent, so it's your script.composer.json or forking. Budget for owning this code.^0.8 silently excludes every Symfony 5 app; ^0.7 excludes Symfony 6. There is no overlap window and no deprecation path, so a composer update on an older project will either fail resolution or strand you. Lock the minor and test the jump.services.xml into your own config/services.yaml and deleting the bundle. If your needs grow past a handful of endpoints, do that rather than fight the seam — you'll end up with a plain symfony/http-kernel app, which is what this always was. The 0.7→0.8 break is a good rehearsal for that move.How can I help you explore Laravel packages today?