awurth/slim-base
A Composer-ready Slim Framework skeleton with routing, dependency injection, Twig views, authentication, validation, and database support, giving PHP developers a clean starting point for building web apps and APIs without boilerplate.
awurth/slim-base is not a library you composer require — it is a create-project skeleton for Slim 3. Treat it the way you'd treat laravel/laravel: you own the code the moment it's generated, and upstream changes do not flow into your app.
composer create-project awurth/slim-base my-app
cd my-app && php -S localhost:8080 -t public
The first five files to read, in order:
public/index.php — the bootstrap; shows how the Slim\App is constructed and which config files are loaded.app/settings.php — the settings array (debug flags, view paths, logger, DB credentials). This is your config/ directory.app/dependencies.php — the Pimple container. This is where every service lives.app/middleware.php — global middleware stack.app/routes.php — route definitions, plus app/src/ for your App\ namespaced code.Mental mapping from Laravel:
| Laravel | slim-base |
|---|---|
routes/web.php |
app/routes.php (split per domain as you grow) |
app/Http/Controllers |
app/src/Controllers |
| Blade | Twig (app/templates) |
| Service container + autowiring | Pimple, explicit registration only |
| Form Requests | app/src/Validation (awurth/slim-validation) |
| Eloquent | Not included — wire up Doctrine or Eloquent yourself |
php artisan |
Nothing. No CLI beyond Composer. |
First use case: add a route in app/routes.php, a controller in app/src/Controllers/, and a Twig template. That single vertical slice shows you the container, the renderer, and the response flow in about ten minutes.
Routes. Keep routes.php as an include of per-domain files (app/routes/api.php, app/routes/admin.php) as soon as you exceed ~20 routes. Use groups for prefixes and shared middleware, and name routes so you can generate URLs in Twig instead of hardcoding paths.
$app->group('/users', function () {
$this->get('', UserController::class . ':index')->setName('users.index');
$this->post('', UserController::class . ':store');
})->add($container->get('csrf'));
Controllers. Resolve dependencies through the container and render through the Twig view service. Prefer injecting the specific services a controller needs over grabbing $this->container everywhere — the latter is the Slim equivalent of app() abuse and makes controllers untestable.
class UserController
{
public function __construct($view, UserRepository $users) { ... }
public function index($request, $response)
{
return $this->view->render($response, 'users/index.twig', [
'users' => $this->users->all(),
]);
}
}
Container. Every service is a closure in dependencies.php, keyed by a string. Register repositories, mailers, and clients there. Services are shared by default, which is what you want for a DB connection and usually not what you want for anything holding request state.
Middleware. Two tiers: global in app/middleware.php, per-route via ->add(). Remember Slim 3 middleware is double-pass — function ($request, $response, $next) — not PSR-15. Add your session middleware before CSRF, and error-handling middleware outermost.
Validation. The skeleton's validation layer wraps Respect Validation. Define one validator class per form/endpoint, run it at the top of the controller action, and redirect back with flashed errors on failure. Do not scatter if (empty($request->getParsedBody()['x'])) checks through controllers.
Persistence. This is the biggest gap coming from Laravel. Pick Doctrine ORM, Doctrine DBAL, or illuminate/database and bind it in dependencies.php; then add Phinx or Doctrine Migrations for schema. Decide this before writing models, because the three options produce very different repository code.
Cross-cutting. Monolog is registered — use it instead of error_log. Twig layouts go in app/templates/layout.twig and everything {% extends %} it. Environment config goes through .env plus environment-keyed arrays in settings.php.
composer update will update Slim and your libraries but never the skeleton itself. If you care about upstream fixes, keep a bare create-project of the latest tag in a scratch directory and diff app/ occasionally.UserController type-hints UserRepository, the container will not build it unless you registered UserRepository::class as a key. Symptom: NotFoundException or "Identifier not defined". Fallback: swap Pimple for PHP-DI via the Slim container bridge, or write a small reflection-based resolver.$app->add() runs first. Session → CSRF → routing → error handler is the order you almost always want, which means adding them in reverse.getParsedBody() is null for JSON. It only parses form-encoded and multipart bodies. For a JSON API, add body-parsing middleware or decode (string) $request->getBody() yourself. This is the single most common "why is my request empty" bug.displayErrorDetails must be driven off the environment, not left true in settings.php. Pair it with Whoops in dev and a plain 500 handler in prod.var/cache must exist and be writable. Cached templates silently serve stale markup in dev — disable the cache in your dev settings block.Slim\App via $app->process($request, $response) — integration tests catch middleware-ordering and container mistakes far better than unit-testing controllers in isolation.app/middleware.php (global stack), app/dependencies.php (service overrides), the environment-keyed branches in settings.php, and the Slim\App constructor itself — passing in your own container is the cleanest way to retrofit PHP-DI without touching framework code.How can I help you explore Laravel packages today?