translator = $translatorInterface; $this->exportManager = $exportManager; } public function buildForm( FormBuilderInterface $builder, $exportAlias, array $aggregatorAliases ) { // choosing between formats $builder->add('format', ChoiceType::class, [ 'choices' => [ 'OpenDocument Format (.ods) (LibreOffice, ...)' => 'ods', 'Microsoft Excel 2007-2013 XML (.xlsx) (Microsoft Excel, LibreOffice)' => 'xlsx', 'Comma separated values (.csv)' => 'csv', ], 'placeholder' => 'Choose the format', ]); // ordering aggregators $aggregators = $this->exportManager->getAggregators($aggregatorAliases); $nb = count($aggregatorAliases); foreach ($aggregators as $alias => $aggregator) { $builderAggregator = $builder->create($alias, FormType::class, [ 'label' => $aggregator->getTitle(), 'block_name' => '_aggregator_placement_spreadsheet_formatter', ]); $this->appendAggregatorForm($builderAggregator, $nb); $builder->add($builderAggregator); } } public function getName() { return 'SpreadSheet (xlsx, ods)'; } public function getResponse( $result, $formatterData, $exportAlias, array $exportData, array $filtersData, array $aggregatorsData ) { // store all data when the process is initiated $this->result = $result; $this->formatterData = $formatterData; $this->export = $this->exportManager->getExport($exportAlias); $this->exportData = $exportData; $this->filtersData = $filtersData; $this->aggregatorsData = $aggregatorsData; // reset cache $this->cacheDisplayableResult = []; $this->cacheDisplayableResultIsInitialized = false; $response = new Response(); $response->headers->set( 'Content-Type', $this->getContentType($this->formatterData['format']) ); $this->tempfile = tempnam(sys_get_temp_dir(), ''); $this->generateContent(); $f = fopen($this->tempfile, 'r'); $response->setContent(stream_get_contents($f)); fclose($f); // remove the temp file from disk unlink($this->tempfile); return $response; } public function getType() { return 'tabular'; } protected function addContentTable( Worksheet $worksheet, $sortedResults, $line ) { $worksheet->fromArray( $sortedResults, null, 'A' . $line ); return $line + count($sortedResults) + 1; } /** * Add filter description since line 3. * * return the line number after the last description * * @return int the line number after the last description */ protected function addFiltersDescription(Worksheet &$worksheet) { $line = 3; foreach ($this->filtersData as $alias => $data) { $filter = $this->exportManager->getFilter($alias); $description = $filter->describeAction($data, 'string'); if (is_array($description)) { $description = $this->translator ->trans( $description[0], $description[1] ?? [] ); } $worksheet->setCellValue('A' . $line, $description); ++$line; } return $line; } /** * add headers to worksheet. * * return the line number where the next content (i.e. result) should * be appended. * * @param int $line * * @return int */ protected function addHeaders( Worksheet &$worksheet, array $globalKeys, $line ) { // get the displayable form of headers $displayables = []; foreach ($globalKeys as $key) { $displayables[] = $this->translator->trans( $this->getDisplayableResult($key, '_header') ); } // add headers on worksheet $worksheet->fromArray( $displayables, null, 'A' . $line ); return $line + 1; } /** * Add the title to the worksheet and merge the cell containing * the title. */ protected function addTitleToWorkSheet(Worksheet &$worksheet) { $worksheet->setCellValue('A1', $this->getTitle()); $worksheet->mergeCells('A1:G1'); } /** * Create a spreadsheet and a working worksheet. * * @return array where 1st member is spreadsheet, 2nd is worksheet */ protected function createSpreadsheet() { $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); $worksheet = $spreadsheet->getActiveSheet(); // setting the worksheet title and code name $worksheet ->setTitle($this->getTitle()) ->setCodeName('result'); return [$spreadsheet, $worksheet]; } /** * Generate the content and write it to php://temp. */ protected function generateContent() { [$spreadsheet, $worksheet] = $this->createSpreadsheet(); $this->addTitleToWorkSheet($worksheet); $line = $this->addFiltersDescription($worksheet); // at this point, we are going to sort retsults for an easier manipulation [$sortedResult, $exportKeys, $aggregatorKeys, $globalKeys] = $this->sortResult(); $line = $this->addHeaders($worksheet, $globalKeys, $line); $line = $this->addContentTable($worksheet, $sortedResult, $line); switch ($this->formatterData['format']) { case 'ods': $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Ods'); break; case 'xlsx': $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xlsx'); break; case 'csv': $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Csv'); break; default: // this should not happen // throw an exception to ensure that the error is catched throw new LogicException(); } $writer->save($this->tempfile); } /** * get an array of aggregator keys. The keys are sorted as asked * by user in the form. * * @return string[] an array containing the keys of aggregators */ protected function getAggregatorKeysSorted() { // empty array for aggregators keys $keys = []; // this association between key and aggregator alias will be used // during sorting $aggregatorKeyAssociation = []; foreach ($this->aggregatorsData as $alias => $data) { $aggregator = $this->exportManager->getAggregator($alias); $aggregatorsKeys = $aggregator->getQueryKeys($data); // append the keys from aggregator to the $keys existing array $keys = array_merge($keys, $aggregatorsKeys); // append the key with the alias, which will be use later for sorting foreach ($aggregatorsKeys as $key) { $aggregatorKeyAssociation[$key] = $alias; } } // sort the result using the form usort($keys, function ($a, $b) use ($aggregatorKeyAssociation) { $A = $this->formatterData[$aggregatorKeyAssociation[$a]]['order']; $B = $this->formatterData[$aggregatorKeyAssociation[$b]]['order']; if ($A === $B) { return 0; } if ($A > $B) { return 1; } return -1; }); return $keys; } protected function getContentType($format) { switch ($format) { case 'csv': return 'text/csv'; case 'ods': return 'application/vnd.oasis.opendocument.spreadsheet'; case 'xlsx': return 'application/vnd.openxmlformats-officedocument.' . 'spreadsheetml.sheet'; } } /** * Get the displayable result. * * @param string $key * @param string $value * * @return string */ protected function getDisplayableResult($key, $value) { if (false === $this->cacheDisplayableResultIsInitialized) { $this->initializeCache($key); } return call_user_func($this->cacheDisplayableResult[$key], $value); } protected function getTitle() { return $this->translator->trans($this->export->getTitle()); } protected function initializeCache($key) { /* * this function follows the following steps : * * 1. associate all keys used in result with their export element * (export or aggregator) and data; * 2. associate all keys used in result with all the possible values : * this array will be necessary to call `getLabels` * 3. store the `callable` in an associative array, in cache */ // 1. create an associative array with key and export / aggregator $keysExportElementAssociation = []; // keys for export foreach ($this->export->getQueryKeys($this->exportData) as $key) { $keysExportElementAssociation[$key] = [$this->export, $this->exportData, ]; } // keys for aggregator foreach ($this->aggregatorsData as $alias => $data) { $aggregator = $this->exportManager->getAggregator($alias); foreach ($aggregator->getQueryKeys($data) as $key) { $keysExportElementAssociation[$key] = [$aggregator, $data]; } } // 2. collect all the keys before iteration $keys = array_keys($keysExportElementAssociation); $allValues = []; // store all the values in an array foreach ($this->result as $row) { foreach ($keys as $key) { $allValues[$key][] = $row[$key]; } } // 3. iterate on `keysExportElementAssociation` to store the callable // in cache foreach ($keysExportElementAssociation as $key => [$element, $data]) { $this->cacheDisplayableResult[$key] = $element->getLabels($key, array_unique($allValues[$key]), $data); } // the cache is initialized ! $this->cacheDisplayableResultIsInitialized = true; } /** * sort the results, and return an array where : * - 0 => sorted results * - 1 => export keys * - 2 => aggregator keys * - 3 => global keys (aggregator keys and export keys). * * Example, assuming that the result contains two aggregator keys : * * array in result : * * ``` * array( * array( //row 1 * 'export_result' => 1, * 'aggregator_1' => 2, * 'aggregator_2' => 3 * ), * array( // row 2 * 'export_result' => 4, * 'aggregator_1' => 5, * 'aggregator_2' => 6 * ) * ) * ``` * * the sorted result will be : * * ``` * array( * array( 2, 3, 1 ), * array( 5, 6, 4 ) * ) * ``` */ protected function sortResult() { // get the keys for each row $exportKeys = $this->export->getQueryKeys($this->exportData); $aggregatorKeys = $this->getAggregatorKeysSorted(); $globalKeys = array_merge($aggregatorKeys, $exportKeys); $sortedResult = array_map(function ($row) use ($globalKeys) { $newRow = []; foreach ($globalKeys as $key) { $newRow[] = $this->getDisplayableResult($key, $row[$key]); } return $newRow; }, $this->result); array_multisort($sortedResult); return [$sortedResult, $exportKeys, $aggregatorKeys, $globalKeys]; } /** * append a form line by aggregator on the formatter form. * * This form allow to choose the aggregator position (row or column) and * the ordering * * @param string $nbAggregators */ private function appendAggregatorForm(FormBuilderInterface $builder, $nbAggregators) { $builder->add('order', ChoiceType::class, [ 'choices' => array_combine( range(1, $nbAggregators), range(1, $nbAggregators) ), 'multiple' => false, 'expanded' => false, ]); } }