Как синхронизировать Google Календарь с Laravel
Оглавление
Концепция синхронизации
Для правильной синхронизации важно понимать принципы работы 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]
);
}
Приведённый выше код получает список календарей пользователей, к которым у него есть доступ владельца. После этого каждый календарь проверяется на согласованность в базе данных и выполняются действия по удалению, обновлению или созданию.
В результате мы запоминаем токен синхронизации для текущей учётной записи в базе данных.
Примечания к коду
- Токен обновления будет реализован в следующих статьях.
- По умолчанию Google API возвращает 100 календарей. Давайте опустим тот момент, что мы включаем больше.
- Мы получаем календари, в которых пользователь может читать и изменять события, а также списки контроля доступа.
- Наличие
syncToken
в запросе не позволяет использовать другие параметры. Выбрасывается исключение. nextSyncToken
кодируется перед записью в базу данных.
Советы по производительности
Чтобы получить ответ в сжатом gzip формате, вам нужно сделать следующее:
- Установить заголовок
Accept-Encoding
. - Измените свой
user agent
так, чтобы он содержал строку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));
}));
}
}
- Процессы и команды Artisan в Laravel
- Создание изолируемых команд в Laravel
- Инъекция зависимостей в командах Laravel Artisan
Итог
В этой статье мы рассмотрели, как синхронизировать Google календари с приложением Laravel. Мы рассмотрели основные параметры для получения api-ресурсов. Создали код и оптимизировали его.
Некоторые аспекты реализации были опущены, но весь список изменений можно увидеть по ссылке.
В следующей статье мы рассмотрим, как синхронизировать события Google (Google Events).