Version 10.70.0 adds callable arrays in queues
This version introduces Storable Array Callables as a high-performance, lightweight alternative to SerializableClosures. By allowing developers to pass structured arrays instead of closures to the Queue and Event systems, we significantly reduce serialization overhead and SQS/Redis payload sizes while maintaining full Dependency Injection support.
Queued Object (including Closures) are still supported in parallel but not recommended.app/Jobs has now the alternative app/CallablesAsArray folder from which all public methods are cached by autowiring:cache.
How to Use
You can now pass an array in the format [Class::class, 'method', ['named' => 'param']] to almost any framework entry point that previously required a Job object or a Closure.
- Basic Dispatching
No need to create a Job class for simple logic.
dispatch([EmailService::class, 'sendWelcomeEmail', ['userId' => $user->id]]); // it is not auto-serializing models// If a developer chains an Object (BLOCKED):
dispatch(['Class', 'method'])->chain([
new SendEmailJob()
]);
// If a developer chains an Array (ALLOWED & WORKING):
dispatch(['Class', 'method'])->chain([
['EmailService', 'sendWelcomeEmail']
]);- Event Listeners
Queueable listeners without the ShouldQueue boilerplate.
Event::listen(OrderCreated::class, queueableArray([InventoryManager::class, 'reserveStock']));// ALLOWED: Primitive string event with primitive payload
Event::dispatch(OrderShipped::class, ['order_id' => $order->id]);
// BLOCKED: Object event (Caught by ensureNoObjects)
Event::dispatch(new OrderShipped($order));- Batches and Chains
Bus::batch([
[ImageProcessor::class, 'optimize', ['path' => 'photo1.jpg']],
[ImageProcessor::class, 'optimize', ['path' => 'photo2.jpg']],
])->dispatch();Under the Hood: The "Manual DI" Engine
The core of this feature is a custom execution engine inside CallQueuedCallable. It bridges the gap between native PHP performance and Laravel's magic.
Positional vs. Named Resolution
The engine intelligently resolves parameters from four distinct sources in order:
User Overrides: Explicit parameters passed in the 3rd element of the array.
Contextual Logic: Automatic injection of Batch instances or Throwable exceptions during failure.
Positional Framework Data: Automatic mapping of Event payloads (e.g., [123, 'status']) by tracking parameter indices.
Container Autowiring: Falling back to the Container for type-hinted services (Loggers, Repositories, etc.).
Performance Optimizations
The Fast Path: If a class has zero dependencies or has been precompiled, we skip the Container's heavy Reflection and use a static constructor call.
Precompiled Cache: Integrates with BoundMethod to cache parameter maps, ensuring that even complex DI resolution happens at lightning speed after the first hit.
Corner Cases & Safety
We’ve implemented several safeguards to ensure this is a drop-in improvement without breaking existing patterns:
Contextual Binding Awareness: The Container optimization automatically detects if a class has a when()->needs()->give() rule. If a contextual binding exists, the cache "steps aside" and lets the core framework handle the resolution to ensure rules are never ignored.
Strict Validation: The Queue::storableCallable method enforces that arrays are properly formatted. It prevents common mistakes like passing objects (which aren't storable) or using positional lists for user parameters.
Failure Symmetry: Failure handlers (catch) use the exact same sophisticated DI logic as the main handler, allowing for failure signatures like failed(int $userId, \Throwable $e).
Safe Short-circuiting: If a payload is positional (like an Event), we intentionally bypass $container->call() to avoid its inherent limitations with mixed primitive/object lists, routing instead to our optimized manual mapper.
Impact
Reduced CPU cycles: Bypasses laravel/serializable-closure for standard tasks.
Storage Efficiency: Queue payloads are now simple strings and arrays, making them easier to read and search in failed_jobs or Redis.
Developer Experience: Provides a "middle ground" between a full Job class and a Closure, perfect for service-layer architecture.
One final note for the maintainers: The Container::build optimization includes a logic check for contextual bindings using findInContextualBindings, ensuring this performance boost is 100% safe for complex enterprise applications.
Enforced Primitive Payloads
To guarantee a 100% object-free serialization path, the Queue::storableCallable validator now recursively scans the parameter array. If any object is detected, an InvalidArgumentException is thrown. This prevents accidental bloat and eliminates the risk of PHP Object Injection at the architectural level.
Important Limitations
No Objects Allowed: To guarantee 100% protection against PHP Object Injection, queueableArray strictly forbids objects in payloads.
Event Payloads: If you use queueableArray for Event Listeners, the event being fired must not pass Eloquent Models or objects in its payload (e.g., Event::dispatch(new OrderShipped($order))). Doing so will cause the queue's ensureNoObjects() validator to throw an exception. You must refactor your events to pass primitive data only (e.g., Event::dispatch(OrderShipped::class, ['order_id' => $order->id])).
State Management: Because Eloquent models are not serialized and re-fetched natively, developers are responsible for passing primitive IDs and querying fresh database state inside their array callables. Alternatively, if capturing the exact current state of the model is important, you can pass its array shape (e.g., $model->toArray()).
Examples
Here are the examples showing the class definitions (stored in app/CallablesAsArray) and the usage (how to dispatch them).
- The Service Job
This replaces a standard "Job" class. It uses autowiring for the framework service and accepts the Model via the array parameters.
Definition: app/CallablesAsArray/UserProcessor.php
namespace App\CallablesAsArray;
use App\Models\User;
use Illuminate\Mail\MailManager;
class UserProcessor
{
/**
* The method called by the queue.
* MailManager is autowired, $user is passed from the dispatch array.
*/
public function sendWelcomeEmail(MailManager $mailer, int $userId)
{
$user = User::query()->findOrFail($userId);
$mailer->to($user->email)->send(new \App\Mail\Welcome($user));
}
}Usage:
dispatch([App\CallablesAsArray\UserProcessor::class, 'sendWelcomeEmail', ['userId' => $user->id]]);- The Event Listener
This demonstrates how positional event payloads are mapped to parameters while still allowing for injected services.
Definition: app/CallablesAsArray/InventoryNotifier.php
namespace App\CallablesAsArray;
use App\Events\OrderPlaced;
use Illuminate\Support\Facades\Log;
class InventoryNotifier
{
/**
* $event is positionally injected from the Event payload.
* Log is autowired by the container.
*/
public function notifyWarehouse(OrderPlaced $event)
{
Log::info("Adjusting stock for order: " . $event->orderId);
}
/**
* Failure handler using our new symmetrical DI engine.
*/
public function failed(OrderPlaced $event, \Throwable $e)
{
Log::error("Failed to notify warehouse for order {$event->orderId}: {$e->getMessage()}");
}
}Usage:
use App\Events\OrderPlaced;
use App\CallablesAsArray\InventoryNotifier;
Event::listen(
OrderPlaced::class,
queueableArray([InventoryNotifier::class, 'notifyWarehouse'])
);- The Batch Callback
This shows how the engine handles framework-specific objects like Batch combined with custom logic.
Definition: app/CallablesAsArray/BatchFinalizer.php
namespace App\CallablesAsArray;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Notification;
class BatchFinalizer
{
/**
* $batch is automatically injected by the Batch system.
* 'adminEmail' is passed from the user array.
*/
public function handleSuccess(Batch $batch, string $adminEmail)
{
Notification::route('mail', $adminEmail)
->notify(new \App\Notifications\ImportCompleted($batch->id));
}
}Usage:
use App\CallablesAsArray\BatchFinalizer;
Bus::batch([
// ... jobs ...
])->then([
BatchFinalizer::class,
'handleSuccess',
['adminEmail' => 'admin@example.com']
])->dispatch();- Complex Failure Callbacks (Chains)
You can even use these for complex failure logic in chains, catching specific exceptions.
Definition: app/CallablesAsArray/FailureLogger.php
namespace App\CallablesAsArray;
use Illuminate\Support\Facades\DB;
class FailureLogger
{
public function logQueueError(\Throwable $e, array $context = [])
{
DB::table('queue_log')->insert([
'message' => $e->getMessage(),
'context' => json_encode($context),
'failed_at' => now()
]);
}
}Usage:
dispatch([ProcessPodcast::class, 'handle'])
->catch([
App\CallablesAsArray\FailureLogger::class,
'logQueueError',
['context' => ['source' => 'podcast_import']]
]);Summary of Benefits for the PR
Zero Boilerplate: No need to implement ShouldQueue or use the Dispatchable trait on every single class.
Encapsulation: Logic stays in the CallablesAsArray folder, keeping your app/Jobs and app/Listeners folders clean for only the most complex cases.
Type Safety: You get full IDE autocomplete and type-hinting in the method signatures, unlike raw Closures.
Why?
Because unserialyzing objects is not 100% safe, including Closures.
We recommend you to stop using serializable-closure and once you did that, you can even add to your composer.json/autoloader/exclude-from-classmap "vendor/macropay-solutions/maravel-framework/serializable/closure/"
