friendship ended with social-app. php is my new best friend
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
8declare(strict_types=1);
9
10namespace Nette\Utils;
11
12use Nette;
13use function explode, func_get_args, ini_get, is_array, is_callable, is_object, is_string, preg_replace, restore_error_handler, set_error_handler, sprintf, str_contains, str_ends_with;
14
15
16/**
17 * PHP callable tools.
18 */
19final class Callback
20{
21 use Nette\StaticClass;
22
23 /**
24 * Invokes internal PHP function with own error handler.
25 */
26 public static function invokeSafe(string $function, array $args, callable $onError): mixed
27 {
28 $prev = set_error_handler(function ($severity, $message, $file) use ($onError, &$prev, $function): ?bool {
29 if ($file === __FILE__) {
30 $msg = ini_get('html_errors')
31 ? Html::htmlToText($message)
32 : $message;
33 $msg = preg_replace("#^$function\\(.*?\\): #", '', $msg);
34 if ($onError($msg, $severity) !== false) {
35 return null;
36 }
37 }
38
39 return $prev ? $prev(...func_get_args()) : false;
40 });
41
42 try {
43 return $function(...$args);
44 } finally {
45 restore_error_handler();
46 }
47 }
48
49
50 /**
51 * Checks that $callable is valid PHP callback. Otherwise throws exception. If the $syntax is set to true, only verifies
52 * that $callable has a valid structure to be used as a callback, but does not verify if the class or method actually exists.
53 * @return callable
54 * @throws Nette\InvalidArgumentException
55 */
56 public static function check(mixed $callable, bool $syntax = false)
57 {
58 if (!is_callable($callable, $syntax)) {
59 throw new Nette\InvalidArgumentException(
60 $syntax
61 ? 'Given value is not a callable type.'
62 : sprintf("Callback '%s' is not callable.", self::toString($callable)),
63 );
64 }
65
66 return $callable;
67 }
68
69
70 /**
71 * Converts PHP callback to textual form. Class or method may not exists.
72 */
73 public static function toString(mixed $callable): string
74 {
75 if ($callable instanceof \Closure) {
76 $inner = self::unwrap($callable);
77 return '{closure' . ($inner instanceof \Closure ? '}' : ' ' . self::toString($inner) . '}');
78 } else {
79 is_callable(is_object($callable) ? [$callable, '__invoke'] : $callable, true, $textual);
80 return $textual;
81 }
82 }
83
84
85 /**
86 * Returns reflection for method or function used in PHP callback.
87 * @param callable $callable type check is escalated to ReflectionException
88 * @throws \ReflectionException if callback is not valid
89 */
90 public static function toReflection($callable): \ReflectionMethod|\ReflectionFunction
91 {
92 if ($callable instanceof \Closure) {
93 $callable = self::unwrap($callable);
94 }
95
96 if (is_string($callable) && str_contains($callable, '::')) {
97 return new ReflectionMethod(...explode('::', $callable, 2));
98 } elseif (is_array($callable)) {
99 return new ReflectionMethod($callable[0], $callable[1]);
100 } elseif (is_object($callable) && !$callable instanceof \Closure) {
101 return new ReflectionMethod($callable, '__invoke');
102 } else {
103 return new \ReflectionFunction($callable);
104 }
105 }
106
107
108 /**
109 * Checks whether PHP callback is function or static method.
110 */
111 public static function isStatic(callable $callable): bool
112 {
113 return is_string(is_array($callable) ? $callable[0] : $callable);
114 }
115
116
117 /**
118 * Unwraps closure created by Closure::fromCallable().
119 */
120 public static function unwrap(\Closure $closure): callable|array
121 {
122 $r = new \ReflectionFunction($closure);
123 $class = $r->getClosureScopeClass()?->name;
124 if (str_ends_with($r->name, '}')) {
125 return $closure;
126
127 } elseif (($obj = $r->getClosureThis()) && $obj::class === $class) {
128 return [$obj, $r->name];
129
130 } elseif ($class) {
131 return [$class, $r->name];
132
133 } else {
134 return $r->name;
135 }
136 }
137}