Adobe Sign for Symfonyconceptlz/adobe-sign-symfony GitHub

Start here

Installation

From composer require to a verified connection in about ten minutes. This page installs the bundle, gives the SDK an HTTP client, puts your key in the environment, imports the routes, and checks the credentials against Acrobat Sign. Optional pieces (Doctrine, Messenger, Security) are at the end.

Requirements

WhatVersionWhy
PHP8.2 or newerThe minimum the core SDK and the bundle are tested on.
ext-sodiumanyEncrypts stored OAuth tokens. Bundled with PHP; some minimal Docker images need docker-php-ext-install sodium.
Symfony7.4 or 8.xThe bundle is an AbstractBundle and requires ^7.4 || ^8.0 of FrameworkBundle, HttpKernel, Routing, Cache, Console and friends.
A PSR-18 clientanyThe core SDK sends requests through PSR-18. See the HTTP client.
Acrobat Sign accountdeveloper or paidA free developer account works for building and testing; agreements from it get a [DEMO USE ONLY] prefix.

The core package, conceptlz/adobe-sign, comes in as a dependency; you don't require it separately. The bundle and the core share one version number and are released together.

Install in six steps

  1. Require the bundle
    composer require conceptlz/adobe-sign-symfony

    The package type is symfony-bundle, so Symfony Flex adds it to config/bundles.php for you. There is no recipe, so Flex doesn't create config files; the next steps do that by hand.

  2. Register the bundle if Flex didn't
    config/bundles.php
    return [
        Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
        // ...
        Conceptlz\AdobeSign\Symfony\AdobeSignBundle::class => ['all' => true],
    ];

    Register it in every environment. Disabling it in test would remove the services your tests need; swap the HTTP client instead (see Testing).

  3. Make sure a PSR-18 client exists
    composer require symfony/http-client nyholm/psr7

    Skip this if your app already has symfony/http-client and a PSR-17 implementation. Details in the HTTP client.

  4. Put the key in the environment

    Declare the variable (empty) in the committed .env and give it a value in .env.local, which is git-ignored. See credentials.

  5. Create the configuration and import the routes

    Two small YAML files: config/packages/adobe_sign.yaml and config/routes/adobe_sign.yaml. See the config file and importing the routes.

  6. Verify
    php bin/console adobe-sign:verify

    It calls Acrobat Sign with your credentials and prints who they belong to. See checking the connection.

The HTTP client (PSR-18)

The core SDK never creates its own HTTP stack. It accepts any PSR-18 client, which keeps it framework-free and lets your tests swap in a fake. The bundle decides which client to hand it in this order:

  1. The service named in adobe_sign.http_client, if you set one.
  2. Otherwise the framework's PSR-18 client, service psr18.http_client. FrameworkBundle registers it when symfony/http-client is installed together with a PSR-17 implementation such as nyholm/psr7. It inherits your framework.http_client settings (timeouts, proxies, retry options).
  3. Otherwise php-http/discovery looks for any installed PSR-18 client, for example Guzzle 7.

The PSR-17 request and stream factories are always found by discovery, which is why nyholm/psr7 (or guzzlehttp/psr7) must be installed. If nothing can be found, the first API call throws Http\Discovery\Exception\NotFoundException; see Troubleshooting.

config/packages/framework.yaml: timeouts for every PSR-18 call, including the SDK's
framework:
    http_client:
        default_options:
            timeout: 10          # seconds to wait for the first byte
            max_duration: 120    # large signed PDFs can take a while

A typo in http_client fails quietly

The bundle references the http_client service with "null on invalid", so an id that doesn't exist falls back to discovery instead of failing the container build. If requests don't go where you expect, check the id with php bin/console debug:container <id>.

Credentials in the environment

Acrobat Sign accepts two kinds of credentials, and the bundle supports both. Pick one now; you can add the other later.

Integration Key or OAuth?

Integration KeyOAuth
Who sendsOne Acrobat Sign user (usually a service account) for the whole appEach of your users, from their own Acrobat Sign account
SetupOne environment variableAn API application at Adobe, a client id and secret, routes, token storage, a firewall
ExpiryNever, until revoked in Acrobat SignAccess tokens expire after about an hour and refresh automatically
Where agreements liveIn that one accountIn each user's account, under their name
Good forInternal tools, one company sending to its clientsSaaS products whose customers already use Acrobat Sign

Create an Integration Key in Acrobat Sign under Account → Acrobat Sign API → API Information → Integration Key. Give it the scopes your app needs (at least agreement_read, agreement_write, agreement_send, user_login, and webhook_read/webhook_write if you register webhooks).

.env (committed: declares the variables, no secrets)
###> conceptlz/adobe-sign-symfony ###
ADOBE_SIGN_INTEGRATION_KEY=
ADOBE_SIGN_CLIENT_ID=
ADOBE_SIGN_CLIENT_SECRET=
###< conceptlz/adobe-sign-symfony ###
.env.local (git-ignored: the real values on your machine)
ADOBE_SIGN_INTEGRATION_KEY=3AAABLblqZhB...your-key...
# Only for OAuth or webhooks:
ADOBE_SIGN_CLIENT_ID=CBJCHBCAABAA...
ADOBE_SIGN_CLIENT_SECRET=...

Declaring the variables empty in .env matters: a %env(ADOBE_SIGN_INTEGRATION_KEY)% reference to an undefined variable throws EnvNotFoundException when the container builds the manager, while an empty one gives you the bundle's clearer MisconfiguredException that names the setting. On servers, prefer real environment variables or Symfony secrets:

php bin/console secrets:set ADOBE_SIGN_INTEGRATION_KEY --env=prod

Treat the key like a password

An Integration Key can send, read and cancel agreements as its owner. Never commit it, never log it, and never put it in front-end code. The SDK redacts the Authorization header from every exception it creates, so error trackers don't receive it.

The config file

The bundle's configuration root is adobe_sign. For an Integration Key, one line is enough; every other key has a default.

config/packages/adobe_sign.yaml (Integration Key)
adobe_sign:
    integration_key: '%env(ADOBE_SIGN_INTEGRATION_KEY)%'
config/packages/adobe_sign.yaml (OAuth, users connect their own accounts)
adobe_sign:
    auth: oauth
    oauth:
        client_id: '%env(ADOBE_SIGN_CLIENT_ID)%'
        client_secret: '%env(ADOBE_SIGN_CLIENT_SECRET)%'
        token_store: doctrine          # or keep "cache" with a persistent pool
        redirect_after: /settings/integrations

See Configuration for every key, its default and what it changes. Two defaults are worth knowing now: cache_pool and oauth.token_cache_pool are both cache.app. That is fine on one machine, but on several servers or for OAuth tokens you want a shared, persistent pool; see cache and HTTP.

Importing the routes

The bundle ships its routes as PHP files but doesn't load them by itself, so you decide which ones exist and under which prefix.

config/routes/adobe_sign.yaml
# Everything: OAuth connect routes and the webhook
adobe_sign:
    resource: '@AdobeSignBundle/config/routes.php'

# Or pick one:
# adobe_sign_oauth:
#     resource: '@AdobeSignBundle/config/routes/oauth.php'
# adobe_sign_webhook:
#     resource: '@AdobeSignBundle/config/routes/webhook.php'
Route nameMethodPathFile
adobe_sign_oauth_redirectGET/adobe-sign/oauth/redirectroutes/oauth.php
adobe_sign_oauth_callbackGET/adobe-sign/oauth/callbackroutes/oauth.php
adobe_sign_webhookGET, POST/adobe-sign/webhookroutes/webhook.php

Add prefix: /integrations to an import to move its paths; the route names stay the same, and the default OAuth redirect URI follows the callback route automatically. Check the result with:

php bin/console debug:router | grep adobe_sign

Using only an Integration Key and no webhooks? Then you don't need to import anything: the services work without routes.

Checking the connection

$ php bin/console adobe-sign:verify

 [OK] Connected to Acrobat Sign.

 ---------------- -------------------------
  User             sender@example.com
  Account admin    yes
  Account          CBJCHBCAABAA...
 ---------------- -------------------------

The command calls GET /users/me with the default client. The first call of a fresh app also discovers the account's data centre through GET /baseUris and caches it in cache_pool. On any SDK error it prints the SDK's message (for example INVALID_ACCESS_TOKEN) and exits with status 1, so you can run it in a deploy pipeline as a smoke test. It never prints the key.

For OAuth, check one user's connection by their token key, which is their user identifier by default:

php bin/console adobe-sign:verify --user=jane@example.com

Optional pieces

The bundle has hard dependencies only on Symfony components every full-stack app already has. Three features need more, and the bundle registers their services only when you configure them.

Doctrine DBAL, for a token table

composer require doctrine/dbal          # usually via symfony/orm-pack

Needed only for oauth.token_store: doctrine. With Doctrine ORM installed as well, the table is added to the ORM schema so doctrine:migrations:diff generates its migration; without the ORM, run php bin/console adobe-sign:create-token-table. See Token storage & Doctrine.

Symfony Messenger, for async webhooks

composer require symfony/messenger

Needed only for webhooks.messenger. Enabling it without Messenger installed fails the container build with a missing messenger.default_bus service, which is deliberate: a silently ignored setting would lose notifications. See Async with Messenger.

Symfony Security, for OAuth

composer require symfony/security-bundle

The OAuth callback stores tokens under the signed-in user, which it reads from security.token_storage. Without SecurityBundle there is never a signed-in user, so the callback answers 401. Your app also needs sessions enabled (framework.session), because the single-use OAuth state lives there. See protecting the routes.

Upgrading

Before 1.0, a minor version (0.5 → 0.6) may contain breaking changes; patch versions don't. Update the bundle and the core together, read the changelog, clear the cache and restart workers:

composer update conceptlz/adobe-sign-symfony conceptlz/adobe-sign --with-dependencies
php bin/console cache:clear
php bin/console messenger:stop-workers

Common questions

Why didn't Flex create config/packages/adobe_sign.yaml?

There is no Flex recipe for the bundle yet. Flex still registers it in config/bundles.php because the package type is symfony-bundle, but the config file and the route import are yours to create. Both are a few lines; copy them from this page.

Can I use Guzzle instead of symfony/http-client?

Yes. Guzzle 7 is a PSR-18 client. Either let discovery find it (if psr18.http_client isn't registered), or register it as a service and set adobe_sign.http_client to that service id.

Do I have to import the routes?

Only for the features that need them: the OAuth routes for users connecting their accounts, the webhook route for Adobe's callbacks. Sending agreements with an Integration Key works without any route.

Can I run adobe-sign:verify in CI?

Yes, as a post-deploy smoke test with real credentials: it exits 1 on failure. Don't run it in the test suite, where the HTTP client should be a mock; it would then only test your mock.

Composer says ext-sodium is missing. Do I need it for an Integration Key?

The requirement is unconditional, because the token encryptor is part of the bundle. Sodium ships with PHP; enable it in php.ini or install it in your Docker image (docker-php-ext-install sodium).