Skip to content

Instantly share code, notes, and snippets.

@thepsion5
Last active September 22, 2015 18:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thepsion5/53371e7cbae5f523a851 to your computer and use it in GitHub Desktop.
Save thepsion5/53371e7cbae5f523a851 to your computer and use it in GitHub Desktop.
Excerpts from proprietary code used to import items from a CSV
<?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);
}
<?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);
}
<?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