Completed
Push — master ( f1c264...f58245 )
by Thomas
04:22
created

GenerateResponseCommand::generateResponder()   C

Complexity

Conditions 16
Paths 74

Size

Total Lines 69
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 272

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 69
ccs 0
cts 0
cp 0
rs 5.6565
cc 16
eloc 36
nc 74
nop 1
crap 272

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\codegen\model\PhpClass;
5
use keeko\framework\utils\NameUtils;
6
use keeko\tools\generator\GeneratorFactory;
7
use keeko\tools\generator\responder\ApiJsonResponderGenerator;
8
use keeko\tools\generator\responder\SkeletonHtmlResponderGenerator;
9
use keeko\tools\generator\responder\SkeletonJsonResponderGenerator;
10
use keeko\tools\generator\responder\ToManyRelationshipJsonResponderGenerator;
11
use keeko\tools\generator\responder\ToOneRelationshipJsonResponderGenerator;
12
use keeko\tools\generator\responder\TwigHtmlResponderGenerator;
13
use keeko\tools\helpers\QuestionHelperTrait;
14
use keeko\tools\utils\NamespaceResolver;
15
use phootwork\collection\Set;
16
use phootwork\file\File;
17
use phootwork\lang\Text;
18
use Symfony\Component\Console\Input\InputArgument;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Input\InputOption;
21
use Symfony\Component\Console\Output\OutputInterface;
22
use Symfony\Component\Console\Question\ConfirmationQuestion;
23
use Symfony\Component\Console\Question\Question;
24 20
25 20
class GenerateResponseCommand extends AbstractGenerateCommand {
26 20
27 20
	use QuestionHelperTrait;
28 20
	
29 20
	protected $traits;
30 20
	
31
	protected function configure() {
32 20
		$this->traits = new Set();
33 20
		
34 20
		$this
35 20
			->setName('generate:response')
36 20
			->setDescription('Generates code for a responder')
37 20
			->addArgument(
38
				'name',
39 20
				InputArgument::OPTIONAL,
40 20
				'The name of the action, which should be generated. Typically in the form %nomen%-%verb% (e.g. user-create)'
41 20
			)
42 20
			->addOption(
43 20
				'model',
44 20
				'm',
45 1
				InputOption::VALUE_OPTIONAL,
46 20
				'The model for which the response should be generated, when there is no name argument (if ommited all models will be generated)'
47
			)
48
			->addOption(
49 20
				'format',
50
				'',
51 20
				InputOption::VALUE_OPTIONAL,
52 20
				'The response format to create',
53
				'json'
54
			)
55
			->addOption(
56
				'template',
57
				'',
58 7
				InputOption::VALUE_OPTIONAL,
59 7
				'The template for the body method (blank or twig)',
60 7
				'blank'
61 1
			)
62
			->addOption(
63 6
				'serializer',
64
				'',
65
				InputOption::VALUE_OPTIONAL,
66
				'The serializer to be used for the json api template'
67
			)
68
		;
69
		
70
		$this->configureGenerateOptions();
71
72
		parent::configure();
73
	}
74
75
	/**
76
	 * Checks whether actions can be generated at all by reading composer.json and verify
77
	 * all required information are available
78
	 */
79
	private function preCheck() {
80
		$module = $this->packageService->getModule();
81
		if ($module === null || count($module->getActionNames()) == 0) {
82
			throw new \DomainException('No action definition found in composer.json - please run `keeko generate:action`.');
83
		}
84
	}
85
86
	protected function interact(InputInterface $input, OutputInterface $output) {
87
		$this->preCheck();
88
		
89
		// check if the dialog can be skipped
90
		$name = $input->getArgument('name');
91
		$model = $input->getOption('model');
92
		
93
		if ($model !== null) {
94
			return;
95
		} else if ($name !== null) {
96
			$generateModel = false;
97
		} else {
98
			$modelQuestion = new ConfirmationQuestion('Do you want to generate an action based off a model?');
99
			$generateModel = $this->askConfirmation($modelQuestion);
100
		}
101
		
102
		// ask questions for a model
103
		if ($generateModel !== false) {
104
			$schema = str_replace(getcwd(), '', $this->modelService->getSchema());
105 7
			$allQuestion = new ConfirmationQuestion(sprintf('For all models in the schema (%s)?', $schema));
106 7
			$allModels = $this->askConfirmation($allQuestion);
107
			
108 6
			if (!$allModels) {
109
				$modelQuestion = new Question('Which model');
110
				$modelQuestion->setAutocompleterValues($this->modelService->getModelNames());
111 6
				$model = $this->askQuestion($modelQuestion);
112 5
				$input->setOption('model', $model);
113 4
			}
114
		}
115
		
116
		// ask questions for a skeleton
117 1
		else {
118
			$names = [];
119 1
			$module = $this->packageService->getModule();
120 1
			foreach ($module->getActionNames() as $name) {
121 1
				$names[] = $name;
122
			}
123
				
124 5
			$actionQuestion = new Question('Which action');
125 5
			$actionQuestion->setAutocompleterValues($names);
126
			$name = $this->askQuestion($actionQuestion);
127 6
			$input->setArgument('name', $name);
128 6
			
129
			// ask which format
130 6
			$formatQuestion = new Question('Which format', 'json');
131 1
			$formatQuestion->setAutocompleterValues(['json', 'html']);
132
			$format = $this->askQuestion($formatQuestion);
133
			$input->setOption('format', $format);
134 5
			
135 5
			// ask which template
136 5
			$action = $this->packageService->getAction($name);
137 5
			if (!($format == 'json' && $this->modelService->isModelAction($action))) {
138 5
				$templates = [
139
					'html' => ['twig', 'blank'],
140 5
					'json' => ['api', 'blank']
141 5
				];
142 5
				
143
				$suggestions = isset($templates[$format]) ? $templates[$format] : [];
144
				$default = count($suggestions) ? $suggestions[0] : '';
145 5
				$templateQuestion = new Question('Which template', $default);
146 5
				$templateQuestion->setAutocompleterValues($suggestions);
147 5
				$template = $this->askQuestion($templateQuestion);
148
				$input->setOption('template', $template);
149
				
150 5
				// aks for serializer
151 2
				if ($format == 'json' && $template == 'api') {
152 2
					$guessedSerializer = NameUtils::toStudlyCase($name) . 'Serializer';
153
					$serializerQuestion = new Question('Which format', $guessedSerializer);
154
					$serializer = $this->askQuestion($serializerQuestion);
155 4
					$input->setOption('serializer', $serializer);
156 2
				}
157 2
			}
158
		}
159
	}
160 2
161 1
	protected function execute(InputInterface $input, OutputInterface $output) {
162 1
		$this->preCheck();
163
		
164
		$name = $input->getArgument('name');
165 1
		$model = $input->getOption('model');
166 1
167 1
		// generate responser for a specific action
168
		if ($name) {
169
			$this->generateResponder($name);
170 5
		}
171
		
172 5
		// generate a responder for a specific model
173
		else if ($model) {
174
			$this->generateModel($model);
175 5
		}
176 2
		
177 2
		// generate responders for all models
178 2
		else {
179
			foreach ($this->modelService->getModels() as $model) {
180 2
				$this->generateModel($model->getOriginCommonName());
181 2
			}
182 2
		}
183 2
		
184 2
		$this->packageService->savePackage();
185
	}
186
	
187 5
	protected function generateModel($modelName) {
188 5
		$model = $this->modelService->getModel($modelName);
189 5
		$types = $model->isReadOnly() ? ['read', 'list'] : ['read', 'list', 'create', 'update', 'delete'];
190
	
191
		// generate responders for crud actions
192
		foreach ($types as $type) {
193
			$actionName = $modelName . '-' . $type;
194
	
195
			$this->generateResponder($actionName);
196
		}
197
		
198
		// generate responders for relationships
199
		if (!$model->isReadOnly()) {
200
			$types = [
201
				'one' => ['read', 'update'],
202
				'many' => ['read', 'add', 'update', 'remove']
203
			];
204
			$relationships = $this->modelService->getRelationships($model);
205
			foreach ($relationships['all'] as $relationship) {
206
				$fk = $relationship['fk'];
207
				$foreignName = $fk->getForeignTable()->getOriginCommonName();
208
				foreach ($types[$relationship['type']] as $type) {
209
					$this->generateResponder($modelName . '-to-' . $foreignName . '-relationship-' . $type);
210
				}
211
			}
212
		}
213
	}
214
	
215
	protected function generateResponder($actionName) {
216
		$this->logger->info('Generate Responder for: ' . $actionName);
217
		$module = $this->packageService->getModule();
218
		
219
		if (!$module->hasAction($actionName)) {
220
			throw new \RuntimeException(sprintf('action (%s) not found', $actionName));
221
		}
222
		
223
		$input = $this->io->getInput();
224
		$format = $input->getOption('format');
225
		$template = $input->getOption('template');
226
		
227
		// check if relationship response
228
		if (Text::create($actionName)->contains('relationship') && $format == 'json') {
229
			return $this->generateRelationshipResponder($actionName);
230
		}
231
232
		$action = $module->getAction($actionName);
233
		$modelName = $this->modelService->getModelNameByAction($action);
234
235
		if (!$action->hasResponse($format)) {
236
			$className = str_replace('action', 'responder', $action->getClass());
237
			$className = preg_replace('/Action$/', ucwords($format) . 'Responder', $className);
238
			$action->setResponse($format, $className);
239
		}
240
241
		// find generator
242
		$overwrite = false;
0 ignored issues
show
Unused Code introduced by
$overwrite 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...
243
		$generator = null;
244
		$type = $this->packageService->getActionType($actionName, $modelName);
245
		$isModel = $type && $this->modelService->isModelAction($action); 
246
247
		// model given and format is json
248
		if ($isModel && $format == 'json') {
249
			$generator = GeneratorFactory::createModelJsonResponderGenerator($type, $this->service);
250
		}
251
		
252
		// json + dump
253
		else if ($format == 'json' && $template == 'api') {
254
			$generator = new ApiJsonResponderGenerator($this->service);
255
			$generator->setSerializer($this->getSerializer());
256
		}
257
		
258
		// blank json
259
		else if ($format == 'json') {
260
			$generator = new SkeletonJsonResponderGenerator($this->service);
261
		}
262
		
263
		// html + twig
264
		else if ($format == 'html' && $template == 'twig') {
265
			$generator = new TwigHtmlResponderGenerator($this->service);
266
		}
267
		
268
		// blank html as default
269
		else if ($format == 'html') {
270
			$generator = new SkeletonHtmlResponderGenerator($this->service);
271
		}
272
		
273
		// run generation, if generator was chosen
274
		if ($generator !== null) {
275
			/* @var $class PhpClass */
276
			$class = $generator->generate($action);
277
278
			// write to file
279
			$file = $this->codegenService->getFile($class);
280
			$overwrite = !$file->exists() || $input->getOption('force');
281
			$this->codegenService->dumpStruct($class, $overwrite);
282
		}
283
	}
284
285
	protected function generateRelationshipResponder($actionName) {
286
		$module = $this->packageService->getModule();
287
		$action = $module->getAction($actionName);
288
		$prefix = substr($actionName, 0, strpos($actionName, 'relationship') + 12);
289
		$readAction = $module->getAction($prefix.'-read');
290
		
291
		// get modules names
292
		$matches = [];
293
		preg_match('/([a-z_]+)-to-([a-z_]+)-relationship.*/i', $actionName, $matches);
294
		$model = $this->modelService->getModel($matches[1]);
295
		$foreign = $this->modelService->getModel($matches[2]);
296
297
		// response class name
298
		$responder = sprintf('%s\\responder\\%s%sJsonResponder',
299
			$this->packageService->getNamespace(),
300
			$model->getPhpName(),
301
			$foreign->getPhpName()
302
		);
303
		
304
		$many = $module->hasAction($prefix . '-read')
305
			&& $module->hasAction($prefix . '-update')
306
			&& $module->hasAction($prefix . '-add')
307
			&& $module->hasAction($prefix . '-remove')
308
		;
309
		$single = $module->hasAction($prefix . '-read')
310
			&& $module->hasAction($prefix . '-update')
311
			&& !$many
312
		;
313
		
314
		$generator = null;
315
		if ($many) {
316
			$generator = new ToManyRelationshipJsonResponderGenerator($this->service, $model, $foreign);
1 ignored issue
show
Bug introduced by
It seems like $model defined by $this->modelService->getModel($matches[1]) on line 294 can be null; however, keeko\tools\generator\re...enerator::__construct() 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...
Bug introduced by
It seems like $foreign defined by $this->modelService->getModel($matches[2]) on line 295 can be null; however, keeko\tools\generator\re...enerator::__construct() 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...
317
		} else if ($single) {
318
			$generator = new ToOneRelationshipJsonResponderGenerator($this->service, $model, $foreign);
0 ignored issues
show
Bug introduced by
It seems like $model defined by $this->modelService->getModel($matches[1]) on line 294 can be null; however, keeko\tools\generator\re...enerator::__construct() 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...
Bug introduced by
It seems like $foreign defined by $this->modelService->getModel($matches[2]) on line 295 can be null; however, keeko\tools\generator\re...enerator::__construct() 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...
319
		}
320
		
321
		if ($generator !== null) {
322
			$action->setResponse('json', $responder);
323
			$responder = $generator->generate($readAction);
324
			$this->codegenService->dumpStruct($responder, true);
325
		}
326
	}
327
	
328
	private function getSerializer() {
329
		$input = $this->io->getInput();
330
		$serializer = $input->getOption('serializer');
331
		
332
		if (empty($serializer)) {
333
			throw new \RuntimeException('No serializer given, please pass --serializer for template');
334
		}
335
		
336
		// check fqcn
337
		$class = PhpClass::create($serializer);
338
		if ($class->getQualifiedName() == $serializer) {
339
			$class->setQualifiedName(NamespaceResolver::getNamespace('src/serializer', $this->package) . 
340
				'\\' . $serializer);
341
		}
342
		
343
		// check serializer exists
344
		$file = new File($this->codegenService->getFilename($class));
345
		if (!$file->exists()) {
346
			$this->io->writeln(sprintf('<error>Warning:</error> Serializer <info>%s</info> does not exists, please run `keeko generate:serializer %s`', $serializer, $class->getName()));
347
		}
348
349
		return $class->getQualifiedName();
350
	}
351
}
352