diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2d8606 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5c5505 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# SimpleXLSXGen class 0.9.10 (Official) +[](https://www.patreon.com/shuchkin) [](https://github.com/shuchkin/simplexlsxgen/blob/master/license.md) [](https://github.com/shuchkin/simplexlsxgen/stargazers) [](https://github.com/shuchkin/simplexlsxgen/network) [](https://github.com/shuchkin/simplexlsxgen/issues) + +Export data to Excel XLSX file. PHP XLSX generator. No external tools and libraries.
+(!) XLSX reader [here](https://github.com/shuchkin/simplexlsx). + +**Sergey Shuchkin** 2020
+ +*Hey, bro, please ★ the package for my motivation :)* + +## Basic Usage +```php +$books = [ + ['ISBN', 'title', 'author', 'publisher', 'ctry' ], + [618260307, 'The Hobbit', 'J. R. R. Tolkien', 'Houghton Mifflin', 'USA'], + [908606664, 'Slinky Malinki', 'Lynley Dodd', 'Mallinson Rendel', 'NZ'] +]; +$xlsx = SimpleXLSXGen::fromArray( $books ); +$xlsx->saveAs('books.xlsx'); +``` +![XLSX screenshot](books.png) +``` +// SimpleXLSXGen::download() or SimpleXSLSXGen::downloadAs('table.xlsx'); +``` + +## Installation +The recommended way to install this library is [through Composer](https://getcomposer.org). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This will install the latest supported version: +```bash +$ composer require shuchkin/simplexlsxgen +``` +or download class [here](https://github.com/shuchkin/simplexlsxgen/blob/master/src/SimpleXLSXGen.php) + +## Examples +### Data types +```php +$data = [ + ['Integer', 123], + ['Float', 12.35], + ['Procent', '12%'], + ['Date','2020-05-20'], + ['Datetime', '2020-05-20 02:38:00'], + ['String', 'See SimpleXLSXGen column autosize feature'] +]; +SimpleXLSXGen::fromArray( $data )->saveAs('datatypes.xlsx'); +``` +![XLSX screenshot](datatypes.png) + +## History +v0.9.10 (2020-05-20) initial relese \ No newline at end of file diff --git a/books.png b/books.png new file mode 100644 index 0000000..eb713ec Binary files /dev/null and b/books.png differ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4547ccf --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "shuchkin/simplexlsxgen", + "description": "Export data to Excel XLSx file. PHP XLSX generator.", + "keywords": ["php", "excel", "xlsx", "generator", "creator", "backend"], + "homepage": "https://github.com/shuchkin/simplexlsxgen", + "license": "MIT", + "authors": [ + { + "name": "Sergey Shuchkin (SMSPILOT)", + "email": "sergey.shuchkin@gmail.com", + "homepage": "https://shuchkin.ru/" + } + ], + "autoload": { + "classmap": [ + "src/SimpleXLSXGen.php" + ] + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..bcf801c --- /dev/null +++ b/composer.lock @@ -0,0 +1,20 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "9e2d3c362fd0bf028d5ee5081bdf5e4b", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "ext-mbstring": "*", + "ext-zlib": "*" + }, + "platform-dev": [] +} diff --git a/datatypes.png b/datatypes.png new file mode 100644 index 0000000..af9fd04 Binary files /dev/null and b/datatypes.png differ diff --git a/LICENSE b/license.md similarity index 93% rename from LICENSE rename to license.md index 20e375f..367e366 100644 --- a/LICENSE +++ b/license.md @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2020 Sergey Shuchkin +Copyright (c) 2014 Lukas Martinelli Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/src/SimpleXLSXGen.php b/src/SimpleXLSXGen.php new file mode 100644 index 0000000..5856b35 --- /dev/null +++ b/src/SimpleXLSXGen.php @@ -0,0 +1,383 @@ +rows = []; + $this->template = [ + '[Content_Types].xml' => ' + + + + + + + + + +', +/* */ + '_rels/.rels' => ' + + + + +', + 'docProps/app.xml' => ' + +0 +'.__CLASS__.'', + 'docProps/core.xml' => ' + +{DATE} +en-US +{DATE} +1 +', + 'xl/_rels/workbook.xml.rels' => ' + + + +', + 'xl/worksheets/sheet1.xml' => ' +{COLS}{ROWS}', + 'xl/sharedStrings.xml' => ' +{STRINGS}', + 'xl/styles.xml' => ' + + + + + + + + + + + + + + +', + 'xl/workbook.xml' => ' + + + + + + + + +' + ]; + // + // 01001200 + // Простой шаблонБудем делать генератор + } + public static function fromArray( array $rows ) { + $xlsx = new self(); + $xlsx->setRows( $rows ); + return $xlsx; + } + public function setRows( $rows ) { + if ( is_array( $rows ) && isset( $rows[0] ) && is_array($rows[0]) ) { + $this->rows = $rows; + } else { + $this->rows = []; + } + } + public function __toString() { + $fh = fopen( 'php://memory', 'wb' ); + if ( ! $fh ) { + return ''; + } + + if ( ! $this->_generate( $fh ) ) { + fclose( $fh ); + return ''; + } + $size = ftell( $fh ); + fseek( $fh, 0); + + return (string) fread( $fh, $size ); + } + public function saveAs( $filename ) { + $fh = fopen( $filename, 'wb' ); + if (!$fh) { + return false; + } + if ( !$this->_generate($fh) ) { + fclose($fh); + return false; + } + fclose($fh); + + return true; + } + public function download() { + return $this->downloadAs( gmdate('YmdHi.xlsx') ); + } + public function downloadAs( $filename ) { + $fh = fopen('php://memory','wb'); + if (!$fh) { + return false; + } + + if ( !$this->_generate( $fh )) { + fclose( $fh ); + return false; + } + + $size = ftell($fh); + + header('Content-type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + header('Content-Disposition: attachment; filename="'.$filename.'"'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T' , time() )); + header('Content-Length: '.$size); + + while( ob_get_level() ) { + ob_end_clean(); + } + fseek($fh,0); + fpassthru( $fh ); + + fclose($fh); + return true; + } + + private function _generate( $fh ) { + + $zipSignature = "\x50\x4b\x03\x04"; // local file header signature + $dirSignature = "\x50\x4b\x01\x02"; // central dir header signature + $dirSignatureE= "\x50\x4b\x05\x06"; // end of central dir signature + + $zipComments = 'Generated by '.__CLASS__.' PHP class, thanks sergey.shuchkin@gmail.com'; + +// $fh = fopen( $filename, 'wb' ); + + if (!$fh) { + return false; + } + + $SI = []; + $COLS = []; + $ROWS = []; + if ( count($this->rows) ) { + $CUR_ROW = 0; + $COL = []; + foreach( $this->rows as $r ) { + $CUR_ROW++; + $row = ''; + $CUR_COL = 0; + foreach( $r as $k => $v ) { + $CUR_COL++; + if ( !isset($COL[ $CUR_COL ])) { + $COL[ $CUR_COL ] = 0; + } + if ( $v === null || $v === '' ) { + continue; + } + $COL[ $CUR_COL ] = max( mb_strlen( (string) $v ), $COL[ $CUR_COL ] ); + + $cname = $this->_num2name($CUR_COL).$CUR_ROW; + + $ct = $cs = null; + + if ( is_string($v) ) { + + if ( preg_match( '/^[-+]?\d{1,18}$/', $v ) ) { + $cv = ltrim($v,'+'); + } elseif ( preg_match('/^[-+]?\d+\.?\d*$/', $v ) ) { + $cv = ltrim($v,'+'); + } elseif ( preg_match('/^([-+]?\d+)%$/', $v, $m) ) { + $cv = round( $m[1] / 100, 2); + $cs = 1; // [9] 0% + } elseif ( preg_match('/^([-+]\d+\.\d*)%$/', $v, $m) ) { + $cv = round( $m[1] / 100, 4 ); + $cs = 2; // [10] 0.00% + } elseif ( preg_match('/^(\d\d\d\d)-(\d\d)-(\d\d)$/', $v, $m ) ){ + $cv = $this->_date2excel($m[1],$m[2],$m[3]); + $cs = 3; // [14] mm-dd-yy + } elseif ( preg_match('/^(\d\d):(\d\d):(\d\d)$/', $v, $m ) ){ + $cv = $this->_date2excel(0,0,0,$m[1],$m[2],$m[3]); + $cs = 4; // [14] mm-dd-yy + } elseif ( preg_match('/^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/', $v, $m ) ){ + $cv = $this->_date2excel($m[1],$m[2],$m[3],$m[4],$m[5],$m[6]); + $cs = 5; // [22] m/d/yy h:mm + } else { + $ct = 's'; // shared string + $v = htmlentities($v, ENT_QUOTES); + $cv = array_search( $v, $SI, true ); + if ( $cv === false ) { + $SI[] = $v; + $cv = count( $SI ) - 1; + } + } + } elseif ( is_int( $v ) || is_float( $v ) ) { + $cv = $v; + } else { + continue; + } + + $row .= '' . $cv . "\r\n"; + } + $ROWS[] = $row . "\r\n"; + } + foreach ( $COL as $k => $max ) { + $COLS[] = ''; + } + $REF = 'A1:'.$this->_num2name(count($COLS)).$CUR_ROW; + } else { +// $COLS[] = ''; + $COLS[] = ''; + $ROWS[] = '0'; + $REF = 'A1:A1'; + $SI[] = 'No Data'; + } + $SI_CNT = count($SI); + $SI = ''.implode("\r\n", $SI).''; + + $cdrec = ''; + + foreach ($this->template as $cfilename => $data ) { + + if ( $cfilename === 'docProps/core.xml' ) { + $data = str_replace('{DATE}', gmdate('Y-m-d\TH:i:s\Z'), $data); + } elseif ( $cfilename === 'xl/sharedStrings.xml' ) { + $data = str_replace(['{CNT}', '{STRINGS}'], [ $SI_CNT, $SI ], $data ); + } elseif ( $cfilename === 'xl/worksheets/sheet1.xml' ) { + $data = str_replace(['{REF}','{COLS}','{ROWS}'],[ $REF, implode("\r\n", $COLS), implode("\r\n",$ROWS) ], $data ); + } + + $e = []; + $e['uncsize'] = mb_strlen($data, '8bit'); + + // if data to compress is too small, just store it + if($e['uncsize'] < 256){ + $e['comsize'] = $e['uncsize']; + $e['vneeded'] = 10; + $e['cmethod'] = 0; + $zdata = $data; + } else{ // otherwise, compress it + $zdata = gzcompress($data); + $zdata = substr(substr($zdata, 0, - 4 ), 2); // fix crc bug (thanks to Eric Mueller) + $e['comsize'] = mb_strlen($zdata, '8bit'); + $e['vneeded'] = 10; + $e['cmethod'] = 8; + } + + $e['bitflag'] = 0; + $e['crc_32'] = crc32($data); + + // Convert date and time to DOS Format, and set then + $lastmod_timeS = str_pad(decbin(date('s')>=32?date('s')-32:date('s')), 5, '0', STR_PAD_LEFT); + $lastmod_timeM = str_pad(decbin(date('i')), 6, '0', STR_PAD_LEFT); + $lastmod_timeH = str_pad(decbin(date('H')), 5, '0', STR_PAD_LEFT); + $lastmod_dateD = str_pad(decbin(date('d')), 5, '0', STR_PAD_LEFT); + $lastmod_dateM = str_pad(decbin(date('m')), 4, '0', STR_PAD_LEFT); + $lastmod_dateY = str_pad(decbin(date('Y')-1980), 7, '0', STR_PAD_LEFT); + + # echo "ModTime: $lastmod_timeS-$lastmod_timeM-$lastmod_timeH (".date("s H H").")\n"; + # echo "ModDate: $lastmod_dateD-$lastmod_dateM-$lastmod_dateY (".date("d m Y").")\n"; + $e['modtime'] = bindec("$lastmod_timeH$lastmod_timeM$lastmod_timeS"); + $e['moddate'] = bindec("$lastmod_dateY$lastmod_dateM$lastmod_dateD"); + + $e['offset'] = ftell($fh); + + /** @noinspection DisconnectedForeachInstructionInspection */ + fwrite($fh, $zipSignature); + fwrite($fh, pack('s', $e['vneeded'])); // version_needed + fwrite($fh, pack('s', $e['bitflag'])); // general_bit_flag + fwrite($fh, pack('s', $e['cmethod'])); // compression_method + fwrite($fh, pack('s', $e['modtime'])); // lastmod_time + fwrite($fh, pack('s', $e['moddate'])); // lastmod_date + fwrite($fh, pack('V', $e['crc_32'])); // crc-32 + fwrite($fh, pack('I', $e['comsize'])); // compressed_size + fwrite($fh, pack('I', $e['uncsize'])); // uncompressed_size + fwrite($fh, pack('s', mb_strlen($cfilename, '8bit'))); // file_name_length + /** @noinspection DisconnectedForeachInstructionInspection */ + fwrite($fh, pack('s', 0)); // extra_field_length + fwrite($fh, $cfilename); // file_name + // ignoring extra_field + fwrite($fh, $zdata); + + // Append it to central dir + $e['external_attributes'] = (substr($cfilename, -1) === '/'&&!$zdata)?16:32; // Directory or file name + $e['comments'] = ''; + + $cdrec .= $dirSignature; + $cdrec .= "\x0\x0"; // version made by + $cdrec .= pack('v', $e['vneeded']); // version needed to extract + $cdrec .= "\x0\x0"; // general bit flag + $cdrec .= pack('v', $e['cmethod']); // compression method + $cdrec .= pack('v', $e['modtime']); // lastmod time + $cdrec .= pack('v', $e['moddate']); // lastmod date + $cdrec .= pack('V', $e['crc_32']); // crc32 + $cdrec .= pack('V', $e['comsize']); // compressed filesize + $cdrec .= pack('V', $e['uncsize']); // uncompressed filesize + $cdrec .= pack('v', mb_strlen($cfilename,'8bit')); // file name length + $cdrec .= pack('v', 0); // extra field length + $cdrec .= pack('v', mb_strlen($e['comments'],'8bit')); // file comment length + $cdrec .= pack('v', 0); // disk number start + $cdrec .= pack('v', 0); // internal file attributes + $cdrec .= pack('V', $e['external_attributes']); // internal file attributes + $cdrec .= pack('V', $e['offset']); // relative offset of local header + $cdrec .= $cfilename; + $cdrec .= $e['comments']; + } + $before_cd = ftell($fh); + fwrite($fh, $cdrec); + + // end of central dir + fwrite($fh, $dirSignatureE); + fwrite($fh, pack('v', 0)); // number of this disk + fwrite($fh, pack('v', 0)); // number of the disk with the start of the central directory + fwrite($fh, pack('v', count( $this->template ))); // total # of entries "on this disk" + fwrite($fh, pack('v', count( $this->template ))); // total # of entries overall + fwrite($fh, pack('V', mb_strlen($cdrec,'8bit'))); // size of central dir + fwrite($fh, pack('V', $before_cd)); // offset to start of central dir + fwrite($fh, pack('v', mb_strlen($zipComments,'8bit'))); // .zip file comment length + fwrite($fh, $zipComments); + + return true; + } + private function _num2name($num) { + $numeric = ($num-1) % 26; + $letter = chr( 65 + $numeric ); + $num2 = (int) ( ($num-1) / 26 ); + if ( $num2 > 0 ) { + return $this->_num2name( $num2 - 1 ) . $letter; + } + + return $letter; + } + private function _date2excel($year, $month, $day, $hours=0, $minutes=0, $seconds=0) { + // self::CALENDAR_WINDOWS_1900 + $excel1900isLeapYear = True; + if (((int)$year === 1900) && ($month <= 2)) { $excel1900isLeapYear = False; } + $myExcelBaseDate = 2415020; + + // Julian base date Adjustment + if ($month > 2) { + $month -= 3; + } else { + $month += 9; + --$year; + } + // Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0) + $century = substr($year,0,2); + $decade = substr($year,2,2); + $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myExcelBaseDate + $excel1900isLeapYear; + + $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400; + + return (float) $excelDate + $excelTime; + } +} \ No newline at end of file