Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/node_modules
/public/hot
/public/storage
/public/js
/public/css
/storage/*.key
/vendor
.idea
Expand Down
14 changes: 0 additions & 14 deletions app/Http/Controllers/CsvExport.php

This file was deleted.

51 changes: 51 additions & 0 deletions app/Http/Controllers/CsvExportController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Requests\CsvExportRequest;
use App\Services\Csv\CsvService;
use App\Services\Csv\Exceptions\InvalidColumnException;
use App\Services\Csv\Exceptions\InvalidRowException;

class CsvExportController extends Controller
{
/**
* @var CsvService
*/
private $service;

public function __construct(CsvService $service)
{
$this->service = $service;
}

/**
* Converts the user input into a CSV file and streams the file back to the user.
*/
public function convert(CsvExportRequest $request)
{
$inputDto = $request->getDto();

try {
$csv = $this->service->create($inputDto);

return response()->streamDownload(function () use ($csv) {
echo $csv;
}, 'data.csv');
} catch (InvalidColumnException $e) {
return response()->json([
'message' => $e->getMessage(),
'code' => $e->getCode(),
'row' => $e->getRowIndex(),
'column' => $e->getColumnIndex(),
], 400);
} catch (InvalidRowException $e) {
return response()->json([
'message' => $e->getMessage(),
'code' => $e->getCode(),
'row' => $e->getRowIndex(),
], 400);
}
}
}
38 changes: 38 additions & 0 deletions app/Http/Requests/CsvExportRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);

namespace App\Http\Requests;

use App\Services\Csv\InputDto;
use Illuminate\Foundation\Http\FormRequest;

/**
* @property string[] $header
* @property string[] $data
*
* Class CsvExportRequest
*
* @package App\Http\Requests
*/
class CsvExportRequest extends FormRequest
{
/**
* @return string[]
*/
public function rules(): array
{
return [
'header' => ['required', 'array'],
'header.*' => ['string', 'nullable'],

'data' => ['required', 'array'],
'data.*' => ['array'],
'data.*.*' => ['string', 'nullable'],
];
}

public function getDto(): InputDto
{
return InputDto::create($this->header, $this->data);
}
}
42 changes: 42 additions & 0 deletions app/Services/Csv/CsvService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);

namespace App\Services\Csv;

final class CsvService
{
/**
* @var InputValidator
*/
private $validator;

public function __construct(InputValidator $validator)
{
$this->validator = $validator;
}

/**
* @throws Exceptions\InvalidColumnException
* @throws Exceptions\InvalidRowException
*/
public function create(InputDto $inputDto): string
{
$this->validator->validate($inputDto);

$data = $inputDto->getData();
array_unshift($data, $inputDto->getHeader());

return $this->arrayToCsv($data);
}

private function arrayToCsv(array $data): string
{
$f = fopen('php://memory', 'r+');
foreach ($data as $item) {
fputcsv($f, $item);
}
rewind($f);

return stream_get_contents($f);
}
}
37 changes: 37 additions & 0 deletions app/Services/Csv/Exceptions/InvalidColumnException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);

namespace App\Services\Csv\Exceptions;

use Exception;

final class InvalidColumnException extends Exception
{
/**
* @var int
*/
private $rowIndex;

/**
* @var int
*/
private $columnIndex;

public function __construct(string $message, int $code, int $rowIndex, int $columnIndex)
{
parent::__construct($message, $code);

$this->rowIndex = $rowIndex;
$this->columnIndex = $columnIndex;
}

public function getRowIndex(): int
{
return $this->rowIndex;
}

public function getColumnIndex(): int
{
return $this->columnIndex;
}
}
26 changes: 26 additions & 0 deletions app/Services/Csv/Exceptions/InvalidRowException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);

namespace App\Services\Csv\Exceptions;

use Exception;

final class InvalidRowException extends Exception
{
/**
* @var int
*/
private $rowIndex;

public function __construct(string $message, int $code, int $rowIndex)
{
parent::__construct($message, $code);

$this->rowIndex = $rowIndex;
}

public function getRowIndex(): int
{
return $this->rowIndex;
}
}
38 changes: 38 additions & 0 deletions app/Services/Csv/InputDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);

namespace App\Services\Csv;

final class InputDto
{
/**
* @var array
*/
private $header;

/**
* @var array
*/
private $data;

private function __construct(array $header, array $data)
{
$this->header = $header;
$this->data = $data;
}

public function getHeader(): array
{
return $this->header;
}

public function getData(): array
{
return $this->data;
}

public static function create(array $header, array $data): self
{
return new self($header, $data);
}
}
86 changes: 86 additions & 0 deletions app/Services/Csv/InputValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);

namespace App\Services\Csv;

use App\Services\Csv\Exceptions\InvalidColumnException;
use App\Services\Csv\Exceptions\InvalidRowException;

final class InputValidator
{
private const ROW_COLUMNS_COUNT_ERROR_CODE = 1000;
private const COLUMN_LENGTH_ERROR_CODE = 2000;

/**
* @var int
*/
private $columnMaxLength;

public function __construct(int $columnMaxLength = 255)
{
$this->columnMaxLength = $columnMaxLength;
}

/**
* @throws InvalidColumnException
* @throws InvalidRowException
*/
public function validate(InputDto $input): void
{
$this->validateRowsColumnsCount($input);
$this->validateColumnsLength($input);
}

/**
* @throws InvalidRowException
*/
private function validateRowsColumnsCount(InputDto $input): void
{
$headerLen = count($input->getHeader());

$longerRow = collect($input->getData())->search(function (array $data) use ($headerLen) {
return count($data) > $headerLen;
});

if ($longerRow !== false) {
throw new InvalidRowException(
'Invalid row columns count, row must be smaller or equal to header',
self::ROW_COLUMNS_COUNT_ERROR_CODE,
$longerRow
);
}
}

/**
* @throws InvalidColumnException
*/
private function validateColumnsLength(InputDto $input): void
{
$data = $input->getData();
array_unshift($data, $input->getHeader());
$data = array_values($data);

foreach ($data as $rowIndex => $row) {
$invalidLenColumnIndex = collect($row)->search(function ($column) {
return $this->columnLength($column) > $this->columnMaxLength;
});

if ($invalidLenColumnIndex !== false) {
$message = sprintf(
'Invalid column %d:%d length, current length (%d) more than acceptable (%d)',
$rowIndex,
$invalidLenColumnIndex,
$this->columnLength($row[$invalidLenColumnIndex]),
$this->columnMaxLength
);

throw new InvalidColumnException($message, self::COLUMN_LENGTH_ERROR_CODE, $rowIndex, $invalidLenColumnIndex);
}
}
}

private function columnLength($column): int
{
return strlen((string) $column);
}
}
Loading