Can We Use Laravel to Build a Custom Google Drive UI?

Share this article

Can We Use Laravel to Build a Custom Google Drive UI?

In this tutorial, we’re going to build an app that talks to the Google Drive API. It will have file search, upload, download and delete features. If you want to follow along, you can clone the repo from Github.

Google Drive logo

Creating a New Google Project

The first thing that we need to do when working with any of Google’s APIs is create a new project in the Google Console.

new project

Then, we need to enable the Google+ API and Google Drive API. We’ll need G+ to get the user’s information.

dashboard

In the credentials menu, we pick Add credentials, selecting OAuth 2.0 client ID.

configure consent screen

Next is the configure consent screen button. This allows us to enter the details of the project such as the name, homepage URL, and other details that are helpful to the user. For now we only really need to enter the name of the project.

consent screen

Next, we create a client ID. We need web application for the application type, and then we need to enter a value for the Authorized redirect URIs. This is the URL Google will redirect to after the user gives the app permission.

app type

Building the Project

For this project, we will be using the Laravel Framework.

composer create-project --prefer-dist laravel/laravel driver

Installing the Dependencies

We’re going to need the official Google API client for PHP, as well as the Carbon library. Update composer.json to add those:

composer require nesbot/carbon google/apiclient

Configuring the Project

We need to specify a few global settings for our project. In Laravel, that is done through the .env file. Let’s edit the file and add the app title, timezone and Google configuration below the default configuration:

APP_ENV=local
APP_DEBUG=true
APP_KEY=base64:iZ9uWJVHemk5wa8disC8JZ8YRVWeGNyDiUygtmHGXp0=
APP_URL=http://localhost

SESSION_DRIVER=file

#add these:
APP_TITLE=Driver
APP_TIMEZONE="Asia/Manila"
GOOGLE_CLIENT_ID="YOUR GOOGLE CLIENT ID"
GOOGLE_CLIENT_SECRET="YOUR GOOGLE CLIENT SECRET"
GOOGLE_REDIRECT_URL="YOUR GOOGLE LOGIN REDIRECT URL"
GOOGLE_SCOPES="email,profile,https://www.googleapis.com/auth/drive"
GOOGLE_APPROVAL_PROMPT="force"
GOOGLE_ACCESS_TYPE="offline"

Breaking it down we have:

  • GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET – the client ID and secret that we got earlier from the Google console.
  • GOOGLE_REDIRECT_URL – the URL to which Google will redirect after the user gives permission to our app. This should be the same as the one added earlier in the Google console. It should also be a valid route in the app. If we used artisan to serve the project, the redirect URL will be something like http://localhost:8000/login.
  • GOOGLE_SCOPES – the permissions that we need from the user. email gives our app access to the user’s email. profile gives us the basic user info such as the first name and the last name. https://www.googleapis.com/auth/drive gives us permission to manage files on the user’s Google Drive.

Googl

The Googl class (app\Googl.php) is used for initializing the Google and Google Drive Client libraries. It has two methods: client and drive.

The client method is where we initialize the Google API client and then set the different options that we’ve added earlier in the .env file. Once all the different options are set, we simply return the newly created client.

The drive method is used for initializing the Google Drive service. This accepts the client as its argument.

<?php
namespace App;

class Googl
{
    public function client()
    {
        $client = new \Google_Client();
        $client->setClientId(env('GOOGLE_CLIENT_ID'));
        $client->setClientSecret(env('GOOGLE_CLIENT_SECRET'));
        $client->setRedirectUri(env('GOOGLE_REDIRECT_URL'));
        $client->setScopes(explode(',', env('GOOGLE_SCOPES')));
        $client->setApprovalPrompt(env('GOOGLE_APPROVAL_PROMPT'));
        $client->setAccessType(env('GOOGLE_ACCESS_TYPE'));

        return $client;
    }


    public function drive($client)
    {
        $drive = new \Google_Service_Drive($client);
        return $drive;
    }
}

Routes

The different pages in the app are defined in the app/Http/routes.php file:

// This is where the user can see a login button for logging into Google
Route::get('/', 'HomeController@index');

// This is where the user gets redirected upon clicking the login button on the home page
Route::get('/login', 'HomeController@login');

// Shows a list of things that the user can do in the app
Route::get('/dashboard', 'AdminController@index');

// Shows a list of files in the users' Google drive
Route::get('/files', 'AdminController@files');

// Allows the user to search for a file in the Google drive
Route::get('/search', 'AdminController@search');

// Allows the user to upload new files
Route::get('/upload', 'AdminController@upload');
Route::post('/upload', 'AdminController@doUpload');

// Allows the user to delete a file
Route::get('/delete/{id}', 'AdminController@delete');

Route::get('/logout', 'AdminController@logout');

Home Controller

The Home Controller (app/Http/Controller/HomeController.php) is responsible for serving the home page and handling the user login.

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Googl;

class HomeController extends Controller
{
    public function index()
    {
        return view('login');
    }


    public function login(Googl $googl, Request $request)
    {
        $client = $googl->client();

        if ($request->has('code')) {

            $client->authenticate($request->input('code'));
            $token = $client->getAccessToken();

            $plus = new \Google_Service_Plus($client);

            $google_user = $plus->people->get('me');
            $id = $google_user['id'];

            $email = $google_user['emails'][0]['value'];
            $first_name = $google_user['name']['givenName'];
            $last_name = $google_user['name']['familyName'];

            session([
                'user' => [
                    'email' => $email,
                    'first_name' => $first_name,
                    'last_name' => $last_name,
                    'token' => $token
                ]
            ]);

            return redirect('/dashboard')
                ->with('message', ['type' => 'success', 'text' => 'You are now logged in.']);

        } else {
            $auth_url = $client->createAuthUrl();
            return redirect($auth_url);
        }
   }
}

Breaking down the code above, first we return the login view:

public function index()
{
    return view('login');
}

The login view is stored in resources/views/login.blade.php and contains the following code:

@extends('layouts.default')

@section('content')
<form method="GET" action="/login">
    <button class="button-primary">Login with Google</button>
</form>
@stop

The login view inherits from the resources/views/layouts/default.blade.php file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ env('APP_TITLE') }}</title>
    <link rel="stylesheet" href="{{ url('assets/css/skeleton.min.css') }}">
    <link rel="stylesheet" href="{{ url('assets/css/style.css') }}">
</head>
<body>
    <div class="container">
        <header>
            <h1>{{ env('APP_TITLE') }}</h1>
        </header>
        @include('partials.alert')
        @yield('content')
    </div>
</body>
</html>

We’re using the Skeleton CSS framework for this app. Since Skeleton already has us covered in making things look good, we only have minimal CSS code (public/assets/css/style.css):

.file-title {
    font-size: 18px;
}

ul#files li {
    list-style: none;
}

.file {
    padding-bottom: 20px;
}

.file-modified {
    color: #5F5F5F;
}

.file-links a {
    margin-right: 10px;
}

.alert {
    padding: 20px;
}

.alert-success {
    background-color: #61ec58;
}

.alert-danger {
    background-color: #ff5858;
}

Going back to the Home Controller, we have the login method where we check if there is a code passed in as a query parameter. If there isn’t one, we create the authentication URL and redirect the user:

if ($request->has('code')) {
    ...
} else {
    $auth_url = $client->createAuthUrl();
    return redirect($auth_url);
}

The authentication URL is basically a Google login page which the user can use to give permission to the app. After the user has given the permission, Google redirects back to the redirect URL we’ve specified. A code is passed to that URL, which we use to acquire an access token.

$client->authenticate($request->input('code'));
$token = $client->getAccessToken();

From there, we get the basic user information which we then save into the session along with the access token:

$plus = new \Google_Service_Plus($client);

$google_user = $plus->people->get('me');
$id = $google_user['id'];

$email = $google_user['emails'][0]['value'];
$first_name = $google_user['name']['givenName'];
$last_name = $google_user['name']['familyName'];

session([
    'user' => [
        'email' => $email,
        'first_name' => $first_name,
        'last_name' => $last_name,
        'token' => $token
    ]
]);

Lastly, we redirect the user to the dashboard page:

return redirect('/dashboard')
                ->with('message', ['type' => 'success', 'text' => 'You are now logged in.']);

Admin Controller

The Admin Controller (app/Http/Controllers/AdminController.php) deals with operations that only logged in users can perform. This is where the code for listing, searching, uploading and deleting files resides.

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Googl;
use Carbon\Carbon;

class AdminController extends Controller
{
    private $client;
    private $drive;

    public function __construct(Googl $googl)
    {
        $this->client = $googl->client();
        $this->client->setAccessToken(session('user.token'));
        $this->drive = $googl->drive($this->client);
    }


    public function index()
    {
        return view('admin.dashboard');
    }


    public function files()
    {
        $result = [];
        $pageToken = NULL;

        $three_months_ago = Carbon::now()->subMonths(3)->toRfc3339String();

        do {
            try {
                $parameters = [
                    'q' => "viewedByMeTime >= '$three_months_ago' or modifiedTime >= '$three_months_ago'",
                    'orderBy' => 'modifiedTime',
                    'fields' => 'nextPageToken, files(id, name, modifiedTime, iconLink, webViewLink, webContentLink)',
                ];

                if ($pageToken) {
                    $parameters['pageToken'] = $pageToken;
                }

                $result = $this->drive->files->listFiles($parameters);
                $files = $result->files;

                $pageToken = $result->getNextPageToken();

            } catch (Exception $e) {
                return redirect('/files')->with('message',
                    [
                        'type' => 'error',
                        'text' => 'Something went wrong while trying to list the files'
                    ]
                );
              $pageToken = NULL;
            }
        } while ($pageToken);

        $page_data = [
            'files' => $files
        ];

        return view('admin.files', $page_data);
   }


    public function search(Request $request)
    {
        $query = '';
        $files = [];

        if ($request->has('query')) {
            $query = $request->input('query');

            $parameters = [
                'q' => "name contains '$query'",
                'fields' => 'files(id, name, modifiedTime, iconLink, webViewLink, webContentLink)',
            ];

            $result = $this->drive->files->listFiles($parameters);
            if($result){
                $files = $result->files;
            }
        }

        $page_data = [
            'query' => $query,
            'files' => $files
        ];

        return view('admin.search', $page_data);
   }


    public function delete($id)
    {
        try {
            $this->drive->files->delete($id);
        } catch (Exception $e) {
            return redirect('/search')
                ->with('message', [
                    'type' => 'error',
                    'text' => 'Something went wrong while trying to delete the file'
                ]);
        }

        return redirect('/search')
            ->with('message', [
                'type' => 'success',
                'text' => 'File was deleted'
            ]);
    }


    public function upload()
    {
        return view('admin.upload');
    }


    public function doUpload(Request $request)
    {
        if ($request->hasFile('file')) {

            $file = $request->file('file');

            $mime_type = $file->getMimeType();
            $title = $file->getClientOriginalName();
            $description = $request->input('description');

            $drive_file = new \Google_Service_Drive_DriveFile();
            $drive_file->setName($title);
            $drive_file->setDescription($description);
            $drive_file->setMimeType($mime_type);

            try {
                $createdFile = $this->drive->files->create($drive_file, [
                    'data' => $file,
                    'mimeType' => $mime_type,
                    'uploadType' => 'multipart'
                ]);

                $file_id = $createdFile->getId();

                return redirect('/upload')
                    ->with('message', [
                        'type' => 'success',
                        'text' => "File was uploaded with the following ID: {$file_id}"
                ]);

            } catch (Exception $e) {

                return redirect('/upload')
                    ->with('message', [
                        'type' => 'error',
                        'text' => 'An error occurred while trying to upload the file'
                    ]);

            }
        }

    }


    public function logout(Request $request)
    {
        $request->session()->flush();
        return redirect('/')->with('message', ['type' => 'success', 'text' => 'You are now logged out']);
    }

}

Breaking down the code above, first we create two private variables for the Google client and the Google Drive Service and initialize them in the constructor.

private $client;
private $drive;

public function __construct(Googl $googl)
{
    $this->client = $googl->client();
    $this->client->setAccessToken(session('user.token'));
    $this->drive = $googl->drive($this->client);
}

Dashboard

To build the dashboard, we add the index method which serves the dashboard page:

public function index()
{
    return view('admin.dashboard');
}

The dashboard page contains the links to the different pages in the app:

@extends('layouts.default')

@section('content')
<h3>What do you like to do?</h3>
<ul>
    <li><a href="/files">List Files</a></li>
    <li><a href="/search">Search File</a></li>
    <li><a href="/upload">Upload File</a></li>
    <li><a href="/logout">Logout</a></li>
</ul>
@stop

Listing Files

Next we have the files method which is responsible for listing the files that have been recently modified or viewed. In the example, we’re filtering the files that have been modified or viewed during the last three months. We do that by providing the request for listing files with some params.

$three_months_ago = Carbon::now()->subMonths(3)->toRfc3339String();

$parameters = [
    'q' => "viewedByMeTime >= '$three_months_ago' or modifiedTime >= '$three_months_ago'",
    'orderBy' => 'modifiedTime',
    'fields' => 'nextPageToken, files(id, name, modifiedTime, iconLink, webViewLink, webContentLink)',
];

Most of the filtering can be done through the use of the q parameter. Here we’re using viewedByMeTime to specify that the file should have been viewed by the current user during the past three months. We use the modifiedTime to specify that the file should have been modified by any user who has access to the document in that same period. These two conditions are linked together by using the or keyword.

The results can then be ordered by specifying the orderBy parameter. In this case, we’re ordering the files using the modifiedTime field in descending order. This means that the files that have been recently modified are returned first. Also note that we’re passing in a fields parameter which is used for specifying the fields we want returned.

The full list of values usable as the q parameter is in the documentation.

We’re also specifying an additional parameter called pageToken. This allows us to paginate through a big result set. So while there’s a page token being returned, we continue fetching results from the API.

if ($pageToken) {
    $parameters['pageToken'] = $pageToken;
}

To perform the request, we use the Google Drive service to get the list of files that meets the criteria that we’ve specified earlier. The page token can also be fetched from the results.

$result = $this->drive->files->listFiles($parameters);
$files = $result->files;

$pageToken = $result->getNextPageToken();

Note that this is all done inside a do..while loop. So while the page token is not null it continues to execute.

do {
    ...
} while ($pageToken);

The following (resources/views/admin/files.blade.php) is the view used for listing the files:

@extends('layouts.default')

@section('content')
<h3>List Files</h3>
<ul id="files">
    @foreach($files as $file)
    <li>
        <div class="file">

            <div class="file-title">
                <img src="{{ $file->iconLink }}">
                {{ $file->name }}
            </div>
            <div class="file-modified">
                last modified: {{ Date::format($file->modifiedTime) }}
            </div>
            <div class="file-links">
                <a href="{{ $file->webViewLink }}">view</a>
                @if(!empty($file->webContentLink))
                <a href="{{ $file->webContentLink }}">download</a>
                @endif
            </div>
        </div>
    </li>
    @endforeach
</ul>
@stop

Here we’re looping through the files and then outputting them inside a list. Each list item contains the Google drive icon associated with the file, the title, the modification date, and a couple of links for viewing and downloading the file. Note that we’re using a custom helper class called Date. We’ll take a look at that in the Date Helper section.

Searching for Files

The search method is responsible for returning the search page. Inside, we check if the query is passed in as a request parameter. Then we we use that as the value for the filename to be searched for. Note that this time we’re not passing in nextPageToken and orderBy as parameters because we’re assuming that there will only be a few items in the result set.

if ($request->has('query')) {
    $query = $request->input('query');

    $parameters = [
        'q' => "name contains '$query'",
        'fields' => 'files(id, name, modifiedTime, iconLink, webViewLink, webContentLink)',
    ];

    $result = $this->drive->files->listFiles($parameters);
    if($result){
        $files = $result->files;
    }
}

The following (resources/views/admin/search.blade.php) is the corresponding view:

@extends('layouts.default')

@section('content')
<h3>Search Files</h3>
<form method="GET">
    <div class="row">
        <label for="query">Query</label>
        <input type="text" name="query" id="query" value="{{ $query }}">
    </div>
    <button class="button-primary">Search</button>
</form>
@if(!empty($files))
<ul id="files">
    <h4>Search results for: {{ $query }}</h4>
    @foreach($files as $file)
    <li>
        <div class="file">

            <div class="file-title">
                <img src="{{ $file->iconLink }}">
                {{ $file->name }}
            </div>
            <div class="file-modified">
                last modified: {{ Date::format($file->modifiedTime) }}
            </div>
            <div class="file-links">
                <a href="{{ $file->webViewLink }}">view</a>
                @if(!empty($webContentLink))
                <a href="{{ $file->webContentLink }}">download</a>
                @endif
                <a href="/delete/{{ $file->id }}">delete</a>
            </div>
        </div>
    </li>
    @endforeach
</ul>
@else
No results for your query: {{ $query }}
@endif
@stop

Deleting Files

When the delete link is clicked, the delete method is executed. It accepts the ID as the parameter. This is the ID of the file in Google Drive.

public function delete($id)
{
    try {
        $this->drive->files->delete($id);
    } catch (Exception $e) {
        return redirect('/search')
            ->with('message', [
                'type' => 'error',
                'text' => 'Something went wrong while trying to delete the file'
            ]);
    }

    return redirect('/search')
        ->with('message', [
            'type' => 'success',
            'text' => 'File was deleted'
        ]);
}

Uploading Files

Uploading files requires a different method for showing the view and actually uploading the file. This is because we need to use the POST method for uploading the file and GET method for showing the view.

The upload view (resources/views/admin/upload.blade.php) contains the form for uploading a single file:

@extends('layouts.default')

@section('content')
<h3>Upload Files</h3>
<form method="POST" enctype="multipart/form-data">
    <input type="hidden" name="_token" value="{{ csrf_token() }}">
    <div class="row">
        <label for="file">File</label>
        <input type="file" name="file" id="file">
    </div>
    <div class="row">
        <label for="description">Description</label>
        <input type="text" name="description" id="description">
    </div>
    <button class="button-primary">Upload</button>
</form>
@stop

Once the form is submitted, the doUpload method is executed. This checks if a file was uploaded. We then get the file and its info (mime type, file name). That information is then used to create a new Google Drive File.

if ($request->hasFile('file')) {

    $file = $request->file('file');

    $mime_type = $file->getMimeType();
    $title = $file->getClientOriginalName();
    $description = $request->input('description');

    $drive_file = new \Google_Service_Drive_DriveFile();
    $drive_file->setName($title);
    $drive_file->setDescription($description);
    $drive_file->setMimeType($mime_type);
}

Next, we upload the file to Google drive by using the files->create method. This accepts the drive file as its first argument and an array containing the actual contents of the file, mime type and upload type as its second argument. The upload type can either have a value of media, multipart or resumable. You can check out the documentation for uploads if you want to explore those upload types. For our example though, we’re only going to use multipart since we are directly uploading the files to Google Drive by submitting a form. The create method returns the ID of the file if the upload was successful.

try {
    $createdFile = $this->drive->files->create($drive_file, [
        'data' => $file,
        'mimeType' => $mime_type,
        'uploadType' => 'multipart'
    ]);

    $file_id = $createdFile->getId();

    return redirect('/upload')
        ->with('message', [
            'type' => 'success',
            'text' => "File was uploaded with the following ID: {$file_id}"
    ]);

} catch (Exception $e) {

    return redirect('/upload')
        ->with('message', [
            'type' => 'error',
            'text' => 'An error occured while trying to upload the file'
        ]);

}

Date Helper

The Date helper is used for formatting the dates returned by the Google Drive API. Here we only have one static method called format which accepts two arguments: date and format. The date is required and for the format we specify a default: M j, which outputs the date in the following format: Feb 28. Carbon is used to do the formatting.

<?php

use Carbon\Carbon;

class Date {

    public static function format($date, $format = 'M j'){

        return Carbon::parse($date)->format($format);
    }
}

In order to autoload the helper, we need to add the path app/Helpers to the classmap array in the composer.json file. Once that’s done, executing composer dump-autoload will apply the changes.

"autoload": {
    "psr-4": {
        "App\\": "app/"
    },
    "classmap": [
        "database/",
        "app/Helpers"
    ]
},

Conclusion

That’s it! In this tutorial we’ve seen how to work with the Google Drive API using PHP. Specifically, we looked at how to list, search, upload and delete files from a user’s Google drive.

We’ve barely scratched the surface in this tutorial. There’s so much more you can do: there are file revisions and real-time capabilities, to name a few. Be sure to check out the docs if you want to learn more.

Questions? Comments? Leave your feedback below!

Frequently Asked Questions (FAQs) about Laravel and Google Drive UI

How can I integrate Google Drive with Laravel?

Integrating Google Drive with Laravel involves using Google’s API client. First, you need to create a project in the Google Cloud Console and enable the Drive API. Then, you need to create credentials for the API and download the JSON file. In Laravel, you can use the google/apiclient package to interact with the API. You need to set up the client with your credentials and then you can use the Drive service to interact with Google Drive.

Can Laravel handle large files on Google Drive?

Yes, Laravel can handle large files on Google Drive. Laravel’s filesystem abstraction allows for handling files of any size. When dealing with large files, it’s important to use streaming downloads and uploads to avoid running out of memory. This can be done using the Storage::stream method for downloads and the Storage::put method for uploads.

How can I use Google Drive as a filesystem in Laravel?

Laravel’s filesystem abstraction allows you to use Google Drive as a filesystem. This involves setting up a custom filesystem driver. You can use the nao-pon/flysystem-google-drive package to easily set up a Google Drive filesystem. Once set up, you can use the Storage facade to interact with Google Drive just like any other filesystem.

Is it possible to display Google Drive content in a Laravel application?

Yes, it is possible to display Google Drive content in a Laravel application. Once you have set up the Google Drive API and integrated it with Laravel, you can use the Drive service to list files, download files, and display them in your application. You can use the listFiles method to get a list of files and the get method to download a specific file.

How can I handle Google Drive API errors in Laravel?

Google Drive API errors can be handled in Laravel using exception handling. The Google API client throws exceptions when an error occurs. You can catch these exceptions and handle them appropriately in your application. For example, you might want to log the error and display a user-friendly message to the user.

Can I use Google Drive for file storage in a Laravel application?

Yes, you can use Google Drive for file storage in a Laravel application. Laravel’s filesystem abstraction allows you to use any filesystem for file storage, including Google Drive. This can be a cost-effective solution for storing large amounts of data.

How can I search for files on Google Drive in a Laravel application?

You can search for files on Google Drive in a Laravel application using the files.list method of the Drive service. This method allows you to specify a query parameter to search for files. The query language allows for complex searches, such as searching for files of a specific type or files that contain a specific word.

How can I upload files to Google Drive in a Laravel application?

You can upload files to Google Drive in a Laravel application using the files.create method of the Drive service. This method allows you to specify the file data and metadata. You can use the Storage::put method to upload the file data.

How can I download files from Google Drive in a Laravel application?

You can download files from Google Drive in a Laravel application using the files.get method of the Drive service. This method allows you to specify the file ID. You can use the Storage::get method to download the file data.

How can I delete files from Google Drive in a Laravel application?

You can delete files from Google Drive in a Laravel application using the files.delete method of the Drive service. This method allows you to specify the file ID. You can use the Storage::delete method to delete the file data.

Wern AnchetaWern Ancheta
View Author

Wern is a web developer from the Philippines. He loves building things for the web and sharing the things he has learned by writing in his blog. When he's not coding or learning something new, he enjoys watching anime and playing video games.

apiBrunoSfile uploadframeworkgoogleGoogle DrivelaravelOOPHPPHPphp frameworkphp frameworks
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week