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/example@gmail.com/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/example@gmail.com/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.
Top comments (0)