ylly / salesforcebundle
Symfony Bundle for Salesforce
Requires
- php: ^8.1
- ehyiah/mapping-bundle: ^0.4.1
- symfony/cache: ^6.2|^7.0
- symfony/config: ^6.2|^7.0
- symfony/console: ^6.2|^7.0
- symfony/form: ^6.2|^7.0
- symfony/framework-bundle: ^6.2|^7.0
- symfony/http-client: ^6.2|^7.0
- symfony/http-kernel: ^6.2|^7.0
- symfony/messenger: ^6.2|^7.0
- symfony/monolog-bundle: ^3.8
- symfony/runtime: ^6.2|^7.0
- symfony/serializer: ^6.2|^7.0
Requires (Dev)
- composer/composer: ^2.5
- ekino/phpstan-banned-code: ^1
- friendsofphp/php-cs-fixer: ^3.2
- phpstan/extension-installer: ^1.1
- phpstan/phpstan: ^1
- phpstan/phpstan-symfony: ^1
- symfony/yaml: ^6.2|^7.0
README
Bridge bundle for salesforce
Installation
Installation for usage purpose
1On the project you want to use this bundle:
- On composer.json, please add these lines :
"repositories": [ { "type": "vcs", "url": "git@gitlab-azure.castelis.com:ylly/ylly_salesforcebundle_sf.git", } ],
"scripts": { "post-package-install": [ "Ylly\\SalesforceBundle\\Composer\\ComposerScript::postPackageInstall" ], "pre-package-uninstall": [ "Ylly\\SalesforceBundle\\Composer\\ComposerScript::prePackageUninstall" ] }
- Run
composer require ylly/salesforcebundle:dev-{your-branch}
orcomposer require ylly/salesforcebundle:{tag}
Installation for development purpose on this bundle
- clone this project wherever you want.
- On the project you want to use this bundle:
- On composer.json, please add these lines :
"repositories": [ { "type": "path", "url": "../ylly_salesforcebundle_sf", "options": { "symlink": false } } ],
- Run
composer require ylly/salesforcebundle:dev-{your-branch}
Warning: If you run this command inside the php container, it'll not work because the URL is a relative path. (Unless if the bundle project is inside the container but I don't think it's a good idea)
Enable the bundle
Since there is no flex recipe yet, you need to enable the bundle manually:
On your project, open Bundle.php
and add this line :
Ylly\SalesforceBundle\YllySalesforceBundle::class => ['all' => true],
and then add, these parameters to you .env
or .env.local
(SALESFORCE_LOGIN_URL)
(SALESFORCE_CLIENT_ID)
(SALESFORCE_CLIENT_SECRET)
(SALESFORCE_USERNAME)
(SALESFORCE_PASSWORD)
(bool:SALESFORCE_CACHE_TOKEN)
Usage for Querying salesforce
Configuration
If you want to configure the bundle, create ylly_salesforce.yaml
in config/packages
ylly_salesforce: authentication: salesforce_login_url: '%env(SALESFORCE_LOGIN_URL)%' salesforce_client_id: '%env(SALESFORCE_CLIENT_ID)%' salesforce_client_secret: '%env(SALESFORCE_CLIENT_SECRET)%' salesforce_url_account: '%env(SALESFORCE_URL_ACCOUNT)%' salesforce_username: '%env(SALESFORCE_USERNAME)%' salesforce_password: '%env(SALESFORCE_PASSWORD)%' salesforce_grant_type: 'password' #type of grant api: salesforce_action_url: "%env(SALESFORCE_QUERY_URL)%" salesforce_query_url: "%env(SALESFORCE_ACTION_URL)%" salesforce_api_version: "%env(SALESFORCE_API_VERSION)%" salesforce_cache_token: '%env(bool:SALESFORCE_CACHE_TOKEN)%' #set this to true if you want to use the cached token
Overriding the Salesforce Query Service
If you want to override the SalesforceQueryService class, you'll need to use the Decorator Design pattern.
Fortunately, Since symfony 6.2, it's simple to do so.
On your server service class, use the Class attribute AsDecorator
Where YllySalesforceQueryService
is the original queryService.
Then give your service an YllySalesforceQueryService property named inner
use src\Service\SalesforceQueryService as YllySalesforceQueryService;use Symfony\Component\DependencyInjection\Attribute\AsDecorator; #[AsDecorator(decorates: YllySalesforceQueryService::class)] final class SalesforceQueryService { private YllySalesforceQueryService $inner; }
Then, instantiate it with constructor :
use Symfony\Component\DependencyInjection\Attribute\MapDecorated; //... public function __construct (#[MapDecorated] YllySalesforceQueryService $inner) { $this->inner = $inner }
Webhooks:
Webhooks salesforce are supported in this bundle.
a new ylly_salesforce.yaml
has been created in the config/routes directory.
You can update it if you want to change the default route.
The built-in controller will take care of received the salesforce webhooks, handle errors and return code.
Supported webhooks formats
2 formats are supported out of the BOX : XML and JSON according to content type in the webhook request.
You can add more format by adding new Notification Resolver implementing NotificationResolverInterface
and create a Response Generator by implementing ResponseGeneratorInterface
.
Json Format
Json NotificationResolver need this type of payload
{ "type": "delete", "notifications": [ { "object": "Account", "Id": "ADHERENT_SALESFORCE_ID" }, { "object": "Adhesion__c", "Id": "SALESFORCEID" } ] }
type : can be delete, update, create.
Json ResponseGeneator will return response like this :
{ "success": "false", "message": "An error message" }
XML format
XML format is according to salesforce specification.
Webhooks handling
By default the webhooks are synchronously handled. You can use symfony messenger to handle them asynchronously. see below
Custom header
A custom Header will be added to the Request object X-Webhook-From
with value salesforce
, so you can always know if you are working inside the webhook request (can be useful to not send back modification to salesforce after receiving webhook if you have entity listener for exemple).
Webhooks Built-in service
A Default service implementing SalesforceWebhookMappingServiceInterface
exist in the bundle.
But you may need to create a service that implements the SalesforceWebhookMappingServiceInterface
to replace the default built-in.
The default built-in service use the Mapping Bundle to auto-map properties from DTO to the entity. So in your DTO use the MappingAware attribute eg : this will map the property of the DTO inside the property with the same name inside the Entity. and the email to the mail.
namespace App\DTO\SalesforceWebhookUserDTO; #[MappingAware(target: User::class)] class SalesforceWebhookUserDTO { #[MappingAware] public string $lastname; #[MappingAware(target: 'mail')] public string $email; }
You can subscribe to events that are dispatched within the bundle when handling webhooks. see events webhook in details below
Create your own service
The default Service combined with the events should be enough to deal with the webhooks, but if you want you can create your own service that will replace the default built-in.
To create your own service :
- Just create a service that implements the interface, there is nothing more to do, no need to register or override the default built-in. The compiler pass do the work for you.
- In this service you will handle the logic (create/update/delete) to handle the records from salesforce. Check the default built-in service if you need some exemple.
Usage :
For each Salesforce Object :
- Create a DTO that extends the
AbstractSalesforceWebhookDTO
. - Create a Denormalizer that implements the
SalesforceWebhookDenormalizerInterface
to handle the Denormalization from the received salesforce XML webhook. Inside this denormalizer map the date from the received XML to the DTO.
If you created a custom mappingService, and are not using the default built-in, Then in the Service implementing the SalesforceWebhookMappingServiceInterface
just put your logic (the create/update/delete logic for each action).
otherwise there is nothing else to do.
Events
For each notification received from salesforce, one of YllySalesforceWebhookEvents
type event is dispatched with the DTO as first argument
after your SalesforceWebhookMappingServiceInterface
service managed to handle it.
So you can access your DTO inside the event subscriber.
A notification is an action (create/update/delete) for a single Entity. This bundle accept batch or single notification from salesforce.
Ignore events
If you use built-in service, you can use the $ignoreHandling
and $skipEvents
variables in AbstractSalesforceWebhookDTO::class
to ignore incoming notification and/or not sending events for this notification. Modify these variables in your custom normalizer.
Dealing with Webhooks asynchronously
For each call received by salesforce, we can receive more than one notification (max is 100 per call). If your process need heavy computation for exemple you may need to deal them asynchronously. In this case the number of notifications per batch can be customized (default is 20 per batch).
For each batch of salesforce notifications a new WebhookHandleNotificationsMessage
is created.
Exemple of configuration in messenger.yaml configuration file
framework: messenger: transports: ylly_webhook_notifications_handle: dsn: '%env(MESSENGER_TRANSPORT_DSN)%?queue_name=ylly_webhook' retry_strategy: max_retries: 5 delay: 2000 multiplier: 3 routing: 'Ylly\SalesforceBundle\Messenger\Message\WebhookHandleNotificationsMessage': 'ylly_webhook_notifications_handle'
And, as every asynchronous work start a worker to consume the queue, if you follow the exemple above with bin/console messenger:consume ylly_webhook_notifications_handle
Webhook Security
Webhook calls can be protected by Authentication at will
Please ensure these parameters are set up in your configuration
ylly_salesforce: webhook: security: activate: '%env(bool:WEBHOOK_SECURITY_ACTIVATE)%' ### if the value is true then, a token check will be made. username: '%env(WEBHOOK_SECURITY_USERNAME)%' password: '%env(WEBHOOK_SECURITY_PASSWORD)%' secret: '%env(WEBHOOK_SECURITY_SECRET)%' token_ttl: '%env(int:WEBHOOK_SECURITY_TOKEN_TTL)%' ### token's ttl in second
Please set up environment variable as you need to.
Avoiding login check conflict issue
If you use JWT plugin on your project, you could encounter trouble with token check due to this JWT plugin. To solve that, add another firewall to your security.
firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false webhook_salesforce: pattern: ^/api/external/webhook/salesforce stateless: true entry_point: ~ login: pattern: ^/api/login stateless: true
Important: Webhook firewall must be BEFORE another api firewall. And don't forget to change the pattern according to your needs
Example Request Body/response for Login
Request body
{ "username": "string", "password": "string" }
Response
{ "token": "eyxxxxxx.yyyyyyy.zzzzzz" }
Using the token add Bearer {{your token}}
on Authorization header