Skip to content

Commit 70a05ab

Browse files
committed
Doc and PSR-2
1 parent fc76e32 commit 70a05ab

6 files changed

+77
-43
lines changed

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ It will return the list of foreign keys it used to link the 2 tables.
5555
Internals:
5656

5757
- Each foreign key has a *cost* of 1
58-
- Junction tables have a cost of 1.5, instead of 2 (one for each foreign key)
58+
- Junction tables have a *cost* of 1.5, instead of 2 (one for each foreign key)
5959

6060
```php
6161
// $conn is the DBAL connection.
@@ -66,4 +66,25 @@ $fks = $schemaAnalyzer->getShortestPath("users", "rights");
6666
// This will return an array of Doctrine\DBAL\Schema\ForeignKeyConstraint objects
6767
```
6868

69-
// TODO: Ambiguity exception!
69+
<div class="alert alert-info"><strong>Heads up!</strong> The shortest path is based on the <em>cost</em> of the
70+
foreign keys. It is perfectly possible to have several shortest paths (if several paths have the same total cost).
71+
If there are several shortest paths, rather than choosing one path amongst the others, SchemaAnalyzer will throw
72+
a <code>ShortestPathAmbiguityException</code>. The exception message details all the possible shortest
73+
paths.</div>
74+
75+
## Caching results
76+
77+
Analyzing the full data model and looking for shortest paths can take a long time. For anything that should run
78+
in a production environment, it is recommended to cache the result. `SchemaAnalyzer` can be passed a Doctrine cache,
79+
along a cache prefix. The cache prefix is a string that will be used to prefix all cache keys. It is useful to
80+
avoid cache collisions between several databases.
81+
82+
Usage:
83+
84+
```php
85+
// $conn is the DBAL connection.
86+
// Let's use the ApcCache (or any other Doctrine cache...)
87+
$cache = new ApcCache();
88+
$schemaAnalyzer = new SchemaAnalyzer($conn->getSchemaManager(), $cache, "my_prefix");
89+
```
90+

src/MultiDijkstra.php

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,24 @@
55
use Fhaculty\Graph\Edge;
66
use Fhaculty\Graph\Exception\UnexpectedValueException;
77
use Fhaculty\Graph\Vertex;
8-
use \SplPriorityQueue;
8+
use SplPriorityQueue;
99

1010
/**
1111
* Dijkstra's shortest path algorithm modified to measure all possible shortest paths.
1212
*/
1313
class MultiDijkstra
1414
{
1515
/**
16-
* Get all edges on shortest path for this vertex
16+
* Get all edges on shortest path for this vertex.
1717
*
18-
* @throws UnexpectedValueException when encountering an Edge with negative weight
18+
* @throws UnexpectedValueException when encountering an Edge with negative weight
1919
* @throws MultiDijkstraNoPathException
20+
*
2021
* @return array<string, Vertex[]> where key is the destination vertex name and value is an array of possible origin vertex
2122
*/
2223
public static function findShortestPaths(Vertex $startVertex, Vertex $endVertex)
2324
{
24-
$totalCostOfCheapestPathTo = [];
25+
$totalCostOfCheapestPathTo = [];
2526
// start node distance
2627
$totalCostOfCheapestPathTo[$startVertex->getId()] = 0;
2728

@@ -33,18 +34,18 @@ public static function findShortestPaths(Vertex $startVertex, Vertex $endVertex)
3334
$cheapestVertex->insert($startVertex, 0);
3435

3536
// predecessors
36-
$predecesEdgeOfCheapestPathTo = [];
37+
$predecesEdgeOfCheapestPathTo = [];
3738

3839
// mark vertices when their cheapest path has been found
39-
$usedVertices = [ $startVertex->getId() => true ];
40+
$usedVertices = [$startVertex->getId() => true];
4041

4142
$isFirst = true;
4243

4344
// Repeat until all vertices have been marked
4445
$totalCountOfVertices = count($startVertex->getGraph()->getVertices());
4546
for ($i = 0; $i < $totalCountOfVertices; ++$i) {
46-
$currentVertex = NULL;
47-
$currentVertexId = NULL;
47+
$currentVertex = null;
48+
$currentVertexId = null;
4849
$isEmpty = false;
4950
if ($isFirst) {
5051
$currentVertex = $startVertex;
@@ -87,7 +88,6 @@ public static function findShortestPaths(Vertex $startVertex, Vertex $endVertex)
8788
break;
8889
}
8990

90-
9191
// check for all edges of current vertex if there is a cheaper path (or IN OTHER WORDS: Add reachable nodes from currently added node and refresh the current possible distances)
9292
foreach ($currentVertex->getEdgesOut() as $edge) {
9393
$weight = $edge->getWeight();
@@ -105,23 +105,22 @@ public static function findShortestPaths(Vertex $startVertex, Vertex $endVertex)
105105

106106
if ((!isset($predecesEdgeOfCheapestPathTo[$targetVertexId]))
107107
// is the new path cheaper?
108-
|| $totalCostOfCheapestPathTo[$targetVertexId] > $newCostsToTargetVertex){
108+
|| $totalCostOfCheapestPathTo[$targetVertexId] > $newCostsToTargetVertex) {
109109

110110
// Not an update, just a new insert with lower cost
111-
$cheapestVertex->insert($targetVertex, - $newCostsToTargetVertex);
111+
$cheapestVertex->insert($targetVertex, -$newCostsToTargetVertex);
112112
// so the lowest cost will be extracted first
113113
// and higher cost will be skipped during extraction
114114

115115
// update/set costs found with the new connection
116116
$totalCostOfCheapestPathTo[$targetVertexId] = $newCostsToTargetVertex;
117117
// update/set predecessor vertex from the new connection
118-
$predecesEdgeOfCheapestPathTo[$targetVertexId] = [ $edge ];
118+
$predecesEdgeOfCheapestPathTo[$targetVertexId] = [$edge];
119119
} elseif ($totalCostOfCheapestPathTo[$targetVertexId] == $newCostsToTargetVertex) {
120120
// Same length paths. We need to add the predecessor to the list of possible predecessors.
121121
$predecesEdgeOfCheapestPathTo[$targetVertexId][] = $edge;
122122
}
123123
}
124-
125124
}
126125
}
127126

@@ -134,10 +133,12 @@ public static function findShortestPaths(Vertex $startVertex, Vertex $endVertex)
134133
}
135134

136135
/**
137-
* @param array<string, Vertex[]> $predecesEdgesArray key is the destination vertex name and value is an array of possible origin vertex
136+
* @param array<string, Vertex[]> $predecesEdgesArray key is the destination vertex name and value is an array of possible origin vertex
137+
*
138138
* @return Edge\Base[]
139139
*/
140-
public static function getCheapestPathFromPredecesArray(Vertex $startVertex, Vertex $endVertex, array $predecesEdgesArray) {
140+
public static function getCheapestPathFromPredecesArray(Vertex $startVertex, Vertex $endVertex, array $predecesEdgesArray)
141+
{
141142
$edges = [];
142143
$currentVertex = $endVertex;
143144
while ($currentVertex !== $startVertex) {
@@ -161,10 +162,12 @@ public static function getCheapestPathFromPredecesArray(Vertex $startVertex, Ver
161162
/**
162163
* @param Vertex $startVertex
163164
* @param Vertex $endVertex
164-
* @param array $predecesEdgesArray
165+
* @param array $predecesEdgesArray
166+
*
165167
* @return Edge\Base[][]
166168
*/
167-
public static function getAllPossiblePathsFromPredecesArray(Vertex $startVertex, Vertex $endVertex, array $predecesEdgesArray) {
169+
public static function getAllPossiblePathsFromPredecesArray(Vertex $startVertex, Vertex $endVertex, array $predecesEdgesArray)
170+
{
168171
$edgesPaths = [];
169172

170173
if ($startVertex === $endVertex) {
@@ -186,10 +189,9 @@ public static function getAllPossiblePathsFromPredecesArray(Vertex $startVertex,
186189
$edges2[] = $edge;
187190
}
188191
} else {
189-
$edgesPaths2 = [ [ $edge ] ];
192+
$edgesPaths2 = [[$edge]];
190193
}
191194

192-
193195
$edgesPaths = array_merge($edgesPaths, $edgesPaths2);
194196
}
195197

src/MultiDijkstraAmbiguityException.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
2+
23
namespace Mouf\Database\SchemaAnalyzer;
34

45
class MultiDijkstraAmbiguityException extends \Exception

src/MultiDijkstraNoPathException.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
2+
23
namespace Mouf\Database\SchemaAnalyzer;
34

45
class MultiDijkstraNoPathException extends \Exception

src/SchemaAnalyzer.php

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use Fhaculty\Graph\Edge\Base;
1212
use Fhaculty\Graph\Graph;
1313
use Fhaculty\Graph\Vertex;
14-
use Graphp\Algorithms\ShortestPath\Dijkstra;
1514

1615
/**
1716
* This class can analyze a database model.
@@ -47,8 +46,8 @@ class SchemaAnalyzer
4746

4847
/**
4948
* @param AbstractSchemaManager $schemaManager
50-
* @param Cache|null $cache The Doctrine cache service to use to cache results (optional)
51-
* @param string|null $schemaCacheKey The unique identifier for the schema manager. Compulsory if cache is set.
49+
* @param Cache|null $cache The Doctrine cache service to use to cache results (optional)
50+
* @param string|null $schemaCacheKey The unique identifier for the schema manager. Compulsory if cache is set.
5251
*/
5352
public function __construct(AbstractSchemaManager $schemaManager, Cache $cache = null, $schemaCacheKey = null)
5453
{
@@ -66,7 +65,7 @@ public function __construct(AbstractSchemaManager $schemaManager, Cache $cache =
6665

6766
/**
6867
* Detect all junctions tables in the schema.
69-
* A table is a junction table if:
68+
* A table is a junction table if:.
7069
*
7170
* - it has exactly 2 foreign keys
7271
* - it has only 2 columns (or 3 columns if the third one is an autoincremented primary key).
@@ -76,18 +75,19 @@ public function __construct(AbstractSchemaManager $schemaManager, Cache $cache =
7675
*/
7776
public function detectJunctionTables()
7877
{
79-
$junctionTablesKey = $this->cachePrefix."_junctiontables";
78+
$junctionTablesKey = $this->cachePrefix.'_junctiontables';
8079
$junctionTables = $this->cache->fetch($junctionTablesKey);
8180
if ($junctionTables === false) {
8281
$junctionTables = array_filter($this->getSchema()->getTables(), [$this, 'isJunctionTable']);
8382
$this->cache->save($junctionTablesKey, $junctionTables);
8483
}
84+
8585
return $junctionTables;
8686
}
8787

8888
/**
8989
* Returns true if $table is a junction table.
90-
* I.e:
90+
* I.e:.
9191
*
9292
* - it must have exactly 2 foreign keys
9393
* - it must have only 2 columns (or 3 columns if the third one is an autoincremented primary key).
@@ -147,17 +147,20 @@ private function isJunctionTable(Table $table)
147147
*
148148
* @param string $fromTable
149149
* @param string $toTable
150+
*
150151
* @return \Doctrine\DBAL\Schema\ForeignKeyConstraint[]
152+
*
151153
* @throws SchemaAnalyzerException
152154
*/
153155
public function getShortestPath($fromTable, $toTable)
154156
{
155-
$cacheKey = $this->cachePrefix."_shortest_".$fromTable."```".$toTable;
157+
$cacheKey = $this->cachePrefix.'_shortest_'.$fromTable.'```'.$toTable;
156158
$path = $this->cache->fetch($cacheKey);
157159
if ($path === false) {
158160
$path = $this->getShortestPathWithoutCache($fromTable, $toTable);
159161
$this->cache->save($cacheKey, $path);
160162
}
163+
161164
return $path;
162165
}
163166

@@ -166,7 +169,9 @@ public function getShortestPath($fromTable, $toTable)
166169
*
167170
* @param string $fromTable
168171
* @param string $toTable
172+
*
169173
* @return \Doctrine\DBAL\Schema\ForeignKeyConstraint[]
174+
*
170175
* @throws SchemaAnalyzerException
171176
*/
172177
private function getShortestPathWithoutCache($fromTable, $toTable)
@@ -255,33 +260,37 @@ private function buildSchemaGraph()
255260
}
256261

257262
/**
258-
* Returns the schema (from the schema manager or the cache if needed)
263+
* Returns the schema (from the schema manager or the cache if needed).
264+
*
259265
* @return Schema
260266
*/
261-
private function getSchema() {
267+
private function getSchema()
268+
{
262269
if ($this->schema === null) {
263-
$schemaKey = $this->cachePrefix."_schema";
270+
$schemaKey = $this->cachePrefix.'_schema';
264271
$this->schema = $this->cache->fetch($schemaKey);
265272
if (empty($this->schema)) {
266273
$this->schema = $this->schemaManager->createSchema();
267274
$this->cache->save($schemaKey, $this->schema);
268275
}
269276
}
277+
270278
return $this->schema;
271279
}
272280

273281
/**
274282
* Returns the full exception message when an ambiguity arises.
275283
*
276284
* @param Base[][] $paths
277-
* @param Vertex $startVertex
285+
* @param Vertex $startVertex
278286
*/
279-
private function getAmbiguityExceptionMessage(array $paths, Vertex $startVertex, Vertex $endVertex) {
287+
private function getAmbiguityExceptionMessage(array $paths, Vertex $startVertex, Vertex $endVertex)
288+
{
280289
$textPaths = [];
281290
$i = 1;
282291
foreach ($paths as $path) {
283-
$textPaths[] = "Path ".$i.": ".$this->getTextualPath($path, $startVertex);
284-
$i++;
292+
$textPaths[] = 'Path '.$i.': '.$this->getTextualPath($path, $startVertex);
293+
++$i;
285294
}
286295

287296
$msg = sprintf("There are many possible shortest paths between table '%s' and table '%s'\n\n",
@@ -298,7 +307,8 @@ private function getAmbiguityExceptionMessage(array $paths, Vertex $startVertex,
298307
* @param Base[] $path
299308
* @param Vertex $startVertex
300309
*/
301-
private function getTextualPath(array $path, Vertex $startVertex) {
310+
private function getTextualPath(array $path, Vertex $startVertex)
311+
{
302312
$currentVertex = $startVertex;
303313
$currentTable = $currentVertex->getId();
304314

@@ -317,9 +327,9 @@ private function getTextualPath(array $path, Vertex $startVertex) {
317327

318328
$columns = implode(',', $fk->getLocalColumns());
319329

320-
$textPath .= " ".(!$isForward?"<":"");
321-
$textPath .= "--(".$columns.")--";
322-
$textPath .= ($isForward?">":"")." ";
330+
$textPath .= ' '.(!$isForward ? '<' : '');
331+
$textPath .= '--('.$columns.')--';
332+
$textPath .= ($isForward ? '>' : '').' ';
323333
$textPath .= $currentTable;
324334
} elseif ($junctionTable = $edge->getAttribute('junction')) {
325335
/* @var $junctionTable Table */
@@ -331,7 +341,7 @@ private function getTextualPath(array $path, Vertex $startVertex) {
331341
} else {
332342
$currentTable = $fk->getForeignTableName();
333343
}
334-
$textPath .= " <=(".$junctionTable->getName().")=> ".$currentTable;
344+
$textPath .= ' <=('.$junctionTable->getName().')=> '.$currentTable;
335345
} else {
336346
// @codeCoverageIgnoreStart
337347
throw new SchemaAnalyzerException('Unexpected edge. We should have a fk or a junction attribute.');
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
<?php
2-
namespace Mouf\Database\SchemaAnalyzer;
32

3+
namespace Mouf\Database\SchemaAnalyzer;
44

55
class ShortestPathAmbiguityException extends SchemaAnalyzerException
66
{
7-
8-
}
7+
}

0 commit comments

Comments
 (0)