Repository: walkor/webman-framework Branch: master Commit: 313d5fe1071a Files: 68 Total size: 237.9 KB Directory structure: gitextract_e73a02zd/ ├── .gitignore ├── README.md ├── composer.json └── src/ ├── App.php ├── Bootstrap.php ├── Config.php ├── Container.php ├── Context.php ├── Exception/ │ ├── BusinessException.php │ ├── ExceptionHandler.php │ ├── ExceptionHandlerInterface.php │ ├── FileException.php │ └── NotFoundException.php ├── File.php ├── Finder/ │ ├── ControllerFinder.php │ ├── FileInfo.php │ └── Finder.php ├── Http/ │ ├── Request.php │ ├── Response.php │ └── UploadFile.php ├── Install.php ├── Middleware.php ├── MiddlewareInterface.php ├── Route/ │ └── Route.php ├── Route.php ├── Session/ │ ├── FileSessionHandler.php │ ├── RedisClusterSessionHandler.php │ └── RedisSessionHandler.php ├── Util.php ├── View.php ├── start.php ├── support/ │ ├── App.php │ ├── Container.php │ ├── Context.php │ ├── Log.php │ ├── Plugin.php │ ├── Request.php │ ├── Response.php │ ├── Translation.php │ ├── View.php │ ├── annotation/ │ │ ├── DisableDefaultRoute.php │ │ ├── Middleware.php │ │ └── route/ │ │ ├── Any.php │ │ ├── Delete.php │ │ ├── DisableDefaultRoute.php │ │ ├── Get.php │ │ ├── Head.php │ │ ├── Options.php │ │ ├── Patch.php │ │ ├── Post.php │ │ ├── Put.php │ │ ├── Route.php │ │ └── RouteGroup.php │ ├── bootstrap/ │ │ └── Session.php │ ├── bootstrap.php │ ├── exception/ │ │ ├── BusinessException.php │ │ ├── Handler.php │ │ ├── InputTypeException.php │ │ ├── InputValueException.php │ │ ├── MissingInputException.php │ │ ├── NotFoundException.php │ │ └── PageNotFoundException.php │ ├── helpers.php │ └── view/ │ ├── Blade.php │ ├── Raw.php │ ├── ThinkPHP.php │ └── Twig.php └── windows.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ composer.lock vendor vendor/ .idea .idea/ ================================================ FILE: README.md ================================================ # webman-framework Note: This repository is the core code of the webman framework. If you want to build an application using webman, visit the main [webman](https://github.com/walkor/webman) repository. ## LICENSE MIT ================================================ FILE: composer.json ================================================ { "name": "workerman/webman-framework", "type": "library", "keywords": [ "high performance", "http service" ], "homepage": "https://www.workerman.net", "license": "MIT", "description": "High performance HTTP Service Framework.", "authors": [ { "name": "walkor", "email": "walkor@workerman.net", "homepage": "https://www.workerman.net", "role": "Developer" } ], "support": { "email": "walkor@workerman.net", "issues": "https://github.com/walkor/webman/issues", "forum": "https://wenda.workerman.net/", "wiki": "https://doc.workerman.net/", "source": "https://github.com/walkor/webman-framework" }, "require": { "php": ">=8.1", "ext-json": "*", "workerman/workerman": "^5.1 || dev-master", "nikic/fast-route": "^1.3", "psr/container": ">=1.0", "psr/log": "^2.0 || ^3.0" }, "suggest": { "ext-event": "For better performance. " }, "autoload": { "psr-4": { "Webman\\": "./src", "support\\": "./src/support", "Support\\": "./src/support", "Support\\Bootstrap\\": "./src/support/bootstrap", "Support\\Exception\\": "./src/support/exception", "Support\\View\\": "./src/support/view" }, "files": [ "./src/support/helpers.php" ] }, "minimum-stability": "dev", "prefer-stable": true } ================================================ FILE: src/App.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman; use ArrayObject; use Closure; use Exception; use FastRoute\Dispatcher; use Illuminate\Database\Eloquent\Model; use Psr\Log\LoggerInterface; use ReflectionEnum; use support\exception\InputValueException; use support\exception\PageNotFoundException; use think\Model as ThinkModel; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use ReflectionClass; use ReflectionException; use ReflectionFunction; use ReflectionFunctionAbstract; use ReflectionMethod; use support\exception\MissingInputException; use support\exception\RecordNotFoundException; use support\exception\InputTypeException; use Throwable; use Webman\Exception\ExceptionHandler; use Webman\Exception\ExceptionHandlerInterface; use Webman\Http\Request; use Webman\Http\Response; use Webman\Route\Route as RouteObject; use support\annotation\route\Route as RouteAttribute; use Workerman\Connection\TcpConnection; use Workerman\Protocols\Http; use Workerman\Worker; use function array_merge; use function array_pop; use function array_reduce; use function array_splice; use function array_values; use function class_exists; use function clearstatcache; use function count; use function current; use function end; use function explode; use function get_class_methods; use function gettype; use function implode; use function is_a; use function is_array; use function is_dir; use function is_file; use function is_numeric; use function is_object; use function is_string; use function key; use function method_exists; use function ob_get_clean; use function ob_start; use function pathinfo; use function scandir; use function str_replace; use function strpos; use function strtolower; use function substr; use function trim; /** * Class App * @package Webman */ class App { /** * @var callable[] */ protected static $callbacks = []; /** * @var array */ protected static array $reflectorCache = []; /** * @var array>> */ protected static array $parameterMetadataCache = []; /** * @var Worker */ protected static $worker = null; /** * @var ?LoggerInterface */ protected static ?LoggerInterface $logger = null; /** * @var string */ protected static $appPath = ''; /** * @var string */ protected static $publicPath = ''; /** * @var string */ protected static $requestClass = ''; /** * App constructor. * @param string $requestClass * @param LoggerInterface $logger * @param string $appPath * @param string $publicPath */ public function __construct(string $requestClass, LoggerInterface $logger, string $appPath, string $publicPath) { static::$requestClass = $requestClass; static::$logger = $logger; static::$publicPath = $publicPath; static::$appPath = $appPath; } /** * OnMessage. * @param TcpConnection|mixed $connection * @param Request|mixed $request * @return null * @throws Throwable */ public function onMessage($connection, $request) { try { Context::reset(new ArrayObject([Request::class => $request])); $path = $request->path(); $key = $request->method() . $path; if (isset(static::$callbacks[$key])) { [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key]; static::send($connection, $callback($request), $request); return null; } $status = 200; if ( static::unsafeUri($connection, $path, $request) || static::findFile($connection, $path, $key, $request) || static::findRoute($connection, $path, $key, $request, $status) ) { return null; } $controllerAndAction = static::parseControllerAction($path); $plugin = $controllerAndAction['plugin'] ?? static::getPluginByPath($path); if (!$controllerAndAction || Route::isDefaultRouteDisabled($plugin, $controllerAndAction['app'] ?: '*') || Route::isDefaultRouteDisabled($controllerAndAction['controller']) || Route::isDefaultRouteDisabled([$controllerAndAction['controller'], $controllerAndAction['action']])) { $request->plugin = $plugin; $callback = static::getFallback($plugin, $status); $request->app = $request->controller = $request->action = ''; static::send($connection, $callback($request), $request); return null; } $app = $controllerAndAction['app']; $controller = $controllerAndAction['controller']; $action = $controllerAndAction['action']; if ($methodNotAllowed = static::defaultRouteMethodNotAllowedResponse($controller, $action, $request->method())) { $callback = $methodNotAllowed; static::collectCallbacks($key, [$callback, $plugin, $app, $controller, $action, null]); [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key]; static::send($connection, $callback($request), $request); return null; } $callback = static::getCallback($plugin, $app, [$controller, $action]); static::collectCallbacks($key, [$callback, $plugin, $app, $controller, $action, null]); [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key]; static::send($connection, $callback($request), $request); } catch (Throwable $e) { static::send($connection, static::exceptionResponse($e, $request), $request); } return null; } /** * Method allowlist for default route (no explicit route path). * If action method has Route attributes with empty path, they are treated as allowed methods. * Returns a cached callback producing 405 response when current method is not allowed, otherwise null. * @param string $controllerClass * @param string $action * @param string $httpMethod * @return callable|null */ protected static function defaultRouteMethodNotAllowedResponse(string $controllerClass, string $action, string $httpMethod): ?callable { $httpMethod = strtoupper($httpMethod); static $allowedCache = []; $cacheKey = $controllerClass . '::' . $action; if (!isset($allowedCache[$cacheKey])) { $allowed = []; try { if (method_exists($controllerClass, $action)) { $ref = new ReflectionMethod($controllerClass, $action); $attrs = $ref->getAttributes(RouteAttribute::class, \ReflectionAttribute::IS_INSTANCEOF); foreach ($attrs as $attr) { /** @var RouteAttribute $route */ $route = $attr->newInstance(); if ($route->path !== null) { continue; } foreach ($route->methods as $m) { $m = strtoupper((string)$m); $allowed[$m] = $m; } } } } catch (Throwable $e) { } $allowedCache[$cacheKey] = $allowed; if (count($allowedCache) > 1024) { unset($allowedCache[key($allowedCache)]); } } else { $allowed = $allowedCache[$cacheKey]; } if (!$allowed || isset($allowed[$httpMethod])) { return null; } $allowHeader = implode(', ', array_values($allowed)); return static function () use ($allowHeader) { return new Response(405, ['Allow' => $allowHeader], '405 Method Not Allowed'); }; } /** * OnWorkerStart. * @param $worker * @return void */ public function onWorkerStart($worker) { static::$worker = $worker; Http::requestClass(static::$requestClass); } /** * CollectCallbacks. * @param string $key * @param array $data * @return void */ protected static function collectCallbacks(string $key, array $data) { static::$callbacks[$key] = $data; if (count(static::$callbacks) > 1024) { unset(static::$callbacks[key(static::$callbacks)]); } } /** * UnsafeUri. * @param TcpConnection $connection * @param string $path * @param $request * @return bool */ protected static function unsafeUri(TcpConnection $connection, string $path, $request): bool { if (!$path || $path[0] !== '/' || static::containsPathTraversal($path)) { $callback = static::getFallback('', 400); $request->plugin = $request->app = $request->controller = $request->action = ''; static::send($connection, $callback($request, 400), $request); return true; } return false; } /** * Check if a path contains directory traversal or dangerous sequences. * @param string $path * @return bool */ protected static function containsPathTraversal(string $path): bool { return strpos($path, '/../') !== false || substr($path, -3) === '/..' || strpos($path, "\\") !== false || strpos($path, "\0") !== false; } /** * GetFallback. * @param string $plugin * @param int $status * @return Closure * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface * @throws ReflectionException */ protected static function getFallback(string $plugin = '', int $status = 404): Closure { // When route, controller and action not found, try to use Route::fallback return Route::getFallback($plugin, $status) ?: function () { throw new PageNotFoundException(); }; } /** * ExceptionResponse. * @param Throwable $e * @param $request * @return Response */ protected static function exceptionResponse(Throwable $e, $request): Response { try { $app = $request->app ?: ''; $plugin = $request->plugin ?: ''; $exceptionConfig = static::config($plugin, 'exception'); $appExceptionConfig = static::config("", 'exception'); if (!isset($exceptionConfig['']) && isset($appExceptionConfig['@'])) { //如果插件没有配置自己的异常处理器并且配置了全局@异常处理器 则使用全局异常处理器 $defaultException = $appExceptionConfig['@'] ?? ExceptionHandler::class; } else { $defaultException = $exceptionConfig[''] ?? ExceptionHandler::class; } $exceptionHandlerClass = $exceptionConfig[$app] ?? $defaultException; /** @var ExceptionHandlerInterface $exceptionHandler */ $exceptionHandler = (static::container($plugin) ?? static::container(''))->make($exceptionHandlerClass, [ 'logger' => static::$logger, 'debug' => static::config($plugin, 'app.debug') ]); $exceptionHandler->report($e); $response = $exceptionHandler->render($request, $e); $response->exception($e); return $response; } catch (Throwable $e) { $response = new Response(500, [], static::config($plugin ?? '', 'app.debug') ? (string)$e : $e->getMessage()); $response->exception($e); return $response; } } /** * GetCallback. * @param string $plugin * @param string $app * @param $call * @param array $args * @param bool $withGlobalMiddleware * @param RouteObject|null $route * @return callable * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface * @throws ReflectionException */ public static function getCallback(string $plugin, string $app, $call, array $args = [], bool $withGlobalMiddleware = true, ?RouteObject $route = null) { $isController = is_array($call) && is_string($call[0]); $middlewares = Middleware::getMiddleware($plugin, $app, $call, $route, $withGlobalMiddleware); $container = static::container($plugin) ?? static::container(''); foreach ($middlewares as $key => $item) { $middleware = $item[0]; if (is_string($middleware)) { $middleware = $container->get($middleware); } elseif ($middleware instanceof Closure) { $middleware = call_user_func($middleware, $container); } $middlewares[$key][0] = $middleware; } $needInject = static::isNeedInject($call, $args); $anonymousArgs = array_values($args); // Pre-compute return type for Response check optimization in middleware chain $alwaysReturnsResponse = false; if ($middlewares) { try { $returnType = static::getReflector($call)->getReturnType(); $alwaysReturnsResponse = $returnType instanceof \ReflectionNamedType && !$returnType->allowsNull() && is_a($returnType->getName(), Response::class, true); } catch (Throwable $e) { } } if ($isController) { $controllerReuse = static::config($plugin, 'app.controller_reuse', true); if (!$controllerReuse) { if ($needInject) { // Pre-compute metadata at closure creation time $reflector = static::getReflector($call); $metadataList = static::getMethodParameterMetadata($reflector); $debug = static::config($plugin, 'app.debug'); $call = function ($request) use ($call, $plugin, $args, $container, $metadataList, $debug) { $call[0] = $container->make($call[0]); $inputs = $args ? array_merge($request->all(), $args) : $request->all(); $resolvedArgs = array_values(static::resolveMethodDependenciesFromMetadata($container, $request, $inputs, $metadataList, $debug)); return $call(...$resolvedArgs); }; $needInject = false; } else { $call = function ($request, ...$anonymousArgs) use ($call, $plugin, $container) { $call[0] = $container->make($call[0]); return $call($request, ...$anonymousArgs); }; } } else { $call[0] = $container->get($call[0]); } } if ($needInject) { $call = static::resolveInject($container, $call, $args, static::config($plugin, 'app.debug')); } if ($middlewares) { if ($alwaysReturnsResponse) { $innermost = function ($request) use ($call, $anonymousArgs) { try { return $call($request, ...$anonymousArgs); } catch (Throwable $e) { return static::exceptionResponse($e, $request); } }; } else { $innermost = function ($request) use ($call, $anonymousArgs) { try { $response = $call($request, ...$anonymousArgs); } catch (Throwable $e) { return static::exceptionResponse($e, $request); } if (!$response instanceof Response) { if (!is_string($response)) { $response = static::stringify($response); } $response = new Response(200, [], $response); } return $response; }; } $callback = array_reduce($middlewares, function ($carry, $pipe) { return function ($request) use ($carry, $pipe) { try { return $pipe($request, $carry); } catch (Throwable $e) { return static::exceptionResponse($e, $request); } }; }, $innermost); } else { if (!$anonymousArgs) { $callback = $call; } else { $callback = function ($request) use ($call, $anonymousArgs) { return $call($request, ...$anonymousArgs); }; } } return $callback; } /** * ResolveInject. * @param ContainerInterface $container * @param array|Closure $call * @param array $args * @param bool $debug * @return Closure * @see Dependency injection through reflection information */ protected static function resolveInject(ContainerInterface $container, $call, array $args, bool $debug): Closure { // Pre-compute metadata at closure creation time (once), not at execution time (every request) $metadataList = static::getMethodParameterMetadata(static::getReflector($call)); return function (Request $request) use ($container, $call, $args, $metadataList, $debug) { $inputs = $args ? array_merge($request->all(), $args) : $request->all(); $resolvedArgs = array_values(static::resolveMethodDependenciesFromMetadata( $container, $request, $inputs, $metadataList, $debug )); return $call(...$resolvedArgs); }; } /** * Check whether inject is required. * @param $call * @param array $args * @return bool * @throws ReflectionException */ protected static function isNeedInject($call, array &$args): bool { if (is_array($call) && !method_exists($call[0], $call[1])) { return false; } $reflector = static::getReflector($call); $reflectionParameters = $reflector->getParameters(); if (!$reflectionParameters) { return false; } $firstParameter = current($reflectionParameters); unset($reflectionParameters[key($reflectionParameters)]); $adaptersList = ['int', 'string', 'bool', 'array', 'object', 'float', 'mixed', 'resource']; $keys = []; $needInject = false; foreach ($reflectionParameters as $parameter) { $parameterName = $parameter->name; $keys[] = $parameterName; if ($parameter->hasType()) { $type = $parameter->getType(); if (!$type instanceof \ReflectionNamedType) { throw new \RuntimeException( sprintf('Union/intersection types are not supported for controller parameter $%s. Use a single type instead.', $parameter->name) ); } $typeName = $type->getName(); if (!in_array($typeName, $adaptersList)) { $needInject = true; continue; } if (!array_key_exists($parameterName, $args)) { $needInject = true; continue; } switch ($typeName) { case 'int': case 'float': if (!is_numeric($args[$parameterName])) { return true; } $args[$parameterName] = $typeName === 'int' ? (int)$args[$parameterName]: (float)$args[$parameterName]; break; case 'bool': $args[$parameterName] = (bool)$args[$parameterName]; break; case 'array': case 'object': if (!is_array($args[$parameterName])) { return true; } $args[$parameterName] = $typeName === 'array' ? $args[$parameterName] : (object)$args[$parameterName]; break; case 'string': case 'mixed': case 'resource': break; } } } if (array_keys($args) !== $keys) { return true; } if (!$firstParameter->hasType()) { return $firstParameter->getName() !== 'request'; } $firstType = $firstParameter->getType(); if (!$firstType instanceof \ReflectionNamedType) { return true; } if (!is_a(static::$requestClass, $firstType->getName(), true)) { return true; } return $needInject; } /** * Get reflector. * @param $call * @return ReflectionFunction|ReflectionMethod * @throws ReflectionException */ protected static function getReflector($call) { $cacheKey = static::getReflectorCacheKey($call); if ($cacheKey !== null && isset(static::$reflectorCache[$cacheKey])) { return static::$reflectorCache[$cacheKey]; } if ($call instanceof Closure || is_string($call)) { $reflector = new ReflectionFunction($call); } else { $reflector = new ReflectionMethod($call[0], $call[1]); } if ($cacheKey !== null) { static::$reflectorCache[$cacheKey] = $reflector; if (count(static::$reflectorCache) > 1024) { unset(static::$reflectorCache[key(static::$reflectorCache)]); } } return $reflector; } /** * Get reflector cache key. * @param mixed $call * @return string|null */ protected static function getReflectorCacheKey($call): ?string { if (is_string($call)) { return 'func:' . $call; } if (is_array($call) && isset($call[0], $call[1])) { $class = is_object($call[0]) ? get_class($call[0]) : $call[0]; return 'method:' . $class . '::' . $call[1]; } // Closures may be short-lived; avoid caching to prevent key reuse risks. return null; } /** * Return dependent parameters * @param ContainerInterface $container * @param Request $request * @param array $inputs * @param ReflectionFunctionAbstract $reflector * @param bool $debug * @return array * @throws ReflectionException */ protected static function resolveMethodDependencies(ContainerInterface $container, Request $request, array $inputs, ReflectionFunctionAbstract $reflector, bool $debug): array { $metadataList = static::getMethodParameterMetadata($reflector); return static::resolveMethodDependenciesFromMetadata($container, $request, $inputs, $metadataList, $debug); } /** * Return dependent parameters from pre-computed metadata. * @param ContainerInterface $container * @param Request $request * @param array $inputs * @param array $metadataList * @param bool $debug * @return array * @throws ReflectionException */ protected static function resolveMethodDependenciesFromMetadata(ContainerInterface $container, Request $request, array $inputs, array $metadataList, bool $debug): array { $parameters = []; foreach ($metadataList as $metadata) { $parameterName = $metadata['name']; $typeName = $metadata['type']; if (!empty($metadata['isRequest'])) { $parameters[$parameterName] = $request; continue; } if (!array_key_exists($parameterName, $inputs)) { if (!$metadata['hasDefault']) { if (!$typeName || (!$metadata['isClass'] && !$metadata['isEnum']) || $metadata['isEnum']) { throw (new MissingInputException())->data([ 'parameter' => $parameterName, ])->debug($debug); } } else { $parameters[$parameterName] = $metadata['default']; continue; } } $parameterValue = $inputs[$parameterName] ?? null; switch ($typeName) { case 'int': case 'float': if (!is_numeric($parameterValue)) { throw (new InputTypeException())->data([ 'parameter' => $parameterName, 'exceptType' => $typeName, 'actualType' => gettype($parameterValue), ])->debug($debug); } $parameters[$parameterName] = $typeName === 'float' ? (float)$parameterValue : (int)$parameterValue; break; case 'bool': $parameters[$parameterName] = (bool)$parameterValue; break; case 'array': case 'object': if (!is_array($parameterValue)) { throw (new InputTypeException())->data([ 'parameter' => $parameterName, 'exceptType' => $typeName, 'actualType' => gettype($parameterValue), ])->debug($debug); } $parameters[$parameterName] = $typeName === 'object' ? (object)$parameterValue : $parameterValue; break; case 'string': case 'mixed': case 'resource': case null: $parameters[$parameterName] = $parameterValue; break; default: $subInputs = is_array($parameterValue) ? $parameterValue : []; if (!empty($metadata['isModel'])) { $parameters[$parameterName] = $container->make($typeName, [ 'attributes' => $subInputs ]); break; } if (!empty($metadata['isThinkModel'])) { $parameters[$parameterName] = $container->make($typeName, [ 'data' => $subInputs ]); break; } if (!empty($metadata['isEnum'])) { // Use pre-computed enum case mappings (avoids per-request ReflectionEnum) if (isset($metadata['enumCases'][$parameterValue])) { $parameters[$parameterName] = $metadata['enumCases'][$parameterValue]; break; } if (!empty($metadata['enumIsBacked']) && isset($metadata['enumBackedValues'][$parameterValue])) { $parameters[$parameterName] = $metadata['enumBackedValues'][$parameterValue]; break; } throw (new InputValueException())->data([ 'parameter' => $parameterName, 'enum' => $typeName ])->debug($debug); } if (is_array($subInputs) && !empty($metadata['hasConstructor'])) { $constructorReflector = static::getReflector([$typeName, '__construct']); $parameters[$parameterName] = $container->make($typeName, static::resolveMethodDependencies($container, $request, $subInputs, $constructorReflector, $debug)); } else { $parameters[$parameterName] = $container->make($typeName); } break; } } return $parameters; } /** * Get method parameter metadata from cache. * @param ReflectionFunctionAbstract $reflector * @return array> * @throws ReflectionException */ protected static function getMethodParameterMetadata(ReflectionFunctionAbstract $reflector): array { $cacheKey = static::getParameterMetadataCacheKey($reflector); if ($cacheKey !== null && isset(static::$parameterMetadataCache[$cacheKey])) { return static::$parameterMetadataCache[$cacheKey]; } $metadataList = []; foreach ($reflector->getParameters() as $parameter) { $type = $parameter->getType(); if ($type !== null && !$type instanceof \ReflectionNamedType) { throw new \RuntimeException( sprintf('Union/intersection types are not supported for controller parameter $%s. Use a single type instead.', $parameter->name) ); } $typeName = $type?->getName(); $hasDefault = $parameter->isDefaultValueAvailable(); $isEnum = $typeName && enum_exists($typeName); $isClass = $typeName && class_exists($typeName); $isRequest = $typeName && is_a(static::$requestClass, $typeName, true); $isModel = $typeName && is_a($typeName, Model::class, true); $isThinkModel = $typeName && is_a($typeName, ThinkModel::class, true); $metadata = [ 'name' => $parameter->name, 'type' => $typeName, 'hasDefault' => $hasDefault, 'default' => $hasDefault ? $parameter->getDefaultValue() : null, 'isRequest' => $isRequest, 'isEnum' => $isEnum, 'isClass' => $isClass, 'isModel' => $isModel, 'isThinkModel' => $isThinkModel, ]; // Pre-compute enum case mappings to avoid per-request ReflectionEnum if ($isEnum) { $enumReflection = new ReflectionEnum($typeName); $enumCases = []; $enumBackedValues = []; $isBacked = $enumReflection->isBacked(); foreach ($enumReflection->getCases() as $case) { $caseValue = $case->getValue(); $enumCases[$case->getName()] = $caseValue; if ($isBacked) { $enumBackedValues[$caseValue->value] = $caseValue; } } $metadata['enumCases'] = $enumCases; $metadata['enumBackedValues'] = $enumBackedValues; $metadata['enumIsBacked'] = $isBacked; } // Pre-compute class constructor info to avoid per-request ReflectionClass if ($isClass && !$isRequest && !$isEnum && !$isModel && !$isThinkModel) { $classRef = new ReflectionClass($typeName); $constructor = $classRef->getConstructor(); $metadata['hasConstructor'] = $constructor !== null; if ($constructor) { // Pre-cache constructor reflector for use by getReflector() $constructorKey = 'method:' . $typeName . '::__construct'; if (!isset(static::$reflectorCache[$constructorKey])) { static::$reflectorCache[$constructorKey] = $constructor; } } } $metadataList[] = $metadata; } if ($cacheKey !== null) { static::$parameterMetadataCache[$cacheKey] = $metadataList; if (count(static::$parameterMetadataCache) > 1024) { unset(static::$parameterMetadataCache[key(static::$parameterMetadataCache)]); } } return $metadataList; } /** * Get parameter metadata cache key. * @param ReflectionFunctionAbstract $reflector * @return string|null */ protected static function getParameterMetadataCacheKey(ReflectionFunctionAbstract $reflector): ?string { if ($reflector instanceof ReflectionMethod) { return 'method:' . $reflector->getDeclaringClass()->getName() . '::' . $reflector->getName(); } if ($reflector instanceof ReflectionFunction && $reflector->isClosure()) { return null; } if ($reflector instanceof ReflectionFunction) { return 'func:' . $reflector->getName(); } return null; } /** * Container. * @param string $plugin * @return ContainerInterface */ public static function container(string $plugin = '') { return static::config($plugin, 'container'); } /** * Get request. * @return Request|\support\Request */ public static function request() { return Context::get(Request::class); } /** * Get worker. * @return Worker */ public static function worker(): ?Worker { return static::$worker; } /** * Find Route. * @param TcpConnection $connection * @param string $path * @param string $key * @param $request * @param $status * @return bool * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface * @throws ReflectionException|Throwable */ protected static function findRoute(TcpConnection $connection, string $path, string $key, $request, &$status): bool { $routeInfo = Route::dispatch($request->method(), $path); if ($routeInfo[0] === Dispatcher::FOUND) { $status = 200; $routeInfo[0] = 'route'; $callback = $routeInfo[1]['callback']; $route = clone $routeInfo[1]['route']; $app = $controller = $action = ''; $args = !empty($routeInfo[2]) ? $routeInfo[2] : []; if ($args) { $route->setParams($args); } $args = array_merge($route->param(), $args); if (is_array($callback)) { $controller = $callback[0]; $plugin = static::getPluginByClass($controller); $app = static::getAppByController($controller); $action = static::getRealMethod($controller, $callback[1]) ?? ''; } else { $plugin = static::getPluginByPath($path); } $callback = static::getCallback($plugin, $app, $callback, $args, true, $route); static::collectCallbacks($key, [$callback, $plugin, $app, $controller ?: '', $action, $route]); [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key]; static::send($connection, $callback($request), $request); return true; } $status = $routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED ? 405 : 404; return false; } /** * Find File. * @param TcpConnection $connection * @param string $path * @param string $key * @param $request * @return bool * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface * @throws ReflectionException */ protected static function findFile(TcpConnection $connection, string $path, string $key, $request): bool { if (preg_match('/%[0-9a-f]{2}/i', $path)) { $path = urldecode($path); if (static::unsafeUri($connection, $path, $request)) { return true; } } $pathExplodes = explode('/', trim($path, '/')); $plugin = ''; if (isset($pathExplodes[1]) && $pathExplodes[0] === 'app') { $plugin = $pathExplodes[1]; $publicDir = static::config($plugin, 'app.public_path') ?: BASE_PATH . "/plugin/$pathExplodes[1]/public"; $path = substr($path, strlen("/app/$pathExplodes[1]/")); } else { $publicDir = static::$publicPath; } $file = "$publicDir/$path"; if (!is_file($file)) { return false; } if (pathinfo($file, PATHINFO_EXTENSION) === 'php') { if (!static::config($plugin, 'app.support_php_files', false)) { return false; } static::collectCallbacks($key, [function () use ($file) { return static::execPhpFile($file); }, $plugin, '', '', '', null]); [, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key]; static::send($connection, static::execPhpFile($file), $request); return true; } if (!static::config($plugin, 'static.enable', false)) { return false; } static::collectCallbacks($key, [static::getCallback($plugin, '__static__', function ($request) use ($file, $plugin) { clearstatcache(true, $file); if (!is_file($file)) { $callback = static::getFallback($plugin); return $callback($request); } return (new Response())->file($file); }, [], false), '', '', '', '', null]); [$callback, $request->plugin, $request->app, $request->controller, $request->action, $request->route] = static::$callbacks[$key]; static::send($connection, $callback($request), $request); return true; } /** * Send. * @param TcpConnection|mixed $connection * @param mixed|Response $response * @param Request|mixed $request * @return void */ protected static function send($connection, $response, $request) { Context::destroy(); // Remove the reference of request to session. unset($request->context['session']); $keepAlive = $request->header('connection'); if ($keepAlive === null) { if ($request->protocolVersion() === '1.1') { $connection->send($response); return; } } elseif (\strcasecmp($keepAlive, 'keep-alive') === 0) { $connection->send($response); return; } if ($response instanceof Response && $response->getHeader('Transfer-Encoding') === 'chunked') { $connection->send($response); return; } $connection->close($response); } /** * ParseControllerAction. * @param string $path * @return array|false * @throws ReflectionException */ protected static function parseControllerAction(string $path) { $path = str_replace(['-', '//'], ['', '/'], $path); if (static::containsPathTraversal($path)) { return false; } static $cache = []; if (isset($cache[$path])) { return $cache[$path]; } $pathExplode = explode('/', trim($path, '/')); $isPlugin = isset($pathExplode[1]) && $pathExplode[0] === 'app'; $configPrefix = $isPlugin ? "plugin.$pathExplode[1]." : ''; $pathPrefix = $isPlugin ? "/app/$pathExplode[1]" : ''; $classPrefix = $isPlugin ? "plugin\\$pathExplode[1]" : ''; $suffix = Config::get("{$configPrefix}app.controller_suffix", ''); $relativePath = trim(substr($path, strlen($pathPrefix)), '/'); $pathExplode = $relativePath ? explode('/', $relativePath) : []; $action = 'index'; if (!$controllerAction = static::guessControllerAction($pathExplode, $action, $suffix, $classPrefix)) { if (count($pathExplode) <= 1) { return false; } $action = end($pathExplode); unset($pathExplode[count($pathExplode) - 1]); $controllerAction = static::guessControllerAction($pathExplode, $action, $suffix, $classPrefix); } if ($controllerAction && !isset($path[256])) { $cache[$path] = $controllerAction; if (count($cache) > 1024) { unset($cache[key($cache)]); } } return $controllerAction; } /** * GuessControllerAction. * @param $pathExplode * @param $action * @param $suffix * @param $classPrefix * @return array|false * @throws ReflectionException */ protected static function guessControllerAction($pathExplode, $action, $suffix, $classPrefix) { $map[] = trim("$classPrefix\\app\\controller\\" . implode('\\', $pathExplode), '\\'); foreach ($pathExplode as $index => $section) { $tmp = $pathExplode; array_splice($tmp, $index, 1, [$section, 'controller']); $map[] = trim("$classPrefix\\" . implode('\\', array_merge(['app'], $tmp)), '\\'); } foreach ($map as $item) { $map[] = $item . '\\index'; } foreach ($map as $controllerClass) { // Remove xx\xx\controller if (substr($controllerClass, -11) === '\\controller') { continue; } $controllerClass .= $suffix; if ($controllerAction = static::getControllerAction($controllerClass, $action)) { return $controllerAction; } } return false; } /** * GetControllerAction. * @param string $controllerClass * @param string $action * @return array|false * @throws ReflectionException */ protected static function getControllerAction(string $controllerClass, string $action) { // Disable calling magic methods if (strpos($action, '__') === 0) { return false; } if (($controllerClass = static::getController($controllerClass)) && ($action = static::getAction($controllerClass, $action))) { return [ 'plugin' => static::getPluginByClass($controllerClass), 'app' => static::getAppByController($controllerClass), 'controller' => $controllerClass, 'action' => $action ]; } return false; } /** * GetController. * @param string $controllerClass * @return string|false * @throws ReflectionException */ protected static function getController(string $controllerClass) { if (class_exists($controllerClass)) { return (new ReflectionClass($controllerClass))->name; } $explodes = explode('\\', strtolower(ltrim($controllerClass, '\\'))); $basePath = $explodes[0] === 'plugin' ? BASE_PATH . '/plugin' : static::$appPath; unset($explodes[0]); $fileName = array_pop($explodes) . '.php'; $found = true; foreach ($explodes as $pathSection) { if (!$found) { break; } $dirs = Util::scanDir($basePath, false); $found = false; foreach ($dirs as $name) { $path = "$basePath/$name"; if (is_dir($path) && strtolower($name) === $pathSection) { $basePath = $path; $found = true; break; } } } if (!$found) { return false; } foreach (scandir($basePath) ?: [] as $name) { if (strtolower($name) === $fileName) { require_once "$basePath/$name"; if (class_exists($controllerClass, false)) { return (new ReflectionClass($controllerClass))->name; } } } return false; } /** * GetAction. * @param string $controllerClass * @param string $action * @return string|false */ protected static function getAction(string $controllerClass, string $action) { $methods = get_class_methods($controllerClass); $lowerAction = strtolower($action); $found = false; foreach ($methods as $candidate) { if (strtolower($candidate) === $lowerAction) { $action = $candidate; $found = true; break; } } if ($found) { return $action; } // Action is not public method if (method_exists($controllerClass, $action)) { return false; } if (method_exists($controllerClass, '__call')) { return $action; } return false; } /** * GetPluginByClass. * @param string $controllerClass * @return mixed|string */ public static function getPluginByClass(string $controllerClass) { $controllerClass = trim($controllerClass, '\\'); $tmp = explode('\\', $controllerClass, 3); if ($tmp[0] !== 'plugin') { return ''; } return $tmp[1] ?? ''; } /** * GetPluginByPath. * @param string $path * @return mixed|string */ public static function getPluginByPath(string $path) { $path = trim($path, '/'); $tmp = explode('/', $path, 3); if ($tmp[0] !== 'app') { return ''; } $plugin = $tmp[1] ?? ''; if ($plugin && !static::config('', "plugin.$plugin.app")) { return ''; } return $plugin; } /** * GetAppByController. * @param string $controllerClass * @return mixed|string */ protected static function getAppByController(string $controllerClass) { $controllerClass = trim($controllerClass, '\\'); $tmp = explode('\\', $controllerClass, 5); $pos = $tmp[0] === 'plugin' ? 3 : 1; if (!isset($tmp[$pos])) { return ''; } return strtolower($tmp[$pos]) === 'controller' ? '' : $tmp[$pos]; } /** * ExecPhpFile. * @param string $file * @return false|string */ public static function execPhpFile(string $file) { ob_start(); // Try to include php file. try { include $file; } catch (Throwable $e) { ob_get_clean(); throw $e; } return ob_get_clean(); } /** * GetRealMethod. * @param string $class * @param string $method * @return string */ protected static function getRealMethod(string $class, string $method): string { $method = strtolower($method); $methods = get_class_methods($class); foreach ($methods as $candidate) { if (strtolower($candidate) === $method) { return $candidate; } } return $method; } /** * Config. * @param string $plugin * @param string $key * @param mixed $default * @return mixed */ protected static function config(string $plugin, string $key, mixed $default = null) { return Config::get($plugin ? "plugin.$plugin.$key" : $key, $default); } /** * @param mixed $data * @return string */ protected static function stringify($data): string { $type = gettype($data); switch ($type) { case 'boolean': return $data ? 'true' : 'false'; case 'NULL': return 'NULL'; case 'array': return 'Array'; case 'object': if (!method_exists($data, '__toString')) { return 'Object'; } default: return (string)$data; } } } ================================================ FILE: src/Bootstrap.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman; use Workerman\Worker; interface Bootstrap { /** * onWorkerStart * * @param Worker|null $worker * @return mixed */ public static function start(?Worker $worker); } ================================================ FILE: src/Config.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman; use FilesystemIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use function array_replace_recursive; use function array_reverse; use function count; use function explode; use function in_array; use function is_array; use function is_dir; use function is_file; use function key; use function str_replace; class Config { /** * @var array */ protected static $config = []; /** * @var string */ protected static $configPath = ''; /** * @var bool */ protected static $loaded = false; /** * @var array Flat cache for repeated get() lookups. */ protected static $flatCache = []; /** * Load. * @param string $configPath * @param array $excludeFile * @param string|null $key * @return void */ public static function load(string $configPath, array $excludeFile = [], ?string $key = null) { static::$configPath = $configPath; static::$flatCache = []; if (!$configPath) { return; } static::$loaded = false; $config = static::loadFromDir($configPath, $excludeFile); if (!$config) { static::$loaded = true; return; } if ($key !== null) { foreach (array_reverse(explode('.', $key)) as $k) { $config = [$k => $config]; } } static::$config = array_replace_recursive(static::$config, $config); static::formatConfig(); static::$loaded = true; } /** * This deprecated method will certainly be removed in the future. * @param string $configPath * @param array $excludeFile * @return void * @deprecated */ public static function reload(string $configPath, array $excludeFile = []) { static::load($configPath, $excludeFile); } /** * Clear. * @return void */ public static function clear() { static::$config = []; static::$flatCache = []; } /** * FormatConfig. * @return void */ protected static function formatConfig() { $config = static::$config; // Merge log config foreach ($config['plugin'] ?? [] as $firm => $projects) { if (isset($projects['app'])) { foreach ($projects['log'] ?? [] as $key => $item) { $config['log']["plugin.$firm.$key"] = $item; } } foreach ($projects as $name => $project) { if (!is_array($project)) { continue; } foreach ($project['log'] ?? [] as $key => $item) { $config['log']["plugin.$firm.$name.$key"] = $item; } } } // Merge database config foreach ($config['plugin'] ?? [] as $firm => $projects) { if (isset($projects['app'])) { foreach ($projects['database']['connections'] ?? [] as $key => $connection) { $config['database']['connections']["plugin.$firm.$key"] = $connection; } } foreach ($projects as $name => $project) { if (!is_array($project)) { continue; } foreach ($project['database']['connections'] ?? [] as $key => $connection) { $config['database']['connections']["plugin.$firm.$name.$key"] = $connection; } } } if (!empty($config['database']['connections'])) { $config['database']['default'] = $config['database']['default'] ?? key($config['database']['connections']); } // Merge thinkorm config foreach ($config['plugin'] ?? [] as $firm => $projects) { if (isset($projects['app'])) { foreach ($projects['thinkorm']['connections'] ?? [] as $key => $connection) { $config['thinkorm']['connections']["plugin.$firm.$key"] = $connection; } foreach ($projects['think-orm']['connections'] ?? [] as $key => $connection) { $config['think-orm']['connections']["plugin.$firm.$key"] = $connection; } } foreach ($projects as $name => $project) { if (!is_array($project)) { continue; } foreach ($project['thinkorm']['connections'] ?? [] as $key => $connection) { $config['thinkorm']['connections']["plugin.$firm.$name.$key"] = $connection; } foreach ($project['think-orm']['connections'] ?? [] as $key => $connection) { $config['think-orm']['connections']["plugin.$firm.$name.$key"] = $connection; } } } if (!empty($config['thinkorm']['connections'])) { $config['thinkorm']['default'] = $config['thinkorm']['default'] ?? key($config['thinkorm']['connections']); } if (!empty($config['think-orm']['connections'])) { $config['think-orm']['default'] = $config['think-orm']['default'] ?? key($config['think-orm']['connections']); } // Merge redis config foreach ($config['plugin'] ?? [] as $firm => $projects) { if (isset($projects['app'])) { foreach ($projects['redis'] ?? [] as $key => $connection) { $config['redis']["plugin.$firm.$key"] = $connection; } } foreach ($projects as $name => $project) { if (!is_array($project)) { continue; } foreach ($project['redis'] ?? [] as $key => $connection) { $config['redis']["plugin.$firm.$name.$key"] = $connection; } } } static::$config = $config; } /** * LoadFromDir. * @param string $configPath * @param array $excludeFile * @return array */ public static function loadFromDir(string $configPath, array $excludeFile = []): array { $allConfig = []; $dirIterator = new RecursiveDirectoryIterator($configPath, FilesystemIterator::FOLLOW_SYMLINKS); $iterator = new RecursiveIteratorIterator($dirIterator); foreach ($iterator as $file) { /** var SplFileInfo $file */ if (is_dir($file) || $file->getExtension() != 'php' || in_array($file->getBaseName('.php'), $excludeFile)) { continue; } $appConfigFile = $file->getPath() . '/app.php'; if (!is_file($appConfigFile)) { continue; } $relativePath = str_replace($configPath . DIRECTORY_SEPARATOR, '', substr($file, 0, -4)); $explode = array_reverse(explode(DIRECTORY_SEPARATOR, $relativePath)); if (count($explode) >= 2) { $appConfig = include $appConfigFile; if (empty($appConfig['enable'])) { continue; } } $config = include $file; foreach ($explode as $section) { $tmp = []; $tmp[$section] = $config; $config = $tmp; } $allConfig = array_replace_recursive($allConfig, $config); } return $allConfig; } /** * Get. * @param string|null $key * @param mixed $default * @return mixed */ public static function get(?string $key = null, mixed $default = null) { if ($key === null) { return static::$config; } if (isset(static::$flatCache[$key])) { return static::$flatCache[$key]; } $keyArray = explode('.', $key); $value = static::$config; $found = true; foreach ($keyArray as $index) { if (!isset($value[$index])) { if (static::$loaded) { return $default; } $found = false; break; } $value = $value[$index]; } if ($found) { if (static::$loaded) { static::$flatCache[$key] = $value; if (count(static::$flatCache) > 1024) { unset(static::$flatCache[key(static::$flatCache)]); } } return $value; } return static::read($key, $default); } /** * Read. * @param string $key * @param mixed $default * @return mixed */ protected static function read(string $key, mixed $default = null) { $path = static::$configPath; if ($path === '') { return $default; } $keys = $keyArray = explode('.', $key); foreach ($keyArray as $index => $section) { unset($keys[$index]); if (is_file($file = "$path/$section.php")) { $config = include $file; return static::find($keys, $config, $default); } if (!is_dir($path = "$path/$section")) { return $default; } } return $default; } /** * Find. * @param array $keyArray * @param mixed $stack * @param mixed $default * @return array|mixed */ protected static function find(array $keyArray, $stack, $default) { if (!is_array($stack)) { return $default; } $value = $stack; foreach ($keyArray as $index) { if (!isset($value[$index])) { return $default; } $value = $value[$index]; } return $value; } } ================================================ FILE: src/Container.php ================================================ instances[$name])) { if (isset($this->definitions[$name])) { $this->instances[$name] = call_user_func($this->definitions[$name], $this); } else { if (!class_exists($name)) { throw new NotFoundException("Class '$name' not found"); } $this->instances[$name] = new $name(); } } return $this->instances[$name]; } /** * Has. * @param string $name * @return bool */ public function has(string $name): bool { return array_key_exists($name, $this->instances) || array_key_exists($name, $this->definitions); } /** * Make. * @param string $name * @param array $constructor * @return mixed * @throws NotFoundException */ public function make(string $name, array $constructor = []) { if (!class_exists($name)) { throw new NotFoundException("Class '$name' not found"); } return new $name(... array_values($constructor)); } /** * AddDefinitions. * @param array $definitions * @return $this */ public function addDefinitions(array $definitions): Container { $this->definitions = array_merge($this->definitions, $definitions); return $this; } } ================================================ FILE: src/Context.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Exception; use RuntimeException; use Throwable; use Webman\Http\Request; use Webman\Http\Response; use function json_encode; /** * Class BusinessException * @package support\exception */ class BusinessException extends RuntimeException { /** * @var array */ protected $data = []; /** * @var bool */ protected $debug = false; /** * Render an exception into an HTTP response. * @param Request $request * @return Response|null */ public function render(Request $request): ?Response { if ($request->expectsJson()) { $code = $this->getCode(); $json = ['code' => $code ?: 500, 'msg' => $this->getMessage(), 'data' => $this->data]; return new Response(200, ['Content-Type' => 'application/json'], json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } return new Response(200, [], $this->getMessage()); } /** * Set data. * @param array|null $data * @return array|$this */ public function data(?array $data = null): array|static { if ($data === null) { return $this->data; } $this->data = $data; return $this; } /** * Set debug. * @param bool|null $value * @return $this|bool */ public function debug(?bool $value = null): bool|static { if ($value === null) { return $this->debug; } $this->debug = $value; return $this; } /** * Get data. * @return array */ public function getData(): array { return $this->data; } /** * Translate message. * @param string $message * @param array $parameters * @param string|null $domain * @param string|null $locale * @return string */ protected function trans(string $message, array $parameters = [], ?string $domain = null, ?string $locale = null): string { $args = []; foreach ($parameters as $key => $parameter) { $args[":$key"] = $parameter; } try { $message = trans($message, $args, $domain, $locale); } catch (Throwable $e) { } foreach ($parameters as $key => $value) { $message = str_replace(":$key", $value, $message); } return $message; } } ================================================ FILE: src/Exception/ExceptionHandler.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Exception; use Psr\Log\LoggerInterface; use Throwable; use Webman\Http\Request; use Webman\Http\Response; use function json_encode; use function nl2br; use function trim; /** * Class Handler * @package support\exception */ class ExceptionHandler implements ExceptionHandlerInterface { /** * @var LoggerInterface */ protected $logger = null; /** * @var bool */ protected $debug = false; /** * @var array */ public $dontReport = []; /** * ExceptionHandler constructor. * @param $logger * @param $debug */ public function __construct($logger, $debug) { $this->logger = $logger; $this->debug = $debug; } /** * @param Throwable $exception * @return void */ public function report(Throwable $exception) { if ($this->shouldntReport($exception)) { return; } $logs = ''; if ($request = \request()) { $logs = $request->getRealIp() . ' ' . $request->method() . ' ' . trim($request->fullUrl(), '/'); } $this->logger->error($logs . PHP_EOL . $exception); } /** * @param Request $request * @param Throwable $exception * @return Response */ public function render(Request $request, Throwable $exception): Response { if (method_exists($exception, 'render') && ($response = $exception->render($request))) { return $response; } $code = $exception->getCode(); if ($request->expectsJson()) { $json = ['code' => $code ?: 500, 'msg' => $this->debug ? $exception->getMessage() : 'Server internal error']; $this->debug && $json['traces'] = (string)$exception; return new Response(200, ['Content-Type' => 'application/json'], json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } $error = $this->debug ? nl2br((string)$exception) : 'Server internal error'; return new Response(500, [], $error); } /** * @param Throwable $e * @return bool */ protected function shouldntReport(Throwable $e): bool { foreach ($this->dontReport as $type) { if ($e instanceof $type) { return true; } } return false; } /** * Compatible $this->_debug * * @param string $name * @return bool|null */ public function __get(string $name) { if ($name === '_debug') { return $this->debug; } return null; } } ================================================ FILE: src/Exception/ExceptionHandlerInterface.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Exception; use Throwable; use Webman\Http\Request; use Webman\Http\Response; interface ExceptionHandlerInterface { /** * @param Throwable $exception * @return mixed */ public function report(Throwable $exception); /** * @param Request $request * @param Throwable $exception * @return Response */ public function render(Request $request, Throwable $exception): Response; } ================================================ FILE: src/Exception/FileException.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Exception; use RuntimeException; /** * Class FileException * @package Webman\Exception */ class FileException extends RuntimeException { } ================================================ FILE: src/Exception/NotFoundException.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Exception; use Psr\Container\NotFoundExceptionInterface; /** * Class NotFoundException * @package Webman\Exception */ class NotFoundException extends \Exception implements NotFoundExceptionInterface { } ================================================ FILE: src/File.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman; use SplFileInfo; use Webman\Exception\FileException; use function chmod; use function is_dir; use function mkdir; use function pathinfo; use function restore_error_handler; use function set_error_handler; use function sprintf; use function strip_tags; use function umask; class File extends SplFileInfo { /** * Move. * @param string $destination * @return File */ public function move(string $destination): File { set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); $path = pathinfo($destination, PATHINFO_DIRNAME); if (!is_dir($path) && !mkdir($path, 0777, true)) { restore_error_handler(); throw new FileException(sprintf('Unable to create the "%s" directory (%s)', $path, strip_tags($error))); } if (!rename($this->getPathname(), $destination)) { restore_error_handler(); throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $destination, strip_tags($error))); } restore_error_handler(); @chmod($destination, 0666 & ~umask()); return new self($destination); } } ================================================ FILE: src/Finder/ControllerFinder.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Finder; use InvalidArgumentException; use Webman\Config; use function is_array; use function is_dir; use function preg_match; use function preg_quote; use function scandir; use function str_starts_with; /** * ControllerFinder * * Discover controller files in main app and/or plugins. * * Scope examples: * - null : main app only * - '*' : main app + all enabled plugins * - 'plugin.*' : all enabled plugins * - 'plugin.xxx': single plugin (strict: throws when plugin directory/config missing) */ class ControllerFinder { /** * Find controller files by scope. * * @param string|null $scope * @return FileInfo[] */ public static function files(?string $scope = null): array { $roots = static::resolveRoots($scope); if (!$roots) { return []; } $resultsByPath = []; foreach ($roots as $root) { $dir = $root['dir']; $suffix = $root['suffix'] ?? ''; $controllerFiles = static::findControllerFiles($dir, $suffix); foreach ($controllerFiles as $file) { $resultsByPath[$file->getPathname()] = $file; } } return array_values($resultsByPath); } /** * Resolve search roots by scope. * * @param string|null $scope * @return array */ protected static function resolveRoots(?string $scope): array { if ($scope === null) { return static::mainAppRoots(); } if ($scope === '*') { return array_merge(static::mainAppRoots(), static::allPluginRoots()); } if ($scope === 'plugin.*') { return static::allPluginRoots(); } if (str_starts_with($scope, 'plugin.')) { $plugin = substr($scope, strlen('plugin.')); if ($plugin === '' || $plugin === '*') { throw new InvalidArgumentException("Invalid controller scope: $scope"); } return static::singlePluginRoots($plugin); } throw new InvalidArgumentException("Invalid controller scope: $scope"); } /** * Main app roots. * * @return array */ protected static function mainAppRoots(): array { $roots = []; $appRoot = app_path(); if (is_dir($appRoot)) { $roots[] = [ 'dir' => $appRoot, 'suffix' => (string)Config::get('app.controller_suffix', ''), ]; } return $roots; } /** * Roots for all enabled plugins. * * Rule (A): if plugin app config is missing/empty, skip it silently. * * @return array */ protected static function allPluginRoots(): array { $roots = []; $pluginBase = base_path('plugin'); if (!is_dir($pluginBase)) { return []; } foreach (scandir($pluginBase) ?: [] as $entry) { if ($entry === '.' || $entry === '..') { continue; } if (!static::isValidIdentifier($entry)) { continue; } $pluginDir = $pluginBase . DIRECTORY_SEPARATOR . $entry; if (!is_dir($pluginDir)) { continue; } // Only load enabled plugins (same semantics as Route::loadAnnotationRoutes()). $pluginAppConfig = Config::get("plugin.$entry.app"); if (!$pluginAppConfig) { continue; } $pluginAppDir = $pluginDir . DIRECTORY_SEPARATOR . 'app'; if (!is_dir($pluginAppDir)) { continue; } $roots[] = [ 'dir' => $pluginAppDir, 'suffix' => is_array($pluginAppConfig) ? (string)($pluginAppConfig['controller_suffix'] ?? '') : (string)Config::get("plugin.$entry.app.controller_suffix", ''), ]; } return $roots; } /** * Roots for a single plugin (strict). * * @param string $plugin * @return array */ protected static function singlePluginRoots(string $plugin): array { if (!static::isValidIdentifier($plugin)) { throw new InvalidArgumentException("Invalid plugin identifier: $plugin"); } $pluginBase = base_path('plugin'); $pluginDir = $pluginBase . DIRECTORY_SEPARATOR . $plugin; if (!is_dir($pluginDir)) { throw new InvalidArgumentException("Plugin directory not found: $plugin"); } $pluginAppConfig = Config::get("plugin.$plugin.app"); if (!$pluginAppConfig) { throw new InvalidArgumentException("Plugin app config not found or empty: plugin.$plugin.app"); } $pluginAppDir = $pluginDir . DIRECTORY_SEPARATOR . 'app'; if (!is_dir($pluginAppDir)) { throw new InvalidArgumentException("Plugin app directory not found: plugin/$plugin/app"); } return [[ 'dir' => $pluginAppDir, 'suffix' => is_array($pluginAppConfig) ? (string)($pluginAppConfig['controller_suffix'] ?? '') : (string)Config::get("plugin.$plugin.app.controller_suffix", ''), ]]; } /** * Find controller files. * * @param string $rootDir * @param string $controllerSuffix * @return FileInfo[] */ protected static function findControllerFiles(string $rootDir, string $controllerSuffix = ''): array { $controllerPathRegex = $controllerSuffix !== '' ? ('/(^|[\/\\\\])controller[\/\\\\].*' . preg_quote($controllerSuffix, '/') . '\.php$/i') : '/(^|[\/\\\\])controller[\/\\\\].+\.php$/i'; $finder = Finder::in($rootDir) ->files() ->path($controllerPathRegex) ->hasAttributes(true) ->typeIn(['class']) ->psr4(true); return $finder->find(); } /** * Is valid identifier (plugin name). * * @param string $name * @return bool */ protected static function isValidIdentifier(string $name): bool { return $name !== '' && (bool)preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name); } } ================================================ FILE: src/Finder/FileInfo.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Finder; use Webman\File; /** * Class FileInfo * @package Webman\Finder */ class FileInfo extends File { /** * @var array PHP file meta info (hasAttributes, type, class, psr4, etc.) */ protected array $meta = []; /** * @var string Root directory this file belongs to */ protected string $rootDir = ''; // Root namespace is intentionally not stored. /** * Constructor. * @param string $path * @param array $meta * @param string $rootDir */ public function __construct(string $path, array $meta = [], string $rootDir = '') { parent::__construct($path); $this->meta = $meta; $this->rootDir = $rootDir; } /** * Get PHP file meta info. * @return array */ public function meta(): array { return $this->meta; } /** * Get declared class (FQCN) from meta. * Example: "App\\Controller\\IndexController" * * @return string|null */ public function class(): ?string { $class = $this->meta['class'] ?? null; if (!is_string($class) || $class === '') { return null; } return ltrim($class, '\\'); } /** * Get declared short class name (without namespace). * Example: "IndexController" * * @return string|null */ public function className(): ?string { $fqcn = $this->class(); if ($fqcn === null) { return null; } $pos = strrpos($fqcn, '\\'); return $pos === false ? $fqcn : substr($fqcn, $pos + 1); } /** * Get declared namespace. * Example: "App\\Controller" * * @return string|null */ public function namespace(): ?string { $fqcn = $this->class(); if ($fqcn === null) { return null; } $pos = strrpos($fqcn, '\\'); return $pos === false ? null : substr($fqcn, 0, $pos); } /** * Set meta info. * @param array $meta * @return $this */ public function setMeta(array $meta): static { $this->meta = $meta; return $this; } /** * Get root directory. * @return string */ public function rootDir(): string { return $this->rootDir; } /** * Get relative pathname from root directory. * @return string */ public function relativePathname(): string { if ($this->rootDir === '') { return $this->getPathname(); } $rootDir = rtrim(str_replace('\\', '/', $this->rootDir), '/'); $pathname = str_replace('\\', '/', $this->getPathname()); $rootLen = strlen($rootDir); if (strncasecmp($pathname, $rootDir, $rootLen) === 0) { return ltrim(substr($pathname, $rootLen), '/'); } return $this->getPathname(); } } ================================================ FILE: src/Finder/Finder.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Finder; use FilesystemIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use RecursiveCallbackFilterIterator; /** * Class Finder * File finder with PHP file caching support. * @package Webman\Finder */ class Finder { /** * @var string[] Root directories */ protected array $roots = []; /** * @var bool Only match files (not directories) */ protected bool $onlyFiles = false; /** * @var array File name patterns (glob or regex) */ protected array $names = []; /** * @var array Path patterns (regex) */ protected array $paths = []; /** * @var array Directories to exclude */ protected array $excludeDirs = ['vendor', 'runtime', '.git', 'storage', 'tests', 'node_modules']; /** * @var bool Enable PHP meta analysis */ protected bool $phpMetaEnabled = false; /** * @var bool|null Filter by hasAttributes */ protected ?bool $filterHasAttributes = null; /** * @var array|null Filter by type */ protected ?array $filterTypes = null; /** * @var bool|null Filter by psr4 */ protected ?bool $filterPsr4 = null; /** * @var array PHP file cache: [path => meta, ...] */ protected static array $phpCache = []; /** * @var array Cache dirty flags by root */ protected static array $cacheDirty = []; /** * @var string Cache directory */ protected static string $cacheDir = ''; /** * Create a new Finder instance with root directories. * @param string|array $dirs * @return static */ public static function in(string|array $dirs): static { $instance = new static(); foreach ((array)$dirs as $dir) { if (is_dir($dir)) { $instance->roots[] = static::normalizePath($dir); } } return $instance; } /** * Create a new Finder instance. * Use in() to set search directories. * @return static */ public static function create(): static { return new static(); } /** * Set cache directory. * @param string $dir * @return void */ public static function setCacheDir(string $dir): void { static::$cacheDir = rtrim($dir, '/\\'); } /** * Get cache directory. * @return string */ protected static function getCacheDir(): string { if (static::$cacheDir === '') { static::$cacheDir = rtrim(runtime_path('cache/framework/finder'), '/\\'); if (!is_dir(static::$cacheDir)) { @mkdir(static::$cacheDir, 0755, true); } } return static::$cacheDir; } /** * Only match files. * @return $this */ public function files(): static { $this->onlyFiles = true; return $this; } /** * Filter by file name pattern (glob or regex). * @param string|array $patterns * @return $this */ public function name(string|array $patterns): static { $this->names = array_merge($this->names, (array)$patterns); return $this; } /** * Filter by path pattern (regex). * @param string|array $patterns * @return $this */ public function path(string|array $patterns): static { $this->paths = array_merge($this->paths, (array)$patterns); return $this; } /** * Exclude directories. * @param string|array $dirs * @return $this */ public function exclude(string|array $dirs): static { $this->excludeDirs = array_merge($this->excludeDirs, (array)$dirs); return $this; } /** * Set exclude directories (replace default). * @param array $dirs * @return $this */ public function excludeDirs(array $dirs): static { $this->excludeDirs = $dirs; return $this; } /** * Enable PHP meta analysis and caching. * @return $this */ public function withPhpMeta(): static { $this->phpMetaEnabled = true; return $this; } /** * Whether any PHP meta filters are requested. * @return bool */ protected function phpFiltersRequested(): bool { return $this->filterHasAttributes !== null || $this->filterTypes !== null || $this->filterPsr4 !== null; } /** * Filter by hasAttributes. * @param bool $value * @return $this */ public function hasAttributes(bool $value): static { // PHP-specific filter: enable PHP meta automatically. $this->phpMetaEnabled = true; $this->filterHasAttributes = $value; return $this; } /** * Filter by type (class, interface, trait, enum, non_class). * @param array $types * @return $this */ public function typeIn(array $types): static { // PHP-specific filter: enable PHP meta automatically. $this->phpMetaEnabled = true; $this->filterTypes = $types; return $this; } /** * Filter by PSR-4 compliance. * @param bool $value * @return $this */ public function psr4(bool $value): static { // PHP-specific filter: enable PHP meta automatically. $this->phpMetaEnabled = true; $this->filterPsr4 = $value; return $this; } /** * Find files and return FileInfo array. * @return FileInfo[] */ public function find(): array { $results = []; $phpFiltersRequested = $this->phpFiltersRequested(); // phpMetaEnabled can be turned on explicitly (withPhpMeta) or implicitly (by PHP filters). $phpMetaEnabled = $this->phpMetaEnabled || $phpFiltersRequested; foreach ($this->roots as $rootDir) { // Load cache for this root if ($phpMetaEnabled) { $this->loadCache($rootDir); } // Scan directory $files = $this->scanDirectory($rootDir); // Apply filters foreach ($files as $filePath) { // Apply name filter if (!$this->matchesName($filePath)) { continue; } // Apply path filter if (!$this->matchesPath($filePath, $rootDir)) { continue; } // Get or compute PHP meta $meta = []; if ($phpMetaEnabled) { // If any PHP filters were requested, skip non-PHP files. if ($phpFiltersRequested && !$this->isPhpFile($filePath)) { continue; } if ($this->isPhpFile($filePath)) { $meta = $this->getPhpMeta($filePath, $rootDir); // Apply PHP meta filters if ($phpFiltersRequested && !$this->matchesPhpFilters($meta, $filePath, $rootDir)) { continue; } } } $results[] = new FileInfo($filePath, $meta, $rootDir); } // Save cache if dirty if ($phpMetaEnabled) { $this->saveCache($rootDir); } } return $results; } /** * Find files and return paths array. * @return string[] */ public function findPaths(): array { return array_map(fn(FileInfo $f) => $f->getPathname(), $this->find()); } /** * Scan directory recursively. * @param string $dir * @return array */ protected function scanDirectory(string $dir): array { $files = []; $excludeSet = array_flip($this->excludeDirs); try { $directoryIterator = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); $filterIterator = new RecursiveCallbackFilterIterator( $directoryIterator, function (\SplFileInfo $current) use ($excludeSet) { if ($current->isDir()) { return !isset($excludeSet[$current->getBasename()]); } return true; } ); $iterator = new RecursiveIteratorIterator($filterIterator, RecursiveIteratorIterator::SELF_FIRST); foreach ($iterator as $item) { /** @var \SplFileInfo $item */ $basename = $item->getBasename(); // Skip excluded directories if ($item->isDir()) { continue; } // Skip if only files mode and not a file if ($this->onlyFiles && !$item->isFile()) { continue; } $files[] = static::normalizePath($item->getPathname()); } } catch (\Throwable $e) { // Ignore unreadable directories } return $files; } /** * Check if file matches name patterns. * @param string $filePath * @return bool */ protected function matchesName(string $filePath): bool { if (empty($this->names)) { return true; } $basename = basename($filePath); foreach ($this->names as $pattern) { if ($this->matchPattern($basename, $pattern)) { return true; } } return false; } /** * Check if file matches path patterns. * @param string $filePath * @param string $rootDir * @return bool */ protected function matchesPath(string $filePath, string $rootDir): bool { if (empty($this->paths)) { return true; } // Use relative path for matching $relativePath = $this->getRelativePath($filePath, $rootDir); foreach ($this->paths as $pattern) { if ($this->isRegex($pattern) && preg_match($pattern, $relativePath)) { return true; } if (!$this->isRegex($pattern) && stripos($relativePath, $pattern) !== false) { return true; } } return false; } /** * Match a pattern (glob or regex). * @param string $value * @param string $pattern * @return bool */ protected function matchPattern(string $value, string $pattern): bool { // Regex pattern if ($this->isRegex($pattern)) { return (bool)preg_match($pattern, $value); } // Glob pattern return fnmatch($pattern, $value, FNM_CASEFOLD); } /** * Check if pattern is a regex. * @param string $pattern * @return bool */ protected function isRegex(string $pattern): bool { if ($pattern === '') { return false; } $delimiter = $pattern[0]; if (!in_array($delimiter, ['/', '#', '~', '%'], true)) { return false; } return (bool)preg_match('/^' . preg_quote($delimiter, '/') . '.*' . preg_quote($delimiter, '/') . '[imsxuADU]*$/', $pattern); } /** * Check if file is a PHP file. * @param string $filePath * @return bool */ protected function isPhpFile(string $filePath): bool { return strtolower(pathinfo($filePath, PATHINFO_EXTENSION)) === 'php'; } /** * Get PHP file meta, using cache when possible. * @param string $filePath * @param string $rootDir * @return array */ protected function getPhpMeta(string $filePath, string $rootDir): array { $cacheKey = $this->getCacheKey($rootDir); $mtime = @filemtime($filePath); // Check cache if (isset(static::$phpCache[$cacheKey][$filePath])) { $cached = static::$phpCache[$cacheKey][$filePath]; if (isset($cached['mtime']) && $cached['mtime'] === $mtime) { return $cached; } } // Compute new meta $meta = $this->computePhpMeta($filePath, $mtime); // Update cache static::$phpCache[$cacheKey][$filePath] = $meta; static::$cacheDirty[$cacheKey] = true; return $meta; } /** * Ensure psr4 is computed and cached in meta. * Only computes once per (file, mtime) cache entry. * @param string $filePath * @param string $rootDir * @param array $meta * @return array Updated meta */ protected function ensurePsr4Cached(string $filePath, string $rootDir, array $meta): array { if (array_key_exists('psr4', $meta)) { return $meta; } $psr4 = $this->checkPsr4($filePath, $rootDir, $meta['class'] ?? null); $meta['psr4'] = $psr4; $cacheKey = $this->getCacheKey($rootDir); static::$phpCache[$cacheKey][$filePath] = $meta; static::$cacheDirty[$cacheKey] = true; return $meta; } /** * Compute PHP file meta info. * @param string $filePath * @param int|false $mtime * @return array */ protected function computePhpMeta(string $filePath, int|false $mtime): array { $meta = [ 'mtime' => $mtime ?: 0, 'hasAttributes' => false, 'class' => null, ]; $code = @file_get_contents($filePath); if ($code === false || $code === '') { $meta['type'] = 'non_class'; return $meta; } // Fast check for attributes $meta['hasAttributes'] = str_contains($code, '#['); // Parse to get type and declared class $parseResult = $this->parsePhpFile($code); $meta['type'] = $parseResult['type']; $meta['class'] = $parseResult['class']; return $meta; } /** * Parse PHP file to extract type and declared class. * @param string $code * @return array{type: string, class: string|null} */ protected function parsePhpFile(string $code): array { $result = [ 'type' => 'non_class', 'class' => null, ]; try { $tokens = token_get_all($code); } catch (\Throwable $e) { return $result; } $namespace = ''; $count = count($tokens); $prevSignificant = null; for ($i = 0; $i < $count; $i++) { $token = $tokens[$i]; $id = is_array($token) ? $token[0] : null; // Extract namespace if ($id === T_NAMESPACE) { $ns = ''; for ($j = $i + 1; $j < $count; $j++) { $t = $tokens[$j]; if (is_array($t)) { if ($t[0] === T_STRING || $t[0] === T_NS_SEPARATOR) { $ns .= $t[1]; continue; } if (defined('T_NAME_QUALIFIED') && $t[0] === T_NAME_QUALIFIED) { $ns .= $t[1]; continue; } if ($t[0] === T_WHITESPACE) { continue; } } else { if ($t === ';' || $t === '{') { break; } } } $namespace = trim($ns, '\\'); continue; } // Detect class/interface/trait/enum if ($id === T_CLASS || $id === T_INTERFACE || $id === T_TRAIT || (defined('T_ENUM') && $id === T_ENUM)) { // Skip ::class usage if ($prevSignificant === T_DOUBLE_COLON) { $prevSignificant = null; continue; } // Skip anonymous class if ($id === T_CLASS && $prevSignificant === T_NEW) { continue; } // Determine type $type = match ($id) { T_CLASS => 'class', T_INTERFACE => 'interface', T_TRAIT => 'trait', default => defined('T_ENUM') && $id === T_ENUM ? 'enum' : 'class', }; // Extract class name for ($j = $i + 1; $j < $count; $j++) { $t = $tokens[$j]; if (!is_array($t)) { continue; } if ($t[0] === T_WHITESPACE) { continue; } if ($t[0] === T_STRING) { $className = $t[1]; $result['type'] = $type; $result['class'] = $namespace !== '' ? ($namespace . '\\' . $className) : $className; return $result; // Return first declaration only } break; } } // Track previous significant token if (is_array($token)) { if ($id !== T_WHITESPACE && $id !== T_COMMENT && $id !== T_DOC_COMMENT) { $prevSignificant = $id; } } else { if (trim($token) !== '') { $prevSignificant = $token; } } } return $result; } /** * Check if meta matches PHP filters. * @param array $meta * @param string $filePath * @param string $rootDir * @return bool */ protected function matchesPhpFilters(array $meta, string $filePath, string $rootDir): bool { // Filter by hasAttributes if ($this->filterHasAttributes !== null && $meta['hasAttributes'] !== $this->filterHasAttributes) { return false; } // Filter by type if ($this->filterTypes !== null && !in_array($meta['type'], $this->filterTypes, true)) { return false; } // Filter by PSR-4 if ($this->filterPsr4 !== null) { $meta = $this->ensurePsr4Cached($filePath, $rootDir, $meta); $psr4Ok = $meta['psr4']; if ($psr4Ok !== $this->filterPsr4) { return false; } } return true; } /** * Check if file complies with PSR-4. * @param string $filePath * @param string $rootDir * @param string|null $declaredClass * @return bool */ protected function checkPsr4(string $filePath, string $rootDir, ?string $declaredClass): bool { if ($declaredClass === null) { return false; } // Namespace-agnostic check: // declared class must end with the PSR-4 relative class path derived from file path. $relativePath = $this->getRelativePath($filePath, $rootDir); if ($relativePath === '' || !str_ends_with($relativePath, '.php')) { return false; } $relativeClassPath = substr($relativePath, 0, -4); $relativeClassPath = str_replace('/', '\\', $relativeClassPath); if (!$this->isValidPsr4ClassPath($relativeClassPath)) { return false; } $declaredClass = ltrim($declaredClass, '\\'); $suffix = '\\' . $relativeClassPath; return $declaredClass === $relativeClassPath || str_ends_with($declaredClass, $suffix); } /** * Derive expected class name from file path (PSR-4 style). * @param string $filePath * @param string $rootDir * @param string $rootNamespace * @return string|null */ protected function classFromFile(string $filePath, string $rootDir, string $rootNamespace): ?string { $rootDir = rtrim(static::normalizePath($rootDir), '/'); $filePath = static::normalizePath($filePath); $rootLen = strlen($rootDir); if (strncasecmp($filePath, $rootDir, $rootLen) !== 0) { return null; } $relative = ltrim(substr($filePath, $rootLen), '/'); if ($relative === '' || !str_ends_with($relative, '.php')) { return null; } $relative = substr($relative, 0, -4); // Remove .php $relative = str_replace('/', '\\', $relative); if (!$this->isValidPsr4ClassPath($relative)) { return null; } return rtrim($rootNamespace, '\\') . '\\' . $relative; } /** * Check if relative class path is valid PSR-4. * @param string $relativeClassPath * @return bool */ protected function isValidPsr4ClassPath(string $relativeClassPath): bool { return (bool)preg_match('/^[A-Za-z_][A-Za-z0-9_]*(\\\\[A-Za-z_][A-Za-z0-9_]*)*$/', $relativeClassPath); } /** * Get relative path from root directory. * @param string $filePath * @param string $rootDir * @return string */ protected function getRelativePath(string $filePath, string $rootDir): string { $rootDir = rtrim(static::normalizePath($rootDir), '/'); $filePath = static::normalizePath($filePath); $rootLen = strlen($rootDir); if (strncasecmp($filePath, $rootDir, $rootLen) === 0) { return ltrim(substr($filePath, $rootLen), '/'); } return $filePath; } /** * Get cache key for a root directory. * @param string $rootDir * @return string */ protected function getCacheKey(string $rootDir): string { return hash('sha256', $rootDir); } /** * Get cache file path for a root directory. * @param string $rootDir * @return string */ protected function getCacheFile(string $rootDir): string { $cacheKey = $this->getCacheKey($rootDir); return static::getCacheDir() . DIRECTORY_SEPARATOR . "php_files_{$cacheKey}.php"; } /** * Load cache for a root directory. * @param string $rootDir * @return void */ protected function loadCache(string $rootDir): void { $cacheKey = $this->getCacheKey($rootDir); if (isset(static::$phpCache[$cacheKey])) { return; // Already loaded } $cacheFile = $this->getCacheFile($rootDir); if (is_file($cacheFile)) { try { $data = include $cacheFile; if (is_array($data)) { static::$phpCache[$cacheKey] = $data; return; } } catch (\Throwable $e) { // Ignore corrupted cache } } static::$phpCache[$cacheKey] = []; } /** * Save cache for a root directory. * @param string $rootDir * @return void */ protected function saveCache(string $rootDir): void { $cacheKey = $this->getCacheKey($rootDir); if (empty(static::$cacheDirty[$cacheKey])) { return; } $cacheFile = $this->getCacheFile($rootDir); $cacheDir = dirname($cacheFile); if (!is_dir($cacheDir)) { @mkdir($cacheDir, 0755, true); } $data = static::$phpCache[$cacheKey] ?? []; // Clean up deleted files foreach ($data as $path => $meta) { if (!is_file($path)) { unset($data[$path]); } } static::$phpCache[$cacheKey] = $data; $content = " * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Http; use Webman\Route\Route; use function current; use function filter_var; use function ip2long; use function is_array; use function strpos; use const FILTER_FLAG_IPV4; use const FILTER_FLAG_NO_PRIV_RANGE; use const FILTER_FLAG_NO_RES_RANGE; use const FILTER_VALIDATE_IP; /** * Class Request * @package Webman\Http */ class Request extends \Workerman\Protocols\Http\Request { /** * @var string */ public $plugin = null; /** * @var string */ public $app = null; /** * @var string */ public $controller = null; /** * @var string */ public $action = null; /** * @var Route */ public $route = null; /** * @var bool */ protected $isDirty = false; /** * @return mixed|null */ public function all() { return $this->get() + $this->post(); } /** * Input * @param string $name * @param mixed $default * @return mixed */ public function input(string $name, mixed $default = null) { return $this->get($name, $this->post($name, $default)); } /** * Only * @param array $keys * @return array */ public function only(array $keys): array { $all = $this->all(); $result = []; foreach ($keys as $key) { if (isset($all[$key])) { $result[$key] = $all[$key]; } } return $result; } /** * Except * @param array $keys * @return mixed|null */ public function except(array $keys) { $all = $this->all(); foreach ($keys as $key) { unset($all[$key]); } return $all; } /** * File * @param string|null $name * @return UploadFile|UploadFile[]|null */ public function file(?string $name = null): array|null|UploadFile { $files = parent::file($name); if (null === $files) { return $name === null ? [] : null; } if ($name !== null) { // Multi files if (is_array(current($files))) { return $this->parseFiles($files); } return $this->parseFile($files); } $uploadFiles = []; foreach ($files as $name => $file) { // Multi files if (is_array(current($file))) { $uploadFiles[$name] = $this->parseFiles($file); } else { $uploadFiles[$name] = $this->parseFile($file); } } return $uploadFiles; } /** * ParseFile * @param array $file * @return UploadFile */ protected function parseFile(array $file): UploadFile { return new UploadFile($file['tmp_name'], $file['name'], $file['type'], $file['error']); } /** * ParseFiles * @param array $files * @return array */ protected function parseFiles(array $files): array { $uploadFiles = []; foreach ($files as $key => $file) { if (is_array(current($file))) { $uploadFiles[$key] = $this->parseFiles($file); } else { $uploadFiles[$key] = $this->parseFile($file); } } return $uploadFiles; } /** * GetRemoteIp * @return string */ public function getRemoteIp(): string { return $this->connection ? $this->connection->getRemoteIp() : '0.0.0.0'; } /** * GetRemotePort * @return int */ public function getRemotePort(): int { return $this->connection ? $this->connection->getRemotePort() : 0; } /** * GetLocalIp * @return string */ public function getLocalIp(): string { return $this->connection ? $this->connection->getLocalIp() : '0.0.0.0'; } /** * GetLocalPort * @return int */ public function getLocalPort(): int { return $this->connection ? $this->connection->getLocalPort() : 0; } /** * GetRealIp * @param bool $safeMode * @return string */ public function getRealIp(bool $safeMode = true): string { $remoteIp = $this->getRemoteIp(); if ($safeMode && !static::isIntranetIp($remoteIp)) { return $remoteIp; } $ip = $this->header('x-forwarded-for') ?? $this->header('x-real-ip') ?? $this->header('client-ip') ?? $this->header('x-client-ip') ?? $this->header('via') ?? $remoteIp; if (is_string($ip)) { $ip = current(explode(',', $ip)); } return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : $remoteIp; } /** * Url * @return string */ public function url(): string { return '//' . $this->host() . $this->path(); } /** * FullUrl * @return string */ public function fullUrl(): string { return '//' . $this->host() . $this->uri(); } /** * IsAjax * @return bool */ public function isAjax(): bool { return $this->header('X-Requested-With') === 'XMLHttpRequest'; } /** * IsGet * @return bool */ public function isGet(): bool { return $this->method() === 'GET'; } /** * IsPost * @return bool */ public function isPost(): bool { return $this->method() === 'POST'; } /** * IsPjax * @return bool */ public function isPjax(): bool { return (bool)$this->header('X-PJAX'); } /** * ExpectsJson * @return bool */ public function expectsJson(): bool { return ($this->isAjax() && !$this->isPjax()) || $this->acceptJson(); } /** * AcceptJson * @return bool */ public function acceptJson(): bool { return false !== strpos($this->header('accept', ''), 'json'); } /** * IsIntranetIp * @param string $ip * @return bool */ public static function isIntranetIp(string $ip): bool { // Not validate ip . if (!filter_var($ip, FILTER_VALIDATE_IP)) { return false; } // Is intranet ip ? For IPv4, the result of false may not be accurate, so we need to check it manually later . if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return true; } // Manual check only for IPv4 . if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { return false; } // Manual check . $reservedIps = [ 1681915904 => 1686110207, // 100.64.0.0 - 100.127.255.255 3221225472 => 3221225727, // 192.0.0.0 - 192.0.0.255 3221225984 => 3221226239, // 192.0.2.0 - 192.0.2.255 3227017984 => 3227018239, // 192.88.99.0 - 192.88.99.255 3323068416 => 3323199487, // 198.18.0.0 - 198.19.255.255 3325256704 => 3325256959, // 198.51.100.0 - 198.51.100.255 3405803776 => 3405804031, // 203.0.113.0 - 203.0.113.255 3758096384 => 4026531839, // 224.0.0.0 - 239.255.255.255 ]; $ipLong = ip2long($ip); foreach ($reservedIps as $ipStart => $ipEnd) { if (($ipLong >= $ipStart) && ($ipLong <= $ipEnd)) { return true; } } return false; } /** * Set get. * @param array|string $input * @param mixed $value * @return Request */ public function setGet(array|string $input, mixed $value = null): Request { $this->isDirty = true; $input = is_array($input) ? $input : array_merge($this->get(), [$input => $value]); if (isset($this->data)) { $this->data['get'] = $input; } else { $this->_data['get'] = $input; } return $this; } /** * Set post. * @param array|string $input * @param mixed $value * @return Request */ public function setPost(array|string $input, mixed $value = null): Request { $this->isDirty = true; $input = is_array($input) ? $input : array_merge($this->post(), [$input => $value]); if (isset($this->data)) { $this->data['post'] = $input; } else { $this->_data['post'] = $input; } return $this; } /** * Set header. * @param array|string $input * @param mixed $value * @return Request */ public function setHeader(array|string $input, mixed $value = null): Request { $this->isDirty = true; $input = is_array($input) ? $input : array_merge($this->header(), [$input => $value]); if (isset($this->data)) { $this->data['headers'] = $input; } else { $this->_data['headers'] = $input; } return $this; } /** * Destroy */ public function destroy(): void { if ($this->isDirty) { unset($this->data['get'], $this->data['post'], $this->data['headers']); } parent::destroy(); } } ================================================ FILE: src/Http/Response.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Http; use Throwable; use Webman\App; use function filemtime; use function gmdate; /** * Class Response * @package Webman\Http */ class Response extends \Workerman\Protocols\Http\Response { /** * @var Throwable */ protected $exception = null; /** * File * @param string $file * @return $this */ public function file(string $file): Response { if ($this->notModifiedSince($file)) { return $this->withStatus(304); } return $this->withFile($file); } /** * Download * @param string $file * @param string $downloadName * @return $this */ public function download(string $file, string $downloadName = ''): Response { $this->withFile($file); if ($downloadName) { // Sanitize to prevent header injection $downloadName = str_replace(['"', "\r", "\n", "\0"], '', $downloadName); $this->header('Content-Disposition', "attachment; filename=\"$downloadName\""); } return $this; } /** * NotModifiedSince * @param string $file * @return bool */ protected function notModifiedSince(string $file): bool { $ifModifiedSince = App::request()->header('if-modified-since'); if ($ifModifiedSince === null || !is_file($file) || !($mtime = filemtime($file))) { return false; } return $ifModifiedSince === gmdate('D, d M Y H:i:s', $mtime) . ' GMT'; } /** * Exception * @param Throwable|null $exception * @return Throwable|null */ public function exception(?Throwable $exception = null): ?Throwable { if ($exception) { $this->exception = $exception; } return $this->exception; } } ================================================ FILE: src/Http/UploadFile.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Http; use Webman\File; use function pathinfo; /** * Class UploadFile * @package Webman\Http */ class UploadFile extends File { /** * @var string */ protected $uploadName = null; /** * @var string */ protected $uploadMimeType = null; /** * @var int */ protected $uploadErrorCode = null; /** * UploadFile constructor. * * @param string $fileName * @param string $uploadName * @param string $uploadMimeType * @param int $uploadErrorCode */ public function __construct(string $fileName, string $uploadName, string $uploadMimeType, int $uploadErrorCode) { $this->uploadName = $uploadName; $this->uploadMimeType = $uploadMimeType; $this->uploadErrorCode = $uploadErrorCode; parent::__construct($fileName); } /** * GetUploadName * @return string */ public function getUploadName(): ?string { return $this->uploadName; } /** * GetUploadMimeType * @return string */ public function getUploadMimeType(): ?string { return $this->uploadMimeType; } /** * GetUploadExtension * @return string */ public function getUploadExtension(): string { return pathinfo($this->uploadName, PATHINFO_EXTENSION); } /** * GetUploadErrorCode * @return int */ public function getUploadErrorCode(): ?int { return $this->uploadErrorCode; } /** * IsValid * @return bool */ public function isValid(): bool { return $this->uploadErrorCode === UPLOAD_ERR_OK; } /** * GetUploadMineType * @return string * @deprecated */ public function getUploadMineType(): ?string { return $this->uploadMimeType; } } ================================================ FILE: src/Install.php ================================================ 'start.php', 'windows.php' => 'windows.php', ]; /** * Install * @return void */ public static function install() { static::installByRelation(); } /** * Uninstall * @return void */ public static function uninstall() { } /** * InstallByRelation * @return void */ public static function installByRelation() { foreach (static::$pathRelation as $source => $dest) { $parentDir = base_path(dirname($dest)); if (!is_dir($parentDir)) { mkdir($parentDir, 0777, true); } $sourceFile = __DIR__ . "/$source"; copy_dir($sourceFile, base_path($dest), true); echo "Create $dest\r\n"; if (is_file($sourceFile)) { @unlink($sourceFile); } } if (is_file($file = base_path('support/helpers.php'))) { file_put_contents($file, " * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman; use Closure; use ReflectionAttribute; use support\annotation\Middleware as MiddlewareAttribute; use Webman\Route\Route; use ReflectionClass; use ReflectionMethod; use RuntimeException; use function array_merge; use function array_reverse; use function is_array; use function method_exists; class Middleware { /** * @var array */ protected static $instances = []; /** * @var array Cache for controller middleware resolved via reflection/attributes. */ protected static $controllerMiddlewareCache = []; /** * @param mixed $allMiddlewares * @param string $plugin * @return void */ public static function load($allMiddlewares, string $plugin = '') { if (!is_array($allMiddlewares)) { return; } foreach ($allMiddlewares as $appName => $middlewares) { if (!is_array($middlewares)) { throw new RuntimeException('Bad middleware config'); } $pluginKey = $plugin; $appKey = $appName; if ($appKey === '@') { $pluginKey = ''; } elseif (strpos($appKey, 'plugin.') !== false) { $explode = explode('.', $appKey, 4); $pluginKey = $explode[1]; $appKey = $explode[2] ?? ''; } foreach ($middlewares as $className) { if (method_exists($className, 'process')) { static::$instances[$pluginKey][$appKey][] = [$className, 'process']; } else { // @todo Log echo "middleware $className::process not exsits\n"; } } } } /** * @param string $plugin * @param string $appName * @param string|array|Closure $controller * @param Route|null $route * @param bool $withGlobalMiddleware * @return array */ public static function getMiddleware(string $plugin, string $appName, string|array|Closure $controller, Route|null $route, bool $withGlobalMiddleware = true): array { $isController = is_array($controller) && is_string($controller[0]); $globalMiddleware = $withGlobalMiddleware ? static::$instances['']['@'] ?? [] : []; $appGlobalMiddleware = $withGlobalMiddleware && isset(static::$instances[$plugin]['']) ? static::$instances[$plugin][''] : []; $middlewares = $routeMiddlewares = []; // Route middleware if ($route) { foreach (array_reverse($route->getMiddleware()) as $className) { $routeMiddlewares[] = [$className, 'process']; } } if ($isController && $controller[0] && class_exists($controller[0])) { $cacheKey = $controller[0] . '::' . $controller[1]; if (isset(static::$controllerMiddlewareCache[$cacheKey])) { $cached = static::$controllerMiddlewareCache[$cacheKey]; $middlewares = array_merge($cached['before_route'], $routeMiddlewares, $cached['after_route']); } else { $beforeRoute = []; $afterRoute = []; // Controller middleware annotation $reflectionClass = new ReflectionClass($controller[0]); self::prepareAttributeMiddlewares($beforeRoute, $reflectionClass); // Controller middleware property if ($reflectionClass->hasProperty('middleware')) { $defaultProperties = $reflectionClass->getDefaultProperties(); $middlewaresClasses = $defaultProperties['middleware']; foreach ((array)$middlewaresClasses as $className) { $beforeRoute[] = [$className, 'process']; } } // Method middleware annotation (route must be between controller and method) if ($reflectionClass->hasMethod($controller[1])) { self::prepareAttributeMiddlewares($afterRoute, $reflectionClass->getMethod($controller[1])); } $middlewares = array_merge($beforeRoute, $routeMiddlewares, $afterRoute); static::$controllerMiddlewareCache[$cacheKey] = ['before_route' => $beforeRoute, 'after_route' => $afterRoute]; if (count(static::$controllerMiddlewareCache) > 1024) { unset(static::$controllerMiddlewareCache[key(static::$controllerMiddlewareCache)]); } } } else { // Route middleware $middlewares = array_merge($middlewares, $routeMiddlewares); } if ($appName === '') { return array_reverse(array_merge($globalMiddleware, $appGlobalMiddleware, $middlewares)); } $appMiddleware = static::$instances[$plugin][$appName] ?? []; return array_reverse(array_merge($globalMiddleware, $appGlobalMiddleware, $appMiddleware, $middlewares)); } /** * @param array $middlewares * @param ReflectionClass|ReflectionMethod $reflection * @return void */ private static function prepareAttributeMiddlewares(array &$middlewares, ReflectionClass|ReflectionMethod $reflection): void { if ($reflection instanceof ReflectionClass && $parent_ref = $reflection->getParentClass()) { self::prepareAttributeMiddlewares($middlewares, $parent_ref); } $middlewareAttributes = $reflection->getAttributes(MiddlewareAttribute::class, ReflectionAttribute::IS_INSTANCEOF); foreach ($middlewareAttributes as $middlewareAttribute) { $middlewareAttributeInstance = $middlewareAttribute->newInstance(); $middlewares = array_merge($middlewares, $middlewareAttributeInstance->getMiddlewares()); } } /** * @return void * @deprecated */ public static function container($_) { } } ================================================ FILE: src/MiddlewareInterface.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman; use Webman\Http\Request; use Webman\Http\Response; interface MiddlewareInterface { /** * Process an incoming server request. * * Processes an incoming server request in order to produce a response. * If unable to produce the response itself, it may delegate to the provided * request handler to do so. */ public function process(Request $request, callable $handler): Response; } ================================================ FILE: src/Route/Route.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Route; use Webman\Route as Router; use function array_merge; use function count; use function preg_replace_callback; use function str_replace; /** * Class Route * @package Webman */ class Route { /** * @var string|null */ protected $name = null; /** * @var array */ protected $methods = []; /** * @var string */ protected $path = ''; /** * @var callable */ protected $callback = null; /** * @var array */ protected $middlewares = []; /** * @var array */ protected $params = []; /** * Route constructor. * @param array $methods * @param string $path * @param callable $callback */ public function __construct($methods, string $path, $callback) { $this->methods = (array)$methods; $this->path = $path; $this->callback = $callback; } /** * Get name. * @return string|null */ public function getName(): ?string { return $this->name ?? null; } /** * Name. * @param string $name * @return $this */ public function name(string $name): Route { $this->name = $name; Router::setByName($name, $this); return $this; } /** * Middleware. * @param mixed $middleware * @return $this|array */ public function middleware(mixed $middleware = null) { if ($middleware === null) { return $this->middlewares; } $this->middlewares = array_merge($this->middlewares, is_array($middleware) ? array_reverse($middleware) : [$middleware]); return $this; } /** * GetPath. * @return string */ public function getPath(): string { return $this->path; } /** * GetMethods. * @return array */ public function getMethods(): array { return $this->methods; } /** * GetCallback. * @return callable|null */ public function getCallback() { return $this->callback; } /** * GetMiddleware. * @return array */ public function getMiddleware(): array { return $this->middlewares; } /** * Param. * @param string|null $name * @param mixed $default * @return mixed */ public function param(?string $name = null, mixed $default = null) { if ($name === null) { return $this->params; } return $this->params[$name] ?? $default; } /** * SetParams. * @param array $params * @return $this */ public function setParams(array $params): Route { $this->params = array_merge($this->params, $params); return $this; } /** * Url. * @param array $parameters * @return string */ public function url(array $parameters = []): string { if (empty($parameters)) { return $this->path; } $path = str_replace(['[', ']'], '', $this->path); $path = preg_replace_callback('/\{(.*?)(?:\:[^\}]*?)*?\}/', function ($matches) use (&$parameters) { if (!$parameters) { return $matches[0]; } if (isset($parameters[$matches[1]])) { $value = $parameters[$matches[1]]; unset($parameters[$matches[1]]); return $value; } $key = key($parameters); if (is_int($key)) { $value = $parameters[$key]; unset($parameters[$key]); return $value; } return $matches[0]; }, $path); return count($parameters) > 0 ? $path . '?' . http_build_query($parameters) : $path; } } ================================================ FILE: src/Route.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman; use FastRoute\Dispatcher\GroupCountBased; use FastRoute\RouteCollector; use FilesystemIterator; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ReflectionAttribute; use ReflectionClass; use ReflectionException; use ReflectionMethod; use RuntimeException; use support\annotation\Middleware as MiddlewareAttribute; use support\annotation\route\DisableDefaultRoute; use support\annotation\route\Route as RouteAttribute; use support\annotation\route\RouteGroup as RouteGroupAttribute; use Webman\Finder\FileInfo; use Webman\Finder\ControllerFinder; use Webman\Route\Route as RouteObject; use function array_diff; use function array_values; use function class_exists; use function explode; use function FastRoute\simpleDispatcher; use function in_array; use function is_array; use function is_callable; use function is_file; use function is_scalar; use function is_string; use function json_encode; use function method_exists; use function strpos; /** * Class Route * @package Webman */ class Route { /** * @var Route */ protected static $instance = null; /** * @var GroupCountBased */ protected static $dispatcher = null; /** * @var RouteCollector */ protected static $collector = null; /** * @var RouteObject[] */ protected static $fallbackRoutes = []; /** * @var array */ protected static $fallback = []; /** * @var array */ protected static $nameList = []; /** * @var string */ protected static $groupPrefix = ''; /** * @var bool */ protected static $disabledDefaultRoutes = []; /** * @var array */ protected static $disabledDefaultRouteControllers = []; /** * @var array */ protected static $disabledDefaultRouteActions = []; /** * @var RouteObject[] */ protected static $allRoutes = []; /** * Index for conflict detection: ["METHOD path" => "callback string"] * @var array */ protected static array $methodPathIndex = []; /** * @var string|null */ protected static ?string $registeringSource = null; /** * @var RouteObject[] */ protected $routes = []; /** * @var Route[] */ protected $children = []; /** * Add GET route. * @param string $path * @param callable|mixed $callback * @return RouteObject */ public static function get(string $path, $callback): RouteObject { return static::addRoute('GET', $path, $callback); } /** * Add POST route. * @param string $path * @param callable|mixed $callback * @return RouteObject */ public static function post(string $path, $callback): RouteObject { return static::addRoute('POST', $path, $callback); } /** * Add PUT route. * @param string $path * @param callable|mixed $callback * @return RouteObject */ public static function put(string $path, $callback): RouteObject { return static::addRoute('PUT', $path, $callback); } /** * Add PATCH route. * @param string $path * @param callable|mixed $callback * @return RouteObject */ public static function patch(string $path, $callback): RouteObject { return static::addRoute('PATCH', $path, $callback); } /** * Add DELETE route. * @param string $path * @param callable|mixed $callback * @return RouteObject */ public static function delete(string $path, $callback): RouteObject { return static::addRoute('DELETE', $path, $callback); } /** * @param string $path * @param callable|mixed $callback * @return RouteObject */ public static function head(string $path, $callback): RouteObject { return static::addRoute('HEAD', $path, $callback); } /** * Add HEAD route. * @param string $path * @param callable|mixed $callback * @return RouteObject */ public static function options(string $path, $callback): RouteObject { return static::addRoute('OPTIONS', $path, $callback); } /** * Add OPTIONS route. * @param string $path * @param callable|mixed $callback * @return RouteObject */ public static function any(string $path, $callback): RouteObject { return static::addRoute(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], $path, $callback); } /** * Add route. * @param $method * @param string $path * @param callable|mixed $callback * @return RouteObject */ public static function add($method, string $path, $callback): RouteObject { return static::addRoute($method, $path, $callback); } /** * Add group. * @param string|callable $path * @param callable|null $callback * @return static */ public static function group($path, ?callable $callback = null): Route { if ($callback === null) { $callback = $path; $path = ''; } $previousGroupPrefix = static::$groupPrefix; static::$groupPrefix = $previousGroupPrefix . $path; $previousInstance = static::$instance; $instance = static::$instance = new static; static::$collector->addGroup($path, $callback); static::$groupPrefix = $previousGroupPrefix; static::$instance = $previousInstance; if ($previousInstance) { $previousInstance->addChild($instance); } return $instance; } /** * Add resource. * @param string $name * @param string $controller * @param array $options * @return void */ public static function resource(string $name, string $controller, array $options = []) { $name = trim($name, '/'); if (is_array($options) && !empty($options)) { $diffOptions = array_diff($options, ['index', 'create', 'store', 'update', 'show', 'edit', 'destroy', 'recovery']); if (!empty($diffOptions)) { foreach ($diffOptions as $action) { static::any("/$name/{$action}[/{id}]", [$controller, $action])->name("$name.{$action}"); } } // 注册路由 由于顺序不同会导致路由无效 因此不适用循环注册 if (in_array('index', $options)) static::get("/$name", [$controller, 'index'])->name("$name.index"); if (in_array('create', $options)) static::get("/$name/create", [$controller, 'create'])->name("$name.create"); if (in_array('store', $options)) static::post("/$name", [$controller, 'store'])->name("$name.store"); if (in_array('update', $options)) static::put("/$name/{id}", [$controller, 'update'])->name("$name.update"); if (in_array('patch', $options)) static::patch("/$name/{id}", [$controller, 'patch'])->name("$name.patch"); if (in_array('show', $options)) static::get("/$name/{id}", [$controller, 'show'])->name("$name.show"); if (in_array('edit', $options)) static::get("/$name/{id}/edit", [$controller, 'edit'])->name("$name.edit"); if (in_array('destroy', $options)) static::delete("/$name/{id}", [$controller, 'destroy'])->name("$name.destroy"); if (in_array('recovery', $options)) static::put("/$name/{id}/recovery", [$controller, 'recovery'])->name("$name.recovery"); } else { //为空时自动注册所有常用路由 if (method_exists($controller, 'index')) static::get("/$name", [$controller, 'index'])->name("$name.index"); if (method_exists($controller, 'create')) static::get("/$name/create", [$controller, 'create'])->name("$name.create"); if (method_exists($controller, 'store')) static::post("/$name", [$controller, 'store'])->name("$name.store"); if (method_exists($controller, 'update')) static::put("/$name/{id}", [$controller, 'update'])->name("$name.update"); if (method_exists($controller, 'patch')) static::patch("/$name/{id}", [$controller, 'patch'])->name("$name.patch"); if (method_exists($controller, 'show')) static::get("/$name/{id}", [$controller, 'show'])->name("$name.show"); if (method_exists($controller, 'edit')) static::get("/$name/{id}/edit", [$controller, 'edit'])->name("$name.edit"); if (method_exists($controller, 'destroy')) static::delete("/$name/{id}", [$controller, 'destroy'])->name("$name.destroy"); if (method_exists($controller, 'recovery')) static::put("/$name/{id}/recovery", [$controller, 'recovery'])->name("$name.recovery"); } } /** * Get routes. * @return RouteObject[] */ public static function getRoutes(): array { return static::$allRoutes; } /** * Disable default route. * @param array|string $plugin * @param string|null $app * @return bool */ public static function disableDefaultRoute(array|string $plugin = '', ?string $app = null): bool { // Is [controller action] if (is_array($plugin)) { $controllerAction = $plugin; if (!isset($controllerAction[0]) || !is_string($controllerAction[0]) || !isset($controllerAction[1]) || !is_string($controllerAction[1])) { return false; } $controller = $controllerAction[0]; $action = $controllerAction[1]; static::$disabledDefaultRouteActions[$controller][$action] = $action; return true; } // Is plugin if (is_string($plugin) && (preg_match('/^[a-zA-Z0-9_]+$/', $plugin) || $plugin === '')) { if (!isset(static::$disabledDefaultRoutes[$plugin])) { static::$disabledDefaultRoutes[$plugin] = []; } $app = $app ?? '*'; static::$disabledDefaultRoutes[$plugin][$app] = $app; return true; } // Is controller if (is_string($plugin) && class_exists($plugin)) { static::$disabledDefaultRouteControllers[$plugin] = $plugin; return true; } return false; } /** * Is default route disabled. * @param array|string $plugin * @param string|null $app * @return bool */ public static function isDefaultRouteDisabled(array|string $plugin = '', ?string $app = null): bool { // Is [controller action] if (is_array($plugin)) { if (!isset($plugin[0]) || !is_string($plugin[0]) || !isset($plugin[1]) || !is_string($plugin[1])) { return false; } return isset(static::$disabledDefaultRouteActions[$plugin[0]][$plugin[1]]) || static::isDefaultRouteDisabledByAnnotation($plugin[0], $plugin[1]); } // Is plugin if (is_string($plugin) && (preg_match('/^[a-zA-Z0-9_]+$/', $plugin) || $plugin === '')) { $app = $app ?? '*'; return isset(static::$disabledDefaultRoutes[$plugin]['*']) || isset(static::$disabledDefaultRoutes[$plugin][$app]); } // Is controller if (is_string($plugin) && class_exists($plugin)) { return isset(static::$disabledDefaultRouteControllers[$plugin]); } return false; } /** * Is default route disabled by annotation. * @param string $controller * @param string|null $action * @return bool */ protected static function isDefaultRouteDisabledByAnnotation(string $controller, ?string $action = null): bool { if (class_exists($controller)) { $reflectionClass = new ReflectionClass($controller); if (static::isRefHasDefaultRouteDisabledAnnotation($reflectionClass)) { return true; } if ($action && $reflectionClass->hasMethod($action)) { $reflectionMethod = $reflectionClass->getMethod($action); if ($reflectionMethod->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF)) { return true; } } } return false; } /** * Is reflection class has default route disabled annotation. * @param ReflectionClass $reflectionClass * @return bool */ protected static function isRefHasDefaultRouteDisabledAnnotation(ReflectionClass $reflectionClass): bool { $has = $reflectionClass->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF); if ($has) { return true; } if (method_exists($reflectionClass, 'getParentClass')) { $parent = $reflectionClass->getParentClass(); if ($parent) { return static::isRefHasDefaultRouteDisabledAnnotation($parent); } } return false; } /** * Add middleware. * @param $middleware * @return $this */ public function middleware($middleware): Route { foreach ($this->routes as $route) { $route->middleware($middleware); } foreach ($this->getChildren() as $child) { $child->middleware($middleware); } return $this; } /** * Collect route. * @param RouteObject $route */ public function collect(RouteObject $route) { $this->routes[] = $route; } /** * Set by name. * @param string $name * @param RouteObject $instance */ public static function setByName(string $name, RouteObject $instance) { static::$nameList[$name] = $instance; } /** * Get by name. * @param string $name * @return null|RouteObject */ public static function getByName(string $name): ?RouteObject { return static::$nameList[$name] ?? null; } /** * Add child. * @param Route $route * @return void */ public function addChild(Route $route) { $this->children[] = $route; } /** * Get children. * @return Route[] */ public function getChildren() { return $this->children; } /** * Dispatch. * @param string $method * @param string $path * @return array */ public static function dispatch(string $method, string $path): array { return static::$dispatcher->dispatch($method, $path); } /** * Convert to callable. * @param string $path * @param callable|mixed $callback * @return callable|false|string[] */ public static function convertToCallable(string $path, $callback) { if (is_string($callback) && strpos($callback, '@')) { $callback = explode('@', $callback, 2); } if (!is_array($callback)) { if (!is_callable($callback)) { $callStr = is_scalar($callback) ? $callback : 'Closure'; echo "Route $path $callStr is not callable\n"; return false; } } else { $callback = array_values($callback); if (!isset($callback[1]) || !class_exists($callback[0]) || !method_exists($callback[0], $callback[1])) { echo "Route $path " . json_encode($callback) . " is not callable\n"; return false; } } return $callback; } /** * Add route. * @param array|string $methods * @param string $path * @param callable|mixed $callback * @return RouteObject */ protected static function addRoute($methods, string $path, $callback): RouteObject { $fullPath = static::$groupPrefix . $path; foreach ((array)$methods as $method) { $method = strtoupper((string)$method); $key = $method . ' ' . $fullPath; if (isset(static::$methodPathIndex[$key])) { $old = static::$methodPathIndex[$key]; $new = static::callbackToString($callback); $source = static::$registeringSource ? (' from ' . static::$registeringSource) : ''; throw new RuntimeException("Route conflict: [$key] already registered as $old, cannot register $new$source"); } static::$methodPathIndex[$key] = static::callbackToString($callback); } $route = new RouteObject($methods, static::$groupPrefix . $path, $callback); static::$allRoutes[] = $route; if ($callback = static::convertToCallable($path, $callback)) { static::$collector->addRoute($methods, $path, ['callback' => $callback, 'route' => $route]); } if (static::$instance) { static::$instance->collect($route); } return $route; } /** * Load. * @param mixed $paths * @return void */ public static function load($paths) { if (!is_array($paths)) { return; } static::$dispatcher = null; static::$collector = null; static::$fallbackRoutes = []; static::$fallback = []; static::$nameList = []; static::$disabledDefaultRoutes = []; static::$disabledDefaultRouteControllers = []; static::$disabledDefaultRouteActions = []; static::$allRoutes = []; static::$methodPathIndex = []; static::$registeringSource = null; static::$dispatcher = simpleDispatcher(function (RouteCollector $route) use ($paths) { Route::setCollector($route); foreach ($paths as $configPath) { $routeConfigFile = $configPath . '/route.php'; if (is_file($routeConfigFile)) { require_once $routeConfigFile; } if (!is_dir($pluginConfigPath = $configPath . '/plugin')) { continue; } $dirIterator = new RecursiveDirectoryIterator($pluginConfigPath, FilesystemIterator::FOLLOW_SYMLINKS); $iterator = new RecursiveIteratorIterator($dirIterator); foreach ($iterator as $file) { if ($file->getBaseName('.php') !== 'route') { continue; } $appConfigFile = pathinfo($file, PATHINFO_DIRNAME) . '/app.php'; if (!is_file($appConfigFile)) { continue; } $appConfig = include $appConfigFile; if (empty($appConfig['enable'])) { continue; } require_once $file; } } static::loadAnnotationRoutes(); }); } /** * SetCollector. * @param RouteCollector $route * @return void */ public static function setCollector(RouteCollector $route) { static::$collector = $route; } /** * Fallback. * @param callable|mixed $callback * @param string $plugin * @return RouteObject */ public static function fallback(callable $callback, string $plugin = '') { $route = new RouteObject([], '', $callback); static::$fallbackRoutes[$plugin] = $route; return $route; } /** * GetFallBack. * @param string $plugin * @param int $status * @return callable|null * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface * @throws ReflectionException */ public static function getFallback(string $plugin = '', int $status = 404) { if (!isset(static::$fallback[$plugin])) { $callback = null; $route = static::$fallbackRoutes[$plugin] ?? null; static::$fallback[$plugin] = $route ? App::getCallback($plugin, 'NOT_FOUND', $route->getCallback(), ['status' => $status], false, $route) : null; } return static::$fallback[$plugin]; } /** * Load annotation routes. * @return void */ protected static function loadAnnotationRoutes(): void { $controllerFiles = ControllerFinder::files('*'); if (!$controllerFiles) { return; } $routes = static::buildAnnotationRouteDefinitions($controllerFiles); static::registerAnnotationRouteDefinitions($routes); } /** * Build annotation route definitions. * @param FileInfo[] $controllerFiles * @return array */ protected static function buildAnnotationRouteDefinitions(array $controllerFiles): array { $definitions = []; foreach ($controllerFiles as $foundFile) { $meta = $foundFile->meta(); $controllerClass = $meta['class'] ?? null; if (!$controllerClass) { continue; } $file = $foundFile->getPathname(); if (!class_exists($controllerClass)) { require_once $file; } if (!class_exists($controllerClass)) { continue; } $ref = new ReflectionClass($controllerClass); if ($ref->isAbstract() || $ref->isInterface()) { continue; } $prefix = ''; $groupAttrs = $ref->getAttributes(RouteGroupAttribute::class, ReflectionAttribute::IS_INSTANCEOF); if ($groupAttrs) { /** @var RouteGroupAttribute $group */ $group = $groupAttrs[0]->newInstance(); $prefix = static::normalizeRoutePrefix($group->prefix); } foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { if ($method->isConstructor() || $method->isDestructor()) { continue; } if ($method->getDeclaringClass()->getName() !== $controllerClass) { continue; } $routeAttrs = $method->getAttributes(RouteAttribute::class, ReflectionAttribute::IS_INSTANCEOF); if (!$routeAttrs) { continue; } foreach ($routeAttrs as $routeAttr) { /** @var RouteAttribute $route */ $route = $routeAttr->newInstance(); if ($route->path === null) { // Null path means "method restriction only" for default route, do not register. continue; } $source = $controllerClass . '::' . $method->getName(); $path = static::normalizeRoutePath($route->path, $source); $fullPath = $prefix . $path; if ($fullPath === '') { throw new RuntimeException("Annotation route resolves to empty path: #[Get('')] requires a #[RouteGroup] prefix ($source)"); } $methods = []; foreach ($route->methods as $m) { $methods[] = strtoupper((string)$m); } $definitions[] = [ 'methods' => $methods, 'path' => $fullPath, 'callback' => [$controllerClass, $method->getName()], 'name' => $route->name, ]; } } } return $definitions; } /** * Collect middlewares from attributes. * @param array $attributes * @return array */ protected static function collectMiddlewaresFromAttributes(array $attributes): array { $middlewares = []; foreach ($attributes as $attribute) { /** @var MiddlewareAttribute $instance */ $instance = $attribute->newInstance(); foreach ($instance->getMiddlewares() as $middleware) { if (is_string($middleware)) { $middlewares[] = $middleware; continue; } if (is_array($middleware) && isset($middleware[0]) && is_string($middleware[0])) { $middlewares[] = $middleware[0]; } } } return $middlewares; } /** * Register annotation route definitions. * @param array $definitions * @return void */ protected static function registerAnnotationRouteDefinitions(array $definitions): void { foreach ($definitions as $definition) { static::$registeringSource = 'annotation ' . $definition['callback'][0] . '::' . $definition['callback'][1]; $route = static::add($definition['methods'], $definition['path'], $definition['callback']); if (!empty($definition['name'])) { $route->name($definition['name']); } if (!empty($definition['middlewares'])) { $route->middleware($definition['middlewares']); } static::$registeringSource = null; } } /** * Normalize route prefix. * @param string $prefix * @return string */ protected static function normalizeRoutePrefix(string $prefix): string { $prefix = trim($prefix); if ($prefix === '') { return ''; } if ($prefix[0] !== '/') { $prefix = '/' . $prefix; } return rtrim($prefix, '/'); } /** * Normalize route path. * Empty string is allowed (means "use group prefix only"). * Non-empty path must start with '/'. * @param string $path * @param string $source * @return string */ protected static function normalizeRoutePath(string $path, string $source): string { $path = trim($path); if ($path === '') { return ''; } if ($path[0] !== '/') { throw new RuntimeException("Annotation route path must start with '/': '$path' ($source)"); } return $path; } /** * Callback to string. * @param mixed $callback * @return string */ protected static function callbackToString(mixed $callback): string { if (is_array($callback)) { $callback = array_values($callback); $class = $callback[0] ?? ''; $method = $callback[1] ?? ''; return $class && $method ? ($class . '::' . $method) : json_encode($callback); } if ($callback instanceof \Closure) { return 'Closure'; } if (is_string($callback)) { return $callback; } return get_debug_type($callback); } /** * @return void * @deprecated */ public static function container() { } } ================================================ FILE: src/Session/FileSessionHandler.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Session; use Workerman\Protocols\Http\Session\FileSessionHandler as FileHandler; /** * Class FileSessionHandler * @package Webman */ class FileSessionHandler extends FileHandler { } ================================================ FILE: src/Session/RedisClusterSessionHandler.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Session; use Workerman\Protocols\Http\Session\RedisClusterSessionHandler as RedisClusterHandler; class RedisClusterSessionHandler extends RedisClusterHandler { } ================================================ FILE: src/Session/RedisSessionHandler.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman\Session; use Workerman\Protocols\Http\Session\RedisSessionHandler as RedisHandler; /** * Class FileSessionHandler * @package Webman */ class RedisSessionHandler extends RedisHandler { } ================================================ FILE: src/Util.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman; use function array_diff; use function array_map; use function scandir; /** * Class Util * @package Webman */ class Util { /** * ScanDir. * @param string $basePath * @param bool $withBasePath * @return array */ public static function scanDir(string $basePath, bool $withBasePath = true): array { if (!is_dir($basePath)) { return []; } $paths = array_diff(scandir($basePath), array('.', '..')) ?: []; return $withBasePath ? array_map(static function ($path) use ($basePath) { return $basePath . DIRECTORY_SEPARATOR . $path; }, $paths) : $paths; } } ================================================ FILE: src/View.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace Webman; interface View { /** * Render. * @param string $template * @param array $vars * @param string|null $app * @return string */ public static function render(string $template, array $vars, ?string $app = null): string; } ================================================ FILE: src/start.php ================================================ #!/usr/bin/env php load(); } else { Dotenv::createMutable(run_path())->load(); } } if (!$appConfigFile = config_path('app.php')) { throw new RuntimeException('Config file not found: app.php'); } $appConfig = require $appConfigFile; if ($timezone = $appConfig['default_timezone'] ?? '') { date_default_timezone_set($timezone); } static::loadAllConfig(['route', 'container']); if (!is_phar() && DIRECTORY_SEPARATOR === '\\' && empty(config('server.listen'))) { echo "Please run 'php windows.php' on windows system." . PHP_EOL; exit; } $errorReporting = config('app.error_reporting'); if (isset($errorReporting)) { error_reporting($errorReporting); } $runtimeLogsPath = runtime_path() . DIRECTORY_SEPARATOR . 'logs'; if (!file_exists($runtimeLogsPath) || !is_dir($runtimeLogsPath)) { if (!mkdir($runtimeLogsPath, 0777, true)) { throw new RuntimeException("Failed to create runtime logs directory. Please check the permission."); } } $runtimeViewsPath = runtime_path() . DIRECTORY_SEPARATOR . 'views'; if (!file_exists($runtimeViewsPath) || !is_dir($runtimeViewsPath)) { if (!mkdir($runtimeViewsPath, 0777, true)) { throw new RuntimeException("Failed to create runtime views directory. Please check the permission."); } } Worker::$onMasterReload = function () { if (function_exists('opcache_get_status')) { if ($status = opcache_get_status()) { if (isset($status['scripts']) && $scripts = $status['scripts']) { foreach (array_keys($scripts) as $file) { opcache_invalidate($file, true); } } } } }; $config = config('server'); Worker::$pidFile = $config['pid_file']; Worker::$stdoutFile = $config['stdout_file']; Worker::$logFile = $config['log_file']; Worker::$eventLoopClass = $config['event_loop'] ?? ''; TcpConnection::$defaultMaxPackageSize = $config['max_package_size'] ?? 10 * 1024 * 1024; if (property_exists(Worker::class, 'statusFile')) { Worker::$statusFile = $config['status_file'] ?? ''; } if (property_exists(Worker::class, 'stopTimeout')) { Worker::$stopTimeout = $config['stop_timeout'] ?? 2; } if ($config['listen'] ?? false) { $worker = new Worker($config['listen'], $config['context'] ?? []); $propertyMap = [ 'name', 'count', 'user', 'group', 'reusePort', 'transport', 'protocol' ]; foreach ($propertyMap as $property) { if (isset($config[$property])) { $worker->$property = $config[$property]; } } $worker->onWorkerStart = function ($worker) { require_once base_path() . '/support/bootstrap.php'; $app = new \Webman\App(config('app.request_class', Request::class), Log::channel('default'), app_path(), public_path()); $worker->onMessage = [$app, 'onMessage']; call_user_func([$app, 'onWorkerStart'], $worker); }; } $windowsWithoutServerListen = is_phar() && DIRECTORY_SEPARATOR === '\\' && empty($config['listen']); $process = config('process', []); if ($windowsWithoutServerListen && $process) { $processName = isset($process['webman']) ? 'webman' : key($process); worker_start($processName, $process[$processName]); } else if (DIRECTORY_SEPARATOR === '/') { foreach (config('process', []) as $processName => $config) { worker_start($processName, $config); } foreach (config('plugin', []) as $firm => $projects) { foreach ($projects as $name => $project) { if (!is_array($project)) { continue; } foreach ($project['process'] ?? [] as $processName => $config) { worker_start("plugin.$firm.$name.$processName", $config); } } foreach ($projects['process'] ?? [] as $processName => $config) { worker_start("plugin.$firm.$processName", $config); } } } Worker::runAll(); } /** * LoadAllConfig. * @param array $excludes * @return void */ public static function loadAllConfig(array $excludes = []) { Config::load(config_path(), $excludes); $directory = base_path() . '/plugin'; foreach (Util::scanDir($directory, false) as $name) { $dir = "$directory/$name/config"; if (is_dir($dir)) { Config::load($dir, $excludes, "plugin.$name"); } } } } ================================================ FILE: src/support/Container.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support; use Webman\Config; /** * Class Container * @package support * @method static mixed get($name) * @method static mixed make($name, array $parameters) * @method static bool has($name) */ class Container { /** * Instance * @param string $plugin * @return array|mixed|void|null */ public static function instance(string $plugin = '') { return Config::get($plugin ? "plugin.$plugin.container" : 'container'); } /** * @param string $name * @param array $arguments * @return mixed */ public static function __callStatic(string $name, array $arguments) { return static::instance()->{$name}(... $arguments); } } ================================================ FILE: src/support/Context.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support; /** * Class Context * @package Webman */ class Context extends \Webman\Context { } ================================================ FILE: src/support/Log.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support; use Monolog\Formatter\FormatterInterface; use Monolog\Handler\FormattableHandlerInterface; use Monolog\Handler\HandlerInterface; use Monolog\Logger; use function array_values; use function config; use function is_array; /** * Class Log * @package support * * @method static void log($level, $message, array $context = []) * @method static void debug($message, array $context = []) * @method static void info($message, array $context = []) * @method static void notice($message, array $context = []) * @method static void warning($message, array $context = []) * @method static void error($message, array $context = []) * @method static void critical($message, array $context = []) * @method static void alert($message, array $context = []) * @method static void emergency($message, array $context = []) */ class Log { /** * @var array */ protected static $instance = []; /** * Channel. * @param string $name * @return Logger */ public static function channel(string $name = 'default'): Logger { if (!isset(static::$instance[$name])) { $config = config('log', [])[$name]; $handlers = self::handlers($config); $processors = self::processors($config); $logger = new Logger($name, $handlers, $processors); if (method_exists($logger, 'useLoggingLoopDetection')) { $logger->useLoggingLoopDetection(false); } static::$instance[$name] = $logger; } return static::$instance[$name]; } /** * Handlers. * @param array $config * @return array */ protected static function handlers(array $config): array { $handlerConfigs = $config['handlers'] ?? [[]]; $handlers = []; foreach ($handlerConfigs as $value) { $class = $value['class'] ?? []; $constructor = $value['constructor'] ?? []; $formatterConfig = $value['formatter'] ?? []; $class && $handlers[] = self::handler($class, $constructor, $formatterConfig); } return $handlers; } /** * Handler. * @param string $class * @param array $constructor * @param array $formatterConfig * @return HandlerInterface */ protected static function handler(string $class, array $constructor, array $formatterConfig): HandlerInterface { /** @var HandlerInterface $handler */ $handler = new $class(... array_values($constructor)); if ($handler instanceof FormattableHandlerInterface && $formatterConfig) { $formatterClass = $formatterConfig['class']; $formatterConstructor = $formatterConfig['constructor']; /** @var FormatterInterface $formatter */ $formatter = new $formatterClass(... array_values($formatterConstructor)); $handler->setFormatter($formatter); } return $handler; } /** * Processors. * @param array $config * @return array */ protected static function processors(array $config): array { $result = []; if (!isset($config['processors']) && isset($config['processor'])) { $config['processors'] = [$config['processor']]; } foreach ($config['processors'] ?? [] as $value) { if (is_array($value) && isset($value['class'])) { $value = new $value['class'](... array_values($value['constructor'] ?? [])); } $result[] = $value; } return $result; } /** * @param string $name * @param array $arguments * @return mixed */ public static function __callStatic(string $name, array $arguments) { return static::channel()->{$name}(... $arguments); } } ================================================ FILE: src/support/Plugin.php ================================================ $path) { $pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN"; if (!defined($pluginConst)) { continue; } $installFunction = "\\{$namespace}Install::install"; if (is_callable($installFunction)) { $installFunction(true); } } } /** * Update. * @param mixed $event * @return void */ public static function update($event) { static::findHelper(); $psr4 = static::getPsr4($event); foreach ($psr4 as $namespace => $path) { $pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN"; if (!defined($pluginConst)) { continue; } $updateFunction = "\\{$namespace}Install::update"; if (is_callable($updateFunction)) { $updateFunction(); continue; } $installFunction = "\\{$namespace}Install::install"; if (is_callable($installFunction)) { $installFunction(false); } } } /** * Uninstall. * @param mixed $event * @return void */ public static function uninstall($event) { static::findHelper(); $psr4 = static::getPsr4($event); foreach ($psr4 as $namespace => $path) { $pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN"; if (!defined($pluginConst)) { continue; } $uninstallFunction = "\\{$namespace}Install::uninstall"; if (is_callable($uninstallFunction)) { $uninstallFunction(); } } } /** * Get psr-4 info * * @param mixed $event * @return array */ protected static function getPsr4($event) { $operation = $event->getOperation(); $autoload = method_exists($operation, 'getPackage') ? $operation->getPackage()->getAutoload() : $operation->getTargetPackage()->getAutoload(); return $autoload['psr-4'] ?? []; } /** * FindHelper. * @return void */ protected static function findHelper() { // Plugin.php in webman require_once __DIR__ . '/helpers.php'; } } ================================================ FILE: src/support/Request.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support; /** * Class Request * @package support */ class Request extends \Webman\Http\Request { } ================================================ FILE: src/support/Response.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support; /** * Class Response * @package support */ class Response extends \Webman\Http\Response { } ================================================ FILE: src/support/Translation.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support; use FilesystemIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use RegexIterator; use Symfony\Component\Translation\Translator; use Webman\Exception\NotFoundException; use function basename; use function config; use function get_realpath; use function pathinfo; use function request; use function substr; /** * Class Translation * @package support * @method static string trans(?string $id, array $parameters = [], string $domain = null, string $locale = null) * @method static void setLocale(string $locale) * @method static string getLocale() */ class Translation { /** * @var Translator[] */ protected static $instance = []; /** * Instance. * @param string $plugin * @param array|null $config * @return Translator * @throws NotFoundException */ public static function instance(string $plugin = '', ?array $config = null): Translator { if (!isset(static::$instance[$plugin])) { $config = $config ?? config($plugin ? "plugin.$plugin.translation" : 'translation', []); $paths = (array)($config['path'] ?? []); static::$instance[$plugin] = $translator = new Translator($config['locale']); $translator->setFallbackLocales($config['fallback_locale']); $classes = $config['loader'] ?? [ 'Symfony\Component\Translation\Loader\PhpFileLoader' => [ 'extension' => '.php', 'format' => 'phpfile' ], 'Symfony\Component\Translation\Loader\PoFileLoader' => [ 'extension' => '.po', 'format' => 'pofile' ] ]; foreach ($paths as $path) { // Phar support. Compatible with the 'realpath' function in the phar file. if (!$translationsPath = get_realpath($path)) { continue; } foreach ($classes as $class => $opts) { $translator->addLoader($opts['format'], new $class); $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($translationsPath, FilesystemIterator::SKIP_DOTS)); $files = new RegexIterator($iterator, '/^.+' . preg_quote($opts['extension']) . '$/i', RegexIterator::GET_MATCH); foreach ($files as $file) { $file = $file[0]; $domain = basename($file, $opts['extension']); $dirName = pathinfo($file, PATHINFO_DIRNAME); $locale = substr(strrchr($dirName, DIRECTORY_SEPARATOR), 1); if ($domain && $locale) { $translator->addResource($opts['format'], $file, $locale, $domain); } } } } } return static::$instance[$plugin]; } /** * @param string $name * @param array $arguments * @return mixed * @throws NotFoundException */ public static function __callStatic(string $name, array $arguments) { $request = request(); $plugin = $request->plugin ?? ''; return static::instance($plugin)->{$name}(... $arguments); } } ================================================ FILE: src/support/View.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support; use function config; use function request; class View { /** * Assign. * @param mixed $name * @param mixed $value * @return void */ public static function assign($name, mixed $value = null) { $request = request(); $plugin = $request->plugin ?? ''; $handler = config($plugin ? "plugin.$plugin.view.handler" : 'view.handler'); $handler::assign($name, $value); } } ================================================ FILE: src/support/annotation/DisableDefaultRoute.php ================================================ middlewares = $middlewares; } /** * Convert to webman middleware callable format: [MiddlewareClass, 'process']. * @return array */ public function getMiddlewares(): array { $middlewares = []; foreach ($this->middlewares as $middleware) { $middlewares[] = [$middleware, 'process']; } return $middlewares; } } ================================================ FILE: src/support/annotation/route/Any.php ================================================ path = $path; $this->methods = is_array($methods) ? $methods : [$methods]; $this->name = $name; } } ================================================ FILE: src/support/annotation/route/RouteGroup.php ================================================ prefix = $prefix; } } ================================================ FILE: src/support/bootstrap/Session.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support\bootstrap; use Webman\Bootstrap; use Workerman\Protocols\Http; use Workerman\Protocols\Http\Session as SessionBase; use Workerman\Worker; use function config; use function property_exists; /** * Class Session * @package support */ class Session implements Bootstrap { /** * @param Worker|null $worker * @return void */ public static function start(?Worker $worker) { $config = config('session'); if (property_exists(SessionBase::class, 'name')) { SessionBase::$name = $config['session_name']; } else { Http::sessionName($config['session_name']); } SessionBase::handlerClass($config['handler'], $config['config'][$config['type']]); $map = [ 'auto_update_timestamp' => 'autoUpdateTimestamp', 'cookie_lifetime' => 'cookieLifetime', 'gc_probability' => 'gcProbability', 'cookie_path' => 'cookiePath', 'http_only' => 'httpOnly', 'same_site' => 'sameSite', 'lifetime' => 'lifetime', 'domain' => 'domain', 'secure' => 'secure', ]; foreach ($map as $key => $name) { if (isset($config[$key]) && property_exists(SessionBase::class, $name)) { SessionBase::${$name} = $config[$key]; } } } } ================================================ FILE: src/support/bootstrap.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ use Dotenv\Dotenv; use support\Log; use Webman\Bootstrap; use Webman\Config; use Webman\Middleware; use Webman\Route; use Webman\Util; use Workerman\Events\Select; use Workerman\Worker; $worker = $worker ?? null; if (empty(Worker::$eventLoopClass)) { Worker::$eventLoopClass = Select::class; } set_error_handler(function ($level, $message, $file = '', $line = 0) { if (error_reporting() & $level) { throw new ErrorException($message, 0, $level, $file, $line); } }); if ($worker) { register_shutdown_function(function ($startTime) { if (time() - $startTime <= 0.1) { sleep(1); } }, time()); } if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) { if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) { Dotenv::createUnsafeMutable(base_path(false))->load(); } else { Dotenv::createMutable(base_path(false))->load(); } } Config::clear(); support\App::loadAllConfig(['route']); if ($timezone = config('app.default_timezone')) { date_default_timezone_set($timezone); } foreach (config('autoload.files', []) as $file) { include_once $file; } foreach (config('plugin', []) as $firm => $projects) { foreach ($projects as $name => $project) { if (!is_array($project)) { continue; } foreach ($project['autoload']['files'] ?? [] as $file) { include_once $file; } } foreach ($projects['autoload']['files'] ?? [] as $file) { include_once $file; } } Middleware::load(config('middleware', [])); foreach (config('plugin', []) as $firm => $projects) { foreach ($projects as $name => $project) { if (!is_array($project) || $name === 'static') { continue; } Middleware::load($project['middleware'] ?? []); } Middleware::load($projects['middleware'] ?? [], $firm); if ($staticMiddlewares = config("plugin.$firm.static.middleware")) { Middleware::load(['__static__' => $staticMiddlewares], $firm); } } Middleware::load(['__static__' => config('static.middleware', [])]); foreach (config('bootstrap', []) as $className) { if (!class_exists($className)) { $log = "Warning: Class $className setting in config/bootstrap.php not found\r\n"; echo $log; Log::error($log); continue; } /** @var Bootstrap $className */ $className::start($worker); } foreach (config('plugin', []) as $firm => $projects) { foreach ($projects as $name => $project) { if (!is_array($project)) { continue; } foreach ($project['bootstrap'] ?? [] as $className) { if (!class_exists($className)) { $log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n"; echo $log; Log::error($log); continue; } /** @var Bootstrap $className */ $className::start($worker); } } foreach ($projects['bootstrap'] ?? [] as $className) { /** @var string $className */ if (!class_exists($className)) { $log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n"; echo $log; Log::error($log); continue; } /** @var Bootstrap $className */ $className::start($worker); } } $directory = base_path() . '/plugin'; $paths = [config_path()]; foreach (Util::scanDir($directory) as $path) { if (is_dir($path = "$path/config")) { $paths[] = $path; } } Route::load($paths); ================================================ FILE: src/support/exception/BusinessException.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support\exception; /** * Class BusinessException * @package support\exception */ class BusinessException extends \Webman\Exception\BusinessException { } ================================================ FILE: src/support/exception/Handler.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support\exception; use Throwable; use Webman\Exception\ExceptionHandler; use Webman\Http\Request; use Webman\Http\Response; use Webman\Exception\BusinessException; /** * Class Handler * @package support\exception */ class Handler extends ExceptionHandler { public $dontReport = [ BusinessException::class, ]; public function report(Throwable $exception) { parent::report($exception); } public function render(Request $request, Throwable $exception): Response { return parent::render($request, $exception); } } ================================================ FILE: src/support/exception/InputTypeException.php ================================================ getCode() ?: 404; $debug = config($request->plugin ? "plugin.$request->plugin.app.debug" : 'app.debug'); $data = $debug ? $this->data : ['parameter' => '']; $message = $this->trans($this->getMessage(), $data); if ($request->expectsJson()) { $json = ['code' => $code, 'msg' => $message, 'data' => $data]; return new Response(200, ['Content-Type' => 'application/json'], json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } return new Response($code, [], $this->html($message)); } } ================================================ FILE: src/support/exception/NotFoundException.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support\exception; class NotFoundException extends BusinessException { } ================================================ FILE: src/support/exception/PageNotFoundException.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support\exception; use Throwable; use Webman\Http\Request; use Webman\Http\Response; class PageNotFoundException extends NotFoundException { /** * @var string */ protected $template = '/app/view/404'; /** * PageNotFoundException constructor. * @param string $message * @param int $code * @param Throwable|null $previous */ public function __construct(string $message = '404 Not Found', int $code = 404, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); } /** * Render an exception into an HTTP response. * @param Request $request * @return Response|null * @throws Throwable */ public function render(Request $request): ?Response { $code = $this->getCode() ?: 404; $data = $this->data; $message = $this->trans($this->getMessage(), $data); if ($request->expectsJson()) { $json = ['code' => $code, 'msg' => $message, 'data' => $data]; return new Response(200, ['Content-Type' => 'application/json'], json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } return new Response($code, [], $this->html($message)); } /** * Get the HTML representation of the exception. * @param string $message * @return string * @throws Throwable */ protected function html(string $message): string { $message = htmlspecialchars($message); if (is_file(base_path("$this->template.html"))) { return raw_view($this->template, ['message' => $message])->rawBody(); } return << $message

$message


webman
EOF; } } ================================================ FILE: src/support/helpers.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ use support\Container; use support\Request; use support\Response; use support\Translation; use support\view\Blade; use support\view\Raw; use support\view\ThinkPHP; use support\view\Twig; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Webman\App; use Webman\Config; use Webman\Route; use Workerman\Protocols\Http\Session; use Workerman\Worker; /** * Get the base path of the application */ if (!defined('BASE_PATH')) { if (!$basePath = Phar::running()) { $basePath = getcwd(); while ($basePath !== dirname($basePath)) { if (is_dir("$basePath/vendor") && is_file("$basePath/start.php")) { break; } $basePath = dirname($basePath); } if ($basePath === dirname($basePath)) { $basePath = __DIR__ . '/../../../../../'; } } define('BASE_PATH', realpath($basePath) ?: $basePath); } if (!function_exists('run_path')) { /** * return the program execute directory * @param string $path * @return string */ function run_path(string $path = ''): string { static $runPath = ''; if (!$runPath) { $runPath = is_phar() ? dirname(Phar::running(false)) : BASE_PATH; } return path_combine($runPath, $path); } } if (!function_exists('base_path')) { /** * if the param $path equal false,will return this program current execute directory * @param string|false $path * @return string */ function base_path($path = ''): string { if (false === $path) { return run_path(); } return path_combine(BASE_PATH, $path); } } if (!function_exists('app_path')) { /** * App path * @param string $path * @return string */ function app_path(string $path = ''): string { return path_combine(BASE_PATH . DIRECTORY_SEPARATOR . 'app', $path); } } if (!function_exists('public_path')) { /** * Public path * @param string $path * @param string|null $plugin * @return string */ function public_path(string $path = '', ?string $plugin = null): string { static $publicPaths = []; $plugin = $plugin ?? ''; if (isset($publicPaths[$plugin])) { $publicPath = $publicPaths[$plugin]; } else { $prefix = $plugin ? "plugin.$plugin." : ''; $pathPrefix = $plugin ? 'plugin' . DIRECTORY_SEPARATOR . $plugin . DIRECTORY_SEPARATOR : ''; $publicPath = \config("{$prefix}app.public_path", run_path("{$pathPrefix}public")); if (count($publicPaths) > 32) { $publicPaths = []; } $publicPaths[$plugin] = $publicPath; } return $path === '' ? $publicPath : path_combine($publicPath, $path); } } if (!function_exists('config_path')) { /** * Config path * @param string $path * @return string */ function config_path(string $path = ''): string { return path_combine(BASE_PATH . DIRECTORY_SEPARATOR . 'config', $path); } } if (!function_exists('runtime_path')) { /** * Runtime path * @param string $path * @return string */ function runtime_path(string $path = ''): string { static $runtimePath = ''; if (!$runtimePath) { $runtimePath = \config('app.runtime_path') ?: run_path('runtime'); } return path_combine($runtimePath, $path); } } if (!function_exists('path_combine')) { /** * Generate paths based on given information * @param string $front * @param string $back * @return string */ function path_combine(string $front, string $back): string { return $front . ($back ? (DIRECTORY_SEPARATOR . ltrim($back, DIRECTORY_SEPARATOR)) : $back); } } if (!function_exists('response')) { /** * Response * @param int $status * @param array $headers * @param string $body * @return Response */ function response(string $body = '', int $status = 200, array $headers = []): Response { return new Response($status, $headers, $body); } } if (!function_exists('json')) { /** * Json response * @param $data * @param int $options * @return Response */ function json($data, int $options = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR): Response { return new Response(200, ['Content-Type' => 'application/json'], json_encode($data, $options)); } } if (!function_exists('xml')) { /** * Xml response * @param $xml * @return Response */ function xml($xml): Response { if ($xml instanceof SimpleXMLElement) { $xml = $xml->asXML(); } return new Response(200, ['Content-Type' => 'text/xml'], $xml); } } if (!function_exists('jsonp')) { /** * Jsonp response * @param $data * @param string $callbackName * @return Response */ function jsonp($data, string $callbackName = 'callback'): Response { if (!is_scalar($data) && null !== $data) { $data = json_encode($data); } return new Response(200, [], "$callbackName($data)"); } } if (!function_exists('redirect')) { /** * Redirect response * @param string $location * @param int $status * @param array $headers * @return Response */ function redirect(string $location, int $status = 302, array $headers = []): Response { $response = new Response($status, ['Location' => $location]); if (!empty($headers)) { $response->withHeaders($headers); } return $response; } } if (!function_exists('view')) { /** * View response * @param mixed $template * @param array $vars * @param string|null $app * @param string|null $plugin * @return Response */ function view(mixed $template = null, array $vars = [], ?string $app = null, ?string $plugin = null): Response { [$template, $vars, $app, $plugin] = template_inputs($template, $vars, $app, $plugin); $handler = \config($plugin ? "plugin.$plugin.view.handler" : 'view.handler'); return new Response(200, [], $handler::render($template, $vars, $app, $plugin)); } } if (!function_exists('raw_view')) { /** * Raw view response * @param mixed $template * @param array $vars * @param string|null $app * @param string|null $plugin * @return Response * @throws Throwable */ function raw_view(mixed $template = null, array $vars = [], ?string $app = null, ?string $plugin = null): Response { return new Response(200, [], Raw::render(...template_inputs($template, $vars, $app, $plugin))); } } if (!function_exists('blade_view')) { /** * Blade view response * @param mixed $template * @param array $vars * @param string|null $app * @param string|null $plugin * @return Response */ function blade_view(mixed $template = null, array $vars = [], ?string $app = null, ?string $plugin = null): Response { return new Response(200, [], Blade::render(...template_inputs($template, $vars, $app, $plugin))); } } if (!function_exists('think_view')) { /** * Think view response * @param mixed $template * @param array $vars * @param string|null $app * @param string|null $plugin * @return Response */ function think_view(mixed $template = null, array $vars = [], ?string $app = null, ?string $plugin = null): Response { return new Response(200, [], ThinkPHP::render(...template_inputs($template, $vars, $app, $plugin))); } } if (!function_exists('twig_view')) { /** * Twig view response * @param mixed $template * @param array $vars * @param string|null $app * @param string|null $plugin * @return Response */ function twig_view(mixed $template = null, array $vars = [], ?string $app = null, ?string $plugin = null): Response { return new Response(200, [], Twig::render(...template_inputs($template, $vars, $app, $plugin))); } } if (!function_exists('request')) { /** * Get request * @return \Webman\Http\Request|Request|null */ function request() { return App::request(); } } if (!function_exists('config')) { /** * Get config * @param string|null $key * @param mixed $default * @return mixed */ function config(?string $key = null, mixed $default = null) { return Config::get($key, $default); } } if (!function_exists('route')) { /** * Create url * @param string $name * @param ...$parameters * @return string */ function route(string $name, ...$parameters): string { $route = Route::getByName($name); if (!$route) { return ''; } if (!$parameters) { return $route->url(); } if (is_array(current($parameters))) { $parameters = current($parameters); } return $route->url($parameters); } } if (!function_exists('session')) { /** * Session * @param array|string|null $key * @param mixed $default * @return mixed|bool|Session * @throws Exception */ function session(array|string|null $key = null, mixed $default = null): mixed { $session = \request()->session(); if (null === $key) { return $session; } if (is_array($key)) { $session->put($key); return null; } if (strpos($key, '.')) { $keyArray = explode('.', $key); $value = $session->all(); foreach ($keyArray as $index) { if (!isset($value[$index])) { return $default; } $value = $value[$index]; } return $value; } return $session->get($key, $default); } } if (!function_exists('trans')) { /** * Translation * @param string $id * @param array $parameters * @param string|null $domain * @param string|null $locale * @return string */ function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string { $res = Translation::trans($id, $parameters, $domain, $locale); return $res === '' ? $id : $res; } } if (!function_exists('locale')) { /** * Locale * @param string|null $locale * @return string */ function locale(?string $locale = null): string { if (!$locale) { return Translation::getLocale(); } Translation::setLocale($locale); return $locale; } } if (!function_exists('not_found')) { /** * 404 not found * @return Response */ function not_found(): Response { return new Response(404, [], file_get_contents(public_path() . '/404.html')); } } if (!function_exists('copy_dir')) { /** * Copy dir * @param string $source * @param string $dest * @param bool $overwrite * @return void */ function copy_dir(string $source, string $dest, bool $overwrite = false) { if (is_dir($source)) { if (!is_dir($dest)) { mkdir($dest); } $files = scandir($source); foreach ($files as $file) { if ($file !== "." && $file !== "..") { copy_dir("$source/$file", "$dest/$file", $overwrite); } } } else if (file_exists($source) && ($overwrite || !file_exists($dest))) { copy($source, $dest); } } } if (!function_exists('remove_dir')) { /** * Remove dir * @param string $dir * @return bool */ function remove_dir(string $dir): bool { if (is_link($dir) || is_file($dir)) { return unlink($dir); } $files = array_diff(scandir($dir), array('.', '..')); foreach ($files as $file) { (is_dir("$dir/$file") && !is_link($dir)) ? remove_dir("$dir/$file") : unlink("$dir/$file"); } return rmdir($dir); } } if (!function_exists('worker_bind')) { /** * Bind worker * @param $worker * @param $class */ function worker_bind($worker, $class) { $callbackMap = [ 'onConnect', 'onMessage', 'onClose', 'onError', 'onBufferFull', 'onBufferDrain', 'onWorkerStop', 'onWebSocketConnect', 'onWorkerReload' ]; foreach ($callbackMap as $name) { if (method_exists($class, $name)) { $worker->$name = [$class, $name]; } } if (method_exists($class, 'onWorkerStart')) { call_user_func([$class, 'onWorkerStart'], $worker); } } } if (!function_exists('worker_start')) { /** * Start worker * @param $processName * @param $config * @return void */ function worker_start($processName, $config) { if (isset($config['enable']) && !$config['enable']) { return; } // feat:custom worker class [default: Workerman\Worker] $class = is_a($class = $config['workerClass'] ?? '', Worker::class, true) ? $class : Worker::class; $worker = new $class($config['listen'] ?? null, $config['context'] ?? []); $properties = [ 'count', 'user', 'group', 'reloadable', 'reusePort', 'transport', 'protocol', 'eventLoop', ]; $worker->name = $processName; foreach ($properties as $property) { if (isset($config[$property])) { $worker->$property = $config[$property]; } } $worker->onWorkerStart = function ($worker) use ($config) { require_once base_path('/support/bootstrap.php'); if (isset($config['handler'])) { if (!class_exists($config['handler'])) { echo "process error: class {$config['handler']} not exists\r\n"; return; } $instance = Container::make($config['handler'], $config['constructor'] ?? []); worker_bind($worker, $instance); } }; } } if (!function_exists('get_realpath')) { /** * Get realpath * @param string $filePath * @return string */ function get_realpath(string $filePath): string { if (strpos($filePath, 'phar://') === 0) { return $filePath; } else { return realpath($filePath); } } } if (!function_exists('is_phar')) { /** * Is phar * @return bool */ function is_phar(): bool { return class_exists(Phar::class, false) && Phar::running(); } } if (!function_exists('template_inputs')) { /** * Get template vars * @param mixed $template * @param array $vars * @param string|null $app * @param string|null $plugin * @return array */ function template_inputs(mixed $template, array $vars, ?string $app, ?string $plugin): array { $request = \request(); $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin; if (is_array($template)) { $vars = $template; $template = null; } if ($template === null && $controller = $request->controller) { $controllerSuffix = config($plugin ? "plugin.$plugin.app.controller_suffix" : "app.controller_suffix", ''); $controllerName = $controllerSuffix !== '' ? substr($controller, 0, -strlen($controllerSuffix)) : $controller; $path = str_replace(['controller', 'Controller', '\\'], ['view', 'view', '/'], $controllerName); $path = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $path)); $action = $request->action; $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); foreach ($backtrace as $backtraceItem) { if (!isset($backtraceItem['class']) || !isset($backtraceItem['function'])) { continue; } if ($backtraceItem['class'] === App::class) { break; } if (preg_match('/\\\\controller\\\\/i', $backtraceItem['class'])) { $action = $backtraceItem['function']; break; } } $actionFileBaseName = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $action)); $template = "/$path/$actionFileBaseName"; } return [$template, $vars, $app, $plugin]; } } if (!function_exists('cpu_count')) { /** * Get cpu count * @return int */ function cpu_count(): int { // Windows does not support the number of processes setting. if (DIRECTORY_SEPARATOR === '\\') { return 1; } $count = 4; if (is_callable('shell_exec')) { if (strtolower(PHP_OS) === 'darwin') { $count = (int)shell_exec('sysctl -n machdep.cpu.core_count'); } else { try { $count = (int)shell_exec('nproc'); } catch (\Throwable $ex) { // Do nothing } } } return $count > 0 ? $count : 4; } } if (!function_exists('input')) { /** * Get request parameters, if no parameter name is passed, an array of all values is returned, default values is supported * @param string|null $param param's name * @param mixed $default default value * @return mixed */ function input(?string $param = null, mixed $default = null): mixed { return is_null($param) ? request()->all() : request()->input($param, $default); } } ================================================ FILE: src/support/view/Blade.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support\view; use Jenssegers\Blade\Blade as BladeView; use Webman\View; use function app_path; use function array_merge; use function base_path; use function config; use function is_array; use function request; use function runtime_path; /** * Class Blade * composer require jenssegers/blade * @package support\view */ class Blade implements View { /** * Assign. * @param string|array $name * @param mixed $value */ public static function assign(string|array $name, mixed $value = null): void { $request = request(); $request->_view_vars = array_merge((array) $request->_view_vars, is_array($name) ? $name : [$name => $value]); } /** * Render. * @param string $template * @param array $vars * @param string|null $app * @param string|null $plugin * @return string */ public static function render(string $template, array $vars, ?string $app = null, ?string $plugin = null): string { static $views = []; $request = request(); $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin; $app = $app === null ? ($request->app ?? '') : $app; $configPrefix = $plugin ? "plugin.$plugin." : ''; $baseViewPath = $plugin ? base_path() . "/plugin/$plugin/app" : app_path(); if ($template[0] === '/') { if (strpos($template, '/view/') !== false) { [$viewPath, $template] = explode('/view/', $template, 2); $viewPath = base_path("$viewPath/view"); } else { $viewPath = base_path(); $template = ltrim($template, '/'); } } else { $viewPath = $app === '' ? "$baseViewPath/view" : "$baseViewPath/$app/view"; } if (!isset($views[$viewPath])) { $views[$viewPath] = new BladeView($viewPath, runtime_path() . '/views'); $extension = config("{$configPrefix}view.extension"); if ($extension) { $extension($views[$viewPath]); } } if(isset($request->_view_vars)) { $vars = array_merge((array)$request->_view_vars, $vars); } return $views[$viewPath]->render($template, $vars); } } ================================================ FILE: src/support/view/Raw.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support\view; use Throwable; use Webman\View; use function app_path; use function array_merge; use function base_path; use function config; use function extract; use function is_array; use function ob_end_clean; use function ob_get_clean; use function ob_start; use function request; /** * Class Raw * @package support\view */ class Raw implements View { /** * Assign. * @param string|array $name * @param mixed $value */ public static function assign(string|array $name, mixed $value = null): void { $request = request(); $request->_view_vars = array_merge((array) $request->_view_vars, is_array($name) ? $name : [$name => $value]); } /** * Render. * @param string $template * @param array $vars * @param string|null $app * @param string|null $plugin * @return string */ public static function render(string $template, array $vars, ?string $app = null, ?string $plugin = null): string { $request = request(); $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin; $configPrefix = $plugin ? "plugin.$plugin." : ''; $viewSuffix = config("{$configPrefix}view.options.view_suffix", 'html'); $app = $app === null ? ($request->app ?? '') : $app; $baseViewPath = $plugin ? base_path() . "/plugin/$plugin/app" : app_path(); $__template_path__ = $template[0] === '/' ? base_path() . "$template.$viewSuffix" : ($app === '' ? "$baseViewPath/view/$template.$viewSuffix" : "$baseViewPath/$app/view/$template.$viewSuffix"); if(isset($request->_view_vars)) { extract((array)$request->_view_vars); } extract($vars); ob_start(); // Try to include php file. try { include $__template_path__; } catch (Throwable $e) { ob_end_clean(); throw $e; } return ob_get_clean(); } } ================================================ FILE: src/support/view/ThinkPHP.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support\view; use think\Template; use Webman\View; use function app_path; use function array_merge; use function base_path; use function config; use function is_array; use function ob_get_clean; use function ob_start; use function request; use function runtime_path; /** * Class Blade * @package support\view */ class ThinkPHP implements View { /** * Assign. * @param string|array $name * @param mixed $value */ public static function assign(string|array $name, mixed $value = null): void { $request = request(); $request->_view_vars = array_merge((array) $request->_view_vars, is_array($name) ? $name : [$name => $value]); } /** * Render. * @param string $template * @param array $vars * @param string|null $app * @param string|null $plugin * @return string */ public static function render(string $template, array $vars, ?string $app = null, ?string $plugin = null): string { $request = request(); $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin; $app = $app === null ? ($request->app ?? '') : $app; $configPrefix = $plugin ? "plugin.$plugin." : ''; $viewSuffix = config("{$configPrefix}view.options.view_suffix", 'html'); $baseViewPath = $plugin ? base_path() . "/plugin/$plugin/app" : app_path(); if ($template[0] === '/') { if (strpos($template, '/view/') !== false) { [$viewPath, $template] = explode('/view/', $template, 2); $viewPath = base_path("$viewPath/view/"); } else { $viewPath = base_path() . dirname($template) . '/'; $template = basename($template); } } else { $viewPath = $app === '' ? "$baseViewPath/view/" : "$baseViewPath/$app/view/"; } $defaultOptions = [ 'view_path' => $viewPath, 'cache_path' => runtime_path() . '/views/', 'view_suffix' => $viewSuffix ]; $options = array_merge($defaultOptions, config("{$configPrefix}view.options", [])); $views = new Template($options); ob_start(); if(isset($request->_view_vars)) { $vars = array_merge((array)$request->_view_vars, $vars); } $views->fetch($template, $vars); return ob_get_clean(); } } ================================================ FILE: src/support/view/Twig.php ================================================ * @copyright walkor * @link http://www.workerman.net/ * @license http://www.opensource.org/licenses/mit-license.php MIT License */ namespace support\view; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Loader\FilesystemLoader; use Webman\View; use function app_path; use function array_merge; use function base_path; use function config; use function is_array; use function request; /** * Class Blade * @package support\view */ class Twig implements View { /** * Assign. * @param string|array $name * @param mixed $value */ public static function assign(string|array $name, mixed $value = null): void { $request = request(); $request->_view_vars = array_merge((array) $request->_view_vars, is_array($name) ? $name : [$name => $value]); } /** * Render. * @param string $template * @param array $vars * @param string|null $app * @param string|null $plugin * @return string */ public static function render(string $template, array $vars, ?string $app = null, ?string $plugin = null): string { static $views = []; $request = request(); $plugin = $plugin === null ? ($request->plugin ?? '') : $plugin; $app = $app === null ? ($request->app ?? '') : $app; $configPrefix = $plugin ? "plugin.$plugin." : ''; $viewSuffix = config("{$configPrefix}view.options.view_suffix", 'html'); $baseViewPath = $plugin ? base_path() . "/plugin/$plugin/app" : app_path(); if ($template[0] === '/') { $template = ltrim($template, '/'); if (strpos($template, '/view/') !== false) { [$viewPath, $template] = explode('/view/', $template, 2); $viewPath = base_path("$viewPath/view"); } else { $viewPath = base_path(); } } else { $viewPath = $app === '' ? "$baseViewPath/view/" : "$baseViewPath/$app/view/"; } if (!isset($views[$viewPath])) { $views[$viewPath] = new Environment(new FilesystemLoader($viewPath), config("{$configPrefix}view.options", [])); $extension = config("{$configPrefix}view.extension"); if ($extension) { $extension($views[$viewPath]); } } if(isset($request->_view_vars)) { $vars = array_merge((array)$request->_view_vars, $vars); } return $views[$viewPath]->render("$template.$viewSuffix", $vars); } } ================================================ FILE: src/windows.php ================================================ load(); } else { Dotenv::createMutable(base_path())->load(); } } App::loadAllConfig(['route']); $errorReporting = config('app.error_reporting'); if (isset($errorReporting)) { error_reporting($errorReporting); } $runtimeProcessPath = runtime_path() . DIRECTORY_SEPARATOR . '/windows'; $paths = [ $runtimeProcessPath, runtime_path('logs'), runtime_path('views') ]; foreach ($paths as $path) { if (!is_dir($path)) { mkdir($path, 0777, true); } } $processFiles = []; if (config('server.listen')) { $processFiles[] = __DIR__ . DIRECTORY_SEPARATOR . 'start.php'; } foreach (config('process', []) as $processName => $config) { $processFiles[] = write_process_file($runtimeProcessPath, $processName, ''); } foreach (config('plugin', []) as $firm => $projects) { foreach ($projects as $name => $project) { if (!is_array($project)) { continue; } foreach ($project['process'] ?? [] as $processName => $config) { $processFiles[] = write_process_file($runtimeProcessPath, $processName, "$firm.$name"); } } foreach ($projects['process'] ?? [] as $processName => $config) { $processFiles[] = write_process_file($runtimeProcessPath, $processName, $firm); } } function write_process_file($runtimeProcessPath, $processName, $firm): string { $processParam = $firm ? "plugin.$firm.$processName" : $processName; $configParam = $firm ? "config('plugin.$firm.process')['$processName']" : "config('process')['$processName']"; $fileContent = << true]); if (!$resource) { exit("Can not execute $cmd\r\n"); } return $resource; } $resource = popen_processes($processFiles); echo "\r\n"; while (1) { sleep(1); if (!empty($monitor) && $monitor->checkAllFilesChange()) { $status = proc_get_status($resource); $pid = $status['pid']; shell_exec("taskkill /F /T /PID $pid"); proc_close($resource); $resource = popen_processes($processFiles); } }