Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Service;
  4. use App\Domain\Entity\ParsedPortfolio;
  5. use App\Domain\Entity\PortfolioDetailItem;
  6. use App\Domain\Entity\PortfolioHeader;
  7. use App\Domain\Entity\PortfolioMovementItem;
  8. class XmlParser
  9. {
  10. private const XML_HEADER_AGRMT = 'ШапкаГенСог';
  11. private const XML_HEADER_PERIOD = 'ШапкаПериод';
  12. private const XML_PORTFOLIO_TABLE = 'ШапкаПортфель';
  13. private const XML_PORTFOLIO_SECTION = 'ЗаголовокПортфель';
  14. private const XML_MOVEMENTS_SECTION = 'ШапкаДвиженияИОстаткиЦБ';
  15. private const PREFIX_DETAILS = 'Детали';
  16. private const ATTR_CLIENT = 'Клиент';
  17. private const ATTR_START_DATE = 'НачПериода';
  18. private const ATTR_END_DATE = 'КонПериода';
  19. private const ATTR_PERIOD_LINE = 'СтрокаПериода';
  20. private const ATTR_PERIOD = 'Период';
  21. private const ATTR_SECURITY = 'ЦБ';
  22. private const ATTR_ISSUER = 'Эмитент';
  23. private const ATTR_PRICE_START = 'НОЦена';
  24. private const ATTR_PRICE_END = 'КОЦена';
  25. private const ATTR_QTY_START = 'КоличествоНО';
  26. private const ATTR_QTY_END = 'КоличествоКО';
  27. private const ATTR_SUM_START = 'СуммаНКДНО';
  28. private const ATTR_SUM_END = 'СуммаНКДКО';
  29. private const ATTR_SUM_TOTAL = 'КОСумма';
  30. private const ATTR_QTY_START_MV = 'НО';
  31. private const ATTR_QTY_END_MV = 'КО';
  32. private const ATTR_QTY_INCOME = 'Приход';
  33. private const ATTR_QTY_OUTCOME = 'Расход';
  34. private const DATE_FORMAT = 'd.m.Y';
  35. public function processXml(\SimpleXMLElement $xml): ParsedPortfolio
  36. {
  37. return new ParsedPortfolio(
  38. header: $this->processXmlHeader($xml),
  39. details: $this->processDetails(
  40. $xml->{self::XML_PORTFOLIO_SECTION}->{self::XML_PORTFOLIO_TABLE}
  41. ),
  42. movements: $this->processMovements(
  43. $xml->{self::XML_MOVEMENTS_SECTION}
  44. ),
  45. );
  46. }
  47. private function processXmlHeader(\SimpleXMLElement $xml): PortfolioHeader
  48. {
  49. $attrs = [];
  50. foreach ([self::XML_HEADER_AGRMT, self::XML_HEADER_PERIOD] as $section) {
  51. foreach ($xml->{$section}->attributes() as $key => $value) {
  52. $attrs[$key] = get_object_vars($value)[0];
  53. }
  54. }
  55. if (array_key_exists(self::ATTR_START_DATE, $attrs)) {
  56. $startDate = \DateTimeImmutable::createFromFormat(
  57. self::DATE_FORMAT,
  58. $attrs[self::ATTR_START_DATE],
  59. );
  60. $endDate = \DateTimeImmutable::createFromFormat(
  61. self::DATE_FORMAT,
  62. $attrs[self::ATTR_END_DATE],
  63. );
  64. } elseif (array_key_exists(self::ATTR_PERIOD_LINE, $attrs)) {
  65. $chunks = explode(' ', $attrs[self::ATTR_PERIOD_LINE]);
  66. if (count($chunks) !== 4) {
  67. throw new \RuntimeException('Unexpected period format');
  68. }
  69. $startDate = \DateTimeImmutable::createFromFormat(self::DATE_FORMAT, $chunks[1]);
  70. $endDate = \DateTimeImmutable::createFromFormat(self::DATE_FORMAT, $chunks[3]);
  71. } else {
  72. throw new \RuntimeException('Cannot detect XML header version');
  73. }
  74. if (!$startDate || !$endDate) {
  75. throw new \RuntimeException('Invalid date format in XML header');
  76. }
  77. return new PortfolioHeader(
  78. clientAgreement: $attrs[self::ATTR_CLIENT],
  79. startDate: $startDate,
  80. endDate: $endDate,
  81. );
  82. }
  83. private function processDetails(\SimpleXMLElement $xml): array
  84. {
  85. $items = [];
  86. foreach (get_object_vars($xml) as $key => $value) {
  87. if (mb_strpos($key, self::PREFIX_DETAILS) === false) {
  88. continue;
  89. }
  90. if (!is_array($value)) {
  91. $value = [$value];
  92. }
  93. foreach ($value as $line) {
  94. $attr = [];
  95. foreach ($line->attributes() as $attrName => $attrValue) {
  96. $attr[$attrName] = get_object_vars($attrValue)[0];
  97. }
  98. $items[] = new PortfolioDetailItem(
  99. issuer: $attr[self::ATTR_ISSUER],
  100. security: $attr[self::ATTR_SECURITY],
  101. priceStart: $attr[self::ATTR_PRICE_START],
  102. priceEnd: $attr[self::ATTR_PRICE_END],
  103. quantityStart: (int) $attr[self::ATTR_QTY_START],
  104. quantityEnd: (int) $attr[self::ATTR_QTY_END],
  105. sumStart: $attr[self::ATTR_SUM_START],
  106. sumEnd: $attr[self::ATTR_SUM_END],
  107. sumTotal: $attr[self::ATTR_SUM_TOTAL],
  108. );
  109. }
  110. }
  111. return $items;
  112. }
  113. private function processMovements(\SimpleXMLElement $xml): array
  114. {
  115. $items = [];
  116. foreach (get_object_vars($xml) as $key => $value) {
  117. if (mb_strpos($key, self::PREFIX_DETAILS) === false) {
  118. continue;
  119. }
  120. if (!is_array($value)) {
  121. $value = [$value];
  122. }
  123. foreach ($value as $line) {
  124. $attr = [];
  125. foreach ($line->attributes() as $attrName => $attrValue) {
  126. $attr[$attrName] = get_object_vars($attrValue)[0];
  127. }
  128. $items[] = new PortfolioMovementItem(
  129. security: $attr[self::ATTR_SECURITY],
  130. period: new \DateTimeImmutable($attr[self::ATTR_PERIOD]),
  131. quantityStart: (int) $attr[self::ATTR_QTY_START_MV],
  132. quantityEnd: (int) $attr[self::ATTR_QTY_END_MV],
  133. quantityIncome: (int) $attr[self::ATTR_QTY_INCOME],
  134. quantityOutcome:(int) $attr[self::ATTR_QTY_OUTCOME],
  135. );
  136. }
  137. }
  138. return $items;
  139. }
  140. }