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 is_array, is_object, is_string; 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($callable); 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} 138