ModelReader   F
last analyzed

Complexity

Total Complexity 55

Size/Duplication

Total Lines 317
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 17
dl 0
loc 317
rs 3.5483
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
B loadExcludedModels() 0 17 6
A getExcluded() 0 3 1
A getProject() 0 3 1
A load() 0 6 2
C loadDatabase() 0 37 7
B renameForeignKeys() 0 21 7
B includeExternalSchemas() 0 22 4
A getDatabase() 0 3 1
A loadRelationships() 0 5 2
C loadRelationshipsForModel() 0 64 10
A getRelationships() 0 6 2
A hasModel() 0 3 1
A getModel() 0 7 1
A getTableName() 0 8 2
B getModels() 0 18 6
A getModelNames() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like ModelReader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ModelReader, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace keeko\tools\services;
3
4
use keeko\framework\schema\GeneratorDefinitionSchema;
5
use keeko\tools\command\GenerateActionCommand;
6
use keeko\tools\command\GenerateApiCommand;
7
use keeko\tools\command\GenerateDomainCommand;
8
use keeko\tools\command\GenerateEmberModelsCommand;
9
use keeko\tools\command\GenerateSerializerCommand;
10
use keeko\tools\model\ManyToManyRelationship;
11
use keeko\tools\model\OneToManyRelationship;
12
use keeko\tools\model\OneToOneRelationship;
13
use keeko\tools\model\Project;
14
use keeko\tools\model\Relationship;
15
use keeko\tools\model\Relationships;
16
use keeko\tools\model\ReverseOneToOneRelationship;
17
use phootwork\collection\ArrayList;
18
use phootwork\collection\Map;
19
use phootwork\collection\Set;
20
use phootwork\lang\Text;
21
use Propel\Generator\Builder\Util\SchemaReader;
22
use Propel\Generator\Config\GeneratorConfig;
23
use Propel\Generator\Model\Database;
24
use Propel\Generator\Model\Table;
25
26
class ModelReader {
27
28
	/** @var Project */
29
	private $project;
30
31
	/** @var Database */
32
	private $database;
33
34
	/** @var GeneratorDefinitionSchema */
35
	private $generatorDefinition;
36
37
	/** @var Map */
38
	private $relationships;
39
40
	/** @var Set */
41
	private $relationshipsLoaded;
42
43
	/** @var Map */
44
	private $models;
45
46
	/** @var Set */
47
	private $excluded;
48
49
	/** @var CommandService */
50
	private $service;
51
52
	public function __construct(Project $project, CommandService $service) {
53
		$this->project = $project;
54
		$this->service = $service;
55
		$this->relationships = new Map();
56
		$this->relationshipsLoaded = new Set();
57
		$this->generatorDefinition = $project->getGeneratorDefinition();
58
		$this->excluded = $this->loadExcludedModels();
59
60
		$this->load();
61
	}
62
63
	private function loadExcludedModels() {
64
		$list = new ArrayList();
65
		$command = $this->service->getCommand();
66
		if ($command instanceof GenerateActionCommand) {
67
			$list = $this->generatorDefinition->getExcludedAction();
68
		} else if ($command instanceof GenerateApiCommand) {
69
			$list = $this->generatorDefinition->getExcludedApi();
70
		} else if ($command instanceof GenerateDomainCommand) {
71
			$list = $this->generatorDefinition->getExcludedDomain();
72
		} else if ($command instanceof GenerateEmberModelsCommand) {
73
			$list = $this->generatorDefinition->getExcludedEmber();
74
		} else if ($command instanceof GenerateSerializerCommand) {
75
			$list = $this->generatorDefinition->getExcludedSerializer();
76
		}
77
78
		return new Set($list);
79
	}
80
81
	public function getExcluded() {
82
		return $this->excluded;
83
	}
84
85
	public function getProject() {
86
		return $this->project;
87
	}
88
89
	private function load() {
90
		if ($this->project->hasSchemaFile()) {
91
			$this->loadDatabase();
92
			$this->loadRelationships();
93
		}
94
	}
95
96
	private function loadDatabase() {
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...
97
		if ($this->database === null) {
98
			$dom = new \DOMDocument('1.0', 'UTF-8');
99
			$dom->load($this->project->getSchemaFileName());
100
			$this->includeExternalSchemas($dom);
101
102
			$config = new GeneratorConfig($this->project->getRootPath());
103
			$reader = new SchemaReader($config->getConfiguredPlatform());
104
			$reader->setGeneratorConfig($config);
105
			$schema = $reader->parseString($dom->saveXML(), $this->project->getSchemaFileName());
106
			$this->database = $schema->getDatabase();
107
108
			// extend excluded list with parents when using a certain behavior
109
			foreach ($this->database->getTables() as $table) {
110
				foreach ($table->getBehaviors() as $behavior) {
111
112
					switch ($behavior->getName()) {
113
						case 'concrete_inheritance':
114
							$parent = $behavior->getParameter('extends');
115
							$this->excluded->add($parent);
116
							$this->renameForeignKeys($table, $parent);
117
							break;
118
119
						case 'versionable':
120
							$versionTableName = $behavior->getParameter('version_table')
121
								? $behavior->getParameter('version_table')
122
								: ($table->getOriginCommonName() . '_version');
123
124
							$this->excluded->add($versionTableName);
125
							break;
126
					}
127
				}
128
			}
129
		}
130
131
		return $this->database;
132
	}
133
134
	private function renameForeignKeys(Table $table, $parent) {
135
		$parent = $this->getModel($parent);
136
137
		foreach ($table->getForeignKeys() as $fk) {
138
			// find fk in parent
139
			foreach ($parent->getForeignKeys() as $pfk) {
140
				if ($pfk->getForeignTableCommonName() == $fk->getForeignTableCommonName()
141
						&& $pfk->getLocalColumnName() == $fk->getLocalColumnName()
142
						&& $pfk->getForeignColumnName() == $fk->getForeignColumnName()) {
143
144
					// replace
145
					$name = new Text($pfk->getName());
146
					if ($name->contains($parent->getOriginCommonName())) {
147
						$name = $name->replace($parent->getOriginCommonName(), $table->getOriginCommonName());
148
						$fk->setName($name->toString());
149
					}
150
					break;
151
				}
152
			}
153
		}
154
	}
155
156
	private function includeExternalSchemas(\DOMDocument $dom) {
157
		$databaseNode = $dom->getElementsByTagName('database')->item(0);
158
		$externalSchemaNodes = $dom->getElementsByTagName('external-schema');
159
160
		while ($externalSchema = $externalSchemaNodes->item(0)) {
161
			$include = $externalSchema->getAttribute('filename');
162
			$externalSchema->parentNode->removeChild($externalSchema);
163
164
			if (!is_readable($include)) {
165
				throw new \RuntimeException("External schema '$include' does not exist");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $include instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
166
			}
167
168
			$externalSchemaDom = new \DOMDocument('1.0', 'UTF-8');
169
			$externalSchemaDom->load(realpath($include));
170
171
			// The external schema may have external schemas of its own ; recurs
172
			$this->includeExternalSchemas($externalSchemaDom);
173
			foreach ($externalSchemaDom->getElementsByTagName('table') as $tableNode) {
174
				$databaseNode->appendChild($dom->importNode($tableNode, true));
175
			}
176
		}
177
	}
178
179
	public function getDatabase() {
180
		return $this->database;
181
	}
182
183
	private function loadRelationships() {
184
		foreach ($this->getModels() as $table) {
185
			$this->loadRelationshipsForModel($table);
186
		}
187
	}
188
189
	/**
190
	 * Returns all model relationships.
191
	 *
192
	 * @param Table $model
193
	 */
194
	private function loadRelationshipsForModel(Table $model) {
195
		if ($this->relationshipsLoaded->contains($model->getName())) {
196
			return;
197
		}
198
199
		if ($this->excluded->contains($model->getOriginCommonName())) {
200
			return;
201
		}
202
203
		$relationships = $this->getRelationships($model);
204
		$definition = $this->generatorDefinition->getRelationships($model->getOriginCommonName());
205
206
		// one-to-* relationships
207
		foreach ($model->getForeignKeys() as $fk) {
208
			// skip, if fk is excluded
209
			if ($this->excluded->contains($fk->getForeignTable()->getOriginCommonName())) {
210
				continue;
211
			}
212
213
			$type = Relationship::ONE_TO_MANY;
214
			if ($definition->has($fk->getName())) {
215
				$type = $definition->get($fk->getName());
216
			}
217
218
			$foreign = $fk->getForeignTable();
219
220
			switch ($type) {
221
				case Relationship::ONE_TO_ONE:
222
					$relationship = new OneToOneRelationship($model, $foreign, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 218 can be null; however, keeko\tools\model\OneToO...tionship::__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...
223
					$relationships->add($relationship);
224
225
					$reverse = new ReverseOneToOneRelationship($foreign, $model, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 218 can be null; however, keeko\tools\model\OneToO...tionship::__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...
226
					$relationship->setReverseRelationship($reverse);
227
					$this->getRelationships($foreign)->add($reverse);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 218 can be null; however, keeko\tools\services\Mod...der::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...
228
					break;
229
230
				case Relationship::ONE_TO_MANY:
231
					$relationship = new OneToManyRelationship($foreign, $model, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 218 can be null; however, keeko\tools\model\OneToO...tionship::__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...
232
					$this->getRelationships($foreign)->add($relationship);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 218 can be null; however, keeko\tools\services\Mod...der::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...
233
234
					$reverse = new OneToOneRelationship($model, $foreign, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 218 can be null; however, keeko\tools\model\OneToO...tionship::__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...
235
					$relationship->setReverseRelationship($reverse);
236
					$relationships->add($reverse);
237
					break;
238
			}
239
		}
240
241
		// many-to-many relationships
242
		foreach ($model->getCrossFks() as $cfk) {
243
			$relationship = new ManyToManyRelationship($model, $cfk);
244
245
			// skip, if fk is excluded
246
			if ($this->excluded->contains($relationship->getForeign()->getOriginCommonName())) {
247
				continue;
248
			}
249
250
			$relationships->add($relationship);
251
		}
252
253
		$this->relationships->set($model->getName(), $relationships);
254
		$this->relationshipsLoaded->add($model->getName());
255
256
		return $relationships;
257
	}
258
259
	/**
260
	 *
261
	 * @param Table $model
262
	 * @return Relationships
263
	 */
264
	public function getRelationships(Table $model) {
265
		if (!$this->relationships->has($model->getName())) {
266
			$this->relationships->set($model->getName(), new Relationships($model));
267
		}
268
		return $this->relationships->get($model->getName());
269
	}
270
271
	/**
272
	 * Checks whether the given model exists
273
	 *
274
	 * @param String $name tableName or modelName
275
	 * @return boolean
276
	 */
277
	public function hasModel($name) {
278
		return $this->getDatabase()->hasTable($this->getTableName($name), true);
279
	}
280
281
	/**
282
	 * Returns the model for the given name
283
	 *
284
	 * @param String $name modelName or tableName
285
	 * @return Table
286
	 */
287
	public function getModel($name) {
288
		$tableName = $this->getTableName($name);
289
		$db = $this->getDatabase();
290
		$table = $db->getTable($tableName);
291
292
		return $table;
293
	}
294
295
	/**
296
	 * Returns the tableName for a given name
297
	 *
298
	 * @param String $name tableName or modelName
299
	 * @return String tableName
300
	 */
301
	public function getTableName($name) {
302
		$db = $this->getDatabase();
303
		if (!Text::create($name)->startsWith($db->getTablePrefix())) {
304
			$name = $db->getTablePrefix() . $name;
305
		}
306
307
		return $name;
308
	}
309
310
	/**
311
	 * Returns the models from the database, where table namespace matches package namespace
312
	 *
313
	 * @return Map
314
	 */
315
	public function getModels() {
316
		if ($this->models === null) {
317
			$database = $this->getDatabase();
318
			$namespace = $database->getNamespace();
319
320
			$this->models = new Map();
321
322
			foreach ($database->getTables() as $table) {
323
				if (!$table->isCrossRef()
324
						&& $table->getNamespace() == $namespace
325
						&& !$this->excluded->contains($table->getOriginCommonName())) {
326
					$this->models->set($table->getOriginCommonName(), $table);
327
				}
328
			}
329
		}
330
331
		return $this->models;
332
	}
333
334
	/**
335
	 * Returns all model names
336
	 *
337
	 * @return Set
338
	 */
339
	public function getModelNames() {
340
		return $this->getModels()->keys();
341
	}
342
}