This commit is contained in:
2026-05-01 23:40:14 +08:00
commit b8f599a617
3867 changed files with 478663 additions and 0 deletions
@@ -0,0 +1,5 @@
composer.lock
vendor
vendor/
.idea
.idea/
+5
View File
@@ -0,0 +1,5 @@
# 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
+52
View File
@@ -0,0 +1,52 @@
{
"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 diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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);
}
+321
View File
@@ -0,0 +1,321 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace Webman;
use Psr\Container\ContainerInterface;
use Webman\Exception\NotFoundException;
use function array_key_exists;
use function class_exists;
/**
* Class Container
* @package Webman
*/
class Container implements ContainerInterface
{
/**
* @var array
*/
protected $instances = [];
/**
* @var array
*/
protected $definitions = [];
/**
* Get.
* @param string $name
* @return mixed
* @throws NotFoundException
*/
public function get(string $name)
{
if (!isset($this->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;
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Webman;
use Workerman\Coroutine\Context as WorkermanContext;
use Workerman\Coroutine\Utils\DestructionWatcher;
use Closure;
/**
* Class Context
* @package Webman
*/
class Context extends WorkermanContext
{
public static function onDestroy(Closure $closure): void
{
$obj = static::get('context.onDestroy');
if (!$obj) {
$obj = new \stdClass();
static::set('context.onDestroy', $obj);
}
DestructionWatcher::watch($obj, $closure);
}
}
@@ -0,0 +1,117 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
}
@@ -0,0 +1,121 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
}
@@ -0,0 +1,35 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
@@ -0,0 +1,25 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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
{
}
@@ -0,0 +1,25 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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
{
}
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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);
}
}
@@ -0,0 +1,235 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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<int, array{dir: string, suffix: string}>
*/
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<int, array{dir: string, suffix: string}>
*/
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<int, array{dir: string, suffix: string}>
*/
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<int, array{dir: string, suffix: string}>
*/
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);
}
}
@@ -0,0 +1,142 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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();
}
}
+905
View File
@@ -0,0 +1,905 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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<string, array> PHP file cache: [path => meta, ...]
*/
protected static array $phpCache = [];
/**
* @var array<string, bool> 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 = "<?php\nreturn " . var_export($data, true) . ";\n";
$suffix = (string)getmypid();
try {
$suffix .= '.' . bin2hex(random_bytes(6));
} catch (\Throwable $e) {
$suffix .= '.' . uniqid('', true);
}
$tempFile = $cacheFile . '.tmp.' . $suffix;
// No locks: write temp file then atomic rename.
if (@file_put_contents($tempFile, $content) !== false) {
// On Windows, rename() fails if target exists. Use a backup to swap.
if (!@rename($tempFile, $cacheFile)) {
$backupFile = $cacheFile . '.bak.' . $suffix;
// Best-effort: move old cache away, then move new cache in.
@rename($cacheFile, $backupFile);
if (@rename($tempFile, $cacheFile)) {
@unlink($backupFile);
} else {
// Restore old cache if possible.
@rename($backupFile, $cacheFile);
@unlink($tempFile);
}
}
}
static::$cacheDirty[$cacheKey] = false;
}
/**
* Clear all caches.
* @return void
*/
public static function clearCache(): void
{
static::$phpCache = [];
static::$cacheDirty = [];
}
/**
* Normalize path separators.
* @param string $path
* @return string
*/
protected static function normalizePath(string $path): string
{
$realPath = realpath($path);
if ($realPath !== false) {
return str_replace('\\', '/', $realPath);
}
return str_replace('\\', '/', $path);
}
}
+409
View File
@@ -0,0 +1,409 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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();
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
}
@@ -0,0 +1,111 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace Webman;
class Install
{
const WEBMAN_PLUGIN = true;
/**
* @var array
*/
protected static $pathRelation = [
'start.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, "<?php\n// This file is generated by Webman, please don't modify it.\n");
echo "Clear helpers.php\r\n";
}
}
}
+162
View File
@@ -0,0 +1,162 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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($_)
{
}
}
@@ -0,0 +1,30 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
+848
View File
@@ -0,0 +1,848 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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<string, string>
*/
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<int,array{methods: string[], path: string, callback: array{0:string,1:string}, name: ?string, middlewares: 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;
}
$path = static::normalizeRoutePath($route->path, $controllerClass . '::' . $method->getName());
$fullPath = $prefix ? rtrim($prefix, '/') . $path : $path;
$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<ReflectionAttribute> $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.
* @param string $path
* @param string $source
* @return string
*/
protected static function normalizeRoutePath(string $path, string $source): string
{
$path = trim($path);
if ($path === '' || $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()
{
}
}
+199
View File
@@ -0,0 +1,199 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
}
@@ -0,0 +1,26 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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
{
}
@@ -0,0 +1,22 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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
{
}
@@ -0,0 +1,26 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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
{
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
+167
View File
@@ -0,0 +1,167 @@
<?php
namespace support;
use Dotenv\Dotenv;
use RuntimeException;
use Throwable;
use Webman\Config;
use Webman\Util;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;
use function base_path;
use function call_user_func;
use function is_dir;
use function opcache_get_status;
use function opcache_invalidate;
use const DIRECTORY_SEPARATOR;
class App
{
/**
* Run.
* @return void
* @throws Throwable
*/
public static function run()
{
ini_set('display_errors', 'on');
error_reporting(E_ALL);
if (class_exists(Dotenv::class) && file_exists(run_path('.env'))) {
if (method_exists(Dotenv::class, 'createUnsafeImmutable')) {
Dotenv::createUnsafeImmutable(run_path())->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");
}
}
}
}
@@ -0,0 +1,47 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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);
}
}
@@ -0,0 +1,25 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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
{
}
+143
View File
@@ -0,0 +1,143 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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);
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace support;
use function defined;
use function is_callable;
use function is_file;
use function method_exists;
class Plugin
{
/**
* Install.
* @param mixed $event
* @return void
*/
public static function install($event)
{
static::findHelper();
$psr4 = static::getPsr4($event);
foreach ($psr4 as $namespace => $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';
}
}
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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
{
}
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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
{
}
@@ -0,0 +1,108 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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);
}
}
@@ -0,0 +1,13 @@
<?php
namespace support\annotation;
use Attribute;
/**
* @deprecated Use support\annotation\route\DisableDefaultRoute instead.
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class DisableDefaultRoute extends \support\annotation\route\DisableDefaultRoute
{
}
@@ -0,0 +1,41 @@
<?php
namespace support\annotation;
use Attribute;
/**
* Attach middlewares to routes/controllers/functions via attributes.
*
* Example:
* #[Middleware(AuthMiddleware::class, RateLimitMiddleware::class)]
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class Middleware
{
/**
* @var array
*/
protected array $middlewares = [];
/**
* @param mixed ...$middlewares Middleware class names.
*/
public function __construct(...$middlewares)
{
$this->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;
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: ['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS'], ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Any extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'DELETE', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Delete extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'DELETE', $name);
}
}
@@ -0,0 +1,13 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Disable webman's default route mapping for a controller or action.
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class DisableDefaultRoute
{
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'GET', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Get extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'GET', $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'HEAD', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Head extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'HEAD', $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'OPTIONS', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Options extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'OPTIONS', $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'PATCH', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Patch extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'PATCH', $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'POST', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Post extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'POST', $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'PUT', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Put extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'PUT', $name);
}
}
@@ -0,0 +1,40 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Define an explicit route, or restrict allowed HTTP methods for default route when path is null.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Route
{
/**
* Route path. Null means "method restriction only" for default route.
*/
public ?string $path;
/**
* @var string[]
*/
public array $methods;
/**
* Route name for URL generation.
*/
public ?string $name;
/**
* @param string|null $path Route path, must start with "/". Null means "method restriction only" for default route.
* @param string|string[] $methods HTTP methods
* @param string|null $name Route name
*/
public function __construct(?string $path = null, array|string $methods = ['GET'], ?string $name = null)
{
$this->path = $path;
$this->methods = is_array($methods) ? $methods : [$methods];
$this->name = $name;
}
}
@@ -0,0 +1,26 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Group routes by controller-level prefix.
*/
#[Attribute(Attribute::TARGET_CLASS)]
class RouteGroup
{
/**
* Prefix for all routes in this controller.
*/
public string $prefix;
/**
* @param string $prefix Route group prefix, e.g. "/api/v1"
*/
public function __construct(string $prefix = '')
{
$this->prefix = $prefix;
}
}
@@ -0,0 +1,139 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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);
@@ -0,0 +1,61 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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];
}
}
}
}
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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
{
}
@@ -0,0 +1,43 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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);
}
}
@@ -0,0 +1,24 @@
<?php
namespace support\exception;
use Throwable;
class InputTypeException extends PageNotFoundException
{
/**
* @var string
*/
protected $template = '/app/view/400';
/**
* InputTypeException constructor.
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = 'Input :parameter must be of type :exceptType, :actualType given', int $code = 400, ?Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,24 @@
<?php
namespace support\exception;
use Throwable;
class InputValueException extends PageNotFoundException
{
/**
* @var string
*/
protected $template = '/app/view/400';
/**
* InputTypeException constructor.
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = 'Input :parameter is invalid.', int $code = 400, ?Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,46 @@
<?php
namespace support\exception;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
class MissingInputException extends PageNotFoundException
{
/**
* @var string
*/
protected $template = '/app/view/400';
/**
* MissingInputException constructor.
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = 'Missing input parameter :parameter', int $code = 400, ?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;
$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));
}
}
@@ -0,0 +1,20 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support\exception;
class NotFoundException extends BusinessException
{
}
@@ -0,0 +1,93 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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 <<<EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$message</title>
<style>
.center {
text-align: center;
}
</style>
</head>
<body>
<h1 class="center">$message</h1>
<hr>
<div class="center">webman</div>
</body>
</html>
EOF;
}
}
@@ -0,0 +1,672 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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;
}
// featcustom 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);
}
}
@@ -0,0 +1,84 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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);
}
}
@@ -0,0 +1,79 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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();
}
}
@@ -0,0 +1,87 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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();
}
}
@@ -0,0 +1,87 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @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);
}
}