diff --git a/README.md b/README.md
index 37766c2..a30258c 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
# SimpleXLSXGen
+
[](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.
@@ -11,6 +12,7 @@ Export data to Excel XLSX file. PHP XLSX generator. No external tools and librar
*Hey, bro, please ★ the package for my motivation :) and [donate](https://opencollective.com/simplexlsx) for more motivation!*
## Basic Usage
+
```php
$books = [
['ISBN', 'title', 'author', 'publisher', 'ctry' ],
@@ -23,6 +25,7 @@ $xlsx->saveAs('books.xlsx'); // or downloadAs('books.xlsx') or $xlsx_content = (
![XLSX screenshot](books.png)
## Installation
+
The recommended way to install this library is [through Composer](https://getcomposer.org).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
@@ -33,6 +36,7 @@ $ composer require shuchkin/simplexlsxgen
or download class [here](https://github.com/shuchkin/simplexlsxgen/blob/master/src/SimpleXLSXGen.php)
## Examples
+
Use UTF-8 encoded strings.
### Data types
@@ -136,6 +140,9 @@ exit();
// Autofilter
$xlsx->autoFilter('A1:B10');
+// Comment cell
+$xlsx->setComment("B2", "Comment text", "Comment author");
+
// Freeze rows and columns from top-left corner up to, but not including,
// the row and column of the indicated cell (in A1 format as string or
// R1C1 format as 2-element integer array)
@@ -158,6 +165,7 @@ $xlsx->setAuthor('Sergey Shuchkin ')
->setCategory('This is Сategory')
->setApplication('Shuchkin\SimpleXLSXGen')
```
+
### JS array to Excel (AJAX)
```php
defaultFont = 'Calibri';
$this->defaultFontSize = 10;
$this->rtl = false;
- $this->sheets = [['name' => 'Sheet1', 'rows' => [], 'hyperlinks' => [], 'mergecells' => [], 'colwidth' => [], 'autofilter' => '']];
+ $this->sheets = [];
$this->extLinkId = 0;
- $this->SI = []; // sharedStrings index
+ $this->SI = []; // sharedStrings index
$this->SI_KEYS = []; // & keys
// https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_numFmts_topic_ID0E6KK6.html
@@ -179,18 +179,27 @@ class SimpleXLSXGen
'' . self::NEWLINE .
'{RELS}' . self::NEWLINE .
'',
+ 'xl/drawings/vmlDrawing1.vml' => '' . self::NEWLINE .
+ '' . self::NEWLINE .
+ '' . self::NEWLINE .
+ '{SHAPES}',
'xl/worksheets/sheet1.xml' => '' . self::NEWLINE .
'' . self::NEWLINE .
'' . self::NEWLINE .
'{SHEETVIEWS}' . self::NEWLINE .
'{COLS}' . self::NEWLINE .
'' . self::NEWLINE . '{ROWS}' . self::NEWLINE . '' . self::NEWLINE .
- '{AUTOFILTER}' . '{MERGECELLS}' . self::NEWLINE . '{HYPERLINKS}' . self::NEWLINE .
+ '{AUTOFILTER}' . '{MERGECELLS}' . self::NEWLINE . '{HYPERLINKS}' . self::NEWLINE . '{COMMENTS}' .
'',
'xl/worksheets/_rels/sheet1.xml.rels' => '' . self::NEWLINE .
'' . self::NEWLINE .
'{HYPERLINKS}' . self::NEWLINE .
'',
+ 'xl/comments1.xml' => '' . self::NEWLINE .
+ '' . self::NEWLINE .
+ '{AUTHORS}' . self::NEWLINE .
+ '{COMMENTS}' . self::NEWLINE .
+ '',
'xl/sharedStrings.xml' => '' . self::NEWLINE .
'' . self::NEWLINE .
'{STRINGS}' . self::NEWLINE .
@@ -258,11 +267,23 @@ class SimpleXLSXGen
}
}
}
- $this->sheets[$this->curSheet] = ['name' => $name, 'hyperlinks' => [], 'mergecells' => [], 'colwidth' => [], 'autofilter' => '', 'frozen' => ''];
- if (isset($rows[0]) && is_array($rows[0])) {
- $this->sheets[$this->curSheet]['rows'] = $rows;
- } else {
- $this->sheets[$this->curSheet]['rows'] = [];
+ $this->sheets[$this->curSheet] = [
+ 'name' => $name,
+ 'rows' => [],
+ 'hyperlinks' => [],
+ 'mergecells' => [],
+ 'colwidth' => [],
+ 'autofilter' => '',
+ 'frozen' => '',
+ 'authors' => [],
+ 'comments' => [],
+ 'commentDrawingId' => null
+ ];
+ if (is_array($rows)) {
+ foreach ($rows as $row) {
+ if (is_array($row)) $this->sheets[$this->curSheet]['rows'] = $rows;
+ break;
+ }
}
return $this;
}
@@ -340,22 +361,22 @@ class SimpleXLSXGen
if ($cfilename === 'xl/_rels/workbook.xml.rels') {
$s = '';
for ($i = 0; $i < $cnt_sheets; $i++) {
- $s .= '' . self::NEWLINE;
+ $s .= '' . self::NEWLINE;
}
- $s .= '' . self::NEWLINE;
- $s .= '';
+ $s .= '' . self::NEWLINE;
+ $s .= '';
$template = str_replace('{RELS}', $s, $template);
$this->_writeEntry($fh, $cdrec, $cfilename, $template);
$entries++;
} elseif ($cfilename === 'xl/workbook.xml') {
$s = '';
foreach ($this->sheets as $k => $v) {
- $s .= '';
+ $s .= '';
}
$template = str_replace(
['{SHEETS}', '{APP}'],
- [$s, $this->esc($this->application)],
+ [$s, self::esc($this->application)],
$template
);
$this->_writeEntry($fh, $cdrec, $cfilename, $template);
@@ -363,7 +384,7 @@ class SimpleXLSXGen
} elseif ($cfilename === 'docProps/app.xml') {
$template = str_replace(
['{APP}', '{COMPANY}', '{MANAGER}'],
- [$this->esc($this->application), $this->esc($this->company), $this->esc($this->manager)],
+ [self::esc($this->application), self::esc($this->company), self::esc($this->manager)],
$template
);
$this->_writeEntry($fh, $cdrec, $cfilename, $template);
@@ -371,7 +392,7 @@ class SimpleXLSXGen
} elseif ($cfilename === 'docProps/core.xml') {
$template = str_replace(
['{DATE}', '{AUTHOR}', '{TITLE}', '{SUBJECT}', '{KEYWORD}', '{DESCRIPTION}', '{CATEGORY}', '{LAST_MODIFY_BY}'],
- [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)],
+ [gmdate('Y-m-d\TH:i:s\Z'), self::esc($this->author), self::esc($this->title), self::esc($this->subject), self::esc($this->keywords), self::esc($this->description), self::esc($this->category), self::esc($this->lastModifiedBy)],
$template
);
$this->_writeEntry($fh, $cdrec, $cfilename, $template);
@@ -395,6 +416,70 @@ class SimpleXLSXGen
$entries++;
}
$xml = null;
+ } elseif ($cfilename === 'xl/comments1.xml') {
+ foreach ($this->sheets as $k => $v) {
+ $cComments = count($v['comments']);
+ $cAuthors = count($v['authors']);
+ if ($cComments) {
+ $filename = 'xl/comments' . ($k + 1) . '.xml';
+ //create author(s) list and set author Id
+ $authorId = -1; //index for document author to use in comments
+ $AUTHORS = '' . self::NEWLINE;
+ for ($i = 0; $i < $cAuthors; $i++) {
+ $AUTHORS .= ' ' . $v['authors'][$i] . '' . self::NEWLINE;
+ if ($v['authors'][$i] === __CLASS__) $authorId = $i;
+ }
+ if ($authorId === -1) {
+ $authorId = $cAuthors;
+ $AUTHORS .= ' ' . __CLASS__ . '' . self::NEWLINE;
+ }
+ $AUTHORS .= '';
+ //comments list
+ $COMMENTS = '' . self::NEWLINE;
+ for ($i = 0; $i < $cComments; $i++) {
+ $COMMENTS .= '' . self::NEWLINE;
+ //prefix with author name if exist
+ if ($v['comments'][$i]['authorId'] !== -1) {
+ $COMMENTS .= ' ' . $v['authors'][$v['comments'][$i]['authorId']] . ':' . self::NEWLINE . '';
+ }
+ $preserve = (strpbrk($v['comments'][$i]['comment'], "\t\r\n") !== false) ? ' xml:space="preserve"' : '';
+ $COMMENTS .= ' ' . $v['comments'][$i]['comment'] . '' . self::NEWLINE;
+ $COMMENTS .= '' . self::NEWLINE;
+ }
+ $COMMENTS .= '';
+ $xml = str_replace(['{AUTHORS}', '{COMMENTS}'], [$AUTHORS, $COMMENTS], $template);
+ $this->_writeEntry($fh, $cdrec, $filename, $xml);
+ $entries++;
+ }
+ }
+ $xml = null;
+ } elseif ($cfilename === 'xl/drawings/vmlDrawing1.vml') {
+ foreach ($this->sheets as $k => $v) {
+ $cComments = count($v['comments']);
+ if ($cComments) {
+ $SHAPES = '';
+ $filename = 'xl/drawings/vmlDrawing' . ($k + 1) . '.vml';
+ for ($i = 0; $i < $cComments; $i++) {
+ [$row, $col] = self::cell2coord($v['comments'][$i]['cell']);
+ $SHAPES .= '' . self::NEWLINE;
+ }
+ $xml = str_replace('{SHAPES}', $SHAPES, $template);
+ $this->_writeEntry($fh, $cdrec, $filename, $xml);
+ $entries++;
+ }
+ }
+ $xml = null;
} elseif ($cfilename === 'xl/worksheets/_rels/sheet1.xml.rels') {
foreach ($this->sheets as $k => $v) {
if ($this->extLinkId) {
@@ -402,9 +487,13 @@ class SimpleXLSXGen
$filename = 'xl/worksheets/_rels/sheet' . ($k + 1) . '.xml.rels';
foreach ($v['hyperlinks'] as $h) {
if ($h['ID']) {
- $RH[] = ' ';
+ $RH[] = ' ';
}
}
+ if ($v['commentDrawingId']) {
+ $RH[] = ' ';
+ $RH[] = ' ';
+ }
$xml = str_replace('{HYPERLINKS}', implode(self::NEWLINE, $RH), $template);
$this->_writeEntry($fh, $cdrec, $filename, $xml);
$entries++;
@@ -412,13 +501,20 @@ class SimpleXLSXGen
}
$xml = null;
} elseif ($cfilename === '[Content_Types].xml') {
+ $hasComments = false;
+ //if $TYPES is initialized always with this value, why not add it to the template?
$TYPES = [''];
foreach ($this->sheets as $k => $v) {
$TYPES[] = '';
if ($this->extLinkId) {
$TYPES[] = '';
}
+ if (count($v['comments'])) {
+ $hasComments = true;
+ $TYPES[] = '';
+ }
}
+ if ($hasComments) $TYPES[] = '';
$template = str_replace('{TYPES}', implode(self::NEWLINE, $TYPES), $template);
$this->_writeEntry($fh, $cdrec, $cfilename, $template);
$entries++;
@@ -499,7 +595,7 @@ class SimpleXLSXGen
$ba[] = $ba[0];
}
if (!isset($ba[4])) { // diagonal
- $ba[] = 'none';
+ $ba[] = 'none';
}
$sides = ['left' => 3, 'right' => 1, 'top' => 0, 'bottom' => 2, 'diagonal' => 4];
foreach ($sides as $side => $idx) {
@@ -710,7 +806,7 @@ class SimpleXLSXGen
}
$cname = $this->num2name($CUR_COL) . $CUR_ROW;
if ($v === null || $v === '') {
- $row .= ' ';
+ $row .= ' ';
continue;
}
$ct = $cv = $cf = null;
@@ -873,7 +969,7 @@ class SimpleXLSXGen
}
}
if ($cv === null) {
- $v = $this->esc($v);
+ $v = self::esc($v);
if ($cf) {
$ct = 'str';
$cv = $v;
@@ -963,16 +1059,23 @@ class SimpleXLSXGen
if (count($this->sheets[$idx]['hyperlinks'])) {
$HYPERLINKS[] = '';
foreach ($this->sheets[$idx]['hyperlinks'] as $h) {
- $HYPERLINKS[] = ' ';
+ $HYPERLINKS[] = ' ';
}
$HYPERLINKS[] = '';
}
+ $COMMENTS = '';
+ if (count($this->sheets[$idx]['comments'])) {
+ $this->extLinkId++;
+ $this->sheets[$idx]['commentDrawingId'] = $this->extLinkId;
+ $COMMENTS = '' . self::NEWLINE;
+ }
+
//restore locale
setlocale(LC_NUMERIC, $_loc);
return str_replace(
- ['{REF}', '{COLS}', '{ROWS}', '{AUTOFILTER}', '{MERGECELLS}', '{HYPERLINKS}', '{SHEETVIEWS}'],
+ ['{REF}', '{COLS}', '{ROWS}', '{AUTOFILTER}', '{MERGECELLS}', '{HYPERLINKS}', '{SHEETVIEWS}', '{COMMENTS}'],
[
$REF,
implode(self::NEWLINE, $COLS),
@@ -980,7 +1083,8 @@ class SimpleXLSXGen
$AUTOFILTER,
implode(self::NEWLINE, $MERGECELLS),
implode(self::NEWLINE, $HYPERLINKS),
- $SHEETVIEWS
+ $SHEETVIEWS,
+ $COMMENTS
],
$template
);
@@ -1089,6 +1193,11 @@ class SimpleXLSXGen
$this->lastModifiedBy = $lastModifiedBy;
return $this;
}
+ public function rightToLeft($value = true)
+ {
+ $this->rtl = $value;
+ return $this;
+ }
public function autoFilter($range)
{
@@ -1105,18 +1214,44 @@ class SimpleXLSXGen
$this->sheets[$this->curSheet]['colwidth'][$col] = $width;
return $this;
}
-
- public function rightToLeft($value = true)
+ public function freezePanes($cell)
{
- $this->rtl = $value;
+ $this->sheets[$this->curSheet]['frozen'] = $cell;
return $this;
}
-
- public function esc($str)
+ /**
+ * Set comment of a cell of the current sheet
+ *
+ * @param string|array $cell Cell reference in A1 format (string) or R1C1 format (2-element array)
+ * @param string $comment Text comment
+ * @param string $author Optional. If specified, the comment will have $author as a preffix (in bold) of comment
+ *
+ * @return void
+ */
+ public function setComment($cell, $comment, $author = '')
{
- // XML UTF-8: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
- // but we use fast version
- return str_replace(['&', '<', '>', "\x00", "\x03", "\x0B"], ['&', '<', '>', '', '', ''], $str);
+ if (is_array($cell)) {
+ $cell = self::coord2cell($cell);
+ } else {
+ $cell = trim(strtoupper($cell));
+ }
+ //search author index
+ $index = -1;
+ if ($author !== '') {
+ $cAuthors = count($this->sheets[$this->curSheet]['authors']);
+ for ($i = 0; $i < $cAuthors; $i++) {
+ if ($author === $this->sheets[$this->curSheet]['authors'][$i]) {
+ $index = $i;
+ break;
+ }
+ }
+ if ($index === -1) {
+ $this->sheets[$this->curSheet]['authors'][] = $author;
+ $index = $cAuthors;
+ }
+ }
+ $this->sheets[$this->curSheet]['comments'][] = ['cell' => $cell, 'comment' => $comment, 'authorId' => $index];
+ return $this;
}
public function getNumFmtId($code)
@@ -1133,17 +1268,18 @@ class SimpleXLSXGen
return $id;
}
+ public static function esc($str)
+ {
+ // XML UTF-8: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+ // but we use fast version
+ return str_replace(['&', '<', '>', "\x00", "\x03", "\x0B"], ['&', '<', '>', '', '', ''], $str);
+ }
+
public static function raw($value)
{
return "\0" . $value;
}
- public function freezePanes($cell)
- {
- $this->sheets[$this->curSheet]['frozen'] = $cell;
- return $this;
- }
-
/**
* Convert A1 cell reference format to R1C1 cell reference format (row/col number starting from 1)
*