1<?php declare(strict_types=1); 2/* 3 * This file is part of sebastian/type. 4 * 5 * (c) Sebastian Bergmann <sebastian@phpunit.de> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10namespace SebastianBergmann\Type; 11 12use function assert; 13use function class_exists; 14use function count; 15use function explode; 16use function function_exists; 17use function is_array; 18use function is_object; 19use function is_string; 20use function strpos; 21use Closure; 22use ReflectionClass; 23use ReflectionException; 24use ReflectionObject; 25 26final class CallableType extends Type 27{ 28 /** 29 * @var bool 30 */ 31 private $allowsNull; 32 33 public function __construct(bool $nullable) 34 { 35 $this->allowsNull = $nullable; 36 } 37 38 /** 39 * @throws RuntimeException 40 */ 41 public function isAssignable(Type $other): bool 42 { 43 if ($this->allowsNull && $other instanceof NullType) { 44 return true; 45 } 46 47 if ($other instanceof self) { 48 return true; 49 } 50 51 if ($other instanceof ObjectType) { 52 if ($this->isClosure($other)) { 53 return true; 54 } 55 56 if ($this->hasInvokeMethod($other)) { 57 return true; 58 } 59 } 60 61 if ($other instanceof SimpleType) { 62 if ($this->isFunction($other)) { 63 return true; 64 } 65 66 if ($this->isClassCallback($other)) { 67 return true; 68 } 69 70 if ($this->isObjectCallback($other)) { 71 return true; 72 } 73 } 74 75 return false; 76 } 77 78 public function name(): string 79 { 80 return 'callable'; 81 } 82 83 public function allowsNull(): bool 84 { 85 return $this->allowsNull; 86 } 87 88 private function isClosure(ObjectType $type): bool 89 { 90 return !$type->className()->isNamespaced() && $type->className()->simpleName() === Closure::class; 91 } 92 93 /** 94 * @throws RuntimeException 95 */ 96 private function hasInvokeMethod(ObjectType $type): bool 97 { 98 $className = $type->className()->qualifiedName(); 99 assert(class_exists($className)); 100 101 try { 102 $class = new ReflectionClass($className); 103 // @codeCoverageIgnoreStart 104 } catch (ReflectionException $e) { 105 throw new RuntimeException( 106 $e->getMessage(), 107 (int) $e->getCode(), 108 $e 109 ); 110 // @codeCoverageIgnoreEnd 111 } 112 113 if ($class->hasMethod('__invoke')) { 114 return true; 115 } 116 117 return false; 118 } 119 120 private function isFunction(SimpleType $type): bool 121 { 122 if (!is_string($type->value())) { 123 return false; 124 } 125 126 return function_exists($type->value()); 127 } 128 129 private function isObjectCallback(SimpleType $type): bool 130 { 131 if (!is_array($type->value())) { 132 return false; 133 } 134 135 if (count($type->value()) !== 2) { 136 return false; 137 } 138 139 if (!is_object($type->value()[0]) || !is_string($type->value()[1])) { 140 return false; 141 } 142 143 [$object, $methodName] = $type->value(); 144 145 return (new ReflectionObject($object))->hasMethod($methodName); 146 } 147 148 private function isClassCallback(SimpleType $type): bool 149 { 150 if (!is_string($type->value()) && !is_array($type->value())) { 151 return false; 152 } 153 154 if (is_string($type->value())) { 155 if (strpos($type->value(), '::') === false) { 156 return false; 157 } 158 159 [$className, $methodName] = explode('::', $type->value()); 160 } 161 162 if (is_array($type->value())) { 163 if (count($type->value()) !== 2) { 164 return false; 165 } 166 167 if (!is_string($type->value()[0]) || !is_string($type->value()[1])) { 168 return false; 169 } 170 171 [$className, $methodName] = $type->value(); 172 } 173 174 assert(isset($className) && is_string($className) && class_exists($className)); 175 assert(isset($methodName) && is_string($methodName)); 176 177 try { 178 $class = new ReflectionClass($className); 179 180 if ($class->hasMethod($methodName)) { 181 $method = $class->getMethod($methodName); 182 183 return $method->isPublic() && $method->isStatic(); 184 } 185 // @codeCoverageIgnoreStart 186 } catch (ReflectionException $e) { 187 throw new RuntimeException( 188 $e->getMessage(), 189 (int) $e->getCode(), 190 $e 191 ); 192 // @codeCoverageIgnoreEnd 193 } 194 195 return false; 196 } 197} 198