setConfiguration($configuration); // Path is required. if (empty($this->configuration['path'])) { throw new \InvalidArgumentException('You must declare the "path" to the source CSV file in your source settings.'); } // IDs are required. if (empty($this->configuration['ids']) || !is_array($this->configuration['ids'])) { throw new \InvalidArgumentException('You must declare "ids" as a unique array of fields in your source settings.'); } // IDs must be an array of strings. if ($this->configuration['ids'] !== array_unique(array_filter($this->configuration['ids'], 'is_string'))) { throw new \InvalidArgumentException('The ids must a flat array with unique string values.'); } // CSV character control characters must be exactly 1 character. foreach (['delimiter', 'enclosure', 'escape'] as $character) { if (1 !== strlen($this->configuration[$character])) { throw new \InvalidArgumentException(sprintf('%s must be a single character; %s given', $character, $this->configuration[$character])); } } // The configuration "header_offset" must be null or an integer. if (!(NULL === $this->configuration['header_offset'] || is_int($this->configuration['header_offset']))) { throw new \InvalidArgumentException('The configuration "header_offset" must be null or an integer.'); } // The configuration "header_offset" must be greater or equal to 0. if (NULL !== $this->configuration['header_offset'] && 0 > $this->configuration['header_offset']) { throw new \InvalidArgumentException('The configuration "header_offset" must be greater or equal to 0.'); } // If set, all fields must have a least a defined "name" property. if ($this->configuration['fields']) { foreach ($this->configuration['fields'] as $delta => $field) { if (!isset($field['name'])) { throw new \InvalidArgumentException(sprintf('The "name" configuration for "fields" in index position %s is not defined.', $delta)); } } } // If "create_record_number" is specified, "record_number_field" must be a // non-empty string. if ($this->configuration['create_record_number'] && (!is_scalar($this->configuration['record_number_field']) || (empty($this->configuration['record_number_field'])))) { throw new \InvalidArgumentException('The configuration "record_number_field" must be a non-empty string.'); } } /** * {@inheritdoc} */ public function defaultConfiguration() { return [ 'path' => '', 'ids' => [], 'header_offset' => 0, 'fields' => [], 'delimiter' => ",", 'enclosure' => "\"", 'escape' => "\\", 'create_record_number' => FALSE, 'record_number_field' => 'record_number', ]; } /** * {@inheritdoc} */ public function getConfiguration() { return $this->configuration; } /** * {@inheritdoc} */ public function setConfiguration(array $configuration) { // We must preserve integer keys for column_name mapping. $this->configuration = NestedArray::mergeDeepArray([$this->defaultConfiguration(), $configuration], TRUE); } /** * Return a string representing the source file path. * * @return string * The file path. */ public function __toString() { return $this->configuration['path']; } /** * {@inheritdoc} * * @throws \Drupal\migrate\MigrateException * @throws \League\Csv\Exception */ public function initializeIterator() { $header = $this->getReader()->getHeader(); if ($this->configuration['fields']) { // If there is no header record, we need to flip description and name so // the name becomes the header record. $header = array_flip($this->fields()); } return $this->getGenerator($this->getReader()->getRecords($header)); } /** * {@inheritdoc} */ public function getIds() { $ids = []; foreach ($this->configuration['ids'] as $value) { $ids[$value]['type'] = 'string'; } return $ids; } /** * {@inheritdoc} */ public function fields() { // If fields are not defined, use the header record. if (empty($this->configuration['fields'])) { $header = $this->getReader()->getHeader(); return array_combine($header, $header); } $fields = []; foreach ($this->configuration['fields'] as $field) { $fields[$field['name']] = isset($field['label']) ? $field['label'] : $field['name']; } return $fields; } /** * Get the generator. * * @param \Iterator $records * The CSV records. * * @codingStandardsIgnoreStart * * @return \Generator * The records generator. * * @codingStandardsIgnoreEnd */ protected function getGenerator(\Iterator $records) { $record_num = $this->configuration['header_offset'] ?? 0; foreach ($records as $record) { if ($this->configuration['create_record_number']) { $record[$this->configuration['record_number_field']] = ++$record_num; } yield $record; } } /** * Get the CSV reader. * * @return \League\Csv\Reader * The reader. * * @throws \Drupal\migrate\MigrateException * @throws \League\Csv\Exception */ protected function getReader() { $reader = $this->createReader(); $reader->setDelimiter($this->configuration['delimiter']); $reader->setEnclosure($this->configuration['enclosure']); $reader->setEscape($this->configuration['escape']); $reader->setHeaderOffset($this->configuration['header_offset']); return $reader; } /** * Construct a new CSV reader. * * @return \League\Csv\Reader * The reader. */ protected function createReader() { if (!file_exists($this->configuration['path'])) { throw new \RuntimeException(sprintf('File "%s" was not found.', $this->configuration['path'])); } $csv = fopen($this->configuration['path'], 'r'); if (!$csv) { throw new \RuntimeException(sprintf('File "%s" could not be opened.', $this->configuration['path'])); } return Reader::createFromStream($csv); } }