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