Completed
Push — master ( 966759...8da8ca )
by Thomas
07:52
created

GenerateApiCommand::generateErrorDefinition()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 11
ccs 0
cts 0
cp 0
rs 9.4285
cc 1
eloc 9
nc 1
nop 1
crap 2
1
<?php
2
namespace keeko\tools\command;
3
4
use gossi\swagger\collections\Definitions;
5
use gossi\swagger\collections\Paths;
6
use gossi\swagger\Swagger;
7
use gossi\swagger\Tag;
8
use keeko\framework\utils\NameUtils;
9
use phootwork\file\File;
10
use phootwork\lang\Text;
11
use Propel\Generator\Model\Table;
12
use Symfony\Component\Console\Input\InputInterface;
13 20
use Symfony\Component\Console\Input\InputOption;
14 20
use Symfony\Component\Console\Output\OutputInterface;
15 20
use keeko\tools\model\Relationship;
16 20
17
class GenerateApiCommand extends AbstractKeekoCommand {
18
	
19 20
	private $needsResourceIdentifier = false;
20 20
	private $needsPagedMeta = false;
21
22
	protected function configure() {
23
		$this
24
			->setName('generate:api')
25
			->setDescription('Generates the api for the module')
26
			->addOption(
27
				'model',
28
				'm',
29
				InputOption::VALUE_OPTIONAL,
30
				'The model for which the actions should be generated, when there is no name argument (if ommited all models will be generated)'
31
			)
32
		;
33
		
34
		$this->configureGenerateOptions();
35
			
36
		parent::configure();
37
	}
38
	
39
	/**
40
	 * Checks whether api can be generated at all by reading composer.json and verify
41
	 * all required information are available
42
	 */
43
	private function preCheck() {
44
		$module = $this->packageService->getModule();
45
		if ($module === null) {
46
			throw new \DomainException('No module definition found in composer.json - please run `keeko init`.');
47
		}
48
	}
49
50
	protected function execute(InputInterface $input, OutputInterface $output) {
51
		$this->preCheck();
52
		
53
		// generate api
54
		$api = new File($this->project->getApiFileName());
55
		
56
		// if generation is forced, generate new API from scratch
57
		if ($input->getOption('force')) {
58
			$swagger = new Swagger();
59
		}
60
		
61
		// ... anyway reuse existing one
62
		else {
63
			if (!$api->exists()) {
64
				$api->write('{}');
65
			}
66
			
67
			$swagger = Swagger::fromFile($this->project->getApiFileName());
68
		}
69
70
		$module = $this->package->getKeeko()->getModule();
71
		$swagger->setVersion('2.0');
72
		$swagger->getInfo()->setTitle($module->getTitle() . ' API');
73
		$swagger->getTags()->clear();
74
		$swagger->getTags()->add(new Tag(['name' => $module->getSlug()]));
75
		
76
		$this->generatePaths($swagger);
77
		$this->generateDefinitions($swagger);
78
		
79
		$this->jsonService->write($api->getPathname(), $swagger->toArray());
80
		$this->io->writeln(sprintf('API for <info>%s</info> written at <info>%s</info>', $this->package->getFullName(), $api->getPathname()));
81
	}
82
83
	protected function generatePaths(Swagger $swagger) {
84
		$paths = $swagger->getPaths();
85
		
86
		foreach ($this->packageService->getModule()->getActionNames() as $name) {
87
			$this->generateOperation($paths, $name);
88
		}
89
	}
90
	
91
	protected function generateOperation(Paths $paths, $actionName) {
92
		$this->logger->notice('Generating Operation for: ' . $actionName);
93
94
		if (Text::create($actionName)->contains('relationship')) {
95
			$this->generateRelationshipOperation($paths, $actionName);
96
		} else {
97
			$this->generateCRUDOperation($paths, $actionName);
98
		}
99
	}
100
101
	protected function generateRelationshipOperation(Paths $paths, $actionName) {
102
		$this->logger->notice('Generating Relationship Operation for: ' . $actionName);
103
		$prefix = substr($actionName, 0, strrpos($actionName, 'relationship') + 12);
104
		$suffix = substr($actionName, strrpos($actionName, 'relationship') + 13);
105
		$module = $this->packageService->getModule();
106
107
		// test for to-many relationship:
108
		$many = $module->hasAction($prefix . '-read') 
109
			&& $module->hasAction($prefix . '-update')
110
			&& $module->hasAction($prefix . '-add')
111
			&& $module->hasAction($prefix . '-remove')
112
		;
113
		$single = $module->hasAction($prefix . '-read') 
114
			&& $module->hasAction($prefix . '-update')
115
			&& !$many
116
		;
117
		
118
		if (!$many && !$single) {
119
			$this->io->writeln(sprintf('<comment>Couldn\'t detect whether %s is a to-one or to-many relationship, skin generating endpoints</comment>', $actionName));
120
			return;
121
		}
122
		
123
		// find model name
124
		$codegen = $this->codegenService->getCodegen();
125
		$excluded = $codegen->getExcludedApi();
0 ignored issues
show
Bug introduced by
The method getExcludedApi() 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...
126
		$modelName = substr($actionName, 0, strpos($actionName, 'to') - 1);
127
		if ($excluded->contains($modelName)) {
128
			return;
129
		}
130
		
131
		// find related name
132
		$start = strpos($actionName, 'to') + 3;
133
		$relatedName = NameUtils::dasherize(substr($actionName, $start, strpos($actionName, 'relationship') - 1 - $start));
134
		$model = $this->modelService->getModel($modelName);
135
		$relationship = $this->modelService->getRelationship($model, $relatedName);
136
		if ($relationship === null) {
137
			return;
138
		}
139
		$foreignModelName = $relationship->getForeign()->getOriginCommonName();
140
		if ($excluded->contains($foreignModelName)) {
141
			return;
142
		}
143
		
144
		// continue if neither model nor related model is excluded
145
		$action = $this->packageService->getAction($actionName);
146
		$type = substr($actionName, strrpos($actionName, '-') + 1);
147
		$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...
148
		$endpoint = '/' . NameUtils::pluralize(NameUtils::dasherize($modelName)) . '/{id}/relationship/' . 
149
			NameUtils::dasherize($single ? $relatedName : NameUtils::pluralize($relatedName));
150
151
		$path = $paths->get($endpoint);
152
		$method = $this->getMethod($type);
153
		$operation = $path->getOperation($method);
154
		$operation->setDescription($action->getTitle());
155
		$operation->setOperationId($action->getName());
156
		$operation->getTags()->clear();
157
		$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...
158
		
159
		$params = $operation->getParameters();
160
		$responses = $operation->getResponses();
161
		
162
		// general model related params
163
		// params
164
		$id = $params->getByName('id');
165
		$id->setIn('path');
166
		$id->setDescription(sprintf('The %s id', $modelName));
167
		$id->setRequired(true);
168
		$id->setType('integer');
169
		
170
		if ($suffix == 'add' || $suffix == 'update') {
171
			$body = $params->getByName('body');
172
			$body->setName('body');
173
			$body->setIn('body');
174
			$body->setDescription(sprintf('%ss %s', ucfirst($suffix), $relatedName));
175
			$body->setRequired(true);
176
			
177
			$props = $body->getSchema()->setType('object')->getProperties();
178
			$data = $props->get('data');
179
			
180
			if ($single) {
181
				$data->setRef('#/definitions/ResourceIdentifier');
182
			} else if ($many) {
183
				$data
184
					->setType('array')
185
					->getItems()->setRef('#/definitions/ResourceIdentifier');
186
			}
187
		}
188
		
189
		// response
190
		$ok = $responses->get('200');
191
		$ok->setDescription('Retrieve ' . $relatedName . ' from ' . $modelName);
192
		$props = $ok->getSchema()->setType('object')->getProperties();
193
		$links = $props->get('links')->setType('object')->getProperties();
194
		$links->get('self')->setType('string');
195
		if ($single) {
196
			$links->get('related')->setType('string');
197
		}
198
		$data = $props->get('data');
199
		if ($single) {
200
			$data->setType('object')->setRef('#/definitions/ResourceIdentifier');
201
		} else if ($many) {
202
			$data
203
				->setType('array')
204
				->getItems()->setRef('#/definitions/ResourceIdentifier');
205
		}
206
		
207
		$invalid = $responses->get('400');
208
		$invalid->setDescription('Invalid ID supplied');
209
		$invalid->getSchema()->setRef('#/definitions/Errors');
210
		
211
		$notfound = $responses->get('404');
212
		$notfound->setDescription(sprintf('No %s found', $modelName));
213
		$notfound->getSchema()->setRef('#/definitions/Errors');
214
	}
215
	
216
	protected function generateCRUDOperation(Paths $paths, $actionName) {
217
		$this->logger->notice('Generating CRUD Operation for: ' . $actionName);
218
		$database = $this->modelService->getDatabase();
219
		$action = $this->packageService->getAction($actionName);
220
		$modelName = $this->modelService->getModelNameByAction($action);
221
		$tableName = $this->modelService->getTableName($modelName);
222
		$codegen = $this->codegenService->getCodegen();
223
	
224
		if (!$database->hasTable($tableName)) {
225
			return;
226
		}
227
		
228
		if ($codegen->getExcludedApi()->contains($modelName)) {
0 ignored issues
show
Bug introduced by
The method getExcludedApi() 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...
229
			return;
230
		}
231
	
232
		$type = $this->packageService->getActionType($actionName, $modelName);
233
		$modelObjectName = $database->getTable($tableName)->getPhpName();
234
		$modelPluralName = NameUtils::pluralize($modelName);
235
	
236
		// find path branch
237
		switch ($type) {
238
			case 'list':
239
			case 'create':
240
				$endpoint = '/' . NameUtils::dasherize($modelPluralName);
241
				break;
242
	
243
			case 'read':
244
			case 'update':
245
			case 'delete':
246
				$endpoint = '/' . NameUtils::dasherize($modelPluralName) . '/{id}';
247
				break;
248
	
249
			default:
250
				throw new \RuntimeException(sprintf('type (%s) not found, can\'t continue.', $type));
251
				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...
252
		}
253
254
		$path = $paths->get($endpoint);
255
		$method = $this->getMethod($type);
256
		$operation = $path->getOperation($method);
257
		$operation->setDescription($action->getTitle());
258
		$operation->setOperationId($action->getName());
259
		$operation->getTags()->clear();
260
		$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...
261
	
262
		$params = $operation->getParameters();
263
		$responses = $operation->getResponses();
264
	
265
		switch ($type) {
266
			case 'list':
267
				$ok = $responses->get('200');
268
				$ok->setDescription(sprintf('Array of %s', $modelPluralName));
269
				$ok->getSchema()->setRef('#/definitions/' . 'Paged' . NameUtils::pluralize($modelObjectName));
270
				break;
271
	
272
			case 'create':
273
				// params
274
				$body = $params->getByName('body');
275
				$body->setName('body');
276
				$body->setIn('body');
277
				$body->setDescription(sprintf('The new %s', $modelName));
278
				$body->setRequired(true);
279
				$body->getSchema()->setRef('#/definitions/Writable' . $modelObjectName);
280
	
281
				// response
282
				$ok = $responses->get('201');
283
				$ok->setDescription(sprintf('%s created', $modelName));
284
				break;
285
	
286
			case 'read':
287
				// response
288
				$ok = $responses->get('200');
289
				$ok->setDescription(sprintf('gets the %s', $modelName));
290
				$ok->getSchema()->setRef('#/definitions/' . $modelObjectName);
291
				break;
292
	
293
			case 'update':
294
				// response
295
				$ok = $responses->get('200');
296
				$ok->setDescription(sprintf('%s updated', $modelName));
297
				$ok->getSchema()->setRef('#/definitions/' . $modelObjectName);
298
				break;
299
	
300
			case 'delete':
301
				// response
302
				$ok = $responses->get('204');
303
				$ok->setDescription(sprintf('%s deleted', $modelName));
304
				break;
305
		}
306
	
307
		if ($type == 'read' || $type == 'update' || $type == 'delete') {
308
			// params
309
			$id = $params->getByName('id');
310
			$id->setIn('path');
311
			$id->setDescription(sprintf('The %s id', $modelName));
312
			$id->setRequired(true);
313
			$id->setType('integer');
314
	
315
			// response
316
			$invalid = $responses->get('400');
317
			$invalid->setDescription('Invalid ID supplied');
318
			$invalid->getSchema()->setRef('#/definitions/Errors');
319
				
320
			$notfound = $responses->get('404');
321
			$notfound->setDescription(sprintf('No %s found', $modelName));
322
			$notfound->getSchema()->setRef('#/definitions/Errors');
323
		}
324
	}
325
	
326
	private function getMethod($type) {
327
		$methods = [
328
			'list' => 'get',
329
			'create' => 'post',
330
			'read' => 'get',
331
			'update' => 'patch',
332
			'delete' => 'delete',
333
			'add' => 'post',
334
			'remove' => 'delete'
335
		];
336
	
337
		return $methods[$type];
338
	}
339
340
	protected function generateDefinitions(Swagger $swagger) {
341
		$definitions = $swagger->getDefinitions();
342
		
343
		// models
344
		$modelName = $this->io->getInput()->getOption('model');
345
		if ($modelName !== null) {
346
			$model = $this->modelService->getModel($modelName);
347
			$this->generateDefinition($definitions, $model);
348
		} else {
349
			foreach ($this->modelService->getModels() as $model) {
350
				$this->generateDefinition($definitions, $model);
351
			}
352
		}
353
		
354
		// general definitions
355
		$this->generateErrorDefinition($definitions);
356
		$this->generatePagedMeta($definitions);
357
		$this->generateResourceIdentifier($definitions);
358
	}
359
	
360
	protected function generateErrorDefinition(Definitions $definitions) {
361
		$definitions->get('Errors')->setType('array')->getItems()->setRef('#/definitions/Error');
362
		
363
		$error = $definitions->get('Error')->setType('object')->getProperties();
364
		$error->get('id')->setType('string');
365
		$error->get('status')->setType('string');
366
		$error->get('code')->setType('string');
367
		$error->get('title')->setType('string');
368
		$error->get('detail')->setType('string');
369
		$error->get('meta')->setType('object');
370
	}
371
	
372
	protected function generatePagedMeta(Definitions $definitions) {
373
		if ($this->needsPagedMeta) {
374
			$props = $definitions->get('PagedMeta')->setType('object')->getProperties();
375
			$names = ['total', 'first', 'next', 'previous', 'last'];
376
			
377
			foreach ($names as $name) {
378
				$props->get($name)->setType('integer');
379
			}
380
		}
381
	}
382
	
383
	protected function generateResourceIdentifier(Definitions $definitions) {
384
		if ($this->needsResourceIdentifier) {
385
			$props = $definitions->get('ResourceIdentifier')->setType('object')->getProperties();
386
			$this->generateIdentifier($props);
387
		}
388
	}
389
	
390
	protected function generateIdentifier(Definitions $props) {
391
		$props->get('id')->setType('string');
392
		$props->get('type')->setType('string');
393
	}
394
	
395
	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...
396
		$data = $props->get('data')->setType('object')->getProperties();
397
		$this->generateIdentifier($data);
398
		return $data;
399
	}
400
401
	protected function generateDefinition(Definitions $definitions, Table $model) {
402
		$this->logger->notice('Generating Definition for: ' . $model->getOriginCommonName());
403
		$modelObjectName = $model->getPhpName();
404
		$codegen = $this->codegenService->getCodegen();
405
		
406
		// stop if model is excluded
407
		if ($codegen->getExcludedApi()->contains($model->getOriginCommonName())) {
0 ignored issues
show
Bug introduced by
The method getExcludedApi() 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...
408
			return;
409
		}
410
		
411
		// paged model
412
		$this->needsPagedMeta = true;
413
		$pagedModel = 'Paged' . NameUtils::pluralize($modelObjectName);
414
		$paged = $definitions->get($pagedModel)->setType('object')->getProperties();
415
		$paged->get('data')
416
			->setType('array')
417
			->getItems()->setRef('#/definitions/' . $modelObjectName);
418
		$paged->get('meta')->setRef('#/definitions/PagedMeta');
419
		
420
		// writable model
421
		$writable = $definitions->get('Writable' . $modelObjectName)->setType('object')->getProperties();
422
		$this->generateModelProperties($writable, $model, true);
423
424
		// readable model
425
		$readable = $definitions->get($modelObjectName)->setType('object')->getProperties();
426
		$this->generateModelProperties($readable, $model, false);
427
	}
428
	
429
	protected function generateModelProperties(Definitions $props, Table $model, $write = false) {
430
		// links
431
		if (!$write) {
432
			$links = $props->get('links')->setType('object')->getProperties();
433
			$links->get('self')->setType('string');
434
		}
435
		
436
		// data
437
		$data = $this->generateResourceData($props);
438
		
439
		// attributes
440
		$attrs = $data->get('attributes');
441
		$attrs->setType('object');
442
		$this->generateModelAttributes($attrs->getProperties(), $model, $write);
443
444
		// relationships
445
		if ($this->hasRelationships($model)) {
446
			$relationships = $data->get('relationships')->setType('object')->getProperties();
447
			$this->generateModelRelationships($relationships, $model, $write);
448
		}
449
	}
450
	
451
	protected function generateModelAttributes(Definitions $props, Table $model, $write = false) {
452
		$modelName = $model->getOriginCommonName();
453
		$filter = $write 
454
			? $this->codegenService->getCodegen()->getWriteFilter($modelName)
455
			: $this->codegenService->getCodegen()->getReadFilter($modelName);
456
457
		if ($write) {
458
			$filter = array_merge($filter, $this->codegenService->getComputedFields($model));
459
		}
460
		
461
		// no id, already in identifier
462
		$filter[] = 'id';
463
		$types = ['int' => 'integer'];
464
		
465
		foreach ($model->getColumns() as $col) {
466
			$prop = $col->getName();
467
			
468
			if (!in_array($prop, $filter)) {
469
				$type = $col->getPhpType();
470
				if (isset($types[$type])) {
471
					$type = $types[$type];
472
				}
473
				$props->get($prop)->setType($type);
474
			}
475
		}
476
477
		return $props;
478
	}
479
	
480
	protected function hasRelationships(Table $model) {
481
		$relationships = $this->modelService->getRelationships($model);
482
		return $relationships->size() > 0;
483
	}
484
	
485
	protected function generateModelRelationships(Definitions $props, Table $model, $write = false) {
486
		$relationships = $this->modelService->getRelationships($model);
487
		
488
		foreach ($relationships->getAll() as $relationship) {
489
			// one-to-one
490
			if ($relationship->getType() == Relationship::ONE_TO_ONE) {
491
				$typeName = $relationship->getRelatedTypeName();
492
				$rel = $props->get($typeName)->setType('object')->getProperties();
493
				
494
				// links
495
				if (!$write) {
496
					$links = $rel->get('links')->setType('object')->getProperties();
497
					$links->get('self')->setType('string');
498
				}
499
				
500
				// data
501
				$this->generateResourceData($rel);
502
			}
503
		
504
			// ?-to-many
505
			else {
506
				$typeName = $relationship->getRelatedPluralTypeName();
507
				$rel = $props->get($typeName)->setType('object')->getProperties();
508
				
509
				// links
510
				if (!$write) {
511
					$links = $rel->get('links')->setType('object')->getProperties();
512
					$links->get('self')->setType('string');
513
				}
514
				
515
				// data
516
				$this->needsResourceIdentifier = true;
517
				$rel->get('data')
518
					->setType('array')
519
					->getItems()->setRef('#/definitions/ResourceIdentifier');
520
			}
521
		}
522
	}
523
524
}