Completed
Push — master ( c7b947...966759 )
by Thomas
08:09
created

ModelReader::loadDatabase()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 26
rs 8.439
cc 5
eloc 17
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();
0 ignored issues
show
Bug introduced by
The method getExcludedAction() does not seem to exist on object<keeko\framework\schema\CodegenSchema>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
70
		} else if ($command instanceof GenerateApiCommand) {
71
			$list = $this->codegen->getExcludedApi();
0 ignored issues
show
Bug introduced by
The method getExcludedApi() does not seem to exist on object<keeko\framework\schema\CodegenSchema>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
72
		} else if ($command instanceof GenerateDomainCommand) {
73
			$list = $this->codegen->getExcludedDomain();
0 ignored issues
show
Bug introduced by
The method getExcludedDomain() does not seem to exist on object<keeko\framework\schema\CodegenSchema>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
74
		} else if ($command instanceof GenerateEmberModelsCommand) {
75
			$list = $this->codegen->getExcludedEmber();
0 ignored issues
show
Bug introduced by
The method getExcludedEmber() does not seem to exist on object<keeko\framework\schema\CodegenSchema>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
76
		} else if ($command instanceof GenerateSerializerCommand) {
77
			$list = $this->codegen->getExcludedSerializer();
0 ignored issues
show
Bug introduced by
The method getExcludedSerializer() does not seem to exist on object<keeko\framework\schema\CodegenSchema>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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 concrete_inheritance behavior
111
			foreach ($this->database->getTables() as $table) {
112
				foreach ($table->getBehaviors() as $behavior) {
113
					if ($behavior->getName() == 'concrete_inheritance') {
114
						$parent = $behavior->getParameter('extends');
115
						$this->excluded->add($parent);
116
						$this->renameForeignKeys($table, $parent);
117
					}
118
				}
119
			}
120
		}
121
122
		return $this->database;
123
	}
124
125
	private function renameForeignKeys(Table $table, $parent) {
126
		$parent = $this->getModel($parent);
127
		
128
		foreach ($table->getForeignKeys() as $fk) {
129
			// find fk in parent
130
			foreach ($parent->getForeignKeys() as $pfk) {
131
				if ($pfk->getForeignTableCommonName() == $fk->getForeignTableCommonName()
132
						&& $pfk->getLocalColumnName() == $fk->getLocalColumnName()
133
						&& $pfk->getForeignColumnName() == $fk->getForeignColumnName()) {
134
				
135
					// replace
136
					$name = new Text($pfk->getName());
137
					if ($name->contains($parent->getOriginCommonName())) {
138
						$name = $name->replace($parent->getOriginCommonName(), $table->getOriginCommonName());
139
						$fk->setName($name->toString());
140
					}
141
					break;
142
				}
143
			}
144
		}
145
	}
146
	
147
	private function includeExternalSchemas(\DOMDocument $dom) {
148
		$databaseNode = $dom->getElementsByTagName('database')->item(0);
149
		$externalSchemaNodes = $dom->getElementsByTagName('external-schema');
150
	
151
		while ($externalSchema = $externalSchemaNodes->item(0)) {
152
			$include = $externalSchema->getAttribute('filename');
153
			$externalSchema->parentNode->removeChild($externalSchema);
154
	
155
			if (!is_readable($include)) {
156
				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...
157
			}
158
	
159
			$externalSchemaDom = new \DOMDocument('1.0', 'UTF-8');
160
			$externalSchemaDom->load(realpath($include));
161
162
			// The external schema may have external schemas of its own ; recurs
163
			$this->includeExternalSchemas($externalSchemaDom);
164
			foreach ($externalSchemaDom->getElementsByTagName('table') as $tableNode) {
165
				$databaseNode->appendChild($dom->importNode($tableNode, true));
166
			}
167
		}
168
	}
169
	
170
	public function getDatabase() {
171
		return $this->database;
172
	}
173
	
174
	private function loadRelationships() {
175
		foreach ($this->getModels() as $table) {
176
			$this->loadRelationshipsForModel($table);
177
		}
178
	}
179
	
180
	/**
181
	 * Returns all model relationships.
182
	 *
183
	 * @param Table $model
184
	 */
185
	private function loadRelationshipsForModel(Table $model) {
186
		if ($this->relationshipsLoaded->contains($model->getName())) {
187
			return;
188
		}
189
		
190
		if ($this->excluded->contains($model->getOriginCommonName())) {
191
			return;
192
		}
193
194
		$relationships = $this->getRelationships($model);
195
		$definition = $this->codegen->getRelationships($model->getOriginCommonName());
0 ignored issues
show
Bug introduced by
The method getRelationships() does not seem to exist on object<keeko\framework\schema\CodegenSchema>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
196
	
197
		// one-to-* relationships
198
		foreach ($model->getForeignKeys() as $fk) {
199
			// skip, if fk is excluded
200
			if ($this->excluded->contains($fk->getForeignTable()->getOriginCommonName())) {
201
				continue;
202
			}
203
204
			$type = Relationship::ONE_TO_MANY;
205
			if ($definition->has($fk->getName())) {
206
				$type = $definition->get($fk->getName());
207
			}
208
209
			$foreign = $fk->getForeignTable();
210
			
211
			switch ($type) {
212
				case Relationship::ONE_TO_ONE:
213
					$relationship = new OneToOneRelationship($model, $foreign, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 209 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...
214
					$relationships->add($relationship);
215
					
216
					$reverse = new ReverseOneToOneRelationship($foreign, $model, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 209 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...
217
					$this->getRelationships($foreign)->add($reverse);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 209 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...
218
					break;
219
				
220
				case Relationship::ONE_TO_MANY:
221
					$relationship = new OneToManyRelationship($foreign, $model, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 209 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...
222
					$this->getRelationships($foreign)->add($relationship);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 209 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...
223
					
224
					$reverse = new OneToOneRelationship($model, $foreign, $fk);
0 ignored issues
show
Bug introduced by
It seems like $foreign defined by $fk->getForeignTable() on line 209 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($reverse);
226
					break;
227
			}
228
		}
229
230
		// many-to-many relationships
231
		foreach ($model->getCrossFks() as $cfk) {
232
			$relationship = new ManyToManyRelationship($model, $cfk);
233
234
			// skip, if fk is excluded
235
			if ($this->excluded->contains($relationship->getForeign()->getOriginCommonName())) {
236
				continue;
237
			}
238
239
			$relationships->add($relationship);
240
		}
241
242
		$this->relationships->set($model->getName(), $relationships);
243
		$this->relationshipsLoaded->add($model->getName());
244
245
		return $relationships;
246
	}
247
	
248
	/**
249
	 * 
250
	 * @param Table $model
251
	 * @return Relationships
252
	 */
253
	public function getRelationships(Table $model) {
254
		if (!$this->relationships->has($model->getName())) {
255
			$this->relationships->set($model->getName(), new Relationships($model));
256
		}
257
		return $this->relationships->get($model->getName());
258
	}
259
	
260
	/**
261
	 * Checks whether the given model exists
262
	 *
263
	 * @param String $name tableName or modelName
264
	 * @return boolean
265
	 */
266
	public function hasModel($name) {
267
		return $this->getDatabase()->hasTable($this->getTableName($name), true);
268
	}
269
	
270
	/**
271
	 * Returns the model for the given name
272
	 *
273
	 * @param String $name modelName or tableName
274
	 * @return Table
275
	 */
276
	public function getModel($name) {
277
		$tableName = $this->getTableName($name);
278
		$db = $this->getDatabase();
279
		$table = $db->getTable($tableName);
280
		
281
		return $table;
282
	}
283
	
284
	/**
285
	 * Returns the tableName for a given name
286
	 *
287
	 * @param String $name tableName or modelName
288
	 * @return String tableName
289
	 */
290
	public function getTableName($name) {
291
		$db = $this->getDatabase();
292
		if (!Text::create($name)->startsWith($db->getTablePrefix())) {
293
			$name = $db->getTablePrefix() . $name;
294
		}
295
	
296
		return $name;
297
	}
298
	
299
	/**
300
	 * Returns the models from the database, where table namespace matches package namespace
301
	 *
302
	 * @return Map
303
	 */
304
	public function getModels() {
305
		if ($this->models === null) {
306
			$database = $this->getDatabase();
307
			$namespace = $database->getNamespace();
308
	
309
			$this->models = new Map();
310
311
			foreach ($database->getTables() as $table) {
312
				if (!$table->isCrossRef() 
313
						&& $table->getNamespace() == $namespace
314
						&& !$this->excluded->contains($table->getOriginCommonName())) {
315
					$this->models->set($table->getOriginCommonName(), $table);
316
				}
317
			}
318
		}
319
	
320
		return $this->models;
321
	}
322
	
323
	/**
324
	 * Returns all model names
325
	 *
326
	 * @return Set
327
	 */
328
	public function getModelNames() {
329
		return $this->getModels()->keys();
330
	}
331
}