Making Fillable Documents Using Google Docs API

Let’s pick our last example with the contractor. I own an application, through which I am selling my services, in the form of a contract. 

But there is a problem. How am I supposed to generate the contracts? There are many solutions when it comes to pdf generation but I have no idea which one fits my needs.

1.

The requirements

For me as a contractor, generating a pdf contract from scratch doesn’t sound like a good idea. I mean it is possible but when I think about all the legal clauses, mh nope, not a good idea. I need something like a template where I have full control so that I can write and style it as I want. This template should be used by default for all my contracts. Since my customers are already registered, I don’t want them to fill the whole Contract by hand, instead, I want to automate the whole process and prefill the document using the existing user data.

Now you must be thinking, jeez, I wish my clients were as clear as that! 😅

In our today’s article, we are going to investigate the Google Docs api as a solution for our contractor’s problems

2.

Taking a peek at the documentation

From time to time we all have created a Google document, a sheet or a slide, whether it is for work, school, university or something else. The Google Docs api has been around for a while and we will dig into it, step by step so that you can easily follow what’s going on.

https://developers.google.com/docs/api/how-tos/overview
introduction

Right off the bat, we can read from the introduction that they have everything our contractor needs.

3.

API Configuration

Our very first concern is to configure our environment and whatever it is needed in order to work with the api. In the menu on the left click Get started > Overview

https://developers.google.com/workspace/guides/get-started
api configuration

and you should land here

develop on google workspace
4.

Create The Project

The first step is to create a Google Cloud project. Click Google Workspace API Overview https://console.cloud.google.com/workspace-api  and click Create project

create or select project

The button is also visible under Select a project > New project

Type in your project name, click create. Select the newly created project and voila, your Google Cloud project has been created.

5.

Enable APIs and Services

For the second step, in your project dashboard click Go to APIs overview

apis

and enable APIs and services.

apis and services

After that, search for google docs api, google drive api and enable them.

6.

Credentials

In the menu on the left click Credentials

credentials

Before we continue on creating the credentials let’s have a look at the dashboard

api keys

There are 3 types of credentials which your application can use in order to gain access to the Google api services.

Api Keys are mainly used for public data and resources like Google Maps, for an example.

Oauth 2.0 Client IDs are required when your application needs to have access to resources owned by the user.

Service Accounts are accounts that do not represent a human user. They can be used to access apis and resources without any user involved.

Since our contractor’s application only needs access to the Google Docs,

Google Drive api, we are going to create the credentials using a service account.

  1. Click Create Credentials > Service Account
  2. Type in your service account name
  3. Grant your service account access with the basic > viewer role
7.

Add A Key to Your Service Account

For the sake of simplicity we are going to authenticate our service account using a private key. Please keep in mind that this might not be the best authentication method for your application, since the private key can be compromised. Instead, you should have a look at another authentication methods, in their documentation https://cloud.google.com/blog/u/1/products/identity-security/how-to-authenticate-service-accounts-to-help-keep-applications-secure

Finally I am going to move the client credentials json file to the root folder of my project.

Perfect! We are all set. Now it’s time to configure our directory structure in Google Drive so let’s do that.

8.

Structuring Google Drive

Create 2 directories in your Google Drive named

  • Templates
  • User Documents

Share the directories with your service account. Give him read access to the Templates directory and editor access to the User Documents.

9.

The Solution

We are going to use the Merge Text solution described in the Google Docs api documentation https://developers.google.com/docs/api/how-tos/merge.

The idea behind the concept is pretty simple

  1. Create a Google doc template inside the Templates directory
  2. Add the desired placeholders to it
  3. Copy the template inside User Documents directory
  4. Fill the template with the user data

I am going to add 3 additional steps where we will also

  1. Associate the drive document with the user
  2. Export the filled document as pdf
  3. Store the pdf on my local storage

But before continuing with the implementation let’s install the php Google api client.

10.

Google API Client Installation

The github repository can be found here https://github.com/googleapis/google-api-php-client

You can simply install it in your project by running this command

        
            
composer require google/apiclient

        
    
11.

Implementation

I created Document and DriveDocument models using:

        
            
php artisan make:model Document -m
php artisan make:model DriveDocument -m

        
    

commands and configured the migrations as followed

        
            
Schema::create('documents', function (Blueprint $table) {

   $table->id();

   $table->unsignedBigInteger('user_id')->index();

   $table->unsignedBigInteger('drive_doc_id')->index()->nullable();

   $table->string('path');

   $table->string('name');

   $table->string('mime_type');

   $table->timestamps();

   $table->softDeletes();

});


Schema::create('drive_documents', function (Blueprint $table) {

   $table->id();

   $table->unsignedBigInteger('user_id')->index();

   $table->string('source_id');

   $table->string('file_id');

   $table->string('file_name');

   $table->timestamps();

   $table->softDeletes();

});

        
    

The documents table will represent an actual file on our system.

The drive_documents table will represent a virtual document which is stored externally in Google Drive.

Great, now let’s add the relationships to our models

        
            
class Document extends Model

{

   use HasFactory, SoftDeletes;


   protected $guarded = ['id'];


   /**

    * @return BelongsTo

    */

   public function user(): BelongsTo

   {

       return $this->belongsTo(User::class);

   }


   /**

    * @return BelongsTo

    */

   public function driveDocument(): BelongsTo

   {

       return $this->belongsTo(DriveDocument::class, 'drive_doc_id');

   }

}


class DriveDocument extends Model

{

   use HasFactory, SoftDeletes;


   protected $guarded = ['id'];


   /**

    * @return BelongsTo

    */

   public function user(): BelongsTo

   {

       return $this->belongsTo(User::class);

   }


   /**

    * @return HasOne

    */

   public function document(): HasOne

   {

       return $this->hasOne(Document::class, 'drive_doc_id');

   }

}

        
    

Finally we will create 2 services.

GoogleDocsService is going to function as a wrapper around the GoogleClient. Thus simplifying client initialization and the sending of requests to the Google Docs and Drive APIs.

        
            
class GoogleDocsService

{

   private string $documentId;

   private string $directoryId;


   public function __construct()

   {

       $this->documentId = '1zpItKaTXtaQ8HEtuT2ozUsPB96h_3FhlblmdrdQzmwI';

       $this->directoryId = '10x4mz4LQuMI577MFwQVaN3uC74gF3oGu';

   }


   /**

    * @return string

    */

   public function getDocumentId(): string

   {

       return $this->documentId;

   }


   /**

    * @return Client

    * @throws Exception

    */

   public function authorizeClient(): Client

   {

       $clientCredentials = file_get_contents(base_path('client_credentials.json'));

       $config = json_decode($clientCredentials, true);


       $client = new Client();

       $client->setApplicationName(config('app.name'));

       $client->setAuthConfig($config);

       $client->setAccessType('offline');


       $client->setScopes([

           Docs::DRIVE_READONLY,

           Docs::DRIVE_FILE,

       ]);


       return $client;

   }


   /**

    * @return DriveFile

    * @throws Exception

    */

   public function copyGoogleDocTemplate(): DriveFile

   {

       $client = $this->authorizeClient();


       $docs = new Docs($client);

       $googleDoc = $docs->documents->get($this->documentId);


       $driveFile = new DriveFile([

           'name' => "Copy of {$googleDoc->title}",

           'parents' => [

               $this->directoryId

           ]

       ]);


       $drive = new Drive($client);

       return $drive->files->copy($googleDoc->documentId, $driveFile);

   }


   /**

    * @param string $documentId

    * @param array $requests

    * @return Docs\BatchUpdateDocumentResponse

    * @throws Exception

    */

   public function batchUpdateDoc(string $documentId, array $requests): Docs\BatchUpdateDocumentResponse

   {

       $docs = new Docs($this->authorizeClient());

       $batchUpdateRequest = new BatchUpdateDocumentRequest(['requests' => $requests]);


       return $docs->documents->batchUpdate($documentId, $batchUpdateRequest);

   }


   /**

    * @param string $fileId

    * @return Response

    * @throws Exception

    */

   public function exportAsPdf(string $fileId): Response

   {

       $client = $this->authorizeClient();

       $drive = new Drive($client);

       return $drive->files->export($fileId, 'application/pdf');

   }

}

        
    

DriveDocumentsService will combine all steps described in The Solution, creating one unified flow which is going to copy, fill, associate and store the generated drive document for a particular user.

        
            
class DriveDocumentsService

{

   private GoogleDocsService $googleDocsService;


   public function __construct()

   {

       $this->googleDocsService = new GoogleDocsService();

   }


   /**

    * @param User $user

    * @return void

    * @throws \Google\Exception

    * @throws \Exception

    */

   public function generateGoogleDoc(User $user): void

   {

       $driveDocument = $this->copyGoogleDoc($user);


       $this->fillGoogleDoc($driveDocument);


       $response = $this->googleDocsService->exportAsPdf($driveDocument->file_id);

       $contents = $response->getBody()->__toString();


       $document = new Document([

           'drive_doc_id' => $driveDocument->id,

           'name' => $driveDocument->file_name,

           'path' => $user->directory_path,

           'mime_type' => 'application/pdf'

       ]);


       $filePath = "{$document->path}/{$document->name}.pdf";

       $stored = Storage::disk('public')->put($filePath, $contents);


       if(!$stored) {

           throw new \Exception('File could not be stored in storage!');

       }


       $user->documents()->save($document);

   }


   /**

    * @param User $user

    * @return DriveDocument

    * @throws \Google\Exception

    */

   private function copyGoogleDoc(User $user): DriveDocument

   {

       $driveFile = $this->googleDocsService->copyGoogleDocTemplate();

       $driveDocument = new DriveDocument([

           'file_id' => $driveFile->id,

           'file_name' => $driveFile->name,

           'source_id' => $this->googleDocsService->getDocumentId(),

       ]);


       $user->driveDocuments()->save($driveDocument);

       $driveDocument->setRelation('user', $user);


       return $driveDocument;

   }


   /**

    * @throws \Google\Exception

    */

   private function fillGoogleDoc(DriveDocument $driveDocument): void

   {

       $placeholders = [

           'email' => $driveDocument->user->email,

           'name' => $driveDocument->user->name,

           'now' => Carbon::now()->toDateString()

       ];


       $requests = [];

       foreach ($placeholders as $placeholder => $value) {

           $request = new Request();

           $replaceAllTextRequest = new ReplaceAllTextRequest();

           $replaceAllTextRequest->setReplaceText($value);


           $substringMatchCriteria = new SubstringMatchCriteria();

           $substringMatchCriteria->text = '{{' . $placeholder .'}}';

           $substringMatchCriteria->setMatchCase(true);


           $replaceAllTextRequest->setContainsText($substringMatchCriteria);

           $request->setReplaceAllText($replaceAllTextRequest);


           $requests[] = [...json_decode(json_encode($request->toSimpleObject()), true)];

       }


       $this->googleDocsService->batchUpdateDoc($driveDocument->file_id, $requests);

   }

}

        
    
12.

Test of Truth

I grabbed a dummy user from my database, instantiated the DriveDocumentService and ran the whole generation process using the generateGoogleDoc method. No errors occurred which means everything worked fine.

drive documents service

This is my Google Drive directory structure

google drive structure

And this is the contract template that we used in our application

contract

And the final result in Google Drive

contract 2

Let’s also check our storage to see the local file

storage folder

When I open it, the pdf should look the same as in Google Drive.

contract 3

Excellent job, our contractor would be pleased with the results.

13.

Wrap Up

Google Docs api provides a flexible solution to merge information from one or more data sources, be it your users data, data which comes from an api request, or something else, it is a reliable tool for doing the job. And not just only that, Google Docs api is capable of even more. You can generate documents from scratch using insert texts, style your document as you want, insert inline images, which is especially useful when it comes to creating a magazine or a newspaper. The sky is the limit.

I hope you enjoyed this blog article. If you have any questions, don’t forget to leave them in the comments and don’t be shy to share your opinion on the Google Docs API solution.

I dare you go outside for a walk even if it’s raining 🌧️ and as always,

Happy coding!

Useful Links

https://developers.google.com/docs/api/how-tos/overview
https://developers.google.com/workspace/guides/get-started
https://console.cloud.google.com/workspace-api
https://cloud.google.com/blog/u/1/products/identity-security/how-to-authenticate-service-accounts-to-help-keep-applications-secure
https://developers.google.com/docs/api/how-tos/merge

Github repos:

https://github.com/googleapis/google-api-php-client

Laravel related documentation:

https://laravel.com/docs/10.x/filesystem
https://laravel.com/docs/10.x/eloquent
Marius Cristea
Full stack developer @ Control F5
OUR WORK
Case studies

We have helped 20+ companies in industries like Finance, Transportation, Health, Tourism, Events, Education, Sports.

READY TO DO THIS
Let’s build something together