├── .gitignore ├── examplesbasic-xlsx.xlsx ├── examples ├── basic-xlsx.xlsx ├── basic-xml.php ├── basic-xlsx.php └── basic-xml.xml ├── .editorconfig ├── .travis.yml ├── phpcs.xml.dist ├── composer.json ├── src ├── AbstractBuilder.php ├── XmlBuilder.php └── XlsxBuilder.php ├── LICENSE ├── readme.md └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /examplesbasic-xlsx.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasparsd/mini-sheets-php/master/examplesbasic-xlsx.xlsx -------------------------------------------------------------------------------- /examples/basic-xlsx.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kasparsd/mini-sheets-php/master/examples/basic-xlsx.xlsx -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = tab 10 | 11 | [*.{json,yml,yaml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '5.6' 5 | - '7.2' 6 | 7 | install: 8 | - composer install 9 | 10 | script: 11 | - composer lint 12 | 13 | # Pull requests are built by default. 14 | branches: 15 | only: 16 | - master 17 | 18 | notifications: 19 | email: false 20 | 21 | cache: 22 | directories: 23 | - $HOME/.composer/cache 24 | -------------------------------------------------------------------------------- /examples/basic-xml.php: -------------------------------------------------------------------------------- 1 | add_props( 10 | [ 11 | 'Author' => 'Name Surname', 12 | ] 13 | ); 14 | 15 | $builder->add_rows( 16 | [ 17 | [ 18 | 'row 1, col1', 19 | 'row 1, col2', 20 | ], 21 | [ 22 | 'row 2, col1', 23 | 'row 2, col2', 24 | ], 25 | ] 26 | ); 27 | 28 | echo $builder->build(); // See basic-xml.xml for the generated output. 29 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | /vendor/ 16 | 17 | -------------------------------------------------------------------------------- /examples/basic-xlsx.php: -------------------------------------------------------------------------------- 1 | open( $filename, \ZipArchive::CREATE | \ZipArchive::OVERWRITE ); 10 | 11 | $builder = new XlsxBuilder( $zipper ); 12 | $builder->add_rows( 13 | [ 14 | [ 15 | 'row 1, col1', 16 | 'row 1, col2', 17 | ], 18 | [ 19 | 'row 2, col1', 20 | 'row 2, col2', 21 | ], 22 | ] 23 | ); 24 | 25 | $builder->build(); 26 | 27 | $zipper->close(); // See basic-xlsx.xlsx for the generated output. 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kasparsd/mini-sheets-php", 3 | "description": "Library for creating the most basic Office Open XML and Excel XLSX files.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Kaspars Dambis", 9 | "email": "hi@kaspars.net" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "kasparsd\\MiniSheets\\": "src/" 15 | } 16 | }, 17 | "require": {}, 18 | "require-dev": { 19 | "wp-coding-standards/wpcs": "^2.1" 20 | }, 21 | "suggest": { 22 | "ext-zip": "For generating XLSX files which are actually ZIP files." 23 | }, 24 | "scripts": { 25 | "lint": [ 26 | "phpcs ." 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/AbstractBuilder.php: -------------------------------------------------------------------------------- 1 | rows[] = $fields; 11 | } 12 | 13 | public function add_rows( $rows ) { 14 | $this->rows = array_merge( $this->rows, $rows ); 15 | } 16 | 17 | public function rows() { 18 | return $this->rows; 19 | } 20 | 21 | /** 22 | * Return the created timestamp in W3CDTF format. 23 | * 24 | * @return string 25 | */ 26 | public function created() { 27 | return gmdate( 'Y-m-d\TH:i:s\Z' ); 28 | } 29 | 30 | public function escape_xml( $string ) { 31 | return htmlspecialchars( $string, ENT_XML1 | ENT_COMPAT, 'UTF-8' ); 32 | } 33 | 34 | abstract public function build(); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /examples/basic-xml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | Name Surname 10 | 2019-06-06T06:49:06Z 11 | 12 | 13 | row 1, col1 14 | row 1, col2 15 | row 2, col1 16 | row 2, col2
17 |
18 |
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kaspars Dambis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # MiniSheets 2 | 3 | [![Build Status](https://travis-ci.com/kasparsd/mini-sheets-php.svg?branch=master)](https://travis-ci.com/kasparsd/mini-sheets-php) 4 | 5 | Extremely minimal and limited PHP library for creating the most basic Office Open XML and Microsoft Excel XLSX files. 6 | 7 | Created as a learning excerise for creating the most basic possible XLSX file. See [this blog post for the origin story](https://kaspars.net/blog/excel-xlsx-xml-php). 8 | 9 | 10 | ## Requirements 11 | 12 | - [`ZipArchive`](https://www.php.net/manual/en/class.ziparchive.php) for creating XLSX files which are actually ZIP files (TODO: add support for any ZIP library). 13 | 14 | 15 | ## Usage 16 | 17 | Install it as a Composer dependency: 18 | 19 | composer require kasparsd/mini-sheets-php 20 | 21 | See [examples](examples) for how to use it. 22 | 23 | 24 | ## Contribute 25 | 26 | All contributions are welcome! Please create [an issue](https://github.com/kasparsd/mini-sheets-php/issues/new) or [open a pull request](https://github.com/kasparsd/mini-sheets-php/pulls) with the suggested changes. 27 | 28 | 29 | ## Credits 30 | 31 | Created by [Kaspars Dambis](https://kaspars.net). 32 | -------------------------------------------------------------------------------- /src/XmlBuilder.php: -------------------------------------------------------------------------------- 1 | props = array_merge( $this->props, $props ); 11 | } 12 | 13 | public function props() { 14 | return array_merge( 15 | $this->props, 16 | [ 17 | 'Created' => $this->created(), 18 | ] 19 | ); 20 | } 21 | 22 | public function build() { 23 | $props_fields = []; 24 | 25 | foreach ( $this->props() as $prop_key => $prop_value ) { 26 | $props_fields[] = sprintf( 27 | '<%1$s>%2$s', 28 | $this->escape_xml( $prop_key ), 29 | $this->escape_xml( $prop_value ) 30 | ); 31 | } 32 | 33 | return sprintf( 34 | ' 35 | 36 | 41 | 42 | %s 43 | 44 | 45 | %s
46 |
47 |
', 48 | implode( "\n", $props_fields ), 49 | implode( "\n", $this->rows_formatted() ) 50 | ); 51 | } 52 | 53 | protected function rows_formatted() { 54 | $rows = []; 55 | 56 | foreach ( $this->rows() as $row ) { 57 | $cells = []; 58 | 59 | foreach ( $row as $field_value ) { 60 | $field_type = 'String'; 61 | 62 | if ( is_numeric( $field_value ) ) { 63 | $field_type = 'Number'; 64 | } 65 | 66 | $cells[] = sprintf( 67 | '%s', 68 | $this->escape_xml( $field_type ), 69 | $this->escape_xml( $field_value ) 70 | ); 71 | } 72 | 73 | $rows[] = sprintf( 74 | '%s', 75 | implode( "\n", $cells ) 76 | ); 77 | } 78 | 79 | return $rows; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "d8172d48635d35acb78b0a33f495433c", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "squizlabs/php_codesniffer", 12 | "version": "3.4.2", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 16 | "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8", 21 | "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "ext-simplexml": "*", 26 | "ext-tokenizer": "*", 27 | "ext-xmlwriter": "*", 28 | "php": ">=5.4.0" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 32 | }, 33 | "bin": [ 34 | "bin/phpcs", 35 | "bin/phpcbf" 36 | ], 37 | "type": "library", 38 | "extra": { 39 | "branch-alias": { 40 | "dev-master": "3.x-dev" 41 | } 42 | }, 43 | "notification-url": "https://packagist.org/downloads/", 44 | "license": [ 45 | "BSD-3-Clause" 46 | ], 47 | "authors": [ 48 | { 49 | "name": "Greg Sherwood", 50 | "role": "lead" 51 | } 52 | ], 53 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 54 | "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", 55 | "keywords": [ 56 | "phpcs", 57 | "standards" 58 | ], 59 | "time": "2019-04-10T23:49:02+00:00" 60 | }, 61 | { 62 | "name": "wp-coding-standards/wpcs", 63 | "version": "2.1.1", 64 | "source": { 65 | "type": "git", 66 | "url": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git", 67 | "reference": "bd9c33152115e6741e3510ff7189605b35167908" 68 | }, 69 | "dist": { 70 | "type": "zip", 71 | "url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/bd9c33152115e6741e3510ff7189605b35167908", 72 | "reference": "bd9c33152115e6741e3510ff7189605b35167908", 73 | "shasum": "" 74 | }, 75 | "require": { 76 | "php": ">=5.4", 77 | "squizlabs/php_codesniffer": "^3.3.1" 78 | }, 79 | "require-dev": { 80 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", 81 | "phpcompatibility/php-compatibility": "^9.0", 82 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 83 | }, 84 | "suggest": { 85 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." 86 | }, 87 | "type": "phpcodesniffer-standard", 88 | "notification-url": "https://packagist.org/downloads/", 89 | "license": [ 90 | "MIT" 91 | ], 92 | "authors": [ 93 | { 94 | "name": "Contributors", 95 | "homepage": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/graphs/contributors" 96 | } 97 | ], 98 | "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", 99 | "keywords": [ 100 | "phpcs", 101 | "standards", 102 | "wordpress" 103 | ], 104 | "time": "2019-05-21T02:50:00+00:00" 105 | } 106 | ], 107 | "aliases": [], 108 | "minimum-stability": "stable", 109 | "stability-flags": [], 110 | "prefer-stable": false, 111 | "prefer-lowest": false, 112 | "platform": [], 113 | "platform-dev": [] 114 | } 115 | -------------------------------------------------------------------------------- /src/XlsxBuilder.php: -------------------------------------------------------------------------------- 1 | zipper = $zipper; 13 | } 14 | 15 | public function build() { 16 | $this->zipper->addEmptyDir( 'docProps' ); 17 | $this->zipper->addFromString( 'docProps/app.xml', $this->app_xml() ); 18 | $this->zipper->addFromString( 'docProps/core.xml', $this->core_xml() ); 19 | 20 | $this->zipper->addEmptyDir( '_rels' ); 21 | $this->zipper->addFromString( '_rels/.rels', $this->rels_xml() ); 22 | 23 | $this->zipper->addEmptyDir( 'xl/worksheets' ); 24 | $this->zipper->addFromString( 'xl/worksheets/sheet1.xml', $this->sheet_xml() ); 25 | $this->zipper->addFromString( 'xl/workbook.xml', $this->workbook_xml() ); 26 | $this->zipper->addFromString( 'xl/sharedStrings.xml', $this->shared_strings_xml() ); 27 | 28 | $this->zipper->addEmptyDir( 'xl/_rels' ); 29 | $this->zipper->addFromString( 'xl/_rels/workbook.xml.rels', self::workbook_rels_xml() ); 30 | 31 | $this->zipper->addFromString( '[Content_Types].xml', $this->content_types_xml() ); 32 | 33 | return $this->zipper; 34 | } 35 | 36 | public function xlsx_get_shared_string_no( $string ) { 37 | static $string_pos = []; 38 | 39 | if ( isset( $this->shared_strings[ $string ] ) ) { 40 | $this->shared_strings[ $string ] += 1; 41 | } else { 42 | $this->shared_strings[ $string ] = 1; 43 | } 44 | 45 | if ( ! isset( $string_pos[ $string ] ) ) { 46 | $string_pos[ $string ] = array_search( $string, array_keys( $this->shared_strings ), true ); 47 | } 48 | 49 | return $string_pos[ $string ]; 50 | } 51 | 52 | public function xlsx_cell_name( $row_no, $column_no ) { 53 | $n = $column_no; 54 | 55 | for ( $r = ''; $n >= 0; $n = intval( $n / 26 ) - 1 ) { 56 | $r = chr( $n % 26 + 0x41 ) . $r; 57 | } 58 | 59 | return $r . ( $row_no + 1 ); 60 | } 61 | 62 | public function sheet_xml() { 63 | $rows = []; 64 | 65 | foreach ( $this->rows as $row_no => $row ) { 66 | $cells = []; 67 | $row = array_values( $row ); 68 | 69 | foreach ( $row as $col_no => $field_value ) { 70 | $field_type = 's'; 71 | 72 | if ( is_numeric( $field_value ) ) { 73 | $field_type = 'n'; 74 | } 75 | 76 | $field_value_no = $this->xlsx_get_shared_string_no( $field_value ); 77 | 78 | $cells[] = sprintf( 79 | '%d', 80 | $this->escape_xml( $this->xlsx_cell_name( $row_no, $col_no ) ), 81 | $this->escape_xml( $field_type ), 82 | $this->escape_xml( $field_value_no ) 83 | ); 84 | } 85 | 86 | $rows[] = sprintf( 87 | ' 88 | %s 89 | ', 90 | $this->escape_xml( $row_no + 1 ), 91 | implode( "\n", $cells ) 92 | ); 93 | } 94 | 95 | return sprintf( 96 | ' 97 | 98 | 99 | %s 100 | 101 | ', 102 | implode( "\n", $rows ) 103 | ); 104 | } 105 | 106 | public function shared_strings_xml() { 107 | $shared_strings = []; 108 | 109 | foreach ( $this->shared_strings as $string => $string_count ) { 110 | $shared_strings[] = sprintf( 111 | '%s', 112 | $this->escape_xml( $string ) 113 | ); 114 | } 115 | 116 | return sprintf( 117 | ' 118 | 119 | %s 120 | ', 121 | array_sum( $this->shared_strings ), 122 | count( $this->shared_strings ), 123 | implode( "\n", $shared_strings ) 124 | ); 125 | } 126 | 127 | 128 | public function workbook_xml() { 129 | return sprintf( 130 | ' 131 | 132 | 133 | 134 | 135 | ' 136 | ); 137 | } 138 | 139 | public function content_types_xml() { 140 | return sprintf( 141 | ' 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | ' 151 | ); 152 | } 153 | 154 | public function workbook_rels_xml() { 155 | return sprintf( 156 | ' 157 | 158 | 159 | 160 | ' 161 | ); 162 | } 163 | 164 | public function app_xml() { 165 | return sprintf( 166 | ' 167 | 168 | MiniSheets 169 | ' 170 | ); 171 | } 172 | 173 | public function core_xml() { 174 | return sprintf( 175 | ' 176 | 177 | %s 178 | MiniSheets 179 | ', 180 | $this->escape_xml( $this->created() ) 181 | ); 182 | } 183 | 184 | public function rels_xml() { 185 | return sprintf( 186 | ' 187 | 188 | 189 | 190 | 191 | ' 192 | ); 193 | } 194 | 195 | } 196 | --------------------------------------------------------------------------------