Support the ongoing development of Laravel.io →

How to Synchronize Google Events with Laravel

3 Feb, 2024 6 min read

In the previous article, we learned the general principles of resource synchronization from Google and wrote code that synchronizes calendars for Google account.

In this article, we will bind events to calendars. Let's look at what parameters should be sent and what they mean.

When syncing events, it's important to understand what to do after receiving the data: create, update or delete an event, to keep the data up to date and valid.

Concept of Synchronization

Google Event - is an object or resource that is associated with a specific date or time period. It often has additional parameters such as location, description, time zone, status, attachments, etc.

Remember when you log into your email client and are asked if you are coming?

This attribute stores your answer, but has nothing to do with sync status, as we will pay attention to a bunch of factors when we sync.

Types of events

There are only 2 types of events: single and recurring events.

Single events are tied to a single date or time period, as opposed to recurring events which happen several times on a regular schedule (holidays, rallies, birthdays) and have a recurrence rule (RFC 5545).

We will work with an API which will return all the events together, we will not pay attention to this when saving the events.

The parameter singleEvents is responsible for expanding the events. The distinguishing feature of a repeating event is the recurrence parameter, and of all child events is the recurringEventId parameter.

Database shema for Google Events

We will not enter all of the existing fields, only those that are meaningful at this point. The structure of the database table will look like:

Schema::create('calendar_events', function (Blueprint $table) {
    $table->id();
    $table->string('calendar_id');
    $table->string('summary')->nullable();
    $table->string('provider_id');
    $table->string('provider_type');
    $table->longText('description')->nullable();
    $table->boolean('is_all_day')->default(false);
    $table->timestamp('start_at')->nullable();
    $table->timestamp('end_at')->nullable();
    $table->timestamps();

    $table->foreign('calendar_id')->references('provider_id')->on('calendars')->onDelete('CASCADE');
});

Synchronizing Google Events

Following the ProviderInterface interface, we have defined a synchronize function that creates a resource synchronization object GoogleSynchronizer.

 public function synchronize(string $resource, Account $account);

This object, in the previous article, helped us to perform calendar synchronization. Let's add the implementation of event synchronization work for calendar.

For synchronization, we need a calendar ID. You can use the special value calendarId primary - it is a reference to the user's main calendar, which is used by default.

public function synchronizeEvents(Account $account, array $options = [])
{
    $token = $account->getToken();
    $accountId = $account->getId();
    $calendarId = $options['calendarId'] ?? 'primary';
    $pageToken = $options['pageToken'] ?? null;
    $syncToken = $options['syncToken'] ?? null;

    $now = now();

    $query = Arr::only($options, ['timeMin', 'timeMax', 'maxResults']);
    $query = array_merge($query, [
        'maxResults' => 25,
        'timeMin' => $now->copy()->startOfMonth()->toRfc3339String(),
        'timeMax' => $now->copy()->addMonth()->toRfc3339String()
    ]);

    /** @var CalendarRepository $calendarRepository */
    $calendarRepository = $this->repository(CalendarRepository::class);

    if ($token->isExpired()) {
        return false;
    }

    if (isset($syncToken)) {
        $query = [
            'syncToken' => $syncToken,
        ];
    }

    /** @var EventRepository $eventRepository */
    $eventRepository = $this->repository(EventRepository::class);

    $eventIds = $eventRepository
        ->setColumns(['provider_id'])
        ->getByAttributes([
            'calendar_id' => $calendarId,
            'provider_type' => $this->provider->getProviderName()
        ])
        ->pluck('provider_id');

    $url = "/calendar/{$this->provider->getVersion()}/calendars/${calendarId}/events";

    do {
        if (isset($pageToken) && empty($syncToken)) {
            $query = [
                'pageToken' => $pageToken
            ];
        }

        Log::debug('Synchronize Events', [
            'query' => $query
        ]);

        $body = $this->call('GET', $url, [
            'headers' => ['Authorization' => 'Bearer ' . $token->getAccessToken()],
            'query' => $query
        ]);

        $items = $body['items'];

        $pageToken = $body['nextPageToken'] ?? null;

        // Skip loop
        if (count($items) === 0) {
            break;
        }

        $itemIterator = new \ArrayIterator($items);

        while ($itemIterator->valid()) {
            $event = $itemIterator->current();

            $this->synchronizeEvent($event, $calendarId, $eventIds);

            $itemIterator->next();
        }

    } while (is_null($pageToken) === false);

    $syncToken = $body['nextSyncToken'];
    $now = now();

    $calendarRepository->updateByAttributes(
        ['provider_id' => $calendarId, 'account_id' => $accountId],
        [
            'sync_token' => Crypt::encryptString($syncToken),
            'last_sync_at' => $now,
            'updated_at' => $now
        ]
    );
}

This function gets the access token from the sync account and generates a query to request resources from Google API. Endpoint to retrieve the data looks like:

GET /calendars/v3/calendars/[email protected]/events?maxResults=25&timeMin=2023-01-01T00:00:00+00:00&timeMax=2023-02-02T20:54:27+00:00"

To get the second page of data, the query will look like this:

GET /calendars/v3/calendars/[email protected]/events?pageToken=CiAKGjBpNDd2Nmp2Zml2cXRwYjBpOXA"

Once we have the resource page, we process each event separately with the synchronizeEvent function. As a result we have 3 scripts for each event.

Delete Events

If the event status is cancelled we should delete it if it exists in our database.

if ($event['status'] === 'cancelled') {

    if ($eventIds->contains($eventId)) {
        $eventRepository->deleteWhere([
            'calendar_id' => $calendarId,
            'provider_id' => $eventId,
            'provider_type' => $this->provider->getProviderName(),
    ]);
    }

    return;
}

Update Events

Before performing the API query, we got the list of existed IDs associated with the given calendar within the account. We must check if the Event ID is present in the database, we should update it, since Event ID is a unique field.

if ($eventIds->contains($eventId)) {

    $eventRepository->updateByAttributes(
        [
            'calendar_id' => $calendarId,
            'provider_id' => $eventId,
            'provider_type' => $this->provider->getProviderName()
        ],
        [
            'summary' => $event['summary'],
            'is_all_day' => $isAllDay,
            'description' => $event['description'] ?? null,
            'start_at' => $eventStart,
            'end_at' => $eventEnd,
            'updated_at' => new \DateTime(),
        ]
    );
}

Create event

If this event is not found in the database, we need to create it.

$eventRepository->insert([
    'calendar_id' => $calendarId,
    'provider_id' => $eventId,
    'provider_type' => $this->provider->getProviderName(),
    'summary' => $event['summary'],
    'description' => $event['description'] ?? null,
    'start_at' => $eventStart,
    'end_at' =>  $eventEnd,
    'is_all_day' => $isAllDay,
    'created_at' => new \DateTime(),
    'updated_at' => new \DateTime(),
]);

Note that events come with the specified timezone, but we convert them to UTC before saving them. Also, all-day events use the start.date and end.date fields to specify their time of occurrence, while temporary events use the start.dateTime and end.dateTime fields. To do this we will use the date conversion function.

protected function parseDateTime($eventDateTime): Carbon
{
    if (isset($eventDateTime)) {
        $eventDateTime = $eventDateTime['dateTime'] ?? $eventDateTime['date'];
    }

    return Carbon::parse($eventDateTime)->setTimezone('UTC');
}

When the synchronization is complete, we save the synchronization token (syncToken) to the calendar record, for future use and optimization.

Sync Calendar Events command

To check the synchronization result, we will use a command in Laravel. Let's call the command synchronize:events.

The commands will retrieve all calendars of the selected account from the database and synchronize their events.

public function handle()
{
    $accountId = $this->argument('accountId');

    $accountModel = app(AccountRepository::class)->find($accountId);

    throw_if(empty($accountModel), ModelNotFoundException::class);

    /** @var GoogleProvider $provider */
    $provider = app(CalendarManager::class)->driver('google');

    $calendars = app(CalendarRepository::class)->getByAttributes([
        'account_id' => $accountId
    ]);

    $account = tap(new Account(), function ($account) use ($accountModel) {

        $token = Crypt::decrypt($accountModel->token);
        $syncToken = '';

        if (isset($accountModel->sync_token)) {
            $syncToken = Crypt::decryptString($accountModel->sync_token);
        }

        $account
            ->setId($accountModel->id)
            ->setProviderId($accountModel->provider_id)
            ->setUserId($accountModel->user_id)
            ->setName($accountModel->name)
            ->setEmail($accountModel->email)
            ->setPicture($accountModel->picture)
            ->setSyncToken($syncToken)
            ->setToken(TokenFactory::create($token));
    });

    foreach ($calendars as $calendar) {
        $options = ['calendarId' => $calendar->provider_id];

        if (isset($calendar->sync_token)) {
            $options['syncToken'] = Crypt::decryptString($calendar->sync_token);
        }

        $provider->synchronize('Event', $account, $options);
    }
}

Conclusion

We looked at the event as a resource and configured the synchronization of calendars and their events. Next, we look at refresh access tokens of google accounts to automate the entire process. The full code of the article can be found in the commit.

Related Links

Last updated 2 weeks ago.

driesvints, dnsinyukov, karimalik liked this article

3
Like this article? Let the author know and give them a clap!
dnsinyukov (Denis Sinyukov) Web Developerr | FullStack

Other articles you might like

January 16th 2023

Laravel Collections: The Artisan's Guide

Introduction Laravel Collections are really powerful for working with arrays of data. They provide a...

Read article
February 15th 2024

Laravel Real-Time Notifications with SSE

Say Hi to Server-Sent Events If you've ever needed to implement a real-time feature in your web appl...

Read article
February 14th 2024

Laravel Under The Hood - CSRF

Hello TokenMismatchException 👋 I know you've probably encountered this at least once. You copy-past...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2024 Laravel.io - All rights reserved.