32 public const BATCH_PATH =
'https://www.googleapis.com/batch/calendar/v3/';
49 'syncEventMap' => $syncEventMap,
52 $syncEventListForExport = [];
53 $delayExportSyncEventList = [];
56 foreach ($syncEventMap as $syncEvent)
59 $syncEvent->getEventConnection()
60 && ($syncEvent->getEvent()->getVersion() === $syncEvent->getEventConnection()->getVersion())
67 $syncEvent->isRecurrence()
68 && $instanceMap = $syncEvent->getInstanceMap()
71 if (empty($delayExportSyncEventList[$syncEvent->getEvent()->getSection()->getId()]))
73 $delayExportSyncEventList[$syncEvent->getEvent()->getSection()->getId()] = [];
76 array_push($delayExportSyncEventList[$syncEvent->getEvent()->getSection()->getId()], ...$instanceMap);
79 $syncEventListForExport[$syncEvent->getEvent()->getSection()->getId()][$syncEvent->getEvent()->getUid()] = $syncEvent;
83 foreach ($syncSectionMap as $syncSection)
85 if ($syncEventList = ($syncEventListForExport[$syncSection->getSection()->getId()] ??
null))
90 $delayExportSyncEventList[$syncSection->getSection()->getId()] ??
null
104 private function exportBatch(array $syncEventList,
SyncSection $syncSection, ?array $syncEventInstanceList =
null): void
108 foreach (array_chunk($syncEventList, self::CHUNK_LENGTH) as $batch)
110 $body = $this->prepareMultipartMixed($batch, $syncSection);
113 $this->httpClient->post(self::BATCH_PATH, $body);
115 $this->multipartDecode($this->httpClient->getResult(), $syncEventList);
119 if ($syncEventInstanceList !==
null)
121 foreach (array_chunk($syncEventInstanceList, self::CHUNK_LENGTH) as $batch)
123 $body = $this->prepareMultipartMixed($batch, $syncSection, $syncEventList);
126 $this->httpClient->post(self::BATCH_PATH, $body);
128 $this->multipartDecode($this->httpClient->getResult(), $syncEventList);
138 private function prepareMultipartMixed(
139 array $eventCollection,
140 SyncSection $syncSection,
141 array $syncEventList = []
144 $boundary = $this->generateBoundary();
145 $this->setContentTypeHeader($boundary);
146 $data = implode(
'', $this->getBatchItemList(
161 private function calculateHttpMethod(SyncEvent $syncEvent): string
164 $syncEvent->isInstance()
165 || ($syncEvent->getEventConnection() && $syncEvent->getEventConnection()->getVendorEventId())
184 private function multipartDecode($response, array $syncEventList): void
186 $boundary = $this->httpClient->getClient()->getHeaders()->getBoundary();
188 $response = str_replace(
"--$boundary--",
"--$boundary", $response);
189 $parts = explode(
"--$boundary" . self::LINE_SEPARATOR, $response);
191 foreach ($parts as $part)
196 $partEvent = explode(self::LINE_SEPARATOR . self::LINE_SEPARATOR, $part);
197 $data = $this->getMetaInfo($partEvent[1]);
200 $eventId = $this->getId($partEvent[0]);
201 if ($eventId ===
null)
208 $parsedData = Json::decode($partEvent[2]);
210 catch (ArgumentException $e)
215 if ($data[
'status'] === 200)
219 $syncEvent = $syncEventList[$parsedData[
'iCalUID']];
221 if ($syncEvent ===
null)
226 if ($syncEvent->hasInstances() && isset($parsedData[
'originalStartTime']))
228 $syncEvent = $this->getInstanceByOriginalDate($syncEvent, $parsedData);
231 if ($syncEvent ===
null)
237 $eventConnection = (
new BuilderEventConnectionFromExternalEvent(
244 ->setEventConnection($eventConnection)
248 elseif (isset($parsedData[
'error'][
'code'], $parsedData[
'error'][
'message']))
260 private function getMetaInfo($headers): array
264 foreach (explode(
"\n", $headers) as $k => $header)
266 if($k === 0 && preg_match(
'#HTTP\S+ (\d+)#', $header, $find))
268 $data[
'status'] = (int)$find[1];
272 if(mb_strpos($header,
':') !==
false)
274 [$headerName, $headerValue] = explode(
':', $header, 2);
275 if(mb_strtolower($headerName) ===
'etag')
277 $data[
'etag'] = trim($headerValue);
289 private function getId($headers): ?int
291 foreach (explode(
"\n", $headers) as $header)
293 if(mb_strpos($header,
':') !==
false)
295 [$headerName, $headerValue] = explode(
':', $header, 2);
296 if(mb_strtolower($headerName) ===
'content-id')
298 $part = explode(
':', $headerValue);
299 return (
int)rtrim($part[1],
'>');
314 private function prepareEventContextForInstance(
315 ?SyncEvent $masterEvent,
316 SyncEvent $syncEvent,
317 EventContext $eventContext
321 if ($masterEvent && $masterEvent->isSuccessAction())
323 $masterVendorEventId = $masterEvent->getVendorEventId();
331 $prefix = $syncEvent->getEvent()->isFullDayEvent()
332 ? $syncEvent->getEvent()->getOriginalDateFrom()->format(
'Ymd')
333 : $syncEvent->getEvent()->getOriginalDateFrom()->setTimeZone(
Util::prepareTimezone())->format(
'Ymd\THis\Z')
335 $eventContext->setEventConnection(
336 (
new EventConnection())
342 ->setRecurrenceId($masterVendorEventId)
350 private function getEventConverter(SyncEvent $syncEvent): EventConverter
352 return new EventConverter(
353 $syncEvent->getEvent(),
354 $syncEvent->getEventConnection(),
355 $syncEvent->getInstanceMap()
364 private function getInstanceByOriginalDate(SyncEvent $masterEvent, $event): ?SyncEvent
366 if (isset($event[
'originalStartTime'][
'dateTime']))
368 $eventOriginalStart = Date::createDateTimeFromFormat(
369 $event[
'originalStartTime'][
'dateTime'],
370 DateTimeInterface::ATOM
373 elseif (isset($event[
'originalStartTime'][
'date']))
375 $eventOriginalStart = Date::createDateFromFormat(
376 $event[
'originalStartTime'][
'date'],
383 ->getItem(InstanceMap::getKeyByDate($eventOriginalStart));
391 private function prepareEventForInstance(SyncEvent $masterEvent, SyncEvent $syncEvent): void
393 if ($syncEvent->getEvent()->getVersion() < $masterEvent->getEvent()->getVersion())
395 $syncEvent->getEvent()->setVersion($masterEvent->getEvent()->getVersion());
408 private function prepareContextForHttpQuery(
409 SyncEvent $syncEvent,
410 SyncSection $syncSection,
411 array $syncEventList,
412 EventManager $eventManager
415 $method = $this->calculateHttpMethod($syncEvent);
417 $eventContext = (
new EventContext())->setSectionConnection($syncSection->getSectionConnection());
418 if ($syncEvent->isInstance())
420 if ($eventConnection = $syncEvent->getEventConnection())
422 $eventContext->setEventConnection($eventConnection);
426 $this->prepareEventContextForInstance($syncEventList[$syncEvent->getUid()], $syncEvent, $eventContext);
427 $this->prepareEventForInstance($syncEventList[$syncEvent->getUid()], $syncEvent);
428 $syncEvent->setEventConnection($eventContext->getEventConnection());
432 ($eventContext->getSectionConnection() ===
null)
433 || ($eventContext->getEventConnection() ===
null)
436 throw new LogicException(
'you should set event or section info');
439 $methodHeader = $method .
' ' . $eventManager->prepareUpdateUrl($eventContext) .
self::LINE_SEPARATOR;
440 $converter = $this->getEventConverter($syncEvent);
441 $vendorEvent = $converter->convertForUpdate();
443 elseif ($syncEvent->getEventConnection() !==
null)
445 $eventContext->setEventConnection($syncEvent->getEventConnection());
446 $methodHeader = $method .
' ' . $eventManager->prepareUpdateUrl($eventContext) .
self::LINE_SEPARATOR;
447 $converter = $this->getEventConverter($syncEvent);
448 $vendorEvent = $converter->convertForUpdate();
452 $methodHeader = $method .
' ' . $eventManager->prepareCreateUrl($eventContext) .
self::LINE_SEPARATOR;
453 $converter = $this->getEventConverter($syncEvent);
454 $vendorEvent = $converter->convertForCreate();
458 throw new LogicException(
'do not detect action');
461 return [$methodHeader, $vendorEvent];
472 private function prepareBatchItem(
474 SyncEvent $syncEvent,
483 $id = $syncEvent->getEvent()->getId();
486 $content = Json::encode($vendorEvent, JSON_UNESCAPED_SLASHES);
488 $data .= $methodHeader;
490 $data .=
'Content-Length: ' . mb_strlen($content) . self::LINE_SEPARATOR .
self::LINE_SEPARATOR;
510 private function getBatchItemList(
511 array $eventCollection,
512 SyncSection $syncSection,
513 array $syncEventList,
519 foreach ($eventCollection as $syncEvent)
523 $eventManager =
new EventManager($this->connection, $this->userId);
524 [$methodHeader, $vendorEvent] = $this->prepareContextForHttpQuery(
531 $batchItems[] = $this->prepareBatchItem($boundary, $syncEvent, $vendorEvent, $methodHeader);
533 catch (LogicException $e)
546 private function generateBoundary(): string
548 return 'BXC' . md5(mt_rand() . time());
555 private function setContentTypeHeader(
string $boundary): void
557 $this->httpClient->getClient()->setHeader(
'Content-type',
'multipart/mixed; boundary=' . $boundary);
565 private function findSyncEvent(array $syncEventList,
int $eventId): array
567 return array_filter($syncEventList,
function (SyncEvent $syncEvent) use ($eventId) {
568 if ($syncEvent->getEventId() === $eventId)
573 if ($syncEvent->hasInstances())
576 foreach ($syncEvent->getInstanceMap() as $instance)
578 if ($syncEvent->getEventId() === $eventId)
589 private function calculateLastSyncStatusForFailedSyncEvent(SyncEvent $syncEvent, array $error)
591 if ($error[
'code'] === 404)
594 ($error[
'message'] ===
'Not Found')