精简指南:如何使用 Laravel 同步 Google 日历
作者:互联网
同步的概念
要正确同步您的资源,了解 Google API 的原理很重要。这些原则对所有Google资源都适用,但对Outlook会有所不同。我们将弄清楚如何以及为什么使用查询参数并研究最佳实践。
为了优化性能,API 将重要参数用作syncToken
和pageToken
。
大多数情况下,API 数据会分页返回,以免给网络造成负担,并在网络及其缓存上分配资源。
资源分页——pageToken
当响应中有多个页面时,可以看到该nextPageToken
字段,该字段存储接收到的关于下一页的数据。
不要忘记保存该
nextPageToken
字段,以防您在同步其中一个页面时出错并且不想检索成功保存的资源,而只是从某个页面开始。
当要获取下一页数据时,必须指定pageToken
值为nextPageToken
. 您不需要发送额外的参数,因为令牌已经具备了一切。
一个例子看起来像这样:
1. GET /calendars/primary/events
// Response
"items": [...]
"nextPageToken":"CiAKGjBpNDd2Nmp2Zml2cXRwYjBpOXA"
以下查询从 中获取值nextPageToken
并将其作为 的值发送pageToken
。
2. GET /calendars/primary/events?pageToken=CiAKGjBpNDd2Nmp2Zml2cXRwYjBpOXA
您可以通过参数控制响应中显示资源的数量maxResults
。
同步标记 - nextSyncToken
在第一次同步期间,将对集合中要同步的每个资源执行初始查询。
同步令牌表示为nextSyncToken
在列表操作响应中命名的字段。
这nextSyncToken
是优化资源同步、节省带宽的重要领域。它允许您仅检索令牌首次发布时的新数据。
不要忘记保存它
nextPageToken
以从上次收到的页面中检索资源项目。
例如,如果您在日历中创建了一个新事件,则无需检索整个事件列表并检查和处理每个事件,而只需获取更新后的数据。
一个例子看起来像这样:
1. GET https://www.googleapis.com/calendar/v3/users/me/calendarList
// Response
...
"items": [...]
"nextSyncToken": "CPDAlvWDx70CEPDAlvWDx70CGAU=",
该nextSyncToken
字段只会出现在最后一页的响应中,因为所有请求都是逐页给出的,并且会包含 nextPageToken 参数。
以下查询从 中获取值nextSyncToken
并将其作为 的值发送syncToken
。
2. GET https://www.googleapis.com/calendar/v3/users/me/calendarList?syncToken=CPDAlvWDx70CEPDAlvWDx70CGAU=
// Response
...
"items": [...]
"nextSyncToken": "v7GC9pHgvO6kpTHAxRx71KebukwS=",
在您
syncToken
不再有效的情况下,您应该将其从数据库中删除并重新请求整个资源集合。
同步谷歌日历
在上一篇文章中,我们通过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);
// Sync calendars of user account
$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);
// Check user calendars
$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'];
// Delete account calendar by 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,
]);
// Update account calendar by 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,
]
);
// Create account calendar
} 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]
);
}
上面的代码获取拥有所有者访问权限的用户日历列表。在检查每个日历在数据库中的一致性并采取操作以删除、更新或创建之后。