Completed
Push — master ( b393b8...22cf61 )
by brian
01:31
created

Replace::replaceDatabase()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 0
cts 22
cp 0
rs 9.536
c 0
b 0
f 0
cc 3
nc 3
nop 5
crap 12
1
<?php declare(strict_types=1);
2
3
/**
4
 * @copyright   (c) 2017-present brian ridley
5
 * @author      brian ridley <[email protected]>
6
 * @license     http://opensource.org/licenses/MIT MIT
7
 */
8
9
namespace ptlis\GrepDb\Replace;
10
11
use Doctrine\DBAL\Connection;
12
use ptlis\GrepDb\Metadata\MySQL\ColumnMetadata;
13
use ptlis\GrepDb\Metadata\MySQL\MetadataFactory;
14
use ptlis\GrepDb\Replace\Result\FieldReplaceResult;
15
use ptlis\GrepDb\Replace\Result\RowReplaceResult;
16
use ptlis\GrepDb\Replace\Strategy\FieldReplaceStrategy;
17
use ptlis\GrepDb\Replace\Strategy\SerializedFieldReplaceStrategy;
18
use ptlis\GrepDb\Replace\Strategy\StringFieldReplaceStrategy;
19
use ptlis\GrepDb\Search\Result\RowSearchResult;
20
use ptlis\GrepDb\Search\Search;
21
22
/**
23
 * Performs search & replacement.
24
 */
25
final class Replace
26
{
27
    /** @var FieldReplaceStrategy[] */
28
    private $replacementStrategyList;
29
30
31
    /**
32
     * @param FieldReplaceStrategy[] $replacementStrategyList
33
     */
34
    public function __construct(
35
        array $replacementStrategyList = []
36
    ) {
37
        if (!count($replacementStrategyList)) {
38
            $replacementStrategyList = [
39
                new StringFieldReplaceStrategy(),
40
                new SerializedFieldReplaceStrategy()
41
            ];
42
        }
43
        $this->replacementStrategyList = $replacementStrategyList;
44
    }
45
46
    /**
47
     * Performs a search on all tables in the the provided database, batching queries to the specified batch size.
48
     *
49
     * @return \Generator|RowReplaceResult[]
50
     */
51
    public function replaceDatabase(
52
        Connection $connection,
53
        string $databaseName,
54
        string $searchTerm,
55
        string $replaceTerm,
56
        int $batchSize = 100
57
    ): \Generator {
58
        $databaseMetadata = (new MetadataFactory())->getDatabaseMetadata($connection, $databaseName);
59
60
        foreach ($databaseMetadata->getAllTableMetadata() as $tableMetadata) {
61
            $rowResultList = $this->replaceTable(
62
                $connection,
63
                $databaseName,
64
                $tableMetadata->getTableName(),
65
                $searchTerm,
66
                $replaceTerm,
67
                $batchSize
68
            );
69
70
            foreach ($rowResultList as $rowResult) {
71
                yield $rowResult;
72
            }
73
        }
74
    }
75
76
    /**
77
     * Performs a search on the provided table, batching queries to the specified batch size.
78
     *
79
     * @return \Generator|RowReplaceResult[]
80
     */
81
    public function replaceTable(
82
        Connection $connection,
83
        string $databaseName,
84
        string $tableName,
85
        string $searchTerm,
86
        string $replaceTerm,
87
        int $batchSize = 100
88
    ): \Generator {
89
        $connection->query('START TRANSACTION');
90
91
        $rowCount = 0;
92
93
        $rowSearchResultList = (new Search())->searchTable($connection, $databaseName, $tableName, $searchTerm);
94
        foreach ($rowSearchResultList as $rowSearchResult) {
95
            $rowCount++;
96
97
            $rowReplaceResult = $this->replaceFields($rowSearchResult, $searchTerm, $replaceTerm);
98
99
            $queryBuilder = $connection->createQueryBuilder();
100
101
            // Build key => value mapping of replacement data
102
            $replacementData = [];
103
            foreach ($rowReplaceResult->getFieldResultList() as $fieldReplacementResult) {
104
                $replacementData[$fieldReplacementResult->getColumnMetadata()->getColumnName()] = $fieldReplacementResult->getNewValue();
105
            }
106
107
            $queryBuilder
108
                ->update(
109
                    '`' . $rowSearchResult->getTableMetadata()->getDatabaseName() . '`.`' . $rowSearchResult->getTableMetadata()->getTableName() . '`', 'subject'
110
                )
111
                ->setParameters($replacementData)
112
                ->setParameter('key', $rowSearchResult->getPrimaryKeyValue());
113
114
            foreach (array_keys($replacementData) as $columnName) {
115
                $queryBuilder->set('subject.' . $columnName, ':' . $columnName);
116
            }
117
118
            // Update using primary key
119
            if (null !== $rowSearchResult->getPrimaryKeyColumn()) {
120
                $queryBuilder->where('subject.' . $rowSearchResult->getPrimaryKeyColumn()->getColumnName() . ' = :key');
121
122
            // Update using original values
123
            } else {
124
                $whereCount = 0;
125
                foreach ($rowReplaceResult->getFieldResultList() as $fieldReplaceResult) {
126
                    $columnName = $fieldReplaceResult->getColumnMetadata()->getColumnName();
127
                    $whereClause = 'subject.' . $columnName . ' = :old_' . $columnName;
128
129
                    if (0 === $whereCount) {
130
                        $queryBuilder->where($whereClause);
131
                    } else {
132
                        $queryBuilder->andWhere($whereClause);
133
                    }
134
135
                    $queryBuilder->setParameter('old_' . $columnName, $fieldReplaceResult->getOldValue());
136
                }
137
            }
138
139
            // TODO: handle exception
140
            $queryBuilder->execute();
141
142
            if (0 === ($rowCount % $batchSize)) {
143
                $connection->query('COMMIT');
144
                $connection->query('START TRANSACTION');
145
            }
146
147
            yield $rowReplaceResult;
148
        }
149
150
        if (0 !== ($rowCount % $batchSize)) {
151
            $connection->query('COMMIT');
152
        }
153
    }
154
155
    private function replaceFields(
156
        RowSearchResult $rowSearchResult,
157
        string $searchTerm,
158
        string $replaceTerm
159
    ): RowReplaceResult {
160
        $fieldReplaceResultList = [];
161
        $errorList = [];
162
        foreach ($rowSearchResult->getMatchingFields() as $fieldSearchResult) {
163
            $fieldReplaceResult = $this->replace($fieldSearchResult->getMetadata(), $searchTerm, $replaceTerm, $fieldSearchResult->getValue());
164
165
            // Avoid truncation if the replacement string is longer than the source
166
            if (strlen($fieldReplaceResult->getNewValue()) > $fieldSearchResult->getMetadata()->getMaxLength()) {
167
                // Tailor error message depending on whether or not there is a primary key
168
                if (null !== $rowSearchResult->getTableMetadata()->getPrimaryKeyMetadata()) {
169
                    $errorList[] = 'Length of new value (' . strlen($fieldReplaceResult->getNewValue()) . ') exceeds max length (' . $fieldSearchResult->getMetadata()->getMaxLength() . ') for column "' . $fieldSearchResult->getMetadata()->getTableName() . '.' . $fieldSearchResult->getMetadata()->getColumnName() . '" with primary key "' . $rowSearchResult->getPrimaryKeyValue() . '"';
170
                } else {
171
                    $errorList[] = 'Length of new value (' . strlen($fieldReplaceResult->getNewValue()) . ') exceeds max length (' . $fieldSearchResult->getMetadata()->getMaxLength() . ') for column "' . $fieldSearchResult->getMetadata()->getTableName() . '.' . $fieldSearchResult->getMetadata()->getColumnName() . '" of table "' . $fieldSearchResult->getMetadata()->getColumnName() . '", original value was "' . $fieldSearchResult->getValue() . '"';
172
                }
173
            }
174
175
            $fieldReplaceResultList[] = $fieldReplaceResult;
176
        }
177
178
        return new RowReplaceResult($rowSearchResult, $fieldReplaceResultList, $errorList);
179
    }
180
181
    /**
182
     * Perform the string replacement on the field.
183
     *
184
     * @throws \RuntimeException If the replacement fails.
185
     */
186
    private function replace(
187
        ColumnMetadata $columnMetadata,
188
        string $searchTerm,
189
        string $replaceTerm,
190
        string $subject
191
    ): FieldReplaceResult {
192
        $fieldReplaced = null;
193
        foreach ($this->replacementStrategyList as $replacementStrategy) {
194
            if ($replacementStrategy->canReplace($searchTerm, $subject)) {
195
                $fieldReplaced = $replacementStrategy->replace(
196
                    $columnMetadata,
197
                    $searchTerm,
198
                    $replaceTerm,
199
                    $subject
200
                );
201
                break;
202
            }
203
        }
204
205
        if (is_null($fieldReplaced)) {
206
            throw new \RuntimeException('Error trying to replace "' . $searchTerm . '" with "' . $replaceTerm . '"');
207
        }
208
209
        return $fieldReplaced;
210
    }
211
}