Skip to content

Commit d28784f

Browse files
authored
Parse schema from xsd files (#25)
* add xsd files * convert description from yaml to php array * remove yaml usage from lib * add PhpArrayFileRegistry * add PathHelper * add generator to create entites descriptions from xsd files * change primary for stead
1 parent 9dbc094 commit d28784f

35 files changed

+4984
-1246
lines changed

.php_cs.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ $rules = [
1212
'phpdoc_no_empty_return' => false,
1313
'no_superfluous_phpdoc_tags' => false,
1414
'single_line_throw' => false,
15+
'array_indentation' => true,
1516
];
1617

1718
return PhpCsFixer\Config::create()->setRules($rules)->setFinder($finder);

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ before_script:
99
- composer install --no-interaction --prefer-source --dev
1010
script:
1111
- vendor/bin/phpunit -c phpunit.xml.dist
12-
- vendor/bin/php-cs-fixer fix --config=.php_cs.dist -v --dry-run --stop-on-violation --using-cache=no
12+
- vendor/bin/php-cs-fixer fix --config=.php_cs.dist -vvv --dry-run --stop-on-violation --using-cache=no
1313
- vendor/bin/phpcpd src
1414
- vendor/bin/psalm

Makefile

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@ docker_compose_bin := $(shell command -v docker-compose 2> /dev/null)
1010
docker_compose_yml := docker/docker-compose.yml
1111
user_id := $(shell id -u)
1212

13-
.PHONY : help pull build push login test clean \
14-
app-pull app app-push\
15-
sources-pull sources sources-push\
16-
nginx-pull nginx nginx-push\
17-
up down restart shell install
13+
.PHONY : build test fixer linter shell buildEntities
1814
.DEFAULT_GOAL := build
1915

2016
# --- [ Development tasks ] -------------------------------------------------------------------------------------------
@@ -37,3 +33,7 @@ linter: ## Run code checks
3733

3834
shell: ## Run shell environment in container
3935
$(docker_compose_bin) --file "$(docker_compose_yml)" run --rm -u $(user_id) "$(php_container_name)" /bin/bash
36+
37+
buildEntities: ## Build entities
38+
$(docker_compose_bin) --file "$(docker_compose_yml)" run --rm -u $(user_id) "$(php_container_name)" php -f generator/generate_entities.php
39+
$(docker_compose_bin) --file "$(docker_compose_yml)" run --rm -u $(user_id) "$(php_container_name)" vendor/bin/php-cs-fixer fix -q

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
"ext-libxml": "*",
1010
"ext-xmlreader": "*",
1111
"php": ">=7.2",
12-
"symfony/yaml": "^4.0|^5.0",
1312
"symfony/serializer": "^4.0|^5.0",
1413
"symfony/property-access": "^4.0|^5.0",
1514
"symfony/property-info": "^4.0|^5.0",
@@ -27,7 +26,8 @@
2726
"autoload": {
2827
"psr-4": {
2928
"Liquetsoft\\Fias\\Component\\": "src/",
30-
"Liquetsoft\\Fias\\Component\\Tests\\": "tests/src"
29+
"Liquetsoft\\Fias\\Component\\Tests\\": "tests/src",
30+
"Liquetsoft\\Fias\\Component\\Generator\\": "generator"
3131
}
3232
},
3333
"repositories": [
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Liquetsoft\Fias\Component\Generator;
6+
7+
use DOMDocument;
8+
use DOMNode;
9+
use DOMXpath;
10+
use RecursiveDirectoryIterator;
11+
use RecursiveIteratorIterator;
12+
use RuntimeException;
13+
14+
/**
15+
* Объект, который генерирует файл с описаниями сущностей из xsd файлов,
16+
* поставляемых с ФИАС.
17+
*/
18+
class EntitesArrayFromXSDGenerator
19+
{
20+
/**
21+
* Создает файл с описаниемя сущностей ФИАС на основании данных собранных
22+
* из XSD файлов.
23+
*
24+
* @param string $xsdDir
25+
* @param string $resultFile
26+
* @param string $defaultEntitesFile
27+
*/
28+
public function generate(string $xsdDir, string $resultFile, string $defaultEntitesFile): void
29+
{
30+
$files = $this->getXSDFilesFromDir($xsdDir);
31+
$entites = $this->parseEntitesFromFiles($files);
32+
$defaultEntites = $this->loadDefaultEntites($defaultEntitesFile);
33+
$resultEntites = $this->mergeEntites($entites, $defaultEntites);
34+
35+
$fileText = "<?php\n\n";
36+
$fileText .= 'return ' . var_export($resultEntites, true) . ';';
37+
38+
file_put_contents($resultFile, $fileText);
39+
}
40+
41+
/**
42+
* Получает массив с файлами XSD из указанной папки.
43+
*
44+
* @param string $xsdDir
45+
*
46+
* @return string[]
47+
*/
48+
private function getXSDFilesFromDir(string $xsdDir): array
49+
{
50+
$files = [];
51+
52+
$directoryIterator = new RecursiveDirectoryIterator(
53+
$xsdDir,
54+
RecursiveDirectoryIterator::SKIP_DOTS
55+
);
56+
$iterator = new RecursiveIteratorIterator($directoryIterator);
57+
58+
foreach ($iterator as $fileInfo) {
59+
if (strtolower($fileInfo->getExtension()) === 'xsd') {
60+
$files[] = (string) $fileInfo->getRealPath();
61+
}
62+
}
63+
64+
return $files;
65+
}
66+
67+
/**
68+
* Получает описания сущностей из XSD файлов.
69+
*
70+
* @param string[] $files
71+
*
72+
* @return array
73+
*/
74+
private function parseEntitesFromFiles(array $files): array
75+
{
76+
$xsdEntites = [];
77+
78+
foreach ($files as $file) {
79+
$entites = $this->parseEntitiesFormFile($file);
80+
foreach ($entites as $entity) {
81+
$entityName = $entity['entity_name'] ?? null;
82+
if ($entityName === null) {
83+
throw new RuntimeException("Can't find entity name.");
84+
}
85+
unset($entity['entity_name']);
86+
$xsdEntites[$entityName] = $entity;
87+
}
88+
}
89+
90+
return $xsdEntites;
91+
}
92+
93+
/**
94+
* Получает описание сущности из XSD файла.
95+
*
96+
* @param string $filePath
97+
*
98+
* @return array
99+
*
100+
* @psalm-suppress UndefinedMethod
101+
*/
102+
private function parseEntitiesFormFile(string $filePath): array
103+
{
104+
$entites = [];
105+
106+
$schema = new DOMDocument();
107+
$schema->loadXML(file_get_contents($filePath));
108+
109+
$xpath = new DOMXpath($schema);
110+
111+
$elements = $xpath->query('//xs:schema/xs:element');
112+
foreach ($elements as $element) {
113+
$innerElement = $xpath->query('.//xs:complexType/xs:sequence/xs:element', $element)->item(0);
114+
$innerElementName = $innerElement->getAttribute('name');
115+
116+
$entity = [
117+
'entity_name' => $innerElementName === 'Object' ? 'AddressObject' : $innerElementName,
118+
'description' => $xpath->query('.//xs:annotation/xs:documentation', $innerElement)->item(0)->nodeValue,
119+
'xmlPath' => '/' . $element->getAttribute('name') . '/' . $innerElementName,
120+
'fields' => $this->extractFieldsDecription($innerElement, $xpath),
121+
];
122+
123+
$entites[] = $entity;
124+
}
125+
126+
return $entites;
127+
}
128+
129+
/**
130+
* Создает описания полей по XSD схеме.
131+
*
132+
* @param DOMNode $innerElement
133+
* @param DOMXpath $xpath
134+
*
135+
* @return array
136+
*
137+
* @psalm-suppress UndefinedMethod
138+
*/
139+
private function extractFieldsDecription(DOMNode $innerElement, DOMXpath $xpath): array
140+
{
141+
$fieldsList = [];
142+
143+
$fields = $xpath->query('.//xs:complexType/xs:attribute', $innerElement);
144+
foreach ($fields as $field) {
145+
$fieldName = $field->getAttribute('name');
146+
$fieldsList[$fieldName] = $this->extractFieldDescription($field, $xpath);
147+
}
148+
149+
return $fieldsList;
150+
}
151+
152+
/**
153+
* Получает все данные поля из описания.
154+
*
155+
* @param DOMNode $field
156+
* @param DOMXpath $xpath
157+
*
158+
* @return array
159+
*
160+
* @psalm-suppress UndefinedMethod
161+
*/
162+
private function extractFieldDescription(DOMNode $field, DOMXpath $xpath): array
163+
{
164+
$typeArray = $this->extractTypeArray($field, $xpath);
165+
166+
$fieldArray = [
167+
'type' => $typeArray['type'] ?? '',
168+
'subType' => $typeArray['subType'] ?? '',
169+
'isNullable' => $field->getAttribute('use') !== 'required',
170+
'description' => $xpath->query('.//xs:annotation/xs:documentation', $field)->item(0)->nodeValue,
171+
];
172+
173+
if ($fieldArray['type'] === 'string') {
174+
$length = $xpath->query('.//xs:simpleType/xs:restriction/xs:length', $field)->item(0);
175+
$maxLength = $xpath->query('.//xs:simpleType/xs:restriction/xs:maxLength', $field)->item(0);
176+
if ($length) {
177+
$fieldArray['length'] = (int) $length->getAttribute('value');
178+
if ($fieldArray['length'] === 36) {
179+
$fieldArray['subType'] = 'uuid';
180+
}
181+
} elseif ($maxLength) {
182+
$fieldArray['length'] = (int) $maxLength->getAttribute('value');
183+
}
184+
}
185+
186+
if ($fieldArray['type'] === 'int') {
187+
$length = $xpath->query('.//xs:simpleType/xs:restriction/xs:totalDigits', $field)->item(0);
188+
if ($length) {
189+
$fieldArray['length'] = (int) $length->getAttribute('value');
190+
}
191+
}
192+
193+
return $fieldArray;
194+
}
195+
196+
/**
197+
* Получает тип поля из описания.
198+
*
199+
* @param DOMNode $field
200+
* @param DOMXpath $xpath
201+
*
202+
* @return array
203+
*
204+
* @psalm-suppress UndefinedMethod
205+
*/
206+
private function extractTypeArray(DOMNode $field, DOMXpath $xpath): array
207+
{
208+
$type = $field->getAttribute('type');
209+
if (empty($type)) {
210+
$type = $xpath->query('.//xs:simpleType/xs:restriction', $field)->item(0)->getAttribute('base');
211+
}
212+
213+
return $this->convertType($type);
214+
}
215+
216+
/**
217+
* Конвертирует XSD тип в тип пригодный для описания сущностей.
218+
*
219+
* @param string $type
220+
*
221+
* @return array
222+
*/
223+
private function convertType(string $type): array
224+
{
225+
$convertMap = [
226+
'xs:date' => ['type' => 'string', 'subType' => 'date'],
227+
'xs:integer' => ['type' => 'int', 'subType' => ''],
228+
'xs:int' => ['type' => 'int', 'subType' => ''],
229+
'xs:byte' => ['type' => 'int', 'subType' => ''],
230+
];
231+
232+
return $convertMap[$type] ?? ['type' => 'string', 'subType' => ''];
233+
}
234+
235+
/**
236+
* Загружает массив с описанием сущностей по умолчанию.
237+
*
238+
* @param string $defaultEntitesFile
239+
*
240+
* @return array
241+
*
242+
* @psalm-suppress UnresolvableInclude
243+
*/
244+
private function loadDefaultEntites(string $defaultEntitesFile): array
245+
{
246+
return include $defaultEntitesFile;
247+
}
248+
249+
/**
250+
* Мержит массив с описанием текущих сущностей и сущностей по умолчанию.
251+
*
252+
* @param array $entites
253+
* @param array $defaultEntites
254+
*
255+
* @return array
256+
*/
257+
private function mergeEntites(array $entites, array $defaultEntites): array
258+
{
259+
$resultEntities = [];
260+
261+
foreach ($entites as $entityName => $entityDescription) {
262+
$defaultData = $defaultEntites[$entityName] ?? null;
263+
if ($defaultData !== null) {
264+
$entityDescription = array_replace_recursive($defaultData, $entityDescription);
265+
}
266+
$resultEntities[$entityName] = $entityDescription;
267+
}
268+
269+
return $resultEntities;
270+
}
271+
}

generator/generate_entities.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
use Liquetsoft\Fias\Component\Generator\EntitesArrayFromXSDGenerator;
4+
use Liquetsoft\Fias\Component\Helper\PathHelper;
5+
6+
require_once dirname(__DIR__) . '/vendor/autoload.php';
7+
8+
$entitiesArrayGenerator = new EntitesArrayFromXSDGenerator();
9+
$entitiesArrayGenerator->generate(
10+
PathHelper::resource('xsd'),
11+
PathHelper::resource('fias_entities.php'),
12+
PathHelper::resource('fias_entities_default.php')
13+
);

0 commit comments

Comments
 (0)