├── phpstan.neon
├── ecs.php
├── src
├── config.php
├── helperFunctions.php
└── schemagen.php
├── Dockerfile
├── composer.json
├── Makefile
└── README.md
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 5
3 | paths:
4 | - src
5 |
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | paths([
8 | __DIR__ . '/src',
9 | __FILE__,
10 | ]);
11 | $ecsConfig->parallel();
12 | $ecsConfig->sets([SetList::CRAFT_CMS_4]);
13 | };
14 |
--------------------------------------------------------------------------------
/src/config.php:
--------------------------------------------------------------------------------
1 | isDir() ? rmdir($file) : unlink($file);
13 | }
14 | }
15 | }
16 |
17 | /**
18 | * Ensure a folder exists or throw an exception
19 | *
20 | * @param string $dir
21 | * @throws RuntimeException if unable to ensure that a directory exists
22 | */
23 | function ensureDir(string $dir): void
24 | {
25 | if (!@mkdir($dir, 0777, true) && !is_dir($dir)) {
26 | throw new RuntimeException(sprintf('Directory "%s" was not created', $dir));
27 | }
28 | }
29 |
30 | /**
31 | * Get the current schema release version from the GitHub API
32 | *
33 | * @param string $schemaReleases
34 | * @return string
35 | */
36 | function getSchemaVersion(string $schemaReleases): string
37 | {
38 | $schemaRelease = 'latest';
39 | // Per: https://stackoverflow.com/questions/37141315/file-get-contents-gets-403-from-api-github-com-every-time
40 | $opts = [
41 | 'http' => [
42 | 'method' => 'GET',
43 | 'header' => [
44 | 'User-Agent: PHP',
45 | ],
46 | ],
47 | ];
48 | $context = stream_context_create($opts);
49 | $data = file_get_contents($schemaReleases, false, $context);
50 | if ($data) {
51 | $data = json_decode($data);
52 | if (is_array($data)) {
53 | $schemaRelease = $data[0]->tag_name ?? 'latest';
54 | }
55 | }
56 |
57 | return $schemaRelease;
58 | }
59 |
60 | /**
61 | * Given a schema name, generate a class name for the schema.
62 | *
63 | * @param string $schemaName
64 | * @return string
65 | */
66 | function getSchemaClassName(string $schemaName): string
67 | {
68 | $invalidClassnames = ['class', 'false', 'true', 'float'];
69 | if (!preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $schemaName) || in_array(strtolower($schemaName), $invalidClassnames, true)) {
70 | return 'Schema' . $schemaName;
71 | }
72 |
73 | return $schemaName;
74 | }
75 |
76 | /**
77 | * Compile a field's data from property definition array.
78 | *
79 | * @param array $propertyDef
80 | * @return array{propertyDescription: string, propertyType: string, propertyTypesAsArray: array, propertyHandle: string}
81 | */
82 | function compileFieldData(array $propertyDef): array
83 | {
84 | $propertyDescription = getTextValue($propertyDef['rdfs:comment']);
85 |
86 | $propertyTypes = empty($propertyDef['schema:rangeIncludes']['@id']) ? $propertyDef['schema:rangeIncludes'] : [$propertyDef['schema:rangeIncludes']];
87 | $propertyHandle = getTextValue($propertyDef['rdfs:label']);
88 |
89 | $propertyTypesAsArray = [];
90 | $propertyPhpTypesAsArray = [];
91 |
92 | foreach ($propertyTypes as &$schemaType) {
93 | $schemaType = substr($schemaType['@id'], 7);
94 | switch ($schemaType) {
95 | case 'Text':
96 | case 'Url':
97 | $phpType = 'string';
98 | break;
99 | case 'Integer':
100 | $phpType = 'int';
101 | break;
102 | case 'Number':
103 | case 'Float':
104 | $phpType = 'float';
105 | break;
106 | case 'Boolean':
107 | $phpType = 'bool';
108 | break;
109 | default:
110 | $phpType = '';
111 | break;
112 | }
113 | $schemaClassName = getSchemaClassName($schemaType);;
114 | $propertyTypesAsArray[] = 'array';
115 | $propertyTypesAsArray[] = $schemaClassName;
116 | $propertyTypesAsArray[] = $schemaClassName . '[]';
117 | $propertyPhpTypesAsArray[] = $phpType;
118 | }
119 |
120 | $propertyPhpTypesAsArray = array_merge(
121 | array_filter($propertyPhpTypesAsArray),
122 | $propertyTypesAsArray,
123 | );
124 |
125 | $propertyType = implode('|', $propertyPhpTypesAsArray);
126 |
127 | return compact(
128 | 'propertyDescription',
129 | 'propertyType',
130 | 'propertyTypesAsArray',
131 | 'propertyHandle'
132 | );
133 | }
134 |
135 | function printInterfaceFile(string $schemaName, string $schemaRelease): string
136 | {
137 | $schemaInterfaceName = $schemaName . 'Interface';
138 | $schemaScope = getScope($schemaName);
139 |
140 | $file = createFileWithHeader();
141 | $interface = $file->addInterface(MODEL_NAMESPACE . '\\' . $schemaInterfaceName);
142 | $interface->addComment("schema.org version: $schemaRelease")
143 | ->addComment("Interface for $schemaName.\n");
144 | decorateWithPackageInfo($interface, $schemaScope);
145 |
146 | return (new Nette\PhpGenerator\PsrPrinter())->printFile($file);
147 | }
148 |
149 | /**
150 | * Make the trait.
151 | *
152 | * @param string $schemaName
153 | * @param array $properties
154 | * @return string
155 | */
156 | function printTraitFile(string $schemaName, array $properties, string $schemaRelease): string
157 | {
158 | foreach ($properties as &$fieldDef) {
159 | $fieldDef = compileFieldData($fieldDef);
160 | $fieldDef['propertyDescription'] = wordwrap($fieldDef['propertyDescription']);
161 | }
162 | unset($fieldDef);
163 |
164 | $schemaScope = getScope($schemaName);
165 | $schemaTraitName = $schemaName . 'Trait';
166 |
167 | $file = createFileWithHeader();
168 |
169 | $trait = $file->addTrait(MODEL_NAMESPACE . '\\' . $schemaTraitName);
170 | $trait->addComment("schema.org version: $schemaRelease")
171 | ->addComment("Trait for $schemaName.\n");
172 |
173 | decorateWithPackageInfo($trait, $schemaScope);
174 |
175 | foreach ($properties as $fieldDef) {
176 | $trait->addProperty($fieldDef['propertyHandle'])
177 | ->setPublic()
178 | ->addComment($fieldDef['propertyDescription'] . "\n")
179 | ->addComment("@var {$fieldDef['propertyType']}");
180 | }
181 |
182 | return (new Nette\PhpGenerator\PsrPrinter())->printFile($file);
183 | }
184 |
185 | /**
186 | * Get the scope URL for a given schema name.
187 | *
188 | * @param string $schemaName
189 | * @return string
190 | */
191 | function getScope(string $schemaName): string
192 | {
193 | return 'https://schema.org/' . $schemaName;
194 | }
195 |
196 | /**
197 | * Save a generated file.
198 | *
199 | * @param string $path
200 | * @param string $content
201 | */
202 | function saveGeneratedFile(string $path, string $content): void
203 | {
204 | file_put_contents($path, $content);
205 | }
206 |
207 | /**
208 | * Get google fields based on an array of schemas (inherited and current)
209 | *
210 | * @param array $schemas
211 | * @return array{required: array, recommended: array}
212 | */
213 | function getGoogleFields(array $schemas): array
214 | {
215 | $required = [];
216 | $recommended = [];
217 |
218 | foreach ($schemas as $schema) {
219 | switch ($schema) {
220 | case 'Thing':
221 | $required = array_merge($required, ['name', 'description']);
222 | $recommended = array_merge($recommended, ['url', 'image']);
223 | break;
224 |
225 | case 'Article':
226 | case 'NewsArticle':
227 | case 'BlogPosting':
228 | $required = array_merge($required, ['author', 'datePublished', 'headline', 'image', 'publisher']);
229 | $recommended = array_merge($recommended, ['dateModified', 'mainEntityOfPage']);
230 | break;
231 |
232 | case 'SocialMediaPosting':
233 | $required = array_merge($required, ['datePublished', 'headline', 'image']);
234 | break;
235 |
236 | case 'LiveBlogPosting':
237 | $recommended = array_merge($recommended, ['coverageEndTime', 'coverageStartTime']);
238 | break;
239 | }
240 | }
241 |
242 | return compact('required', 'recommended');
243 | }
244 |
245 | function cleanArray(array $array): array
246 | {
247 | $array = array_unique($array);
248 | sort($array);
249 |
250 | return $array;
251 | }
252 |
253 | /**
254 | * If the field value is a localized node, just get the first value and be done with it.
255 | *
256 | * @param mixed $fieldValue
257 | * @param bool $removeBreaks
258 | * @return string
259 | */
260 | function getTextValue(mixed $fieldValue, bool $removeBreaks = true): string
261 | {
262 | if (is_array($fieldValue)) {
263 | $fieldValue = $fieldValue['@value'];
264 | }
265 | $fieldValue = html_entity_decode($fieldValue);
266 | if ($removeBreaks) {
267 | $fieldValue = str_replace(['
', '\n', "\n"], ' ', $fieldValue);
268 | }
269 |
270 | return $fieldValue;
271 | }
272 |
273 | function loadAllAncestors(array &$ancestors, array $entityTree, string $className): void
274 | {
275 | foreach ($entityTree[$className] as $parentClassName) {
276 | $ancestors[] = $parentClassName;
277 | loadAllAncestors($ancestors, $entityTree, $parentClassName);
278 | }
279 | }
280 |
281 | /**
282 | * Wrap an array's values in single quotes.
283 | *
284 | * @param array $array
285 | * @return array
286 | */
287 | function wrapValuesInSingleQuotes(array $array): array
288 | {
289 | array_walk($array, function(&$value) {
290 | $value = "'$value'";
291 | });
292 |
293 | return $array;
294 | }
295 |
296 | /**
297 | * @return PhpFile
298 | */
299 | function createFileWithHeader(): PhpFile
300 | {
301 | $file = new Nette\PhpGenerator\PhpFile();
302 | $file->addComment("SEOmatic plugin for Craft CMS\n")
303 | ->addComment("A turnkey SEO implementation for Craft CMS that is comprehensive, powerful, and flexible\n")
304 | ->addComment('@link https://nystudio107.com')
305 | ->addComment("@copyright Copyright (c) nystudio107");
306 | return $file;
307 | }
308 |
309 | /**
310 | * @param ClassLike $classLike
311 | * @param string $schemaScope
312 | */
313 | function decorateWithPackageInfo(ClassLike $classLike, string $schemaScope)
314 | {
315 | $classLike
316 | ->addComment('@author nystudio107')
317 | ->addComment('@package Seomatic')
318 | ->addComment("@see $schemaScope");
319 | }
320 |
--------------------------------------------------------------------------------
/src/schemagen.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | setName('Download Schema.org data and generate models')
25 | ->setVersion('1.0.0')
26 | ->addArgument('source', InputArgument::OPTIONAL, 'The data source URL or file location')
27 | ->addArgument('outputDir', InputArgument::OPTIONAL, 'The output directory')
28 | ->addOption('skipSuperseded', 's', InputOption::VALUE_OPTIONAL, 'Whether superseded entities should be skipped', false)
29 | ->addOption('craft-version', 'c', InputOption::VALUE_OPTIONAL, 'Craft version to generate the models for. Defaults to 3.', 3)
30 | ->setCode(function(InputInterface $input, OutputInterface $output) {
31 | $source = $input->getArgument('source') ?? SCHEMA_SOURCE;
32 | $outputDir = $input->getArgument('outputDir') ?? OUTPUT_DIR;
33 | $craftVersion = ($input->getOption('craft-version') ?? 3);
34 |
35 | // ensure output folders exist
36 | try {
37 | nukeDir($outputDir);
38 | ensureDir($outputDir);
39 | } catch (RuntimeException $exception) {
40 | $output->writeln("{$exception->getMessage()}");
41 | return Command::FAILURE;
42 | }
43 |
44 | // Fetch the latest schema release name
45 | $schemaReleases = SCHEMA_RELEASES;
46 | $output->writeln("Fetching schema releases data - $schemaReleases");
47 | $schemaRelease = getSchemaVersion($schemaReleases);
48 |
49 | // Fetch the tree.jsonld ref: https://schema.org/docs/developers.html
50 | $schemaTree = SCHEMA_TREE;
51 | $treeDest = dirname($outputDir) . '/' . TREE_FILE_NAME;
52 | $output->writeln("Fetching schema tree - $schemaTree");
53 | $tree = file_get_contents($schemaTree);
54 | if ($tree) {
55 | file_put_contents($treeDest, $tree);
56 | }
57 |
58 | $output->writeln("Fetching data source - $source");
59 | $data = file_get_contents($source);
60 |
61 | $encoders = [new XmlEncoder(), new JsonEncoder()];
62 | $normalizers = [new ObjectNormalizer()];
63 |
64 | $serializer = new Serializer($normalizers, $encoders);
65 | $json = $serializer->decode($data, 'json');
66 |
67 | if (!is_array($json) || !array_key_exists('@graph', $json)) {
68 | $output->writeln('Unrecognized data structure');
69 | return Command::FAILURE;
70 | }
71 |
72 | $properties = [];
73 | $classes = [];
74 | $enums = [];
75 |
76 | $output->writeln('Parsing the received data ...');
77 |
78 | // Parse the JSON into entity groups
79 | foreach ($json['@graph'] as $entity) {
80 | if (!empty($entity['schema:supersededBy'])) {
81 | $output->write("A deprecated entity encountered - {$entity['@id']}");
82 |
83 | // Skip entities if required
84 | if ($input->getOption('skipSuperseded')) {
85 | $output->writeln(" ... skipping");
86 | continue;
87 | }
88 | $output->writeln('');
89 | }
90 |
91 | $id = $entity['@id'];
92 | $type = $entity['@type'];
93 |
94 | $types = (array)$type;
95 |
96 | // Some entities are enums AND classes
97 | foreach ($types as $type) {
98 | switch ($type) {
99 | case 'rdf:Property':
100 | if (empty($entity['schema:domainIncludes'])) {
101 | break;
102 | }
103 |
104 | // Normalize to an array
105 | if (is_array($entity['schema:domainIncludes']) && !is_array(reset($entity['schema:domainIncludes']))) {
106 | $entity['schema:domainIncludes'] = [$entity['schema:domainIncludes']];
107 | }
108 |
109 | foreach ($entity['schema:domainIncludes'] ?? [] as $domainIncludes) {
110 | foreach ($domainIncludes as $key => $containingClass) {
111 | $properties[$containingClass][$id] = $entity;
112 | }
113 | }
114 | break;
115 | case 'rdfs:Class':
116 | $classes[$id] = $entity;
117 | break;
118 | default:
119 | if (substr($type, 0, 7) === 'schema:') {
120 | // Enums should be treated as classes, too
121 | $classes[$id] = $entity;
122 | } else {
123 | $output->writeln("Cannot handle type $type");
124 | return Command::FAILURE;
125 | }
126 | }
127 | }
128 | }
129 |
130 | $output->writeln('Generating traits and building the hierarchy tree ...');
131 | // First pass to generate traits and create hierarchy tree
132 | $entityTree = [];
133 | $propertiesBySchemaName = [];
134 |
135 | foreach ($classes as $id => $classDef) {
136 | if (str_starts_with($id, 'schema:')) {
137 | $schemaName = getTextValue($classDef['rdfs:label']);
138 | $schemaClass = getSchemaClassName($schemaName);
139 | $schemaTraitName = $schemaClass . 'Trait';
140 | $schemaInterfaceName = $schemaClass . 'Interface';
141 | $propertiesBySchemaName[$schemaName] = $properties[$id] ?? [];
142 |
143 | $trait = printTraitFile($schemaClass, $properties[$id] ?? [], $schemaRelease);
144 | saveGeneratedFile($outputDir . $schemaTraitName . '.php', $trait);
145 | $interface = printInterfaceFile($schemaClass, $schemaRelease);
146 | saveGeneratedFile($outputDir . $schemaInterfaceName . '.php', $interface);
147 |
148 | $entityTree[$schemaName] = [];
149 |
150 | if (!empty($classDef['rdfs:subClassOf'])) {
151 | // Ensure normalized form
152 | if (is_array($classDef['rdfs:subClassOf']) && !is_array(reset($classDef['rdfs:subClassOf']))) {
153 | $classDef['rdfs:subClassOf'] = [$classDef['rdfs:subClassOf']];
154 | }
155 |
156 | // Build a list of parent entities
157 | foreach ($classDef['rdfs:subClassOf'] as $subclassDef) {
158 | // Interested only in schema types
159 | if (substr($subclassDef['@id'], 0, 7) === 'schema:') {
160 | $subClassOf = substr($subclassDef['@id'], 7);
161 | $entityTree[$schemaName][] = $subClassOf;
162 | }
163 | }
164 | }
165 |
166 | if (!empty($classDef['@type']) && is_string($classDef['@type']) && str_starts_with($classDef['@type'], 'schema:')) {
167 | $subClassOf = substr($classDef['@type'], 7);
168 | $entityTree[$schemaName][] = $subClassOf;
169 | }
170 | }
171 | }
172 |
173 | $output->writeln('Generating models and including traits ...');
174 |
175 | // Loop again, now with all the traits generated and relations known.
176 | foreach ($classes as $id => $classDef) {
177 | if (str_starts_with($id, 'schema:')) {
178 | $schemaTraits = [];
179 | $schemaInterfaces = [];
180 |
181 | $schemaName = getTextValue($classDef['rdfs:label']);
182 | $schemaClass = getSchemaClassName($schemaName);
183 | $schemaDescriptionRaw = rtrim(getTextValue($classDef['rdfs:comment'], false), "\n");
184 | $schemaDescription = wordwrap(getTextValue($classDef['rdfs:comment']));
185 | $schemaScope = getScope($schemaName);
186 |
187 | // Add the schemaName itself as an ancestor so its properties, Trait, and Interface are included
188 | $ancestors = [];
189 | $ancestors[] = $schemaName;
190 | loadAllAncestors($ancestors, $entityTree, $schemaName);
191 | $ancestors = array_unique($ancestors);
192 |
193 | $schemaExtends = $ancestors[1] ?? 'Thing';
194 |
195 | // Include all ancestor traits
196 | foreach ($ancestors as $ancestor) {
197 | $schemaTraits[] = getSchemaClassName($ancestor) . 'Trait';
198 | $schemaInterfaces[] = getSchemaClassName($ancestor) . 'Interface';
199 | }
200 |
201 | // Load google field information
202 | $googleFields = getGoogleFields(array_merge([$schemaName], $ancestors));
203 | $required = wrapValuesInSingleQuotes(cleanArray($googleFields['required']));
204 | $recommended = wrapValuesInSingleQuotes(cleanArray($googleFields['recommended']));
205 |
206 | $googleRequiredSchemaAsArray = '[' . implode(", ", $required) . ']';
207 | $googleRecommendedSchemaAsArray = '[' . implode(", ", $recommended) . ']';
208 |
209 | $schemaPropertyTypes = [];
210 | $schemaPropertyDescriptions = [];
211 |
212 | $schemaPropertyExpectedTypesAsArray = "[\n";
213 | $schemaPropertyDescriptionsAsArray = "[\n";
214 |
215 | // Load property information
216 | $fieldsToParse = [];
217 | foreach ($ancestors as $ancestor) {
218 | $fields = $propertiesBySchemaName[$ancestor];
219 | foreach ($fields as $fieldDef) {
220 | $handle = getTextValue($fieldDef['rdfs:label']);
221 | $fieldsToParse[$handle] = $fieldDef;
222 | }
223 | }
224 |
225 | ksort($fieldsToParse);
226 |
227 | foreach ($fieldsToParse as $fieldDef) {
228 | $fieldData = compileFieldData($fieldDef);
229 | $types = wrapValuesInSingleQuotes($fieldData['propertyTypesAsArray']);
230 | $description = str_replace("'", '\\\'', $fieldData['propertyDescription']);
231 |
232 | $schemaPropertyTypes[] = " '" . $fieldData['propertyHandle'] . "' => [" . implode(', ', $types) . "]";
233 | $schemaPropertyDescriptions[] = " '" . $fieldData['propertyHandle'] . "' => '" . $description . "'";
234 | }
235 |
236 | $schemaPropertyExpectedTypesAsArray .= implode(",\n", $schemaPropertyTypes) . "\n]";
237 | $schemaPropertyDescriptionsAsArray .= implode(",\n", $schemaPropertyDescriptions) . "\n]";
238 |
239 | $file = createFileWithHeader();
240 | $file->addNamespace(MODEL_NAMESPACE)
241 | ->addUse(PARENT_MODEL);
242 |
243 | $class = $file->addClass(MODEL_NAMESPACE . '\\' . $schemaClass);
244 | $class->addComment("schema.org version: $schemaRelease")
245 | ->addComment("$schemaName - $schemaDescription\n");
246 | decorateWithPackageInfo($class, $schemaScope);
247 |
248 | $class->setExtends(PARENT_MODEL);
249 |
250 | foreach ($schemaInterfaces as $schemaInterface) {
251 | $class->addImplement(MODEL_NAMESPACE . '\\' . $schemaInterface);
252 | }
253 |
254 | $properties = [];
255 | $properties[] = $class->addProperty('schemaTypeName', $schemaName)
256 | ->setStatic()
257 | ->setPublic()
258 | ->addComment("The Schema.org Type Name\n")
259 | ->addComment('@var string');
260 |
261 | $properties[] = $class->addProperty('schemaTypeScope', $schemaScope)
262 | ->setStatic()
263 | ->setPublic()
264 | ->addComment("The Schema.org Type Scope\n")
265 | ->addComment('@var string');
266 |
267 | $properties[] = $class->addProperty('schemaTypeExtends', $schemaExtends)
268 | ->setStatic()
269 | ->setPublic()
270 | ->addComment("The Schema.org Type Extends\n")
271 | ->addComment('@var string');
272 |
273 | $properties[] = $class->addProperty('schemaTypeDescription', $schemaDescriptionRaw)
274 | ->setStatic()
275 | ->setPublic()
276 | ->addComment("The Schema.org Type Description\n")
277 | ->addComment('@var string');
278 |
279 | if ($craftVersion !== 3) {
280 | foreach ($properties as $property) {
281 | $property->setType('string');
282 | }
283 | }
284 |
285 | foreach ($schemaTraits as $schemaTrait) {
286 | $class->addTrait(MODEL_NAMESPACE . '\\' . $schemaTrait);
287 | }
288 |
289 | $class->addMethod('getSchemaPropertyNames')
290 | ->addComment('@inheritdoc')
291 | ->setPublic()
292 | ->setReturnType('array')
293 | ->setBody('return array_keys($this->getSchemaPropertyExpectedTypes());');
294 |
295 | $class->addMethod('getSchemaPropertyExpectedTypes')
296 | ->addComment('@inheritdoc')
297 | ->setPublic()
298 | ->setReturnType('array')
299 | ->setBody("return $schemaPropertyExpectedTypesAsArray;");
300 |
301 | $class->addMethod('getSchemaPropertyDescriptions')
302 | ->addComment('@inheritdoc')
303 | ->setPublic()
304 | ->setReturnType('array')
305 | ->setBody("return $schemaPropertyDescriptionsAsArray;");
306 |
307 | $class->addMethod('getGoogleRequiredSchema')
308 | ->addComment('@inheritdoc')
309 | ->setPublic()
310 | ->setReturnType('array')
311 | ->setBody("return $googleRequiredSchemaAsArray;");
312 |
313 | $class->addMethod('getGoogleRecommendedSchema')
314 | ->addComment('@inheritdoc')
315 | ->setPublic()
316 | ->setReturnType('array')
317 | ->setBody("return $googleRecommendedSchemaAsArray;");
318 |
319 | $class->addMethod('defineRules')
320 | ->addComment('@inheritdoc')
321 | ->setPublic()
322 | ->setReturnType('array')
323 | ->setBody(<<<'METHOD'
324 | $rules = parent::defineRules();
325 | $rules = array_merge($rules, [
326 | [$this->getSchemaPropertyNames(), 'validateJsonSchema'],
327 | [$this->getGoogleRequiredSchema(), 'required', 'on' => ['google'], 'message' => 'This property is required by Google.'],
328 | [$this->getGoogleRecommendedSchema(), 'required', 'on' => ['google'], 'message' => 'This property is recommended by Google.']
329 | ]);
330 |
331 | return $rules;
332 | METHOD
333 | );
334 |
335 | $model = (new Printer())->printFile($file);
336 |
337 | saveGeneratedFile($outputDir . $schemaClass . '.php', $model);
338 | }
339 | }
340 |
341 | $output->writeln('All done!');
342 |
343 | return Command::SUCCESS;
344 | })
345 | ->run();
346 |
347 | $application->run();
348 |
--------------------------------------------------------------------------------