mrclay/props-dic
A lightweight PHP DI container that exposes services as typed properties and factory methods ($c->foo, $c->new_foo()), letting you add @property/@method PHPDoc for IDE autocomplete and static analysis. Supports lazy resolution, caching, and factories.
Install the Package:
composer require mrclay/props-dic
Create a Container Subclass:
Define a container class extending Props\Container with @property-read and @method PHPDoc annotations.
<?php
namespace App\Services;
use Props\Container;
/**
* @property-read UserRepository $users
* @method User new_user()
*/
class AppContainer extends Container
{
public function __construct()
{
$this->users = function (AppContainer $c) {
return new UserRepository();
};
$this->setFactory('user', User::class);
}
}
Use the Container: Access dependencies via properties or methods with IDE autocompletion.
$container = new AppContainer();
// IDE knows this is a UserRepository instance
$userRepo = $container->users;
// Get a fresh User instance
$user = $container->new_user();
Integrate with Laravel (Optional): Bind the container to Laravel’s service container for hybrid use:
$app->singleton(AppContainer::class, function () {
return new AppContainer();
});
Replace a generic DI call in a Laravel controller with a typed property:
// Before (generic)
public function index()
{
$users = app()->get('App\Services\UserRepository');
// No IDE hints, magic string
}
// After (Props)
public function index(AppContainer $container)
{
$users = $container->users; // IDE autocompletes UserRepository
}
Domain-Specific Containers:
Create modular containers for different layers (e.g., AuthContainer, PaymentContainer).
/**
* @property-read AuthService $auth
* @method Token new_token()
*/
class AuthContainer extends Container { ... }
Hybrid Laravel Integration: Use Props for domain logic while keeping Laravel’s container for framework services.
// In a service class
public function __construct(private AuthContainer $auth) {}
public function login()
{
$token = $this->auth->new_token(); // Fresh instance
}
Factory Extensions: Modify resolved instances before returning them.
$container->extend('user', function ($user, AppContainer $c) {
$user->setLogger($c->logger);
return $user;
});
Pimple Migration: Replace Pimple’s array access with property access:
// Pimple
$container['user'] = new User();
// Props
$container->user = new User();
Leverage Laravel’s Container for Framework Services: Keep Laravel’s container for routes, views, and middleware, but use Props for application logic.
$app->bind('auth.container', function () {
return new AuthContainer();
});
Use new_* Methods for Stateless Services:
// Always get a fresh database connection
$connection = $container->new_connection();
Combine with Laravel’s Service Providers: Register Props containers in providers:
public function register()
{
$this->app->singleton(AppContainer::class, function () {
return new AppContainer();
});
}
Static Analysis with Psalm/PHPStan: Annotate containers for type checking:
/**
* @property-read UserRepository $users
* @property-read LoggerInterface $logger
*/
class AppContainer extends Container { ... }
Caching Behavior:
new_* methods for fresh instances.$container->user; // Cached
$container->new_user(); // Fresh
Missing Factory Checks:
new_* on non-factory properties:$container->ccc = new CCC(); // Not a factory
$container->hasFactory('ccc'); // false
$container->new_ccc(); // Throws exception
IDE Annotation Lag:
Laravel Container Conflicts:
Circular Dependencies:
Check Factory Existence:
if (!$container->hasFactory('service')) {
throw new \RuntimeException("No factory for 'service'");
}
Inspect Resolved Values:
$factory = $container->getFactory('service');
dump($factory); // Debug the closure
Clear Cache Manually:
$container->clear(); // Reset all cached values
Use setFactory for Static Methods:
$container->setFactory('user', [UserFactory::class, 'create']);
Extend for Post-Processing:
$container->extend('user', function ($user) {
$user->boot();
return $user;
});
Leverage Props\Pimple for Pimple Users:
$pimple = new Props\Pimple();
$pimple['db'] = function () { return new DB(); };
// Now $pimple->db is IDE-friendly
Combine with Laravel’s app():
$container = app(AppContainer::class);
Type-Hint Container in Constructors:
public function __construct(private AppContainer $container) {}
Use for Configuration:
/**
* @property-read string $app_name
*/
class ConfigContainer extends Container {
public function __construct() {
$this->app_name = config('app.name');
}
}
Avoid Overusing extend:
Document Non-IDE-Friendly Properties:
@property-read even for simple values to keep IDE hints consistent:/**
* @property-read string $version
*/
class AppContainer extends Container {
public function __construct() {
$this->version = '1.0.0';
}
}
How can I help you explore Laravel packages today?