├── LICENSE ├── README.md ├── composer.json ├── resources ├── fias_entities.php ├── fias_entities_default.php └── xsd │ ├── AS_ADDR_OBJ_2_251_01_04_01_01.xsd │ ├── AS_ADDR_OBJ_DIVISION_2_251_19_04_01_01.xsd │ ├── AS_ADDR_OBJ_TYPES_2_251_03_04_01_01.xsd │ ├── AS_ADM_HIERARCHY_2_251_04_04_01_01.xsd │ ├── AS_APARTMENTS_2_251_05_04_01_01.xsd │ ├── AS_APARTMENT_TYPES_2_251_07_04_01_01.xsd │ ├── AS_CARPLACES_2_251_06_04_01_01.xsd │ ├── AS_CHANGE_HISTORY_251_21_04_01_01.xsd │ ├── AS_HOUSES_2_251_08_04_01_01.xsd │ ├── AS_HOUSE_TYPES_2_251_13_04_01_01.xsd │ ├── AS_MUN_HIERARCHY_2_251_10_04_01_01.xsd │ ├── AS_NORMATIVE_DOCS_2_251_11_04_01_01.xsd │ ├── AS_NORMATIVE_DOCS_KINDS_2_251_09_04_01_01.xsd │ ├── AS_NORMATIVE_DOCS_TYPES_2_251_16_04_01_01.xsd │ ├── AS_OBJECT_LEVELS_2_251_12_04_01_01.xsd │ ├── AS_OPERATION_TYPES_2_251_14_04_01_01.xsd │ ├── AS_PARAM_2_251_02_04_01_01.xsd │ ├── AS_PARAM_TYPES_2_251_20_04_01_01.xsd │ ├── AS_REESTR_OBJECTS_2_251_22_04_01_01.xsd │ ├── AS_ROOMS_2_251_15_04_01_01.xsd │ ├── AS_ROOM_TYPES_2_251_17_04_01_01.xsd │ └── AS_STEADS_2_251_18_04_01_01.xsd └── src ├── Downloader ├── Downloader.php └── DownloaderImpl.php ├── EntityDescriptor ├── BaseEntityDescriptor.php └── EntityDescriptor.php ├── EntityField ├── BaseEntityField.php └── EntityField.php ├── EntityManager ├── BaseEntityManager.php └── EntityManager.php ├── EntityRegistry ├── AbstractEntityRegistry.php ├── ArrayEntityRegistry.php ├── EntityRegistry.php └── PhpArrayFileRegistry.php ├── Exception ├── DownloaderException.php ├── EntityRegistryException.php ├── Exception.php ├── FiasInformerException.php ├── HttpTransportException.php ├── PipeException.php ├── StatusCheckerException.php ├── StorageException.php ├── TaskException.php ├── UnpackerException.php └── XmlException.php ├── FiasFile ├── FiasFile.php ├── FiasFileFactory.php └── FiasFileImpl.php ├── FiasFileSelector ├── FiasFileSelector.php ├── FiasFileSelectorArchive.php ├── FiasFileSelectorComposite.php └── FiasFileSelectorDir.php ├── FiasInformer ├── FiasInformer.php ├── FiasInformerImpl.php ├── FiasInformerResponse.php ├── FiasInformerResponseFactory.php └── FiasInformerResponseImpl.php ├── FiasStatusChecker ├── FiasStatusChecker.php ├── FiasStatusCheckerImpl.php ├── FiasStatusCheckerResult.php ├── FiasStatusCheckerResultForService.php ├── FiasStatusCheckerResultForServiceImpl.php ├── FiasStatusCheckerResultImpl.php ├── FiasStatusCheckerService.php └── FiasStatusCheckerStatus.php ├── FilesDispatcher ├── FilesDispatcher.php └── FilesDispatcherImpl.php ├── Filter ├── Filter.php └── RegexpFilter.php ├── Helper ├── ArrayHelper.php ├── FiasLink.php ├── IdHelper.php └── PathHelper.php ├── HttpTransport ├── HttpTransport.php ├── HttpTransportCurl.php ├── HttpTransportResponse.php ├── HttpTransportResponseFactory.php └── HttpTransportResponseImpl.php ├── Pipeline ├── Pipe │ ├── ArrayPipe.php │ └── Pipe.php ├── State │ ├── ArrayState.php │ ├── State.php │ └── StateParameter.php └── Task │ ├── ApplyNestedPipelineToFileTask.php │ ├── CheckStatusTask.php │ ├── CleanupFilesUnpacked.php │ ├── CleanupTask.php │ ├── DataAbstractTask.php │ ├── DataDeleteTask.php │ ├── DataInsertTask.php │ ├── DataUpsertTask.php │ ├── DownloadTask.php │ ├── InformDeltaTask.php │ ├── InformFullTask.php │ ├── LoggableTask.php │ ├── LoggableTaskTrait.php │ ├── PrepareFolderTask.php │ ├── ProcessSwitchTask.php │ ├── SaveFiasFilesTask.php │ ├── SelectFilesToProceedTask.php │ ├── Task.php │ ├── TruncateTask.php │ ├── UnpackTask.php │ ├── VersionGetTask.php │ └── VersionSetTask.php ├── Serializer ├── FiasFileDenormalizer.php ├── FiasFileNormalizer.php ├── FiasFilterEmptyStringsDenormalizer.php ├── FiasNameConverter.php ├── FiasPipelineStateDenormalizer.php ├── FiasPipelineStateNormalizer.php ├── FiasSerializer.php ├── FiasSerializerContextParam.php ├── FiasSerializerFormat.php ├── FiasUnpackerFileDenormalizer.php └── FiasUnpackerFileNormalizer.php ├── Storage ├── CompositeStorage.php └── Storage.php ├── Unpacker ├── Unpacker.php ├── UnpackerFile.php ├── UnpackerFileFactory.php ├── UnpackerFileImpl.php └── UnpackerZip.php ├── VersionManager └── VersionManager.php └── XmlReader ├── BaseXmlReader.php └── XmlReader.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 liquetsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FiasComponent 2 | ============= 3 | 4 | [![Latest Stable Version](https://poser.pugx.org/liquetsoft/fias-component/v)](https://packagist.org/packages/liquetsoft/fias-component) 5 | [![Total Downloads](https://poser.pugx.org/liquetsoft/fias-component/downloads)](https://packagist.org/packages/liquetsoft/fias-component) 6 | [![License](https://poser.pugx.org/liquetsoft/fias-component/license)](https://packagist.org/packages/liquetsoft/fias-component) 7 | [![Build Status](https://github.com/liquetsoft/fias-component/workflows/liquetsoft_fias/badge.svg)](https://github.com/liquetsoft/fias-component/actions?query=workflow%3A%22liquetsoft_fias%22) 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liquetsoft/fias-component", 3 | "description": "FIAS database parser for php.", 4 | "type": "library", 5 | "keywords": ["php", "fias"], 6 | "license": "MIT", 7 | "require": { 8 | "php": ">=8.2", 9 | "ext-libxml": "*", 10 | "ext-xmlreader": "*", 11 | "ext-zip": "*", 12 | "ext-json": "*", 13 | "symfony/serializer": "^5.0|^6.0|^7.0", 14 | "symfony/property-access": "^5.0|^6.0|^7.0", 15 | "symfony/property-info": "^5.0|^6.0|^7.0", 16 | "symfony/process": "^5.0|^6.0|^7.0", 17 | "psr/log": "^1.0|^2.0|^3.0", 18 | "marvin255/file-system-helper": "^5.0|^6.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^11.0", 22 | "fakerphp/faker": "^1.7", 23 | "friendsofphp/php-cs-fixer": "^3.0", 24 | "vimeo/psalm": "^5.0|^6.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Liquetsoft\\Fias\\Component\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Liquetsoft\\Fias\\Component\\Tests\\": "tests/src", 34 | "Liquetsoft\\Fias\\Component\\Tests\\Mock\\": "tests/mock", 35 | "Liquetsoft\\Fias\\Component\\Generator\\": "generator" 36 | } 37 | }, 38 | "scripts": { 39 | "test": "vendor/bin/phpunit --configuration phpunit.xml.dist --display-deprecations --display-phpunit-deprecations", 40 | "coverage": "vendor/bin/phpunit --configuration phpunit.xml.dist --coverage-html=tests/coverage", 41 | "fixer": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --allow-risky=yes -vvv", 42 | "linter": [ 43 | "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --dry-run --stop-on-violation --allow-risky=yes -vvv", 44 | "vendor/bin/psalm --show-info=true --php-version=$(php -r \"echo phpversion();\")" 45 | ], 46 | "xsd": "php -f generator/download_entities.php", 47 | "entities": "php -f generator/generate_entities.php && vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --allow-risky=yes -q" 48 | }, 49 | "repositories": [ 50 | { 51 | "type": "git", 52 | "url": "https://github.com/liquetsoft/fias-component" 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /resources/fias_entities_default.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'fields' => [ 8 | 'CHANGEID' => [ 9 | 'isPrimary' => true, 10 | ], 11 | ], 12 | ], 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/xsd/AS_ADDR_OBJ_DIVISION_2_251_19_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями по операциям переподчинения 7 | 8 | 9 | 10 | 11 | 12 | Сведения по операциям переподчинения 13 | 14 | 15 | 16 | 17 | Уникальный идентификатор записи. Ключевое поле 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Родительский ID 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Дочерний ID 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ID изменившей транзакции 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /resources/xsd/AS_ADDR_OBJ_TYPES_2_251_03_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Состав и структура файла со сведениями по типам адресных объектов 6 | 7 | 8 | 9 | 10 | 11 | Сведения по типам адресных объектов 12 | 13 | 14 | 15 | 16 | Идентификатор записи 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Уровень адресного объекта 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Краткое наименование типа объекта 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Полное наименование типа объекта 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Описание 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Дата внесения (обновления) записи 68 | 69 | 70 | 71 | 72 | Начало действия записи 73 | 74 | 75 | 76 | 77 | Окончание действия записи 78 | 79 | 80 | 81 | 82 | Статус активности 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /resources/xsd/AS_APARTMENT_TYPES_2_251_07_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями по типам помещений 7 | 8 | 9 | 10 | 11 | 12 | Сведения по типам помещений 13 | 14 | 15 | 16 | 17 | Идентификатор типа (ключ) 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Наименование 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Краткое наименование 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Описание 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Дата внесения (обновления) записи 61 | 62 | 63 | 64 | 65 | Начало действия записи 66 | 67 | 68 | 69 | 70 | Окончание действия записи 71 | 72 | 73 | 74 | 75 | Статус активности 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /resources/xsd/AS_CARPLACES_2_251_06_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями по машино-местам 7 | 8 | 9 | 10 | 11 | 12 | Сведения по машино-местам 13 | 14 | 15 | 16 | 17 | Уникальный идентификатор записи. Ключевое поле 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Глобальный уникальный идентификатор объекта типа INTEGER 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Глобальный уникальный идентификатор адресного объекта типа UUID 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ID изменившей транзакции 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Номер машиноместа 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Статус действия над записью – причина появления записи 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Идентификатор записи связывания с предыдущей исторической записью 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Идентификатор записи связывания с последующей исторической записью 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Дата внесения (обновления) записи 99 | 100 | 101 | 102 | 103 | Начало действия записи 104 | 105 | 106 | 107 | 108 | Окончание действия записи 109 | 110 | 111 | 112 | 113 | Статус актуальности адресного объекта ФИАС 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | Признак действующего адресного объекта 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /resources/xsd/AS_CHANGE_HISTORY_251_21_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями по истории изменений 7 | 8 | 9 | 10 | 11 | 12 | Сведения по истории изменений 13 | 14 | 15 | 16 | 17 | ID изменившей транзакции 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Уникальный ID объекта 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Уникальный ID изменившей транзакции (GUID) 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Тип операции 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ID документа 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Дата изменения 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /resources/xsd/AS_HOUSE_TYPES_2_251_13_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями по типам домов 7 | 8 | 9 | 10 | 11 | 12 | Сведения по типам домов 13 | 14 | 15 | 16 | 17 | Идентификатор 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Наименование 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Краткое наименование 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Описание 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Дата внесения (обновления) записи 61 | 62 | 63 | 64 | 65 | Начало действия записи 66 | 67 | 68 | 69 | 70 | Окончание действия записи 71 | 72 | 73 | 74 | 75 | Статус активности 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /resources/xsd/AS_MUN_HIERARCHY_2_251_10_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями по иерархии в муниципальном делении 7 | 8 | 9 | 10 | 11 | 12 | Сведения по иерархии в муниципальном делении 13 | 14 | 15 | 16 | 17 | Уникальный идентификатор записи. Ключевое поле 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Глобальный уникальный идентификатор адресного объекта 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Идентификатор родительского объекта 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ID изменившей транзакции 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Код ОКТМО 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | Идентификатор записи связывания с предыдущей исторической записью 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Идентификатор записи связывания с последующей исторической записью 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | Дата внесения (обновления) записи 90 | 91 | 92 | 93 | 94 | Начало действия записи 95 | 96 | 97 | 98 | 99 | Окончание действия записи 100 | 101 | 102 | 103 | 104 | Признак действующего адресного объекта 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | Материализованный путь к объекту (полная иерархия) 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /resources/xsd/AS_NORMATIVE_DOCS_2_251_11_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями о нормативных документах, являющихся основанием присвоения адресному элементу наименования 7 | 8 | 9 | 10 | 11 | 12 | Сведения о нормативном документе, являющемся основанием присвоения адресному элементу наименования 13 | 14 | 15 | 16 | 17 | Уникальный идентификатор документа 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Наименование документа 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Дата документа 39 | 40 | 41 | 42 | 43 | Номер документа 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | Тип документа 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | Вид документа 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | Дата обновления 75 | 76 | 77 | 78 | 79 | Наименование органа создвшего нормативный документ 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | Номер государственной регистрации 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Дата государственной регистрации 102 | 103 | 104 | 105 | 106 | Дата вступления в силу нормативного документа 107 | 108 | 109 | 110 | 111 | Комментарий 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /resources/xsd/AS_NORMATIVE_DOCS_KINDS_2_251_09_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями по видам нормативных документов 7 | 8 | 9 | 10 | 11 | 12 | Сведения по видам нормативных документов 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Идентификатор записи 23 | 24 | 25 | 26 | 27 | Наименование 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /resources/xsd/AS_NORMATIVE_DOCS_TYPES_2_251_16_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями по типам нормативных документов 7 | 8 | 9 | 10 | 11 | 12 | Сведения по типам нормативных документов 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Идентификатор записи 23 | 24 | 25 | 26 | 27 | Наименование 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Дата начала действия записи 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Дата окончания действия записи 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /resources/xsd/AS_OBJECT_LEVELS_2_251_12_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Состав и структура файла со сведениями по уровням адресных объектов 6 | 7 | 8 | 9 | 10 | 11 | Сведения по уровням адресных объектов 12 | 13 | 14 | 15 | 16 | Уникальный идентификатор записи. Ключевое поле. Номер уровня объекта 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Наименование 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Краткое наименование 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Дата внесения (обновления) записи 49 | 50 | 51 | 52 | 53 | Начало действия записи 54 | 55 | 56 | 57 | 58 | Окончание действия записи 59 | 60 | 61 | 62 | 63 | Признак действующего адресного объекта 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /resources/xsd/AS_OPERATION_TYPES_2_251_14_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями по статусу действия 7 | 8 | 9 | 10 | 11 | 12 | Сведения по статусу действия 13 | 14 | 15 | 16 | 17 | Идентификатор статуса (ключ) 18 | 19 | 20 | 21 | 22 | Наименование 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Краткое наименование 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Описание 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Дата внесения (обновления) записи 56 | 57 | 58 | 59 | 60 | Начало действия записи 61 | 62 | 63 | 64 | 65 | Окончание действия записи 66 | 67 | 68 | 69 | 70 | Статус активности 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /resources/xsd/AS_PARAM_2_251_02_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями о классификаторе параметров адресообразующих элементов и объектов недвижимости 7 | 8 | 9 | 10 | 11 | 12 | Сведения о классификаторе параметров адресообразующих элементов и объектов недвижимости 13 | 14 | 15 | 16 | 17 | Идентификатор записи 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Глобальный уникальный идентификатор адресного объекта 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ID изменившей транзакции 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ID завершившей транзакции 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | Тип параметра 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Значение параметра 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Дата внесения (обновления) записи 79 | 80 | 81 | 82 | 83 | Дата начала действия записи 84 | 85 | 86 | 87 | 88 | Дата окончания действия записи 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /resources/xsd/AS_PARAM_TYPES_2_251_20_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла с типами параметров 7 | 8 | 9 | 10 | 11 | 12 | Сведения по типу параметра 13 | 14 | 15 | 16 | 17 | Идентификатор типа параметра (ключ) 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Наименование 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Краткое наименование 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Описание 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Дата внесения (обновления) записи 61 | 62 | 63 | 64 | 65 | Начало действия записи 66 | 67 | 68 | 69 | 70 | Окончание действия записи 71 | 72 | 73 | 74 | 75 | Статус активности 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /resources/xsd/AS_REESTR_OBJECTS_2_251_22_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Состав и структура файла со сведениями о реестре GUID объектов 7 | 8 | 9 | 10 | 11 | 12 | Сведения об адресном элементе в части его идентификаторов 13 | 14 | 15 | 16 | 17 | Уникальный идентификатор объекта 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Дата создания 28 | 29 | 30 | 31 | 32 | ID изменившей транзакции 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Уровень объекта 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Дата обновления 53 | 54 | 55 | 56 | 57 | GUID объекта 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Признак действующего объекта (1 - действующий, 0 - не действующий) 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /resources/xsd/AS_ROOM_TYPES_2_251_17_04_01_01.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Состав и структура файла со сведениями по типам комнат 6 | 7 | 8 | 9 | 10 | 11 | Сведения по типам комнат 12 | 13 | 14 | 15 | 16 | Идентификатор типа (ключ) 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Наименование 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | Краткое наименование 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Описание 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Дата внесения (обновления) записи 60 | 61 | 62 | 63 | 64 | Начало действия записи 65 | 66 | 67 | 68 | 69 | Окончание действия записи 70 | 71 | 72 | 73 | 74 | Статус активности 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/Downloader/Downloader.php: -------------------------------------------------------------------------------- 1 | transport->head($url); 37 | } catch (\Throwable $e) { 38 | throw DownloaderException::wrap($e); 39 | } 40 | 41 | $fileHandler = $this->openLocalFile($localFile, self::FILE_MODE_NEW); 42 | for ($try = 1; $try <= $this->maxAttempts; ++$try) { 43 | try { 44 | $response = $this->transport->download($url, $fileHandler, $bytesFrom ?? null, $bytesTo ?? null); 45 | if ($response->isOk()) { 46 | break; 47 | } else { 48 | throw DownloaderException::create("Url '%s' returned status: %s", $url, $response->getStatusCode()); 49 | } 50 | } catch (\Throwable $e) { 51 | if ($try === $this->maxAttempts) { 52 | throw DownloaderException::wrap($e); 53 | } 54 | } finally { 55 | $this->closeLocalFile($fileHandler); 56 | } 57 | // php запоминает описания файлов, поэтому чтобы получить 58 | // реальный размер, нужно очистить кэш 59 | clearstatcache(true, $localFile->getRealPath()); 60 | // если уже скачали какие-то данные и сервер поддерживает Range, 61 | // пробуем продолжить с того же места 62 | $fileSize = filesize($localFile->getRealPath()); 63 | if ($fileSize !== 0 && $fileSize !== false && $headResponse->isRangeSupported()) { 64 | $fileHandler = $this->openLocalFile($localFile, self::FILE_MODE_ADD); 65 | $bytesFrom = $fileSize; 66 | $bytesTo = $headResponse->getContentLength() - 1; 67 | } else { 68 | $fileHandler = $this->openLocalFile($localFile, self::FILE_MODE_NEW); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Открывает локальный файл, в который будет запись, и возвращает его ресурс. 75 | * 76 | * @return resource 77 | */ 78 | private function openLocalFile(\SplFileInfo $localFile, string $mode) 79 | { 80 | $hLocal = @fopen($localFile->getPathname(), $mode); 81 | 82 | if (empty($hLocal)) { 83 | throw DownloaderException::create("Can't open local file for writing: %s", $localFile->getPathname()); 84 | } 85 | 86 | if (!flock($hLocal, \LOCK_EX)) { 87 | throw DownloaderException::create('Unable to obtain lock for file: %s', $localFile->getPathname()); 88 | } 89 | 90 | return $hLocal; 91 | } 92 | 93 | /** 94 | * Правильно закрывает ресурс локального файла. 95 | * 96 | * @param resource $hLocal 97 | */ 98 | private function closeLocalFile($hLocal): void 99 | { 100 | flock($hLocal, \LOCK_UN); 101 | fclose($hLocal); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/EntityDescriptor/EntityDescriptor.php: -------------------------------------------------------------------------------- 1 | name = $this->extractStringFromOptions($p, 'name', true); 38 | $this->description = $this->extractStringFromOptions($p, 'description'); 39 | $this->type = $this->extractStringFromOptions($p, 'type', true); 40 | $this->subType = $this->extractStringFromOptions($p, 'subType'); 41 | $this->length = isset($p['length']) ? (int) $p['length'] : null; 42 | $this->isNullable = !empty($p['isNullable']); 43 | $this->isPrimary = !empty($p['isPrimary']); 44 | $this->isIndex = !empty($p['isIndex']); 45 | $this->isPartition = !empty($p['isPartition']); 46 | 47 | if ($this->isPrimary && $this->isIndex) { 48 | throw new \InvalidArgumentException( 49 | 'Field is already primary, no needs to set index.' 50 | ); 51 | } 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | #[\Override] 58 | public function getName(): string 59 | { 60 | return $this->name; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | #[\Override] 67 | public function getDescription(): string 68 | { 69 | return $this->description; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | #[\Override] 76 | public function getType(): string 77 | { 78 | return $this->type; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | #[\Override] 85 | public function getSubType(): string 86 | { 87 | return $this->subType; 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | #[\Override] 94 | public function getLength(): ?int 95 | { 96 | return $this->length; 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | #[\Override] 103 | public function isNullable(): bool 104 | { 105 | return $this->isNullable; 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | #[\Override] 112 | public function isPrimary(): bool 113 | { 114 | return $this->isPrimary; 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | #[\Override] 121 | public function isIndex(): bool 122 | { 123 | return $this->isIndex; 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | #[\Override] 130 | public function isPartition(): bool 131 | { 132 | return $this->isPartition; 133 | } 134 | 135 | /** 136 | * Получает указанную строку из набора опций. 137 | * 138 | * @throws \InvalidArgumentException 139 | */ 140 | protected function extractStringFromOptions(array $options, string $name, bool $required = false): string 141 | { 142 | $return = ''; 143 | 144 | if (!isset($options[$name]) && $required) { 145 | throw new \InvalidArgumentException( 146 | "Option with key '{$name}' is required for EntityField." 147 | ); 148 | } elseif (isset($options[$name])) { 149 | $return = trim((string) $options[$name]); 150 | } 151 | 152 | return $return; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/EntityField/EntityField.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | protected array $bindings; 23 | 24 | /** 25 | * @param array $bindings 26 | * 27 | * @throws \InvalidArgumentException 28 | */ 29 | public function __construct(EntityRegistry $registry, array $bindings) 30 | { 31 | $this->registry = $registry; 32 | 33 | $this->bindings = []; 34 | foreach ($bindings as $entityName => $className) { 35 | $normalizedEntityName = $this->normalizeEntityName($entityName); 36 | $normalizedClassName = $this->normalizeClassName($className); 37 | if ($normalizedClassName === '') { 38 | throw new \InvalidArgumentException( 39 | "There is no class for {$entityName} entity name." 40 | ); 41 | } 42 | $this->bindings[$normalizedEntityName] = $normalizedClassName; 43 | } 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | * 49 | * @throws EntityRegistryException 50 | */ 51 | #[\Override] 52 | public function getDescriptorByEntityName(string $entityName): ?EntityDescriptor 53 | { 54 | $normalizedEntityName = $this->normalizeEntityName($entityName); 55 | $return = null; 56 | 57 | if (isset($this->bindings[$normalizedEntityName]) && $this->registry->hasDescriptor($normalizedEntityName)) { 58 | $return = $this->registry->getDescriptor($normalizedEntityName); 59 | } 60 | 61 | return $return; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | #[\Override] 68 | public function getClassByDescriptor(EntityDescriptor $descriptor): ?string 69 | { 70 | $normalizedEntityName = $this->normalizeEntityName($descriptor->getName()); 71 | 72 | return $this->bindings[$normalizedEntityName] ?? null; 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | * 78 | * @throws EntityRegistryException 79 | */ 80 | #[\Override] 81 | public function getDescriptorByInsertFile(string $insertFileName): ?EntityDescriptor 82 | { 83 | $return = null; 84 | 85 | foreach ($this->bindings as $entityName => $className) { 86 | $descriptor = $this->getDescriptorByEntityName($entityName); 87 | if ($descriptor && $descriptor->isFileNameFitsXmlInsertFileMask($insertFileName)) { 88 | $return = $descriptor; 89 | break; 90 | } 91 | } 92 | 93 | return $return; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | * 99 | * @throws EntityRegistryException 100 | */ 101 | #[\Override] 102 | public function getDescriptorByDeleteFile(string $insertFileName): ?EntityDescriptor 103 | { 104 | $return = null; 105 | 106 | foreach ($this->bindings as $entityName => $className) { 107 | $descriptor = $this->getDescriptorByEntityName($entityName); 108 | if ($descriptor && $descriptor->isFileNameFitsXmlDeleteFileMask($insertFileName)) { 109 | $return = $descriptor; 110 | break; 111 | } 112 | } 113 | 114 | return $return; 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | * 120 | * @throws EntityRegistryException 121 | */ 122 | #[\Override] 123 | public function getDescriptorByClass(string $className): ?EntityDescriptor 124 | { 125 | $normalizedClassName = $this->normalizeClassName($className); 126 | $entityName = null; 127 | 128 | foreach ($this->bindings as $bindEntity => $bindClass) { 129 | if ($normalizedClassName === $bindClass) { 130 | $entityName = $bindEntity; 131 | break; 132 | } 133 | } 134 | 135 | return $entityName !== null && $entityName !== '' 136 | ? $this->getDescriptorByEntityName($entityName) 137 | : null; 138 | } 139 | 140 | /** 141 | * {@inheritdoc} 142 | * 143 | * @throws EntityRegistryException 144 | */ 145 | #[\Override] 146 | public function getDescriptorByObject(mixed $object): ?EntityDescriptor 147 | { 148 | $return = null; 149 | 150 | if (\is_object($object)) { 151 | $return = $this->getDescriptorByClass(\get_class($object)); 152 | } 153 | 154 | return $return; 155 | } 156 | 157 | /** 158 | * {@inheritdoc} 159 | */ 160 | #[\Override] 161 | public function getBindedClasses(): array 162 | { 163 | return array_unique(array_values($this->bindings)); 164 | } 165 | 166 | /** 167 | * Приводит имя сущности к единообразному виду. 168 | */ 169 | protected function normalizeEntityName(string $entityName): string 170 | { 171 | return strtolower(trim($entityName)); 172 | } 173 | 174 | /** 175 | * Приводит имя класса к единообразному виду. 176 | */ 177 | protected function normalizeClassName(string $className): string 178 | { 179 | return trim($className, '\\ '); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/EntityManager/EntityManager.php: -------------------------------------------------------------------------------- 1 | normalizeEntityName($entityName); 35 | 36 | foreach ($this->getDescriptors() as $descriptor) { 37 | $normalizedDescriptorName = $this->normalizeEntityName($descriptor->getName()); 38 | if ($normalizedName === $normalizedDescriptorName) { 39 | $return = true; 40 | break; 41 | } 42 | } 43 | 44 | return $return; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | #[\Override] 51 | public function getDescriptor(string $entityName): EntityDescriptor 52 | { 53 | $return = null; 54 | $normalizedName = $this->normalizeEntityName($entityName); 55 | 56 | foreach ($this->getDescriptors() as $descriptor) { 57 | $normalizedDescriptorName = $this->normalizeEntityName($descriptor->getName()); 58 | if ($normalizedName === $normalizedDescriptorName) { 59 | $return = $descriptor; 60 | break; 61 | } 62 | } 63 | 64 | if (!$return) { 65 | throw new \InvalidArgumentException( 66 | "Can't fin entity with name '{$entityName}'." 67 | ); 68 | } 69 | 70 | return $return; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | #[\Override] 77 | public function getDescriptors(): array 78 | { 79 | if ($this->registry === null) { 80 | try { 81 | $this->registry = $this->createRegistry(); 82 | } catch (\Throwable $e) { 83 | throw new EntityRegistryException($e->getMessage(), 0, $e); 84 | } 85 | } 86 | 87 | return $this->registry; 88 | } 89 | 90 | /** 91 | * Приводит имена сущностей к единообразному виду. 92 | */ 93 | public function normalizeEntityName(string $name): string 94 | { 95 | return trim(strtolower($name)); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/EntityRegistry/ArrayEntityRegistry.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | protected array $arrayRegistry; 18 | 19 | /** 20 | * @throws \InvalidArgumentException 21 | */ 22 | public function __construct(array $registry) 23 | { 24 | $this->arrayRegistry = []; 25 | 26 | foreach ($registry as $key => $descriptor) { 27 | if (!($descriptor instanceof EntityDescriptor)) { 28 | throw new \InvalidArgumentException( 29 | "Item with key {$key} must be an " . EntityDescriptor::class . ' instance.' 30 | ); 31 | } 32 | $this->arrayRegistry[] = $descriptor; 33 | } 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | #[\Override] 40 | protected function createRegistry(): array 41 | { 42 | return $this->arrayRegistry; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/EntityRegistry/EntityRegistry.php: -------------------------------------------------------------------------------- 1 | pathToSource = $pathToSource ?? PathHelper::resource('fias_entities.php'); 26 | } 27 | 28 | /** 29 | * {@inheritDoc} 30 | * 31 | * @psalm-suppress UnresolvableInclude 32 | */ 33 | #[\Override] 34 | protected function createRegistry(): array 35 | { 36 | $registry = []; 37 | 38 | $fileData = include $this->checkAndReturnPath(); 39 | $fileData = \is_array($fileData) ? $fileData : []; 40 | 41 | foreach ($fileData as $key => $entity) { 42 | if (!\is_array($entity)) { 43 | continue; 44 | } 45 | $entity['name'] = $key; 46 | $registry[] = $this->createEntityDescriptor($entity); 47 | } 48 | 49 | return $registry; 50 | } 51 | 52 | /** 53 | * Создает сущность из массива, который был записан в файле. 54 | * 55 | * @param mixed[] $entity 56 | * 57 | * @throws \InvalidArgumentException 58 | */ 59 | private function createEntityDescriptor(array $entity): EntityDescriptor 60 | { 61 | if (!empty($entity['fields']) && \is_array($entity['fields'])) { 62 | $fields = []; 63 | foreach ($entity['fields'] as $key => $field) { 64 | if (!\is_array($field)) { 65 | continue; 66 | } 67 | $field['name'] = $key; 68 | $fields[] = $this->createEntityField($field); 69 | } 70 | $entity['fields'] = $fields; 71 | } 72 | 73 | return new BaseEntityDescriptor($entity); 74 | } 75 | 76 | /** 77 | * Создает поле из массива, который был записан в файле. 78 | * 79 | * @throws \InvalidArgumentException 80 | */ 81 | private function createEntityField(array $field): EntityField 82 | { 83 | return new BaseEntityField($field); 84 | } 85 | 86 | /** 87 | * Проверяет, что путь до файла с описанием сущностей существует и возвращает его. 88 | */ 89 | private function checkAndReturnPath(): string 90 | { 91 | $path = trim($this->pathToSource); 92 | 93 | if (!file_exists($path) || !is_readable($path)) { 94 | $message = \sprintf( 95 | "File '%s' for php entity registry must exists and be readable.", 96 | $this->pathToSource 97 | ); 98 | throw new \InvalidArgumentException($message); 99 | } 100 | 101 | $extension = pathinfo($path, \PATHINFO_EXTENSION); 102 | if ($extension !== 'php') { 103 | $message = \sprintf( 104 | "File '%s' must has 'php' extension, got '%s'.", 105 | $this->pathToSource, 106 | $extension 107 | ); 108 | throw new \InvalidArgumentException($message); 109 | } 110 | 111 | return $path; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Exception/DownloaderException.php: -------------------------------------------------------------------------------- 1 | trim((string) $param), $params); 27 | 28 | array_unshift($params, $message); 29 | 30 | /** @var string */ 31 | $compiledMessage = \call_user_func_array('sprintf', $params); 32 | 33 | return new static($compiledMessage); 34 | } 35 | 36 | /** 37 | * Фабричный метод, который оборачивает готовое исключение другим. 38 | * 39 | * @psalm-suppress PossiblyInvalidArgument 40 | */ 41 | public static function wrap(\Throwable $e): static 42 | { 43 | return $e instanceof static ? $e : new static($e->getMessage(), $e->getCode(), $e); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Exception/FiasInformerException.php: -------------------------------------------------------------------------------- 1 | getPathname(), $file->getSize()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/FiasFile/FiasFileImpl.php: -------------------------------------------------------------------------------- 1 | size; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | #[\Override] 33 | public function getName(): string 34 | { 35 | return $this->name; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function __toString(): string 42 | { 43 | return $this->name; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/FiasFileSelector/FiasFileSelector.php: -------------------------------------------------------------------------------- 1 | unpacker->isArchive($source); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | #[\Override] 38 | public function selectFiles(\SplFileInfo $source): array 39 | { 40 | $selectedFiles = []; 41 | foreach ($this->unpacker->getListOfFiles($source) as $file) { 42 | if ($this->isFileAllowedForSelect($file)) { 43 | $selectedFiles[] = $file; 44 | } 45 | } 46 | 47 | return $selectedFiles; 48 | } 49 | 50 | /** 51 | * Проверяет, что файл подходит для обработки. 52 | */ 53 | private function isFileAllowedForSelect(UnpackerFile $file): bool 54 | { 55 | $fileName = pathinfo($file->getName(), \PATHINFO_BASENAME); 56 | 57 | return $file->getSize() > 0 58 | && $this->filter?->test($file) !== false 59 | && ( 60 | $this->isFileAllowedToInsert($fileName) 61 | || $this->isFileAllowedToDelete($fileName) 62 | ); 63 | } 64 | 65 | /** 66 | * Проверяет нужно ли файл обрабатывать для создания и обновления в рамках данного процесса. 67 | */ 68 | private function isFileAllowedToInsert(string $file): bool 69 | { 70 | $descriptor = $this->entityManager->getDescriptorByInsertFile($file); 71 | 72 | return $descriptor !== null && $this->entityManager->getClassByDescriptor($descriptor) !== null; 73 | } 74 | 75 | /** 76 | * Проверяет нужно ли файл обрабатывать для удаления в рамках данного процесса. 77 | */ 78 | private function isFileAllowedToDelete(string $file): bool 79 | { 80 | $descriptor = $this->entityManager->getDescriptorByDeleteFile($file); 81 | 82 | return $descriptor !== null && $this->entityManager->getClassByDescriptor($descriptor) !== null; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/FiasFileSelector/FiasFileSelectorComposite.php: -------------------------------------------------------------------------------- 1 | $filesSelectors 15 | */ 16 | public function __construct(private readonly iterable $filesSelectors) 17 | { 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | #[\Override] 24 | public function supportSource(\SplFileInfo $source): bool 25 | { 26 | foreach ($this->filesSelectors as $selector) { 27 | if ($selector->supportSource($source)) { 28 | return true; 29 | } 30 | } 31 | 32 | return false; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | #[\Override] 39 | public function selectFiles(\SplFileInfo $source): array 40 | { 41 | foreach ($this->filesSelectors as $selector) { 42 | if ($selector->supportSource($source)) { 43 | return $selector->selectFiles($source); 44 | } 45 | } 46 | 47 | return []; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/FiasFileSelector/FiasFileSelectorDir.php: -------------------------------------------------------------------------------- 1 | isDir(); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | #[\Override] 38 | public function selectFiles(\SplFileInfo $source): array 39 | { 40 | $selectedFiles = []; 41 | $iterator = $this->fs->createDirectoryIterator($source); 42 | foreach ($iterator as $object) { 43 | if ($object->isFile() && $this->isFileAllowedForSelect($object)) { 44 | $selectedFiles[] = FiasFileFactory::createFromSplFileInfo($object); 45 | } 46 | } 47 | 48 | return $selectedFiles; 49 | } 50 | 51 | /** 52 | * Проверяет, что файл подходит для обработки. 53 | */ 54 | private function isFileAllowedForSelect(\SplFileInfo $file): bool 55 | { 56 | return $file->getSize() > 0 57 | && $this->filter?->test($file) !== false 58 | && ( 59 | $this->isFileAllowedToInsert($file->getBasename()) 60 | || $this->isFileAllowedToDelete($file->getBasename()) 61 | ); 62 | } 63 | 64 | /** 65 | * Проверяет нужно ли файл обрабатывать для создания и обновления в рамках данного процесса. 66 | */ 67 | private function isFileAllowedToInsert(string $file): bool 68 | { 69 | $descriptor = $this->entityManager->getDescriptorByInsertFile($file); 70 | 71 | return $descriptor !== null && $this->entityManager->getClassByDescriptor($descriptor) !== null; 72 | } 73 | 74 | /** 75 | * Проверяет нужно ли файл обрабатывать для удаления в рамках данного процесса. 76 | */ 77 | private function isFileAllowedToDelete(string $file): bool 78 | { 79 | $descriptor = $this->entityManager->getDescriptorByDeleteFile($file); 80 | 81 | return $descriptor !== null && $this->entityManager->getClassByDescriptor($descriptor) !== null; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/FiasInformer/FiasInformer.php: -------------------------------------------------------------------------------- 1 | transport = $transport; 29 | $this->endpointAll = $endpointAll instanceof FiasLink ? $endpointAll->value : $endpointAll; 30 | $this->endpointLast = $endpointLast instanceof FiasLink ? $endpointLast->value : $endpointLast; 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | #[\Override] 37 | public function getLatestVersion(): FiasInformerResponse 38 | { 39 | return FiasInformerResponseFactory::createFromJson( 40 | $this->query($this->endpointLast) 41 | ); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | #[\Override] 48 | public function getNextVersion(int|FiasInformerResponse $currentVersion): ?FiasInformerResponse 49 | { 50 | $currentVersionId = $currentVersion instanceof FiasInformerResponse ? $currentVersion->getVersion() : $currentVersion; 51 | if ($currentVersionId <= 0) { 52 | throw FiasInformerException::create('Version number must be more that 0'); 53 | } 54 | 55 | $deltas = $this->getAllVersions(); 56 | foreach ($deltas as $delta) { 57 | if ($delta->getVersion() > $currentVersionId) { 58 | return $delta; 59 | } 60 | } 61 | 62 | return null; 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | #[\Override] 69 | public function getAllVersions(): array 70 | { 71 | $data = $this->query($this->endpointAll); 72 | 73 | $list = []; 74 | foreach ($data as $item) { 75 | if (\is_array($item)) { 76 | $list[] = FiasInformerResponseFactory::createFromJson($item); 77 | } 78 | } 79 | 80 | usort( 81 | $list, 82 | fn (FiasInformerResponse $a, FiasInformerResponse $b): int => $a->getVersion() - $b->getVersion() 83 | ); 84 | 85 | return $list; 86 | } 87 | 88 | /** 89 | * Отправляет запрос и проверяет ответ. 90 | */ 91 | private function query(string $url): array 92 | { 93 | try { 94 | $response = $this->transport->get($url); 95 | } catch (\Throwable $e) { 96 | throw FiasInformerException::wrap($e); 97 | } 98 | 99 | if (!$response->isOk()) { 100 | throw FiasInformerException::create("Informer '%s' responsed with bad status: %s", $url, $response->getStatusCode()); 101 | } 102 | 103 | try { 104 | $data = $response->getJsonPayload(); 105 | } catch (\Throwable $e) { 106 | throw FiasInformerException::wrap($e); 107 | } 108 | 109 | if (!\is_array($data)) { 110 | throw FiasInformerException::create('Response from informer is malformed'); 111 | } 112 | 113 | return $data; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/FiasInformer/FiasInformerResponse.php: -------------------------------------------------------------------------------- 1 | checkUrl($fullUrl); 27 | } 28 | if ($deltaUrl !== '') { 29 | $this->checkUrl($deltaUrl); 30 | } 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | #[\Override] 37 | public function getVersion(): int 38 | { 39 | return $this->version; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | #[\Override] 46 | public function getFullUrl(): string 47 | { 48 | return $this->fullUrl; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | #[\Override] 55 | public function getDeltaUrl(): string 56 | { 57 | return $this->deltaUrl; 58 | } 59 | 60 | /** 61 | * Выбрасывает исключение, если ссылка задана в неверном формате. 62 | */ 63 | private function checkUrl(string $url): void 64 | { 65 | if (!preg_match('#https?://.+#', $url)) { 66 | throw FiasInformerException::create("String '%s' is not an url", $url); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/FiasStatusChecker/FiasStatusChecker.php: -------------------------------------------------------------------------------- 1 | getFiasInformerStatus(), 30 | $this->getFileServerStatus(), 31 | ]; 32 | 33 | foreach ($statusesPerServices as $status) { 34 | if ($status->getStatus() !== FiasStatusCheckerStatus::AVAILABLE) { 35 | return new FiasStatusCheckerResultImpl(FiasStatusCheckerStatus::NOT_AVAILABLE, $statusesPerServices); 36 | } 37 | } 38 | 39 | return new FiasStatusCheckerResultImpl(FiasStatusCheckerStatus::AVAILABLE, $statusesPerServices); 40 | } 41 | 42 | /** 43 | * Возвращает состояние сервиса информирования. 44 | */ 45 | private function getFiasInformerStatus(): FiasStatusCheckerResultForService 46 | { 47 | $status = FiasStatusCheckerStatus::AVAILABLE; 48 | $service = FiasStatusCheckerService::INFORMER; 49 | $reason = ''; 50 | 51 | try { 52 | $this->informer->getLatestVersion(); 53 | } catch (\Throwable $e) { 54 | $status = FiasStatusCheckerStatus::NOT_AVAILABLE; 55 | $reason = $e->getMessage(); 56 | } 57 | 58 | return new FiasStatusCheckerResultForServiceImpl($status, $service, $reason); 59 | } 60 | 61 | /** 62 | * Возвращает состояние файлового сервера. 63 | */ 64 | private function getFileServerStatus(): FiasStatusCheckerResultForService 65 | { 66 | $service = FiasStatusCheckerService::FILE_SERVER; 67 | 68 | try { 69 | $url = $this->informer->getLatestVersion()->getFullUrl(); 70 | } catch (\Throwable $e) { 71 | return new FiasStatusCheckerResultForServiceImpl( 72 | FiasStatusCheckerStatus::UNKNOWN, 73 | $service, 74 | 'Informer is unavailable' 75 | ); 76 | } 77 | 78 | try { 79 | $response = $this->transport->head($url); 80 | if (!$response->isOk()) { 81 | throw HttpTransportException::create( 82 | "Can't reach file '%s', bad status '%s'", 83 | $url, 84 | $response->getStatusCode() 85 | ); 86 | } 87 | } catch (\Throwable $e) { 88 | return new FiasStatusCheckerResultForServiceImpl( 89 | FiasStatusCheckerStatus::NOT_AVAILABLE, 90 | $service, 91 | $e->getMessage() 92 | ); 93 | } 94 | 95 | return new FiasStatusCheckerResultForServiceImpl(FiasStatusCheckerStatus::AVAILABLE, $service); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/FiasStatusChecker/FiasStatusCheckerResult.php: -------------------------------------------------------------------------------- 1 | status; 26 | } 27 | 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | #[\Override] 32 | public function getService(): FiasStatusCheckerService 33 | { 34 | return $this->service; 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | */ 40 | #[\Override] 41 | public function getReason(): string 42 | { 43 | return $this->reason; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/FiasStatusChecker/FiasStatusCheckerResultImpl.php: -------------------------------------------------------------------------------- 1 | resultStatus; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | #[\Override] 34 | public function getPerServiceStatuses(): array 35 | { 36 | return $this->perServiceStatuses; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | #[\Override] 43 | public function canProceed(): bool 44 | { 45 | return $this->resultStatus === FiasStatusCheckerStatus::AVAILABLE; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/FiasStatusChecker/FiasStatusCheckerService.php: -------------------------------------------------------------------------------- 1 | sortBySizeDesc($files); 26 | 27 | $threadsFiles = []; 28 | $filesSizeInThreads = array_fill(0, $processesCount, 0); 29 | $relatedItems = []; 30 | 31 | foreach ($files as $file) { 32 | $entity = $this->getEntityNameToInsert($file); 33 | if ($entity !== null) { 34 | $region = $this->getRegionNumberForFile($file); 35 | $thread = $relatedItems["{$region}_{$entity}"] ?? $this->getThreadIdWithMinSize($filesSizeInThreads); 36 | $relatedItems["{$region}_{$entity}"] = $thread; 37 | $filesSizeInThreads[$thread] += $file->getSize(); 38 | $threadsFiles[$thread][] = $file; 39 | } 40 | } 41 | 42 | foreach ($files as $file) { 43 | $entity = $this->getEntityNameToDelete($file); 44 | if ($entity !== null) { 45 | $region = $this->getRegionNumberForFile($file); 46 | $thread = $relatedItems["{$region}_{$entity}"] ?? $this->getThreadIdWithMinSize($filesSizeInThreads); 47 | $relatedItems["{$region}_{$entity}"] = $thread; 48 | $filesSizeInThreads[$thread] += $file->getSize(); 49 | $threadsFiles[$thread][] = $file; 50 | } 51 | } 52 | 53 | return $threadsFiles; 54 | } 55 | 56 | /** 57 | * Сортирует файлы по размеру по убыванию, чтобы было легче балансировать количество данных в потоках. 58 | * 59 | * @param FiasFile[] $files 60 | * 61 | * @return FiasFile[] 62 | */ 63 | private function sortBySizeDesc(array $files): array 64 | { 65 | usort( 66 | $files, 67 | fn (FiasFile $a, FiasFile $b): int => $b->getSize() <=> $a->getSize() 68 | ); 69 | 70 | return $files; 71 | } 72 | 73 | /** 74 | * Возвращает имя сущности, к которой привязан указанный файл, если такая сущность указана. 75 | */ 76 | private function getEntityNameToInsert(FiasFile $file): ?string 77 | { 78 | $fileName = pathinfo($file->getName(), \PATHINFO_BASENAME); 79 | 80 | return $this->entityManager->getDescriptorByInsertFile($fileName)?->getName(); 81 | } 82 | 83 | /** 84 | * Возвращает имя сущности, к которой привязан указанный файл, если такая сущность указана. 85 | */ 86 | private function getEntityNameToDelete(FiasFile $file): ?string 87 | { 88 | $fileName = pathinfo($file->getName(), \PATHINFO_BASENAME); 89 | 90 | return $this->entityManager->getDescriptorByDeleteFile($fileName)?->getName(); 91 | } 92 | 93 | /** 94 | * Возвращает номер региона для указанного имени файла. 95 | */ 96 | private function getRegionNumberForFile(FiasFile $file): ?int 97 | { 98 | if (preg_match("#^/?(\d+)/.*#", $file->getName(), $matches)) { 99 | return (int) $matches[1]; 100 | } 101 | 102 | return null; 103 | } 104 | 105 | /** 106 | * Возвращает идентификатор трэда с наименьшим размером файлов. 107 | * 108 | * @param array $filesSizeInThreads 109 | */ 110 | private function getThreadIdWithMinSize(array $filesSizeInThreads): int 111 | { 112 | $minId = 0; 113 | $minSize = null; 114 | foreach ($filesSizeInThreads as $id => $size) { 115 | if ($size === 0) { 116 | return $id; 117 | } 118 | if ($minSize === null || $minSize > $size) { 119 | $minId = $id; 120 | $minSize = $size; 121 | } 122 | } 123 | 124 | return $minId; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Filter/Filter.php: -------------------------------------------------------------------------------- 1 | __toString(); 30 | } else { 31 | $message = 'This filter supports only strings or objects that can be coverted to strings.'; 32 | throw new \InvalidArgumentException($message); 33 | } 34 | 35 | if (empty($this->regexps)) { 36 | return true; 37 | } 38 | 39 | $isTested = false; 40 | foreach ($this->regexps as $regexp) { 41 | if ($regexp !== '' && preg_match($regexp, $testData)) { 42 | $isTested = true; 43 | break; 44 | } 45 | } 46 | 47 | return $isTested; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Helper/ArrayHelper.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function getHeaders(): array; 23 | 24 | /** 25 | * Возвращает правду, если ответ был успешным. 26 | */ 27 | public function isOk(): bool; 28 | 29 | /** 30 | * Возвращает длину тела ответ. 31 | */ 32 | public function getContentLength(): int; 33 | 34 | /** 35 | * Возвращает правду, если сервер поддерживает докачку файла. 36 | */ 37 | public function isRangeSupported(): bool; 38 | 39 | /** 40 | * Возвращает тело ответа. 41 | */ 42 | public function getPayload(): string; 43 | 44 | /** 45 | * Возвращает декодированное из json тело ответа. 46 | */ 47 | public function getJsonPayload(): mixed; 48 | } 49 | -------------------------------------------------------------------------------- /src/HttpTransport/HttpTransportResponseFactory.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | private readonly array $headers; 18 | 19 | public function __construct( 20 | private readonly int $statusCode, 21 | array $headers = [], 22 | private readonly string $payload = '', 23 | private readonly mixed $payloadJson = null, 24 | ) { 25 | $this->headers = $this->prepareHeaders($headers); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | #[\Override] 32 | public function getStatusCode(): int 33 | { 34 | return $this->statusCode; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | #[\Override] 41 | public function getHeaders(): array 42 | { 43 | return $this->headers; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | #[\Override] 50 | public function isOk(): bool 51 | { 52 | return $this->statusCode >= 200 && $this->statusCode < 300; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | #[\Override] 59 | public function getContentLength(): int 60 | { 61 | return (int) ($this->headers['content-length'] ?? 0); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | #[\Override] 68 | public function isRangeSupported(): bool 69 | { 70 | return $this->getContentLength() > 0 && ($this->headers['accept-ranges'] ?? '') === 'bytes'; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | #[\Override] 77 | public function getPayload(): string 78 | { 79 | return $this->payload; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | #[\Override] 86 | public function getJsonPayload(): mixed 87 | { 88 | return $this->payloadJson; 89 | } 90 | 91 | /** 92 | * Подготавливает заголовки для использования. 93 | * 94 | * @return array 95 | */ 96 | private function prepareHeaders(array $headers): array 97 | { 98 | $preparedHeaders = []; 99 | foreach ($headers as $name => $value) { 100 | $name = str_replace('_', '-', strtolower(trim((string) $name))); 101 | $value = strtolower(trim((string) $value)); 102 | $preparedHeaders[$name] = $value; 103 | } 104 | 105 | return $preparedHeaders; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Pipeline/Pipe/Pipe.php: -------------------------------------------------------------------------------- 1 | */ 14 | private readonly array $parameters = [], 15 | private readonly bool $isCompleted = false, 16 | ) { 17 | foreach ($this->parameters as $name => $value) { 18 | if (!StateParameter::tryFrom($name)) { 19 | throw new \InvalidArgumentException("'{$name}' isn't found in " . StateParameter::class); 20 | } 21 | } 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | #[\Override] 28 | public function setParameter(StateParameter $parameter, mixed $parameterValue): self 29 | { 30 | $parameters = $this->parameters; 31 | $parameters[$parameter->value] = $parameterValue; 32 | 33 | return new self($parameters, $this->isCompleted); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | #[\Override] 40 | public function complete(): self 41 | { 42 | return new self($this->parameters, true); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | #[\Override] 49 | public function getParameter(StateParameter $parameter, mixed $default = null): mixed 50 | { 51 | return $this->parameters[$parameter->value] ?? $default; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | #[\Override] 58 | public function getParameterInt(StateParameter $parameter, int $default = 0): int 59 | { 60 | return (int) $this->getParameter($parameter, $default); 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | #[\Override] 67 | public function getParameterString(StateParameter $parameter, string $default = ''): string 68 | { 69 | return (string) $this->getParameter($parameter, $default); 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | #[\Override] 76 | public function isCompleted(): bool 77 | { 78 | return $this->isCompleted; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Pipeline/State/State.php: -------------------------------------------------------------------------------- 1 | getParameter(StateParameter::FILES_TO_PROCEED); 30 | if (!\is_array($files)) { 31 | throw TaskException::create("'%s' param must be an array", StateParameter::FILES_TO_PROCEED->value); 32 | } 33 | 34 | foreach ($files as $file) { 35 | $fileState = $state->setParameter( 36 | StateParameter::FILES_TO_PROCEED, 37 | $this->createNewFilesArrayFromFile($file) 38 | ); 39 | $this->pipe->run($fileState); 40 | } 41 | 42 | return $state; 43 | } 44 | 45 | /** 46 | * Превращает файл в новый массив для StateParameter::FILES_TO_PROCEED. 47 | */ 48 | private function createNewFilesArrayFromFile(mixed $file): array 49 | { 50 | if (($file instanceof FiasFile) && !($file instanceof UnpackerFile)) { 51 | return [ 52 | $file->getName(), 53 | ]; 54 | } 55 | 56 | return [ 57 | $file, 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Pipeline/Task/CheckStatusTask.php: -------------------------------------------------------------------------------- 1 | checker->check(); 30 | 31 | if (!$status->canProceed()) { 32 | $message = 'There are some troubles on the FIAS side. Please try again later'; 33 | $this->log( 34 | LogLevel::ERROR, 35 | $message, 36 | [ 37 | 'services_statuses' => $status->getPerServiceStatuses(), 38 | ] 39 | ); 40 | throw new StatusCheckerException($message); 41 | } 42 | 43 | return $state; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Pipeline/Task/CleanupFilesUnpacked.php: -------------------------------------------------------------------------------- 1 | getParameter(StateParameter::FILES_UNPACKED); 30 | if (!\is_array($files)) { 31 | return $state; 32 | } 33 | 34 | foreach ($files as $file) { 35 | $this->fs->removeIfExists((string) $file); 36 | $this->log( 37 | LogLevel::INFO, 38 | 'Item is cleaned up', 39 | [ 40 | 'path' => $file, 41 | ] 42 | ); 43 | } 44 | 45 | return $state; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Pipeline/Task/CleanupTask.php: -------------------------------------------------------------------------------- 1 | getParameterString(StateParameter::PATH_TO_DOWNLOAD_FILE), 31 | $state->getParameterString(StateParameter::PATH_TO_EXTRACT_FOLDER), 32 | ]; 33 | 34 | foreach ($toRemove as $path) { 35 | if ($path !== '') { 36 | $this->fs->removeIfExists($path); 37 | $this->log( 38 | LogLevel::INFO, 39 | 'Item is cleaned up', 40 | [ 41 | 'path' => $path, 42 | ] 43 | ); 44 | } 45 | } 46 | 47 | return $state; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Pipeline/Task/DataAbstractTask.php: -------------------------------------------------------------------------------- 1 | getParameter(StateParameter::FILES_TO_PROCEED); 56 | $allFiles = \is_array($allFiles) ? $allFiles : []; 57 | 58 | foreach ($allFiles as $file) { 59 | $fileInfo = new \SplFileInfo((string) $file); 60 | if ($descriptor = $this->getFileDescriptor($fileInfo)) { 61 | $this->processFile($fileInfo, $descriptor); 62 | } 63 | } 64 | 65 | return $state; 66 | } 67 | 68 | /** 69 | * Обрабатывает указанный файл. 70 | * 71 | * @throws TaskException 72 | * @throws StorageException 73 | * @throws XmlException 74 | */ 75 | protected function processFile(\SplFileInfo $fileInfo, EntityDescriptor $descriptor): void 76 | { 77 | $entityClass = $this->entityManager->getClassByDescriptor($descriptor); 78 | if ($entityClass !== null && $entityClass !== '') { 79 | $this->processDataFromFile($fileInfo, $descriptor->getXmlPath(), $entityClass); 80 | gc_collect_cycles(); 81 | } 82 | } 83 | 84 | /** 85 | * Обрабатывает данные из файла и передает в хранилище. 86 | * 87 | * @throws TaskException 88 | * @throws StorageException 89 | * @throws XmlException 90 | */ 91 | protected function processDataFromFile(\SplFileInfo $fileInfo, string $xpath, string $entityClass): void 92 | { 93 | $this->log( 94 | LogLevel::INFO, 95 | "Start processing '{$fileInfo->getRealPath()}' file for '{$entityClass}' entity", 96 | [ 97 | 'entity' => $entityClass, 98 | 'path' => $fileInfo->getRealPath(), 99 | ] 100 | ); 101 | 102 | $total = 0; 103 | $this->xmlReader->open($fileInfo, $xpath); 104 | $this->storage->start(); 105 | try { 106 | foreach ($this->xmlReader as $xml) { 107 | $item = $this->deserializeXmlStringToObject($xml, $entityClass); 108 | if (!$this->storage->supports($item)) { 109 | continue; 110 | } 111 | $this->processItem($item); 112 | unset($item, $xml); 113 | ++$total; 114 | } 115 | } finally { 116 | $this->storage->stop(); 117 | $this->xmlReader->close(); 118 | } 119 | 120 | $this->log( 121 | LogLevel::INFO, 122 | "Completed processing '{$fileInfo->getRealPath()}' file for '{$entityClass}' entity. {$total} items processed", 123 | [ 124 | 'entity' => $entityClass, 125 | 'path' => $fileInfo->getRealPath(), 126 | ] 127 | ); 128 | } 129 | 130 | /** 131 | * Преобразует xml строку в объект указанного класса. 132 | * 133 | * @throws TaskException 134 | */ 135 | protected function deserializeXmlStringToObject(?string $xml, string $entityClass): object 136 | { 137 | try { 138 | $entity = $this->serializer->deserialize( 139 | $xml, 140 | $entityClass, 141 | FiasSerializerFormat::XML->value, 142 | [ 143 | FiasSerializerContextParam::FIAS_FLAG->value => true, 144 | FiasSerializerContextParam::FIAS_ENTITY->value => $entityClass, 145 | XmlEncoder::TYPE_CAST_ATTRIBUTES => false, 146 | ] 147 | ); 148 | } catch (\Throwable $e) { 149 | throw new TaskException( 150 | message: "Deserialization error while deserialization of '{$xml}' string to object with '{$entityClass}' class", 151 | previous: $e 152 | ); 153 | } 154 | 155 | if (!\is_object($entity)) { 156 | throw new TaskException('Serializer must returns an object instance'); 157 | } 158 | 159 | return $entity; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Pipeline/Task/DataDeleteTask.php: -------------------------------------------------------------------------------- 1 | entityManager->getDescriptorByDeleteFile($file->getBasename()); 22 | } 23 | 24 | /** 25 | * {@inheritDoc} 26 | * 27 | * @throws StorageException 28 | */ 29 | #[\Override] 30 | protected function processItem(object $item): void 31 | { 32 | $this->storage->delete($item); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Pipeline/Task/DataInsertTask.php: -------------------------------------------------------------------------------- 1 | entityManager->getDescriptorByInsertFile($file->getBasename()); 22 | } 23 | 24 | /** 25 | * {@inheritDoc} 26 | * 27 | * @throws StorageException 28 | */ 29 | #[\Override] 30 | protected function processItem(object $item): void 31 | { 32 | $this->storage->insert($item); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Pipeline/Task/DataUpsertTask.php: -------------------------------------------------------------------------------- 1 | entityManager->getDescriptorByInsertFile($file->getBasename()); 23 | } 24 | 25 | /** 26 | * {@inheritDoc} 27 | * 28 | * @throws StorageException 29 | */ 30 | #[\Override] 31 | protected function processItem(object $item): void 32 | { 33 | $this->storage->upsert($item); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Pipeline/Task/DownloadTask.php: -------------------------------------------------------------------------------- 1 | getParameterString(StateParameter::FIAS_VERSION_ARCHIVE_URL); 32 | if ($url === '') { 33 | throw TaskException::create("Source url isn't set"); 34 | } 35 | 36 | $filePath = $state->getParameterString(StateParameter::PATH_TO_DOWNLOAD_FILE); 37 | if ($filePath === '') { 38 | throw TaskException::create("Destination path isn't set"); 39 | } 40 | 41 | $this->log( 42 | LogLevel::INFO, 43 | 'Downloading file', 44 | [ 45 | 'url' => $url, 46 | 'destination' => $filePath, 47 | ] 48 | ); 49 | 50 | $this->downloader->download($url, new \SplFileInfo($filePath)); 51 | 52 | $this->log( 53 | LogLevel::INFO, 54 | 'File downloaded', 55 | [ 56 | 'url' => $url, 57 | 'destination' => $filePath, 58 | ] 59 | ); 60 | 61 | return $state->setParameter(StateParameter::PATH_TO_SOURCE, $filePath); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Pipeline/Task/InformDeltaTask.php: -------------------------------------------------------------------------------- 1 | getParameterInt(StateParameter::FIAS_VERSION_NUMBER); 32 | if ($version <= 0) { 33 | throw new TaskException('Version parameter must exist and be greater than zero'); 34 | } 35 | 36 | $info = $this->informer->getNextVersion($version); 37 | if ($info === null) { 38 | $state = $state->complete(); 39 | $this->log( 40 | LogLevel::INFO, 41 | "Current version '{$version}' is up to date", 42 | [ 43 | 'current_version' => $version, 44 | ] 45 | ); 46 | } else { 47 | $this->log( 48 | LogLevel::INFO, 49 | "Current version of FIAS is '{$version}', next version is '{$info->getVersion()}' and can be downloaded from '{$info->getDeltaUrl()}'", 50 | [ 51 | 'current_version' => $version, 52 | 'next_version' => $info->getVersion(), 53 | 'url' => $info->getDeltaUrl(), 54 | ] 55 | ); 56 | $state = $state->setParameter(StateParameter::FIAS_NEXT_VERSION_NUMBER, $info->getVersion()) 57 | ->setParameter(StateParameter::FIAS_NEXT_VERSION_FULL_URL, $info->getFullUrl()) 58 | ->setParameter(StateParameter::FIAS_NEXT_VERSION_DELTA_URL, $info->getDeltaUrl()) 59 | ->setParameter(StateParameter::FIAS_VERSION_ARCHIVE_URL, $info->getDeltaUrl()); 60 | } 61 | 62 | return $state; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Pipeline/Task/InformFullTask.php: -------------------------------------------------------------------------------- 1 | informer->getLatestVersion(); 30 | 31 | $this->log( 32 | LogLevel::INFO, 33 | "Full version of FIAS is '{$info->getVersion()}' and can be downloaded from '{$info->getFullUrl()}'", 34 | [ 35 | 'next_version' => $info->getVersion(), 36 | 'url' => $info->getFullUrl(), 37 | ] 38 | ); 39 | 40 | return $state->setParameter(StateParameter::FIAS_NEXT_VERSION_NUMBER, $info->getVersion()) 41 | ->setParameter(StateParameter::FIAS_NEXT_VERSION_FULL_URL, $info->getFullUrl()) 42 | ->setParameter(StateParameter::FIAS_NEXT_VERSION_DELTA_URL, $info->getDeltaUrl()) 43 | ->setParameter(StateParameter::FIAS_VERSION_ARCHIVE_URL, $info->getFullUrl()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Pipeline/Task/LoggableTask.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 24 | $this->defaultContext = $defaultContext; 25 | } 26 | 27 | /** 28 | * Записывает сообщение в лог. 29 | */ 30 | public function log(string $logLevel, string $message, array $context = []): void 31 | { 32 | if ($this->logger) { 33 | $context = array_merge($this->defaultContext, $context); 34 | $this->logger->log($logLevel, $message, $context); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Pipeline/Task/PrepareFolderTask.php: -------------------------------------------------------------------------------- 1 | folder = new \SplFileInfo($trimmedFolder); 28 | } 29 | 30 | /** 31 | * {@inheritDoc} 32 | */ 33 | #[\Override] 34 | public function run(State $state): State 35 | { 36 | $this->log(LogLevel::INFO, "Emptying '{$this->folder->getPathname()}' folder"); 37 | $this->fs->mkdirIfNotExist($this->folder); 38 | $this->fs->emptyDir($this->folder); 39 | 40 | $downloadToFile = new \SplFileInfo($this->folder->getRealPath() . '/archive'); 41 | $extractToFolder = new \SplFileInfo($this->folder->getRealPath() . '/extracted'); 42 | 43 | $this->log(LogLevel::INFO, "Creating '{$this->folder->getRealPath()}/extracted' folder"); 44 | $this->fs->mkdir($extractToFolder); 45 | 46 | return $state->setParameter(StateParameter::PATH_TO_DOWNLOAD_FILE, $downloadToFile->getPathname()) 47 | ->setParameter(StateParameter::PATH_TO_EXTRACT_FOLDER, $extractToFolder->getPathname()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Pipeline/Task/SaveFiasFilesTask.php: -------------------------------------------------------------------------------- 1 | moveArchiveTo !== null) { 35 | $movePaths[StateParameter::PATH_TO_DOWNLOAD_FILE->value] = $this->moveArchiveTo; 36 | } 37 | if ($this->moveExtractedTo !== null) { 38 | $movePaths[StateParameter::PATH_TO_EXTRACT_FOLDER->value] = $this->moveExtractedTo; 39 | } 40 | 41 | foreach ($movePaths as $paramName => $moveTo) { 42 | $moveFrom = $state->getParameterString(StateParameter::from($paramName)); 43 | $this->log(LogLevel::INFO, "Moving '{$moveFrom}' to '{$moveTo}'"); 44 | $this->fs->rename($moveFrom, $moveTo); 45 | } 46 | 47 | return $state; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Pipeline/Task/SelectFilesToProceedTask.php: -------------------------------------------------------------------------------- 1 | getParameterString(StateParameter::PATH_TO_SOURCE); 32 | if ($pathToSource === '') { 33 | throw TaskException::create("'%s' is not a valid source", $pathToSource); 34 | } 35 | 36 | $source = new \SplFileInfo($pathToSource); 37 | 38 | $this->log( 39 | LogLevel::INFO, 40 | 'Selecting files from source', 41 | [ 42 | 'path' => $pathToSource, 43 | ] 44 | ); 45 | 46 | if ($this->fiasFileSelector->supportSource($source)) { 47 | $files = $this->fiasFileSelector->selectFiles($source); 48 | } else { 49 | $files = []; 50 | } 51 | 52 | if (\count($files) === 0) { 53 | $this->log( 54 | LogLevel::INFO, 55 | 'No files selected from source', 56 | [ 57 | 'source' => $pathToSource, 58 | 'files' => 0, 59 | ] 60 | ); 61 | 62 | return $state->complete(); 63 | } 64 | 65 | $this->log( 66 | LogLevel::INFO, 67 | 'Files selected from source', 68 | [ 69 | 'source' => $pathToSource, 70 | 'files' => \count($files), 71 | ] 72 | ); 73 | 74 | return $state->setParameter(StateParameter::FILES_TO_PROCEED, $files); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Pipeline/Task/Task.php: -------------------------------------------------------------------------------- 1 | storage->start(); 33 | foreach ($this->entityManager->getBindedClasses() as $className) { 34 | if (!$this->storage->supportsClass($className)) { 35 | continue; 36 | } 37 | $this->log( 38 | LogLevel::INFO, "Truncating '{$className}' entity", 39 | [ 40 | 'entity' => $className, 41 | ] 42 | ); 43 | $this->storage->truncate($className); 44 | } 45 | $this->storage->stop(); 46 | 47 | return $state; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Pipeline/Task/UnpackTask.php: -------------------------------------------------------------------------------- 1 | getParameter(StateParameter::FILES_TO_PROCEED); 32 | if (!\is_array($rawFiles)) { 33 | throw TaskException::create("'%s' param must be an array", StateParameter::FILES_TO_PROCEED->value); 34 | } 35 | 36 | $files = []; 37 | $filesUnpacked = []; 38 | foreach ($rawFiles as $rawFile) { 39 | if ($rawFile instanceof UnpackerFile) { 40 | $files[] = $filesUnpacked[] = $this->unpackFile($rawFile, $state); 41 | } else { 42 | $files[] = $rawFile; 43 | } 44 | } 45 | 46 | return $state->setParameter(StateParameter::FILES_TO_PROCEED, $files) 47 | ->setParameter(StateParameter::FILES_UNPACKED, $filesUnpacked); 48 | } 49 | 50 | /** 51 | * Распаковывает файл и возвращает путь к нему. 52 | */ 53 | private function unpackFile(UnpackerFile $file, State $state): string 54 | { 55 | $destination = $state->getParameterString(StateParameter::PATH_TO_EXTRACT_FOLDER); 56 | if ($destination === '') { 57 | throw new TaskException('Destination path must be a non empty string'); 58 | } else { 59 | $destination = new \SplFileInfo($destination); 60 | } 61 | 62 | $res = $this->unpacker->unpackFile( 63 | $file->getArchiveFile(), 64 | $file->getName(), 65 | $destination 66 | ); 67 | 68 | $this->log( 69 | LogLevel::INFO, 70 | 'File is unpacked', 71 | [ 72 | 'name' => $file->getName(), 73 | 'archive' => $file->getArchiveFile()->getPathname(), 74 | 'destination' => $destination->getPathname(), 75 | 'path' => $res->getRealPath(), 76 | ] 77 | ); 78 | 79 | return $res->getRealPath(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Pipeline/Task/VersionGetTask.php: -------------------------------------------------------------------------------- 1 | versionManager->getCurrentVersion(); 28 | 29 | if ($version === null) { 30 | throw TaskException::create('There is no version of FIAS installed'); 31 | } 32 | 33 | return $state->setParameter(StateParameter::FIAS_VERSION_NUMBER, $version->getVersion()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Pipeline/Task/VersionSetTask.php: -------------------------------------------------------------------------------- 1 | getParameterInt(StateParameter::FIAS_NEXT_VERSION_NUMBER); 28 | 29 | if ($version > 0) { 30 | $version = FiasInformerResponseFactory::create( 31 | $version, 32 | $state->getParameterString(StateParameter::FIAS_NEXT_VERSION_FULL_URL), 33 | $state->getParameterString(StateParameter::FIAS_NEXT_VERSION_DELTA_URL) 34 | ); 35 | $this->versionManager->setCurrentVersion($version); 36 | } 37 | 38 | return $state; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Serializer/FiasFileDenormalizer.php: -------------------------------------------------------------------------------- 1 | true, 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Serializer/FiasFileNormalizer.php: -------------------------------------------------------------------------------- 1 | $data->getName(), 29 | 'size' => $data->getSize(), 30 | ]; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | #[\Override] 37 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 38 | { 39 | return $data instanceof FiasFile; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getSupportedTypes(?string $format): array 46 | { 47 | return [ 48 | FiasFileImpl::class => true, 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Serializer/FiasFilterEmptyStringsDenormalizer.php: -------------------------------------------------------------------------------- 1 | denormalizer = $denormalizer; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | #[\Override] 30 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed 31 | { 32 | if (FiasSerializerFormat::XML->isEqual($format) && \is_array($data)) { 33 | $filteredData = []; 34 | foreach ($data as $key => $value) { 35 | if ($value !== '') { 36 | $filteredData[$key] = $value; 37 | } 38 | } 39 | } else { 40 | $filteredData = $data; 41 | } 42 | 43 | if ($this->denormalizer !== null) { 44 | return $this->denormalizer->denormalize($filteredData, $type, $format, $context); 45 | } else { 46 | return $filteredData; 47 | } 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | #[\Override] 54 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 55 | { 56 | if (FiasSerializerFormat::XML->isEqual($format) && \is_array($data)) { 57 | foreach ($data as $value) { 58 | if ($value === '') { 59 | return true; 60 | } 61 | } 62 | } 63 | 64 | return false; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function getSupportedTypes(?string $format): array 71 | { 72 | if (FiasSerializerFormat::XML->isEqual($format)) { 73 | return [ 74 | '*' => false, 75 | ]; 76 | } 77 | 78 | return []; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Serializer/FiasNameConverter.php: -------------------------------------------------------------------------------- 1 | isEqual($format)) { 23 | return $propertyName; 24 | } 25 | 26 | $propertyName = trim($propertyName); 27 | if (strpos($propertyName, '@') !== 0) { 28 | return '@' . $propertyName; 29 | } 30 | 31 | return $propertyName; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | #[\Override] 38 | public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string 39 | { 40 | if (!FiasSerializerFormat::XML->isEqual($format)) { 41 | return $propertyName; 42 | } 43 | 44 | $propertyName = trim($propertyName); 45 | if (strpos($propertyName, '@') === 0) { 46 | return substr($propertyName, 1); 47 | } 48 | 49 | return $propertyName; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Serializer/FiasPipelineStateDenormalizer.php: -------------------------------------------------------------------------------- 1 | denormalizer = $denormalizer; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | #[\Override] 33 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed 34 | { 35 | /** @var array */ 36 | $parameters = \is_array($data) && isset($data['parameters']) && \is_array($data['parameters']) 37 | ? $data['parameters'] 38 | : []; 39 | 40 | /** @var bool */ 41 | $isCompleted = \is_array($data) && isset($data['isCompleted']) 42 | ? (bool) $data['isCompleted'] 43 | : false; 44 | 45 | $preparedParameters = []; 46 | foreach (StateParameter::cases() as $case) { 47 | $parameterValue = $parameters[$case->value] ?? null; 48 | $preparedValue = $this->prepareStateParameterValue($parameterValue, $format, $context); 49 | if ($preparedValue !== null) { 50 | $preparedParameters[$case->value] = $preparedValue; 51 | } 52 | } 53 | 54 | return new ArrayState($preparedParameters, $isCompleted); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | #[\Override] 61 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 62 | { 63 | return State::class === $type || is_a($type, State::class, true); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function getSupportedTypes(?string $format): array 70 | { 71 | return [ 72 | State::class => true, 73 | ]; 74 | } 75 | 76 | /** 77 | * Приводит значение параметра к состоянию пригодному для использования в объекте. 78 | */ 79 | private function prepareStateParameterValue(mixed $value, ?string $format, array $context): mixed 80 | { 81 | if (\is_array($value) && $this->denormalizer !== null && isset($value['class'], $value['data'])) { 82 | return $this->denormalizer->denormalize( 83 | $value['data'], 84 | (string) $value['class'], 85 | $format, 86 | $context 87 | ); 88 | } elseif (\is_array($value)) { 89 | return array_map( 90 | fn (mixed $item): mixed => $this->prepareStateParameterValue($item, $format, $context), 91 | $value 92 | ); 93 | } 94 | 95 | return $value; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Serializer/FiasPipelineStateNormalizer.php: -------------------------------------------------------------------------------- 1 | normalizer = $normalizer; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | #[\Override] 33 | public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null 34 | { 35 | if (!($data instanceof State)) { 36 | throw new InvalidArgumentException("Instance of '" . State::class . "' is expected"); 37 | } 38 | 39 | $parameters = []; 40 | foreach (StateParameter::cases() as $case) { 41 | $stateValue = $data->getParameter($case, null); 42 | $value = $this->prepareStateParameterValue($stateValue, $format, $context); 43 | if ($value !== null) { 44 | $parameters[$case->value] = $value; 45 | } 46 | } 47 | 48 | return [ 49 | 'parameters' => $parameters, 50 | 'isCompleted' => $data->isCompleted(), 51 | ]; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | #[\Override] 58 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 59 | { 60 | return $data instanceof State; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function getSupportedTypes(?string $format): array 67 | { 68 | return [ 69 | State::class => true, 70 | ]; 71 | } 72 | 73 | /** 74 | * Приводит значение параметра к состоянию пригодному для отправки в json. 75 | */ 76 | private function prepareStateParameterValue(mixed $value, ?string $format, array $context): mixed 77 | { 78 | if (\is_array($value)) { 79 | return array_map( 80 | fn (mixed $item): mixed => $this->prepareStateParameterValue($item, $format, $context), 81 | $value 82 | ); 83 | } elseif (\is_object($value) && $this->normalizer !== null) { 84 | return [ 85 | 'class' => \get_class($value), 86 | 'data' => $this->normalizer->normalize($value, $format, $context), 87 | ]; 88 | } 89 | 90 | return $value; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Serializer/FiasSerializer.php: -------------------------------------------------------------------------------- 1 | |null $normalizers 28 | * @param array|null $encoders 29 | */ 30 | public function __construct(?array $normalizers = null, ?array $encoders = null) 31 | { 32 | if ($normalizers === null) { 33 | $normalizers = [ 34 | new DateTimeNormalizer(), 35 | new FiasPipelineStateNormalizer(), 36 | new FiasPipelineStateDenormalizer(), 37 | new FiasUnpackerFileNormalizer(), 38 | new FiasUnpackerFileDenormalizer(), 39 | new FiasFileNormalizer(), 40 | new FiasFileDenormalizer(), 41 | new ObjectNormalizer( 42 | nameConverter: new FiasNameConverter(), 43 | propertyTypeExtractor: new ReflectionExtractor(), 44 | defaultContext: [ 45 | ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true, 46 | ] 47 | ), 48 | ]; 49 | } 50 | 51 | array_unshift($normalizers, new FiasFilterEmptyStringsDenormalizer()); 52 | 53 | if ($encoders === null) { 54 | $encoders = [ 55 | new XmlEncoder( 56 | [ 57 | XmlEncoder::TYPE_CAST_ATTRIBUTES => false, 58 | ] 59 | ), 60 | new JsonEncoder(), 61 | ]; 62 | } 63 | 64 | $this->nestedSerializer = new Serializer($normalizers, $encoders); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | #[\Override] 71 | public function serialize(mixed $data, string $format, array $context = []): string 72 | { 73 | return $this->nestedSerializer->serialize($data, $format, $context); 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | * 79 | * @psalm-suppress MixedReturnStatement 80 | */ 81 | #[\Override] 82 | public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed 83 | { 84 | return $this->nestedSerializer->deserialize($data, $type, $format, $context); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Serializer/FiasSerializerContextParam.php: -------------------------------------------------------------------------------- 1 | value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Serializer/FiasUnpackerFileDenormalizer.php: -------------------------------------------------------------------------------- 1 | true, 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Serializer/FiasUnpackerFileNormalizer.php: -------------------------------------------------------------------------------- 1 | $data->getArchiveFile()->getPathname(), 29 | 'name' => $data->getName(), 30 | 'index' => $data->getIndex(), 31 | 'size' => $data->getSize(), 32 | ]; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | #[\Override] 39 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 40 | { 41 | return $data instanceof UnpackerFile; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function getSupportedTypes(?string $format): array 48 | { 49 | return [ 50 | UnpackerFileImpl::class => true, 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Storage/CompositeStorage.php: -------------------------------------------------------------------------------- 1 | internalStorages as $storage) { 26 | $storage->start(); 27 | } 28 | } 29 | 30 | /** 31 | * {@inheritDoc} 32 | */ 33 | #[\Override] 34 | public function stop(): void 35 | { 36 | foreach ($this->internalStorages as $storage) { 37 | $storage->stop(); 38 | } 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | #[\Override] 45 | public function supports(object $entity): bool 46 | { 47 | $isSupport = false; 48 | foreach ($this->internalStorages as $storage) { 49 | if ($storage->supports($entity)) { 50 | $isSupport = true; 51 | break; 52 | } 53 | } 54 | 55 | return $isSupport; 56 | } 57 | 58 | /** 59 | * {@inheritDoc} 60 | */ 61 | #[\Override] 62 | public function supportsClass(string $class): bool 63 | { 64 | $isSupport = false; 65 | foreach ($this->internalStorages as $storage) { 66 | if ($storage->supportsClass($class)) { 67 | $isSupport = true; 68 | break; 69 | } 70 | } 71 | 72 | return $isSupport; 73 | } 74 | 75 | /** 76 | * {@inheritDoc} 77 | */ 78 | #[\Override] 79 | public function insert(object $entity): void 80 | { 81 | foreach ($this->internalStorages as $storage) { 82 | if (!$storage->supports($entity)) { 83 | continue; 84 | } 85 | $storage->insert($entity); 86 | } 87 | } 88 | 89 | /** 90 | * {@inheritDoc} 91 | */ 92 | #[\Override] 93 | public function delete(object $entity): void 94 | { 95 | foreach ($this->internalStorages as $storage) { 96 | if (!$storage->supports($entity)) { 97 | continue; 98 | } 99 | $storage->delete($entity); 100 | } 101 | } 102 | 103 | /** 104 | * {@inheritDoc} 105 | */ 106 | #[\Override] 107 | public function upsert(object $entity): void 108 | { 109 | foreach ($this->internalStorages as $storage) { 110 | if (!$storage->supports($entity)) { 111 | continue; 112 | } 113 | $storage->upsert($entity); 114 | } 115 | } 116 | 117 | /** 118 | * {@inheritDoc} 119 | */ 120 | #[\Override] 121 | public function truncate(string $entityClassName): void 122 | { 123 | foreach ($this->internalStorages as $storage) { 124 | if (!$storage->supportsClass($entityClassName)) { 125 | continue; 126 | } 127 | $storage->truncate($entityClassName); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Storage/Storage.php: -------------------------------------------------------------------------------- 1 | 35 | * 36 | * @throws UnpackerException 37 | */ 38 | public function getListOfFiles(\SplFileInfo $archive): iterable; 39 | 40 | /** 41 | * Возвращает правду, если файл является валидным архивом. 42 | */ 43 | public function isArchive(\SplFileInfo $archive): bool; 44 | } 45 | -------------------------------------------------------------------------------- /src/Unpacker/UnpackerFile.php: -------------------------------------------------------------------------------- 1 | archiveFile; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | #[\Override] 35 | public function getIndex(): int 36 | { 37 | return $this->index; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | #[\Override] 44 | public function getSize(): int 45 | { 46 | return $this->size; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | #[\Override] 53 | public function getName(): string 54 | { 55 | return $this->name; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function __toString(): string 62 | { 63 | return $this->name; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Unpacker/UnpackerZip.php: -------------------------------------------------------------------------------- 1 | extractTo($destination->getPathName()); 23 | 24 | if ($res !== true) { 25 | throw UnpackerException::create( 26 | "Can't unpack archive '%s' to '%s'", 27 | $archive->getPathname(), 28 | $destination->getPathname() 29 | ); 30 | } 31 | 32 | return new \SplFileInfo($destination->getRealPath()); 33 | }; 34 | 35 | return $this->runInZipContext($archive, $callback); 36 | } 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | #[\Override] 42 | public function unpackFile(\SplFileInfo $archive, string $fileName, \SplFileInfo $destination): \SplFileInfo 43 | { 44 | $callback = function (\ZipArchive $archiveHandler) use ($archive, $fileName, $destination): \SplFileInfo { 45 | $res = $archiveHandler->extractTo($destination->getPathname(), $fileName); 46 | 47 | if ($res !== true) { 48 | throw UnpackerException::create( 49 | "Can't extract entity '%s' form archive '%s'", 50 | $fileName, 51 | $archive->getPathname() 52 | ); 53 | } 54 | 55 | return new \SplFileInfo($destination->getPathname() . '/' . $fileName); 56 | }; 57 | 58 | return $this->runInZipContext($archive, $callback); 59 | } 60 | 61 | /** 62 | * {@inheritDoc} 63 | */ 64 | #[\Override] 65 | public function getListOfFiles(\SplFileInfo $archive): iterable 66 | { 67 | $archiveHandler = $this->openArchive($archive); 68 | 69 | for ($i = 0; $i < $archiveHandler->numFiles; ++$i) { 70 | $stats = $archiveHandler->statIndex($i); 71 | if (\is_array($stats) && ArrayHelper::extractIntFromArrayByName('crc', $stats) !== 0) { 72 | yield UnpackerFileFactory::createFromZipStats($archive, $stats); 73 | } 74 | } 75 | 76 | $archiveHandler->close(); 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | #[\Override] 83 | public function isArchive(\SplFileInfo $archive): bool 84 | { 85 | try { 86 | return $this->runInZipContext($archive, fn (): bool => true); 87 | } catch (\Throwable $e) { 88 | return false; 89 | } 90 | } 91 | 92 | /** 93 | * Запускает коллбэк в контексте открытого архива. 94 | * 95 | * @template T 96 | * 97 | * @psalm-param callable(\ZipArchive): T $callback 98 | * 99 | * @psalm-return T 100 | */ 101 | private function runInZipContext(\SplFileInfo $path, callable $callback): mixed 102 | { 103 | $archive = $this->openArchive($path); 104 | 105 | try { 106 | $res = $callback($archive); 107 | } catch (\Throwable $e) { 108 | throw UnpackerException::wrap($e); 109 | } finally { 110 | $archive->close(); 111 | } 112 | 113 | return $res; 114 | } 115 | 116 | /** 117 | * Создает обработчик архива и открывает его для чтения. 118 | */ 119 | private function openArchive(\SplFileInfo $path): \ZipArchive 120 | { 121 | $archive = new \ZipArchive(); 122 | 123 | $res = $archive->open($path->getPathName(), \ZipArchive::RDONLY); 124 | if ($res !== true) { 125 | throw UnpackerException::create( 126 | "Can't open '%s' archive, error: %s", 127 | $path->getPathName(), 128 | $res 129 | ); 130 | } 131 | 132 | return $archive; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/VersionManager/VersionManager.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface XmlReader extends \Iterator 16 | { 17 | /** 18 | * Открывает файл на чтение, пытается найти указанный путь, если 19 | * путь найден, то открывает файл и возвращает правду, если не найден, то 20 | * возвращает ложь. 21 | * 22 | * @throws XmlException 23 | */ 24 | public function open(\SplFileInfo $file, string $xpath): bool; 25 | 26 | /** 27 | * Закрывает открытый файл, если такой был. 28 | */ 29 | public function close(): void; 30 | } 31 | --------------------------------------------------------------------------------