├── CHANGES.md ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── examples ├── class_resolution.php ├── config.php └── writing.php └── src ├── Encoder ├── ArrayEncoder.php ├── BooleanEncoder.php ├── Encoder.php ├── FloatEncoder.php ├── GMPEncoder.php ├── IntegerEncoder.php ├── NullEncoder.php ├── ObjectEncoder.php └── StringEncoder.php ├── InvalidOptionException.php ├── PHPEncoder.php └── autoload.php /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog # 2 | 3 | ## v2.4.2 (2022-12-10) ## 4 | 5 | * Added tests to ensure PHP 8.2 compatibility 6 | * Changed `export` type object conversion have more consistent index order 7 | 8 | ## v2.4.1 (2020-11-29) ## 9 | 10 | * Migrated to Github actions for CI 11 | * Added automated builds to test for PHP versions from 5.6 to 8.0 12 | 13 | ## v2.4.0 (2018-07-03) ## 14 | 15 | * Added `string.classes` option, which allows to define an array of classes or 16 | namespaces to encode using the `::class` format, when encountered as strings 17 | * Added `string.imports` options, which allows to define the used imports to write 18 | the `::class` format strings using shorter imported notation 19 | * Support for HHVM has been dropped, as HHVM no longer aims for PHP compatibility 20 | * Added travis builds for PHP 7.2 21 | * Change some rules in the used coding standard 22 | 23 | ## v2.3.0 (2017-07-15) ## 24 | 25 | * Added `string.utf8` option which causes the string encoder to escape all 26 | valid multibyte UTF-8 characters using the PHP7 unicode code point syntax. 27 | * Added `string.binary` option which causes the string encoder to encode all 28 | non UTF-8 strings using a `base64_encode()`. 29 | * Added `integer.type` option that accepts values `binary`, `octal`, `decimal` 30 | or `hexadecimal` which can be used to change the output syntax of integers. 31 | * Added `hex.capitalize` option that causes all hexadecimal character in 32 | output to appear in upper case 33 | * Added `float.export` option that forces float encoder to use `var_export` 34 | for encoding floating point numbers 35 | * Float encoder now delegates integer encoding to the integer encoder (to 36 | allow different integer types). 37 | 38 | ## v2.2.0 (2017-07-08) ## 39 | 40 | * Increase the minimum PHP version requirement to 5.6 41 | * Update to latest coding standards 42 | * Update tests to work with PHPUnit 6 43 | * Update travis build to test for PHP 7.1 44 | 45 | ## v2.1.3 (2015-11-08) ## 46 | 47 | * Ensure the tests run on both HHVM and PHP7 48 | 49 | ## v2.1.2 (2015-08-22) ## 50 | 51 | * The GMP encoder no longer tries to encode classes that extend the GMP class 52 | 53 | ## v2.1.1 (2015-08-08) ## 54 | 55 | * Fixed object encoder not throwing an exception on some incorrect 56 | object.format values 57 | * Fixed coding standards issues around the code and api documentation 58 | * Improved Travis build process 59 | 60 | ## v2.1.0 (2015-04-18) ## 61 | 62 | * Encoder options with `null` default value will now be recognized 63 | * The integer encoder will now add an `(int)` cast in front of integers, if 64 | their value equals `PHP_INT_MAX * -1 - 1`. 65 | * If `float.integers` is set to `true`, the float encoder will now only encode 66 | floats as integers if the value is accurately represented by the float. Set 67 | the value to `"all"` to restore the previous behavior. 68 | * The float encoder no longer breaks if the PHP locale uses comma as a decimal 69 | separator. 70 | * The float encoder now behaves slightly differently when deciding whether to 71 | use the exponential float notation or not. 72 | * The float encoder now uses `serialize_precision` when the option `precision` 73 | is set to `false` 74 | * Several methods will now throw an InvalidOptionException if any invalid 75 | encoder options have been provided 76 | 77 | ## v2.0.2 (2015-01-21) ## 78 | 79 | * `array.align` will now respect `array.omit` and `array.inline` settings if 80 | all the keys in the array can be omitted. 81 | * Clarified documentation on how these settings are intended to work 82 | together. 83 | 84 | ## v2.0.1 (2015-01-11) ## 85 | 86 | * Improvements on code quality, documentation and tests 87 | 88 | ## v2.0.0 (2014-12-29) ## 89 | 90 | * Encoding is now separated into external encoding classes 91 | * Various options are now set via PHPEncoder::setOption() instead 92 | * GMP encoding now supports PHP 5.6.0 and later 93 | * Encoder now detects and throws an exception on recursion 94 | * Null and boolean capitalization can now be customized 95 | * It's possible to choose from long or short array notation 96 | * Encoder now supports inlining simple arrays 97 | * The end of line character for arrays can now be modified 98 | 99 | ## v1.5.0 (2014-07-10) ## 100 | 101 | * Added new object conversion flags OBJECT_VARS and OBJECT_SET_STATE. 102 | * The default object conversion is now OBJECT_VARS | OBJECT_CAST 103 | * Documentation now acknowledges the existence of var_export 104 | 105 | ## v1.4.0 (2014-05-31) ## 106 | 107 | * GMP Integer resources are now encoded using 'gmp_init()' 108 | * The library now correctly disables align keys when whitespace is disabled 109 | * Code cleanup and documentation fixes 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-2022 Riikka Kalliomäki 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 | # PHP Variable Exporter # 2 | 3 | *PHPEncoder* is a PHP library for exporting variables and generating PHP code 4 | representations for said variables similar to the built in function 5 | `var_export()`. Compared to the built in function, however, this library 6 | provides more options to customize the output, which makes it easier to generate 7 | code for different kinds of purposes such as readable configuration files or 8 | optimized cache files. 9 | 10 | The purpose of this library is to address some of the shortcomings with the 11 | built in `var_export()`. For example, there is no way to control the amount of 12 | whitespace in the output and there is no way to choose between different array 13 | notations. This library also provides functionality to convert objects into PHP 14 | code that is actually useful when compared to the built in function. 15 | 16 | The large number of customization options in this library allows you to create 17 | code that fits your purposes. You can create very compact code, when you need to 18 | limit the size of the output, or you can create code in the style that actually 19 | fits in any of your dynamically generated PHP files. 20 | 21 | The API documentation is available at: http://kit.riimu.net/api/phpencoder/ 22 | 23 | [![CI](https://img.shields.io/github/workflow/status/Riimu/Kit-PHPEncoder/CI/master?style=flat-square)](https://github.com/Riimu/Kit-PHPEncoder/actions) 24 | [![Scrutinizer](https://img.shields.io/scrutinizer/quality/g/Riimu/Kit-PHPEncoder/master?style=flat-square)](https://scrutinizer-ci.com/g/Riimu/Kit-PHPEncoder/) 25 | [![codecov](https://img.shields.io/codecov/c/github/Riimu/Kit-PHPEncoder/master?style=flat-square)](https://codecov.io/gh/Riimu/Kit-PHPEncoder) 26 | [![Packagist](https://img.shields.io/packagist/v/riimu/kit-phpencoder.svg?style=flat-square)](https://packagist.org/packages/riimu/kit-phpencoder) 27 | 28 | ## Requirements ## 29 | 30 | * The minimum supported PHP version is 5.6 31 | 32 | ## Installation ## 33 | 34 | ### Installation with Composer ### 35 | 36 | The easiest way to install this library is to use Composer to handle your 37 | dependencies. In order to install this library via Composer, simply follow 38 | these two steps: 39 | 40 | 1. Acquire the `composer.phar` by running the Composer 41 | [Command-line installation](https://getcomposer.org/download/) 42 | in your project root. 43 | 44 | 2. Once you have run the installation script, you should have the `composer.phar` 45 | file in you project root and you can run the following command: 46 | 47 | ``` 48 | php composer.phar require "riimu/kit-phpencoder:^2.3" 49 | ``` 50 | 51 | After installing this library via Composer, you can load the library by 52 | including the `vendor/autoload.php` file that was generated by Composer during 53 | the installation. 54 | 55 | ### Adding the library as a dependency ### 56 | 57 | If you are already familiar with how to use Composer, you may alternatively add 58 | the library as a dependency by adding the following `composer.json` file to your 59 | project and running the `composer install` command: 60 | 61 | ```json 62 | { 63 | "require": { 64 | "riimu/kit-phpencoder": "^2.3" 65 | } 66 | } 67 | ``` 68 | 69 | ### Manual installation ### 70 | 71 | If you do not wish to use Composer to load the library, you may also download 72 | the library manually by downloading the [latest release](https://github.com/Riimu/Kit-PHPEncoder/releases/latest) 73 | and extracting the `src` folder to your project. You may then include the 74 | provided `src/autoload.php` file to load the library classes. 75 | 76 | ## Usage ## 77 | 78 | The most relevant method provided by this library is the `encode()` method 79 | provided by `PHPEncoder`. The method takes any value as an argument and returns 80 | the PHP code representation for that value. 81 | 82 | For example: 83 | 84 | ```php 85 | encode(['foo' => 'bar', [1, true, false, null, 1.0]]); 90 | ``` 91 | 92 | This would create the following output: 93 | 94 | ``` 95 | [ 96 | 'foo' => 'bar', 97 | [1, true, false, null, 1.0], 98 | ] 99 | ``` 100 | 101 | Of course, the most important feature of this library is the ability to 102 | customize the created the PHP code. As the second argument, the `encode()` 103 | method takes an array of options, which can be used to customize the returned 104 | PHP code. For example: 105 | 106 | ```php 107 | encode(['foo' => 'bar', [1, true, false, null, 1.0]], [ 112 | 'array.inline' => false, 113 | 'array.omit' => false, 114 | 'array.indent' => 2, 115 | 'boolean.capitalize' => true, 116 | 'null.capitalize' => true, 117 | ]); 118 | ``` 119 | 120 | This would create the following output: 121 | 122 | ``` 123 | [ 124 | 'foo' => 'bar', 125 | 0 => [ 126 | 0 => 1, 127 | 1 => TRUE, 128 | 2 => FALSE, 129 | 3 => NULL, 130 | 4 => 1.0, 131 | ], 132 | ] 133 | ``` 134 | 135 | ### Options ### 136 | 137 | Encoding options allow you to customize the output of the `encode()` method. It 138 | is possible to set these options in three different ways: 139 | 140 | * Options can be provided as an array to the `PHPEncoder` constructor. 141 | * Option values can be set via the `setOption()` method. 142 | * Options can be passed as an array as the second argument to the `encode()` 143 | method. 144 | 145 | Note that options passed to the `encode()` method are only temporary and do not 146 | apply to following calls. 147 | 148 | #### List of Options #### 149 | 150 | * **whitespace** : <boolean> (true)
151 | When set to `false`, generation of all extra whitespace is disabled and all 152 | other settings that affect whitespace are ignored. 153 | 154 | * **hex.capitalize** : <boolean> (false)
155 | When set to `true` all hexadecimal characters in the output are written 156 | using upper case instead of lower case. 157 | 158 | * **null.capitalize** : <boolean> (false)
159 | When set to `true`, all `null` values are written in upper case instead of 160 | lower case. 161 | 162 | * **boolean.capitalize** : <boolean> (false)
163 | When set to `true`, all `true` and `false` values are written in upper case 164 | instead of lower case. 165 | 166 | * **integer.type** : <"binary"|"octal"|"decimal"|"hexadecimal"> ("decimal")
167 | Change the output syntax of integers. For example, using the type `"hexadecimal"` 168 | would output the number `15` as `0xf`. 169 | 170 | * **float.integers** : <boolean|"all"> (false)
171 | When set to `true`, any float that represents an integer and has a value 172 | that is accurately represented by the floating point number will be encoded 173 | as an integer instead of a float. (e.g. the value `2.0` will be encoded as 174 | `2`). To include the values that are not accurately represented, you may set 175 | option to `"all"`. 176 | 177 | * **float.export** : <boolean> (false)
178 | When set to `true` floats are encoded using `var_export()`, which causes a 179 | slightly different output on non integer floating point numbers compared to 180 | the standard implemented method. In some cases, this may produce more 181 | accurate numbers but with less cleaner representation. 182 | 183 | * **float.precision** : <integer|false> (17)
184 | The maximum precision of encoded floating point values, which usually also 185 | means the maximum number of digits in encoded floats. If the value is set to 186 | `false`, the PHP ini setting `serialize_precision` will be used instead. 187 | Note that due to the way floating point values work, a value greater than 17 188 | does not provide any additional precision. 189 | 190 | * **string.binary** : <boolean> (false)
191 | When set to `true` any string that is not valid UTF-8 will be encoded in 192 | base 64 and wrapped with `base64_decode()` call. 193 | 194 | * **string.escape** : <boolean> (true)
195 | When set to `true`, all strings containing bytes outside the 32-126 ASCII 196 | range will be written with double quotes and the characters outside the 197 | range will be escaped. 198 | 199 | * **string.utf8** : <boolean> (false)
200 | When both this option and `string.escape` are set to `true`, all valid 201 | multibyte UTF-8 characters in strings are encoded using the PHP7 unicode 202 | code point syntax. Note that this syntax does not work in PHP versions 203 | earlier than 7.0. 204 | 205 | * **string.classes** : <string[]> ([])
206 | Defines a list of classes or class namespace prefixes for classes that 207 | can be encoded using the class resolution operator `::class` when 208 | encountered in strings. e.g. providing the value `['Riimu\\']` would encode 209 | all strings that look like class names in the `Riimu` namespace like 210 | `Riimu\Kit\PHPEncoder::class`. 211 | 212 | * **string.imports** : <string[]> ([])
213 | List of imports used in the resulting code file, which allows class names 214 | to be written using shorter format. The list should be an associative array 215 | with the namespace or class as the key and the used name as the value. Use 216 | empty string to indicate the current namespace. 217 | 218 | For example, if the resulting file would have `namespace Riimu\Kit\PHPEncoder;` 219 | and `use PHPUnit\Framework\TestCase;`, you could set up the array as 220 | `['Riimu\\Kit\\PHPEncoder\\' => '', 'PHPUnit\\Framework\\TestCase' => 'TestCase']` 221 | 222 | * **array.short** : <boolean> (true)
223 | When set to `true`, arrays are enclosed using square brackets `[]` instead 224 | using of the long array notation `array()`. 225 | 226 | * **array.base** : <integer|string> (0)
227 | Base indentation for arrays as a number of spaces or as a string. Provides 228 | convenience when you need to output code to a file with specific level of 229 | indentation. 230 | 231 | * **array.indent** : <integer|string> (4)
232 | Amount of indentation for single level of indentation as a number of spaces 233 | or a string. 234 | 235 | * **array.align** : <boolean> (false)
236 | When set to `true`, array assignment operators `=>` are aligned to the same 237 | column using spaces. Even if enabled, `array.omit` and `array.inline` 238 | options are still respected, but only if all the keys in the specific array 239 | can be omitted. 240 | 241 | * **array.inline** : <boolean|integer> (70)
242 | When set to `true`, any array that can be written without any array keys 243 | will be written in a single line. If an integer is provided instead, the 244 | array will be written as a single line only if it does not exceed that 245 | number of characters. This option has no effect when `array.omit` is set to 246 | false. 247 | 248 | * **array.omit** : <boolean> (true)
249 | When set to `true`, any redundant array keys will not be included (e.g. the 250 | array `[0 => 'a', 1 => 'b']` would be encoded just as `['a', 'b']`). 251 | 252 | * **array.eol** : <string|false> (false)
253 | The end of line character used by array output. When set to `false`, the 254 | default `PHP_EOL` will be used instead. 255 | 256 | * **object.method** : <boolean> (true)
257 | When set to `true`, any encoded object will be checked for methods `toPHP()` 258 | and `toPHPValue()`. If the method `toPHP()` exists, the returned string will 259 | be used as the PHP code representation of the object. If the method 260 | `toPHPValue()` exists instead, the returned value will be encoded as PHP and 261 | used as the code representation of the object. 262 | 263 | * **object.format** : <string> ('vars')
264 | Default object encoding format. The possible values are: 265 | 266 | * `string` casts the object to string and then encodes that string as PHP. 267 | * `serialize` serializes the object and wraps it with `unserialize()` 268 | * `export` mimics the `var_export()` object representation 269 | * `array` casts the object to an array and encodes that array 270 | * `vars` turns object into an array using `get_object_vars()` 271 | * `iterate` turns the object into an array by iterating over it with `foreach` 272 | 273 | * **object.cast** : <boolean> (true)
274 | Whether to add an `(object)` cast in front of arrays generated from objects 275 | or not when using the object encoding formats `vars`, `array` or `iterate`. 276 | 277 | * **recursion.detect** : <boolean> (true)
278 | When set to `true`, the encoder will attempt to detect circular references 279 | in arrays and objects to avoid infinite loops. 280 | 281 | * **recursion.ignore** : <boolean> (false)
282 | When set to `true`, any circular reference will be replaced with `null` 283 | instead of throwing an exception. 284 | 285 | * **recursion.max** : <integer|false> (false)
286 | Maximum number of levels when encoding arrays and objects. Exception is 287 | thrown when the maximum is exceeded. Set to `false` to have no limit. 288 | 289 | ## Credits ## 290 | 291 | This library is Copyright (c) 2013-2022 Riikka Kalliomäki. 292 | 293 | See LICENSE for license and copying information. 294 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riimu/kit-phpencoder", 3 | "description": "Highly customizable alternative to var_export for PHP code generation", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "variable", 8 | "export", 9 | "code", 10 | "generator", 11 | "encoder" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Riikka Kalliomäki", 16 | "email": "riikka.kalliomaki@gmail.com", 17 | "homepage": "http://riimu.net" 18 | } 19 | ], 20 | "homepage": "http://kit.riimu.net", 21 | "require": { 22 | "php": ">=5.6.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Riimu\\Kit\\PHPEncoder\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Riimu\\Kit\\PHPEncoder\\": "tests/tests/", 32 | "Riimu\\Kit\\PHPEncoder\\Test\\": "tests/extra/" 33 | } 34 | }, 35 | "scripts": { 36 | "ci-all": [ 37 | "@test --do-not-cache-result", 38 | "@phpcs --no-cache", 39 | "@php-cs-fixer --dry-run --diff --using-cache=no", 40 | "@composer normalize --dry-run" 41 | ], 42 | "php-cs-fixer": "php-cs-fixer fix -v", 43 | "phpcs": "phpcs -p", 44 | "test": "phpunit" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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": "208f756622b0ba50cbf5073611d343fd", 8 | "packages": [], 9 | "packages-dev": [], 10 | "aliases": [], 11 | "minimum-stability": "stable", 12 | "stability-flags": [], 13 | "prefer-stable": false, 14 | "prefer-lowest": false, 15 | "platform": { 16 | "php": ">=5.6.0" 17 | }, 18 | "platform-dev": [], 19 | "plugin-api-version": "2.3.0" 20 | } 21 | -------------------------------------------------------------------------------- /examples/class_resolution.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'Riimu\\', 11 | 'PHPUnit\\Framework\\TestCase', 12 | 'DateTime', 13 | ], 14 | 'string.imports' => [ 15 | 'Riimu\\Kit\\PHPEncoder\\' => '', 16 | 'PHPUnit\\Framework\\TestCase' => 'TestCase', 17 | ], 18 | ]); 19 | 20 | 21 | echo "encode([ 30 | \PHPUnit\Framework\TestCase::class, 31 | \Riimu\Kit\PHPEncoder\PHPEncoder::class, 32 | \Riimu\Kit\PHPEncoder\Encoder\Encoder::class, 33 | \DateTime::class, 34 | \DateTimeInterface::class, // Will be encoded as plain string, since it's not allowed by string.classes 35 | ]); 36 | 37 | echo ");\n"; 38 | -------------------------------------------------------------------------------- /examples/config.php: -------------------------------------------------------------------------------- 1 | '', 12 | 'database' => '', 13 | 'username' => '', 14 | 'password' => '', 15 | ]; 16 | 17 | if (isset($_POST['config'])) { 18 | $store = array_intersect_key($_POST['config'], $config); 19 | $encoder = new \Riimu\Kit\PHPEncoder\PHPEncoder(); 20 | 21 | file_put_contents($configFile, sprintf( 22 | 'encode(array_map('strval', $store)) 24 | )); 25 | } 26 | 27 | if (file_exists($configFile)) { 28 | $config = (require $configFile) + $config; 29 | } 30 | 31 | ?> 32 | 33 | 34 | 35 | 36 | Database configuration 37 | 38 | 39 |

Database configuration

40 | Configuration saved!

' . PHP_EOL; 44 | } 45 | 46 | ?> 47 |
48 | 49 | $value) { 52 | printf( 53 | ' ' . PHP_EOL, 54 | ucfirst($name), 55 | $name, 56 | htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8') 57 | ); 58 | } 59 | 60 | ?> 61 |
%s
62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /examples/writing.php: -------------------------------------------------------------------------------- 1 | encode(['foo' => 'bar', [1, true, false, null, 1.0]]); 10 | 11 | echo PHP_EOL; 12 | 13 | echo $encoder->encode(['foo' => 'bar', [1, true, false, null, 1.0]], [ 14 | 'whitespace' => false, 15 | ]); 16 | -------------------------------------------------------------------------------- /src/Encoder/ArrayEncoder.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2014-2020 Riikka Kalliomäki 9 | * @license http://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | class ArrayEncoder implements Encoder 12 | { 13 | /** @var array Default values for options in the encoder */ 14 | private static $defaultOptions = [ 15 | 'array.short' => true, 16 | 'array.base' => 0, 17 | 'array.indent' => 4, 18 | 'array.align' => false, 19 | 'array.inline' => 70, 20 | 'array.omit' => true, 21 | 'array.eol' => false, 22 | ]; 23 | 24 | public function getDefaultOptions() 25 | { 26 | return self::$defaultOptions; 27 | } 28 | 29 | public function supports($value) 30 | { 31 | return \is_array($value); 32 | } 33 | 34 | public function encode($value, $depth, array $options, callable $encode) 35 | { 36 | if ($value === []) { 37 | return $this->wrap('', $options['array.short']); 38 | } elseif (!$options['whitespace']) { 39 | return $this->wrap( 40 | implode(',', $this->getPairs($value, '', $options['array.omit'], $encode)), 41 | $options['array.short'] 42 | ); 43 | } elseif ($options['array.align']) { 44 | return $this->getAlignedArray($value, $depth, $options, $encode); 45 | } 46 | 47 | return $this->getFormattedArray($value, $depth, $options, $encode); 48 | } 49 | 50 | /** 51 | * Returns the PHP code for aligned array accounting for omitted keys and inline arrays. 52 | * @param array $array Array to encode 53 | * @param int $depth Current indentation depth of the output 54 | * @param array $options List of encoder options 55 | * @param callable $encode Callback used to encode values 56 | * @return string The PHP code representation for the array 57 | */ 58 | private function getAlignedArray(array $array, $depth, array $options, callable $encode) 59 | { 60 | $next = 0; 61 | $omit = $options['array.omit']; 62 | 63 | foreach (array_keys($array) as $key) { 64 | if ($key !== $next++) { 65 | $omit = false; 66 | break; 67 | } 68 | } 69 | 70 | return $omit 71 | ? $this->getFormattedArray($array, $depth, $options, $encode) 72 | : $this->buildArray($this->getAlignedPairs($array, $encode), $depth, $options); 73 | } 74 | 75 | /** 76 | * Returns the PHP code for the array as inline or multi line array. 77 | * @param array $array Array to encode 78 | * @param int $depth Current indentation depth of the output 79 | * @param array $options List of encoder options 80 | * @param callable $encode Callback used to encode values 81 | * @return string The PHP code representation for the array 82 | */ 83 | private function getFormattedArray(array $array, $depth, array $options, callable $encode) 84 | { 85 | $lines = $this->getPairs($array, ' ', $options['array.omit'], $encode, $omitted); 86 | 87 | if ($omitted && $options['array.inline'] !== false) { 88 | $output = $this->getInlineArray($lines, $options); 89 | 90 | if ($output !== false) { 91 | return $output; 92 | } 93 | } 94 | 95 | return $this->buildArray($lines, $depth, $options); 96 | } 97 | 98 | /** 99 | * Returns the code for the inline array, if possible. 100 | * @param string[] $lines Encoded key and value pairs 101 | * @param array $options List of encoder options 102 | * @return string|false Array encoded as single line of PHP code or false if not possible 103 | */ 104 | private function getInlineArray(array $lines, array $options) 105 | { 106 | $output = $this->wrap(implode(', ', $lines), $options['array.short']); 107 | 108 | if (preg_match('/[\r\n\t]/', $output)) { 109 | return false; 110 | } elseif ($options['array.inline'] === true || \strlen($output) <= (int) $options['array.inline']) { 111 | return $output; 112 | } 113 | 114 | return false; 115 | } 116 | 117 | /** 118 | * Builds the complete array from the encoded key and value pairs. 119 | * @param string[] $lines Encoded key and value pairs 120 | * @param int $depth Current indentation depth of the output 121 | * @param array $options List of encoder options 122 | * @return string Array encoded as PHP code 123 | */ 124 | private function buildArray(array $lines, $depth, array $options) 125 | { 126 | $indent = $this->buildIndent($options['array.base'], $options['array.indent'], $depth + 1); 127 | $last = $this->buildIndent($options['array.base'], $options['array.indent'], $depth); 128 | $eol = $options['array.eol'] === false ? \PHP_EOL : (string) $options['array.eol']; 129 | 130 | return $this->wrap( 131 | sprintf('%s%s%s,%1$s%s', $eol, $indent, implode(',' . $eol . $indent, $lines), $last), 132 | $options['array.short'] 133 | ); 134 | } 135 | 136 | /** 137 | * Wraps the array code using short or long array notation. 138 | * @param string $string Array string representation to wrap 139 | * @param bool $short True to use short notation, false to use long notation 140 | * @return string The array wrapped appropriately 141 | */ 142 | private function wrap($string, $short) 143 | { 144 | return sprintf($short ? '[%s]' : 'array(%s)', $string); 145 | } 146 | 147 | /** 148 | * Builds the indentation based on the options. 149 | * @param string|int $base The base indentation 150 | * @param string|int $indent A single indentation level 151 | * @param int $depth The level of indentation 152 | * @return string The indentation for the current depth 153 | */ 154 | private function buildIndent($base, $indent, $depth) 155 | { 156 | $base = \is_int($base) ? str_repeat(' ', $base) : (string) $base; 157 | 158 | return $depth === 0 ? $base : $base . str_repeat( 159 | \is_int($indent) ? str_repeat(' ', $indent) : (string) $indent, 160 | $depth 161 | ); 162 | } 163 | 164 | /** 165 | * Returns each encoded key and value pair with aligned assignment operators. 166 | * @param array $array Array to convert into code 167 | * @param callable $encode Callback used to encode values 168 | * @return string[] Each of key and value pair encoded as php 169 | */ 170 | private function getAlignedPairs(array $array, callable $encode) 171 | { 172 | $keys = []; 173 | $values = []; 174 | 175 | foreach ($array as $key => $value) { 176 | $keys[] = $encode($key, 1); 177 | $values[] = $encode($value, 1); 178 | } 179 | 180 | $format = sprintf('%%-%ds => %%s', max(array_map('strlen', $keys))); 181 | $pairs = []; 182 | 183 | for ($i = 0, $count = \count($keys); $i < $count; $i++) { 184 | $pairs[] = sprintf($format, $keys[$i], $values[$i]); 185 | } 186 | 187 | return $pairs; 188 | } 189 | 190 | /** 191 | * Returns each key and value pair encoded as array assignment. 192 | * @param array $array Array to convert into code 193 | * @param string $space Whitespace between array assignment operator 194 | * @param bool $omit True to omit unnecessary keys, false to not 195 | * @param callable $encode Callback used to encode values 196 | * @param bool $omitted Set to true, if all the keys were omitted, false otherwise 197 | * @return string[] Each of key and value pair encoded as php 198 | */ 199 | private function getPairs(array $array, $space, $omit, callable $encode, &$omitted = true) 200 | { 201 | $pairs = []; 202 | $nextIndex = 0; 203 | $omitted = true; 204 | $format = '%s' . $space . '=>' . $space . '%s'; 205 | 206 | foreach ($array as $key => $value) { 207 | if ($omit && $this->canOmitKey($key, $nextIndex)) { 208 | $pairs[] = $encode($value, 1); 209 | } else { 210 | $pairs[] = sprintf($format, $encode($key, 1), $encode($value, 1)); 211 | $omitted = false; 212 | } 213 | } 214 | 215 | return $pairs; 216 | } 217 | 218 | /** 219 | * Tells if the key can be omitted from array output based on expected index. 220 | * @param int|string $key Current array key 221 | * @param int $nextIndex Next expected key that can be omitted 222 | * @return bool True if the key can be omitted, false if not 223 | */ 224 | private function canOmitKey($key, &$nextIndex) 225 | { 226 | $result = $key === $nextIndex; 227 | 228 | if (\is_int($key)) { 229 | $nextIndex = max($key + 1, $nextIndex); 230 | } 231 | 232 | return $result; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/Encoder/BooleanEncoder.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2014-2020 Riikka Kalliomäki 9 | * @license http://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | class BooleanEncoder implements Encoder 12 | { 13 | /** @var array Default values for options in the encoder */ 14 | private static $defaultOptions = [ 15 | 'boolean.capitalize' => false, 16 | ]; 17 | 18 | public function getDefaultOptions() 19 | { 20 | return self::$defaultOptions; 21 | } 22 | 23 | public function supports($value) 24 | { 25 | return \is_bool($value); 26 | } 27 | 28 | public function encode($value, $depth, array $options, callable $encode) 29 | { 30 | if ($options['boolean.capitalize']) { 31 | return $value ? 'TRUE' : 'FALSE'; 32 | } 33 | 34 | return $value ? 'true' : 'false'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Encoder/Encoder.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2014-2020 Riikka Kalliomäki 9 | * @license http://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | interface Encoder 12 | { 13 | /** 14 | * Returns a list of options and their default values as an associative array. 15 | * @return array List of options and their default values 16 | */ 17 | public function getDefaultOptions(); 18 | 19 | /** 20 | * Tells if the encoder supports encoding the given value. 21 | * @param mixed $value Value to test 22 | * @return bool True if the value can be encoded, false otherwise 23 | */ 24 | public function supports($value); 25 | 26 | /** 27 | * Generates the PHP code representation for the given value. 28 | * @param mixed $value Value to encode 29 | * @param int $depth Current indentation depth of the output 30 | * @param array $options List of encoder options 31 | * @param callable $encode Callback used to encode values 32 | * @return string The PHP code that represents the given value 33 | */ 34 | public function encode($value, $depth, array $options, callable $encode); 35 | } 36 | -------------------------------------------------------------------------------- /src/Encoder/FloatEncoder.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2014-2020 Riikka Kalliomäki 9 | * @license http://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | class FloatEncoder implements Encoder 12 | { 13 | /** The maximum value that can be accurately represented by a float */ 14 | const FLOAT_MAX = 9007199254740992.0; 15 | 16 | /** @var array Default values for options in the encoder */ 17 | private static $defaultOptions = [ 18 | 'float.integers' => false, 19 | 'float.precision' => 17, 20 | 'float.export' => false, 21 | ]; 22 | 23 | public function getDefaultOptions() 24 | { 25 | return self::$defaultOptions; 26 | } 27 | 28 | public function supports($value) 29 | { 30 | return \is_float($value); 31 | } 32 | 33 | public function encode($value, $depth, array $options, callable $encode) 34 | { 35 | if (is_nan($value)) { 36 | return 'NAN'; 37 | } elseif (is_infinite($value)) { 38 | return $value < 0 ? '-INF' : 'INF'; 39 | } 40 | 41 | return $this->encodeNumber($value, $options, $encode); 42 | } 43 | 44 | /** 45 | * Encodes the number as a PHP number representation. 46 | * @param float $float The number to encode 47 | * @param array $options The float encoding options 48 | * @param callable $encode Callback used to encode values 49 | * @return string The PHP code representation for the number 50 | */ 51 | private function encodeNumber($float, array $options, callable $encode) 52 | { 53 | if ($this->isInteger($float, $options['float.integers'])) { 54 | return $this->encodeInteger($float, $encode); 55 | } elseif ($float === 0.0) { 56 | return '0.0'; 57 | } elseif ($options['float.export']) { 58 | return var_export((float) $float, true); 59 | } 60 | 61 | return $this->encodeFloat($float, $this->determinePrecision($options)); 62 | } 63 | 64 | /** 65 | * Tells if the number can be encoded as an integer. 66 | * @param float $float The number to test 67 | * @param bool|string $allowIntegers Whether integers should be allowed 68 | * @return bool True if the number can be encoded as an integer, false if not 69 | */ 70 | private function isInteger($float, $allowIntegers) 71 | { 72 | if (!$allowIntegers || round($float) !== $float) { 73 | return false; 74 | } elseif (abs($float) < self::FLOAT_MAX) { 75 | return true; 76 | } 77 | 78 | return $allowIntegers === 'all'; 79 | } 80 | 81 | /** 82 | * Encodes the given float as an integer. 83 | * @param float $float The number to encode 84 | * @param callable $encode Callback used to encode values 85 | * @return string The PHP code representation for the number 86 | */ 87 | private function encodeInteger($float, callable $encode) 88 | { 89 | $minimum = \defined('PHP_INT_MIN') ? \PHP_INT_MIN : ~\PHP_INT_MAX; 90 | 91 | if ($float >= $minimum && $float <= \PHP_INT_MAX) { 92 | return $encode((int) $float); 93 | } 94 | 95 | return number_format($float, 0, '.', ''); 96 | } 97 | 98 | /** 99 | * Determines the float precision based on the options. 100 | * @param array $options The float encoding options 101 | * @return int The precision used to encode floats 102 | */ 103 | private function determinePrecision($options) 104 | { 105 | $precision = $options['float.precision']; 106 | 107 | if ($precision === false) { 108 | $precision = ini_get('serialize_precision'); 109 | } 110 | 111 | return max(1, (int) $precision); 112 | } 113 | 114 | /** 115 | * Encodes the number using a floating point representation. 116 | * @param float $float The number to encode 117 | * @param int $precision The maximum precision of encoded floats 118 | * @return string The PHP code representation for the number 119 | */ 120 | private function encodeFloat($float, $precision) 121 | { 122 | $log = (int) floor(log(abs($float), 10)); 123 | 124 | if ($log > -5 && abs($float) < self::FLOAT_MAX && abs($log) < $precision) { 125 | return $this->formatFloat($float, $precision - $log - 1); 126 | } 127 | 128 | // Deal with overflow that results from rounding 129 | $log += (int) (round(abs($float) / 10 ** $log, $precision - 1) / 10); 130 | $string = $this->formatFloat($float / 10 ** $log, $precision - 1); 131 | 132 | return sprintf('%sE%+d', $string, $log); 133 | } 134 | 135 | /** 136 | * Formats the number as a decimal number. 137 | * @param float $float The number to format 138 | * @param int $digits The maximum number of decimal digits 139 | * @return string The number formatted as a decimal number 140 | */ 141 | private function formatFloat($float, $digits) 142 | { 143 | $digits = max((int) $digits, 1); 144 | $string = rtrim(number_format($float, $digits, '.', ''), '0'); 145 | 146 | return substr($string, -1) === '.' ? $string . '0' : $string; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Encoder/GMPEncoder.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2014-2020 Riikka Kalliomäki 9 | * @license http://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | class GMPEncoder implements Encoder 12 | { 13 | public function getDefaultOptions() 14 | { 15 | return []; 16 | } 17 | 18 | public function supports($value) 19 | { 20 | return \is_object($value) && \get_class($value) === \GMP::class; 21 | } 22 | 23 | public function encode($value, $depth, array $options, callable $encode) 24 | { 25 | return sprintf('gmp_init(%s)', $encode(gmp_strval($value))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Encoder/IntegerEncoder.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2014-2020 Riikka Kalliomäki 9 | * @license http://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | class IntegerEncoder implements Encoder 12 | { 13 | /** @var array Default values for options in the encoder */ 14 | private static $defaultOptions = [ 15 | 'integer.type' => 'decimal', 16 | ]; 17 | 18 | /** @var \Closure[] Encoders for different types of integers */ 19 | private $encoders; 20 | 21 | /** 22 | * IntegerEncoder constructor. 23 | */ 24 | public function __construct() 25 | { 26 | $this->encoders = [ 27 | 'binary' => function ($value) { 28 | return $this->encodeBinary($value); 29 | }, 30 | 'octal' => function ($value) { 31 | return $this->encodeOctal($value); 32 | }, 33 | 'decimal' => function ($value, $options) { 34 | return $this->encodeDecimal($value, $options); 35 | }, 36 | 'hexadecimal' => function ($value, $options) { 37 | return $this->encodeHexadecimal($value, $options); 38 | }, 39 | ]; 40 | } 41 | 42 | public function getDefaultOptions() 43 | { 44 | return self::$defaultOptions; 45 | } 46 | 47 | public function supports($value) 48 | { 49 | return \is_int($value); 50 | } 51 | 52 | public function encode($value, $depth, array $options, callable $encode) 53 | { 54 | if (!isset($this->encoders[$options['integer.type']])) { 55 | throw new \InvalidArgumentException('Invalid integer encoding type'); 56 | } 57 | 58 | $callback = $this->encoders[$options['integer.type']]; 59 | 60 | return $callback((int) $value, $options); 61 | } 62 | 63 | /** 64 | * Encodes an integer into binary representation. 65 | * @param int $integer The integer to encode 66 | * @return string The PHP code representation for the integer 67 | */ 68 | public function encodeBinary($integer) 69 | { 70 | return sprintf('%s0b%b', $this->sign($integer), abs($integer)); 71 | } 72 | 73 | /** 74 | * Encodes an integer into octal representation. 75 | * @param int $integer The integer to encode 76 | * @return string The PHP code representation for the integer 77 | */ 78 | public function encodeOctal($integer) 79 | { 80 | return sprintf('%s0%o', $this->sign($integer), abs($integer)); 81 | } 82 | 83 | /** 84 | * Encodes an integer into decimal representation. 85 | * @param int $integer The integer to encode 86 | * @param array $options The integer encoding options 87 | * @return string The PHP code representation for the integer 88 | */ 89 | public function encodeDecimal($integer, $options) 90 | { 91 | if ($integer === 1 << (\PHP_INT_SIZE * 8 - 1)) { 92 | return sprintf('(int)%s%d', $options['whitespace'] ? ' ' : '', $integer); 93 | } 94 | 95 | return var_export($integer, true); 96 | } 97 | 98 | /** 99 | * Encodes an integer into hexadecimal representation. 100 | * @param int $integer The integer to encode 101 | * @param array $options The integer encoding options 102 | * @return string The PHP code representation for the integer 103 | */ 104 | public function encodeHexadecimal($integer, $options) 105 | { 106 | if ($options['hex.capitalize']) { 107 | return sprintf('%s0x%X', $this->sign($integer), abs($integer)); 108 | } 109 | 110 | return sprintf('%s0x%x', $this->sign($integer), abs($integer)); 111 | } 112 | 113 | /** 114 | * Returns the negative sign for negative numbers. 115 | * @param int $integer The number to test for negativity 116 | * @return string The minus sign for negative numbers and empty string for positive numbers 117 | */ 118 | private function sign($integer) 119 | { 120 | if ($integer < 0) { 121 | return '-'; 122 | } 123 | 124 | return ''; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Encoder/NullEncoder.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2014-2020 Riikka Kalliomäki 9 | * @license http://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | class NullEncoder implements Encoder 12 | { 13 | /** @var array Default values for options in the encoder */ 14 | private static $defaultOptions = [ 15 | 'null.capitalize' => false, 16 | ]; 17 | 18 | public function getDefaultOptions() 19 | { 20 | return self::$defaultOptions; 21 | } 22 | 23 | public function supports($value) 24 | { 25 | return $value === null; 26 | } 27 | 28 | public function encode($value, $depth, array $options, callable $encode) 29 | { 30 | return $options['null.capitalize'] ? 'NULL' : 'null'; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Encoder/ObjectEncoder.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2014-2020 Riikka Kalliomäki 9 | * @license http://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | class ObjectEncoder implements Encoder 12 | { 13 | /** @var array Default values for options in the encoder */ 14 | private static $defaultOptions = [ 15 | 'object.method' => true, 16 | 'object.format' => 'vars', 17 | 'object.cast' => true, 18 | ]; 19 | 20 | public function getDefaultOptions() 21 | { 22 | return self::$defaultOptions; 23 | } 24 | 25 | public function supports($value) 26 | { 27 | return \is_object($value); 28 | } 29 | 30 | public function encode($value, $depth, array $options, callable $encode) 31 | { 32 | if ($options['object.method']) { 33 | if (method_exists($value, 'toPHP')) { 34 | return (string) $value->toPHP(); 35 | } elseif (method_exists($value, 'toPHPValue')) { 36 | return $encode($value->toPHPValue()); 37 | } 38 | } 39 | 40 | return $this->encodeObject($value, $options, $encode); 41 | } 42 | 43 | /** 44 | * Encodes the object as string according to encoding options. 45 | * @param object $object Object to encode as PHP 46 | * @param array $options List of encoder options 47 | * @param callable $encode Callback used to encode values 48 | * @return string The object encoded as string 49 | */ 50 | private function encodeObject($object, array $options, callable $encode) 51 | { 52 | if ($options['object.format'] === 'string') { 53 | return $encode((string) $object); 54 | } elseif ($options['object.format'] === 'serialize') { 55 | return sprintf('unserialize(%s)', $encode(serialize($object))); 56 | } elseif ($options['object.format'] === 'export') { 57 | return sprintf('\\%s::__set_state(%s)', \get_class($object), $encode($this->getObjectState($object))); 58 | } 59 | 60 | return $this->encodeObjectArray($object, $options, $encode); 61 | } 62 | 63 | /** 64 | * Encodes the object into one of the array formats. 65 | * @param object $object Object to encode as PHP 66 | * @param array $options List of encoder options 67 | * @param callable $encode Callback used to encode values 68 | * @return string The object encoded as string 69 | * @throws \RuntimeException If the object format is invalid 70 | */ 71 | private function encodeObjectArray($object, array $options, callable $encode) 72 | { 73 | if (!\in_array((string) $options['object.format'], ['array', 'vars', 'iterate'], true)) { 74 | throw new \RuntimeException('Invalid object encoding format: ' . $options['object.format']); 75 | } 76 | 77 | $output = $encode($this->getObjectArray($object, $options['object.format'])); 78 | 79 | if ($options['object.cast']) { 80 | $output = '(object)' . ($options['whitespace'] ? ' ' : '') . $output; 81 | } 82 | 83 | return $output; 84 | } 85 | 86 | /** 87 | * Converts the object into array that can be encoded. 88 | * @param object $object Object to convert to an array 89 | * @param string $format Object conversion format 90 | * @return array The object converted into an array 91 | * @throws \RuntimeException If object conversion format is invalid 92 | */ 93 | private function getObjectArray($object, $format) 94 | { 95 | if ($format === 'array') { 96 | return (array) $object; 97 | } elseif ($format === 'vars') { 98 | return get_object_vars($object); 99 | } 100 | 101 | $array = []; 102 | 103 | foreach ($object as $key => $value) { 104 | $array[$key] = $value; 105 | } 106 | 107 | return $array; 108 | } 109 | 110 | /** 111 | * Returns an array of object properties as would be generated by var_export. 112 | * @param object $object Object to turn into array 113 | * @return array Properties of the object as passed to var_export 114 | */ 115 | private function getObjectState($object) 116 | { 117 | $class = new \ReflectionClass($object); 118 | $visibility = \ReflectionProperty::IS_PRIVATE 119 | | \ReflectionProperty::IS_PROTECTED 120 | | \ReflectionProperty::IS_PUBLIC; 121 | $values = []; 122 | 123 | do { 124 | foreach ($class->getProperties($visibility) as $property) { 125 | $property->setAccessible(true); 126 | $values[$property->getName()] = $property->getValue($object); 127 | } 128 | 129 | $class = $class->getParentClass(); 130 | $visibility = \ReflectionProperty::IS_PRIVATE; 131 | } while ($class !== false); 132 | 133 | // Use get_object_vars to include dynamic properties 134 | return $values + get_object_vars($object); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Encoder/StringEncoder.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2014-2020 Riikka Kalliomäki 9 | * @license http://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | class StringEncoder implements Encoder 12 | { 13 | /** @var array Default values for options in the encoder */ 14 | private static $defaultOptions = [ 15 | 'string.escape' => true, 16 | 'string.binary' => false, 17 | 'string.utf8' => false, 18 | 'string.classes' => [], 19 | 'string.imports' => [], 20 | ]; 21 | 22 | public function getDefaultOptions() 23 | { 24 | return self::$defaultOptions; 25 | } 26 | 27 | public function supports($value) 28 | { 29 | return \is_string($value); 30 | } 31 | 32 | public function encode($value, $depth, array $options, callable $encode) 33 | { 34 | $value = (string) $value; 35 | 36 | if ($this->isClassName($value, $options)) { 37 | return $this->getClassName($value, $options); 38 | } 39 | 40 | if (preg_match('/[^\x20-\x7E]/', $value)) { 41 | return $this->getComplexString($value, $options); 42 | } 43 | 44 | return $this->getSingleQuotedString($value); 45 | } 46 | 47 | /** 48 | * Tests if the given value is a string that could be encoded as a class name constant. 49 | * @param string $value The string to test 50 | * @param array $options The string encoding options 51 | * @return bool True if string can be encoded as class constant, false if not 52 | */ 53 | private function isClassName($value, array $options) 54 | { 55 | if (preg_match('/^([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(\\\\(?1))*$/', $value) !== 1) { 56 | return false; 57 | } 58 | 59 | return array_intersect(iterator_to_array($this->iterateNamespaces($value)), $options['string.classes']) !== []; 60 | } 61 | 62 | /** 63 | * Encodes the given string as a class name constant based on used imports. 64 | * @param string $value The string to encode 65 | * @param array $options The string encoding options 66 | * @return string The class constant PHP code representation 67 | */ 68 | private function getClassName($value, array $options) 69 | { 70 | foreach ($this->iterateNamespaces($value) as $partial) { 71 | if (isset($options['string.imports'][$partial])) { 72 | $trimmed = substr($value, \strlen(rtrim($partial, '\\'))); 73 | return ltrim(sprintf('%s%s::class', rtrim($options['string.imports'][$partial], '\\'), $trimmed), '\\'); 74 | } 75 | } 76 | 77 | return sprintf('\\%s::class', $value); 78 | } 79 | 80 | /** 81 | * Iterates over the variations of the namespace for the given class name. 82 | * @param string $value The class name to iterate over 83 | * @return \Generator|string[] The namespace parts of the string 84 | */ 85 | private function iterateNamespaces($value) 86 | { 87 | yield $value; 88 | 89 | $parts = explode('\\', '\\' . $value); 90 | $count = \count($parts); 91 | 92 | for ($i = 1; $i < $count; $i++) { 93 | yield ltrim(implode('\\', \array_slice($parts, 0, -$i)), '\\') . '\\'; 94 | } 95 | } 96 | 97 | /** 98 | * Returns the PHP code representation for the string that is not just simple ascii characters. 99 | * @param string $value The string to encode 100 | * @param array $options The string encoding options 101 | * @return string The PHP code representation for the complex string 102 | */ 103 | private function getComplexString($value, array $options) 104 | { 105 | if ($this->isBinaryString($value, $options)) { 106 | return $this->encodeBinaryString($value); 107 | } 108 | 109 | if ($options['string.escape']) { 110 | return $this->getDoubleQuotedString($value, $options); 111 | } 112 | 113 | return $this->getSingleQuotedString($value); 114 | } 115 | 116 | /** 117 | * Tells if the string is not a valid UTF-8 string. 118 | * @param string $string The string to test 119 | * @param array $options The string encoding options 120 | * @return bool True if the string is not valid UTF-8 and false if it is 121 | */ 122 | private function isBinaryString($string, $options) 123 | { 124 | if (!$options['string.binary']) { 125 | return false; 126 | } 127 | 128 | // UTF-8 validity test without mbstring extension 129 | $pattern = 130 | '/^(?> 131 | [\x00-\x7F]+ # ASCII 132 | | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte 133 | | \xE0[\xA0-\xBF][\x80-\xBF] # excluding over longs 134 | | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte 135 | | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates 136 | | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 137 | | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 138 | | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 139 | )*$/x'; 140 | 141 | return !preg_match($pattern, $string); 142 | } 143 | 144 | /** 145 | * Encodes the given string into base 64 encoded format. 146 | * @param string $string The string to encode 147 | * @return string A base 64 PHP code representation for the string 148 | */ 149 | private function encodeBinaryString($string) 150 | { 151 | return sprintf("base64_decode('%s')", base64_encode($string)); 152 | } 153 | 154 | /** 155 | * Returns the string wrapped in single quotes and escape appropriately. 156 | * @param string $string String to wrap 157 | * @return string The string wrapped in single quotes 158 | */ 159 | private function getSingleQuotedString($string) 160 | { 161 | return sprintf("'%s'", strtr($string, ["'" => "\\'", '\\' => '\\\\'])); 162 | } 163 | 164 | /** 165 | * Returns the string wrapped in double quotes and all but print characters escaped. 166 | * @param string $string String to wrap and escape 167 | * @param array $options The string encoding options 168 | * @return string The string wrapped in double quotes and escape correctly 169 | */ 170 | private function getDoubleQuotedString($string, $options) 171 | { 172 | $string = strtr($string, [ 173 | "\n" => '\n', 174 | "\r" => '\r', 175 | "\t" => '\t', 176 | '$' => '\$', 177 | '"' => '\"', 178 | '\\' => '\\\\', 179 | ]); 180 | 181 | if ($options['string.utf8']) { 182 | $string = $this->encodeUtf8($string, $options); 183 | } 184 | 185 | $hexFormat = function ($matches) use ($options) { 186 | return sprintf($options['hex.capitalize'] ? '\x%02X' : '\x%02x', \ord($matches[0])); 187 | }; 188 | 189 | return sprintf('"%s"', preg_replace_callback('/[^\x20-\x7E]/', $hexFormat, $string)); 190 | } 191 | 192 | /** 193 | * Encodes all multibyte UTF-8 characters into PHP7 string encoding. 194 | * @param string $string The string to encoder 195 | * @param array $options The string encoding options 196 | * @return string The string with all the multibyte characters encoded 197 | */ 198 | private function encodeUtf8($string, $options) 199 | { 200 | $pattern = 201 | '/ [\xC2-\xDF][\x80-\xBF] 202 | | \xE0[\xA0-\xBF][\x80-\xBF] 203 | | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} 204 | | \xED[\x80-\x9F][\x80-\xBF] 205 | | \xF0[\x90-\xBF][\x80-\xBF]{2} 206 | | [\xF1-\xF3][\x80-\xBF]{3} 207 | | \xF4[\x80-\x8F][\x80-\xBF]{2}/x'; 208 | 209 | return preg_replace_callback($pattern, function ($match) use ($options) { 210 | return sprintf($options['hex.capitalize'] ? '\u{%X}' : '\u{%x}', $this->getCodePoint($match[0])); 211 | }, $string); 212 | } 213 | 214 | /** 215 | * Returns the unicode code point for the given multibyte UTF-8 character. 216 | * @param string $bytes The multibyte character 217 | * @return int The code point for the multibyte character 218 | */ 219 | private function getCodePoint($bytes) 220 | { 221 | if (\strlen($bytes) === 2) { 222 | return ((\ord($bytes[0]) & 0b11111) << 6) 223 | | (\ord($bytes[1]) & 0b111111); 224 | } 225 | 226 | if (\strlen($bytes) === 3) { 227 | return ((\ord($bytes[0]) & 0b1111) << 12) 228 | | ((\ord($bytes[1]) & 0b111111) << 6) 229 | | (\ord($bytes[2]) & 0b111111); 230 | } 231 | 232 | return ((\ord($bytes[0]) & 0b111) << 18) 233 | | ((\ord($bytes[1]) & 0b111111) << 12) 234 | | ((\ord($bytes[2]) & 0b111111) << 6) 235 | | (\ord($bytes[3]) & 0b111111); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/InvalidOptionException.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2015-2020 Riikka Kalliomäki 9 | * @license http://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | class InvalidOptionException extends \InvalidArgumentException 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/PHPEncoder.php: -------------------------------------------------------------------------------- 1 | 14 | * @copyright Copyright (c) 2014-2020 Riikka Kalliomäki 15 | * @license http://opensource.org/licenses/mit-license.php MIT License 16 | */ 17 | class PHPEncoder 18 | { 19 | /** @var Encoder\Encoder[] List of value encoders */ 20 | private $encoders; 21 | 22 | /** @var array List of defined encoder option values. */ 23 | private $options; 24 | 25 | /** @var array Default values for options in the encoder */ 26 | private static $defaultOptions = [ 27 | 'whitespace' => true, 28 | 'recursion.detect' => true, 29 | 'recursion.ignore' => false, 30 | 'recursion.max' => false, 31 | 'hex.capitalize' => false, 32 | ]; 33 | 34 | /** 35 | * Creates a new PHPEncoder instance. 36 | * 37 | * The constructor allows you to provide the list of default encoding options 38 | * used by the encoder. Note that if you are using custom value encoders, you 39 | * must provide them in the constructor if you are providing options for them 40 | * or otherwise the options will be considered invalid. 41 | * 42 | * Using the second parameter you can also provide a list of value encoders used 43 | * by the encoder. If null is provided, the list of default value encoders will 44 | * be used instead. 45 | * 46 | * @param array $options List of encoder options 47 | * @param Encoder\Encoder[]|null $encoders List of encoders to use or null for defaults 48 | * @throws InvalidOptionException If any of the encoder options are invalid 49 | */ 50 | public function __construct(array $options = [], array $encoders = null) 51 | { 52 | $this->options = self::$defaultOptions; 53 | 54 | if ($encoders === null) { 55 | $this->encoders = [ 56 | new Encoder\NullEncoder(), 57 | new Encoder\BooleanEncoder(), 58 | new Encoder\IntegerEncoder(), 59 | new Encoder\FloatEncoder(), 60 | new Encoder\StringEncoder(), 61 | new Encoder\ArrayEncoder(), 62 | new Encoder\GMPEncoder(), 63 | new Encoder\ObjectEncoder(), 64 | ]; 65 | } else { 66 | $this->encoders = []; 67 | array_map([$this, 'addEncoder'], $encoders); 68 | } 69 | 70 | array_map([$this, 'setOption'], array_keys($options), $options); 71 | } 72 | 73 | /** 74 | * Adds a new encoder. 75 | * 76 | * Values are always encoded by the first encoder that supports encoding 77 | * that type of value. By setting the second optional parameter to true, 78 | * you can prepend the encoder to the list to ensure that it will be tested 79 | * first. 80 | * 81 | * @param Encoder\Encoder $encoder Encoder for encoding values 82 | * @param bool $prepend True to prepend the encoder to the list, false to add it as last 83 | */ 84 | public function addEncoder(Encoder\Encoder $encoder, $prepend = false) 85 | { 86 | $prepend ? array_unshift($this->encoders, $encoder) : array_push($this->encoders, $encoder); 87 | } 88 | 89 | /** 90 | * Sets the value for an encoder option. 91 | * @param string $option Name of the option 92 | * @param mixed $value Value for the option 93 | * @throws InvalidOptionException If the provided encoder option is invalid 94 | */ 95 | public function setOption($option, $value) 96 | { 97 | if (!$this->isValidOption($option)) { 98 | throw new InvalidOptionException(sprintf("Invalid encoder option '%s'", $option)); 99 | } 100 | 101 | $this->options[$option] = $value; 102 | } 103 | 104 | /** 105 | * Tells if the given string is a valid option name. 106 | * @param string $option Option name to validate 107 | * @return bool True if the name is a valid option name, false if not 108 | */ 109 | private function isValidOption($option) 110 | { 111 | if (\array_key_exists($option, $this->options)) { 112 | return true; 113 | } 114 | 115 | foreach ($this->encoders as $encoder) { 116 | if (\array_key_exists($option, $encoder->getDefaultOptions())) { 117 | return true; 118 | } 119 | } 120 | 121 | return false; 122 | } 123 | 124 | /** 125 | * Generates the PHP code for the given value. 126 | * @param mixed $variable Value to encode as PHP 127 | * @param array $options List of encoder options 128 | * @return string The PHP code that represents the given value 129 | * @throws InvalidOptionException If any of the encoder options are invalid 130 | * @throws \InvalidArgumentException If the provided value contains an unsupported value type 131 | * @throws \RuntimeException If max depth is reached or a recursive value is detected 132 | */ 133 | public function encode($variable, array $options = []) 134 | { 135 | return $this->generate($variable, 0, $this->getAllOptions($options)); 136 | } 137 | 138 | /** 139 | * Returns a list of all encoder options. 140 | * @param array $overrides Options to override in the returned array 141 | * @return array List of encoder options 142 | * @throws InvalidOptionException If any of the encoder option overrides are invalid 143 | */ 144 | public function getAllOptions(array $overrides = []) 145 | { 146 | $options = $this->options; 147 | 148 | foreach ($this->encoders as $encoder) { 149 | $options += $encoder->getDefaultOptions(); 150 | } 151 | 152 | foreach ($overrides as $name => $value) { 153 | if (!\array_key_exists($name, $options)) { 154 | throw new InvalidOptionException(sprintf("Invalid encoder option '%s'", $name)); 155 | } 156 | 157 | $options[$name] = $value; 158 | } 159 | 160 | ksort($options); 161 | 162 | return $options; 163 | } 164 | 165 | /** 166 | * Generates the code for the given value recursively. 167 | * @param mixed $value Value to encode 168 | * @param int $depth Current indentation depth of the output 169 | * @param array $options List of encoder options 170 | * @param array $recursion Previously encoded values for recursion detection 171 | * @return string The PHP code that represents the given value 172 | * @throws \RuntimeException If max depth is reached or a recursive value is detected 173 | */ 174 | private function generate($value, $depth, array $options, array $recursion = []) 175 | { 176 | if ($this->detectRecursion($value, $options, $recursion)) { 177 | $recursion[] = $value; 178 | } 179 | 180 | if ($options['recursion.max'] !== false && $depth > (int) $options['recursion.max']) { 181 | throw new \RuntimeException('Maximum encoding depth reached'); 182 | } 183 | 184 | $callback = function ($value, $level = 0, array $overrides = []) use ($depth, $options, $recursion) { 185 | return $this->generate($value, $depth + (int) $level, $overrides + $options, $recursion); 186 | }; 187 | 188 | return $this->encodeValue($value, $depth, $options, $callback); 189 | } 190 | 191 | /** 192 | * Attempts to detect circular references in values. 193 | * @param mixed $value Value to try for circular reference 194 | * @param array $options List of encoder options 195 | * @param array $recursion Upper values in the encoding tree 196 | * @return bool True if values should be recorded, false if not 197 | * @throws \RuntimeException If a recursive value is detected 198 | */ 199 | private function detectRecursion(&$value, array $options, array $recursion) 200 | { 201 | if ($options['recursion.detect']) { 202 | if (array_search($value, $recursion, true) !== false) { 203 | if ($options['recursion.ignore']) { 204 | $value = null; 205 | } else { 206 | throw new \RuntimeException('A recursive value was detected'); 207 | } 208 | } 209 | 210 | return true; 211 | } 212 | 213 | return false; 214 | } 215 | 216 | /** 217 | * Encodes the value using one of the encoders that supports the value type. 218 | * @param mixed $value Value to encode 219 | * @param int $depth Current indentation depth of the output 220 | * @param array $options List of encoder options 221 | * @param callable $encode Callback used to encode values 222 | * @return string The PHP code that represents the given value 223 | * @throws \InvalidArgumentException If the provided value contains an unsupported value type 224 | */ 225 | private function encodeValue($value, $depth, array $options, callable $encode) 226 | { 227 | foreach ($this->encoders as $encoder) { 228 | if ($encoder->supports($value)) { 229 | return $encoder->encode($value, $depth, $options, $encode); 230 | } 231 | } 232 | 233 | throw new \InvalidArgumentException(sprintf("Unsupported value type '%s'", \gettype($value))); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/autoload.php: -------------------------------------------------------------------------------- 1 | \DIRECTORY_SEPARATOR]) . '.php'; 9 | 10 | if (file_exists($path)) { 11 | require $path; 12 | } 13 | } 14 | }); 15 | --------------------------------------------------------------------------------