Contract Generation Software

Template-Based PDF Document Generation in PHP


Overview


EDocGen is a powerful, template-driven automation platform that streamlines document generation. Its versatility allows teams to instantly generate files in multiple formats—including PDF, DOCX, PPTX, and XLSX—directly from their data. With built-in support for bulk document generation from databases, and automated email delivery, the system easily scales to handle high-volume enterprise workflows.

For web development, PHP remains a dominant server-side scripting language due to its simplicity, speed, and vast ecosystem. Integrating EDocGen’s API into your existing PHP application is highly straightforward, allowing you to add robust document automation to your business processes with minimal overhead. This article walks you through the entire integration process, built on PHP 8.4/8.5 using a modern, type-safe HTTP stack.

Requirements


  1. PHP 8.4 or later (PHP 8.5 is current; 8.4 remains fully supported)
  2. Composer
  3. An EDocGen account with a template already uploaded, or the API credentials to upload one

Project Setup


          mkdir php-pdf-generation && cd php-pdf-generation
composer init --no-interaction --require=php:^8.4
composer require guzzlehttp/guzzle vlucas/phpdotenv
        

composer.json:

          {
    "name": "edocgen/php-pdf-generation-example",
    "description": "Template-based PDF document generation example using the EDocGen REST API",
    "type": "project",
    "require": {
        "php": "^8.4",
        "guzzlehttp/guzzle": "^7.9",
        "vlucas/phpdotenv": "^5.6"
    },
    "autoload": {
        "psr-4": {
            "EDocGen\\": "src/"
        }
    },
    "config": {
        "sort-packages": true
    }
}
        

Copy .env.example to .env and fill in your real credentials — this file is never committed to source control:

        EDOCGEN_BASE_URL=https://app.edocgen.com/
EDOCGEN_USERNAME=
EDOCGEN_PASSWORD=
EDOCGEN_DEFAULT_TEMPLATE_ID=
      

Project Structure

        composer.json
.env.example
src/
  EDocGenClient.php
  Enums/
    OutputFormat.php
  Exceptions/
    EDocGenException.php
    LoginException.php
    DocumentGenerationException.php
  Support/
    DatabaseSource.php
examples/
  bootstrap.php
  generate-from-json.php
  generate-from-database.php
  email-output.php
      

EDocGenClient: The single class that talks to the EDocGen API — login, template lookup, document generation from JSON or a database, polling for output, downloading, and emailing.

OutputFormat: A backed enum (Pdf, Docx, Pptx) that replaces the loose "pdf" string literals scattered through the original functions.

DatabaseSource: A small readonly value object holding the database vendor, connection URL, query, and row limit for database-driven generation, replacing the long list of loose $db* variables in the original downloadBulkUsingDB() function.

Exceptions: LoginException and DocumentGenerationException (both extending a base EDocGenException) so calling code can catch specific failure modes instead of checking if ($response === false) after every call.

Authentication


To facilitate all API interactions, the API uses a JWT-based authentication token. To acquire it, register and submit your login credentials to the /login endpoint. You’ll then receive an access token that must be included in every subsequent API call.

That part of the flow hasn’t changed. What’s different is where the credentials come from and what happens if login fails. Here’s the relevant piece of EDocGenClient:

          final class EDocGenClient
{
    private readonly Client $http;

    private ?string $token = null;

    public function __construct(
        string $baseUrl,
        private readonly string $username,
        private readonly string $password,
    ) {
        $this->http = new Client([
            'base_uri' => rtrim($baseUrl, '/') . '/',
            'timeout' => 30,
        ]);
    }

    /**
     * Authenticates against the /login endpoint and caches the access token
     * for subsequent calls. Called automatically on first use — you rarely
     * need to call this directly.
     */
    public function login(): string
    {
        $response = $this->request('POST', 'login', [
            RequestOptions::JSON => [
                'username' => $this->username,
                'password' => $this->password,
            ],
        ], LoginException::class);

        $data = $this->decodeJson((string) $response->getBody());

        if (empty($data['token'])) {
            throw new LoginException('Login response did not include an access token.');
        }

        return $this->token = $data['token'];
    }

    private function authHeaders(): array
    {
        return ['x-access-token' => $this->token ?? $this->login()];
    }
}
        

username and password are constructor-promoted readonly properties — PHP 8.0+ shorthand that declares, types, and assigns them in one line instead of the four separate $this->foo = $foo assignments the original constructor would have needed. authHeaders() logs in automatically the first time it’s called and reuses the cached token afterward, so every other method just calls $this->authHeaders() without worrying about whether login has already happened.

Every request in the client goes through one shared request() helper that converts any Guzzle failure into a typed exception instead of a raw GuzzleException (or worse, a silently swallowed false):

          private function request(string $method, string $uri, array $options, string $exceptionClass = EDocGenException::class): ResponseInterface
{
    try {
        return $this->http->request($method, $uri, $options);
    } catch (GuzzleException $e) {
        throw new $exceptionClass(
            "EDocGen API request failed ({$method} {$uri}): {$e->getMessage()}",
            previous: $e,
        );
    }
}
        

Prepare and Upload the Template

A template acts as a blueprint for the generated document, and the system supports PDF, PPTX, XLSX and DOCX as input template formats. In a template, tags like {Enter_Name} and {Enter_Email} are dynamic and get replaced with real data at generation time.

Once a template is ready, it can be uploaded from the EDocGen UI, or via the API:

POST /api/v1/document Upload template to the system
documentFile * file (formData) File to upload
x-access-token string (header) Authorization header obtained by calling login

Retrieve Template ID


After uploading a template, you’ll want its ID to use in later calls:

Method Endpoint Description
GET /api/v1/document Returns template details
          /**
 * Looks up template IDs by the file name they were uploaded with.
 *
 * @return string[] Matching template (document) IDs.
 */
public function getTemplateIdsByFileName(string $fileName): array
{
    $response = $this->request('GET', 'api/v1/document', [
        RequestOptions::QUERY => [
            'search_column' => 'filename',
            'search_value' => $fileName,
        ],
        RequestOptions::HEADERS => $this->authHeaders(),
    ]);

    $data = $this->decodeJson((string) $response->getBody());

    return array_map(
        static fn(array $document): string => $document['_id'],
        $data['documents'] ?? [],
    );
}
        

This replaces the original getTemplateIdviaFileName() function, which opened a raw cURL handle by hand and echo’d each matching ID. Here, the method returns a plain array of IDs — what you do with them (log them, render them, pick one) is up to the calling code, which is a better fit for a reusable library than baking echo statements into the client itself.

Generate Documents


The system supports generating multiple documents with a single API call — useful for producing customized documents in bulk. A template must be uploaded first, and the data source can be JSON, XLSX, or XML, sent to /api/v1/document/generate/bulk.

There are two modes:

  1. Synchronous generation — for a single record, generated immediately.
  2. Asynchronous generation — for multiple records; the API compresses the generated documents into a .zip file automatically.

Generate Documents from JSON

          /**
 * Submits a JSON data file for bulk document generation.
 *
 * @return string The generated output file name, used to poll for completion.
 */
public function generateFromJson(
    string $jsonFilePath,
    string $documentId,
    OutputFormat $format = OutputFormat::Pdf,
): string {
    if (!is_readable($jsonFilePath)) {
        throw new DocumentGenerationException("Cannot read input file: {$jsonFilePath}");
    }

    $outputFileName = $this->generateOutputFileName();

    $this->request('POST', 'api/v1/document/generate/bulk', [
        RequestOptions::HEADERS => $this->authHeaders(),
        RequestOptions::MULTIPART => [
            [
                'name' => 'inputFile',
                'contents' => fopen($jsonFilePath, 'r'),
                'filename' => basename($jsonFilePath),
            ],
            ['name' => 'documentId', 'contents' => $documentId],
            ['name' => 'format', 'contents' => $format->value],
            ['name' => 'outputFileName', 'contents' => $outputFileName],
        ],
    ], DocumentGenerationException::class);

    return $outputFileName;
}
        

This is the same multipart upload the original downloadBulkUsingJson() function performed, but built with Guzzle’s RequestOptions::MULTIPART instead of CURLOPT_POSTFIELDS and a CURLFILE instance, and it no longer mixes the HTTP call together with the polling and download logic — those are now separate methods you call explicitly (see below), so a caller that only wants to kick off generation and check back later doesn’t have to pull in the retry loop too.

Generate Documents from a Database

Generating documents directly from a database query is also unchanged at the API level. What’s changed is that the long list of loose $db* variables from the original downloadBulkUsingDB() function is now a single typed DatabaseSource value object:

          final readonly class DatabaseSource
{
    public function __construct(
        public string $vendor,
        public string $url,
        public string $password,
        public string $query,
        public int $limit,
        public string $keyToFileName = 'Prefix',
    ) {
    }
}
        
          /**
 * Submits a database query as the data source for bulk document generation.
 *
 * @return string The generated output file name, used to poll for completion.
 */
public function generateFromDatabase(
    DatabaseSource $source,
    string $documentId,
    OutputFormat $format = OutputFormat::Pdf,
): string {
    $outputFileName = $this->generateOutputFileName();

    $this->request('POST', 'api/v1/document/generate/bulk', [
        RequestOptions::HEADERS => $this->authHeaders(),
        RequestOptions::FORM_PARAMS => [
            'dbVendor' => $source->vendor,
            'dbUrl' => $source->url,
            'dbPassword' => $source->password,
            'dbQuery' => $source->query,
            'dbLimit' => (string) $source->limit,
            'documentId' => $documentId,
            'format' => $format->value,
            'outputFileName' => $outputFileName,
            'keyToFileName' => $source->keyToFileName,
        ],
    ], DocumentGenerationException::class);

    return $outputFileName;
}
        

Passing a DatabaseSource object instead of six separate scalar arguments means the constructor is self-documenting and PHP will catch a missing or mistyped field (say, passing a string where limit expects an int) before the request ever goes out.

Wait for and Download the Output


Both generation paths return an output file name that needs to be polled until EDocGen finishes processing it. The original example did this with a while loop that retried immediately with no delay between attempts. This version polls on a fixed interval and gives up with a clear exception if the output never shows up:

          /**
 * Polls the output endpoint until the generated file is ready (or retries run out).
 */
public function waitForOutput(
    string $outputFileName,
    OutputFormat $format = OutputFormat::Pdf,
    int $maxRetries = 50,
    int $delaySeconds = 2,
): string {
    for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
        $outputId = $this->findOutputId($outputFileName, $format);

        if ($outputId !== null) {
            return $outputId;
        }

        sleep($delaySeconds);
    }

    throw new DocumentGenerationException(
        "Output '{$outputFileName}' was not ready after {$maxRetries} attempts.",
    );
}

/**
 * Downloads a generated output to a local file path.
 */
public function downloadOutput(string $outputId, string $destinationPath): void
{
    $this->request('GET', "api/v1/output/download/{$outputId}", [
        RequestOptions::HEADERS => $this->authHeaders(),
        RequestOptions::SINK => $destinationPath,
    ], DocumentGenerationException::class);
}
        

RequestOptions::SINK tells Guzzle to stream the response straight to a file on disk instead of loading it into memory — worth knowing about if you’re downloading large .zip archives of bulk-generated documents.

Send Output to Email

Sending a generated document by email — useful for delivering invoices, reports, or contracts straight to a recipient — is a single call:

          /**
 * Emails a generated output to the given address.
 */
public function emailOutput(string $outputId, string $emailAddress): void
{
    $this->request('POST', 'api/v1/output/email', [
        RequestOptions::HEADERS => $this->authHeaders(),
        RequestOptions::JSON => [
            'outId' => $outputId,
            'emailId' => $emailAddress,
        ],
    ]);
}
        

Putting It Together


With the client written, a full generate-and-download run is a few lines:

          <?php

declare(strict_types=1);

require __DIR__ . '/bootstrap.php';

use EDocGen\Enums\OutputFormat;
use EDocGen\Exceptions\EDocGenException;

$documentId = $_ENV['EDOCGEN_DEFAULT_TEMPLATE_ID']
    ?? throw new RuntimeException('EDOCGEN_DEFAULT_TEMPLATE_ID is not set');
$jsonFilePath = __DIR__ . '/JSON_Data.json';

try {
    $client = edocgenClient();

    $outputFileName = $client->generateFromJson($jsonFilePath, $documentId, OutputFormat::Pdf);
    echo "Submitted for generation as '{$outputFileName}', waiting for output...\n";

    $outputId = $client->waitForOutput($outputFileName, OutputFormat::Pdf);

    $destination = __DIR__ . "/output/{$outputFileName}.zip";
    $client->downloadOutput($outputId, $destination);

    echo "Downloaded to {$destination}\n";
} catch (EDocGenException $e) {
    fwrite(STDERR, "Document generation failed: {$e->getMessage()}\n");
    exit(1);
}
        

bootstrap.php loads .env with vlucas/phpdotenv and builds the client once, so every example script shares the same setup:

          <?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Dotenv\Dotenv;
use EDocGen\EDocGenClient;

Dotenv::createImmutable(__DIR__ . '/..')->safeLoad();

function edocgenClient(): EDocGenClient
{
    return new EDocGenClient(
        baseUrl: $_ENV['EDOCGEN_BASE_URL'] ?? 'https://app.edocgen.com/',
        username: $_ENV['EDOCGEN_USERNAME'] ?? throw new RuntimeException('EDOCGEN_USERNAME is not set'),
        password: $_ENV['EDOCGEN_PASSWORD'] ?? throw new RuntimeException('EDOCGEN_PASSWORD is not set'),
    );
}
        

Note the ?? throw expressions — a PHP 8.0 feature that lets you fail fast on missing configuration in a single line, instead of an if (!isset(...)) block ahead of every use.

Run it with:

          php examples/generate-from-json.php
        

Conclusion


EDocGen remains an excellent solution for businesses looking to automate document generation — eliminating manual input, reducing human error, and freeing teams to focus on higher-value work. Generating documents in multiple formats, and emailing them directly, can be seamlessly handled with just a few API calls.

Popular Posts

.