├── .github └── workflows │ └── tests.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── AbstractFile.php └── File ├── Csv.php └── Json.php /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | run-name: Tests for latest commit by ${{ github.actor }} 3 | on: [push] 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: true 9 | matrix: 10 | php: [ 8.0, 8.1 ] 11 | dependency-version: [ prefer-lowest, prefer-stable ] 12 | 13 | name: P${{ matrix.php }} - ${{ matrix.dependency-version }} 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Setup PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 24 | coverage: none 25 | 26 | - name: Install dependencies 27 | run: | 28 | composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader 29 | - name: Execute tests 30 | run: vendor/bin/phpunit 31 | - run: echo " ${{ job.status }}." 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `json-csv` will be documented in this file. 4 | 5 | ## 0.4.0 6 | - Remove support for PHP 7.2. 7 | - Added PHP 8.0 support. 8 | 9 | ## 0.3.0 10 | - Remove support for PHP 7.1. 11 | - Added utf-8 support for csv conversion. 12 | 13 | ## 0.2.0 14 | - Remove support for PHP 7.0. 15 | - Better CSV to JSON conversion method. 16 | 17 | ## 0.1.1 18 | - Fix error with assoc. arrays (splat operator). 19 | 20 | ## 0.1.0 21 | - Fix property error in JSON to CSV conversion. 22 | 23 | ## 0.0.1 24 | - Initial release with basic conversion methods. 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/ozdemirburak/json-csv). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Burak Özdemir 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 13 | > all 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 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON to CSV and CSV to JSON Converter Library in PHP 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE.md) 5 | [![Build Status][ico-travis]][link-travis] 6 | [![Total Downloads][ico-downloads]][link-downloads] 7 | 8 | The most basic CSV to JSON and JSON to CSV converter library in PHP without any dependencies. 9 | 10 | ## Install 11 | 12 | Via Composer 13 | 14 | ``` bash 15 | $ composer require ozdemirburak/json-csv 16 | ``` 17 | 18 | ## Usage 19 | 20 | ### JSON to CSV Converter 21 | 22 | ``` php 23 | use OzdemirBurak\JsonCsv\File\Json; 24 | 25 | // JSON to CSV 26 | $json = new Json(__DIR__ . '/above.json'); 27 | // To convert JSON to CSV string 28 | $csvString = $json->convert(); 29 | // To set a conversion option then convert JSON to CSV and save 30 | $json->setConversionKey('utf8_encoding', true); 31 | $json->convertAndSave(__DIR__ . '/above.csv'); 32 | // To convert JSON to CSV and force download on browser 33 | $json->convertAndDownload(); 34 | ``` 35 | 36 | You can also convert directly from a JSON string using the `fromString` method. 37 | 38 | ``` php 39 | $csvString = (new Json())->fromString('{"name": "Buddha", "age": 80}')->convert(); 40 | ``` 41 | 42 | Assume that the input JSON is something like below. 43 | 44 | ```json 45 | [ 46 | { 47 | "name": { 48 | "common": "Turkey", 49 | "official": "Republic of Turkey", 50 | "native": "T\u00fcrkiye" 51 | }, 52 | "area": 783562, 53 | "latlng": [39, 35] 54 | }, 55 | { 56 | "name": { 57 | "common": "Israel", 58 | "official": "State of Israel", 59 | "native": "\u05d9\u05e9\u05e8\u05d0\u05dc" 60 | }, 61 | "area": 20770, 62 | "latlng": [31.30, 34.45] 63 | } 64 | ] 65 | ``` 66 | 67 | After the conversion, the resulting CSV data will look like below. 68 | 69 | **name\_common**|**name\_official**|**name\_native**|**area**|**latlng\_0**|**latlng\_1** 70 | :-----:|:-----:|:-----:|:-----:|:-----:|:-----: 71 | Turkey|Republic of Turkey|Türkiye|783562|39|35 72 | Israel|State of Israel|ישראל|20770|31.3|34.45 73 | 74 | 75 | ### CSV to JSON Converter 76 | 77 | ``` php 78 | use OzdemirBurak\JsonCsv\File\Csv; 79 | 80 | // CSV to JSON 81 | $csv = new Csv(__DIR__ . '/below.csv'); 82 | $csv->setConversionKey('options', JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); 83 | // To convert CSV to JSON string 84 | $jsonString = $csv->convert(); 85 | // To convert CSV to JSON and save 86 | $csv->convertAndSave(__DIR__ . '/below.json'); 87 | // To convert CSV to JSON and force download on browser 88 | $csv->convertAndDownload(); 89 | ``` 90 | 91 | You can also convert directly from a CSV string using the `fromString` method. 92 | 93 | ``` php 94 | $jsonString = (new Csv())->fromString('[{"name":"Buddha","age":"80"}]')->convert(); 95 | ``` 96 | 97 | Assume that the input CSV file is something like below. 98 | 99 | **SepalLength**|**SepalWidth**|**PetalLength**|**PetalWidth**|**Name** 100 | :-----:|:-----:|:-----:|:-----:|:-----: 101 | 5.1|3.5|1.4|0.2|Iris-setosa 102 | 7.0|3.2|4.7|1.4|Iris-versicolor 103 | 6.3|3.3|6.0|2.5|Iris-virginica 104 | 105 | After the conversion, the resulting JSON data will look like below. 106 | 107 | ```json 108 | [ 109 | { 110 | "SepalLength": "5.1", 111 | "SepalWidth": "3.5", 112 | "PetalLength": "1.4", 113 | "PetalWidth": "0.2", 114 | "Name": "Iris-setosa" 115 | }, 116 | { 117 | "SepalLength": "7.0", 118 | "SepalWidth": "3.2", 119 | "PetalLength": "4.7", 120 | "PetalWidth": "1.4", 121 | "Name": "Iris-versicolor" 122 | }, 123 | { 124 | "SepalLength": "6.3", 125 | "SepalWidth": "3.3", 126 | "PetalLength": "6.0", 127 | "PetalWidth": "2.5", 128 | "Name": "Iris-virginica" 129 | } 130 | ] 131 | ``` 132 | 133 | ## Change log 134 | 135 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 136 | 137 | ## Testing 138 | 139 | ``` bash 140 | $ composer test 141 | ``` 142 | 143 | ## Known Issues 144 | 145 | Currently, there are not any issues that are known. 146 | 147 | ## Contributing 148 | 149 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 150 | 151 | ## Credits 152 | 153 | - [Burak Özdemir][link-author] 154 | - [All Contributors][link-contributors] 155 | 156 | ## License 157 | 158 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 159 | 160 | [ico-version]: https://img.shields.io/packagist/v/ozdemirburak/json-csv.svg?style=flat-square 161 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 162 | [ico-travis]: https://img.shields.io/travis/ozdemirburak/json-csv/master.svg?style=flat-square 163 | [ico-downloads]: https://img.shields.io/packagist/dt/ozdemirburak/json-csv.svg?style=flat-square 164 | 165 | [link-packagist]: https://packagist.org/packages/ozdemirburak/json-csv 166 | [link-travis]: https://travis-ci.org/ozdemirburak/json-csv 167 | [link-downloads]: https://packagist.org/packages/ozdemirburak/json-csv 168 | [link-author]: https://github.com/ozdemirburak 169 | [link-contributors]: ../../contributors 170 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ozdemirburak/json-csv", 3 | "description": "JSON to CSV and CSV to JSON converters in PHP.", 4 | "keywords": ["json", "csv", "json to csv", "csv to json", "json2csv", "csv2json"], 5 | "homepage": "https://github.com/ozdemirburak/json-csv", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Burak Özdemir", 10 | "homepage": "https://ozdemirburak.com" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.3|^8.0", 15 | "ext-json": "*" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit" : "~8.0|~9.0", 19 | "squizlabs/php_codesniffer": "~3.5" 20 | }, 21 | "autoload": { 22 | "psr-4": { "OzdemirBurak\\JsonCsv\\": "src" } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { "OzdemirBurak\\JsonCsv\\Tests\\": "tests" } 26 | }, 27 | "scripts": { 28 | "test": "vendor/bin/phpunit", 29 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 30 | "check-style": "vendor/bin/phpcs -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests", 31 | "fix-style": "vendor/bin/phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/AbstractFile.php: -------------------------------------------------------------------------------- 1 | loadFile($filepath); 31 | } 32 | } 33 | 34 | /** 35 | * Load data from a file. 36 | * 37 | * @param string $filepath 38 | */ 39 | protected function loadFile(string $filepath): void 40 | { 41 | if (!is_readable($filepath)) { 42 | throw new \RuntimeException("File not readable: $filepath"); 43 | } 44 | [$this->filename, $this->data] = [pathinfo($filepath, PATHINFO_FILENAME), file_get_contents($filepath)]; 45 | } 46 | 47 | /** 48 | * @param string|null $filename 49 | * @param bool $exit 50 | */ 51 | public function convertAndDownload(?string $filename = null, bool $exit = true): void 52 | { 53 | $filename = $filename ?? $this->filename; 54 | $this->sendHeaders($filename); 55 | echo $this->convert(); 56 | if ($exit === true) { 57 | exit(); 58 | } 59 | } 60 | 61 | /** 62 | * Send headers for download. 63 | * 64 | * @param string $filename 65 | */ 66 | protected function sendHeaders(string $filename): void 67 | { 68 | header('Content-disposition: attachment; filename=' . $filename . '.' . $this->conversion['extension']); 69 | header('Content-type: ' . $this->conversion['type']); 70 | } 71 | 72 | /** 73 | * @param string $dataString 74 | * 75 | * @return $this 76 | */ 77 | public function fromString(string $dataString): AbstractFile 78 | { 79 | $this->data = $dataString; 80 | return $this; 81 | } 82 | 83 | /** 84 | * @param string $path 85 | * 86 | * @return bool|int 87 | */ 88 | public function convertAndSave(string $path): int 89 | { 90 | return file_put_contents($path, $this->convert()); 91 | } 92 | 93 | /** 94 | * @return string 95 | */ 96 | public function getData(): string 97 | { 98 | return $this->data; 99 | } 100 | 101 | /** 102 | * @return string 103 | */ 104 | public function getFilename(): string 105 | { 106 | return $this->filename; 107 | } 108 | 109 | /** 110 | * @param string $key 111 | * @param string|int $value 112 | * 113 | * @return array 114 | */ 115 | public function setConversionKey(string $key, $value): array 116 | { 117 | $this->conversion[$key] = $value; 118 | return $this->conversion; 119 | } 120 | 121 | /** 122 | * @return string 123 | */ 124 | abstract public function convert(): string; 125 | } 126 | -------------------------------------------------------------------------------- /src/File/Csv.php: -------------------------------------------------------------------------------- 1 | 'json', 14 | 'type' => 'application/json', 15 | 'options' => 0, 16 | 'delimiter' => ',', 17 | 'enclosure' => '"', 18 | 'escape' => '\\', 19 | 'join' => '_', 20 | 'numbers' => 'strings' 21 | ]; 22 | 23 | /** 24 | * Converts CSV data to JSON. 25 | * 26 | * @return string JSON representation of CSV data. 27 | */ 28 | public function convert(): string 29 | { 30 | $data = $this->parseData(); 31 | $keys = $this->parseCsv(array_shift($data)); 32 | $splitKeys = $this->splitKeys($keys); 33 | $jsonObjects = array_map([$this, 'convertLineToJson'], $data, array_fill(0, count($data), $splitKeys)); 34 | $json = json_encode($jsonObjects, $this->conversion['options']); 35 | if (json_last_error() !== JSON_ERROR_NONE) { 36 | throw new \RuntimeException('JSON encoding failed: ' . json_last_error_msg()); 37 | } 38 | return $json; 39 | } 40 | 41 | /** 42 | * Splits keys based on the configured join delimiter. 43 | * 44 | * @param array $keys 45 | * @return array 46 | */ 47 | private function splitKeys(array $keys): array 48 | { 49 | return array_map(function ($key) { 50 | return explode($this->conversion['join'], $key); 51 | }, $keys); 52 | } 53 | 54 | /** 55 | * Converts a CSV line to a JSON object. 56 | * 57 | * @param string $line 58 | * @param array $splitKeys 59 | * @return array 60 | */ 61 | private function convertLineToJson(string $line, array $splitKeys): array 62 | { 63 | return $this->getJsonObject($this->parseCsv($line), $splitKeys); 64 | } 65 | 66 | /** 67 | * Creates a JSON object from a CSV line. 68 | * 69 | * @param array $values CSV values. 70 | * @param array $splitKeys Split keys. 71 | * @return array JSON object. 72 | */ 73 | private function getJsonObject(array $values, array $splitKeys): array 74 | { 75 | $jsonObject = []; 76 | for ($valueIndex = 0, $count = count($values); $valueIndex < $count; $valueIndex++) { 77 | if ($values[$valueIndex] === '') { 78 | continue; 79 | } 80 | $this->setJsonValue($splitKeys[$valueIndex], 0, $jsonObject, $values[$valueIndex]); 81 | } 82 | return $jsonObject; 83 | } 84 | 85 | /** 86 | * Sets a value in a JSON object. 87 | * 88 | * @param array $splitKey Split key. 89 | * @param int $splitKeyIndex Split key index. 90 | * @param array $jsonObject JSON object. 91 | * @param mixed $value Value. 92 | */ 93 | private function setJsonValue(array $splitKey, int $splitKeyIndex, array &$jsonObject, $value): void 94 | { 95 | $keyPart = $splitKey[$splitKeyIndex]; 96 | if (count($splitKey) > $splitKeyIndex + 1) { 97 | if (!array_key_exists($keyPart, $jsonObject)) { 98 | $jsonObject[$keyPart] = []; 99 | } 100 | $this->setJsonValue($splitKey, $splitKeyIndex + 1, $jsonObject[$keyPart], $value); 101 | } else { 102 | if (is_numeric($value) && $this->conversion['numbers'] === 'numbers') { 103 | $value = 0 + $value; 104 | } 105 | $jsonObject[$keyPart] = $value; 106 | } 107 | } 108 | 109 | /** 110 | * Parses a CSV line. 111 | * 112 | * @param string $line CSV line. 113 | * @return array Parsed CSV line. 114 | */ 115 | private function parseCsv(string $line): array 116 | { 117 | return str_getcsv( 118 | $line, 119 | $this->conversion['delimiter'], 120 | $this->conversion['enclosure'], 121 | $this->conversion['escape'] 122 | ); 123 | } 124 | 125 | /** 126 | * Parses CSV data. 127 | * 128 | * @return array Parsed CSV data. 129 | */ 130 | private function parseData(): array 131 | { 132 | $data = explode("\n", $this->data); 133 | if (end($data) === '') { 134 | array_pop($data); 135 | } 136 | return $data; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/File/Json.php: -------------------------------------------------------------------------------- 1 | 'csv', 14 | 'type' => 'text/csv', 15 | 'delimiter' => ',', 16 | 'enclosure' => '"', 17 | 'escape' => '\\', 18 | 'join' => '_', 19 | 'null' => null, 20 | 'utf8_encoding' => false 21 | ]; 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function convert(): string 27 | { 28 | $data = json_decode($this->data, true); 29 | if (json_last_error() !== JSON_ERROR_NONE) { 30 | throw new \RuntimeException('Invalid JSON data.'); 31 | } 32 | if ($this->isAssociativeArray($data) && !$this->containsArray($data)) { 33 | return $this->toCsvString([$data]); 34 | } 35 | $flattened = array_map([$this, 'flatten'], $data); 36 | $default = $this->getArrayOfNulls($flattened); 37 | $merged = array_map( 38 | function ($d) use ($default) { 39 | return array_merge($default, $d); 40 | }, 41 | $flattened 42 | ); 43 | return $this->toCsvString($merged); 44 | } 45 | 46 | /** 47 | * @param array $data 48 | * @return string 49 | */ 50 | protected function toCsvString(array $data): string 51 | { 52 | $f = fopen('php://temp', 'wb'); 53 | if ($this->conversion['utf8_encoding']) { 54 | fprintf($f, chr(0xEF) . chr(0xBB) . chr(0xBF)); 55 | } 56 | $this->putCsv($f, array_keys(current($data))); 57 | array_walk($data, function ($row) use ($f) { 58 | $this->putCsv($f, $row); 59 | }); 60 | rewind($f); 61 | $csv = stream_get_contents($f); 62 | fclose($f); 63 | return ! \is_bool($csv) ? $csv : ''; 64 | } 65 | 66 | /** 67 | * @param array $array 68 | * @param string $prefix 69 | * @param array $result 70 | * @return array 71 | */ 72 | protected function flatten(array $array = [], string $prefix = '', array $result = []): array 73 | { 74 | foreach ($array as $key => $value) { 75 | if (\is_array($value)) { 76 | $result = array_merge($result, $this->flatten($value, $prefix . $key . $this->conversion['join'])); 77 | } else { 78 | $result[$prefix . $key] = $value; 79 | } 80 | } 81 | return $result; 82 | } 83 | 84 | /** 85 | * @param array $flattened 86 | * @return array 87 | */ 88 | protected function getArrayOfNulls(array $flattened): array 89 | { 90 | $flattened = array_values($flattened); 91 | $keys = array_keys(array_merge(...$flattened)); 92 | return array_fill_keys($keys, $this->conversion['null']); 93 | } 94 | 95 | /** 96 | * @param resource $handle 97 | * @param array $fields 98 | * @return bool|int 99 | */ 100 | private function putCsv($handle, array $fields) 101 | { 102 | return fputcsv( 103 | $handle, 104 | $fields, 105 | $this->conversion['delimiter'], 106 | $this->conversion['enclosure'], 107 | $this->conversion['escape'] 108 | ); 109 | } 110 | 111 | /** 112 | * @param array $data 113 | * @return bool 114 | */ 115 | private function isAssociativeArray(array $data): bool 116 | { 117 | return array_keys($data) !== range(0, count($data) - 1); 118 | } 119 | 120 | /** 121 | * Check if the file/data contains nested arrays 122 | * 123 | * @param $array 124 | * 125 | * @return bool 126 | */ 127 | private function containsArray(array $array): bool 128 | { 129 | foreach ($array as $data) { 130 | if (is_iterable($data)) { 131 | foreach ($data as $d) { 132 | if (is_array($d)) { 133 | return true; 134 | } 135 | } 136 | } 137 | } 138 | return false; 139 | } 140 | } 141 | --------------------------------------------------------------------------------