├── PhpXlsxGenerator.php └── README.md /PhpXlsxGenerator.php: -------------------------------------------------------------------------------- 1 | subject = ''; 88 | $this->title = ''; 89 | $this->author = 'CodexWorld Dev '; 90 | $this->company = 'CodexWorld Dev '; 91 | $this->manager = 'CodexWorld Dev '; 92 | $this->description = ''; 93 | $this->keywords = ''; 94 | $this->category = ''; 95 | $this->lastModifiedBy = 'CodexWorld Dev '; 96 | $this->application = __CLASS__; 97 | 98 | $this->curSheet = -1; 99 | $this->defaultFont = 'Calibri'; 100 | $this->defaultFontSize = 10; 101 | $this->rtl = false; 102 | $this->sheets = [['name' => 'Sheet1', 'rows' => [], 'hyperlinks' => [], 'mergecells' => [], 'colwidth' => [], 'autofilter' => '']]; 103 | $this->extLinkId = 0; 104 | $this->SI = []; // sharedStrings index 105 | $this->SI_KEYS = []; // & keys 106 | 107 | $this->NF = [ 108 | self::N_RUB => '#,##0.00\ "₽"', 109 | self::N_DOLLAR => '[$$-1]#,##0.00', 110 | self::N_EURO => '#,##0.00\ [$€-1]' 111 | ]; 112 | $this->NF_KEYS = array_flip($this->NF); 113 | 114 | $this->BR_STYLE = [ 115 | self::B_NONE => 'none', 116 | self::B_THIN => 'thin', 117 | self::B_MEDIUM => 'medium', 118 | self::B_DASHED => 'dashed', 119 | self::B_DOTTED => 'dotted', 120 | self::B_THICK => 'thick', 121 | self::B_DOUBLE => 'double', 122 | self::B_HAIR => 'hair', 123 | self::B_MEDIUM_DASHED => 'mediumDashed', 124 | self::B_DASH_DOT => 'dashDot', 125 | self::B_MEDIUM_DASH_DOT => 'mediumDashDot', 126 | self::B_DASH_DOT_DOT => 'dashDotDot', 127 | self::B_MEDIUM_DASH_DOT_DOT => 'mediumDashDotDot', 128 | self::B_SLANT_DASH_DOT => 'slantDashDot' 129 | ]; 130 | 131 | $this->XF = [ // styles 0 - num fmt, 1 - align, 2 - font, 3 - fill, 4 - font color, 5 - bgcolor, 6 - border, 7 - font size 132 | [self::N_NORMAL, self::A_DEFAULT, self::F_NORMAL, self::FL_NONE, 0, 0, '', 0], 133 | [self::N_NORMAL, self::A_DEFAULT, self::F_NORMAL, self::FL_GRAY_125, 0, 0, '', 0], // hack 134 | ]; 135 | $this->XF_KEYS[implode('-', $this->XF[0])] = 0; // & keys 136 | $this->XF_KEYS[implode('-', $this->XF[1])] = 1; 137 | $this->template = [ 138 | '_rels/.rels' => ' 139 | 140 | 141 | 142 | 143 | ', 144 | 'docProps/app.xml' => ' 145 | 146 | 0 147 | {APP} 148 | {COMPANY} 149 | {MANAGER} 150 | ', 151 | 'docProps/core.xml' => ' 152 | 153 | {DATE} 154 | {TITLE} 155 | {SUBJECT} 156 | {AUTHOR} 157 | {LAST_MODIFY_BY} 158 | {KEYWORD} 159 | {DESCRIPTION} 160 | {CATEGORY} 161 | en-US 162 | {DATE} 163 | 1 164 | ', 165 | 'xl/_rels/workbook.xml.rels' => ' 166 | 167 | {RELS} 168 | ', 169 | 'xl/worksheets/sheet1.xml' => ' 170 | 171 | 172 | {SHEETVIEWS} 173 | {COLS} 174 | {ROWS} 175 | {AUTOFILTER}{MERGECELLS}{HYPERLINKS} 176 | ', 177 | 'xl/worksheets/_rels/sheet1.xml.rels' => ' 178 | {HYPERLINKS}', 179 | 'xl/sharedStrings.xml' => ' 180 | {STRINGS}', 181 | 'xl/styles.xml' => ' 182 | 183 | {NUMFMTS} 184 | {FONTS} 185 | {FILLS} 186 | {BORDERS} 187 | 188 | {XF} 189 | 190 | ', 191 | 'xl/workbook.xml' => ' 192 | 193 | 194 | 195 | {SHEETS} 196 | 197 | ', 198 | '[Content_Types].xml' => ' 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | {TYPES} 208 | ', 209 | ]; 210 | } 211 | 212 | public static function fromArray(array $rows, $sheetName = null) 213 | { 214 | return (new static())->addSheet($rows, $sheetName); 215 | } 216 | 217 | public function addSheet(array $rows, $name = null) 218 | { 219 | $this->curSheet++; 220 | if ($name === null) { // autogenerated sheet names 221 | $name = 'Sheet' . ($this->curSheet + 1); 222 | } else { 223 | $name = mb_substr($name, 0, 31); 224 | $names = []; 225 | foreach ($this->sheets as $sh) { 226 | $names[mb_strtoupper($sh['name'])] = 1; 227 | } 228 | for ($i = 0; $i < 100; $i++) { 229 | $postfix = ' (' . $i . ')'; 230 | $new_name = ($i === 0) ? $name : $name . $postfix; 231 | if (mb_strlen($new_name) > 31) { 232 | $new_name = mb_substr($name, 0, 31 - mb_strlen($postfix)) . $postfix; 233 | } 234 | $NEW_NAME = mb_strtoupper($new_name); 235 | if (!isset($names[$NEW_NAME])) { 236 | $name = $new_name; 237 | break; 238 | } 239 | } 240 | } 241 | $this->sheets[$this->curSheet] = ['name' => $name, 'hyperlinks' => [], 'mergecells' => [], 'colwidth' => [], 'autofilter' => '', 'frozen' => '']; 242 | if (isset($rows[0]) && is_array($rows[0])) { 243 | $this->sheets[$this->curSheet]['rows'] = $rows; 244 | } else { 245 | $this->sheets[$this->curSheet]['rows'] = []; 246 | } 247 | return $this; 248 | } 249 | 250 | public function __toString() 251 | { 252 | $fh = fopen('php://memory', 'wb'); 253 | if (!$fh) { 254 | return ''; 255 | } 256 | if (!$this->_write($fh)) { 257 | fclose($fh); 258 | return ''; 259 | } 260 | $size = ftell($fh); 261 | fseek($fh, 0); 262 | return (string)fread($fh, $size); 263 | } 264 | 265 | public function saveAs($filename) 266 | { 267 | $fh = fopen($filename, 'wb'); 268 | if (!$fh) { 269 | return false; 270 | } 271 | if (!$this->_write($fh)) { 272 | fclose($fh); 273 | return false; 274 | } 275 | fclose($fh); 276 | return true; 277 | } 278 | 279 | public function download() 280 | { 281 | return $this->downloadAs(gmdate('YmdHi') . '.xlsx'); 282 | } 283 | 284 | public function downloadAs($filename) 285 | { 286 | $fh = fopen('php://memory', 'wb'); 287 | if (!$fh) { 288 | return false; 289 | } 290 | if (!$this->_write($fh)) { 291 | fclose($fh); 292 | return false; 293 | } 294 | $size = ftell($fh); 295 | header('Content-type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); 296 | header('Content-Disposition: attachment; filename="' . $filename . '"'); 297 | header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T', time())); 298 | header('Content-Length: ' . $size); 299 | while (ob_get_level()) { 300 | ob_end_clean(); 301 | } 302 | fseek($fh, 0); 303 | fpassthru($fh); 304 | fclose($fh); 305 | return true; 306 | } 307 | 308 | protected function _write($fh) 309 | { 310 | $dirSignatureE = "\x50\x4b\x05\x06"; // end of central dir signature 311 | $zipComments = 'Generated by ' . __CLASS__ . ' PHP class, thanks codexworld.com@gmail.com'; 312 | if (!$fh) { 313 | return false; 314 | } 315 | $cdrec = ''; // central directory content 316 | $entries = 0; // number of zipped files 317 | $cnt_sheets = count($this->sheets); 318 | foreach ($this->template as $cfilename => $template) { 319 | if ($cfilename === 'xl/_rels/workbook.xml.rels') { 320 | $s = ''; 321 | for ($i = 0; $i < $cnt_sheets; $i++) { 322 | $s .= '\r\n"; 324 | } 325 | $s .= '' . "\r\n"; 326 | $s .= ''; 327 | 328 | $template = str_replace('{RELS}', $s, $template); 329 | $this->_writeEntry($fh, $cdrec, $cfilename, $template); 330 | $entries++; 331 | } elseif ($cfilename === 'xl/workbook.xml') { 332 | $s = ''; 333 | foreach ($this->sheets as $k => $v) { 334 | $s .= ''; 335 | } 336 | $search = ['{SHEETS}', '{APP}']; 337 | $replace = [$s, $this->esc($this->application)]; 338 | $template = str_replace($search, $replace, $template); 339 | $this->_writeEntry($fh, $cdrec, $cfilename, $template); 340 | $entries++; 341 | } elseif ($cfilename === 'docProps/app.xml') { 342 | $search = ['{APP}', '{COMPANY}', '{MANAGER}']; 343 | $replace = [$this->esc($this->application), $this->esc($this->company), $this->esc($this->manager)]; 344 | $template = str_replace($search, $replace, $template); 345 | $this->_writeEntry($fh, $cdrec, $cfilename, $template); 346 | $entries++; 347 | } elseif ($cfilename === 'docProps/core.xml') { 348 | $search = ['{DATE}', '{AUTHOR}', '{TITLE}', '{SUBJECT}', '{KEYWORD}', '{DESCRIPTION}', '{CATEGORY}', '{LAST_MODIFY_BY}']; 349 | $replace = [gmdate('Y-m-d\TH:i:s\Z'), $this->esc($this->author), $this->esc($this->title), $this->esc($this->subject), $this->esc($this->keywords), $this->esc($this->description), $this->esc($this->category), $this->esc($this->lastModifiedBy)]; 350 | $template = str_replace($search, $replace, $template); 351 | $this->_writeEntry($fh, $cdrec, $cfilename, $template); 352 | $entries++; 353 | } elseif ($cfilename === 'xl/sharedStrings.xml') { 354 | if (!count($this->SI)) { 355 | $this->SI[] = 'No Data'; 356 | } 357 | $si_cnt = count($this->SI); 358 | $si = '' . implode("\r\n", $this->SI) . ''; 359 | $template = str_replace(['{CNT}', '{STRINGS}'], [$si_cnt, $si], $template); 360 | $this->_writeEntry($fh, $cdrec, $cfilename, $template); 361 | $entries++; 362 | } elseif ($cfilename === 'xl/worksheets/sheet1.xml') { 363 | foreach ($this->sheets as $k => $v) { 364 | $filename = 'xl/worksheets/sheet' . ($k + 1) . '.xml'; 365 | $xml = $this->_sheetToXML($k, $template); 366 | $this->_writeEntry($fh, $cdrec, $filename, $xml); 367 | $entries++; 368 | } 369 | $xml = null; 370 | } elseif ($cfilename === 'xl/worksheets/_rels/sheet1.xml.rels') { 371 | foreach ($this->sheets as $k => $v) { 372 | if ($this->extLinkId) { 373 | $RH = []; 374 | $filename = 'xl/worksheets/_rels/sheet' . ($k + 1) . '.xml.rels'; 375 | foreach ($v['hyperlinks'] as $h) { 376 | if ($h['ID']) { 377 | $RH[] = ''; 378 | } 379 | } 380 | $xml = str_replace('{HYPERLINKS}', implode("\r\n", $RH), $template); 381 | $this->_writeEntry($fh, $cdrec, $filename, $xml); 382 | $entries++; 383 | } 384 | } 385 | $xml = null; 386 | } elseif ($cfilename === '[Content_Types].xml') { 387 | $TYPES = ['']; 388 | foreach ($this->sheets as $k => $v) { 389 | $TYPES[] = ''; 390 | if ($this->extLinkId) { 391 | $TYPES[] = ''; 392 | } 393 | } 394 | $template = str_replace('{TYPES}', implode("\r\n", $TYPES), $template); 395 | $this->_writeEntry($fh, $cdrec, $cfilename, $template); 396 | $entries++; 397 | } elseif ($cfilename === 'xl/styles.xml') { 398 | $NF = $XF = $FONTS = $F_KEYS = $FILLS = $FL_KEYS = []; 399 | $BR = ['']; 400 | $BR_KEYS = [0 => 0]; 401 | foreach ($this->NF as $k => $v) { 402 | $NF[] = ''; 403 | } 404 | foreach ($this->XF as $xf) { 405 | // 0 - num fmt, 1 - align, 2 - font, 3 - fill, 4 - font color, 5 - bgcolor, 6 - border, 7 - font size 406 | // fonts 407 | $F_KEY = $xf[2] . '-' . $xf[4] . '-' . $xf[7]; 408 | if (isset($F_KEYS[$F_KEY])) { 409 | $F_ID = $F_KEYS[$F_KEY]; 410 | } else { 411 | $F_ID = $F_KEYS[$F_KEY] = count($FONTS); 412 | $FONTS[] = '' 413 | . ($xf[7] ? '' : '') 414 | . ($xf[2] & self::F_BOLD ? '' : '') 415 | . ($xf[2] & self::F_ITALIC ? '' : '') 416 | . ($xf[2] & self::F_UNDERLINE ? '' : '') 417 | . ($xf[2] & self::F_STRIKE ? '' : '') 418 | . ($xf[2] & self::F_HYPERLINK ? '' : '') 419 | . ($xf[2] & self::F_COLOR ? '' : '') 420 | . ''; 421 | } 422 | // fills 423 | $FL_KEY = $xf[3] . '-' . $xf[5]; 424 | if (isset($FL_KEYS[$FL_KEY])) { 425 | $FL_ID = $FL_KEYS[$FL_KEY]; 426 | } else { 427 | $FL_ID = $FL_KEYS[$FL_KEY] = count($FILLS); 428 | $FILLS[] = '' : ' />') 437 | . ''; 438 | } 439 | $align = ''; 440 | if ($xf[1] & self::A_LEFT) { 441 | $align .= ' horizontal="left"'; 442 | } elseif ($xf[1] & self::A_RIGHT) { 443 | $align .= ' horizontal="right"'; 444 | } elseif ($xf[1] & self::A_CENTER) { 445 | $align .= ' horizontal="center"'; 446 | } 447 | if ($xf[1] & self::A_TOP) { 448 | $align .= ' vertical="top"'; 449 | } elseif ($xf[1] & self::A_MIDDLE) { 450 | $align .= ' vertical="center"'; 451 | } elseif ($xf[1] & self::A_BOTTOM) { 452 | $align .= ' vertical="bottom"'; 453 | } 454 | if ($xf[1] & self::A_WRAPTEXT) { 455 | $align .= ' wrapText="1"'; 456 | } 457 | 458 | // border 459 | $BR_ID = 0; 460 | if ($xf[6] !== '') { 461 | $b = $xf[6]; 462 | if (isset($BR_KEYS[$b])) { 463 | $BR_ID = $BR_KEYS[$b]; 464 | } else { 465 | $BR_ID = count($BR_KEYS); 466 | $BR_KEYS[$b] = $BR_ID; 467 | $border = ''; 468 | $ba = explode(' ', $b); 469 | if (!isset($ba[1])) { 470 | $ba[] = $ba[0]; 471 | $ba[] = $ba[0]; 472 | $ba[] = $ba[0]; 473 | } 474 | if (!isset($ba[4])) { // diagonal 475 | $ba[] = 'none'; 476 | } 477 | $sides = ['left' => 3, 'right' => 1, 'top' => 0, 'bottom' => 2, 'diagonal' => 4]; 478 | foreach ($sides as $side => $idx) { 479 | $s = 'thin'; 480 | $c = ''; 481 | $va = explode('#', $ba[$idx]); 482 | if (isset($va[1])) { 483 | $s = $va[0] === '' ? 'thin' : $va[0]; 484 | $c = $va[1]; 485 | } elseif (in_array($va[0], $this->BR_STYLE, true)) { 486 | $s = $va[0]; 487 | } else { 488 | $c = $va[0]; 489 | } 490 | if (strlen($c) === 6) { 491 | $c = 'FF' . $c; 492 | } 493 | if ($s && $s !== 'none') { 494 | $border .= '<' . $side . ' style="' . $s . '">' 495 | . '' 496 | . ''; 497 | } else { 498 | $border .= '<' . $side . '/>'; 499 | } 500 | } 501 | $border .= ''; 502 | $BR[] = $border; 503 | } 504 | } 505 | $XF[] = ' 0 ? ' applyNumberFormat="1"' : '') 507 | . ($F_ID > 0 ? ' applyFont="1"' : '') 508 | . ($FL_ID > 0 ? ' applyFill="1"' : '') 509 | . ($BR_ID > 0 ? ' applyBorder="1"' : '') 510 | . ($align ? ' applyAlignment="1">' : '/>'); 511 | } 512 | // wrap collections 513 | array_unshift($NF, ''); 514 | $NF[] = ''; 515 | array_unshift($XF, ''); 516 | $XF[] = ''; 517 | array_unshift($FONTS, ''); 518 | $FONTS[] = ''; 519 | array_unshift($FILLS, ''); 520 | $FILLS[] = ''; 521 | array_unshift($BR, ''); 522 | $BR[] = ''; 523 | 524 | $template = str_replace( 525 | ['{NUMFMTS}', '{FONTS}', '{XF}', '{FILLS}', '{BORDERS}'], 526 | [implode("\r\n", $NF), implode("\r\n", $FONTS), implode("\r\n", $XF), implode("\r\n", $FILLS), implode("\r\n", $BR)], 527 | $template 528 | ); 529 | $this->_writeEntry($fh, $cdrec, $cfilename, $template); 530 | $entries++; 531 | } else { 532 | $this->_writeEntry($fh, $cdrec, $cfilename, $template); 533 | $entries++; 534 | } 535 | } 536 | $before_cd = ftell($fh); 537 | fwrite($fh, $cdrec); 538 | // end of central dir 539 | fwrite($fh, $dirSignatureE); 540 | fwrite($fh, pack('v', 0)); // number of this disk 541 | fwrite($fh, pack('v', 0)); // number of the disk with the start of the central directory 542 | fwrite($fh, pack('v', $entries)); // total # of entries "on this disk" 543 | fwrite($fh, pack('v', $entries)); // total # of entries overall 544 | fwrite($fh, pack('V', mb_strlen($cdrec, '8bit'))); // size of central dir 545 | fwrite($fh, pack('V', $before_cd)); // offset to start of central dir 546 | fwrite($fh, pack('v', mb_strlen($zipComments, '8bit'))); // .zip file comment length 547 | fwrite($fh, $zipComments); 548 | 549 | return true; 550 | } 551 | 552 | protected function _writeEntry($fh, &$cdrec, $cfilename, $data) 553 | { 554 | $zipSignature = "\x50\x4b\x03\x04"; // local file header signature 555 | $dirSignature = "\x50\x4b\x01\x02"; // central dir header signature 556 | 557 | $e = []; 558 | $e['uncsize'] = mb_strlen($data, '8bit'); 559 | // if data to compress is too small, just store it 560 | if ($e['uncsize'] < 256) { 561 | $e['comsize'] = $e['uncsize']; 562 | $e['vneeded'] = 10; 563 | $e['cmethod'] = 0; 564 | $zdata = $data; 565 | } else { // otherwise, compress it 566 | $zdata = gzcompress($data); 567 | $zdata = substr(substr($zdata, 0, -4), 2); // fix crc bug (thanks to Eric Mueller) 568 | $e['comsize'] = mb_strlen($zdata, '8bit'); 569 | $e['vneeded'] = 10; 570 | $e['cmethod'] = 8; 571 | } 572 | $e['bitflag'] = 0; 573 | $e['crc_32'] = crc32($data); 574 | 575 | // Convert date and time to DOS Format, and set then 576 | $lastmod_timeS = str_pad(decbin(date('s') >= 32 ? date('s') - 32 : date('s')), 5, '0', STR_PAD_LEFT); 577 | $lastmod_timeM = str_pad(decbin(date('i')), 6, '0', STR_PAD_LEFT); 578 | $lastmod_timeH = str_pad(decbin(date('H')), 5, '0', STR_PAD_LEFT); 579 | $lastmod_dateD = str_pad(decbin(date('d')), 5, '0', STR_PAD_LEFT); 580 | $lastmod_dateM = str_pad(decbin(date('m')), 4, '0', STR_PAD_LEFT); 581 | $lastmod_dateY = str_pad(decbin(date('Y') - 1980), 7, '0', STR_PAD_LEFT); 582 | 583 | $e['modtime'] = bindec("$lastmod_timeH$lastmod_timeM$lastmod_timeS"); 584 | $e['moddate'] = bindec("$lastmod_dateY$lastmod_dateM$lastmod_dateD"); 585 | $e['offset'] = ftell($fh); 586 | 587 | fwrite($fh, $zipSignature); 588 | fwrite($fh, pack('s', $e['vneeded'])); // version_needed 589 | fwrite($fh, pack('s', $e['bitflag'])); // general_bit_flag 590 | fwrite($fh, pack('s', $e['cmethod'])); // compression_method 591 | fwrite($fh, pack('s', $e['modtime'])); // lastmod_time 592 | fwrite($fh, pack('s', $e['moddate'])); // lastmod_date 593 | fwrite($fh, pack('V', $e['crc_32'])); // crc-32 594 | fwrite($fh, pack('I', $e['comsize'])); // compressed_size 595 | fwrite($fh, pack('I', $e['uncsize'])); // uncompressed_size 596 | fwrite($fh, pack('s', mb_strlen($cfilename, '8bit'))); // file_name_length 597 | fwrite($fh, pack('s', 0)); // extra_field_length 598 | fwrite($fh, $cfilename); // file_name 599 | // ignoring extra_field 600 | fwrite($fh, $zdata); 601 | 602 | // Append it to central dir 603 | $e['external_attributes'] = (substr($cfilename, -1) === '/' && !$zdata) ? 16 : 32; // Directory or file name 604 | $e['comments'] = ''; 605 | 606 | $cdrec .= $dirSignature; 607 | $cdrec .= "\x0\x0"; // version made by 608 | $cdrec .= pack('v', $e['vneeded']); // version needed to extract 609 | $cdrec .= "\x0\x0"; // general bit flag 610 | $cdrec .= pack('v', $e['cmethod']); // compression method 611 | $cdrec .= pack('v', $e['modtime']); // lastmod time 612 | $cdrec .= pack('v', $e['moddate']); // lastmod date 613 | $cdrec .= pack('V', $e['crc_32']); // crc32 614 | $cdrec .= pack('V', $e['comsize']); // compressed filesize 615 | $cdrec .= pack('V', $e['uncsize']); // uncompressed filesize 616 | $cdrec .= pack('v', mb_strlen($cfilename, '8bit')); // file name length 617 | $cdrec .= pack('v', 0); // extra field length 618 | $cdrec .= pack('v', mb_strlen($e['comments'], '8bit')); // file comment length 619 | $cdrec .= pack('v', 0); // disk number start 620 | $cdrec .= pack('v', 0); // internal file attributes 621 | $cdrec .= pack('V', $e['external_attributes']); // internal file attributes 622 | $cdrec .= pack('V', $e['offset']); // relative offset of local header 623 | $cdrec .= $cfilename; 624 | $cdrec .= $e['comments']; 625 | } 626 | 627 | protected function _sheetToXML($idx, $template) 628 | { 629 | // locale floats fr_FR 1.234,56 -> 1234.56 630 | $_loc = setlocale(LC_NUMERIC, 0); 631 | setlocale(LC_NUMERIC, 'C'); 632 | $COLS = []; 633 | $ROWS = []; 634 | // $SHEETVIEWS = 'rtl ? ' rightToLeft="1"' : '').'>'; 635 | $SHEETVIEWS = ''; 636 | $PANE = ''; 637 | if (count($this->sheets[$idx]['rows'])) { 638 | if ($this->sheets[$idx]['frozen'] !== '' || isset($this->sheets[$idx]['frozen'][0]) || isset($this->sheets[$idx]['frozen'][1])) { 639 | // $AC = 'A1'; // Active Cell 640 | $x = $y = 0; 641 | if (is_string($this->sheets[$idx]['frozen'])) { 642 | $AC = $this->sheets[$idx]['frozen']; 643 | self::cell2coord($AC, $x, $y); 644 | } else { 645 | if (isset($this->sheets[$idx]['frozen'][0])) { 646 | $x = $this->sheets[$idx]['frozen'][0]; 647 | } 648 | if (isset($this->sheets[$idx]['frozen'][1])) { 649 | $y = $this->sheets[$idx]['frozen'][1]; 650 | } 651 | $AC = self::coord2cell($x, $y); 652 | } 653 | if ($x > 0 || $y > 0) { 654 | $split = ''; 655 | if ($x > 0) { 656 | $split .= ' xSplit="' . $x . '"'; 657 | } 658 | if ($y > 0) { 659 | $split .= ' ySplit="' . $y . '"'; 660 | } 661 | $activepane = 'bottomRight'; 662 | if ($x > 0 && $y === 0) { 663 | $activepane = 'topRight'; 664 | } 665 | if ($x === 0 && $y > 0) { 666 | $activepane = 'bottomLeft'; 667 | } 668 | $PANE .= ''; 669 | $PANE .= ''; 670 | } 671 | } 672 | if ($this->rtl || $PANE) { 673 | $SHEETVIEWS .= ' 674 | rtl ? ' rightToLeft="1"' : ''); 675 | $SHEETVIEWS .= $PANE ? ">\r\n" . $PANE . "\r\n" : ' />'; 676 | $SHEETVIEWS .= "\r\n"; 677 | } 678 | $COLS[] = ''; 679 | $CUR_ROW = 0; 680 | $COL = []; 681 | foreach ($this->sheets[$idx]['rows'] as $r) { 682 | $CUR_ROW++; 683 | $row = ''; 684 | $CUR_COL = 0; 685 | $RH = 0; // row height 686 | foreach ($r as $v) { 687 | $CUR_COL++; 688 | if (!isset($COL[$CUR_COL])) { 689 | $COL[$CUR_COL] = 0; 690 | } 691 | $cname = $this->num2name($CUR_COL) . $CUR_ROW; 692 | if ($v === null || $v === '') { 693 | $row .= ''; 694 | continue; 695 | } 696 | $ct = $cv = $cf = null; 697 | $N = $A = $F = $FL = $C = $BG = $FS = 0; 698 | $BR = ''; 699 | if (is_string($v)) { 700 | if ($v[0] === "\0") { // RAW value as string 701 | $v = substr($v, 1); 702 | $vl = mb_strlen($v); 703 | } else { 704 | if (strpos($v, '<') !== false) { // tags? 705 | if (strpos($v, '') !== false) { 706 | $F += self::F_BOLD; 707 | } 708 | if (strpos($v, '') !== false) { 709 | $F += self::F_ITALIC; 710 | } 711 | if (strpos($v, '') !== false) { 712 | $F += self::F_UNDERLINE; 713 | } 714 | if (strpos($v, '') !== false) { 715 | $F += self::F_STRIKE; 716 | } 717 | if (preg_match('/]+)>/', $v, $m)) { 718 | if (preg_match('/ color="([^"]+)"/', $m[1], $m2)) { 719 | $F += self::F_COLOR; 720 | $c = ltrim($m2[1], '#'); 721 | $C = strlen($c) === 8 ? $c : ('FF' . $c); 722 | } 723 | if (preg_match('/ bgcolor="([^"]+)"/', $m[1], $m2)) { 724 | $FL += self::FL_COLOR; 725 | $c = ltrim($m2[1], '#'); 726 | $BG = strlen($c) === 8 ? $c : ('FF' . $c); 727 | } 728 | if (preg_match('/ height="([^"]+)"/', $m[1], $m2)) { 729 | $RH = $m2[1]; 730 | } 731 | if (preg_match('/ nf="([^"]+)"/', $m[1], $m2)) { 732 | $c = htmlspecialchars_decode($m2[1], ENT_QUOTES); 733 | $N = $this->getNumFmtId($c); 734 | } 735 | if (preg_match('/ border="([^"]+)"/', $m[1], $m2)) { 736 | $b = htmlspecialchars_decode($m2[1], ENT_QUOTES); 737 | if ($b && $b !== 'none') { 738 | $BR = $b; 739 | } 740 | } 741 | if (preg_match('/ font-size="([^"]+)"/', $m[1], $m2)) { 742 | $FS = (int)$m2[1]; 743 | if ($RH === 0) { // fix row height 744 | $RH = ($FS > $this->defaultFontSize) ? round($FS * 1.50, 1) : 0; 745 | } 746 | } 747 | } 748 | if (strpos($v, '') !== false) { 749 | $A += self::A_LEFT; 750 | } 751 | if (strpos($v, '
') !== false) { 752 | $A += self::A_CENTER; 753 | } 754 | if (strpos($v, '') !== false) { 755 | $A += self::A_RIGHT; 756 | } 757 | if (strpos($v, '') !== false) { 758 | $A += self::A_TOP; 759 | } 760 | if (strpos($v, '') !== false) { 761 | $A += self::A_MIDDLE; 762 | } 763 | if (strpos($v, '') !== false) { 764 | $A += self::A_BOTTOM; 765 | } 766 | if (strpos($v, '') !== false) { 767 | $A += self::A_WRAPTEXT; 768 | } 769 | if (preg_match('/(.*?)<\/a>/i', $v, $m)) { 770 | $h = explode('#', $m[1]); 771 | $this->extLinkId++; 772 | $this->sheets[$idx]['hyperlinks'][] = ['ID' => 'rId' . $this->extLinkId, 'R' => $cname, 'H' => $h[0], 'L' => isset($h[1]) ? $h[1] : '']; 773 | $F += self::F_HYPERLINK; // Hyperlink 774 | } 775 | if (preg_match('/(.*?)<\/a>/i', $v, $m)) { 776 | $this->extLinkId++; 777 | $this->sheets[$idx]['hyperlinks'][] = ['ID' => 'rId' . $this->extLinkId, 'R' => $cname, 'H' => $m[1], 'L' => '']; 778 | $F += self::F_HYPERLINK; // mailto hyperlink 779 | } 780 | if (preg_match('/(.*?)<\/a>/i', $v, $m)) { 781 | $this->sheets[$idx]['hyperlinks'][] = ['ID' => null, 'R' => $cname, 'H' => null, 'L' => $m[1]]; 782 | $F += self::F_HYPERLINK; // internal hyperlink 783 | } 784 | if (preg_match('/]*)>/', $v, $m)) { 785 | $cf = strip_tags($v); 786 | $v = 'formula'; 787 | if (preg_match('/ v="([^"]+)"/', $m[1], $m2)) { 788 | $v = $m2[1]; 789 | } 790 | } else { 791 | $v = strip_tags($v); 792 | } 793 | } // tags 794 | $vl = mb_strlen($v); 795 | if ($N) { 796 | $cv = ltrim($v, '+'); 797 | } elseif ($v === '0' || preg_match('/^[-+]?[1-9]\d{0,14}$/', $v)) { // Integer as General 798 | $cv = ltrim($v, '+'); 799 | if ($vl > 10) { 800 | $N = self::N_INT; // [1] 0 801 | } 802 | } elseif (preg_match('/^[-+]?(0|[1-9]\d*)\.(\d+)$/', $v, $m)) { 803 | $cv = ltrim($v, '+'); 804 | if (strlen($m[2]) < 3) { 805 | $N = self::N_DEC; 806 | } 807 | } elseif (preg_match('/^\$[-+]?[0-9\.]+$/', $v)) { // currency $? 808 | $N = self::N_DOLLAR; 809 | $cv = ltrim($v, '+$'); 810 | } elseif (preg_match('/^[-+]?[0-9\.]+( ₽| €)$/u', $v, $m)) { // currency ₽ €? 811 | if ($m[1] === ' ₽') { 812 | $N = self::N_RUB; 813 | } elseif ($m[1] === ' €') { 814 | $N = self::N_EURO; 815 | } 816 | $cv = trim($v, ' +₽€'); 817 | } elseif (preg_match('/^([-+]?\d+)%$/', $v, $m)) { 818 | $cv = round($m[1] / 100, 2); 819 | $N = self::N_PERCENT_INT; // [9] 0% 820 | } elseif (preg_match('/^([-+]?\d+\.\d+)%$/', $v, $m)) { 821 | $cv = round($m[1] / 100, 4); 822 | $N = self::N_PRECENT_DEC; // [10] 0.00% 823 | } elseif (preg_match('/^(\d\d\d\d)-(\d\d)-(\d\d)$/', $v, $m)) { 824 | $cv = $this->date2excel($m[1], $m[2], $m[3]); 825 | $N = self::N_DATE; // [14] mm-dd-yy 826 | } elseif (preg_match('/^(\d\d)\/(\d\d)\/(\d\d\d\d)$/', $v, $m)) { 827 | $cv = $this->date2excel($m[3], $m[2], $m[1]); 828 | $N = self::N_DATE; // [14] mm-dd-yy 829 | } elseif (preg_match('/^(\d\d):(\d\d):(\d\d)$/', $v, $m)) { 830 | $cv = $this->date2excel(0, 0, 0, $m[1], $m[2], $m[3]); 831 | $N = self::N_TIME; // time 832 | } elseif (preg_match('/^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/', $v, $m)) { 833 | $cv = $this->date2excel($m[1], $m[2], $m[3], $m[4], $m[5], $m[6]); 834 | $N = ((int)$m[1] === 0) ? self::N_TIME : self::N_DATETIME; // [22] m/d/yy h:mm 835 | } elseif (preg_match('/^(\d\d)\/(\d\d)\/(\d\d\d\d) (\d\d):(\d\d):(\d\d)$/', $v, $m)) { 836 | $cv = $this->date2excel($m[3], $m[2], $m[1], $m[4], $m[5], $m[6]); 837 | $N = self::N_DATETIME; // [22] m/d/yy h:mm 838 | } elseif (preg_match('/^[0-9+-.]+$/', $v)) { // Long ? 839 | $A += ($A & (self::A_LEFT | self::A_CENTER)) ? 0 : self::A_RIGHT; 840 | } elseif (preg_match('/^https?:\/\/\S+$/i', $v)) { 841 | $h = explode('#', $v); 842 | $this->extLinkId++; 843 | $this->sheets[$idx]['hyperlinks'][] = ['ID' => 'rId' . $this->extLinkId, 'R' => $cname, 'H' => $h[0], 'L' => isset($h[1]) ? $h[1] : '']; 844 | $F += self::F_HYPERLINK; // Hyperlink 845 | } elseif (preg_match("/^[a-zA-Z0-9_\.\-]+@([a-zA-Z0-9][a-zA-Z0-9\-]*\.)+[a-zA-Z]{2,}$/", $v)) { 846 | $this->extLinkId++; 847 | $this->sheets[$idx]['hyperlinks'][] = ['ID' => 'rId' . $this->extLinkId, 'R' => $cname, 'H' => 'mailto:' . $v, 'L' => '']; 848 | $F += self::F_HYPERLINK; // Hyperlink 849 | } 850 | if (($N === self::N_DATE || $N === self::N_DATETIME) && $cv < 0) { 851 | $cv = null; 852 | $N = 0; 853 | } 854 | } 855 | if ($cv === null) { 856 | $v = $this->esc($v); 857 | if ($cf) { 858 | $ct = 'str'; 859 | $cv = $v; 860 | } elseif (mb_strlen($v) > 160) { 861 | $ct = 'inlineStr'; 862 | $cv = $v; 863 | } else { 864 | $ct = 's'; // shared string 865 | $cv = false; 866 | $skey = '~' . $v; 867 | if (isset($this->SI_KEYS[$skey])) { 868 | $cv = $this->SI_KEYS[$skey]; 869 | } 870 | if ($cv === false) { 871 | $this->SI[] = $v; 872 | $cv = count($this->SI) - 1; 873 | $this->SI_KEYS[$skey] = $cv; 874 | } 875 | } 876 | } 877 | } elseif (is_int($v)) { 878 | $vl = mb_strlen((string)$v); 879 | $cv = $v; 880 | } elseif (is_float($v)) { 881 | $vl = mb_strlen((string)$v); 882 | $cv = $v; 883 | } elseif ($v instanceof \DateTime) { 884 | $vl = 16; 885 | $cv = $this->date2excel($v->format('Y'), $v->format('m'), $v->format('d'), $v->format('H'), $v->format('i'), $v->format('s')); 886 | $N = self::N_DATETIME; // [22] m/d/yy h:mm 887 | } else { 888 | continue; 889 | } 890 | $COL[$CUR_COL] = max($vl, $COL[$CUR_COL]); 891 | $cs = 0; 892 | if (($N + $A + $F + $FL + $FS > 0) || $BR !== '') { 893 | if ($FL === self::FL_COLOR) { 894 | $FL += self::FL_SOLID; 895 | } 896 | if (($F & self::F_HYPERLINK) && !($F & self::F_COLOR)) { 897 | $F += self::F_COLOR; 898 | $C = 'FF0563C1'; 899 | } 900 | $XF_KEY = $N . '-' . $A . '-' . $F . '-' . $FL . '-' . $C . '-' . $BG . '-' . $BR . '-' . $FS; 901 | if (isset($this->XF_KEYS[$XF_KEY])) { 902 | $cs = $this->XF_KEYS[$XF_KEY]; 903 | } 904 | if ($cs === 0) { 905 | $cs = count($this->XF); 906 | $this->XF_KEYS[$XF_KEY] = $cs; 907 | $this->XF[] = [$N, $A, $F, $FL, $C, $BG, $BR, $FS]; 908 | } 909 | } 910 | $row .= '' 911 | . ($cf ? '' . $cf . '' : '') 912 | . ($ct === 'inlineStr' ? '' . $cv . '' : '' . $cv . '') . "\r\n"; 913 | } 914 | $ROWS[] = '' . $row . ""; 915 | } 916 | foreach ($COL as $k => $max) { 917 | $w = isset($this->sheets[$idx]['colwidth'][$k]) ? $this->sheets[$idx]['colwidth'][$k] : min($max + 1, 60); 918 | $COLS[] = ''; 919 | } 920 | $COLS[] = ''; 921 | $REF = 'A1:' . $this->num2name(count($COL)) . $CUR_ROW; 922 | } else { 923 | $ROWS[] = '0'; 924 | $REF = 'A1:A1'; 925 | } 926 | 927 | $AUTOFILTER = ''; 928 | if ($this->sheets[$idx]['autofilter']) { 929 | $AUTOFILTER = ''; 930 | } 931 | 932 | $MERGECELLS = []; 933 | if (count($this->sheets[$idx]['mergecells'])) { 934 | $MERGECELLS[] = ''; 935 | $MERGECELLS[] = ''; 936 | foreach ($this->sheets[$idx]['mergecells'] as $m) { 937 | $MERGECELLS[] = ''; 938 | } 939 | $MERGECELLS[] = ''; 940 | } 941 | 942 | $HYPERLINKS = []; 943 | if (count($this->sheets[$idx]['hyperlinks'])) { 944 | $HYPERLINKS[] = ''; 945 | foreach ($this->sheets[$idx]['hyperlinks'] as $h) { 946 | $HYPERLINKS[] = ''; 947 | } 948 | $HYPERLINKS[] = ''; 949 | } 950 | 951 | //restore locale 952 | setlocale(LC_NUMERIC, $_loc); 953 | 954 | return str_replace( 955 | ['{REF}', '{COLS}', '{ROWS}', '{AUTOFILTER}', '{MERGECELLS}', '{HYPERLINKS}', '{SHEETVIEWS}'], 956 | [ 957 | $REF, 958 | implode("\r\n", $COLS), 959 | implode("\r\n", $ROWS), 960 | $AUTOFILTER, 961 | implode("\r\n", $MERGECELLS), 962 | implode("\r\n", $HYPERLINKS), 963 | $SHEETVIEWS 964 | ], 965 | $template 966 | ); 967 | } 968 | 969 | public function num2name($num) 970 | { 971 | $numeric = ($num - 1) % 26; 972 | $letter = chr(65 + $numeric); 973 | $num2 = (int)(($num - 1) / 26); 974 | if ($num2 > 0) { 975 | return $this->num2name($num2) . $letter; 976 | } 977 | return $letter; 978 | } 979 | 980 | public function date2excel($year, $month, $day, $hours = 0, $minutes = 0, $seconds = 0) 981 | { 982 | $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400; 983 | if ((int)$year === 0) { 984 | return $excelTime; 985 | } 986 | // self::CALENDAR_WINDOWS_1900 987 | $excel1900isLeapYear = True; 988 | if (($year === 1900) && ($month <= 2)) { 989 | $excel1900isLeapYear = False; 990 | } 991 | $myExcelBaseDate = 2415020; 992 | // Julian base date Adjustment 993 | if ($month > 2) { 994 | $month -= 3; 995 | } else { 996 | $month += 9; 997 | --$year; 998 | } 999 | $century = substr($year, 0, 2); 1000 | $decade = substr($year, 2, 2); 1001 | // Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0) 1002 | $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myExcelBaseDate + $excel1900isLeapYear; 1003 | return (float)$excelDate + $excelTime; 1004 | } 1005 | 1006 | public function setDefaultFont($name) 1007 | { 1008 | $this->defaultFont = $name; 1009 | return $this; 1010 | } 1011 | 1012 | public function setDefaultFontSize($size) 1013 | { 1014 | $this->defaultFontSize = $size; 1015 | return $this; 1016 | } 1017 | 1018 | public function setTitle($title) 1019 | { 1020 | $this->title = $title; 1021 | return $this; 1022 | } 1023 | public function setSubject($subject) 1024 | { 1025 | $this->subject = $subject; 1026 | return $this; 1027 | } 1028 | public function setAuthor($author) 1029 | { 1030 | $this->author = $author; 1031 | return $this; 1032 | } 1033 | public function setCompany($company) 1034 | { 1035 | $this->company = $company; 1036 | return $this; 1037 | } 1038 | public function setManager($manager) 1039 | { 1040 | $this->manager = $manager; 1041 | return $this; 1042 | } 1043 | public function setKeywords($keywords) 1044 | { 1045 | $this->keywords = $keywords; 1046 | return $this; 1047 | } 1048 | public function setDescription($description) 1049 | { 1050 | $this->description = $description; 1051 | return $this; 1052 | } 1053 | public function setCategory($category) 1054 | { 1055 | $this->category = $category; 1056 | return $this; 1057 | } 1058 | 1059 | public function setApplication($application) 1060 | { 1061 | $this->application = $application; 1062 | return $this; 1063 | } 1064 | public function setLastModifiedBy($lastModifiedBy) 1065 | { 1066 | $this->lastModifiedBy = $lastModifiedBy; 1067 | return $this; 1068 | } 1069 | 1070 | public function autoFilter($range) 1071 | { 1072 | $this->sheets[$this->curSheet]['autofilter'] = $range; 1073 | return $this; 1074 | } 1075 | 1076 | public function mergeCells($range) 1077 | { 1078 | $this->sheets[$this->curSheet]['mergecells'][] = $range; 1079 | return $this; 1080 | } 1081 | 1082 | public function setColWidth($col, $width) 1083 | { 1084 | $this->sheets[$this->curSheet]['colwidth'][$col] = $width; 1085 | return $this; 1086 | } 1087 | public function rightToLeft($value = true) 1088 | { 1089 | $this->rtl = $value; 1090 | return $this; 1091 | } 1092 | 1093 | public function esc($str) 1094 | { 1095 | // XML UTF-8: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] 1096 | // but we use fast version 1097 | return str_replace(['&', '<', '>', "\x00", "\x03", "\x0B"], ['&', '<', '>', '', '', ''], $str); 1098 | } 1099 | 1100 | public function getNumFmtId($code) 1101 | { 1102 | if (isset($this->NF[$code])) { // id? 1103 | return (int)$code; 1104 | } 1105 | if (isset($this->NF_KEYS[$code])) { 1106 | return $this->NF_KEYS[$code]; 1107 | } 1108 | $id = 197 + count($this->NF); // custom 1109 | $this->NF[$id] = $code; 1110 | $this->NF_KEYS[$code] = $id; 1111 | return $id; 1112 | } 1113 | 1114 | 1115 | public static function raw($value) 1116 | { 1117 | return "\0" . $value; 1118 | } 1119 | 1120 | public static function cell2coord($cell, &$x, &$y) 1121 | { 1122 | $x = $y = 0; 1123 | $lettercount = 0; 1124 | $cell = str_replace([' ', '\t', '\r', '\n', '\v', '\0'], '', $cell); 1125 | if (empty($cell)) { 1126 | return; 1127 | } 1128 | $cell = strtoupper($cell); 1129 | for ($i = 0, $len = strlen($cell); $i < $len; $i++) { 1130 | if ($cell[$i] >= 'A' && $cell[$i] <= 'Z') { 1131 | $lettercount++; 1132 | } 1133 | } 1134 | if ($lettercount > 0) { 1135 | $x = ord($cell[$lettercount - 1]) - ord('A'); 1136 | $e = 1; 1137 | for ($i = $lettercount - 2; $i >= 0; $i--) { 1138 | $x += (ord($cell[$i]) - ord('A') + 1) * (26 ** $e); 1139 | $e++; 1140 | } 1141 | } 1142 | if ($lettercount < strlen($cell)) { 1143 | $y = ((int)substr($cell, $lettercount)) - 1; 1144 | } 1145 | } 1146 | 1147 | public static function coord2cell($x, $y) 1148 | { 1149 | $c = ''; 1150 | for ($i = $x; $i >= 0; $i = ((int)($i / 26)) - 1) { 1151 | $c = chr(ord('A') + $i % 26) . $c; 1152 | } 1153 | return $c . ($y + 1); 1154 | } 1155 | 1156 | public function freezePanes($cell) 1157 | { 1158 | $this->sheets[$this->curSheet]['frozen'] = $cell; 1159 | return $this; 1160 | } 1161 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhpXlsxGenerator 2 | PHP XLSX generator - Export data to Excel XLSX file. No external tools and libraries are required. 3 | 4 | Developed by CodexWorld - codexworld.com@gmail.com - [visit author website](https://www.codexworld.com/) 5 | 6 | _Please [donate](https://www.paypal.me/codexworld) to us for more motivation :)_ 7 | 8 | ## Usage Guide 9 | Please see our tutorial for detailed instructions - [Export Data to Excel using PHP](https://www.codexworld.com/export-data-to-excel-in-php/) 10 | --------------------------------------------------------------------------------