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

Источник: «Compact Guide: How to sync Google calendar with Laravel»
В этой статье мы рассмотрим, как синхронизировать Google Календари с Laravel приложением, основные параметры для получения API-ресурсов. Создадим и оптимизируем код.

Оглавление

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

Для правильной синхронизации важно понимать принципы работы Google API. Эти принципы работают одинаково для всех ресурсов Google, но будут отличаться для Outlook. Мы разберёмся, как и зачем использовать параметры запроса, и изучим лучшие практики.

Для оптимизации производительности API использует такие важные параметры, как syncToken и pageToken.

Данные API в большинстве случаев возвращаются с пагинацией, чтобы не нагружать сеть и не выделять ресурсы на сеть и их кэширование.

Пагинация ресурса — pageToken

Если в ответе содержится более одной страницы, вы можете увидеть поле nextPageToken, хранящее полученные данные о следующей странице.

Не забудьте сохранить поле nextPageToken на случай, если при возникновении ошибки синхронизации одной из страниц вы не хотите получать успешно сохранённые ресурсы, а только начиная с определённой страницы.

Когда нужно получить следующую страницу данных, необходимо указать pageToken со значением nextPageToken. Вам не нужно передавать дополнительные параметры, поскольку в токене уже всё есть.

В качестве примера можно привести следующее:

1. GET /calendars/primary/events

// Ответ
"items": [...]
"nextPageToken":"CiAKGjBpNDd2Nmp2Zml2cXRwYjBpOXA"

Следующий запрос берет значение из nextPageToken и отправляет его в качестве значения для pageToken.

2. GET /calendars/primary/events?pageToken=CiAKGjBpNDd2Nmp2Zml2cXRwYjBpOXA

Вы можете контролировать количество отображаемых ресурсов в ответе с помощью параметра maxResults.

Маркер синхронизации — nextSyncToken

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

Токен синхронизации представлен в виде поля с именем nextSyncToken в ответе операции со списком.

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

Не забудьте сохранить nextSyncToken, чтобы получить элементы ресурса с последней полученной страницы.

Например, если вы создаёте новое событие в календаре, вам не нужно получать весь список событий, проверять и обрабатывать каждое из них, вместо этого вы получаете только обновлённые данные.

Пример выглядит следующим образом:

1. GET https://www.googleapis.com/calendar/v3/users/me/calendarList

// Ответ
...
"items": [...]
"nextSyncToken": "CPDAlvWDx70CEPDAlvWDx70CGAU=",

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

Следующий запрос получает значение из nextSyncToken и отправляет его в качестве значения для syncToken.

2. GET https://www.googleapis.com/calendar/v3/users/me/calendarList?syncToken=CPDAlvWDx70CEPDAlvWDx70CGAU=

// Ответ
...
"items": [...]
"nextSyncToken": "v7GC9pHgvO6kpTHAxRx71KebukwS=",

В случаях, когда syncToken больше не действителен, следует удалить его из базы данных и повторно запросить всю коллекцию ресурсов.

Синхронизация календарей Google

В предыдущей статье мы настроили авторизацию через oauth2, после чего данные Google Account были записаны в базу данных.

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

public function callback(string $driver): RedirectResponse
{
/** @var ProviderInterface $provider */
$provider = $this->manager->driver($driver);

/** @var Account $account */
$account = $provider->callback();

$accountId = app(AccountService::class)->createFrom($account, $driver);

$account->setId($accountId);

// Синхронизация календарей учётной записи пользователя
$provider->synchronize('Calendar', $account);

return redirect()->to(
config('services.' . $driver . '.redirect_callback', '/')
);
}

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

Schema::create('calendars', function (Blueprint $table) {
$table->id();
$table->string('summary')->nullable();
$table->string('timezone')->nullable();
$table->string('provider_id');
$table->string('provider_type');
$table->text('description')->nullable();
$table->text('page_token')->nullable();
$table->text('sync_token')->nullable();
$table->timestamp('last_sync_at')->nullable();
$table->boolean('selected')->default(false);
$table->unsignedBigInteger('account_id');
$table->foreign('account_id')->references('id')->on('calendar_accounts')->onDelete('CASCADE');
$table->index(['provider_id', 'provider_type']);
$table->timestamps();
});

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

Чтобы выполнить синхронизацию, добавим немного логики в наш драйвер календаря — GoogleProvider.php.

public function synchronize(string $resource, Account $account, array $options = [])
{
$resource = Str::ucfirst($resource);

$method = 'synchronize' . Str::plural($resource);

$synchronizer = $this->getSynchronizer();

if (method_exists($synchronizer, $method) === false) {
throw new \InvalidArgumentException('Method is not allowed.', 400);
}

return call_user_func([$synchronizer, $method], $account, $options);
}

Функция getSynchronizer() вернёт класс синхронизатора, который будет посредником между ресурсами. В нем есть метод: synchronizeCalendars().

public function synchronizeCalendars(Account $account, array $options = [])
{
$token = $account->getToken();
$accountId = $account->getId();
$syncToken = $account->getSyncToken();

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

$query = array_merge([
'maxResults' => 100,
'minAccessRole' => 'owner',
], $options['query'] ?? []);

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

$body = $this->call('GET', "/calendar/{$this->provider->getVersion()}/users/me/calendarList", [
'headers' => ['Authorization' => 'Bearer ' . $token->getAccessToken()],
'query' => $query
]);

$nextSyncToken = $body['nextSyncToken'];
$calendarIterator = new \ArrayIterator($body['items']);

/** @var CalendarRepository $calendarRepository */
$calendarRepository = app(CalendarRepository::class);

// Проверка календарей пользователей
$providersIds = $calendarRepository
->setColumns(['provider_id'])
->getByAttributes(['account_id' => $accountId, 'provider_type' => $this->provider->getProviderName()])
->pluck('provider_id');

$now = now();

while ($calendarIterator->valid()) {
$calendar = $calendarIterator->current();
$calendarId = $calendar['id'];

// Удаление аккаунта календаря по ID
if (key_exists('deleted', $calendar) && $calendar['deleted'] === true && $providersIds->contains($calendarId)) {
$calendarRepository->deleteWhere([
'provider_id' => $calendarId,
'provider_type' => $this->provider->getProviderName(),
'account_id' => $accountId,
]);

// Обновление аккаунта календаря по ID
} else if ($providersIds->contains($calendarId)) {
$calendarRepository->updateByAttributes(
[
'provider_id' => $calendarId,
'provider_type' => $this->provider->getProviderName(),
'account_id' => $accountId,
],
[
'summary' => $calendar['summary'],
'timezone' => $calendar['timeZone'],
'description' => $calendar['description'] ?? null,
'updated_at' => $now,
]
);
// Создание аккаунта календаря
} else {
$calendarRepository->insert([
'provider_id' => $calendarId,
'provider_type' => $this->provider->getProviderName(),
'account_id' => $accountId,
'summary' => $calendar['summary'],
'timezone' => $calendar['timeZone'],
'description' => $calendar['description'] ?? null,
'selected' => $calendar['selected'] ?? false,
'created_at' => $now,
'updated_at' => $now,
]);
}

$calendarIterator->next();
}

$this->getAccountRepository()->updateByAttributes(
['id' => $accountId],
['sync_token' => Crypt::encryptString($nextSyncToken), 'updated_at' => $now]
);
}

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

В результате мы запоминаем токен синхронизации для текущей учётной записи в базе данных.

Примечания к коду

Советы по производительности

Чтобы получить ответ в сжатом gzip формате, вам нужно сделать следующее:

Каждый запрос к api google будет содержать эти заголовки.

protected function headers(array $headers = []): array
{
return array_merge([
'Content-Type' => 'application/json',
'Accept-Encoding' => 'gzip',
'User-Agent' => config('app.name') . ' (gzip)',

], $headers);
}

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

Чтобы проверить результат синхронизации, воспользуемся командой в Laravel. Назовём её synchronize:calendars.

Команды будут извлекать все учётные записи из базы данных и при этом синхронизировать список календарей.

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

$accounts = app(AccountRepository::class)->get();

foreach ($accounts as $accountModel) {
$provider->synchronize('Calendar', 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));
}));
}
}

Итог

В этой статье мы рассмотрели, как синхронизировать Google календари с приложением Laravel. Мы рассмотрели основные параметры для получения api-ресурсов. Создали код и оптимизировали его.

Некоторые аспекты реализации были опущены, но весь список изменений можно увидеть по ссылке.

В следующей статье мы рассмотрим, как синхронизировать события Google (Google Events).

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

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

Laravel: Решение проблемы "Vite manifest not found"

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

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