*/
abstract class CRM_Import_DataSource {
+ /**
+ * @var \CRM_Core_DAO
+ */
+ private $queryResultObject;
+
+ /**
+ * @var int
+ */
+ private $limit;
+
+ /**
+ * @param int $limit
+ *
+ * @return CRM_Import_DataSource
+ */
+ public function setLimit(int $limit): CRM_Import_DataSource {
+ $this->limit = $limit;
+ $this->queryResultObject = NULL;
+ return $this;
+ }
+
+ /**
+ * @param int $offset
+ *
+ * @return CRM_Import_DataSource
+ */
+ public function setOffset(int $offset): CRM_Import_DataSource {
+ $this->offset = $offset;
+ $this->queryResultObject = NULL;
+ return $this;
+ }
+
+ /**
+ * @var int
+ */
+ private $offset;
+
+ /**
+ * Statuses of rows to fetch.
+ *
+ * @var array
+ */
+ private $statuses = [];
+
+ /**
+ * Current row.
+ *
+ * @var array
+ */
+ private $row;
+
+ /**
+ * @param array $statuses
+ *
+ * @return self
+ */
+ public function setStatuses(array $statuses): self {
+ $this->statuses = $statuses;
+ $this->queryResultObject = NULL;
+ return $this;
+ }
+
/**
* Class constructor.
*
*
* The array has all values.
*
- * @param int $limit
- * @param int $offset
+ * @param bool $nonAssociative
+ * Return as a non-associative array?
*
* @return array
*
* @throws \API_Exception
* @throws \CRM_Core_Exception
*/
- public function getRows(int $limit = 0, int $offset = 0) {
- $query = 'SELECT * FROM ' . $this->getTableName();
- if ($limit) {
- $query .= ' LIMIT ' . $limit . ($offset ? (' OFFSET ' . $offset) : NULL);
- }
+ public function getRows(bool $nonAssociative = TRUE): array {
$rows = [];
- $result = CRM_Core_DAO::executeQuery($query);
- while ($result->fetch()) {
- $values = $result->toArray();
- /* trim whitespace around the values */
- foreach ($values as $k => $v) {
- $values[$k] = trim($v, " \t\r\n");
- }
+ while ($this->getRow()) {
// Historically we expect a non-associative array...
- $rows[] = array_values($values);
+ $rows[] = $nonAssociative ? array_values($this->row) : $this->row;
}
+ $this->queryResultObject = NULL;
return $rows;
}
+ /**
+ * Get the next row.
+ *
+ * @return array|null
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
+ */
+ public function getRow(): ?array {
+ if (!$this->queryResultObject) {
+ $this->instantiateQueryObject();
+ }
+ if (!$this->queryResultObject->fetch()) {
+ return NULL;
+ }
+ $values = $this->queryResultObject->toArray();
+ /* trim whitespace around the values */
+ foreach ($values as $k => $v) {
+ $values[$k] = trim($v, " \t\r\n");
+ }
+ $this->row = $values;
+ return $values;
+ }
+
+ /**
+ * Get row count.
+ *
+ * The array has all values.
+ *
+ * @return int
+ *
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
+ */
+ public function getRowCount(array $statuses = []): int {
+ $this->statuses = $statuses;
+ $query = 'SELECT count(*) FROM ' . $this->getTableName() . ' ' . $this->getStatusClause();
+ return CRM_Core_DAO::singleValueQuery($query);
+ }
+
/**
* Get an array of column headers, if any.
*
*/
abstract public function getInfo();
- /**
- * Set variables up before form is built.
- *
- * @param CRM_Core_Form $form
- */
- abstract public function preProcess(&$form);
-
/**
* This is function is called by the form object to get the DataSource's form snippet.
*
abstract public function buildQuickForm(&$form);
/**
- * Process the form submission.
+ * Initialize the datasource, based on the submitted values stored in the user job.
*
- * @param array $params
- * @param string $db
- * @param CRM_Core_Form $form
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
*/
- abstract public function postProcess(&$params, &$db, &$form);
+ public function initialize(): void {
+
+ }
/**
* Determine if the current user has access to this data source.
$this->userJob['metadata'] = $metaData;
}
+ /**
+ * Purge any datasource related assets when the datasource is dropped.
+ *
+ * This is the datasource's chance to delete any tables etc that it created
+ * which will now not be used.
+ *
+ * @param array $newParams
+ * If the dataSource is being updated to another variant of the same
+ * class (eg. the csv upload was set to no column headers and they
+ * have resubmitted WITH skipColumnHeader (first row is a header) then
+ * the dataSource is still CSV and the params for the new intance
+ * are passed in. When changing from csv to SQL (for example) newParams is
+ * empty.
+ *
+ * @return array
+ * The details to update the DataSource key in the userJob metadata to.
+ * Generally and empty array but it the datasource decided (for example)
+ * that the table it created earlier is still consistent with the new params
+ * then it might decided not to drop the table and would want to retain
+ * some metadata.
+ *
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
+ *
+ * @noinspection PhpUnusedParameterInspection
+ */
+ public function purge(array $newParams = []) :array {
+ // The old name is still stored...
+ $oldTableName = $this->getTableName();
+ if ($oldTableName) {
+ CRM_Core_DAO::executeQuery('DROP TABLE IF EXISTS ' . $oldTableName);
+ }
+ return [];
+ }
+
+ /**
+ * Add a status columns to the import table.
+ *
+ * We add
+ * _id - primary key
+ * _status
+ * _statusMsg
+ *
+ * Note that
+ * 1) the use of the preceding underscore has 2 purposes - it avoids clashing
+ * with an id field (code comments from 14 years ago suggest perhaps there
+ * could be cases where it still clashes but time didn't tell in this case)
+ * 2) the show fields query used to get the column names excluded the
+ * administrative fields, relying on this convention.
+ * 3) we have the capitalisation on _statusMsg - @param string $tableName
+ *
+ * @throws \API_Exception
+ * @todo change to _status_message
+ */
+ protected function addTrackingFieldsToTable(string $tableName): void {
+ CRM_Core_DAO::executeQuery("
+ ALTER TABLE $tableName
+ ADD COLUMN _entity_id INT,
+ " . $this->getAdditionalTrackingFields() . "
+ ADD COLUMN _status VARCHAR(32) DEFAULT 'NEW' NOT NULL,
+ ADD COLUMN _status_message TEXT,
+ ADD COLUMN _id INT PRIMARY KEY NOT NULL AUTO_INCREMENT"
+ );
+ }
+
+ /**
+ * Get any additional import specific tracking fields.
+ *
+ * @throws \API_Exception
+ */
+ private function getAdditionalTrackingFields(): string {
+ $sql = '';
+ $fields = $this->getParser()->getTrackingFields();
+ foreach ($fields as $fieldName => $spec) {
+ $sql .= 'ADD COLUMN _' . $fieldName . ' ' . $spec . ',';
+ }
+ return $sql;
+ }
+
+ /**
+ * Get the import parser.
+ *
+ * @return CRM_Import_Parser
+ *
+ * @throws \API_Exception
+ */
+ private function getParser() {
+ $parserClass = '';
+ foreach (CRM_Core_BAO_UserJob::getTypes() as $type) {
+ if ($this->getUserJob()['type_id'] === $type['id']) {
+ $parserClass = $type['class'];
+ }
+ }
+ /* @var \CRM_Import_Parser */
+ $parser = new $parserClass();
+ $parser->setUserJobID($this->getUserJobID());
+ return $parser;
+ }
+
+ /**
+ * Has the import job completed.
+ *
+ * @return bool
+ * True if no rows remain to be imported.
+ *
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
+ */
+ public function isCompleted(): bool {
+ return (bool) $this->getRowCount(['new']);
+ }
+
+ /**
+ * Update the status of the import row to reflect the processing outcome.
+ *
+ * @param int $id
+ * @param string $status
+ * @param string $message
+ * @param int|null $entityID
+ * Optional created entity ID
+ * @param array $additionalFields
+ * Optional array e.g ['related_contact' => 4]
+ *
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
+ */
+ public function updateStatus(int $id, string $status, string $message, ? int $entityID = NULL, array $additionalFields = []): void {
+ $sql = 'UPDATE ' . $this->getTableName() . ' SET _status = %1, _status_message = %2 ';
+ $params = [1 => [$status, 'String'], 2 => [$message, 'String']];
+ if ($entityID) {
+ $sql .= ', _entity_id = %3';
+ $params[3] = [$entityID, 'Integer'];
+ }
+ $nextParam = 4;
+ foreach ($additionalFields as $fieldName => $value) {
+ $sql .= ', _' . $fieldName . ' = %' . $nextParam;
+ $params[$nextParam] = is_numeric($value) ? [$value, 'Int'] : [json_encode($value), 'String'];
+ $nextParam++;
+ }
+ CRM_Core_DAO::executeQuery($sql . ' WHERE _id = ' . $id, $params);
+ }
+
+ /**
+ *
+ * @throws \API_Exception
+ * @throws \CRM_Core_Exception
+ */
+ private function instantiateQueryObject(): void {
+ $query = 'SELECT * FROM ' . $this->getTableName() . ' ' . $this->getStatusClause();
+ if ($this->limit) {
+ $query .= ' LIMIT ' . $this->limit . ($this->offset ? (' OFFSET ' . $this->offset) : NULL);
+ }
+ $this->queryResultObject = CRM_Core_DAO::executeQuery($query);
+ }
+
+ /**
+ * Get the mapping of constants to database status codes.
+ *
+ * @return array[]
+ */
+ protected function getStatusMapping(): array {
+ return [
+ CRM_Import_Parser::VALID => ['imported', 'new'],
+ CRM_Import_Parser::ERROR => ['error', 'invalid'],
+ CRM_Import_Parser::DUPLICATE => ['duplicate'],
+ CRM_Import_Parser::NO_MATCH => ['invalid_no_match'],
+ CRM_Import_Parser::UNPARSED_ADDRESS_WARNING => ['warning_unparsed_address'],
+ 'new' => ['new'],
+ ];
+ }
+
+ /**
+ * Get the status filter clause.
+ *
+ * @return string
+ */
+ private function getStatusClause(): string {
+ if (!empty($this->statuses)) {
+ $statuses = [];
+ foreach ($this->statuses as $status) {
+ foreach ($this->getStatusMapping()[$status] as $statusName) {
+ $statuses[] = '"' . $statusName . '"';
+ }
+ }
+ return ' WHERE _status IN (' . implode(',', $statuses) . ')';
+ }
+ return '';
+ }
+
}