Files

94 lines
3.1 KiB
PHP

<?php
namespace App\Polls;
use App\Models\Poll;
use App\Models\PollResponse;
use Illuminate\Support\Collection;
/**
* The results of a poll as CSV rows: one row per vote, one column per question.
* Anonymous polls leave out who voted and when, and shuffle the rows.
*/
class ResultsCsv
{
public function __construct(private Poll $poll, private string $locale)
{
}
/** @return array<int, string> */
public function header(): array
{
$identity = $this->poll->anonymous ? [] : [
__('Voted'), __('Changed'), __('Profile type'), __('Profile'), __('Submitted by'),
];
return array_merge($identity, $this->questions()->map(fn ($question) => $question->labelFor($this->locale))->all());
}
/**
* Rows in chunks, so large polls are not loaded into memory at once.
*
* @return \Generator<int, array<int, string>>
*/
public function rows(): \Generator
{
$questions = $this->questions();
$query = $this->poll->responses()->with(['answers', 'voter', 'user']);
if ($this->poll->anonymous) {
// Shuffled, so the order does not reveal who voted when
foreach ($query->inRandomOrder()->get() as $response) {
yield $this->row($response, $questions);
}
return;
}
foreach ($query->orderBy('submitted_at')->orderBy('id')->lazy(200) as $response) {
yield $this->row($response, $questions);
}
}
private function row(PollResponse $response, Collection $questions): array
{
$answers = $response->answers->keyBy('poll_question_id');
$cells = [];
if (!$this->poll->anonymous) {
$changed = $response->updated_at && $response->updated_at->gt($response->submitted_at->copy()->addSecond());
$cells = [
$response->submitted_at->format('Y-m-d H:i'),
$changed ? $response->updated_at->format('Y-m-d H:i') : '',
__(class_basename($response->voter_type)),
$response->voter?->name ?? __('Deleted profile'),
$response->user?->name ?? '',
];
}
foreach ($questions as $question) {
$answer = $answers->get($question->id);
$cells[] = $answer
? $question->questionType()->answerToText($answer->value, $answer->text, $question->settings ?? [], $this->locale)
: '';
}
return array_map([self::class, 'safeCell'], $cells);
}
private function questions(): Collection
{
return $this->poll->questions()->get()->filter(
fn ($question) => app(QuestionTypeRegistry::class)->has($question->type)
)->values();
}
/**
* Prevent spreadsheet formula injection: a cell starting with = + - @ (or a tab / carriage return)
* would be run as a formula when the file is opened in Excel or LibreOffice.
*/
public static function safeCell(string $cell): string
{
return $cell !== '' && str_contains("=+-@\t\r", $cell[0]) ? "'" . $cell : $cell;
}
}