├── 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 | [](https://github.com/Riimu/Kit-PHPEncoder/actions)
24 | [](https://scrutinizer-ci.com/g/Riimu/Kit-PHPEncoder/)
25 | [](https://codecov.io/gh/Riimu/Kit-PHPEncoder)
26 | [](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 |
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 |
--------------------------------------------------------------------------------