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

Think Container Laravel Package

topthink/think-container

Think Container is a lightweight PSR-11 compatible dependency injection container from ThinkPHP. It supports binding, singleton instances, aliases, and constructor injection, giving PHP applications a simple, flexible IoC foundation for managing class dependencies.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with composer require topthink/think-container. There is no service provider, no config file, no bootstrap script — the package gives you two building blocks and expects you to wire them yourself.

Start by reading, in order:

  1. src/Container.php — the real substance. Skim every public method; bind, make, invoke, invokeMethod, instance, extend, resolving cover 95% of daily use.
  2. src/Facade.php — ~150 lines, mostly a __callStatic proxy with a static instance cache.
  3. src/exception/ — knowing the three exception classes (ClassNotFoundException, FuncNotFoundException, ContainerException) makes debugging reflection failures much faster.

Your first working snippet:

use think\Container;

$app = Container::getInstance();          // process-wide singleton
$app->bind(CacheInterface::class, RedisCache::class);
$app->bind('config', fn() => new Config(__DIR__.'/config.php'), true);

$cache = $app->make(CacheInterface::class);   // autowired via reflection
echo $cache->get('key');

Then register one facade to see the pattern:

use think\Facade;

class Cache extends Facade
{
    protected static function getFacadeClass() { return CacheInterface::class; }
}
Cache::setFacadeContainer($app);   // or rely on Container::getInstance()
Cache::get('key');                // proxies to $app->make(CacheInterface::class)->get('key')

That is the entire mental model. If you have used any PSR-11 container, the only novel piece is that make() also auto-invokes constructors and invokeMethod() will satisfy method parameters from the container.

Implementation Patterns

Interface binding is the main use case. bind($abstract, $concrete, $shared = false) stores a closure that re-resolves on every make() unless $shared is true. Use true for stateless services (config, logger, DB connection pool) and false (or instance() for a pre-built object) when you need a fresh instance per resolution.

Constructor autowiring. make() reflects the class, walks the constructor parameters, and for each one:

  • uses a value from the $vars array keyed by parameter name,
  • otherwise tries make() on the type-hinted class,
  • otherwise uses the default value,
  • otherwise throws ClassNotFoundException.

This means the $vars key is the variable name in the constructor, not the class name. $app->make(Report::class, ['format' => 'pdf']) matches __construct(Formatter $f, string $format).

Method injection for controllers/handlers. invokeMethod($method, $class, $vars) and invoke($callable, $vars) reflect the method signature and autowire. This is how ThinkPHP dispatches controllers — each action's typed parameters (request objects, services) get resolved automatically. If you write your own dispatcher, this is the method to copy.

extend() for decoration. Registered after a bind(), it wraps the resolved instance:

$app->bind('logger', fn() => new FileLogger());
$app->extend('logger', function ($logger, $app) {
    return new TimestampLogger($logger);
});

The closure receives the resolved instance and the container. Note: extend fails silently-ish if the abstract isn’t already bound — check getBind() first.

resolving() for cross-cutting hooks. $app->resolving(SomeService::class, fn($svc) => $svc->boot()) runs after every resolution of that abstract. Useful for lazy bootstrapping.

Facades are the public API layer. Extend think\Facade, implement getFacadeClass(), and everything static forwards to the container instance. Keep facades thin — they are service locators and will hide dependencies from static analysis. Use them at boundaries (controllers, Blade-style templates), not in service classes.

Testing. Reset the static state in setUp()/tearDown():

Container::setInstance(null);               // clear the singleton
Facade::clearResolvedInstances();           // clear facade cache

Then Container::setInstance(new Container()) or a test double. Because the container is a process-wide singleton, anything that caches a resolved instance across tests will bleed.

Gotchas and Tips

The container is a global static. Container::getInstance() returns the same object everywhere. Swapping it mid-request (e.g. per-tenant scoping) requires setInstance() plus Facade::clearResolvedInstances(), otherwise facades keep the old instance. Prefer passing the container explicitly where you can.

Facade caching is the #1 source of confusion. Facade caches resolved instances in a static $instances array keyed by class name. If you rebind the underlying abstract, the facade keeps serving the stale object until you call clearResolvedInstance(). In long-running workers (Swoole, RoadRunner) you must clear it between requests.

Constructor parameter names are the $vars keys. Type hints give you type-based resolution; primitives are matched by name. A typo in $vars produces a ClassNotFoundException that mentions the class passed to make, not the missing parameter — read the previous stack frame.

Circular dependencies throw, not resolve. There is no lazy proxy fallback. If A depends on B and B on A, you get a ContainerException. Break the cycle with instance(), bind() to a closure, or by introducing a setter/method injection step.

shared = true caches lazily on first make(). The closure runs once. If it throws, the cache is not populated, so retries are safe — but a closure that returns null will be cached as null, which is nearly always a bug.

invoke handles closures and [object, 'method'] arrays, not just ClassName::method strings. This is handy for one-off command handlers, but the reflection cost is non-trivial; in hot loops, make() the class once and call methods directly.

PSR-11 surface is partial. get() and has() exist and throw NotFoundExceptionInterface / ContainerExceptionInterface on miss. But set() is a ThinkPHP extension, and bind()/instance() are preferred — code relying on set() won’t port to another PSR-11 container cleanly.

Scoping helpers live outside this package. app(), bind(), make(), invoke() as global functions, and App::bindTo() for swapping containers on the app, are in topthink/framework, not here. Don’t hunt for them in this repo.

Version pinning. A 2025-05-06 release signals active maintenance, but 18 stars and zero dependents mean the ecosystem contract is ThinkPHP itself, not the PHP community at large. Pin to a minor (^2.0 or whatever your app requires) and read the diff on upgrade — there is no changelog discipline, and the container's reflection behavior is load-bearing for the whole framework.

Debugging reflection failures. Enable error_reporting(E_ALL); the container suppresses some notices around missing default values. When an injection fails, dump (new ReflectionClass($class))->getConstructor()->getParameters() and compare names against your $vars array — nine times out of ten that resolves it.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package