Passed
Push — master ( c950cb...85bd60 )
by Malte
03:07
created

Task::extractTableNamesFromLoggedQuery()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 18
rs 9.2
cc 4
eloc 9
nc 4
nop 1
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 StyleInterface */
39
    private $ioStyle;
40
41
    /**
42
     * @param Connection $connection
43
     */
44
    public function __construct(Connection $connection)
45
    {
46
        $this->connection = $connection;
47
        $this->connection->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
48
    }
49
50
    /**
51
     * @param StyleInterface|null $ioStyle
52
     */
53
    public function getUnusedTableNames(StyleInterface $ioStyle = null)
54
    {
55
        $this->ioStyle = $ioStyle ?: new NullStyle();
56
        $this->ioStyle->text('Started.');
57
58
        $unusedTableNames = array_diff($this->getAllTablesNames(), $this->getUsedTableNames());
59
60
        $this->ioStyle->newLine();
61
        $this->ioStyle->text('Calculated ' . count($unusedTableNames) . ' potentially unused tables:');
62
        $this->ioStyle->listing($unusedTableNames);
63
        $this->ioStyle->success('Finished listing potentially unused tables.');
64
    }
65
66
    /**
67
     * @return string[]
68
     */
69
    private function getAllTablesNames()
70
    {
71
        $tables = $this->connection->getSchemaManager()->listTables();
72
        $tableNames = array_map(
73
            function (Table $table) {
74
                return $table->getName();
75
            },
76
            $tables
77
        );
78
79
        $this->ioStyle->text('Found ' . count($tableNames) . ' tables in the database "' . $this->connection->getDatabase() . '".');
80
81
        return $tableNames;
82
    }
83
84
    /**
85
     * @return string[]
86
     */
87
    private function getUsedTableNames()
88
    {
89
        $stmt = $this->getLoggedQueriesStatement();
90
        $numberOfLoggedQueries = $stmt->rowCount();
91
        $this->ioStyle->text('Analyzing ' . $numberOfLoggedQueries . ' logged queries (among all databases):');
92
93
        $this->ioStyle->progressStart($numberOfLoggedQueries);
94
95
        $usedTableNames = [];
96
        while ($loggedQuery = $stmt->fetch(\PDO::FETCH_COLUMN)) {
97
            $usedTableNames = array_merge($usedTableNames, $this->extractTableNamesFromLoggedQuery($loggedQuery));
98
            $this->ioStyle->progressAdvance();
99
        }
100
101
        $usedTableNames = array_unique($usedTableNames);
102
103
        $this->ioStyle->newLine();
104
        $this->ioStyle->text('Found ' . count($usedTableNames) . ' used tables (among all databases).');
105
106
        return $usedTableNames;
107
    }
108
109
    /**
110
     * @return Statement
111
     */
112
    private function getLoggedQueriesStatement()
113
    {
114
        return $this->connection->createQueryBuilder()
115
                                ->select('argument')
116
                                ->from('mysql.general_log')
117
                                ->where("command_type = 'Query'")
118
                                ->execute();
119
    }
120
121
    /**
122
     * @param string $loggedQuery
123
     * @return string[]
124
     */
125
    private function extractTableNamesFromLoggedQuery($loggedQuery)
126
    {
127
        $usedTableNames = [];
128
129
        $parser = new PHPSQLParser();
130
        $parsedQuery = $parser->parse($loggedQuery);
131
132
        if (!array_key_exists('FROM', $parsedQuery)) {
133
            return [];
134
        }
135
136
        foreach ($parsedQuery['FROM'] as $fromDescription) {
137
            if ($fromDescription['expr_type'] === 'table') {
138
                $usedTableNames[] = $fromDescription['table'];
139
            }
140
        }
141
142
        return $usedTableNames;
143
    }
144
}
145