Skip to content

Commit a7777b7

Browse files
author
Francois Suter
committed
[FEATURE] Migration tool for old references
Add a migration tool to change 3-part links to 4-part links. See README file for usage.
1 parent f22d6eb commit a7777b7

File tree

6 files changed

+431
-2
lines changed

6 files changed

+431
-2
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
<?php
2+
namespace Cobweb\Linkhandler\Command;
3+
4+
/*
5+
* This file is part of the TYPO3 CMS project.
6+
*
7+
* It is free software; you can redistribute it and/or modify it under
8+
* the terms of the GNU General Public License, either version 2
9+
* of the License, or any later version.
10+
*
11+
* For the full copyright and license information, please read the
12+
* LICENSE.txt file that was distributed with this source code.
13+
*
14+
* The TYPO3 project - inspiring people to share!
15+
*/
16+
17+
use Cobweb\Linkhandler\Domain\Repository\GenericRepository;
18+
use Cobweb\Linkhandler\Exception\FailedQueryException;
19+
use TYPO3\CMS\Core\Utility\GeneralUtility;
20+
use TYPO3\CMS\Extbase\Mvc\Cli\ConsoleOutput;
21+
use TYPO3\CMS\Extbase\Mvc\Controller\CommandController;
22+
23+
/**
24+
* Command-line controller for migrating old record links (with a syntax like record:table:id)
25+
* to the new syntax (record:key:table:id).
26+
*
27+
* @package TYPO3\CMS\Extbase\Command
28+
*/
29+
class LinkMigrationCommandController extends CommandController {
30+
/**
31+
* Default list of fields where to search for record references to migrate
32+
*/
33+
const DEFAULT_FIELDS = 'tt_content.header_link, tt_content.bodytext, sys_file_reference.link';
34+
35+
/**
36+
* @var array List of fields to handle, grouped by table
37+
*/
38+
protected $tablesAndFields = array();
39+
40+
/**
41+
* @var array List of tables being linked to in record links
42+
*/
43+
protected $tablesForMigration = array();
44+
45+
/**
46+
* @var array Structured list of records that contain data to migrate
47+
*/
48+
protected $recordsForMigration = array();
49+
50+
/**
51+
* @var GenericRepository
52+
*/
53+
protected $genericRepository;
54+
55+
/**
56+
* @var ConsoleOutput
57+
*/
58+
protected $console;
59+
60+
/**
61+
* @param GenericRepository $repository
62+
* @return void
63+
*/
64+
public function injectGenericRepository(GenericRepository $repository)
65+
{
66+
$this->genericRepository = $repository;
67+
}
68+
69+
/**
70+
* @param ConsoleOutput $consoleOutput
71+
* @return void
72+
*/
73+
public function injectConsole(ConsoleOutput $consoleOutput)
74+
{
75+
$this->console = $consoleOutput;
76+
}
77+
78+
/**
79+
* Migrates old-style records links (syntax: "record:table:id") to new-style record links (syntax: "record:key:table:id").
80+
*
81+
* @param string $fields Name of the field to migrate (syntax is "table.field"; use comma-separated values for several fields). Ignore to migrate default fields (tt_content.header_link, tt_content.bodytext, sys_file_reference.link)
82+
*/
83+
public function migrateCommand($fields = '')
84+
{
85+
// Set default value if argument is empty
86+
if ($fields === '') {
87+
$fields = self::DEFAULT_FIELDS;
88+
}
89+
try {
90+
$this->setFields($fields);
91+
// Loop on all tables and fields
92+
foreach ($this->tablesAndFields as $table => $listOfFields) {
93+
$this->gatherRecordsToMigrate(
94+
$table,
95+
$listOfFields
96+
);
97+
}
98+
// Ask the user for configuration key for each table
99+
$this->getConfigurationKeys();
100+
// Replace in fields and save modified data
101+
$this->migrateRecords();
102+
}
103+
catch (\InvalidArgumentException $e) {
104+
$this->outputLine(
105+
$e->getMessage() . ' (' . $e->getCode() . ')'
106+
);
107+
$this->quit(1);
108+
}
109+
}
110+
111+
/**
112+
* Sets the internal list of fields to handle.
113+
*
114+
* @param string $fields Comma-separated list of fields (syntax table.field)
115+
* @throws \InvalidArgumentException
116+
* @return void
117+
*/
118+
protected function setFields($fields)
119+
{
120+
$listOfFields = GeneralUtility::trimExplode(',', $fields, true);
121+
foreach ($listOfFields as $aField) {
122+
list($table, $field) = explode('.', $aField);
123+
if (empty($table) || empty($field)) {
124+
throw new \InvalidArgumentException(
125+
sprintf(
126+
'Invalid argument "%s". Use "table.field" syntax',
127+
$aField
128+
),
129+
1457434202
130+
);
131+
} else {
132+
if (!array_key_exists($table, $this->tablesAndFields)) {
133+
$this->tablesAndFields[$table] = array();
134+
}
135+
$this->tablesAndFields[$table][] = $field;
136+
}
137+
}
138+
}
139+
140+
/**
141+
* Gathers all records that contain data to migrate.
142+
*
143+
* Also extracts a list of all the different tables being linked to.
144+
* This is used later to ask the user about a configuration key for each table.
145+
*
146+
* @param string $table Name of the table to check
147+
* @param array $listOfFields List of fields to check
148+
* @return void
149+
*/
150+
protected function gatherRecordsToMigrate($table, $listOfFields)
151+
{
152+
try {
153+
$records = $this->genericRepository->findByRecordLink(
154+
$table,
155+
$listOfFields
156+
);
157+
foreach ($records as $record) {
158+
$id = (int)$record['uid'];
159+
foreach ($listOfFields as $field) {
160+
$matches = array();
161+
// Find all element that have a syntax like "record:string:string(:string)"
162+
// The last string is optional. If it exists, it is already a 4-part record reference,
163+
// i.e. a reference using the new syntax and which does not need to be migrated.
164+
preg_match_all(
165+
'/record:(\w+):(\w+)(:\w+)?/',
166+
$record[$field],
167+
$matches
168+
);
169+
foreach ($matches as $index => $match) {
170+
// Consider only matches that have 3 parts (i.e. 4th part is empty)
171+
// NOTE: although not captured, the first part is "record:"
172+
if ($matches[3][$index] === '') {
173+
$linkedTable = $matches[1][$index];
174+
// First, add the table to the list of table that are targeted by record links
175+
if (!array_key_exists($linkedTable, $this->tablesForMigration)) {
176+
$this->tablesForMigration[$linkedTable] = '';
177+
}
178+
// Next keep track of the record that needs migration
179+
// Data is stored per table, per record, per field and per record link to migrate
180+
if (!array_key_exists($table, $this->recordsForMigration)) {
181+
$this->recordsForMigration[$table] = array();
182+
}
183+
if (!array_key_exists($id, $this->recordsForMigration[$table])) {
184+
$this->recordsForMigration[$table][$id] = array();
185+
}
186+
if (!array_key_exists($field, $this->recordsForMigration[$table][$id])) {
187+
$this->recordsForMigration[$table][$id][$field] = array(
188+
'content' => $record[$field],
189+
'matches' => array()
190+
);
191+
}
192+
$this->recordsForMigration[$table][$id][$field]['matches'][] = $matches[0][$index];
193+
}
194+
}
195+
}
196+
}
197+
}
198+
catch (FailedQueryException $e) {
199+
$this->outputLine(
200+
sprintf(
201+
'Table "%s" skipped. An error occurred: %s',
202+
$table,
203+
$e->getMessage()
204+
)
205+
);
206+
}
207+
}
208+
209+
/**
210+
* Asks the user to give a configuration key for each table that is being linked to.
211+
*
212+
* @return void
213+
*/
214+
protected function getConfigurationKeys() {
215+
foreach ($this->tablesForMigration as $table => &$dummy) {
216+
$key = null;
217+
do {
218+
try {
219+
$key = $this->console->ask(
220+
sprintf(
221+
'Please enter the configuration key to use for table "%s": ',
222+
$table
223+
)
224+
);
225+
}
226+
catch (\Exception $e) {
227+
// Do nothing, just let it try again
228+
}
229+
} while ($key === null);
230+
$dummy = $key;
231+
}
232+
}
233+
234+
/**
235+
* Updates all fields that needed some migration and saves the modified data.
236+
*
237+
* @return void
238+
*/
239+
protected function migrateRecords()
240+
{
241+
foreach ($this->recordsForMigration as $table => $records) {
242+
$recordsForTable = array();
243+
foreach ($records as $id => $fields) {
244+
foreach ($fields as $field => $fieldInformation) {
245+
$updatedField = $fieldInformation['content'];
246+
foreach ($fieldInformation['matches'] as $link) {
247+
$linkParts = explode(':', $link);
248+
$newLink = 'record:' . $this->tablesForMigration[$linkParts[1]] . ':' . $linkParts[1] . ':' . $linkParts[2];
249+
$updatedField = str_replace(
250+
$link,
251+
$newLink,
252+
$updatedField
253+
);
254+
}
255+
if (!array_key_exists($id, $recordsForTable)) {
256+
$recordsForTable[$id] = array();
257+
}
258+
$recordsForTable[$id][$field] = $updatedField;
259+
}
260+
}
261+
$result = $this->genericRepository->massUpdate(
262+
$table,
263+
$recordsForTable
264+
);
265+
if (!$result) {
266+
$this->outputLine(
267+
'Some database updates failed for table "%s"',
268+
$table
269+
);
270+
}
271+
}
272+
}
273+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
namespace Cobweb\Linkhandler\Domain\Repository;
3+
4+
/*
5+
* This file is part of the TYPO3 CMS project.
6+
*
7+
* It is free software; you can redistribute it and/or modify it under
8+
* the terms of the GNU General Public License, either version 2
9+
* of the License, or any later version.
10+
*
11+
* For the full copyright and license information, please read the
12+
* LICENSE.txt file that was distributed with this source code.
13+
*
14+
* The TYPO3 project - inspiring people to share!
15+
*/
16+
17+
use Cobweb\Linkhandler\Exception\FailedQueryException;
18+
use TYPO3\CMS\Backend\Utility\BackendUtility;
19+
use TYPO3\CMS\Core\Database\DatabaseConnection;
20+
21+
/**
22+
* Non-Extbase repository for accessing any table contain record links.
23+
*
24+
* @package Cobweb\Linkhandler\Domain\Repository
25+
*/
26+
class GenericRepository {
27+
/**
28+
* Fetches a list of records from given table that contain links to records.
29+
*
30+
* @param string $table Name of the table to query
31+
* @param array $listOfFields List of fields to get in the query
32+
* @throws FailedQueryException
33+
* @return array
34+
*/
35+
public function findByRecordLink($table, $listOfFields)
36+
{
37+
$records = array();
38+
// Add a simple condition for detecting record references to avoid getting all rows from the table
39+
// NOTE: this may catch false positives, but this is okay at that point
40+
$conditions = array();
41+
foreach ($listOfFields as $field) {
42+
$conditions[] = $field . ' LIKE \'%record:%\'';
43+
}
44+
$where = implode(' OR ', $conditions) . BackendUtility::deleteClause($table);
45+
try {
46+
$fields = implode(', ', $listOfFields);
47+
$result = $this->getDatabaseConnection()->exec_SELECTgetRows(
48+
'uid, ' . $fields,
49+
$table,
50+
$where
51+
);
52+
if ($result === null) {
53+
throw new FailedQueryException(
54+
sprintf(
55+
'A SQL error occurred querying table "%s" with fields "%s": %s',
56+
$table,
57+
$fields,
58+
$this->getDatabaseConnection()->sql_error()
59+
),
60+
1457441163
61+
);
62+
} else {
63+
$records = $result;
64+
}
65+
}
66+
catch (\InvalidArgumentException $e) {
67+
// Nothing to do here
68+
}
69+
return $records;
70+
}
71+
72+
/**
73+
* Saves a bunch of records to the database.
74+
*
75+
* @param string $table Name of the table to update
76+
* @param array $records List of records to update
77+
* @return bool
78+
*/
79+
public function massUpdate($table, $records)
80+
{
81+
$globalResult = true;
82+
// @todo: this could be improved to provide better reporting on errors (and maybe use transactions to roll everything back)
83+
foreach ($records as $id => $fields) {
84+
$result = $this->getDatabaseConnection()->exec_UPDATEquery(
85+
$table,
86+
'uid = ' . (int)$id,
87+
$fields
88+
);
89+
$globalResult &= $result;
90+
}
91+
return $globalResult;
92+
}
93+
94+
/**
95+
* @return DatabaseConnection
96+
*/
97+
protected function getDatabaseConnection()
98+
{
99+
return $GLOBALS['TYPO3_DB'];
100+
}
101+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
namespace Cobweb\Linkhandler\Exception;
3+
4+
/*
5+
* This file is part of the TYPO3 CMS project.
6+
*
7+
* It is free software; you can redistribute it and/or modify it under
8+
* the terms of the GNU General Public License, either version 2
9+
* of the License, or any later version.
10+
*
11+
* For the full copyright and license information, please read the
12+
* LICENSE.txt file that was distributed with this source code.
13+
*
14+
* The TYPO3 project - inspiring people to share!
15+
*/
16+
17+
class FailedQueryException extends \Exception
18+
{
19+
20+
}

0 commit comments

Comments
 (0)