Completed
Push — master ( 8754cb...8aa0e6 )
by Thomas
04:19
created

GenerateApiCommand::generateDefinition()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 26
ccs 0
cts 0
cp 0
rs 8.8571
cc 2
eloc 16
nc 2
nop 2
crap 6
1
<?php
2
namespace keeko\tools\command;
3
4
use gossi\codegen\model\PhpClass;
5
use gossi\codegen\model\PhpMethod;
6
use gossi\swagger\collections\Definitions;
7
use gossi\swagger\collections\Paths;
8
use gossi\swagger\Swagger;
9
use gossi\swagger\Tag;
10
use keeko\framework\utils\NameUtils;
11
use phootwork\file\File;
12
use phootwork\lang\Text;
13 20
use Propel\Generator\Model\Table;
14 20
use Symfony\Component\Console\Input\InputInterface;
15 20
use Symfony\Component\Console\Output\OutputInterface;
16 20
17
class GenerateApiCommand extends AbstractKeekoCommand {
18
19 20
	protected function configure() {
20 20
		$this
21
			->setName('generate:api')
22
			->setDescription('Generates the api for the module')
23
		;
24
		
25
		$this->configureGenerateOptions();
26
			
27
		parent::configure();
28
	}
29
	
30
	/**
31
	 * Checks whether api can be generated at all by reading composer.json and verify
32
	 * all required information are available
33
	 */
34
	private function preCheck() {
35
		$module = $this->packageService->getModule();
36
		if ($module === null) {
37
			throw new \DomainException('No module definition found in composer.json - please run `keeko init`.');
38
		}
39
	}
40
41
	protected function execute(InputInterface $input, OutputInterface $output) {
42
		$this->preCheck();
43
		
44
		// generate api
45
		$api = new File($this->project->getApiFileName());
46
		
47
		// if generation is forced, generate new API from scratch
48
		if ($input->getOption('force')) {
49
			$swagger = new Swagger();
50
		}
51
		
52
		// ... anyway reuse existing one
53
		else {
54
			if (!$api->exists()) {
55
				$api->write('{}');
56
			}
57
			
58
			$swagger = Swagger::fromFile($this->project->getApiFileName());
59
		}
60
61
		$module = $this->package->getKeeko()->getModule();
62
		$swagger->setVersion('2.0');
63
		$swagger->getInfo()->setTitle($module->getTitle() . ' API');
64
		$swagger->getTags()->clear();
65
		$swagger->getTags()->add(new Tag(['name' => $module->getSlug()]));
66
		
67
		$this->generatePaths($swagger);
68
		$this->generateDefinitions($swagger);
69
		
70
		$this->jsonService->write($api->getPathname(), $swagger->toArray());
71
		$this->io->writeln(sprintf('API for <info>%s</info> written at <info>%s</info>', $this->package->getFullName(), $api->getPathname()));
72
	}
73
	
74
// 	/**
75
// 	 * Adds the APIModelInterface to package models
76
// 	 * 
77
// 	 */
78
// 	protected function prepareModels() {
79
// 		$models = $this->modelService->getPackageModelNames();
80
		
81
// 		foreach ($models as $modelName) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
82
// 			$tableName = $this->modelService->getTableName($modelName);
83
// 			$model = $this->modelService->getModel($tableName);
84
// 			$class = new PhpClass(str_replace('\\\\', '\\', $model->getNamespace() . '\\' . $model->getPhpName()));
85
// 			$file = new File($this->codegenService->getFilename($class));
86
			
87
// 			if ($file->exists()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
57% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
88
// 				$class = PhpClass::fromFile($this->codegenService->getFilename($class));
89
// 				if (!$class->hasInterface('APIModelInterface')) {
90
// 					$class->addUseStatement('keeko\\core\\model\\types\\APIModelInterface');
91
// 					$class->addInterface('APIModelInterface');
92
// // 					$typeName =  $this->package->getCanonicalName() . '.' . NameUtils::dasherize($modelName);
93
// // 					$class->setMethod(PhpMethod::create('getAPIType')
94
// // 						->setBody('return \''.$typeName . '\';')
95
// // 					);
96
	
97
// 					$this->codegenService->dumpStruct($class, true);
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
98
// 				}
99
// 			}
100
// 		}
101
// 	}
102
103
	protected function generatePaths(Swagger $swagger) {
104
		$paths = $swagger->getPaths();
105
		
106
		foreach ($this->packageService->getModule()->getActionNames() as $name) {
107
			$this->generateOperation($paths, $name);
108
		}
109
	}
110
	
111
	protected function generateOperation(Paths $paths, $actionName) {
112
		$this->logger->notice('Generating Operation for: ' . $actionName);
113
114
		if (Text::create($actionName)->contains('relationship')) {
115
			$this->generateRelationshipOperation($paths, $actionName);
116
		} else {
117
			$this->generateCRUDOperation($paths, $actionName);
118
		}
119
	}
120
121
	protected function generateRelationshipOperation(Paths $paths, $actionName) {
122
		$this->logger->notice('Generating Relationship Operation for: ' . $actionName);
123
		$prefix = substr($actionName, 0, strrpos($actionName, 'relationship') + 12);
124
		$module = $this->packageService->getModule();
125
126
		// test for to-many relationship:
127
		$many = $module->hasAction($prefix . '-read') 
128
			&& $module->hasAction($prefix . '-update')
129
			&& $module->hasAction($prefix . '-add')
130
			&& $module->hasAction($prefix . '-remove')
131
		;
132
		$single = $module->hasAction($prefix . '-read') 
133
			&& $module->hasAction($prefix . '-update')
134
			&& !$many
135
		;
136
		
137
		if (!$many && !$single) {
138
			$this->io->writeln(sprintf('<comment>Couldn\'t detect whether %s is a to-one or to-many relationship, skin generating endpoints</comment>', $actionName));
139
			return;
140
		}
141
		
142
		// find model names
143
		$modelName = substr($actionName, 0, strpos($actionName, 'to') - 1);
144
		$start = strpos($actionName, 'to') + 3;
145
		$foreignModelName = substr($actionName, $start, strpos($actionName, 'relationship') - 1 - $start);
146
		
147
		// stop, if one of the models is excluded from api
148
		$codegen = $this->codegenService->getCodegen();
149
		$excluded = $codegen->getExcludedModels();
0 ignored issues
show
Bug introduced by
The method getExcludedModels() does not seem to exist on object<keeko\framework\schema\CodegenSchema>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
150
		if ($excluded->contains($modelName) || $excluded->contains($foreignModelName)) {
151
			return;
152
		}
153
		
154
		$action = $this->packageService->getAction($actionName);
155
		$type = substr($actionName, strrpos($actionName, '-') + 1);
156
		$method = $this->getMethod($type);
0 ignored issues
show
Unused Code introduced by
$method is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
157
		$endpoint = '/' . NameUtils::pluralize($modelName) . '/{id}/relationship/' . ($single ?
158
			$foreignModelName : NameUtils::pluralize($foreignModelName));
159
		
160
		$path = $paths->get($endpoint);
161
		$method = $this->getMethod($type);
162
		$operation = $path->getOperation($method);
163
		$operation->setDescription($action->getTitle());
164
		$operation->setOperationId($action->getName());
165
		$operation->getTags()->clear();
166
		$operation->getTags()->add(new Tag($this->package->getKeeko()->getModule()->getSlug()));
0 ignored issues
show
Documentation introduced by
$this->package->getKeeko...>getModule()->getSlug() is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
167
		
168
		$params = $operation->getParameters();
169
		$responses = $operation->getResponses();
170
		
171
		// general model related params
172
		// params
173
		$id = $params->getByName('id');
174
		$id->setIn('path');
175
		$id->setDescription(sprintf('The %s id', $modelName));
176
		$id->setRequired(true);
177
		$id->setType('integer');
178
		
179
		// response
180
		$invalid = $responses->get('400');
181
		$invalid->setDescription('Invalid ID supplied');
182
		
183
		$notfound = $responses->get('404');
184
		$notfound->setDescription(sprintf('No %s found', $modelName));
185
	}
186
	
187
	protected function generateCRUDOperation(Paths $paths, $actionName) {
188
		$this->logger->notice('Generating CRUD Operation for: ' . $actionName);
189
		$database = $this->modelService->getDatabase();
190
		$action = $this->packageService->getAction($actionName);
191
		$modelName = $this->modelService->getModelNameByAction($action);
192
		$tableName = $this->modelService->getTableName($modelName);
193
		$codegen = $this->codegenService->getCodegen();
194
	
195
		if (!$database->hasTable($tableName)) {
196
			return;
197
		}
198
		
199
		if ($codegen->getExcludedModels()->contains($modelName)) {
0 ignored issues
show
Bug introduced by
The method getExcludedModels() does not seem to exist on object<keeko\framework\schema\CodegenSchema>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
200
			return;
201
		}
202
	
203
		$type = $this->packageService->getActionType($actionName, $modelName);
204
		$modelObjectName = $database->getTable($tableName)->getPhpName();
205
		$modelPluralName = NameUtils::pluralize($modelName);
206
	
207
		// find path branch
208
		switch ($type) {
209
			case 'list':
210
			case 'create':
211
				$endpoint = '/' . $modelPluralName;
212
				break;
213
	
214
			case 'read':
215
			case 'update':
216
			case 'delete':
217
				$endpoint = '/' . $modelPluralName . '/{id}';
218
				break;
219
	
220
			default:
221
				throw new \RuntimeException(sprintf('type (%s) not found, can\'t continue.', $type));
222
				break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
223
		}
224
225
		$path = $paths->get($endpoint);
226
		$method = $this->getMethod($type);
227
		$operation = $path->getOperation($method);
228
		$operation->setDescription($action->getTitle());
229
		$operation->setOperationId($action->getName());
230
		$operation->getTags()->clear();
231
		$operation->getTags()->add(new Tag($this->package->getKeeko()->getModule()->getSlug()));
0 ignored issues
show
Documentation introduced by
$this->package->getKeeko...>getModule()->getSlug() is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
232
	
233
		$params = $operation->getParameters();
234
		$responses = $operation->getResponses();
235
	
236
		switch ($type) {
237
			case 'list':
238
				$ok = $responses->get('200');
239
				$ok->setDescription(sprintf('Array of %s', $modelPluralName));
240
				$ok->getSchema()->setRef('#/definitions/' . 'Paged' . NameUtils::pluralize($modelObjectName));
241
				break;
242
	
243
			case 'create':
244
				// params
245
				$body = $params->getByName('body');
246
				$body->setName('body');
247
				$body->setIn('body');
248
				$body->setDescription(sprintf('The new %s', $modelName));
249
				$body->setRequired(true);
250
				$body->getSchema()->setRef('#/definitions/Writable' . $modelObjectName);
251
	
252
				// response
253
				$ok = $responses->get('201');
254
				$ok->setDescription(sprintf('%s created', $modelName));
255
				break;
256
	
257
			case 'read':
258
				// response
259
				$ok = $responses->get('200');
260
				$ok->setDescription(sprintf('gets the %s', $modelName));
261
				$ok->getSchema()->setRef('#/definitions/' . $modelObjectName);
262
				break;
263
	
264
			case 'update':
265
				// response
266
				$ok = $responses->get('200');
267
				$ok->setDescription(sprintf('%s updated', $modelName));
268
				$ok->getSchema()->setRef('#/definitions/' . $modelObjectName);
269
				break;
270
	
271
			case 'delete':
272
				// response
273
				$ok = $responses->get('204');
274
				$ok->setDescription(sprintf('%s deleted', $modelName));
275
				break;
276
		}
277
	
278
		if ($type == 'read' || $type == 'update' || $type == 'delete') {
279
			// params
280
			$id = $params->getByName('id');
281
			$id->setIn('path');
282
			$id->setDescription(sprintf('The %s id', $modelName));
283
			$id->setRequired(true);
284
			$id->setType('integer');
285
	
286
			// response
287
			$invalid = $responses->get('400');
288
			$invalid->setDescription('Invalid ID supplied');
289
				
290
			$notfound = $responses->get('404');
291
			$notfound->setDescription(sprintf('No %s found', $modelName));
292
		}
293
	
294
		// response - @TODO Error model
295
	}
296
	
297
	private function getMethod($type) {
298
		$methods = [
299
			'list' => 'get',
300
			'create' => 'post',
301
			'read' => 'get',
302
			'update' => 'patch',
303
			'delete' => 'delete',
304
			'add' => 'post',
305
			'remove' => 'delete'
306
		];
307
	
308
		return $methods[$type];
309
	}
310
311
	protected function generateDefinitions(Swagger $swagger) {
312
		$definitions = $swagger->getDefinitions();
313
		
314
		// general definitions
315
		$this->generatePagedMeta($definitions);
316
		$this->generateResourceIdentifier($definitions); 
317
318
		// models
319
		$modelName = $this->modelService->getModelName();
320
		if ($modelName !== null) {
321
			$this->generateDefinition($definitions, $modelName);
0 ignored issues
show
Documentation introduced by
$modelName is of type string, but the function expects a object<Propel\Generator\Model\Table>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
322
		} else {
323
			foreach ($this->modelService->getModels() as $model) {
324
				$this->generateDefinition($definitions, $model);
325
			}
326
		}
327
	}
328
	
329
	protected function generatePagedMeta(Definitions $definitions) {
330
		$props = $definitions->get('PagedMeta')->setType('object')->getProperties();
331
		$names = ['total', 'first', 'next', 'previous', 'last'];
332
		
333
		foreach ($names as $name) {
334
			$props->get($name)->setType('integer');
335
		}
336
	}
337
	
338
	protected function generateResourceIdentifier(Definitions $definitions) {
339
		$props = $definitions->get('ResourceIdentifier')->setType('object')->getProperties();
340
		$this->generateIdentifier($props);
341
	}
342
	
343
	protected function generateIdentifier(Definitions $props) {
344
		$props->get('id')->setType('string');
345
		$props->get('type')->setType('string');
346
	}
347
	
348
	protected function generateResourceData(Definitions $props) {
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
349
		$data = $props->get('data')->setType('object')->getProperties();
350
		$this->generateIdentifier($data);
351
		return $data;
352
	}
353
354
	protected function generateDefinition(Definitions $definitions, Table $model) {
355
		$this->logger->notice('Generating Definition for: ' . $model->getOriginCommonName());
356
		$modelObjectName = $model->getPhpName();
357
		$codegen = $this->codegenService->getCodegen();
358
		
359
		// stop if model is excluded
360
		if ($codegen->getExcludedModels()->contains($model->getOriginCommonName())) {
0 ignored issues
show
Bug introduced by
The method getExcludedModels() does not seem to exist on object<keeko\framework\schema\CodegenSchema>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
361
			return;
362
		}
363
		
364
		// paged model
365
		$pagedModel = 'Paged' . NameUtils::pluralize($modelObjectName);
366
		$paged = $definitions->get($pagedModel)->setType('object')->getProperties();
367
		$paged->get('data')
368
			->setType('array')
369
			->getItems()->setRef('#/definitions/' . $modelObjectName);
370
		$paged->get('meta')->setRef('#/definitions/PagedMeta');
371
		
372
		// writable model
373
		$writable = $definitions->get('Writable' . $modelObjectName)->setType('object')->getProperties();
374
		$this->generateModelProperties($writable, $model, true);
375
376
		// readable model
377
		$readable = $definitions->get($modelObjectName)->setType('object')->getProperties();
378
		$this->generateModelProperties($readable, $model, false);
379
	}
380
	
381
	protected function generateModelProperties(Definitions $props, Table $model, $write = false) {
382
		// links
383
		if (!$write) {
384
			$links = $props->get('links')->setType('object')->getProperties();
385
			$links->get('self')->setType('string');
386
		}
387
		
388
		// data
389
		$data = $this->generateResourceData($props);
390
		
391
		// attributes
392
		$attrs = $data->get('attributes');
393
		$attrs->setType('object');
394
		$this->generateModelAttributes($attrs->getProperties(), $model, $write);
395
396
		// relationships
397
		if ($this->hasRelationships($model)) {
398
			$relationships = $data->get('relationships')->setType('object')->getProperties();
399
			$this->generateModelRelationships($relationships, $model, $write);
400
		}
401
	}
402
	
403
	protected function generateModelAttributes(Definitions $props, Table $model, $write = false) {
404
		$modelName = $model->getOriginCommonName();
405
		$filter = $write 
406
			? $this->codegenService->getCodegen()->getWriteFilter($modelName)
407
			: $this->codegenService->getCodegen()->getReadFilter($modelName);
408
409
		if ($write) {
410
			$filter = array_merge($filter, $this->codegenService->getComputedFields($model));
411
		}
412
		
413
		// no id, already in identifier
414
		$filter[] = 'id';
415
		$types = ['int' => 'integer'];
416
		
417
		foreach ($model->getColumns() as $col) {
418
			$prop = $col->getName();
419
			
420
			if (!in_array($prop, $filter)) {
421
				$type = $col->getPhpType();
422
				if (isset($types[$type])) {
423
					$type = $types[$type];
424
				}
425
				$props->get($prop)->setType($type);
426
			}
427
		}
428
429
		return $props;
430
	}
431
	
432
	protected function hasRelationships(Table $model) {
433
		return (count($model->getForeignKeys()) + count($model->getCrossFks())) > 0;
434
	}
435
	
436
	protected function generateModelRelationships(Definitions $props, Table $model, $write = false) {
437
		$relationships = $this->modelService->getRelationships($model);
438
		
439
		// to-one
440
		foreach ($relationships->getOne() as $one) {
441
			$typeName = $one->getRelatedTypeName();
442
			$rel = $props->get($typeName)->setType('object')->getProperties();
443
			
444
			// links
445
			if (!$write) {
446
				$links = $rel->get('links')->setType('object')->getProperties();
447
				$links->get('self')->setType('string');
448
			}
449
			
450
			// data
451
			$this->generateResourceData($rel);
452
		}
453
		
454
		// to-many
455
		foreach ($relationships->getMany() as $many) {
456
			$typeName = $many->getRelatedTypeName();
457
			$rel = $props->get($typeName)->setType('object')->getProperties();
458
			
459
			// links
460
			if (!$write) {
461
				$links = $rel->get('links')->setType('object')->getProperties();
462
				$links->get('self')->setType('string');
463
			}
464
			
465
			// data
466
			$rel->get('data')
467
				->setType('array')
468
				->getItems()->setRef('#/definitions/ResourceIdentifier');
469
		}
470
	}
471
472
}