Completed
Push — master ( d6c5c0...118f52 )
by Thomas
04:57
created

generateRelationshipOperation()   C

Complexity

Conditions 10
Paths 14

Size

Total Lines 93
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 7
Bugs 1 Features 0
Metric Value
c 7
b 1
f 0
dl 0
loc 93
ccs 0
cts 63
cp 0
rs 5.0515
cc 10
eloc 67
nc 14
nop 2
crap 110

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
use keeko\tools\generator\Types;
17
18
class GenerateApiCommand extends AbstractKeekoCommand {
19 20
	
20 20
	private $needsResourceIdentifier = false;
21
	private $needsPagedMeta = false;
22
23
	protected function configure() {
24
		$this
25
			->setName('generate:api')
26
			->setDescription('Generates the api for the module')
27
			->addOption(
28
				'model',
29
				'm',
30
				InputOption::VALUE_OPTIONAL,
31
				'The model for which the actions should be generated, when there is no name argument (if ommited all models will be generated)'
32
			)
33
		;
34
		
35
		$this->configureGenerateOptions();
36
			
37
		parent::configure();
38
	}
39
	
40
	/**
41
	 * Checks whether api can be generated at all by reading composer.json and verify
42
	 * all required information are available
43
	 */
44
	private function check() {
45
		$module = $this->packageService->getModule();
46
		if ($module === null) {
47
			throw new \DomainException('No module definition found in composer.json - please run `keeko init`.');
48
		}
49
	}
50
51
	protected function execute(InputInterface $input, OutputInterface $output) {
52
		$this->check();
53
		
54
		// generate api
55
		$api = new File($this->project->getApiFileName());
56
		
57
		// if generation is forced, generate new API from scratch
58
		if ($input->getOption('force')) {
59
			$swagger = new Swagger();
60
		}
61
		
62
		// ... anyway reuse existing one
63
		else {
64
			if (!$api->exists()) {
65
				$api->write('{}');
66
			}
67
			
68
			$swagger = Swagger::fromFile($this->project->getApiFileName());
69
		}
70
71
		$module = $this->package->getKeeko()->getModule();
72
		$swagger->setVersion('2.0');
73
		$swagger->getInfo()->setTitle($module->getTitle() . ' API');
74
		$swagger->getTags()->clear();
75
		$swagger->getTags()->add(new Tag(['name' => $module->getSlug()]));
76
		
77
		$this->generatePaths($swagger);
78
		$this->generateDefinitions($swagger);
79
		
80
		$this->jsonService->write($api->getPathname(), $swagger->toArray());
81
		$this->io->writeln(sprintf('API for <info>%s</info> written at <info>%s</info>', $this->package->getFullName(), $api->getPathname()));
82
	}
83
84
	protected function generatePaths(Swagger $swagger) {
85
		$paths = $swagger->getPaths();
86
		
87
		foreach ($this->packageService->getModule()->getActionNames() as $name) {
88
			$this->generateOperation($paths, $name);
89
		}
90
	}
91
	
92
	protected function generateOperation(Paths $paths, $actionName) {
93
		$this->logger->notice('Generating Operation for: ' . $actionName);
94
95
		if (Text::create($actionName)->contains('relationship')) {
96
			$this->generateRelationshipOperation($paths, $actionName);
97
		} else {
98
			$this->generateCRUDOperation($paths, $actionName);
99
		}
100
	}
101
102
	protected function generateRelationshipOperation(Paths $paths, $actionName) {
103
		$this->logger->notice('Generating Relationship Operation for: ' . $actionName);
104
		$parsed = $this->factory->getActionNameGenerator()->parseRelationship($actionName);
105
		$type = $parsed['type'];
106
		$modelName = $parsed['modelName'];
107
		$model = $this->modelService->getModel($modelName);
108
		$relatedTypeName = NameUtils::dasherize($parsed['relatedName']);
109
		$relationship = $this->modelService->getRelationship($model, $relatedTypeName);
110
		
111
		if ($relationship === null) {
112
			return;
113
		}
114
115
		// see if either one of them is excluded
116
		$relatedName = $relationship->getRelatedName();
117
		$foreignModelName = $relationship->getForeign()->getOriginCommonName();
118
		$excluded = $this->codegenService->getCodegen()->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...
119
		if ($excluded->contains($modelName) || $excluded->contains($foreignModelName)) {
120
			return;
121
		}
122
		
123
		// continue if neither model nor related model is excluded
124
		$action = $this->packageService->getAction($actionName);
125
		$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...
126
		$endpoint = '/' . NameUtils::pluralize(NameUtils::dasherize($modelName)) . '/{id}/relationship/' . 
127
			NameUtils::dasherize($relationship->isOneToOne() 
128
				? $relatedTypeName 
129
				: NameUtils::pluralize($relatedTypeName));
130
131
		$path = $paths->get($endpoint);
132
		$method = $this->getMethod($type);
133
		$operation = $path->getOperation($method);
134
		$operation->setDescription($action->getTitle());
135
		$operation->setOperationId($action->getName());
136
// 		$operation->getTags()->clear();
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% 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...
137
// 		$operation->getTags()->add(new Tag($this->package->getKeeko()->getModule()->getSlug()));
138
		
139
		$params = $operation->getParameters();
140
		$responses = $operation->getResponses();
141
		
142
		// general model related params
143
		// params
144
		$id = $params->getByName('id');
145
		$id->setIn('path');
146
		$id->setDescription(sprintf('The %s id', $modelName));
147
		$id->setRequired(true);
148
		$id->setType('integer');
149
		
150
		if ($type == Types::ADD || $type == Types::UPDATE) {
151
			$body = $params->getByName('body');
152
			$body->setName('body');
153
			$body->setIn('body');
154
			$body->setDescription(sprintf('%ss %s', ucfirst($type), $relatedName));
155
			$body->setRequired(true);
156
			
157
			$props = $body->getSchema()->setType('object')->getProperties();
158
			$data = $props->get('data');
159
			
160
			if ($relationship->isOneToOne()) {
161
				$data->setRef('#/definitions/ResourceIdentifier');
162
			} else {
163
				$data
164
					->setType('array')
165
					->getItems()->setRef('#/definitions/ResourceIdentifier');
166
			}
167
		}
168
		
169
		// response
170
		$ok = $responses->get('200');
171
		$ok->setDescription('Retrieve ' . $relatedName . ' from ' . $modelName);
172
		$props = $ok->getSchema()->setType('object')->getProperties();
173
		$links = $props->get('links')->setType('object')->getProperties();
174
		$links->get('self')->setType('string');
175
		if ($relationship->isOneToOne()) {
176
			$links->get('related')->setType('string');
177
		}
178
		$data = $props->get('data');
179
		if ($relationship->isOneToOne()) {
180
			$data->setType('object')->setRef('#/definitions/ResourceIdentifier');
181
		} else {
182
			$data
183
				->setType('array')
184
				->getItems()->setRef('#/definitions/ResourceIdentifier');
185
		}
186
		
187
		$invalid = $responses->get('400');
188
		$invalid->setDescription('Invalid ID supplied');
189
		$invalid->getSchema()->setRef('#/definitions/Errors');
190
		
191
		$notfound = $responses->get('404');
192
		$notfound->setDescription(sprintf('No %s found', $modelName));
193
		$notfound->getSchema()->setRef('#/definitions/Errors');
194
	}
195
	
196
	protected function generateCRUDOperation(Paths $paths, $actionName) {
197
		$this->logger->notice('Generating CRUD Operation for: ' . $actionName);
198
		$database = $this->modelService->getDatabase();
199
		$action = $this->packageService->getAction($actionName);
200
		$modelName = $this->modelService->getModelNameByAction($action);
0 ignored issues
show
Bug introduced by
It seems like $action defined by $this->packageService->getAction($actionName) on line 199 can be null; however, keeko\tools\services\Mod...:getModelNameByAction() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
201
		$tableName = $this->modelService->getTableName($modelName);
202
		$codegen = $this->codegenService->getCodegen();
203
	
204
		if (!$database->hasTable($tableName)) {
205
			return;
206
		}
207
		
208
		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...
209
			return;
210
		}
211
	
212
		$type = $this->packageService->getActionType($actionName, $modelName);
213
		$modelObjectName = $database->getTable($tableName)->getPhpName();
214
		$modelPluralName = NameUtils::pluralize($modelName);
215
	
216
		// find path branch
217
		switch ($type) {
218
			case Types::PAGINATE:
219
			case Types::CREATE:
220
				$endpoint = '/' . NameUtils::dasherize($modelPluralName);
221
				break;
222
	
223
			case Types::READ:
224
			case Types::UPDATE:
225
			case Types::DELETE:
226
				$endpoint = '/' . NameUtils::dasherize($modelPluralName) . '/{id}';
227
				break;
228
	
229
			default:
230
				throw new \RuntimeException(sprintf('type (%s) not found, can\'t continue.', $type));
231
				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...
232
		}
233
234
		$path = $paths->get($endpoint);
235
		$method = $this->getMethod($type);
236
		$operation = $path->getOperation($method);
237
		$operation->setDescription($action->getTitle());
238
		$operation->setOperationId($action->getName());
239
// 		$operation->getTags()->clear();
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% 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...
240
// 		$operation->getTags()->add(new Tag($this->package->getKeeko()->getModule()->getSlug()));
241
	
242
		$params = $operation->getParameters();
243
		$responses = $operation->getResponses();
244
	
245
		switch ($type) {
246
			case Types::PAGINATE:
247
				$ok = $responses->get('200');
248
				$ok->setDescription(sprintf('Array of %s', $modelPluralName));
249
				$ok->getSchema()->setRef('#/definitions/' . 'Paged' . NameUtils::pluralize($modelObjectName));
250
				break;
251
	
252
			case Types::CREATE:
253
				// params
254
				$body = $params->getByName('body');
255
				$body->setName('body');
256
				$body->setIn('body');
257
				$body->setDescription(sprintf('The new %s', $modelName));
258
				$body->setRequired(true);
259
				$body->getSchema()->setRef('#/definitions/Writable' . $modelObjectName);
260
	
261
				// response
262
				$ok = $responses->get('201');
263
				$ok->setDescription(sprintf('%s created', $modelName));
264
				break;
265
	
266
			case Types::READ:
267
				// response
268
				$ok = $responses->get('200');
269
				$ok->setDescription(sprintf('gets the %s', $modelName));
270
				$ok->getSchema()->setRef('#/definitions/' . $modelObjectName);
271
				break;
272
	
273
			case Types::UPDATE:
274
				// response
275
				$ok = $responses->get('200');
276
				$ok->setDescription(sprintf('%s updated', $modelName));
277
				$ok->getSchema()->setRef('#/definitions/' . $modelObjectName);
278
				break;
279
	
280
			case Types::DELETE:
281
				// response
282
				$ok = $responses->get('204');
283
				$ok->setDescription(sprintf('%s deleted', $modelName));
284
				break;
285
		}
286
	
287
		if ($type == Types::READ || $type == Types::UPDATE || $type == Types::DELETE) {
288
			// params
289
			$id = $params->getByName('id');
290
			$id->setIn('path');
291
			$id->setDescription(sprintf('The %s id', $modelName));
292
			$id->setRequired(true);
293
			$id->setType('integer');
294
	
295
			// response
296
			$invalid = $responses->get('400');
297
			$invalid->setDescription('Invalid ID supplied');
298
			$invalid->getSchema()->setRef('#/definitions/Errors');
299
				
300
			$notfound = $responses->get('404');
301
			$notfound->setDescription(sprintf('No %s found', $modelName));
302
			$notfound->getSchema()->setRef('#/definitions/Errors');
303
		}
304
	}
305
	
306
	private function getMethod($type) {
307
		$methods = [
308
			Types::PAGINATE => 'get',
309
			Types::CREATE => 'post',
310
			Types::READ => 'get',
311
			Types::UPDATE => 'patch',
312
			Types::DELETE => 'delete',
313
			Types::ADD => 'post',
314
			Types::REMOVE => 'delete'
315
		];
316
	
317
		return $methods[$type];
318
	}
319
320
	protected function generateDefinitions(Swagger $swagger) {
321
		$definitions = $swagger->getDefinitions();
322
		
323
		// models
324
		$modelName = $this->io->getInput()->getOption('model');
325
		if ($modelName !== null) {
326
			$model = $this->modelService->getModel($modelName);
327
			$this->generateDefinition($definitions, $model);
328
		} else {
329
			foreach ($this->modelService->getModels() as $model) {
330
				$this->generateDefinition($definitions, $model);
331
			}
332
		}
333
		
334
		// general definitions
335
		$this->generateErrorDefinition($definitions);
336
		$this->generatePagedMeta($definitions);
337
		$this->generateResourceIdentifier($definitions);
338
	}
339
	
340
	protected function generateErrorDefinition(Definitions $definitions) {
341
		$definitions->get('Errors')->setType('array')->getItems()->setRef('#/definitions/Error');
342
		
343
		$error = $definitions->get('Error')->setType('object')->getProperties();
344
		$error->get('id')->setType('string');
345
		$error->get('status')->setType('string');
346
		$error->get('code')->setType('string');
347
		$error->get('title')->setType('string');
348
		$error->get('detail')->setType('string');
349
		$error->get('meta')->setType('object');
350
	}
351
	
352
	protected function generatePagedMeta(Definitions $definitions) {
353
		if ($this->needsPagedMeta) {
354
			$props = $definitions->get('PagedMeta')->setType('object')->getProperties();
355
			$names = ['total', 'first', 'next', 'previous', 'last'];
356
			
357
			foreach ($names as $name) {
358
				$props->get($name)->setType('integer');
359
			}
360
		}
361
	}
362
	
363
	protected function generateResourceIdentifier(Definitions $definitions) {
364
		if ($this->needsResourceIdentifier) {
365
			$props = $definitions->get('ResourceIdentifier')->setType('object')->getProperties();
366
			$this->generateIdentifier($props);
367
		}
368
	}
369
	
370
	protected function generateIdentifier(Definitions $props) {
371
		$props->get('id')->setType('string');
372
		$props->get('type')->setType('string');
373
	}
374
	
375
	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...
376
		$data = $props->get('data')->setType('object')->getProperties();
377
		$this->generateIdentifier($data);
378
		return $data;
379
	}
380
381
	protected function generateDefinition(Definitions $definitions, Table $model) {
382
		$this->logger->notice('Generating Definition for: ' . $model->getOriginCommonName());
383
		$modelObjectName = $model->getPhpName();
384
		$codegen = $this->codegenService->getCodegen();
385
		
386
		// stop if model is excluded
387
		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...
388
			return;
389
		}
390
		
391
		// paged model
392
		$this->needsPagedMeta = true;
393
		$pagedModel = 'Paged' . NameUtils::pluralize($modelObjectName);
394
		$paged = $definitions->get($pagedModel)->setType('object')->getProperties();
395
		$paged->get('data')
396
			->setType('array')
397
			->getItems()->setRef('#/definitions/' . $modelObjectName);
398
		$paged->get('meta')->setRef('#/definitions/PagedMeta');
399
		
400
		// writable model
401
		$writable = $definitions->get('Writable' . $modelObjectName)->setType('object')->getProperties();
402
		$this->generateModelProperties($writable, $model, true);
403
404
		// readable model
405
		$readable = $definitions->get($modelObjectName)->setType('object')->getProperties();
406
		$this->generateModelProperties($readable, $model, false);
407
	}
408
	
409
	protected function generateModelProperties(Definitions $props, Table $model, $write = false) {
410
		// links
411
		if (!$write) {
412
			$links = $props->get('links')->setType('object')->getProperties();
413
			$links->get('self')->setType('string');
414
		}
415
		
416
		// data
417
		$data = $this->generateResourceData($props);
418
		
419
		// attributes
420
		$attrs = $data->get('attributes');
421
		$attrs->setType('object');
422
		$this->generateModelAttributes($attrs->getProperties(), $model, $write);
423
424
		// relationships
425
		if ($this->hasRelationships($model)) {
426
			$relationships = $data->get('relationships')->setType('object')->getProperties();
427
			$this->generateModelRelationships($relationships, $model, $write);
428
		}
429
	}
430
	
431
	protected function generateModelAttributes(Definitions $props, Table $model, $write = false) {
432
		$modelName = $model->getOriginCommonName();
433
		$filter = $write 
434
			? $this->codegenService->getCodegen()->getWriteFilter($modelName)
435
			: $this->codegenService->getCodegen()->getReadFilter($modelName);
436
437
		if ($write) {
438
			$filter = array_merge($filter, $this->codegenService->getComputedFields($model));
439
		}
440
		
441
		// no id, already in identifier
442
		$filter[] = 'id';
443
		$types = ['int' => 'integer'];
444
		
445
		foreach ($model->getColumns() as $col) {
446
			$prop = $col->getName();
447
			
448
			if (!in_array($prop, $filter)) {
449
				$type = $col->getPhpType();
450
				if (isset($types[$type])) {
451
					$type = $types[$type];
452
				}
453
				$props->get($prop)->setType($type);
454
			}
455
		}
456
457
		return $props;
458
	}
459
	
460
	protected function hasRelationships(Table $model) {
461
		$relationships = $this->modelService->getRelationships($model);
462
		return $relationships->size() > 0;
463
	}
464
	
465
	protected function generateModelRelationships(Definitions $props, Table $model, $write = false) {
466
		$relationships = $this->modelService->getRelationships($model);
467
		
468
		foreach ($relationships->getAll() as $relationship) {
469
			// one-to-one
470
			if ($relationship->getType() == Relationship::ONE_TO_ONE) {
471
				$typeName = $relationship->getRelatedTypeName();
472
				$rel = $props->get($typeName)->setType('object')->getProperties();
473
				
474
				// links
475
				if (!$write) {
476
					$links = $rel->get('links')->setType('object')->getProperties();
477
					$links->get('self')->setType('string');
478
				}
479
				
480
				// data
481
				$this->generateResourceData($rel);
482
			}
483
		
484
			// ?-to-many
485
			else {
486
				$typeName = $relationship->getRelatedPluralTypeName();
487
				$rel = $props->get($typeName)->setType('object')->getProperties();
488
				
489
				// links
490
				if (!$write) {
491
					$links = $rel->get('links')->setType('object')->getProperties();
492
					$links->get('self')->setType('string');
493
				}
494
				
495
				// data
496
				$this->needsResourceIdentifier = true;
497
				$rel->get('data')
498
					->setType('array')
499
					->getItems()->setRef('#/definitions/ResourceIdentifier');
500
			}
501
		}
502
	}
503
504
}