ExcelExport.class.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. <?php
  2. require_once (LIB_PATH.'XLSXWriter'.SLASH.'XLSXWriter.class.php');
  3. class ExcelExport{
  4. public $mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  5. public $extension = 'xlsx';
  6. public $description = 'Fichier classeur de données Excel';
  7. public $tempFiles = array();
  8. public $source = '';
  9. public $spreadsheet;
  10. public function sample($dataset, $level=0, $parent=''){
  11. $data = $this->recursive_sample($dataset,$level ,$parent);
  12. $stream = Excel::exportArray($data, null ,'Sans titre');
  13. return $stream;
  14. }
  15. public function recursive_sample($dataset, $level=0, $parent=''){
  16. $stream = array();
  17. $parent = ($parent!=''?$parent.'.':'');
  18. $indentation = str_repeat("\t", $level);
  19. foreach($dataset as $macro => $infos){
  20. $infos['type'] = isset($infos['type']) ? $infos['type'] : '';
  21. switch($infos['type']){
  22. case 'list':
  23. $stream[] = array('Macros disponibles :' => $indentation.'-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').' (liste)'.PHP_EOL);
  24. $stream[] = array('Macros disponibles :' => $indentation.'{{#'.$macro.'}}'.PHP_EOL);
  25. if(is_array($infos['value']) && isset($infos['value'][0])) $stream = array_merge($stream, self::recursive_sample($infos['value'][0],$level+1));
  26. $stream[] = array('Macros disponibles :' =>$indentation.'{{/'.$macro.'}}');
  27. break;
  28. case 'object':
  29. $stream[] = array('Macros disponibles :' => $indentation.'-'.$parent.$macro.' '.(isset($infos['label'])?': '.$infos['label']:'').PHP_EOL);
  30. $stream = array_merge($stream, self::recursive_sample($infos['value'],$level+1,$parent.$macro));
  31. break;
  32. case 'image' :
  33. $stream[] = array('Macros disponibles :' => $indentation.'{{'.$parent.$macro.'::image}} : '.( !isset($infos['label']) ? '': $infos['label']).PHP_EOL);
  34. break;
  35. default :
  36. $stream[] = array('Macros disponibles :' => $indentation.'{{'.$parent.$macro.'}} : '.( !isset($infos['label']) ? '': $infos['label']).PHP_EOL);
  37. break;
  38. }
  39. }
  40. return $stream;
  41. }
  42. //Remplacement du tag d'image par l'image concernée
  43. //dans le fichier modèle
  44. public function add_image($worksheet, $macro, $value, $cellCoord, $scale=2){
  45. $finfo = new finfo(FILEINFO_MIME);
  46. $mime = $finfo->buffer($value);
  47. $ext = 'jpg';
  48. switch($mime){
  49. case 'image/jpeg': $ext = 'jpeg'; break;
  50. case 'image/png': $ext = 'png'; break;
  51. case 'image/gif': $ext = 'gif'; break;
  52. }
  53. if($mime == 'image/jpg') $mime = 'image/jpeg';
  54. $path = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.'.$ext;
  55. file_put_contents($path, $value);
  56. $this->tempFiles[] = $path;
  57. //On supprime le tag
  58. $cellVal = str_replace('{{'.$macro.'}}', '', $worksheet->getCell($cellCoord)->getValue());
  59. $worksheet->getCell($cellCoord)->setValue($cellVal);
  60. preg_match_all('~[A-Z]+|\d+~', $cellCoord, $cellMatches);
  61. $cellMatches = reset($cellMatches);
  62. $cellIndex = $cellMatches[0];
  63. $rowIndex = $cellMatches[1];
  64. //On récupère les infos de l'image
  65. list($width, $height) = getimagesize($path);
  66. //On définit la taille de la cellule à celle de l'image
  67. $cellWidth = $worksheet->getColumnDimension($cellIndex);
  68. $rowHeight = $worksheet->getRowDimension($rowIndex);
  69. //La largeur d'une colonne est définie en unité (Microsoft). 1px correpond à peu près à 0.2 unité
  70. $cellWidth->setWidth(($width/$scale)*0.2);
  71. //0.75 correspond à 1px en pt, et la hauteur d'une ligne est définie en pt
  72. $rowHeight->setRowHeight(($height/$scale)*0.75);
  73. //On ajoute l'image dans la feuille
  74. $drawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
  75. $drawing->setName($macro);
  76. $drawing->setPath($path);
  77. $drawing->setResizeProportional(true);
  78. $drawing->setWidthAndHeight($width/$scale,$height/$scale);
  79. $drawing->setCoordinates($cellCoord);
  80. $drawing->setWorksheet($worksheet);
  81. }
  82. public function start($stream){
  83. require(LIB_PATH.'PhpSpreadsheet'.SLASH.'vendor'.SLASH.'autoload.php');
  84. $this->source = File::dir().'tmp'.SLASH.'template.'.time().'-'.rand(0,100).'.xlsx';
  85. file_put_contents($this->source,utf8_decode($stream));
  86. $this->spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($this->source);
  87. $this->spreadsheet->getActiveSheet()->getPageMargins()->setTop(0.2);
  88. $this->spreadsheet->getActiveSheet()->getPageMargins()->setRight(0.2);
  89. $this->spreadsheet->getActiveSheet()->getPageMargins()->setLeft(0.2);
  90. $this->spreadsheet->getActiveSheet()->getPageMargins()->setBottom(0.2);
  91. $this->spreadsheet->getActiveSheet()->getPageMargins()->setFooter(0.5);
  92. $this->spreadsheet->getActiveSheet()->getPageMargins()->setHeader(0.5);
  93. $this->spreadsheet->getActiveSheet()->getPageSetup()->setHorizontalCentered(true);
  94. }
  95. //Récupère et gère la structure du remplacement
  96. //des données dans le fichier template fourni
  97. public function from_template($stream, $data){
  98. require(LIB_PATH.'PhpSpreadsheet'.SLASH.'vendor'.SLASH.'autoload.php');
  99. //Pour chaque feuille dans le classeur Excel
  100. foreach ($this->spreadsheet->getAllSheets() as $wrkSheetIdx => $worksheet) {
  101. //On récupère la zone de travail (pour ne pas se perdre dans des cellules vide)
  102. //Avec la colonne max et la ligne max
  103. $maxCol = 'A';
  104. $maxRow = 0;
  105. foreach ($worksheet->getCoordinates() as $coord) {
  106. preg_match_all("/[A-Z]+|\d+/", $coord, $matches);
  107. $matches = reset($matches);
  108. $currCol = PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($matches[0]);
  109. $currRow = $matches[1];
  110. $currValue = $worksheet->getCell($coord)->getValue();
  111. if($maxCol < $currCol && !empty($currValue)) $maxCol = $currCol;
  112. if($maxRow < $currRow && !empty($currValue)) $maxRow = $currRow;
  113. }
  114. //On parcourt une fois le contenu du la feuille
  115. //pour avoir les différents bords des boucles, si
  116. //il y en a dans le fichier.
  117. $rows = $worksheet->toArray('', true, true, true);
  118. $loopDatas = array('startLine'=>0, 'endLine'=>0);
  119. $finalValues = array();
  120. foreach ($rows as $rowIdx => $cell) {
  121. if($rowIdx>$maxRow) continue;
  122. foreach ($cell as $cellIdx => $content) {
  123. if(empty($content) && PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol) continue;
  124. //@TODO: Faire en récursif !
  125. if(preg_match("/\{\{\#([^\/\#}]*)\}\}/is", $content, $matches)){
  126. $loopDatas['key'] = $matches[1];
  127. $loopDatas['startLine'] = $rowIdx;
  128. $loopDatas['startColumn'] = $cellIdx;
  129. $loopDatas['startCoord'] = $cellIdx.$rowIdx;
  130. }
  131. if(preg_match("/\{\{\/([^\/\#}]*)\}\}/is", $content, $matches)){
  132. $loopDatas['endLine'] = $rowIdx;
  133. $loopDatas['endColumn'] = $cellIdx;
  134. $loopDatas['endCoord'] = $cellIdx.$rowIdx;
  135. $loopDatas['totalRow'] = $loopDatas['endLine'] - $loopDatas['startLine'];
  136. $loopDatas['totalCol'] = letters_to_numbers($loopDatas['endColumn']) - letters_to_numbers($loopDatas['startColumn']);
  137. }
  138. }
  139. }
  140. //Définition type de boucle
  141. if($loopDatas['startLine'] != 0 && $loopDatas['endLine'] != 0)
  142. $loopDatas['loopType'] = ($loopDatas['startLine'] != $loopDatas['endLine']) ? 'vertical' : 'horizontal';
  143. //Récupération des infos en fonction du type de boucle
  144. if(isset($loopDatas['loopType'])){
  145. if($loopDatas['loopType'] == 'vertical'){
  146. //On parcourt par ligne puis par colonne
  147. foreach ($rows as $rowIdx => $cell) {
  148. if($rowIdx>$maxRow) continue;
  149. foreach ($cell as $cellIdx => $content) {
  150. if(PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol || ($rowIdx <= $loopDatas['startLine'] || $rowIdx >= $loopDatas['endLine'])) continue;
  151. $finalValues[$rowIdx][$cellIdx][] = $content;
  152. }
  153. }
  154. } else {
  155. //On parcourt par colonne puis par ligne
  156. for($col = $loopDatas['startColumn']; $col <= $loopDatas['endColumn']; ++$col){
  157. if($col == $loopDatas['startColumn'] || $col == $loopDatas['endColumn']) continue;
  158. for($row = $loopDatas['startLine']; $row <= $maxRow; ++$row) {
  159. $finalValues[$col][$row][] = $worksheet->getCell($col.$row)->getValue();
  160. }
  161. }
  162. }
  163. }
  164. //On remplace les données à l'intérieur
  165. //des boucles si des boucles sont présentes
  166. if(isset($finalValues) && !empty($finalValues)){
  167. $values = $this->decomposeKey($data, $loopDatas['key']);
  168. //Pour chaque entité
  169. foreach ($values as $i => $entity) {
  170. $rowIterator = $colIterator = 0;
  171. if($loopDatas['loopType'] == 'vertical'){
  172. unset($finalValues[$loopDatas['endLine']]);
  173. //On ajoute 1 car on insère AVANT la ligne, et non APRÈS
  174. $rowWhereInsert = $i==0 ? $loopDatas['endLine']+1 : $rowWhereInsert += 1;
  175. $worksheet->insertNewRowBefore($rowWhereInsert, 1);
  176. //Pour chaque ligne
  177. foreach ($finalValues as $rIdx => $cell) {
  178. $lineIdx = ($loopDatas['startLine']+1) + (($loopDatas['totalRow']-1)*$i) + $rowIterator;
  179. $maxRow += 1;
  180. //Pour chaque cellule
  181. foreach ($cell as $cIdx => $content) {
  182. $currCoord = $cIdx.$lineIdx;
  183. $lineRef = $lineIdx-($loopDatas['totalRow']-1);
  184. //Dans le cas particulier où $i vaut 0, on remplace directement les données dans la feuille
  185. $referentCell = $i==0 ? $currCoord : $cIdx.$lineRef;
  186. $worksheet->setCellValue($currCoord, $content[0]);
  187. $worksheet->duplicateStyle($worksheet->getStyle($referentCell),$currCoord);
  188. $maxCol += 1;
  189. //On remplace les données
  190. ExcelExport::replace_data($worksheet, $entity, $currCoord, 4);
  191. }
  192. $rowIterator++;
  193. }
  194. } else {
  195. //On ajoute 1 car on insère AVANT la ligne, et non APRÈS
  196. $colWhereInsert = $i==0 ? numbers_to_letters(letters_to_numbers($loopDatas['endColumn'])+1) : numbers_to_letters(letters_to_numbers($colWhereInsert)+1);
  197. $worksheet->insertNewColumnBefore($colWhereInsert, 1);
  198. //Pour chaque colonne
  199. foreach ($finalValues as $cIdx => $col) {
  200. $colIdx = numbers_to_letters((letters_to_numbers($loopDatas['startColumn'])+1)+(($loopDatas['totalCol']-1)*$i)+$colIterator);
  201. $maxCol += 1;
  202. //Pour chaque cellule
  203. foreach ($col as $rIdx => $content) {
  204. $currCoord = $colIdx.$rIdx;
  205. //Dans le cas où $i vaut 0, cas particulier, on remplace directement les données dans la feuille
  206. $referentCell = $i==0 ? $currCoord : $cIdx.$rIdx;
  207. $worksheet->setCellValue($currCoord, $content[0]);
  208. $worksheet->duplicateStyle($worksheet->getStyle($referentCell),$currCoord);
  209. $maxRow += 1;
  210. //On remplace les données
  211. ExcelExport::replace_data($worksheet, $entity, $currCoord, 4);
  212. }
  213. $colIterator++;
  214. }
  215. }
  216. }
  217. }
  218. //On remplace le reste des tag présents
  219. //sur la feuille de calcul du fichier template
  220. foreach ($worksheet->getRowIterator() as $rowIdx => $row) {
  221. if($rowIdx>$maxRow) continue;
  222. foreach ($row->getCellIterator() as $cellIdx => $cell) {
  223. if(empty($cell->getValue()) && PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cellIdx)>$maxCol) continue;
  224. $this->replace_data($worksheet, $data, $cellIdx.$rowIdx);
  225. }
  226. }
  227. //@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
  228. //(cf. fin de fichier)
  229. //En fct du type de boucle on supprime les lignes/colonnes de début et fin de boucle
  230. if(isset($loopDatas['loopType'])){
  231. if($loopDatas['loopType'] == 'vertical'){
  232. //On supprime la ligne de début de boucle (la ligne avec le tag de début de loop)
  233. $worksheet->removeRow($loopDatas['startLine']);
  234. //Si le nb d'itération sur les lignes est supérieur au nb de lignes de données à remplacer pour 1 itération
  235. //Alors on indique à -1 sinon -2
  236. $rowToDelete = $rowIterator > $loopDatas['endLine']-$loopDatas['startLine']-1 ? $rowWhereInsert-1 : $rowWhereInsert-2;
  237. //On supprime la ligne de fin de boucle (la ligne avec le tag de fin de loop)
  238. $worksheet->removeRow($rowToDelete);
  239. //On supprime les N lignes insérées à chaque itération de boucle
  240. for ($i=1; $i<=$loopDatas['endLine']-$loopDatas['startLine']-1; $i++)
  241. $worksheet->removeRow($rowToDelete+$i);
  242. } else if($loopDatas['loopType'] == 'horizontal'){
  243. // On supprime la colonne de début de boucle
  244. $worksheet->removeColumn($loopDatas['startColumn']);
  245. //Si le nb d'itération sur les colonnes est supérieur au nb de colonnes de données à remplacer pour 1 itération
  246. //Alors on indique à -1 sinon -2
  247. $colToDelete = $colIterator > letters_to_numbers($loopDatas['endColumn'])-letters_to_numbers($loopDatas['startColumn'])-1 ? letters_to_numbers($colWhereInsert)-1 : letters_to_numbers($colWhereInsert)-2;
  248. //On supprime la colonne de fin de boucle (la colonne avec le tag de fin de loop)
  249. $worksheet->removeColumn(numbers_to_letters($colToDelete));
  250. for ($i=1; $i<=$loopDatas['endColumn']-$loopDatas['startColumn']-1; $i++)
  251. $worksheet->removeColumn(numbers_to_letters($colToDelete+$i));
  252. }
  253. }
  254. }
  255. }
  256. //Remplace les données d'un jeu de données en fonction
  257. //des tags rencontrés et fournis par le jeu de données
  258. public function replace_data($worksheet, $data=array(), $cellCoord, $imgScale=2){
  259. $cell = $worksheet->getCell($cellCoord);
  260. $cellVal = $cell->getValue();
  261. //gestion des simples variables
  262. $cellVal = preg_replace_callback('/{{([^#\/}]*)}}/',function($matches) use ($data,$cellCoord,$imgScale,$worksheet) {
  263. $key = $matches[1];
  264. $keyInfos = explode('::',$key);
  265. $key = $keyInfos[0];
  266. $type = isset($keyInfos[1]) ? $keyInfos[1] : 'string';
  267. $value = $this->decomposeKey($data,$key);
  268. if($type =='image'){
  269. if(isset($value)) ExcelExport::add_image($worksheet, $key, $value, $cellCoord, $imgScale);
  270. return;
  271. }
  272. if(is_numeric($value))
  273. $worksheet->getStyle($cellCoord)->getNumberFormat()->setFormatCode(PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_NUMBER);
  274. else if(preg_match("/\d+(?:\.\d{1,2})? €/",$value))
  275. $worksheet->getStyle($cellCoord)->getNumberFormat()->setFormatCode(PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE);
  276. if(!isset($value)) return $this->formatValue($type,$matches[0]);
  277. if(is_array($value)) return 'Array';
  278. return $this->formatValue($type,$value);
  279. },$cellVal);
  280. $cell->setValue($cellVal);
  281. }
  282. public function end($stream,$data){
  283. $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($this->spreadsheet, 'Xlsx');
  284. $writer->save($this->source);
  285. $stream = file_get_contents($this->source);
  286. unlink($this->source);
  287. //suppression des images temporaires
  288. foreach ($this->tempFiles as $file) {
  289. if(file_exists($file)) unlink($file);
  290. }
  291. return $stream;
  292. }
  293. public function formatValue($type,$value){
  294. return $value;
  295. }
  296. public function decomposeKey($datas,$key){
  297. if(array_key_exists($key, $datas)) return isset($datas[$key]) ? $datas[$key] : '' ;
  298. $attributes = explode('.',$key);
  299. $current = $datas;
  300. $value = null;
  301. foreach ($attributes as $attribute) {
  302. if(!array_key_exists($attribute, $current)) break;
  303. $current = $current[$attribute];
  304. $value = isset($current) ? $current : '';
  305. }
  306. return $value;
  307. }
  308. //Copie l'intégralité de la ligne depuis des
  309. //positions données à l'endroit voulu
  310. public static function copy_full_row(&$ws_from, &$ws_to, $row_from, $row_to) {
  311. $ws_to->getRowDimension($row_to)->setRowHeight($ws_from->getRowDimension($row_from)->getRowHeight());
  312. $lastColumn = $ws_from->getHighestColumn();
  313. ++$lastColumn;
  314. for($c = 'A'; $c != $lastColumn; ++$c) {
  315. $cell_from = $ws_from->getCell($c.$row_from);
  316. $cell_to = $ws_to->getCell($c.$row_to);
  317. $cell_to->setXfIndex($cell_from->getXfIndex()); // black magic here
  318. $cell_to->setValue($cell_from->getValue());
  319. }
  320. }
  321. //Gère la suppression des lignes/colonnes et la ré-adaptation des indexs de bornes de boucles
  322. // public static function removeShiftRow($worksheet, $loops, $idx, $nb){
  323. // //$i vaut 2i puis 2i+1
  324. // foreach ($loops as $i => $loop) {
  325. // if($loop['loopType'] == 'vertical'){
  326. // $worksheet->removeRow($loop['startLine'] - (2*$i));
  327. // $worksheet->removeRow($loop['endLine'] - ((2*$i)+1));
  328. //
  329. // $loops[$i]['startLine'] = $loop['startLine'] - (2*$i)
  330. //
  331. // //@TODO: Gérer la ré-assignation des coordonnées
  332. // startCoord
  333. // endCoord
  334. // } else if($loop['loopType'] == 'horizontal'){
  335. // $worksheet->removeColumn(chr(ord($loop['startColumn']) - (2*$i));
  336. // $worksheet->removeColumn(chr(ord($loop['endColumn']) - ((2*$i)+1));
  337. // }
  338. // }
  339. // }
  340. }