Updated to Drupal 8.6.4, which is PHP 7.3 friendly. Also updated HTMLaw library....
[yaffs-website] / web / core / modules / migrate / src / Plugin / migrate / source / SqlBase.php
1 <?php
2
3 namespace Drupal\migrate\Plugin\migrate\source;
4
5 use Drupal\Core\Database\ConnectionNotDefinedException;
6 use Drupal\Core\Database\Database;
7 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
8 use Drupal\Core\State\StateInterface;
9 use Drupal\migrate\Exception\RequirementsException;
10 use Drupal\migrate\MigrateException;
11 use Drupal\migrate\Plugin\MigrationInterface;
12 use Drupal\migrate\Plugin\migrate\id_map\Sql;
13 use Drupal\migrate\Plugin\MigrateIdMapInterface;
14 use Drupal\migrate\Plugin\RequirementsInterface;
15 use Symfony\Component\DependencyInjection\ContainerInterface;
16
17 /**
18  * Sources whose data may be fetched via a database connection.
19  *
20  * Available configuration keys:
21  * - database_state_key: (optional) Name of the state key which contains an
22  *   array with database connection information.
23  * - key: (optional) The database key name. Defaults to 'migrate'.
24  * - target: (optional) The database target name. Defaults to 'default'.
25  * - batch_size: (optional) Number of records to fetch from the database during
26  *   each batch. If omitted, all records are fetched in a single query.
27  * - ignore_map: (optional) Source data is joined to the map table by default to
28  *   improve migration performance. If set to TRUE, the map table will not be
29  *   joined. Using expressions in the query may result in column aliases in the
30  *   JOIN clause which would be invalid SQL. If you run into this, set
31  *   ignore_map to TRUE.
32  *
33  * For other optional configuration keys inherited from the parent class, refer
34  * to \Drupal\migrate\Plugin\migrate\source\SourcePluginBase.
35  *
36  * About the source database determination:
37  * - If the source plugin configuration contains 'database_state_key', its value
38  *   is taken as the name of a state key which contains an array with the
39  *   database configuration.
40  * - Otherwise, if the source plugin configuration contains 'key', the database
41  *   configuration with that name is used.
42  * - If both 'database_state_key' and 'key' are omitted in the source plugin
43  *   configuration, the database connection named 'migrate' is used by default.
44  * - If all of the above steps fail, RequirementsException is thrown.
45  *
46  * Drupal Database API supports multiple database connections. The connection
47  * parameters are defined in $databases array in settings.php or
48  * settings.local.php. It is also possible to modify the $databases array in
49  * runtime. For example, Migrate Drupal, which provides the migrations from
50  * Drupal 6 / 7, asks for the source database connection parameters in the UI
51  * and then adds the $databases['migrate'] connection in runtime before the
52  * migrations are executed.
53  *
54  * As described above, the default source database is $databases['migrate']. If
55  * the source plugin needs another source connection, the database connection
56  * parameters should be added to the $databases array as, for instance,
57  * $databases['foo']. The source plugin can then use this connection by setting
58  * 'key' to 'foo' in its configuration.
59  *
60  * For a complete example on migrating data from an SQL source, refer to
61  * https://www.drupal.org/docs/8/api/migrate-api/migrating-data-from-sql-source
62  *
63  * @see https://www.drupal.org/docs/8/api/database-api
64  * @see \Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase
65  */
66 abstract class SqlBase extends SourcePluginBase implements ContainerFactoryPluginInterface, RequirementsInterface {
67
68   /**
69    * The query string.
70    *
71    * @var \Drupal\Core\Database\Query\SelectInterface
72    */
73   protected $query;
74
75   /**
76    * The database object.
77    *
78    * @var \Drupal\Core\Database\Connection
79    */
80   protected $database;
81
82   /**
83    * State service for retrieving database info.
84    *
85    * @var \Drupal\Core\State\StateInterface
86    */
87   protected $state;
88
89   /**
90    * The count of the number of batches run.
91    *
92    * @var int
93    */
94   protected $batch = 0;
95
96   /**
97    * Number of records to fetch from the database during each batch.
98    *
99    * A value of zero indicates no batching is to be done.
100    *
101    * @var int
102    */
103   protected $batchSize = 0;
104
105   /**
106    * {@inheritdoc}
107    */
108   public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state) {
109     parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
110     $this->state = $state;
111   }
112
113   /**
114    * {@inheritdoc}
115    */
116   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
117     return new static(
118       $configuration,
119       $plugin_id,
120       $plugin_definition,
121       $migration,
122       $container->get('state')
123     );
124   }
125
126   /**
127    * Prints the query string when the object is used as a string.
128    *
129    * @return string
130    *   The query string.
131    */
132   public function __toString() {
133     return (string) $this->query();
134   }
135
136   /**
137    * Gets the database connection object.
138    *
139    * @return \Drupal\Core\Database\Connection
140    *   The database connection.
141    */
142   public function getDatabase() {
143     if (!isset($this->database)) {
144       // Look first for an explicit state key containing the configuration.
145       if (isset($this->configuration['database_state_key'])) {
146         $this->database = $this->setUpDatabase($this->state->get($this->configuration['database_state_key']));
147       }
148       // Next, use explicit configuration in the source plugin.
149       elseif (isset($this->configuration['key'])) {
150         $this->database = $this->setUpDatabase($this->configuration);
151       }
152       // Next, try falling back to the global state key.
153       elseif (($fallback_state_key = $this->state->get('migrate.fallback_state_key'))) {
154         $this->database = $this->setUpDatabase($this->state->get($fallback_state_key));
155       }
156       // If all else fails, let setUpDatabase() fallback to the 'migrate' key.
157       else {
158         $this->database = $this->setUpDatabase([]);
159       }
160     }
161     return $this->database;
162   }
163
164   /**
165    * Gets a connection to the referenced database.
166    *
167    * This method will add the database connection if necessary.
168    *
169    * @param array $database_info
170    *   Configuration for the source database connection. The keys are:
171    *    'key' - The database connection key.
172    *    'target' - The database connection target.
173    *    'database' - Database configuration array as accepted by
174    *      Database::addConnectionInfo.
175    *
176    * @return \Drupal\Core\Database\Connection
177    *   The connection to use for this plugin's queries.
178    *
179    * @throws \Drupal\migrate\Exception\RequirementsException
180    *   Thrown if no source database connection is configured.
181    */
182   protected function setUpDatabase(array $database_info) {
183     if (isset($database_info['key'])) {
184       $key = $database_info['key'];
185     }
186     else {
187       // If there is no explicit database configuration at all, fall back to a
188       // connection named 'migrate'.
189       $key = 'migrate';
190     }
191     if (isset($database_info['target'])) {
192       $target = $database_info['target'];
193     }
194     else {
195       $target = 'default';
196     }
197     if (isset($database_info['database'])) {
198       Database::addConnectionInfo($key, $target, $database_info['database']);
199     }
200     try {
201       $connection = Database::getConnection($target, $key);
202     }
203     catch (ConnectionNotDefinedException $e) {
204       // If we fell back to the magic 'migrate' connection and it doesn't exist,
205       // treat the lack of the connection as a RequirementsException.
206       if ($key == 'migrate') {
207         throw new RequirementsException("No database connection configured for source plugin " . $this->pluginId, [], 0, $e);
208       }
209       else {
210         throw $e;
211       }
212     }
213     return $connection;
214   }
215
216   /**
217    * {@inheritdoc}
218    */
219   public function checkRequirements() {
220     if ($this->pluginDefinition['requirements_met'] === TRUE) {
221       $this->getDatabase();
222     }
223   }
224
225   /**
226    * Wrapper for database select.
227    */
228   protected function select($table, $alias = NULL, array $options = []) {
229     $options['fetch'] = \PDO::FETCH_ASSOC;
230     return $this->getDatabase()->select($table, $alias, $options);
231   }
232
233   /**
234    * Adds tags and metadata to the query.
235    *
236    * @return \Drupal\Core\Database\Query\SelectInterface
237    *   The query with additional tags and metadata.
238    */
239   protected function prepareQuery() {
240     $this->query = clone $this->query();
241     $this->query->addTag('migrate');
242     $this->query->addTag('migrate_' . $this->migration->id());
243     $this->query->addMetaData('migration', $this->migration);
244
245     return $this->query;
246   }
247
248   /**
249    * {@inheritdoc}
250    */
251   protected function initializeIterator() {
252     // Initialize the batch size.
253     if ($this->batchSize == 0 && isset($this->configuration['batch_size'])) {
254       // Valid batch sizes are integers >= 0.
255       if (is_int($this->configuration['batch_size']) && ($this->configuration['batch_size']) >= 0) {
256         $this->batchSize = $this->configuration['batch_size'];
257       }
258       else {
259         throw new MigrateException("batch_size must be greater than or equal to zero");
260       }
261     }
262
263     // If a batch has run the query is already setup.
264     if ($this->batch == 0) {
265       $this->prepareQuery();
266
267       // Get the key values, for potential use in joining to the map table.
268       $keys = [];
269
270       // The rules for determining what conditions to add to the query are as
271       // follows (applying first applicable rule):
272       // 1. If the map is joinable, join it. We will want to accept all rows
273       //    which are either not in the map, or marked in the map as NEEDS_UPDATE.
274       //    Note that if high water fields are in play, we want to accept all rows
275       //    above the high water mark in addition to those selected by the map
276       //    conditions, so we need to OR them together (but AND with any existing
277       //    conditions in the query). So, ultimately the SQL condition will look
278       //    like (original conditions) AND (map IS NULL OR map needs update
279       //      OR above high water).
280       $conditions = $this->query->orConditionGroup();
281       $condition_added = FALSE;
282       $added_fields = [];
283       if (empty($this->configuration['ignore_map']) && $this->mapJoinable()) {
284         // Build the join to the map table. Because the source key could have
285         // multiple fields, we need to build things up.
286         $count = 1;
287         $map_join = '';
288         $delimiter = '';
289         foreach ($this->getIds() as $field_name => $field_schema) {
290           if (isset($field_schema['alias'])) {
291             $field_name = $field_schema['alias'] . '.' . $this->query->escapeField($field_name);
292           }
293           $map_join .= "$delimiter$field_name = map.sourceid" . $count++;
294           $delimiter = ' AND ';
295         }
296
297         $alias = $this->query->leftJoin($this->migration->getIdMap()
298           ->getQualifiedMapTableName(), 'map', $map_join);
299         $conditions->isNull($alias . '.sourceid1');
300         $conditions->condition($alias . '.source_row_status', MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
301         $condition_added = TRUE;
302
303         // And as long as we have the map table, add its data to the row.
304         $n = count($this->getIds());
305         for ($count = 1; $count <= $n; $count++) {
306           $map_key = 'sourceid' . $count;
307           $this->query->addField($alias, $map_key, "migrate_map_$map_key");
308           $added_fields[] = "$alias.$map_key";
309         }
310         if ($n = count($this->migration->getDestinationIds())) {
311           for ($count = 1; $count <= $n; $count++) {
312             $map_key = 'destid' . $count++;
313             $this->query->addField($alias, $map_key, "migrate_map_$map_key");
314             $added_fields[] = "$alias.$map_key";
315           }
316         }
317         $this->query->addField($alias, 'source_row_status', 'migrate_map_source_row_status');
318         $added_fields[] = "$alias.source_row_status";
319       }
320       // 2. If we are using high water marks, also include rows above the mark.
321       //    But, include all rows if the high water mark is not set.
322       if ($this->getHighWaterProperty()) {
323         $high_water_field = $this->getHighWaterField();
324         $high_water = $this->getHighWater();
325         if ($high_water) {
326           $conditions->condition($high_water_field, $high_water, '>');
327           $condition_added = TRUE;
328         }
329         // Always sort by the high water field, to ensure that the first run
330         // (before we have a high water value) also has the results in a
331         // consistent order.
332         $this->query->orderBy($high_water_field);
333       }
334       if ($condition_added) {
335         $this->query->condition($conditions);
336       }
337       // If the query has a group by, our added fields need it too, to keep the
338       // query valid.
339       // @see https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html
340       $group_by = $this->query->getGroupBy();
341       if ($group_by && $added_fields) {
342         foreach ($added_fields as $added_field) {
343           $this->query->groupBy($added_field);
344         }
345       }
346     }
347
348     // Download data in batches for performance.
349     if (($this->batchSize > 0)) {
350       $this->query->range($this->batch * $this->batchSize, $this->batchSize);
351     }
352     $statement = $this->query->execute();
353     $statement->setFetchMode(\PDO::FETCH_ASSOC);
354     return new \IteratorIterator($statement);
355   }
356
357   /**
358    * Position the iterator to the following row.
359    */
360   protected function fetchNextRow() {
361     $this->getIterator()->next();
362     // We might be out of data entirely, or just out of data in the current
363     // batch. Attempt to fetch the next batch and see.
364     if ($this->batchSize > 0 && !$this->getIterator()->valid()) {
365       $this->fetchNextBatch();
366     }
367   }
368
369   /**
370    * Prepares query for the next set of data from the source database.
371    */
372   protected function fetchNextBatch() {
373     $this->batch++;
374     unset($this->iterator);
375     $this->getIterator()->rewind();
376   }
377
378   /**
379    * @return \Drupal\Core\Database\Query\SelectInterface
380    */
381   abstract public function query();
382
383   /**
384    * {@inheritdoc}
385    */
386   public function count($refresh = FALSE) {
387     return (int) $this->query()->countQuery()->execute()->fetchField();
388   }
389
390   /**
391    * Checks if we can join against the map table.
392    *
393    * This function specifically catches issues when we're migrating with
394    * unique sets of credentials for the source and destination database.
395    *
396    * @return bool
397    *   TRUE if we can join against the map table otherwise FALSE.
398    */
399   protected function mapJoinable() {
400     if (!$this->getIds()) {
401       return FALSE;
402     }
403     // With batching, we want a later batch to return the same rows that would
404     // have been returned at the same point within a monolithic query. If we
405     // join to the map table, the first batch is writing to the map table and
406     // thus affecting the results of subsequent batches. To be safe, we avoid
407     // joining to the map table when batching.
408     if ($this->batchSize > 0) {
409       return FALSE;
410     }
411     $id_map = $this->migration->getIdMap();
412     if (!$id_map instanceof Sql) {
413       return FALSE;
414     }
415     $id_map_database_options = $id_map->getDatabase()->getConnectionOptions();
416     $source_database_options = $this->getDatabase()->getConnectionOptions();
417
418     // Special handling for sqlite which deals with files.
419     if ($id_map_database_options['driver'] === 'sqlite' &&
420       $source_database_options['driver'] === 'sqlite' &&
421       $id_map_database_options['database'] != $source_database_options['database']
422     ) {
423       return FALSE;
424     }
425
426     // FALSE if driver is PostgreSQL and database doesn't match.
427     if ($id_map_database_options['driver'] === 'pgsql' &&
428       $source_database_options['driver'] === 'pgsql' &&
429       $id_map_database_options['database'] != $source_database_options['database']
430       ) {
431       return FALSE;
432     }
433
434     foreach (['username', 'password', 'host', 'port', 'namespace', 'driver'] as $key) {
435       if (isset($source_database_options[$key])) {
436         if ($id_map_database_options[$key] != $source_database_options[$key]) {
437           return FALSE;
438         }
439       }
440     }
441     return TRUE;
442   }
443
444 }