├── .gitignore
├── examples
├── spec.xlsx
├── preserve-formulas
│ ├── spec.xlsx
│ ├── test.xlsx
│ ├── example.php
│ └── performance.php
├── example.php
└── performance.php
├── phpbench.json
├── phpspec.yml
├── .github
├── dependabot.yml
└── workflows
│ ├── phpspec.yml
│ └── phpbench.yml
├── .markdownlint.json
├── composer.json
├── src
└── Svrnm
│ └── ExcelDataTables
│ ├── ExcelDataTablesServiceProvider.php
│ ├── ExcelWorksheet.php
│ ├── ExcelDataTable.php
│ └── ExcelWorkbook.php
├── tests
└── Svrnm
│ └── ExcelDataTables
│ └── Benchmark
│ └── AttachToFileBench.php
├── CONTRIBUTING.md
├── spec
└── Svrnm
│ └── ExcelDataTables
│ ├── ExcelWorkbookSpec.php
│ ├── ExcelWorksheetSpec.php
│ └── ExcelDataTableSpec.php
├── README.md
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | examples/test.xlsx
2 | /vendor
3 | composer.lock
4 | *.swp
5 |
--------------------------------------------------------------------------------
/examples/spec.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svrnm/exceldatatables/HEAD/examples/spec.xlsx
--------------------------------------------------------------------------------
/examples/preserve-formulas/spec.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svrnm/exceldatatables/HEAD/examples/preserve-formulas/spec.xlsx
--------------------------------------------------------------------------------
/examples/preserve-formulas/test.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/svrnm/exceldatatables/HEAD/examples/preserve-formulas/test.xlsx
--------------------------------------------------------------------------------
/phpbench.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json",
3 | "runner.bootstrap": "vendor/autoload.php"
4 | }
5 |
--------------------------------------------------------------------------------
/phpspec.yml:
--------------------------------------------------------------------------------
1 | phpspec:
2 | namespace: Svrnm\ExcelDataTables
3 | psr4_prefix: src
4 | spec_path: src
5 | spec_prefix: Spec\Svrnm\ExcelDataTables
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | ---
2 | version: 2
3 | updates:
4 | - package-ecosystem: 'github-actions'
5 | directory: '/'
6 | groups:
7 | github:
8 | patterns:
9 | - 'actions/*'
10 | - 'github/*'
11 | schedule:
12 | interval: 'weekly'
13 | - package-ecosystem: 'composer'
14 | directory: '/'
15 | schedule:
16 | interval: 'weekly'
17 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "default": true,
3 | "MD013": {
4 | "line_length": 10000,
5 | "headings": false,
6 | "code_blocks": false,
7 | "tables": false
8 | },
9 | "MD024": {
10 | "siblings_only": true
11 | },
12 | "MD025": {
13 | "front_matter_title": ""
14 | },
15 | "MD033": {
16 | "allowed_elements": ["a", "img", "p"]
17 | },
18 | "MD041": false
19 | }
20 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svrnm/exceldatatables",
3 | "description": "Replace a worksheet within an Excel workbook (.xlsx) without changing any other properties of the file",
4 | "keywords": [
5 | "excel",
6 | "php",
7 | "datatables",
8 | "worksheet",
9 | "workbook",
10 | "xls",
11 | "spreadsheet"
12 | ],
13 | "license": "Apache-2.0",
14 | "authors": [
15 | {
16 | "name": "Severin Neumann",
17 | "email": "severin.neumann@altmuehlnet.de"
18 | }
19 | ],
20 | "require": {
21 | "php": ">=7.0.0"
22 | },
23 | "require-dev": {
24 | "phpspec/phpspec": "~7.5",
25 | "mikey179/vfsstream": "~1.6",
26 | "phpbench/phpbench": "^1.3"
27 | },
28 | "autoload": {
29 | "psr-0": {
30 | "Svrnm\\ExcelDataTables": "src/"
31 | }
32 | },
33 | "minimum-stability": "dev"
34 | }
35 |
--------------------------------------------------------------------------------
/src/Svrnm/ExcelDataTables/ExcelDataTablesServiceProvider.php:
--------------------------------------------------------------------------------
1 |
9 | * @license Apache-2.0
10 | */
11 | class ExcelDataTablesServiceProvider extends ServiceProvider {
12 |
13 |
14 | /**
15 | * Register the service provider.
16 | *
17 | * @return void
18 | */
19 | public function register()
20 | {
21 | $this->app->singleton('exceldatatables', function () {
22 | return new ExcelDataTable();
23 | });
24 | }
25 |
26 | /**
27 | * Get the services provided by the provider.
28 | *
29 | * @return array
30 | */
31 | public function provides()
32 | {
33 | return array(
34 | 'exceldatatables'
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/examples/preserve-formulas/example.php:
--------------------------------------------------------------------------------
1 | new \DateTime('2014-01-01 13:00:00'), "Value 1" => 0, "Value 2" => 1),
8 | array("Date" => new \DateTime('2014-01-02 14:00:00'), "Value 1" => 1, "Value 2" => 0),
9 | array("Date" => new \DateTime('2014-01-03 15:00:00'), "Value 1" => 2, "Value 2" => -1),
10 | array("Date" => new \DateTime('2014-01-04 16:00:00'), "Value 1" => 3, "Value 2" => -2),
11 | array("Date" => new \DateTime('2014-01-05 17:00:00'), "Value 1" => 4, "Value 2" => -3),
12 | array("Date" => new \DateTime('2014-01-03 15:00:00'), "Value 1" => 30, "Value 2" => -1),
13 | array("Date" => new \DateTime('2014-01-04 16:00:00'), "Value 1" => 3, "Value 2" => -2),
14 | array("Date" => new \DateTime('2014-01-05 17:00:00'), "Value 1" => 4, "Value 2" => -3),
15 | );
16 | $dataTable->showHeaders()->preserveFormulas('Data')->addRows($data)->refreshTableRange('Data')->attachToFile($in, $out, false);
17 | ?>
18 |
--------------------------------------------------------------------------------
/examples/example.php:
--------------------------------------------------------------------------------
1 | delete if exists
7 | $out = 'test.xlsx';
8 | if( file_exists($out) ) {
9 | if( !@unlink ( $out ) )
10 | {
11 | echo "CRITIC! - destination file: $out - has to be deleted, and I can't
";
12 | echo "CRITIC! - check directory and file permissions
";
13 | die();
14 | }
15 | }
16 |
17 | $data = array(
18 | array("Date" => new \DateTime('2014-01-01 13:00:00'), "Value 1" => 0, "Value 2" => 1),
19 | array("Date" => new \DateTime('2014-01-02 14:00:00'), "Value 1" => 1, "Value 2" => 0),
20 | array("Date" => new \DateTime('2014-01-03 15:00:00'), "Value 1" => 2, "Value 2" => -1),
21 | array("Date" => new \DateTime('2014-01-04 16:00:00'), "Value 1" => 3, "Value 2" => -2),
22 | array("Date" => new \DateTime('2014-01-05 17:00:00'), "Value 1" => 4, "Value 2" => -3),
23 | );
24 | $dataTable->showHeaders()->addRows($data)->attachToFile($in, $out, false);
25 |
26 |
--------------------------------------------------------------------------------
/.github/workflows/phpspec.yml:
--------------------------------------------------------------------------------
1 | name: Run PhpSpec
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 | workflow_dispatch:
11 |
12 | jobs:
13 | phpspec:
14 | runs-on: ubuntu-22.04
15 |
16 | strategy:
17 | matrix:
18 | php-version: ['8.1', '8.3']
19 |
20 | steps:
21 | - name: Checkout code
22 | uses: actions/checkout@v6
23 |
24 | - name: Set up PHP
25 | uses: shivammathur/setup-php@v2
26 | with:
27 | php-version: ${{ matrix.php-version }}
28 | ini-values: |
29 | memory_limit=512M
30 | coverage: none
31 |
32 | - name: Get Composer Cache
33 | id: composer-cache
34 | uses: actions/cache@v5
35 | with:
36 | path: ~/.composer/cache
37 | key:
38 | ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}-${{
39 | matrix.php-version }}
40 | restore-keys: ${{ runner.os }}-composer-${{ matrix.php-version }}
41 |
42 | - name: Install dependencies
43 | run: composer install --prefer-dist --no-progress --no-suggest
44 |
45 | - name: Run PhpSpec
46 | run: vendor/bin/phpspec run
47 |
--------------------------------------------------------------------------------
/examples/performance.php:
--------------------------------------------------------------------------------
1 | showHeaders();
36 | $dataTable->addRows($data);
37 | $time0 = microtime(true) - $start;
38 | $dataTable->attachToFile($in, $out);
39 | $time1 = microtime(true)-$start;
40 | $lastPeak = memory_get_peak_usage();
41 | echo $rows.' x '.$cols.":\t";
42 | echo ($time0)." s\t";
43 | echo ($time1)." s\t";
44 | echo floor((($rows)/$time1))." rows/s\t";
45 | echo floor((($rows*$cols)/$time1))." entries/s\t";
46 | echo ($lastPeak/(1024*1024)).' MB'.PHP_EOL;
47 | $rows*=2;
48 | }
49 |
--------------------------------------------------------------------------------
/.github/workflows/phpbench.yml:
--------------------------------------------------------------------------------
1 | name: Run PhpBench
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 | workflow_dispatch:
11 |
12 | jobs:
13 | phpbench:
14 | runs-on: ubuntu-22.04
15 |
16 | strategy:
17 | matrix:
18 | php-version: ['8.1', '8.3']
19 |
20 | steps:
21 | - name: Checkout code
22 | uses: actions/checkout@v6
23 |
24 | - name: Set up PHP
25 | uses: shivammathur/setup-php@v2
26 | with:
27 | php-version: ${{ matrix.php-version }}
28 | ini-values: |
29 | memory_limit=512M
30 | coverage: none
31 |
32 | - name: Get Composer Cache
33 | id: composer-cache
34 | uses: actions/cache@v5
35 | with:
36 | path: ~/.composer/cache
37 | key:
38 | ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}-${{
39 | matrix.php-version }}
40 | restore-keys: ${{ runner.os }}-composer-${{ matrix.php-version }}
41 |
42 | - name: Install dependencies
43 | run: composer install --prefer-dist --no-progress --no-suggest
44 |
45 | - name: Run PhpBench
46 | run: vendor/bin/phpbench run tests/Svrnm/ExcelDataTables/Benchmark --report=default
47 |
--------------------------------------------------------------------------------
/examples/preserve-formulas/performance.php:
--------------------------------------------------------------------------------
1 | showHeaders();
36 | $dataTable->preserveFormulas('Data');
37 | $dataTable->addRows($data);
38 | $time0 = microtime(true) - $start;
39 | $dataTable->attachToFile($in, $out);
40 | $time1 = microtime(true)-$start;
41 | $lastPeak = memory_get_peak_usage();
42 | echo $rows.' x '.$cols.":\t";
43 | echo ($time0)." s\t";
44 | echo ($time1)." s\t";
45 | echo floor((($rows)/$time1))." rows/s\t";
46 | echo floor((($rows*$cols)/$time1))." entries/s\t";
47 | echo ($lastPeak/(1024*1024)).' MB'.PHP_EOL;
48 | $rows*=2;
49 | }
50 | ?>
51 |
--------------------------------------------------------------------------------
/tests/Svrnm/ExcelDataTables/Benchmark/AttachToFileBench.php:
--------------------------------------------------------------------------------
1 | delete if exists
19 | $out = __DIR__ . '/../../../../examples/test.xlsx';
20 | if( file_exists($out) ) {
21 | if( !@unlink ( $out ) )
22 | {
23 | echo "CRITIC! - destination file: $out - has to be deleted, and I can't
";
24 | echo "CRITIC! - check directory and file permissions
";
25 | die();
26 | }
27 | }
28 | $dataTable->showHeaders()->addRows($params["data"])->attachToFile($in, $out, false);
29 | }
30 |
31 | private static function generate($rows, $cols) {
32 | $data = array();
33 | for($i = 0; $i < $rows; $i++) {
34 | $row = array();
35 | for($j = 0; $j < $cols; $j++) {
36 | switch($j%3) {
37 | case 0:
38 | $row[] = $i;
39 | break;
40 | case 1:
41 | $row[] = '('.$i.','.$j.')' ;
42 | break;
43 | case 2:
44 | $row[] = new \DateTime('2024-01-01');
45 | break;
46 | }
47 | }
48 | $data[] = $row;
49 | }
50 | return $data;
51 | }
52 |
53 | public function provideAttachToFile() {
54 | yield '100x100' => ['data' => self::generate(100, 100)];
55 | yield '200x200' => ['data' => self::generate(200, 200)];
56 | yield '400x400' => ['data' => self::generate(400, 400)];
57 | }
58 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | Thanks for your interest in contributing to `ExcelDataTables`! Here are a few
4 | general guidelines on contributing and reporting bugs that we ask you to review.
5 | Following these guidelines helps to communicate that you respect the time of the
6 | contributors managing and developing this open source project. In return, they
7 | should reciprocate that respect in addressing your issue, assessing changes, and
8 | helping you finalize your pull requests. In that spirit of mutual respect, we
9 | endeavor to review incoming issues and pull requests within 10 days, and will
10 | close any lingering issues or pull requests after 60 days of inactivity.
11 |
12 | ## Reporting Issues
13 |
14 | Before reporting a new issue, please ensure that the issue was not already
15 | reported or fixed by searching through our
16 | [issues list](https://github.com/svrnm/exceldatatables/issues).
17 |
18 | When creating a new issue, please be sure to include a **title and clear
19 | description**, as much relevant information as possible, and, if possible, a
20 | test case.
21 |
22 | ## Sending Pull Requests
23 |
24 | Before sending a new pull request, take a look at existing pull requests and
25 | issues to see if the proposed change or fix has been discussed in the past, or
26 | if the change was already implemented but not yet released.
27 |
28 | We expect new pull requests to include tests for any affected behavior, and, as
29 | we follow semantic versioning, we may reserve breaking changes until the next
30 | major version release.
31 |
32 | ## Other Ways to Contribute
33 |
34 | We welcome anyone that wants to contribute to `ExcelDataTables` to triage and
35 | reply to open issues to help troubleshoot and fix existing bugs. Here is what
36 | you can do:
37 |
38 | - Help ensure that existing issues follows the recommendations from the
39 | _[Reporting Issues](#reporting-issues)_ section, providing feedback to the
40 | issue's author on what might be missing.
41 | - Review existing pull requests, and testing patches against real existing
42 | applications that use `ExcelDataTables`.
43 | - Write a test, or add a missing test case to an existing test.
44 |
45 | Thanks again for your interest on contributing to `ExcelDataTables`!
46 |
47 | :heart:
48 |
--------------------------------------------------------------------------------
/spec/Svrnm/ExcelDataTables/ExcelWorkbookSpec.php:
--------------------------------------------------------------------------------
1 |
14 | * @license Apache-2.0
15 | */
16 |
17 | class ExcelWorkbookSpec extends ObjectBehavior
18 | {
19 | protected $testFilename;
20 |
21 | function let()
22 | {
23 | /* vfsStream and ZipArchive are not working together... */
24 | $this->testFilename = sys_get_temp_dir() . '/exceldatatables-test-spec.xlsx';
25 | copy('./examples/spec.xlsx', $this->testFilename);
26 | $this->beConstructedWith($this->testFilename);
27 | }
28 |
29 | function letGo()
30 | {
31 | # unlink($this->testFilename);
32 | }
33 |
34 | function it_is_initializable()
35 | {
36 | $this->shouldHaveType('Svrnm\ExcelDataTables\ExcelWorkbook');
37 | }
38 |
39 | function it_provides_a_workbook_xml()
40 | {
41 | $this->getWorkbook()->shouldHaveType('\DOMDocument');
42 | }
43 |
44 | function it_is_a_countable()
45 | {
46 | $this->count()->shouldReturn(2);
47 | }
48 |
49 | function it_has_index_worksheets()
50 | {
51 | $this->getWorksheetById(1, true)->shouldHaveType('\DOMDocument');
52 | $this->getWorksheetById(3, true)->shouldReturn(false);
53 | }
54 |
55 | function it_provides_a_styles_xml()
56 | {
57 | $this->getStyles()->shouldHaveType('\DOMDocument');
58 | }
59 |
60 | function it_modifies_an_existing_excel_workbook(\ZipArchive $test)
61 | {
62 | $worksheet = new ExcelWorksheet();
63 |
64 | $worksheet->addRows(
65 | array(
66 | array("Date", "Value 1", "Value 2"),
67 | array(new \DateTime("2014-07-30"), 13, 4),
68 | array(new \DateTime("2014-07-31"), 18, 5),
69 | array(new \DateTime("2014-08-01"), 14, 6),
70 | array(new \DateTime("2014-08-02"), 9, 7),
71 | array(new \DateTime("2014-08-03"), 3, 4),
72 | array(new \DateTime("2014-08-04"), 1, 3),
73 | array(new \DateTime("2014-08-05"), 4, 2),
74 | array(new \DateTime("2014-08-09"), 4, 0),
75 | array(new \DateTime("2014-08-10"), 13, 1),
76 | array(new \DateTime("2014-08-11"), 23, 3),
77 | array(new \DateTime("2014-08-12"), 18, 23),
78 | array(new \DateTime("2014-08-13"), 19, 0),
79 | array(new \DateTime("2014-08-14"), 21, 13),
80 | )
81 | );
82 | $this->addWorksheet($worksheet, 2)->save();
83 | /* TODO: Validate?! */
84 |
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/spec/Svrnm/ExcelDataTables/ExcelWorksheetSpec.php:
--------------------------------------------------------------------------------
1 |
12 | * @license Apache-2.0
13 | */
14 | class ExcelWorksheetSpec extends ObjectBehavior
15 | {
16 |
17 | static function wrapWorksheet($inner) {
18 | $xml = ''.PHP_EOL;
19 | $xml .= '';
20 |
21 | $xml .= $inner;
22 |
23 | $xml .= ''.PHP_EOL;
24 |
25 | return $xml;
26 | }
27 |
28 | static function wrapSheetData($inner) {
29 | return self::wrapWorksheet(''.$inner.'');
30 | }
31 |
32 | static function wrapRow($inner, $id) {
33 | return ''.$inner.'
';
34 | }
35 |
36 | function it_is_initializable()
37 | {
38 | $this->shouldHaveType('Svrnm\ExcelDataTables\ExcelWorksheet');
39 | }
40 |
41 | function it_converts_to_xml()
42 | {
43 | $this->setupDefaultDocument()->toXML()->shouldReturn(self::wrapWorksheet(''));
44 | }
45 |
46 | function it_provides_a_dom_document()
47 | {
48 | $this->getDocument()->shouldHaveType('\DOMDocument');
49 | }
50 |
51 | function it_provides_a_worksheet_root_element()
52 | {
53 | $this->getWorksheet()->shouldHaveType('\DOMElement');
54 | }
55 |
56 | function it_provides_a_sheetdata_element()
57 | {
58 | $this->getSheetData()->shouldHaveType('\DOMElement');
59 | }
60 |
61 | function it_adds_new_rows()
62 | {
63 | $this->addRow()->addRow()->toXML()->shouldReturn(self::wrapSheetData('|
'));;
64 | }
65 |
66 | function it_adds_strings_to_string_columns()
67 | {
68 | $this->addRow(array('a', 'b', 'c'))->toXML()->shouldReturn(self::wrapSheetData(self::wrapRow('abc', 1)));
69 | }
70 |
71 | function it_adds_numbers_to_number_columns()
72 | {
73 | $this->addRow(array(1,2,3))->toXML()->shouldReturn(self::wrapSheetData(self::wrapRow('123', 1)));
74 | }
75 |
76 | function it_adds_numbers_to_string_typed_columns()
77 | {
78 | $this->addRow(array(array('type' => 'string', 'value' => 13)))->toXML()->shouldReturn(self::wrapSheetData(self::wrapRow('13', 1)));
79 | }
80 |
81 | function it_adds_datetimes_todatetime_columns()
82 | {
83 | $this->addRow(array(new \DateTime('2013-04-05')))->toXML()->shouldReturn(self::wrapSheetData(self::wrapRow('41369', 1)));
84 | }
85 |
86 | function it_accepts_multidimensional_arrays_as_rows()
87 | {
88 | $this->addRows(array(
89 | array(1,2,3),
90 | array(2,3,4),
91 | array(5,6,7)
92 | ))->toXML()->shouldReturn(self::wrapSheetData('123
234
567
'));
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ExcelDataTables
2 |
3 | Replace a worksheet within an Excel workbook (`.xlsx`) without changing any
4 | other properties of the file.
5 |
6 | ## License
7 |
8 | This program is free software; see [LICENSE](./LICENSE) for more details.
9 |
10 | ## Details
11 |
12 | The main purpose of this library is adding a "data table" to an existing excel
13 | file without modifying any other components. This is especially useful if you
14 | have a template file, e.g. for reporting including "advanced" features like
15 | charts, pivot tables, macros and you'd like to change a base data table within a
16 | PHP application.
17 |
18 | ## Setup
19 |
20 | Use composer to add this repository to your dependencies:
21 |
22 | ```JSON
23 | {
24 | "require": {
25 | "svrnm/exceldatatables": "dev-master"
26 | }
27 | }
28 | ```
29 |
30 | If you use the [Laravel framework](https://laravel.io/) you can additionally add
31 | the `ServiceProvider` to your `config/app.php`:
32 |
33 | ```PHP
34 | ...
35 | 'providers' => array(
36 | ...
37 | 'Svrnm\ExcelDataTables\ExcelDataTablesServiceProvider'
38 | )
39 | ...
40 | ```
41 |
42 | ## Example
43 |
44 | The following example demonstrates how to use ExcelDataTables. The following is
45 | also contained as `example.php` within the folder `examples/`:
46 |
47 | ```php
48 | new \DateTime('2014-01-01 13:00:00'), "Value 1" => 0, "Value 2" => 1),
59 | array("Date" => new \DateTime('2014-01-02 14:00:00'), "Value 1" => 1, "Value 2" => 0),
60 | array("Date" => new \DateTime('2014-01-03 15:00:00'), "Value 1" => 2, "Value 2" => -1),
61 | array("Date" => new \DateTime('2014-01-04 16:00:00'), "Value 1" => 3, "Value 2" => -2),
62 | array("Date" => new \DateTime('2014-01-05 17:00:00'), "Value 1" => 4, "Value 2" => -3),
63 | );
64 | // Attach the data table and copy the new xlsx file to the output file.
65 | $dataTable->showHeaders()->addRows($data)->attachToFile($in, $out);
66 | ?>
67 | ```
68 |
69 | In this example the method `attachToFile` creates a new excel file. If you use
70 | this library within a web application you might prefer the `fillXLSX()` function
71 | which returns a string representation of the excel document. The following
72 | examples demonstrates this case within a Laravel application
73 |
74 | ```php
75 | dataTable = $dataTable;
81 | }
82 |
83 | public function show($month) {
84 | $data = DB::select('select date,value1,value2 from reporting where MONTH(date) = ?', array($month));
85 | $path = storage_path() . '/reports/example.xlsx';
86 | $xlsx = $this->dataTable->showHeaders()->addRows($data)->fillXLSX($path);
87 | return Response::make($xlsx, 200, array(
88 | 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
89 | 'Content-Disposition' => 'attachment; filename="report.xlsx"',
90 | 'Content-Length' => strlen($xlsx)
91 | ));
92 | // ...
93 | }
94 | }
95 | ?>
96 | ```
97 |
98 | ## Contact
99 |
100 | For any questions you can contact Severin Neumann
101 | .
102 |
--------------------------------------------------------------------------------
/spec/Svrnm/ExcelDataTables/ExcelDataTableSpec.php:
--------------------------------------------------------------------------------
1 |
12 | * @license Apache-2.0
13 | */
14 | class ExcelDataTableSpec extends ObjectBehavior
15 | {
16 | /**
17 | * Default specification: Check if the class can be instantiated.
18 | */
19 | function it_is_initializable()
20 | {
21 | $this->shouldHaveType('Svrnm\ExcelDataTables\ExcelDataTable');
22 | }
23 |
24 | /**
25 | * The data table can have a special header row which is set using
26 | * setHeaders.
27 | */
28 | function it_has_headers()
29 | {
30 | $headers = array("A" => "A", "B" => "B", "C" => "C");
31 | $this->setHeaders($headers)->getHeaders()->shouldReturn($headers);
32 | }
33 |
34 | /**
35 | * A simple numerical array is converted in a single row.
36 | * Tow arrays are converted into two rows.
37 | * ...
38 | */
39 | function it_converts_an_numerical_array_to_a_row()
40 | {
41 | $array = array(1, 2, 3);
42 | $this->addRow($array)->toArray()->shouldReturn(array($array));
43 | $array2 = array(3, 4, 5);
44 | $this->addRow($array2)->toArray()->shouldReturn(array($array, $array2));
45 | }
46 |
47 | /**
48 | * An assocative array is converted into a single row. The keys of the array
49 | * are internaly used as column identifier.
50 | *
51 | * Adding a second assocative array which does not have entires in all columns
52 | * will introduce empty cells.
53 | */
54 | function it_converts_an_assocative_to_a_row()
55 | {
56 | $array = array("A" => 1, "B" => 2, "C" => 3);
57 | $this->addRow($array)->toArray()->shouldReturn(array(array(1, 2, 3)));
58 |
59 | $array2 = array("C" => 3);
60 | $this->addRow($array2)->toArray()->shouldReturn(array(array(1, 2, 3), array('', '', 3)));
61 | }
62 |
63 | /**
64 | * A simple object is treated like an assocative array
65 | */
66 | function it_converts_an_object_to_row()
67 | {
68 | $object = new \stdClass();
69 | $object->A = 1;
70 | $object->B = 2;
71 | $object->C = 3;
72 | $this->addRow($object)->toCsv()->shouldReturn('1,2,3');
73 | }
74 |
75 | function it_converts_multidimensional_arrays_to_multiple_rows()
76 | {
77 | $array = array(
78 | array(1, 2, 3),
79 | array(1, 2, 3)
80 | );
81 | $this->addRows($array)->toArray()->shouldReturn($array);
82 | }
83 |
84 | function it_has_a_fluent_interface()
85 | {
86 | $array = array(1);
87 | $this->addRow($array)->shouldReturn($this);
88 | $this->setHeaders($array)->shouldReturn($this);
89 | /* ... */
90 | }
91 |
92 | function it_has_implicit_headers()
93 | {
94 | $array = array("A" => 1, "B" => 2, "C" => 3);
95 | $array2 = array("C" => 3);
96 | $this->addRow($array)->addRow($array2)->showHeaders()->toArray()->shouldReturn(array(array("A", "B", "C"), array(1, 2, 3), array("", "", 3)));
97 | }
98 |
99 | function it_converts_data_to_xml()
100 | {
101 | $array = array(
102 | array("Names" => "Test 1", "Value 1" => 13, "Value 2" => new \DateTime('2013-03-04 13:00:00')),
103 | array("Names" => "Test 2", "Value 1" => 23, "Value 2" => new \DateTime('2014-04-01 13:12:00')),
104 | array("Names" => "Test 3", "Value 1" => 33, "Value 2" => new \DateTime('1900-01-01 00:00:00')),
105 | );
106 |
107 | $this->addRows($array)->showHeaders()->toXML()->shouldReturn('' . PHP_EOL . 'NamesValue 1Value 2
Test 11341337.541666667
Test 22341730.55
Test 3331
' . PHP_EOL);
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/src/Svrnm/ExcelDataTables/ExcelWorksheet.php:
--------------------------------------------------------------------------------
1 |
12 | * @license Apache-2.0
13 | */
14 | class ExcelWorksheet
15 | {
16 | /**
17 | * This namespaces are used to setup the XML document.
18 | *
19 | * @var array
20 | */
21 | protected static $namespaces = array(
22 | "spreadsheets" => "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
23 | "relationships" => "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
24 | "xmlns" => "http://www.w3.org/2000/xmlns/"
25 | );
26 |
27 | /**
28 | * The base date which is used to compute date field values
29 | *
30 | * @var string
31 | */
32 | protected static $baseDate = "1899-12-31 00:00:00";
33 |
34 | protected static $fixDays = 1;
35 |
36 | /**
37 | * The XML base document
38 | *
39 | * @var \DOMDocument
40 | */
41 | protected $document;
42 |
43 | /**
44 | * The worksheet element. This is the root element of the XML document
45 | *
46 | * @var \DOMElement
47 | */
48 | protected $worksheet;
49 | /**
50 | * The sheetData element. This element contains all rows of the spreadsheet
51 | *
52 | * @var \DOMElement
53 | */
54 | protected $sheetData;
55 |
56 | /**
57 | * The formatId used for date and time values. The correct id is specified
58 | * in the styles.xml of a workbook. The default value 1 is a placeholder
59 | *
60 | * @var int
61 | */
62 | protected $dateTimeFormatId = 1;
63 |
64 | protected $dateTimeColumns = array();
65 |
66 | protected $rows = array();
67 |
68 | protected $dirty = false;
69 |
70 | protected $rowCounter = 1;
71 |
72 | const COLUMN_TYPE_STRING = 0;
73 | const COLUMN_TYPE_NUMBER = 1;
74 | const COLUMN_TYPE_DATETIME = 2;
75 | const COLUMN_TYPE_FORMULA = 3;
76 |
77 | protected static $columnTypes = array(
78 | 'string' => 0,
79 | 'number' => 1,
80 | 'datetime' => 2,
81 | 'formula' => 3
82 | );
83 |
84 | /**
85 | * Setup a default document: XML head, Worksheet element, SheetData element.
86 | *
87 | * @return $this
88 | */
89 | public function setupDefaultDocument() {
90 | $this->getSheetData();
91 | return $this;
92 | }
93 |
94 | /**
95 | * Change the formatId for date time values.
96 | *
97 | * @return $this
98 | */
99 | public function setDateTimeFormatId($id) {
100 | $this->dirty = true;
101 | $this->dateTimeFormatId = $id;
102 | /*foreach($this->dateTimeColumns as $column) {
103 | $column->setAttribute('s', $id);
104 | }*/
105 | return $this;
106 | }
107 |
108 | /**
109 | * Convert DateTime to excel time format. This function is
110 | * a copy from PHPExcel.
111 | *
112 | * @see https://github.com/PHPOffice/PHPExcel/blob/78a065754dd0b233d67f26f1ef8a8a66cd449e7f/Classes/PHPExcel/Shared/Date.php
113 | */
114 | public static function convertDate(\DateTime $date) {
115 |
116 | $year = $date->format('Y');
117 | $month = $date->format('m');
118 | $day = $date->format('d');
119 | $hours = $date->format('H');
120 | $minutes = $date->format('i');
121 | $seconds = $date->format('s');
122 |
123 | $excel1900isLeapYear = TRUE;
124 | if (($year == 1900) && ($month <= 2)) { $excel1900isLeapYear = FALSE; }
125 | $my_excelBaseDate = 2415020;
126 | if ($month > 2) {
127 | $month -= 3;
128 | } else {
129 | $month += 9;
130 | $year -= 1;
131 | }
132 | // Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0)
133 | $century = substr($year,0,2);
134 | $decade = substr($year,2,2);
135 | $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $my_excelBaseDate + $excel1900isLeapYear;
136 |
137 | $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400;
138 |
139 | return (float) $excelDate + $excelTime;
140 |
141 | }
142 |
143 | /**
144 | * By default the XML document is generated without format. This can be
145 | * changed with this function.
146 | *
147 | * @param $value
148 | * @return $this
149 | */
150 | public function setFormatOutput($value = true) {
151 | $this->getDocument()->formatOutput = true;
152 | return $this;
153 | }
154 |
155 | /**
156 | * Returns the given worksheet in its XML representation
157 | *
158 | * @return string
159 | */
160 | public function toXML() {
161 | $document = $this->getDocument();
162 | return $document->saveXML();
163 | }
164 |
165 | /**
166 | * Generate and return a new empty row within the sheetData
167 | *
168 | * @return \DOMElement
169 | */
170 | protected function getNewRow() {
171 | /*$sheetData = $this->getSheetData();
172 | $row = $this->append('row', array(), $sheetData);
173 | $row->setAttribute('r', $this->rowCounter++);
174 | return $row;*/
175 | $this->rows[] = array();
176 | return count($this->rows)-1;
177 | }
178 |
179 | /*
180 | * Set the inner text for $element to $text. Returns the DOMNode
181 | * representing the text.
182 | *
183 | * @return \DOMText
184 | */
185 | /*protected function setText($element, $text) {
186 | $textElement = $this->getDocument()->createTextNode($text);
187 | $element->appendChild($textElement);
188 | return $textElement;
189 | }*/
190 |
191 | /*
192 | * Add an inline string column to the given $row.
193 | *
194 | * @param \DOMElement $row
195 | * @param string $column
196 | * @return \DOMElement
197 | */
198 | /*protected function addStringColumnToRow($row, $column) {
199 | $c = $this->append('c', array('t' => 'inlineStr'), $row);
200 | $is = $this->append('is', array(), $c);
201 | $t = $this->append('t', array(), $is);
202 | $this->setText($t, $column);
203 | return $t;
204 | }*/
205 |
206 | /*
207 | * Add an number column to the given $row.
208 | *
209 | * @param \DOMElement $row
210 | * @param int|string $column
211 | * @return \DOMElement
212 | */
213 | /*protected function addNumberColumnToRow($row, $column) {
214 | $c = $this->append('c', array(), $row);
215 | $v = $this->append('v', array(), $c);
216 | $this->setText($v, $column);
217 | return $v;
218 | }*/
219 |
220 | /*
221 | * Add a date time dolumn to the given $row. $column is converted to a numerical
222 | * value relativ to the static::$baseDate value
223 | *
224 | * @param \DOMElement $row
225 | * @param \DateTimeInterface $column
226 | * @return \DOMElement
227 | */
228 | /*protected function addDateTimeColumnToRow($row, \DateTimeInterface $column) {
229 | $c = $this->append('c', array('s' => $this->dateTimeFormatId), $row);
230 | $this->dateTimeColumns[] = $c;
231 | $v = $this->append('v', array(), $c);
232 | $this->setText($v, static::convertDate($column) );
233 | return $v;
234 | }*/
235 |
236 | /**
237 | * Add a column to a row. The type of the column is deferred by its value
238 | *
239 | * @param \DOMElement $row
240 | * @param mixed $column
241 | * @return \DOMElement
242 | */
243 | protected function addColumnToRow($row, $column) {
244 | if(is_array($column)
245 | && isset($column['type'])
246 | && isset($column['value'])
247 | && in_array($column['type'], array('string', 'number', 'datetime', 'formula'))) {
248 | //$function = 'add'.ucfirst($column['type']).'ColumnToRow';
249 | //return $this->$function($row, $column['value']);
250 | $this->rows[$row][] = array(self::$columnTypes[$column['type']], $column['value']);
251 | } elseif(is_numeric($column)) {
252 | $this->rows[$row][] = array(self::COLUMN_TYPE_NUMBER, $column);
253 | //return $this->addNumberColumnToRow($row, $column);
254 | } elseif($column instanceof \DateTime) {
255 | $this->rows[$row][] = array(self::COLUMN_TYPE_DATETIME, $column);
256 | //return $this->addDateTimeColumnToRow($row, $column);
257 | } else {
258 | $this->rows[$row][] = array(self::COLUMN_TYPE_STRING, $column);
259 | }
260 | //return $this->addStringColumnToRow($row, (string)$column);
261 | }
262 |
263 | public function toXMLColumn($column) {
264 | switch($column[0]) {
265 | case self::COLUMN_TYPE_NUMBER:
266 | return ''.$column[1].'';
267 | break;
268 | case self::COLUMN_TYPE_DATETIME:
269 | return ''.static::convertDate($column[1]).'';
270 | break;
271 | // case self::COLUMN_TYPE_STRING:
272 | case self::COLUMN_TYPE_FORMULA:
273 | return ''.$column[1].'';
274 | break;
275 | default:
276 | return ''.strtr($column[1], array(
277 | "&" => "&",
278 | "<" => "<",
279 | ">" => ">",
280 | '"' => """,
281 | "'" => "'",
282 | )).'';
283 | break;
284 | }
285 | }
286 |
287 | public function incrementRowCounter() {
288 | return $this->rowCounter++;
289 | }
290 |
291 | protected function updateDocument() {
292 | if($this->dirty) {
293 | $this->dirty = false;
294 | $self = $this;
295 | $this->rowCounter = 1;
296 | $fragment = $this->document->createDocumentFragment();
297 | $xml = implode('', array_map(function($row) use ($self) {
298 | return ''.implode('', array_map(function($column) use ($self) {
299 | return $self->toXMLColumn($column);
300 | }, $row)).'
';
301 | }, $this->rows));
302 | if(!$fragment->appendXML($xml)) {
303 | throw new \Exception('Parsing XML failed');
304 | }
305 | $this->getSheetData()->parentNode->replaceChild(
306 | $s = $this->getSheetData()->cloneNode( false ),
307 | $this->getSheetData()
308 | );
309 | $this->sheetData = $s;
310 | $this->getSheetData()->appendChild($fragment);
311 |
312 | }
313 | }
314 |
315 | /**
316 | * Add a row to the spreadsheet. The columns are inserted and their type is deferred by their type:
317 | *
318 | * - Arrays having a type and value element are inserted as defined by the type. Possible types
319 | * are: string, number, datetime
320 | * - Numerical values are inserted as number columns.
321 | * - Objects implementing the DateTimeInterface are inserted as datetime column.
322 | * - Everything else is converted to a string and inserted as (inline) string column.
323 | *
324 | * @param array $columns
325 | * @return $this
326 | */
327 | public function addRow($columns = array()) {
328 | $this->dirty = true;
329 | $row = $this->getNewRow();
330 | foreach($columns as $column) {
331 | $this->addColumnToRow($row, $column);
332 | }
333 | return $this;
334 | }
335 |
336 | /**
337 | * Returns the DOMDocument representation of the current instance
338 | *
339 | * @return \DOMDocument
340 | */
341 | public function getDocument() {
342 | if(is_null($this->document)) {
343 | $this->document = new \DOMDocument('1.0', 'utf-8');
344 | $this->document->xmlStandalone = true;
345 | }
346 | $this->updateDocument();
347 | return $this->document;
348 | }
349 |
350 | /**
351 | * Returns the DOMElement representation of the sheet data
352 | *
353 | * @return \DOMElement
354 | */
355 | public function getSheetData() {
356 | if(is_null($this->sheetData)) {
357 | $this->sheetData = $this->append('sheetData');
358 | }
359 | $this->updateDocument();
360 | return $this->sheetData;
361 | }
362 |
363 | /**
364 | * Crate a new \DOMElement within the scope of the current document.
365 | *
366 | * @param string name
367 | * @return \DOMElement
368 | */
369 | protected function createElement($name) {
370 | return $this->getDocument()->createElementNS(static::$namespaces['spreadsheets'], $name);
371 | }
372 |
373 | /**
374 | * Returns the DOMElement representation of the worksheet
375 | *
376 | * @return \DOMElement
377 | */
378 | public function getWorksheet() {
379 | if(is_null($this->worksheet)) {
380 | $document = $this->getDocument();
381 | $this->worksheet = $this->append('worksheet', array(), $document);
382 | $this->worksheet->setAttributeNS(static::$namespaces['xmlns'], 'xmlns:r', static::$namespaces['relationships']);
383 | }
384 | $this->updateDocument();
385 | return $this->worksheet;
386 | }
387 |
388 | /**
389 | * Append a new element (tag) to the XML Document. By default the new tag <$name/> will be attachted
390 | * to the root element (i.e. ). Attributes for the new tag can be specified with the second
391 | * parameter $attribute. Each element of the $attributes array is added as attribute whereas the key
392 | * is the attribute name and the value is the attribute value.
393 | * If the new element should be appended to another parent element in the XML Document the third
394 | * parameter can be used to specify the parent
395 | *
396 | * The function returns the newly created element as \DOMElement instance.
397 | *
398 | * @param string name
399 | * @param array attributes
400 | * @param \DOMElement parent
401 | * @return \DOMElement
402 | */
403 | protected function append($name, $attributes = array(), $parent = null) {
404 | if(is_null($parent)) {
405 | $parent = $this->getWorksheet();
406 | }
407 | $element = $this->createElement($name);
408 | foreach($attributes as $key => $value) {
409 | $element->setAttribute($key, $value);
410 | }
411 | $parent->appendChild($element);
412 | return $element;
413 | }
414 |
415 | public function addRows($array, $calculatedColumns = null){
416 | foreach($array as $key => $row) {
417 | if(isset($calculatedColumns)){
418 | foreach ($calculatedColumns as $calculatedColumn) {
419 | if($key == 0){
420 | array_splice($row, $calculatedColumn['index'], 0, $calculatedColumn['header']);
421 | } else {
422 | array_splice($row, $calculatedColumn['index'], 0, $calculatedColumn['content']);
423 | }
424 | }
425 | }
426 | $this->addRow($row);
427 | }
428 | return $this;
429 | }
430 | }
431 |
--------------------------------------------------------------------------------
/src/Svrnm/ExcelDataTables/ExcelDataTable.php:
--------------------------------------------------------------------------------
1 | addRows($data)->attachToFile('./example.xlsx');
14 | *
15 | * @author Severin Neumann
16 | * @license Apache-2.0
17 | */
18 | class ExcelDataTable
19 | {
20 |
21 | /**
22 | * The internal representation of the data table
23 | *
24 | * @var array
25 | */
26 | protected $data = array();
27 |
28 | /**
29 | * The names/identifiers for the data columns
30 | *
31 | * @var array
32 | */
33 | protected $headerNames = array();
34 |
35 | /**
36 | * The numbers for the data columns, may be sparse
37 | *
38 | * @var array
39 | */
40 | protected $headerNumbers = array();
41 |
42 | /**
43 | * The (optional) labels of the data column. If $headersVisible is true
44 | * these are written in the first row ("Row 1" in Excel)
45 | *
46 | * @var array
47 | */
48 | protected $headerLabels = array();
49 |
50 | /**
51 | * A list of cells which couldn't be added during a call of addRow()
52 | *
53 | * @var array
54 | */
55 | protected $failedCells = array();
56 |
57 | /**
58 | * True if the headers are defined.
59 | *
60 | * @var boolean
61 | */
62 | protected $headersDefined = false;
63 |
64 | /**
65 | * True if headers should be displayed during an export
66 | *
67 | * @var boolean
68 | */
69 | protected $headersVisible = false;
70 |
71 | /**
72 | * The sheet which will be overwritten when attachToFile is called
73 | *
74 | * @var int|null
75 | */
76 | protected $sheetId = null;
77 |
78 | /**
79 | * The name of the sheet when attachToFile is called
80 | *
81 | * @var string
82 | */
83 | protected $sheetName = 'Data';
84 |
85 | /**
86 | * If set, regenerates the range in the data table with the specified name
87 | *
88 | * @var null|string
89 | */
90 | protected $refreshTableRange = null;
91 |
92 | /**
93 | * If set, injects column formulas into the output
94 | *
95 | * @var null|string
96 | */
97 | protected $preserveFormulas = null;
98 |
99 | /**
100 | * Variable to hold calculated columns from source
101 | *
102 | * @var null|array
103 | */
104 | protected $calculatedColumns = null;
105 |
106 | /**
107 | * Instantiate a new ExcelDataTable object
108 | *
109 | */
110 | public function __construct() {
111 |
112 | }
113 |
114 | /**
115 | * Add multiple rows to the data table. $rows is expected to be an array of
116 | * possible rows (array, object).
117 | *
118 | * @param array $rows
119 | * @return $this
120 | */
121 | public function addRows($rows) {
122 | foreach($rows as $row) {
123 | $this->addRow($row);
124 | }
125 | return $this;
126 | }
127 |
128 | /**
129 | * Add a new row to the data table. $row is expected to be an assocative array
130 | * or an object, the keys/property names are used to choose the correct
131 | * column in the following manner:
132 | *
133 | * - If no header property for the data table is specified, the given $row is used to define the header
134 | * - If a header property for the date table is specified, the given $row is added accordingly
135 | *
136 | * @param array $row
137 | * @return $this
138 | */
139 | public function addRow($row) {
140 | $result = array();
141 |
142 | if(!$this->headersDefined) {
143 | $headers = array();
144 | foreach($row as $key => $value) {
145 | $headers[$key] = $key;
146 | }
147 | $this->setHeaders($headers);
148 | }
149 |
150 |
151 | foreach($row as $name => $value) {
152 | $number = $this->headerNameToHeaderNumber($name);
153 | if($number !== false) {
154 | $result[$number] = $value;
155 | } else {
156 | $this->failedCells[count($this->data)] = array($name => $value);
157 | }
158 | }
159 | $this->data[] = $result;
160 |
161 | return $this;
162 | }
163 |
164 | /**
165 | * Return a list of cells which couldn't be inserted during all previous calls of "addRow"
166 | * The reason for this might be, that the key for a certain value is not defined as header
167 | * and therfore the value can't be placed in a cell
168 | *
169 | * @return array A copy of $this->failedCells
170 | */
171 | public function getFailedCells() {
172 | return $this->failedCells;
173 | }
174 |
175 | /**
176 | * Convert the name of a header/column into the equivalent number. Returns false if
177 | * the given name can't be converted.
178 | *
179 | * @param string $name
180 | * @return int|boolean
181 | */
182 | protected function headerNameToHeaderNumber($name) {
183 | return isset($this->headerNumbers[$name]) ? $this->headerNumbers[$name] : false;
184 | }
185 |
186 | /**
187 | * Convert the number of a header/column into the equivalent name. Returns false if
188 | * the given name can't be converted.
189 | *
190 | * @param int $number
191 | * @return string|boolean
192 | */
193 | protected function headerNumberToHeaderName($number) {
194 | return isset($this->headerNames[$number]) ? $this->headerNames[$number] : false;
195 | }
196 |
197 | /**
198 | * Retrieve the label for a certain column by header/column number. Retruns false if the
199 | * requested label can't be retrieved.
200 | *
201 | * @param int $number
202 | * @return string|boolean
203 | */
204 | protected function getHeaderLabelByNumber($number) {
205 | return isset($this->headerLabels[$number]) ? $this->headerLabels[$number] : false;
206 | }
207 |
208 | /**
209 | * Add a name with label for the next column. Increments the column count by one.
210 | *
211 | * @param string name
212 | * @param string label
213 | * @return this
214 | */
215 | protected function addHeader($name, $label) {
216 | $this->headerNames[] = $name;
217 | $this->headerLabels[] = $label;
218 | $this->headerNumbers[$name] = count($this->headerNames) - 1;
219 | return $this;
220 | }
221 |
222 | /**
223 | * Set the label of a certain column. The column is selected by its name/identifier.
224 | *
225 | * @param string name
226 | * @param string label
227 | * @return this
228 | */
229 | public function setLabel($name, $label) {
230 | $this->headerLabels[$this->headerNameToHeaderNumber($name)] = $label;
231 | return $this;
232 | }
233 |
234 | /**
235 | * Show the header row during export
236 | *
237 | * @return this
238 | */
239 | public function showHeaders() {
240 | $this->headersVisible = true;
241 | return $this;
242 | }
243 |
244 | /**
245 | * Do not show the header row during export
246 | *
247 | * @return this
248 | */
249 | public function hideHeaders() {
250 | $this->headersVisible = false;
251 | return $this;
252 | }
253 |
254 | /**
255 | * Check if the headers are shown during export
256 | *
257 | * @return boolean
258 | */
259 | public function areHeadersVisible() {
260 | return $this->headersVisible;
261 | }
262 |
263 | /**
264 | * Set headers for the data table. Expects an array or an object, which will
265 | * be casted to an array. The keys are used as names for the columns, the values
266 | * are used as labels for the columns, if the header is printed (see: showHeader()/hideHeader())
267 | *
268 | * @param array|object $header
269 | * @return this
270 | */
271 | public function setHeaders($header) {
272 | foreach((array)$header as $name => $label) {
273 | $this->addHeader($name, $label);
274 | }
275 | $this->headersDefined = true;
276 | return $this;
277 | }
278 |
279 | /**
280 | * Returns a representation of the header row.
281 | *
282 | * @return array
283 | */
284 | public function getHeaders() {
285 | $result = array();
286 | foreach($this->headerNumbers as $number) {
287 | $result[$this->headerNumberToHeaderName($number)] = $this->getHeaderLabelByNumber($number);
288 | }
289 | return $result;
290 | }
291 |
292 | /**
293 | * Change the type of the column identified by $columnKey. $type can be 'string', 'number', 'date', 'datetime'.
294 | * $columnKey can be a numeric identifier or a key as specified by addHeader().
295 | *
296 | * NOT YET IMPLEMENTED
297 | *
298 | * @param int|string columnKey
299 | * @param string type
300 | * @return this
301 | */
302 | public function setColumnType($columnKey, $type) {
303 | throw new \Exception('"setColumnType" is not yet implemented');
304 | return $this;
305 | }
306 |
307 | /**
308 | * Iterate over a given row and convert it into a dense representation
309 | * for export.
310 | *
311 | * @param array arr
312 | * @return array
313 | */
314 | protected function fillRow($arr) {
315 | $result = array();
316 | for($i = 0; $i < count($this->headerNumbers); $i++) {
317 | $result[$i] = isset($arr[$i]) ? $arr[$i] : '';
318 | }
319 | return $result;
320 | }
321 |
322 | /**
323 | * Exports the data table into an array representation. If the headers are visible
324 | * they are at index 0 of the multidimensional array
325 | *
326 | * @return array
327 | */
328 | public function toArray() {
329 | $arr = array();
330 | if($this->areHeadersVisible()) {
331 | $arr[] = $this->headerLabels;
332 | }
333 | foreach($this->data as $row) {
334 | $arr[] = $this->fillRow($row);
335 | }
336 | return $arr;
337 | }
338 |
339 | /**
340 | * Exports the data table into a csv formatted string. If the headers are visible
341 | * they are added as first row
342 | *
343 | * @return string
344 | */
345 | public function toCsv($separator = ',', $quote = '', $newLine = PHP_EOL) {
346 | return implode(
347 | $newLine,
348 | array_map(
349 | function($elem) use($separator, $quote) {
350 | $s = $quote.$separator.$quote;
351 | return $quote.implode($s, $elem).$quote;
352 | },
353 | $this->toArray()
354 | )
355 | );
356 |
357 | }
358 |
359 | /**
360 | * Creates and returns the created worksheet as spreadsheetml
361 | *
362 | * @return string
363 | */
364 | public function toXML() {
365 | $worksheet = new ExcelWorksheet();
366 | return $worksheet->addRows($this->toArray())->toXML();
367 | }
368 |
369 | /**
370 | * Return a string representation of the data table.
371 | *
372 | * This is currently equivalent to a call of toCsv(), but might change in future releases.
373 | *
374 | * @return string
375 | */
376 | public function __toString() {
377 | $r = $this->toCsv();
378 | return $r;
379 | }
380 |
381 | /**
382 | * Change the id of the sheet which will be overwritten when attachToFile is called.
383 | *
384 | * @param int id
385 | * @return $this
386 | */
387 | public function setSheetId($id) {
388 | $this->sheetId = $id;
389 | return $this;
390 | }
391 |
392 | /**
393 | * Change the name of the sheet which will be attached when attachToFile is called
394 | *
395 | * @param string name
396 | * @return $this
397 | */
398 | public function setSheetName($name) {
399 | $this->sheetName = $name;
400 | return $this;
401 | }
402 |
403 | /**
404 | * Attach the data table to an existing xlsx file. The file location is given via the
405 | * first parameter. If a second parameter is given the source file will not be overwritten
406 | * and a new file will be created. The third parameter can be used to force updating the
407 | * auto calculation in the excel workbook.
408 | *
409 | * @param string srcFilename
410 | * @param string|null targetFilename
411 | * @param bool|null forceAutoCalculation
412 | * @return $this
413 | */
414 | public function attachToFile($srcFilename, $targetFilename = null, $forceAutoCalculation = false) {
415 | $calculatedColumns = null;
416 | if ($this->preserveFormulas){
417 | $temp_xlsx = new ExcelWorkbook($srcFilename);
418 | $calculatedColumns = $temp_xlsx->getCalculatedColumns($this->preserveFormulas);
419 | unset($temp_xlsx);
420 | }
421 |
422 | $xlsx = new ExcelWorkbook($srcFilename);
423 | $worksheet = new ExcelWorksheet();
424 | if(!is_null($targetFilename)) {
425 | $xlsx->setFilename($targetFilename);
426 | }
427 | $worksheet->addRows($this->toArray(), $calculatedColumns);
428 | $xlsx->addWorksheet($worksheet, $this->sheetId, $this->sheetName);
429 | if($forceAutoCalculation) {
430 | $xlsx->enableAutoCalculation();
431 | }
432 |
433 | if ($this->refreshTableRange) {
434 | $xlsx->refreshTableRange($this->refreshTableRange, count($this->data) + 1);
435 | }
436 |
437 | $xlsx->save();
438 | unset($xlsx);
439 | return $this;
440 | }
441 |
442 | /**
443 | * This functions takes an XLSX-file and an multidimensional and returns a string representation of the
444 | * XLSX file including the data table.
445 | * This function is especially useful if the file should be provided as download for a http request.
446 | *
447 | * @param string srcFilename
448 | * @return string
449 | */
450 | public function fillXLSX($srcFilename) {
451 | $targetFilename = tempnam(sys_get_temp_dir(), 'exceldatatables-');
452 | $this->attachToFile($srcFilename, $targetFilename);
453 | $result = file_get_contents($targetFilename);
454 | unlink($targetFilename);
455 | return $result;
456 | }
457 |
458 | /**
459 | * This function regenerates the range of the dynamic table to match
460 | * the total rows inserted
461 | *
462 | * @param string $table_name name of the excel table
463 | * @return $this
464 | */
465 | public function refreshTableRange($table_name = null)
466 | {
467 | $table_name = !is_null($table_name) ? $table_name : $this->sheetName;
468 | $this->refreshTableRange = $table_name;
469 | return $this;
470 | }
471 |
472 | /**
473 | * This function extracts the existing column formulas and injects them.
474 | *
475 | * @param string $table_name name of the excel table
476 | * @return $this
477 | */
478 | public function preserveFormulas($table_name)
479 | {
480 | $table_name = !is_null($table_name) ? $table_name : $this->sheetName;
481 | $this->preserveFormulas = $table_name;
482 | return $this;
483 | }
484 |
485 | }
486 |
--------------------------------------------------------------------------------
/src/Svrnm/ExcelDataTables/ExcelWorkbook.php:
--------------------------------------------------------------------------------
1 |
11 | * @license Apache-2.0
12 | */
13 | class ExcelWorkbook implements \Countable
14 | {
15 | /**
16 | * The source filename
17 | *
18 | * @var string
19 | */
20 | protected $srcFilename;
21 |
22 | /**
23 | * The target filename
24 | *
25 | * @var string
26 | */
27 | protected $targetFilename;
28 |
29 |
30 | /**
31 | * The ZipArchive representation of the workbook
32 | *
33 | * @var \ZipArchive
34 | */
35 | protected $xlsx;
36 |
37 | /**
38 | * The DomDocument representation of the workbook
39 | *
40 | * @var \DOMDocument
41 | */
42 | protected $workbook;
43 |
44 | /**
45 | * The DOMDocument representation of the styles.xml included in the workbook
46 | *
47 | * @var \DOMDocument
48 | */
49 | protected $styles;
50 |
51 |
52 | /**
53 | * If true all operations are closing and reopening the zip archive to store
54 | * modifications
55 | *
56 | * @var boolean
57 | */
58 | protected $autoSave = false;
59 |
60 | /**
61 | * If true the workbook is modified to to contain the fullCalcOnLoad attribute
62 | *
63 | * @var boolean
64 | */
65 | protected $autoCalculation = false;
66 |
67 | /**
68 | * The default name of the sheet when attachToFile is called
69 | *
70 | * @var string
71 | */
72 | protected $sheetName = 'Data';
73 |
74 | /**
75 | * Instantiate a new object of the type ExcelWorkbook. Expects a filename which
76 | * contains a spreadsheet of type xlsx.
77 | *
78 | * @param string $filename
79 | */
80 | public function __construct($filename) {
81 | $this->srcFilename = $filename;
82 | $this->targetFilename = $filename;
83 | }
84 |
85 | public function __destruct() {
86 | if(!is_null($this->xlsx)) {
87 | $this->getXLSX()->close();
88 | }
89 | }
90 |
91 | /**
92 | * Turn on auto saving
93 | *
94 | * @return $this
95 | */
96 | public function enableAutoSave() {
97 | $this->autoSave = true;
98 | return $this;
99 | }
100 |
101 | /**
102 | * Turn on auto calculation
103 | *
104 | * @return $this
105 | */
106 | public function enableAutoCalculation($value = true) {
107 | $this->autoCalculation = (bool)$value;
108 |
109 | $calcPr = $this->getWorkbook()->getElementsByTagName('calcPr')->item(0);
110 |
111 | if(is_null($calcPr)) {
112 | $calcPr = $this->getWorkbook()->createElement('calcPr');
113 | }
114 |
115 |
116 | $calcPr->setAttribute('fullCalcOnLoad', $this->autoCalculation ? '1' : '0');
117 |
118 | $this->saveWorkbook();
119 |
120 | return $this;
121 | }
122 |
123 | /**
124 | * Turn off auto saving
125 | *
126 | * @return $this
127 | */
128 | public function disableAutoSave() {
129 | $this->autoSave = false;
130 | return $this;
131 | }
132 |
133 | /**
134 | * Return true if auto saving is enabled
135 | *
136 | * @return $this
137 | */
138 | public function isAutoSaveEnabled() {
139 | return $this->autoSave;
140 | }
141 |
142 | /**
143 | * Returns the count of worksheets contained within the workbook
144 | *
145 | * @return int
146 | */
147 | public function count(): int {
148 | $sheets = $this->getWorkbook()->getElementsByTagName('sheet');
149 | return $sheets->length;
150 |
151 | }
152 |
153 | /**
154 | * Change the filepath where the modified XLSX is stored. This should
155 | * be called before a worksheet is added.
156 | *
157 | * @param string $filename
158 | * @return $this
159 | */
160 | public function setFilename($filename) {
161 | $this->targetFilename = $filename;
162 | return $this;
163 | }
164 |
165 | /**
166 | * Get the filepath where the modifiex XLSX will be stored.
167 | *
168 | * @return string
169 | */
170 | public function getFilename() {
171 | return $this->targetFilename;
172 | }
173 |
174 | /**
175 | * Get the XML representation of the worksheet with id $id. If the second
176 | * parameter is set not false the result is returned as DOMDocument.
177 | *
178 | * @param int id
179 | * @param boolean asDocument
180 | * @return string|DOMDocument
181 | */
182 | public function getWorksheetById($id, $asDocument = false) {
183 | $r = $this->getXLSX()->getFromName('xl/worksheets/sheet'.$id.'.xml');
184 | if($asDocument && $r !== false) {
185 | $dom = new \DOMDocument();
186 | $dom->loadXML($r);
187 | return $dom;
188 | } else {
189 | return $r;
190 | }
191 | }
192 |
193 | /**
194 | *
195 | */
196 | public function getSheetIdByName($name)
197 | {
198 | $sheets = $this->getWorkbook()->getElementsByTagName('sheet');
199 | foreach ($sheets as $index => $sheet) {
200 | if ($sheet->getAttribute('name') === $name) {
201 | return $index + 1;
202 | }
203 | }
204 | return null;
205 | }
206 |
207 | public function getTableIdByName($name)
208 | {
209 | $id = 1;
210 | while ($this->getXLSX()->statName('xl/tables/table' . $id . '.xml') !== false) {
211 |
212 | $xml = $this->getXLSX()->getFromName('xl/tables/table' . $id . '.xml');
213 | $dom = new \DOMDocument();
214 | $dom->loadXML($xml);
215 | $table = $dom->getElementsByTagName('table')->item(0);
216 |
217 | if ($table->getAttribute('name') === $name) {
218 | return $id;
219 | }
220 |
221 | $id++;
222 | }
223 | }
224 |
225 | /**
226 | * Check if a worksheet with name $name already exists. If not $name is
227 | * returned. Otherwise $name_ is incremented until the name is unique.
228 | *
229 | * @param $name
230 | * @return string
231 | */
232 | protected function uniqName($name) {
233 | $sheets = $this->getWorkbook()->getElementsByTagName('sheet');
234 | $names = array();
235 | foreach($sheets as $sheet) {
236 | $names[] = $sheet->getAttribute('name');
237 | }
238 | $i = 0;
239 | $origName = $name;
240 | while(in_array($name, $names)) {
241 | $name = $origName.'_'.$i;
242 | }
243 | return $name;
244 | }
245 |
246 | /**
247 | * Extract or create a date time format from the styles.xml. Some guessing is used to find
248 | * a matching id. If no interesting format is find, a new format is created
249 | *
250 | * @return int
251 | */
252 | protected function dateTimeFormatId() {
253 | $formats = $this->getStyles()->getElementsByTagName('numFmt');
254 | $exists = false;
255 | $generalId = 14;
256 | $numFmtId = false;
257 | $highestId = 164;
258 | $maxScore = 3;
259 | for($id = 0; $id < $formats->length; ++$id) {
260 | $format = $formats->item($id);
261 | $code = strtoupper($format->getAttribute('formatCode'));
262 | $currentId = $format->getAttribute('numFmtId');
263 | if($currentId > $highestId) {
264 | $highestId = $currentId;
265 | }
266 | if($code === "DD/MM/YYYY\ HH:MM:SS") {
267 | $numFmtId = $currentId;
268 | $exists = true;
269 | } else {
270 | // Do some "guessing" if the current format is "good enough"
271 | $score = (strpos($code, 'YY') !== false ? 1 : 0)
272 | + (strpos($code, 'YYYY') !== false ? 1 : 0)
273 | + (strpos($code, 'MM') !== false ? 1 : 0)
274 | + (strpos($code, 'D') !== false ? 1 : 0)
275 | + (strpos($code, 'DD') !== false ? 1 : 0)
276 | + (strpos($code, 'HH') !== false ? 1 : 0)
277 | + (strpos($code, 'HH:MM') !== false ? 1 : 0)
278 | + (strpos($code, 'HH:MM:SS') !== false ? 1 : 0);
279 | if($score > $maxScore) {
280 | $maxScore = $score;
281 | $exists = true;
282 | $numFmtId = $currentId;
283 | }
284 | }
285 | }
286 | if($numFmtId === false) {
287 | $numFmtId = $highestId+1;
288 | $numFmts = $this->getStyles()->getElementsByTagName('numFmts')->item(0);
289 |
290 | if(is_null($numFmts)) {
291 | $numFmts = $this->getStyles()->createElement('numFmts');
292 | }
293 |
294 | $numFmt = $this->getStyles()->createElement('numFmt');
295 | $numFmt->setAttribute('numFmtId', $numFmtId);
296 | $numFmt->setAttribute('formatCode', 'DD/MM/YYYY\ HH:MM:SS');
297 |
298 | $numFmts->appendChild($numFmt);
299 | $numFmts->setAttribute('count', (int)$numFmts->getAttribute('count')+1);
300 | }
301 |
302 | $cellXfs = $this->getStyles()->getElementsByTagName('cellXfs')->item(0);
303 | //$xfs = $this->getStyles()->getElementsByTagName('xf');
304 | $xfs = $cellXfs->childNodes;
305 | $result = false;
306 | for($i = 0; $i < $xfs->length; $i++) {
307 | $xf = $xfs->item($i);
308 | if($xf->getAttribute('numFmtId') == $numFmtId) {
309 | $result = $i;
310 | }
311 | }
312 | if($result === false) {
313 | $result = $cellXfs->getAttribute('count');
314 | $xf = $this->getStyles()->createElement('xf');
315 | $xf->setAttribute('numFmtId', $numFmtId);
316 | $xf->setAttribute('applyNumberFormat', 1);
317 | $cellXfs->appendChild($xf);
318 | $cellXfs->setAttribute('count', $result+1);
319 | }
320 | $this->saveStyles();
321 | return $result;
322 | }
323 |
324 | /**
325 | * Add a worksheet into the workbook with id $id and name $name. If $id is null the last
326 | * worksheet is replaced. If $name is empty, its default value is set to the default.
327 | *
328 | * Currently this replaces an existing worksheet. Adding new worksheets is not yet supported
329 | *
330 | * @param ExcelWorksheet $worksheet
331 | * @param int $id
332 | * @param string $name
333 | */
334 | public function addWorksheet(ExcelWorksheet $worksheet, $id = null, $name = null) {
335 | $name = !is_null($name) ? $name : $this->sheetName;
336 | if ($id === null) $id = $this->getSheetIdByName($name);
337 |
338 | if(is_null($id) || $id <= 0) {
339 | throw new \Exception('Sheet with name "'.$name.'" not found in file '.$this->srcFilename.'. Appending is not yet implemented.');
340 | /*
341 | // find a unused id in the worksheets
342 | $id = 1;
343 | while($this->getXLSX()->statName('xl/worksheets/sheet'.($id++).'.xml') !== false) {}
344 | */
345 | }
346 |
347 | $old = $this->getXLSX()->getFromName('xl/worksheets/sheet'.$id.'.xml');
348 | if($old === false) {
349 | throw new \Exception('Appending new sheets is not yet implemented: SheetId:' . $id .', SourceFile:'. $this->srcFilename.', TargetFile:'.$this->targetFilename);
350 | } else {
351 | $document = new \DOMDocument();
352 | $document->loadXML($old);
353 | $oldSheetData = $document->getElementsByTagName('sheetData')->item(0);
354 | $worksheet->setDateTimeFormatId($this->dateTimeFormatId());
355 | $newSheetData = $document->importNode( $worksheet->getDocument()->getElementsByTagName('sheetData')->item(0), true );
356 | $oldSheetData->parentNode->replaceChild($newSheetData, $oldSheetData);
357 | $xml = $document->saveXML();
358 | $this->getXLSX()->addFromString('xl/worksheets/sheet'.$id.'.xml', $xml);
359 | }
360 | if($this->isAutoSaveEnabled()) {
361 | $this->save();
362 | }
363 | return $this;
364 | }
365 |
366 | /**
367 | * Refresh the table range in the excel with the number of rows added
368 | *
369 | * @param string $tableName Name of the table
370 | * @param int $numRows number of rows
371 | * @return $this
372 | */
373 | public function refreshTableRange($tableName, $numRows)
374 | {
375 | $id = $this->getTableIdByName($tableName);
376 | if (is_null($id)) {
377 | throw new \Exception('table "' . $tableName . '" not found');
378 | }
379 |
380 | $document = new \DOMDocument();
381 | $document->loadXML($this->getXLSX()->getFromName('xl/tables/table' . $id . '.xml'));
382 |
383 | $table = $document->getElementsByTagName('table')->item(0);
384 | if (is_null($table)) {
385 | throw new \Exception('could not read "table" from document; '.$document);
386 | }
387 |
388 | $ref = $table->getAttribute('ref');
389 |
390 | $nref = preg_replace('/^(\w+\:[A-Z]+)(\d+)$/', '${1}' . $numRows, $ref);
391 |
392 | $table->setAttribute('ref', $nref);
393 |
394 | $this->getXLSX()->addFromString('xl/tables/table' . $id . '.xml', $document->saveXML());
395 |
396 | return $this;
397 | }
398 |
399 | public function getCalculatedColumns($tableName)
400 | {
401 | $id = $this->getTableIdByName($tableName);
402 | if(isset($id)){
403 | $document = new \DOMDocument();
404 | $document->loadXML($this->getXLSX()->getFromName('xl/tables/table' . $id . '.xml'));
405 | $columns = $document->getElementsByTagName('tableColumn');
406 | foreach($columns as $key => $column) {
407 | if($column->getElementsByTagName("calculatedColumnFormula")->length){
408 | $header = $column->getAttribute('name');
409 | $formula = $column->nodeValue;
410 | $calculatedColumn = array(
411 | 'index' => $key,
412 | 'header' => $header,
413 | 'content' => array(
414 | $header => array(
415 | 'type' => 'formula',
416 | 'value' => $formula,
417 | )
418 | )
419 | );
420 | $calculatedColumns[] = $calculatedColumn;
421 | }
422 | }
423 | return $calculatedColumns;
424 | }
425 | }
426 |
427 | /**
428 | * Return the ZipArchive representation of the current workbook
429 | *
430 | * @return ZipArchive
431 | */
432 | public function getXLSX() {
433 | if(is_null($this->xlsx)) {
434 | $this->openXLSX();
435 | }
436 | return $this->xlsx;
437 | }
438 |
439 | /**
440 | * Open the excel file and create the ZipArchive representation. If the file
441 | * does not exists or is not valid an exception is thrown.
442 | *
443 | * @throws Excepetion
444 | * @return $this
445 | */
446 | protected function openXLSX() {
447 | $this->xlsx = new \ZipArchive;
448 | if(!file_exists($this->srcFilename) && is_readable($this->srcFilename)) {
449 | throw new \Exception('File does not exists: '.$this->srcFilename);
450 | }
451 | if($this->srcFilename !== $this->targetFilename) {
452 | file_put_contents($this->targetFilename, file_get_contents($this->srcFilename));
453 | $this->srcFilename = $this->targetFilename;
454 | }
455 | $isOpen = $this->xlsx->open($this->targetFilename);
456 | if($isOpen !== true) {
457 | throw new \Exception('Could not open file: '.$this->targetFilename.' [ZipArchive error code: '.$isOpen.']');
458 | }
459 | return $this;
460 | }
461 |
462 | /**
463 | * Save the modifications
464 | *
465 | * @return $this
466 | */
467 | public function save() {
468 | $this->getXLSX()->close();
469 | $this->srcFilename = $this->targetFilename;
470 | $this->openXLSX();
471 | return $this;
472 | }
473 |
474 |
475 | /**
476 | * Return the DOMDocument representation of the current workbook
477 | *
478 | * @return DOMDocument
479 | */
480 | public function getWorkbook() {
481 | if(is_null($this->workbook)) {
482 | $this->workbook = new \DOMDocument();
483 | $workbookFile = $this->getXLSX()->getFromName('xl/workbook.xml');
484 | if ($workbookFile === false) {
485 | throw new \Exception('Could not find xl/workbook.xml in "'.$this->targetFilename.'"');
486 | }
487 | $this->workbook->loadXML($workbookFile);
488 | }
489 | return $this->workbook;
490 | }
491 |
492 | /**
493 | * Return the DOMDocument representation of the styles.xml included in the current workbook
494 | *
495 | * @return DOMDocument
496 | */
497 | public function getStyles() {
498 | if(is_null($this->styles)) {
499 | $this->styles = new \DOMDocument();
500 | $this->styles->loadXML($this->getXLSX()->getFromName('xl/styles.xml'));
501 | }
502 | return $this->styles;
503 | }
504 |
505 | /**
506 | * Save modifications of workbook.xml
507 | *
508 | * return @this
509 | */
510 | public function saveWorkbook() {
511 | $this->getXLSX()->addFromString('xl/workbook.xml', $this->getWorkbook()->saveXML());
512 | if($this->isAutoSaveEnabled()) {
513 | $this->save();
514 | }
515 | return $this;
516 | }
517 |
518 | /**
519 | * Save modifications of styles.xml
520 | *
521 | * return @this
522 | */
523 | public function saveStyles() {
524 | $this->getXLSX()->addFromString('xl/styles.xml', $this->getStyles()->saveXML());
525 | if($this->isAutoSaveEnabled()) {
526 | $this->save();
527 | }
528 | return $this;
529 | }
530 |
531 | }
532 |
--------------------------------------------------------------------------------