zhiephie/boilerplate-slim3
A Slim 3 boilerplate project skeleton for PHP, bundling a ready-made application structure, routing, and dependency setup so you can start building APIs and web apps quickly without wiring the framework from scratch.
This is a skeleton project, not a library. You do not composer require it into an existing app and you will not get a service provider out of it — the whole value is the directory layout, bootstrap file, and pre-wired dependencies you copy once and then own forever.
First contact, in this order:
composer.json — read this before anything else. It is the entire compatibility story: the php constraint, the pinned Slim 3 minor, and which third-party packages were wired in (Twig, Monolog, dotenv, Eloquent/illuminate/*, PHPUnit). Last release here is April 2016, so assume PHP 5.6/7.0-era constraints until proven otherwise.public/index.php — the entry point. This is where new \Slim\App($settings) happens, where $container is populated, where middleware is registered, and where routes are pulled in. Everything else hangs off this file.settings array (usually ['settings' => ['displayErrorDetails' => ..., 'db' => [...]]]), frequently hydrated from a .env via vlucas/phpdotenv. Find where .env is loaded and mirror .env.example before your first boot.src/ and the routes file — controllers/Action classes, and a routes.php or app/routes.php that gets required. This is where you will spend 90% of your day.Minimal boot:
git clone https://github.com/zhiephie/boilerplate-slim3 myapp
cd myapp && composer install
cp .env.example .env # if present
php -S localhost:8080 -t public public/index.php
The public/index.php argument to the built-in server matters — without it, pretty URLs 404 because there is no rewrite.
First use case: add a JSON endpoint end-to-end. Add the route, point it at a controller, inject the container's DB/logger, return withJson. That single vertical slice exercises every seam a Slim 3 app has (router → callable resolver → container → PSR-7 response), and it will surface any wiring breakage in the boilerplate immediately.
If you are coming from Laravel: this is a bring-your-own-everything skeleton. No Artisan, no facades, no migrations, no queue, no Blade (it is Twig if it is anything). The mental translation is AppServiceProvider → Pimple container closures, app/Http/Middleware → PSR-7/PSR-15-ish middleware, Eloquent → only if someone explicitly wired illuminate/database, and even then without migrations.
Layer it yourself on day one. The boilerplate gives you a router and a container; it does not give you architecture. The pattern that survives contact with production:
routes.php — pure route declarations, grouped by prefix and middleware, nothing else.src/Controller or src/Action) — resolve input, delegate, build the response. Keep them thin.\Slim\App.Wire the container explicitly. Slim 3's default container is Pimple. Services are closures bound to the container, so $this inside a closure is the container:
$container['db'] = function ($c) {
return new PDO($c['settings']['db']['dsn'], /* ... */);
};
$container['HomeController'] = function ($c) {
return new App\Controller\HomeController($c->get('db'), $c->get('logger'));
};
Then route to it by name: $app->get('/', 'HomeController:index');. Note the closure receives the container as an argument — use that, not $this, when you might later swap the container implementation.
Middleware composition. Global middleware via $app->add() executes LIFO — the last added runs outermost. Route-level middleware is $app->get('/x', ...)->add($m), and groups let you attach once: $app->group('/api', function () { ... })->add(new AuthMiddleware($container));. For day-to-day work the practical rules are: CORS and request-ID/logging go global (outermost), auth goes on the group, and anything that rewrites the response body goes innermost.
PSR-7 responses are immutable. This is the single biggest source of "why is my body empty" bugs:
// WRONG — the return value is discarded
$response->withJson(['ok' => true]);
return $response;
// RIGHT
return $response->withJson(['ok' => true], 201);
Same for headers: return $response->withHeader('X-Foo', 'bar');.
Input handling. $request->getParsedBody() only returns an array for form-encoded bodies, and for JSON only when the Content-Type is application/json. If clients send text/plain or omit the header, you get null — fall back to json_decode((string) $request->getBody(), true) behind a small helper rather than debugging this per-endpoint.
Views. If Twig is wired (slim/twig-view), the idiom is return $this->view->render($response, 'home.twig', $data); — the view object is in the container and returns a new response. Do not echo.
Errors. Replace the defaults in the container rather than try/catching in every controller: errorHandler (caught exceptions), phpErrorHandler (fatal/type errors), notFoundHandler, notAllowedHandler. Each must return a ResponseInterface; throwing from inside them in Slim 3 produces recursion. Emit problem+json from these four and you get consistent API errors for free.
Testing. Two tiers: instantiate services directly for unit tests; for integration, build a Slim\Http\Environment with REQUEST_METHOD/REQUEST_URI, construct a Slim\Http\Request, call $app->process($request, $response), and assert on the returned response. That is fast, needs no HTTP server, and is exactly what the boilerplate's PHPUnit wiring is for — check the PHPUnit major, it is likely 4/5 and will need upgrading before it runs on modern PHP.
Config. Keep the settings array for framework-level knobs (displayErrorDetails, addContentLengthHeader, determineRouteBeforeAppMiddleware) and put real application config in typed config objects resolved from the container, so tests can inject fakes without touching .env.
Decide the longevity question before you invest. Slim 3 is end-of-life and this skeleton was last released in 2016. The dependency tree from that period carries known CVEs (old Twig, Guzzle, Symfony components, PHPUnit). If the app is a throwaway internal tool, fine — pin it, run it behind auth, move on. If it is a product with a multi-year life, use the boilerplate as a reference for structure and start on Slim 4, which has first-class PSR-11 container support (php-di/slim-bridge), PSR-15 middleware, and active security releases. Choosing the 2016 skeleton for a new long-lived service is the main risk in this package.
PHP compatibility is the first gate, not an afterthought. Before writing a line of business logic, run composer install and the test suite on your target PHP version. Expect friction on PHP 7.4+ (deprecations: create_function, each(), non-nullable parameter ordering) and outright breakage on PHP 8 (Pimple 3 is fine, but old illuminate/* and Twig 1.x are not). If it does not boot, upgrade the leaf dependencies first rather than patching vendor code.
displayErrorDetails must be false in production. The default in dev-friendly skeletons is true, and it leaks stack traces, file paths, and query fragments to clients. Drive it from APP_DEBUG in .env and assert the production value in a test.
addContentLengthHeader = false when streaming. If you stream file downloads, the default auto-Content-Length behavior buffers and can double-set the header. Turn it off globally and set it manually where needed.
Middleware ordering is the second-biggest bug source after immutability. Because global middleware is LIFO, adding an auth middleware after your CORS middleware means CORS is outermost and 401s still get CORS headers — usually what you want. Adding it before means browser clients see opaque CORS failures on 401. Write the order down in public/index.php as a comment.
No route cache exists in Slim 3. Dispatch is regex-matching on every request. Keep the route table flat and small, do not generate hundreds of routes from data, and lean on OPcache (opcache.validate_timestamps=0 in prod) instead — that is where the real win is.
Container closures bind $this to the container. If you copy a closure out of the container or reassign it, $this changes and you get "Call to undefined method". Always prefer the $c parameter form. Also: once a service is resolved it is cached — do not mutate it per-request expecting fresh state.
Rewrites are configuration, not code. The classic "it works with php -S but 404s in nginx" failure is a missing try_files $uri /index.php$is_args$args;. Ship the nginx/Apache config with the repo, and remember the built-in server needs the router script argument.
Treat the skeleton as scaffolding, then cut the cord. Copy it, do not track upstream. Keep public/index.php to bootstrapping only (create app, register container, register middleware, require routes, run) so your own code lives entirely in src/ and is portable to Slim 4 later. The trap is letting business logic grow inside the boilerplate's bootstrap and route closures — that is what makes these projects impossible to upgrade.
Extension points worth knowing: the container is swappable (\Slim\App accepts a container implementing its interface, and the PHP-DI bridge exists for Slim 3), callableResolver can be replaced to change how 'Class:method' strings resolve, and foundHandler/errorHandler/notFoundHandler are all container keys you can override. Those four keys are the honest extension surface of a Slim 3 app — everything else you build yourself.
How can I help you explore Laravel packages today?