├── .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('123234567')); 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 2Test 11341337.541666667Test 22341730.55Test 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 | --------------------------------------------------------------------------------