Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Lean Laravel Package

jenssegers/lean

Lean is a lightweight Laravel/PHP package from Jens Segers that provides a simple, focused API for adding minimal functionality to your applications without extra overhead. Ideal for developers who want clean, easy-to-integrate tools.

View on GitHub
Deep Wiki
Context7

Getting Started

Pin the identity of the package before you pin the version. The three pieces of evidence disagree about what jenssegers/lean even is:

Source Claims
Repository description Slim 3 container core using league/container + auto-wiring
release/v1.0.2.md Adds request->getAttribute support to method injection
description.md A lightweight, array-returning alternative to Eloquent for Laravel

Only the first two are consistent with each other — attribute-aware method injection is a Slim 3 dispatch feature, not a Laravel one. Treat the Eloquent write-up as an artifact of the wrong package and stop reading it. If you arrived here looking for Lean::table('users')->get(), you are in the wrong repository; Laravel's own query builder or DB::table() already does that with zero dependencies.

Install against a Slim 3 app, pinned:

composer require jenssegers/lean:^1.0
composer show jenssegers/lean league/container slim/slim

The last command is not optional. Nothing else in this package tells you what you're actually running against.

Read the vendor directory, in this order. There is one changelog line and no docs, so the source is the specification:

  1. vendor/jenssegers/lean/composer.json — the constraint set is the compatibility contract.
  2. vendor/jenssegers/lean/src/ — likely a single App/container bootstrap class, ~200 lines.
  3. vendor/jenssegers/lean/tests/ — the only executable statement of intended behaviour.
  4. The v1.0.2 diff — shows exactly where in the dispatch path attribute resolution was inserted.

Budget an hour. It's a thin adapter; reading it fully is cheaper than debugging it blindly.

Bootstrap shape — the whole package is "replace Slim 3's default Pimple container with a reflection-capable league/container." Confirm class and method names against src/ before committing to this snippet:

// public/index.php
require __DIR__ . '/../vendor/autoload.php';

$app = new Jenssegers\Lean\App([
    'settings' => require __DIR__ . '/../config/settings.php',
]);

$app->get('/orders/{id}', OrderController::class . ':show');
$app->run();

Your first use case: delete one binding. Pick a controller you currently wire by hand in dependencies.php:

// Before — explicit Pimple closures you maintain forever
$container['OrderService'] = function ($c) {
    return new OrderService($c['OrderRepository'], $c['Logger']);
};

// After — nothing at all
final class OrderService
{
    public function __construct(
        private OrderRepository $orders,
        private LoggerInterface $logger,
    ) {}
}

That deletion is the value proposition. If your controllers don't have a hand-wired closure chain like this, the package has nothing to give you — see the alternatives at the end.

Implementation Patterns

The split that matters: autowire your code, bind your boundaries. Reflection can resolve any concrete class whose constructor parameters are themselves resolvable classes. It cannot invent an interface implementation, a scalar (string $dsn), or an SDK client that needs config. So:

// Autowired — no registration needed
final class OrderService {
    public function __construct(
        private SqlOrderRepository $repo,   // concrete: inferable
        private AuditLogger $audit,         // concrete: inferable
    ) {}
}

// Bound — the seams only
$container->add(OrderRepository::class, SqlOrderRepository::class);
$container->add(LoggerInterface::class, MonologLogger::class)->addArgument($config['log']);
$container->add('db.dsn', $config['db']['dsn']);   // scalar: never inferable

Every time you're tempted to register a concrete class, ask whether the constructor is genuinely ambiguous. Usually it isn't, and the registration is dead weight.

Cache resolutions on day one. Reflection on every get() is a per-request tax with no upside in production:

use League\Container\ReflectionContainer;

$container->delegate((new ReflectionContainer())->cacheResolutions());

If you skip this, you will benchmark the package against Pimple and conclude autowiring is "slow" — you'd be measuring the reflection cost, not the container.

Middleware populates, handlers declare. The v1.0.2 feature is the one pattern worth building a convention around. Upstream middleware writes request attributes; handlers receive them as typed parameters instead of digging through the request body:

// Middleware — the only place that knows how to derive context
$request = $request->withAttribute('tenantId', $tenant->getId());
$request = $request->withAttribute('actor', $jwt->getSubject());

// Handler — declares what it needs
public function show(Request $request, Response $response, string $tenantId, Actor $actor)
{
    return $this->orders->forTenant($tenantId)->forActor($actor);
}

This makes handler signatures into a readable contract: the parameter list tells you what middleware must have run. Compare with $request->getAttribute('tenantId') repeated in every action, where the dependency is invisible until runtime.

Keep $request/$response first, always. Method injection appends attribute arguments after the PSR-7 pair. Don't reorder parameters hoping reflection infers intent from types alone; the position is load-bearing.

One test per attribute, at the dispatch boundary. Don't unit-test the container; test the app through process():

$app = new Jenssegers\Lean\App(['settings' => ['logger' => ['name' => 'test']]]);
$app->get('/orders/{id}', OrderController::class . ':show');

$response = $app->process(
    (new ServerRequestFactory)->createServerRequest('GET', '/orders/1')
        ->withAttribute('tenantId', 'acme')
        ->withAttribute('actor', $actor)
);

$this->assertSame(200, $response->getStatusCode());

Unit tests of controllers stay unchanged — instantiate directly with mocks. Autowiring buys you nothing at the unit layer and adds a resolution step.

Adopt it in one commit that does nothing else. The adoption commit should be only the container swap plus the deletions it enables. Keeping it isolated makes rollback a single revert rather than an archaeology project.

Gotchas and Tips

Autowiring fails later, and sometimes it fails quietly. Pimple throws at boot when a key is missing — loud, immediate, caught by your first smoke test. Reflection resolves lazily and can succeed with a different concrete than you expected, which is worse. Before and after the swap, assert identity:

$before = get_class($old->get(OrderService::class));
$after  = get_class($new->get(OrderService::class));
// assertSame — a mismatch here is a class of bug you can't find by reading

Grep every $container->get(...) and $container['...'] in the codebase and reconcile each one. The ones inside rarely-hit branches are where this bites.

Missing attributes: does it throw or pass null? This is the single most important behavioural unknown in the package, and the changelog does not say. If middleware ordering breaks and tenantId never gets set, do you get an exception at the dispatch boundary or a null that flows into a query? Test it deliberately:

// Omit the middleware that sets tenantId
$response = $app->process($request);   // want: exception, not a 200

A silent null tenant is a data-leak-shaped bug wearing a convenience feature's clothes. If the package swallows it, add a guard middleware that asserts required attributes are present before dispatch, and run it ahead of every route in that group. Write the test even if you're confident — this is the kind of thing that changes between patch releases with no changelog entry.

Three things now compete for a handler parameter, and only one wins. The resolution order is roughly: PSR-7 pair → route placeholders → request attributes → container services. Collisions to test on day one, not in staging:

  • A route placeholder /orders/{id} and a middleware attribute literally named id.
  • A required service parameter whose class is also an attribute name.
  • Any form-request/validation layer that reads attributes before the populating middleware has run.

One test per collision. The failure mode is a handler receiving the tenant's id where the order's id was expected — plausible-looking output, wrong data.

Pin league/container explicitly and record why. The package's last release is January 2019, which means it was built against league/container 1.x/2.x. Version 3 changed registration and auto-wiring semantics; 4/5 moved further. The wrapper's assumptions do not survive that jump, and upstream will not tell you. Put the pin and a one-line rationale in the PR description — future-you is the only maintainer this package will ever have.

PHP 8 will find the rough edges first. Reflection-based autowiring touches typed properties, union types, and promoted constructor parameters — precisely what this pre-PHP-8 code never saw. Run the package's own suite against your target runtime before it touches a request path:

vendor/bin/phpunit -c vendor/jenssegers/lean/phpunit.xml.dist

A red run here costs ten minutes. Discovering it in production costs the afternoon you needed for something else.

Extension points live in league/container, not here. Everything you'd want to customize — delegate(), addArgument(), inflectors, ReflectionContainer::cacheResolutions() — is upstream API. There are no package-specific hooks, and searching for them is wasted time. Learn league/container instead; that knowledge transfers even if you drop this wrapper.

You are the maintenance plan. 31 stars, zero dependents, no release since 2019-01-14. For ~200 lines of adapter that may be acceptable — but only if you price it in:

  • Vendor a fork if it sits on a critical path.
  • Name an owner for the PHP/container upgrade cadence now, not when someone bumps PHP under deadline.
  • Assume zero community support. No issues will be answered.

The honest alternative, stated plainly: (new ReflectionContainer())->cacheResolutions() handed to a stock Slim 3 app gets you the autowiring win with a maintained dependency and no wrapper. The only thing jenssegers/lean adds on top is the v1.0.2 attribute-as-parameter behaviour. So the real decision is narrower than it looks:

If your handlers... Then
Read request attributes in the body ($request->getAttribute(...)) Adopt — the method-injection feature is the whole point
Never touch request attributes Skip it; use ReflectionContainer directly
Are greenfield or mid-migration Skip it; Slim 4's AppFactory::setContainer() makes this moot

Pick it when you're already deep in Slim 3 Pimple boilerplate, the attribute convention is real in your codebase, and the removal of glue code is worth owning a dormant dependency.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core