Completed
Push — master ( 118f52...c5048d )
by Thomas
10:09
created

ModelReader::loadDatabase()   C

Complexity

Conditions 7
Paths 2

Size

Total Lines 37
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 37
rs 6.7272
cc 7
eloc 25
nc 2
nop 0
1
<?php
2
namespace keeko\tools\services;
3
4
use keeko\framework\schema\CodegenSchema;
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 phootwork\collection\ArrayList;
17
use phootwork\collection\Map;
18
use phootwork\collection\Set;
19
use phootwork\lang\Text;
20
use Propel\Generator\Builder\Util\SchemaReader;
21
use Propel\Generator\Config\GeneratorConfig;
22
use Propel\Generator\Model\Database;
23
use Propel\Generator\Model\Table;
24
use keeko\tools\model\ReverseOneToOneRelationship;
25
26
class ModelReader {
27
28
	/** @var Project */
29
	private $project;
30
31
	/** @var Database */
32
	private $database;
33
34
	/** @var CodegenSchema */
35
	private $codegen;
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->codegen = $project->hasCodegenFile()
58
			? CodegenSchema::fromFile($project->getCodegenFileName())
59
			: new CodegenSchema();
60
		$this->excluded = $this->loadExcludedModels();
61
62
		$this->load();
63
	}
64
65
	private function loadExcludedModels() {
66
		$list = new ArrayList();
67
		$command = $this->service->getCommand();
68
		if ($command instanceof GenerateActionCommand) {
69
			$list = $this->codegen->getExcludedAction();
70
		} else if ($command instanceof GenerateApiCommand) {
71
			$list = $this->codegen->getExcludedApi();
72
		} else if ($command instanceof GenerateDomainCommand) {
73
			$list = $this->codegen->getExcludedDomain();
74
		} else if ($command instanceof GenerateEmberModelsCommand) {
75
			$list = $this->codegen->getExcludedEmber();
76
		} else if ($command instanceof GenerateSerializerCommand) {
77
			$list = $this->codegen->getExcludedSerializer();
78
		}
79
80
		return new Set($list);
81
	}
82
83
	public function getExcluded() {
84
		return $this->excluded;
85
	}
86
87
	public function getProject() {
88
		return $this->project;
89
	}
90
91
	private function load() {
92
		if ($this->project->hasSchemaFile()) {
93
			$this->loadDatabase();
94
			$this->loadRelationships();
95
		}
96
	}
97
98
	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...
99
		if ($this->database === null) {
100
			$dom = new \DOMDocument('1.0', 'UTF-8');
101
			$dom->load($this->project->getSchemaFileName());
102
			$this->includeExternalSchemas($dom);
103
104
			$config = new GeneratorConfig($this->project->getRootPath());
105
			$reader = new SchemaReader($config->getConfiguredPlatform());
106
			$reader->setGeneratorConfig($config);
107
			$schema = $reader->parseString($dom->saveXML(), $this->project->getSchemaFileName());
108
			$this->database = $schema->getDatabase();
109
110
			// extend excluded list with parents when using a certain behavior
111
			foreach ($this->database->getTables() as $table) {
112
				foreach ($table->getBehaviors() as $behavior) {
113
114
					switch ($behavior->getName()) {
115
						case 'concrete_inheritance':
116
							$parent = $behavior->getParameter('extends');
117
							$this->excluded->add($parent);
118
							$this->renameForeignKeys($table, $parent);
119
							break;
120
121
						case 'versionable':
122
							$versionTableName = $behavior->getParameter('version_table')
123
								? $behavior->getParameter('version_table')
124
								: ($table->getOriginCommonName() . '_version');
125
126
							$this->excluded->add($versionTableName);
127
							break;
128
					}
129
				}
130
			}
131
		}
132
133
		return $this->database;
134
	}
135
136
	private function renameForeignKeys(Table $table, $parent) {
137
		$parent = $this->getModel($parent);
138
139
		foreach ($table->getForeignKeys() as $fk) {
140
			// find fk in parent
141
			foreach ($parent->getForeignKeys() as $pfk) {
142
				if ($pfk->getForeignTableCommonName() == $fk->getForeignTableCommonName()
143
						&& $pfk->getLocalColumnName() == $fk->getLocalColumnName()
144
						&& $pfk->getForeignColumnName() == $fk->getForeignColumnName()) {
145
146
					// replace
147
					$name = new Text($pfk->getName());
148
					if ($name->contains($parent->getOriginCommonName())) {
149
						$name = $name->replace($parent->getOriginCommonName(), $table->getOriginCommonName());
150
						$fk->setName($name->toString());
151
					}
152
					break;
153
				}
154
			}
155
		}
156
	}
157
158
	private function includeExternalSchemas(\DOMDocument $dom) {
159
		$databaseNode = $dom->getElementsByTagName('database')->item(0);
160
		$externalSchemaNodes = $dom->getElementsByTagName('external-schema');
161
162
		while ($externalSchema = $externalSchemaNodes->item(0)) {
163
			$include = $externalSchema->getAttribute('filename');
164
			$externalSchema->parentNode->removeChild($externalSchema);
165
166
			if (!is_readable($include)) {
167
				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...
168
			}
169
170
			$externalSchemaDom = new \DOMDocument('1.0', 'UTF-8');
171
			$externalSchemaDom->load(realpath($include));
172
173
			// The external schema may have external schemas of its own ; recurs
174
			$this->includeExternalSchemas($externalSchemaDom);
175
			foreach ($externalSchemaDom->getElementsByTagName('table') as $tableNode) {
176
				$databaseNode->appendChild($dom->importNode($tableNode, true));
177
			}
178
		}
179
	}
180
181
	public function getDatabase() {
182
		return $this->database;
183
	}
184
185
	private function loadRelationships() {
186
		foreach ($this->getModels() as $table) {
187
			$this->loadRelationshipsForModel($table);
188
		}
189
	}
190
191
	/**
192
	 * Returns all model relationships.
193
	 *
194
	 * @param Table $model
195
	 */
196
	private function loadRelationshipsForModel(Table $model) {
197
		if ($this->relationshipsLoaded->contains($model->getName())) {
198
			return;
199
		}
200
201
		if ($this->excluded->contains($model->getOriginCommonName())) {
202
			return;
203
		}
204
205
		$relationships = $this->getRelationships($model);
206
		$definition = $this->codegen->getRelationships($model->getOriginCommonName());
207
208
		// one-to-* relationships
209
		foreach ($model->getForeignKeys() as $fk) {
210
			// skip, if fk is excluded
211
			if ($this->excluded->contains($fk->getForeignTable()->getOriginCommonName())) {
212
				continue;
213
			}
214
215
			$type = Relationship::ONE_TO_MANY;
216
			if ($definition->has($fk->getName())) {
217
				$type = $definition->get($fk->getName());
218
			}
219
220
			$foreign = $fk->getForeignTable();
221
222
			switch ($type) {
223
				case Relationship::ONE_TO_ONE:
224
					$relationship = new OneToOneRelationship($model, $foreign, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 220 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...
225
					$relationships->add($relationship);
226
227
					$reverse = new ReverseOneToOneRelationship($foreign, $model, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 220 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...
228
					$relationship->setReverseRelationship($reverse);
229
					$this->getRelationships($foreign)->add($reverse);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 220 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...
230
					break;
231
232
				case Relationship::ONE_TO_MANY:
233
					$relationship = new OneToManyRelationship($foreign, $model, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 220 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...
234
					$this->getRelationships($foreign)->add($relationship);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 220 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...
235
236
					$reverse = new OneToOneRelationship($model, $foreign, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 220 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...
237
					$relationship->setReverseRelationship($reverse);
238
					$relationships->add($reverse);
239
					break;
240
			}
241
		}
242
243
		// many-to-many relationships
244
		foreach ($model->getCrossFks() as $cfk) {
245
			$relationship = new ManyToManyRelationship($model, $cfk);
246
247
			// skip, if fk is excluded
248
			if ($this->excluded->contains($relationship->getForeign()->getOriginCommonName())) {
249
				continue;
250
			}
251
252
			$relationships->add($relationship);
253
		}
254
255
		$this->relationships->set($model->getName(), $relationships);
256
		$this->relationshipsLoaded->add($model->getName());
257
258
		return $relationships;
259
	}
260
261
	/**
262
	 *
263
	 * @param Table $model
264
	 * @return Relationships
265
	 */
266
	public function getRelationships(Table $model) {
267
		if (!$this->relationships->has($model->getName())) {
268
			$this->relationships->set($model->getName(), new Relationships($model));
269
		}
270
		return $this->relationships->get($model->getName());
271
	}
272
273
	/**
274
	 * Checks whether the given model exists
275
	 *
276
	 * @param String $name tableName or modelName
277
	 * @return boolean
278
	 */
279
	public function hasModel($name) {
280
		return $this->getDatabase()->hasTable($this->getTableName($name), true);
281
	}
282
283
	/**
284
	 * Returns the model for the given name
285
	 *
286
	 * @param String $name modelName or tableName
287
	 * @return Table
288
	 */
289
	public function getModel($name) {
290
		$tableName = $this->getTableName($name);
291
		$db = $this->getDatabase();
292
		$table = $db->getTable($tableName);
293
294
		return $table;
295
	}
296
297
	/**
298
	 * Returns the tableName for a given name
299
	 *
300
	 * @param String $name tableName or modelName
301
	 * @return String tableName
302
	 */
303
	public function getTableName($name) {
304
		$db = $this->getDatabase();
305
		if (!Text::create($name)->startsWith($db->getTablePrefix())) {
306
			$name = $db->getTablePrefix() . $name;
307
		}
308
309
		return $name;
310
	}
311
312
	/**
313
	 * Returns the models from the database, where table namespace matches package namespace
314
	 *
315
	 * @return Map
316
	 */
317
	public function getModels() {
318
		if ($this->models === null) {
319
			$database = $this->getDatabase();
320
			$namespace = $database->getNamespace();
321
322
			$this->models = new Map();
323
324
			foreach ($database->getTables() as $table) {
325
				if (!$table->isCrossRef()
326
						&& $table->getNamespace() == $namespace
327
						&& !$this->excluded->contains($table->getOriginCommonName())) {
328
					$this->models->set($table->getOriginCommonName(), $table);
329
				}
330
			}
331
		}
332
333
		return $this->models;
334
	}
335
336
	/**
337
	 * Returns all model names
338
	 *
339
	 * @return Set
340
	 */
341
	public function getModelNames() {
342
		return $this->getModels()->keys();
343
	}
344
}