The Assinafy PHP SDK follows clean architecture principles with clear separation of concerns, dependency inversion, and adherence to SOLID principles.
assinafy-php-sdk/
├── src/
│ ├── AssinafyClient.php # Main SDK entry point
│ ├── Configuration.php # Configuration object
│ ├── Exceptions/ # Exception hierarchy
│ │ ├── AssinafyException.php
│ │ ├── ApiException.php
│ │ ├── ValidationException.php
│ │ └── NetworkException.php
│ ├── Http/ # HTTP abstraction layer
│ │ ├── HttpClientInterface.php
│ │ ├── GuzzleHttpClient.php
│ │ └── Response.php
│ ├── Resources/ # API resource classes
│ │ ├── AbstractResource.php
│ │ ├── DocumentResource.php
│ │ ├── SignerResource.php
│ │ ├── AssignmentResource.php
│ │ ├── TemplateResource.php
│ │ ├── TagResource.php
│ │ ├── FieldResource.php
│ │ ├── WebhookResource.php
│ │ ├── AuthResource.php
│ │ ├── SignerSessionResource.php
│ │ └── SignerDocumentResource.php
│ └── Support/ # Helper classes
│ └── WebhookVerifier.php
├── tests/ # PHPUnit test suites
│ ├── Unit/ # - mocked HTTP, no network
│ └── Integration/ # - opt-in live API tests
├── docs/ # Documentation
│ ├── index.php
│ ├── EXAMPLES.md
│ └── INSTALLATION.md
├── docker/ # Docker setup
│ ├── Dockerfile
│ └── nginx/
│ └── default.conf
├── composer.json
├── docker-compose.yml
├── README.md
├── MIGRATION.md
└── ARCHITECTURE.md
AssinafyClient is the main entry point for users:
$client = AssinafyClient::create($apiKey, $accountId);
$client->documents()->upload(...);Responsibilities:
- Facade for all SDK functionality
- Factory for resource classes
- Dependency injection management
Resource classes encapsulate domain logic for each API resource:
- DocumentResource: Document operations, including artifacts, public endpoints, document tags, and template-driven creation
- SignerResource: Workspace signer CRUD
- AssignmentResource: Signature requests (virtual + collect), cost estimation, resend, expiration reset, WhatsApp notification history
- TemplateResource: Template create (PDF upload), list, retrieval, update, delete, and page download
- TagResource: Workspace tag CRUD
- FieldResource: Field-definition CRUD, value validation, and the global type catalog
- WebhookResource: Webhook subscription upsert / read / inactivate, plus dispatch history, retry, and event-type discovery
- AuthResource: Login, social login, API-key lifecycle, password reset / change
- SignerSessionResource: Signer-facing session endpoints authenticated with a
signer-access-code(identity, signature image, sign/decline) - SignerDocumentResource: Signer-facing document list / sign-multiple / decline-multiple / download, authenticated with a
signer-access-code
Pattern: Each resource extends AbstractResource and follows the same structure.
Abstraction over HTTP communication:
HttpClientInterface:
- Defines contract for HTTP operations
- Allows swapping implementations
GuzzleHttpClient:
- Default implementation using Guzzle
- Handles request/response transformation
- Implements error handling
Response:
- Value object for HTTP responses
- Automatic JSON parsing
- Status checking helpers
Configuration class:
- Immutable configuration object
- Validation of required parameters
- Default value management
- Factory methods for various input formats
Hierarchical exception structure:
AssinafyException (base)
├── ApiException (HTTP errors)
├── ValidationException (validation errors)
└── NetworkException (network errors)
Helper classes that don't fit other layers:
- WebhookVerifier: HMAC signature verification
AssinafyClient provides a simplified interface:
$client->uploadAndRequestSignatures(...);Instead of:
$document = $client->documents()->upload(...);
$client->documents()->waitUntilReady($documentId);
$signer = $client->signers()->create(...);
$assignment = $client->assignments()->create(...);Multiple factory methods for flexibility:
AssinafyClient::create($apiKey, $accountId);
AssinafyClient::fromArray($config);
new AssinafyClient($config, $httpClient, $logger);HTTP client is pluggable:
interface HttpClientInterface {
public function get(string $uri, ...): Response;
public function post(string $uri, ...): Response;
}
class GuzzleHttpClient implements HttpClientInterface { }
class CustomHttpClient implements HttpClientInterface { }AbstractResource defines common behavior:
abstract class AbstractResource
{
protected function extractData(array $response): array { }
protected function accountPath(string $suffix = ''): string { }
}All dependencies injected via constructor:
public function __construct(
HttpClientInterface $httpClient,
Configuration $config,
?LoggerInterface $logger = null
)Each class has one reason to change:
Configuration: Manages configurationDocumentResource: Document operationsGuzzleHttpClient: HTTP communicationWebhookVerifier: Signature verification
Open for extension, closed for modification:
- Add new HTTP clients via interface
- Add new loggers via PSR-3
- Extend resources without changing core
Any HttpClientInterface can replace GuzzleHttpClient:
$client = new AssinafyClient($config, new CustomHttpClient());Small, focused interfaces:
HttpClientInterface: Only HTTP methodsLoggerInterface(PSR-3): Only logging methods
Depend on abstractions:
- Resources depend on
HttpClientInterface, not Guzzle - Client depends on
LoggerInterface, not Monolog
User Code
↓
AssinafyClient::uploadAndRequestSignatures()
↓
DocumentResource::upload()
↓
HttpClientInterface::uploadFile()
↓
GuzzleHttpClient::request()
↓
Guzzle HTTP Client
↓
Assinafy API
Webhook Request
↓
User Webhook Handler
↓
WebhookVerifier::verify()
↓
HMAC Comparison
↓
Event Processing
try {
$document = $client->documents()->upload(...);
} catch (ValidationException $e) {
$errors = $e->getErrors();
} catch (ApiException $e) {
$statusCode = $e->getStatusCode();
$responseData = $e->getResponseData();
} catch (NetworkException $e) {
echo "Network error: {$e->getMessage()}";
} catch (AssinafyException $e) {
$context = $e->getContext();
}- 400-499:
ApiException(client errors) - 500-599:
ApiException(server errors) - Network failures:
NetworkException - Invalid input:
ValidationException
class MyHttpClient implements HttpClientInterface
{
public function get(string $uri, array $params = [], array $headers = []): Response
{
}
}
$client = new AssinafyClient($config, new MyHttpClient());use Psr\Log\LoggerInterface;
class MyLogger implements LoggerInterface
{
}
$client->setLogger(new MyLogger());class CustomResource extends AbstractResource
{
public function myCustomMethod(): array
{
$response = $this->httpClient->get('custom-endpoint');
return $response->getData();
}
}$client->addMiddleware(new RateLimitMiddleware());
$client->addMiddleware(new RetryMiddleware());Test each class in isolation:
class DocumentResourceTest extends TestCase
{
public function testUpload()
{
$httpClient = $this->createMock(HttpClientInterface::class);
$config = new Configuration('key', 'account');
$resource = new DocumentResource($httpClient, $config);
}
}Test with real API:
$client = AssinafyClient::create($_ENV['API_KEY'], $_ENV['ACCOUNT_ID']);
$document = $client->documents()->upload('test.pdf', 'Test.pdf');$mockClient = new MockHttpClient([
new Response(200, [], '{"data": {...}}'),
]);
$client = new AssinafyClient($config, $mockClient);Resources are created on-demand:
public function documents(): DocumentResource
{
if ($this->documents === null) {
$this->documents = new DocumentResource(...);
}
return $this->documents;
}Guzzle maintains connection pool automatically.
new Configuration(
apiKey: '...',
accountId: '...',
timeout: 30,
connectTimeout: 10
);$verifier = $client->webhookVerifier();
if (!$verifier->verify($payload, $signature)) {
exit('Invalid signature');
}Default base URL uses HTTPS.
Sensitive data never logged.
$promise = $client->documents()->uploadAsync(...);$client->withRetry(maxAttempts: 3, backoff: 'exponential');$client->withCache($cachePool, ttl: 3600);$client->batch()
->uploadDocument(...)
->createSigner(...)
->execute();The SDK architecture is:
- Modular: Clear separation of concerns
- Extensible: Easy to add new features
- Testable: All dependencies injectable
- Maintainable: Follows industry standards
- Type-Safe: Full PHP 8.1+ type coverage
- Framework-Agnostic: Works anywhere