| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* This file is part of the Nette Framework (https://nette.org) |
| 5 |
* Copyright (c) 2004 David Grudl (https://davidgrudl.com) |
| 6 |
*/ |
| 7 |
declare (strict_types=1); |
| 8 |
namespace Packetery\Nette\DI; |
| 9 |
|
| 10 |
use Packetery\Nette; |
| 11 |
use Packetery\Nette\DI\Definitions\Reference; |
| 12 |
use Packetery\Nette\DI\Definitions\Statement; |
| 13 |
use Packetery\Nette\Utils\Reflection; |
| 14 |
use Packetery\Nette\Utils\Type; |
| 15 |
/** |
| 16 |
* The DI helpers. |
| 17 |
* @internal |
| 18 |
*/ |
| 19 |
final class Helpers |
| 20 |
{ |
| 21 |
use \Packetery\Nette\StaticClass; |
| 22 |
/** |
| 23 |
* Expands %placeholders%. |
| 24 |
* @param mixed $var |
| 25 |
* @param bool|array $recursive |
| 26 |
* @return mixed |
| 27 |
* @throws \Packetery\Nette\InvalidArgumentException |
| 28 |
*/ |
| 29 |
public static function expand($var, array $params, $recursive = \false) |
| 30 |
{ |
| 31 |
if (\is_array($var)) { |
| 32 |
$res = []; |
| 33 |
foreach ($var as $key => $val) { |
| 34 |
$res[self::expand($key, $params, $recursive)] = self::expand($val, $params, $recursive); |
| 35 |
} |
| 36 |
return $res; |
| 37 |
} elseif ($var instanceof Statement) { |
| 38 |
return new Statement(self::expand($var->getEntity(), $params, $recursive), self::expand($var->arguments, $params, $recursive)); |
| 39 |
} elseif ($var === '%parameters%' && !\array_key_exists('parameters', $params)) { |
| 40 |
return $recursive ? self::expand($params, $params, $recursive) : $params; |
| 41 |
} elseif (\is_string($var)) { |
| 42 |
$recursive = \is_array($recursive) ? $recursive : ($recursive ? [] : null); |
| 43 |
return self::expandString($var, $params, $recursive); |
| 44 |
} else { |
| 45 |
return $var; |
| 46 |
} |
| 47 |
} |
| 48 |
/** |
| 49 |
* Expands %placeholders% in string |
| 50 |
* @throws \Packetery\Nette\InvalidArgumentException |
| 51 |
*/ |
| 52 |
private static function expandString(string $string, array $params, ?array $recursive, bool $onlyString = \false) |
| 53 |
{ |
| 54 |
$parts = \preg_split('#%([\\w.-]*)%#i', $string, -1, \PREG_SPLIT_DELIM_CAPTURE); |
| 55 |
$res = []; |
| 56 |
$dynamic = \false; |
| 57 |
foreach ($parts as $n => $part) { |
| 58 |
if ($n % 2 === 0) { |
| 59 |
$res[] = $part; |
| 60 |
} elseif ($part === '') { |
| 61 |
$res[] = '%'; |
| 62 |
} else { |
| 63 |
$res[] = $val = self::expandParameter($part, $params, $recursive, $onlyString); |
| 64 |
if (\strlen($part) + 2 === \strlen($string)) { |
| 65 |
return $val; |
| 66 |
} elseif ($val instanceof DynamicParameter || $val instanceof Statement) { |
| 67 |
$dynamic = \true; |
| 68 |
} elseif (!\is_scalar($val)) { |
| 69 |
throw new \Packetery\Nette\InvalidArgumentException(\sprintf("Unable to concatenate non-scalar parameter '%s' into '%s'.", $part, $string)); |
| 70 |
} |
| 71 |
} |
| 72 |
} |
| 73 |
return $dynamic ? new Statement('::implode', ['', $res]) : \implode('', $res); |
| 74 |
} |
| 75 |
private static function expandParameter(string $parameter, array $params, ?array $recursive, bool $onlyString) |
| 76 |
{ |
| 77 |
$val = $params; |
| 78 |
$path = []; |
| 79 |
$keys = \explode('.', $parameter); |
| 80 |
while (($key = $path[] = \array_shift($keys)) !== null) { |
| 81 |
if (\is_array($val) && \array_key_exists($key, $val)) { |
| 82 |
$val = $val[$key]; |
| 83 |
$fullExpand = !$onlyString && !$keys; |
| 84 |
// last |
| 85 |
if (\is_array($recursive) && ($fullExpand || \is_string($val))) { |
| 86 |
$pathStr = \implode('.', $path); |
| 87 |
if (isset($recursive[$pathStr])) { |
| 88 |
throw new \Packetery\Nette\InvalidArgumentException('Circular reference detected for parameters: %' . \implode('%, %', \array_keys($recursive)) . '%'); |
| 89 |
} |
| 90 |
$val = $fullExpand ? self::expand($val, $params, $recursive + [$pathStr => 1]) : self::expandString($val, $params, $recursive + [$pathStr => 1], \true); |
| 91 |
} |
| 92 |
} elseif ($val instanceof DynamicParameter) { |
| 93 |
$val = new DynamicParameter($val . '[' . \var_export($key, \true) . ']'); |
| 94 |
} elseif ($val instanceof Statement) { |
| 95 |
$val = new Statement('(?)[?]', [$val, $key]); |
| 96 |
} else { |
| 97 |
throw new \Packetery\Nette\InvalidArgumentException(\sprintf("Missing parameter '%s'.", $parameter)); |
| 98 |
} |
| 99 |
} |
| 100 |
return $val; |
| 101 |
} |
| 102 |
/** |
| 103 |
* Escapes '%' and '@' |
| 104 |
* @param mixed $value |
| 105 |
* @return mixed |
| 106 |
*/ |
| 107 |
public static function escape($value) |
| 108 |
{ |
| 109 |
if (\is_array($value)) { |
| 110 |
$res = []; |
| 111 |
foreach ($value as $key => $val) { |
| 112 |
$key = \is_string($key) ? \str_replace('%', '%%', $key) : $key; |
| 113 |
$res[$key] = self::escape($val); |
| 114 |
} |
| 115 |
return $res; |
| 116 |
} elseif (\is_string($value)) { |
| 117 |
return \preg_replace('#^@|%#', '$0$0', $value); |
| 118 |
} |
| 119 |
return $value; |
| 120 |
} |
| 121 |
/** |
| 122 |
* Process constants recursively. |
| 123 |
*/ |
| 124 |
public static function filterArguments(array $args) : array |
| 125 |
{ |
| 126 |
foreach ($args as $k => $v) { |
| 127 |
if (\PHP_VERSION_ID >= 80100 && \is_string($v) && \preg_match('#^([\\w\\\\]+)::\\w+$#D', $v, $m) && \enum_exists($m[1])) { |
| 128 |
$args[$k] = new \Packetery\Nette\PhpGenerator\PhpLiteral($v); |
| 129 |
} elseif (\is_string($v) && \preg_match('#^[\\w\\\\]*::[A-Z][a-zA-Z0-9_]*$#D', $v)) { |
| 130 |
$args[$k] = new \Packetery\Nette\PhpGenerator\PhpLiteral(\ltrim($v, ':')); |
| 131 |
} elseif (\is_string($v) && \preg_match('#^@[\\w\\\\]+$#D', $v)) { |
| 132 |
$args[$k] = new Reference(\substr($v, 1)); |
| 133 |
} elseif (\is_array($v)) { |
| 134 |
$args[$k] = self::filterArguments($v); |
| 135 |
} elseif ($v instanceof Statement) { |
| 136 |
[$tmp] = self::filterArguments([$v->getEntity()]); |
| 137 |
$args[$k] = new Statement($tmp, self::filterArguments($v->arguments)); |
| 138 |
} |
| 139 |
} |
| 140 |
return $args; |
| 141 |
} |
| 142 |
/** |
| 143 |
* Replaces @extension with real extension name in service definition. |
| 144 |
* @param mixed $config |
| 145 |
* @return mixed |
| 146 |
*/ |
| 147 |
public static function prefixServiceName($config, string $namespace) |
| 148 |
{ |
| 149 |
if (\is_string($config)) { |
| 150 |
if (\strncmp($config, '@extension.', 10) === 0) { |
| 151 |
$config = '@' . $namespace . '.' . \substr($config, 11); |
| 152 |
} |
| 153 |
} elseif ($config instanceof Reference) { |
| 154 |
if (\strncmp($config->getValue(), 'extension.', 9) === 0) { |
| 155 |
$config = new Reference($namespace . '.' . \substr($config->getValue(), 10)); |
| 156 |
} |
| 157 |
} elseif ($config instanceof Statement) { |
| 158 |
return new Statement(self::prefixServiceName($config->getEntity(), $namespace), self::prefixServiceName($config->arguments, $namespace)); |
| 159 |
} elseif (\is_array($config)) { |
| 160 |
foreach ($config as &$val) { |
| 161 |
$val = self::prefixServiceName($val, $namespace); |
| 162 |
} |
| 163 |
} |
| 164 |
return $config; |
| 165 |
} |
| 166 |
/** |
| 167 |
* Returns an annotation value. |
| 168 |
* @param \ReflectionFunctionAbstract|\ReflectionProperty|\ReflectionClass $ref |
| 169 |
*/ |
| 170 |
public static function parseAnnotation(\Reflector $ref, string $name) : ?string |
| 171 |
{ |
| 172 |
if (!Reflection::areCommentsAvailable()) { |
| 173 |
throw new \Packetery\Nette\InvalidStateException('You have to enable phpDoc comments in opcode cache.'); |
| 174 |
} |
| 175 |
$re = '#[\\s*]@' . \preg_quote($name, '#') . '(?=\\s|$)(?:[ \\t]+([^@\\s]\\S*))?#'; |
| 176 |
if ($ref->getDocComment() && \preg_match($re, \trim($ref->getDocComment(), '/*'), $m)) { |
| 177 |
return $m[1] ?? ''; |
| 178 |
} |
| 179 |
return null; |
| 180 |
} |
| 181 |
public static function getReturnTypeAnnotation(\ReflectionFunctionAbstract $func) : ?Type |
| 182 |
{ |
| 183 |
$type = \preg_replace('#[|\\s].*#', '', (string) self::parseAnnotation($func, 'return')); |
| 184 |
if (!$type || $type === 'object' || $type === 'mixed') { |
| 185 |
return null; |
| 186 |
} elseif ($func instanceof \ReflectionMethod) { |
| 187 |
$type = $type === '$this' ? 'static' : $type; |
| 188 |
$type = Reflection::expandClassName($type, $func->getDeclaringClass()); |
| 189 |
} |
| 190 |
return Type::fromString($type); |
| 191 |
} |
| 192 |
public static function ensureClassType(?Type $type, string $hint, bool $allowNullable = \false) : string |
| 193 |
{ |
| 194 |
if (!$type) { |
| 195 |
throw new ServiceCreationException(\sprintf('%s is not declared.', \ucfirst($hint))); |
| 196 |
} elseif (!$type->isClass() || !$allowNullable && $type->allows('null')) { |
| 197 |
throw new ServiceCreationException(\sprintf("%s is expected to not be %sbuilt-in/complex, '%s' given.", \ucfirst($hint), $allowNullable ? '' : 'nullable/', $type)); |
| 198 |
} |
| 199 |
$class = $type->getSingleName(); |
| 200 |
if (!\class_exists($class) && !\interface_exists($class)) { |
| 201 |
throw new ServiceCreationException(\sprintf("Class '%s' not found.\nCheck the %s.", $class, $hint)); |
| 202 |
} |
| 203 |
return $class; |
| 204 |
} |
| 205 |
public static function normalizeClass(string $type) : string |
| 206 |
{ |
| 207 |
return \class_exists($type) || \interface_exists($type) ? (new \ReflectionClass($type))->name : $type; |
| 208 |
} |
| 209 |
/** |
| 210 |
* Non data-loss type conversion. |
| 211 |
* @param mixed $value |
| 212 |
* @return mixed |
| 213 |
* @throws \Packetery\Nette\InvalidStateException |
| 214 |
*/ |
| 215 |
public static function convertType($value, string $type) |
| 216 |
{ |
| 217 |
if (\is_scalar($value)) { |
| 218 |
$norm = $value === \false ? '0' : (string) $value; |
| 219 |
if ($type === 'float') { |
| 220 |
$norm = \preg_replace('#\\.0*$#D', '', $norm); |
| 221 |
} |
| 222 |
$orig = $norm; |
| 223 |
\settype($norm, $type); |
| 224 |
if ($orig === ($norm === \false ? '0' : (string) $norm)) { |
| 225 |
return $norm; |
| 226 |
} |
| 227 |
} |
| 228 |
throw new \Packetery\Nette\InvalidStateException(\sprintf('Cannot convert %s to %s.', \is_scalar($value) ? "'{$value}'" : \gettype($value), $type)); |
| 229 |
} |
| 230 |
} |
| 231 |
|