comsave/safe-salesforce-saver-bundle
Install the Package
composer require comsave/safe-salesforce-saver-bundle
For Laravel (Symfony-based), register the bundle in config/app.php under the providers array:
Comsave\SafeSalesforceSaverBundle\ComsaveSafeSalesforceSaverBundle::class,
Configure RabbitMQ
Add the following to config/packages/old_sound_rabbit_mq.yaml (or equivalent Laravel queue config):
old_sound_rabbit_mq:
producers:
sss_async_processor:
class: Comsave\SafeSalesforceSaverBundle\Producer\AsyncSfSaverProducer
connection: default
exchange_options: { name: 'sss_async_queue', type: direct }
consumers:
sss_async_processor:
connection: default
exchange_options: { name: 'sss_async_queue', type: direct }
queue_options: { name: 'sss_async_queue' }
qos_options: { prefetch_count: 1, global: false }
callback: Comsave\SafeSalesforceSaverBundle\Consumers\AsyncSfSaveConsumer
rpc_clients:
parallel:
connection: default
expect_serialized_response: false
rpc_servers:
safe_salesforce_saver_server:
connection: default
callback: Comsave\SafeSalesforceSaverBundle\Consumers\SafeSalesforceSaverServer
qos_options: { prefetch_count: 1, global: false }
queue_options: { name: sss_rpc_queue, durable: true, auto_delete: false }
Annotate Models
Ensure your Salesforce models are annotated using salesforce-mapper-bundle. Example:
/**
* @SalesforceObject("Account")
*/
class Account {}
First Use Case
Inject SafeSalesforceSaver into a service and save an object synchronously:
use Comsave\SafeSalesforceSaverBundle\Services\SafeSalesforceSaver;
class AccountService {
public function __construct(private SafeSalesforceSaver $saver) {}
public function createAccount(array $data): string {
$account = new Account($data);
$this->saver->save($account); // Blocks until Salesforce responds
return $account->getId();
}
}
Synchronous Saves (Blocking) Use when you need immediate feedback (e.g., redirecting to a detail page after creation):
$this->saver->save($object); // Waits for Salesforce response
$id = $object->getId(); // Safe to use after save()
Asynchronous Saves (Non-Blocking) Use for background processing (e.g., bulk imports, notifications):
$this->saver->aSyncSave($object); // Fire-and-forget
Bulk Operations Batch saves to reduce queue overhead:
$this->saver->save([$obj1, $obj2, $obj3]); // Synchronous bulk
$this->saver->aSyncSave([$obj1, $obj2]); // Asynchronous bulk
Queue Workers Start consumers with:
php bin/console rabbitmq:consumer sss_async_processor
For Laravel, use the queue:work command:
php artisan queue:work --queue=salesforce
Error Handling
The bundle auto-requeues failed jobs with exponential backoff. Customize retry logic via config/packages/comsave_safe_salesforce_saver.yaml:
comsave_safe_salesforce_saver:
max_retries: 5
retry_delay: 300 # seconds
Testing
Mock the SafeSalesforceSaver in unit tests:
$mockSaver = $this->createMock(SafeSalesforceSaver::class);
$mockSaver->expects($this->once())->method('save')->with($object);
Monitoring Track queue health with:
php bin/console rabbitmq:status
Or log consumer metrics:
$this->saver->setLogger($logger); // Inject PSR-3 logger
Queue Naming
Issue: Changing queue/exchange names (e.g., sss_async_queue) breaks consumers.
Fix: Use the exact names from the config. For customization, extend the bundle:
// config/packages/comsave_safe_salesforce_saver.yaml
comsave_safe_salesforce_saver:
queue_name: custom_salesforce_queue
Serialization Errors
Issue: Complex objects (e.g., with closures) may fail to serialize for async saves.
Fix: Implement __serialize()/__unserialize() or use aSyncSave() only with simple DTOs.
Salesforce API Limits Issue: Bulk saves may hit Salesforce’s 24-hour batch limit. Fix: Split large arrays into chunks:
$chunkSize = 100;
array_chunk($objects, $chunkSize, function($chunk) {
$this->saver->aSyncSave($chunk);
});
Consumer Lag
Issue: Slow consumers cause queue backlog.
Fix: Monitor rabbitmq:status and scale workers:
php artisan queue:work --queue=salesforce --daemon
Failed Jobs
Check the failed_jobs table (Laravel) or RabbitMQ dead-letter queue:
php bin/console rabbitmq:consumer dead_letter_queue
Logging
Enable debug logs in config/packages/monolog.yaml:
handlers:
salesforce:
type: stream
path: "%kernel.logs_dir%/salesforce.log"
level: debug
channels: ["salesforce"]
Common Errors
| Error | Cause | Solution |
|---|---|---|
Connection refused |
RabbitMQ not running | Start services: docker-compose up |
Invalid object type |
Missing @SalesforceObject |
Annotate models correctly |
Max retries exceeded |
Persistent Salesforce error | Check failed_jobs table |
Serialization of 'Closure' is not allowed |
Async save with closures | Use synchronous saves or DTOs |
Custom Retry Logic Override the consumer to add business logic:
// src/Consumer/CustomSfSaveConsumer.php
class CustomSfSaveConsumer extends AsyncSfSaveConsumer {
protected function handleException(Throwable $e, $object) {
if ($e instanceof SalesforceException && $e->isRateLimited()) {
$this->retryAfter(60); // Custom delay
}
parent::handleException($e, $object);
}
}
Update the config to use your consumer:
consumers:
sss_async_processor:
callback: App\Consumer\CustomSfSaveConsumer
Pre/Post-Save Hooks Extend the saver to add validation or transformations:
class ExtendedSafeSalesforceSaver extends SafeSalesforceSaver {
protected function beforeSave($object) {
if (!$object->isValid()) {
throw new \RuntimeException("Invalid object");
}
}
}
Bind it in config/services.yaml:
services:
Comsave\SafeSalesforceSaverBundle\Services\SafeSalesforceSaver:
class: App\Services\ExtendedSafeSalesforceSaver
Dead-Letter Queue
Configure a DLQ in old_sound_rabbit_mq.yaml:
consumers:
sss_async_processor:
queue_options:
name: sss_async_queue
dead_letter_exchange: salesforce_dlq
100–500 objects per batch to avoid Salesforce timeouts.1–2 workers per queue to prevent API throttling.How can I help you explore Laravel packages today?