add: full multi-tenancy control
This commit is contained in:
186
packages/Webkul/DataTransfer/src/Helpers/Error.php
Normal file
186
packages/Webkul/DataTransfer/src/Helpers/Error.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers;
|
||||
|
||||
class Error
|
||||
{
|
||||
/**
|
||||
* Error Items.
|
||||
*/
|
||||
protected array $items = [];
|
||||
|
||||
/**
|
||||
* Invalid rows.
|
||||
*/
|
||||
protected array $invalidRows = [];
|
||||
|
||||
/**
|
||||
* Skipped rows.
|
||||
*/
|
||||
protected array $skippedRows = [];
|
||||
|
||||
/**
|
||||
* Errors count.
|
||||
*/
|
||||
protected int $errorsCount = 0;
|
||||
|
||||
/**
|
||||
* Error message template.
|
||||
*/
|
||||
protected array $messageTemplate = [];
|
||||
|
||||
/**
|
||||
* Add error message template.
|
||||
*/
|
||||
public function addErrorMessage(string $code, string $template): self
|
||||
{
|
||||
$this->messageTemplate[$code] = $template;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add error message.
|
||||
*/
|
||||
public function addError(string $code, ?int $rowNumber = null, ?string $columnName = null, ?string $message = null): self
|
||||
{
|
||||
if ($this->isErrorAlreadyAdded($rowNumber, $code, $columnName)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->addRowToInvalid($rowNumber);
|
||||
|
||||
$message = $this->getErrorMessage($code, $message, $columnName);
|
||||
|
||||
$this->items[$rowNumber][] = [
|
||||
'code' => $code,
|
||||
'column' => $columnName,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
$this->errorsCount++;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is already added for the row, code and column.
|
||||
*/
|
||||
public function isErrorAlreadyAdded(?int $rowNumber, string $code, ?string $columnName): bool
|
||||
{
|
||||
return collect($this->items[$rowNumber] ?? [])
|
||||
->where('code', $code)
|
||||
->where('column', $columnName)
|
||||
->isNotEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add specific row to invalid list via row number.
|
||||
*/
|
||||
protected function addRowToInvalid(?int $rowNumber): self
|
||||
{
|
||||
if (is_null($rowNumber)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (! in_array($rowNumber, $this->invalidRows)) {
|
||||
$this->invalidRows[] = $rowNumber;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add specific row to invalid list via row number.
|
||||
*/
|
||||
public function addRowToSkip(?int $rowNumber): self
|
||||
{
|
||||
if (is_null($rowNumber)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (! in_array($rowNumber, $this->skippedRows)) {
|
||||
$this->skippedRows[] = $rowNumber;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if row is invalid by row number.
|
||||
*/
|
||||
public function isRowInvalid(int $rowNumber): bool
|
||||
{
|
||||
return in_array($rowNumber, array_merge($this->invalidRows, $this->skippedRows));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an error message via code, message and column name.
|
||||
*/
|
||||
protected function getErrorMessage(?string $code, ?string $message, ?string $columnName): string
|
||||
{
|
||||
if (
|
||||
empty($message)
|
||||
&& isset($this->messageTemplate[$code])
|
||||
) {
|
||||
$message = (string) $this->messageTemplate[$code];
|
||||
}
|
||||
|
||||
if (
|
||||
$columnName
|
||||
&& $message
|
||||
) {
|
||||
$message = sprintf($message, $columnName);
|
||||
}
|
||||
|
||||
if (! $message) {
|
||||
$message = $code;
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of invalid rows.
|
||||
*/
|
||||
public function getInvalidRowsCount(): int
|
||||
{
|
||||
return count($this->invalidRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current error count.
|
||||
*/
|
||||
public function getErrorsCount(): int
|
||||
{
|
||||
return $this->errorsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all errors from an import process.
|
||||
*/
|
||||
public function getAllErrors(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all errors grouped by code.
|
||||
*/
|
||||
public function getAllErrorsGroupedByCode(): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
foreach ($this->items as $rowNumber => $rowErrors) {
|
||||
foreach ($rowErrors as $error) {
|
||||
if ($rowNumber === '') {
|
||||
$errors[$error['code']][$error['message']] = null;
|
||||
} else {
|
||||
$errors[$error['code']][$error['message']][] = $rowNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
573
packages/Webkul/DataTransfer/src/Helpers/Import.php
Normal file
573
packages/Webkul/DataTransfer/src/Helpers/Import.php
Normal file
@@ -0,0 +1,573 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Csv;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xls;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use Webkul\DataTransfer\Contracts\Import as ImportContract;
|
||||
use Webkul\DataTransfer\Contracts\ImportBatch as ImportBatchContract;
|
||||
use Webkul\DataTransfer\Helpers\Importers\AbstractImporter;
|
||||
use Webkul\DataTransfer\Helpers\Sources\AbstractSource;
|
||||
use Webkul\DataTransfer\Helpers\Sources\CSV as CSVSource;
|
||||
use Webkul\DataTransfer\Helpers\Sources\Excel as ExcelSource;
|
||||
use Webkul\DataTransfer\Repositories\ImportBatchRepository;
|
||||
use Webkul\DataTransfer\Repositories\ImportRepository;
|
||||
|
||||
class Import
|
||||
{
|
||||
/**
|
||||
* Import state for pending import.
|
||||
*/
|
||||
public const STATE_PENDING = 'pending';
|
||||
|
||||
/**
|
||||
* Import state for validated import.
|
||||
*/
|
||||
public const STATE_VALIDATED = 'validated';
|
||||
|
||||
/**
|
||||
* Import state for processing import.
|
||||
*/
|
||||
public const STATE_PROCESSING = 'processing';
|
||||
|
||||
/**
|
||||
* Import state for processed import.
|
||||
*/
|
||||
public const STATE_PROCESSED = 'processed';
|
||||
|
||||
/**
|
||||
* Import state for linking import.
|
||||
*/
|
||||
public const STATE_LINKING = 'linking';
|
||||
|
||||
/**
|
||||
* Import state for linked import.
|
||||
*/
|
||||
public const STATE_LINKED = 'linked';
|
||||
|
||||
/**
|
||||
* Import state for indexing import.
|
||||
*/
|
||||
public const STATE_INDEXING = 'indexing';
|
||||
|
||||
/**
|
||||
* Import state for indexed import.
|
||||
*/
|
||||
public const STATE_INDEXED = 'indexed';
|
||||
|
||||
/**
|
||||
* Import state for completed import.
|
||||
*/
|
||||
public const STATE_COMPLETED = 'completed';
|
||||
|
||||
/**
|
||||
* Validation strategy for skipping the error during the import process.
|
||||
*/
|
||||
public const VALIDATION_STRATEGY_SKIP_ERRORS = 'skip-errors';
|
||||
|
||||
/**
|
||||
* Validation strategy for stopping the import process on error.
|
||||
*/
|
||||
public const VALIDATION_STRATEGY_STOP_ON_ERROR = 'stop-on-errors';
|
||||
|
||||
/**
|
||||
* Action constant for updating/creating for the resource.
|
||||
*/
|
||||
public const ACTION_APPEND = 'append';
|
||||
|
||||
/**
|
||||
* Action constant for deleting the resource.
|
||||
*/
|
||||
public const ACTION_DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* Import instance.
|
||||
*/
|
||||
protected ImportContract $import;
|
||||
|
||||
/**
|
||||
* Error helper instance.
|
||||
*
|
||||
* @var \Webkul\DataTransfer\Helpers\Error
|
||||
*/
|
||||
protected $typeImporter;
|
||||
|
||||
/**
|
||||
* Create a new helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ImportRepository $importRepository,
|
||||
protected ImportBatchRepository $importBatchRepository,
|
||||
protected Error $errorHelper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Set import instance.
|
||||
*/
|
||||
public function setImport(ImportContract $import): self
|
||||
{
|
||||
$this->import = $import;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns import instance.
|
||||
*/
|
||||
public function getImport(): ImportContract
|
||||
{
|
||||
return $this->import;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error helper instance.
|
||||
*
|
||||
* @return \Webkul\DataTransfer\Helpers\Error
|
||||
*/
|
||||
public function getErrorHelper()
|
||||
{
|
||||
return $this->errorHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns source helper instance.
|
||||
*/
|
||||
public function getSource(): AbstractSource
|
||||
{
|
||||
if (Str::contains($this->import->file_path, '.csv')) {
|
||||
$source = new CSVSource(
|
||||
$this->import->file_path,
|
||||
$this->import->field_separator,
|
||||
);
|
||||
} else {
|
||||
$source = new ExcelSource(
|
||||
$this->import->file_path,
|
||||
$this->import->field_separator,
|
||||
);
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates import and returns validation result.
|
||||
*/
|
||||
public function validate(): bool
|
||||
{
|
||||
try {
|
||||
$source = $this->getSource();
|
||||
|
||||
$typeImporter = $this->getTypeImporter()->setSource($source);
|
||||
|
||||
$typeImporter->validateData();
|
||||
} catch (\Exception $e) {
|
||||
$this->errorHelper->addError(
|
||||
AbstractImporter::ERROR_CODE_SYSTEM_EXCEPTION,
|
||||
null,
|
||||
null,
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$import = $this->importRepository->update([
|
||||
'state' => self::STATE_VALIDATED,
|
||||
'processed_rows_count' => $this->getProcessedRowsCount(),
|
||||
'invalid_rows_count' => $this->errorHelper->getInvalidRowsCount(),
|
||||
'errors_count' => $this->errorHelper->getErrorsCount(),
|
||||
'errors' => $this->getFormattedErrors(),
|
||||
'error_file_path' => $this->uploadErrorReport(),
|
||||
], $this->import->id);
|
||||
|
||||
$this->setImport($import);
|
||||
|
||||
return $this->isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts import process.
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
if ($this->isErrorLimitExceeded()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->import->processed_rows_count <= $this->import->invalid_rows_count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error limit has been exceeded.
|
||||
*/
|
||||
public function isErrorLimitExceeded(): bool
|
||||
{
|
||||
if (
|
||||
$this->import->validation_strategy == self::VALIDATION_STRATEGY_STOP_ON_ERROR
|
||||
&& $this->import->errors_count > $this->import->allowed_errors
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts import process.
|
||||
*/
|
||||
public function start(?ImportBatchContract $importBatch = null): bool
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$typeImporter = $this->getTypeImporter();
|
||||
|
||||
$typeImporter->importData($importBatch);
|
||||
} catch (\Exception $e) {
|
||||
/**
|
||||
* Rollback transaction.
|
||||
*/
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
/**
|
||||
* Commit transaction.
|
||||
*/
|
||||
DB::commit();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link import resources.
|
||||
*/
|
||||
public function link(ImportBatchContract $importBatch): bool
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$typeImporter = $this->getTypeImporter();
|
||||
|
||||
$typeImporter->linkData($importBatch);
|
||||
} catch (\Exception $e) {
|
||||
/**
|
||||
* Rollback transaction.
|
||||
*/
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
/**
|
||||
* Commit transaction.
|
||||
*/
|
||||
DB::commit();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index import resources.
|
||||
*/
|
||||
public function index(ImportBatchContract $importBatch): bool
|
||||
{
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
$typeImporter = $this->getTypeImporter();
|
||||
|
||||
$typeImporter->indexData($importBatch);
|
||||
} catch (\Exception $e) {
|
||||
/**
|
||||
* Rollback transaction.
|
||||
*/
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
/**
|
||||
* Commit transaction.
|
||||
*/
|
||||
DB::commit();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Started the import process.
|
||||
*/
|
||||
public function started(): void
|
||||
{
|
||||
$import = $this->importRepository->update([
|
||||
'state' => self::STATE_PROCESSING,
|
||||
'started_at' => now(),
|
||||
'summary' => [],
|
||||
], $this->import->id);
|
||||
|
||||
$this->setImport($import);
|
||||
|
||||
Event::dispatch('data_transfer.imports.started', $import);
|
||||
}
|
||||
|
||||
/**
|
||||
* Started the import linking process.
|
||||
*/
|
||||
public function linking(): void
|
||||
{
|
||||
$import = $this->importRepository->update([
|
||||
'state' => self::STATE_LINKING,
|
||||
], $this->import->id);
|
||||
|
||||
$this->setImport($import);
|
||||
|
||||
Event::dispatch('data_transfer.imports.linking', $import);
|
||||
}
|
||||
|
||||
/**
|
||||
* Started the import indexing process.
|
||||
*/
|
||||
public function indexing(): void
|
||||
{
|
||||
$import = $this->importRepository->update([
|
||||
'state' => self::STATE_INDEXING,
|
||||
], $this->import->id);
|
||||
|
||||
$this->setImport($import);
|
||||
|
||||
Event::dispatch('data_transfer.imports.indexing', $import);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the import process.
|
||||
*/
|
||||
public function completed(): void
|
||||
{
|
||||
$summary = $this->importBatchRepository
|
||||
->select(
|
||||
DB::raw('SUM(json_unquote(json_extract(summary, \'$."created"\'))) AS created'),
|
||||
DB::raw('SUM(json_unquote(json_extract(summary, \'$."updated"\'))) AS updated'),
|
||||
DB::raw('SUM(json_unquote(json_extract(summary, \'$."deleted"\'))) AS deleted'),
|
||||
)
|
||||
->where('import_id', $this->import->id)
|
||||
->groupBy('import_id')
|
||||
->first()
|
||||
->toArray();
|
||||
|
||||
$import = $this->importRepository->update([
|
||||
'state' => self::STATE_COMPLETED,
|
||||
'summary' => $summary,
|
||||
'completed_at' => now(),
|
||||
], $this->import->id);
|
||||
|
||||
$this->setImport($import);
|
||||
|
||||
Event::dispatch('data_transfer.imports.completed', $import);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns import stats.
|
||||
*/
|
||||
public function stats(string $state): array
|
||||
{
|
||||
$total = $this->import->batches->count();
|
||||
|
||||
$completed = $this->import->batches->where('state', $state)->count();
|
||||
|
||||
$progress = $total
|
||||
? round($completed / $total * 100)
|
||||
: 0;
|
||||
|
||||
$summary = $this->importBatchRepository
|
||||
->select(
|
||||
DB::raw('SUM(json_unquote(json_extract(summary, \'$."created"\'))) AS created'),
|
||||
DB::raw('SUM(json_unquote(json_extract(summary, \'$."updated"\'))) AS updated'),
|
||||
DB::raw('SUM(json_unquote(json_extract(summary, \'$."deleted"\'))) AS deleted'),
|
||||
)
|
||||
->where('import_id', $this->import->id)
|
||||
->where('state', $state)
|
||||
->groupBy('import_id')
|
||||
->first()
|
||||
?->toArray();
|
||||
|
||||
return [
|
||||
'batches' => [
|
||||
'total' => $total,
|
||||
'completed' => $completed,
|
||||
'remaining' => $total - $completed,
|
||||
],
|
||||
'progress' => $progress,
|
||||
'summary' => $summary ?? [
|
||||
'created' => 0,
|
||||
'updated' => 0,
|
||||
'deleted' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all error grouped by error code.
|
||||
*/
|
||||
public function getFormattedErrors(): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
foreach ($this->errorHelper->getAllErrorsGroupedByCode() as $groupedErrors) {
|
||||
foreach ($groupedErrors as $errorMessage => $rowNumbers) {
|
||||
if (! empty($rowNumbers)) {
|
||||
$errors[] = 'Row(s) '.implode(', ', $rowNumbers).': '.$errorMessage;
|
||||
} else {
|
||||
$errors[] = $errorMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads error report and save the path to the database.
|
||||
*/
|
||||
public function uploadErrorReport(): ?string
|
||||
{
|
||||
/**
|
||||
* Return null if there are no errors.
|
||||
*/
|
||||
if (! $this->errorHelper->getErrorsCount()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return null if there are no invalid rows.
|
||||
*/
|
||||
if (! $this->errorHelper->getInvalidRowsCount()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$errors = $this->errorHelper->getAllErrors();
|
||||
|
||||
$source = $this->getTypeImporter()->getSource();
|
||||
|
||||
$source->rewind();
|
||||
|
||||
$spreadsheet = new Spreadsheet;
|
||||
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
/**
|
||||
* Add headers with extra error column.
|
||||
*/
|
||||
$sheet->fromArray(
|
||||
[array_merge($source->getColumnNames(), [
|
||||
'error',
|
||||
])],
|
||||
null,
|
||||
'A1'
|
||||
);
|
||||
|
||||
$rowNumber = 2;
|
||||
|
||||
while ($source->valid()) {
|
||||
try {
|
||||
$rowData = $source->current();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$source->next();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$rowErrors = $errors[$source->getCurrentRowNumber()] ?? [];
|
||||
|
||||
if (! empty($rowErrors)) {
|
||||
$rowErrors = Arr::pluck($rowErrors, 'message');
|
||||
}
|
||||
|
||||
$rowData[] = implode('|', $rowErrors);
|
||||
|
||||
$sheet->fromArray([$rowData], null, 'A'.$rowNumber++);
|
||||
|
||||
$source->next();
|
||||
}
|
||||
|
||||
$fileType = pathinfo($this->import->file_path, PATHINFO_EXTENSION);
|
||||
|
||||
switch ($fileType) {
|
||||
case 'csv':
|
||||
$writer = new Csv($spreadsheet);
|
||||
|
||||
$writer->setDelimiter($this->import->field_separator);
|
||||
|
||||
break;
|
||||
|
||||
case 'xls':
|
||||
$writer = new Xls($spreadsheet);
|
||||
|
||||
case 'xlsx':
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException("Unsupported file type: $fileType");
|
||||
}
|
||||
|
||||
$errorFilePath = 'imports/'.time().'-error-report.'.$fileType;
|
||||
|
||||
$writer->save(Storage::disk('public')->path($errorFilePath));
|
||||
|
||||
return $errorFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates source file and returns validation result.
|
||||
*/
|
||||
public function getTypeImporter(): AbstractImporter
|
||||
{
|
||||
if (! $this->typeImporter) {
|
||||
$importerConfig = config('importers.'.$this->import->type);
|
||||
|
||||
$this->typeImporter = app()->make($importerConfig['importer'])
|
||||
->setImport($this->import)
|
||||
->setErrorHelper($this->errorHelper);
|
||||
}
|
||||
|
||||
return $this->typeImporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of checked rows.
|
||||
*/
|
||||
public function getProcessedRowsCount(): int
|
||||
{
|
||||
return $this->getTypeImporter()->getProcessedRowsCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is linking resource required for the import operation.
|
||||
*/
|
||||
public function isLinkingRequired(): bool
|
||||
{
|
||||
return $this->getTypeImporter()->isLinkingRequired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is indexing resource required for the import operation.
|
||||
*/
|
||||
public function isIndexingRequired(): bool
|
||||
{
|
||||
return $this->getTypeImporter()->isIndexingRequired();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers\Importers;
|
||||
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\Attribute\Repositories\AttributeValueRepository;
|
||||
use Webkul\Core\Contracts\Validations\Decimal;
|
||||
use Webkul\DataTransfer\Contracts\Import as ImportContract;
|
||||
use Webkul\DataTransfer\Contracts\ImportBatch as ImportBatchContract;
|
||||
use Webkul\DataTransfer\Helpers\Import;
|
||||
use Webkul\DataTransfer\Jobs\Import\Completed as CompletedJob;
|
||||
use Webkul\DataTransfer\Jobs\Import\ImportBatch as ImportBatchJob;
|
||||
use Webkul\DataTransfer\Jobs\Import\IndexBatch as IndexBatchJob;
|
||||
use Webkul\DataTransfer\Jobs\Import\Indexing as IndexingJob;
|
||||
use Webkul\DataTransfer\Jobs\Import\LinkBatch as LinkBatchJob;
|
||||
use Webkul\DataTransfer\Jobs\Import\Linking as LinkingJob;
|
||||
use Webkul\DataTransfer\Repositories\ImportBatchRepository;
|
||||
|
||||
abstract class AbstractImporter
|
||||
{
|
||||
/**
|
||||
* Error code for system exception.
|
||||
*/
|
||||
public const ERROR_CODE_SYSTEM_EXCEPTION = 'system_exception';
|
||||
|
||||
/**
|
||||
* Error code for column not found.
|
||||
*/
|
||||
public const ERROR_CODE_COLUMN_NOT_FOUND = 'column_not_found';
|
||||
|
||||
/**
|
||||
* Error code for column empty header.
|
||||
*/
|
||||
public const ERROR_CODE_COLUMN_EMPTY_HEADER = 'column_empty_header';
|
||||
|
||||
/**
|
||||
* Error code for column name invalid.
|
||||
*/
|
||||
public const ERROR_CODE_COLUMN_NAME_INVALID = 'column_name_invalid';
|
||||
|
||||
/**
|
||||
* Error code for invalid attribute.
|
||||
*/
|
||||
public const ERROR_CODE_INVALID_ATTRIBUTE = 'invalid_attribute_name';
|
||||
|
||||
/**
|
||||
* Error code for wrong quotes.
|
||||
*/
|
||||
public const ERROR_CODE_WRONG_QUOTES = 'wrong_quotes';
|
||||
|
||||
/**
|
||||
* Error code for wrong columns number.
|
||||
*/
|
||||
public const ERROR_CODE_COLUMNS_NUMBER = 'wrong_columns_number';
|
||||
|
||||
/**
|
||||
* Error message templates.
|
||||
*/
|
||||
protected array $errorMessages = [
|
||||
self::ERROR_CODE_SYSTEM_EXCEPTION => 'data_transfer::app.validation.errors.system',
|
||||
self::ERROR_CODE_COLUMN_NOT_FOUND => 'data_transfer::app.validation.errors.column-not-found',
|
||||
self::ERROR_CODE_COLUMN_EMPTY_HEADER => 'data_transfer::app.validation.errors.column-empty-headers',
|
||||
self::ERROR_CODE_COLUMN_NAME_INVALID => 'data_transfer::app.validation.errors.column-name-invalid',
|
||||
self::ERROR_CODE_INVALID_ATTRIBUTE => 'data_transfer::app.validation.errors.invalid-attribute',
|
||||
self::ERROR_CODE_WRONG_QUOTES => 'data_transfer::app.validation.errors.wrong-quotes',
|
||||
self::ERROR_CODE_COLUMNS_NUMBER => 'data_transfer::app.validation.errors.column-numbers',
|
||||
];
|
||||
|
||||
public const BATCH_SIZE = 100;
|
||||
|
||||
/**
|
||||
* Is linking required.
|
||||
*/
|
||||
protected bool $linkingRequired = false;
|
||||
|
||||
/**
|
||||
* Is indexing required.
|
||||
*/
|
||||
protected bool $indexingRequired = false;
|
||||
|
||||
/**
|
||||
* Error helper instance.
|
||||
*
|
||||
* @var \Webkul\DataTransfer\Helpers\Error
|
||||
*/
|
||||
protected $errorHelper;
|
||||
|
||||
/**
|
||||
* Import instance.
|
||||
*/
|
||||
protected ImportContract $import;
|
||||
|
||||
/**
|
||||
* Source instance.
|
||||
*
|
||||
* @var \Webkul\DataTransfer\Helpers\Source
|
||||
*/
|
||||
protected $source;
|
||||
|
||||
/**
|
||||
* Valid column names.
|
||||
*/
|
||||
protected array $validColumnNames = [];
|
||||
|
||||
/**
|
||||
* Array of numbers of validated rows as keys and boolean TRUE as values.
|
||||
*/
|
||||
protected array $validatedRows = [];
|
||||
|
||||
/**
|
||||
* Number of rows processed by validation.
|
||||
*/
|
||||
protected int $processedRowsCount = 0;
|
||||
|
||||
/**
|
||||
* Number of created items.
|
||||
*/
|
||||
protected int $createdItemsCount = 0;
|
||||
|
||||
/**
|
||||
* Number of updated items.
|
||||
*/
|
||||
protected int $updatedItemsCount = 0;
|
||||
|
||||
/**
|
||||
* Number of deleted items.
|
||||
*/
|
||||
protected int $deletedItemsCount = 0;
|
||||
|
||||
/**
|
||||
* Create a new helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ImportBatchRepository $importBatchRepository,
|
||||
protected AttributeRepository $attributeRepository,
|
||||
protected AttributeValueRepository $attributeValueRepository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validate data row.
|
||||
*/
|
||||
abstract public function validateRow(array $rowData, int $rowNumber): bool;
|
||||
|
||||
/**
|
||||
* Import data rows.
|
||||
*/
|
||||
abstract public function importBatch(ImportBatchContract $importBatchContract): bool;
|
||||
|
||||
/**
|
||||
* Initialize Product error messages.
|
||||
*/
|
||||
protected function initErrorMessages(): void
|
||||
{
|
||||
foreach ($this->errorMessages as $errorCode => $message) {
|
||||
$this->errorHelper->addErrorMessage($errorCode, trans($message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import instance.
|
||||
*/
|
||||
public function setImport(ImportContract $import): self
|
||||
{
|
||||
$this->import = $import;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import instance.
|
||||
*
|
||||
* @param \Webkul\DataTransfer\Helpers\Source $errorHelper
|
||||
*/
|
||||
public function setSource($source)
|
||||
{
|
||||
$this->source = $source;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import instance.
|
||||
*
|
||||
* @param \Webkul\DataTransfer\Helpers\Error $errorHelper
|
||||
*/
|
||||
public function setErrorHelper($errorHelper): self
|
||||
{
|
||||
$this->errorHelper = $errorHelper;
|
||||
|
||||
$this->initErrorMessages();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import instance.
|
||||
*
|
||||
* @return \Webkul\DataTransfer\Helpers\Source
|
||||
*/
|
||||
public function getSource()
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve valid column names.
|
||||
*/
|
||||
public function getValidColumnNames(): array
|
||||
{
|
||||
return $this->validColumnNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate data.
|
||||
*/
|
||||
public function validateData(): void
|
||||
{
|
||||
Event::dispatch('data_transfer.imports.validate.before', $this->import);
|
||||
|
||||
$errors = [];
|
||||
|
||||
$absentColumns = array_diff($this->permanentAttributes, $columns = $this->getSource()->getColumnNames());
|
||||
|
||||
if (! empty($absentColumns)) {
|
||||
$errors[self::ERROR_CODE_COLUMN_NOT_FOUND] = $absentColumns;
|
||||
}
|
||||
|
||||
foreach ($columns as $columnNumber => $columnName) {
|
||||
if (empty($columnName)) {
|
||||
$errors[self::ERROR_CODE_COLUMN_EMPTY_HEADER][] = $columnNumber + 1;
|
||||
} elseif (! preg_match('/^[a-z][a-z0-9_]*$/', $columnName)) {
|
||||
$errors[self::ERROR_CODE_COLUMN_NAME_INVALID][] = $columnName;
|
||||
} elseif (! in_array($columnName, $this->getValidColumnNames())) {
|
||||
$errors[self::ERROR_CODE_INVALID_ATTRIBUTE][] = $columnName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Columns Errors.
|
||||
*/
|
||||
foreach ($errors as $errorCode => $error) {
|
||||
$this->addErrors($errorCode, $error);
|
||||
}
|
||||
|
||||
if (! $this->errorHelper->getErrorsCount()) {
|
||||
$this->saveValidatedBatches();
|
||||
}
|
||||
|
||||
Event::dispatch('data_transfer.imports.validate.after', $this->import);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save validated batches.
|
||||
*/
|
||||
protected function saveValidatedBatches(): self
|
||||
{
|
||||
$source = $this->getSource();
|
||||
|
||||
$batchRows = [];
|
||||
|
||||
$source->rewind();
|
||||
|
||||
/**
|
||||
* Clean previous saved batches.
|
||||
*/
|
||||
$this->importBatchRepository->deleteWhere([
|
||||
'import_id' => $this->import->id,
|
||||
]);
|
||||
|
||||
while (
|
||||
$source->valid()
|
||||
|| count($batchRows)
|
||||
) {
|
||||
if (
|
||||
count($batchRows) == self::BATCH_SIZE
|
||||
|| ! $source->valid()
|
||||
) {
|
||||
$this->importBatchRepository->create([
|
||||
'import_id' => $this->import->id,
|
||||
'data' => $batchRows,
|
||||
]);
|
||||
|
||||
$batchRows = [];
|
||||
}
|
||||
|
||||
if ($source->valid()) {
|
||||
$rowData = $source->current();
|
||||
|
||||
if ($this->validateRow($rowData, $source->getCurrentRowNumber())) {
|
||||
$batchRows[] = $this->prepareRowForDb($rowData);
|
||||
}
|
||||
|
||||
$this->processedRowsCount++;
|
||||
|
||||
$source->next();
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare validation rules.
|
||||
*/
|
||||
public function getValidationRules(string $entityType, array $rowData): array
|
||||
{
|
||||
if (empty($entityType)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rules = [];
|
||||
|
||||
$attributes = $this->attributeRepository->scopeQuery(fn ($query) => $query->whereIn('code', array_keys($rowData))->where('entity_type', $entityType))->get();
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
$validations = [];
|
||||
|
||||
if ($attribute->type == 'boolean') {
|
||||
continue;
|
||||
} elseif ($attribute->type == 'address') {
|
||||
if (! $attribute->is_required) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$validations = [
|
||||
$attribute->code.'.address' => 'required',
|
||||
$attribute->code.'.country' => 'required',
|
||||
$attribute->code.'.state' => 'required',
|
||||
$attribute->code.'.city' => 'required',
|
||||
$attribute->code.'.postcode' => 'required',
|
||||
];
|
||||
} elseif ($attribute->type == 'email') {
|
||||
$validations = [
|
||||
$attribute->code => [$attribute->is_required ? 'required' : 'nullable'],
|
||||
$attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable', 'email'],
|
||||
$attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable',
|
||||
];
|
||||
} elseif ($attribute->type == 'phone') {
|
||||
$validations = [
|
||||
$attribute->code => [$attribute->is_required ? 'required' : 'nullable'],
|
||||
$attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable'],
|
||||
$attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable',
|
||||
];
|
||||
} else {
|
||||
$validations[$attribute->code] = [$attribute->is_required ? 'required' : 'nullable'];
|
||||
|
||||
if ($attribute->type == 'text' && $attribute->validation) {
|
||||
array_push($validations[$attribute->code],
|
||||
$attribute->validation == 'decimal'
|
||||
? new Decimal
|
||||
: $attribute->validation
|
||||
);
|
||||
}
|
||||
|
||||
if ($attribute->type == 'price') {
|
||||
array_push($validations[$attribute->code], new Decimal);
|
||||
}
|
||||
}
|
||||
|
||||
if ($attribute->is_unique) {
|
||||
array_push($validations[in_array($attribute->type, ['email', 'phone'])
|
||||
? $attribute->code.'.*.value'
|
||||
: $attribute->code
|
||||
], function ($field, $value, $fail) use ($attribute) {
|
||||
if (! $this->attributeValueRepository->isValueUnique(null, $attribute->entity_type, $attribute, $field)) {
|
||||
$fail(trans('data_transfer::app.validation.errors.already-exists', ['attribute' => $attribute->name]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$rules = [
|
||||
...$rules,
|
||||
...$validations,
|
||||
];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the import process.
|
||||
*/
|
||||
public function importData(?ImportBatchContract $importBatch = null): bool
|
||||
{
|
||||
if ($importBatch) {
|
||||
$this->importBatch($importBatch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$typeBatches = [];
|
||||
|
||||
foreach ($this->import->batches as $batch) {
|
||||
$typeBatches['import'][] = new ImportBatchJob($batch);
|
||||
|
||||
if ($this->isLinkingRequired()) {
|
||||
$typeBatches['link'][] = new LinkBatchJob($batch);
|
||||
}
|
||||
|
||||
if ($this->isIndexingRequired()) {
|
||||
$typeBatches['index'][] = new IndexBatchJob($batch);
|
||||
}
|
||||
}
|
||||
|
||||
$chain[] = Bus::batch($typeBatches['import']);
|
||||
|
||||
if (! empty($typeBatches['link'])) {
|
||||
$chain[] = new LinkingJob($this->import);
|
||||
|
||||
$chain[] = Bus::batch($typeBatches['link']);
|
||||
}
|
||||
|
||||
if (! empty($typeBatches['index'])) {
|
||||
$chain[] = new IndexingJob($this->import);
|
||||
|
||||
$chain[] = Bus::batch($typeBatches['index']);
|
||||
}
|
||||
|
||||
$chain[] = new CompletedJob($this->import);
|
||||
|
||||
Bus::chain($chain)->dispatch();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link resource data.
|
||||
*/
|
||||
public function linkData(ImportBatchContract $importBatch): bool
|
||||
{
|
||||
$this->linkBatch($importBatch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index resource data.
|
||||
*/
|
||||
public function indexData(ImportBatchContract $importBatch): bool
|
||||
{
|
||||
$this->indexBatch($importBatch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add errors to error aggregator.
|
||||
*/
|
||||
protected function addErrors(string $code, mixed $errors): void
|
||||
{
|
||||
$this->errorHelper->addError(
|
||||
$code,
|
||||
null,
|
||||
implode('", "', $errors)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add row as skipped.
|
||||
*
|
||||
* @param int|null $rowNumber
|
||||
* @param string|null $columnName
|
||||
* @param string|null $errorMessage
|
||||
* @return $this
|
||||
*/
|
||||
protected function skipRow($rowNumber, string $errorCode, $columnName = null, $errorMessage = null): self
|
||||
{
|
||||
$this->errorHelper->addError(
|
||||
$errorCode,
|
||||
$rowNumber,
|
||||
$columnName,
|
||||
$errorMessage
|
||||
);
|
||||
|
||||
$this->errorHelper->addRowToSkip($rowNumber);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare row data to save into the database.
|
||||
*/
|
||||
protected function prepareRowForDb(array $rowData): array
|
||||
{
|
||||
$rowData = array_map(function ($value) {
|
||||
return $value === '' ? null : $value;
|
||||
}, $rowData);
|
||||
|
||||
return $rowData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of checked rows.
|
||||
*/
|
||||
public function getProcessedRowsCount(): int
|
||||
{
|
||||
return $this->processedRowsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of created items count.
|
||||
*/
|
||||
public function getCreatedItemsCount(): int
|
||||
{
|
||||
return $this->createdItemsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of updated items count.
|
||||
*/
|
||||
public function getUpdatedItemsCount(): int
|
||||
{
|
||||
return $this->updatedItemsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of deleted items count.
|
||||
*/
|
||||
public function getDeletedItemsCount(): int
|
||||
{
|
||||
return $this->deletedItemsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is linking resource required for the import operation.
|
||||
*/
|
||||
public function isLinkingRequired(): bool
|
||||
{
|
||||
if ($this->import->action == Import::ACTION_DELETE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->linkingRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is indexing resource required for the import operation.
|
||||
*/
|
||||
public function isIndexingRequired(): bool
|
||||
{
|
||||
if ($this->import->action == Import::ACTION_DELETE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->indexingRequired;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers\Importers\Leads;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\Attribute\Repositories\AttributeValueRepository;
|
||||
use Webkul\Core\Contracts\Validations\Decimal;
|
||||
use Webkul\DataTransfer\Contracts\ImportBatch as ImportBatchContract;
|
||||
use Webkul\DataTransfer\Helpers\Import;
|
||||
use Webkul\DataTransfer\Helpers\Importers\AbstractImporter;
|
||||
use Webkul\DataTransfer\Repositories\ImportBatchRepository;
|
||||
use Webkul\Lead\Repositories\LeadRepository;
|
||||
use Webkul\Lead\Repositories\ProductRepository as LeadProductRepository;
|
||||
|
||||
class Importer extends AbstractImporter
|
||||
{
|
||||
/**
|
||||
* Error code for non existing id.
|
||||
*/
|
||||
const ERROR_ID_NOT_FOUND_FOR_DELETE = 'id_not_found_to_delete';
|
||||
|
||||
/**
|
||||
* Permanent entity columns.
|
||||
*/
|
||||
protected array $validColumnNames = [
|
||||
'id',
|
||||
'title',
|
||||
'description',
|
||||
'lead_value',
|
||||
'status',
|
||||
'lost_reason',
|
||||
'closed_at',
|
||||
'user_id',
|
||||
'person_id',
|
||||
'lead_source_id',
|
||||
'lead_type_id',
|
||||
'lead_pipeline_id',
|
||||
'lead_pipeline_stage_id',
|
||||
'expected_close_date',
|
||||
'product',
|
||||
];
|
||||
|
||||
/**
|
||||
* Error message templates.
|
||||
*/
|
||||
protected array $messages = [
|
||||
self::ERROR_ID_NOT_FOUND_FOR_DELETE => 'data_transfer::app.importers.leads.validation.errors.id-not-found',
|
||||
];
|
||||
|
||||
/**
|
||||
* Permanent entity columns.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $permanentAttributes = ['title'];
|
||||
|
||||
/**
|
||||
* Permanent entity column.
|
||||
*/
|
||||
protected string $masterAttributeCode = 'id';
|
||||
|
||||
/**
|
||||
* Is linking required
|
||||
*/
|
||||
protected bool $linkingRequired = true;
|
||||
|
||||
/**
|
||||
* Create a new helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ImportBatchRepository $importBatchRepository,
|
||||
protected LeadRepository $leadRepository,
|
||||
protected LeadProductRepository $leadProductRepository,
|
||||
protected AttributeRepository $attributeRepository,
|
||||
protected AttributeValueRepository $attributeValueRepository,
|
||||
protected Storage $leadsStorage,
|
||||
) {
|
||||
parent::__construct(
|
||||
$importBatchRepository,
|
||||
$attributeRepository,
|
||||
$attributeValueRepository,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize leads error templates.
|
||||
*/
|
||||
protected function initErrorMessages(): void
|
||||
{
|
||||
foreach ($this->messages as $errorCode => $message) {
|
||||
$this->errorHelper->addErrorMessage($errorCode, trans($message));
|
||||
}
|
||||
|
||||
parent::initErrorMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate data.
|
||||
*/
|
||||
public function validateData(): void
|
||||
{
|
||||
$this->leadsStorage->init();
|
||||
|
||||
parent::validateData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates row.
|
||||
*/
|
||||
public function validateRow(array $rowData, int $rowNumber): bool
|
||||
{
|
||||
/**
|
||||
* If row is already validated than no need for further validation.
|
||||
*/
|
||||
if (isset($this->validatedRows[$rowNumber])) {
|
||||
return ! $this->errorHelper->isRowInvalid($rowNumber);
|
||||
}
|
||||
|
||||
$this->validatedRows[$rowNumber] = true;
|
||||
|
||||
/**
|
||||
* If import action is delete than no need for further validation.
|
||||
*/
|
||||
if ($this->import->action == Import::ACTION_DELETE) {
|
||||
if (! $this->isTitleExist($rowData['title'])) {
|
||||
$this->skipRow($rowNumber, self::ERROR_ID_NOT_FOUND_FOR_DELETE, 'id');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! empty($rowData['product'])) {
|
||||
$product = $this->parseProducts($rowData['product']);
|
||||
|
||||
$validator = Validator::make($product, [
|
||||
'id' => 'required|exists:products,id',
|
||||
'price' => 'required',
|
||||
'quantity' => 'required',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$failedAttributes = $validator->failed();
|
||||
|
||||
foreach ($validator->errors()->getMessages() as $attributeCode => $message) {
|
||||
$errorCode = array_key_first($failedAttributes[$attributeCode] ?? []);
|
||||
|
||||
$this->skipRow($rowNumber, $errorCode, $attributeCode, current($message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate leads attributes.
|
||||
*/
|
||||
$validator = Validator::make($rowData, [
|
||||
...$this->getValidationRules('leads|persons', $rowData),
|
||||
'id' => 'numeric',
|
||||
'status' => 'sometimes|required|in:0,1',
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'person_id' => 'required|exists:persons,id',
|
||||
'lead_source_id' => 'required|exists:lead_sources,id',
|
||||
'lead_type_id' => 'required|exists:lead_types,id',
|
||||
'lead_pipeline_id' => 'required|exists:lead_pipelines,id',
|
||||
'lead_pipeline_stage_id' => 'required|exists:lead_pipeline_stages,id',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$failedAttributes = $validator->failed();
|
||||
|
||||
foreach ($validator->errors()->getMessages() as $attributeCode => $message) {
|
||||
$errorCode = array_key_first($failedAttributes[$attributeCode] ?? []);
|
||||
|
||||
$this->skipRow($rowNumber, $errorCode, $attributeCode, current($message));
|
||||
}
|
||||
}
|
||||
|
||||
return ! $this->errorHelper->isRowInvalid($rowNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare row data for lead product.
|
||||
*/
|
||||
protected function parseProducts(?string $products): array
|
||||
{
|
||||
$productData = [];
|
||||
|
||||
$productArray = explode(',', $products);
|
||||
|
||||
foreach ($productArray as $product) {
|
||||
if (empty($product)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$key, $value] = explode('=', $product);
|
||||
|
||||
$productData[$key] = $value;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($productData['price'])
|
||||
&& isset($productData['quantity'])
|
||||
) {
|
||||
$productData['amount'] = $productData['price'] * $productData['quantity'];
|
||||
}
|
||||
|
||||
return $productData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation rules.
|
||||
*/
|
||||
public function getValidationRules(string $entityTypes, array $rowData): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
foreach (explode('|', $entityTypes) as $entityType) {
|
||||
$attributes = $this->attributeRepository->scopeQuery(fn ($query) => $query->whereIn('code', array_keys($rowData))->where('entity_type', $entityType))->get();
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
if ($entityType == 'persons') {
|
||||
$attribute->code = 'person.'.$attribute->code;
|
||||
}
|
||||
|
||||
$validations = [];
|
||||
|
||||
if ($attribute->type == 'boolean') {
|
||||
continue;
|
||||
} elseif ($attribute->type == 'address') {
|
||||
if (! $attribute->is_required) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$validations = [
|
||||
$attribute->code.'.address' => 'required',
|
||||
$attribute->code.'.country' => 'required',
|
||||
$attribute->code.'.state' => 'required',
|
||||
$attribute->code.'.city' => 'required',
|
||||
$attribute->code.'.postcode' => 'required',
|
||||
];
|
||||
} elseif ($attribute->type == 'email') {
|
||||
$validations = [
|
||||
$attribute->code => [$attribute->is_required ? 'required' : 'nullable'],
|
||||
$attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable', 'email'],
|
||||
$attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable',
|
||||
];
|
||||
} elseif ($attribute->type == 'phone') {
|
||||
$validations = [
|
||||
$attribute->code => [$attribute->is_required ? 'required' : 'nullable'],
|
||||
$attribute->code.'.*.value' => [$attribute->is_required ? 'required' : 'nullable'],
|
||||
$attribute->code.'.*.label' => $attribute->is_required ? 'required' : 'nullable',
|
||||
];
|
||||
} else {
|
||||
$validations[$attribute->code] = [$attribute->is_required ? 'required' : 'nullable'];
|
||||
|
||||
if ($attribute->type == 'text' && $attribute->validation) {
|
||||
array_push($validations[$attribute->code],
|
||||
$attribute->validation == 'decimal'
|
||||
? new Decimal
|
||||
: $attribute->validation
|
||||
);
|
||||
}
|
||||
|
||||
if ($attribute->type == 'price') {
|
||||
array_push($validations[$attribute->code], new Decimal);
|
||||
}
|
||||
}
|
||||
|
||||
if ($attribute->is_unique) {
|
||||
array_push($validations[in_array($attribute->type, ['email', 'phone'])
|
||||
? $attribute->code.'.*.value'
|
||||
: $attribute->code
|
||||
], function ($field, $value, $fail) use ($attribute) {
|
||||
if (! $this->attributeValueRepository->isValueUnique(
|
||||
null,
|
||||
$attribute->entity_type,
|
||||
$attribute,
|
||||
request($field)
|
||||
)
|
||||
) {
|
||||
$fail(trans('data_transfer::app.validation.errors.already-exists', ['attribute' => $attribute->name]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$rules = [
|
||||
...$rules,
|
||||
...$validations,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the import process.
|
||||
*/
|
||||
public function importBatch(ImportBatchContract $batch): bool
|
||||
{
|
||||
Event::dispatch('data_transfer.imports.batch.import.before', $batch);
|
||||
|
||||
if ($batch->import->action == Import::ACTION_DELETE) {
|
||||
$this->deleteLeads($batch);
|
||||
} else {
|
||||
$this->saveLeads($batch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update import batch summary.
|
||||
*/
|
||||
$batch = $this->importBatchRepository->update([
|
||||
'state' => Import::STATE_PROCESSED,
|
||||
|
||||
'summary' => [
|
||||
'created' => $this->getCreatedItemsCount(),
|
||||
'updated' => $this->getUpdatedItemsCount(),
|
||||
'deleted' => $this->getDeletedItemsCount(),
|
||||
],
|
||||
], $batch->id);
|
||||
|
||||
Event::dispatch('data_transfer.imports.batch.import.after', $batch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the products linking process
|
||||
*/
|
||||
public function linkBatch(ImportBatchContract $batch): bool
|
||||
{
|
||||
Event::dispatch('data_transfer.imports.batch.linking.before', $batch);
|
||||
|
||||
/**
|
||||
* Load leads storage with batch ids.
|
||||
*/
|
||||
$this->leadsStorage->load(Arr::pluck($batch->data, 'title'));
|
||||
|
||||
$products = [];
|
||||
|
||||
foreach ($batch->data as $rowData) {
|
||||
/**
|
||||
* Prepare products.
|
||||
*/
|
||||
$this->prepareProducts($rowData, $products);
|
||||
}
|
||||
|
||||
$this->saveProducts($products);
|
||||
|
||||
/**
|
||||
* Update import batch summary
|
||||
*/
|
||||
$this->importBatchRepository->update([
|
||||
'state' => Import::STATE_LINKED,
|
||||
], $batch->id);
|
||||
|
||||
Event::dispatch('data_transfer.imports.batch.linking.after', $batch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare products.
|
||||
*/
|
||||
public function prepareProducts($rowData, &$product): void
|
||||
{
|
||||
if (! empty($rowData['product'])) {
|
||||
$product[$rowData['title']] = $this->parseProducts($rowData['product']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save products.
|
||||
*/
|
||||
public function saveProducts(array $products): void
|
||||
{
|
||||
$leadProducts = [];
|
||||
|
||||
foreach ($products as $title => $product) {
|
||||
$lead = $this->leadsStorage->get($title);
|
||||
|
||||
$leadProducts['insert'][] = [
|
||||
'lead_id' => $lead['id'],
|
||||
'product_id' => $product['id'],
|
||||
'price' => $product['price'],
|
||||
'quantity' => $product['quantity'],
|
||||
'amount' => $product['amount'],
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($leadProducts['insert'] as $key => $leadProduct) {
|
||||
$this->leadProductRepository->deleteWhere([
|
||||
'lead_id' => $leadProduct['lead_id'],
|
||||
'product_id' => $leadProduct['product_id'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->leadProductRepository->upsert($leadProducts['insert'], ['lead_id', 'product_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete leads from current batch.
|
||||
*/
|
||||
protected function deleteLeads(ImportBatchContract $batch): bool
|
||||
{
|
||||
/**
|
||||
* Load leads storage with batch ids.
|
||||
*/
|
||||
$this->leadsStorage->load(Arr::pluck($batch->data, 'title'));
|
||||
|
||||
$idsToDelete = [];
|
||||
|
||||
foreach ($batch->data as $rowData) {
|
||||
if (! $this->isTitleExist($rowData['title'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$idsToDelete[] = $this->leadsStorage->get($rowData['title']);
|
||||
}
|
||||
|
||||
$idsToDelete = array_unique($idsToDelete);
|
||||
|
||||
$this->deletedItemsCount = count($idsToDelete);
|
||||
|
||||
$this->leadRepository->deleteWhere([['id', 'IN', $idsToDelete]]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save leads from current batch.
|
||||
*/
|
||||
protected function saveLeads(ImportBatchContract $batch): bool
|
||||
{
|
||||
/**
|
||||
* Load lead storage with batch unique title.
|
||||
*/
|
||||
$this->leadsStorage->load(Arr::pluck($batch->data, 'title'));
|
||||
|
||||
$leads = [];
|
||||
|
||||
/**
|
||||
* Prepare leads for import.
|
||||
*/
|
||||
foreach ($batch->data as $rowData) {
|
||||
if (isset($rowData['id'])) {
|
||||
$leads['update'][$rowData['id']] = Arr::except($rowData, ['product']);
|
||||
} else {
|
||||
$leads['insert'][$rowData['title']] = [
|
||||
...Arr::except($rowData, ['id', 'product']),
|
||||
'created_at' => $rowData['created_at'] ?? now(),
|
||||
'updated_at' => $rowData['updated_at'] ?? now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($leads['update'])) {
|
||||
$this->updatedItemsCount += count($leads['update']);
|
||||
|
||||
$this->leadRepository->upsert(
|
||||
$leads['update'],
|
||||
$this->masterAttributeCode
|
||||
);
|
||||
}
|
||||
|
||||
if (! empty($leads['insert'])) {
|
||||
$this->createdItemsCount += count($leads['insert']);
|
||||
|
||||
$this->leadRepository->insert($leads['insert']);
|
||||
|
||||
/**
|
||||
* Update the sku storage with newly created products
|
||||
*/
|
||||
$newLeads = $this->leadRepository->findWhereIn(
|
||||
'title',
|
||||
array_keys($leads['insert']),
|
||||
[
|
||||
'id',
|
||||
'title',
|
||||
]
|
||||
);
|
||||
|
||||
foreach ($newLeads as $lead) {
|
||||
$this->leadsStorage->set($lead->title, [
|
||||
'id' => $lead->id,
|
||||
'title' => $lead->title,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if title exists.
|
||||
*/
|
||||
public function isTitleExist(string $title): bool
|
||||
{
|
||||
return $this->leadsStorage->has($title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare row data to save into the database.
|
||||
*/
|
||||
protected function prepareRowForDb(array $rowData): array
|
||||
{
|
||||
return parent::prepareRowForDb($rowData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers\Importers\Leads;
|
||||
|
||||
use Webkul\Lead\Repositories\LeadRepository;
|
||||
|
||||
class Storage
|
||||
{
|
||||
/**
|
||||
* Items contains identifier as key and product information as value.
|
||||
*/
|
||||
protected array $items = [];
|
||||
|
||||
/**
|
||||
* Columns which will be selected from database.
|
||||
*/
|
||||
protected array $selectColumns = ['id', 'title'];
|
||||
|
||||
/**
|
||||
* Create a new helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected LeadRepository $leadRepository) {}
|
||||
|
||||
/**
|
||||
* Initialize storage.
|
||||
*/
|
||||
public function init(): void
|
||||
{
|
||||
$this->items = [];
|
||||
|
||||
$this->load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the leads.
|
||||
*/
|
||||
public function load(array $titles = []): void
|
||||
{
|
||||
if (empty($titles)) {
|
||||
$leads = $this->leadRepository->all($this->selectColumns);
|
||||
} else {
|
||||
$leads = $this->leadRepository->findWhereIn('title', $titles, $this->selectColumns);
|
||||
}
|
||||
|
||||
foreach ($leads as $lead) {
|
||||
$this->set($lead->title, [
|
||||
'id' => $lead->id,
|
||||
'title' => $lead->title,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Ids and Unique Id.
|
||||
*/
|
||||
public function set(string $title, array $data): self
|
||||
{
|
||||
$this->items[$title] = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if unique id exists.
|
||||
*/
|
||||
public function has(string $title): bool
|
||||
{
|
||||
return isset($this->items[$title]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique id information.
|
||||
*/
|
||||
public function get(string $title): ?array
|
||||
{
|
||||
if (! $this->has($title)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->items[$title];
|
||||
}
|
||||
|
||||
public function getItems(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is storage is empty.
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers\Importers\Persons;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\Attribute\Repositories\AttributeValueRepository;
|
||||
use Webkul\Contact\Repositories\PersonRepository;
|
||||
use Webkul\DataTransfer\Contracts\ImportBatch as ImportBatchContract;
|
||||
use Webkul\DataTransfer\Helpers\Import;
|
||||
use Webkul\DataTransfer\Helpers\Importers\AbstractImporter;
|
||||
use Webkul\DataTransfer\Repositories\ImportBatchRepository;
|
||||
|
||||
class Importer extends AbstractImporter
|
||||
{
|
||||
/**
|
||||
* Error code for non existing email.
|
||||
*/
|
||||
const ERROR_EMAIL_NOT_FOUND_FOR_DELETE = 'email_not_found_to_delete';
|
||||
|
||||
/**
|
||||
* Error code for duplicated email.
|
||||
*/
|
||||
const ERROR_DUPLICATE_EMAIL = 'duplicated_email';
|
||||
|
||||
/**
|
||||
* Error code for duplicated phone.
|
||||
*/
|
||||
const ERROR_DUPLICATE_PHONE = 'duplicated_phone';
|
||||
|
||||
/**
|
||||
* Permanent entity columns.
|
||||
*/
|
||||
protected array $validColumnNames = [
|
||||
'contact_numbers',
|
||||
'emails',
|
||||
'job_title',
|
||||
'name',
|
||||
'organization_id',
|
||||
'user_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* Error message templates.
|
||||
*/
|
||||
protected array $messages = [
|
||||
self::ERROR_EMAIL_NOT_FOUND_FOR_DELETE => 'data_transfer::app.importers.persons.validation.errors.email-not-found',
|
||||
self::ERROR_DUPLICATE_EMAIL => 'data_transfer::app.importers.persons.validation.errors.duplicate-email',
|
||||
self::ERROR_DUPLICATE_PHONE => 'data_transfer::app.importers.persons.validation.errors.duplicate-phone',
|
||||
];
|
||||
|
||||
/**
|
||||
* Permanent entity columns.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $permanentAttributes = ['emails'];
|
||||
|
||||
/**
|
||||
* Permanent entity column.
|
||||
*/
|
||||
protected string $masterAttributeCode = 'unique_id';
|
||||
|
||||
/**
|
||||
* Emails storage.
|
||||
*/
|
||||
protected array $emails = [];
|
||||
|
||||
/**
|
||||
* Phones storage.
|
||||
*/
|
||||
protected array $phones = [];
|
||||
|
||||
/**
|
||||
* Create a new helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ImportBatchRepository $importBatchRepository,
|
||||
protected PersonRepository $personRepository,
|
||||
protected AttributeRepository $attributeRepository,
|
||||
protected AttributeValueRepository $attributeValueRepository,
|
||||
protected Storage $personStorage,
|
||||
) {
|
||||
parent::__construct(
|
||||
$importBatchRepository,
|
||||
$attributeRepository,
|
||||
$attributeValueRepository,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Product error templates.
|
||||
*/
|
||||
protected function initErrorMessages(): void
|
||||
{
|
||||
foreach ($this->messages as $errorCode => $message) {
|
||||
$this->errorHelper->addErrorMessage($errorCode, trans($message));
|
||||
}
|
||||
|
||||
parent::initErrorMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate data.
|
||||
*/
|
||||
public function validateData(): void
|
||||
{
|
||||
$this->personStorage->init();
|
||||
|
||||
parent::validateData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates row.
|
||||
*/
|
||||
public function validateRow(array $rowData, int $rowNumber): bool
|
||||
{
|
||||
$rowData = $this->parsedRowData($rowData);
|
||||
|
||||
/**
|
||||
* If row is already validated than no need for further validation.
|
||||
*/
|
||||
if (isset($this->validatedRows[$rowNumber])) {
|
||||
return ! $this->errorHelper->isRowInvalid($rowNumber);
|
||||
}
|
||||
|
||||
$this->validatedRows[$rowNumber] = true;
|
||||
|
||||
/**
|
||||
* If import action is delete than no need for further validation.
|
||||
*/
|
||||
if ($this->import->action == Import::ACTION_DELETE) {
|
||||
foreach ($rowData['emails'] as $email) {
|
||||
if (! $this->isEmailExist($email['value'])) {
|
||||
$this->skipRow($rowNumber, self::ERROR_EMAIL_NOT_FOUND_FOR_DELETE, 'email');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate row data.
|
||||
*/
|
||||
$validator = Validator::make($rowData, [
|
||||
...$this->getValidationRules('persons', $rowData),
|
||||
'organization_id' => 'required|exists:organizations,id',
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'contact_numbers' => 'required|array',
|
||||
'contact_numbers.*.value' => 'required|numeric',
|
||||
'contact_numbers.*.label' => 'required|in:home,work',
|
||||
'emails' => 'required|array',
|
||||
'emails.*.value' => 'required|email',
|
||||
'emails.*.label' => 'required|in:home,work',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$failedAttributes = $validator->failed();
|
||||
|
||||
foreach ($validator->errors()->getMessages() as $attributeCode => $message) {
|
||||
$errorCode = array_key_first($failedAttributes[$attributeCode] ?? []);
|
||||
|
||||
$this->skipRow($rowNumber, $errorCode, $attributeCode, current($message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if email is unique.
|
||||
*/
|
||||
if (! empty($emails = $rowData['emails'])) {
|
||||
foreach ($emails as $email) {
|
||||
if (! in_array($email['value'], $this->emails)) {
|
||||
$this->emails[] = $email['value'];
|
||||
} else {
|
||||
$message = sprintf(
|
||||
trans($this->messages[self::ERROR_DUPLICATE_EMAIL]),
|
||||
$email['value']
|
||||
);
|
||||
|
||||
$this->skipRow($rowNumber, self::ERROR_DUPLICATE_EMAIL, 'email', $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if phone(s) are unique.
|
||||
*/
|
||||
if (! empty($rowData['contact_numbers'])) {
|
||||
foreach ($rowData['contact_numbers'] as $phone) {
|
||||
if (! in_array($phone['value'], $this->phones)) {
|
||||
if (! empty($phone['value'])) {
|
||||
$this->phones[] = $phone['value'];
|
||||
}
|
||||
} else {
|
||||
$message = sprintf(
|
||||
trans($this->messages[self::ERROR_DUPLICATE_PHONE]),
|
||||
$phone['value']
|
||||
);
|
||||
|
||||
$this->skipRow($rowNumber, self::ERROR_DUPLICATE_PHONE, 'phone', $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ! $this->errorHelper->isRowInvalid($rowNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the import process.
|
||||
*/
|
||||
public function importBatch(ImportBatchContract $batch): bool
|
||||
{
|
||||
Event::dispatch('data_transfer.imports.batch.import.before', $batch);
|
||||
|
||||
if ($batch->import->action == Import::ACTION_DELETE) {
|
||||
$this->deletePersons($batch);
|
||||
} else {
|
||||
$this->savePersonData($batch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update import batch summary.
|
||||
*/
|
||||
$batch = $this->importBatchRepository->update([
|
||||
'state' => Import::STATE_PROCESSED,
|
||||
|
||||
'summary' => [
|
||||
'created' => $this->getCreatedItemsCount(),
|
||||
'updated' => $this->getUpdatedItemsCount(),
|
||||
'deleted' => $this->getDeletedItemsCount(),
|
||||
],
|
||||
], $batch->id);
|
||||
|
||||
Event::dispatch('data_transfer.imports.batch.import.after', $batch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete persons from current batch.
|
||||
*/
|
||||
protected function deletePersons(ImportBatchContract $batch): bool
|
||||
{
|
||||
/**
|
||||
* Load person storage with batch emails.
|
||||
*/
|
||||
$emails = collect(Arr::pluck($batch->data, 'emails'))
|
||||
->map(function ($emails) {
|
||||
$emails = json_decode($emails, true);
|
||||
|
||||
foreach ($emails as $email) {
|
||||
return $email['value'];
|
||||
}
|
||||
});
|
||||
|
||||
$this->personStorage->load($emails->toArray());
|
||||
|
||||
$idsToDelete = [];
|
||||
|
||||
foreach ($batch->data as $rowData) {
|
||||
$rowData = $this->parsedRowData($rowData);
|
||||
|
||||
foreach ($rowData['emails'] as $email) {
|
||||
if (! $this->isEmailExist($email['value'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$idsToDelete[] = $this->personStorage->get($email['value']);
|
||||
}
|
||||
}
|
||||
|
||||
$idsToDelete = array_unique($idsToDelete);
|
||||
|
||||
$this->deletedItemsCount = count($idsToDelete);
|
||||
|
||||
$this->personRepository->deleteWhere([['id', 'IN', $idsToDelete]]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save person from current batch.
|
||||
*/
|
||||
protected function savePersonData(ImportBatchContract $batch): bool
|
||||
{
|
||||
/**
|
||||
* Load person storage with batch email.
|
||||
*/
|
||||
$emails = collect(Arr::pluck($batch->data, 'emails'))
|
||||
->map(function ($emails) {
|
||||
$emails = json_decode($emails, true);
|
||||
|
||||
foreach ($emails as $email) {
|
||||
return $email['value'];
|
||||
}
|
||||
});
|
||||
|
||||
$this->personStorage->load($emails->toArray());
|
||||
|
||||
$persons = [];
|
||||
|
||||
$attributeValues = [];
|
||||
|
||||
/**
|
||||
* Prepare persons for import.
|
||||
*/
|
||||
foreach ($batch->data as $rowData) {
|
||||
$this->preparePersons($rowData, $persons);
|
||||
|
||||
$this->prepareAttributeValues($rowData, $attributeValues);
|
||||
}
|
||||
|
||||
$this->savePersons($persons);
|
||||
|
||||
$this->saveAttributeValues($attributeValues);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare persons from current batch.
|
||||
*/
|
||||
public function preparePersons(array $rowData, array &$persons): void
|
||||
{
|
||||
$emails = $this->prepareEmail($rowData['emails']);
|
||||
|
||||
foreach ($emails as $email) {
|
||||
$contactNumber = json_decode($rowData['contact_numbers'], true);
|
||||
|
||||
$rowData['unique_id'] = "{$rowData['user_id']}|{$rowData['organization_id']}|{$email}|{$contactNumber[0]['value']}";
|
||||
|
||||
if ($this->isEmailExist($email)) {
|
||||
$persons['update'][$email] = $rowData;
|
||||
} else {
|
||||
$persons['insert'][$email] = [
|
||||
...$rowData,
|
||||
'created_at' => $rowData['created_at'] ?? now(),
|
||||
'updated_at' => $rowData['updated_at'] ?? now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save persons from current batch.
|
||||
*/
|
||||
public function savePersons(array $persons): void
|
||||
{
|
||||
if (! empty($persons['update'])) {
|
||||
$this->updatedItemsCount += count($persons['update']);
|
||||
|
||||
$this->personRepository->upsert(
|
||||
$persons['update'],
|
||||
$this->masterAttributeCode,
|
||||
);
|
||||
}
|
||||
|
||||
if (! empty($persons['insert'])) {
|
||||
$this->createdItemsCount += count($persons['insert']);
|
||||
|
||||
$this->personRepository->insert($persons['insert']);
|
||||
|
||||
/**
|
||||
* Update the sku storage with newly created products
|
||||
*/
|
||||
$emails = array_keys($persons['insert']);
|
||||
|
||||
$newPersons = $this->personRepository->where(function ($query) use ($emails) {
|
||||
foreach ($emails as $email) {
|
||||
$query->orWhereJsonContains('emails', [['value' => $email]]);
|
||||
}
|
||||
})->get();
|
||||
|
||||
foreach ($newPersons as $person) {
|
||||
$this->personStorage->set($person->emails[0]['value'], $person->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save attribute values for the person.
|
||||
*/
|
||||
public function saveAttributeValues(array $attributeValues): void
|
||||
{
|
||||
$personAttributeValues = [];
|
||||
|
||||
foreach ($attributeValues as $email => $attributeValue) {
|
||||
foreach ($attributeValue as $attribute) {
|
||||
$attribute['entity_id'] = (int) $this->personStorage->get($email);
|
||||
|
||||
$attribute['unique_id'] = implode('|', array_filter([
|
||||
$attribute['entity_id'],
|
||||
$attribute['attribute_id'],
|
||||
]));
|
||||
|
||||
$attribute['entity_type'] = 'persons';
|
||||
|
||||
$personAttributeValues[$attribute['unique_id']] = $attribute;
|
||||
}
|
||||
}
|
||||
|
||||
$this->attributeValueRepository->upsert($personAttributeValues, 'unique_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if email exists.
|
||||
*/
|
||||
public function isEmailExist(string $email): bool
|
||||
{
|
||||
return $this->personStorage->has($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare attribute values for the person.
|
||||
*/
|
||||
public function prepareAttributeValues(array $rowData, array &$attributeValues): void
|
||||
{
|
||||
foreach ($rowData as $code => $value) {
|
||||
if (is_null($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$where = ['code' => $code];
|
||||
|
||||
if ($code === 'name') {
|
||||
$where['entity_type'] = 'persons';
|
||||
}
|
||||
|
||||
$attribute = $this->attributeRepository->findOneWhere($where);
|
||||
|
||||
if (! $attribute) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$typeFields = $this->personRepository->getModel()::$attributeTypeFields;
|
||||
|
||||
$attributeTypeValues = array_fill_keys(array_values($typeFields), null);
|
||||
|
||||
$emails = $this->prepareEmail($rowData['emails']);
|
||||
|
||||
foreach ($emails as $email) {
|
||||
$attributeValues[$email][] = array_merge($attributeTypeValues, [
|
||||
'attribute_id' => $attribute->id,
|
||||
$typeFields[$attribute->type] => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed email and phone.
|
||||
*/
|
||||
private function parsedRowData(array $rowData): array
|
||||
{
|
||||
$rowData['emails'] = json_decode($rowData['emails'], true);
|
||||
|
||||
$rowData['contact_numbers'] = json_decode($rowData['contact_numbers'], true);
|
||||
|
||||
return $rowData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare email from row data.
|
||||
*/
|
||||
private function prepareEmail(array|string $emails): Collection
|
||||
{
|
||||
static $cache = [];
|
||||
|
||||
return collect($emails)
|
||||
->map(function ($emailString) use (&$cache) {
|
||||
if (isset($cache[$emailString])) {
|
||||
return $cache[$emailString];
|
||||
}
|
||||
|
||||
$decoded = json_decode($emailString, true);
|
||||
|
||||
$emailValue = is_array($decoded)
|
||||
&& isset($decoded[0]['value'])
|
||||
? $decoded[0]['value']
|
||||
: null;
|
||||
|
||||
return $cache[$emailString] = $emailValue;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers\Importers\Persons;
|
||||
|
||||
use Webkul\Contact\Repositories\PersonRepository;
|
||||
|
||||
class Storage
|
||||
{
|
||||
/**
|
||||
* Items contains email as key and product information as value.
|
||||
*/
|
||||
protected array $items = [];
|
||||
|
||||
/**
|
||||
* Columns which will be selected from database.
|
||||
*/
|
||||
protected array $selectColumns = [
|
||||
'id',
|
||||
'emails',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected PersonRepository $personRepository) {}
|
||||
|
||||
/**
|
||||
* Initialize storage.
|
||||
*/
|
||||
public function init(): void
|
||||
{
|
||||
$this->items = [];
|
||||
|
||||
$this->load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the Emails.
|
||||
*/
|
||||
public function load(array $emails = []): void
|
||||
{
|
||||
if (empty($emails)) {
|
||||
$persons = $this->personRepository->all($this->selectColumns);
|
||||
} else {
|
||||
$persons = $this->personRepository->scopeQuery(function ($query) use ($emails) {
|
||||
return $query->where(function ($subQuery) use ($emails) {
|
||||
foreach ($emails as $email) {
|
||||
$subQuery->orWhereJsonContains('emails', ['value' => $email]);
|
||||
}
|
||||
});
|
||||
})->all($this->selectColumns);
|
||||
}
|
||||
|
||||
$persons->each(function ($person) {
|
||||
collect($person->emails)
|
||||
->each(fn ($email) => $this->set($email['value'], $person->id));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email information.
|
||||
*/
|
||||
public function set(string $email, int $id): self
|
||||
{
|
||||
$this->items[$email] = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if email exists.
|
||||
*/
|
||||
public function has(string $email): bool
|
||||
{
|
||||
return isset($this->items[$email]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email information.
|
||||
*/
|
||||
public function get(string $email): ?int
|
||||
{
|
||||
if (! $this->has($email)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->items[$email];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is storage is empty.
|
||||
*/
|
||||
public function isEmpty(): int
|
||||
{
|
||||
return empty($this->items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers\Importers\Products;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Webkul\Attribute\Repositories\AttributeOptionRepository;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\Attribute\Repositories\AttributeValueRepository;
|
||||
use Webkul\DataTransfer\Contracts\ImportBatch as ImportBatchContract;
|
||||
use Webkul\DataTransfer\Helpers\Import;
|
||||
use Webkul\DataTransfer\Helpers\Importers\AbstractImporter;
|
||||
use Webkul\DataTransfer\Repositories\ImportBatchRepository;
|
||||
use Webkul\Product\Repositories\ProductInventoryRepository;
|
||||
use Webkul\Product\Repositories\ProductRepository;
|
||||
|
||||
class Importer extends AbstractImporter
|
||||
{
|
||||
/**
|
||||
* Error code for non existing SKU.
|
||||
*/
|
||||
const ERROR_SKU_NOT_FOUND_FOR_DELETE = 'sku_not_found_to_delete';
|
||||
|
||||
/**
|
||||
* Error message templates.
|
||||
*/
|
||||
protected array $messages = [
|
||||
self::ERROR_SKU_NOT_FOUND_FOR_DELETE => 'data_transfer::app.importers.products.validation.errors.sku-not-found',
|
||||
];
|
||||
|
||||
/**
|
||||
* Permanent entity columns.
|
||||
*/
|
||||
protected array $permanentAttributes = ['sku'];
|
||||
|
||||
/**
|
||||
* Permanent entity column.
|
||||
*/
|
||||
protected string $masterAttributeCode = 'sku';
|
||||
|
||||
/**
|
||||
* Cached attributes.
|
||||
*/
|
||||
protected mixed $attributes = [];
|
||||
|
||||
/**
|
||||
* Valid csv columns.
|
||||
*/
|
||||
protected array $validColumnNames = [
|
||||
'sku',
|
||||
'name',
|
||||
'description',
|
||||
'quantity',
|
||||
'price',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected ImportBatchRepository $importBatchRepository,
|
||||
protected AttributeRepository $attributeRepository,
|
||||
protected AttributeOptionRepository $attributeOptionRepository,
|
||||
protected ProductRepository $productRepository,
|
||||
protected ProductInventoryRepository $productInventoryRepository,
|
||||
protected AttributeValueRepository $attributeValueRepository,
|
||||
protected SKUStorage $skuStorage
|
||||
) {
|
||||
parent::__construct(
|
||||
$importBatchRepository,
|
||||
$attributeRepository,
|
||||
$attributeValueRepository
|
||||
);
|
||||
|
||||
$this->initAttributes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all attributes and families to use later.
|
||||
*/
|
||||
protected function initAttributes(): void
|
||||
{
|
||||
$this->attributes = $this->attributeRepository->all();
|
||||
|
||||
foreach ($this->attributes as $attribute) {
|
||||
$this->validColumnNames[] = $attribute->code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Product error templates.
|
||||
*/
|
||||
protected function initErrorMessages(): void
|
||||
{
|
||||
foreach ($this->messages as $errorCode => $message) {
|
||||
$this->errorHelper->addErrorMessage($errorCode, trans($message));
|
||||
}
|
||||
|
||||
parent::initErrorMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save validated batches.
|
||||
*/
|
||||
protected function saveValidatedBatches(): self
|
||||
{
|
||||
$source = $this->getSource();
|
||||
|
||||
$source->rewind();
|
||||
|
||||
$this->skuStorage->init();
|
||||
|
||||
while ($source->valid()) {
|
||||
try {
|
||||
$rowData = $source->current();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$source->next();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->validateRow($rowData, $source->getCurrentRowNumber());
|
||||
|
||||
$source->next();
|
||||
}
|
||||
|
||||
parent::saveValidatedBatches();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates row.
|
||||
*/
|
||||
public function validateRow(array $rowData, int $rowNumber): bool
|
||||
{
|
||||
/**
|
||||
* If row is already validated than no need for further validation.
|
||||
*/
|
||||
if (isset($this->validatedRows[$rowNumber])) {
|
||||
return ! $this->errorHelper->isRowInvalid($rowNumber);
|
||||
}
|
||||
|
||||
$this->validatedRows[$rowNumber] = true;
|
||||
|
||||
/**
|
||||
* If import action is delete than no need for further validation.
|
||||
*/
|
||||
if ($this->import->action == Import::ACTION_DELETE) {
|
||||
if (! $this->isSKUExist($rowData['sku'])) {
|
||||
$this->skipRow($rowNumber, self::ERROR_SKU_NOT_FOUND_FOR_DELETE, 'sku');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate product attributes
|
||||
*/
|
||||
$validator = Validator::make($rowData, $this->getValidationRules('products', $rowData));
|
||||
|
||||
if ($validator->fails()) {
|
||||
foreach ($validator->errors()->getMessages() as $attributeCode => $message) {
|
||||
$failedAttributes = $validator->failed();
|
||||
|
||||
$errorCode = array_key_first($failedAttributes[$attributeCode] ?? []);
|
||||
|
||||
$this->skipRow($rowNumber, $errorCode, $attributeCode, current($message));
|
||||
}
|
||||
}
|
||||
|
||||
return ! $this->errorHelper->isRowInvalid($rowNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the import process.
|
||||
*/
|
||||
public function importBatch(ImportBatchContract $batch): bool
|
||||
{
|
||||
Event::dispatch('data_transfer.imports.batch.import.before', $batch);
|
||||
|
||||
if ($batch->import->action == Import::ACTION_DELETE) {
|
||||
$this->deleteProducts($batch);
|
||||
} else {
|
||||
$this->saveProductsData($batch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update import batch summary.
|
||||
*/
|
||||
$batch = $this->importBatchRepository->update([
|
||||
'state' => Import::STATE_PROCESSED,
|
||||
|
||||
'summary' => [
|
||||
'created' => $this->getCreatedItemsCount(),
|
||||
'updated' => $this->getUpdatedItemsCount(),
|
||||
'deleted' => $this->getDeletedItemsCount(),
|
||||
],
|
||||
], $batch->id);
|
||||
|
||||
Event::dispatch('data_transfer.imports.batch.import.after', $batch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete products from current batch.
|
||||
*/
|
||||
protected function deleteProducts(ImportBatchContract $batch): bool
|
||||
{
|
||||
/**
|
||||
* Load SKU storage with batch skus.
|
||||
*/
|
||||
$this->skuStorage->load(Arr::pluck($batch->data, 'sku'));
|
||||
|
||||
$idsToDelete = [];
|
||||
|
||||
foreach ($batch->data as $rowData) {
|
||||
if (! $this->isSKUExist($rowData['sku'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$product = $this->skuStorage->get($rowData['sku']);
|
||||
|
||||
$idsToDelete[] = $product['id'];
|
||||
}
|
||||
|
||||
$idsToDelete = array_unique($idsToDelete);
|
||||
|
||||
$this->deletedItemsCount = count($idsToDelete);
|
||||
|
||||
$this->productRepository->deleteWhere([['id', 'IN', $idsToDelete]]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save products from current batch.
|
||||
*/
|
||||
protected function saveProductsData(ImportBatchContract $batch): bool
|
||||
{
|
||||
/**
|
||||
* Load SKU storage with batch skus.
|
||||
*/
|
||||
$this->skuStorage->load(Arr::pluck($batch->data, 'sku'));
|
||||
|
||||
$products = [];
|
||||
|
||||
/**
|
||||
* Prepare products for import.
|
||||
*/
|
||||
foreach ($batch->data as $rowData) {
|
||||
$this->prepareProducts($rowData, $products);
|
||||
}
|
||||
|
||||
$this->saveProducts($products);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare products from current batch.
|
||||
*/
|
||||
public function prepareProducts(array $rowData, array &$products): void
|
||||
{
|
||||
if ($this->isSKUExist($rowData['sku'])) {
|
||||
$products['update'][$rowData['sku']] = $rowData;
|
||||
} else {
|
||||
$products['insert'][$rowData['sku']] = [
|
||||
...$rowData,
|
||||
'created_at' => $rowData['created_at'] ?? now(),
|
||||
'updated_at' => $rowData['updated_at'] ?? now(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save products from current batch.
|
||||
*/
|
||||
public function saveProducts(array $products): void
|
||||
{
|
||||
if (! empty($products['update'])) {
|
||||
$this->updatedItemsCount += count($products['update']);
|
||||
|
||||
$this->productRepository->upsert(
|
||||
$products['update'],
|
||||
$this->masterAttributeCode
|
||||
);
|
||||
}
|
||||
|
||||
if (! empty($products['insert'])) {
|
||||
$this->createdItemsCount += count($products['insert']);
|
||||
|
||||
$this->productRepository->insert($products['insert']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save channels from current batch.
|
||||
*/
|
||||
public function saveChannels(array $channels): void
|
||||
{
|
||||
$productChannels = [];
|
||||
|
||||
foreach ($channels as $sku => $channelIds) {
|
||||
$product = $this->skuStorage->get($sku);
|
||||
|
||||
foreach (array_unique($channelIds) as $channelId) {
|
||||
$productChannels[] = [
|
||||
'product_id' => $product['id'],
|
||||
'channel_id' => $channelId,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
DB::table('product_channels')->upsert(
|
||||
$productChannels,
|
||||
[
|
||||
'product_id',
|
||||
'channel_id',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save links.
|
||||
*/
|
||||
public function loadUnloadedSKUs(array $skus): void
|
||||
{
|
||||
$notLoadedSkus = [];
|
||||
|
||||
foreach ($skus as $sku) {
|
||||
if ($this->skuStorage->has($sku)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$notLoadedSkus[] = $sku;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load not loaded SKUs to the sku storage.
|
||||
*/
|
||||
if (! empty($notLoadedSkus)) {
|
||||
$this->skuStorage->load($notLoadedSkus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SKU exists.
|
||||
*/
|
||||
public function isSKUExist(string $sku): bool
|
||||
{
|
||||
return $this->skuStorage->has($sku);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare row data to save into the database.
|
||||
*/
|
||||
protected function prepareRowForDb(array $rowData): array
|
||||
{
|
||||
return parent::prepareRowForDb($rowData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers\Importers\Products;
|
||||
|
||||
use Webkul\Product\Repositories\ProductRepository;
|
||||
|
||||
class SKUStorage
|
||||
{
|
||||
/**
|
||||
* Delimiter for SKU information.
|
||||
*/
|
||||
private const DELIMITER = '|';
|
||||
|
||||
/**
|
||||
* Items contains SKU as key and product information as value.
|
||||
*/
|
||||
protected array $items = [];
|
||||
|
||||
/**
|
||||
* Columns which will be selected from database.
|
||||
*/
|
||||
protected array $selectColumns = [
|
||||
'id',
|
||||
'sku',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(protected ProductRepository $productRepository) {}
|
||||
|
||||
/**
|
||||
* Initialize storage.
|
||||
*/
|
||||
public function init(): void
|
||||
{
|
||||
$this->items = [];
|
||||
|
||||
$this->load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the SKU.
|
||||
*/
|
||||
public function load(array $skus = []): void
|
||||
{
|
||||
if (empty($skus)) {
|
||||
$products = $this->productRepository->all($this->selectColumns);
|
||||
} else {
|
||||
$products = $this->productRepository->findWhereIn('sku', $skus, $this->selectColumns);
|
||||
}
|
||||
|
||||
foreach ($products as $product) {
|
||||
$this->set($product->sku, [
|
||||
'id' => $product->id,
|
||||
'sku' => $product->sku,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SKU information.
|
||||
*/
|
||||
public function set(string $sku, array $data): self
|
||||
{
|
||||
$this->items[$sku] = implode(self::DELIMITER, [
|
||||
$data['id'],
|
||||
$data['sku'],
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SKU exists.
|
||||
*/
|
||||
public function has(string $sku): bool
|
||||
{
|
||||
return isset($this->items[$sku]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SKU information.
|
||||
*/
|
||||
public function get(string $sku): ?array
|
||||
{
|
||||
if (! $this->has($sku)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = explode(self::DELIMITER, $this->items[$sku]);
|
||||
|
||||
return [
|
||||
'id' => $data[0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is storage is empty.
|
||||
*/
|
||||
public function isEmpty(): int
|
||||
{
|
||||
return empty($this->items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers\Sources;
|
||||
|
||||
use Webkul\DataTransfer\Helpers\Importers\AbstractImporter;
|
||||
|
||||
abstract class AbstractSource
|
||||
{
|
||||
/**
|
||||
* Column names.
|
||||
*/
|
||||
protected array $columnNames = [];
|
||||
|
||||
/**
|
||||
* Quantity of columns.
|
||||
*/
|
||||
protected int $totalColumns = 0;
|
||||
|
||||
/**
|
||||
* Current row.
|
||||
*/
|
||||
protected array $currentRowData = [];
|
||||
|
||||
/**
|
||||
* Current row number.
|
||||
*/
|
||||
protected int $currentRowNumber = -1;
|
||||
|
||||
/**
|
||||
* Flag to indicate that wrong quote was found.
|
||||
*/
|
||||
protected bool $foundWrongQuoteFlag = false;
|
||||
|
||||
/**
|
||||
* Read next line from source.
|
||||
*/
|
||||
abstract protected function getNextRow(): array|bool;
|
||||
|
||||
/**
|
||||
* Return the key of the current row.
|
||||
*/
|
||||
public function getCurrentRowNumber(): int
|
||||
{
|
||||
return $this->currentRowNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current position is valid.
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return $this->currentRowNumber !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read next line from source.
|
||||
*/
|
||||
public function current(): array
|
||||
{
|
||||
$row = $this->currentRowData;
|
||||
|
||||
if (count($row) != $this->totalColumns) {
|
||||
if ($this->foundWrongQuoteFlag) {
|
||||
throw new \InvalidArgumentException(AbstractImporter::ERROR_CODE_WRONG_QUOTES);
|
||||
} else {
|
||||
throw new \InvalidArgumentException(AbstractImporter::ERROR_CODE_COLUMNS_NUMBER);
|
||||
}
|
||||
}
|
||||
|
||||
return array_combine($this->columnNames, $row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read next line from source.
|
||||
*/
|
||||
public function next(): void
|
||||
{
|
||||
$this->currentRowNumber++;
|
||||
|
||||
$row = $this->getNextRow();
|
||||
|
||||
if ($row === false || $row === []) {
|
||||
$this->currentRowData = [];
|
||||
|
||||
$this->currentRowNumber = -1;
|
||||
} else {
|
||||
$this->currentRowData = $row;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the iterator to the first row.
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->currentRowNumber = 0;
|
||||
|
||||
$this->currentRowData = [];
|
||||
|
||||
$this->getNextRow();
|
||||
|
||||
$this->next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Column names getter.
|
||||
*/
|
||||
public function getColumnNames(): array
|
||||
{
|
||||
return $this->columnNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Column names getter.
|
||||
*/
|
||||
public function getTotalColumns(): int
|
||||
{
|
||||
return count($this->columnNames);
|
||||
}
|
||||
}
|
||||
79
packages/Webkul/DataTransfer/src/Helpers/Sources/CSV.php
Normal file
79
packages/Webkul/DataTransfer/src/Helpers/Sources/CSV.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers\Sources;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CSV extends AbstractSource
|
||||
{
|
||||
/**
|
||||
* CSV reader.
|
||||
*/
|
||||
protected mixed $reader;
|
||||
|
||||
/**
|
||||
* Create a new helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
string $filePath,
|
||||
protected string $delimiter = ','
|
||||
) {
|
||||
try {
|
||||
$this->reader = fopen(Storage::disk('public')->path($filePath), 'r');
|
||||
|
||||
$this->columnNames = fgetcsv($this->reader, 4096, $delimiter);
|
||||
|
||||
$this->totalColumns = count($this->columnNames);
|
||||
} catch (\Exception $e) {
|
||||
throw new \LogicException("Unable to open file: '{$filePath}'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close file handle.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (! is_object($this->reader)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->reader->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read next line from csv.
|
||||
*/
|
||||
protected function getNextRow(): array
|
||||
{
|
||||
$parsed = fgetcsv($this->reader, 4096, $this->delimiter);
|
||||
|
||||
if (is_array($parsed) && count($parsed) != $this->totalColumns) {
|
||||
foreach ($parsed as $element) {
|
||||
if ($element && strpos($element, "'") !== false) {
|
||||
$this->foundWrongQuoteFlag = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->foundWrongQuoteFlag = false;
|
||||
}
|
||||
|
||||
return is_array($parsed) ? $parsed : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the iterator to the first row.
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
rewind($this->reader);
|
||||
|
||||
parent::rewind();
|
||||
}
|
||||
}
|
||||
70
packages/Webkul/DataTransfer/src/Helpers/Sources/Excel.php
Normal file
70
packages/Webkul/DataTransfer/src/Helpers/Sources/Excel.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\DataTransfer\Helpers\Sources;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
|
||||
class Excel extends AbstractSource
|
||||
{
|
||||
/**
|
||||
* CSV reader.
|
||||
*/
|
||||
protected mixed $reader;
|
||||
|
||||
/**
|
||||
* Current row number.
|
||||
*/
|
||||
protected int $currentRowNumber = 1;
|
||||
|
||||
/**
|
||||
* Create a new helper instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(string $filePath)
|
||||
{
|
||||
try {
|
||||
$factory = IOFactory::load(Storage::disk('public')->path($filePath));
|
||||
|
||||
$this->reader = $factory->getActiveSheet();
|
||||
|
||||
$highestColumn = $this->reader->getHighestColumn();
|
||||
|
||||
$this->totalColumns = Coordinate::columnIndexFromString($highestColumn);
|
||||
|
||||
$this->columnNames = $this->getNextRow();
|
||||
} catch (\Exception $e) {
|
||||
throw new \LogicException("Unable to open file: '{$filePath}'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read next line from csv.
|
||||
*/
|
||||
protected function getNextRow(): array|bool
|
||||
{
|
||||
for ($column = 1; $column <= $this->totalColumns; $column++) {
|
||||
$rowData[] = $this->reader->getCellByColumnAndRow($column, $this->currentRowNumber)->getValue();
|
||||
}
|
||||
|
||||
$filteredRowData = array_filter($rowData);
|
||||
|
||||
if (empty($filteredRowData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $rowData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewind the iterator to the first row.
|
||||
*/
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->currentRowNumber = 1;
|
||||
|
||||
$this->next();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user