11 private const STR_DELIMITER_PLACEHOLDER =
'#S#';
12 private const REGEX_COMMA_AMONG_EMPTY_SPACE =
'\\s*,\\s*';
13 private const REGEX_GROUP_DELIMITER =
'(\\"([^"\\\\]*|\\\\"|\\\\\\\\|\\\\)*")';
14 private const REGEX_GROUP_FIELD_TEXT = self::REGEX_GROUP_DELIMITER;
15 private const REGEX_GROUP_FIELD_NAME =
'([a-zA-Z][a-zA-Z_0-9]*(:(NU|UN|N|U))?)';
16 private const REGEX_GROUP_FIELD_LIST_END =
'\\s*\\]';
17 private const REGEX_GROUP_END = self::REGEX_GROUP_FIELD_LIST_END;
18 private const REGEX_PART_FROM_DELIMITER_TO_FIELD_LIST =
'\\s*,\\s*\\[\\s*';
19 private const REGEX_GROUP_PART_BEFORE_FIELDS =
20 '(([^\\[\\\\]|\\\\\\[|\\\\\\\\)*)(\\[\\s*)("([^"\\\\]*|\\\\"|\\\\\\\\|\\\\)*")\\s*,\\s*\\[\\s*';
22 private const ERR_PARSE_GROUP_START_POSITION = 1100;
23 private const ERR_PARSE_GROUP_START = 1110;
24 private const ERR_PARSE_GROUP_DELIMITER = 1120;
25 private const ERR_PARSE_PART_FROM_DELIMITER_TO_FIELD_LIST = 1130;
26 private const ERR_PARSE_GROUP_FIELD_TEXT = 1140;
27 private const ERR_PARSE_GROUP_FIELD_NAME = 1150;
28 private const ERR_PARSE_GROUP_FIELD = 1160;
29 private const ERR_PARSE_GROUP_FIELD_LIST = 1170;
30 private const ERR_PARSE_GROUP_FIELD_LIST_DELIMITER = 1180;
31 private const ERR_PARSE_GROUP_FIELD_LIST_END = 1190;
32 private const ERR_PARSE_GROUP_END = 1200;
33 private const ERR_PARSE_GROUP = 1210;
36 private $template =
'';
47 public function __construct(
string $template,
string $delimiter,
bool $htmlEncode,
Format $format =
null)
49 $this->
template = $template;
50 $this->delimiter = $delimiter;
51 $this->htmlEncode = $htmlEncode;
52 $this->format = $format;
55 private function getErrorCodes(): array
57 static $errorMap =
null;
59 if ($errorMap !==
null)
65 $refClass = new \ReflectionClass(__CLASS__);
66 foreach ($refClass->getConstants() as $name => $value)
68 if (substr($name, 0, 4) ===
'ERR_')
70 $errorMap[constant(
"self::{$name}")] = $name;
77 private function getErrorsText(array $context): string
81 $errCodes = $this->getErrorCodes();
82 foreach ($context[
'error'][
'errors'] as $errInfo)
84 $result .=
"Error: {$errInfo['position']}, {$errCodes[$errInfo['code']]}" . PHP_EOL;
85 if (!empty($errInfo[
'info']) && is_array($errInfo[
'info']))
88 foreach($errInfo[
'info'] as $paramName => $paramValue)
91 if (is_string($paramValue))
93 $paramValue =
"\"{$paramValue}\"";
96 elseif (is_int($paramValue) || is_double($paramValue))
100 elseif (is_bool($paramValue))
102 $paramValue = $paramValue ?
'true' :
'false';
105 elseif (is_array($paramValue))
107 $paramValue =
'[...]';
110 elseif (is_object($paramValue))
112 $paramValue =
'{...}';
119 $result .=
" Error info:" . PHP_EOL;
122 $result .=
" {$paramName}: {$paramValue}" . PHP_EOL;
128 $result .=
'Template: "' . str_replace([
"\n",
"\""], [
'\\n',
'\\"'], $context[
'template']) .
'"'
134 private function createContext(): array
152 private function clearContextInfo(array $context): array
154 $context[
'info'] = [];
159 private function clearContextError(array $context): array
161 $context[
'hasError'] =
false;
162 $context[
'error'] = [
172 private function clearContextInfoAndError(array $context): array
174 $context = $this->clearContextInfo($context);
175 $context = $this->clearContextError($context);
180 private function unescapeText(
string $text): string
184 $length = strlen($text);
186 for ($i = 0; $i < $length; $i++)
188 if ($text[$i] ===
'\\')
190 if (($length - $i) > 1)
192 $result .= $text[++$i];
197 $result .= $text[$i];
204 private function parseGroupDelimiter(array $context): array
207 $delimiterStartPosition = $context[
'position'];
211 '/' . self::REGEX_GROUP_DELIMITER .
'/ms' . BX_UTF_PCRE_MODIFIER,
212 $context[
'template'],
215 $delimiterStartPosition
217 && $matches[0][1] === $delimiterStartPosition
221 'position' => $delimiterStartPosition,
222 'end' => $delimiterStartPosition + strlen($matches[0][0]),
223 'value' => $this->unescapeText(
225 $context[
'template'],
226 $delimiterStartPosition + 1,
227 strlen($matches[1][0]) - 2
231 $context[
'position'] = $context[
'info'][
'end'];
235 $this->addContextError($context, self::ERR_PARSE_GROUP_DELIMITER, $delimiterStartPosition);
241 private function parseFieldText(array $context): array
243 $textBlockStartPosition = $context[
'position'];
248 '/' . self::REGEX_GROUP_FIELD_TEXT .
'/ms' . BX_UTF_PCRE_MODIFIER,
249 $context[
'template'],
254 && $matches[0][1] === $textBlockStartPosition
259 'position' => $textBlockStartPosition,
260 'end' => $textBlockStartPosition + strlen($matches[0][0]),
261 'value' => $this->unescapeText(
263 $context[
'template'],
264 $textBlockStartPosition + 1,
265 strlen($matches[1][0]) - 2
269 $context[
'position'] = $context[
'info'][
'end'];
273 $this->addContextError($context, self::ERR_PARSE_GROUP_FIELD_TEXT, $textBlockStartPosition);
279 private function splitFieldName(
string $fieldName): array
281 $fieldParts = explode(
':', $fieldName);
282 $fieldName = $fieldParts[0] ??
'';
283 $fieldModifiers = $fieldParts[1] ??
'';
284 if (!is_string($fieldModifiers))
286 $fieldModifiers =
'';
289 return [$fieldName, $fieldModifiers];
296 private function isTemplateForFieldExists(
string $fieldName): bool
298 return $this->format && $this->format->getTemplate($fieldName) !==
null;
306 private function getFieldValueByTemplate(
string $fieldName, Address $address): ?string
308 if(!$this->isTemplateForFieldExists($fieldName))
313 $template = $this->format->getTemplate($fieldName)->getTemplate();
314 $templateConverter =
new StringTemplateConverter(
321 return $templateConverter->convert($address);
324 private function getAlterFieldValue(Address $address,
int $fieldType): string
327 $localityValue = is_string($localityValue) ? $localityValue :
'';
328 $result = $address->getFieldValue($fieldType);
329 $result = is_string($result) ? $result :
'';
330 if ($result !==
'' && $localityValue !==
'')
332 $localityValueUpper = mb_strtoupper($localityValue);
333 $localityValueUpperLength = mb_strlen($localityValueUpper);
334 $targetValueUpper = mb_strtoupper($result);
335 $targetValueUpperLength = mb_strlen($targetValueUpper);
336 if ($targetValueUpperLength >= $localityValueUpperLength)
338 $targetValueSubstr = mb_substr(
340 $targetValueUpperLength - $localityValueUpperLength
342 if ($localityValueUpper === $targetValueSubstr)
352 private function getAddressFieldValue(Address $address,
string $fieldName,
string $fieldModifiers): string
358 $addressFieldType = constant(FieldType::class.
'::'.$fieldName);
360 if ($fieldName ===
'ADM_LEVEL_1' || $fieldName ===
'ADM_LEVEL_2')
363 $result = $this->getAlterFieldValue($address, $addressFieldType);
367 $result = $address->getFieldValue($addressFieldType);
370 if ($result ===
null)
372 $result = $this->getFieldValueByTemplate($fieldName, $address);
375 if (!is_string($result))
381 if (strpos($fieldModifiers,
'N') !==
false)
383 $result = str_replace([
"\r\n",
"\n",
"\r"],
'#S#', $result);
385 if (strpos($fieldModifiers,
'U') !==
false)
387 $result = mb_strtoupper($result);
394 private function parseFieldName(array $context): array
396 $fieldNameStartPosition = $context[
'position'];
400 if ($context[
'address'] instanceof Address
402 '/' . self::REGEX_GROUP_FIELD_NAME .
'/ms' . BX_UTF_PCRE_MODIFIER,
403 $context[
'template'],
408 && $matches[0][1] === $fieldNameStartPosition
411 $context[
'position'] = $fieldNameStartPosition + strlen($matches[0][0]);
412 list($fieldName, $fieldModifiers) = $this->splitFieldName($matches[0][0]);
413 $fieldValue = $this->getAddressFieldValue($context[
'address'], $fieldName, $fieldModifiers);
416 'position' => $fieldNameStartPosition,
417 'end' => $context[
'position'],
418 'modifiers' => $fieldModifiers,
419 'name' => $fieldName,
420 'value' => $fieldValue,
425 $this->addContextError($context, self::ERR_PARSE_GROUP_FIELD_NAME, $fieldNameStartPosition);
431 private function parseFieldListDelimiter(array $context): array
433 $markerStartPosition = $context[
'position'];
438 '/' . self::REGEX_COMMA_AMONG_EMPTY_SPACE .
'/ms' . BX_UTF_PCRE_MODIFIER,
439 $context[
'template'],
444 && $matches[0][1] === $markerStartPosition
447 $context[
'position'] = $markerStartPosition + strlen($matches[0][0]);
451 $this->addContextError($context, self::ERR_PARSE_GROUP_FIELD_LIST_DELIMITER, $markerStartPosition);
457 private function parseFieldListEnd(array $context): array
459 $markerStartPosition = $context[
'position'];
464 '/' . self::REGEX_GROUP_FIELD_LIST_END .
'/ms' . BX_UTF_PCRE_MODIFIER,
465 $context[
'template'],
470 && $matches[0][1] === $markerStartPosition
473 $context[
'position'] = $markerStartPosition + strlen($matches[0][0]);
477 $this->addContextError($context, self::ERR_PARSE_GROUP_FIELD_LIST_END, $markerStartPosition);
483 private function parseField(array $context): array
486 $fieldStartPosition = $context[
'position'];
490 $context = $this->parseFieldText($context);
492 if ($context[
'hasError'])
494 $this->unshiftError($errors, $context[
'error'][
'code'], $context[
'error'][
'position']);
495 $context = $this->clearContextInfoAndError($context);
497 $context = $this->parseFieldName($context);
500 if ($context[
'hasError'])
502 $this->unshiftError($errors, $context[
'error'][
'code'], $context[
'error'][
'position']);
503 $context = $this->clearContextInfoAndError($context);
505 $context = $this->parseGroup($context);
506 if ($context[
'hasError'])
508 $this->unshiftError($errors, $context[
'error'][
'code'], $context[
'error'][
'position']);
510 else if ($context[
'info'][
'position'] > $fieldStartPosition)
513 $this->addContextError($context, self::ERR_PARSE_GROUP_START_POSITION, $fieldStartPosition);
514 $this->unshiftError($errors, $context[
'error'][
'code'], $context[
'error'][
'position']);
518 if (!$context[
'hasError'])
520 $fieldInfo = $context[
'info'];
521 $fieldInfo[
'isFieldListEnd'] =
false;
522 $context = $this->clearContextInfo($context);
525 $context = $this->parseFieldListDelimiter($context);
527 if ($context[
'hasError'])
529 $this->unshiftError($errors, $context[
'error'][
'code'], $context[
'error'][
'position']);
530 $context = $this->clearContextInfoAndError($context);
532 $context = $this->parseFieldListEnd($context);
533 if ($context[
'hasError'])
535 $this->unshiftError($errors, $context[
'error'][
'code'], $context[
'error'][
'position']);
539 $fieldInfo[
'isFieldListEnd'] =
true;
544 if ($context[
'hasError'])
546 $this->unshiftError($errors, self::ERR_PARSE_GROUP_FIELD, $fieldStartPosition);
547 $this->addContextErrors($context, $errors);
551 $context[
'info'] = $fieldInfo;
557 private function parseGroupFieldListStart(array $context): array
559 $fieldListStartPosition = $context[
'position'];
565 '/' . self::REGEX_PART_FROM_DELIMITER_TO_FIELD_LIST .
'/ms' . BX_UTF_PCRE_MODIFIER,
566 $context[
'template'],
571 && $matches[0][1] === $fieldListStartPosition
574 $context[
'position'] = $matches[0][1] + strlen($matches[0][0]);
575 $isFieldListEnd =
false;
576 while (!($context[
'hasError'] || $isFieldListEnd))
578 $context = $this->parseField($context);
579 if (!$context[
'hasError'])
582 isset($context[
'info'][
'isFieldListEnd'])
583 && $context[
'info'][
'isFieldListEnd']
585 if ($context[
'info'][
'value'] !==
'')
587 $fieldValues[] = $context[
'info'][
'value'];
589 $context = $this->clearContextInfo($context);
593 if (!$context[
'hasError'])
595 $context[
'info'] = [
'fieldValues' => $fieldValues];
600 $this->addContextError(
602 self::ERR_PARSE_PART_FROM_DELIMITER_TO_FIELD_LIST,
603 $fieldListStartPosition
607 if ($context[
'hasError'])
609 $this->addContextError($context, self::ERR_PARSE_GROUP_FIELD_LIST, $fieldListStartPosition);
615 private function parseGroupStart(array $context): array
621 '/' . self::REGEX_GROUP_PART_BEFORE_FIELDS .
'/ms' . BX_UTF_PCRE_MODIFIER,
622 $context[
'template'],
629 $context[
'info'][
'groupStartPosition'] = $matches[3][1];
630 $context[
'info'][
'groupDelimiterStartPosition'] = $matches[4][1];
634 $this->addContextError($context, self::ERR_PARSE_GROUP_START, $context[
'position']);
640 private function parseGroupEnd(array $context): array
642 $markerStartPosition = $context[
'position'];
647 '/' . self::REGEX_GROUP_END .
'/ms' . BX_UTF_PCRE_MODIFIER,
648 $context[
'template'],
653 && $matches[0][1] === $markerStartPosition
656 $context[
'position'] = $markerStartPosition + strlen($matches[0][0]);
660 $this->addContextError($context, self::ERR_PARSE_GROUP_END, $markerStartPosition);
666 private function parseGroup(array $context): array
668 $startSearchPosition = $context[
'position'];
669 $groupStartPosition = 0;
670 $delimiterValue =
'';
676 $context = $this->parseGroupStart($context);
678 if (!$context[
'hasError'])
681 $groupStartPosition = $context[
'info'][
'groupStartPosition'];
682 $context[
'position'] = $context[
'info'][
'groupDelimiterStartPosition'];
683 $context = $this->clearContextInfo($context);
684 $context = $this->parseGroupDelimiter($context);
687 if (!$context[
'hasError'])
690 $delimiterValue = $context[
'info'][
'value'];
691 $context = $this->clearContextInfo($context);
692 $context = $this->parseGroupFieldListStart($context);
695 if (!$context[
'hasError'])
698 $fieldValues = $context[
'info'][
'fieldValues'];
699 $context = $this->clearContextInfo($context);
700 $context = $this->parseGroupEnd($context);
703 if (!$context[
'hasError'])
708 'position' => $groupStartPosition,
709 'end' => $context[
'position'],
721 if ($context[
'hasError'])
723 $this->addContextError(
725 self::ERR_PARSE_GROUP,
726 $startSearchPosition,
727 [
'groupStartPosition' => $groupStartPosition]
734 private function appendTextBlock(array &$blocks,
int $position,
string $value)
736 $lastBlock = end($blocks);
737 $lastBlockIndex = key($blocks);
738 if (is_array($lastBlock) && $lastBlock[
'type'] ===
'text')
740 $blocks[$lastBlockIndex][
'value'] .= $value;
741 $blocks[$lastBlockIndex][
'length'] += strlen($value);
747 'position' => $position,
748 'length' => strlen($value),
754 private function appendGroupBlock(array &$blocks,
int $position,
string $value)
758 'position' => $position,
759 'length' => strlen($value),
764 private function unshiftError(array &$errors,
int $code,
int $position, array $info =
null)
770 'position' => $position,
771 'info' => (!empty($info) && is_array($info)) ? $info : [],
776 private function addContextError(array &$context,
int $code,
int $position, array $info =
null)
778 $context[
'hasError'] =
true;
779 $context[
'error'][
'code'] = $code;
780 $context[
'error'][
'position'] = $position;
781 $context[
'error'][
'info'] = (!empty($info) && is_array($info)) ? $info : [];
782 $this->unshiftError($context[
'error'][
'errors'], $code, $position, $info);
785 private function addContextErrors(array &$context, array $errors, array $info =
null)
787 $context[
'hasError'] =
true;
788 $context[
'error'][
'code'] = $errors[0][
'code'];
789 $context[
'error'][
'position'] = $errors[0][
'position'];
790 $context[
'error'][
'info'] = (!empty($info) && is_array($info)) ? $info : [];
791 array_splice($context[
'error'][
'errors'], 0, 0, $errors);
794 private function parseBlocks(array $context): array
802 $templateLength = strlen($context[
'template']);
803 while ($context[
'position'] < $templateLength)
805 $blockStartPosition = $context[
'position'];
806 $context = $this->parseGroup($context);
807 if ($context[
'hasError'])
816 $errorInfo = $context[
'error'][
'info'];
817 if (!empty($errorInfo)
818 && is_array($errorInfo)
819 && isset($errorInfo[
'groupStartPosition'])
820 && $errorInfo[
'groupStartPosition'] > $blockStartPosition)
822 $blockLength = $errorInfo[
'groupStartPosition'] - $blockStartPosition + 1;
829 $this->appendTextBlock(
831 $context[
'error'][
'position'],
832 substr($context[
'template'], $blockStartPosition, $blockLength)
834 $context = $this->clearContextInfoAndError($context);
835 $context[
'position'] = $blockStartPosition + $blockLength;
839 $groupStartPosition = $context[
'info'][
'position'];
840 if ($groupStartPosition > $blockStartPosition)
842 $this->appendTextBlock(
846 $context[
'template'],
848 $groupStartPosition - $blockStartPosition
853 if ($context[
'info'][
'value'] !==
'')
855 $this->appendGroupBlock(
858 $context[
'info'][
'value']
862 $context = $this->clearContextInfo($context);
866 if (!$context[
'hasError'])
868 $context[
'info'] = [
'blocks' => $blocks];
878 $context = $this->createContext();
879 $context[
'template'] = $this->template;
880 $context[
'address'] = $address;
882 $context = $this->parseBlocks($context);
884 if (!$context[
'hasError'])
886 foreach ($context[
'info'][
'blocks'] as $block)
888 if ($block[
'type'] ===
'text')
890 $result .= $this->unescapeText($block[
'value']);
894 $result .= $block[
'value'];
901 $result = explode(self::STR_DELIMITER_PLACEHOLDER, $result);
902 $result = array_values(
903 array_filter($result,
function ($part) {
904 return ($part !==
'');
907 if ($this->htmlEncode && !empty($result) && is_array($result))
909 array_walk($result,
function (&$part) {
910 $part = htmlspecialcharsbx($part);
914 $result = implode($this->delimiter, $result);