Laravel Contextual Attributes: Elegant Dependency Injection Made Simple
Laravel's service container has always been powerful, but contextual attributes introduced in recent versions take dependency injection to a new level of elegance. Instead of manually defining contextual bindings in service providers, you can now use PHP attributes to declaratively specify exactly what should be injected into your classes.
What Are Contextual Attributes?
Contextual attributes are PHP attributes that tell Laravel's service container exactly what to inject into a specific parameter, without requiring manual binding configuration. They provide a clean, readable way to specify dependencies directly where they're used.
<?php
namespace App\Http\Controllers;
use Illuminate\Container\Attributes\Storage;
use Illuminate\Contracts\Filesystem\Filesystem;
class PhotoController extends Controller
{
public function __construct(
#[Storage('local')] protected Filesystem $localDisk,
#[Storage('s3')] protected Filesystem $s3Disk,
) {
// Laravel automatically injects the correct storage disk instances
}
}
Why Use Contextual Attributes?
Before: Manual Contextual Binding
Traditionally, you'd need to define contextual bindings in a service provider:
// In a service provider
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when(VideoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});
After: Contextual Attributes
With contextual attributes, the intent is clear and co-located with the code:
class PhotoController extends Controller
{
public function __construct(
#[Storage('local')] protected Filesystem $photos,
) {}
}
class VideoController extends Controller
{
public function __construct(
#[Storage('s3')] protected Filesystem $videos,
) {}
}
Built-in Contextual Attributes
Laravel provides several built-in contextual attributes for common use cases:
Storage Attribute
Inject specific filesystem disks:
use Illuminate\Container\Attributes\Storage;
use Illuminate\Contracts\Filesystem\Filesystem;
class FileManager
{
public function __construct(
#[Storage('local')] protected Filesystem $local,
#[Storage('s3')] protected Filesystem $cloud,
#[Storage('backup')] protected Filesystem $backup,
) {}
}
Config Attribute
Inject configuration values:
use Illuminate\Container\Attributes\Config;
class AppService
{
public function __construct(
#[Config('app.name')] protected string $appName,
#[Config('app.timezone')] protected string $timezone,
#[Config('services.stripe.key')] protected string $stripeKey,
) {}
}
Auth Attribute
Inject specific authentication guards:
use Illuminate\Container\Attributes\Auth;
use Illuminate\Contracts\Auth\Guard;
class SecurityService
{
public function __construct(
#[Auth('web')] protected Guard $webGuard,
#[Auth('api')] protected Guard $apiGuard,
) {}
}
Cache Attribute
Inject specific cache stores:
use Illuminate\Container\Attributes\Cache;
use Illuminate\Contracts\Cache\Repository;
class CacheService
{
public function __construct(
#[Cache('redis')] protected Repository $redis,
#[Cache('file')] protected Repository $file,
) {}
}
Database Attribute
Inject specific database connections:
use Illuminate\Container\Attributes\DB;
use Illuminate\Database\Connection;
class DatabaseService
{
public function __construct(
#[DB('mysql')] protected Connection $mysql,
#[DB('postgres')] protected Connection $postgres,
) {}
}
Log Attribute
Inject specific log channels:
use Illuminate\Container\Attributes\Log;
use Psr\Log\LoggerInterface;
class LoggingService
{
public function __construct(
#[Log('daily')] protected LoggerInterface $dailyLog,
#[Log('slack')] protected LoggerInterface $slackLog,
) {}
}
Context Attribute
Inject values from Laravel's context system:
use Illuminate\Container\Attributes\Context;
class RequestHandler
{
public function __construct(
#[Context('user-id')] protected string $userId,
#[Context('request-id')] protected string $requestId,
) {}
}
Give Attribute
Inject specific implementations:
use App\Contracts\PaymentGateway;
use App\Services\StripeGateway;
use Illuminate\Container\Attributes\Give;
class PaymentService
{
public function __construct(
#[Give(StripeGateway::class)] protected PaymentGateway $gateway,
) {}
}
Tag Attribute
Inject all services tagged with a specific name:
use Illuminate\Container\Attributes\Tag;
class ReportGenerator
{
public function __construct(
#[Tag('reports')] protected iterable $reportGenerators,
) {}
}
CurrentUser Attribute
Inject the currently authenticated user:
use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
Route::get('/profile', function (#[CurrentUser] User $user) {
return view('profile', compact('user'));
})->middleware('auth');
When using this in a controller:
Before:
public function __invoke(Request $request)
{
$user = auth()->user();
throw_if(! $user instanceof User, new AuthenticationException);
}
After:
public function __invoke(Request $request, #[CurrentUser] User $user)
{
// User is automatically injected and type-safe
}
The CurrentUser
attribute automatically handles authentication checking and provides type-safe user injection.
RouteParameter Attribute
Inject route parameters directly:
use App\Models\Post;
use Illuminate\Container\Attributes\RouteParameter;
class PostController
{
public function show(
#[RouteParameter('post')] Post $post
) {
return view('posts.show', compact('post'));
}
}
Comprehensive Example
Here's a real-world example showing multiple contextual attributes in action:
<?php
namespace App\Services;
use App\Models\User;
use App\Contracts\NotificationService;
use App\Services\EmailNotificationService;
use Illuminate\Container\Attributes\Auth;
use Illuminate\Container\Attributes\Cache;
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Container\Attributes\DB;
use Illuminate\Container\Attributes\Give;
use Illuminate\Container\Attributes\Log;
use Illuminate\Container\Attributes\Storage;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Database\Connection;
use Psr\Log\LoggerInterface;
class UserProfileService
{
public function __construct(
#[CurrentUser] protected User $user,
#[Auth('web')] protected Guard $auth,
#[Cache('redis')] protected Repository $cache,
#[Config('app.name')] protected string $appName,
#[DB('mysql')] protected Connection $database,
#[Give(EmailNotificationService::class)] protected NotificationService $notifications,
#[Log('user-activity')] protected LoggerInterface $logger,
#[Storage('avatars')] protected Filesystem $avatarStorage,
) {}
public function updateProfile(array $data): bool
{
$this->logger->info('Profile update started', [
'user_id' => $this->user->id,
'app' => $this->appName,
]);
// Use specific database connection
$updated = $this->database->transaction(function () use ($data) {
return $this->user->update($data);
});
if ($updated) {
// Clear user cache
$this->cache->forget("user.{$this->user->id}");
// Send notification
$this->notifications->send($this->user, 'profile_updated');
// Handle avatar upload if present
if (isset($data['avatar'])) {
$this->avatarStorage->put(
"user-{$this->user->id}.jpg",
$data['avatar']
);
}
}
return $updated;
}
}
Creating Custom Contextual Attributes
You can create your own contextual attributes by implementing the ContextualAttribute
contract:
<?php
namespace App\Attributes;
use Attribute;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Container\ContextualAttribute;
#[Attribute(Attribute::TARGET_PARAMETER)]
class ApiClient implements ContextualAttribute
{
public function __construct(
public string $service,
public ?string $version = null
) {}
public static function resolve(self $attribute, Container $container)
{
$config = $container->make('config');
$baseUrl = $config->get("services.{$attribute->service}.url");
$apiKey = $config->get("services.{$attribute->service}.key");
$version = $attribute->version ?? $config->get("services.{$attribute->service}.version");
return new \GuzzleHttp\Client([
'base_uri' => $baseUrl,
'headers' => [
'Authorization' => "Bearer {$apiKey}",
'Accept' => "application/vnd.api+json;version={$version}",
],
]);
}
}
Usage of your custom attribute:
use App\Attributes\ApiClient;
use GuzzleHttp\Client;
class ExternalService
{
public function __construct(
#[ApiClient('stripe', 'v1')] protected Client $stripeClient,
#[ApiClient('github')] protected Client $githubClient,
) {}
}
Best Practices
1. Use Descriptive Parameter Names
// Good - clear intent
public function __construct(
#[Storage('local')] protected Filesystem $localFiles,
#[Storage('s3')] protected Filesystem $cloudStorage,
) {}
// Less clear
public function __construct(
#[Storage('local')] protected Filesystem $disk1,
#[Storage('s3')] protected Filesystem $disk2,
) {}
2. Group Related Dependencies
class OrderProcessor
{
public function __construct(
// Payment-related dependencies
#[Give(StripeGateway::class)] protected PaymentGateway $payments,
#[Config('stripe.webhook_secret')] protected string $webhookSecret,
// Storage dependencies
#[Storage('invoices')] protected Filesystem $invoiceStorage,
#[Storage('receipts')] protected Filesystem $receiptStorage,
// Logging
#[Log('orders')] protected LoggerInterface $logger,
) {}
}
3. Consider Type Safety
// Use typed properties for configuration
#[Config('app.debug')] protected bool $debug,
#[Config('mail.timeout')] protected int $mailTimeout,
#[Config('app.name')] protected string $appName,
4. Document Complex Custom Attributes
/**
* Injects a configured HTTP client for external API integration.
*
* @param string $service The service name from config/services.php
* @param string|null $version Optional API version override
*/
#[Attribute(Attribute::TARGET_PARAMETER)]
class ApiClient implements ContextualAttribute
{
// Implementation...
}
Migration Strategy
If you have existing contextual bindings, you can migrate gradually:
Step 1: Identify Contextual Bindings
Find existing when()->needs()->give()
patterns in your service providers.
Step 2: Convert Simple Cases First
Start with straightforward config and storage injections:
// Before
$this->app->when(EmailService::class)
->needs('$fromAddress')
->giveConfig('mail.from.address');
// After
class EmailService
{
public function __construct(
#[Config('mail.from.address')] protected string $fromAddress,
) {}
}
Step 3: Handle Complex Bindings
For complex logic, consider creating custom attributes or keeping the service provider binding.
Testing with Contextual Attributes
Contextual attributes work seamlessly with Laravel's testing helpers:
class UserServiceTest extends TestCase
{
public function test_user_profile_update()
{
// Mock storage
Storage::fake('avatars');
// Mock config
config(['app.name' => 'Test App']);
// Create authenticated user
$user = User::factory()->create();
$this->actingAs($user);
$service = app(UserProfileService::class);
$result = $service->updateProfile([
'name' => 'Updated Name',
'avatar' => UploadedFile::fake()->image('avatar.jpg'),
]);
$this->assertTrue($result);
Storage::disk('avatars')->assertExists("user-{$user->id}.jpg");
}
}
Performance Considerations
Contextual attributes are resolved at runtime, but Laravel caches the reflection information. The performance impact is minimal, and the benefits in code clarity typically outweigh any small overhead.
For high-performance scenarios, you can still use traditional service provider bindings, but for most applications, contextual attributes provide an excellent balance of performance and developer experience.
Conclusion
Laravel contextual attributes represent a significant improvement in how we handle dependency injection. They make code more readable, reduce boilerplate in service providers, and provide a declarative way to specify exactly what should be injected where.
Key benefits:
- Clearer Intent: Dependencies are specified where they're used
- Reduced Boilerplate: No need for manual contextual bindings
- Better Developer Experience: IDE support and autocompletion
- Type Safety: Full type hinting support
- Flexible: Custom attributes for specific needs
Start using contextual attributes in your Laravel applications today to write cleaner, more maintainable dependency injection code.