diff --git a/src/Common/Controllers/ResponseFactory.php b/src/Common/Controllers/ResponseFactory.php index c995cea1..9cccee6a 100644 --- a/src/Common/Controllers/ResponseFactory.php +++ b/src/Common/Controllers/ResponseFactory.php @@ -3,6 +3,7 @@ namespace VSV\GVQ_API\Common\Controllers; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; class ResponseFactory { @@ -31,6 +32,31 @@ public function createCsvResponse(string $data, string $model): Response return $response; } + /** + * @param \Closure $callBack + * @param string $model + * @return StreamedResponse + */ + public function createStreamedCsvResponse(\Closure $callBack, string $model): StreamedResponse + { + $response = new StreamedResponse(); + + $response->headers->set('Content-Encoding', 'UTF-8'); + $response->headers->set('Content-Type', 'application/csv; charset=UTF-8'); + $response->headers->set('Content-Transfer-Encoding', 'binary'); + + $now = new \DateTime(); + $fileName = $model.'_'.$now->format(\DateTime::ATOM).'.csv'; + $response->headers->set( + 'Content-Disposition', + 'attachment; filename="'.$fileName.'"' + ); + + $response->setCallback($callBack); + + return $response; + } + /** * @param string $data * @return string diff --git a/src/Contest/Controllers/ContestViewController.php b/src/Contest/Controllers/ContestViewController.php index 648231fd..3ba61f27 100644 --- a/src/Contest/Controllers/ContestViewController.php +++ b/src/Contest/Controllers/ContestViewController.php @@ -9,11 +9,13 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Translation\TranslatorInterface; use VSV\GVQ_API\Common\Controllers\ResponseFactory; use VSV\GVQ_API\Contest\Forms\ContestFormType; +use VSV\GVQ_API\Contest\Models\ContestParticipation; use VSV\GVQ_API\Contest\Models\TieBreaker; use VSV\GVQ_API\Contest\Repositories\TieBreakerRepository; use VSV\GVQ_API\Contest\Service\ContestService; @@ -166,24 +168,68 @@ public function contest(Request $request, string $quizId): Response } /** - * @return Response + * @return StreamedResponse */ public function export(): Response { - $contestParticipations = $this->contestService->getAll(); - $contestParticipationsAsCsv = $this->serializer->serialize( - $contestParticipations, - 'csv' - ); + $traversableContestParticipations = $this->contestService->getTraversableContestParticipations(); - $response = $this->responseFactory->createCsvResponse( - $contestParticipationsAsCsv, - 'contest_participations' - ); + $callback = $this->createCallBackForStreamedCsvResponse($traversableContestParticipations); + $response = $this->responseFactory->createStreamedCsvResponse($callback, 'contest_participations'); return $response; } + /** + * @param \Traversable $traversableContestParticipations + * @return \Closure + */ + private function createCallBackForStreamedCsvResponse(\Traversable $traversableContestParticipations): \Closure + { + return function () use ($traversableContestParticipations) { + $handle = fopen('php://output', 'r+'); + fwrite( + $handle, + chr(0xFF).chr(0xFE).$this->convertEncoding('sep=,'.PHP_EOL) + ); + + $headerSet = false; + foreach ($traversableContestParticipations as $contestParticipation) { + /** @var ContestParticipation $contestParticipation */ + + $contestParticipationAsCsv = $this->serializer->serialize( + $contestParticipation, + 'csv' + ); + $headerValuesArray = explode("\n", $contestParticipationAsCsv); + + if (!$headerSet) { + $header = $headerValuesArray[0]; + fwrite( + $handle, + $this->convertEncoding($header.PHP_EOL) + ); + $headerSet = true; + } + + fwrite( + $handle, + $this->convertEncoding($headerValuesArray[1].PHP_EOL) + ); + } + fclose($handle); + }; + } + + /** + * @param string $string + * @return string + */ + private function convertEncoding(string $string): string + { + return mb_convert_encoding($string, 'UTF-16LE', 'UTF-8'); + } + /** * @return FormInterface */ @@ -194,7 +240,7 @@ private function createContestForm(): FormInterface $this->contestFormType->buildForm( $formBuilder, [ - 'translator' => $this->translator + 'translator' => $this->translator, ] ); diff --git a/src/Contest/Repositories/ContestParticipationDoctrineRepository.php b/src/Contest/Repositories/ContestParticipationDoctrineRepository.php index 3f16ff89..ccefd026 100644 --- a/src/Contest/Repositories/ContestParticipationDoctrineRepository.php +++ b/src/Contest/Repositories/ContestParticipationDoctrineRepository.php @@ -64,6 +64,38 @@ public function getAll(): ?ContestParticipations return $this->toContestParticipations($contestParticipationEntities); } + /** + * @return \Traversable + */ + public function getAllAsTraversable(): \Traversable + { + $batchSize = 10; + $firstResult = 0; + + do { + $queryBuilder = $this->entityManager->createQueryBuilder(); + + $query = $queryBuilder->select('e') + ->from( + 'VSV\GVQ_API\Contest\Repositories\Entities\ContestParticipationEntity', + 'e' + ) + ->orderBy('e.id', 'ASC') + ->setMaxResults($batchSize) + ->setFirstResult($firstResult) + ->getQuery(); + + $currentBatchItemCount = 0; + + foreach ($query->iterate() as $contestParticipationEntities) { + $currentBatchItemCount++; + yield $contestParticipationEntities[0]->toContestParticipation(); + } + + $firstResult += $batchSize; + } while ($currentBatchItemCount == $batchSize); + } + /** * @param Year $year * @param Email $email diff --git a/src/Contest/Repositories/ContestParticipationRepository.php b/src/Contest/Repositories/ContestParticipationRepository.php index 73f4f9b2..d86de5e0 100644 --- a/src/Contest/Repositories/ContestParticipationRepository.php +++ b/src/Contest/Repositories/ContestParticipationRepository.php @@ -32,6 +32,11 @@ public function getByYearAndEmailAndChannel( */ public function getAll(): ?ContestParticipations; + /** + * @return \Traversable + */ + public function getAllAsTraversable(): \Traversable; + /** * @param Year $year * @param Email $email diff --git a/src/Contest/Service/ContestService.php b/src/Contest/Service/ContestService.php index 046cb990..4db3602c 100644 --- a/src/Contest/Service/ContestService.php +++ b/src/Contest/Service/ContestService.php @@ -51,6 +51,14 @@ public function getAll(): ?ContestParticipations return $this->contestParticipationRepository->getAll(); } + /** + * @return \Traversable + */ + public function getTraversableContestParticipations(): \Traversable + { + return $this->contestParticipationRepository->getAllAsTraversable(); + } + /** * Can only participate if 11 or more and no previous participation for given channel. *