diff --git a/README.md b/README.md
index 6490de86..57768a07 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,14 @@
-# PHP2021
\ No newline at end of file
+# PHP2021
+# Домашнее задание №13. Архитектура кода
+
+В рамках разработки рабочего проекта есть необходимость доработки ранее написанного модуля парсера продукции из предоставляемых клиентом данных, для последующего размещения на платформе. На данный момент парсер может работать только с файлами формата XLSX. время от времени производители присылают разного рода XML файлы, которые сейчас приходится парсить в Excel, и в теории можно будет продумать подгрузку файлов других типов.
+
+В качестве объекта анализа выступает модуль-парсер, который в данный момент состоит из запускаемого через консоль или через Битриксового агента скрипта, который последовательно экземпляр класса FeedsImporter и вызывает его метод run, внутри run создается FeedParser, который в свою очередь вызывает методы ExcelParser и только тогда заполняет временную таблицу данных из файла фида. Добавление еще одного парсера в такой системе выглядит как большое количество переписывания всего модуля.
+
+Во-первых создадим интерфейс FileParserInterface, для того что бы можно было безболезненно добавлять парсеры других загружаемых файлов. На основе этого интерфейса создадим пока два парсера: для Excel (ExcelParser) и XML (XmlParser).
+
+На данный момент такой задачи нет, но на всякий случай создадим интерфейс для парсера фидов (поиск файлов фида, настроек пользователя и т.д.) под потенциально разные задачи. В данный момент фиды есть только у Производителей в основном каталоге продукции, но, например, может потребоваться добавлять какие-то элементы в разрабатываемую на платформе в данный момент секцию библиотеки или в каталоги других типов пользователей. Поэтому создадим FeedParserInterface и к нему пока что одну реализацию интерфейса для загрузки данных в товарный каталог ManufacturerFeedParser.
+
+Управлять всем будет ImportService, который будет использовать например все тем же битриксовым агентом при периодической выгрузке файлов из очереди.
+
+Отдельно вынесем вспомогательные методы (ImportHelper.php) и запись данных во временную таблицу (FeedsImportTempTable.php)
diff --git a/after/FeedParser/FeedParserInterface.php b/after/FeedParser/FeedParserInterface.php
new file mode 100644
index 00000000..3583dd86
--- /dev/null
+++ b/after/FeedParser/FeedParserInterface.php
@@ -0,0 +1,21 @@
+logger = $logger;
+ }
+
+ public function parseFeedToTable(array $arFeed): void
+ {
+ $labelName = "import_{$arFeed['UF_USER_ID']}";
+ Debug::startTimeLabel($labelName);
+
+ if (empty($arFeed['file_path'])) {
+ $this->errors['Critical'][] = ['msg' => 'Не найдены файлы фида'];
+ $this->logger->addToLog('parse feed', 'error', ['msg' => 'Пустой массив путей к файлам фида', 'feed_id' => $this->feedId]);
+ throw new Exception('Пустой массив путей к файлам фида');
+ }
+
+ try {
+ if (isset($this->fileParser)) {
+ $this->fileParser->run($arFeed['UF_USER_ID'],$arFeed['ID'], $arFeed['file_path']);
+ } else {
+ throw new Exception('Не задан класс парсера файла');
+ }
+ } catch (\Exception $e) {
+ $this->logger->addToLog('parse feed', 'error', ['msg' => $e->getMessage(), 'feed_id' => $this->feedId]);
+ $this->errors['Critical'][] = ['msg' => $e->getMessage()];
+ }
+
+ $parseErrors = $this->fileParser->getErrors();
+ if (!empty($parseErrors)) {
+ $this->errors = array_merge($this->errors, $parseErrors);
+ }
+
+ Debug::endTimeLabel($labelName);
+ $arLabels = Debug::getTimeLabels();
+ $timeSpent = round($arLabels[$labelName]['time'], 2);
+ $this->logger->addToLog('parse feed', 'success', ['msg' => "Время обработки файла: $timeSpent сек", 'feed_id' => $this->feedId]);
+ }
+
+ public function getFileParser(): FileParserInterface
+ {
+ return $this->fileParser;
+ }
+
+ public function setFileParser(FileParserInterface $fileParser): void
+ {
+ $this->fileParser = $fileParser;
+ }
+
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+}
diff --git a/after/FileParser/ExcelParser.php b/after/FileParser/ExcelParser.php
new file mode 100644
index 00000000..d7057b63
--- /dev/null
+++ b/after/FileParser/ExcelParser.php
@@ -0,0 +1,26 @@
+errors;
+ }
+}
diff --git a/after/FileParser/FileParserInterface.php b/after/FileParser/FileParserInterface.php
new file mode 100644
index 00000000..7940d95c
--- /dev/null
+++ b/after/FileParser/FileParserInterface.php
@@ -0,0 +1,14 @@
+errors;
+ }
+}
diff --git a/after/Service/FeedsImportTempTable.php b/after/Service/FeedsImportTempTable.php
new file mode 100644
index 00000000..62ab5784
--- /dev/null
+++ b/after/Service/FeedsImportTempTable.php
@@ -0,0 +1,18 @@
+importHelper = $importHelper;
+ $this->logger = $logger;
+ $this->feedParser = $feedParser;
+ $this->obElement = $this->obElement = new \CIBlockElement();
+ $this->iblockIdCatalog = Core::getInstance()->getIblockId($feedParser::IBLOCK_CODE_CATALOG_NEW);
+ }
+
+ public function importFeed(int $feedId): void
+ {
+ $this->feedId = $feedId;
+ try {
+ $this->arFeedInfo = $this->importHelper->getFeedInfo($this->feedId);
+ $this->fileParser = $this->importHelper->getFileParserInterface((int)$this->arFeedInfo['UF_FILE_ID']);
+ $this->feedParser->setFileParser($this->fileParser);
+ $this->feedParser->parseFeedToTable($this->arFeedInfo);
+ $this->importItems();
+ } catch (Exception $e) {
+ $this->logger->addToLog('feed import', 'error', ['msg' => $e->getMessage(), 'feed_id' => $this->feedId]);
+ throw new Exception($e->getMessage());
+ }
+ }
+
+ /**
+ * Импортируем товары из временной таблицы
+ */
+ private function importItems(): void
+ {
+ $resItems = FeedsImportTempTable::getList([
+ 'select' => ['*'],
+ 'filter' => ['feed_id'=> (int)$this->arFeedInfo['UF_FILE_ID']]
+ ]);
+
+ $count = 0;
+
+ while ($row = $resItems->fetch()) {
+ $arElementFounded = $this->findElement($row['ekn']);
+ if ($row['errors'] != '') {
+ continue;
+ }
+
+ if (!empty($arElementFounded)) {
+ $this->updateElement($arElementFounded['ID'], $row);
+ } else {
+ $this->addElement($row);
+ }
+
+ $count++;
+ }
+
+ $this->countItems = $count;
+ }
+
+
+ private function findElement(string $ekn): array
+ {
+ /*
+ * Некоторая логика по поиску сущестующих элементов каталога
+ */
+ return $arElement;
+ }
+
+
+ private function updateElement(int $id, array $row): void
+ {
+ /*
+ * Некоторая логика по подготовке данных для обновления существующего элемента каталога
+ */
+ $this->obElement->SetPropertyValues($id, $this->iblockIdCatalog, $arPropertyValues);
+ $this->obElement->Update($arFields);
+ }
+
+
+ private function addElement(array $row): bool
+ {
+ /*
+ * Некоторая логика по подготовке данных для добавления нового элемента в каталог
+ */
+ $this->obElement->Add($arFields);
+ }
+
+}
\ No newline at end of file
diff --git a/after/Tools/ImportHelper.php b/after/Tools/ImportHelper.php
new file mode 100644
index 00000000..aedc78d4
--- /dev/null
+++ b/after/Tools/ImportHelper.php
@@ -0,0 +1,35 @@
+
+
+
+
\ No newline at end of file
diff --git a/before/FeedParser.php b/before/FeedParser.php
new file mode 100644
index 00000000..66531c7e
--- /dev/null
+++ b/before/FeedParser.php
@@ -0,0 +1,104 @@
+logger = new TableLogger(new FeedsImporterDebugLogTable());
+ $this->tempFolder = "{$_SERVER['DOCUMENT_ROOT']}/upload/feeds_temp/";
+ $this->excelParser = new ExcelParser();
+ }
+
+ /**
+ * Считываем содержимое фида во временную таблицу
+ * @param $arFeed
+ * @return void
+ * @throws \Bitrix\Main\ObjectException
+ */
+ public function parseFeedToTable($arFeed)
+ {
+ $labelName = "import_{$arFeed['UF_USER_ID']}";
+ Debug::startTimeLabel($labelName);
+
+ $this->feedId = $arFeed['ID'];
+ $this->manufacturerId = $arFeed['UF_USER_ID'];
+ $this->checkfeedFile($arFeed['UF_FILE']);
+
+ if (empty($this->feedFilePath)) {
+ $this->errors['Critical'][] = ['msg' => 'Не найдены файлы фида'];
+ $this->logger->addToLog('parse feed', 'error', ['msg' => 'Пустой массив путей к файлам фида', 'feed_id' => $this->feedId]);
+ return;
+ }
+
+ try {
+ $this->excelParser->run($this->manufacturerId, $this->feedId, $this->feedFilePath);
+ } catch (\Exception $e) {
+ $this->logger->addToLog('parse feed', 'error', ['msg' => $e->getMessage(), 'feed_id' => $this->feedId]);
+ $this->errors['Critical'][] = ['msg' => 'Критическая ошибка в работе модуля ExcelParser'];
+ }
+
+ $parseErrors = $this->excelParser->getErrors();
+ if (!empty($parseErrors)) {
+ $this->errors = array_merge($this->errors, $parseErrors);
+ }
+
+ Debug::endTimeLabel($labelName);
+ $arLabels = Debug::getTimeLabels();
+ $timeSpent = round($arLabels[$labelName]['time'], 2);
+ $this->logger->addToLog('parse feed', 'success', ['msg' => "Время обработки файла: $timeSpent сек", 'feed_id' => $this->feedId]);
+ }
+
+ /**
+ * Проверка файла фида на валидность
+ */
+ private function checkfeedFile(int $fileId): bool
+ {
+
+ if ($fileId <= 0) {
+ $this->errors['Critical'][] = ['msg' => 'Получен некорректный ID файла'];
+ $this->logger->addToLog('parser set files paths', 'error', ['msg' => 'Некорректный ID файла', 'feed_id' => $this->feedId]);
+ return false;
+ }
+
+ $filePath = $_SERVER['DOCUMENT_ROOT'] . \CFile::GetPath($fileId);
+ $arFile = \CFile::MakeFileArray($filePath);
+
+ if ($arFile['type'] != 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
+ $this->errors['Critical'][] = ['msg' => 'Предоставлен неверный тип файла'];
+ $this->logger->addToLog('parser set files paths', 'error', ['msg' => 'Неверный тип', 'feed_id' => $this->feedId, 'file_id' => $fileId]);
+ return false;
+ }
+
+ if (!file_exists($arFile['tmp_name'])) {
+ $this->errors['Critical'][] = ['msg' => 'Получен путь до несуществующего файла'];
+ return false;
+ }
+
+ $this->feedFilePath = $filePath;
+ return $arFile;
+ }
+
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+}
diff --git a/before/FeedsImporter.php b/before/FeedsImporter.php
new file mode 100644
index 00000000..3110263f
--- /dev/null
+++ b/before/FeedsImporter.php
@@ -0,0 +1,164 @@
+loggerDebug = new TableLogger(new FeedsImporterDebugLogTable());
+
+ $core = Core::getInstance();
+ $this->iblockIdCatalog = $core->getIblockId($core::IBLOCK_CODE_CATALOG_NEW);
+ $this->obElement = new \CIBlockElement();
+ }
+
+
+ /**
+ * @param $feedId
+ * ID загруженного файла фида
+ * @throws \Bitrix\Main\ObjectException
+ */
+ public function run(int $feedId): void
+ {
+ $this->loggerDebug->addToLog('feeds importer', 'start', "feed_id $feedId");
+
+ if ($feedId !== false) {
+ try {
+ $this->importFeed($feedId);
+ } catch (Exception $e) {
+ AddMessage2Log($e->getMessage());
+ $this->loggerDebug->addToLog('feeds importer', 'critical error', [
+ 'feed_id' => $feedId,
+ 'msg' => $e->getMessage(),
+ ]);
+ }
+ }
+
+ $this->loggerDebug->addToLog('feeds importer', 'end', '');
+ }
+
+ /**
+ * Загрузка данных из фида
+ * @param $feedId
+ * @return bool
+ */
+ private function importFeed(int $feedId): bool
+ {
+ if (!$feedId) {
+ $this->loggerDebug->addToLog('import feed', 'error', ['msg' => 'incorrect feed id']);
+ return false;
+ }
+
+ $this->loggerDebug->addToLog('import feed', 'info', ['feed_id' => $feedId, 'msg' => 'start feed import']);
+
+ $arFeed = ImportHelper::getFeedInfo((int)$feedId);
+
+ $parser = new FeedParser();
+
+ if ($parser === false) {
+ return false;
+ }
+
+ $parser->parseFeedToTable($arFeed);
+
+ $arErrors = $parser->getErrors();
+ if (empty($arErrors['Critical'])) {
+ $this->importItems($feedId);
+
+ } else {
+ $this->loggerDebug->addToLog('import feed', 'error', ['feed_id' => $feedId, 'msg' => json_encode($arErrors['Critical'], JSON_UNESCAPED_UNICODE)]);
+ }
+
+ return true;
+ }
+
+ /**
+ * Импортируем товары из временной таблицы
+ * @param $feedId
+ */
+ private function importItems($feedId)
+ {
+ $resItems = FeedsImportTempTable::getList([
+ 'select' => ['*'],
+ 'filter' => ['feed_id'=> $feedId]
+ ]);
+
+ $count = 0;
+
+ while ($row = $resItems->fetch()) {
+ $arElementFounded = $this->findElement($row['ekn']);
+ if ($row['errors'] != '') {
+ continue;
+ }
+
+ if (!empty($arElementFounded)) {
+ $this->updateElement($arElementFounded['ID'], $row);
+ } else {
+ $this->addElement($row);
+ }
+
+ $count++;
+ }
+
+ $this->countItems = $count;
+ }
+
+
+ private function findElement($ekn)
+ {
+ /*
+ * Некоторая логика по поиску сущестующих элементов каталога
+ */
+ return $arElement;
+ }
+
+
+ private function updateElement($ID, $row)
+ {
+ /*
+ * Некоторая логика по подготовке данных для обновления существующего элемента каталога
+ */
+ $this->obElement->SetPropertyValues($ID, $this->iblockIdCatalog, $arPropertyValues);
+ $this->obElement->Update($arFields);
+ }
+
+
+ private function addElement($row): bool
+ {
+ /*
+ * Некоторая логика по подготовке данных для добавления нового элемента в каталог
+ */
+ $resId = $this->obElement->Add($arFields);
+
+ return $resId ? true : false;
+ }
+}
diff --git a/before/before.svg b/before/before.svg
new file mode 100644
index 00000000..d3e539bb
--- /dev/null
+++ b/before/before.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file