diff --git a/formwork/Parsers/PHP.php b/formwork/Parsers/PHP.php new file mode 100644 index 00000000..e70e4de1 --- /dev/null +++ b/formwork/Parsers/PHP.php @@ -0,0 +1,123 @@ +<?php + +namespace Formwork\Parsers; + +use Formwork\Utils\Arr; +use Formwork\Utils\FileSystem; +use Formwork\Utils\Str; +use LogicException; +use UnexpectedValueException; + +class PHP extends AbstractEncoder +{ + /** + * Number of spaces used to indent arrays + * + * @var int + */ + protected const INDENT_SPACES = 4; + + /** + * Class names of objects which cannot be encoded + * + * @var array + */ + protected const UNENCODABLE_CLASSES = [\Closure::class, \Reflector::class, \ReflectionGenerator::class, \ReflectionType::class, \IteratorIterator::class, \RecursiveIteratorIterator::class]; + + /** + * @inheritdoc + */ + public static function parse(string $data, array $options = []): array + { + throw new LogicException('Parsing a string of PHP code is not allowed'); + } + + /** + * @inheritdoc + */ + public static function parseFile(string $file, array $options = []) + { + return include $file; + } + + /** + * @inheritdoc + */ + public static function encode(array $data, array $options = []): string + { + return static::encodeData($data); + } + + /** + * @inheritdoc + */ + public static function encodeToFile(array $data, string $file, array $options = []): bool + { + if (function_exists('opcache_invalidate') && ($options['invalidateOPcache'] ?? true)) { + // Invalidate OPcache when a file is encoded again + opcache_invalidate($file, true); + } + return FileSystem::write($file, "<?php\n\nreturn " . static::encodeData($data) . ";\n"); + } + + /** + * Encodes the given data like var_export() would do, but uses the short array syntax, avoids unneeded integer + * array keys, outputs lowercase null and serializes objects which don't implement the __set_state() method + */ + protected static function encodeData($data, int $indent = 0): string + { + switch (($type = gettype($data))) { + case 'array': + $parts = []; + + foreach ($data as $key => $value) { + $parts[] = str_repeat(' ', $indent + self::INDENT_SPACES) + . (Arr::isAssociative($data) ? static::encodeData($key) . ' => ' : '') + . static::encodeData($value, $indent + self::INDENT_SPACES); + } + + return '[' . Str::wrap(implode(",\n", $parts), "\n") . str_repeat(' ', $indent) . ']'; + + case 'boolean': + case 'double': + case 'integer': + case 'string': + return var_export($data, true); + + case 'NULL': + return 'null'; + + case 'object': + $class = get_class($data); + + // stdClass objects are encoded as object casts + if ($class === \stdClass::class) { + return '(object) ' . static::encodeData((array) $data, $indent); + } + + foreach (self::UNENCODABLE_CLASSES as $c) { + if ($data instanceof $c) { + throw new UnexpectedValueException('Objects of class "' . $class . '" cannot be encoded'); + } + } + + // Check if the class has a callable __set_state() magic method + if (is_callable([$data, '__set_state'])) { + $properties = []; + foreach ((array) $data as $property => $value) { + // Private and protected properties begin with the class name or an asterisk enclosed + // between two NUL bytes, so we need to skip that sequence + $properties[Str::afterLast($property, "\0")] = $value; + } + return '\\' . $class . '::__set_state(' . static::encodeData($properties, $indent) . ')'; + } + + // In the end we try to serialize the object + return 'unserialize(' . static::encodeData(serialize($data), $indent) . ')'; + + default: + // Resources and unknown types cannot be encoded + throw new UnexpectedValueException('Data of type "' . $type . '" cannot be encoded'); + } + } +}