startDateOption = new StartDateOption($this, 'visits'); $this->endDateOption = new EndDateOption($this, 'visits'); } final protected function execute(InputInterface $input, OutputInterface $output): ?int { $startDate = $this->startDateOption->get($input, $output); $endDate = $this->endDateOption->get($input, $output); $paginator = $this->getVisitsPaginator($input, buildDateRange($startDate, $endDate)); [$rows, $headers] = $this->resolveRowsAndHeaders($paginator); ShlinkTable::default($output)->render($headers, $rows); return ExitCode::EXIT_SUCCESS; } private function resolveRowsAndHeaders(Paginator $paginator): array { $extraKeys = []; $rows = array_map(function (Visit $visit) use (&$extraKeys) { $extraFields = $this->mapExtraFields($visit); $extraKeys = array_keys($extraFields); $rowData = [ ...$visit->jsonSerialize(), 'country' => $visit->getVisitLocation()?->getCountryName() ?? 'Unknown', 'city' => $visit->getVisitLocation()?->getCityName() ?? 'Unknown', ...$extraFields, ]; // Filter out unknown keys return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]); }, [...$paginator->getCurrentPageResults()]); $extra = array_map(camelCaseToHumanFriendly(...), $extraKeys); return [ $rows, ['Referer', 'Date', 'User agent', 'Country', 'City', ...$extra], ]; } abstract protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator; /** * @return array */ abstract protected function mapExtraFields(Visit $visit): array; }