|
| 1 | +<?php |
| 2 | + |
| 3 | +/* |
| 4 | + * This file is part of Respect/Stringifier. |
| 5 | + * Copyright (c) Henrique Moody <[email protected]> |
| 6 | + * SPDX-License-Identifier: MIT |
| 7 | + */ |
| 8 | + |
| 9 | +declare(strict_types=1); |
| 10 | + |
| 11 | +namespace Respect\Stringifier\Stringifiers; |
| 12 | + |
| 13 | +use Closure; |
| 14 | +use ReflectionFunction; |
| 15 | +use ReflectionFunctionAbstract; |
| 16 | +use ReflectionIntersectionType; |
| 17 | +use ReflectionMethod; |
| 18 | +use ReflectionNamedType; |
| 19 | +use ReflectionParameter; |
| 20 | +use ReflectionType; |
| 21 | +use ReflectionUnionType; |
| 22 | +use Respect\Stringifier\Helpers\ObjectHelper; |
| 23 | +use Respect\Stringifier\Quoter; |
| 24 | +use Respect\Stringifier\Stringifier; |
| 25 | + |
| 26 | +use function array_keys; |
| 27 | +use function array_map; |
| 28 | +use function count; |
| 29 | +use function implode; |
| 30 | +use function is_array; |
| 31 | +use function is_callable; |
| 32 | +use function is_object; |
| 33 | +use function is_string; |
| 34 | +use function sprintf; |
| 35 | +use function str_contains; |
| 36 | +use function strrchr; |
| 37 | +use function strstr; |
| 38 | +use function substr; |
| 39 | + |
| 40 | +final class CallableStringifier implements Stringifier |
| 41 | +{ |
| 42 | + use ObjectHelper; |
| 43 | + |
| 44 | + public function __construct( |
| 45 | + private readonly Stringifier $stringifier, |
| 46 | + private readonly Quoter $quoter, |
| 47 | + ) { |
| 48 | + } |
| 49 | + |
| 50 | + public function stringify(mixed $raw, int $depth): ?string |
| 51 | + { |
| 52 | + if (!is_callable($raw)) { |
| 53 | + return null; |
| 54 | + } |
| 55 | + |
| 56 | + if ($raw instanceof Closure) { |
| 57 | + return $this->buildFunction(new ReflectionFunction($raw), $depth); |
| 58 | + } |
| 59 | + |
| 60 | + if (is_object($raw)) { |
| 61 | + return $this->buildMethod(new ReflectionMethod($raw, '__invoke'), $raw, $depth); |
| 62 | + } |
| 63 | + |
| 64 | + if (is_array($raw) && is_object($raw[0])) { |
| 65 | + return $this->buildMethod(new ReflectionMethod($raw[0], $raw[1]), $raw[0], $depth); |
| 66 | + } |
| 67 | + |
| 68 | + if (is_array($raw) && is_string($raw[0])) { |
| 69 | + return $this->buildStaticMethod(new ReflectionMethod($raw[0], $raw[1]), $depth); |
| 70 | + } |
| 71 | + |
| 72 | + /** @var callable-string $raw */ |
| 73 | + if (str_contains($raw, ':')) { |
| 74 | + /** @var class-string $class */ |
| 75 | + $class = (string) strstr($raw, ':', true); |
| 76 | + $method = substr((string) strrchr($raw, ':'), 1); |
| 77 | + |
| 78 | + return $this->buildStaticMethod(new ReflectionMethod($class, $method), $depth); |
| 79 | + } |
| 80 | + |
| 81 | + return $this->buildFunction(new ReflectionFunction($raw), $depth); |
| 82 | + } |
| 83 | + |
| 84 | + public function buildFunction(ReflectionFunction $raw, int $depth): ?string |
| 85 | + { |
| 86 | + return $this->quoter->quote($this->buildSignature($raw, $depth), $depth); |
| 87 | + } |
| 88 | + |
| 89 | + private function buildMethod(ReflectionMethod $reflection, object $object, int $depth): string |
| 90 | + { |
| 91 | + return $this->quoter->quote( |
| 92 | + sprintf('%s->%s', $this->getName($object), $this->buildSignature($reflection, $depth)), |
| 93 | + $depth |
| 94 | + ); |
| 95 | + } |
| 96 | + |
| 97 | + private function buildStaticMethod(ReflectionMethod $reflection, int $depth): string |
| 98 | + { |
| 99 | + return $this->quoter->quote( |
| 100 | + sprintf('%s::%s', $reflection->class, $this->buildSignature($reflection, $depth)), |
| 101 | + $depth |
| 102 | + ); |
| 103 | + } |
| 104 | + |
| 105 | + private function buildSignature(ReflectionFunctionAbstract $function, int $depth): string |
| 106 | + { |
| 107 | + $signature = $function->isClosure() ? 'function ' : $function->getName(); |
| 108 | + $signature .= sprintf( |
| 109 | + '(%s)', |
| 110 | + implode( |
| 111 | + ', ', |
| 112 | + array_map( |
| 113 | + fn(ReflectionParameter $parameter): string => $this->buildParameter( |
| 114 | + $parameter, |
| 115 | + $depth + 1 |
| 116 | + ), |
| 117 | + $function->getParameters() |
| 118 | + ) |
| 119 | + ), |
| 120 | + ); |
| 121 | + |
| 122 | + $closureUsedVariables = $function->getClosureUsedVariables(); |
| 123 | + if (count($closureUsedVariables)) { |
| 124 | + $signature .= sprintf( |
| 125 | + ' use ($%s)', |
| 126 | + implode( |
| 127 | + ', $', |
| 128 | + array_keys($closureUsedVariables) |
| 129 | + ), |
| 130 | + ); |
| 131 | + } |
| 132 | + |
| 133 | + $returnType = $function->getReturnType(); |
| 134 | + if ($returnType !== null) { |
| 135 | + $signature .= ': ' . $this->buildType($returnType, $depth); |
| 136 | + } |
| 137 | + |
| 138 | + return $signature; |
| 139 | + } |
| 140 | + |
| 141 | + private function buildParameter(ReflectionParameter $reflectionParameter, int $depth): string |
| 142 | + { |
| 143 | + $parameter = ''; |
| 144 | + |
| 145 | + $type = $reflectionParameter->getType(); |
| 146 | + if ($type !== null) { |
| 147 | + $parameter .= $this->buildType($type, $depth); |
| 148 | + } |
| 149 | + |
| 150 | + if ($reflectionParameter->isVariadic()) { |
| 151 | + return $parameter . ' ...$' . $reflectionParameter->getName(); |
| 152 | + } |
| 153 | + |
| 154 | + $parameter .= ' $' . $reflectionParameter->getName(); |
| 155 | + if ($reflectionParameter->isOptional()) { |
| 156 | + $parameter .= ' = ' . $this->buildValue($reflectionParameter, $depth); |
| 157 | + } |
| 158 | + |
| 159 | + return $parameter; |
| 160 | + } |
| 161 | + |
| 162 | + private function buildValue(ReflectionParameter $reflectionParameter, int $depth): ?string |
| 163 | + { |
| 164 | + $value = $reflectionParameter->getDefaultValueConstantName(); |
| 165 | + if ($value !== null) { |
| 166 | + return $value; |
| 167 | + } |
| 168 | + |
| 169 | + return $this->stringifier->stringify($reflectionParameter->getDefaultValue(), $depth); |
| 170 | + } |
| 171 | + |
| 172 | + private function buildType(ReflectionType $raw, int $depth): string |
| 173 | + { |
| 174 | + if ($raw instanceof ReflectionUnionType) { |
| 175 | + return implode( |
| 176 | + '|', |
| 177 | + array_map(fn(ReflectionType $type) => $this->buildType($type, $depth), $raw->getTypes()) |
| 178 | + ); |
| 179 | + } |
| 180 | + |
| 181 | + if ($raw instanceof ReflectionIntersectionType) { |
| 182 | + return implode( |
| 183 | + '&', |
| 184 | + array_map(fn(ReflectionType $type) => $this->buildType($type, $depth), $raw->getTypes()) |
| 185 | + ); |
| 186 | + } |
| 187 | + |
| 188 | + /** @var ReflectionNamedType $raw */ |
| 189 | + |
| 190 | + return ($raw->allowsNull() ? '?' : '') . $raw->getName(); |
| 191 | + } |
| 192 | +} |
0 commit comments