Skip to content

Commit dbaef00

Browse files
committed
Adding capability to compute shortest path
1 parent 43a33ae commit dbaef00

File tree

3 files changed

+124
-13
lines changed

3 files changed

+124
-13
lines changed

src/SchemaAnalyzer.php

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
<?php
2+
23
namespace Mouf\Database\SchemaAnalyzer;
4+
5+
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
36
use Doctrine\DBAL\Schema\Schema;
47
use Doctrine\DBAL\Schema\Table;
8+
use Fhaculty\Graph\Edge\Base;
59
use Fhaculty\Graph\Graph;
10+
use Graphp\Algorithms\ShortestPath\Dijkstra;
611

712
/**
813
* This class can analyze a database model.
@@ -33,25 +38,28 @@ public function __construct(Schema $schema)
3338
* Detect all junctions tables in the schema.
3439
* A table is a junction table if:
3540
* - it has exactly 2 foreign keys
36-
* - it has only 2 columns (or 3 columns if the third one is an autoincremented primary key)
41+
* - it has only 2 columns (or 3 columns if the third one is an autoincremented primary key).
3742
*
3843
*
3944
* @return Table[]
4045
*/
41-
public function detectJunctionTables() {
42-
return array_filter($this->schema->getTables(), [$this, "isJunctionTable"]);
46+
public function detectJunctionTables()
47+
{
48+
return array_filter($this->schema->getTables(), [$this, 'isJunctionTable']);
4349
}
4450

4551
/**
4652
* Returns true if $table is a junction table.
4753
* I.e:
4854
* - it must have exactly 2 foreign keys
49-
* - it must have only 2 columns (or 3 columns if the third one is an autoincremented primary key)
55+
* - it must have only 2 columns (or 3 columns if the third one is an autoincremented primary key).
5056
*
5157
* @param Table $table
58+
*
5259
* @return bool
5360
*/
54-
private function isJunctionTable(Table $table) {
61+
private function isJunctionTable(Table $table)
62+
{
5563
$foreignKeys = $table->getForeignKeys();
5664
if (count($foreignKeys) != 2) {
5765
return false;
@@ -99,15 +107,57 @@ private function isJunctionTable(Table $table) {
99107
/**
100108
* Get the shortest path between 2 tables.
101109
*
102-
* @param $fromTable
103-
* @param $toTable
110+
* @param string $fromTable
111+
* @param string $toTable
112+
*
113+
* @return ForeignKeyConstraint[]
104114
*/
105-
public function getShortestPath($fromTable, $toTable) {
115+
public function getShortestPath($fromTable, $toTable)
116+
{
106117
$graph = $this->buildSchemaGraph();
107-
// TODO
118+
119+
$dijkstra = new Dijkstra($graph->getVertex($fromTable));
120+
$walk = $dijkstra->getWalkTo($graph->getVertex($toTable));
121+
122+
$foreignKeys = [];
123+
124+
$currentTable = $fromTable;
125+
126+
foreach ($walk->getEdges() as $edge) {
127+
/* @var $edge Base */
128+
129+
if ($fk = $edge->getAttribute('fk')) {
130+
/* @var $fk ForeignKeyConstraint */
131+
$foreignKeys[] = $fk;
132+
if ($fk->getForeignTableName() == $currentTable) {
133+
$currentTable = $fk->getLocalTable()->getName();
134+
} else {
135+
$currentTable = $fk->getForeignTableName();
136+
}
137+
} elseif ($junctionTable = $edge->getAttribute('junction')) {
138+
/* @var $junctionTable Table */
139+
$junctionFks = array_values($junctionTable->getForeignKeys());
140+
// We need to order the 2 FKs. The first one is the one that has a common point with the current table.
141+
$fk = $junctionFks[0];
142+
if ($fk->getForeignTableName() == $currentTable) {
143+
$foreignKeys[] = $fk;
144+
$foreignKeys[] = $junctionFks[1];
145+
} else {
146+
$foreignKeys[] = $junctionFks[1];
147+
$foreignKeys[] = $fk;
148+
}
149+
} else {
150+
// @codeCoverageIgnoreStart
151+
throw new SchemaAnalyzerException('Unexpected edge. We should have a fk or a junction attribute.');
152+
// @codeCoverageIgnoreEnd
153+
}
154+
}
155+
156+
return $foreignKeys;
108157
}
109158

110-
public function buildSchemaGraph() {
159+
private function buildSchemaGraph()
160+
{
111161
$graph = new Graph();
112162

113163
// First, let's create all the vertex
@@ -121,7 +171,7 @@ public function buildSchemaGraph() {
121171
// Create an undirected edge, with weight = 1
122172
$edge = $graph->getVertex($table->getName())->createEdge($graph->getVertex($fk->getForeignTableName()));
123173
$edge->setWeight(self::$WEIGHT_FK);
124-
$edge->getAttributeBag()->setAttribute("fk", $fk);
174+
$edge->getAttributeBag()->setAttribute('fk', $fk);
125175
}
126176
}
127177

@@ -132,9 +182,9 @@ public function buildSchemaGraph() {
132182
$tables[] = $fk->getForeignTableName();
133183
}
134184

135-
$edge = $graph->getVertex($tables[0]->getName())->createEdge($graph->getVertex($tables[1]->getName()));
185+
$edge = $graph->getVertex($tables[0])->createEdge($graph->getVertex($tables[1]));
136186
$edge->setWeight(self::$WEIGHT_JOINTURE_TABLE);
137-
$edge->getAttributeBag()->setAttribute("junction", $junctionTable);
187+
$edge->getAttributeBag()->setAttribute('junction', $junctionTable);
138188
}
139189

140190
return $graph;

src/SchemaAnalyzerException.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Mouf\Database\SchemaAnalyzer;
4+
5+
class SchemaAnalyzerException extends \Exception
6+
{
7+
}

tests/SchemaAnalyzerTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,59 @@ public function testJointureTableDetectionWith3ColumnsWithPkIsFk() {
171171

172172
$this->assertCount(0, $junctionTables);
173173
}
174+
175+
public function testShortestPathInJointure() {
176+
$schema = $this->getBaseSchema();
177+
178+
$role_right = $schema->createTable("role_right");
179+
$role_right->addColumn("role_id", "integer", array("unsigned" => true));
180+
$role_right->addColumn("right_id", "integer", array("unsigned" => true));
181+
$role_right->addForeignKeyConstraint($schema->getTable('role'), array("role_id"), array("id"), array("onUpdate" => "CASCADE"));
182+
$role_right->addForeignKeyConstraint($schema->getTable('right'), array("right_id"), array("id"), array("onUpdate" => "CASCADE"));
183+
$role_right->setPrimaryKey(["role_id", "right_id"]);
184+
185+
$schemaAnalyzer = new SchemaAnalyzer($schema);
186+
187+
$fks = $schemaAnalyzer->getShortestPath("role", "right");
188+
189+
$this->assertCount(2, $fks);
190+
$this->assertEquals("role_right", $fks[0]->getLocalTable()->getName());
191+
$this->assertEquals("role", $fks[0]->getForeignTableName());
192+
$this->assertEquals("role_right", $fks[1]->getLocalTable()->getName());
193+
$this->assertEquals("right", $fks[1]->getForeignTableName());
194+
195+
$fks = $schemaAnalyzer->getShortestPath("right", "role");
196+
197+
$this->assertCount(2, $fks);
198+
$this->assertEquals("role_right", $fks[0]->getLocalTable()->getName());
199+
$this->assertEquals("right", $fks[0]->getForeignTableName());
200+
$this->assertEquals("role_right", $fks[1]->getLocalTable()->getName());
201+
$this->assertEquals("role", $fks[1]->getForeignTableName());
202+
}
203+
204+
public function testShortestPathInLine() {
205+
$schema = $this->getBaseSchema();
206+
207+
$role_right = $schema->createTable("role_right");
208+
$role_right->addColumn("role_id", "integer", array("unsigned" => true));
209+
$role_right->addColumn("right_id", "integer", array("unsigned" => true));
210+
$role_right->addForeignKeyConstraint($schema->getTable('role'), array("role_id"), array("id"), array("onUpdate" => "CASCADE"));
211+
$role_right->addForeignKeyConstraint($schema->getTable('right'), array("right_id"), array("id"), array("onUpdate" => "CASCADE"));
212+
$role_right->setPrimaryKey(["role_id", "right_id"]);
213+
214+
$schemaAnalyzer = new SchemaAnalyzer($schema);
215+
216+
$fks = $schemaAnalyzer->getShortestPath("role", "role_right");
217+
218+
$this->assertCount(1, $fks);
219+
$this->assertEquals("role_right", $fks[0]->getLocalTable()->getName());
220+
$this->assertEquals("role", $fks[0]->getForeignTableName());
221+
222+
$fks = $schemaAnalyzer->getShortestPath("role_right", "role");
223+
224+
$this->assertCount(1, $fks);
225+
$this->assertEquals("role_right", $fks[0]->getLocalTable()->getName());
226+
$this->assertEquals("role", $fks[0]->getForeignTableName());
227+
}
174228
}
175229

0 commit comments

Comments
 (0)