connection->executeStatement( 'INSERT INTO student_import_batches (id, tenant_id, original_filename, total_rows, detected_columns, detected_format, status, mapping_data, imported_count, error_count, rows_data, created_at, completed_at) VALUES (:id, :tenant_id, :original_filename, :total_rows, :detected_columns, :detected_format, :status, :mapping_data, :imported_count, :error_count, :rows_data, :created_at, :completed_at) ON CONFLICT (id) DO UPDATE SET total_rows = EXCLUDED.total_rows, status = EXCLUDED.status, mapping_data = EXCLUDED.mapping_data, imported_count = EXCLUDED.imported_count, error_count = EXCLUDED.error_count, rows_data = EXCLUDED.rows_data, completed_at = EXCLUDED.completed_at', [ 'id' => (string) $batch->id, 'tenant_id' => (string) $batch->tenantId, 'original_filename' => $batch->originalFilename, 'total_rows' => $batch->totalRows, 'detected_columns' => json_encode($batch->detectedColumns, JSON_THROW_ON_ERROR), 'detected_format' => $batch->detectedFormat?->value, 'status' => $batch->status->value, 'mapping_data' => $batch->mapping !== null ? json_encode($this->serializeMapping($batch->mapping), JSON_THROW_ON_ERROR) : null, 'imported_count' => $batch->importedCount, 'error_count' => $batch->errorCount, 'rows_data' => json_encode($this->serializeRows($batch->lignes()), JSON_THROW_ON_ERROR), 'created_at' => $batch->createdAt->format(DateTimeImmutable::ATOM), 'completed_at' => $batch->completedAt?->format(DateTimeImmutable::ATOM), ], ); } #[Override] public function get(ImportBatchId $id): StudentImportBatch { return $this->findById($id) ?? throw ImportBatchNotFoundException::withId($id); } #[Override] public function findById(ImportBatchId $id): ?StudentImportBatch { $row = $this->connection->fetchAssociative( 'SELECT * FROM student_import_batches WHERE id = :id', ['id' => (string) $id], ); if ($row === false) { return null; } return $this->hydrate($row); } #[Override] public function findByTenant(TenantId $tenantId): array { $rows = $this->connection->fetchAllAssociative( 'SELECT * FROM student_import_batches WHERE tenant_id = :tenant_id ORDER BY created_at DESC', ['tenant_id' => (string) $tenantId], ); return array_map(fn ($row) => $this->hydrate($row), $rows); } /** * @param array $row */ private function hydrate(array $row): StudentImportBatch { /** @var string $id */ $id = $row['id']; /** @var string $tenantId */ $tenantId = $row['tenant_id']; /** @var string $originalFilename */ $originalFilename = $row['original_filename']; /** @var string|int $totalRowsRaw */ $totalRowsRaw = $row['total_rows']; $totalRows = (int) $totalRowsRaw; /** @var string $detectedColumnsJson */ $detectedColumnsJson = $row['detected_columns']; /** @var string|null $detectedFormat */ $detectedFormat = $row['detected_format']; /** @var string $status */ $status = $row['status']; /** @var string|null $mappingJson */ $mappingJson = $row['mapping_data']; /** @var string|int $importedCountRaw */ $importedCountRaw = $row['imported_count']; $importedCount = (int) $importedCountRaw; /** @var string|int $errorCountRaw */ $errorCountRaw = $row['error_count']; $errorCount = (int) $errorCountRaw; /** @var string $rowsJson */ $rowsJson = $row['rows_data']; /** @var string $createdAt */ $createdAt = $row['created_at']; /** @var string|null $completedAt */ $completedAt = $row['completed_at']; /** @var list $detectedColumns */ $detectedColumns = json_decode($detectedColumnsJson, true, 512, JSON_THROW_ON_ERROR); $mapping = $mappingJson !== null ? $this->hydrateMapping($mappingJson) : null; $batch = StudentImportBatch::reconstitute( id: ImportBatchId::fromString($id), tenantId: TenantId::fromString($tenantId), originalFilename: $originalFilename, totalRows: $totalRows, detectedColumns: $detectedColumns, detectedFormat: $detectedFormat !== null ? KnownImportFormat::from($detectedFormat) : null, status: ImportStatus::from($status), mapping: $mapping, importedCount: $importedCount, errorCount: $errorCount, createdAt: new DateTimeImmutable($createdAt), completedAt: $completedAt !== null ? new DateTimeImmutable($completedAt) : null, ); $rows = $this->hydrateRows($rowsJson); $batch->enregistrerLignes($rows); return $batch; } private function hydrateMapping(string $json): ColumnMapping { /** @var array{mapping: array, format: string} $data */ $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); /** @var array $mapping */ $mapping = []; foreach ($data['mapping'] as $column => $fieldValue) { $mapping[$column] = StudentImportField::from($fieldValue); } return ColumnMapping::creer($mapping, KnownImportFormat::from($data['format'])); } /** * @return list */ private function hydrateRows(string $json): array { /** @var list, mappedData: array, errors: list}> $data */ $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); return array_map( static fn (array $rowData) => new ImportRow( lineNumber: $rowData['lineNumber'], rawData: $rowData['rawData'], mappedData: $rowData['mappedData'], errors: array_map( static fn (array $err) => new ImportRowError($err['column'], $err['message']), $rowData['errors'], ), ), $data, ); } /** * @return array{mapping: array, format: string} */ private function serializeMapping(ColumnMapping $mapping): array { $serialized = []; foreach ($mapping->mapping as $column => $field) { $serialized[$column] = $field->value; } return [ 'mapping' => $serialized, 'format' => $mapping->format->value, ]; } /** * @param list $rows * * @return list, mappedData: array, errors: list}> */ private function serializeRows(array $rows): array { return array_map( static fn (ImportRow $row) => [ 'lineNumber' => $row->lineNumber, 'rawData' => $row->rawData, 'mappedData' => $row->mappedData, 'errors' => array_map( static fn (ImportRowError $error) => [ 'column' => $error->column, 'message' => $error->message, ], $row->errors, ), ], $rows, ); } }