1. Go to this page and download the library: Download jul6art/dataflow-bundle library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
use Jul6Art\DataflowBundle\Report\Spec\ReportSpecInterpreter;
$spec = new ReportSpecInterpreter()->interpret($savedDefinition); // your stored payload
$result = $runner->run($spec, $actor, limit: 50_000, scope: $this->scopeToTenant(...));
return $this->responses->stream($writer, $result->header(), $result->rows(), 'invoices');
use Jul6Art\DataflowBundle\Report\Format\ColumnFormat;
use Jul6Art\DataflowBundle\Report\Spec\ReportColumn;
new ReportColumn('total', 'Total', format: ColumnFormat::money('EUR'));
new ReportColumn('vatRate', 'VAT', format: ColumnFormat::percent(1));
new ReportColumn('issuedAt', 'Issued', format: ColumnFormat::date());
final readonly class NumberFormatterAdapter implements NumberFormatterInterface
{
public function __construct(private \Jul6Art\CoreBundle\Service\NumberFormatter $inner) {}
public function format(int|float|string|null $value, ?int $decimals = null): string
{
return $this->inner->format($value, $decimals);
}
public function formatMoney(int|float|string|null $value, string $currency, ?int $decimals = null): string
{
return $this->inner->formatMoney($value, $currency, $decimals);
}
public function formatPercent(int|float|string|null $value, int $decimals = 0): string
{
return $this->inner->formatPercent($value, $decimals);
}
}
final class CrmReportableEntityProvider implements ReportableEntityProviderInterface
{
public function entities(): array
{
return [
Contact::class => new ReportableEntity('report.entity.contact', 'crm:contact:read', 'crm.manage'),
];
}
}
use Jul6Art\DataflowBundle\Io\Dialect\CsvDialect;
use Jul6Art\DataflowBundle\Io\Reader\CsvReader;
$reader = new CsvReader(CsvDialect::excelFr());
foreach ($reader->read($path) as $record => $cells) {
// $record is 1-based and the header is record 1
}
use Jul6Art\DataflowBundle\Io\Reader\XlsxReader;
$reader = new XlsxReader();
foreach ($reader->read($path) as $record => $cells) {
// Same contract as CsvReader: 1-based, header
public function __construct(
#[AutowireIterator(tag: 'dataflow.tabular_reader')]
private readonly iterable $readers,
) {}
private function readerFor(string $filePath): TabularReaderInterface
{
foreach ($this->readers as $reader) {
if ($reader->supports($filePath)) {
return $reader;
}
}
throw UnreadableFileException::cannotOpen($filePath);
}
use Jul6Art\DataflowBundle\Import\HeaderInspector;
$inspector = new HeaderInspector();
$headers = $inspector->peek($reader, $path); // reads ONE record
$inspection = $inspector->inspect($headers, $mapper->fields());
$inspection->mapping; // [0 => 'firstName', 2 => 'email'] — column INDEX → field
$inspection->ambiguous; // indices whose header collides with another's
$inspection->unknown; // indices no field matched — normal, not an error
$inspection->missing; // fields no column supplied
use Jul6Art\DataflowBundle\Import\ImportRunner;
use Jul6Art\DataflowBundle\Import\Spec\ImportSpec;
$spec = new ImportSpec($path, $inspection->mapping, dryRun: true);
$report = $runner->run($spec, $mapper, $reader, $resolver);
$report->imported(); // rows that would land — see isDryRun() before wording this
$report->updated(); // rows that would overwrite an existing record — see onDuplicate below
$report->skipped();
$report->errorCount(); // exact
$report->errors(); // the first 100
$report->errorsWereTruncated();
use Jul6Art\DataflowBundle\Import\Spec\DuplicatePolicy;
use Jul6Art\DataflowBundle\Import\Spec\ImportSpec;
$spec = new ImportSpec($path, $inspection->mapping, onDuplicate: DuplicatePolicy::Update);
$report = $runner->run($spec, $mapper, $reader, $resolver);
$report->updated(); // rows that overwrote an existing record, disjoint from imported()
public function map(array $row, ?object $existing = null): Customer
{
$customer = $existing ?? new Customer();
$customer->name = $row['name'];
return $customer;
}
use Jul6Art\DataflowBundle\Import\Sink\CsvErrorSink;
$handle = fopen('php://temp', 'w+');
$sink = new CsvErrorSink($handle);
$report = $runner->run($spec, $mapper, $reader, $resolver, errorSink: $sink);
if ($report->errorCount() > 0) {
rewind($handle);
// stream $handle back as a download — every error, not the report's hundred-row sample
}
fclose($handle);
use Jul6Art\DataflowBundle\Import\TemplateBuilder;
use Jul6Art\DataflowBundle\Io\Http\TabularResponseFactory;
$builder = new TemplateBuilder();
return $this->responses->stream(
$writer,
$builder->header($mapper),
$builder->rows($mapper),
TabularResponseFactory::basename(['template', 'customers']),
);
use Jul6Art\DataflowBundle\Import\Async\ImportMessage;
$bus->dispatch(new ImportMessage(
importId: (string) Uuid::v4(), // yours: the key ImportProgressStoreInterface keys on
filePath: $uploadedPath,
mapping: $inspection->mapping,
mapperId: 'customer-import', // yours: what ImportMapperFactoryInterface turns back into a mapper
context: ['accountId' => $account->getId()],
));
final readonly class MyMapperFactory implements ImportMapperFactoryInterface
{
public function __construct(private EntityManagerInterface $em) {}
public function mapper(string $mapperId, array $context): RowMapperInterface
{
return match ($mapperId) {
'customer-import' => new CustomerRowMapper($this->em->getReference(Account::class, $context['accountId'])),
default => throw new \InvalidArgumentException($mapperId),
};
}
public function resolver(?string $resolverId, array $context): ?DuplicateResolverInterface
{
return null;
}
}
public function save(Request $request, ReportBuilderCsrfChecker $csrf): JsonResponse
{
if (!$csrf->isValid($request)) {
throw new AccessDeniedHttpException('invalid_csrf_token');
}
// …
}
protected static function declaredKeys(): array // the BROWSER's catalogue
{
return static::getContainer()->get(DeclaredTranslationKeys::class)->keys();
}
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.