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

Container Laravel Package

internal/container

A lightweight service container for Laravel and PHP applications, offering straightforward bindings, singleton management, and dependency resolution to keep internal wiring explicit, modular, and testable. Ideal for package internals and smaller codebases that need clean, predictable object construc...

View on GitHub
Deep Wiki
Context7

Getting Started

Install it as a normal Composer dependency:

composer require internal/container

Look first at the README sections for autowiring, scopes, shared services / #[ScopeShared], inflectors, and destruction, then inspect src/Container.php and the tests. The tests are the fastest way to confirm the exact lifecycle semantics, especially how scopes are exited, what "shared within a scope" means in practice, and what destroy() actually tears down.

The first practical use case for a Laravel developer is a CLI tool that is shipped alongside a Laravel app but should not boot the full framework: a package command, a one-off maintenance binary, a queue-consumer helper, or a standalone import/export tool.

Minimal bootstrap:

use Internal\Container\Container;

$container = new Container();

// Interface binding.
$container->bind(
    App\Contracts\Clock::class,
    App\Support\SystemClock::class
);

// Factory binding for things that need runtime configuration.
$container->bind(
    App\Reports\ReportWriter::class,
    fn (Container $c) => new App\Reports\FileReportWriter(
        $c->get(App\Support\Filesystem::class),
        getenv('REPORT_PATH') ?: '/tmp/reports'
    )
);

// Autowiring resolves concrete classes and their constructor dependencies.
$command = $container->get(App\Commands\GenerateReportCommand::class);
$command->run();

If the class is concrete and all constructor parameters are resolvable class types, you usually do not need an explicit binding. That is the main day-to-day convenience: command classes, handlers, and small services can be resolved without a service-provider graph.

Since 1.1.0 the container also supports PHP 8.1 and exposes #[ScopeShared], an attribute-driven way to declare that a class should be shared within the current scope. That replaces the PHP 8.2+ only paths some people were using for the same effect, so a project pinned to 8.1 can adopt it without upgrading the runtime.

Implementation Patterns

Bind interfaces at the CLI boundary, autowire everything below it. Keep the container setup in the CLI entrypoint rather than scattering bindings through command classes. Bind only the external edges: clock, filesystem, HTTP client, database connection, environment-specific implementations. Let autowiring handle application services.

$container->bind(App\Contracts\HttpClient::class, App\Http\CurlClient::class);
$container->bind(App\Contracts\Clock::class, App\Support\SystemClock::class);

$container->get(App\Commands\SyncUsersCommand::class)->run();

Use factory closures for scalars and runtime values. Autowiring cannot invent primitive constructor arguments such as API keys, paths, timeouts, or flags. Bind those through closures or a small config object.

$container->bind(
    App\Mail\Mailer::class,
    fn (Container $c) => new App\Mail\Mailer(
        $c->get(App\Contracts\HttpClient::class),
        getenv('MAIL_API_KEY'),
        (int) (getenv('MAIL_TIMEOUT') ?: 10)
    )
);

Model each CLI invocation as a scope. This is the package’s most useful feature for long-running or repeated command execution. Register command-specific bindings inside a scope, resolve and execute the command, then let the scope end so state does not leak into the next invocation. Confirm the exact method name in the README; the pattern is:

$exitCode = $container->scope('sync-users', function (Container $c): int {
    $c->bind(App\Contracts\ProgressOutput::class, App\Cli\ConsoleProgressOutput::class);

    return $c->get(App\Commands\SyncUsersCommand::class)->run();
});

Use scopes for per-command configuration, per-tenant clients, request-like contexts, and anything that should not survive beyond a single unit of work.

Declare scope-shared services with #[ScopeShared] instead of binding ceremony. When a service should be constructed once per scope — a per-tenant HTTP client, a per-command buffer or accumulator, a connection wrapper — mark the class and let autowiring handle sharing. This keeps the bootstrap file focused on edge bindings and pushes lifetime declarations onto the class that owns them.

use Internal\Container\Attributes\ScopeShared; // confirm namespace in README

#[ScopeShared]
final class App\Cli\CommandBuffer
{
    private array $lines = [];

    public function push(string $line): void { $this->lines[] = $line; }
    public function flush(): array { return $this->lines; }
}

Two consumers resolved inside the same scope receive the same instance; resolving again in a new scope yields a fresh one. This is the 1.1.0 replacement for the ad-hoc "bind it as shared inside the scope" pattern, and it is what makes PHP 8.1 viable for teams that previously needed a newer runtime to express it.

Use inflectors for cross-cutting setter injection. Inflectors are useful when autowiring creates an object but a setter still needs to be called, such as logger injection, event dispatcher wiring, or applying a common configuration interface.

$container->inflector(
    App\Contracts\LoggerAware::class,
    fn (App\Contracts\LoggerAware $object, Container $c) => $object->setLogger(
        $c->get(App\Contracts\Logger::class)
    )
);

Keep inflectors small and deterministic. They run during resolution, so they should not perform I/O or depend on mutable global state.

Use explicit destruction at the end of each command or job. This is especially relevant in Laravel queue workers and long-running Artisan commands. Wrap execution in try/finally and explicitly release scoped or stateful services.

try {
    $container->get(App\Commands\ImportCommand::class)->run();
} finally {
    $container->destroy(App\Commands\ImportCommand::class);
}

If the package exposes destruction for a scope or for all scoped instances, prefer that over manually tracking individual objects — this matters more now that #[ScopeShared] instances exist and are easy to forget. The important discipline is: every scope should have a matching teardown path.

Integrate with Laravel as an internal tool container, not as a replacement container. Register the package container as a singleton in a Laravel service provider, then resolve it only inside commands or services that need explicit CLI lifecycle management.

// AppServiceProvider::register()
$this->app->singleton(\Internal\Container\Container::class, function () {
    $container = new \Internal\Container\Container();

    $container->bind(App\Contracts\Clock::class, App\Support\SystemClock::class);
    $container->bind(App\Contracts\HttpClient::class, App\Http\CurlClient::class);

    return $container;
});

Then in an Artisan command:

public function handle(\Internal\Container\Container $container): int
{
    return $container->scope('import', function ($c): int {
        return $c->get(App\Commands\ImportCommand::class)->run();
    });
}

Do not try to make Laravel’s facades, helpers, or framework internals resolve through this container. The boundary should be explicit: Laravel resolves the package container; the package container resolves the CLI tool’s internal object graph.

Use it for package-level PSR-11 compatibility. If you maintain a package that accepts a PSR-11 container but you do not want to require Laravel’s container, this package gives you a small PSR-11 implementation with autowiring, attribute-declared scope sharing, and lifecycle control. That is a better fit than pulling in a full DI framework.

Test with explicit binding swaps and teardown. In tests, bind fakes before resolving the subject, and destroy stateful services after each test. Do not rely on process isolation to hide leaked singletons — and remember that a #[ScopeShared] class resolved outside any scope may behave differently than inside one, so assert the lifetime you actually care about.

$container->bind(App\Contracts\Clock::class, new FrozenClock('2026-01-01 00:00:00'));

$command = $container->get(App\Commands\GenerateReportCommand::class);
$command->run();

$container->destroy(App\Commands\GenerateReportCommand::class);

Gotchas and Tips

Two containers mean two singleton graphs. If a service is bound in Laravel’s container and also in this container, you can accidentally create two instances with different state. Pick one owner per object graph. For Laravel applications, keep Laravel as the owner of framework services and use this container only for the isolated CLI subgraph.

#[ScopeShared] is invisible in your bootstrap file. Lifetime is now declared on the class, not in the wiring you read at startup. When debugging "why is this stale?" or "why is this a different instance?", grep the codebase for the attribute before assuming the container is misbehaving. Conversely, if you see a class that is expensive to construct and shared implicitly, the attribute is the reason.

Attributes only apply to classes the container constructs. If you hand it a pre-built instance via bind($id, $object) or a factory closure, the attribute is not consulted — your factory decides the lifetime. Mixing an instance binding with a #[ScopeShared] class is a silent override, not a conflict error.

Attribute precedence versus explicit bindings. Do not assume the attribute wins over a bind call (or vice versa) — read the tests for the resolution order in 1.1.0. Pick one style per service: attribute for library-owned classes you control, explicit binding for third-party or configurable ones. Duplicating both is how you get a class that is shared in one scope and fresh in another.

Autowiring fails on primitives and unions it cannot resolve. A constructor parameter like string $apiKey, ?LoggerInterface $logger, or an untyped $config will not be invented. Bind those explicitly or introduce a typed configuration object. When a resolution fails, read the exception chain carefully; the useful information is usually the deepest "cannot resolve parameter" message.

Scope leakage is the main production risk. In a normal CLI process, the process exits and everything is cleaned up. In a queue worker or long-running command, scopes can persist and retain stale tenant data, database connections, or configuration. Always destroy or exit scopes in finally. Treat every loop iteration as a new scope — otherwise a #[ScopeShared] accumulator will keep growing across iterations.

Inflectors can run more often than expected. They are applied during resolution, including nested autowiring. Avoid registering duplicate inflectors across bootstrap files, and avoid inflectors that mutate shared services. If an inflector depends on order relative to other inflectors, make that explicit in tests.

Destruction order matters. If service A depends on service B and both have teardown logic, destroying A before B can cause shutdown code to use a closed connection. Where the package supports ordering, declare it. Otherwise, destroy high-level command objects first and low-level infrastructure last, and make sure scope-shared instances with teardown get released at all.

PSR-11 exceptions are not Laravel exceptions. Catch Psr\Container\NotFoundExceptionInterface and Psr\Container\ContainerExceptionInterface separately when optional services are involved. Do not assume Laravel’s BindingResolutionException behavior; the package is PSR-11-first.

Do not replace Laravel’s container in an HTTP application. Laravel’s container already implements PSR-11, supports scoped bindings, and is deeply integrated with the framework. Replacing it will break facades, middleware, events, and package discovery. This package is best used as a narrow CLI tool container or as a lightweight PSR-11 implementation inside a standalone binary.

Pin the minor, and re-read the tests after each bump. The surface is stable, but 1.1.0 changed how lifetimes can be declared (#[ScopeShared]), and the semantics of an attribute interact with scopes and destruction in ways a changelog will not describe. Read the tests for scope exit and teardown before adopting the attribute pattern broadly, and pin ^1.1 rather than floating.

Debug resolution with a decorator, not with var_dump() in bindings. Wrap the container in a small PSR-11 decorator that logs every get() call, then swap it in only in development. This gives you a resolution trace without polluting production bindings — and it is the fastest way to see whether a #[ScopeShared] class is actually being shared or being rebuilt.

final class TracingContainer implements \Psr\Container\ContainerInterface
{
    public function __construct(
        private \Psr\Container\ContainerInterface $inner,
        private \Psr\Log\LoggerInterface $logger
    ) {}

    public function get(string $id): mixed
    {
        $this->logger->debug('Resolving', ['id' => $id]);

        return $this->inner->get($id);
    }

    public function has(string $id): bool
    {
        return $this->inner->has($id);
    }
}

Use has() only for genuinely optional services. Using has() before every get() hides wiring mistakes and turns missing bindings into silent nulls. Prefer failing fast in CLI tools; a missing dependency should stop the command with a clear error, not degrade output.

Overall fit for a Laravel developer: useful for isolated CLI tools, package commands, and long-running processes where explicit scope teardown matters, and now a reasonable choice for 8.1 shops that want declarative scope-shared services without a full DI framework. Not a replacement for Laravel’s container in a typical web application. Adopt it at the CLI boundary, keep the container graph small, and make destruction part of the command contract.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo