Как синхронизировать Google Events с Laravel

Источник: «How to Synchronize Google Events with Laravel»
В этой статье мы будем привязывать Google Events к Google Календарям. Давайте рассмотрим, какие параметры нужно отправлять и что они означают.

Оглавление

В предыдущей статье "Как синхронизировать Google Календарь с Laravel" мы познакомились с общими принципами синхронизации ресурсов от Google и написали код, синхронизирующий календари для аккаунта Google.

В этой статье мы будем привязывать события к календарям. Давайте рассмотрим, какие параметры нужно отправлять и что они означают.

При синхронизации событий важно понимать, что нужно делать после получения данных: создавать, обновлять или удалять событие, чтобы данные оставались актуальными и валидными.

Концепция синхронизации

Google Event — это объект или ресурс, связанный с определённой датой или периодом времени. Часто он имеет дополнительные параметры, такие как местоположение, описание, часовой пояс, статус, вложения и т. д.

Помните, когда вы заходите в свой почтовый клиент и вас спрашивают, приедете ли вы?

Этот атрибут хранит ваш ответ, но не имеет никакого отношения к статусу синхронизации, поскольку при синхронизации мы будем обращать внимание на множество факторов.

Типы событий

Существует только 2 типа событий: одиночное и повторяющиеся события.

Одиночные события привязаны к одной дате или периоду времени, в отличие от повторяющихся событий, которые происходят несколько раз по расписанию (праздники, собрания, дни рождения) и имеют правило повторения (RFC 5545).

Мы будем работать с API, возвращающим все события вместе, и не будем обращать на это внимание при сохранении событий.

Параметр singleEvents отвечает за расширение событий. Отличительной чертой повторяющегося события является параметр recurrence, а всех дочерних событий — параметр recurringEventId.

Схема базы данных для Google Events

Мы не будем вносить все существующие поля, а только имеющие значение на данный момент. Структура таблицы базы данных будет выглядеть следующим образом:

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');
});

Синхронизация событий Google Events

Следуя интерфейсу ProviderInterface, мы определили функцию synchronize, создающую объект синхронизации ресурсов GoogleSynchronizer.

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

Этот объект помог в предыдущей статье выполнить синхронизацию календаря. Добавим реализацию работы по синхронизации событий для календаря.

Для синхронизации нам нужен идентификатор календаря. Вы можете использовать специальное значение calendarIdprimary, это ссылка на основной календарь пользователя, используемый по умолчанию.

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
]
);
}

Эта функция получает токен доступа от аккаунта синхронизации и формирует запрос для запроса ресурсов из Google API. Конечная точка для получения данных выглядит следующим образом:

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"

Для получения второй страницы данных запрос будет выглядеть следующим образом:

GET /calendars/v3/calendars/example@gmail.com/events?pageToken=CiAKGjBpNDd2Nmp2Zml2cXRwYjBpOXA"

Получив страницу ресурса, мы обрабатываем каждое событие отдельно с помощью функции synchronizeEvent. В результате у нас есть 3 сценария для каждого события.

Удаление событий

Если статус события cancelled, мы должны удалить его, если оно существует в нашей базе данных.

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

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

return;
}

Обновление событий

Перед выполнением API-запроса мы получили список существующих ID, связанных с данным календарём в учётной записи. Мы должны проверить, присутствует ли Event ID в базе данных, и обновить его, поскольку Event ID является уникальным полем.

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(),
]
);
}

Создание события

Если это событие не найдено в базе данных, нам нужно его создать.

$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(),
]);

Обратите внимание, что события приходят с указанным часовым поясом, но перед сохранением мы конвертируем их в UTC. Кроме того, события на весь день используют поля start.date и end.date для указания времени их проведения, а временные события — поля start.dateTime и end.dateTime. Для этого мы воспользуемся функцией преобразования даты.

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

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

Когда синхронизация завершена, мы сохраняем маркер синхронизации (syncToken) в записи календаря для дальнейшего использования и оптимизации.

Команда синхронизации событий календаря

Для проверки результата синхронизации мы воспользуемся командой в Laravel. Назовём команду synchronize: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);
}
}

Заключение

Мы рассмотрели событие как ресурс и настроили синхронизацию календарей и их событий. Затем мы рассмотрели обновление токенов доступа к аккаунтам google, для автоматизации всего процесса. Полный код из статьи можно найти в коммите.

Дополнительные материалы

Предыдущая Статья

Как синхронизировать Google Календарь с Laravel

Следующая Статья

Простая целостность данных с валидацией массивов в Laravel