Completed
Push — master ( 66405f...98cd2e )
by Thomas
06:52
created

GenerateApiCommand::generateRelationshipActions()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 23
ccs 0
cts 18
cp 0
rs 8.7972
cc 4
eloc 13
nc 5
nop 0
crap 20
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 keeko\tools\command\AbstractGenerateCommand;
12
use phootwork\file\File;
13 20
use phootwork\lang\Text;
14 20
use Propel\Generator\Model\Table;
15 20
use Symfony\Component\Console\Input\InputInterface;
16 20
use Symfony\Component\Console\Output\OutputInterface;
17
18
class GenerateApiCommand extends AbstractGenerateCommand {
19 20
20 20
	protected function configure() {
21
		$this
22
			->setName('generate:api')
23
			->setDescription('Generates the api for the module')
24
		;
25
		
26
		$this->configureGenerateOptions();
27
			
28
		parent::configure();
29
	}
30
	
31
	/**
32
	 * Checks whether api can be generated at all by reading composer.json and verify
33
	 * all required information are available
34
	 */
35
	private function preCheck() {
36
		$module = $this->packageService->getModule();
37
		if ($module === null) {
38
			throw new \DomainException('No module definition found in composer.json - please run `keeko init`.');
39
		}
40
	}
41
42
	protected function execute(InputInterface $input, OutputInterface $output) {
43
		$this->preCheck();
44
		
45
		// generate api
46
		$api = new File($this->project->getApiFileName());
47
		
48
		// if generation is forced, generate new API from scratch
49
		if ($input->getOption('force')) {
50
			$swagger = new Swagger();
51
		}
52
		
53
		// ... anyway reuse existing one
54
		else {
55
			if (!$api->exists()) {
56
				$api->write('{}');
57
			}
58
			
59
			$swagger = Swagger::fromFile($this->project->getApiFileName());
60
		}
61
62
		$module = $this->package->getKeeko()->getModule();
63
		$swagger->setVersion('2.0');
64
		$swagger->getInfo()->setTitle($module->getTitle() . ' API');
65
		$swagger->getTags()->clear();
66
		$swagger->getTags()->add(new Tag(['name' => $module->getSlug()]));
67
		
68
		$this->generatePaths($swagger);
69
		$this->generateDefinitions($swagger);
70
		
71
		$this->jsonService->write($api->getPathname(), $swagger->toArray());
72
		$this->io->writeln(sprintf('API for <info>%s</info> written at <info>%s</info>', $this->package->getFullName(), $api->getPathname()));
73
	}
74
	
75
	/**
76
	 * Adds the APIModelInterface to package models
77
	 * 
78
	 */
79
	protected function prepareModels() {
80
		$models = $this->modelService->getPackageModelNames();
81
		
82
		foreach ($models as $modelName) {
83
			$tableName = $this->modelService->getTableName($modelName);
84
			$model = $this->modelService->getModel($tableName);
85
			$class = new PhpClass(str_replace('\\\\', '\\', $model->getNamespace() . '\\' . $model->getPhpName()));
86
			$file = new File($this->codegenService->getFilename($class));
87
			
88
			if ($file->exists()) {
89
				$class = PhpClass::fromFile($this->codegenService->getFilename($class));
90
				if (!$class->hasInterface('APIModelInterface')) {
91
					$class->addUseStatement('keeko\\core\\model\\types\\APIModelInterface');
92
					$class->addInterface('APIModelInterface');
93
// 					$typeName =  $this->package->getCanonicalName() . '.' . NameUtils::dasherize($modelName);
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...
94
// 					$class->setMethod(PhpMethod::create('getAPIType')
95
// 						->setBody('return \''.$typeName . '\';')
96
// 					);
97
	
98
					$this->codegenService->dumpStruct($class, true);
99
				}
100
			}
101
		}
102
	}
103
104
	protected function generatePaths(Swagger $swagger) {
105
		$paths = $swagger->getPaths();
106
		
107
		foreach ($this->packageService->getModule()->getActionNames() as $name) {
108
			$this->generateOperation($paths, $name);
109
		}
110
	}
111
	
112
	protected function generateOperation(Paths $paths, $actionName) {
113
		$this->logger->notice('Generating Operation for: ' . $actionName);
114
115
		if (Text::create($actionName)->contains('relationship')) {
116
			$this->generateRelationshipOperation($paths, $actionName);
117
		} else {
118
			$this->generateCRUDOperation($paths, $actionName);
119
		}
120
	}
121
122
	protected function generateRelationshipOperation(Paths $paths, $actionName) {
123
		$this->logger->notice('Generating Relationship Operation for: ' . $actionName);
124
		$prefix = substr($actionName, 0, strrpos($actionName, 'relationship') + 12);
125
		$module = $this->packageService->getModule();
126
127
		// test for to-many relationship:
128
		$many = $module->hasAction($prefix . '-read') 
129
			&& $module->hasAction($prefix . '-update')
130
			&& $module->hasAction($prefix . '-add')
131
			&& $module->hasAction($prefix . '-remove')
132
		;
133
		$single = $module->hasAction($prefix . '-read') 
134
			&& $module->hasAction($prefix . '-update')
135
			&& !$many
136
		;
137
		
138
		if (!$many && !$single) {
139
			$this->io->writeln(sprintf('<comment>Couldn\'t detect whether %s is a to-one or to-many relationship, skin generating endpoints</comment>', $actionName));
140
			return;
141
		}
142
		
143
		// find model names
144
		$modelName = substr($actionName, 0, strpos($actionName, 'to') - 1);
145
		$start = strpos($actionName, 'to') + 3;
146
		$foreignModelName = substr($actionName, $start, strpos($actionName, 'relationship') - 1 - $start);
147
148
		// find relationship objects
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
149
// 		$model = $this->modelService->getModel(NameUtils::toSnakeCase($modelName));
150
// 		$foreignModel = $this->modelService->getModel(NameUtils::toSnakeCase($foreignModelName));
151
// 		$fk = null;
152
// 		foreach ($model->getForeignKeys() as $key) {
153
// 			if ($key->getForeignTable() == $foreignModel) {
154
// 				$fk = $key;
155
// 			}
156
// 		}
157
// 		$fkName = $fk->getLocalColumn()->getName();
158
		
159
		$action = $this->packageService->getAction($actionName);
160
		$type = substr($actionName, strrpos($actionName, '-') + 1);
161
		$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...
162
		$endpoint = '/' . NameUtils::pluralize($modelName) . '/{id}/relationship/' . ($single ?
163
			$foreignModelName : NameUtils::pluralize($foreignModelName));
164
		
165
		$path = $paths->get($endpoint);
166
		$method = $this->getMethod($type);
167
		$operation = $path->getOperation($method);
168
		$operation->setDescription($action->getTitle());
169
		$operation->setOperationId($action->getName());
170
		$operation->getTags()->clear();
171
		$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...
172
		
173
		$params = $operation->getParameters();
174
		$responses = $operation->getResponses();
175
		
176
		// general model related params
177
		// params
178
		$id = $params->getByName('id');
179
		$id->setIn('path');
180
		$id->setDescription(sprintf('The %s id', $modelName));
181
		$id->setRequired(true);
182
		$id->setType('integer');
183
		
184
		// response
185
		$invalid = $responses->get('400');
186
		$invalid->setDescription('Invalid ID supplied');
187
		
188
		$notfound = $responses->get('404');
189
		$notfound->setDescription(sprintf('No %s found', $modelName));
190
	}
191
	
192
	protected function generateCRUDOperation(Paths $paths, $actionName) {
193
		$this->logger->notice('Generating CRUD Operation for: ' . $actionName);
194
		$database = $this->modelService->getDatabase();
195
		$action = $this->packageService->getAction($actionName);
196
		$modelName = $this->modelService->getModelNameByAction($action);
197
		$tableName = $this->modelService->getTableName($modelName);
198
	
199
		if (!$database->hasTable($tableName)) {
200
			return $paths;
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
		
358
		// paged model
359
		$pagedModel = 'Paged' . NameUtils::pluralize($modelObjectName);
360
		$paged = $definitions->get($pagedModel)->setType('object')->getProperties();
361
		$paged->get('data')
362
			->setType('array')
363
			->getItems()->setRef('#/definitions/' . $modelObjectName);
364
		$paged->get('meta')->setRef('#/definitions/PagedMeta');
365
		
366
		// writable model
367
		$writable = $definitions->get('Writable' . $modelObjectName)->setType('object')->getProperties();
368
		$this->generateModelProperties($writable, $model, true);
369
370
		// readable model
371
		$readable = $definitions->get($modelObjectName)->setType('object')->getProperties();
372
		$this->generateModelProperties($readable, $model, false);
373
	}
374
	
375
	protected function generateModelProperties(Definitions $props, Table $model, $write = false) {
376
		// links
377
		if (!$write) {
378
			$links = $props->get('links')->setType('object')->getProperties();
379
			$links->get('self')->setType('string');
380
		}
381
		
382
		// data
383
		$data = $this->generateResourceData($props);
384
		
385
		// attributes
386
		$attrs = $data->get('attributes');
387
		$attrs->setType('object');
388
		$this->generateModelAttributes($attrs->getProperties(), $model, $write);
389
390
		// relationships
391
		if ($this->hasRelationships($model)) {
392
			$relationships = $data->get('relationships')->setType('object')->getProperties();
393
			$this->generateModelRelationships($relationships, $model, $write);
394
		}
395
	}
396
	
397
	protected function generateModelAttributes(Definitions $props, Table $model, $write = false) {
398
		$modelName = $model->getOriginCommonName();
399
		$filter = $write 
400
			? $this->codegenService->getCodegen()->getWriteFilter($modelName)
401
			: $this->codegenService->getCodegen()->getReadFilter($modelName);
402
403
		if ($write) {
404
			$filter = array_merge($filter, $this->codegenService->getComputedFields($model));
405
		}
406
		
407
		// no id, already in identifier
408
		$filter[] = 'id';
409
		$types = ['int' => 'integer'];
410
		
411
		foreach ($model->getColumns() as $col) {
412
			$prop = $col->getName();
413
			
414
			if (!in_array($prop, $filter)) {
415
				$type = $col->getPhpType();
416
				if (isset($types[$type])) {
417
					$type = $types[$type];
418
				}
419
				$props->get($prop)->setType($type);
420
			}
421
		}
422
423
		return $props;
424
	}
425
	
426
	protected function hasRelationships(Table $model) {
427
		return (count($model->getForeignKeys()) + count($model->getCrossFks())) > 0;
428
	}
429
	
430
	protected function generateModelRelationships(Definitions $props, Table $model, $write = false) {
431
		$relationships = $this->modelService->getRelationships($model);
432
		
433
		// to-one
434
		foreach ($relationships['one'] as $one) {
435
			$fk = $one['fk'];
436
			$typeName = NameUtils::dasherize($fk->getForeignTable()->getOriginCommonName());
1 ignored issue
show
Bug introduced by
The method dasherize() cannot be called from this context as it is declared private in class keeko\framework\utils\NameUtils.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
437
			$rel = $props->get($typeName)->setType('object')->getProperties();
438
			
439
			// links
440
			if (!$write) {
441
				$links = $rel->get('links')->setType('object')->getProperties();
442
				$links->get('self')->setType('string');
443
			}
444
			
445
			// data
446
			$this->generateResourceData($rel);
447
		}
448
		
449
		// to-many
450
		foreach ($relationships['many'] as $many) {
451
			$fk = $many['fk'];
452
			$foreignModel = $fk->getForeignTable();
453
			$typeName = NameUtils::pluralize(NameUtils::dasherize($foreignModel->getOriginCommonName()));
1 ignored issue
show
Bug introduced by
The method dasherize() cannot be called from this context as it is declared private in class keeko\framework\utils\NameUtils.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
454
			$rel = $props->get($typeName)->setType('object')->getProperties();
455
			
456
			// links
457
			if (!$write) {
458
				$links = $rel->get('links')->setType('object')->getProperties();
459
				$links->get('self')->setType('string');
460
			}
461
			
462
			// data
463
			$rel->get('data')
464
				->setType('array')
465
				->getItems()->setRef('#/definitions/ResourceIdentifier');
466
		}
467
	}
468
469
}