Completed
Push — master ( 98cd2e...56265b )
by Thomas
09:01
created

GenerateActionCommand::getAction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 9.6666
cc 2
eloc 7
nc 2
nop 1
crap 2
1
<?php
2
namespace keeko\tools\command;
3
4
use gossi\codegen\model\PhpClass;
5
use gossi\codegen\model\PhpTrait;
6
use keeko\framework\schema\ActionSchema;
7
use keeko\framework\utils\NameUtils;
8
use keeko\tools\generator\action\BlankActionGenerator;
9
use keeko\tools\generator\action\NoopActionGenerator;
10
use keeko\tools\generator\action\ToManyRelationshipAddActionGenerator;
11
use keeko\tools\generator\action\ToManyRelationshipReadActionGenerator;
12
use keeko\tools\generator\action\ToManyRelationshipRemoveActionGenerator;
13
use keeko\tools\generator\action\ToManyRelationshipUpdateActionGenerator;
14
use keeko\tools\generator\action\ToOneRelationshipReadActionGenerator;
15
use keeko\tools\generator\action\ToOneRelationshipUpdateActionGenerator;
16
use keeko\tools\generator\GeneratorFactory;
17
use keeko\tools\helpers\QuestionHelperTrait;
18
use keeko\tools\utils\NamespaceResolver;
19
use phootwork\file\File;
20
use phootwork\lang\Text;
21
use Propel\Generator\Model\ForeignKey;
22
use Propel\Generator\Model\Table;
23
use Symfony\Component\Console\Input\InputArgument;
24
use Symfony\Component\Console\Input\InputInterface;
25 20
use Symfony\Component\Console\Input\InputOption;
26 20
use Symfony\Component\Console\Output\OutputInterface;
27 20
use Symfony\Component\Console\Question\ConfirmationQuestion;
28 20
use Symfony\Component\Console\Question\Question;
29 20
30 20
class GenerateActionCommand extends AbstractGenerateCommand {
31 20
32
	use QuestionHelperTrait;
33 20
	
34 20
	private $twig;
35 20
36 20
	protected function configure() {
37 20
		$this
38 20
			->setName('generate:action')
39
			->setDescription('Generates an action')
40 20
			->addArgument(
41 20
				'name',
42 20
				InputArgument::OPTIONAL,
43 20
				'The name of the action, which should be generated. Typically in the form %nomen%-%verb% (e.g. user-create)'
44 20
			)
45
			->addOption(
46 20
				'classname',
47 20
				'c',
48 20
				InputOption::VALUE_OPTIONAL,
49 20
				'The main class name (If ommited, class name will be guessed from action name)',
50 20
				null
51
			)
52 20
			->addOption(
53 20
				'model',
54 20
				'm',
55 20
				InputOption::VALUE_OPTIONAL,
56 20
				'The model for which the actions should be generated, when there is no name argument (if ommited all models will be generated)'
57
			)
58 20
			->addOption(
59 20
				'title',
60 20
				'',
61 20
				InputOption::VALUE_OPTIONAL,
62
				'The title for the generated option'
63 20
			)
64
			->addOption(
65
				'type',
66
				'',
67
				InputOption::VALUE_OPTIONAL,
68
				'The type of this action (list|create|read|update|delete) (if ommited template is guessed from action name)'
69
			)->addOption(
70
				'acl',
71 1
				'',
72
				InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
73 20
				'The acl\s for this action (guest, user and/or admin)'
74
			)
75 20
// 			->addOption(
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% 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...
76 20
// 				'schema',
77
// 				's',
78
// 				InputOption::VALUE_OPTIONAL,
79
// 				'Path to the database schema (if ommited, database/schema.xml is used)',
80
// 				null
81
// 			)
82 10
		;
83 10
		
84 10
		$this->configureGenerateOptions();
85 1
		
86 4
		parent::configure();
87 9
	}
88
89
	protected function initialize(InputInterface $input, OutputInterface $output) {
90
		parent::initialize($input, $output);
91
92
		$loader = new \Twig_Loader_Filesystem($this->service->getConfig()->getTemplateRoot() . '/actions');
93
		$this->twig = new \Twig_Environment($loader);
94
	}
95
96
	/**
97
	 * Checks whether actions can be generated at all by reading composer.json and verify
98
	 * all required information are available
99
	 */
100
	private function preCheck() {
101
		$module = $this->packageService->getModule();
102
		if ($module === null) {
103
			throw new \DomainException('No module definition found in composer.json - please run `keeko init`.');
104
		}
105
	}
106
	
107
	protected function interact(InputInterface $input, OutputInterface $output) {
108
		$this->preCheck();
109
		
110
		// check if the dialog can be skipped
111
		$name = $input->getArgument('name');
112
		$model = $input->getOption('model');
113
		
114
		if ($model !== null) {
115
			return;
116
		} else if ($name !== null) {
117
			$generateModel = false;
118
		} else {
119
			$modelQuestion = new ConfirmationQuestion('Do you want to generate an action based off a model?');
120
			$generateModel = $this->askConfirmation($modelQuestion);
121
		}
122
		
123
		// ask questions for a model
124
		if ($generateModel /*&& !($this->package->getVendor() === 'keeko' && $this->modelService->isCoreSchema())*/) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% 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...
125
126
			$schema = str_replace(getcwd(), '', $this->modelService->getSchema());
127
			$allQuestion = new ConfirmationQuestion(sprintf('For all models in the schema (%s)?', $schema));
128
			$allModels = $this->askConfirmation($allQuestion);
129
130
			if (!$allModels) {
131
				$modelQuestion = new Question('Which model');
132
				$modelQuestion->setAutocompleterValues($this->modelService->getModelNames());
133
				$model = $this->askQuestion($modelQuestion);
134
				$input->setOption('model', $model);
135
			}
136
		} else if (!$generateModel) {
137
			$action = $this->getAction($name);
138
			
139
			// ask for title
140
			$pkgTitle = $action->getTitle();
141
			$title = $input->getOption('title');
142
			if ($title === null && !empty($pkgTitle)) {
143
				$title = $pkgTitle;
144
			}
145
			$titleQuestion = new Question('What\'s the title for your action?', $title);
146
			$title = $this->askQuestion($titleQuestion);
147
			$input->setOption('title', $title);
148
			
149
			// ask for classname
150
			$pkgClass = $action->getClass();
151
			$classname = $input->getOption('classname');
152 10
			if ($classname === null) {
153 10
				if (!empty($pkgClass)) {
154
					$classname = $pkgClass;
155
				} else {
156
					$classname = $this->guessClassname($name);
157
				}
158
			}
159 9
			$classname = $this->askQuestion(new Question('Classname', $classname));
160 9
			$input->setOption('classname', $classname);
161
			
162
			// ask for acl
163 9
			$acls = $this->getAcl($action);
164 3
			$aclQuestion = new Question('ACL (comma separated list, with these options: guest, user, admin)', implode(', ', $acls));
165 2
			$acls = $this->askQuestion($aclQuestion);
166
			$input->setOption('acl', $acls);
167
		}
168 6
	}
169 2
170 2
	protected function execute(InputInterface $input, OutputInterface $output) {
171
		$this->preCheck();
172
		
173 4
		// 1. find out which action(s) to generate
174 3
		// 2. generate the information in the package
175 3
		// 3. generate the code for the action
176 2
		
177 2
		$name = $input->getArgument('name');
178 2
		$model = $input->getOption('model');
179 1
180
		// only a specific action
181 3
		if ($name) {
182
			$this->generateAction($name);
183
		}
184
185 1
		// create action(s) from a model
186 1
		else if ($model) {
187 1
			$this->generateModel($model);
188
		}
189
		
190 8
		// if this is a core-module, find the related model
191 8
// 		else /*if ($this->package->getVendor() == 'keeko' && $this->modelService->isCoreSchema()) */ {
192
// 			$model = $this->package->getName();
193 5
// 			if ($this->modelService->hasModel($model)) {
194 5
// 				$input->setOption('model', $model);
195 5
// 				$this->generateModel($model);
196 5
// 			} else {
197 5
// 				$this->logger->error('Tried to find model on my own, wasn\'t lucky - please provide model with the --model option');
198 1
// 			}
199 1
// 		}
200 4
201
		// anyway, generate all
202
		else {
203 5
			foreach ($this->modelService->getModels() as $model) {
204 5
				$modelName = $model->getOriginCommonName();
205 5
				$input->setOption('model', $modelName);
206 5
				$this->generateModel($modelName);
207 5
			}
208 5
		}
209 4
		
210 5
		$this->packageService->savePackage();
211 5
	}
212 5
213
	private function generateModel($modelName) {
214 5
		$this->logger->info('Generate Action from Model: ' . $modelName);
215 5
		$input = $this->io->getInput();
216
		$model = $this->modelService->getModel($modelName);
217 4
		$typeDump = $input->getOption('type');
218
		if ($typeDump !== null) {
219 4
			$types = [$typeDump];
220 3
		} else {
221
			$types = ['create', 'read', 'list', 'update', 'delete'];
222 4
		}
223 4
224 4
		foreach ($types as $type) {
225 4
			$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...
226 4
			$input->setOption('type', $type);
227
			$actionName = $modelName . '-' . $type;
228
			
229
			if ($model->isReadOnly() && in_array($type, ['create', 'update', 'delete'])) {
230
				$this->logger->info(sprintf('Skip generate Action (%s), because Model (%s) is read-only', $actionName, $modelName));
231
				continue;
232
			}
233
			
234
			$action = $this->getAction($actionName);
235
			if (Text::create($action->getTitle())->isEmpty()) {
236
				$action->setTitle($this->getActionTitle($modelName, $type));
237 8
			}
238 8
			$this->generateAction($actionName);
239 8
		}
240
		
241
		// generate relationship actions
242 8
		if (!$model->isReadOnly()) {
243
			$relationships = $this->modelService->getRelationships($model);
1 ignored issue
show
Bug introduced by
It seems like $model defined by $this->modelService->getModel($modelName) on line 216 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...
244 8
				
245 2
			// to-one relationships
246 2
			foreach ($relationships['one'] as $one) {
247
				$fk = $one['fk'];
248 8
				$this->generateToOneRelationshipAction($model, $fk->getForeignTable(), $fk);
1 ignored issue
show
Bug introduced by
It seems like $model defined by $this->modelService->getModel($modelName) on line 216 can be null; however, keeko\tools\command\Gene...OneRelationshipAction() 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...
249 1
			}
250
			
251
			// to-many relationships
252 7
			foreach ($relationships['many'] as $many) {
253 2
				$fk = $many['fk'];
254 2
				$cfk = $many['cfk'];
255
				$this->generateToManyRelationshipAction($model, $fk->getForeignTable(), $cfk->getMiddleTable());
1 ignored issue
show
Bug introduced by
It seems like $model defined by $this->modelService->getModel($modelName) on line 216 can be null; however, keeko\tools\command\Gene...anyRelationshipAction() 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...
256
			}
257 7
		}
258 4
		
259 4
		$input->setOption('type', $typeDump);
260
	}
261
262 7
	private function getActionTitle($modelName, $type) {
263 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...
264 7
		switch ($type) {
265
			case 'list':
266
				return 'List all ' . NameUtils::pluralize($name);
267
268
			case 'create':
269
			case 'read':
270
			case 'update':
271 7
			case 'delete':
272
				return ucfirst($type) . 's ' . (in_array($name[0], ['a', 'e', 'i', 'o', 'u']) ? 'an' : 'a') . ' ' . $name;
273
		}
274 7
	}
275 7
276
	
277 4
	/**
278 4
	 * Generates an action.
279 4
	 *  
280
	 * @param string $actionName
281
	 * @param ActionSchema $action the action node from composer.json
0 ignored issues
show
Documentation introduced by
There is no parameter named $action. Did you maybe mean $actionName?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
282
	 */
283
	private function generateAction($actionName) {
284
		$this->logger->info('Generate Action: ' . $actionName);
285
		$input = $this->io->getInput();
286
		
287 8
		// get action and create it if it doesn't exist
288 8
		$action = $this->getAction($actionName);
289 8
		
290 7
		if (($title = $input->getOption('title')) !== null) {
291 7
			$action->setTitle($title);
292 7
		}
293 7
294 8
		if (Text::create($action->getTitle())->isEmpty()) {
295
			throw new \RuntimeException(sprintf('Cannot create action %s, because I am missing a title for it', $actionName));
296
		}
297 7
298 7
		if (($classname = $input->getOption('classname')) !== null) {
299 7
			$action->setClass($classname);
300 7
		}
301 7
		
302 2
		// guess classname if there is none set yet
303 2
		if (Text::create($action->getClass())->isEmpty()) {
304 7
			$action->setClass($this->guessClassname($actionName));
305 7
		}
306 1
		
307 1
		// guess title if there is none set yet
308 1
		if (Text::create($action->getTitle())->isEmpty() 
309 1
				&& $this->modelService->isModelAction($action)
310 1
				&& $this->modelService->isCrudAction($action)) {
311 6
			$modelName = $this->modelService->getModelNameByAction($action);
312
			$type = $this->modelService->getOperationByAction($action);
313 7
			$action->setTitle($this->getActionTitle($modelName, $type));
314
		}
315 7
		
316
		// set acl
317
		$action->setAcl($this->getAcl($action));
318
		
319
		// generate code
320
		$this->generateCode($action);
321
	}
322
	
323
	private function guessClassname($name) {
324
		$namespace = NamespaceResolver::getNamespace('src/action', $this->package);
325
		return $namespace . '\\' . NameUtils::toStudlyCase($name) . 'Action';
326
	}
327
	
328
	/**
329
	 * 
330
	 * @param string $actionName
331 7
	 * @return ActionSchema
332 7
	 */
333 7
	private function getAction($actionName) {
334
		$action = $this->packageService->getAction($actionName);
335
		if ($action == null) {
336 7
			$action = new ActionSchema($actionName);
337 7
			$module = $this->packageService->getModule();
338 7
			$module->addAction($action);
339 7
		}
340 7
		return $action;
341
	}
342
	
343 7
	private function getAcl(ActionSchema $action) {
344
		$acls = [];
345 1
		$acl = $this->io->getInput()->getOption('acl');
346 1
		if ($acl !== null && count($acl) > 0) {
347
			if (!is_array($acl)) {
348 1
				$acl = [$acl];
349 1
			}
350 1
			foreach ($acl as $group) {
351
				if (strpos($group, ',') !== false) {
352
					$groups = explode(',', $group);
353 1
					foreach ($groups as $g) {
354 1
						$acls[] = trim($g);
355 1
					}
356
				} else {
357
					$acls[] = $group;
358
				}
359 6
			}
360 6
			
361 6
			return $acls;
362 6
		}
363 6
		
364
		// read default from package
365
		if (!$action->getAcl()->isEmpty()) {
366
			return $action->getAcl()->toArray();
367 7
		}
368 5
369 1
		return $acls;
370 1
	}
371 1
	
372 5
	/**
373 5
	 * Generates code for an action
374
	 * 
375 5
	 * @param ActionSchema $action
376 5
	 */
377
	private function generateCode(ActionSchema $action) {
378 5
		$input = $this->io->getInput();
379 4
		$trait = null;
0 ignored issues
show
Unused Code introduced by
$trait 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...
380 4
381 4
		// class
382 5
		$class = new PhpClass($action->getClass());
383
		$filename = $this->codegenService->getFilename($class);
384 2
		$traitNs = $class->getNamespace() . '\\base';
385 2
		$traitName = $class->getName() . 'Trait';
386 2
		$overwrite = false;
387 2
		
388 2
		// load from file, when class exists
389
		if (file_exists($filename)) {
390
			// load trait
391 7
			$trait = new PhpTrait($traitNs . '\\' . $traitName);
392 7
			$traitFile = new File($this->codegenService->getFilename($trait));
393
394
			if ($traitFile->exists()) {
395
				$trait = PhpTrait::fromFile($traitFile->getPathname());
0 ignored issues
show
Unused Code introduced by
$trait 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...
396
			}
397
		
398
			// load class
399
			$class = PhpClass::fromFile($filename);
400
		}
401
		
402
		// anyway seed class information
403
		else {
404
			$overwrite = true;
405
			$class->setParentClassName('AbstractAction');
406
			$class->setDescription($action->getTitle());
407
			$class->setLongDescription($action->getDescription());
408
			$this->codegenService->addAuthors($class, $this->package);
409
		}
410
		
411
		// create base trait
412
		$modelName = $input->getOption('model');
413
		if ($modelName !== null) {
414
			$type = $this->packageService->getActionType($action->getName(), $modelName);
415
			$generator = GeneratorFactory::createActionTraitGenerator($type, $this->service);
416
			$trait = $generator->generate($traitNs . '\\' . $traitName, $action);
417
418
			$this->codegenService->addAuthors($trait, $this->package);
419
			$this->codegenService->dumpStruct($trait, true);
420
			
421
			if (!$class->hasTrait($trait)) {
422
				$class->addTrait($trait);
423
				$overwrite = true;
424
			}
425
		}
426
		
427
		// create class generator
428
		if ($modelName === null && !$class->hasMethod('run')) {
429
			$overwrite = true;
430
			$generator = new BlankActionGenerator($this->service);
431
		} else {
432
			$generator = new NoopActionGenerator($this->service);
433
		}
434
435
		$class = $generator->generate($class);
436
		$overwrite = $overwrite || $input->getOption('force');
437
438
		$this->codegenService->dumpStruct($class, $overwrite);
439
	}
440
	
441
	private function generateToOneRelationshipAction(Table $model, Table $foreign, ForeignKey $fk) {
442
		$module = $this->package->getKeeko()->getModule();
443
		$fkModelName = $foreign->getPhpName();
444
		$actionNamePrefix = sprintf('%s-to-%s-relationship', $model->getOriginCommonName(), $foreign->getOriginCommonName());
445
	
446
		$generators = [
447
			'read' => new ToOneRelationshipReadActionGenerator($this->service),
448
			'update' => new ToOneRelationshipUpdateActionGenerator($this->service)
449
		];
450
		$titles = [
451
			'read' => 'Reads the relationship of {model} to {foreign}',
452
			'update' => 'Updates the relationship of {model} to {foreign}'
453
		];
454
	
455
		foreach (array_keys($generators) as $type) {
456
			// generate fqcn
457
			$className = sprintf('%s%s%sAction', $model->getPhpName(), $fkModelName, ucfirst($type));
458
			$fqcn = $this->packageService->getNamespace() . '\\action\\' . $className;
459
	
460
			// generate action
461
			$action = new ActionSchema($actionNamePrefix . '-' . $type);
462
			$action->addAcl('admin');
463
			$action->setClass($fqcn);
464
			$action->setTitle(str_replace(
465
				['{model}', '{foreign}'],
466
				[$model->getOriginCommonName(), $foreign->getoriginCommonName()],
467
				$titles[$type])
468
			);
469
			$module->addAction($action);
470
	
471
			// generate class
472
			$generator = $generators[$type];
473
			$class = $generator->generate(new PhpClass($fqcn), $model, $foreign, $fk);
474
			$this->codegenService->dumpStruct($class, true);
475
		}
476
	}
477
	
478
	private function generateToManyRelationshipAction(Table $model, Table $foreign, Table $middle) {
479
		$module = $this->package->getKeeko()->getModule();
480
		$fkModelName = $foreign->getPhpName();
481
		$actionNamePrefix = sprintf('%s-to-%s-relationship', $model->getOriginCommonName(), $foreign->getOriginCommonName());
482
		
483
		$generators = [
484
			'read' => new ToManyRelationshipReadActionGenerator($this->service),
485
			'update' => new ToManyRelationshipUpdateActionGenerator($this->service),
486
			'add' => new ToManyRelationshipAddActionGenerator($this->service),
487
			'remove' => new ToManyRelationshipRemoveActionGenerator($this->service)
488
		];
489
		$titles = [
490
			'read' => 'Reads the relationship of {model} to {foreign}',
491
			'update' => 'Updates the relationship of {model} to {foreign}',
492
			'add' => 'Adds {foreign} as relationship to {model}',
493
			'remove' => 'Removes {foreign} as relationship of {model}'
494
		];
495
	
496
		foreach (array_keys($generators) as $type) {
497
			// generate fqcn
498
			$className = sprintf('%s%s%sAction', $model->getPhpName(), $fkModelName, ucfirst($type));
499
			$fqcn = $this->packageService->getNamespace() . '\\action\\' . $className;
500
	
501
			// generate action
502
			$action = new ActionSchema($actionNamePrefix . '-' . $type);
503
			$action->addAcl('admin');
504
			$action->setClass($fqcn);
505
			$action->setTitle(str_replace(
506
				['{model}', '{foreign}'],
507
				[$model->getOriginCommonName(), $foreign->getoriginCommonName()],
508
				$titles[$type])
509
			);
510
			$module->addAction($action);
511
	
512
			// generate class
513
			$generator = $generators[$type];
514
			$class = $generator->generate(new PhpClass($fqcn), $model, $foreign, $middle);
515
			$this->codegenService->dumpStruct($class, true);
516
		}
517
	}
518
519
}
520