Service Container Resolution Analyzer
| Analyzer ID | Category | Severity | Time To Fix |
|---|---|---|---|
service-container-resolution | 🏅 Best Practices | Medium | 25 minutes |
Detects manual service container resolution (Service Locator anti-pattern) and recommends constructor dependency injection for improved testability and maintainability.
What This Checks
The Service Container Resolution Analyzer identifies usage of app(), resolve(), App::make(), and Container::getInstance() in application code where constructor dependency injection should be used instead. It detects manual container resolution patterns that hide dependencies and make code harder to test.
Detected Patterns:
app(OrderRepository::class)- Shorthand resolutionapp()->make(),app()->makeWith()- Direct container methodsresolve(PaymentGateway::class)- Global resolve helperApp::make(),App::makeWith()- Static facade callsContainer::getInstance()->make()- Container singleton accessapp()->bind(),app()->singleton()outside service providers - Binding in wrong location
Why It Matters
Manual container resolution is a recognized anti-pattern that creates several problems:
- Hidden Dependencies - Class dependencies are not visible in the constructor signature, making it unclear what a class needs to function
- Difficult Testing - Cannot easily inject mocks or stubs for testing since dependencies are resolved internally
- Tight Coupling - Creates direct dependency on Laravel's container, making code less portable
- Violation of SOLID - Breaks the Dependency Inversion Principle by depending on concrete implementations
Example Problem:
Before (❌):
class OrderProcessor
{
public function process($orderId)
{
// Hidden dependencies - you must read implementation to know what's needed
$repository = app(OrderRepository::class);
$payment = resolve(PaymentGateway::class);
$mailer = App::make('mailer');
$order = $repository->find($orderId);
$result = $payment->charge($order);
$mailer->send(new OrderConfirmation($order));
return $result;
}
}
// Testing is difficult - how do you inject mocks?
$processor = new OrderProcessor(); // No way to inject dependencies!After (✅):
class OrderProcessor
{
public function __construct(
private OrderRepository $repository,
private PaymentGateway $payment,
private Mailer $mailer
) {}
public function process($orderId)
{
$order = $this->repository->find($orderId);
$result = $this->payment->charge($order);
$this->mailer->send(new OrderConfirmation($order));
return $result;
}
}
// Testing is easy with explicit dependencies
$mockPayment = Mockery::mock(PaymentGateway::class);
$processor = new OrderProcessor($repository, $mockPayment, $mailer);How to Fix
Quick Fix (~5 minutes per class)
For simple cases with few dependencies, refactor to use constructor injection:
Before (❌):
class UserController extends Controller
{
public function index()
{
$repository = app(UserRepository::class);
return $repository->all();
}
}After (✅):
class UserController extends Controller
{
public function __construct(
private UserRepository $repository
) {}
public function index()
{
return $this->repository->all();
}
}Steps:
- Add constructor with type-hinted dependencies
- Store dependencies as private properties (PHP 8.0+) or assign in constructor
- Replace
app()calls with$this->dependency - Laravel automatically resolves constructor dependencies
Proper Fix (~25 minutes per class)
For complex classes with multiple dependencies, properly refactor to follow SOLID principles:
Before (❌):
class OrderService
{
public function create(array $data)
{
$validator = app(OrderValidator::class);
$repository = resolve(OrderRepository::class);
$mailer = App::make('mailer');
$logger = app(LoggerInterface::class);
$validator->validate($data);
$order = $repository->create($data);
$mailer->send(new OrderCreated($order));
$logger->info('Order created', ['id' => $order->id]);
return $order;
}
}After (✅):
class OrderService
{
public function __construct(
private OrderValidator $validator,
private OrderRepository $repository,
private Mailer $mailer,
private LoggerInterface $logger
) {}
public function create(array $data)
{
$this->validator->validate($data);
$order = $this->repository->create($data);
$this->mailer->send(new OrderCreated($order));
$this->logger->info('Order created', ['id' => $order->id]);
return $order;
}
}Steps:
- Identify all
app(),resolve(), andApp::make()calls - Create constructor with all dependencies as type-hinted parameters
- Store dependencies as private properties
- Replace all manual resolution with property access
- Consider using interfaces instead of concrete classes for flexibility
- Update tests to inject mocks/stubs
Configuration
Publish the config to customize behaviour:
php artisan vendor:publish --tag=shieldci-configThen in config/shieldci.php:
'analyzers' => [
'best-practices' => [
'enabled' => true,
'service-container-resolution' => [
// Directories to skip entirely
'whitelist_dirs' => [
'tests', // Tests legitimately resolve services
'database/migrations', // Migrations don't support constructor DI
'database/seeders', // Seeders need to resolve factories
'database/factories', // Factories may resolve services
'routes', // Route files use closures without DI support
],
// Class name patterns to skip (wildcards supported)
'whitelist_classes' => [
'*Command', // Artisan commands
'*Seeder', // Database seeders
'DatabaseSeeder',
'*Job', // Queued jobs
'*Listener', // Event listeners
'*Middleware', // HTTP middleware
'*Observer', // Model observers
'*Factory', // Model factories
'*Handler', // Various handler classes
],
// app() methods to skip (environment checks, container inspection)
'whitelist_methods' => [
'environment', // app()->environment()
'isLocal', // app()->isLocal()
'isProduction', // app()->isProduction()
'runningInConsole', // app()->runningInConsole()
'runningUnitTests', // app()->runningUnitTests()
'bound', // app()->bound()
'has', // app()->has()
'call', // app()->call() - method injection
],
// Service aliases that are legitimate to resolve by string
'whitelist_services' => [
'config', 'request', 'log', 'cache', 'session',
'view', 'validator', 'translator', 'events',
'router', 'db', 'auth', 'hash', 'url',
],
// Detect PSR-11 ->get() calls (default: false)
'detect_psr_get' => false,
// Detect manual instantiation of service-like classes (default: false)
'detect_manual_instantiation' => false,
// Class patterns to flag for manual instantiation
'manual_instantiation_patterns' => [
'*Service',
'*Repository',
'*Handler',
],
// Exclusions for manual instantiation (DTOs, value objects, etc.)
'manual_instantiation_exclude_patterns' => [
'*DTO',
'*Data',
'*ValueObject',
'*Request',
'*Response',
'*Entity',
'*Model',
],
],
],
],detect_manual_instantiation
When enabled, the analyzer also flags new SomeService() patterns for classes matching manual_instantiation_patterns. This catches cases where constructor injection is bypassed entirely rather than via app(). Disabled by default to avoid noise.
detect_psr_get
When enabled, also detects PSR-11 ->get() calls on the container. Disabled by default since get() is a common method name and false positives are likely without full type information.
References
- Laravel Dependency Injection - Official Laravel documentation on DI
- Service Container - Understanding Laravel's IoC container
- Service Locator is an Anti-Pattern - Martin Fowler's explanation
- SOLID Principles - Object-oriented design principles
- Dependency Inversion Principle - The "D" in SOLID
- Testing with Mocks - Laravel testing documentation
Related Analyzers
- Helper Function Abuse - Detects overuse of global helper functions
- Logic in Routes - Detects business logic in route files that should be in controllers
- Fat Model - Detects models with too many responsibilities