lafourchette/adobe-campaign-client-bundle
Symfony bundle that integrates the Adobe Campaign client into your application, providing configuration and services for connecting to and interacting with Adobe Campaign APIs.
Install and register
composer require lafourchette/adobe-campaign-client-bundle
config/bundles.php:
Lafourchette\AdobeCampaignClientBundle\AdobeCampaignClientBundle::class => ['all' => true],
app/AppKernel.php inside registerBundles().Configure credentials
Put connection details in config/packages/adobe_campaign_client.yaml (or config.yml) and back the secrets with env vars:
adobe_campaign_client:
# verify exact key names in DependencyInjection/Configuration.php
endpoint: '%env(ADOBE_CAMPAIGN_ENDPOINT)%'
login: '%env(ADOBE_CAMPAIGN_LOGIN)%'
password: '%env(ADOBE_CAMPAIGN_PASSWORD)%'
# security_token / tenant / timeout may also be supported
Then add the matching entries to .env and, ideally, a real secrets store for production.
Where to look first
README.md — usually the only place the client service ID and supported operations are documented.DependencyInjection/Configuration.php — the authoritative list of config keys and defaults.Resources/config/services.xml (or services.yaml) — the service IDs you will inject. These are the contract you build against.lafourchette/adobe-campaign-client or equivalent in composer.json) — the bundle is mostly a thin Symfony wrapper; the real API surface lives there.First use case
The fastest end-to-end validation is: boot the kernel, fetch the client service from the container in a console command, authenticate, and retrieve a single record from a Campaign schema (e.g. a profile or recipient by primary key). If that round-trips, you have config, networking, and credentials correct, and you can start building real sync jobs on top.
Always inject, never instantiate
Resolve the client through the container (constructor injection in your own services, or $container->get() in a command). Never new the underlying client — you lose config, logging, and any decorators the bundle wires in.
Wrap the client in your own adapter
Create a thin App\AdobeCampaign\CampaignGateway service that owns the client and exposes domain methods (findProfileByEmail(), subscribeToNewsletter(), triggerDelivery()). This gives you:
Treat the session as a resource, not a per-call login
Adobe Campaign sessions are authenticated and time-limited. Log in once per process, reuse the token for the duration of the job, and log out in a finally block. In long-running CLI workers, check token validity before each batch and re-authenticate on expiry rather than letting the first call fail.
Push bulk work off the request cycle
Campaign calls are network-bound and slow. Do not call them inline in a controller on a hot path. Instead:
Model the direction of data flow explicitly
Most integrations are one of three shapes, and each deserves its own service:
Configuration per environment
Point dev/staging at a Campaign sandbox instance and never let a local run touch production. Because the bundle reads standard Symfony config, this is just config/packages/dev/adobe_campaign_client.yaml overriding the endpoint. Guard against accidental prod writes with an explicit adobe_campaign_client.dry_run flag checked in your gateway.
Observability
Tag the client service (or your adapter) with a Monolog channel so SOAP/REST traffic, timings, and faults land in a dedicated log file. This is the single highest-value thing you can do — Campaign errors are opaque otherwise.
SOAP faults are not HTTP errors
Campaign Classic exposes XML/SOAP web services; failures frequently arrive as a SoapFault with a numeric code and a message string, not an HTTP status. Catch \SoapFault explicitly in your gateway, map the known codes to domain exceptions, and log the raw fault string — the message often contains the offending field or query.
Schema field names, not labels
Campaign distinguishes the internal name (@email, @firstName) from the UI label. Your code must use internal names, and they are not always what you'd guess from the interface. Dump the schema metadata once during development and keep it next to your mapping code.
Query conditions need escaping
If your queries are built by concatenating conditions (the common queryDef pattern in Campaign Classic), values containing quotes, &, <, or > will break the XML or, worse, alter the query. Escape every interpolated value, and prefer parameterized helpers in the underlying client where available. Treat any user-supplied string that reaches a query as hostile.
Pagination is mandatory, not optional
Campaign will happily truncate at the default line count. Always set an explicit page size and a start line, and loop until fewer rows than the page size come back. A sync that silently stops at 200 records is the classic first bug in these integrations.
Timeouts and retries
Set a connect/response timeout in config; the default can hang a worker indefinitely. Retry only on transient transport faults with exponential backoff — retrying a validation fault just multiplies the noise.
Timezones and date formats
Campaign stores and compares dates in its own timezone settings, and date literals in queries must match the expected format. Normalize to UTC at the boundary and format explicitly; never rely on date('Y-m-d H:i:s') with PHP's ambient timezone.
Session leakage across workers
If you build a single long-lived worker, be aware that a stale session token will produce confusing, intermittent auth errors that look like credential problems. Add an explicit re-auth on the first auth-shaped failure before giving up.
Extension points
decorates: in services.yaml) to add logging, metrics, or caching without forking.Maintenance risk
The package has essentially no dependents and no recent signal of activity. Pin the version in composer.json, vendor the underlying client knowledge into your own adapter, and write integration tests against a sandbox Campaign instance so that a future replacement of this bundle is a contained change rather than an archaeology project. Verify the bundle's Symfony version constraints before upgrading your framework — old bundles typically break on Symfony 5/6 due to removed container APIs.
How can I help you explore Laravel packages today?