Как синхронизировать Google Events с Laravel
Оглавление
- Концепция синхронизации
- Схема базы данных для Google Events
- Синхронизация событий Google Events
- Команда синхронизации событий календаря
- Заключение
В предыдущей статье "Как синхронизировать 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);
Этот объект помог в предыдущей статье выполнить синхронизацию календаря. Добавим реализацию работы по синхронизации событий для календаря.
Для синхронизации нам нужен идентификатор календаря. Вы можете использовать специальное значение calendarId
— primary
, это ссылка на основной календарь пользователя, используемый по умолчанию.
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);
}
}
- Процессы и команды Artisan в Laravel
- Создание изолируемых команд в Laravel
- Инъекция зависимостей в командах Laravel Artisan
Заключение
Мы рассмотрели событие как ресурс и настроили синхронизацию календарей и их событий. Затем мы рассмотрели обновление токенов доступа к аккаунтам google, для автоматизации всего процесса. Полный код из статьи можно найти в коммите.