30 public const BATCH_PATH =
'https://www.googleapis.com/batch/calendar/v3/';
47 'syncEventMap' => $syncEventMap,
50 $syncEventListForExport = [];
51 $delayExportSyncEventList = [];
54 foreach ($syncEventMap as $syncEvent)
57 $syncEvent->getEventConnection()
58 && ($syncEvent->getEvent()->getVersion() === $syncEvent->getEventConnection()->getVersion())
65 $syncEvent->isRecurrence()
66 && $instanceMap = $syncEvent->getInstanceMap()
69 if (empty($delayExportSyncEventList[$syncEvent->getEvent()->getSection()->getId()]))
71 $delayExportSyncEventList[$syncEvent->getEvent()->getSection()->getId()] = [];
74 array_push($delayExportSyncEventList[$syncEvent->getEvent()->getSection()->getId()], ...$instanceMap);
77 $syncEventListForExport[$syncEvent->getEvent()->getSection()->getId()][$syncEvent->getEvent()->getUid()] = $syncEvent;
81 foreach ($syncSectionMap as $syncSection)
83 if ($syncEventList = ($syncEventListForExport[$syncSection->getSection()->getId()] ??
null))
88 $delayExportSyncEventList[$syncSection->getSection()->getId()] ??
null
102 private function exportBatch(
array $syncEventList,
SyncSection $syncSection, ?
array $syncEventInstanceList =
null): void
106 foreach (array_chunk($syncEventList, self::CHUNK_LENGTH) as $batch)
108 $body = $this->prepareMultipartMixed($batch, $syncSection);
111 $this->httpClient->post(self::BATCH_PATH, $body);
113 $this->multipartDecode($this->httpClient->getResult(), $syncEventList);
117 if ($syncEventInstanceList !==
null)
119 foreach (array_chunk($syncEventInstanceList, self::CHUNK_LENGTH) as $batch)
121 $body = $this->prepareMultipartMixed($batch, $syncSection, $syncEventList);
124 $this->httpClient->post(self::BATCH_PATH, $body);
126 $this->multipartDecode($this->httpClient->getResult(), $syncEventList);
136 private function prepareMultipartMixed(
137 array $eventCollection,
138 SyncSection $syncSection,
139 array $syncEventList = []
142 $boundary = $this->generateBoundary();
143 $this->setContentTypeHeader($boundary);
144 $data = implode(
'', $this->getBatchItemList(
150 $data .=
"--{$boundary}--" . self::LINE_SEPARATOR;
159 private function calculateHttpMethod(SyncEvent $syncEvent): string
162 $syncEvent->isInstance()
163 || ($syncEvent->getEventConnection() && $syncEvent->getEventConnection()->getVendorEventId())
166 return HttpClient::HTTP_PUT;
171 return HttpClient::HTTP_DELETE;
174 return HttpClient::HTTP_POST;
182 private function multipartDecode(
$response,
array $syncEventList): void
184 $boundary = $this->httpClient->getClient()->getHeaders()->getBoundary();
187 $parts = explode(
"--$boundary" . self::LINE_SEPARATOR,
$response);
189 foreach ($parts as $part)
194 $partEvent = explode(self::LINE_SEPARATOR . self::LINE_SEPARATOR, $part);
195 $data = $this->getMetaInfo($partEvent[1]);
198 $eventId = $this->getId($partEvent[0]);
199 if ($eventId ===
null)
206 $parsedData = Json::decode($partEvent[2]);
208 catch (ArgumentException $e)
213 if (
$data[
'status'] === 200)
217 $syncEvent = $syncEventList[$parsedData[
'iCalUID']];
219 if ($syncEvent ===
null)
224 if ($syncEvent->hasInstances() && isset($parsedData[
'originalStartTime']))
226 $syncEvent = $this->getInstanceByOriginalDate($syncEvent, $parsedData);
229 if ($syncEvent ===
null)
235 $eventConnection = (
new BuilderEventConnectionFromExternalEvent(
242 ->setEventConnection($eventConnection)
246 elseif (isset($parsedData[
'error'][
'code'], $parsedData[
'error'][
'message']))
258 private function getMetaInfo($headers):
array
262 foreach (explode(
"\n", $headers) as
$k => $header)
264 if(
$k === 0 && preg_match(
'#HTTP\S+ (\d+)#', $header, $find))
266 $data[
'status'] = (int)$find[1];
270 if(mb_strpos($header,
':') !==
false)
272 [$headerName, $headerValue] = explode(
':', $header, 2);
273 if(mb_strtolower($headerName) ===
'etag')
275 $data[
'etag'] = trim($headerValue);
287 private function getId($headers): ?int
289 foreach (explode(
"\n", $headers) as $header)
291 if(mb_strpos($header,
':') !==
false)
293 [$headerName, $headerValue] = explode(
':', $header, 2);
294 if(mb_strtolower($headerName) ===
'content-id')
296 $part = explode(
':', $headerValue);
297 return (
int)rtrim($part[1],
'>');
312 private function prepareEventContextForInstance(
313 ?SyncEvent $masterEvent,
314 SyncEvent $syncEvent,
315 EventContext $eventContext
319 if ($masterEvent && $masterEvent->isSuccessAction())
321 $masterVendorEventId = $masterEvent->getVendorEventId();
329 $prefix = $syncEvent->getEvent()->isFullDayEvent()
330 ? $syncEvent->getEvent()->getOriginalDateFrom()->format(
'Ymd')
331 : $syncEvent->getEvent()->getOriginalDateFrom()->setTimeZone(
Util::prepareTimezone())->format(
'Ymd\THis\Z')
333 $eventContext->setEventConnection(
334 (
new EventConnection())
340 ->setRecurrenceId($masterVendorEventId)
348 private function getEventConverter(SyncEvent $syncEvent): EventConverter
350 return new EventConverter(
351 $syncEvent->getEvent(),
352 $syncEvent->getEventConnection(),
353 $syncEvent->getInstanceMap()
362 private function getInstanceByOriginalDate(SyncEvent $masterEvent,
$event): ?SyncEvent
364 if (isset(
$event[
'originalStartTime'][
'dateTime']))
366 $eventOriginalStart = Date::createDateTimeFromFormat(
367 $event[
'originalStartTime'][
'dateTime'],
368 DateTimeInterface::ATOM
373 $eventOriginalStart = Date::createDateFromFormat(
374 $event[
'originalStartTime'][
'date'],
381 ->getItem(InstanceMap::getKeyByDate($eventOriginalStart));
389 private function prepareEventForInstance(SyncEvent $masterEvent, SyncEvent $syncEvent): void
391 if ($syncEvent->getEvent()->getVersion() < $masterEvent->getEvent()->getVersion())
393 $syncEvent->getEvent()->setVersion($masterEvent->getEvent()->getVersion());
406 private function prepareContextForHttpQuery(
407 SyncEvent $syncEvent,
408 SyncSection $syncSection,
409 array $syncEventList,
413 $method = $this->calculateHttpMethod($syncEvent);
415 $eventContext = (
new EventContext())->setSectionConnection($syncSection->getSectionConnection());
416 if ($syncEvent->isInstance())
418 if ($eventConnection = $syncEvent->getEventConnection())
420 $eventContext->setEventConnection($eventConnection);
424 $this->prepareEventContextForInstance($syncEventList[$syncEvent->getUid()], $syncEvent, $eventContext);
425 $this->prepareEventForInstance($syncEventList[$syncEvent->getUid()], $syncEvent);
426 $syncEvent->setEventConnection($eventContext->getEventConnection());
430 ($eventContext->getSectionConnection() ===
null)
431 || ($eventContext->getEventConnection() ===
null)
434 throw new LogicException(
'you should set event or section info');
437 $methodHeader =
$method .
' ' .
$eventManager->prepareUpdateUrl($eventContext) . self::LINE_SEPARATOR;
438 $converter = $this->getEventConverter($syncEvent);
439 $vendorEvent = $converter->convertForUpdate();
441 elseif ($syncEvent->getEventConnection() !==
null)
443 $eventContext->setEventConnection($syncEvent->getEventConnection());
444 $methodHeader =
$method .
' ' .
$eventManager->prepareUpdateUrl($eventContext) . self::LINE_SEPARATOR;
445 $converter = $this->getEventConverter($syncEvent);
446 $vendorEvent = $converter->convertForUpdate();
450 $methodHeader =
$method .
' ' .
$eventManager->prepareCreateUrl($eventContext) . self::LINE_SEPARATOR;
451 $converter = $this->getEventConverter($syncEvent);
452 $vendorEvent = $converter->convertForCreate();
456 throw new LogicException(
'do not detect action');
459 return [$methodHeader, $vendorEvent];
470 private function prepareBatchItem(
472 SyncEvent $syncEvent,
477 $data =
'--' . $boundary . self::LINE_SEPARATOR;
479 $data .=
'Content-Type: application/http' . self::LINE_SEPARATOR;
481 $id = $syncEvent->getEvent()->getId();
482 $data .=
"Content-ID: item{$id}:{$id}" . self::LINE_SEPARATOR . self::LINE_SEPARATOR;
484 $content = Json::encode($vendorEvent, JSON_UNESCAPED_SLASHES);
486 $data .= $methodHeader;
487 $data .=
'Content-type: application/json' . self::LINE_SEPARATOR;
488 $data .=
'Content-Length: ' . mb_strlen(
$content) . self::LINE_SEPARATOR . self::LINE_SEPARATOR;
491 $data .= self::LINE_SEPARATOR . self::LINE_SEPARATOR;
508 private function getBatchItemList(
509 array $eventCollection,
510 SyncSection $syncSection,
511 array $syncEventList,
517 foreach ($eventCollection as $syncEvent)
521 $eventManager =
new EventManager($this->connection, $this->userId);
522 [$methodHeader, $vendorEvent] = $this->prepareContextForHttpQuery(
529 $batchItems[] = $this->prepareBatchItem($boundary, $syncEvent, $vendorEvent, $methodHeader);
531 catch (LogicException $e)
544 private function generateBoundary(): string
546 return 'BXC' . md5(mt_rand() . time());
553 private function setContentTypeHeader(
string $boundary): void
555 $this->httpClient->getClient()->setHeader(
'Content-type',
'multipart/mixed; boundary=' . $boundary);
563 private function findSyncEvent(
array $syncEventList,
int $eventId):
array
565 return array_filter($syncEventList,
function (SyncEvent $syncEvent) use ($eventId) {
566 if ($syncEvent->getEventId() === $eventId)
571 if ($syncEvent->hasInstances())
574 foreach ($syncEvent->getInstanceMap() as
$instance)
576 if ($syncEvent->getEventId() === $eventId)
587 private function calculateLastSyncStatusForFailedSyncEvent(SyncEvent $syncEvent,
array $error)
589 if ($error[
'code'] === 404)
592 ($error[
'message'] ===
'Not Found')