Passed
Push — master ( 5bea28...77cf0d )
by Malte
02:11
created

Task::getUsedTableNames()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 15
nc 4
nop 0
1
<?php
2
3
namespace AppBundle\ShowUnusedMySQLTables;
4
5
use Doctrine\DBAL\Connection;
6
use Doctrine\DBAL\Driver\Statement;
7
use Doctrine\DBAL\Schema\Table;
8
use Helper\NullStyle;
9
use PHPSQLParser\PHPSQLParser;
10
use Symfony\Component\Console\Style\StyleInterface;
11
12
/**
13
 * Get the names of unused MySQL tables.
14
 *
15
 * The idea is analogous to the code coverage. First, enable logging in MySQL, e.g. with
16
 *
17
 * SET global general_log = 1;
18
 * SET global log_output = 'table';
19
 *
20
 * You might want to delete old log data:
21
 *
22
 * TRUNCATE mysql.general_log;
23
 *
24
 * Then execute all use cases of your application, e.g. with behat tests. After that, you can disable MySQl logging with
25
 *
26
 * SET global general_log = 0;
27
 *
28
 * Then, parse the logged queries and extract the names of the queried tables. Finally, intersect this set with the set
29
 * of all table names (retrieved via the default Doctrine connection) and you have the names of the unused tables.
30
 */
31
final class Task
32
{
33
    /**
34
     * @var Connection
35
     */
36
    private $connection;
37
38
    /** @var string, e.g 'mysql.' for accessing mysql database tables from the connection  */
39
    private $dbSystemCatalogPrefix;
40
41
    /** @var StyleInterface */
42
    private $ioStyle;
43
44
    /**
45
     * @param Connection $connection
46
     * @param string $dbSystemCatalogPrefix
47
     */
48
    public function __construct(Connection $connection, $dbSystemCatalogPrefix)
49
    {
50
        $this->connection = $connection;
51
        $this->connection->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
52
        $this->dbSystemCatalogPrefix = $dbSystemCatalogPrefix;
53
    }
54
55
    /**
56
     * @param StyleInterface|null $ioStyle
57
     */
58
    public function getUnusedTableNames(StyleInterface $ioStyle = null)
59
    {
60
        $this->ioStyle = $ioStyle ?: new NullStyle();
61
        $this->ioStyle->text('Started.');
62
63
        $unusedTableNames = array_diff($this->getAllTablesNames(), $this->getUsedTableNames());
64
65
        $this->ioStyle->newLine();
66
        $this->ioStyle->text('Calculated ' . count($unusedTableNames) . ' potentially unused tables:');
67
        $this->ioStyle->listing($unusedTableNames);
68
        $this->ioStyle->success('Finished listing potentially unused tables.');
69
    }
70
71
    /**
72
     * @return string[]
73
     */
74
    private function getAllTablesNames()
75
    {
76
        $tables = $this->connection->getSchemaManager()->listTables();
77
        $tableNames = array_map(
78
            function (Table $table) {
79
                return $table->getName();
80
            },
81
            $tables
82
        );
83
84
        $this->ioStyle->text('Found ' . count($tableNames) . ' tables in the database "' . $this->connection->getDatabase() . '".');
85
86
        return $tableNames;
87
    }
88
89
    /**
90
     * @return string[]
91
     */
92
    private function getUsedTableNames()
93
    {
94
        $stmt = $this->getLoggedQueriesStatement();
95
        $numberOfLoggedQueries = $stmt->rowCount();
96
        $this->ioStyle->text('Analyzing ' . $numberOfLoggedQueries . ' unique logged queries (among all databases):');
97
98
        $this->ioStyle->progressStart($numberOfLoggedQueries);
99
100
        $usedTableNames = [];
101
        while ($loggedQuery = $stmt->fetch(\PDO::FETCH_COLUMN)) {
102
            $usedTableNames[] = $this->extractTableNamesFromLoggedQuery($loggedQuery);
103
            $this->ioStyle->progressAdvance();
104
        }
105
106
        if ($usedTableNames === []) {
107
            return $usedTableNames;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $usedTableNames returns an array which contains values of type string[] which are incompatible with the documented value type string.
Loading history...
108
        }
109
110
        $usedTableNames = call_user_func_array('array_merge', $usedTableNames);
111
        $usedTableNames = array_unique($usedTableNames);
112
113
        $this->ioStyle->newLine();
114
        $this->ioStyle->text('Found ' . count($usedTableNames) . ' used tables (among all databases).');
115
116
        return $usedTableNames;
117
    }
118
119
    /**
120
     * @return Statement
121
     */
122
    private function getLoggedQueriesStatement()
123
    {
124
        return $this->connection->createQueryBuilder()
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->connection...('argument')->execute() also could return the type integer which is incompatible with the documented return type Doctrine\DBAL\Driver\Statement.
Loading history...
125
                                ->select('argument')
126
                                ->from($this->dbSystemCatalogPrefix . 'general_log')
127
                                ->where("command_type = 'Query'")
128
                                ->groupBy('argument')
129
                                ->execute();
130
    }
131
132
    /**
133
     * @param string $loggedQuery
134
     * @return string[]
135
     */
136
    private function extractTableNamesFromLoggedQuery($loggedQuery)
137
    {
138
        $usedTableNames = [];
139
140
        $parser = new PHPSQLParser();
141
        $parsedQuery = $parser->parse($loggedQuery);
142
143
        if (!is_array($parsedQuery) || !array_key_exists('FROM', $parsedQuery)) {
144
            return [];
145
        }
146
147
        foreach ($parsedQuery['FROM'] as $fromDescription) {
148
            if ($fromDescription['expr_type'] === 'table') {
149
                $usedTableNames[] = $fromDescription['table'];
150
            }
151
        }
152
153
        return $usedTableNames;
154
    }
155
}
156