94 lines
3.1 KiB
PHP
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;
|
|
}
|
|
}
|