Skip to content

Commit 7fe1589

Browse files
committed
Initial commit
0 parents  commit 7fe1589

File tree

9 files changed

+411
-0
lines changed

9 files changed

+411
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
composer.lock
2+
/vendor/

.travis.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
language: php
2+
3+
php:
4+
- 5.5
5+
6+
before_script:
7+
- wget http://getcomposer.org/composer.phar
8+
- php composer.phar install --no-interaction
9+
10+
script:
11+
- mkdir -p build/logs
12+
- phpunit --coverage-clover build/logs/clover.xml
13+
14+
after_script:
15+
- php vendor/bin/coveralls -v

LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 container-interop
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of
6+
this software and associated documentation files (the "Software"), to deal in
7+
the Software without restriction, including without limitation the rights to
8+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9+
the Software, and to permit persons to whom the Software is furnished to do so,
10+
subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/thecodingmachine/schema-analyzer/badges/quality-score.png?b=1.0)](https://scrutinizer-ci.com/g/thecodingmachine/schema-analyzer/?branch=1.0)
2+
[![Build Status](https://travis-ci.org/thecodingmachine/schema-analyzer.svg?branch=1.0)](https://travis-ci.org/thecodingmachine/schema-analyzer)
3+
[![Coverage Status](https://coveralls.io/repos/thecodingmachine/schema-analyzer/badge.svg?branch=1.0&service=github)](https://coveralls.io/github/thecodingmachine/schema-analyzer?branch=1.0)
4+
5+
# Schema analyzer for DBAL
6+
7+
This package offer utility functions to analyze database schemas. It is built on top of Doctrine DBAL.
8+
9+
In this package, you will find:
10+
11+
- Functions to automatically detect **junction tables**
12+
- Functions to compute the shortest path between 2 tables based on the relationships stored in the schema.
13+
14+
## Installation
15+
16+
You can install this package through Composer:
17+
18+
```json
19+
{
20+
"require": {
21+
"mouf/schema-analyzer": "~1.0"
22+
}
23+
}
24+
```
25+
26+
The packages adheres to the [SemVer](http://semver.org/) specification, and there will be full backward compatibility
27+
between minor versions.
28+
29+
## Usage
30+
31+
Start by instanciating

composer.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "mouf/schema-analyzer",
3+
"type": "library",
4+
"description": "A package that offers utility tools to analyze database schemas (on top of Doctrine DBAL)",
5+
"license": "MIT",
6+
"autoload": {
7+
"psr-4": {
8+
"Mouf\\Database\\SchemaAnalyzer\\": "src/"
9+
}
10+
},
11+
"autoload-dev": {
12+
"psr-4": {
13+
"Mouf\\Database\\SchemaAnalyzer\\": "src/"
14+
}
15+
},
16+
"require": {
17+
"clue/graph": "~0.9.0",
18+
"doctrine/dbal": "~2.4",
19+
"graphp/algorithms": "~0.8.0"
20+
},
21+
"require-dev": {
22+
"phpunit/phpunit": "~4.0",
23+
"satooshi/php-coveralls": "dev-master"
24+
}
25+
}

phpunit.xml.dist

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<phpunit backupGlobals="false"
4+
backupStaticAttributes="false"
5+
colors="true"
6+
convertErrorsToExceptions="true"
7+
convertNoticesToExceptions="true"
8+
convertWarningsToExceptions="true"
9+
processIsolation="false"
10+
stopOnFailure="false"
11+
syntaxCheck="false"
12+
bootstrap="tests/bootstrap.php"
13+
>
14+
<testsuites>
15+
<testsuite name="Test Suite">
16+
<directory>./tests/</directory>
17+
</testsuite>
18+
</testsuites>
19+
<filter>
20+
<whitelist processUncoveredFilesFromWhitelist="true">
21+
<directory suffix=".php">src</directory>
22+
</whitelist>
23+
</filter>
24+
</phpunit>

src/SchemaAnalyzer.php

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
namespace Mouf\Database\SchemaAnalyzer;
3+
use Doctrine\DBAL\Schema\Schema;
4+
use Doctrine\DBAL\Schema\Table;
5+
use Fhaculty\Graph\Graph;
6+
7+
/**
8+
* This class can analyze a database model.
9+
* In this class you will find:
10+
*
11+
* - Functions to automatically detect **junction tables**
12+
* - Functions to compute the shortest path between 2 tables based on the relationships stored in the schema.
13+
*/
14+
class SchemaAnalyzer
15+
{
16+
private static $WEIGHT_FK = 1;
17+
private static $WEIGHT_JOINTURE_TABLE = 1.5;
18+
19+
/**
20+
* @var Schema
21+
*/
22+
private $schema;
23+
24+
/**
25+
* @param Schema $schema
26+
*/
27+
public function __construct(Schema $schema)
28+
{
29+
$this->schema = $schema;
30+
}
31+
32+
/**
33+
* Detect all junctions tables in the schema.
34+
* A table is a junction table if:
35+
* - it has exactly 2 foreign keys
36+
* - it has only 2 columns (or 3 columns if the third one is an autoincremented primary key)
37+
*
38+
*
39+
* @return Table[]
40+
*/
41+
public function detectJunctionTables() {
42+
return array_filter($this->schema->getTables(), [$this, "isJunctionTable"]);
43+
}
44+
45+
/**
46+
* Returns true if $table is a junction table.
47+
* I.e:
48+
* - 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)
50+
*
51+
* @param Table $table
52+
* @return bool
53+
*/
54+
private function isJunctionTable(Table $table) {
55+
$foreignKeys = $table->getForeignKeys();
56+
if (count($foreignKeys) != 2) {
57+
return false;
58+
}
59+
60+
$columns = $table->getColumns();
61+
if (count($columns) < 2 || count($columns) > 3) {
62+
return false;
63+
}
64+
65+
$pkColumns = $table->getPrimaryKeyColumns();
66+
67+
if (count($pkColumns) == 1 && count($columns) == 2) {
68+
return false;
69+
}
70+
71+
if (count($pkColumns) != 1 && count($columns) == 3) {
72+
return false;
73+
}
74+
75+
$fkColumnNames = [];
76+
foreach ($foreignKeys as $foreignKey) {
77+
$fkColumns = $foreignKey->getColumns();
78+
if (count($fkColumns) != 1) {
79+
return false;
80+
}
81+
$fkColumnNames[$fkColumns[0]] = true;
82+
}
83+
84+
// Let's check that the third column (the ID is NOT a foreign key)
85+
if (count($columns) == 3 && isset($fkColumnNames[$pkColumns[0]])) {
86+
return false;
87+
}
88+
89+
return true;
90+
}
91+
92+
/**
93+
* Get the shortest path between 2 tables.
94+
*
95+
* @param $fromTable
96+
* @param $toTable
97+
*/
98+
public function getShortestPath($fromTable, $toTable) {
99+
$graph = $this->buildSchemaGraph();
100+
// TODO
101+
}
102+
103+
public function buildSchemaGraph() {
104+
$graph = new Graph();
105+
106+
// First, let's create all the vertex
107+
foreach ($this->schema->getTables() as $table) {
108+
$graph->createVertex($table->getName());
109+
}
110+
111+
// Then, let's create all the edges
112+
foreach ($this->schema->getTables() as $table) {
113+
foreach ($table->getForeignKeys() as $fk) {
114+
// Create an undirected edge, with weight = 1
115+
$edge = $graph->getVertex($table->getName())->createEdge($graph->getVertex($fk->getForeignTableName()));
116+
$edge->setWeight(self::$WEIGHT_FK);
117+
$edge->getAttributeBag()->setAttribute("fk", $fk);
118+
}
119+
}
120+
121+
// Finally, let's add virtual edges for the junction tables
122+
foreach ($this->detectJunctionTables() as $junctionTable) {
123+
$tables = [];
124+
foreach ($junctionTable->getForeignKeys() as $fk) {
125+
$tables[] = $fk->getForeignTableName();
126+
}
127+
128+
$edge = $graph->getVertex($tables[0]->getName())->createEdge($graph->getVertex($tables[1]->getName()));
129+
$edge->setWeight(self::$WEIGHT_JOINTURE_TABLE);
130+
$edge->getAttributeBag()->setAttribute("junction", $junctionTable);
131+
}
132+
133+
return $graph;
134+
}
135+
}

0 commit comments

Comments
 (0)