Last active
September 22, 2015 18:10
-
-
Save thepsion5/53371e7cbae5f523a851 to your computer and use it in GitHub Desktop.
Excerpts from proprietary code used to import items from a CSV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
//namespaces and imports excluded for brevity | |
abstract class AbstractImporter implements Importer | |
{ | |
/** | |
* @var ImportTransformer | |
*/ | |
private $transformer; | |
/** | |
* @var Validator | |
*/ | |
private $validator; | |
/** | |
* Contains the results of the last completed import | |
* | |
* @var array | |
*/ | |
protected $resultData = [ | |
'attempted' => 0, | |
'created' => 0, | |
'updated' => 0, | |
'errors' => [] | |
]; | |
private $currentItemNumber = 1; | |
/** | |
* Sets the class that transforms the incoming data before it is validated | |
* and stored | |
* | |
* @param callable|ImportTransformer $transformer | |
* @return $this | |
* @throws InvalidArgumentException If the supplied transformer is not a valid type | |
*/ | |
public function setTransformer(ImportTransformer $transformer = null) | |
{ | |
$this->transformer = $transformer; | |
return $this; | |
} | |
/** | |
* Retrieves the set import transformer if one has been set | |
* | |
* @return ImportTransformer|null | |
*/ | |
public function getTransformer() | |
{ | |
return $this->transformer; | |
} | |
/** | |
* Sets the class that validates the incoming data before it is stored | |
* | |
* @param Validator|null $validator | |
* @return $this | |
*/ | |
public function setValidator(Validator $validator = null) | |
{ | |
$this->validator = $validator; | |
return $this; | |
} | |
/** | |
* Retrieves the current validator if one has been set | |
* | |
* @return Validator|null | |
*/ | |
public function getValidator() | |
{ | |
return $this->validator; | |
} | |
/** | |
* Imports an array or traversable collection of items | |
* | |
* @param array|Traversable $incomingItems | |
* @return ImportResult | |
* @throws InvalidArgumentException if the provided data isn't an array or instance of Traversable | |
*/ | |
public function importItems($incomingItems) | |
{ | |
if( !(is_array($incomingItems) ||$incomingItems instanceof Traversable) ) { | |
throw new InvalidArgumentException('The collections of items to import must be an array or Traversable instance.'); | |
} | |
$this->reset(); | |
foreach ($incomingItems as $index => $item) { | |
$this->currentItemNumber = $index; | |
$this->importItem($item); | |
} | |
return ImportResult::fromArray($this->resultData); | |
} | |
private function reset() | |
{ | |
$this->resultData = [ | |
'attempted' => 0, | |
'created' => 0, | |
'updated' => 0, | |
'errors' => [] | |
]; | |
$this->currentItemNumber = 1; | |
} | |
private function importItem(array $incomingItem) | |
{ | |
$this->resultData['attempted']++; | |
if ($this->transformerExists() && !$this->transformAll) { | |
$incomingItem = $this->transformer->transform($incomingItem); | |
} | |
$exists = $this->itemExists($incomingItem); | |
if( !$this->validate($incomingItem, $exists) ) { | |
return false; | |
} | |
if(!$exists) { | |
$this->createItem($incomingItem); | |
$this->resultData['created']++; | |
} else { | |
$this->updateItem($incomingItem); | |
$this->resultData['updated']++; | |
} | |
return true; | |
} | |
private function validate(array $incomingItem, $update = false) | |
{ | |
if (!$this->validatorExists()) { | |
return true; | |
} | |
$mode = ($update) ? Validator::MODE_UPDATE : Validator::MODE_CREATE; | |
$result = $this->validator->validate($incomingItem, $mode); | |
$valid = $result->passed(); | |
if (!$valid) { | |
$this->addErrors($result->errors(), $incomingItem); | |
} | |
return $valid; | |
} | |
private function addErrors(array $errors, array $incomingItem) | |
{ | |
$itemErrors = []; | |
foreach ($errors as $fieldErrors) { | |
foreach ($fieldErrors as $error) { | |
$itemErrors[] = $error; | |
} | |
} | |
$this->resultData['errors'][ $this->getItemErrorKey($incomingItem) ] = $itemErrors; | |
} | |
/** | |
* Allows extending classes to use a custom key for grouping errors for a | |
* specific item being imported | |
* | |
* @api | |
* @param array $incomingItem | |
* @return int|string | |
*/ | |
protected function getItemErrorKey(array $incomingItem) | |
{ | |
return $this->currentItemNumber; | |
} | |
/** | |
* Returns true if an item exists and needs to be updated, false of it does | |
* not exist and needs to be created | |
* | |
* @api | |
* @param array $incomingItem | |
* @return bool | |
*/ | |
protected abstract function itemExists(array $incomingItem); | |
/** | |
* Creates an imported item based on the incoming data | |
* | |
* @api | |
* @param array $incomingItem | |
*/ | |
protected abstract function createItem(array $incomingItem); | |
/** | |
* Updates an imported item based on the incoming data | |
* | |
* @api | |
* @param array $incomingItem | |
*/ | |
protected abstract function updateItem(array $incomingItem); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
//namespaces and imports excluded for brevity | |
trait CsvImporterTrait | |
{ | |
/** | |
* Valid CSV file MIME types | |
* | |
* @var array | |
*/ | |
private $csvMimeTypes = [ | |
'text/csv', | |
'text/plain', | |
'text/tsv', | |
'application/vnd.ms-excel' | |
]; | |
/** | |
* Sets the valid MIME types for CSV files | |
* | |
* @api | |
* @param array $mimeTypes | |
* @return $this | |
*/ | |
protected function setCsvMimeTypes(array $mimeTypes) | |
{ | |
if(empty($mimeTypes)) { | |
throw new \InvalidArgumentException('At least one valid CSV MIME type must be specified.'); | |
} | |
$this->csvMimeTypes = $mimeTypes; | |
return $this; | |
} | |
/** | |
* Retrieves the currently-set valid MIME types for CSV files | |
* | |
* @internal | |
* @return array | |
*/ | |
protected function getCsvMimeTypes() | |
{ | |
return $this->csvMimeTypes; | |
} | |
/** | |
* Returns true if the file is a CSV based on MIME type, false otherwise | |
* | |
* @param File|string $file | |
* @return bool | |
*/ | |
public function fileIsCsv($file) | |
{ | |
if($file instanceof File) { | |
$mime = $file->getMimeType(); | |
} else { | |
$file = new File($file, false); | |
$mime = ($file->isFile()) ? $file->getMimeType() : null; | |
} | |
return in_array($mime, $this->csvMimeTypes); | |
} | |
/** | |
* Creates an iterator that traverses each line of a CSV and returns data | |
* based as an associative array based on a supplied header row index | |
* | |
* @param string $csvPath | |
* @param int $headerRowIndex | |
* @return \Iterator | |
*/ | |
private function getCsvIterator($csvPath, $headerRowIndex = 0) | |
{ | |
$reader = Reader::createFromPath($csvPath) | |
->setFlags(\SplFileObject::READ_AHEAD|\SplFileObject::SKIP_EMPTY); | |
$headers = $reader->fetchOne($headerRowIndex); | |
return $reader->addFilter(function($row, $rowIndex) use($headerRowIndex) | |
{ | |
return is_array($row) && $rowIndex != $headerRowIndex; | |
}) | |
->query(function (array $row) use ($headers) | |
{ | |
return array_combine($headers, $row); | |
}); | |
} | |
/** | |
* Imports data from a CSV file with the assumption that the first row | |
* consists of column headers | |
* | |
* @param File|string $file | |
* @return ImportResult | |
*/ | |
public function importItemsFromCsv($file) | |
{ | |
if( !$this->fileIsCsv($file) ) { | |
throw new \InvalidArgumentException('The file to import must be a valid CSV file.'); | |
} | |
$missingCols = []; | |
if(!$this->hasRequiredCsvColumns($file, $missingCols)) { | |
$message = 'The CSV file is missing the following required columns: ' . implode(',', $missingCols); | |
throw new \InvalidArgumentException($message); | |
} | |
$csvPath = $this->getPath($file); | |
$csvIterator = $this->getCsvIterator($csvPath); | |
return $this->importItems($csvIterator); | |
} | |
private function getPath($file) | |
{ | |
return ($file instanceof File) ? $file->getPathName() : (string) $file; | |
} | |
/** | |
* {@inheritdoc} | |
* | |
* By default, the columns are assumed to be stored in a 'requiredCsvCols' property. If the | |
* property does not exist, an empty array will be returned | |
*/ | |
public function getRequiredCsvColumns() | |
{ | |
return (property_exists($this, 'requiredCsvCols')) ? (array) $this->requiredCsvCols : []; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function hasRequiredCsvColumns($csvFile, array &$missing = []) | |
{ | |
$requiredColumns = $this->getRequiredCsvColumns(); | |
if(!empty($requiredColumns)) { | |
$columnNames = $this->getCsvColumnsFromFile($csvFile); | |
$missing = array_values( array_diff($requiredColumns, $columnNames) ); | |
} else { | |
$missing = []; | |
} | |
return empty($missing); | |
} | |
private function getCsvColumnsFromFile($csvFile) | |
{ | |
$path = $this->getPath($csvFile); | |
$fileHandle = fopen($path, 'r'); | |
$columnNames = fgetcsv($fileHandle); | |
fclose($fileHandle); | |
return array_map('trim', $columnNames); | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
public abstract function importItems($incomingItems); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
//namespace and imports excluded for brevity | |
class ImportResult | |
{ | |
/** | |
* @var int | |
*/ | |
private $attempted; | |
/** | |
* @var int | |
*/ | |
private $created; | |
/** | |
* @var int | |
*/ | |
private $updated; | |
/** | |
* @var array | |
*/ | |
private $errors; | |
public function __construct($attempted, $created, $updated, array $errors = []) | |
{ | |
$this->attempted = (int) $attempted; | |
$this->created = (int) $created; | |
$this->updated = (int) $updated; | |
$this->errors = $errors; | |
} | |
/** | |
* Adds additional data to the results and returns a new instance | |
* | |
* @param array $resultData | |
* @return static | |
*/ | |
public function addData(array $resultData) | |
{ | |
$combinedResultData = $this->toArray(); | |
foreach($resultData as $key => $value) { | |
$combinedResultData[$key] += $value; | |
} | |
return static::fromArray($combinedResultData); | |
} | |
/** | |
* Creates a new ImportResult from an array of data | |
* | |
* @param array $resultData | |
* @return static | |
*/ | |
public static function fromArray(array $resultData) | |
{ | |
$merged = $resultData + [ | |
'attempted' => 0, | |
'created' => 0, | |
'updated' => 0, | |
'errors' => [] | |
]; | |
return new static($merged['attempted'], $merged['created'], $merged['updated'], $merged['errors']); | |
} | |
/** | |
* The number of items that were attempted to be imported | |
* | |
* @return int | |
*/ | |
public function attempted() | |
{ | |
return $this->attempted; | |
} | |
/** | |
* The number of items that were successfully created | |
* | |
* @return int | |
*/ | |
public function created() | |
{ | |
return $this->created; | |
} | |
/** | |
* The number of items that were successfully updated | |
* | |
* @return int | |
*/ | |
public function updated() | |
{ | |
return $this->updated; | |
} | |
/** | |
* The number of items that were not imported due to failed validation | |
* | |
* @return int | |
*/ | |
public function failed() | |
{ | |
return $this->attempted - ($this->created + $this->updated); | |
} | |
/** | |
* Returns true if any items failed to import | |
* | |
* @return bool | |
*/ | |
public function hasFailures() | |
{ | |
return ($this->failed() > 0); | |
} | |
/** | |
* Returns an array of any errors encountered | |
* | |
* @return array | |
*/ | |
public function errors() | |
{ | |
return $this->errors; | |
} | |
/** | |
* Returns true if any errors were encountered during the import process | |
* | |
* @return bool | |
*/ | |
public function hasErrors() | |
{ | |
return !empty($this->errors); | |
} | |
/** | |
* Returns this instance's data as an array, optionally excluding any errors | |
* | |
* @param bool $excludeErrors | |
* @return array | |
*/ | |
public function toArray($excludeErrors = false) | |
{ | |
$importResultData = [ | |
'attempted' => $this->attempted, | |
'created' => $this->created, | |
'updated' => $this->updated, | |
'failed' => $this->failed() | |
]; | |
if(!$excludeErrors) { | |
$importResultData['errors'] = $this->errors; | |
} | |
return $importResultData; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment