recursive_sample($dataset,$level ,$parent); $stream = Excel::exportArray($data, null ,'Sans titre'); return $stream; } public function recursive_sample($dataset, $level=0, $parent=''){ $stream = array(); $parent = ($parent!=''?$parent.'.':''); $indentation = str_repeat("\t", $level); foreach($dataset as $macro => $infos){ $infos['type'] = isset($infos['type']) ? $infos['type'] : ''; switch($infos['type']){ case 'list': $stream[] = array('Macros disponibles :' => $indentation.'-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').' (liste)'.PHP_EOL); $stream[] = array('Macros disponibles :' => $indentation.'{{#'.$macro.'}}'.PHP_EOL); if(is_array($infos['value']) && isset($infos['value'][0])) $stream = array_merge($stream, self::recursive_sample($infos['value'][0],$level+1)); $stream[] = array('Macros disponibles :' =>$indentation.'{{/'.$macro.'}}'); break; case 'object': $stream[] = array('Macros disponibles :' => $indentation.'-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').PHP_EOL); $stream = array_merge($stream, self::recursive_sample($infos['value'],$level+1,$parent.$macro)); break; case 'image' : $stream[] = array('Macros disponibles :' => $indentation.'{{'.$parent.$macro.'::image}} : '.( !isset($infos['label']) ? '': $infos['label']).PHP_EOL); break; default : $stream[] = array('Macros disponibles :' => $indentation.'{{'.$parent.$macro.'}} : '.( !isset($infos['label']) ? '': $infos['label']).PHP_EOL); break; } } return $stream; } //Remplacement du tag d'image par l'image concernée //dans le fichier modèle public function add_image($worksheet, $macro, $value, $cellCoord, $scale=2){ $finfo = new finfo(FILEINFO_MIME); $mime = $finfo->buffer($value); $ext = 'jpg'; switch($mime){ case 'image/jpeg': $ext = 'jpeg'; break; case 'image/png': $ext = 'png'; break; case 'image/gif': $ext = 'gif'; break; } if($mime == 'image/jpg') $mime = 'image/jpeg'; $path = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.'.$ext; file_put_contents($path, $value); $this->tempFiles[] = $path; //On supprime le tag $cellVal = str_replace('{{'.$macro.'}}', '', $worksheet->getCell($cellCoord)->getValue()); $worksheet->getCell($cellCoord)->setValue($cellVal); preg_match_all('~[A-Z]+|\d+~', $cellCoord, $cellMatches); $cellMatches = reset($cellMatches); $cellIndex = $cellMatches[0]; $rowIndex = $cellMatches[1]; //On récupère les infos de l'image list($width, $height) = getimagesize($path); //On définit la taille de la cellule à celle de l'image $cellWidth = $worksheet->getColumnDimension($cellIndex); $rowHeight = $worksheet->getRowDimension($rowIndex); //La largeur d'une colonne est définie en unité (Microsoft). 1px correpond à peu près à 0.2 unité $cellWidth->setWidth(($width/$scale)*0.2); //0.75 correspond à 1px en pt, et la hauteur d'une ligne est définie en pt $rowHeight->setRowHeight(($height/$scale)*0.75); //On ajoute l'image dans la feuille $drawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); $drawing->setName($macro); $drawing->setPath($path); $drawing->setResizeProportional(true); $drawing->setWidthAndHeight($width/$scale,$height/$scale); $drawing->setCoordinates($cellCoord); $drawing->setWorksheet($worksheet); } public function start($stream){ require(LIB_PATH.'PhpSpreadsheet'.SLASH.'vendor'.SLASH.'autoload.php'); $this->source = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.xlsx'; file_put_contents($this->source,utf8_decode($stream)); $this->spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($this->source); $this->spreadsheet->getActiveSheet()->getPageMargins()->setTop(0.2); $this->spreadsheet->getActiveSheet()->getPageMargins()->setRight(0.2); $this->spreadsheet->getActiveSheet()->getPageMargins()->setLeft(0.2); $this->spreadsheet->getActiveSheet()->getPageMargins()->setBottom(0.2); $this->spreadsheet->getActiveSheet()->getPageMargins()->setFooter(0.5); $this->spreadsheet->getActiveSheet()->getPageMargins()->setHeader(0.5); $this->spreadsheet->getActiveSheet()->getPageSetup()->setHorizontalCentered(true); } //Récupère et gère la structure du remplacement //des données dans le fichier template fourni public function from_template($stream, $data){ require(LIB_PATH.'PhpSpreadsheet'.SLASH.'vendor'.SLASH.'autoload.php'); //Pour chaque feuille dans le classeur Excel foreach ($this->spreadsheet->getAllSheets() as $wrkSheetIdx => $worksheet) { //On récupère la zone de travail (pour ne pas se perdre dans des cellules vide) //Avec la colonne max et la ligne max $maxCol = 'A'; $maxRow = 0; foreach ($worksheet->getCoordinates() as $coord) { preg_match_all("/[A-Z]+|\d+/", $coord, $matches); $matches = reset($matches); $currCol = PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($matches[0]); $currRow = $matches[1]; $currValue = $worksheet->getCell($coord)->getValue(); if($maxCol < $currCol && !empty($currValue)) $maxCol = $currCol; if($maxRow < $currRow && !empty($currValue)) $maxRow = $currRow; } //On parcourt une fois le contenu du la feuille //pour avoir les différents bords des boucles, si //il y en a dans le fichier. $rows = $worksheet->toArray('', true, true, true); $loopDatas = array('startLine'=>0, 'endLine'=>0); $finalValues = array(); foreach ($rows as $rowIdx => $cell) { if($rowIdx>$maxRow) continue; foreach ($cell as $cellIdx => $content) { if(empty($content) && PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol) continue; //@TODO: Faire en récursif ! if(preg_match("/\{\{\#([^\/\#}]*)\}\}/is", $content, $matches)){ $loopDatas['key'] = $matches[1]; $loopDatas['startLine'] = $rowIdx; $loopDatas['startColumn'] = $cellIdx; $loopDatas['startCoord'] = $cellIdx.$rowIdx; } if(preg_match("/\{\{\/([^\/\#}]*)\}\}/is", $content, $matches)){ $loopDatas['endLine'] = $rowIdx; $loopDatas['endColumn'] = $cellIdx; $loopDatas['endCoord'] = $cellIdx.$rowIdx; $loopDatas['totalRow'] = $loopDatas['endLine'] - $loopDatas['startLine']; $loopDatas['totalCol'] = letters_to_numbers($loopDatas['endColumn']) - letters_to_numbers($loopDatas['startColumn']); } } } //Définition type de boucle if($loopDatas['startLine'] != 0 && $loopDatas['endLine'] != 0) $loopDatas['loopType'] = ($loopDatas['startLine'] != $loopDatas['endLine']) ? 'vertical' : 'horizontal'; //Récupération des infos en fonction du type de boucle if(isset($loopDatas['loopType'])){ if($loopDatas['loopType'] == 'vertical'){ //On parcourt par ligne puis par colonne foreach ($rows as $rowIdx => $cell) { if($rowIdx>$maxRow) continue; foreach ($cell as $cellIdx => $content) { if(PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol || ($rowIdx <= $loopDatas['startLine'] || $rowIdx >= $loopDatas['endLine'])) continue; $finalValues[$rowIdx][$cellIdx][] = $content; } } } else { //On parcourt par colonne puis par ligne for($col = $loopDatas['startColumn']; $col <= $loopDatas['endColumn']; ++$col){ if($col == $loopDatas['startColumn'] || $col == $loopDatas['endColumn']) continue; for($row = $loopDatas['startLine']; $row <= $maxRow; ++$row) { $finalValues[$col][$row][] = $worksheet->getCell($col.$row)->getValue(); } } } } //On remplace les données à l'intérieur //des boucles si des boucles sont présentes if(isset($finalValues) && !empty($finalValues)){ $values = $this->decomposeKey($data, $loopDatas['key']); //Pour chaque entité foreach ($values as $i => $entity) { $rowIterator = $colIterator = 0; if($loopDatas['loopType'] == 'vertical'){ unset($finalValues[$loopDatas['endLine']]); //On ajoute 1 car on insère AVANT la ligne, et non APRÈS $rowWhereInsert = $i==0 ? $loopDatas['endLine']+1 : $rowWhereInsert += 1; $worksheet->insertNewRowBefore($rowWhereInsert, 1); //Pour chaque ligne foreach ($finalValues as $rIdx => $cell) { $lineIdx = ($loopDatas['startLine']+1) + (($loopDatas['totalRow']-1)*$i) + $rowIterator; $maxRow += 1; //Pour chaque cellule foreach ($cell as $cIdx => $content) { $currCoord = $cIdx.$lineIdx; $lineRef = $lineIdx-($loopDatas['totalRow']-1); //Dans le cas particulier où $i vaut 0, on remplace directement les données dans la feuille $referentCell = $i==0 ? $currCoord : $cIdx.$lineRef; $worksheet->setCellValue($currCoord, $content[0]); $worksheet->duplicateStyle($worksheet->getStyle($referentCell),$currCoord); $maxCol += 1; //On remplace les données ExcelExport::replace_data($worksheet, $entity, $currCoord, 4); } $rowIterator++; } } else { //On ajoute 1 car on insère AVANT la ligne, et non APRÈS $colWhereInsert = $i==0 ? numbers_to_letters(letters_to_numbers($loopDatas['endColumn'])+1) : numbers_to_letters(letters_to_numbers($colWhereInsert)+1); $worksheet->insertNewColumnBefore($colWhereInsert, 1); //Pour chaque colonne foreach ($finalValues as $cIdx => $col) { $colIdx = numbers_to_letters((letters_to_numbers($loopDatas['startColumn'])+1)+(($loopDatas['totalCol']-1)*$i)+$colIterator); $maxCol += 1; //Pour chaque cellule foreach ($col as $rIdx => $content) { $currCoord = $colIdx.$rIdx; //Dans le cas où $i vaut 0, cas particulier, on remplace directement les données dans la feuille $referentCell = $i==0 ? $currCoord : $cIdx.$rIdx; $worksheet->setCellValue($currCoord, $content[0]); $worksheet->duplicateStyle($worksheet->getStyle($referentCell),$currCoord); $maxRow += 1; //On remplace les données ExcelExport::replace_data($worksheet, $entity, $currCoord, 4); } $colIterator++; } } } } //On remplace le reste des tag présents //sur la feuille de calcul du fichier template foreach ($worksheet->getRowIterator() as $rowIdx => $row) { if($rowIdx>$maxRow) continue; foreach ($row->getCellIterator() as $cellIdx => $cell) { if(empty($cell->getValue()) && PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol) continue; $this->replace_data($worksheet, $data, $cellIdx.$rowIdx); } } //@TODO: Gérer plusieurs boucles sur le document, et décrémenter pour chaque boucle présente les début et fin par suppression de lignes/colonnes //(cf. fin de fichier) //En fct du type de boucle on supprime les lignes/colonnes de début et fin de boucle if(isset($loopDatas['loopType'])){ if($loopDatas['loopType'] == 'vertical'){ //On supprime la ligne de début de boucle (la ligne avec le tag de début de loop) $worksheet->removeRow($loopDatas['startLine']); //Si le nb d'itération sur les lignes est supérieur au nb de lignes de données à remplacer pour 1 itération //Alors on indique à -1 sinon -2 $rowToDelete = $rowIterator > $loopDatas['endLine']-$loopDatas['startLine']-1 ? $rowWhereInsert-1 : $rowWhereInsert-2; //On supprime la ligne de fin de boucle (la ligne avec le tag de fin de loop) $worksheet->removeRow($rowToDelete); //On supprime les N lignes insérées à chaque itération de boucle for ($i=1; $i<=$loopDatas['endLine']-$loopDatas['startLine']-1; $i++) $worksheet->removeRow($rowToDelete+$i); } else if($loopDatas['loopType'] == 'horizontal'){ // On supprime la colonne de début de boucle $worksheet->removeColumn($loopDatas['startColumn']); //Si le nb d'itération sur les colonnes est supérieur au nb de colonnes de données à remplacer pour 1 itération //Alors on indique à -1 sinon -2 $colToDelete = $colIterator > letters_to_numbers($loopDatas['endColumn'])-letters_to_numbers($loopDatas['startColumn'])-1 ? letters_to_numbers($colWhereInsert)-1 : letters_to_numbers($colWhereInsert)-2; //On supprime la colonne de fin de boucle (la colonne avec le tag de fin de loop) $worksheet->removeColumn(numbers_to_letters($colToDelete)); for ($i=1; $i<=$loopDatas['endColumn']-$loopDatas['startColumn']-1; $i++) $worksheet->removeColumn(numbers_to_letters($colToDelete+$i)); } } } } //Remplace les données d'un jeu de données en fonction //des tags rencontrés et fournis par le jeu de données public function replace_data($worksheet, $data=array(), $cellCoord, $imgScale=2){ $cell = $worksheet->getCell($cellCoord); $cellVal = $cell->getValue(); //gestion des simples variables $cellVal = preg_replace_callback('/{{([^#\/}]*)}}/',function($matches) use ($data,$cellCoord,$imgScale,$worksheet) { $key = $matches[1]; $keyInfos = explode('::',$key); $key = $keyInfos[0]; $type = isset($keyInfos[1]) ? $keyInfos[1] : 'string'; $value = $this->decomposeKey($data,$key); if($type =='image'){ if(isset($value)) ExcelExport::add_image($worksheet, $key, $value, $cellCoord, $imgScale); return; } if(is_numeric($value)) $worksheet->getStyle($cellCoord)->getNumberFormat()->setFormatCode(PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_NUMBER); else if(preg_match("/\d+(?:\.\d{1,2})? €/",$value)) $worksheet->getStyle($cellCoord)->getNumberFormat()->setFormatCode(PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE); if(!isset($value)) return $this->formatValue($type,$matches[0]); if(is_array($value)) return 'Array'; return $this->formatValue($type,$value); },$cellVal); $cell->setValue($cellVal); } public function end($stream,$data){ $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($this->spreadsheet, 'Xlsx'); $writer->save($this->source); $stream = file_get_contents($this->source); unlink($this->source); //suppression des images temporaires foreach ($this->tempFiles as $file) { if(file_exists($file)) unlink($file); } return $stream; } public function formatValue($type,$value){ return $value; } public function decomposeKey($datas,$key){ if(array_key_exists($key, $datas)) return isset($datas[$key]) ? $datas[$key] : '' ; $attributes = explode('.',$key); $current = $datas; $value = null; foreach ($attributes as $attribute) { if(!array_key_exists($attribute, $current)) break; $current = $current[$attribute]; $value = isset($current) ? $current : ''; } return $value; } //Copie l'intégralité de la ligne depuis des //positions données à l'endroit voulu public static function copy_full_row(&$ws_from, &$ws_to, $row_from, $row_to) { $ws_to->getRowDimension($row_to)->setRowHeight($ws_from->getRowDimension($row_from)->getRowHeight()); $lastColumn = $ws_from->getHighestColumn(); ++$lastColumn; for($c = 'A'; $c != $lastColumn; ++$c) { $cell_from = $ws_from->getCell($c.$row_from); $cell_to = $ws_to->getCell($c.$row_to); $cell_to->setXfIndex($cell_from->getXfIndex()); // black magic here $cell_to->setValue($cell_from->getValue()); } } //Gère la suppression des lignes/colonnes et la ré-adaptation des indexs de bornes de boucles // public static function removeShiftRow($worksheet, $loops, $idx, $nb){ // //$i vaut 2i puis 2i+1 // foreach ($loops as $i => $loop) { // if($loop['loopType'] == 'vertical'){ // $worksheet->removeRow($loop['startLine'] - (2*$i)); // $worksheet->removeRow($loop['endLine'] - ((2*$i)+1)); // // $loops[$i]['startLine'] = $loop['startLine'] - (2*$i) // // //@TODO: Gérer la ré-assignation des coordonnées // startCoord // endCoord // } else if($loop['loopType'] == 'horizontal'){ // $worksheet->removeColumn(chr(ord($loop['startColumn']) - (2*$i)); // $worksheet->removeColumn(chr(ord($loop['endColumn']) - ((2*$i)+1)); // } // } // } }