* * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ namespace Chill\MainBundle\Export\Formatter; use Symfony\Component\HttpFoundation\Response; use Chill\MainBundle\Export\FormatterInterface; use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Form\FormBuilderInterface; use Chill\MainBundle\Export\ExportManager; use Symfony\Component\Form\Extension\Core\Type\FormType; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; /** * * * @author Julien Fastré */ class SpreadSheetFormatter implements FormatterInterface { /** * * @var TranslatorInterface */ protected $translator; /** * * @var ExportManager */ protected $exportManager; /** * The result, as returned by the export * * replaced when `getResponse` is called. * * @var type */ protected $result; /** * * replaced when `getResponse` is called. * * @var type */ protected $formatterData; /** * The export * * replaced when `getResponse` is called. * * @var \Chill\MainBundle\Export\ExportInterface */ protected $export; /** * * replaced when `getResponse` is called. * * @var type */ //protected $aggregators; /** * array containing value of export form * * replaced when `getResponse` is called. * * @var array */ protected $exportData; /** * an array where keys are the aggregators aliases and * values are the data * * replaced when `getResponse` is called. * * @var type */ protected $aggregatorsData; /** * * replaced when `getResponse` is called. * * @var array */ protected $filtersData; /** * * replaced when `getResponse` is called. * * @var array */ //protected $labels; /** * temporary file to store spreadsheet * * @var string */ protected $tempfile; /** * cache for displayable result. * * This cache is reset when `getResponse` is called. * * The array's keys are the keys in the raw result, and * values are the callable which will transform the raw result to * displayable result. * * @var array */ private $cacheDisplayableResult; /** * Whethe `cacheDisplayableResult` is initialized or not. * * @var boolean */ private $cacheDisplayableResultIsInitialized = false; public function __construct(TranslatorInterface $translatorInterface, ExportManager $exportManager) { $this->translator = $translatorInterface; $this->exportManager = $exportManager; } public function buildForm( FormBuilderInterface $builder, $exportAlias, array $aggregatorAliases ) { // choosing between formats $builder->add('format', ChoiceType::class, array( 'choices' => array( '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, array( 'label' => $aggregator->getTitle(), 'block_name' => '_aggregator_placement_spreadsheet_formatter' )); $this->appendAggregatorForm($builderAggregator, $nb); $builder->add($builderAggregator); } } /** * 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 FormBuilderInterface $builder * @param string $nbAggregators */ private function appendAggregatorForm(FormBuilderInterface $builder, $nbAggregators) { $builder->add('order', ChoiceType::class, array( 'choices' => array_combine( range(1, $nbAggregators), range(1, $nbAggregators) ), 'multiple' => false, 'expanded' => false )); } 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 = array(); $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; } /** * Generate the content and write it to php://temp */ protected function generateContent() { list($spreadsheet, $worksheet) = $this->createSpreadsheet(); $this->addTitleToWorkSheet($worksheet); $line = $this->addFiltersDescription($worksheet); // at this point, we are going to sort retsults for an easier manipulation list($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); } /** * 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]; } /** * Add the title to the worksheet and merge the cell containing * the title * * @param Worksheet $worksheet */ protected function addTitleToWorkSheet(Worksheet &$worksheet) { $worksheet->setCellValue('A1', $this->getTitle()); $worksheet->mergeCells('A1:G1'); } /** * Add filter description since line 3. * * return the line number after the last description * * @param Worksheet $worksheet * @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], isset($description[1]) ? $description[1] : [] ); } $worksheet->setCellValue('A'.$line, $description); $line ++; } return $line; } /** * 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 = array(); foreach ($globalKeys as $key) { $newRow[] = $this->getDisplayableResult($key, $row[$key]); } return $newRow; }, $this->result); \array_multisort($sortedResult); return array($sortedResult, $exportKeys, $aggregatorKeys, $globalKeys); } /** * 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 = array(); // this association between key and aggregator alias will be used // during sorting $aggregatorKeyAssociation = array(); 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; } elseif ($A > $B) { return 1; } else { return -1; } }); return $keys; } /** * add headers to worksheet * * return the line number where the next content (i.e. result) should * be appended. * * @param Worksheet $worksheet * @param array $aggregatorKeys * @param array $exportKeys * @param int $line * @return int */ protected function addHeaders( Worksheet &$worksheet, array $globalKeys, $line ) { // get the displayable form of headers $displayables = array(); 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; } protected function addContentTable(Worksheet $worksheet, $sortedResults, $line ) { $worksheet->fromArray( $sortedResults, NULL, 'A'.$line); return $line + count($sortedResults) + 1; } protected function getTitle() { return $this->translator->trans($this->export->getTitle()); } /** * Get the displayable result. * * @param string $key * @param string $value * @return string */ protected function getDisplayableResult($key, $value) { if ($this->cacheDisplayableResultIsInitialized === false) { $this->initializeCache($key); } return call_user_func($this->cacheDisplayableResult[$key], $value); } 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 = array(); // 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 = array(); // 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 => list($element, $data)) { $this->cacheDisplayableResult[$key] = $element->getLabels($key, \array_unique($allValues[$key]), $data); } // the cache is initialized ! $this->cacheDisplayableResultIsInitialized = true; } 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'; } } public function getType() { return 'tabular'; } }