├── .gitignore ├── LICENSE.md ├── README.md ├── cli ├── getversion.php ├── init-db.php ├── init.php └── update.php ├── composer.json ├── composer.lock ├── config.ini.dist ├── database ├── 01_tables.sql ├── 02_system_data.sql ├── 03_indexes.sql ├── 04_constraints.sql ├── 05_clean_up.sql └── 06_fakes.sql ├── docs ├── Описание службы получения обновлений.doc └── Сведения о составе информации ФИАС.doc ├── huyak-package.xml ├── import.php.dist ├── phpunit.xml.dist ├── src ├── AddressObjectsImporter.php ├── AddressObjectsUpdater.php ├── AddressStorage.php ├── ApiAction │ ├── AddressCompletion.php │ ├── AddressPostalCode.php │ ├── ApiActionInterface.php │ ├── PlaceCompletion.php │ ├── PostalCodeLocation.php │ └── Validation.php ├── BadRequestException.php ├── Container.php ├── Controller │ └── ApiController.php ├── DataSource │ ├── DataSource.php │ ├── IntervalGenerator.php │ └── XmlReader.php ├── DatabaseDumpManager.php ├── DbHelper.php ├── FileSystem │ ├── Dearchiver.php │ ├── Directory.php │ ├── FileException.php │ └── FileHelper.php ├── HousesImporter.php ├── HousesUpdater.php ├── Importer.php ├── ImporterException.php ├── Loader │ ├── Base.php │ ├── InitLoader.php │ ├── SoapResultWrapper.php │ └── UpdateLoader.php ├── PlaceStorage.php ├── RawDataHelper.php ├── Remover.php └── UpdateLogHelper.php ├── tests ├── AddressObjectsUpdaterTest.php ├── AddressStorageTest.php ├── ApiActionAddressCompletionTest.php ├── ApiActionAddressPostalCodeTest.php ├── ApiActionPlaceCompletionTest.php ├── ApiActionPostalCodeLocationTest.php ├── ApiActionValidationTest.php ├── ApiControllerTest.php ├── DbHelperTest.php ├── DearchiverTest.php ├── DirectoryTest.php ├── HousesUpdaterTest.php ├── ImporterTest.php ├── IntervalGeneratorTest.php ├── RawDataHelperTest.php ├── RemoverTest.php ├── TestAbstract.php ├── UpdateLoaderTest.php ├── UpdateLogHelperTest.php ├── XmlTest.php └── resources │ ├── correctScript.sql │ ├── directoryTest │ ├── AS_ADDROBJ_20131221_5316e71a-a8d8-49df-b17c-66d3a981906a.XML │ ├── AS_DEL_ADDROBJ_20131221_8a0076a7-1f52-4423-8fc6-58dec367832b.XML │ ├── AS_DEL_HOUSE_20131221_ea93b12d-129d-46a0-9cfb-429b64a28873.XML │ └── AS_HOUSE_20131221_bccfd0d0-af7a-49db-8401-df23dc3d2efa.XML │ ├── inCorrectScript.sql │ ├── load.csv │ ├── load.jmx │ └── readerTest.xml └── web └── index.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /logs 3 | /vendor 4 | /config.ini 5 | /tests/file_directory/* 6 | /cache 7 | /phpunit.xml 8 | /import.php 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alex Polev, Mikhail Natarov 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 | FIAS 2 | ==== 3 | 4 | Автодополнение физических адресов по базе ФИАС. 5 | 6 | ## Инициалиация базы данных 7 | 8 | Для инициализации необходимо запустить `init.php`. Поддерживаются 3 режима работы: 9 | 10 | 1. `php cli/init.php` — скачает с сайта ФИАСа последнюю версию базы, распакует и импортирует; 11 | 2. `php cli/init.php /path/to/archive.rar` — распакует и импортирует архив; 12 | 3. `php cli/init.php /path/to/fias_directory` — импортирует уже распакованный архив. 13 | 14 | ## API 15 | 16 | ### `/api/complete` — дополнение адреса 17 | 18 | Пример запроса: 19 | 20 | http://fias.loc/api/complete?pattern=Невск&limit=20 21 | 22 | Ответ: 23 | { 24 | "items": [ 25 | {"title": "г Москва, пр Невский", "is_complete": false, "tags": ["address"]}, 26 | {"title": "г Москва, Невское урочище", "is_complete": false, "tags": ["address"]}, 27 | {"title": "Невский вокзал", "is_complete": true, "tags": ["place", "railway"]} 28 | ] 29 | } 30 | 31 | GET-параметры: 32 | 33 | * `pattern` — дополняемый адрес; 34 | * `limit` — максимальное количество вариантов дополнения в ответе (не более 50, см. `config/config.ini`); 35 | * `regions` — массив номеров регионов для ограничения поиска адресов (см. `database/02_system_data.sql`); 36 | * `max_address_level` — максимальная детализация адреса. 37 | 38 | Максимальная детализация влияет на состав дополняемых вариантов (см. ниже). 39 | 40 | Поля ответа: 41 | 42 | * `items` — массив вариантов дополнения адреса; 43 | * `title` — текст варианта дополнения; 44 | * `is_complete` — `true` для адресов, которым не нужно дальнейшее дополнение (набран точный адрес, либо достигнута максимальная детализация адреса); 45 | * `tags` — присущие варианту ответа свойства (см. раздел теги). 46 | 47 | Параметр `is_complete` помогает отличить точные адреса от промежуточных вариантов дополнения. 48 | Например, если на Невском проспекте есть дом 11, то 49 | при дополнении строки "Санкт-Петербург" для варианта "Санкт-Петербург, Невский проспект" `is_complete=false`, 50 | а дополнении строки "Санкт-Петербург, Невский проспект" для варианта "Санкт-Петербург, Невский проспект 11" `is_complete=true`. 51 | Параметр `is_complete` не учитывает параметры детализации. 52 | 53 | Примеры запросов с ограничением детализации: 54 | 55 | http://fias.loc/api/complete?pattern=Москва, Невский пр.&limit=20 56 | 57 | В ответе будут все варианты вплоть до номеров домов: 58 | { 59 | "items": [ 60 | {"title": "г Москва, пр Невский, 10", "is_complete": true, "tags": ["address"]}, 61 | {"title": "г Москва, пр Невский, 11", "is_complete": true, "tags": ["address"]} 62 | ] 63 | } 64 | 65 | 66 | http://fias.loc/api/complete?pattern=Мос&limit=20&max_address_level=region 67 | 68 | В ответе будут только регионы без дальнейшей детализации: 69 | { 70 | "items": [ 71 | {"title": "г Москва", "is_complete": true, "tags": ["address"]}, 72 | {"title": "обл Московская", "is_complete": true, "tags": ["address"]} 73 | ] 74 | } 75 | 76 | 77 | ### `/api/validate` — валидация элемента 78 | 79 | Пример запроса: 80 | 81 | http://fias.loc/api/validate?pattern=Москва, Невский пр. 82 | 83 | Ответ: 84 | { 85 | "items": [ 86 | { 87 | "is_complete": false, 88 | "tags": ["address"] 89 | }, 90 | { 91 | "is_complete": true, 92 | "tags": ["place", "railway"] 93 | } 94 | ] 95 | } 96 | 97 | GET-параметры: 98 | 99 | * `pattern` — проверяемый адрес. 100 | 101 | Поля ответа: 102 | 103 | * `items` — массив вариантов корректных объектов; 104 | * `is_complete` — `true` для точного адреса (вместе с домом, корпусом и т.п.); 105 | * `tags` — присущие варианту ответа свойства (см. раздел теги): 106 | 107 | 108 | ### `/api/postal_code_location` — получение адреса по почтовому индексу 109 | 110 | Пример запроса: 111 | 112 | http://fias.loc/api/postal_code_location?postal_code=198504 113 | 114 | Ответ: 115 | { 116 | "address_parts": [ 117 | {"title": "г Санкт-Петербург", "address_level": "region"}, 118 | {"title": "р-н Петродворцовый", "address_level": "city_district"} 119 | ] 120 | } 121 | 122 | GET-параметры: 123 | 124 | * `postal_code` — почтовый индекс. 125 | 126 | Поля ответа: 127 | 128 | * `address_parts` — массив частей адреса по уровням детализации; 129 | * `title` — название; 130 | * `address_level` — уровень детализации (район, город и т.п.). 131 | 132 | Если соединить по порядку `title` всех частей адреса в строку, 133 | получится общий префикс для всех адресов по указанному почтовому индексу. 134 | 135 | ### `/api/address_postal_code` — получение почтового индекса по адресу 136 | 137 | Пример запроса: 138 | 139 | http://fias.loc/api/address_postal_code?address=обл Псковская, р-н Новосокольнический, д Мошино 140 | 141 | Ответ: 142 | { 143 | "postal_code": 182200 144 | } 145 | 146 | GET-параметры: 147 | 148 | * `address` — адрес. 149 | 150 | Поля ответа: 151 | 152 | * `postal_code` — почтовый индекс или `null`, если индекс не найден. 153 | 154 | 155 | ### Уровни детализации частей адреса 156 | 157 | 1. `region` — регион: Санкт-Петербург, Московская область, Хабаровский край; 158 | 2. `area` — округ: пока данные отсутствуют, заложено для дальнейшей совместимости с ФИАС, когда ФИАС перенесет часть элементов из region; 159 | 3. `area_district` — район округа/региона: Волжский район, Ломоносовский район, Гатчинский район; 160 | 4. `city` — город: Петергоф, Сосновый бор, Пушкин; 161 | 5. `city_district` — район города: микрорайон № 13, Кировский район, Центральный район; 162 | 6. `settlement` — населенный пункт: поселок Парголово, станция Разлив, поселок Металлострой; 163 | 7. `street` — улица: проспект Косыгина, улица Ярославская, проспект Художников; 164 | 8. `territory` — дополнительная территория: Рябинушка снт (садовое некоммерческое товарищество), Победа гск (гаражно-строительный кооператив); 165 | 9. `sub_territory` — часть дополнительной территории: Садовая улица, 7-я линия; 166 | 10. `building` — конкретный дом (максимальная детализация). 167 | 168 | ### Теги 169 | 170 | * `"address"` — текст найден в ФИАС; 171 | * `"place"` — текст найден в списке places (аэропорты, вокзалы, порты и т.д.); 172 | * `"airport"` — аэропорт; 173 | * `"railway_station"` — вокзал; 174 | * `"bus_terminal"` — автовокзал; 175 | * `"port"` — порт; 176 | * `"airport_terminal"` — терминал аэропорта; 177 | * `"riverside_station"` — речной вокзал. 178 | 179 | 180 | ### Выбор формата 181 | 182 | Для указания формата необходимо добавить его к названию ресурса: 183 | 184 | * `.json` для JSON (по умолчанию) 185 | * `.jsonp` для JSONP. Для JSONP требуется дополнительный GET параметр callback. 186 | 187 | Пример запроса: 188 | 189 | http://fias.loc/api/complete.jsonp?pattern=Невск&limit=20&callback=someFunction 190 | 191 | Ответ: 192 | someFunction( 193 | { 194 | "items": [ 195 | {"title": "г Москва, пр Невский", "is_complete": false, "tags": ["address"]}, 196 | {"title": "г Москва, Невское урочище", "is_complete": false, "tags": ["address"]}, 197 | {"title": "Невский вокзал", "is_complete": true, "tags": ["place", "railway"]} 198 | ] 199 | } 200 | ) 201 | -------------------------------------------------------------------------------- /cli/getversion.php: -------------------------------------------------------------------------------- 1 | getUpdateLoader(); 7 | 8 | echo "Последняя версия: ", $loader->getLastFileInfo()->getVersionId(), "\n"; 9 | -------------------------------------------------------------------------------- /cli/init-db.php: -------------------------------------------------------------------------------- 1 | getDb(); 7 | $dbPath = $container->getDatabaseSourcesDirectory(); 8 | 9 | $db->execute(file_get_contents($dbPath . '/01_tables.sql')); 10 | $db->execute(file_get_contents($dbPath . '/02_system_data.sql')); 11 | $db->execute(file_get_contents($dbPath . '/03_indexes.sql')); 12 | $db->execute(file_get_contents($dbPath . '/04_constraints.sql')); 13 | $db->execute(file_get_contents($dbPath . '/05_clean_up.sql')); 14 | $db->execute(file_get_contents($dbPath . '/06_fakes.sql')); 15 | -------------------------------------------------------------------------------- /cli/init.php: -------------------------------------------------------------------------------- 1 | getDb(); 12 | $dataBaseName = $container->getDatabaseName(); 13 | $logger = $container->getErrorLogger(); 14 | $dbPath = $container->getDatabaseSourcesDirectory(); 15 | 16 | FailureHandler::setup(function ($error) use ($logger) { 17 | $logger->error($error['message'], $error); 18 | fwrite(STDERR, "В процессе инициализации произошла ошибка:\n{$error['message']}\n"); 19 | exit(1); 20 | }); 21 | 22 | set_time_limit(0); 23 | 24 | if ($_SERVER['argc'] == 2) { 25 | $path = $_SERVER['argv']['1']; 26 | if (!is_dir($path)) { 27 | $path = Dearchiver::extract($container->getFileDirectory(), $path); 28 | } 29 | 30 | $directory = new Directory($path); 31 | } else { 32 | $loader = $container->getInitLoader(); 33 | $directory = $loader->load(); 34 | } 35 | // Получаем VersionId поскольку если его не окажется, то сообщение об этом мы получим только в самом конце 15-ти минутного процесса, что не очень приятно. 36 | $versionId = $directory->getVersionId(); 37 | 38 | DbHelper::runFile($dataBaseName, $dbPath . '/01_tables.sql'); 39 | DbHelper::runFile($dataBaseName, $dbPath . '/02_system_data.sql'); 40 | 41 | $addressObjectsConfig = $container->getAddressObjectsImportConfig(); 42 | $addressObjects = new AddressObjectsImporter($db, $addressObjectsConfig['table_name'], $addressObjectsConfig['fields']); 43 | $reader = new XmlReader( 44 | $directory->getAddressObjectFile(), 45 | $addressObjectsConfig['node_name'], 46 | array_keys($addressObjectsConfig['fields']), 47 | $addressObjectsConfig['filters'] 48 | ); 49 | 50 | $addressObjects->import($reader); 51 | 52 | $housesConfig = $container->getHousesImportConfig(); 53 | 54 | if ($housesConfig) { 55 | $houses = new HousesImporter($db, $housesConfig['table_name'], $housesConfig['fields']); 56 | 57 | $reader = new XmlReader( 58 | $directory->getHouseFile(), 59 | $housesConfig['node_name'], 60 | array_keys($housesConfig['fields']) 61 | ); 62 | 63 | $houses->import($reader); 64 | } 65 | 66 | DbHelper::runFile($dataBaseName, $dbPath . '/03_indexes.sql'); 67 | 68 | $addressObjects->modifyDataAfterImport(); 69 | 70 | if ($housesConfig) { 71 | $houses->modifyDataAfterImport(); 72 | } 73 | 74 | DbHelper::runFile($dataBaseName, $dbPath . '/04_constraints.sql'); 75 | DbHelper::runFile($dataBaseName, $dbPath . '/05_clean_up.sql'); 76 | 77 | UpdateLogHelper::addVersionIdToLog($db, $versionId); 78 | -------------------------------------------------------------------------------- /cli/update.php: -------------------------------------------------------------------------------- 1 | getDb(); 12 | $logger = $container->getErrorLogger(); 13 | 14 | FailureHandler::setup(function ($error) use ($logger) { 15 | $logger->error($error['message'], $error); 16 | fwrite(STDERR, "В процессе инициализации произошла ошибка:\n{$error['message']}\n"); 17 | exit(1); 18 | }); 19 | 20 | $db->start(); 21 | 22 | if ($_SERVER['argc'] == 2) { 23 | $path = $_SERVER['argv']['1']; 24 | if (!is_dir($path)) { 25 | $path = Dearchiver::extract($container->getFileDirectory(), $path); 26 | } 27 | 28 | $directory = new Directory($path); 29 | } else { 30 | $loader = $container->getUpdateLoader(); 31 | $directory = $loader->load(); 32 | } 33 | 34 | $oldVersionId = UpdateLogHelper::getLastVersionId($db); 35 | $newVersionId = $directory->getVersionId(); 36 | 37 | if ($newVersionId != ($oldVersionId + 1)) { 38 | throw new \LogicException("Попытка обновления с версии {$oldVersionId} на версию {$newVersionId}."); 39 | } 40 | 41 | $db->execute('SET CONSTRAINTS "address_objects_parent_id_fkey", "houses_parent_id_fkey" DEFERRED'); 42 | 43 | $housesConfig = $container->getHousesImportConfig(); 44 | $addressObjectsConfig = $container->getAddressObjectsImportConfig(); 45 | 46 | $deletedHouseFile = $directory->getDeletedHouseFile(); 47 | if ($deletedHouseFile && $housesConfig) { 48 | $houseRemover = new Remover( 49 | $db, 50 | $housesConfig['table_name'], 51 | $housesConfig['xml_key'], 52 | $housesConfig['database_key'] 53 | ); 54 | $houseRemover->remove( 55 | new XmlReader( 56 | $deletedHouseFile, 57 | $housesConfig['node_name'], 58 | [$housesConfig['primary_key']], 59 | [] 60 | ) 61 | ); 62 | } 63 | 64 | $deletedAddressObjectsFile = $directory->getDeletedAddressObjectFile(); 65 | if ($deletedAddressObjectsFile) { 66 | $addressObjectsRemover = new Remover( 67 | $db, 68 | $addressObjectsConfig['table_name'], 69 | $addressObjectsConfig['xml_key'], 70 | $addressObjectsConfig['database_key'] 71 | ); 72 | $addressObjectsRemover->remove( 73 | new XmlReader( 74 | $deletedAddressObjectsFile, 75 | $addressObjectsConfig['node_name'], 76 | [$addressObjectsConfig['xml_key']], 77 | [] 78 | ) 79 | ); 80 | } 81 | 82 | $addressObjectFields = $addressObjectsConfig['fields']; 83 | $addressObjectFields['PREVID'] = ['name' => 'previous_id', 'type' => 'uuid']; 84 | $addressObjectsUpdater = new AddressObjectsUpdater($db, $addressObjectsConfig['table_name'], $addressObjectFields); 85 | $addressObjectsUpdater->update( 86 | new XmlReader( 87 | $directory->getAddressObjectFile(), 88 | $addressObjectsConfig['node_name'], 89 | array_keys($addressObjectFields), 90 | $addressObjectsConfig['filters'] 91 | ) 92 | ); 93 | 94 | if ($housesConfig) { 95 | $houseFields = $housesConfig['fields']; 96 | $houseFields['PREVID'] = ['name' => 'previous_id', 'type' => 'uuid']; 97 | $housesUpdater = new HousesUpdater($db, $housesConfig['table_name'], $houseFields); 98 | $housesUpdater->update(new XmlReader( 99 | $directory->getHouseFile(), 100 | $housesConfig['node_name'], 101 | array_keys($houseFields), 102 | [] 103 | )); 104 | } 105 | 106 | UpdateLogHelper::addVersionIdToLog($db, $directory->getVersionId()); 107 | 108 | $db->commit(); 109 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stiffbeards/fias", 3 | "minimum-stability": "dev", 4 | "require": { 5 | "php": ">=5.5.0", 6 | "miknatr/grace-dbal": "dev-master", 7 | "miknatr/bravicility": "1.0.0", 8 | "stiffbeards/kola": "dev-master", 9 | "stiffbeards/browser": "dev-master" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "5.2.*" 13 | }, 14 | "autoload": { 15 | "psr-0": { 16 | "": ["src", "tests"] 17 | } 18 | }, 19 | "repositories": [ 20 | { 21 | "type": "vcs", 22 | "url": "https://github.com/miknatr/bravicility.git" 23 | }, 24 | { 25 | "type": "vcs", 26 | "url": "https://bitbucket.org/miknatr/tos-kola.git" 27 | }, 28 | { 29 | "type": "vcs", 30 | "url": "https://github.com/miknatr/browser.git" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /config.ini.dist: -------------------------------------------------------------------------------- 1 | 2 | app.host = fias.loc 3 | app.file_directory = /var/www/upload 4 | app.wsdl_url = http://fias.nalog.ru/WebServices/Public/DownloadService.asmx?WSDL 5 | app.max_completion_limit = 50 6 | 7 | db.uri = pgsql://postgres:1@localhost:5432/fias 8 | 9 | router.skip_cache_invalidation = off 10 | 11 | logging.error_log = logs/error.log 12 | -------------------------------------------------------------------------------- /database/01_tables.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS houses; 2 | CREATE TABLE houses ( 3 | id UUID PRIMARY KEY NOT NULL, 4 | house_id UUID NOT NULL, 5 | address_id UUID DEFAULT NULL, 6 | number VARCHAR, 7 | full_number VARCHAR, 8 | building VARCHAR, 9 | structure VARCHAR, 10 | postal_code INTEGER 11 | ); 12 | COMMENT ON TABLE houses IS 'данные по домам'; 13 | COMMENT ON COLUMN houses.id IS 'идентификационный код записи'; 14 | COMMENT ON COLUMN houses.house_id IS 'идентификационный код дома'; 15 | COMMENT ON COLUMN houses.address_id IS 'идентификационный код адресного объекта'; 16 | COMMENT ON COLUMN houses.number IS 'номер дома'; 17 | COMMENT ON COLUMN houses.building IS 'корпус'; 18 | COMMENT ON COLUMN houses.structure IS 'строение'; 19 | COMMENT ON COLUMN houses.postal_code IS 'индекс'; 20 | 21 | DROP TABLE IF EXISTS address_objects; 22 | CREATE TABLE address_objects ( 23 | id UUID PRIMARY KEY NOT NULL, 24 | address_id UUID NOT NULL, 25 | parent_id UUID DEFAULT NULL, 26 | level INTEGER, 27 | address_level INTEGER, 28 | house_count INTEGER, 29 | next_address_level INTEGER, 30 | title VARCHAR, 31 | full_title VARCHAR, 32 | postal_code INTEGER, 33 | region VARCHAR, 34 | prefix VARCHAR 35 | ); 36 | COMMENT ON TABLE address_objects IS 'данные по адресным объектам(округам, улицам, городам)'; 37 | COMMENT ON COLUMN address_objects.id IS 'идентификационный код записи'; 38 | COMMENT ON COLUMN address_objects.address_id IS 'идентификационный код адресного объекта'; 39 | COMMENT ON COLUMN address_objects.parent_id IS 'идентификационный код родительского адресного объекта'; 40 | COMMENT ON COLUMN address_objects.level IS 'уровень объекта по parent_id (0 для региона и далее по возрастающей'; 41 | COMMENT ON COLUMN address_objects.address_level IS 'уровень объекта по ФИАС'; 42 | COMMENT ON COLUMN address_objects.parent_id IS 'идентификационный код родительского адресного объекта'; 43 | COMMENT ON COLUMN address_objects.title IS 'наименование объекта'; 44 | COMMENT ON COLUMN address_objects.full_title IS 'полное наименование объекта'; 45 | COMMENT ON COLUMN address_objects.postal_code IS 'индекс'; 46 | COMMENT ON COLUMN address_objects.region IS 'регион'; 47 | COMMENT ON COLUMN address_objects.prefix IS 'ул., пр. и так далее'; 48 | COMMENT ON COLUMN address_objects.house_count IS 'количество домов'; 49 | COMMENT ON COLUMN address_objects.next_address_level IS 'уровень следующего дочернего объекта по ФИАС'; 50 | 51 | DROP TABLE IF EXISTS address_object_levels; 52 | CREATE TABLE address_object_levels ( 53 | id INTEGER PRIMARY KEY, 54 | title VARCHAR, 55 | code VARCHAR 56 | ); 57 | COMMENT ON TABLE address_object_levels IS 'перечень уровня адресных объектов по ФИАС'; 58 | COMMENT ON COLUMN address_object_levels.id IS 'идентификационный код записи'; 59 | COMMENT ON COLUMN address_object_levels.title IS 'описание уровня'; 60 | COMMENT ON COLUMN address_object_levels.title IS 'код уровня'; 61 | 62 | DROP TABLE IF EXISTS update_log; 63 | CREATE TABLE update_log ( 64 | id SERIAL PRIMARY KEY, 65 | version_id INTEGER NOT NULL, 66 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(0) 67 | ); 68 | COMMENT ON TABLE update_log IS 'лог обновлений'; 69 | COMMENT ON COLUMN update_log.version_id IS 'id версии, полученной от базы ФИАС'; 70 | COMMENT ON COLUMN update_log.created_at IS 'дата установки обновления/инициализации'; 71 | 72 | DROP TABLE IF EXISTS places; 73 | CREATE TABLE places ( 74 | id SERIAL PRIMARY KEY, 75 | title VARCHAR, 76 | full_title VARCHAR, 77 | parent_id INTEGER, 78 | type_id INTEGER NOT NULL, 79 | have_children BOOLEAN DEFAULT FALSE 80 | ); 81 | COMMENT ON TABLE places IS 'справочник мест'; 82 | COMMENT ON COLUMN places.title IS 'название места'; 83 | COMMENT ON COLUMN places.full_title IS 'название места с типом'; 84 | COMMENT ON COLUMN places.parent_id IS 'идентификатор родительского места'; 85 | COMMENT ON COLUMN places.type_id IS 'идентификатор типа места'; 86 | COMMENT ON COLUMN places.have_children IS 'есть ли дочерние сущности'; 87 | 88 | DROP TABLE IF EXISTS place_types; 89 | CREATE TABLE place_types( 90 | id SERIAL PRIMARY KEY, 91 | parent_id INTEGER, 92 | title VARCHAR NOT NULL UNIQUE, 93 | system_name VARCHAR UNIQUE 94 | ); 95 | COMMENT ON TABLE place_types IS 'справочник типов мест'; 96 | COMMENT ON COLUMN place_types.parent_id IS 'идентификатор типа родителя'; 97 | COMMENT ON COLUMN place_types.title IS 'название типа для пользователя'; 98 | COMMENT ON COLUMN place_types.system_name IS 'системное имя типа, для использования в программном коде'; 99 | 100 | DROP TABLE IF EXISTS regions; 101 | CREATE TABLE regions ( 102 | number VARCHAR PRIMARY KEY, 103 | title VARCHAR 104 | ); 105 | COMMENT ON TABLE regions IS 'список регионов'; 106 | COMMENT ON COLUMN regions.number IS 'номер региона'; 107 | COMMENT ON COLUMN regions.number IS 'название региона'; 108 | -------------------------------------------------------------------------------- /database/02_system_data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO place_types(id, title) 2 | VALUES 3 | (1, 'транспортный объект') 4 | ; 5 | 6 | INSERT INTO place_types(id, title, parent_id, system_name) 7 | VALUES 8 | (2, 'аэропорт', 1, 'airport'), 9 | (3, 'вокзал', 1, 'railway_station'), 10 | (4, 'автовокзал', 1, 'bus_terminal'), 11 | (5, 'порт', 1, 'port'), 12 | (6, 'терминал', 2, 'airport_terminal'), 13 | (7, 'речной вокзал', 1, 'riverside_station') 14 | ; 15 | 16 | SELECT setval('place_types_id_seq', (SELECT MAX(id) FROM place_types LIMIT 1)); 17 | 18 | INSERT INTO places(id, parent_id, title, type_id) 19 | VALUES 20 | (1, NULL, 'Внуково', 2), 21 | (2, 1, 'A', 6), 22 | (3, 1, 'B', 6), 23 | (4, 1, 'D', 6), 24 | (5, NULL, 'Домодедово', 2), 25 | (6, NULL, 'Шереметьево', 2), 26 | (7, 6, 'B', 6), 27 | (8, 6, 'C', 6), 28 | (9, 6, 'D', 6), 29 | (10, 6, 'E', 6), 30 | (11, 6, 'F', 6), 31 | (12, NULL, 'Остафьево', 2), 32 | (13, NULL, 'Раменское', 2), 33 | (14, NULL, 'Чкаловский', 2), 34 | (15, NULL, 'Пулково', 2), 35 | (16, 15, '1', 6), 36 | (17, 15, '2', 6), 37 | (18, 15, 'новый', 6) 38 | ; 39 | 40 | SELECT setval('places_id_seq', (SELECT MAX(id) FROM places LIMIT 1)); 41 | 42 | INSERT INTO places(title, type_id) 43 | VALUES 44 | ('Балтийский', 3), 45 | ('Витебский', 3), 46 | ('Ладожский', 3), 47 | ('Московский', 3), 48 | ('Финляндский', 3), 49 | ('Белорусский', 3), 50 | ('Казанский', 3), 51 | ('Киевский', 3), 52 | ('Курский', 3), 53 | ('Ленинградский', 3), 54 | ('Павелецкий', 3), 55 | ('Рижский', 3), 56 | ('Савеловский', 3), 57 | ('Ярославский', 3), 58 | ('Щелковский', 4), 59 | ('Павелецкий', 4), 60 | ('Морской', 5), 61 | ('Пролетарский', 7), 62 | ('Уткина заводь', 7) 63 | ; 64 | 65 | UPDATE places p 66 | SET full_title = p.title || ' ' || pt.title 67 | FROM place_types pt 68 | WHERE pt.id = p.type_id; 69 | 70 | -- Формируем полный заголовок 71 | UPDATE places p 72 | SET full_title = tmp.title 73 | FROM ( 74 | WITH RECURSIVE required_places(id, title) AS ( 75 | SELECT DISTINCT pw.id, pw.title || ' ' || pwt.title 76 | FROM places pw 77 | INNER JOIN place_types pwt 78 | ON pwt.id = pw.type_id 79 | WHERE pw.parent_id IS NULL 80 | UNION ALL 81 | SELECT pwr.id, pw.title || ', ' || pwr.title || ' ' || pwt.title 82 | FROM places pwr 83 | INNER JOIN required_places pw 84 | ON pw.id = pwr.parent_id 85 | INNER JOIN place_types pwt 86 | ON pwt.id = pwr.type_id 87 | ) 88 | SELECT * FROM required_places 89 | ) tmp 90 | WHERE tmp.id = p.id; 91 | 92 | -- Определяем наличие дочерних записей 93 | UPDATE places 94 | SET have_children = TRUE 95 | WHERE id IN (SELECT DISTINCT parent_id FROM places); 96 | 97 | INSERT INTO address_object_levels (id, title, code) 98 | VALUES 99 | (1, 'Регион', 'region'), 100 | (2, 'Округ', 'area'), 101 | (3, 'Район округа', 'area_district'), 102 | (4, 'Город', 'city'), 103 | (5, 'Район города', 'city_district'), 104 | (6, 'Населенный пункт', 'settlement'), 105 | (7, 'Улица', 'street'), 106 | (90, 'Дополнительная территория', 'territory'), 107 | (91, 'Часть дополнительной территории', 'sub_territory'), 108 | (0, 'Здание', 'building') 109 | ; 110 | 111 | INSERT INTO regions (number, title) 112 | VALUES 113 | ('01', 'Адыгея Респ'), 114 | ('02', 'Башкортостан Респ'), 115 | ('03', 'Бурятия Респ'), 116 | ('04', 'Алтай Респ'), 117 | ('05', 'Дагестан Респ'), 118 | ('06', 'Ингушетия Респ'), 119 | ('07', 'Кабардино-Балкарская Респ'), 120 | ('08', 'Калмыкия Респ'), 121 | ('09', 'Карачаево-Черкесская Респ'), 122 | ('10', 'Карелия Респ'), 123 | ('11', 'Коми Респ'), 124 | ('12', 'Марий Эл Респ'), 125 | ('13', 'Мордовия Респ'), 126 | ('14', 'Саха /Якутия/ Респ'), 127 | ('15', 'Северная Осетия - Алания Респ'), 128 | ('16', 'Татарстан Респ'), 129 | ('17', 'Тыва Респ'), 130 | ('18', 'Удмуртская Респ'), 131 | ('19', 'Хакасия Респ'), 132 | ('20', 'Чеченская Респ'), 133 | ('21', 'Чувашская Республика - Чувашия'), 134 | ('22', 'Алтайский край'), 135 | ('23', 'Краснодарский край'), 136 | ('24', 'Красноярский край'), 137 | ('25', 'Приморский край'), 138 | ('26', 'Ставропольский край'), 139 | ('27', 'Хабаровский край'), 140 | ('28', 'Амурская обл'), 141 | ('29', 'Архангельская обл'), 142 | ('30', 'Астраханская обл'), 143 | ('31', 'Белгородская обл'), 144 | ('32', 'Брянская обл'), 145 | ('33', 'Владимирская обл'), 146 | ('34', 'Волгоградская обл'), 147 | ('35', 'Вологодская обл'), 148 | ('36', 'Воронежская обл'), 149 | ('37', 'Ивановская обл'), 150 | ('38', 'Иркутская обл'), 151 | ('39', 'Калининградская обл'), 152 | ('40', 'Калужская обл'), 153 | ('41', 'Камчатский край'), 154 | ('42', 'Кемеровская обл'), 155 | ('43', 'Кировская обл'), 156 | ('44', 'Костромская обл'), 157 | ('45', 'Курганская обл'), 158 | ('46', 'Курская обл'), 159 | ('47', 'Ленинградская обл'), 160 | ('48', 'Липецкая обл'), 161 | ('49', 'Магаданская обл'), 162 | ('50', 'Московская обл'), 163 | ('51', 'Мурманская обл'), 164 | ('52', 'Нижегородская обл'), 165 | ('53', 'Новгородская обл'), 166 | ('54', 'Новосибирская обл'), 167 | ('55', 'Омская обл'), 168 | ('56', 'Оренбургская обл'), 169 | ('57', 'Орловская обл'), 170 | ('58', 'Пензенская обл'), 171 | ('59', 'Пермский край'), 172 | ('60', 'Псковская обл'), 173 | ('61', 'Ростовская обл'), 174 | ('62', 'Рязанская обл'), 175 | ('63', 'Самарская обл'), 176 | ('64', 'Саратовская обл'), 177 | ('65', 'Сахалинская обл'), 178 | ('66', 'Свердловская обл'), 179 | ('67', 'Смоленская обл'), 180 | ('68', 'Тамбовская обл'), 181 | ('69', 'Тверская обл'), 182 | ('70', 'Томская обл'), 183 | ('71', 'Тульская обл'), 184 | ('72', 'Тюменская обл'), 185 | ('73', 'Ульяновская обл'), 186 | ('74', 'Челябинская обл'), 187 | ('75', 'Забайкальский край'), 188 | ('76', 'Ярославская обл'), 189 | ('77', 'Москва г'), 190 | ('78', 'Санкт-Петербург г'), 191 | ('79', 'Еврейская Аобл'), 192 | ('80', 'Забайкальский край Агинский Бурятский округ'), 193 | ('81', 'Коми-Пермяцкий АО'), 194 | ('82', 'Корякский АО'), 195 | ('83', 'Ненецкий АО'), 196 | ('84', 'Таймырский (Долгано-Ненецкий) АО'), 197 | ('85', 'Иркутская обл Усть-Ордынский Бурятский округ'), 198 | ('86', 'Ханты-Мансийский Автономный округ - Югра АО'), 199 | ('87', 'Чукотский АО'), 200 | ('88', 'Эвенкийский АО'), 201 | ('89', 'Ямало-Ненецкий АО'), 202 | ('91', 'Крым Респ'), 203 | ('92', 'Севастополь г'), 204 | ('99', 'Байконур г') 205 | ; 206 | -------------------------------------------------------------------------------- /database/03_indexes.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX address_objects_address_id_uq_idx 2 | ON address_objects 3 | USING BTREE (address_id) 4 | ; 5 | 6 | CREATE INDEX address_objects_parent_id_fkey_idx 7 | ON address_objects 8 | USING BTREE (parent_id) 9 | ; 10 | 11 | CREATE INDEX address_objects_level_full_title_lower_idx 12 | ON address_objects 13 | USING BTREE (level, lower(full_title)) 14 | ; 15 | 16 | CREATE INDEX address_objects_title_lower_idx 17 | ON address_objects 18 | USING BTREE (lower(title)) 19 | ; 20 | 21 | CREATE INDEX houses_address_id_fkey_idx 22 | ON houses 23 | USING BTREE (address_id) 24 | ; 25 | 26 | CREATE INDEX houses_full_number_idx 27 | ON houses 28 | USING BTREE (full_number) 29 | ; 30 | 31 | CREATE INDEX tmp_houses_number_id_fkey_idx 32 | ON houses 33 | USING BTREE (number) 34 | ; 35 | 36 | CREATE INDEX tmp_houses_building_id_fkey_idx 37 | ON houses 38 | USING BTREE (building) 39 | ; 40 | 41 | CREATE INDEX tmp_houses_structure_fkey_idx 42 | ON houses 43 | USING BTREE (structure) 44 | ; 45 | 46 | CREATE UNIQUE INDEX update_log_version_id_idx 47 | ON update_log 48 | USING BTREE (version_id) 49 | ; 50 | 51 | CREATE INDEX place_types_parent_id_fkey_idx 52 | ON place_types 53 | USING BTREE (parent_id) 54 | ; 55 | 56 | CREATE INDEX places_type_id_fkey_idx 57 | ON places 58 | USING BTREE (type_id) 59 | ; 60 | 61 | CREATE UNIQUE INDEX places_title_type_id_parent_id_uq_idx 62 | ON places 63 | USING BTREE (title, type_id, parent_id) 64 | ; 65 | 66 | CREATE INDEX places_title_idx 67 | ON places 68 | USING BTREE (title) 69 | ; 70 | 71 | -- Что бы если заглючит оптимизатор,он планы составил исходя из индексов все равно в момент массовой правки 72 | ANALYZE; 73 | -------------------------------------------------------------------------------- /database/04_constraints.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE address_objects 2 | ADD CONSTRAINT address_objects_parent_id_fkey 3 | FOREIGN KEY(parent_id) REFERENCES address_objects(address_id) 4 | ON UPDATE CASCADE ON DELETE CASCADE 5 | DEFERRABLE INITIALLY IMMEDIATE 6 | ; 7 | 8 | ALTER TABLE address_objects 9 | ADD CONSTRAINT address_objects_address_level_fkey 10 | FOREIGN KEY(address_level) REFERENCES address_object_levels(id) 11 | ON UPDATE CASCADE ON DELETE CASCADE 12 | DEFERRABLE INITIALLY IMMEDIATE 13 | ; 14 | 15 | ALTER TABLE houses 16 | ADD CONSTRAINT houses_parent_id_fkey 17 | FOREIGN KEY(address_id) REFERENCES address_objects(address_id) 18 | ON UPDATE CASCADE ON DELETE CASCADE 19 | DEFERRABLE INITIALLY IMMEDIATE 20 | ; 21 | 22 | ALTER TABLE place_types 23 | ADD CONSTRAINT place_types_parent_id_fkey 24 | FOREIGN KEY(parent_id) REFERENCES place_types(id) 25 | ON UPDATE CASCADE ON DELETE CASCADE 26 | ; 27 | 28 | ALTER TABLE places 29 | ADD CONSTRAINT places_type_id_fkey 30 | FOREIGN KEY (type_id) REFERENCES place_types(id) 31 | ON UPDATE CASCADE ON DELETE RESTRICT 32 | ; 33 | -------------------------------------------------------------------------------- /database/05_clean_up.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX tmp_houses_number_id_fkey_idx; 2 | DROP INDEX tmp_houses_building_id_fkey_idx; 3 | DROP INDEX tmp_houses_structure_fkey_idx; 4 | 5 | CLUSTER address_objects USING address_objects_level_full_title_lower_idx; 6 | CLUSTER houses USING houses_full_number_idx; 7 | ANALYZE; 8 | -------------------------------------------------------------------------------- /database/06_fakes.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO address_objects (id, address_id, parent_id, level, full_title, title, prefix, house_count, postal_code, address_level, region, next_address_level) 2 | VALUES 3 | ('29251dcf-00a1-4e34-98d4-5c47484a36d4', '29251dcf-00a1-4e34-98d4-5c47484a36d4', NULL, '0', 'г Москва', 'Москва', 'г', '0', NULL, 1, 77, 7), 4 | ('77303f7c-452b-4e73-b2b0-cbc59fe636c2', '77303f7c-452b-4e73-b2b0-cbc59fe636c2', '29251dcf-00a1-4e34-98d4-5c47484a36d4', '1', 'г Москва, ул Стахановская', 'Стахановская', 'ул', '4', 123456, 7, 77, NULL), 5 | ('77303f7c-452b-4e73-b2b0-cbc59fe636c4', '77303f7c-452b-4e73-b2b0-cbc59fe636c4', '29251dcf-00a1-4e34-98d4-5c47484a36d4', '1', 'г Москва, пр Ставропольский', 'Ставропольский', 'пр', '0', NULL, 7, 77, NULL), 6 | ('77303f7c-452b-4e73-b2b0-cbc59fe636c5', '77303f7c-452b-4e73-b2b0-cbc59fe636c5', '29251dcf-00a1-4e34-98d4-5c47484a36d4', '1', 'г Москва, пл Сталина', 'Сталина', 'пл', '0', NULL, 7, 77, NULL), 7 | ('77303f7c-452b-4e73-b2b0-cbc59fe636c6', '77303f7c-452b-4e73-b2b0-cbc59fe636c6', '29251dcf-00a1-4e34-98d4-5c47484a36d4', '1', 'г Москва, ал Старших Бобров', 'Старших Бобров', 'ал', '0', 123456, 7, 77, NULL), 8 | ('77303f7c-452b-4e73-b2b0-cbc59fe636c7', '77303f7c-452b-4e73-b2b0-cbc59fe636c7', '29251dcf-00a1-4e34-98d4-5c47484a36d4', '1', 'г Москва, ул Машинная', 'Машинная', 'ул', '0', NULL, 7, 77, NULL), 9 | ('77303f7c-452b-4e73-b2b0-cbc59fe636c8', '77303f7c-452b-4e73-b2b0-cbc59fe636c8', '77303f7c-452b-4e73-b2b0-cbc59fe636d0', '2', 'г Москва, р-н Мытищинский, ул Стахановская', 'Стахановская', 'ул', '0', 1234567, 7, 77, NULL), 10 | ('77303f7c-452b-4e73-b2b0-cbc59fe636c9', '77303f7c-452b-4e73-b2b0-cbc59fe636c9', '77303f7c-452b-4e73-b2b0-cbc59fe636d0', '2', 'г Москва, р-н Мытищинский, ул Раздолбайская', 'Раздолбайская', 'ул', '0', 1234567, 7, 77, NULL), 11 | ('77303f7c-452b-4e73-b2b0-cbc59fe636d0', '77303f7c-452b-4e73-b2b0-cbc59fe636d0', '29251dcf-00a1-4e34-98d4-5c47484a36d4', '1', 'г Москва, р-н Мытищинский', 'Мытищинский', 'р-н', '0', NULL, 7, 77, NULL) 12 | ; 13 | 14 | INSERT INTO houses(full_number, id, house_id, address_id, postal_code) 15 | VALUES 16 | ('16с17', '841254dc-0074-41fe-99ba-0c8501526c04', '841254dc-0074-41fe-99ba-0c8501526c04', '77303f7c-452b-4e73-b2b0-cbc59fe636c2', 654321), 17 | ('16с18', '841254dc-0074-41fe-99ba-0c8501526c05', '841254dc-0074-41fe-99ba-0c8501526c05', '77303f7c-452b-4e73-b2b0-cbc59fe636c2', NULL), 18 | ('23', '841254dc-0074-41fe-99ba-0c8501526c06', '841254dc-0074-41fe-99ba-0c8501526c06', '77303f7c-452b-4e73-b2b0-cbc59fe636c2', NULL), 19 | ('1к1', '841254dc-0074-41fe-99ba-0c8501526c07', '841254dc-0074-41fe-99ba-0c8501526c07', '77303f7c-452b-4e73-b2b0-cbc59fe636c2', NULL) 20 | ; 21 | -------------------------------------------------------------------------------- /docs/Описание службы получения обновлений.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miknatr/fias/dde1717f78136d1af179c281a86b99237d105c32/docs/Описание службы получения обновлений.doc -------------------------------------------------------------------------------- /docs/Сведения о составе информации ФИАС.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miknatr/fias/dde1717f78136d1af179c281a86b99237d105c32/docs/Сведения о составе информации ФИАС.doc -------------------------------------------------------------------------------- /huyak-package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | mkdir -p logs 24 | mkdir -p cache 25 | mkdir -p tests/file_directory 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | composer install 37 | 38 | 39 | 40 | rm -rf cache/* 41 | 42 | 43 | 44 | php cli/init-db.php 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /import.php.dist: -------------------------------------------------------------------------------- 1 | [ 5 | 'table_name' => 'address_objects', 6 | 'node_name' => 'Object', 7 | 'xml_key' => 'AOID', 8 | 'database_key' => 'id', 9 | 'fields' => [ 10 | 'AOID' => ['name' => 'id', 'type' => 'uuid'], 11 | 'AOGUID' => ['name' => 'address_id', 'type' => 'uuid'], 12 | 'AOLEVEL' => ['name' => 'address_level', 'type' => 'integer'], 13 | 'PARENTGUID' => ['name' => 'parent_id', 'type' => 'uuid'], 14 | 'FORMALNAME' => ['name' => 'title'], 15 | 'POSTALCODE' => ['name' => 'postal_code', 'type' => 'integer'], 16 | 'SHORTNAME' => ['name' => 'prefix'], 17 | 'REGIONCODE' => ['name' => 'region', 'type' => 'integer'], 18 | ], 19 | 'filters' => [ 20 | ['field' => 'ACTSTATUS', 'type' => 'eq', 'value' => 1], 21 | ], 22 | ], 23 | // При простановке false вместо массива загрузка домов не будет производиться. 24 | 'houses' => [ 25 | 'table_name' => 'houses', 26 | 'node_name' => 'House', 27 | 'xml_key' => 'HOUSEID', 28 | 'database_key' => 'id', 29 | 'fields' => [ 30 | 'HOUSEID' => ['name' => 'id', 'type' => 'uuid'], 31 | 'HOUSEGUID' => ['name' => 'house_id', 'type' => 'uuid'], 32 | 'AOGUID' => ['name' => 'address_id', 'type' => 'uuid'], 33 | 'HOUSENUM' => ['name' => 'number'], 34 | 'BUILDNUM' => ['name' => 'building'], 35 | 'STRUCNUM' => ['name' => 'structure'], 36 | ], 37 | ], 38 | ]; 39 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | tests 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/AddressObjectsImporter.php: -------------------------------------------------------------------------------- 1 | db); 16 | } 17 | 18 | protected $rowsPerInsert = 10000; 19 | } 20 | -------------------------------------------------------------------------------- /src/AddressObjectsUpdater.php: -------------------------------------------------------------------------------- 1 | import($reader); 16 | $this->modifyDataAfterImport(); 17 | } 18 | 19 | public function modifyDataAfterImport() 20 | { 21 | $this->updateOldRecords(); 22 | $this->addNewRecords(); 23 | 24 | RawDataHelper::cleanAddressObjects($this->db); 25 | } 26 | 27 | private function updateOldRecords() 28 | { 29 | $this->db->execute( 30 | "UPDATE address_objects ao_old 31 | SET title = ao_new.title, 32 | postal_code = ao_new.postal_code, 33 | prefix = ao_new.prefix, 34 | parent_id = ao_new.parent_id 35 | FROM ?f ao_new 36 | WHERE (ao_old.address_id = ao_new.address_id OR ao_old.id = ao_new.previous_id) 37 | AND ( 38 | COALESCE(ao_old.title, '') != COALESCE(ao_new.title, '') 39 | OR COALESCE(ao_old.postal_code, 0) != COALESCE(ao_new.postal_code, 0) 40 | OR COALESCE(ao_old.prefix, '') != COALESCE(ao_new.prefix, '') 41 | OR COALESCE(ao_old.parent_id, '00000000-0000-0000-0000-000000000000') != COALESCE(ao_new.parent_id, '00000000-0000-0000-0000-000000000000') 42 | )", 43 | [$this->table] 44 | ); 45 | } 46 | 47 | private function addNewRecords() 48 | { 49 | $this->db->execute( 50 | 'INSERT INTO address_objects(id, address_id, parent_id, title, postal_code, prefix) 51 | SELECT ao_new.id, ao_new.address_id, ao_new.parent_id, ao_new.title, ao_new.postal_code, ao_new.prefix 52 | FROM ?f ao_new 53 | LEFT JOIN address_objects ao_old 54 | ON (ao_old.address_id = ao_new.address_id OR ao_old.id = ao_new.previous_id) 55 | WHERE ao_old.id IS NULL 56 | ', 57 | [$this->table] 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/AddressStorage.php: -------------------------------------------------------------------------------- 1 | db = $db; 13 | } 14 | 15 | public function findAddress($address) 16 | { 17 | $level = count(explode(',', $address)) - 1; 18 | $sql = ' 19 | SELECT * 20 | FROM address_objects 21 | WHERE level = ?q 22 | AND lower(full_title) = lower(?q) 23 | LIMIT 1' 24 | ; 25 | 26 | return $this->db->execute($sql, [$level, $address])->fetchOneOrFalse(); 27 | } 28 | 29 | public function findHouse($address) 30 | { 31 | $tmp = explode(',', $address); 32 | $house = trim(array_pop($tmp)); 33 | 34 | $address = $this->findAddress(implode(',', $tmp)); 35 | if ($address) { 36 | $addressId = $address['address_id']; 37 | $sql = ' 38 | SELECT * 39 | FROM houses 40 | WHERE address_id = ?q 41 | AND full_number = lower(?q) 42 | LIMIT 1' 43 | ; 44 | 45 | return $this->db->execute($sql, [$addressId, $house])->fetchOneOrFalse(); 46 | } 47 | 48 | return false; 49 | } 50 | 51 | public function findAddressById($id) 52 | { 53 | $sql = ' 54 | SELECT * 55 | FROM address_objects 56 | WHERE address_id = ?q 57 | ORDER BY level DESC 58 | LIMIT 1' 59 | ; 60 | 61 | return $this->db->execute($sql, [$id])->fetchOneOrFalse(); 62 | } 63 | 64 | public function findHousesByPostalCode($postalCode) 65 | { 66 | $sql = ' 67 | SELECT * 68 | FROM houses 69 | WHERE postal_code = ?q' 70 | ; 71 | 72 | return $this->db->execute($sql, [$postalCode])->fetchResult(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ApiAction/AddressCompletion.php: -------------------------------------------------------------------------------- 1 | db = $db; 24 | $this->limit = $limit; 25 | $this->address = $address; 26 | $this->regions = $regions; 27 | 28 | if ($maxAddressLevel) { 29 | $this->maxAddressLevel = $this->getAddressLevelId($maxAddressLevel); 30 | } 31 | } 32 | 33 | public function run() 34 | { 35 | $storage = new AddressStorage($this->db); 36 | $addressParts = static::splitAddress($this->address); 37 | 38 | $address = $storage->findAddress($addressParts['address']); 39 | $this->parentId = $address ? $address['address_id'] : null; 40 | $houseCount = $address ? $address['house_count'] : null; 41 | 42 | if ($houseCount && $this->maxAddressLevel) { 43 | return []; 44 | } 45 | 46 | if ($this->getHouseCount()) { 47 | $rows = $this->findHouses($addressParts['pattern']); 48 | $rows = $this->setIsCompleteFlag($rows); 49 | } else { 50 | $rows = $this->findAddresses($addressParts['pattern']); 51 | $rows = $this->setIsCompleteFlag($rows); 52 | } 53 | 54 | foreach ($rows as $key => $devNull) { 55 | $rows[$key]['tags'] = ['address']; 56 | } 57 | 58 | return $rows; 59 | } 60 | 61 | private function getHouseCount() 62 | { 63 | if (!$this->parentId) { 64 | return null; 65 | } 66 | 67 | $sql = 'SELECT house_count FROM address_objects WHERE address_id = ?q'; 68 | 69 | return $this->db->execute($sql, [$this->parentId])->fetchResult(); 70 | } 71 | 72 | private function findAddressesWithParentId($pattern) 73 | { 74 | $where = $this->generateGeneralWherePart($pattern); 75 | $sql = " 76 | SELECT full_title title, address_level, next_address_level 77 | FROM address_objects ao 78 | WHERE ?p 79 | AND (parent_id = ?q) 80 | ORDER BY ao.title 81 | LIMIT ?e" 82 | ; 83 | 84 | return $this->db->execute( 85 | $sql, 86 | [$where, $this->parentId, $this->limit] 87 | )->fetchAll(); 88 | } 89 | 90 | private function findAddressesWithoutParentId($pattern) 91 | { 92 | $sql = " 93 | ( 94 | SELECT full_title title, address_level, next_address_level 95 | FROM address_objects ao 96 | WHERE ?p:where: 97 | AND (parent_id IS NULL) 98 | LIMIT ?e:limit: 99 | ) 100 | UNION 101 | ( 102 | SELECT ao.full_title title, ao.address_level, ao.next_address_level 103 | FROM address_objects ao 104 | INNER JOIN address_objects AS aop 105 | ON aop.parent_id IS NULL 106 | AND aop.address_id = ao.parent_id 107 | WHERE ?p:where: 108 | LIMIT ?e:limit: 109 | ) 110 | ORDER BY title 111 | LIMIT ?e:limit: 112 | " 113 | ; 114 | 115 | $where = $this->generateGeneralWherePart($pattern); 116 | $values = [ 117 | 'where' => $where, 118 | 'limit' => $this->limit 119 | ]; 120 | 121 | return $this->db->execute($sql, $values)->fetchAll(); 122 | } 123 | 124 | private function findAddresses($pattern) 125 | { 126 | if ($this->parentId) { 127 | return $this->findAddressesWithParentId($pattern); 128 | } 129 | 130 | return $this->findAddressesWithoutParentId($pattern); 131 | } 132 | 133 | private function generateGeneralWherePart($pattern) 134 | { 135 | $whereParts = [$this->db->replacePlaceholders("ao.title ilike '?e%'", [$pattern])]; 136 | 137 | if ($this->maxAddressLevel) { 138 | $whereParts[] = $this->db->replacePlaceholders('ao.address_level <= ?q', [$this->maxAddressLevel]); 139 | } 140 | 141 | if ($this->regions) { 142 | $whereParts[] = $this->db->replacePlaceholders('ao.region IN (?l)', [$this->regions]); 143 | } 144 | 145 | return '(' . implode(') AND (', $whereParts) . ')'; 146 | } 147 | 148 | private function findHouses($pattern) 149 | { 150 | $sql = " 151 | SELECT full_title||', '||full_number title, ?q address_level, NULL next_address_level 152 | FROM houses h 153 | INNER JOIN address_objects ao 154 | ON ao.address_id = h.address_id 155 | WHERE h.address_id = ?q 156 | AND full_number ilike '?e%' 157 | ORDER BY (regexp_matches(full_number, '^[0-9]+', 'g'))[1] 158 | LIMIT ?e" 159 | ; 160 | $values = [static::BUILDING_ADDRESS_LEVEL, $this->parentId, $pattern, $this->limit]; 161 | 162 | return $this->db->execute($sql, $values)->fetchAll(); 163 | } 164 | 165 | private function setIsCompleteFlag(array $values) 166 | { 167 | foreach ($values as $key => $value) { 168 | $isMaxLevelReached = $value['address_level'] == $this->maxAddressLevel; 169 | $doChildrenSuitNextLevel = ($value['next_address_level'] <= $this->maxAddressLevel) 170 | || (!$this->maxAddressLevel && !empty($value['house_count'])) 171 | ; 172 | 173 | $values[$key]['is_complete'] = $isMaxLevelReached || !$doChildrenSuitNextLevel; 174 | 175 | unset($values[$key]['address_level']); 176 | } 177 | 178 | return $values; 179 | } 180 | 181 | private static function splitAddress($address) 182 | { 183 | $tmp = explode(',', $address); 184 | 185 | return [ 186 | 'pattern' => static::cleanAddressPart(array_pop($tmp)), 187 | 'address' => implode(',', $tmp), 188 | ]; 189 | } 190 | 191 | private static function cleanAddressPart($rawAddress) 192 | { 193 | // избавляемся от популярных префиксов/постфиксов (Вопросы по поводу регулярки к johnnywoo, сам я ее слабо понимаю). 194 | $cleanAddress = preg_replace(' 195 | { 196 | (?<= ^ | [^а-яА-ЯЁё] ) 197 | 198 | (?:ул|улица|снт|деревня|тер|пер|переулок|ал|аллея|линия|проезд|гск|ш|шоссе|г|город|обл|область|пр|проспект) 199 | 200 | (?= [^а-яА-ЯЁё] | $ ) 201 | 202 | [.,-]* 203 | }x', 204 | '', 205 | $rawAddress 206 | ); 207 | 208 | return trim($cleanAddress); 209 | } 210 | 211 | public function getAddressLevelId($code) 212 | { 213 | $result = $this->db->execute( 214 | 'SELECT id FROM address_object_levels WHERE code = ?q', 215 | [$code] 216 | )->fetchResult(); 217 | 218 | if ($result === null) { 219 | throw new BadRequestException('Некорректное значение уровня адреса: ' . $code); 220 | } 221 | 222 | return $result; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/ApiAction/AddressPostalCode.php: -------------------------------------------------------------------------------- 1 | db = $db; 17 | $this->address = $address; 18 | } 19 | 20 | public function run() 21 | { 22 | $storage = new AddressStorage($this->db); 23 | $address = $storage->findAddress($this->address); 24 | if ($address) { 25 | return $address['postal_code']; 26 | } 27 | 28 | $house = $storage->findHouse($this->address); 29 | if ($house) { 30 | if ($house['postal_code']) { 31 | return $house['postal_code']; 32 | } 33 | 34 | $address = $storage->findAddressById($house['address_id']); 35 | return $address['postal_code']; 36 | } 37 | 38 | // ничего не найдено 39 | return null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ApiAction/ApiActionInterface.php: -------------------------------------------------------------------------------- 1 | db = $db; 19 | $this->limit = $limit; 20 | 21 | $this->textForCompletion = $textForCompletion; 22 | } 23 | 24 | public function run() 25 | { 26 | $placeParts = $this->splitPlaceTitle($this->textForCompletion); 27 | $parentId = null; 28 | 29 | if (!empty($placeParts['parent_place'])) { 30 | $storage = new PlaceStorage($this->db); 31 | $parentId = $storage->findPlace($placeParts['parent_place'])['id']; 32 | if (!$parentId) { 33 | return []; 34 | } 35 | } 36 | 37 | $placeWords = $this->splitPatternToWords($placeParts['pattern']); 38 | $type = $this->extractType($placeWords); 39 | 40 | if ($type) { 41 | unset($placeWords[$type['title']]); 42 | } 43 | 44 | $rows = $this->findPlaces($placeWords, $type, $parentId); 45 | 46 | return $rows; 47 | } 48 | 49 | private function splitPlaceTitle($title) 50 | { 51 | $tmp = explode(',', $title); 52 | 53 | return [ 54 | 'pattern' => strtolower(trim(array_pop($tmp))), 55 | 'parent_place' => implode(',', $tmp), 56 | ]; 57 | } 58 | 59 | private function extractType(array $words) 60 | { 61 | $type = $this->db->execute( 62 | 'SELECT id, title 63 | FROM place_types 64 | WHERE title IN (?l) 65 | ', 66 | [$words] 67 | )->fetchOneOrFalse(); 68 | 69 | return $type ?: []; 70 | } 71 | 72 | private function findPlaces(array $placeWords, array $type, $parentId) 73 | { 74 | $pattern = implode(' ', $placeWords); 75 | $sql = " 76 | SELECT full_title title, (CASE WHEN have_children THEN 0 ELSE 1 END) is_complete, pt.system_name type_system_name 77 | FROM places p 78 | INNER JOIN place_types pt 79 | ON p.type_id = pt.id 80 | WHERE ?p 81 | ORDER BY p.title 82 | LIMIT ?e" 83 | ; 84 | 85 | $whereParts = [$this->db->replacePlaceholders("p.title ilike '?e%'", [$pattern])]; 86 | 87 | if ($type) { 88 | $whereParts[] = $this->db->replacePlaceholders('type_id = ?q', [$type['id']]); 89 | } 90 | 91 | if ($parentId) { 92 | $whereParts[] = $this->db->replacePlaceholders('p.parent_id = ?q', [$parentId]); 93 | } 94 | 95 | $values = ['(' . implode($whereParts, ') AND (') . ')', $this->limit]; 96 | 97 | $items = $this->db->execute($sql, $values)->fetchAll(); 98 | if ($items) { 99 | foreach ($items as $key => $item) { 100 | $items[$key]['is_complete'] = $item['is_complete'] ? true : false; 101 | $items[$key]['tags'] = ['place', $item['type_system_name']]; 102 | } 103 | } 104 | 105 | return $items; 106 | } 107 | 108 | private function splitPatternToWords($pattern) 109 | { 110 | $tmp = explode(' ', $pattern); 111 | $result = []; 112 | 113 | foreach ($tmp as $word) { 114 | $trimmedWord = trim($word); 115 | $result[$trimmedWord] = $trimmedWord; 116 | } 117 | 118 | return $result; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/ApiAction/PostalCodeLocation.php: -------------------------------------------------------------------------------- 1 | db = $db; 16 | $this->postalCode = $postalCode; 17 | } 18 | 19 | public function run() 20 | { 21 | $count = $this->getAddressCount(); 22 | if (!$count) { 23 | return []; 24 | } 25 | 26 | $stack = $this->getBiggestSuitableStack(); 27 | 28 | return $this->filterAddressStack($stack, $count); 29 | } 30 | 31 | private function getAddressCount($pattern = null) 32 | { 33 | $values = [$this->postalCode]; 34 | $sql = 'SELECT COUNT(*) FROM address_objects WHERE postal_code = ?q'; 35 | 36 | if ($pattern) { 37 | $values[] = $pattern; 38 | $sql .= " AND full_title LIKE '?e%'"; 39 | } 40 | 41 | return $this->db->execute($sql, $values)->fetchResult(); 42 | } 43 | 44 | private function getBiggestSuitableStack() 45 | { 46 | // Берем наименьший адрес и через родительские связки строим стек. 47 | return $this->db->execute( 48 | "WITH RECURSIVE 49 | address_stack(parent_id, title, full_title, level, address_level) AS ( 50 | SELECT parent_id, prefix || ' ' || title, full_title, level, address_level 51 | FROM address_objects ao 52 | WHERE address_id IN ( 53 | SELECT address_id 54 | FROM address_objects 55 | WHERE postal_code = ?q 56 | ORDER BY level 57 | LIMIT 1 58 | ) 59 | UNION ALL 60 | SELECT r.parent_id, r.prefix || ' ' || r.title, r.full_title, r.level, r.address_level 61 | FROM address_objects r 62 | INNER JOIN address_stack ads 63 | ON ads.parent_id = r.address_id 64 | 65 | ) 66 | SELECT parent_id, a.title, full_title, level, al.code address_level 67 | FROM address_stack a 68 | INNER JOIN address_object_levels al 69 | ON al.id = address_level 70 | ORDER BY level 71 | ", 72 | [$this->postalCode] 73 | )->fetchAll(); 74 | } 75 | 76 | private function filterAddressStack(array $partsForChecking, $totalCount, array $currentStack = []) 77 | { 78 | $part = array_shift($partsForChecking); 79 | $levelCount = $this->getAddressCount($part['full_title']); 80 | 81 | if ($levelCount == $totalCount) { 82 | $currentStack[] = ['title' => $part['title'], 'address_level' => $part['address_level']]; 83 | } 84 | 85 | if (!$partsForChecking || ($levelCount != $totalCount)) { 86 | return $currentStack; 87 | } 88 | 89 | return $this->filterAddressStack($partsForChecking, $totalCount, $currentStack); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/ApiAction/Validation.php: -------------------------------------------------------------------------------- 1 | address = $address; 18 | $this->db = $db; 19 | } 20 | 21 | public function run() 22 | { 23 | $result = []; 24 | 25 | $addressData = $this->lookUpInFias(); 26 | if ($addressData) { 27 | $addressData['tags'] = ['address']; 28 | $result[] = $addressData; 29 | } 30 | 31 | $placeData = $this->lookUpInPlaces(); 32 | if ($placeData) { 33 | $result[] = [ 34 | 'is_valid' => $placeData['is_valid'], 35 | 'is_complete' => $placeData['is_complete'], 36 | 'tags' => ['place', $placeData['type_system_name']] 37 | ]; 38 | } 39 | 40 | return $result; 41 | } 42 | 43 | private function lookUpInFias() 44 | { 45 | $storage = new AddressStorage($this->db); 46 | 47 | $completeAddress = $storage->findHouse($this->address); 48 | if ($completeAddress) { 49 | return ['is_complete' => true, 'is_valid' => true]; 50 | } 51 | 52 | $incompleteAddress = $storage->findAddress($this->address); 53 | if ($incompleteAddress) { 54 | return ['is_complete' => false, 'is_valid' => true]; 55 | } 56 | 57 | // ничего не нашлось 58 | return null; 59 | } 60 | 61 | private function lookUpInPlaces() 62 | { 63 | $storage = new PlaceStorage($this->db); 64 | $place = $storage->findPlace($this->address); 65 | 66 | if ($place) { 67 | return [ 68 | 'type_system_name' => $place['type_system_name'], 69 | 'is_valid' => true, 70 | 'is_complete' => true 71 | ]; 72 | } 73 | 74 | return null; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/BadRequestException.php: -------------------------------------------------------------------------------- 1 | config = parse_ini_file(__DIR__ . '/../config.ini', true); 21 | $this->ensureParameters( 22 | $this->config, 23 | ['app.host', 'app.file_directory', 'app.wsdl_url', 'app.max_completion_limit', 'db.uri'] 24 | ); 25 | 26 | $this->importConfig = require(__DIR__ . '/../import.php'); 27 | $this->loadDbConfig($this->config); 28 | $this->loadRouterConfig($this->config, $this->getRootDirectory()); 29 | $this->loadLoggingConfig($this->config, $this->getRootDirectory()); 30 | } 31 | 32 | protected function ensureParameters(array $config, array $parameterNames) 33 | { 34 | $undefinedMessages = []; 35 | foreach ($parameterNames as $name) { 36 | if (!isset($config[$name])) { 37 | $undefinedMessages[] = "Config parameter {$name} is not defined"; 38 | } 39 | } 40 | 41 | if (count($undefinedMessages) > 0) { 42 | throw new \LogicException(implode("\n", $undefinedMessages)); 43 | } 44 | } 45 | 46 | public function getDbUri() 47 | { 48 | return $this->config['db.uri']; 49 | } 50 | 51 | public function getRootDirectory() 52 | { 53 | return __DIR__ . '/..'; 54 | } 55 | 56 | public function getHost() 57 | { 58 | return $this->config['app.host']; 59 | } 60 | 61 | public function getWsdlUrl() 62 | { 63 | return $this->config['app.wsdl_url']; 64 | } 65 | 66 | public function getFileDirectory() 67 | { 68 | return $this->config['app.file_directory']; 69 | } 70 | 71 | public function getMaxCompletionLimit() 72 | { 73 | return $this->config['app.max_completion_limit']; 74 | } 75 | 76 | public function getDatabaseName() 77 | { 78 | $parts = explode('/', $this->config['db.uri']); 79 | 80 | return array_pop($parts); 81 | } 82 | 83 | public function getDatabaseSourcesDirectory() 84 | { 85 | return __DIR__ . '/../database'; 86 | } 87 | 88 | public function getHousesImportConfig() 89 | { 90 | return $this->importConfig['houses']; 91 | } 92 | 93 | public function getAddressObjectsImportConfig() 94 | { 95 | return $this->importConfig['address_objects']; 96 | } 97 | 98 | public function getUpdateLoader() 99 | { 100 | return new UpdateLoader($this->getWsdlUrl(), $this->getFileDirectory()); 101 | } 102 | 103 | public function getInitLoader(){ 104 | return new InitLoader($this->getWsdlUrl(), $this->getFileDirectory()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Controller/ApiController.php: -------------------------------------------------------------------------------- 1 | container = $container; 25 | } 26 | 27 | /** 28 | * @route GET /api/complete 29 | * @route GET /api/complete.json 30 | * @route GET /api/complete.jsonp format=jsonp 31 | * @param Request $request 32 | * @return JsonpResponse|JsonResponse|Response 33 | * @throws RouteNotFoundException 34 | */ 35 | public function complete(Request $request) 36 | { 37 | $maxLimit = $this->container->getMaxCompletionLimit(); 38 | 39 | $maxAddressLevel = $request->get('max_address_level'); 40 | $regions = $request->get('regions', []); 41 | $pattern = $request->get('pattern', ''); 42 | $limit = $request->get('limit', $maxLimit); 43 | 44 | if ($limit > $maxLimit) { 45 | return $this->makeErrorResponse($request, 'Превышен допустимый лимит на количество записей. Лимит должен быть не более: ' . $maxLimit); 46 | } 47 | 48 | $places = (new PlaceCompletion($this->container->getDb(), $pattern, $limit))->run(); 49 | 50 | $addresses = (new AddressCompletion( 51 | $this->container->getDb(), 52 | $pattern, 53 | $limit, 54 | $maxAddressLevel, 55 | $regions 56 | ))->run(); 57 | 58 | $result = ['items' => array_merge($places, $addresses)]; 59 | 60 | return $this->makeResponse($request, $result); 61 | } 62 | 63 | /** 64 | * @route GET /api/validate 65 | * @route GET /api/validate.json 66 | * @route GET /api/validate.jsonp format=jsonp 67 | * @param Request $request 68 | * @return JsonpResponse|JsonResponse|Response 69 | * @throws RouteNotFoundException 70 | */ 71 | public function validate(Request $request) 72 | { 73 | $pattern = $request->get('pattern'); 74 | if (!$pattern) { 75 | return $this->makeErrorResponse($request, 'Отсутствует обязательный параметр: pattern.'); 76 | } 77 | 78 | $result = ['items' => (new Validation($this->container->getDb(), $pattern))->run()]; 79 | 80 | return $this->makeResponse($request, $result); 81 | } 82 | 83 | /** 84 | * @route GET /api/address_postal_code 85 | * @route GET /api/address_postal_code.json 86 | * @route GET /api/address_postal_code.jsonp format=jsonp 87 | * @param Request $request 88 | * @return JsonpResponse|JsonResponse|Response 89 | * @throws RouteNotFoundException 90 | */ 91 | public function postalCode(Request $request) 92 | { 93 | $address = $request->get('address', ''); 94 | if (!$address) { 95 | return $this->makeErrorResponse($request, 'Отсутствует обязательный параметр: address.'); 96 | } 97 | 98 | $result = (new AddressPostalCode($this->container->getDb(), $address))->run(); 99 | 100 | return $this->makeResponse($request, ['postal_code' => $result]); 101 | } 102 | 103 | /** 104 | * @route GET /api/postal_code_location 105 | * @route GET /api/postal_code_location.json 106 | * @route GET /api/postal_code_location.jsonp format=jsonp 107 | * @param Request $request 108 | * @return JsonpResponse|JsonResponse|Response 109 | * @throws RouteNotFoundException 110 | */ 111 | public function postalCodeLocation(Request $request) 112 | { 113 | $postalCode = $request->get('postal_code', ''); 114 | if (!$postalCode) { 115 | return $this->makeErrorResponse($request, 'Отсутствует обязательный параметр: postal_code.'); 116 | } 117 | 118 | $result = ['address_parts' => (new PostalCodeLocation($this->container->getDb(), $postalCode))->run()]; 119 | 120 | return $this->makeResponse($request, $result); 121 | } 122 | 123 | private function makeErrorResponse(Request $request, $message) 124 | { 125 | return $this->makeResponse($request, ['error_message' => $message], 400); 126 | } 127 | 128 | private function makeResponse(Request $request, array $values, $status = 200) 129 | { 130 | $format = $request->option('format', 'json'); 131 | switch ($format) { 132 | case 'json': 133 | $response = new JsonResponse($status, $values); 134 | break; 135 | case 'jsonp': 136 | $callback = $request->get('callback'); 137 | 138 | if (!$callback) { 139 | return new Response(400, 'Отсутствует обязательный параметр: callback.'); 140 | } 141 | $response = new JsonpResponse($values, $callback); 142 | break; 143 | default: 144 | throw new RouteNotFoundException; 145 | } 146 | 147 | return $response; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/DataSource/DataSource.php: -------------------------------------------------------------------------------- 1 | dataSource = $dataSource; 25 | 26 | $this->startAttribute = $startAttribute; 27 | $this->endAttribute = $endAttribute; 28 | $this->typeAttribute = $typeAttribute; 29 | $this->resultAttribute = $resultAttribute; 30 | } 31 | 32 | private $currentPosition; 33 | private $currentRow; 34 | private $startPosition; 35 | private $endPosition; 36 | 37 | public function getRows($maxCount = 1000) 38 | { 39 | $this->ensureMaxCountIsValid($maxCount); 40 | 41 | $count = 0; 42 | $result = []; 43 | 44 | // Первый раз $this->currentRow гарантировано равна null. Поэтому do - while. 45 | do { 46 | if (($this->currentPosition <= $this->endPosition) && $this->currentRow) { 47 | $resultPart = $this->getRowsFromInterval($maxCount - $count); 48 | $count += count($resultPart); 49 | $result = array_merge($result, $resultPart); 50 | } else { 51 | $this->setCurrentRow(); 52 | } 53 | } while (($count < $maxCount) && $this->currentRow); 54 | 55 | return $result; 56 | } 57 | 58 | private function ensureMaxCountIsValid($maxCount) 59 | { 60 | if ($maxCount < 1) { 61 | throw new \LogicException('Неверное значение максимального количества строк: ' . $maxCount); 62 | } 63 | } 64 | 65 | private function getRowsFromInterval($maxCount) 66 | { 67 | $result = []; 68 | $count = 0; 69 | 70 | while (($count < $maxCount) && ($this->currentPosition <= $this->endPosition)) { 71 | switch ($this->currentRow[$this->typeAttribute]) { 72 | case static::BASE_TYPE: 73 | $value = $this->currentPosition++; 74 | break; 75 | case static::EVEN_TYPE: 76 | $value = ($this->currentPosition % 2) == 0 ? $this->currentPosition : $this->currentPosition + 1; 77 | $this->currentPosition = ($this->currentPosition % 2) == 0 ? $this->currentPosition + 2 : $this->currentPosition + 3; 78 | break; 79 | case static::ODD_TYPE: 80 | $value = ($this->currentPosition % 2) == 1 ? $this->currentPosition : $this->currentPosition + 1; 81 | $this->currentPosition = ($this->currentPosition % 2) == 1 ? $this->currentPosition + 2 : $this->currentPosition + 3; 82 | break; 83 | default: 84 | throw new \Exception('Задан неверный тип работы.'); 85 | } 86 | 87 | $this->currentRow[$this->resultAttribute] = $value; 88 | 89 | $result[] = $this->currentRow; 90 | ++$count; 91 | } 92 | 93 | return $result; 94 | } 95 | 96 | private function setCurrentRow() 97 | { 98 | $tmpResult = $this->dataSource->getRows(1); 99 | if (!$tmpResult) { 100 | $this->currentRow = null; 101 | return; 102 | } 103 | 104 | $this->currentRow = array_shift($tmpResult); 105 | $this->startPosition = $this->currentRow[$this->startAttribute]; 106 | $this->currentPosition = $this->currentRow[$this->startAttribute]; 107 | $this->endPosition = $this->currentRow[$this->endAttribute]; 108 | 109 | if ($this->startPosition > $this->endPosition) { 110 | throw new \Exception('Начало интервала не может быть больше его окончания'); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/DataSource/XmlReader.php: -------------------------------------------------------------------------------- 1 | nodeName = $nodeName; 19 | $this->attributes = $attributes; 20 | $this->filters = $filters; 21 | 22 | $this->initializeReader($pathToFile); 23 | } 24 | 25 | private function initializeReader($pathToFile) 26 | { 27 | FileHelper::ensureIsReadable($pathToFile); 28 | 29 | $this->reader = new \XMLReader(); 30 | 31 | $success = $this->reader->open($pathToFile); 32 | if (!$success) { 33 | throw new ImporterException('Ошибка открытия XML файла по адресу: ' . $pathToFile); 34 | } 35 | } 36 | 37 | public function getRows($maxCount = 1000) 38 | { 39 | $this->ensureMaxCountIsValid($maxCount); 40 | 41 | $result = []; 42 | $count = 0; 43 | 44 | while (($count < $maxCount) && $this->reader->read()) { 45 | if ($this->checkIsNodeAccepted($this->reader->name)) { 46 | $result[] = $this->getRowAttributes(); 47 | ++$count; 48 | } 49 | } 50 | 51 | return $result; 52 | } 53 | 54 | private function ensureMaxCountIsValid($maxCount) 55 | { 56 | if ($maxCount < 1) { 57 | throw new \LogicException('Неверное значение максимального количества строк: ' . $maxCount); 58 | } 59 | } 60 | 61 | private function checkIsNodeAccepted($node) 62 | { 63 | if ($node != $this->nodeName) { 64 | return false; 65 | } 66 | 67 | foreach ($this->filters as $filter) { 68 | $value = $this->reader->getAttribute($filter['field']); 69 | 70 | switch ($filter['type']) { 71 | case 'in': 72 | if ($filter['value'] && !in_array($value, $filter['value'])) { 73 | return false; 74 | } 75 | break; 76 | case 'nin': 77 | if ($filter['value'] && in_array($value, $filter['value'])) { 78 | return false; 79 | } 80 | break; 81 | case 'hash': 82 | if ($filter['value'] && empty($filter['value'][$value])) { 83 | return false; 84 | } 85 | break; 86 | case 'eq': 87 | // no break 88 | default : 89 | if ($filter['value'] != $value) { 90 | return false; 91 | } 92 | } 93 | } 94 | 95 | return true; 96 | } 97 | 98 | private function getRowAttributes() 99 | { 100 | $result = []; 101 | foreach ($this->attributes as $attribute) { 102 | // Если атрибут отсутствует, в $result[$attribute] окажется null, также передаем null вместо пустого значения 103 | $result[$attribute] = $this->reader->getAttribute($attribute) ?: null; 104 | } 105 | 106 | return $result; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/DatabaseDumpManager.php: -------------------------------------------------------------------------------- 1 | execute($sql, $params); 34 | } 35 | 36 | public static function runFile($db, $path) 37 | { 38 | FileHelper::ensureIsReadable($path); 39 | $path = escapeshellarg($path); 40 | $db = escapeshellarg($db); 41 | 42 | exec('psql -f ' . $path . ' ' . $db . ' 2>&1', $output, $result); 43 | 44 | if ($result !== 0) { 45 | throw new \Exception('Ошибка выполнения SQL файла: ' . implode("\n", $output)); 46 | } 47 | 48 | if ($output) { 49 | foreach ($output as $line) { 50 | if (preg_match('/psql:(.*)ERROR:/', $line)) { 51 | throw new \Exception('Ошибка выполнения SQL файла: ' . implode("\n", $output)); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/FileSystem/Dearchiver.php: -------------------------------------------------------------------------------- 1 | &1', $output, $result); 42 | 43 | if ($result !== 0) { 44 | throw new \Exception('Ошибка разархивации: ' . implode("\n", $output)); 45 | } 46 | 47 | return $directoryForExtract; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/FileSystem/Directory.php: -------------------------------------------------------------------------------- 1 | directoryPath = $path; 15 | } 16 | 17 | public function getVersionId() 18 | { 19 | $prefix = 'VERSION_ID_'; 20 | return str_replace($prefix, '', $this->find($prefix)); 21 | } 22 | 23 | public function getDeletedAddressObjectFile() 24 | { 25 | $fileName = $this->find('AS_DEL_ADDROBJ', false); 26 | return $fileName ? $this->directoryPath . '/' . $fileName : null; 27 | } 28 | 29 | public function getDeletedHouseFile() 30 | { 31 | $fileName = $this->find('AS_DEL_HOUSE_', false); 32 | return $fileName ? $this->directoryPath . '/' . $fileName : null; 33 | } 34 | 35 | public function getAddressObjectFile() 36 | { 37 | return $this->directoryPath . '/' . $this->find('AS_ADDROBJ'); 38 | } 39 | 40 | public function getHouseFile() 41 | { 42 | return $this->directoryPath . '/' . $this->find('AS_HOUSE_'); 43 | } 44 | 45 | public function getPath() 46 | { 47 | return $this->directoryPath; 48 | } 49 | 50 | private function find($prefix, $isIndispensable = true) 51 | { 52 | $files = scandir($this->directoryPath); 53 | foreach ($files as $file) { 54 | if (in_array($file, ['.', '..'])) { 55 | continue; 56 | } 57 | 58 | if (mb_strpos($file, $prefix) === 0) { 59 | return $file; 60 | } 61 | } 62 | 63 | if ($isIndispensable) { 64 | throw new FileException('Файл с префиксом ' . $prefix . ' не найден в директории: ' . $this->directoryPath); 65 | } 66 | 67 | return null; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/FileSystem/FileException.php: -------------------------------------------------------------------------------- 1 | db, $this->table); 16 | RawDataHelper::updateHousesCount($this->db); 17 | RawDataHelper::updateNextAddressLevelFlag($this->db); 18 | } 19 | 20 | protected $rowsPerInsert = 10000; 21 | } 22 | -------------------------------------------------------------------------------- /src/HousesUpdater.php: -------------------------------------------------------------------------------- 1 | 'full_number']; 11 | parent::__construct($db, $table, $fields, true); 12 | } 13 | 14 | public function update(DataSource $reader) 15 | { 16 | $this->import($reader); 17 | $this->modifyDataAfterImport(); 18 | } 19 | 20 | public function modifyDataAfterImport() 21 | { 22 | $this->createTemporaryIndexes(); 23 | 24 | RawDataHelper::cleanHouses($this->db, $this->table); 25 | 26 | $this->removeOldRecords(); 27 | $this->addNewRecords(); 28 | 29 | RawDataHelper::updateHousesCount($this->db); 30 | RawDataHelper::updateNextAddressLevelFlag($this->db); 31 | } 32 | 33 | private function removeOldRecords() 34 | { 35 | $this->db->execute( 36 | 'DELETE FROM ?f:temp_table: h 37 | USING ( 38 | SELECT DISTINCT h.address_id 39 | FROM ?f:temp_table: h 40 | LEFT JOIN address_objects ao 41 | ON ao.address_id = h.address_id 42 | WHERE ao.id IS NULL 43 | ) a 44 | WHERE a.address_id = h.address_id 45 | ', 46 | ['temp_table' => $this->table] 47 | ); 48 | 49 | $this->db->execute( 50 | 'DELETE FROM houses h_old 51 | USING ?f h_new 52 | WHERE (h_old.house_id = h_new.house_id OR h_old.id = h_new.previous_id) 53 | ', 54 | [$this->table] 55 | ); 56 | } 57 | 58 | private function addNewRecords() 59 | { 60 | $this->db->execute( 61 | 'INSERT INTO houses(id, house_id, address_id, number, building, structure, full_number) 62 | SELECT h_new.id, h_new.house_id, h_new.address_id, h_new.number, h_new.building, h_new.structure, h_new.full_number 63 | FROM ?f h_new 64 | ', 65 | [$this->table] 66 | ); 67 | } 68 | 69 | private function createTemporaryIndexes() 70 | { 71 | $sql = 'CREATE INDEX tmp_' . rand() . '_idx ON ?f USING BTREE(building)'; 72 | $this->db->execute($sql, [$this->table]); 73 | 74 | $sql = 'CREATE INDEX tmp_2' . rand() . '_idx ON ?f USING BTREE(structure)'; 75 | $this->db->execute($sql, [$this->table]); 76 | 77 | $sql = 'CREATE INDEX tmp_3' . rand() . '_idx ON ?f USING BTREE(house_id)'; 78 | $this->db->execute($sql, [$this->table]); 79 | 80 | $sql = 'CREATE INDEX tmp_4' . rand() . '_idx ON ?f USING BTREE(previous_id)'; 81 | $this->db->execute($sql, [$this->table]); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Importer.php: -------------------------------------------------------------------------------- 1 | db = $db; 25 | $this->fields = $fields; 26 | $this->table = $table; 27 | 28 | if ($isTemp) { 29 | $this->table .= '_xml_importer'; 30 | DbHelper::createTable($this->db, $this->table, $this->fields, $isTemp); 31 | } 32 | } 33 | 34 | protected $rowsPerInsert = 1000; 35 | 36 | public function import(DataSource $reader) 37 | { 38 | $i = 0; 39 | while ($rows = $reader->getRows($this->rowsPerInsert)) { 40 | $this->db->execute($this->getQuery($rows[0]), [$rows]); 41 | ++$i; 42 | if (($i % 100) == 0) { 43 | $this->db->getLogger()->reset(); 44 | } 45 | } 46 | 47 | return $this->table; 48 | } 49 | 50 | private $sqlHeader; 51 | 52 | private function getQuery($rowExample) 53 | { 54 | if (!$this->sqlHeader) { 55 | $fields = []; 56 | foreach ($rowExample as $attribute => $devNull) { 57 | $fields[] = $this->fields[$attribute]['name']; 58 | } 59 | 60 | $headerPart = $this->db->replacePlaceholders('INSERT INTO ?f(?i) VALUES ', [$this->table, $fields]); 61 | 62 | $this->sqlHeader = $headerPart . ' ?v'; 63 | } 64 | 65 | return $this->sqlHeader; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ImporterException.php: -------------------------------------------------------------------------------- 1 | wsdlUrl = $wsdlUrl; 20 | $this->fileDirectory = $fileDirectory; 21 | 22 | FileHelper::ensureIsDirectory($fileDirectory); 23 | FileHelper::ensureIsWritable($fileDirectory); 24 | } 25 | 26 | /** @var SoapResultWrapper */ 27 | private $fileInfoResult = null; 28 | 29 | public function getLastFileInfo() 30 | { 31 | if (!$this->fileInfoResult) { 32 | $this->fileInfoResult = $this->getLastFileInfoRaw(); 33 | } 34 | 35 | return $this->fileInfoResult; 36 | } 37 | 38 | private function getLastFileInfoRaw() 39 | { 40 | $client = new \SoapClient($this->wsdlUrl); 41 | $rawResult = $client->__soapCall('GetLastDownloadFileInfo', []); 42 | 43 | return new SoapResultWrapper($rawResult); 44 | } 45 | 46 | protected function loadFile($fileName, $url) 47 | { 48 | $filePath = $this->fileDirectory . '/' . $fileName; 49 | if (file_exists($filePath)) { 50 | if ($this->isFileSizeCorrect($filePath, $url)) { 51 | return $filePath; 52 | } 53 | 54 | unlink($filePath); 55 | } 56 | 57 | $fp = fopen($filePath, 'w'); 58 | $ch = curl_init($url); 59 | 60 | curl_setopt($ch, CURLOPT_FILE, $fp); 61 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 62 | 63 | curl_exec($ch); 64 | 65 | curl_close($ch); 66 | fclose($fp); 67 | 68 | return $filePath; 69 | } 70 | 71 | protected function wrap($path) 72 | { 73 | $pathToDirectory = Dearchiver::extract($this->fileDirectory, $path); 74 | $this->addVersionId($pathToDirectory); 75 | 76 | return new Directory($pathToDirectory); 77 | } 78 | 79 | private function addVersionId($pathToDirectory) 80 | { 81 | $versionId = $this->getLastFileInfo()->getVersionId(); 82 | file_put_contents($pathToDirectory . '/VERSION_ID_' . $versionId, 'Версия: ' . $versionId); 83 | } 84 | 85 | public function isFileSizeCorrect($filePath, $url) 86 | { 87 | $ch = curl_init($url); 88 | 89 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 90 | curl_setopt($ch, CURLOPT_HEADER, true); 91 | curl_setopt($ch, CURLOPT_NOBODY, true); 92 | 93 | curl_exec($ch); 94 | 95 | $correctSize = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); 96 | 97 | curl_close($ch); 98 | 99 | return (filesize($filePath) == $correctSize); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Loader/InitLoader.php: -------------------------------------------------------------------------------- 1 | getLastFileInfo(); 15 | 16 | return $this->wrap( 17 | $this->loadFile($filesInfo->getInitFileName(), $filesInfo->getInitFileUrl()) 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Loader/SoapResultWrapper.php: -------------------------------------------------------------------------------- 1 | GetLastDownloadFileInfoResult; 14 | $this->versionId = $rawResult->VersionId; 15 | $this->initFileUrl = $rawResult->FiasCompleteXmlUrl; 16 | $this->updateFileUrl = $rawResult->FiasDeltaXmlUrl; 17 | } 18 | 19 | public function getVersionId() 20 | { 21 | return $this->versionId; 22 | } 23 | 24 | public function getUpdateFileUrl() 25 | { 26 | return $this->updateFileUrl; 27 | } 28 | 29 | public function getInitFileUrl() 30 | { 31 | return $this->initFileUrl; 32 | } 33 | 34 | public function getUpdateFileName() 35 | { 36 | $fileName = basename($this->updateFileUrl); 37 | return $this->versionId . '_' . $fileName; 38 | } 39 | 40 | public function getInitFileName() 41 | { 42 | $fileName = basename($this->initFileUrl); 43 | return $this->versionId . '_' . $fileName; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Loader/UpdateLoader.php: -------------------------------------------------------------------------------- 1 | getLastFileInfo(); 15 | 16 | return $this->wrap( 17 | $this->loadFile($filesInfo->getUpdateFileName(), $filesInfo->getUpdateFileUrl()) 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/PlaceStorage.php: -------------------------------------------------------------------------------- 1 | db = $db; 13 | } 14 | 15 | public function findPlace($place) 16 | { 17 | $sql = ' 18 | SELECT p.id, pt.system_name type_system_name 19 | FROM places p 20 | INNER JOIN place_types pt 21 | ON pt.id = p.type_id 22 | WHERE lower(full_title) = lower(?q)' 23 | ; 24 | 25 | return $this->db->execute($sql, [$place])->fetchOneOrFalse(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/RawDataHelper.php: -------------------------------------------------------------------------------- 1 | execute($sql); 31 | } 32 | 33 | public static function cleanHouses(ConnectionInterface $db, $table = 'houses') 34 | { 35 | $inCorrectValues = ['нет', '-', 'стр.', 'стр1']; 36 | 37 | // Если будем импортировать больше половины регионов из фиаса, перенести на сторону PHP. 38 | $db->execute( 39 | "UPDATE ?f 40 | SET number = lower(number), 41 | building = CASE WHEN building IN (?l) THEN NULL ELSE lower(building) END, 42 | structure = CASE WHEN structure IN (?l) THEN NULL ELSE lower(structure) END 43 | WHERE number ~ '[^0-9]+' 44 | OR building ~ '[^0-9]+' 45 | OR structure ~ '[^0-9]+' 46 | ", 47 | [$table, $inCorrectValues, $inCorrectValues] 48 | ); 49 | 50 | // Убираем ложные данные по корпусам и строениям ("1а" и в корпусе и в номере, например) 51 | $db->execute( 52 | "UPDATE ?f 53 | SET building = NULL, 54 | structure = NULL 55 | WHERE number ~ '[^0-9]+' 56 | AND ( 57 | (structure ~ '[^0-9]+' AND number = structure) 58 | OR 59 | (building ~ '[^0-9]+' AND number = building) 60 | ) 61 | ", 62 | [$table] 63 | ); 64 | 65 | // нормализуем адрес по яндексу 66 | $db->execute( 67 | "UPDATE ?f 68 | SET full_number = COALESCE(number, '') 69 | ||COALESCE('к'||building, '') 70 | ||COALESCE('с'||structure, '') 71 | ", 72 | [$table] 73 | ); 74 | } 75 | 76 | public static function updateHousesCount(ConnectionInterface $db) 77 | { 78 | // прописываем данные по домам в address_objects 79 | $db->execute( 80 | "UPDATE address_objects ao 81 | SET house_count = tmp.count, 82 | next_address_level = 0 83 | FROM ( 84 | SELECT address_id, count(*) count 85 | FROM houses GROUP BY 1 86 | ) tmp 87 | WHERE tmp.address_id = ao.address_id 88 | " 89 | ); 90 | } 91 | 92 | public static function updateNextAddressLevelFlag(ConnectionInterface $db) 93 | { 94 | $db->execute( 95 | "UPDATE address_objects ao 96 | SET next_address_level = tmp.address_level 97 | FROM ( 98 | SELECT DISTINCT ON (parent_id) parent_id, address_level 99 | FROM address_objects 100 | WHERE parent_id IS NOT NULL 101 | ORDER BY parent_id, address_level 102 | ) tmp 103 | WHERE tmp.parent_id = address_id 104 | " 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Remover.php: -------------------------------------------------------------------------------- 1 | db = $db; 17 | $this->table = $table; 18 | $this->keyFieldXml = $keyFieldXml; 19 | $this->keyFieldDatabase = $keyFieldDatabase; 20 | } 21 | 22 | public function remove(DataSource $reader) 23 | { 24 | while ($rows = $reader->getRows()) { 25 | $this->removeRows($rows); 26 | } 27 | } 28 | 29 | private function removeRows(array $rows) 30 | { 31 | $ids = []; 32 | 33 | foreach ($rows as $row) { 34 | if (empty($row[$this->keyFieldXml])) { 35 | throw new \LogicException('Не найдено поле: ' . $this->keyFieldXml); 36 | } 37 | 38 | $ids[] = $row[$this->keyFieldXml]; 39 | } 40 | 41 | $this->db->execute('DELETE FROM ?f WHERE ?f IN (?l)', [$this->table, $this->keyFieldDatabase, $ids]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/UpdateLogHelper.php: -------------------------------------------------------------------------------- 1 | execute('INSERT INTO update_log(version_id) VALUES (?q)', [$versionId]); 10 | } 11 | 12 | public static function getLastVersionId(ConnectionInterface $db) 13 | { 14 | return (int) $db->execute('SELECT MAX(version_id) FROM update_log')->fetchResult(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/AddressObjectsUpdaterTest.php: -------------------------------------------------------------------------------- 1 | '5c8b06f1-518e-496e-b683-7bf917e0d70b', 17 | 'AOGUID' => '0c5b2444-70a0-4932-980c-b4dc0d3f02b5', 18 | 'PREVIOUSID' => '0c5b2444-70a0-4932-980c-b4dc0d3f02b5', 19 | 'PARENTGUID' => NULL, 20 | 'FORMALNAME' => 'Москваа', 21 | 'POSTALCODE' => NULL, 22 | 'SHORTNAME' => 'г', 23 | ], 24 | [ 25 | 'AOID' => '00000000-0000-0000-0000-000000000000', 26 | 'AOGUID' => '00000000-0000-0000-0000-000000000000', 27 | 'PREVIOUSID' => '0c5b2444-70a0-4932-980c-b4dc0d3f02b5', 28 | 'PARENTGUID' => '0c5b2444-70a0-4932-980c-b4dc0d3f02b5', 29 | 'FORMALNAME' => 'Все вы, питерские, идиоты какие-то', 30 | 'POSTALCODE' => NULL, 31 | 'SHORTNAME' => 'пл', 32 | ], 33 | ]; 34 | 35 | $this->reader = $this->getReaderMock($this, [$results]); 36 | } 37 | 38 | /** @group slow */ 39 | public function testUpdater() 40 | { 41 | $addressObjectConfig = $this->container->getAddressObjectsImportConfig(); 42 | 43 | $addressObjectConfig['fields']['PREVIOUSID'] = ['name' => 'previous_id', 'type' => 'uuid']; 44 | 45 | $updater = new AddressObjectsUpdater($this->db, $addressObjectConfig['table_name'], $addressObjectConfig['fields']); 46 | $updater->update($this->reader); 47 | 48 | $this->assertEquals( 49 | 'г Москваа', 50 | $this->db->execute( 51 | 'SELECT full_title FROM address_objects WHERE address_id = ?q', 52 | ['0c5b2444-70a0-4932-980c-b4dc0d3f02b5'] 53 | )->fetchResult() 54 | ); 55 | 56 | $this->assertEquals( 57 | 'г Москваа, пл Все вы, питерские, идиоты какие-то', 58 | $this->db->execute( 59 | 'SELECT full_title FROM address_objects WHERE address_id = ?q', 60 | ['00000000-0000-0000-0000-000000000000'] 61 | )->fetchResult() 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/AddressStorageTest.php: -------------------------------------------------------------------------------- 1 | storage = new AddressStorage($this->db); 12 | } 13 | 14 | public function testFindAddress() 15 | { 16 | $this->assertEquals( 17 | '29251dcf-00a1-4e34-98d4-5c47484a36d4', 18 | $this->storage->findAddress('г москва')['address_id'] 19 | ); 20 | 21 | $this->assertEquals( 22 | '77303f7c-452b-4e73-b2b0-cbc59fe636c2', 23 | $this->storage->findAddress('г москва, ул стахановская')['address_id'] 24 | ); 25 | 26 | $this->assertFalse($this->storage->findAddress('Ерунда какая-то, а не адрес.')); 27 | } 28 | 29 | public function testFindHouse() 30 | { 31 | $this->assertEquals( 32 | '841254dc-0074-41fe-99ba-0c8501526c04', 33 | $this->storage->findHouse('г москва, ул стахановская, 16с17')['house_id'] 34 | ); 35 | 36 | $this->assertFalse($this->storage->findHouse('Ерунда какая-то, а не дом')); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/ApiActionAddressCompletionTest.php: -------------------------------------------------------------------------------- 1 | db, 'Нави, Главная б', 50); 10 | $this->assertCount(0, $complete->run()); 11 | } 12 | 13 | public function testAddressCompletion() 14 | { 15 | $complete = new AddressCompletion($this->db, 'г Москва, Ста', 50); 16 | $result = $complete->run(); 17 | 18 | $this->assertCount(4, $result); 19 | $this->assertEquals('г Москва, пр Ставропольский', $result[0]['title']); 20 | $this->assertEquals(0, $result[0]['is_complete']); 21 | } 22 | 23 | public function testHomeCompletion() 24 | { 25 | $complete = new AddressCompletion($this->db, 'г Москва, ул Стахановская, 1', 2); 26 | $result = $complete->run(); 27 | 28 | $this->assertCount(2, $result); 29 | $this->assertEquals('г Москва, ул Стахановская, 1к1', $result[0]['title']); 30 | $this->assertEquals(1, $result[0]['is_complete']); 31 | } 32 | 33 | public function testMaxAddressLevel() 34 | { 35 | $complete = new AddressCompletion($this->db, 'г Москва, Ста', 50, 'region'); 36 | $this->assertEmpty($complete->run()); 37 | 38 | $complete = new AddressCompletion($this->db, 'Моск', 50, 'region'); 39 | $result = $complete->run(); 40 | $this->assertCount(1, $result); 41 | $this->assertTrue($result[0]['is_complete']); 42 | 43 | $complete = new AddressCompletion($this->db, 'Моск', 50, 'street'); 44 | $result = $complete->run(); 45 | $this->assertCount(1, $result); 46 | $this->assertFalse($result[0]['is_complete']); 47 | 48 | $complete = new AddressCompletion($this->db, 'г Москва, ул Стахановская, 1', 2, 'building'); 49 | $result = $complete->run(); 50 | 51 | $this->assertCount(2, $result); 52 | } 53 | 54 | public function testRegion() 55 | { 56 | $complete = new AddressCompletion($this->db, 'Моск', 50, null, [78]); 57 | $this->assertEmpty($complete->run()); 58 | 59 | $complete = new AddressCompletion($this->db, 'Моск', 50, null, [77]); 60 | $this->assertCount(1, $complete->run()); 61 | } 62 | 63 | public function testTags() 64 | { 65 | $completion = new AddressCompletion($this->db, 'Моск', 50); 66 | $result = $completion->run(); 67 | 68 | $this->assertEquals(['address'], $result[0]['tags']); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/ApiActionAddressPostalCodeTest.php: -------------------------------------------------------------------------------- 1 | db, 'Точно отсутствующий в базе адрес'); 10 | $this->assertNull($mapping->run()); 11 | } 12 | 13 | public function testAddressToPostalCodeMapping() 14 | { 15 | $mapping = new AddressPostalCode($this->db, 'г Москва, ал Старших Бобров'); 16 | $this->assertEquals(123456, $mapping->run()); 17 | } 18 | 19 | public function testAddressToPostalCodeMappingWithHomes() 20 | { 21 | // У дома есть свой индекс 22 | $mapping = new AddressPostalCode($this->db, 'г Москва, ул Стахановская, 16с17'); 23 | $this->assertEquals(654321, $mapping->run()); 24 | 25 | // У дома нет своего индекса 26 | $mapping = new AddressPostalCode($this->db, 'г Москва, ул Стахановская, 16с18'); 27 | $this->assertEquals(123456, $mapping->run()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/ApiActionPlaceCompletionTest.php: -------------------------------------------------------------------------------- 1 | db, 'Никому неизвестное место', 50); 10 | $this->assertEmpty($completion->run()); 11 | } 12 | 13 | public function testWithType() 14 | { 15 | $completion = new PlaceCompletion($this->db, 'вокзал Паве', 50); 16 | $this->assertEquals('Павелецкий вокзал', $completion->run()[0]['title']); 17 | } 18 | 19 | public function testWithoutType() 20 | { 21 | $completion = new PlaceCompletion($this->db, 'Павел', 50); 22 | $result = $completion->run(); 23 | 24 | $this->assertEquals('Павелецкий автовокзал', $result[0]['title']); 25 | $this->assertEquals('Павелецкий вокзал', $result[1]['title']); 26 | } 27 | 28 | public function testWithParent() 29 | { 30 | $completion = new PlaceCompletion($this->db, 'пулко', 50); 31 | $result = $completion->run(); 32 | 33 | $this->assertEquals('Пулково аэропорт', $result[0]['title']); 34 | $this->assertEquals(0, $result[0]['is_complete']); 35 | 36 | $completion = new PlaceCompletion($this->db, 'Пулково аэропорт, терминал', 50); 37 | $result = $completion->run(); 38 | 39 | $this->assertCount(3, $result); 40 | $this->assertEquals('Пулково аэропорт, 1 терминал', $result[0]['title']); 41 | $this->assertEquals(1, $result[0]['is_complete']); 42 | 43 | $completion = new PlaceCompletion($this->db, 'Пулково аэропорт, но', 50); 44 | $result = $completion->run(); 45 | 46 | $this->assertEquals('Пулково аэропорт, новый терминал', $result[0]['title']); 47 | $this->assertEquals(1, $result[0]['is_complete']); 48 | } 49 | 50 | public function testTags() 51 | { 52 | $completion = new PlaceCompletion($this->db, 'пулко', 50); 53 | $result = $completion->run(); 54 | 55 | $this->assertEquals(['place', 'airport'], $result[0]['tags']); 56 | 57 | $completion = new PlaceCompletion($this->db, 'Павел', 50); 58 | $result = $completion->run(); 59 | 60 | $this->assertEquals(['place', 'bus_terminal'], $result[0]['tags']); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/ApiActionPostalCodeLocationTest.php: -------------------------------------------------------------------------------- 1 | db, '234325432'); 10 | $this->assertEmpty($mapping->run()); 11 | } 12 | 13 | public function testPostalCodeToAddressMapping() 14 | { 15 | $mapping = new PostalCodeLocation($this->db, '123456'); 16 | $parts = $mapping->run(); 17 | 18 | $this->assertCount(1, $parts); 19 | $this->assertEquals($parts[0]['title'], 'г Москва'); 20 | 21 | $mapping = new PostalCodeLocation($this->db, '1234567'); 22 | $parts = $mapping->run(); 23 | 24 | $this->assertCount(2, $parts); 25 | $this->assertEquals($parts[0]['title'], 'г Москва'); 26 | $this->assertEquals($parts[0]['address_level'], 'region'); 27 | $this->assertEquals($parts[1]['title'], 'р-н Мытищинский'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/ApiActionValidationTest.php: -------------------------------------------------------------------------------- 1 | db, 'Непонятный адрес'); 10 | $result = $validate->run(); 11 | 12 | $this->assertEmpty($result); 13 | } 14 | 15 | public function testIncomplete() 16 | { 17 | $validate = new Validation($this->db, 'г москва, ул стахановская'); 18 | $result = $validate->run()[0]; 19 | 20 | $this->assertFalse($result['is_complete']); 21 | } 22 | 23 | public function testValid() 24 | { 25 | $validate = new Validation($this->db, 'г москва, ул стахановская, 16с17'); 26 | $result = $validate->run()[0]; 27 | 28 | $this->assertTrue($result['is_complete']); 29 | $this->assertTrue(in_array('address', $result['tags'])); 30 | $this->assertFalse(in_array('place', $result['tags'])); 31 | 32 | $validate = new Validation($this->db, 'Павелецкий автовокзал'); 33 | $result = $validate->run()[0]; 34 | 35 | $this->assertTrue($result['is_complete']); 36 | $this->assertTrue(in_array('place', $result['tags'])); 37 | $this->assertFalse(in_array('address', $result['tags'])); 38 | $this->assertTrue(in_array('bus_terminal', $result['tags'])); 39 | } 40 | 41 | public function testZeroLevel() 42 | { 43 | $validate = new Validation($this->db, 'г москва'); 44 | $result = $validate->run()[0]; 45 | 46 | $this->assertFalse($result['is_complete']); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/ApiControllerTest.php: -------------------------------------------------------------------------------- 1 | prepareHttpClient('http://' . $this->container->getHost()); 13 | } 14 | 15 | public function testFormat() 16 | { 17 | $this->describe('Проверяем выдачу ошибки при отсутствии callback параметра если выбран jsonp') 18 | ->loadPage('/api/complete.jsonp/?pattern=Москва', 400) 19 | ->ensurePageContains('Отсутствует обязательный параметр: callback.') 20 | ->describe('Проверяем нормальную работу jsonp') 21 | ->loadPage('/api/complete.jsonp/?pattern=Москва&callback=testCallback', 200) 22 | ->ensurePageContains('testCallback(') 23 | ->describe('Проверяем нормальную работу json') 24 | ->loadPage('/api/complete.json/?pattern=Москва', 200) 25 | ->ensureResponse(json_decode($this->curResponse->getContent())->items) 26 | ; 27 | } 28 | 29 | 30 | public function testComplete() 31 | { 32 | $tooBigLimit = $this->container->getMaxCompletionLimit() + 1; 33 | $this->describe('Проверяем отсечение слишком большого лимита.') 34 | ->loadPage('/api/complete/?pattern=Москва&limit=' . $tooBigLimit, 400) 35 | ->describe('Проверяем автоподстановку для адресного объекта.') 36 | ->loadPage('/api/complete/?pattern=Моск', 200) 37 | ->ensureResponse(json_decode($this->curResponse->getContent())->items[0]->title == 'Московский вокзал') 38 | ->ensureResponse(json_decode($this->curResponse->getContent())->items[1]->title == 'г Москва') 39 | ->describe('Проверяем автоподстановку с ограничением максимально допустимого уровня') 40 | ->loadPage('/api/complete/?pattern=г Москва, Ста') 41 | ->ensureResponse(json_decode($this->curResponse->getContent())->items) 42 | ->loadPage('/api/complete/?pattern=г Москва, Ста&max_address_level=region') 43 | ->ensureResponse(!json_decode($this->curResponse->getContent())->items) 44 | ->loadPage('/api/complete/?pattern=Моск&max_address_level=region') 45 | ->ensureResponse(json_decode($this->curResponse->getContent())->items) 46 | ->describe('Проверяем автоподстановку для дома.') 47 | ->loadPage('/api/complete/?pattern=г Москва, ул Стахановская, 1') 48 | ->ensureResponse(json_decode($this->curResponse->getContent())->items[0]->title == 'г Москва, ул Стахановская, 1к1') 49 | ->describe('Проверяем автоподстановку для места') 50 | ->loadPage('/api/complete/?pattern=Паве') 51 | ->ensureResponse(count(json_decode($this->curResponse->getContent())->items) == 2) 52 | ; 53 | } 54 | 55 | public function testValidate() 56 | { 57 | $this->describe('Проверяем выдачу ошибки при отсутствии необходимого параметра') 58 | ->loadPage('/api/validate/', 400) 59 | ->describe('Проверяем валидацию корректного адресного объекта') 60 | ->loadPage('/api/validate/?pattern=г Москва', 200) 61 | ->ensureResponse(!json_decode($this->curResponse->getContent())->items[0]->is_complete) 62 | ->describe('Проверяем валидацию некорректного адресного объекта') 63 | ->loadPage('/api/validate/?pattern=г Мосва', 200) 64 | ->ensureResponse(!json_decode($this->curResponse->getContent())->items) 65 | ->describe('Проверяем валидацию корректного дома') 66 | ->loadPage('/api/validate/?pattern=г Москва, ул Стахановская, 1к1', 200) 67 | ->ensureResponse(json_decode($this->curResponse->getContent())->items[0]->is_complete) 68 | ->describe('Проверяем валидацию некорректного дома') 69 | ->loadPage('/api/validate/?pattern=г Москва, ул Стахановская, 1324326с1', 200) 70 | ->ensureResponse(!json_decode($this->curResponse->getContent())->items) 71 | ; 72 | } 73 | 74 | public function testMapping() 75 | { 76 | $this->describe('Проверяем выдачу ошибки при отсутствии необходимого параметра') 77 | ->loadPage('/api/address_postal_code', 400) 78 | ->loadPage('/api/postal_code_location', 400) 79 | ->describe('Проверяем выдачу корректного почтового индекса') 80 | ->loadPage('/api/address_postal_code/?address=г Москва, ул Стахановская', 200) 81 | ->ensureResponse(json_decode($this->curResponse->getContent())->postal_code == 123456) 82 | ->describe('Проверяем выдачу адресов') 83 | ->loadPage('/api/postal_code_location/?postal_code=123456', 200) 84 | ->ensureResponse(count(json_decode($this->curResponse->getContent())->address_parts) == 1) 85 | ; 86 | } 87 | 88 | public function testNotFound() 89 | { 90 | $this->describe('Проверяем выдачу 404 ошибки при ошибочном uri') 91 | ->loadPage('/totally/wrong/destination', 404) 92 | ; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/DbHelperTest.php: -------------------------------------------------------------------------------- 1 | container->getDatabaseName(), __DIR__ . '/resources/inCorrectScript.sql'); 12 | } 13 | 14 | public function testRun() 15 | { 16 | DbHelper::runFile($this->container->getDatabaseName(), __DIR__ . '/resources/correctScript.sql'); 17 | $this->assertEquals(2, $this->db->execute('SELECT * FROM "correctTable"')->getNumRows()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/DearchiverTest.php: -------------------------------------------------------------------------------- 1 | fileDirectory = __DIR__ . '/file_directory'; 16 | $this->testTxtFile = $this->fileDirectory . '/dearchiverTestFile.txt'; 17 | $this->testRarFile = $this->fileDirectory . '/dearchiverTestFile.rar'; 18 | 19 | file_put_contents($this->testTxtFile, $text); 20 | 21 | $cmd = 'rar a ' 22 | . escapeshellarg($this->testRarFile) 23 | . ' ' 24 | . escapeshellarg($this->testTxtFile) 25 | . ' 2>&1' 26 | ; 27 | 28 | exec($cmd, $output, $result); 29 | 30 | if ($result !== 0) { 31 | throw new \Exception('Ошибка архивации: ' . implode("\n", $output)); 32 | } 33 | } 34 | 35 | protected function tearDown() 36 | { 37 | $this->cleanUpFileDirectory(); 38 | } 39 | 40 | /** @expectedException \FileSystem\FileException */ 41 | public function testBadFile() 42 | { 43 | Dearchiver::extract($this->fileDirectory, 'bad_file'); 44 | } 45 | 46 | /** @expectedException \FileSystem\FileException */ 47 | public function testBadDirectory() 48 | { 49 | Dearchiver::extract('bad_directory', $this->testRarFile); 50 | } 51 | 52 | private $extractedFiles; 53 | 54 | public function testNormalFile() 55 | { 56 | $this->extractedFiles = Dearchiver::extract($this->fileDirectory, $this->testRarFile); 57 | $this->assertEquals( 58 | md5_file($this->testTxtFile), 59 | md5_file($this->extractedFiles . '/' . basename($this->testTxtFile)) 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/DirectoryTest.php: -------------------------------------------------------------------------------- 1 | directory = new Directory(__DIR__ . '/resources/directoryTest'); 13 | } 14 | 15 | public function testGet() 16 | { 17 | $this->assertEquals( 18 | $this->directory->getPath() . '/AS_ADDROBJ_20131221_5316e71a-a8d8-49df-b17c-66d3a981906a.XML', 19 | $this->directory->getAddressObjectFile() 20 | ); 21 | 22 | $this->assertEquals( 23 | $this->directory->getPath() . '/AS_HOUSE_20131221_bccfd0d0-af7a-49db-8401-df23dc3d2efa.XML', 24 | $this->directory->getHouseFile() 25 | ); 26 | 27 | $this->assertEquals( 28 | $this->directory->getPath() . '/AS_DEL_ADDROBJ_20131221_8a0076a7-1f52-4423-8fc6-58dec367832b.XML', 29 | $this->directory->getDeletedAddressObjectFile() 30 | ); 31 | 32 | $this->assertEquals( 33 | $this->directory->getPath() . '/AS_DEL_HOUSE_20131221_ea93b12d-129d-46a0-9cfb-429b64a28873.XML', 34 | $this->directory->getDeletedHouseFile() 35 | ); 36 | } 37 | 38 | /** 39 | * @expectedException \FileSystem\FileException 40 | * @expectedExceptionMessage Файл с префиксом 41 | */ 42 | public function testFileNotFound() 43 | { 44 | $this->directory = new Directory(__DIR__); 45 | $this->directory->getHouseFile(); 46 | } 47 | 48 | /** @expectedException \FileSystem\FileException */ 49 | public function testDirectoryNotFound() 50 | { 51 | new Directory('badDir'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/HousesUpdaterTest.php: -------------------------------------------------------------------------------- 1 | 'a64330e3-7a41-41ee-a8a2-41db8693c584', 16 | 'HOUSEGUID' => 'a64330e3-7a41-41ee-a8a2-41db8693c584', 17 | 'PREVIOUSID' => 'a64330e3-7a41-41ee-a8a2-41db8693c584', 18 | 'AOGUID' => '77303f7c-452b-4e73-b2b0-cbc59fe636c5', 19 | 'HOUSENUM' => '02', 20 | 'BUILDNUM' => '123', 21 | 'STRUCNUM' => 'нет' 22 | ], 23 | [ 24 | 'HOUSEID' => '00000000-0000-0000-0000-000000000000', 25 | 'HOUSEGUID' => '00000000-0000-0000-0000-000000000000', 26 | 'PREVIOUSID' => '00000000-0000-0000-0000-000000000000', 27 | 'AOGUID' => '77303f7c-452b-4e73-b2b0-cbc59fe636c5', 28 | 'HOUSENUM' => '1', 29 | 'BUILDNUM' => '2', 30 | 'STRUCNUM' => '3' 31 | ], 32 | ]; 33 | 34 | $this->reader = $this->getReaderMock($this, [$results]); 35 | } 36 | 37 | /** @group slow */ 38 | public function testUpdater() 39 | { 40 | $countBeforeUpdate = (int) $this->db->execute( 41 | 'SELECT house_count FROM address_objects WHERE id = ?q', 42 | ['0c5b2444-70a0-4932-980c-b4dc0d3f02b5'] 43 | )->fetchResult(); 44 | 45 | $housesConfig = $this->container->getHousesImportConfig(); 46 | 47 | $housesConfig['fields']['PREVIOUSID'] = ['name' => 'previous_id', 'type' => 'uuid']; 48 | 49 | $updater = new HousesUpdater($this->db, $housesConfig['table_name'], $housesConfig['fields']); 50 | $updater->update($this->reader); 51 | 52 | $this->assertEquals( 53 | '02к123', 54 | $this->db->execute( 55 | 'SELECT full_number FROM houses WHERE house_id = ?q', 56 | ['a64330e3-7a41-41ee-a8a2-41db8693c584'] 57 | )->fetchResult() 58 | ); 59 | 60 | $this->assertEquals( 61 | '1к2с3', 62 | $this->db->execute( 63 | 'SELECT full_number FROM houses WHERE house_id = ?q', 64 | ['00000000-0000-0000-0000-000000000000'] 65 | )->fetchResult() 66 | ); 67 | 68 | $this->assertEquals( 69 | $countBeforeUpdate + 2, 70 | $this->db->execute( 71 | 'SELECT house_count FROM address_objects WHERE address_id = ?q', 72 | ['77303f7c-452b-4e73-b2b0-cbc59fe636c5'] 73 | )->fetchResult() 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/ImporterTest.php: -------------------------------------------------------------------------------- 1 | table = 'test_table'; 11 | } 12 | 13 | /** 14 | * @expectedException \ImporterException 15 | * @expectedExceptionMessage таблица 16 | */ 17 | public function testEmptyTable() 18 | { 19 | new Importer($this->db, '', ['one', 'two', 'three']); 20 | } 21 | 22 | /** 23 | * @expectedException \ImporterException 24 | * @expectedExceptionMessage поля 25 | */ 26 | public function testEmptyFields() 27 | { 28 | new Importer($this->db, 'some_table_name', []); 29 | } 30 | 31 | public function testImport() 32 | { 33 | $results = [ 34 | [ 35 | ['id' => 1, 'madeIn' => 'China', 'title' => 'Phone'], 36 | ['id' => 2, 'madeIn' => 'USA', 'title' => 'Chicken wings'], 37 | ['id' => 3, 'madeIn' => 'Russia', 'title' => 'Topol-M'], 38 | ], 39 | [ 40 | ['id' => 4, 'madeIn' => 'France', 'title' => 'Wine'], 41 | ['id' => 5, 'madeIn' => 'Germany', 'title' => 'Audi'], 42 | ['id' => 6, 'madeIn' => 'Denmark', 'title' => 'Tulip'], 43 | ], 44 | ]; 45 | 46 | $reader = $this->getReaderMock($this, $results); 47 | $fields = [ 48 | 'madeIn' => ['name' => 'two'], 49 | 'id' => ['name' => 'one'], 50 | 'title' => ['name' => 'three'], 51 | ]; 52 | 53 | $importer = new Importer($this->db, $this->table, $fields); 54 | $tableName = $importer->import($reader); 55 | 56 | $this->assertEquals( 57 | 6, 58 | $this->db->execute('SELECT COUNT(*) count FROM ?F', [$tableName])->fetchResult() 59 | ); 60 | 61 | $this->assertEquals( 62 | 'USA', 63 | $this->db->execute("SELECT two FROM ?F WHERE one = '2'", [$tableName])->fetchResult() 64 | ); 65 | 66 | $this->assertEquals( 67 | 'Tulip', 68 | $this->db->execute("SELECT three FROM ?F WHERE one = '6'", [$tableName])->fetchResult() 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/IntervalGeneratorTest.php: -------------------------------------------------------------------------------- 1 | getGenerator(); 14 | $reader->getRows(0); 15 | } 16 | 17 | public function testGeneratedRow() 18 | { 19 | $reader = $this->getGenerator( 20 | [ 21 | [['title' => 'Title 1', 'start' => 5, 'end' => 9, 'type' => 1]], 22 | ] 23 | ); 24 | 25 | $rows = $reader->getRows(); 26 | 27 | $this->assertEquals(5, count($rows)); 28 | $this->assertEquals('Title 1', $rows[3]['title']); 29 | $this->assertEquals(5, $rows[0]['result']); 30 | $this->assertEquals(9, $rows[4]['result']); 31 | } 32 | 33 | public function testGetRowsWithCount() 34 | { 35 | $reader = $this->getGenerator( 36 | [ 37 | [['title' => 'Title 1', 'start' => 5, 'end' => 9, 'type' => 1]], 38 | [['title' => 'Title 2', 'start' => 10, 'end' => 14, 'type' => 2]], 39 | [['title' => 'Title 3', 'start' => 15, 'end' => 20, 'type' => 3]], 40 | ] 41 | ); 42 | 43 | // Выборка прервана на getRowsFromInterval 44 | $result1 = $reader->getRows(3); 45 | // Выборка должна прерваться в конце второй строки 46 | $result2 = $reader->getRows(5); 47 | // Тут только по 3-й строке 48 | $result3 = $reader->getRows(); 49 | 50 | // Смотрим количество 51 | $this->assertEquals(3, count($result1)); 52 | $this->assertEquals(5, count($result2)); 53 | $this->assertEquals(3, count($result3)); 54 | 55 | // Проверяем значения на стыках 56 | $this->assertEquals(7, $result1[2]['result']); 57 | $this->assertEquals(8, $result2[0]['result']); 58 | $this->assertEquals(14, $result2[4]['result']); 59 | $this->assertEquals(15, $result3[0]['result']); 60 | } 61 | 62 | /** 63 | * @dataProvider provider 64 | */ 65 | public function testGeneration($correctResult, $readerData) 66 | { 67 | $reader = $this->getGenerator([$readerData]); 68 | $this->assertEquals($correctResult, count($reader->getRows())); 69 | } 70 | 71 | public function provider() 72 | { 73 | return [ 74 | [ 75 | 3, 76 | [ 77 | ['title' => 'Title', 'start' => 9, 'end' => 15, 'type' => 2], 78 | ], 79 | ], 80 | [ 81 | 4, 82 | [ 83 | ['title' => 'Title', 'start' => 10, 'end' => 16, 'type' => 2], 84 | ], 85 | ], 86 | [ 87 | 3, 88 | [ 89 | ['title' => 'Title', 'start' => 8, 'end' => 14, 'type' => 3], 90 | ], 91 | ], 92 | [ 93 | 4, 94 | [ 95 | ['title' => 'Title', 'start' => 9, 'end' => 15, 'type' => 3], 96 | ], 97 | ], 98 | ]; 99 | } 100 | 101 | public function getGenerator($results = []) 102 | { 103 | return new IntervalGenerator($this->getReaderMock($this, $results), 'start', 'end', 'type', 'result'); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/RawDataHelperTest.php: -------------------------------------------------------------------------------- 1 | addressObjectTable = 'address_objects'; 13 | $this->housesTable = 'houses'; 14 | 15 | $this->db->execute('TRUNCATE TABLE address_objects CASCADE'); 16 | $this->db->execute('TRUNCATE TABLE houses CASCADE'); 17 | 18 | $addressObjects = [ 19 | ['5c8b06f1-518e-496e-b683-7bf917e0d70b', '0c5b2444-70a0-4932-980c-b4dc0d3f02b5', NULL, 'Москва', NULL, 'г'], 20 | ['afdda482-42ae-45d3-9af1-61ac6da41105', '0ecde158-a58f-43af-9707-aa6dd3484b56', '0c5b2444-70a0-4932-980c-b4dc0d3f02b5', 'Тверская', NULL, 'ул'], 21 | ]; 22 | $this->db->execute( 23 | 'INSERT INTO ?f ("id", "address_id", "parent_id", "title", "postal_code", "prefix") 24 | VALUES ?v', 25 | [$this->addressObjectTable, $addressObjects] 26 | ); 27 | 28 | $houses = [ 29 | ['a64330e3-7a41-41ee-a8a2-41db8693c584', 'a64330e3-7a41-41ee-a8a2-41db8693c584', '0ecde158-a58f-43af-9707-aa6dd3484b56', '02', '1', 'нет'], 30 | ['b3ace9e8-dead-4e2c-9c56-e524aef28082', '4fd3b082-34bf-4ad9-8f27-c5c92952554c', '0ecde158-a58f-43af-9707-aa6dd3484b56', '02a', '02a', null], 31 | ]; 32 | $this->db->execute( 33 | 'INSERT INTO ?f ("id", "house_id", "address_id", "number", "structure", "building") 34 | VALUES ?v', 35 | [$this->housesTable, $houses] 36 | ); 37 | } 38 | 39 | public function testCleanAddressObjects() 40 | { 41 | RawDataHelper::cleanAddressObjects($this->db); 42 | 43 | $this->assertEquals( 44 | 'г Москва', 45 | $this->db->execute( 46 | "SELECT full_title FROM ?f WHERE id = '5c8b06f1-518e-496e-b683-7bf917e0d70b'", 47 | [$this->addressObjectTable] 48 | )->fetchResult() 49 | ); 50 | 51 | $this->assertEquals( 52 | 'г Москва, ул Тверская', 53 | $this->db->execute( 54 | "SELECT full_title FROM ?f WHERE id = 'afdda482-42ae-45d3-9af1-61ac6da41105'", 55 | [$this->addressObjectTable] 56 | )->fetchResult() 57 | ); 58 | 59 | $this->assertEquals( 60 | 0, 61 | $this->db->execute( 62 | "SELECT level FROM ?f WHERE id = '5c8b06f1-518e-496e-b683-7bf917e0d70b'", 63 | [$this->addressObjectTable] 64 | )->fetchResult() 65 | ); 66 | 67 | $this->assertEquals( 68 | 1, 69 | $this->db->execute( 70 | "SELECT level FROM ?f WHERE id = 'afdda482-42ae-45d3-9af1-61ac6da41105'", 71 | [$this->addressObjectTable] 72 | )->fetchResult() 73 | ); 74 | } 75 | 76 | public function testCleanHouses() 77 | { 78 | RawDataHelper::cleanHouses($this->db, $this->housesTable); 79 | 80 | $this->assertEquals( 81 | 1, 82 | $this->db->execute( 83 | 'SELECT COUNT(*) FROM ?f WHERE building IS NULL AND id = ?q', 84 | [$this->housesTable, 'a64330e3-7a41-41ee-a8a2-41db8693c584'] 85 | )->fetchResult() 86 | ); 87 | 88 | $this->assertEquals( 89 | 1, 90 | $this->db->execute( 91 | 'SELECT COUNT(*) FROM ?f WHERE building IS NULL AND structure IS NULL AND id = ?q', 92 | [$this->housesTable, 'b3ace9e8-dead-4e2c-9c56-e524aef28082'] 93 | )->fetchResult() 94 | ); 95 | 96 | $this->assertEquals( 97 | 1, 98 | $this->db->execute( 99 | 'SELECT COUNT(*) FROM ?f WHERE full_number = ?q AND id = ?q', 100 | [$this->housesTable, '02с1', 'a64330e3-7a41-41ee-a8a2-41db8693c584'] 101 | )->fetchResult() 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/RemoverTest.php: -------------------------------------------------------------------------------- 1 | db = $this->container->getDb(); 15 | $this->table = 'test_table'; 16 | 17 | $results = []; 18 | for ($i = 1; $i < 200; ++$i) { 19 | $results[] = ['xmlId' => $i]; 20 | } 21 | $this->reader = $this->getReaderMock($this, [$results]); 22 | 23 | $this->db->execute('DROP TABLE IF EXISTS ?f', [$this->table]); 24 | $this->db->execute('CREATE TEMP TABLE ?f (id integer)', [$this->table]); 25 | $this->db->execute('INSERT INTO ?f SELECT generate_series(0,499)', [$this->table]); 26 | } 27 | 28 | /** 29 | * @expectedException \LogicException 30 | * @expectedExceptionMessage Не найдено 31 | */ 32 | public function testRemoveWithBadParams() 33 | { 34 | $remover = new Remover($this->db, $this->table, 'fakeKey', 'keyFake'); 35 | $remover->remove($this->reader); 36 | } 37 | 38 | public function testRemove() 39 | { 40 | $this->assertEquals('500', $this->db->execute('SELECT COUNT(*) FROM ?f', [$this->table])->fetchResult()); 41 | 42 | $remover = new Remover($this->db, $this->table, 'xmlId', 'id'); 43 | $remover->remove($this->reader); 44 | 45 | $this->assertEquals('301', $this->db->execute('SELECT COUNT(*) FROM ?f', [$this->table])->fetchResult()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/TestAbstract.php: -------------------------------------------------------------------------------- 1 | container = new Container(); 20 | 21 | if (!static::$dumpManager) { 22 | static::$dumpManager = new DatabaseDumpManager(null, $this->container->getDbUri()); 23 | } 24 | 25 | static::$dumpManager->restore('init', function () { 26 | static::$dumpManager->clean(false); 27 | exec('php ' . __DIR__ . '/../cli/init-db.php'); 28 | }); 29 | 30 | $this->db = $this->container->getDb(); 31 | } 32 | 33 | public static function cleanUpFileDirectory() 34 | { 35 | static::removeFilesInDirectory(__DIR__ . '/file_directory'); 36 | } 37 | 38 | private static function removeFilesInDirectory($directoryPath) 39 | { 40 | $files = scandir($directoryPath); 41 | foreach ($files as $file) { 42 | if ($file == '.' || $file == '..') { 43 | continue; 44 | } 45 | 46 | $filePath = $directoryPath . '/' . $file; 47 | 48 | if (is_dir($filePath)) { 49 | static::removeFilesInDirectory($filePath); 50 | rmdir($filePath); 51 | } else { 52 | unlink($filePath); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * @param \PHPUnit_Framework_TestCase $testCase 59 | * @param array $results 60 | * @return XmlReader 61 | */ 62 | public function getReaderMock(\PHPUnit_Framework_TestCase $testCase, array $results) 63 | { 64 | $result = new PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls(array_merge($results, [])); 65 | $reader = $testCase->getMockBuilder('\DataSource\XmlReader') 66 | ->disableOriginalConstructor() 67 | ->getMock() 68 | ; 69 | 70 | $reader->expects(static::any()) 71 | ->method('getRows') 72 | ->will($result) 73 | ; 74 | 75 | return $reader; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/UpdateLoaderTest.php: -------------------------------------------------------------------------------- 1 | fileDirectory = __DIR__ . '/file_directory'; 13 | 14 | if (!is_dir($this->fileDirectory)) { 15 | mkdir($this->fileDirectory); 16 | } 17 | 18 | $information = $this->getInformationAboutCurrentUpdateFile(); 19 | @unlink($this->fileDirectory . '/' . $information['version'] . '_fias_delta_xml.rar'); 20 | } 21 | 22 | protected function tearDown() 23 | { 24 | $this->cleanUpFileDirectory(); 25 | } 26 | 27 | public function testLoad() 28 | { 29 | $loader = new UpdateLoader($this->container->getWsdlUrl(), $this->fileDirectory); 30 | $filesCount = count(scandir($loader->load()->getPath())); 31 | $this->assertGreaterThan(16, $filesCount); 32 | } 33 | 34 | public function testReWritingBadFile() 35 | { 36 | $message = 'Really bad file'; 37 | $filePath = $this->fileDirectory 38 | . '/' 39 | . $this->getInformationAboutCurrentUpdateFile()['version'] 40 | . '_fias_delta_xml.rar' 41 | ; 42 | 43 | file_put_contents($filePath, $message); 44 | 45 | $loader = new UpdateLoader($this->container->getWsdlUrl(), $this->fileDirectory); 46 | $loader->load(); 47 | 48 | $this->assertTrue(strlen($message) != filesize($filePath)); 49 | } 50 | 51 | public function testNoRewritingGoodFile() 52 | { 53 | $loader = new UpdateLoader($this->container->getWsdlUrl(), $this->fileDirectory); 54 | $loader->load(); 55 | 56 | $filePath = $this->fileDirectory 57 | . '/' 58 | . $this->getInformationAboutCurrentUpdateFile()['version'] 59 | . '_fias_delta_xml.rar' 60 | ; 61 | 62 | $this->assertTrue($loader->isFileSizeCorrect($filePath, $this->getInformationAboutCurrentUpdateFile()['url'])); 63 | } 64 | 65 | private $updateInformation = []; 66 | 67 | private function getInformationAboutCurrentUpdateFile() 68 | { 69 | if (!$this->updateInformation) { 70 | try { 71 | $client = new \SoapClient($this->container->getWsdlUrl()); 72 | $filesInfo = $client->__soapCall('GetLastDownloadFileInfo', []); 73 | } catch (SoapFault $e) { 74 | $this->markTestSkipped($e->getMessage()); 75 | return []; // IDE calmer, markTestSkipped throws an exception 76 | } 77 | 78 | $ch = curl_init($filesInfo->GetLastDownloadFileInfoResult->FiasDeltaXmlUrl); 79 | 80 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 81 | curl_setopt($ch, CURLOPT_HEADER, true); 82 | curl_setopt($ch, CURLOPT_NOBODY, true); 83 | 84 | curl_exec($ch); 85 | 86 | $this->updateInformation = [ 87 | 'url' => $filesInfo->GetLastDownloadFileInfoResult->FiasDeltaXmlUrl, 88 | 'version' => $filesInfo->GetLastDownloadFileInfoResult->VersionId, 89 | 'file_size' => curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD), 90 | ]; 91 | 92 | curl_close($ch); 93 | } 94 | 95 | return $this->updateInformation; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/UpdateLogHelperTest.php: -------------------------------------------------------------------------------- 1 | db, 100000); 8 | 9 | $this->assertEquals( 10 | 100000, 11 | $this->db->execute('SELECT MAX(version_id) FROM update_log')->fetchResult() 12 | ); 13 | } 14 | 15 | public function testGetLastVersionId() 16 | { 17 | $values = [ 18 | [12], 19 | [18], 20 | [180], 21 | ]; 22 | $this->db->execute('INSERT INTO update_log(version_id) VALUES ?v', [$values]); 23 | 24 | $this->assertEquals( 25 | 180, 26 | UpdateLogHelper::getLastVersionId($this->db) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/XmlTest.php: -------------------------------------------------------------------------------- 1 | 'available', 'type' => 'eq', 'value' => 1], 14 | ['field' => 'madeIn', 'type' => 'in', 'value' => ['USA', 'China', 'Germany', 'Rwanda']], 15 | ['field' => 'title', 'type' => 'in', 'value' => []], 16 | ['field' => 'id', 'type' => 'nin', 'value' => [6]], 17 | [ 18 | 'field' => 'id', 19 | 'type' => 'hash', 20 | 'value' => [1 => true, 2 => true, 3 => true, 4 => true, 5 => true, 6 => true, 7 => false] 21 | ], 22 | ]; 23 | 24 | $this->reader = new XmlReader( 25 | __DIR__ . '/resources/readerTest.xml', 26 | 'Computer', 27 | [ 28 | 'id', 29 | 'madeIn', 30 | 'fakeAttribute' 31 | ], 32 | $filters 33 | ); 34 | } 35 | 36 | public function testRead() 37 | { 38 | $rows = $this->reader->getRows(); 39 | 40 | $this->assertEquals(2, count($rows)); 41 | $this->assertEquals('USA', $rows[1]['madeIn']); 42 | $this->assertEquals(null, $rows[0]['fakeAttribute']); 43 | $this->assertTrue(!isset($rows[0]['title'])); 44 | } 45 | 46 | public function testReadWithCount() 47 | { 48 | $this->assertEquals(1, count($this->reader->getRows(1))); 49 | $this->assertEquals(1, count($this->reader->getRows(1))); 50 | $this->assertEquals(0, count($this->reader->getRows(1))); 51 | } 52 | 53 | /** 54 | * @expectedException \LogicException 55 | * @expectedExceptionMessage количества 56 | */ 57 | public function testBadCount() 58 | { 59 | $this->reader->getRows(0); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/resources/correctScript.sql: -------------------------------------------------------------------------------- 1 | START TRANSACTION; 2 | 3 | DROP TABLE IF EXISTS "correctTable"; 4 | CREATE TABLE "correctTable" (id SERIAL, title VARCHAR); 5 | 6 | INSERT INTO "correctTable" (id, title) 7 | VALUES (1, 'test1'), (2, 'test2'); 8 | 9 | COMMIT; 10 | -------------------------------------------------------------------------------- /tests/resources/directoryTest/AS_ADDROBJ_20131221_5316e71a-a8d8-49df-b17c-66d3a981906a.XML: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/resources/directoryTest/AS_DEL_ADDROBJ_20131221_8a0076a7-1f52-4423-8fc6-58dec367832b.XML: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/resources/directoryTest/AS_DEL_HOUSE_20131221_ea93b12d-129d-46a0-9cfb-429b64a28873.XML: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/resources/directoryTest/AS_HOUSE_20131221_bccfd0d0-af7a-49db-8401-df23dc3d2efa.XML: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/resources/inCorrectScript.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM "totallyInCorrectTableName"; 2 | -------------------------------------------------------------------------------- /tests/resources/load.csv: -------------------------------------------------------------------------------- 1 | e8562dcc-8e4e-4673-86cc-467f5705f5fb г Москва, Пар г Москва, ул Парковая 9-я, 1 г Москва, ул Парковая 9-я, 11а 2 | e3a4f8a1-9bbd-4528-905f-8d0eb5a10733 г Москва, Хор г Москва, проезд Хорошевский 2-й, 7 г Москва, проезд Хорошевский 2-й, 7с1 3 | cb2a1efe-dcee-4f0f-9d4c-5a703b9550f6 г Москва, Мож г Москва, ш Можайское, 4 г Москва, ш Можайское, 4к1с2 4 | 9c3e9392-0324-4d21-9cf5-70076f1b5e15 г Москва, Лен г Москва, пр-кт Ленинградский, 7 г Москва, пр-кт Ленинградский, 74к6 5 | bae2a8c8-1bee-44d5-800e-ffb906d8460f г Москва, Хло г Москва, ул Хлобыстова, 2 г Москва, ул Хлобыстова, 20к3 6 | 13dc3c29-418f-4789-a235-647ab5a58503 г Москва, Шип г Москва, проезд Шипиловский, 2 г Москва, проезд Шипиловский, 28с2 7 | 5d835f2e-b03d-40e3-bc8b-1df81f6f36c4 г Москва, Кав г Москва, б-р Кавказский, 1 г Москва, б-р Кавказский, 17а 8 | 2d846b8c-d2d6-4c48-aa02-acea32de48e2 г Москва, Гол г Москва, ул Гольяновская, 4 г Москва, ул Гольяновская, 4ас7 9 | 7eb9ee15-936a-4cc5-b191-e1d601e60d04 г Москва, Пер г Москва, пер Переведеновский, 1 г Москва, пер Переведеновский, 13с5 10 | 25d692b4-0b3e-406f-8936-51207019ae2f г Москва, Кля г Москва, ул Клязьминская, 2 г Москва, ул Клязьминская, 21а 11 | 7879822d-005a-4499-8924-465399eb3e40 г Москва, Сим г Москва, наб Симоновская, 1 г Москва, наб Симоновская, 1с49 12 | 1735120d-e814-4ca2-bf8d-5f53b42ea541 г Москва, Тве г Москва, ул Тверская-Ямская 3-Я, 2 г Москва, ул Тверская-Ямская 3-Я, 22с5 13 | e8a84c63-3907-41a1-b3da-f338a94be538 г Москва, Кас г Москва, ул Каскадная, 1 г Москва, ул Каскадная, 15 14 | 27f5dd0f-9040-48ea-8631-52b7f5852670 г Москва, Изу г Москва, ул Изумрудная, 3 г Москва, ул Изумрудная, 32с1 15 | 6d7e8cd6-5688-4737-8c1e-ec485ec11637 г Москва, Жук г Москва, проезд Жуков, 8 г Москва, проезд Жуков, 8с4 16 | 4d3154e3-61e1-4dfb-a299-18e290fbffd7 г Москва, Физ г Москва, проезд Физкультурный, 3 г Москва, проезд Физкультурный, 3к2 17 | fcbf0d82-176c-473f-80bb-b4048d03f9a0 г Москва, Каш г Москва, ш Каширское, 3 г Москва, ш Каширское, 33к13 18 | 2202650e-00a5-4e05-b7dc-d2138a2d2468 г Москва, Дми г Москва, ш Дмитровское, 1 г Москва, ш Дмитровское, 107с25а 19 | 7122adb7-6f1c-4d48-b2b9-436a6d9832b2 г Москва, Вод г Москва, ул Водников, 2 г Москва, ул Водников, 20 20 | 36b4d93f-38cd-4b02-9d0d-c005f14e38eb г Москва, Ряз г Москва, пр-кт Рязанский, 4 г Москва, пр-кт Рязанский, 49к1 21 | -------------------------------------------------------------------------------- /tests/resources/load.jmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | false 8 | 9 | 10 | 11 | SERVER 12 | fias.loc 13 | = 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | false 23 | -1 24 | 25 | 400 26 | 5 27 | 1391087137000 28 | 1391087137000 29 | false 30 | continue 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ${SERVER} 40 | 41 | 42 | 43 | http 44 | 45 | /api/complete/?pattern=Москв 46 | GET 47 | true 48 | false 49 | true 50 | false 51 | 52 | 53 | 54 | false 55 | 56 | 57 | 58 | 59 | 60 | 61 | Connection 62 | keep-alive 63 | 64 | 65 | Accept-Language 66 | en-US,en;q=0.5 67 | 68 | 69 | Accept 70 | text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 71 | 72 | 73 | User-Agent 74 | Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:26.0) Gecko/20100101 Firefox/26.0 75 | 76 | 77 | Accept-Encoding 78 | gzip, deflate 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ${SERVER} 89 | 90 | 91 | 92 | http 93 | 94 | /api/complete/?pattern=${ADDRESS_PATTERN} 95 | GET 96 | true 97 | false 98 | true 99 | false 100 | 101 | 102 | 103 | false 104 | 105 | 106 | 107 | 108 | 109 | 110 | Connection 111 | keep-alive 112 | 113 | 114 | Accept-Language 115 | en-US,en;q=0.5 116 | 117 | 118 | Accept 119 | text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 120 | 121 | 122 | User-Agent 123 | Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:26.0) Gecko/20100101 Firefox/26.0 124 | 125 | 126 | Accept-Encoding 127 | gzip, deflate 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | ${SERVER} 138 | 139 | 140 | 141 | http 142 | 143 | /api/complete/?pattern=${HOUSE_PATTERN} 144 | GET 145 | true 146 | false 147 | true 148 | false 149 | 150 | 151 | 152 | false 153 | 154 | 155 | 156 | 157 | 158 | 159 | Connection 160 | keep-alive 161 | 162 | 163 | Accept-Language 164 | en-US,en;q=0.5 165 | 166 | 167 | Accept 168 | text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 169 | 170 | 171 | User-Agent 172 | Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:26.0) Gecko/20100101 Firefox/26.0 173 | 174 | 175 | Accept-Encoding 176 | gzip, deflate 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | ${SERVER} 187 | 188 | 189 | 190 | 191 | 192 | /api/validate/?pattern=${FULL_ADDRESS} 193 | GET 194 | false 195 | true 196 | true 197 | false 198 | 199 | 200 | 201 | false 202 | 203 | 204 | 205 | 206 | false 207 | 208 | saveConfig 209 | 210 | 211 | true 212 | true 213 | true 214 | 215 | true 216 | true 217 | true 218 | true 219 | false 220 | true 221 | true 222 | false 223 | false 224 | true 225 | false 226 | false 227 | false 228 | false 229 | false 230 | 0 231 | true 232 | 233 | 234 | 235 | 236 | 237 | 238 | false 239 | 240 | saveConfig 241 | 242 | 243 | true 244 | true 245 | true 246 | 247 | true 248 | true 249 | true 250 | true 251 | false 252 | true 253 | true 254 | false 255 | false 256 | true 257 | false 258 | false 259 | false 260 | false 261 | false 262 | 0 263 | true 264 | 265 | 266 | 267 | 268 | 269 | 270 | \t 271 | 272 | ./load.csv 273 | false 274 | true 275 | All threads 276 | false 277 | ADDRESS_ID,ADDRESS_PATTERN,HOUSE_PATTERN,FULL_ADDRESS 278 | 279 | 280 | 281 | 500 282 | 500.0 283 | 284 | 285 | 286 | 287 | 288 | 289 | -------------------------------------------------------------------------------- /tests/resources/readerTest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | getErrorLogger(); 13 | FailureHandler::setup(function ($error) use ($logger) { 14 | (new TextResponse(500, 'Произошла ошибка сервера'))->send(); 15 | $logger->error($error['message'], $error); 16 | exit; 17 | }); 18 | 19 | try { 20 | $request = Request::createFromGlobals(); 21 | $route = $container->getRouter()->route($request->getMethod(), $request->getUrlPath()); 22 | $request->setOptions($route->vars); 23 | 24 | /** @var Response $response */ 25 | $response = (new $route->class($container))->{$route->method}($request); 26 | } catch (RouteNotFoundException $e) { 27 | $response = new Response(404); 28 | } catch (BadRequestException $e) { 29 | $response = new Response(400, $e->getMessage()); 30 | } 31 | 32 | $response->addHeader('Access-Control-Allow-Origin: *'); 33 | $response->send(); 34 | --------------------------------------------------------------------------------