Completed
Push — master ( 159932...f1c264 )
by Thomas
08:09
created

generateToManyRelationshipActions()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 40
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 40
ccs 0
cts 0
cp 0
rs 8.8571
cc 2
eloc 28
nc 2
nop 2
crap 6
1
<?php
2
namespace keeko\tools\command;
3
4
use gossi\codegen\model\PhpClass;
5
use keeko\framework\schema\ActionSchema;
6
use keeko\framework\utils\NameUtils;
7
use keeko\tools\generator\action\SkeletonActionGenerator;
8
use keeko\tools\generator\action\ToManyRelationshipAddActionGenerator;
9
use keeko\tools\generator\action\ToManyRelationshipReadActionGenerator;
10
use keeko\tools\generator\action\ToManyRelationshipRemoveActionGenerator;
11
use keeko\tools\generator\action\ToManyRelationshipUpdateActionGenerator;
12
use keeko\tools\generator\action\ToOneRelationshipReadActionGenerator;
13
use keeko\tools\generator\action\ToOneRelationshipUpdateActionGenerator;
14
use keeko\tools\generator\GeneratorFactory;
15
use keeko\tools\helpers\QuestionHelperTrait;
16
use keeko\tools\utils\NamespaceResolver;
17
use phootwork\lang\Text;
18
use Propel\Generator\Model\ForeignKey;
19
use Propel\Generator\Model\Table;
20
use Symfony\Component\Console\Input\InputArgument;
21
use Symfony\Component\Console\Input\InputInterface;
22
use Symfony\Component\Console\Input\InputOption;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Console\Question\ConfirmationQuestion;
25 20
use Symfony\Component\Console\Question\Question;
26 20
27 20
class GenerateActionCommand extends AbstractGenerateCommand {
28 20
29 20
	use QuestionHelperTrait;
30 20
31 20
	protected function configure() {
32
		$this
33 20
			->setName('generate:action')
34 20
			->setDescription('Generates an action')
35 20
			->addArgument(
36 20
				'name',
37 20
				InputArgument::OPTIONAL,
38 20
				'The name of the action, which should be generated. Typically in the form %nomen%-%verb% (e.g. user-create)'
39
			)
40 20
			->addOption(
41 20
				'classname',
42 20
				'c',
43 20
				InputOption::VALUE_OPTIONAL,
44 20
				'The main class name (If ommited, class name will be guessed from action name)',
45
				null
46 20
			)
47 20
			->addOption(
48 20
				'model',
49 20
				'm',
50 20
				InputOption::VALUE_OPTIONAL,
51
				'The model for which the actions should be generated, when there is no name argument (if ommited all models will be generated)'
52 20
			)
53 20
			->addOption(
54 20
				'title',
55 20
				'',
56 20
				InputOption::VALUE_OPTIONAL,
57
				'The title for the generated option'
58 20
			)
59 20
			->addOption(
60 20
				'type',
61 20
				'',
62
				InputOption::VALUE_OPTIONAL,
63 20
				'The type of this action (list|create|read|update|delete) (if ommited template is guessed from action name)'
64
			)->addOption(
65
				'acl',
66
				'',
67
				InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
68
				'The acl\s for this action (guest, user and/or admin)'
69
			)
70
		;
71 1
		
72
		$this->configureGenerateOptions();
73 20
		
74
		parent::configure();
75 20
	}
76 20
77
	protected function initialize(InputInterface $input, OutputInterface $output) {
78
		parent::initialize($input, $output);
79
	}
80
81
	/**
82 10
	 * Checks whether actions can be generated at all by reading composer.json and verify
83 10
	 * all required information are available
84 10
	 */
85 1
	private function preCheck() {
86 4
		$module = $this->packageService->getModule();
87 9
		if ($module === null) {
88
			throw new \DomainException('No module definition found in composer.json - please run `keeko init`.');
89
		}
90
	}
91
	
92
	protected function interact(InputInterface $input, OutputInterface $output) {
93
		$this->preCheck();
94
		
95
		// check if the dialog can be skipped
96
		$name = $input->getArgument('name');
97
		$model = $input->getOption('model');
98
		
99
		if ($model !== null) {
100
			return;
101
		} else if ($name !== null) {
102
			$generateModel = false;
103
		} else {
104
			$modelQuestion = new ConfirmationQuestion('Do you want to generate an action based off a model?');
105
			$generateModel = $this->askConfirmation($modelQuestion);
106
		}
107
		
108
		// ask questions for a model
109
		if ($generateModel) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $generateModel of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
110
			$schema = str_replace(getcwd(), '', $this->modelService->getSchema());
111
			$allQuestion = new ConfirmationQuestion(sprintf('For all models in the schema (%s)?', $schema));
112
			$allModels = $this->askConfirmation($allQuestion);
113
114
			if (!$allModels) {
115
				$modelQuestion = new Question('Which model');
116
				$modelQuestion->setAutocompleterValues($this->modelService->getModelNames());
117
				$model = $this->askQuestion($modelQuestion);
118
				$input->setOption('model', $model);
119
			}
120
		} else {
121
			if ($name === null) {
122
				$nameQuestion = new Question('What\'s the name for your action (must be a unique identifier)?', '');
123
				$name = $this->askQuestion($nameQuestion);
124
				$input->setArgument('name', $name);
125
			}
126
			$action = $this->getAction($name);
127
			
128
			// ask for title
129
			$pkgTitle = $action->getTitle();
130
			$title = $input->getOption('title');
131
			if ($title === null && !empty($pkgTitle)) {
132
				$title = $pkgTitle;
133
			}
134
			$titleQuestion = new Question('What\'s the title for your action?', $title);
135
			$title = $this->askQuestion($titleQuestion);
136
			$input->setOption('title', $title);
137
			
138
			// ask for classname
139
			$pkgClass = $action->getClass();
140
			$classname = $input->getOption('classname');
141
			if ($classname === null) {
142
				if (!empty($pkgClass)) {
143
					$classname = $pkgClass;
144
				} else {
145
					$classname = $this->guessClassname($name);
146
				}
147
			}
148
			$classname = $this->askQuestion(new Question('Classname', $classname));
149
			$input->setOption('classname', $classname);
150
			
151
			// ask for acl
152 10
			$acls = $this->getAcl($action);
153 10
			$aclQuestion = new Question('ACL (comma separated list, with these options: guest, user, admin)', implode(', ', $acls));
154
			$acls = $this->askQuestion($aclQuestion);
155
			$input->setOption('acl', $acls);
156
		}
157
	}
158
159 9
	protected function execute(InputInterface $input, OutputInterface $output) {
160 9
		$this->preCheck();
161
162
		$name = $input->getArgument('name');
163 9
		$model = $input->getOption('model');
164 3
165 2
		// generate a skeleton action (or model, if action name belongs to a model)
166
		if ($name) {
167
			$action = $this->getAction($name);
168 6
			if ($this->modelService->isModelAction($action)) {
169 2
				$this->generateModel($this->modelService->getModelNameByAction($action));
170 2
			} else {
171
				$this->generateSkeleton($name);
172
			}
173 4
		}
174 3
175 3
		// generate an action for a specific model
176 2
		else if ($model) {
177 2
			$this->generateModel($model);
178 2
		}
179 1
180
		// generate actions for all models
181 3
		else {
182
			foreach ($this->modelService->getModels() as $model) {
183
				$modelName = $model->getOriginCommonName();
184
				$input->setOption('model', $modelName);
185 1
				$this->generateModel($modelName);
186 1
			}
187 1
		}
188
		
189
		$this->packageService->savePackage();
190 8
	}
191 8
192
	private function generateModel($modelName) {
193 5
		$this->logger->info('Generate Action from Model: ' . $modelName);
194 5
		$input = $this->io->getInput();
195 5
		$model = $this->modelService->getModel($modelName);
196 5
197 5
		// generate domain + serializer
198 1
		$this->generateDomain($model);
199 1
		$this->generateSerializer($model);
1 ignored issue
show
Bug introduced by
It seems like $model defined by $this->modelService->getModel($modelName) on line 195 can be null; however, keeko\tools\command\Gene...d::generateSerializer() 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...
200 4
201
		// generate action type(s)
202
		$typeDump = $input->getOption('type');
203 5
		if ($typeDump !== null) {
204 5
			$types = [$typeDump];
205 5
		} else {
206 5
			$types = ['create', 'read', 'list', 'update', 'delete'];
207 5
		}
208 5
		
209 4
		foreach ($types as $type) {
210 5
			$input->setOption('acl', ['admin']);
1 ignored issue
show
Documentation introduced by
array('admin') is of type array<integer,string,{"0":"string"}>, but the function expects a string|boolean.

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...
211 5
			$input->setOption('type', $type);
212 5
			$actionName = $modelName . '-' . $type;
213
			
214 5
			if ($model->isReadOnly() && in_array($type, ['create', 'update', 'delete'])) {
215 5
				$this->logger->info(sprintf('Skip generate Action (%s), because Model (%s) is read-only', $actionName, $modelName));
216
				continue;
217 4
			}
218
			
219 4
			$action = $this->getAction($actionName);
220 3
			if (Text::create($action->getTitle())->isEmpty()) {
221
				$action->setTitle($this->getActionTitle($modelName, $type));
222 4
			}
223 4
			$action = $this->generateAction($actionName);
224 4
			
225 4
			// generate code
226 4
			$generator = GeneratorFactory::createModelActionGenerator($type, $this->service);
227
			$class = $generator->generate($action);
228
			$this->codegenService->dumpStruct($class, true);
229
		}
230
		
231
		// generate relationship actions
232
		if (!$model->isReadOnly()) {
233
			$relationships = $this->modelService->getRelationships($model);
1 ignored issue
show
Bug introduced by
It seems like $model defined by $this->modelService->getModel($modelName) on line 195 can be null; however, keeko\tools\services\Mod...ice::getRelationships() 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...
234
				
235
			// to-one relationships
236
			foreach ($relationships['one'] as $one) {
237 8
				$fk = $one['fk'];
238 8
				$this->generateToOneRelationshipActions($model, $fk->getForeignTable(), $fk);
1 ignored issue
show
Bug introduced by
It seems like $model defined by $this->modelService->getModel($modelName) on line 195 can be null; however, keeko\tools\command\Gene...neRelationshipActions() 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...
239 8
			}
240
			
241
			// to-many relationships
242 8
			foreach ($relationships['many'] as $many) {
243
				$fk = $many['fk'];
244 8
				$this->generateToManyRelationshipActions($model, $fk->getForeignTable());
1 ignored issue
show
Bug introduced by
It seems like $model defined by $this->modelService->getModel($modelName) on line 195 can be null; however, keeko\tools\command\Gene...nyRelationshipActions() 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...
245 2
			}
246 2
		}
247
		
248 8
		$input->setOption('type', $typeDump);
249 1
	}
250
251
	private function getActionTitle($modelName, $type) {
252 7
		$name = NameUtils::dasherize($modelName);
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...
253 2
		switch ($type) {
254 2
			case 'list':
255
				return 'List all ' . NameUtils::pluralize($name);
256
257 7
			case 'create':
258 4
			case 'read':
259 4
			case 'update':
260
			case 'delete':
261
				return ucfirst($type) . 's ' . (in_array($name[0], ['a', 'e', 'i', 'o', 'u']) ? 'an' : 'a') . ' ' . $name;
262 7
		}
263 7
	}
264 7
265
	/**
266
	 * Generates a domain with trait for the given model
267
	 * 
268
	 * @param Table $model
269
	 */
270
	private function generateDomain(Table $model) {
271 7
		$this->runCommand('generate:domain', [
272
			'--model' => $model->getOriginCommonName()
273
		]);
274 7
	}
275 7
	
276
	/**
277 4
	 * Generates a serializer for the given model
278 4
	 *
279 4
	 * @param Table $model
280
	 */
281
	private function generateSerializer(Table $model) {
282
		$this->runCommand('generate:serializer', [
283
			'--model' => $model->getOriginCommonName()
284
		]);
285
	}
286
	
287 8
	/**
288 8
	 * Generates an action.
289 8
	 *  
290 7
	 * @param string $actionName
291 7
	 */
292 7
	private function generateSkeleton($actionName) {
293 7
		$this->logger->info('Generate Action: ' . $actionName);
294 8
		$input = $this->io->getInput();
295
		
296
		// generate action
297 7
		$action = $this->generateAction($actionName);
298 7
		
299 7
		// generate code
300 7
		$generator = new SkeletonActionGenerator($this->service);
301 7
		$class = $generator->generate($action);
302 2
		$this->codegenService->dumpStruct($class, $input->getOption('force'));
303 2
	}
304 7
	
305 7
	/**
306 1
	 * Generates the action for the package
307 1
	 * 
308 1
	 * @param string $actionName
309 1
	 * @throws \RuntimeException
310 1
	 * @return ActionSchema
311 6
	 */
312
	private function generateAction($actionName) {
313 7
		$input = $this->io->getInput();
314
		
315 7
		// get action and create it if it doesn't exist
316
		$action = $this->getAction($actionName);
317
		
318
		if (($title = $input->getOption('title')) !== null) {
319
			$action->setTitle($title);
320
		}
321
		
322
		if (Text::create($action->getTitle())->isEmpty()) {
323
			throw new \RuntimeException(sprintf('Cannot create action %s, because I am missing a title for it', $actionName));
324
		}
325
		
326
		if (($classname = $input->getOption('classname')) !== null) {
327
			$action->setClass($classname);
328
		}
329
		
330
		// guess classname if there is none set yet
331 7
		if (Text::create($action->getClass())->isEmpty()) {
332 7
			$action->setClass($this->guessClassname($actionName));
333 7
		}
334
		
335
		// guess title if there is none set yet
336 7
		if (Text::create($action->getTitle())->isEmpty()
337 7
				&& $this->modelService->isModelAction($action)
338 7
				&& $this->modelService->isCrudAction($action)) {
339 7
			$modelName = $this->modelService->getModelNameByAction($action);
340 7
			$type = $this->modelService->getOperationByAction($action);
341
			$action->setTitle($this->getActionTitle($modelName, $type));
342
		}
343 7
	
344
		// set acl
345 1
		$action->setAcl($this->getAcl($action));
346 1
		
347
		return $action;
348 1
	}
349 1
	
350 1
	private function guessClassname($name) {
351
		$namespace = NamespaceResolver::getNamespace('src/action', $this->package);
352
		return $namespace . '\\' . NameUtils::toStudlyCase($name) . 'Action';
353 1
	}
354 1
	
355 1
	/**
356
	 * 
357
	 * @param string $actionName
358
	 * @return ActionSchema
359 6
	 */
360 6
	private function getAction($actionName) {
361 6
		$action = $this->packageService->getAction($actionName);
362 6
		if ($action === null) {
363 6
			$action = new ActionSchema($actionName);
364
			$module = $this->packageService->getModule();
365
			$module->addAction($action);
366
		}
367 7
		return $action;
368 5
	}
369 1
	
370 1
	private function getAcl(ActionSchema $action) {
371 1
		$acls = [];
372 5
		$acl = $this->io->getInput()->getOption('acl');
373 5
		if ($acl !== null && count($acl) > 0) {
374
			if (!is_array($acl)) {
375 5
				$acl = [$acl];
376 5
			}
377
			foreach ($acl as $group) {
378 5
				if (strpos($group, ',') !== false) {
379 4
					$groups = explode(',', $group);
380 4
					foreach ($groups as $g) {
381 4
						$acls[] = trim($g);
382 5
					}
383
				} else {
384 2
					$acls[] = $group;
385 2
				}
386 2
			}
387 2
			
388 2
			return $acls;
389
		}
390
		
391 7
		// read default from package
392 7
		if (!$action->getAcl()->isEmpty()) {
393
			return $action->getAcl()->toArray();
394
		}
395
396
		return $acls;
397
	}
398
	
399
	private function generateToOneRelationshipActions(Table $model, Table $foreign, ForeignKey $fk) {
400
		$module = $this->package->getKeeko()->getModule();
401
		$fkModelName = $foreign->getPhpName();
402
		$actionNamePrefix = sprintf('%s-to-%s-relationship', $model->getOriginCommonName(), $foreign->getOriginCommonName());
403
	
404
		$generators = [
405
			'read' => new ToOneRelationshipReadActionGenerator($this->service),
406
			'update' => new ToOneRelationshipUpdateActionGenerator($this->service)
407
		];
408
		$titles = [
409
			'read' => 'Reads the relationship of {model} to {foreign}',
410
			'update' => 'Updates the relationship of {model} to {foreign}'
411
		];
412
	
413
		foreach (array_keys($generators) as $type) {
414
			// generate fqcn
415
			$className = sprintf('%s%s%sAction', $model->getPhpName(), $fkModelName, ucfirst($type));
416
			$fqcn = $this->packageService->getNamespace() . '\\action\\' . $className;
417
418
			// generate action
419
			$action = new ActionSchema($actionNamePrefix . '-' . $type);
420
			$action->addAcl('admin');
421
			$action->setClass($fqcn);
422
			$action->setTitle(str_replace(
423
				['{model}', '{foreign}'],
424
				[$model->getOriginCommonName(), $foreign->getoriginCommonName()],
425
				$titles[$type])
426
			);
427
			$module->addAction($action);
428
	
429
			// generate class
430
			$generator = $generators[$type];
431
			$class = $generator->generate(new PhpClass($fqcn), $model, $foreign, $fk);
432
			$this->codegenService->dumpStruct($class, true);
433
		}
434
	}
435
	
436
	private function generateToManyRelationshipActions(Table $model, Table $foreign) {
437
		$module = $this->package->getKeeko()->getModule();
438
		$fkModelName = $foreign->getPhpName();
439
		$actionNamePrefix = sprintf('%s-to-%s-relationship', $model->getOriginCommonName(), $foreign->getOriginCommonName());
440
		
441
		$generators = [
442
			'read' => new ToManyRelationshipReadActionGenerator($this->service),
443
			'update' => new ToManyRelationshipUpdateActionGenerator($this->service),
444
			'add' => new ToManyRelationshipAddActionGenerator($this->service),
445
			'remove' => new ToManyRelationshipRemoveActionGenerator($this->service)
446
		];
447
		$titles = [
448
			'read' => 'Reads the relationship of {model} to {foreign}',
449
			'update' => 'Updates the relationship of {model} to {foreign}',
450
			'add' => 'Adds {foreign} as relationship to {model}',
451
			'remove' => 'Removes {foreign} as relationship of {model}'
452
		];
453
	
454
		foreach (array_keys($generators) as $type) {
455
			// generate fqcn
456
			$className = sprintf('%s%s%sAction', $model->getPhpName(), $fkModelName, ucfirst($type));
457
			$fqcn = $this->packageService->getNamespace() . '\\action\\' . $className;
458
	
459
			// generate action
460
			$action = new ActionSchema($actionNamePrefix . '-' . $type);
461
			$action->addAcl('admin');
462
			$action->setClass($fqcn);
463
			$action->setTitle(str_replace(
464
				['{model}', '{foreign}'],
465
				[$model->getOriginCommonName(), $foreign->getoriginCommonName()],
466
				$titles[$type])
467
			);
468
			$module->addAction($action);
469
	
470
			// generate class
471
			$generator = $generators[$type];
472
			$class = $generator->generate(new PhpClass($fqcn), $model, $foreign);
473
			$this->codegenService->dumpStruct($class, true);
474
		}
475
	}
476
477
}
478