Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
ValueFormatter
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
3 / 3
25
100.00% covered (success)
100.00%
1 / 1
 format
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
18
 formatArray
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 formatArrayCompact
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Code\Generator\Support;
16
17use Pop\Code\Generator\Exception;
18use Pop\Code\Generator\Literal;
19
20/**
21 * Value formatter class
22 *
23 * Turns a real PHP value into its PHP literal source form. Shared by PropertyGenerator,
24 * ConstantGenerator, and FunctionTrait's argument-default formatting so all three format values the
25 * same way instead of each carrying its own (previously divergent) copy of this logic.
26 *
27 * @category   Pop
28 * @package    Pop\Code
29 * @author     Nick Sagona, III <dev@noladev.com>
30 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
31 * @license    https://www.popphp.org/license     New BSD License
32 * @version    6.0.0
33 */
34class ValueFormatter
35{
36
37    /**
38     * Format a value as PHP literal source (no trailing semicolon)
39     *
40     * @param  mixed   $value
41     * @param  ?string $type
42     * @param  string  $indent
43     * @param  bool    $compact  render an array value on a single line instead of the default
44     *                           multi-line bracket-literal form -- used for attribute arguments,
45     *                           which always render inline (even a top-level `#[...]` is
46     *                           conventionally one physical line) and would otherwise force a
47     *                           multi-line array literal into the middle of a parameter list
48     * @return string
49     */
50    public static function format(mixed $value, ?string $type = null, string $indent = '', bool $compact = false): string
51    {
52        if ($value === null) {
53            return 'null';
54        }
55
56        // A Literal wraps a raw PHP-source expression (e.g. 'self::FOO') that must be emitted
57        // verbatim rather than quoted/escaped -- must be checked before the object/Stringable
58        // check below, since Literal itself has no __toString() and would otherwise incorrectly
59        // hit that exception path.
60        if ($value instanceof Literal) {
61            return $value->getValue();
62        }
63
64        // Enum cases (e.g. a class constant like `const DEFAULT = self::Active;`) aren't
65        // Stringable, but they do have a well-defined literal form: <ShortClassName>::<CaseName>.
66        // Using the short name (not an FQCN) matches how the rest of this codebase renders
67        // in-namespace type references, and is always valid PHP when the value is a case of the
68        // enum being rendered, since that reference lands back in the same namespace.
69        if ($value instanceof \UnitEnum) {
70            return (new \ReflectionClass($value))->getShortName() . '::' . $value->name;
71        }
72
73        if (is_object($value) && !method_exists($value, '__toString')) {
74            throw new Exception('Error: Cannot format an object value of type ' . get_class($value) . '.');
75        }
76
77        $effectiveType = $type ?? strtolower(gettype($value));
78
79        // A union type string (e.g. 'int|string') doesn't match any of the single-type checks
80        // below, so it fell through to the catch-all string-quoting branch regardless of the
81        // value's actual type -- silently coercing e.g. an int|string default that's really an
82        // int into a quoted string literal. Pick the union member matching the value's actual
83        // PHP type, if present, so formatting proceeds as if that were the declared type.
84        if (str_contains($effectiveType, '|')) {
85            $actualType = strtolower(gettype($value));
86            $actualType = match ($actualType) {
87                'integer' => 'int',
88                'double'  => 'float',
89                'boolean' => 'bool',
90                default   => $actualType,
91            };
92            $members = explode('|', $effectiveType);
93            if (in_array($actualType, $members, true)) {
94                $effectiveType = $actualType;
95            }
96        }
97
98        if ($effectiveType === 'array') {
99            if (count($value) === 0) {
100                return '[]';
101            }
102            return $compact ? self::formatArrayCompact($value) : self::formatArray($value, $indent);
103        }
104
105        if (in_array($effectiveType, ['int', 'integer', 'float', 'double'], true)) {
106            return (string) $value;
107        }
108
109        if (in_array($effectiveType, ['bool', 'boolean'], true)) {
110            return $value ? 'true' : 'false';
111        }
112
113        return "'" . addcslashes((string) $value, "'\\") . "'";
114    }
115
116    /**
117     * Format an array value as PHP bracket-literal source
118     *
119     * @param  array  $value
120     * @param  string $indent
121     * @return string
122     */
123    protected static function formatArray(array $value, string $indent): string
124    {
125        $ary = str_replace(PHP_EOL, PHP_EOL . $indent . '  ', var_export($value, true));
126        $ary = str_replace('array (', '[', $ary);
127        $ary = str_replace('  )', ']', $ary);
128        $ary = str_replace('NULL', 'null', $ary);
129
130        $isAssoc = array_keys($value) !== range(0, count($value) - 1);
131
132        if (!$isAssoc) {
133            $ary = preg_replace('/^(\s*)\d+ => /m', '$1', $ary);
134        }
135
136        return $ary;
137    }
138
139    /**
140     * Format a non-empty array value as a single-line PHP bracket-literal source
141     *
142     * @param  array $value
143     * @return string
144     */
145    protected static function formatArrayCompact(array $value): string
146    {
147        $keys    = array_keys($value);
148        $isAssoc = false;
149
150        for ($i = 0; $i < count($keys); $i++) {
151            if ($keys[$i] != $i) {
152                $isAssoc = true;
153            }
154        }
155
156        $parts = [];
157        foreach ($value as $key => $item) {
158            $formattedItem = self::format($item, null, '', true);
159            $parts[]       = $isAssoc ? var_export($key, true) . ' => ' . $formattedItem : $formattedItem;
160        }
161
162        return '[' . implode(', ', $parts) . ']';
163    }
164
165}