This commit is contained in:
LayFi.de
2025-01-27 20:49:47 +08:00
commit 4f3242bca7
2745 changed files with 309418 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# Crontab
A crontab with precision in seconds written in PHP based on [workerman](https://github.com/walkor/workerman).
# Install
```
composer require workerman/crontab
```
# Usage
start.php
```php
<?php
use Workerman\Worker;
require __DIR__ . '/../vendor/autoload.php';
use Workerman\Crontab\Crontab;
$worker = new Worker();
$worker->onWorkerStart = function () {
// Execute the function in the first second of every minute.
new Crontab('1 * * * * *', function(){
echo date('Y-m-d H:i:s')."\n";
});
};
Worker::runAll();
```
Run with commands `php start.php start` or php `start.php start -d`
+34
View File
@@ -0,0 +1,34 @@
{
"name": "workerman/crontab",
"type": "library",
"keywords": [
"crontab"
],
"homepage": "http://www.workerman.net",
"license": "MIT",
"description": "A crontab written in PHP based on workerman",
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "http://www.workerman.net",
"role": "Developer"
}
],
"support": {
"email": "walkor@workerman.net",
"issues": "https://github.com/walkor/workerman/issues",
"forum": "http://wenda.workerman.net/",
"wiki": "http://doc.workerman.net/",
"source": "https://github.com/walkor/crontab"
},
"require": {
"php": ">=7.0",
"workerman/workerman": ">=4.0.20"
},
"autoload": {
"psr-4": {
"Workerman\\Crontab\\": "./src"
}
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
use Workerman\Worker;
require __DIR__ . '/../vendor/autoload.php';
use Workerman\Crontab\Crontab;
$worker = new Worker();
$worker->onWorkerStart = function () {
// Execute the function in the first second of every minute.
new Crontab('1 * * * * *', function(){
echo date('Y-m-d H:i:s')."\n";
});
};
Worker::runAll();
+177
View File
@@ -0,0 +1,177 @@
<?php
/**
* This file is part of workerman.
*
* 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 Workerman\Crontab;
use Workerman\Timer;
/**
* Class Crontab
* @package Workerman\Crontab
*/
class Crontab
{
/**
* @var string
*/
protected $_rule;
/**
* @var callable
*/
protected $_callback;
/**
* @var string
*/
protected $_name;
/**
* @var int
*/
protected $_id;
/**
* @var array
*/
protected static $_instances = [];
/**
* Crontab constructor.
* @param string $rule
* @param callable $callback
* @param string $name
*/
public function __construct($rule, $callback, $name = '')
{
$this->_rule = $rule;
$this->_callback = $callback;
$this->_name = $name;
$this->_id = static::createId();
static::$_instances[$this->_id] = $this;
static::tryInit();
}
/**
* @return string
*/
public function getRule()
{
return $this->_rule;
}
/**
* @return callable
*/
public function getCallback()
{
return $this->_callback;
}
/**
* @return string
*/
public function getName()
{
return $this->_name;
}
/**
* @return int
*/
public function getId()
{
return $this->_id;
}
/**
* @return bool
*/
public function destroy()
{
return static::remove($this->_id);
}
/**
* @return array
*/
public static function getAll()
{
return static::$_instances;
}
/**
* @param $id
* @return bool
*/
public static function remove($id)
{
if ($id instanceof Crontab) {
$id = $id->getId();
}
if (!isset(static::$_instances[$id])) {
return false;
}
unset(static::$_instances[$id]);
return true;
}
/**
* @return int
*/
protected static function createId()
{
static $id = 0;
return ++$id;
}
/**
* tryInit
*/
protected static function tryInit()
{
static $inited = false;
if ($inited) {
return;
}
$inited = true;
$parser = new Parser();
$callback = function () use ($parser, &$callback) {
foreach (static::$_instances as $crontab) {
$rule = $crontab->getRule();
$cb = $crontab->getCallback();
if (!$cb || !$rule) {
continue;
}
$times = $parser->parse($rule);
$now = time();
foreach ($times as $time) {
$t = $time-$now;
if ($t <= 0) {
$t = 0.000001;
}
Timer::add($t, $cb, null, false);
}
}
Timer::add(60 - time()%60, $callback, null, false);
};
$next_time = time()%60;
if ($next_time == 0) {
$next_time = 0.00001;
} else {
$next_time = 60 - $next_time;
}
Timer::add($next_time, $callback, null, false);
}
}
+171
View File
@@ -0,0 +1,171 @@
<?php
/**
* @author: Jan Konieczny <jkonieczny@gmail.com>, group@hyperf.io
* @license: http://www.gnu.org/licenses/
* @license: https://github.com/hyperf/hyperf/blob/master/LICENSE
*
* This is a simple script to parse crontab syntax to get the execution time
*
* Eg.: $timestamp = Crontab::parse('12 * * * 1-5');
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides basic cron syntax parsing functionality
*
* @author: Jan Konieczny <jkonieczny@gmail.com>, group@hyperf.io
* @license: http://www.gnu.org/licenses/
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Workerman\Crontab;
/**
* Class Parser
* @package Workerman\Crontab
*/
class Parser
{
/**
* Finds next execution time(stamp) parsin crontab syntax.
*
* @param string $crontab_string :
* 0 1 2 3 4 5
* * * * * * *
* - - - - - -
* | | | | | |
* | | | | | +----- day of week (0 - 6) (Sunday=0)
* | | | | +----- month (1 - 12)
* | | | +------- day of month (1 - 31)
* | | +--------- hour (0 - 23)
* | +----------- min (0 - 59)
* +------------- sec (0-59)
*
* @param null|int $start_time
* @throws \InvalidArgumentException
* @return int[]
*/
public function parse($crontab_string, $start_time = null)
{
if (! $this->isValid($crontab_string)) {
throw new \InvalidArgumentException('Invalid cron string: ' . $crontab_string);
}
$start_time = $start_time ? $start_time : time();
$date = $this->parseDate($crontab_string);
if (in_array((int) date('i', $start_time), $date['minutes'])
&& in_array((int) date('G', $start_time), $date['hours'])
&& in_array((int) date('j', $start_time), $date['day'])
&& in_array((int) date('w', $start_time), $date['week'])
&& in_array((int) date('n', $start_time), $date['month'])
) {
$result = [];
foreach ($date['second'] as $second) {
$result[] = $start_time + $second;
}
return $result;
}
return [];
}
public function isValid(string $crontab_string): bool
{
if (! preg_match('/^((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)$/i', trim($crontab_string))) {
if (! preg_match('/^((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)$/i', trim($crontab_string))) {
return false;
}
}
return true;
}
/**
* Parse each segment of crontab string.
*/
protected function parseSegment(string $string, int $min, int $max, ?int $start = null)
{
if ($start === null || $start < $min) {
$start = $min;
}
$result = [];
if ($string === '*') {
for ($i = $start; $i <= $max; ++$i) {
$result[] = $i;
}
} elseif (strpos($string, ',') !== false) {
$exploded = explode(',', $string);
foreach ($exploded as $value) {
if (strpos($value, '/') !== false || strpos($string, '-') !== false) {
$result = array_merge($result, $this->parseSegment($value, $min, $max, $start));
continue;
}
if (trim($value) === '' || ! $this->between((int) $value, (int) ($min > $start ? $min : $start), (int) $max)) {
continue;
}
$result[] = (int) $value;
}
} elseif (strpos($string, '/') !== false) {
$exploded = explode('/', $string);
if (strpos($exploded[0], '-') !== false) {
[$nMin, $nMax] = explode('-', $exploded[0]);
$nMin > $min && $min = (int) $nMin;
$nMax < $max && $max = (int) $nMax;
}
$start < $min && $start = $min;
for ($i = $start; $i <= $max;) {
$result[] = $i;
$i += $exploded[1];
}
} elseif (strpos($string, '-') !== false) {
$result = array_merge($result, $this->parseSegment($string . '/1', $min, $max, $start));
} elseif ($this->between((int) $string, $min > $start ? $min : $start, $max)) {
$result[] = (int) $string;
}
return $result;
}
/**
* Determire if the $value is between in $min and $max ?
*/
private function between(int $value, int $min, int $max): bool
{
return $value >= $min && $value <= $max;
}
private function parseDate(string $crontab_string): array
{
$cron = preg_split('/[\\s]+/i', trim($crontab_string));
if (count($cron) == 6) {
$date = [
'second' => $this->parseSegment($cron[0], 0, 59),
'minutes' => $this->parseSegment($cron[1], 0, 59),
'hours' => $this->parseSegment($cron[2], 0, 23),
'day' => $this->parseSegment($cron[3], 1, 31),
'month' => $this->parseSegment($cron[4], 1, 12),
'week' => $this->parseSegment($cron[5], 0, 6),
];
} else {
$date = [
'second' => [1 => 0],
'minutes' => $this->parseSegment($cron[0], 0, 59),
'hours' => $this->parseSegment($cron[1], 0, 23),
'day' => $this->parseSegment($cron[2], 1, 31),
'month' => $this->parseSegment($cron[3], 1, 12),
'week' => $this->parseSegment($cron[4], 0, 6),
];
}
return $date;
}
}
@@ -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
+51
View File
@@ -0,0 +1,51 @@
{
"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.0",
"ext-json": "*",
"workerman/workerman": "^4.2.1 || ^5.0.0 || dev-master",
"nikic/fast-route": "^1.3",
"psr/container": ">=1.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
}
@@ -0,0 +1,10 @@
<?php
namespace Webman\Annotation;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class DisableDefaultRoute
{
}
@@ -0,0 +1,25 @@
<?php
namespace Webman\Annotation;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class Middleware
{
protected array $middlewares = [];
public function __construct(...$middlewares)
{
$this->middlewares = $middlewares;
}
public function getMiddlewares(): array
{
$middlewares = [];
foreach ($this->middlewares as $middleware) {
$middlewares[] = [$middleware, 'process'];
}
return $middlewares;
}
}
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);
}
+296
View File
@@ -0,0 +1,296 @@
<?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;
/**
* 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;
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 = [];
}
/**
* 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 as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['thinkorm']['connections'] ?? [] as $key => $connection) {
$config['thinkorm']['connections']["plugin.$firm.$name.$key"] = $connection;
}
}
}
if (!empty($config['thinkorm']['connections'])) {
$config['thinkorm']['default'] = $config['thinkorm']['default'] ?? key($config['thinkorm']['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;
}
$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) {
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;
}
}
+137
View File
@@ -0,0 +1,137 @@
<?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 Fiber;
use SplObjectStorage;
use StdClass;
use Swow\Coroutine;
use WeakMap;
use Workerman\Events\Revolt;
use Workerman\Events\Swoole;
use Workerman\Events\Swow;
use Workerman\Worker;
use function property_exists;
/**
* Class Context
* @package Webman
*/
class Context
{
/**
* @var SplObjectStorage|WeakMap
*/
protected static $objectStorage;
/**
* @var StdClass
*/
protected static $object;
/**
* @return void
*/
public static function init()
{
if (!static::$objectStorage) {
static::$objectStorage = class_exists(WeakMap::class) ? new WeakMap() : new SplObjectStorage();
static::$object = new StdClass;
}
}
/**
* @return StdClass
*/
protected static function getObject(): StdClass
{
$key = static::getKey();
if (!isset(static::$objectStorage[$key])) {
static::$objectStorage[$key] = new StdClass;
}
return static::$objectStorage[$key];
}
/**
* @return mixed
*/
protected static function getKey()
{
switch (Worker::$eventLoopClass) {
case Revolt::class:
return Fiber::getCurrent();
case Swoole::class:
return \Swoole\Coroutine::getContext();
case Swow::class:
return Coroutine::getCurrent();
}
return static::$object;
}
/**
* @param string|null $key
* @return mixed
*/
public static function get(?string $key = null)
{
$obj = static::getObject();
if ($key === null) {
return $obj;
}
return $obj->$key ?? null;
}
/**
* @param string $key
* @param $value
* @return void
*/
public static function set(string $key, $value): void
{
$obj = static::getObject();
$obj->$key = $value;
}
/**
* @param string $key
* @return void
*/
public static function delete(string $key): void
{
$obj = static::getObject();
unset($obj->$key);
}
/**
* @param string $key
* @return bool
*/
public static function has(string $key): bool
{
$obj = static::getObject();
return property_exists($obj, $key);
}
/**
* @return void
*/
public static function destroy(): void
{
unset(static::$objectStorage[static::getKey()]);
}
}
@@ -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,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;
/**
* This deprecated class will certainly be removed in the future.
* Please use Webman\Session\FileSessionHandler
* @deprecated
* @package Webman
*/
class FileSessionHandler extends Session\FileSessionHandler
{
}
+407
View File
@@ -0,0 +1,407 @@
<?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;
}
/**
* @return void
*/
public function __clone()
{
if ($this->isDirty) {
unset($this->data['get'], $this->data['post'], $this->data['headers']);
}
}
}
+87
View File
@@ -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 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) {
$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;
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace Webman;
class Install
{
const WEBMAN_PLUGIN = true;
/**
* @var array
*/
protected static $pathRelation = [
'start.php' => 'start.php',
'windows.php' => 'windows.php',
'support/bootstrap.php' => 'support/bootstrap.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";
}
}
}
+141
View File
@@ -0,0 +1,141 @@
<?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 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 = [];
/**
* @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');
}
if ($appName === '@') {
$plugin = '';
}
if (strpos($appName, 'plugin.') !== false) {
$explode = explode('.', $appName, 4);
$plugin = $explode[1];
$appName = $explode[2] ?? '';
}
foreach ($middlewares as $className) {
if (method_exists($className, 'process')) {
static::$instances[$plugin][$appName][] = [$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])) {
// Controller middleware annotation
$reflectionClass = new ReflectionClass($controller[0]);
self::prepareAttributeMiddlewares($middlewares, $reflectionClass);
// Controller middleware property
if ($reflectionClass->hasProperty('middleware')) {
$defaultProperties = $reflectionClass->getDefaultProperties();
$middlewaresClasses = $defaultProperties['middleware'];
foreach ((array)$middlewaresClasses as $className) {
$middlewares[] = [$className, 'process'];
}
}
// Route middleware
$middlewares = array_merge($middlewares, $routeMiddlewares);
// Method middleware annotation
if ($reflectionClass->hasMethod($controller[1])) {
self::prepareAttributeMiddlewares($middlewares, $reflectionClass->getMethod($controller[1]));
}
} 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
{
$middlewareAttributes = $reflection->getAttributes(Annotation\Middleware::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;
}
+572
View File
@@ -0,0 +1,572 @@
<?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 Webman\Annotation\DisableDefaultRoute;
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 = [];
/**
* @var RouteObject[]
*/
protected $routes = [];
/**
* @var Route[]
*/
protected $children = [];
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function get(string $path, $callback): RouteObject
{
return static::addRoute('GET', $path, $callback);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function post(string $path, $callback): RouteObject
{
return static::addRoute('POST', $path, $callback);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function put(string $path, $callback): RouteObject
{
return static::addRoute('PUT', $path, $callback);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function patch(string $path, $callback): RouteObject
{
return static::addRoute('PATCH', $path, $callback);
}
/**
* @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);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function options(string $path, $callback): RouteObject
{
return static::addRoute('OPTIONS', $path, $callback);
}
/**
* @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);
}
/**
* @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);
}
/**
* @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;
}
/**
* @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");
}
}
/**
* @return RouteObject[]
*/
public static function getRoutes(): array
{
return static::$allRoutes;
}
/**
* disableDefaultRoute.
*
* @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;
}
/**
* @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;
}
/**
* @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 ($reflectionClass->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF)) {
return true;
}
if ($action && $reflectionClass->hasMethod($action)) {
$reflectionMethod = $reflectionClass->getMethod($action);
if ($reflectionMethod->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF)) {
return true;
}
}
}
return false;
}
/**
* @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;
}
/**
* @param RouteObject $route
*/
public function collect(RouteObject $route)
{
$this->routes[] = $route;
}
/**
* @param string $name
* @param RouteObject $instance
*/
public static function setByName(string $name, RouteObject $instance)
{
static::$nameList[$name] = $instance;
}
/**
* @param string $name
* @return null|RouteObject
*/
public static function getByName(string $name): ?RouteObject
{
return static::$nameList[$name] ?? null;
}
/**
* @param Route $route
* @return void
*/
public function addChild(Route $route)
{
$this->children[] = $route;
}
/**
* @return Route[]
*/
public function getChildren()
{
return $this->children;
}
/**
* @param string $method
* @param string $path
* @return array
*/
public static function dispatch(string $method, string $path): array
{
return static::$dispatcher->dispatch($method, $path);
}
/**
* @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;
}
/**
* @param array|string $methods
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
protected static function addRoute($methods, string $path, $callback): RouteObject
{
$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 = 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;
}
}
});
}
/**
* SetCollector.
* @param RouteCollector $route
* @return void
*/
public static function setCollector(RouteCollector $route)
{
static::$collector = $route;
}
/**
* Fallback.
* @param callable|mixed $callback
* @param string $plugin
* @return void
*/
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];
}
/**
* @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;
}
+172
View File
@@ -0,0 +1,172 @@
<?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 (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);
};
}
// Windows does not support custom processes.
if (DIRECTORY_SEPARATOR === '/') {
foreach (config('process', []) as $processName => $config) {
if (isset($config['enable']) && $config['enable'] == false) {
continue;
}
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) {
if (isset($config['enable']) && $config['enable'] == false) {
continue;
}
worker_start("plugin.$firm.$name.$processName", $config);
}
}
foreach ($projects['process'] ?? [] as $processName => $config) {
if (isset($config['enable']) && $config['enable'] == false) {
continue;
}
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");
}
}
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace support;
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\PdoAdapter;
use Symfony\Component\Cache\Psr16Cache;
use InvalidArgumentException;
/**
* Class Cache
* @package support\bootstrap
*
* Strings methods
* @method static mixed get($key, $default = null)
* @method static bool set($key, $value, $ttl = null)
* @method static bool delete($key)
* @method static bool clear()
* @method static iterable getMultiple($keys, $default = null)
* @method static bool setMultiple($values, $ttl = null)
* @method static bool deleteMultiple($keys)
* @method static bool has($key)
*/
class Cache
{
/**
* @var Psr16Cache[]
*/
public static $instances = [];
/***
* @param string|null $name
* @return Psr16Cache
*/
public static function store(?string $name = null): Psr16Cache
{
$name = $name ?: config('cache.default', 'redis');
$stores = !config('cache') ? [
'redis' => [
'driver' => 'redis',
'connection' => 'default'
],
] : config('cache.stores', []);
if (!isset($stores[$name])) {
throw new InvalidArgumentException("cache.store.$name is not defined. Please check config/cache.php");
}
if (!isset(static::$instances[$name])) {
$driver = $stores[$name]['driver'];
switch ($driver) {
case 'redis':
$client = Redis::connection($stores[$name]['connection'])->client();
$adapter = new RedisAdapter($client);
break;
case 'file':
$adapter = new FilesystemAdapter('', 0, $stores[$name]['path']);
break;
case 'array':
$adapter = new ArrayAdapter(0, $stores[$name]['serialize'] ?? false, 0, 0);
break;
/**
* Pdo can not reconnect when the connection is lost. So we can not use pdo as cache.
*/
/*case 'database':
$adapter = new PdoAdapter(Db::connection($stores[$name]['connection'])->getPdo());
break;*/
default:
throw new InvalidArgumentException("cache.store.$name.driver=$driver is not supported.");
}
static::$instances[$name] = new Psr16Cache($adapter);
}
return static::$instances[$name];
}
/**
* @param $name
* @param $arguments
* @return mixed
*/
public static function __callStatic($name, $arguments)
{
return static::store()->{$name}(... $arguments);
}
}
@@ -0,0 +1,48 @@
<?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)
{
$plugin = \Webman\App::getPluginByClass($name);
return static::instance($plugin)->{$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
{
}
+51
View File
@@ -0,0 +1,51 @@
<?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 Closure;
use Illuminate\Database\Capsule\Manager;
use Illuminate\Database\Connection;
/**
* Class Db
* @package support
* @method static array select(string $query, $bindings = [], $useReadPdo = true)
* @method static int insert(string $query, $bindings = [])
* @method static int update(string $query, $bindings = [])
* @method static int delete(string $query, $bindings = [])
* @method static bool statement(string $query, $bindings = [])
* @method static mixed transaction(Closure $callback, $attempts = 1)
* @method static void beginTransaction()
* @method static void rollBack($toLevel = null)
* @method static void commit()
*/
class Db extends Manager
{
/**
* @return Manager
*/
public static function getInstance()
{
return static::$instance;
}
/**
* @return Connection[]
*/
public static function getConnections()
{
return static::$instance->getDatabaseManager()->getConnections();
}
}
+139
View File
@@ -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
*/
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);
static::$instance[$name] = new Logger($name, $handlers, $processors);
}
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);
}
}
+262
View File
@@ -0,0 +1,262 @@
<?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 Closure;
use Illuminate\Contracts\Pagination\CursorPaginator;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Model as BaseModel;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Query\Grammars\Grammar;
use Illuminate\Database\Query\Processors\Processor;
use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection;
/**
* @method static BaseModel make($attributes = [])
* @method static \Illuminate\Database\Eloquent\Builder|static withGlobalScope($identifier, $scope)
* @method static \Illuminate\Database\Eloquent\Builder|static withoutGlobalScope($scope)
* @method static \Illuminate\Database\Eloquent\Builder|static withoutGlobalScopes($scopes = null)
* @method static array removedScopes()
* @method static \Illuminate\Database\Eloquent\Builder|static whereKey($id)
* @method static \Illuminate\Database\Eloquent\Builder|static whereKeyNot($id)
* @method static \Illuminate\Database\Eloquent\Builder|static where($column, $operator = null, $value = null, $boolean = 'and')
* @method static BaseModel|null firstWhere($column, $operator = null, $value = null, $boolean = 'and')
* @method static \Illuminate\Database\Eloquent\Builder|static orWhere($column, $operator = null, $value = null)
* @method static \Illuminate\Database\Eloquent\Builder|static latest($column = null)
* @method static \Illuminate\Database\Eloquent\Builder|static oldest($column = null)
* @method static \Illuminate\Database\Eloquent\Collection|static hydrate($items)
* @method static \Illuminate\Database\Eloquent\Collection|static fromQuery($query, $bindings = [])
* @method static BaseModel|\Illuminate\Database\Eloquent\Collection|static[]|static|null find($id, $columns = [])
* @method static \Illuminate\Database\Eloquent\Collection|static findMany($ids, $columns = [])
* @method static BaseModel|\Illuminate\Database\Eloquent\Collection|static|static[] findOrFail($id, $columns = [])
* @method static BaseModel|static findOrNew($id, $columns = [])
* @method static BaseModel|static firstOrNew($attributes = [], $values = [])
* @method static BaseModel|static firstOrCreate($attributes = [], $values = [])
* @method static BaseModel|static updateOrCreate($attributes, $values = [])
* @method static BaseModel|static firstOrFail($columns = [])
* @method static BaseModel|static|mixed firstOr($columns = [], $callback = null)
* @method static BaseModel sole($columns = [])
* @method static mixed value($column)
* @method static \Illuminate\Database\Eloquent\Collection[]|static[] get($columns = [])
* @method static BaseModel[]|static[] getModels($columns = [])
* @method static array eagerLoadRelations($models)
* @method static LazyCollection cursor()
* @method static Collection pluck($column, $key = null)
* @method static LengthAwarePaginator paginate($perPage = null, $columns = [], $pageName = 'page', $page = null)
* @method static Paginator simplePaginate($perPage = null, $columns = [], $pageName = 'page', $page = null)
* @method static CursorPaginator cursorPaginate($perPage = null, $columns = [], $cursorName = 'cursor', $cursor = null)
* @method static BaseModel|$this create($attributes = [])
* @method static BaseModel|$this forceCreate($attributes)
* @method static int upsert($values, $uniqueBy, $update = null)
* @method static void onDelete($callback)
* @method static static|mixed scopes($scopes)
* @method static static applyScopes()
* @method static \Illuminate\Database\Eloquent\Builder|static without($relations)
* @method static \Illuminate\Database\Eloquent\Builder|static withOnly($relations)
* @method static BaseModel newModelInstance($attributes = [])
* @method static \Illuminate\Database\Eloquent\Builder|static withCasts($casts)
* @method static Builder getQuery()
* @method static \Illuminate\Database\Eloquent\Builder|static setQuery($query)
* @method static Builder toBase()
* @method static array getEagerLoads()
* @method static \Illuminate\Database\Eloquent\Builder|static setEagerLoads($eagerLoad)
* @method static BaseModel getModel()
* @method static \Illuminate\Database\Eloquent\Builder|static setModel($model)
* @method static Closure getMacro($name)
* @method static bool hasMacro($name)
* @method static Closure getGlobalMacro($name)
* @method static bool hasGlobalMacro($name)
* @method static static clone ()
* @method static \Illuminate\Database\Eloquent\Builder|static has($relation, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orHas($relation, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static doesntHave($relation, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orDoesntHave($relation)
* @method static \Illuminate\Database\Eloquent\Builder|static whereHas($relation, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereHas($relation, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static whereDoesntHave($relation, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereDoesntHave($relation, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orHasMorph($relation, $types, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static doesntHaveMorph($relation, $types, $boolean = 'and', $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orDoesntHaveMorph($relation, $types)
* @method static \Illuminate\Database\Eloquent\Builder|static whereHasMorph($relation, $types, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereHasMorph($relation, $types, $callback = null, $operator = '>=', $count = 1)
* @method static \Illuminate\Database\Eloquent\Builder|static whereDoesntHaveMorph($relation, $types, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static orWhereDoesntHaveMorph($relation, $types, $callback = null)
* @method static \Illuminate\Database\Eloquent\Builder|static withAggregate($relations, $column, $function = null)
* @method static \Illuminate\Database\Eloquent\Builder|static withCount($relations)
* @method static \Illuminate\Database\Eloquent\Builder|static withMax($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withMin($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withSum($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withAvg($relation, $column)
* @method static \Illuminate\Database\Eloquent\Builder|static withExists($relation)
* @method static \Illuminate\Database\Eloquent\Builder|static mergeConstraintsFrom($from)
* @method static Collection explain()
* @method static bool chunk($count, $callback)
* @method static Collection chunkMap($callback, $count = 1000)
* @method static bool each($callback, $count = 1000)
* @method static bool chunkById($count, $callback, $column = null, $alias = null)
* @method static bool eachById($callback, $count = 1000, $column = null, $alias = null)
* @method static LazyCollection lazy($chunkSize = 1000)
* @method static LazyCollection lazyById($chunkSize = 1000, $column = null, $alias = null)
* @method static BaseModel|object|static|null first($columns = [])
* @method static BaseModel|object|null baseSole($columns = [])
* @method static \Illuminate\Database\Eloquent\Builder|static tap($callback)
* @method static mixed when($value, $callback, $default = null)
* @method static mixed unless($value, $callback, $default = null)
* @method static Builder select($columns = [])
* @method static Builder selectSub($query, $as)
* @method static Builder selectRaw($expression, $bindings = [])
* @method static Builder fromSub($query, $as)
* @method static Builder fromRaw($expression, $bindings = [])
* @method static Builder addSelect($column)
* @method static Builder distinct()
* @method static Builder from($table, $as = null)
* @method static Builder join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)
* @method static Builder joinWhere($table, $first, $operator, $second, $type = 'inner')
* @method static Builder joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false)
* @method static Builder leftJoin($table, $first, $operator = null, $second = null)
* @method static Builder leftJoinWhere($table, $first, $operator, $second)
* @method static Builder leftJoinSub($query, $as, $first, $operator = null, $second = null)
* @method static Builder rightJoin($table, $first, $operator = null, $second = null)
* @method static Builder rightJoinWhere($table, $first, $operator, $second)
* @method static Builder rightJoinSub($query, $as, $first, $operator = null, $second = null)
* @method static Builder crossJoin($table, $first = null, $operator = null, $second = null)
* @method static Builder crossJoinSub($query, $as)
* @method static void mergeWheres($wheres, $bindings)
* @method static array prepareValueAndOperator($value, $operator, $useDefault = false)
* @method static Builder whereColumn($first, $operator = null, $second = null, $boolean = 'and')
* @method static Builder orWhereColumn($first, $operator = null, $second = null)
* @method static Builder whereRaw($sql, $bindings = [], $boolean = 'and')
* @method static Builder orWhereRaw($sql, $bindings = [])
* @method static Builder whereIn($column, $values, $boolean = 'and', $not = false)
* @method static Builder orWhereIn($column, $values)
* @method static Builder whereNotIn($column, $values, $boolean = 'and')
* @method static Builder orWhereNotIn($column, $values)
* @method static Builder whereIntegerInRaw($column, $values, $boolean = 'and', $not = false)
* @method static Builder orWhereIntegerInRaw($column, $values)
* @method static Builder whereIntegerNotInRaw($column, $values, $boolean = 'and')
* @method static Builder orWhereIntegerNotInRaw($column, $values)
* @method static Builder whereNull($columns, $boolean = 'and', $not = false)
* @method static Builder orWhereNull($column)
* @method static Builder whereNotNull($columns, $boolean = 'and')
* @method static Builder whereBetween($column, $values, $boolean = 'and', $not = false)
* @method static Builder whereBetweenColumns($column, $values, $boolean = 'and', $not = false)
* @method static Builder orWhereBetween($column, $values)
* @method static Builder orWhereBetweenColumns($column, $values)
* @method static Builder whereNotBetween($column, $values, $boolean = 'and')
* @method static Builder whereNotBetweenColumns($column, $values, $boolean = 'and')
* @method static Builder orWhereNotBetween($column, $values)
* @method static Builder orWhereNotBetweenColumns($column, $values)
* @method static Builder orWhereNotNull($column)
* @method static Builder whereDate($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereDate($column, $operator, $value = null)
* @method static Builder whereTime($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereTime($column, $operator, $value = null)
* @method static Builder whereDay($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereDay($column, $operator, $value = null)
* @method static Builder whereMonth($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereMonth($column, $operator, $value = null)
* @method static Builder whereYear($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereYear($column, $operator, $value = null)
* @method static Builder whereNested($callback, $boolean = 'and')
* @method static Builder forNestedWhere()
* @method static Builder addNestedWhereQuery($query, $boolean = 'and')
* @method static Builder whereExists($callback, $boolean = 'and', $not = false)
* @method static Builder orWhereExists($callback, $not = false)
* @method static Builder whereNotExists($callback, $boolean = 'and')
* @method static Builder orWhereNotExists($callback)
* @method static Builder addWhereExistsQuery($query, $boolean = 'and', $not = false)
* @method static Builder whereRowValues($columns, $operator, $values, $boolean = 'and')
* @method static Builder orWhereRowValues($columns, $operator, $values)
* @method static Builder whereJsonContains($column, $value, $boolean = 'and', $not = false)
* @method static Builder orWhereJsonContains($column, $value)
* @method static Builder whereJsonDoesntContain($column, $value, $boolean = 'and')
* @method static Builder orWhereJsonDoesntContain($column, $value)
* @method static Builder whereJsonLength($column, $operator, $value = null, $boolean = 'and')
* @method static Builder orWhereJsonLength($column, $operator, $value = null)
* @method static Builder dynamicWhere($method, $parameters)
* @method static Builder groupBy(...$groups)
* @method static Builder groupByRaw($sql, $bindings = [])
* @method static Builder having($column, $operator = null, $value = null, $boolean = 'and')
* @method static Builder orHaving($column, $operator = null, $value = null)
* @method static Builder havingBetween($column, $values, $boolean = 'and', $not = false)
* @method static Builder havingRaw($sql, $bindings = [], $boolean = 'and')
* @method static Builder orHavingRaw($sql, $bindings = [])
* @method static Builder orderBy($column, $direction = 'asc')
* @method static Builder orderByDesc($column)
* @method static Builder inRandomOrder($seed = '')
* @method static Builder orderByRaw($sql, $bindings = [])
* @method static Builder skip($value)
* @method static Builder offset($value)
* @method static Builder take($value)
* @method static Builder limit($value)
* @method static Builder forPage($page, $perPage = 15)
* @method static Builder forPageBeforeId($perPage = 15, $lastId = 0, $column = 'id')
* @method static Builder forPageAfterId($perPage = 15, $lastId = 0, $column = 'id')
* @method static Builder reorder($column = null, $direction = 'asc')
* @method static Builder union($query, $all = false)
* @method static Builder unionAll($query)
* @method static Builder lock($value = true)
* @method static Builder lockForUpdate()
* @method static Builder sharedLock()
* @method static Builder beforeQuery($callback)
* @method static void applyBeforeQueryCallbacks()
* @method static string toSql()
* @method static int getCountForPagination($columns = [])
* @method static string implode($column, $glue = '')
* @method static bool exists()
* @method static bool doesntExist()
* @method static mixed existsOr($callback)
* @method static mixed doesntExistOr($callback)
* @method static int count($columns = '*')
* @method static mixed min($column)
* @method static mixed max($column)
* @method static mixed sum($column)
* @method static mixed avg($column)
* @method static mixed average($column)
* @method static mixed aggregate($function, $columns = [])
* @method static float|int numericAggregate($function, $columns = [])
* @method static bool insert($values)
* @method static int insertOrIgnore($values)
* @method static int insertGetId($values, $sequence = null)
* @method static int insertUsing($columns, $query)
* @method static bool updateOrInsert($attributes, $values = [])
* @method static void truncate()
* @method static Expression raw($value)
* @method static array getBindings()
* @method static array getRawBindings()
* @method static Builder setBindings($bindings, $type = 'where')
* @method static Builder addBinding($value, $type = 'where')
* @method static Builder mergeBindings($query)
* @method static array cleanBindings($bindings)
* @method static Processor getProcessor()
* @method static Grammar getGrammar()
* @method static Builder useWritePdo()
* @method static static cloneWithout($properties)
* @method static static cloneWithoutBindings($except)
* @method static Builder dump()
* @method static void dd()
* @method static void macro($name, $macro)
* @method static void mixin($mixin, $replace = true)
* @method static mixed macroCall($method, $parameters)
*/
class Model extends BaseModel
{
}
+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';
}
}
+316
View File
@@ -0,0 +1,316 @@
<?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 Illuminate\Events\Dispatcher;
use Illuminate\Redis\Connections\Connection;
use Illuminate\Redis\RedisManager;
use Workerman\Timer;
use Workerman\Worker;
use function class_exists;
use function config;
use function in_array;
/**
* Class Redis
* @package support
*
* Strings methods
* @method static int append($key, $value)
* @method static int bitCount($key)
* @method static int decr($key, $value = 1)
* @method static int decrBy($key, $value)
* @method static string|bool get($key)
* @method static int getBit($key, $offset)
* @method static string getRange($key, $start, $end)
* @method static string getSet($key, $value)
* @method static int incr($key, $value = 1)
* @method static int incrBy($key, $value)
* @method static float incrByFloat($key, $value)
* @method static array mGet(array $keys)
* @method static array getMultiple(array $keys)
* @method static bool mSet($pairs)
* @method static bool mSetNx($pairs)
* @method static bool set($key, $value, $expireResolution = null, $expireTTL = null, $flag = null)
* @method static bool setBit($key, $offset, $value)
* @method static bool setEx($key, $ttl, $value)
* @method static bool pSetEx($key, $ttl, $value)
* @method static bool setNx($key, $value)
* @method static string setRange($key, $offset, $value)
* @method static int strLen($key)
* Keys methods
* @method static int del(...$keys)
* @method static int unlink(...$keys)
* @method static false|string dump($key)
* @method static int exists(...$keys)
* @method static bool expire($key, $ttl)
* @method static bool pexpire($key, $ttl)
* @method static bool expireAt($key, $timestamp)
* @method static bool pexpireAt($key, $timestamp)
* @method static array keys($pattern)
* @method static bool|array scan($it)
* @method static void migrate($host, $port, $keys, $dbIndex, $timeout, $copy = false, $replace = false)
* @method static bool select($dbIndex)
* @method static bool move($key, $dbIndex)
* @method static string|int|bool object($information, $key)
* @method static bool persist($key)
* @method static string randomKey()
* @method static bool rename($srcKey, $dstKey)
* @method static bool renameNx($srcKey, $dstKey)
* @method static string type($key)
* @method static int|array sort($key, $options = [])
* @method static int ttl($key)
* @method static int pttl($key)
* @method static void restore($key, $ttl, $value)
* Hashes methods
* @method static false|int hSet($key, $hashKey, $value)
* @method static bool hSetNx($key, $hashKey, $value)
* @method static false|string hGet($key, $hashKey)
* @method static false|int hLen($key)
* @method static false|int hDel($key, ...$hashKeys)
* @method static array hKeys($key)
* @method static array hVals($key)
* @method static array hGetAll($key)
* @method static bool hExists($key, $hashKey)
* @method static int hIncrBy($key, $hashKey, $value)
* @method static float hIncrByFloat($key, $hashKey, $value)
* @method static bool hMSet($key, $members)
* @method static array hMGet($key, $memberKeys)
* @method static array hScan($key, $iterator, $pattern = '', $count = 0)
* @method static int hStrLen($key, $hashKey)
* Lists methods
* @method static array blPop($keys, $timeout)
* @method static array brPop($keys, $timeout)
* @method static false|string bRPopLPush($srcKey, $dstKey, $timeout)
* @method static false|string lIndex($key, $index)
* @method static int lInsert($key, $position, $pivot, $value)
* @method static false|string lPop($key)
* @method static false|int lPush($key, ...$entries)
* @method static false|int lPushx($key, $value)
* @method static array lRange($key, $start, $end)
* @method static false|int lRem($key, $count, $value)
* @method static bool lSet($key, $index, $value)
* @method static false|array lTrim($key, $start, $end)
* @method static false|string rPop($key)
* @method static false|string rPopLPush($srcKey, $dstKey)
* @method static false|int rPush($key, ...$entries)
* @method static false|int rPushX($key, $value)
* @method static false|int lLen($key)
* Sets methods
* @method static int sAdd($key, $value)
* @method static int sCard($key)
* @method static array sDiff($keys)
* @method static false|int sDiffStore($dst, $keys)
* @method static false|array sInter($keys)
* @method static false|int sInterStore($dst, $keys)
* @method static bool sIsMember($key, $member)
* @method static array sMembers($key)
* @method static bool sMove($src, $dst, $member)
* @method static false|string|array sPop($key, $count = 0)
* @method static false|string|array sRandMember($key, $count = 0)
* @method static int sRem($key, ...$members)
* @method static array sUnion(...$keys)
* @method static false|int sUnionStore($dst, ...$keys)
* @method static false|array sScan($key, $iterator, $pattern = '', $count = 0)
* Sorted sets methods
* @method static array bzPopMin($keys, $timeout)
* @method static array bzPopMax($keys, $timeout)
* @method static int zAdd($key, $score, $value)
* @method static int zCard($key)
* @method static int zCount($key, $start, $end)
* @method static double zIncrBy($key, $value, $member)
* @method static int zinterstore($keyOutput, $arrayZSetKeys, $arrayWeights = [], $aggregateFunction = '')
* @method static array zPopMin($key, $count)
* @method static array zPopMax($key, $count)
* @method static array zRange($key, $start, $end, $withScores = false)
* @method static array zRangeByScore($key, $start, $end, $options = [])
* @method static array zRevRangeByScore($key, $start, $end, $options = [])
* @method static array zRangeByLex($key, $min, $max, $offset = 0, $limit = 0)
* @method static int zRank($key, $member)
* @method static int zRevRank($key, $member)
* @method static int zRem($key, ...$members)
* @method static int zRemRangeByRank($key, $start, $end)
* @method static int zRemRangeByScore($key, $start, $end)
* @method static array zRevRange($key, $start, $end, $withScores = false)
* @method static double zScore($key, $member)
* @method static int zunionstore($keyOutput, $arrayZSetKeys, $arrayWeights = [], $aggregateFunction = '')
* @method static false|array zScan($key, $iterator, $pattern = '', $count = 0)
* HyperLogLogs methods
* @method static int pfAdd($key, $values)
* @method static int pfCount($keys)
* @method static bool pfMerge($dstKey, $srcKeys)
* Geocoding methods
* @method static int geoAdd($key, $longitude, $latitude, $member, ...$items)
* @method static array geoHash($key, ...$members)
* @method static array geoPos($key, ...$members)
* @method static double geoDist($key, $members, $unit = '')
* @method static int|array geoRadius($key, $longitude, $latitude, $radius, $unit, $options = [])
* @method static array geoRadiusByMember($key, $member, $radius, $units, $options = [])
* Streams methods
* @method static int xAck($stream, $group, $arrMessages)
* @method static string xAdd($strKey, $strId, $arrMessage, $iMaxLen = 0, $booApproximate = false)
* @method static array xClaim($strKey, $strGroup, $strConsumer, $minIdleTime, $arrIds, $arrOptions = [])
* @method static int xDel($strKey, $arrIds)
* @method static mixed xGroup($command, $strKey, $strGroup, $strMsgId, $booMKStream = null)
* @method static mixed xInfo($command, $strStream, $strGroup = null)
* @method static int xLen($stream)
* @method static array xPending($strStream, $strGroup, $strStart = 0, $strEnd = 0, $iCount = 0, $strConsumer = null)
* @method static array xRange($strStream, $strStart, $strEnd, $iCount = 0)
* @method static array xRead($arrStreams, $iCount = 0, $iBlock = null)
* @method static array xReadGroup($strGroup, $strConsumer, $arrStreams, $iCount = 0, $iBlock = null)
* @method static array xRevRange($strStream, $strEnd, $strStart, $iCount = 0)
* @method static int xTrim($strStream, $iMaxLen, $booApproximate = null)
* Pub/sub methods
* @method static mixed pSubscribe($patterns, $callback)
* @method static mixed publish($channel, $message)
* @method static mixed subscribe($channels, $callback)
* @method static mixed pubSub($keyword, $argument = null)
* Generic methods
* @method static mixed rawCommand(...$commandAndArgs)
* Transactions methods
* @method static \Redis multi()
* @method static mixed exec()
* @method static mixed discard()
* @method static mixed watch($keys)
* @method static mixed unwatch($keys)
* Scripting methods
* @method static mixed eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null)
* @method static mixed evalSha($scriptSha, $numkeys, ...$arguments)
* @method static mixed script($command, ...$scripts)
* @method static mixed client(...$args)
* @method static null|string getLastError()
* @method static bool clearLastError()
* @method static mixed _prefix($value)
* @method static mixed _serialize($value)
* @method static mixed _unserialize($value)
* Introspection methods
* @method static bool isConnected()
* @method static mixed getHost()
* @method static mixed getPort()
* @method static false|int getDbNum()
* @method static false|double getTimeout()
* @method static mixed getReadTimeout()
* @method static mixed getPersistentID()
* @method static mixed getAuth()
*/
class Redis
{
/**
* @var RedisManager
*/
protected static $instance = null;
/**
* need to install phpredis extension
*/
const PHPREDIS_CLIENT = 'phpredis';
/**
* need to install the 'predis/predis' packgage.
* cmd: composer install predis/predis
*/
const PREDIS_CLIENT = 'predis';
/**
* Support client collection
*/
static $allowClient = [
self::PHPREDIS_CLIENT,
self::PREDIS_CLIENT
];
/**
* The Redis server configurations.
*
* @var array
*/
protected static $config = [];
/**
* Static timers facilitate deletion during callbacks.
*
* @var array
*/
protected static $timers = [];
/**
* The number of seconds an idle connection will be terminated.
*
* @var int
*/
protected static $idle_time = 0;
/**
* @return RedisManager
*/
public static function instance(): ?RedisManager
{
if (!static::$instance) {
$config = config('redis');
$client = $config['client'] ?? self::PHPREDIS_CLIENT;
if (!in_array($client, static::$allowClient)) {
$client = self::PHPREDIS_CLIENT;
}
static::$config = $config;
static::$instance = new RedisManager('', $client, $config);
}
return static::$instance;
}
/**
* Connection.
* @param string $name
* @return Connection|\Redis
*/
public static function connection(string $name = 'default'): Connection
{
if (!empty(static::$config[$name]['idle_timeout'])) {
static::$idle_time = time();
}
$connection = static::instance()->connection($name);
if (!isset(static::$timers[$name])) {
static::$timers[$name] = Worker::getAllWorkers() ? Timer::add(55, function () use ($connection, $name) {
if (!empty(static::$config[$name]['idle_timeout'])
&& time() - static::$idle_time > static::$config[$name]['idle_timeout']) {
Timer::del(static::$timers[$name]);
unset(static::$timers[$name]);
return $connection->client()->close();
}
$connection->get('ping');
}) : 1;
if (class_exists(Dispatcher::class)) {
$connection->setEventDispatcher(new Dispatcher());
}
}
return $connection;
}
/**
* @param string $name
* @param array $arguments
* @return mixed
*/
public static function __callStatic(string $name, array $arguments)
{
return static::connection()->{$name}(... $arguments);
}
}
@@ -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,107 @@
<?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
* @return Translator
* @throws NotFoundException
*/
public static function instance(string $plugin = ''): Translator
{
if (!isset(static::$instance[$plugin])) {
$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 = [
'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)) {
throw new NotFoundException("File {$path} not found");
}
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,10 @@
<?php
namespace support\annotation;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class DisableDefaultRoute extends \Webman\Annotation\DisableDefaultRoute
{
}
@@ -0,0 +1,11 @@
<?php
namespace support\annotation;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class Middleware extends \Webman\Annotation\Middleware
{
}
@@ -0,0 +1,126 @@
<?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 Illuminate\Container\Container as IlluminateContainer;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\MySqlConnection;
use Illuminate\Events\Dispatcher;
use Illuminate\Pagination\Paginator;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\Cursor;
use Jenssegers\Mongodb\Connection as JenssegersMongodbConnection;
use MongoDB\Laravel\Connection as LaravelMongodbConnection;
use support\Container;
use Throwable;
use Webman\Bootstrap;
use Workerman\Timer;
use Workerman\Worker;
use function class_exists;
use function config;
/**
* Class Laravel
* @package support\Bootstrap
*/
class LaravelDb implements Bootstrap
{
/**
* @param Worker|null $worker
*
* @return void
*/
public static function start(?Worker $worker)
{
if (!class_exists(Capsule::class)) {
return;
}
$config = config('database', []);
$connections = $config['connections'] ?? [];
if (!$connections) {
return;
}
$capsule = new Capsule(IlluminateContainer::getInstance());
$capsule->getDatabaseManager()->extend('mongodb', function ($config, $name) {
$config['name'] = $name;
return class_exists(LaravelMongodbConnection::class) ? new LaravelMongodbConnection($config) : new JenssegersMongodbConnection($config);
});
$default = $config['default'] ?? false;
$persistent = $config['persistent'] ?? true;
if ($default) {
$defaultConfig = $connections[$config['default']] ?? false;
if ($defaultConfig) {
$capsule->addConnection($defaultConfig);
}
}
foreach ($connections as $name => $config) {
$capsule->addConnection($config, $name);
}
if (class_exists(Dispatcher::class) && !$capsule->getEventDispatcher()) {
$capsule->setEventDispatcher(Container::make(Dispatcher::class, [IlluminateContainer::getInstance()]));
}
$capsule->setAsGlobal();
$capsule->bootEloquent();
// Heartbeat
if ($worker && $persistent) {
Timer::add(55, function () use ($default, $connections, $capsule) {
foreach ($capsule->getDatabaseManager()->getConnections() as $connection) {
/* @var MySqlConnection $connection **/
if ($connection->getConfig('driver') == 'mysql' && $connection->getRawPdo()) {
try {
$connection->select('select 1');
} catch (Throwable $e) {}
}
}
});
}
// Paginator
if (class_exists(Paginator::class)) {
if (method_exists(Paginator::class, 'queryStringResolver')) {
Paginator::queryStringResolver(function () {
$request = request();
return $request ? $request->queryString() : null;
});
}
Paginator::currentPathResolver(function () {
$request = request();
return $request ? $request->path(): '/';
});
Paginator::currentPageResolver(function ($pageName = 'page') {
$request = request();
if (!$request) {
return 1;
}
$page = (int)($request->input($pageName, 1));
return $page > 0 ? $page : 1;
});
if (class_exists(CursorPaginator::class)) {
CursorPaginator::currentCursorResolver(function ($cursorName = 'cursor') {
return Cursor::fromEncoded(request()->input($cursorName));
});
}
}
}
}
@@ -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,683 @@
<?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): 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);
}
}
if (!function_exists('enum_exists')) {
/**
* Enum exists.
* @return bool
*/
function enum_exists(): bool
{
return false;
}
}
@@ -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);
}
}
+21
View File
@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2009-2015 walkor<walkor@workerman.net> and contributors (see https://github.com/walkor/workerman/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+403
View File
@@ -0,0 +1,403 @@
# Workerman
[![Gitter](https://badges.gitter.im/walkor/Workerman.svg)](https://gitter.im/walkor/Workerman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge)
[![Latest Stable Version](https://poser.pugx.org/workerman/workerman/v/stable)](https://packagist.org/packages/workerman/workerman)
[![Total Downloads](https://poser.pugx.org/workerman/workerman/downloads)](https://packagist.org/packages/workerman/workerman)
[![Monthly Downloads](https://poser.pugx.org/workerman/workerman/d/monthly)](https://packagist.org/packages/workerman/workerman)
[![Daily Downloads](https://poser.pugx.org/workerman/workerman/d/daily)](https://packagist.org/packages/workerman/workerman)
[![License](https://poser.pugx.org/workerman/workerman/license)](https://packagist.org/packages/workerman/workerman)
## What is it
Workerman is an asynchronous event-driven PHP framework with high performance to build fast and scalable network applications.
Workerman supports HTTP, Websocket, SSL and other custom protocols.
Workerman supports event extension.
## Requires
A POSIX compatible operating system (Linux, OSX, BSD)
POSIX and PCNTL extensions required
Event extension recommended for better performance
## Installation
```
composer require workerman/workerman
```
## Documentation
[https://manual.workerman.net](https://manual.workerman.net)
## Basic Usage
### A websocket server
```php
<?php
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
// Create a Websocket server
$ws_worker = new Worker('websocket://0.0.0.0:2346');
// Emitted when new connection come
$ws_worker->onConnect = function ($connection) {
echo "New connection\n";
};
// Emitted when data received
$ws_worker->onMessage = function ($connection, $data) {
// Send hello $data
$connection->send('Hello ' . $data);
};
// Emitted when connection closed
$ws_worker->onClose = function ($connection) {
echo "Connection closed\n";
};
// Run worker
Worker::runAll();
```
### An http server
```php
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
// #### http worker ####
$http_worker = new Worker('http://0.0.0.0:2345');
// 4 processes
$http_worker->count = 4;
// Emitted when data received
$http_worker->onMessage = function ($connection, $request) {
//$request->get();
//$request->post();
//$request->header();
//$request->cookie();
//$request->session();
//$request->uri();
//$request->path();
//$request->method();
// Send data to client
$connection->send("Hello World");
};
// Run all workers
Worker::runAll();
```
### A tcp server
```php
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
// #### create socket and listen 1234 port ####
$tcp_worker = new Worker('tcp://0.0.0.0:1234');
// 4 processes
$tcp_worker->count = 4;
// Emitted when new connection come
$tcp_worker->onConnect = function ($connection) {
echo "New Connection\n";
};
// Emitted when data received
$tcp_worker->onMessage = function ($connection, $data) {
// Send data to client
$connection->send("Hello $data \n");
};
// Emitted when connection is closed
$tcp_worker->onClose = function ($connection) {
echo "Connection closed\n";
};
Worker::runAll();
```
### Enable SSL
```php
<?php
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
// SSL context.
$context = [
'ssl' => [
'local_cert' => '/your/path/of/server.pem',
'local_pk' => '/your/path/of/server.key',
'verify_peer' => false,
]
];
// Create a Websocket server with ssl context.
$ws_worker = new Worker('websocket://0.0.0.0:2346', $context);
// Enable SSL. WebSocket+SSL means that Secure WebSocket (wss://).
// The similar approaches for Https etc.
$ws_worker->transport = 'ssl';
$ws_worker->onMessage = function ($connection, $data) {
// Send hello $data
$connection->send('Hello ' . $data);
};
Worker::runAll();
```
### Custom protocol
Protocols/MyTextProtocol.php
```php
namespace Protocols;
/**
* User defined protocol
* Format Text+"\n"
*/
class MyTextProtocol
{
public static function input($recv_buffer)
{
// Find the position of the first occurrence of "\n"
$pos = strpos($recv_buffer, "\n");
// Not a complete package. Return 0 because the length of package can not be calculated
if ($pos === false) {
return 0;
}
// Return length of the package
return $pos + 1;
}
public static function decode($recv_buffer)
{
return trim($recv_buffer);
}
public static function encode($data)
{
return $data . "\n";
}
}
```
```php
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
// #### MyTextProtocol worker ####
$text_worker = new Worker('MyTextProtocol://0.0.0.0:5678');
$text_worker->onConnect = function ($connection) {
echo "New connection\n";
};
$text_worker->onMessage = function ($connection, $data) {
// Send data to client
$connection->send("Hello world\n");
};
$text_worker->onClose = function ($connection) {
echo "Connection closed\n";
};
// Run all workers
Worker::runAll();
```
### Timer
```php
use Workerman\Worker;
use Workerman\Timer;
require_once __DIR__ . '/vendor/autoload.php';
$task = new Worker();
$task->onWorkerStart = function ($task) {
// 2.5 seconds
$time_interval = 2.5;
$timer_id = Timer::add($time_interval, function () {
echo "Timer run\n";
});
};
// Run all workers
Worker::runAll();
```
### AsyncTcpConnection (tcp/ws/text/frame etc...)
```php
use Workerman\Worker;
use Workerman\Connection\AsyncTcpConnection;
require_once __DIR__ . '/vendor/autoload.php';
$worker = new Worker();
$worker->onWorkerStart = function () {
// Websocket protocol for client.
$ws_connection = new AsyncTcpConnection('ws://echo.websocket.org:80');
$ws_connection->onConnect = function ($connection) {
$connection->send('Hello');
};
$ws_connection->onMessage = function ($connection, $data) {
echo "Recv: $data\n";
};
$ws_connection->onError = function ($connection, $code, $msg) {
echo "Error: $msg\n";
};
$ws_connection->onClose = function ($connection) {
echo "Connection closed\n";
};
$ws_connection->connect();
};
Worker::runAll();
```
#### Use HTTP proxy
```php
<?php
require __DIR__ . '/vendor/autoload.php';
use Workerman\Connection\AsyncTcpConnection;
$worker = new \Workerman\Worker();
$worker->onWorkerStart = function($worker){
echo '开始链接' . PHP_EOL;
$url = 'ws://stream.binance.com:9443/ws';
$con = new AsyncTcpConnection($url);
$con->transport = 'ssl';
// $con->proxySocks5 = '127.0.0.1:1080';
$con->proxyHttp = '127.0.0.1:25378';
$con->onConnect = function(AsyncTcpConnection $con) {
$ww = [
'id' => 1,
'method' => 'SUBSCRIBE',
'params' => [
"btcusdt@aggTrade",
"btcusdt@depth"
]
];
echo '链接成功';
$con->send(json_encode($ww));
echo 'ok';
};
$con->onMessage = function(AsyncTcpConnection $con, $data) {
echo $data;
};
$con->onClose = function (AsyncTcpConnection $con) {
echo 'onClose' . PHP_EOL;
};
$con->onError = function (AsyncTcpConnection $con, $code, $msg) {
echo "error [ $code ] $msg\n";
};
$con->connect();
};
\Workerman\Worker::runAll();
```
#### Use Socks5 proxy
```php
<?php
require __DIR__ . '/vendor/autoload.php';
use Workerman\Connection\AsyncTcpConnection;
$worker = new \Workerman\Worker();
$worker->onWorkerStart = function($worker){
echo '开始链接' . PHP_EOL;
$url = 'ws://stream.binance.com:9443/ws';
$con = new AsyncTcpConnection($url);
$con->transport = 'ssl';
$con->proxySocks5 = '127.0.0.1:1080';
// $con->proxyHttp = '127.0.0.1:25378';
$con->onConnect = function(AsyncTcpConnection $con) {
$ww = [
'id' => 1,
'method' => 'SUBSCRIBE',
'params' => [
"btcusdt@aggTrade",
"btcusdt@depth"
]
];
echo '链接成功';
$con->send(json_encode($ww));
echo 'ok';
};
$con->onMessage = function(AsyncTcpConnection $con, $data) {
echo $data;
};
$con->onClose = function (AsyncTcpConnection $con) {
echo 'onClose' . PHP_EOL;
};
$con->onError = function (AsyncTcpConnection $con, $code, $msg) {
echo "error [ $code ] $msg\n";
};
$con->connect();
};
\Workerman\Worker::runAll();
```
proxy supports TLS1.3, no Sniproxy channel
## Available commands
```php start.php start ```
```php start.php start -d ```
```php start.php status ```
```php start.php status -d ```
```php start.php connections```
```php start.php stop ```
```php start.php stop -g ```
```php start.php restart ```
```php start.php reload ```
```php start.php reload -g ```
# Benchmarks
https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext&l=zik073-1r
## Other links with workerman
[webman](https://github.com/walkor/webman)
[AdapterMan](https://github.com/joanhey/AdapterMan)
## Donate
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UQGGS9UB35WWG">PayPal</a>
## LICENSE
Workerman is released under the [MIT license](https://github.com/walkor/workerman/blob/master/MIT-LICENSE.txt).
+6
View File
@@ -0,0 +1,6 @@
# Security Policy
## Reporting a Vulnerability
Please contact by email walkor@workerman.net
+59
View File
@@ -0,0 +1,59 @@
{
"name": "workerman/workerman",
"type": "library",
"keywords": [
"event-loop",
"asynchronous",
"http",
"framework"
],
"homepage": "https://www.workerman.net",
"license": "MIT",
"description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "https://www.workerman.net",
"role": "Developer"
}
],
"support": {
"email": "walkor@workerman.net",
"issues": "https://github.com/walkor/workerman/issues",
"forum": "https://www.workerman.net/questions",
"wiki": "https://www.workerman.net/doc/workerman/",
"source": "https://github.com/walkor/workerman"
},
"require": {
"php": ">=8.1",
"ext-json": "*"
},
"suggest": {
"ext-event": "For better performance. "
},
"autoload": {
"psr-4": {
"Workerman\\": "src"
}
},
"minimum-stability": "dev",
"conflict": {
"ext-swow": "<v1.0.0"
},
"require-dev": {
"pestphp/pest": "2.x-dev",
"mockery/mockery": "^1.6",
"guzzlehttp/guzzle": "^7.0",
"phpstan/phpstan": "1.11.x-dev"
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"scripts": {
"analyze": "phpstan",
"test": "pest --colors=always"
}
}
@@ -0,0 +1,444 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Connection;
use Exception;
use RuntimeException;
use stdClass;
use Throwable;
use Workerman\Timer;
use Workerman\Worker;
use function class_exists;
use function explode;
use function function_exists;
use function is_resource;
use function method_exists;
use function microtime;
use function parse_url;
use function socket_import_stream;
use function socket_set_option;
use function stream_context_create;
use function stream_set_blocking;
use function stream_set_read_buffer;
use function stream_socket_client;
use function stream_socket_get_name;
use function ucfirst;
use const DIRECTORY_SEPARATOR;
use const PHP_INT_MAX;
use const SO_KEEPALIVE;
use const SOL_SOCKET;
use const SOL_TCP;
use const STREAM_CLIENT_ASYNC_CONNECT;
use const TCP_NODELAY;
/**
* AsyncTcpConnection.
*/
class AsyncTcpConnection extends TcpConnection
{
/**
* PHP built-in protocols.
*
* @var array<string, string>
*/
public const BUILD_IN_TRANSPORTS = [
'tcp' => 'tcp',
'udp' => 'udp',
'unix' => 'unix',
'ssl' => 'ssl',
'sslv2' => 'sslv2',
'sslv3' => 'sslv3',
'tls' => 'tls'
];
/**
* Emitted when socket connection is successfully established.
*
* @var ?callable
*/
public $onConnect = null;
/**
* Emitted when websocket handshake completed (Only work when protocol is ws).
*
* @var ?callable
*/
public $onWebSocketConnect = null;
/**
* Transport layer protocol.
*
* @var string
*/
public string $transport = 'tcp';
/**
* Socks5 proxy.
*
* @var string
*/
public string $proxySocks5 = '';
/**
* Http proxy.
*
* @var string
*/
public string $proxyHttp = '';
/**
* Status.
*
* @var int
*/
protected int $status = self::STATUS_INITIAL;
/**
* Remote host.
*
* @var string
*/
protected string $remoteHost = '';
/**
* Remote port.
*
* @var int
*/
protected int $remotePort = 80;
/**
* Connect start time.
*
* @var float
*/
protected float $connectStartTime = 0;
/**
* Remote URI.
*
* @var string
*/
protected string $remoteURI = '';
/**
* Context option.
*
* @var array
*/
protected array $socketContext = [];
/**
* Reconnect timer.
*
* @var int
*/
protected int $reconnectTimer = 0;
/**
* Construct.
*
* @param string $remoteAddress
* @param array $socketContext
*/
public function __construct(string $remoteAddress, array $socketContext = [])
{
$addressInfo = parse_url($remoteAddress);
if (!$addressInfo) {
[$scheme, $this->remoteAddress] = explode(':', $remoteAddress, 2);
if ('unix' === strtolower($scheme)) {
$this->remoteAddress = substr($remoteAddress, strpos($remoteAddress, '/') + 2);
}
if (!$this->remoteAddress) {
throw new RuntimeException('Bad remoteAddress');
}
} else {
$addressInfo['port'] ??= 0;
$addressInfo['path'] ??= '/';
if (!isset($addressInfo['query'])) {
$addressInfo['query'] = '';
} else {
$addressInfo['query'] = '?' . $addressInfo['query'];
}
$this->remoteHost = $addressInfo['host'];
$this->remotePort = $addressInfo['port'];
$this->remoteURI = "{$addressInfo['path']}{$addressInfo['query']}";
$scheme = $addressInfo['scheme'] ?? 'tcp';
$this->remoteAddress = 'unix' === strtolower($scheme)
? substr($remoteAddress, strpos($remoteAddress, '/') + 2)
: $this->remoteHost . ':' . $this->remotePort;
}
$this->id = $this->realId = self::$idRecorder++;
if (PHP_INT_MAX === self::$idRecorder) {
self::$idRecorder = 0;
}
// Check application layer protocol class.
if (!isset(self::BUILD_IN_TRANSPORTS[$scheme])) {
$scheme = ucfirst($scheme);
$this->protocol = '\\Protocols\\' . $scheme;
if (!class_exists($this->protocol)) {
$this->protocol = "\\Workerman\\Protocols\\$scheme";
if (!class_exists($this->protocol)) {
throw new RuntimeException("class \\Protocols\\$scheme not exist");
}
}
} else {
$this->transport = self::BUILD_IN_TRANSPORTS[$scheme];
}
// For statistics.
++self::$statistics['connection_count'];
$this->maxSendBufferSize = self::$defaultMaxSendBufferSize;
$this->maxPackageSize = self::$defaultMaxPackageSize;
$this->socketContext = $socketContext;
static::$connections[$this->realId] = $this;
$this->context = new stdClass;
}
/**
* Reconnect.
*
* @param int $after
* @return void
*/
public function reconnect(int $after = 0): void
{
$this->status = self::STATUS_INITIAL;
static::$connections[$this->realId] = $this;
if ($this->reconnectTimer) {
Timer::del($this->reconnectTimer);
}
if ($after > 0) {
$this->reconnectTimer = Timer::add($after, $this->connect(...), null, false);
return;
}
$this->connect();
}
/**
* Do connect.
*
* @return void
*/
public function connect(): void
{
if ($this->status !== self::STATUS_INITIAL && $this->status !== self::STATUS_CLOSING &&
$this->status !== self::STATUS_CLOSED) {
return;
}
if (!$this->eventLoop) {
$this->eventLoop = Worker::$globalEvent;
}
$this->status = self::STATUS_CONNECTING;
$this->connectStartTime = microtime(true);
set_error_handler(fn() => false);
if ($this->transport !== 'unix') {
if (!$this->remotePort) {
$this->remotePort = $this->transport === 'ssl' ? 443 : 80;
$this->remoteAddress = $this->remoteHost . ':' . $this->remotePort;
}
// Open socket connection asynchronously.
if ($this->proxySocks5) {
$this->socketContext['ssl']['peer_name'] = $this->remoteHost;
$context = stream_context_create($this->socketContext);
$this->socket = stream_socket_client("tcp://$this->proxySocks5", $errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context);
} else if ($this->proxyHttp) {
$this->socketContext['ssl']['peer_name'] = $this->remoteHost;
$context = stream_context_create($this->socketContext);
$this->socket = stream_socket_client("tcp://$this->proxyHttp", $errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context);
} else if ($this->socketContext) {
$context = stream_context_create($this->socketContext);
$this->socket = stream_socket_client("tcp://$this->remoteHost:$this->remotePort",
$errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT, $context);
} else {
$this->socket = stream_socket_client("tcp://$this->remoteHost:$this->remotePort",
$errno, $err_str, 0, STREAM_CLIENT_ASYNC_CONNECT);
}
} else {
$this->socket = stream_socket_client("$this->transport://$this->remoteAddress", $errno, $err_str, 0,
STREAM_CLIENT_ASYNC_CONNECT);
}
restore_error_handler();
// If failed attempt to emit onError callback.
if (!$this->socket || !is_resource($this->socket)) {
$this->emitError(static::CONNECT_FAIL, $err_str);
if ($this->status === self::STATUS_CLOSING) {
$this->destroy();
}
if ($this->status === self::STATUS_CLOSED) {
$this->onConnect = null;
}
return;
}
// Add socket to global event loop waiting connection is successfully established or failed.
$this->eventLoop->onWritable($this->socket, $this->checkConnection(...));
// For windows.
if (DIRECTORY_SEPARATOR === '\\' && method_exists($this->eventLoop, 'onExcept')) {
$this->eventLoop->onExcept($this->socket, $this->checkConnection(...));
}
}
/**
* Try to emit onError callback.
*
* @param int $code
* @param mixed $msg
* @return void
*/
protected function emitError(int $code, mixed $msg): void
{
$this->status = self::STATUS_CLOSING;
if ($this->onError) {
try {
($this->onError)($this, $code, $msg);
} catch (Throwable $e) {
$this->error($e);
}
}
}
/**
* CancelReconnect.
*/
public function cancelReconnect(): void
{
if ($this->reconnectTimer) {
Timer::del($this->reconnectTimer);
$this->reconnectTimer = 0;
}
}
/**
* Get remote address.
*
* @return string
*/
public function getRemoteHost(): string
{
return $this->remoteHost;
}
/**
* Get remote URI.
*
* @return string
*/
public function getRemoteURI(): string
{
return $this->remoteURI;
}
/**
* Check connection is successfully established or failed.
*
* @return void
*/
public function checkConnection(): void
{
// Remove EV_EXPECT for windows.
if (DIRECTORY_SEPARATOR === '\\' && method_exists($this->eventLoop, 'offExcept')) {
$this->eventLoop->offExcept($this->socket);
}
// Remove write listener.
$this->eventLoop->offWritable($this->socket);
if ($this->status !== self::STATUS_CONNECTING) {
return;
}
// Check socket state.
if ($address = stream_socket_get_name($this->socket, true)) {
// Proxy
if ($this->proxySocks5 && $address === $this->proxySocks5) {
fwrite($this->socket, chr(5) . chr(1) . chr(0));
fread($this->socket, 512);
fwrite($this->socket, chr(5) . chr(1) . chr(0) . chr(3) . chr(strlen($this->remoteHost)) . $this->remoteHost . pack("n", $this->remotePort));
fread($this->socket, 512);
}
if ($this->proxyHttp && $address === $this->proxyHttp) {
$str = "CONNECT $this->remoteHost:$this->remotePort HTTP/1.1\r\n";
$str .= "Host: $this->remoteHost:$this->remotePort\r\n";
$str .= "Proxy-Connection: keep-alive\r\n\r\n";
fwrite($this->socket, $str);
fread($this->socket, 512);
}
// Nonblocking.
stream_set_blocking($this->socket, false);
// Compatible with hhvm
if (function_exists('stream_set_read_buffer')) {
stream_set_read_buffer($this->socket, 0);
}
// Try to open keepalive for tcp and disable Nagle algorithm.
if (function_exists('socket_import_stream') && $this->transport === 'tcp') {
$socket = socket_import_stream($this->socket);
socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1);
if (defined('TCP_KEEPIDLE') && defined('TCP_KEEPINTVL') && defined('TCP_KEEPCNT')) {
socket_set_option($socket, SOL_TCP, TCP_KEEPIDLE, static::TCP_KEEPALIVE_INTERVAL);
socket_set_option($socket, SOL_TCP, TCP_KEEPINTVL, static::TCP_KEEPALIVE_INTERVAL);
socket_set_option($socket, SOL_TCP, TCP_KEEPCNT, 1);
}
}
// SSL handshake.
if ($this->transport === 'ssl') {
$this->sslHandshakeCompleted = $this->doSslHandshake($this->socket);
if ($this->sslHandshakeCompleted === false) {
return;
}
} else {
// There are some data waiting to send.
if ($this->sendBuffer) {
$this->eventLoop->onWritable($this->socket, $this->baseWrite(...));
}
}
// Register a listener waiting read event.
$this->eventLoop->onReadable($this->socket, $this->baseRead(...));
$this->status = self::STATUS_ESTABLISHED;
$this->remoteAddress = $address;
// Try to emit onConnect callback.
if ($this->onConnect) {
try {
($this->onConnect)($this);
} catch (Throwable $e) {
$this->error($e);
}
}
// Try to emit protocol::onConnect
if ($this->protocol && method_exists($this->protocol, 'onConnect')) {
try {
$this->protocol::onConnect($this);
} catch (Throwable $e) {
$this->error($e);
}
}
} else {
// Connection failed.
$this->emitError(static::CONNECT_FAIL, 'connect ' . $this->remoteAddress . ' fail after ' . round(microtime(true) - $this->connectStartTime, 4) . ' seconds');
if ($this->status === self::STATUS_CLOSING) {
$this->destroy();
}
if ($this->status === self::STATUS_CLOSED) {
$this->onConnect = null;
}
}
}
}
@@ -0,0 +1,210 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Connection;
use Exception;
use RuntimeException;
use Throwable;
use Workerman\Protocols\ProtocolInterface;
use Workerman\Worker;
use function class_exists;
use function explode;
use function fclose;
use function stream_context_create;
use function stream_set_blocking;
use function stream_socket_client;
use function stream_socket_recvfrom;
use function stream_socket_sendto;
use function strlen;
use function substr;
use function ucfirst;
use const STREAM_CLIENT_CONNECT;
/**
* AsyncUdpConnection.
*/
class AsyncUdpConnection extends UdpConnection
{
/**
* Emitted when socket connection is successfully established.
*
* @var ?callable
*/
public $onConnect = null;
/**
* Emitted when socket connection closed.
*
* @var ?callable
*/
public $onClose = null;
/**
* Connected or not.
*
* @var bool
*/
protected bool $connected = false;
/**
* Context option.
*
* @var array
*/
protected array $contextOption = [];
/**
* Construct.
*
* @param string $remoteAddress
* @throws Throwable
*/
public function __construct($remoteAddress, $contextOption = [])
{
// Get the application layer communication protocol and listening address.
[$scheme, $address] = explode(':', $remoteAddress, 2);
// Check application layer protocol class.
if ($scheme !== 'udp') {
$scheme = ucfirst($scheme);
$this->protocol = '\\Protocols\\' . $scheme;
if (!class_exists($this->protocol)) {
$this->protocol = "\\Workerman\\Protocols\\$scheme";
if (!class_exists($this->protocol)) {
throw new RuntimeException("class \\Protocols\\$scheme not exist");
}
}
}
$this->remoteAddress = substr($address, 2);
$this->contextOption = $contextOption;
}
/**
* For udp package.
*
* @param resource $socket
* @return void
*/
public function baseRead($socket): void
{
$recvBuffer = stream_socket_recvfrom($socket, static::MAX_UDP_PACKAGE_SIZE, 0, $remoteAddress);
if (false === $recvBuffer || empty($remoteAddress)) {
return;
}
if ($this->onMessage) {
if ($this->protocol) {
$recvBuffer = $this->protocol::decode($recvBuffer, $this);
}
++ConnectionInterface::$statistics['total_request'];
try {
($this->onMessage)($this, $recvBuffer);
} catch (Throwable $e) {
$this->error($e);
}
}
}
/**
* Close connection.
*
* @param mixed $data
* @param bool $raw
* @return void
*/
public function close(mixed $data = null, bool $raw = false): void
{
if ($data !== null) {
$this->send($data, $raw);
}
$this->eventLoop->offReadable($this->socket);
fclose($this->socket);
$this->connected = false;
// Try to emit onClose callback.
if ($this->onClose) {
try {
($this->onClose)($this);
} catch (Throwable $e) {
$this->error($e);
}
}
$this->onConnect = $this->onMessage = $this->onClose = $this->eventLoop = $this->errorHandler = null;
}
/**
* Sends data on the connection.
*
* @param mixed $sendBuffer
* @param bool $raw
* @return bool|null
*/
public function send(mixed $sendBuffer, bool $raw = false): bool|null
{
if (false === $raw && $this->protocol) {
$sendBuffer = $this->protocol::encode($sendBuffer, $this);
if ($sendBuffer === '') {
return null;
}
}
if ($this->connected === false) {
$this->connect();
}
return strlen($sendBuffer) === stream_socket_sendto($this->socket, $sendBuffer);
}
/**
* Connect.
*
* @return void
*/
public function connect(): void
{
if ($this->connected === true) {
return;
}
if (!$this->eventLoop) {
$this->eventLoop = Worker::$globalEvent;
}
if ($this->contextOption) {
$context = stream_context_create($this->contextOption);
$this->socket = stream_socket_client("udp://$this->remoteAddress", $errno, $errmsg,
30, STREAM_CLIENT_CONNECT, $context);
} else {
$this->socket = stream_socket_client("udp://$this->remoteAddress", $errno, $errmsg);
}
if (!$this->socket) {
Worker::safeEcho((string)(new Exception($errmsg)));
$this->eventLoop = null;
return;
}
stream_set_blocking($this->socket, false);
if ($this->onMessage) {
$this->eventLoop->onReadable($this->socket, $this->baseRead(...));
}
$this->connected = true;
// Try to emit onConnect callback.
if ($this->onConnect) {
try {
($this->onConnect)($this);
} catch (Throwable $e) {
$this->error($e);
}
}
}
}
@@ -0,0 +1,187 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Connection;
use Throwable;
use Workerman\Events\Event;
use Workerman\Events\EventInterface;
use Workerman\Worker;
use AllowDynamicProperties;
/**
* ConnectionInterface.
*/
#[AllowDynamicProperties]
abstract class ConnectionInterface
{
/**
* Connect failed.
*
* @var int
*/
public const CONNECT_FAIL = 1;
/**
* Send failed.
*
* @var int
*/
public const SEND_FAIL = 2;
/**
* Statistics for status command.
*
* @var array
*/
public static array $statistics = [
'connection_count' => 0,
'total_request' => 0,
'throw_exception' => 0,
'send_fail' => 0,
];
/**
* Application layer protocol.
* The format is like this Workerman\\Protocols\\Http.
*
* @var ?class-string
*/
public ?string $protocol = null;
/**
* Emitted when data is received.
*
* @var ?callable
*/
public $onMessage = null;
/**
* Emitted when the other end of the socket sends a FIN packet.
*
* @var ?callable
*/
public $onClose = null;
/**
* Emitted when an error occurs with connection.
*
* @var ?callable
*/
public $onError = null;
/**
* @var ?EventInterface
*/
public ?EventInterface $eventLoop = null;
/**
* @var ?callable
*/
public $errorHandler = null;
/**
* Sends data on the connection.
*
* @param mixed $sendBuffer
* @param bool $raw
* @return bool|null
*/
abstract public function send(mixed $sendBuffer, bool $raw = false): bool|null;
/**
* Get remote IP.
*
* @return string
*/
abstract public function getRemoteIp(): string;
/**
* Get remote port.
*
* @return int
*/
abstract public function getRemotePort(): int;
/**
* Get remote address.
*
* @return string
*/
abstract public function getRemoteAddress(): string;
/**
* Get local IP.
*
* @return string
*/
abstract public function getLocalIp(): string;
/**
* Get local port.
*
* @return int
*/
abstract public function getLocalPort(): int;
/**
* Get local address.
*
* @return string
*/
abstract public function getLocalAddress(): string;
/**
* Close connection.
*
* @param mixed $data
* @param bool $raw
* @return void
*/
abstract public function close(mixed $data = null, bool $raw = false): void;
/**
* Is ipv4.
*
* return bool.
*/
abstract public function isIpV4(): bool;
/**
* Is ipv6.
*
* return bool.
*/
abstract public function isIpV6(): bool;
/**
* @param Throwable $exception
* @return void
*/
public function error(Throwable $exception): void
{
if (!$this->errorHandler) {
Worker::stopAll(250, $exception);
return;
}
try {
($this->errorHandler)($exception);
} catch (Throwable $exception) {
Worker::stopAll(250, $exception);
return;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,224 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Connection;
use JsonSerializable;
use Workerman\Protocols\ProtocolInterface;
use function stream_socket_get_name;
use function stream_socket_sendto;
use function strlen;
use function strrchr;
use function strrpos;
use function substr;
use function trim;
/**
* UdpConnection.
*/
class UdpConnection extends ConnectionInterface implements JsonSerializable
{
/**
* Max udp package size.
*
* @var int
*/
public const MAX_UDP_PACKAGE_SIZE = 65535;
/**
* Transport layer protocol.
*
* @var string
*/
public string $transport = 'udp';
/**
* Construct.
*
* @param resource $socket
* @param string $remoteAddress
*/
public function __construct(
protected $socket,
protected string $remoteAddress) {}
/**
* Sends data on the connection.
*
* @param mixed $sendBuffer
* @param bool $raw
* @return bool|null
*/
public function send(mixed $sendBuffer, bool $raw = false): bool|null
{
if (false === $raw && $this->protocol) {
$sendBuffer = $this->protocol::encode($sendBuffer, $this);
if ($sendBuffer === '') {
return null;
}
}
return strlen($sendBuffer) === stream_socket_sendto($this->socket, $sendBuffer, 0, $this->isIpV6() ? '[' . $this->getRemoteIp() . ']:' . $this->getRemotePort() : $this->remoteAddress);
}
/**
* Get remote IP.
*
* @return string
*/
public function getRemoteIp(): string
{
$pos = strrpos($this->remoteAddress, ':');
if ($pos) {
return trim(substr($this->remoteAddress, 0, $pos), '[]');
}
return '';
}
/**
* Get remote port.
*
* @return int
*/
public function getRemotePort(): int
{
if ($this->remoteAddress) {
return (int)substr(strrchr($this->remoteAddress, ':'), 1);
}
return 0;
}
/**
* Get remote address.
*
* @return string
*/
public function getRemoteAddress(): string
{
return $this->remoteAddress;
}
/**
* Get local IP.
*
* @return string
*/
public function getLocalIp(): string
{
$address = $this->getLocalAddress();
$pos = strrpos($address, ':');
if (!$pos) {
return '';
}
return substr($address, 0, $pos);
}
/**
* Get local port.
*
* @return int
*/
public function getLocalPort(): int
{
$address = $this->getLocalAddress();
$pos = strrpos($address, ':');
if (!$pos) {
return 0;
}
return (int)substr(strrchr($address, ':'), 1);
}
/**
* Get local address.
*
* @return string
*/
public function getLocalAddress(): string
{
return (string)@stream_socket_get_name($this->socket, false);
}
/**
* Close connection.
*
* @param mixed $data
* @param bool $raw
* @return void
*/
public function close(mixed $data = null, bool $raw = false): void
{
if ($data !== null) {
$this->send($data, $raw);
}
$this->eventLoop = $this->errorHandler = null;
}
/**
* Is ipv4.
*
* return bool.
*/
public function isIpV4(): bool
{
if ($this->transport === 'unix') {
return false;
}
return !str_contains($this->getRemoteIp(), ':');
}
/**
* Is ipv6.
*
* return bool.
*/
public function isIpV6(): bool
{
if ($this->transport === 'unix') {
return false;
}
return str_contains($this->getRemoteIp(), ':');
}
/**
* Get the real socket.
*
* @return resource
*/
public function getSocket()
{
return $this->socket;
}
/**
* Get the json_encode information.
*
* @return array
*/
public function jsonSerialize(): array
{
return [
'transport' => $this->transport,
'getRemoteIp' => $this->getRemoteIp(),
'remotePort' => $this->getRemotePort(),
'getRemoteAddress' => $this->getRemoteAddress(),
'getLocalIp' => $this->getLocalIp(),
'getLocalPort' => $this->getLocalPort(),
'getLocalAddress' => $this->getLocalAddress(),
'isIpV4' => $this->isIpV4(),
'isIpV6' => $this->isIpV6(),
];
}
}
+238
View File
@@ -0,0 +1,238 @@
<?php
/**
* This file is part of workerman.
*
* 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>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Events;
/**
* Ev eventloop
*/
final class Ev implements EventInterface
{
/**
* All listeners for read event.
*
* @var array<int, \EvIo>
*/
private array $readEvents = [];
/**
* All listeners for write event.
*
* @var array<int, \EvIo>
*/
private array $writeEvents = [];
/**
* Event listeners of signal.
*
* @var array<int, \EvSignal>
*/
private array $eventSignal = [];
/**
* All timer event listeners.
*
* @var array<int, \EvTimer>
*/
private array $eventTimer = [];
/**
* @var ?callable
*/
private $errorHandler = null;
/**
* Timer id.
*
* @var int
*/
private static int $timerId = 1;
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$timerId = self::$timerId;
$event = new \EvTimer($delay, 0, function () use ($func, $args, $timerId) {
unset($this->eventTimer[$timerId]);
$this->safeCall($func, $args);
});
$this->eventTimer[self::$timerId] = $event;
return self::$timerId++;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
$this->eventTimer[$timerId]->stop();
unset($this->eventTimer[$timerId]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$event = new \EvTimer($interval, $interval, fn () => $this->safeCall($func, $args));
$this->eventTimer[self::$timerId] = $event;
return self::$timerId++;
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$fdKey = (int)$stream;
$event = new \EvIo($stream, \Ev::READ, fn () => $this->safeCall($func, [$stream]));
$this->readEvents[$fdKey] = $event;
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->readEvents[$fdKey])) {
$this->readEvents[$fdKey]->stop();
unset($this->readEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$fdKey = (int)$stream;
$event = new \EvIo($stream, \Ev::WRITE, fn () => $this->safeCall($func, [$stream]));
$this->writeEvents[$fdKey] = $event;
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->writeEvents[$fdKey])) {
$this->writeEvents[$fdKey]->stop();
unset($this->writeEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
$event = new \EvSignal($signal, fn () => $this->safeCall($func, [$signal]));
$this->eventSignal[$signal] = $event;
}
/**
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
if (isset($this->eventSignal[$signal])) {
$this->eventSignal[$signal]->stop();
unset($this->eventSignal[$signal]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
foreach ($this->eventTimer as $event) {
$event->stop();
}
$this->eventTimer = [];
}
/**
* {@inheritdoc}
*/
public function run(): void
{
\Ev::run();
}
/**
* {@inheritdoc}
*/
public function stop(): void
{
\Ev::stop();
}
/**
* {@inheritdoc}
*/
public function getTimerCount(): int
{
return count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->errorHandler = $errorHandler;
}
/**
* @param callable $func
* @param array $args
* @return void
*/
private function safeCall(callable $func, array $args = []): void
{
try {
$func(...$args);
} catch (\Throwable $e) {
if ($this->errorHandler === null) {
echo $e;
} else {
($this->errorHandler)($e);
}
}
}
}
+292
View File
@@ -0,0 +1,292 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Events;
/**
* libevent eventloop
*/
final class Event implements EventInterface
{
/**
* Event base.
*
* @var \EventBase
*/
private \EventBase $eventBase;
/**
* All listeners for read event.
*
* @var array<int, \Event>
*/
private array $readEvents = [];
/**
* All listeners for write event.
*
* @var array<int, \Event>
*/
private array $writeEvents = [];
/**
* Event listeners of signal.
*
* @var array<int, \Event>
*/
private array $eventSignal = [];
/**
* All timer event listeners.
*
* @var array<int, \Event>
*/
private array $eventTimer = [];
/**
* Timer id.
*
* @var int
*/
private int $timerId = 0;
/**
* Event class name.
*
* @var string
*/
private string $eventClassName = '';
/**
* @var ?callable
*/
private $errorHandler = null;
/**
* Construct.
*/
public function __construct()
{
if (\class_exists('\\\\Event', false)) {
$className = '\\\\Event';
} else {
$className = '\Event';
}
$this->eventClassName = $className;
if (\class_exists('\\\\EventBase', false)) {
$className = '\\\\EventBase';
} else {
$className = '\EventBase';
}
$this->eventBase = new $className();
}
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$className = $this->eventClassName;
$timerId = $this->timerId++;
$event = new $className($this->eventBase, -1, $className::TIMEOUT, function () use ($func, $args, $timerId) {
unset($this->eventTimer[$timerId]);
$this->safeCall($func, $args);
});
if (!$event->addTimer($delay)) {
throw new \RuntimeException("Event::addTimer($delay) failed");
}
$this->eventTimer[$timerId] = $event;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
$this->eventTimer[$timerId]->del();
unset($this->eventTimer[$timerId]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$className = $this->eventClassName;
$timerId = $this->timerId++;
$event = new $className($this->eventBase, -1, $className::TIMEOUT | $className::PERSIST, $func);
if (!$event->addTimer($interval)) {
throw new \RuntimeException("Event::addTimer($interval) failed");
}
$this->eventTimer[$timerId] = $event;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$className = $this->eventClassName;
$fdKey = (int)$stream;
$event = new $className($this->eventBase, $stream, $className::READ | $className::PERSIST, $func);
if ($event->add()) {
$this->readEvents[$fdKey] = $event;
}
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->readEvents[$fdKey])) {
$this->readEvents[$fdKey]->del();
unset($this->readEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$className = $this->eventClassName;
$fdKey = (int)$stream;
$event = new $className($this->eventBase, $stream, $className::WRITE | $className::PERSIST, $func);
if ($event->add()) {
$this->writeEvents[$fdKey] = $event;
}
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->writeEvents[$fdKey])) {
$this->writeEvents[$fdKey]->del();
unset($this->writeEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
$className = $this->eventClassName;
$fdKey = $signal;
$event = $className::signal($this->eventBase, $signal, fn () => $this->safeCall($func, [$signal]));
if ($event->add()) {
$this->eventSignal[$fdKey] = $event;
}
}
/**
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
$fdKey = $signal;
if (isset($this->eventSignal[$fdKey])) {
$this->eventSignal[$fdKey]->del();
unset($this->eventSignal[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
foreach ($this->eventTimer as $event) {
$event->del();
}
$this->eventTimer = [];
}
/**
* {@inheritdoc}
*/
public function run(): void
{
$this->eventBase->loop();
}
/**
* {@inheritdoc}
*/
public function stop(): void
{
$this->eventBase->exit();
}
/**
* {@inheritdoc}
*/
public function getTimerCount(): int
{
return \count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->errorHandler = $errorHandler;
}
/**
* @param callable $func
* @param array $args
* @return void
*/
private function safeCall(callable $func, array $args = []): void
{
try {
$func(...$args);
} catch (\Throwable $e) {
if ($this->errorHandler === null) {
echo $e;
} else {
($this->errorHandler)($e);
}
}
}
}
+143
View File
@@ -0,0 +1,143 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Events;
interface EventInterface
{
/**
* Delay the execution of a callback.
*
* @param float $delay
* @param callable(mixed...): void $func
* @param array $args
* @return int
*/
public function delay(float $delay, callable $func, array $args = []): int;
/**
* Delete a delay timer.
*
* @param int $timerId
* @return bool
*/
public function offDelay(int $timerId): bool;
/**
* Repeatedly execute a callback.
*
* @param float $interval
* @param callable(mixed...): void $func
* @param array $args
* @return int
*/
public function repeat(float $interval, callable $func, array $args = []): int;
/**
* Delete a repeat timer.
*
* @param int $timerId
* @return bool
*/
public function offRepeat(int $timerId): bool;
/**
* Execute a callback when a stream resource becomes readable or is closed for reading.
*
* @param resource $stream
* @param callable(resource): void $func
* @return void
*/
public function onReadable($stream, callable $func): void;
/**
* Cancel a callback of stream readable.
*
* @param resource $stream
* @return bool
*/
public function offReadable($stream): bool;
/**
* Execute a callback when a stream resource becomes writable or is closed for writing.
*
* @param resource $stream
* @param callable(resource): void $func
* @return void
*/
public function onWritable($stream, callable $func): void;
/**
* Cancel a callback of stream writable.
*
* @param resource $stream
* @return bool
*/
public function offWritable($stream): bool;
/**
* Execute a callback when a signal is received.
*
* @param int $signal
* @param callable(int): void $func
* @return void
*/
public function onSignal(int $signal, callable $func): void;
/**
* Cancel a callback of signal.
*
* @param int $signal
* @return bool
*/
public function offSignal(int $signal): bool;
/**
* Delete all timer.
*
* @return void
*/
public function deleteAllTimer(): void;
/**
* Run the event loop.
*
* @return void
*/
public function run(): void;
/**
* Stop event loop.
*
* @return void
*/
public function stop(): void;
/**
* Get Timer count.
*
* @return int
*/
public function getTimerCount(): int;
/**
* Set error handler.
*
* @param callable(\Throwable): void $errorHandler
* @return void
*/
public function setErrorHandler(callable $errorHandler): void;
}
+265
View File
@@ -0,0 +1,265 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Events;
use Revolt\EventLoop;
use Revolt\EventLoop\Driver;
use function count;
use function function_exists;
use function pcntl_signal;
/**
* Revolt eventloop
*/
final class Revolt implements EventInterface
{
/**
* @var Driver
*/
private Driver $driver;
/**
* All listeners for read event.
*
* @var array<int, string>
*/
private array $readEvents = [];
/**
* All listeners for write event.
*
* @var array<int, string>
*/
private array $writeEvents = [];
/**
* Event listeners of signal.
*
* @var array<int, string>
*/
private array $eventSignal = [];
/**
* Event listeners of timer.
*
* @var array<int, string>
*/
private array $eventTimer = [];
/**
* Timer id.
*
* @var int
*/
private int $timerId = 1;
/**
* Construct.
*/
public function __construct()
{
$this->driver = EventLoop::getDriver();
}
/**
* Get driver.
*
* @return Driver
*/
public function driver(): Driver
{
return $this->driver;
}
/**
* {@inheritdoc}
*/
public function run(): void
{
$this->driver->run();
}
/**
* {@inheritdoc}
*/
public function stop(): void
{
foreach ($this->eventSignal as $cbId) {
$this->driver->cancel($cbId);
}
$this->driver->stop();
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGINT, SIG_IGN);
}
}
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$timerId = $this->timerId++;
$closure = function () use ($func, $args, $timerId) {
unset($this->eventTimer[$timerId]);
$func(...$args);
};
$cbId = $this->driver->delay($delay, $closure);
$this->eventTimer[$timerId] = $cbId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$timerId = $this->timerId++;
$cbId = $this->driver->repeat($interval, static fn () => $func(...$args));
$this->eventTimer[$timerId] = $cbId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$fdKey = (int)$stream;
if (isset($this->readEvents[$fdKey])) {
$this->driver->cancel($this->readEvents[$fdKey]);
unset($this->readEvents[$fdKey]);
}
$this->readEvents[$fdKey] = $this->driver->onReadable($stream, static fn () => $func($stream));
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->readEvents[$fdKey])) {
$this->driver->cancel($this->readEvents[$fdKey]);
unset($this->readEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$fdKey = (int)$stream;
if (isset($this->writeEvents[$fdKey])) {
$this->driver->cancel($this->writeEvents[$fdKey]);
unset($this->writeEvents[$fdKey]);
}
$this->writeEvents[$fdKey] = $this->driver->onWritable($stream, static fn () => $func($stream));
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->writeEvents[$fdKey])) {
$this->driver->cancel($this->writeEvents[$fdKey]);
unset($this->writeEvents[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
$fdKey = $signal;
if (isset($this->eventSignal[$fdKey])) {
$this->driver->cancel($this->eventSignal[$fdKey]);
unset($this->eventSignal[$fdKey]);
}
$this->eventSignal[$fdKey] = $this->driver->onSignal($signal, static fn () => $func($signal));
}
/**
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
$fdKey = $signal;
if (isset($this->eventSignal[$fdKey])) {
$this->driver->cancel($this->eventSignal[$fdKey]);
unset($this->eventSignal[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
$this->driver->cancel($this->eventTimer[$timerId]);
unset($this->eventTimer[$timerId]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
foreach ($this->eventTimer as $cbId) {
$this->driver->cancel($cbId);
}
$this->eventTimer = [];
}
/**
* {@inheritdoc}
*/
public function getTimerCount(): int
{
return count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->driver->setErrorHandler($errorHandler);
}
}
+481
View File
@@ -0,0 +1,481 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Events;
use SplPriorityQueue;
use Throwable;
use function count;
use function max;
use function microtime;
use function pcntl_signal;
use function pcntl_signal_dispatch;
use const DIRECTORY_SEPARATOR;
/**
* select eventloop
*/
final class Select implements EventInterface
{
/**
* Running.
*
* @var bool
*/
private bool $running = true;
/**
* All listeners for read/write event.
*
* @var array<int, callable>
*/
private array $readEvents = [];
/**
* All listeners for read/write event.
*
* @var array<int, callable>
*/
private array $writeEvents = [];
/**
* @var array<int, callable>
*/
private array $exceptEvents = [];
/**
* Event listeners of signal.
*
* @var array<int, callable>
*/
private array $signalEvents = [];
/**
* Fds waiting for read event.
*
* @var array<int, resource>
*/
private array $readFds = [];
/**
* Fds waiting for write event.
*
* @var array<int, resource>
*/
private array $writeFds = [];
/**
* Fds waiting for except event.
*
* @var array<int, resource>
*/
private array $exceptFds = [];
/**
* Timer scheduler.
* {['data':timer_id, 'priority':run_timestamp], ..}
*
* @var SplPriorityQueue
*/
private SplPriorityQueue $scheduler;
/**
* All timer event listeners.
* [[func, args, flag, timer_interval], ..]
*
* @var array
*/
private array $eventTimer = [];
/**
* Timer id.
*
* @var int
*/
private int $timerId = 1;
/**
* Select timeout.
*
* @var int
*/
private int $selectTimeout = 100000000;
/**
* Next run time of the timer.
*
* @var float
*/
private float $nextTickTime = 0;
/**
* @var ?callable
*/
private $errorHandler = null;
/**
* Construct.
*/
public function __construct()
{
$this->scheduler = new SplPriorityQueue();
$this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH);
}
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$timerId = $this->timerId++;
$runTime = microtime(true) + $delay;
$this->scheduler->insert($timerId, -$runTime);
$this->eventTimer[$timerId] = [$func, $args];
if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) {
$this->setNextTickTime($runTime);
}
return $timerId;
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$timerId = $this->timerId++;
$runTime = microtime(true) + $interval;
$this->scheduler->insert($timerId, -$runTime);
$this->eventTimer[$timerId] = [$func, $args, $interval];
if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) {
$this->setNextTickTime($runTime);
}
return $timerId;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
unset($this->eventTimer[$timerId]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$count = count($this->readFds);
if ($count >= 1024) {
trigger_error("System call select exceeded the maximum number of connections 1024, please install event extension for more connections.", E_USER_WARNING);
} else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) {
trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING);
}
$fdKey = (int)$stream;
$this->readEvents[$fdKey] = $func;
$this->readFds[$fdKey] = $stream;
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->readEvents[$fdKey])) {
unset($this->readEvents[$fdKey], $this->readFds[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$count = count($this->writeFds);
if ($count >= 1024) {
trigger_error("System call select exceeded the maximum number of connections 1024, please install event/libevent extension for more connections.", E_USER_WARNING);
} else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) {
trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING);
}
$fdKey = (int)$stream;
$this->writeEvents[$fdKey] = $func;
$this->writeFds[$fdKey] = $stream;
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->writeEvents[$fdKey])) {
unset($this->writeEvents[$fdKey], $this->writeFds[$fdKey]);
return true;
}
return false;
}
/**
* On except.
*
* @param resource $stream
* @param callable $func
*/
public function onExcept($stream, callable $func): void
{
$fdKey = (int)$stream;
$this->exceptEvents[$fdKey] = $func;
$this->exceptFds[$fdKey] = $stream;
}
/**
* Off except.
*
* @param resource $stream
* @return bool
*/
public function offExcept($stream): bool
{
$fdKey = (int)$stream;
if (isset($this->exceptEvents[$fdKey])) {
unset($this->exceptEvents[$fdKey], $this->exceptFds[$fdKey]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
if (!function_exists('pcntl_signal')) {
return;
}
$this->signalEvents[$signal] = $func;
pcntl_signal($signal, fn () => $this->safeCall($this->signalEvents[$signal], [$signal]));
}
/**
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
if (!function_exists('pcntl_signal')) {
return false;
}
pcntl_signal($signal, SIG_IGN);
if (isset($this->signalEvents[$signal])) {
unset($this->signalEvents[$signal]);
return true;
}
return false;
}
/**
* Tick for timer.
*
* @return void
*/
protected function tick(): void
{
$tasksToInsert = [];
while (!$this->scheduler->isEmpty()) {
$schedulerData = $this->scheduler->top();
$timerId = $schedulerData['data'];
$nextRunTime = -$schedulerData['priority'];
$timeNow = microtime(true);
$this->selectTimeout = (int)(($nextRunTime - $timeNow) * 1000000);
if ($this->selectTimeout <= 0) {
$this->scheduler->extract();
if (!isset($this->eventTimer[$timerId])) {
continue;
}
// [func, args, timer_interval]
$taskData = $this->eventTimer[$timerId];
if (isset($taskData[2])) {
$nextRunTime = $timeNow + $taskData[2];
$tasksToInsert[] = [$timerId, -$nextRunTime];
} else {
unset($this->eventTimer[$timerId]);
}
$this->safeCall($taskData[0], $taskData[1]);
} else {
break;
}
}
foreach ($tasksToInsert as $item) {
$this->scheduler->insert($item[0], $item[1]);
}
if (!$this->scheduler->isEmpty()) {
$schedulerData = $this->scheduler->top();
$nextRunTime = -$schedulerData['priority'];
$this->setNextTickTime($nextRunTime);
return;
}
$this->setNextTickTime(0);
}
/**
* Set next tick time.
*
* @param float $nextTickTime
* @return void
*/
protected function setNextTickTime(float $nextTickTime): void
{
$this->nextTickTime = $nextTickTime;
if ($nextTickTime == 0) {
$this->selectTimeout = 10000000;
return;
}
$timeNow = microtime(true);
$this->selectTimeout = max((int)(($nextTickTime - $timeNow) * 1000000), 0);
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
$this->scheduler = new SplPriorityQueue();
$this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH);
$this->eventTimer = [];
}
/**
* {@inheritdoc}
*/
public function run(): void
{
while ($this->running) {
$read = $this->readFds;
$write = $this->writeFds;
$except = $this->exceptFds;
if ($read || $write || $except) {
// Waiting read/write/signal/timeout events.
try {
@stream_select($read, $write, $except, 0, $this->selectTimeout);
} catch (Throwable) {
// do nothing
}
} else {
$this->selectTimeout >= 1 && usleep($this->selectTimeout);
}
foreach ($read as $fd) {
$fdKey = (int)$fd;
if (isset($this->readEvents[$fdKey])) {
$this->readEvents[$fdKey]($fd);
}
}
foreach ($write as $fd) {
$fdKey = (int)$fd;
if (isset($this->writeEvents[$fdKey])) {
$this->writeEvents[$fdKey]($fd);
}
}
foreach ($except as $fd) {
$fdKey = (int)$fd;
if (isset($this->exceptEvents[$fdKey])) {
$this->exceptEvents[$fdKey]($fd);
}
}
if ($this->nextTickTime > 0 && microtime(true) >= $this->nextTickTime) {
$this->tick();
}
if ($this->signalEvents) {
// Calls signal handlers for pending signals
pcntl_signal_dispatch();
}
}
}
/**
* {@inheritdoc}
*/
public function stop(): void
{
$this->running = false;
$this->deleteAllTimer();
foreach ($this->signalEvents as $signal => $item) {
$this->offsignal($signal);
}
$this->readFds = [];
$this->writeFds = [];
$this->exceptFds = [];
$this->readEvents = [];
$this->writeEvents = [];
$this->exceptEvents = [];
$this->signalEvents = [];
}
/**
* {@inheritdoc}
*/
public function getTimerCount(): int
{
return count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->errorHandler = $errorHandler;
}
/**
* @param callable $func
* @param array $args
* @return void
*/
private function safeCall(callable $func, array $args = []): void
{
try {
$func(...$args);
} catch (Throwable $e) {
if ($this->errorHandler === null) {
echo $e;
} else {
($this->errorHandler)($e);
}
}
}
}
+289
View File
@@ -0,0 +1,289 @@
<?php
/**
* This file is part of workerman.
*
* 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>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Events;
use Swoole\Coroutine;
use Swoole\Event;
use Swoole\Process;
use Swoole\Timer;
final class Swoole implements EventInterface
{
/**
* All listeners for read timer
*
* @var array<int, int>
*/
private array $eventTimer = [];
/**
* All listeners for read event.
*
* @var array<int, array>
*/
private array $readEvents = [];
/**
* All listeners for write event.
*
* @var array<int, array>
*/
private array $writeEvents = [];
/**
* @var ?callable
*/
private $errorHandler = null;
/**
* Constructor.
*/
public function __construct()
{
Coroutine::set(['hook_flags' => SWOOLE_HOOK_ALL]);
}
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$t = (int)($delay * 1000);
$t = max($t, 1);
$timerId = Timer::after($t, function () use ($func, $args, &$timerId) {
unset($this->eventTimer[$timerId]);
$this->safeCall($func, $args);
});
$this->eventTimer[$timerId] = $timerId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
Timer::clear($timerId);
unset($this->eventTimer[$timerId]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$t = (int)($interval * 1000);
$t = max($t, 1);
$timerId = Timer::tick($t, function () use ($func, $args) {
$this->safeCall($func, $args);
});
$this->eventTimer[$timerId] = $timerId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$fd = (int)$stream;
if (!isset($this->readEvents[$fd]) && !isset($this->writeEvents[$fd])) {
Event::add($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ);
} elseif (isset($this->writeEvents[$fd])) {
Event::set($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ | SWOOLE_EVENT_WRITE);
} else {
Event::set($stream, fn () => $this->callRead($fd), null, SWOOLE_EVENT_READ);
}
$this->readEvents[$fd] = [$func, [$stream]];
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
$fd = (int)$stream;
if (!isset($this->readEvents[$fd])) {
return false;
}
unset($this->readEvents[$fd]);
if (!isset($this->writeEvents[$fd])) {
Event::del($stream);
return true;
}
Event::set($stream, null, null, SWOOLE_EVENT_WRITE);
return true;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$fd = (int)$stream;
if (!isset($this->readEvents[$fd]) && !isset($this->writeEvents[$fd])) {
Event::add($stream, null, fn () => $this->callWrite($fd), SWOOLE_EVENT_WRITE);
} elseif (isset($this->readEvents[$fd])) {
Event::set($stream, null, fn () => $this->callWrite($fd), SWOOLE_EVENT_WRITE | SWOOLE_EVENT_READ);
} else {
Event::set($stream, null, fn () =>$this->callWrite($fd), SWOOLE_EVENT_WRITE);
}
$this->writeEvents[$fd] = [$func, [$stream]];
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fd = (int)$stream;
if (!isset($this->writeEvents[$fd])) {
return false;
}
unset($this->writeEvents[$fd]);
if (!isset($this->readEvents[$fd])) {
Event::del($stream);
return true;
}
Event::set($stream, null, null, SWOOLE_EVENT_READ);
return true;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
Process::signal($signal, fn () => $this->safeCall($func, [$signal]));
}
/**
* Please see https://wiki.swoole.com/#/process/process?id=signal
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
return Process::signal($signal, null);
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
foreach ($this->eventTimer as $timerId) {
Timer::clear($timerId);
}
}
/**
* {@inheritdoc}
*/
public function run(): void
{
// Avoid process exit due to no listening
Timer::tick(100000000, static fn() => null);
Event::wait();
}
/**
* Destroy loop.
*
* @return void
*/
public function stop(): void
{
// Cancel all coroutines before Event::exit
foreach (Coroutine::listCoroutines() as $coroutine) {
Coroutine::cancel($coroutine);
}
// Wait for coroutines to exit
usleep(200000);
Event::exit();
}
/**
* Get timer count.
*
* @return integer
*/
public function getTimerCount(): int
{
return count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->errorHandler = $errorHandler;
}
/**
* @param $fd
* @return void
*/
private function callRead($fd)
{
if (isset($this->readEvents[$fd])) {
$this->safeCall($this->readEvents[$fd][0], $this->readEvents[$fd][1]);
}
}
/**
* @param $fd
* @return void
*/
private function callWrite($fd)
{
if (isset($this->writeEvents[$fd])) {
$this->safeCall($this->writeEvents[$fd][0], $this->writeEvents[$fd][1]);
}
}
/**
* @param callable $func
* @param array $args
* @return void
*/
private function safeCall(callable $func, array $args = []): void
{
try {
$func(...$args);
} catch (\Throwable $e) {
if ($this->errorHandler === null) {
echo $e;
} else {
($this->errorHandler)($e);
}
}
}
}
+297
View File
@@ -0,0 +1,297 @@
<?php
declare(strict_types=1);
namespace Workerman\Events;
use Swow\Coroutine;
use Swow\Signal;
use Swow\SignalException;
use function Swow\Sync\waitAll;
final class Swow implements EventInterface
{
/**
* All listeners for read timer.
*
* @var array<int, int>
*/
private array $eventTimer = [];
/**
* All listeners for read event.
*
* @var array<int, Coroutine>
*/
private array $readEvents = [];
/**
* All listeners for write event.
*
* @var array<int, Coroutine>
*/
private array $writeEvents = [];
/**
* All listeners for signal.
*
* @var array<int, Coroutine>
*/
private array $signalListener = [];
/**
* @var ?callable
*/
private $errorHandler = null;
/**
* Get timer count.
*
* @return integer
*/
public function getTimerCount(): int
{
return count($this->eventTimer);
}
/**
* {@inheritdoc}
*/
public function delay(float $delay, callable $func, array $args = []): int
{
$t = (int)($delay * 1000);
$t = max($t, 1);
$coroutine = Coroutine::run(function () use ($t, $func, $args): void {
msleep($t);
unset($this->eventTimer[Coroutine::getCurrent()->getId()]);
$this->safeCall($func, $args);
});
$timerId = $coroutine->getId();
$this->eventTimer[$timerId] = $timerId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function repeat(float $interval, callable $func, array $args = []): int
{
$t = (int)($interval * 1000);
$t = max($t, 1);
$coroutine = Coroutine::run(function () use ($t, $func, $args): void {
// @phpstan-ignore-next-line While loop condition is always true.
while (true) {
msleep($t);
$this->safeCall($func, $args);
}
});
$timerId = $coroutine->getId();
$this->eventTimer[$timerId] = $timerId;
return $timerId;
}
/**
* {@inheritdoc}
*/
public function offDelay(int $timerId): bool
{
if (isset($this->eventTimer[$timerId])) {
try {
(Coroutine::getAll()[$timerId])->kill();
return true;
} finally {
unset($this->eventTimer[$timerId]);
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function offRepeat(int $timerId): bool
{
return $this->offDelay($timerId);
}
/**
* {@inheritdoc}
*/
public function deleteAllTimer(): void
{
foreach ($this->eventTimer as $timerId) {
$this->offDelay($timerId);
}
}
/**
* {@inheritdoc}
*/
public function onReadable($stream, callable $func): void
{
$fd = (int)$stream;
if (isset($this->readEvents[$fd])) {
$this->offReadable($stream);
}
Coroutine::run(function () use ($stream, $func, $fd): void {
try {
$this->readEvents[$fd] = Coroutine::getCurrent();
while (true) {
if (!is_resource($stream)) {
$this->offReadable($stream);
break;
}
$rEvent = stream_poll_one($stream, STREAM_POLLIN | STREAM_POLLHUP);
if (!isset($this->readEvents[$fd]) || $this->readEvents[$fd] !== Coroutine::getCurrent()) {
break;
}
if ($rEvent !== STREAM_POLLNONE) {
$this->safeCall($func, [$stream]);
}
if ($rEvent !== STREAM_POLLIN) {
$this->offReadable($stream);
break;
}
}
} catch (\RuntimeException) {
$this->offReadable($stream);
}
});
}
/**
* {@inheritdoc}
*/
public function offReadable($stream): bool
{
// 在当前协程执行 $coroutine->kill() 会导致不可预知问题,所以没有使用$coroutine->kill()
$fd = (int)$stream;
if (isset($this->readEvents[$fd])) {
unset($this->readEvents[$fd]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onWritable($stream, callable $func): void
{
$fd = (int)$stream;
if (isset($this->writeEvents[$fd])) {
$this->offWritable($stream);
}
Coroutine::run(function () use ($stream, $func, $fd): void {
try {
$this->writeEvents[$fd] = Coroutine::getCurrent();
while (true) {
$rEvent = stream_poll_one($stream, STREAM_POLLOUT | STREAM_POLLHUP);
if (!isset($this->writeEvents[$fd]) || $this->writeEvents[$fd] !== Coroutine::getCurrent()) {
break;
}
if ($rEvent !== STREAM_POLLNONE) {
$this->safeCall($func, [$stream]);
}
if ($rEvent !== STREAM_POLLOUT) {
$this->offWritable($stream);
break;
}
}
} catch (\RuntimeException) {
$this->offWritable($stream);
}
});
}
/**
* {@inheritdoc}
*/
public function offWritable($stream): bool
{
$fd = (int)$stream;
if (isset($this->writeEvents[$fd])) {
unset($this->writeEvents[$fd]);
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onSignal(int $signal, callable $func): void
{
Coroutine::run(function () use ($signal, $func): void {
$this->signalListener[$signal] = Coroutine::getCurrent();
while (1) {
try {
Signal::wait($signal);
if (!isset($this->signalListener[$signal]) ||
$this->signalListener[$signal] !== Coroutine::getCurrent()) {
break;
}
$this->safeCall($func, [$signal]);
} catch (SignalException) {
// do nothing
}
}
});
}
/**
* {@inheritdoc}
*/
public function offSignal(int $signal): bool
{
if (!isset($this->signalListener[$signal])) {
return false;
}
unset($this->signalListener[$signal]);
return true;
}
/**
* {@inheritdoc}
*/
public function run(): void
{
waitAll();
}
/**
* Destroy loop.
*
* @return void
*/
public function stop(): void
{
Coroutine::killAll();
}
/**
* {@inheritdoc}
*/
public function setErrorHandler(callable $errorHandler): void
{
$this->errorHandler = $errorHandler;
}
/**
* @param callable $func
* @param array $args
* @return void
*/
private function safeCall(callable $func, array $args = []): void
{
try {
$func(...$args);
} catch (\Throwable $e) {
if ($this->errorHandler === null) {
echo $e;
} else {
($this->errorHandler)($e);
}
}
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols;
use function pack;
use function strlen;
use function substr;
use function unpack;
/**
* Frame Protocol.
*/
class Frame
{
/**
* Check the integrity of the package.
*
* @param string $buffer
* @return int
*/
public static function input(string $buffer): int
{
if (strlen($buffer) < 4) {
return 0;
}
$unpackData = unpack('Ntotal_length', $buffer);
return $unpackData['total_length'];
}
/**
* Decode.
*
* @param string $buffer
* @return string
*/
public static function decode(string $buffer): string
{
return substr($buffer, 4);
}
/**
* Encode.
*
* @param string $data
* @return string
*/
public static function encode(string $data): string
{
$totalLength = 4 + strlen($data);
return pack('N', $totalLength) . $data;
}
}
+306
View File
@@ -0,0 +1,306 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Request;
use Workerman\Protocols\Http\Response;
use function clearstatcache;
use function count;
use function explode;
use function filesize;
use function fopen;
use function fread;
use function fseek;
use function ftell;
use function in_array;
use function ini_get;
use function is_array;
use function is_object;
use function preg_match;
use function str_starts_with;
use function strlen;
use function strpos;
use function strstr;
use function substr;
use function sys_get_temp_dir;
/**
* Class Http.
* @package Workerman\Protocols
*/
class Http
{
/**
* Request class name.
*
* @var string
*/
protected static string $requestClass = Request::class;
/**
* Upload tmp dir.
*
* @var string
*/
protected static string $uploadTmpDir = '';
/**
* Get or set the request class name.
*
* @param class-string|null $className
* @return string
*/
public static function requestClass(?string $className = null): string
{
if ($className !== null) {
static::$requestClass = $className;
}
return static::$requestClass;
}
/**
* Check the integrity of the package.
*
* @param string $buffer
* @param TcpConnection $connection
* @return int
*/
public static function input(string $buffer, TcpConnection $connection): int
{
$crlfPos = strpos($buffer, "\r\n\r\n");
if (false === $crlfPos) {
// Judge whether the package length exceeds the limit.
if (strlen($buffer) >= 16384) {
$connection->close("HTTP/1.1 413 Payload Too Large\r\n\r\n", true);
}
return 0;
}
$length = $crlfPos + 4;
$header = substr($buffer, 0, $crlfPos);
if (
!str_starts_with($header, 'GET ') &&
!str_starts_with($header, 'POST ') &&
!str_starts_with($header, 'OPTIONS ') &&
!str_starts_with($header, 'HEAD ') &&
!str_starts_with($header, 'DELETE ') &&
!str_starts_with($header, 'PUT ') &&
!str_starts_with($header, 'PATCH ')
) {
$connection->close("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n", true);
return 0;
}
if (preg_match('/\b(?:Transfer-Encoding\b.*)|(?:Content-Length:\s*(\d+)(?!.*\bTransfer-Encoding\b))/is', $header, $matches)) {
if (!isset($matches[1])) {
$connection->close("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n", true);
return 0;
}
$length += (int)$matches[1];
}
if ($length > $connection->maxPackageSize) {
$connection->close("HTTP/1.1 413 Payload Too Large\r\n\r\n", true);
return 0;
}
return $length;
}
/**
* Http decode.
*
* @param string $buffer
* @param TcpConnection $connection
* @return Request
*/
public static function decode(string $buffer, TcpConnection $connection): Request
{
static $requests = [];
if (isset($requests[$buffer])) {
$request = $requests[$buffer];
$request->connection = $connection;
$connection->request = $request;
$request->destroy();
return $request;
}
$request = new static::$requestClass($buffer);
if (!isset($buffer[TcpConnection::MAX_CACHE_STRING_LENGTH])) {
$requests[$buffer] = $request;
if (count($requests) > TcpConnection::MAX_CACHE_SIZE) {
unset($requests[key($requests)]);
}
$request = clone $request;
}
$request->connection = $connection;
$connection->request = $request;
return $request;
}
/**
* Http encode.
*
* @param string|Response $response
* @param TcpConnection $connection
* @return string
*/
public static function encode(mixed $response, TcpConnection $connection): string
{
if (isset($connection->request)) {
$request = $connection->request;
$request->connection = $connection->request = null;
}
if (!is_object($response)) {
$extHeader = '';
$contentType = 'text/html;charset=utf-8';
foreach ($connection->headers as $name => $value) {
if ($name === 'Content-Type') {
$contentType = $value;
continue;
}
if (is_array($value)) {
foreach ($value as $item) {
$extHeader .= "$name: $item\r\n";
}
} else {
$extHeader .= "$name: $value\r\n";
}
}
$connection->headers = [];
$response = (string)$response;
$bodyLen = strlen($response);
return "HTTP/1.1 200 OK\r\nServer: workerman\r\n{$extHeader}Connection: keep-alive\r\nContent-Type: $contentType\r\nContent-Length: $bodyLen\r\n\r\n$response";
}
if ($connection->headers) {
$response->withHeaders($connection->headers);
$connection->headers = [];
}
if (isset($response->file)) {
$file = $response->file['file'];
$offset = $response->file['offset'];
$length = $response->file['length'];
clearstatcache();
$fileSize = (int)filesize($file);
$bodyLen = $length > 0 ? $length : $fileSize - $offset;
$response->withHeaders([
'Content-Length' => $bodyLen,
'Accept-Ranges' => 'bytes',
]);
if ($offset || $length) {
$offsetEnd = $offset + $bodyLen - 1;
$response->header('Content-Range', "bytes $offset-$offsetEnd/$fileSize");
}
if ($bodyLen < 2 * 1024 * 1024) {
$connection->send($response . file_get_contents($file, false, null, $offset, $bodyLen), true);
return '';
}
$handler = fopen($file, 'r');
if (false === $handler) {
$connection->close(new Response(403, [], '403 Forbidden'));
return '';
}
$connection->send((string)$response, true);
static::sendStream($connection, $handler, $offset, $length);
return '';
}
return (string)$response;
}
/**
* Send remainder of a stream to client.
*
* @param TcpConnection $connection
* @param resource $handler
* @param int $offset
* @param int $length
*/
protected static function sendStream(TcpConnection $connection, $handler, int $offset = 0, int $length = 0): void
{
$connection->context->bufferFull = false;
$connection->context->streamSending = true;
if ($offset !== 0) {
fseek($handler, $offset);
}
$offsetEnd = $offset + $length;
// Read file content from disk piece by piece and send to client.
$doWrite = function () use ($connection, $handler, $length, $offsetEnd) {
// Send buffer not full.
while ($connection->context->bufferFull === false) {
// Read from disk.
$size = 1024 * 1024;
if ($length !== 0) {
$tell = ftell($handler);
$remainSize = $offsetEnd - $tell;
if ($remainSize <= 0) {
fclose($handler);
$connection->onBufferDrain = null;
return;
}
$size = min($remainSize, $size);
}
$buffer = fread($handler, $size);
// Read eof.
if ($buffer === '' || $buffer === false) {
fclose($handler);
$connection->onBufferDrain = null;
$connection->context->streamSending = false;
return;
}
$connection->send($buffer, true);
}
};
// Send buffer full.
$connection->onBufferFull = function ($connection) {
$connection->context->bufferFull = true;
};
// Send buffer drain.
$connection->onBufferDrain = function ($connection) use ($doWrite) {
$connection->context->bufferFull = false;
$doWrite();
};
$doWrite();
}
/**
* Set or get uploadTmpDir.
*
* @param string|null $dir
* @return string
*/
public static function uploadTmpDir(string|null $dir = null): string
{
if (null !== $dir) {
static::$uploadTmpDir = $dir;
}
if (static::$uploadTmpDir === '') {
if ($uploadTmpDir = ini_get('upload_tmp_dir')) {
static::$uploadTmpDir = $uploadTmpDir;
} else if ($uploadTmpDir = sys_get_temp_dir()) {
static::$uploadTmpDir = $uploadTmpDir;
}
}
return static::$uploadTmpDir;
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http;
use Stringable;
use function dechex;
use function strlen;
/**
* Class Chunk
* @package Workerman\Protocols\Http
*/
class Chunk implements Stringable
{
public function __construct(protected string $buffer) {}
public function __toString(): string
{
return dechex(strlen($this->buffer)) . "\r\n$this->buffer\r\n";
}
}
@@ -0,0 +1,768 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http;
use Exception;
use RuntimeException;
use Stringable;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http;
use function array_walk_recursive;
use function bin2hex;
use function clearstatcache;
use function count;
use function explode;
use function file_put_contents;
use function is_file;
use function json_decode;
use function ltrim;
use function microtime;
use function pack;
use function parse_str;
use function parse_url;
use function preg_match;
use function preg_replace;
use function strlen;
use function strpos;
use function strstr;
use function strtolower;
use function substr;
use function tempnam;
use function trim;
use function unlink;
use function urlencode;
/**
* Class Request
* @package Workerman\Protocols\Http
*/
class Request implements Stringable
{
/**
* Connection.
*
* @var ?TcpConnection
*/
public ?TcpConnection $connection = null;
/**
* @var int
*/
public static int $maxFileUploads = 1024;
/**
* Maximum string length for cache
*
* @var int
*/
public const MAX_CACHE_STRING_LENGTH = 4096;
/**
* Maximum cache size.
*
* @var int
*/
public const MAX_CACHE_SIZE = 256;
/**
* Properties.
*
* @var array
*/
public array $properties = [];
/**
* Request data.
*
* @var array
*/
protected array $data = [];
/**
* Is safe.
*
* @var bool
*/
protected bool $isSafe = true;
/**
* Context.
*
* @var array
*/
public array $context = [];
/**
* Request constructor.
*
*/
public function __construct(protected string $buffer) {}
/**
* Get query.
*
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function get(?string $name = null, mixed $default = null): mixed
{
if (!isset($this->data['get'])) {
$this->parseGet();
}
if (null === $name) {
return $this->data['get'];
}
return $this->data['get'][$name] ?? $default;
}
/**
* Get post.
*
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function post(?string $name = null, mixed $default = null): mixed
{
if (!isset($this->data['post'])) {
$this->parsePost();
}
if (null === $name) {
return $this->data['post'];
}
return $this->data['post'][$name] ?? $default;
}
/**
* Get header item by name.
*
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function header(?string $name = null, mixed $default = null): mixed
{
if (!isset($this->data['headers'])) {
$this->parseHeaders();
}
if (null === $name) {
return $this->data['headers'];
}
$name = strtolower($name);
return $this->data['headers'][$name] ?? $default;
}
/**
* Get cookie item by name.
*
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function cookie(?string $name = null, mixed $default = null): mixed
{
if (!isset($this->data['cookie'])) {
$cookies = explode(';', $this->header('cookie', ''));
$mapped = array();
foreach ($cookies as $cookie) {
$cookie = explode('=', $cookie, 2);
if (count($cookie) !== 2) {
continue;
}
$mapped[trim($cookie[0])] = $cookie[1];
}
$this->data['cookie'] = $mapped;
}
if ($name === null) {
return $this->data['cookie'];
}
return $this->data['cookie'][$name] ?? $default;
}
/**
* Get upload files.
*
* @param string|null $name
* @return array|null
*/
public function file(?string $name = null): mixed
{
clearstatcache();
if (!empty($this->data['files'])) {
array_walk_recursive($this->data['files'], function ($value, $key) {
if ($key === 'tmp_name' && !is_file($value)) {
$this->data['files'] = [];
}
});
}
if (empty($this->data['files'])) {
$this->parsePost();
}
if (null === $name) {
return $this->data['files'];
}
return $this->data['files'][$name] ?? null;
}
/**
* Get method.
*
* @return string
*/
public function method(): string
{
if (!isset($this->data['method'])) {
$this->parseHeadFirstLine();
}
return $this->data['method'];
}
/**
* Get http protocol version.
*
* @return string
*/
public function protocolVersion(): string
{
if (!isset($this->data['protocolVersion'])) {
$this->parseProtocolVersion();
}
return $this->data['protocolVersion'];
}
/**
* Get host.
*
* @param bool $withoutPort
* @return string|null
*/
public function host(bool $withoutPort = false): ?string
{
$host = $this->header('host');
if ($host && $withoutPort) {
return preg_replace('/:\d{1,5}$/', '', $host);
}
return $host;
}
/**
* Get uri.
*
* @return string
*/
public function uri(): string
{
if (!isset($this->data['uri'])) {
$this->parseHeadFirstLine();
}
return $this->data['uri'];
}
/**
* Get path.
*
* @return string
*/
public function path(): string
{
return $this->data['path'] ??= (string)parse_url($this->uri(), PHP_URL_PATH);
}
/**
* Get query string.
*
* @return string
*/
public function queryString(): string
{
return $this->data['query_string'] ??= (string)parse_url($this->uri(), PHP_URL_QUERY);
}
/**
* Get session.
*
* @return Session
* @throws Exception
*/
public function session(): Session
{
return $this->context['session'] ??= new Session($this->sessionId());
}
/**
* Get/Set session id.
*
* @param string|null $sessionId
* @return string
* @throws Exception
*/
public function sessionId(?string $sessionId = null): string
{
if ($sessionId) {
unset($this->context['sid']);
}
if (!isset($this->context['sid'])) {
$sessionName = Session::$name;
$sid = $sessionId ? '' : $this->cookie($sessionName);
$sid = $this->isValidSessionId($sid) ? $sid : '';
if ($sid === '') {
if (!$this->connection) {
throw new RuntimeException('Request->session() fail, header already send');
}
$sid = $sessionId ?: static::createSessionId();
$cookieParams = Session::getCookieParams();
$this->setSidCookie($sessionName, $sid, $cookieParams);
}
$this->context['sid'] = $sid;
}
return $this->context['sid'];
}
/**
* Check if session id is valid.
*
* @param mixed $sessionId
* @return bool
*/
public function isValidSessionId(mixed $sessionId): bool
{
return is_string($sessionId) && preg_match('/^[a-zA-Z0-9"]+$/', $sessionId);
}
/**
* Session regenerate id.
*
* @param bool $deleteOldSession
* @return string
* @throws Exception
*/
public function sessionRegenerateId(bool $deleteOldSession = false): string
{
$session = $this->session();
$sessionData = $session->all();
if ($deleteOldSession) {
$session->flush();
}
$newSid = static::createSessionId();
$session = new Session($newSid);
$session->put($sessionData);
$cookieParams = Session::getCookieParams();
$sessionName = Session::$name;
$this->setSidCookie($sessionName, $newSid, $cookieParams);
return $newSid;
}
/**
* Get http raw head.
*
* @return string
*/
public function rawHead(): string
{
return $this->data['head'] ??= strstr($this->buffer, "\r\n\r\n", true);
}
/**
* Get http raw body.
*
* @return string
*/
public function rawBody(): string
{
return substr($this->buffer, strpos($this->buffer, "\r\n\r\n") + 4);
}
/**
* Get raw buffer.
*
* @return string
*/
public function rawBuffer(): string
{
return $this->buffer;
}
/**
* Parse first line of http header buffer.
*
* @return void
*/
protected function parseHeadFirstLine(): void
{
$firstLine = strstr($this->buffer, "\r\n", true);
$tmp = explode(' ', $firstLine, 3);
$this->data['method'] = $tmp[0];
$this->data['uri'] = $tmp[1] ?? '/';
}
/**
* Parse protocol version.
*
* @return void
*/
protected function parseProtocolVersion(): void
{
$firstLine = strstr($this->buffer, "\r\n", true);
$protocolVersion = substr(strstr($firstLine, 'HTTP/'), 5);
$this->data['protocolVersion'] = $protocolVersion ?: '1.0';
}
/**
* Parse headers.
*
* @return void
*/
protected function parseHeaders(): void
{
static $cache = [];
$this->data['headers'] = [];
$rawHead = $this->rawHead();
$endLinePosition = strpos($rawHead, "\r\n");
if ($endLinePosition === false) {
return;
}
$headBuffer = substr($rawHead, $endLinePosition + 2);
$cacheable = !isset($headBuffer[static::MAX_CACHE_STRING_LENGTH]);
if ($cacheable && isset($cache[$headBuffer])) {
$this->data['headers'] = $cache[$headBuffer];
return;
}
$headData = explode("\r\n", $headBuffer);
foreach ($headData as $content) {
if (str_contains($content, ':')) {
[$key, $value] = explode(':', $content, 2);
$key = strtolower($key);
$value = ltrim($value);
} else {
$key = strtolower($content);
$value = '';
}
if (isset($this->data['headers'][$key])) {
$this->data['headers'][$key] = "{$this->data['headers'][$key]},$value";
} else {
$this->data['headers'][$key] = $value;
}
}
if ($cacheable) {
$cache[$headBuffer] = $this->data['headers'];
if (count($cache) > static::MAX_CACHE_SIZE) {
unset($cache[key($cache)]);
}
}
}
/**
* Parse head.
*
* @return void
*/
protected function parseGet(): void
{
static $cache = [];
$queryString = $this->queryString();
$this->data['get'] = [];
if ($queryString === '') {
return;
}
$cacheable = !isset($queryString[static::MAX_CACHE_STRING_LENGTH]);
if ($cacheable && isset($cache[$queryString])) {
$this->data['get'] = $cache[$queryString];
return;
}
parse_str($queryString, $this->data['get']);
if ($cacheable) {
$cache[$queryString] = $this->data['get'];
if (count($cache) > static::MAX_CACHE_SIZE) {
unset($cache[key($cache)]);
}
}
}
/**
* Parse post.
*
* @return void
*/
protected function parsePost(): void
{
static $cache = [];
$this->data['post'] = $this->data['files'] = [];
$contentType = $this->header('content-type', '');
if (preg_match('/boundary="?(\S+)"?/', $contentType, $match)) {
$httpPostBoundary = '--' . $match[1];
$this->parseUploadFiles($httpPostBoundary);
return;
}
$bodyBuffer = $this->rawBody();
if ($bodyBuffer === '') {
return;
}
$cacheable = !isset($bodyBuffer[static::MAX_CACHE_STRING_LENGTH]);
if ($cacheable && isset($cache[$bodyBuffer])) {
$this->data['post'] = $cache[$bodyBuffer];
return;
}
if (preg_match('/\bjson\b/i', $contentType)) {
$this->data['post'] = (array)json_decode($bodyBuffer, true);
} else {
parse_str($bodyBuffer, $this->data['post']);
}
if ($cacheable) {
$cache[$bodyBuffer] = $this->data['post'];
if (count($cache) > static::MAX_CACHE_SIZE) {
unset($cache[key($cache)]);
}
}
}
/**
* Parse upload files.
*
* @param string $httpPostBoundary
* @return void
*/
protected function parseUploadFiles(string $httpPostBoundary): void
{
$httpPostBoundary = trim($httpPostBoundary, '"');
$buffer = $this->buffer;
$postEncodeString = '';
$filesEncodeString = '';
$files = [];
$bodyPosition = strpos($buffer, "\r\n\r\n") + 4;
$offset = $bodyPosition + strlen($httpPostBoundary) + 2;
$maxCount = static::$maxFileUploads;
while ($maxCount-- > 0 && $offset) {
$offset = $this->parseUploadFile($httpPostBoundary, $offset, $postEncodeString, $filesEncodeString, $files);
}
if ($postEncodeString) {
parse_str($postEncodeString, $this->data['post']);
}
if ($filesEncodeString) {
parse_str($filesEncodeString, $this->data['files']);
array_walk_recursive($this->data['files'], function (&$value) use ($files) {
$value = $files[$value];
});
}
}
/**
* Parse upload file.
*
* @param string $boundary
* @param int $sectionStartOffset
* @param string $postEncodeString
* @param string $filesEncodeStr
* @param array $files
* @return int
*/
protected function parseUploadFile(string $boundary, int $sectionStartOffset, string &$postEncodeString, string &$filesEncodeStr, array &$files): int
{
$file = [];
$boundary = "\r\n$boundary";
if (strlen($this->buffer) < $sectionStartOffset) {
return 0;
}
$sectionEndOffset = strpos($this->buffer, $boundary, $sectionStartOffset);
if (!$sectionEndOffset) {
return 0;
}
$contentLinesEndOffset = strpos($this->buffer, "\r\n\r\n", $sectionStartOffset);
if (!$contentLinesEndOffset || $contentLinesEndOffset + 4 > $sectionEndOffset) {
return 0;
}
$contentLinesStr = substr($this->buffer, $sectionStartOffset, $contentLinesEndOffset - $sectionStartOffset);
$contentLines = explode("\r\n", trim($contentLinesStr . "\r\n"));
$boundaryValue = substr($this->buffer, $contentLinesEndOffset + 4, $sectionEndOffset - $contentLinesEndOffset - 4);
$uploadKey = false;
foreach ($contentLines as $contentLine) {
if (!strpos($contentLine, ': ')) {
return 0;
}
[$key, $value] = explode(': ', $contentLine);
switch (strtolower($key)) {
case "content-disposition":
// Is file data.
if (preg_match('/name="(.*?)"; filename="(.*?)"/i', $value, $match)) {
$error = 0;
$tmpFile = '';
$fileName = $match[1];
$size = strlen($boundaryValue);
$tmpUploadDir = HTTP::uploadTmpDir();
if (!$tmpUploadDir) {
$error = UPLOAD_ERR_NO_TMP_DIR;
} else if ($boundaryValue === '' && $fileName === '') {
$error = UPLOAD_ERR_NO_FILE;
} else {
$tmpFile = tempnam($tmpUploadDir, 'workerman.upload.');
if ($tmpFile === false || false === file_put_contents($tmpFile, $boundaryValue)) {
$error = UPLOAD_ERR_CANT_WRITE;
}
}
$uploadKey = $fileName;
// Parse upload files.
$file = [...$file, 'name' => $match[2], 'tmp_name' => $tmpFile, 'size' => $size, 'error' => $error, 'full_path' => $match[2]];
$file['type'] ??= '';
break;
}
// Is post field.
// Parse $POST.
if (preg_match('/name="(.*?)"$/', $value, $match)) {
$k = $match[1];
$postEncodeString .= urlencode($k) . "=" . urlencode($boundaryValue) . '&';
}
return $sectionEndOffset + strlen($boundary) + 2;
case "content-type":
$file['type'] = trim($value);
break;
case "webkitrelativepath":
$file['full_path'] = trim($value);
break;
}
}
if ($uploadKey === false) {
return 0;
}
$filesEncodeStr .= urlencode($uploadKey) . '=' . count($files) . '&';
$files[] = $file;
return $sectionEndOffset + strlen($boundary) + 2;
}
/**
* Create session id.
*
* @return string
* @throws Exception
*/
public static function createSessionId(): string
{
return bin2hex(pack('d', microtime(true)) . random_bytes(8));
}
/**
* @param string $sessionName
* @param string $sid
* @param array $cookieParams
* @return void
*/
protected function setSidCookie(string $sessionName, string $sid, array $cookieParams): void
{
if (!$this->connection) {
throw new RuntimeException('Request->setSidCookie() fail, header already send');
}
$this->connection->headers['Set-Cookie'] = [$sessionName . '=' . $sid
. (empty($cookieParams['domain']) ? '' : '; Domain=' . $cookieParams['domain'])
. (empty($cookieParams['lifetime']) ? '' : '; Max-Age=' . $cookieParams['lifetime'])
. (empty($cookieParams['path']) ? '' : '; Path=' . $cookieParams['path'])
. (empty($cookieParams['samesite']) ? '' : '; SameSite=' . $cookieParams['samesite'])
. (!$cookieParams['secure'] ? '' : '; Secure')
. (!$cookieParams['httponly'] ? '' : '; HttpOnly')];
}
/**
* __toString.
*/
public function __toString(): string
{
return $this->buffer;
}
/**
* Setter.
*
* @param string $name
* @param mixed $value
* @return void
*/
public function __set(string $name, mixed $value): void
{
$this->properties[$name] = $value;
}
/**
* Getter.
*
* @param string $name
* @return mixed
*/
public function __get(string $name): mixed
{
return $this->properties[$name] ?? null;
}
/**
* Isset.
*
* @param string $name
* @return bool
*/
public function __isset(string $name): bool
{
return isset($this->properties[$name]);
}
/**
* Unset.
*
* @param string $name
* @return void
*/
public function __unset(string $name): void
{
unset($this->properties[$name]);
}
/**
* __wakeup.
*
* @return void
*/
public function __wakeup(): void
{
$this->isSafe = false;
}
/**
* Destroy.
*
* @return void
*/
public function destroy(): void
{
if ($this->context) {
$this->context = [];
}
if ($this->properties) {
$this->properties = [];
}
if (isset($this->data['files']) && $this->isSafe) {
clearstatcache();
array_walk_recursive($this->data['files'], function ($value, $key) {
if ($key === 'tmp_name' && is_file($value)) {
unlink($value);
}
});
}
}
}
@@ -0,0 +1,475 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http;
use Stringable;
use function array_merge_recursive;
use function explode;
use function file;
use function filemtime;
use function gmdate;
use function is_array;
use function is_file;
use function pathinfo;
use function preg_match;
use function rawurlencode;
use function strlen;
use function substr;
use const FILE_IGNORE_NEW_LINES;
use const FILE_SKIP_EMPTY_LINES;
/**
* Class Response
* @package Workerman\Protocols\Http
*/
class Response implements Stringable
{
/**
* Http reason.
*
* @var ?string
*/
protected ?string $reason = null;
/**
* Http version.
*
* @var string
*/
protected string $version = '1.1';
/**
* Send file info
*
* @var ?array
*/
public ?array $file = null;
/**
* Mine type map.
* @var array
*/
protected static array $mimeTypeMap = [];
/**
* Phrases.
*
* @var array<int, string>
*
* @link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
*/
public const PHRASES = [
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing', // WebDAV; RFC 2518
103 => 'Early Hints', // RFC 8297
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information', // since HTTP/1.1
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content', // RFC 7233
207 => 'Multi-Status', // WebDAV; RFC 4918
208 => 'Already Reported', // WebDAV; RFC 5842
226 => 'IM Used', // RFC 3229
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found', // Previously "Moved temporarily"
303 => 'See Other', // since HTTP/1.1
304 => 'Not Modified', // RFC 7232
305 => 'Use Proxy', // since HTTP/1.1
306 => 'Switch Proxy',
307 => 'Temporary Redirect', // since HTTP/1.1
308 => 'Permanent Redirect', // RFC 7538
400 => 'Bad Request',
401 => 'Unauthorized', // RFC 7235
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required', // RFC 7235
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed', // RFC 7232
413 => 'Payload Too Large', // RFC 7231
414 => 'URI Too Long', // RFC 7231
415 => 'Unsupported Media Type', // RFC 7231
416 => 'Range Not Satisfiable', // RFC 7233
417 => 'Expectation Failed',
418 => 'I\'m a teapot', // RFC 2324, RFC 7168
421 => 'Misdirected Request', // RFC 7540
422 => 'Unprocessable Entity', // WebDAV; RFC 4918
423 => 'Locked', // WebDAV; RFC 4918
424 => 'Failed Dependency', // WebDAV; RFC 4918
425 => 'Too Early', // RFC 8470
426 => 'Upgrade Required',
428 => 'Precondition Required', // RFC 6585
429 => 'Too Many Requests', // RFC 6585
431 => 'Request Header Fields Too Large', // RFC 6585
451 => 'Unavailable For Legal Reasons', // RFC 7725
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates', // RFC 2295
507 => 'Insufficient Storage', // WebDAV; RFC 4918
508 => 'Loop Detected', // WebDAV; RFC 5842
510 => 'Not Extended', // RFC 2774
511 => 'Network Authentication Required', // RFC 6585
];
/**
* Init.
*
* @return void
*/
public static function init(): void
{
static::initMimeTypeMap();
}
/**
* Response constructor.
*
* @param int $status
* @param array $headers
* @param string $body
*/
public function __construct(
protected int $status = 200,
protected array $headers = [],
protected string $body = ''
) {}
/**
* Set header.
*
* @param string $name
* @param string $value
* @return $this
*/
public function header(string $name, string $value): static
{
$this->headers[$name] = $value;
return $this;
}
/**
* Set header.
*
* @param string $name
* @param string $value
* @return $this
*/
public function withHeader(string $name, string $value): static
{
return $this->header($name, $value);
}
/**
* Set headers.
*
* @param array $headers
* @return $this
*/
public function withHeaders(array $headers): static
{
$this->headers = array_merge_recursive($this->headers, $headers);
return $this;
}
/**
* Remove header.
*
* @param string $name
* @return $this
*/
public function withoutHeader(string $name): static
{
unset($this->headers[$name]);
return $this;
}
/**
* Get header.
*
* @param string $name
* @return null|array|string
*/
public function getHeader(string $name): array|string|null
{
return $this->headers[$name] ?? null;
}
/**
* Get headers.
*
* @return array
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* Set status.
*
* @param int $code
* @param string|null $reasonPhrase
* @return $this
*/
public function withStatus(int $code, ?string $reasonPhrase = null): static
{
$this->status = $code;
$this->reason = $reasonPhrase;
return $this;
}
/**
* Get status code.
*
* @return int
*/
public function getStatusCode(): int
{
return $this->status;
}
/**
* Get reason phrase.
*
* @return ?string
*/
public function getReasonPhrase(): ?string
{
return $this->reason;
}
/**
* Set protocol version.
*
* @param string $version
* @return $this
*/
public function withProtocolVersion(string $version): static
{
$this->version = $version;
return $this;
}
/**
* Set http body.
*
* @param string $body
* @return $this
*/
public function withBody(string $body): static
{
$this->body = $body;
return $this;
}
/**
* Get http raw body.
*
* @return string
*/
public function rawBody(): string
{
return $this->body;
}
/**
* Send file.
*
* @param string $file
* @param int $offset
* @param int $length
* @return $this
*/
public function withFile(string $file, int $offset = 0, int $length = 0): static
{
if (!is_file($file)) {
return $this->withStatus(404)->withBody('<h3>404 Not Found</h3>');
}
$this->file = ['file' => $file, 'offset' => $offset, 'length' => $length];
return $this;
}
/**
* Set cookie.
*
* @param string $name
* @param string $value
* @param int|null $maxAge
* @param string $path
* @param string $domain
* @param bool $secure
* @param bool $httpOnly
* @param string $sameSite
* @return $this
*/
public function cookie(string $name, string $value = '', ?int $maxAge = null, string $path = '', string $domain = '', bool $secure = false, bool $httpOnly = false, string $sameSite = ''): static
{
$this->headers['Set-Cookie'][] = $name . '=' . rawurlencode($value)
. (empty($domain) ? '' : '; Domain=' . $domain)
. ($maxAge === null ? '' : '; Max-Age=' . $maxAge)
. (empty($path) ? '' : '; Path=' . $path)
. (!$secure ? '' : '; Secure')
. (!$httpOnly ? '' : '; HttpOnly')
. (empty($sameSite) ? '' : '; SameSite=' . $sameSite);
return $this;
}
/**
* Create header for file.
*
* @param array $fileInfo
* @return string
*/
protected function createHeadForFile(array $fileInfo): string
{
$file = $fileInfo['file'];
$reason = $this->reason ?: self::PHRASES[$this->status];
$head = "HTTP/$this->version $this->status $reason\r\n";
$headers = $this->headers;
if (!isset($headers['Server'])) {
$head .= "Server: workerman\r\n";
}
foreach ($headers as $name => $value) {
if (is_array($value)) {
foreach ($value as $item) {
$head .= "$name: $item\r\n";
}
continue;
}
$head .= "$name: $value\r\n";
}
if (!isset($headers['Connection'])) {
$head .= "Connection: keep-alive\r\n";
}
$fileInfo = pathinfo($file);
$extension = $fileInfo['extension'] ?? '';
$baseName = $fileInfo['basename'] ?: 'unknown';
if (!isset($headers['Content-Type'])) {
if (isset(self::$mimeTypeMap[$extension])) {
$head .= "Content-Type: " . self::$mimeTypeMap[$extension] . "\r\n";
} else {
$head .= "Content-Type: application/octet-stream\r\n";
}
}
if (!isset($headers['Content-Disposition']) && !isset(self::$mimeTypeMap[$extension])) {
$head .= "Content-Disposition: attachment; filename=\"$baseName\"\r\n";
}
if (!isset($headers['Last-Modified']) && $mtime = filemtime($file)) {
$head .= 'Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT' . "\r\n";
}
return "$head\r\n";
}
/**
* __toString.
*
* @return string
*/
public function __toString(): string
{
if ($this->file) {
return $this->createHeadForFile($this->file);
}
$reason = $this->reason ?: self::PHRASES[$this->status] ?? '';
$bodyLen = strlen($this->body);
if (empty($this->headers)) {
return "HTTP/$this->version $this->status $reason\r\nServer: workerman\r\nContent-Type: text/html;charset=utf-8\r\nContent-Length: $bodyLen\r\nConnection: keep-alive\r\n\r\n$this->body";
}
$head = "HTTP/$this->version $this->status $reason\r\n";
$headers = $this->headers;
if (!isset($headers['Server'])) {
$head .= "Server: workerman\r\n";
}
foreach ($headers as $name => $value) {
if (is_array($value)) {
foreach ($value as $item) {
$head .= "$name: $item\r\n";
}
continue;
}
$head .= "$name: $value\r\n";
}
if (!isset($headers['Connection'])) {
$head .= "Connection: keep-alive\r\n";
}
if (!isset($headers['Content-Type'])) {
$head .= "Content-Type: text/html;charset=utf-8\r\n";
} else if ($headers['Content-Type'] === 'text/event-stream') {
return $head . $this->body;
}
if (!isset($headers['Transfer-Encoding'])) {
$head .= "Content-Length: $bodyLen\r\n\r\n";
} else {
return $bodyLen ? "$head\r\n" . dechex($bodyLen) . "\r\n{$this->body}\r\n" : "$head\r\n";
}
// The whole http package
return $head . $this->body;
}
/**
* Init mime map.
*
* @return void
*/
public static function initMimeTypeMap(): void
{
$mimeFile = __DIR__ . '/mime.types';
$items = file($mimeFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($items as $content) {
if (preg_match("/\s*(\S+)\s+(\S.+)/", $content, $match)) {
$mimeType = $match[1];
$extensionVar = $match[2];
$extensionArray = explode(' ', substr($extensionVar, 0, -1));
foreach ($extensionArray as $fileExtension) {
static::$mimeTypeMap[$fileExtension] = $mimeType;
}
}
}
}
}
Response::init();
@@ -0,0 +1,56 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http;
use Stringable;
use function str_replace;
/**
* Class ServerSentEvents
* @package Workerman\Protocols\Http
*/
class ServerSentEvents implements Stringable
{
/**
* ServerSentEvents constructor.
* $data for example ['event'=>'ping', 'data' => 'some thing', 'id' => 1000, 'retry' => 5000]
*/
public function __construct(protected array $data) {}
public function __toString(): string
{
$buffer = '';
$data = $this->data;
if (isset($data[''])) {
$buffer = ": {$data['']}\n";
}
if (isset($data['event'])) {
$buffer .= "event: {$data['event']}\n";
}
if (isset($data['id'])) {
$buffer .= "id: {$data['id']}\n";
}
if (isset($data['retry'])) {
$buffer .= "retry: {$data['retry']}\n";
}
if (isset($data['data'])) {
$buffer .= 'data: ' . str_replace("\n", "\ndata: ", $data['data']) . "\n";
}
return "$buffer\n";
}
}
@@ -0,0 +1,454 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http;
use Exception;
use Random\RandomException;
use RuntimeException;
use Workerman\Protocols\Http\Session\FileSessionHandler;
use Workerman\Protocols\Http\Session\SessionHandlerInterface;
use function array_key_exists;
use function ini_get;
use function is_array;
use function is_scalar;
use function preg_match;
use function random_int;
use function serialize;
use function session_get_cookie_params;
use function unserialize;
/**
* Class Session
* @package Workerman\Protocols\Http
*/
class Session
{
/**
* Session andler class which implements SessionHandlerInterface.
*
* @var string
*/
protected static string $handlerClass = FileSessionHandler::class;
/**
* Parameters of __constructor for session handler class.
*
* @var mixed
*/
protected static mixed $handlerConfig = null;
/**
* Session name.
*
* @var string
*/
public static string $name = 'PHPSID';
/**
* Auto update timestamp.
*
* @var bool
*/
public static bool $autoUpdateTimestamp = false;
/**
* Session lifetime.
*
* @var int
*/
public static int $lifetime = 1440;
/**
* Cookie lifetime.
*
* @var int
*/
public static int $cookieLifetime = 1440;
/**
* Session cookie path.
*
* @var string
*/
public static string $cookiePath = '/';
/**
* Session cookie domain.
*
* @var string
*/
public static string $domain = '';
/**
* HTTPS only cookies.
*
* @var bool
*/
public static bool $secure = false;
/**
* HTTP access only.
*
* @var bool
*/
public static bool $httpOnly = true;
/**
* Same-site cookies.
*
* @var string
*/
public static string $sameSite = '';
/**
* Gc probability.
*
* @var int[]
*/
public static array $gcProbability = [1, 20000];
/**
* Session handler instance.
*
* @var ?SessionHandlerInterface
*/
protected static ?SessionHandlerInterface $handler = null;
/**
* Session data.
*
* @var array
*/
protected mixed $data = [];
/**
* Session changed and need to save.
*
* @var bool
*/
protected bool $needSave = false;
/**
* Session id.
*
* @var string
*/
protected string $sessionId;
/**
* Is safe.
*
* @var bool
*/
protected bool $isSafe = true;
/**
* Session constructor.
*
* @param string $sessionId
*/
public function __construct(string $sessionId)
{
if (static::$handler === null) {
static::initHandler();
}
$this->sessionId = $sessionId;
if ($data = static::$handler->read($sessionId)) {
$this->data = unserialize($data);
}
}
/**
* Get session id.
*
* @return string
*/
public function getId(): string
{
return $this->sessionId;
}
/**
* Get session.
*
* @param string $name
* @param mixed $default
* @return mixed
*/
public function get(string $name, mixed $default = null): mixed
{
return $this->data[$name] ?? $default;
}
/**
* Store data in the session.
*
* @param string $name
* @param mixed $value
*/
public function set(string $name, mixed $value): void
{
$this->data[$name] = $value;
$this->needSave = true;
}
/**
* Delete an item from the session.
*
* @param string $name
*/
public function delete(string $name): void
{
unset($this->data[$name]);
$this->needSave = true;
}
/**
* Retrieve and delete an item from the session.
*
* @param string $name
* @param mixed $default
* @return mixed
*/
public function pull(string $name, mixed $default = null): mixed
{
$value = $this->get($name, $default);
$this->delete($name);
return $value;
}
/**
* Store data in the session.
*
* @param array|string $key
* @param mixed $value
*/
public function put(array|string $key, mixed $value = null): void
{
if (!is_array($key)) {
$this->set($key, $value);
return;
}
foreach ($key as $k => $v) {
$this->data[$k] = $v;
}
$this->needSave = true;
}
/**
* Remove a piece of data from the session.
*
* @param array|string $name
*/
public function forget(array|string $name): void
{
if (is_scalar($name)) {
$this->delete($name);
return;
}
foreach ($name as $key) {
unset($this->data[$key]);
}
$this->needSave = true;
}
/**
* Retrieve all the data in the session.
*
* @return array
*/
public function all(): array
{
return $this->data;
}
/**
* Remove all data from the session.
*
* @return void
*/
public function flush(): void
{
$this->needSave = true;
$this->data = [];
}
/**
* Determining If An Item Exists In The Session.
*
* @param string $name
* @return bool
*/
public function has(string $name): bool
{
return isset($this->data[$name]);
}
/**
* To determine if an item is present in the session, even if its value is null.
*
* @param string $name
* @return bool
*/
public function exists(string $name): bool
{
return array_key_exists($name, $this->data);
}
/**
* Save session to store.
*
* @return void
*/
public function save(): void
{
if ($this->needSave) {
if (empty($this->data)) {
static::$handler->destroy($this->sessionId);
} else {
static::$handler->write($this->sessionId, serialize($this->data));
}
} elseif (static::$autoUpdateTimestamp) {
$this->refresh();
}
$this->needSave = false;
}
/**
* Refresh session expire time.
*
* @return bool
*/
public function refresh(): bool
{
return static::$handler->updateTimestamp($this->getId());
}
/**
* Init.
*
* @return void
*/
public static function init(): void
{
if (($gcProbability = (int)ini_get('session.gc_probability')) && ($gcDivisor = (int)ini_get('session.gc_divisor'))) {
static::$gcProbability = [$gcProbability, $gcDivisor];
}
if ($gcMaxLifeTime = ini_get('session.gc_maxlifetime')) {
self::$lifetime = (int)$gcMaxLifeTime;
}
$sessionCookieParams = session_get_cookie_params();
static::$cookieLifetime = $sessionCookieParams['lifetime'];
static::$cookiePath = $sessionCookieParams['path'];
static::$domain = $sessionCookieParams['domain'];
static::$secure = $sessionCookieParams['secure'];
static::$httpOnly = $sessionCookieParams['httponly'];
}
/**
* Set session handler class.
*
* @param mixed $className
* @param mixed $config
* @return string
*/
public static function handlerClass(mixed $className = null, mixed $config = null): string
{
if ($className) {
static::$handlerClass = $className;
}
if ($config) {
static::$handlerConfig = $config;
}
return static::$handlerClass;
}
/**
* Get cookie params.
*
* @return array
*/
public static function getCookieParams(): array
{
return [
'lifetime' => static::$cookieLifetime,
'path' => static::$cookiePath,
'domain' => static::$domain,
'secure' => static::$secure,
'httponly' => static::$httpOnly,
'samesite' => static::$sameSite,
];
}
/**
* Init handler.
*
* @return void
*/
protected static function initHandler(): void
{
if (static::$handlerConfig === null) {
static::$handler = new static::$handlerClass();
} else {
static::$handler = new static::$handlerClass(static::$handlerConfig);
}
}
/**
* GC sessions.
*
* @return void
*/
public function gc(): void
{
static::$handler->gc(static::$lifetime);
}
/**
* __wakeup.
*
* @return void
*/
public function __wakeup()
{
$this->isSafe = false;
}
/**
* __destruct.
*
* @return void
* @throws RandomException
*/
public function __destruct()
{
if (!$this->isSafe) {
return;
}
$this->save();
if (random_int(1, static::$gcProbability[1]) <= static::$gcProbability[0]) {
$this->gc();
}
}
}
// Init session.
Session::init();
@@ -0,0 +1,209 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http\Session;
use Exception;
use Workerman\Protocols\Http\Session;
use function clearstatcache;
use function file_get_contents;
use function file_put_contents;
use function filemtime;
use function glob;
use function is_dir;
use function is_file;
use function mkdir;
use function rename;
use function session_save_path;
use function strlen;
use function sys_get_temp_dir;
use function time;
use function touch;
use function unlink;
/**
* Class FileSessionHandler
* @package Workerman\Protocols\Http\Session
*/
class FileSessionHandler implements SessionHandlerInterface
{
/**
* Session save path.
*
* @var string
*/
protected static string $sessionSavePath;
/**
* Session file prefix.
*
* @var string
*/
protected static string $sessionFilePrefix = 'session_';
/**
* Init.
*/
public static function init()
{
$savePath = @session_save_path();
if (!$savePath || str_starts_with($savePath, 'tcp://')) {
$savePath = sys_get_temp_dir();
}
static::sessionSavePath($savePath);
}
/**
* FileSessionHandler constructor.
* @param array $config
*/
public function __construct(array $config = [])
{
if (isset($config['save_path'])) {
static::sessionSavePath($config['save_path']);
}
}
/**
* {@inheritdoc}
*/
public function open(string $savePath, string $name): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function read(string $sessionId): string|false
{
$sessionFile = static::sessionFile($sessionId);
clearstatcache();
if (is_file($sessionFile)) {
if (time() - filemtime($sessionFile) > Session::$lifetime) {
unlink($sessionFile);
return false;
}
$data = file_get_contents($sessionFile);
return $data ?: false;
}
return false;
}
/**
* {@inheritdoc}
* @throws Exception
*/
public function write(string $sessionId, string $sessionData): bool
{
$tempFile = static::$sessionSavePath . uniqid(bin2hex(random_bytes(8)), true);
if (!file_put_contents($tempFile, $sessionData)) {
return false;
}
return rename($tempFile, static::sessionFile($sessionId));
}
/**
* Update session modify time.
*
* @see https://www.php.net/manual/en/class.sessionupdatetimestamphandlerinterface.php
* @see https://www.php.net/manual/zh/function.touch.php
*
* @param string $sessionId Session id.
* @param string $data Session Data.
*
* @return bool
*/
public function updateTimestamp(string $sessionId, string $data = ""): bool
{
$sessionFile = static::sessionFile($sessionId);
if (!file_exists($sessionFile)) {
return false;
}
// set file modify time to current time
$setModifyTime = touch($sessionFile);
// clear file stat cache
clearstatcache();
return $setModifyTime;
}
/**
* {@inheritdoc}
*/
public function close(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function destroy(string $sessionId): bool
{
$sessionFile = static::sessionFile($sessionId);
if (is_file($sessionFile)) {
unlink($sessionFile);
}
return true;
}
/**
* {@inheritdoc}
*/
public function gc(int $maxLifetime): bool
{
$timeNow = time();
foreach (glob(static::$sessionSavePath . static::$sessionFilePrefix . '*') as $file) {
if (is_file($file) && $timeNow - filemtime($file) > $maxLifetime) {
unlink($file);
}
}
return true;
}
/**
* Get session file path.
*
* @param string $sessionId
* @return string
*/
protected static function sessionFile(string $sessionId): string
{
return static::$sessionSavePath . static::$sessionFilePrefix . $sessionId;
}
/**
* Get or set session file path.
*
* @param string $path
* @return string
*/
public static function sessionSavePath(string $path): string
{
if ($path) {
if ($path[strlen($path) - 1] !== DIRECTORY_SEPARATOR) {
$path .= DIRECTORY_SEPARATOR;
}
static::$sessionSavePath = $path;
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
}
return $path;
}
}
FileSessionHandler::init();
@@ -0,0 +1,55 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http\Session;
use Redis;
use RedisCluster;
use RedisClusterException;
use RedisException;
class RedisClusterSessionHandler extends RedisSessionHandler
{
/**
* @param $config
* @throws RedisClusterException
* @throws RedisException
*/
public function __construct($config)
{
$timeout = $config['timeout'] ?? 2;
$readTimeout = $config['read_timeout'] ?? $timeout;
$persistent = $config['persistent'] ?? false;
$auth = $config['auth'] ?? '';
$args = [null, $config['host'], $timeout, $readTimeout, $persistent];
if ($auth) {
$args[] = $auth;
}
$this->redis = new RedisCluster(...$args);
if (empty($config['prefix'])) {
$config['prefix'] = 'redis_session_';
}
$this->redis->setOption(Redis::OPT_PREFIX, $config['prefix']);
}
/**
* {@inheritdoc}
*/
public function read(string $sessionId): string|false
{
return $this->redis->get($sessionId);
}
}
@@ -0,0 +1,166 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http\Session;
use Redis;
use RedisCluster;
use RedisException;
use RuntimeException;
use Throwable;
use Workerman\Protocols\Http\Session;
use Workerman\Timer;
/**
* Class RedisSessionHandler
* @package Workerman\Protocols\Http\Session
*/
class RedisSessionHandler implements SessionHandlerInterface
{
/**
* @var Redis|RedisCluster
*/
protected Redis|RedisCluster $redis;
/**
* @var array
*/
protected array $config;
/**
* RedisSessionHandler constructor.
* @param array $config = [
* 'host' => '127.0.0.1',
* 'port' => 6379,
* 'timeout' => 2,
* 'auth' => '******',
* 'database' => 2,
* 'prefix' => 'redis_session_',
* 'ping' => 55,
* ]
* @throws RedisException
*/
public function __construct(array $config)
{
if (false === extension_loaded('redis')) {
throw new RuntimeException('Please install redis extension.');
}
$config['timeout'] ??= 2;
$this->config = $config;
$this->connect();
Timer::add($config['ping'] ?? 55, function () {
$this->redis->get('ping');
});
}
/**
* @throws RedisException
*/
public function connect()
{
$config = $this->config;
$this->redis = new Redis();
if (false === $this->redis->connect($config['host'], $config['port'], $config['timeout'])) {
throw new RuntimeException("Redis connect {$config['host']}:{$config['port']} fail.");
}
if (!empty($config['auth'])) {
$this->redis->auth($config['auth']);
}
if (!empty($config['database'])) {
$this->redis->select($config['database']);
}
if (empty($config['prefix'])) {
$config['prefix'] = 'redis_session_';
}
$this->redis->setOption(Redis::OPT_PREFIX, $config['prefix']);
}
/**
* {@inheritdoc}
*/
public function open(string $savePath, string $name): bool
{
return true;
}
/**
* {@inheritdoc}
* @param string $sessionId
* @return string|false
* @throws RedisException
* @throws Throwable
*/
public function read(string $sessionId): string|false
{
try {
return $this->redis->get($sessionId);
} catch (Throwable $e) {
$msg = strtolower($e->getMessage());
if ($msg === 'connection lost' || strpos($msg, 'went away')) {
$this->connect();
return $this->redis->get($sessionId);
}
throw $e;
}
}
/**
* {@inheritdoc}
* @throws RedisException
*/
public function write(string $sessionId, string $sessionData): bool
{
return true === $this->redis->setex($sessionId, Session::$lifetime, $sessionData);
}
/**
* {@inheritdoc}
* @throws RedisException
*/
public function updateTimestamp(string $sessionId, string $data = ""): bool
{
return true === $this->redis->expire($sessionId, Session::$lifetime);
}
/**
* {@inheritdoc}
* @throws RedisException
*/
public function destroy(string $sessionId): bool
{
$this->redis->del($sessionId);
return true;
}
/**
* {@inheritdoc}
*/
public function close(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function gc(int $maxLifetime): bool
{
return true;
}
}
@@ -0,0 +1,117 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols\Http\Session;
interface SessionHandlerInterface
{
/**
* Close the session
* @link http://php.net/manual/en/sessionhandlerinterface.close.php
* @return bool <p>
* The return value (usually TRUE on success, FALSE on failure).
* Note this value is returned internally to PHP for processing.
* </p>
* @since 5.4.0
*/
public function close(): bool;
/**
* Destroy a session
* @link http://php.net/manual/en/sessionhandlerinterface.destroy.php
* @param string $sessionId The session ID being destroyed.
* @return bool <p>
* The return value (usually TRUE on success, FALSE on failure).
* Note this value is returned internally to PHP for processing.
* </p>
* @since 5.4.0
*/
public function destroy(string $sessionId): bool;
/**
* Cleanup old sessions
* @link http://php.net/manual/en/sessionhandlerinterface.gc.php
* @param int $maxLifetime <p>
* Sessions that have not updated for
* the last maxlifetime seconds will be removed.
* </p>
* @return bool <p>
* The return value (usually TRUE on success, FALSE on failure).
* Note this value is returned internally to PHP for processing.
* </p>
* @since 5.4.0
*/
public function gc(int $maxLifetime): bool;
/**
* Initialize session
* @link http://php.net/manual/en/sessionhandlerinterface.open.php
* @param string $savePath The path where to store/retrieve the session.
* @param string $name The session name.
* @return bool <p>
* The return value (usually TRUE on success, FALSE on failure).
* Note this value is returned internally to PHP for processing.
* </p>
* @since 5.4.0
*/
public function open(string $savePath, string $name): bool;
/**
* Read session data
* @link http://php.net/manual/en/sessionhandlerinterface.read.php
* @param string $sessionId The session id to read data for.
* @return string|false <p>
* Returns an encoded string of the read data.
* If nothing was read, it must return false.
* Note this value is returned internally to PHP for processing.
* </p>
* @since 5.4.0
*/
public function read(string $sessionId): string|false;
/**
* Write session data
* @link http://php.net/manual/en/sessionhandlerinterface.write.php
* @param string $sessionId The session id.
* @param string $sessionData <p>
* The encoded session data. This data is the
* result of the PHP internally encoding
* the $SESSION superglobal to a serialized
* string and passing it as this parameter.
* Please note sessions use an alternative serialization method.
* </p>
* @return bool <p>
* The return value (usually TRUE on success, FALSE on failure).
* Note this value is returned internally to PHP for processing.
* </p>
* @since 5.4.0
*/
public function write(string $sessionId, string $sessionData): bool;
/**
* Update session modify time.
*
* @see https://www.php.net/manual/en/class.sessionupdatetimestamphandlerinterface.php
*
* @param string $sessionId
* @param string $data Session Data.
*
* @return bool
*/
public function updateTimestamp(string $sessionId, string $data = ""): bool;
}
@@ -0,0 +1,91 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
application/wasm wasm;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
image/svg+xml svg svgz;
image/webp webp;
application/font-woff woff;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.wap.wmlc wmlc;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx;
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
font/ttf ttf;
}
@@ -0,0 +1,55 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols;
use Workerman\Connection\ConnectionInterface;
/**
* Protocol interface
*/
interface ProtocolInterface
{
/**
* Check the integrity of the package.
* Please return the length of package.
* If length is unknown please return 0 that means waiting for more data.
* If the package has something wrong please return -1 the connection will be closed.
*
* @param string $buffer
* @param ConnectionInterface $connection
* @return int
*/
public static function input(string $buffer, ConnectionInterface $connection): int;
/**
* Decode package and emit onMessage($message) callback, $message is the result that decode returned.
*
* @param string $buffer
* @param ConnectionInterface $connection
* @return mixed
*/
public static function decode(string $buffer, ConnectionInterface $connection): mixed;
/**
* Encode package before sending to client.
*
* @param mixed $data
* @param ConnectionInterface $connection
* @return string
*/
public static function encode(mixed $data, ConnectionInterface $connection): string;
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols;
use Workerman\Connection\ConnectionInterface;
use function rtrim;
use function strlen;
use function strpos;
/**
* Text Protocol.
*/
class Text
{
/**
* Check the integrity of the package.
*
* @param string $buffer
* @param ConnectionInterface $connection
* @return int
*/
public static function input(string $buffer, ConnectionInterface $connection): int
{
// Judge whether the package length exceeds the limit.
if (isset($connection->maxPackageSize) && strlen($buffer) >= $connection->maxPackageSize) {
$connection->close();
return 0;
}
// Find the position of "\n".
$pos = strpos($buffer, "\n");
// No "\n", packet length is unknown, continue to wait for the data so return 0.
if ($pos === false) {
return 0;
}
// Return the current package length.
return $pos + 1;
}
/**
* Encode.
*
* @param string $buffer
* @return string
*/
public static function encode(string $buffer): string
{
// Add "\n"
return $buffer . "\n";
}
/**
* Decode.
*
* @param string $buffer
* @return string
*/
public static function decode(string $buffer): string
{
// Remove "\n"
return rtrim($buffer, "\r\n");
}
}
+524
View File
@@ -0,0 +1,524 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols;
use Throwable;
use Workerman\Connection\ConnectionInterface;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Request;
use Workerman\Worker;
use function base64_encode;
use function chr;
use function deflate_add;
use function deflate_init;
use function floor;
use function inflate_add;
use function inflate_init;
use function is_scalar;
use function ord;
use function pack;
use function preg_match;
use function sha1;
use function str_repeat;
use function stripos;
use function strlen;
use function strpos;
use function substr;
use function unpack;
use const ZLIB_DEFAULT_STRATEGY;
use const ZLIB_ENCODING_RAW;
/**
* WebSocket protocol.
*/
class Websocket
{
/**
* Websocket blob type.
*
* @var string
*/
public const BINARY_TYPE_BLOB = "\x81";
/**
* Websocket blob type.
*
* @var string
*/
const BINARY_TYPE_BLOB_DEFLATE = "\xc1";
/**
* Websocket arraybuffer type.
*
* @var string
*/
public const BINARY_TYPE_ARRAYBUFFER = "\x82";
/**
* Websocket arraybuffer type.
*
* @var string
*/
const BINARY_TYPE_ARRAYBUFFER_DEFLATE = "\xc2";
/**
* Check the integrity of the package.
*
* @param string $buffer
* @param TcpConnection $connection
* @return int
*/
public static function input(string $buffer, TcpConnection $connection): int
{
// Receive length.
$recvLen = strlen($buffer);
// We need more data.
if ($recvLen < 6) {
return 0;
}
// Has not yet completed the handshake.
if (empty($connection->context->websocketHandshake)) {
return static::dealHandshake($buffer, $connection);
}
// Buffer websocket frame data.
if ($connection->context->websocketCurrentFrameLength) {
// We need more frame data.
if ($connection->context->websocketCurrentFrameLength > $recvLen) {
// Return 0, because it is not clear the full packet length, waiting for the frame of fin=1.
return 0;
}
} else {
$firstByte = ord($buffer[0]);
$secondByte = ord($buffer[1]);
$dataLen = $secondByte & 127;
$isFinFrame = $firstByte >> 7;
$masked = $secondByte >> 7;
if (!$masked) {
Worker::safeEcho("frame not masked so close the connection\n");
$connection->close();
return 0;
}
$opcode = $firstByte & 0xf;
switch ($opcode) {
case 0x0:
// Blob type.
case 0x1:
// Arraybuffer type.
case 0x2:
// Ping package.
case 0x9:
// Pong package.
case 0xa:
break;
// Close package.
case 0x8:
// Try to emit onWebSocketClose callback.
$closeCb = $connection->onWebSocketClose ?? $connection->worker->onWebSocketClose ?? false;
if ($closeCb) {
try {
$closeCb($connection);
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
} // Close connection.
else {
$connection->close("\x88\x02\x03\xe8", true);
}
return 0;
// Wrong opcode.
default :
Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . bin2hex($buffer) . "\n");
$connection->close();
return 0;
}
// Calculate packet length.
$headLen = 6;
if ($dataLen === 126) {
$headLen = 8;
if ($headLen > $recvLen) {
return 0;
}
$pack = unpack('nn/ntotal_len', $buffer);
$dataLen = $pack['total_len'];
} else {
if ($dataLen === 127) {
$headLen = 14;
if ($headLen > $recvLen) {
return 0;
}
$arr = unpack('n/N2c', $buffer);
$dataLen = $arr['c1'] * 4294967296 + $arr['c2'];
}
}
$currentFrameLength = $headLen + $dataLen;
$totalPackageSize = strlen($connection->context->websocketDataBuffer) + $currentFrameLength;
if ($totalPackageSize > $connection->maxPackageSize) {
Worker::safeEcho("error package. package_length=$totalPackageSize\n");
$connection->close();
return 0;
}
if ($isFinFrame) {
if ($opcode === 0x9) {
if ($recvLen >= $currentFrameLength) {
$pingData = static::decode(substr($buffer, 0, $currentFrameLength), $connection);
$connection->consumeRecvBuffer($currentFrameLength);
$tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB;
$connection->websocketType = "\x8a";
$pingCb = $connection->onWebSocketPing ?? $connection->worker->onWebSocketPing ?? false;
if ($pingCb) {
try {
$pingCb($connection, $pingData);
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
} else {
$connection->send($pingData);
}
$connection->websocketType = $tmpConnectionType;
if ($recvLen > $currentFrameLength) {
return static::input(substr($buffer, $currentFrameLength), $connection);
}
}
return 0;
}
if ($opcode === 0xa) {
if ($recvLen >= $currentFrameLength) {
$pongData = static::decode(substr($buffer, 0, $currentFrameLength), $connection);
$connection->consumeRecvBuffer($currentFrameLength);
$tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB;
$connection->websocketType = "\x8a";
// Try to emit onWebSocketPong callback.
$pongCb = $connection->onWebSocketPong ?? $connection->worker->onWebSocketPong ?? false;
if ($pongCb) {
try {
$pongCb($connection, $pongData);
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
}
$connection->websocketType = $tmpConnectionType;
if ($recvLen > $currentFrameLength) {
return static::input(substr($buffer, $currentFrameLength), $connection);
}
}
return 0;
}
return $currentFrameLength;
}
$connection->context->websocketCurrentFrameLength = $currentFrameLength;
}
// Received just a frame length data.
if ($connection->context->websocketCurrentFrameLength === $recvLen) {
static::decode($buffer, $connection);
$connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength);
$connection->context->websocketCurrentFrameLength = 0;
return 0;
}
// The length of the received data is greater than the length of a frame.
if ($connection->context->websocketCurrentFrameLength < $recvLen) {
static::decode(substr($buffer, 0, $connection->context->websocketCurrentFrameLength), $connection);
$connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength);
$currentFrameLength = $connection->context->websocketCurrentFrameLength;
$connection->context->websocketCurrentFrameLength = 0;
// Continue to read next frame.
return static::input(substr($buffer, $currentFrameLength), $connection);
}
// The length of the received data is less than the length of a frame.
return 0;
}
/**
* Websocket encode.
*
* @param mixed $buffer
* @param TcpConnection $connection
* @return string
*/
public static function encode(mixed $buffer, TcpConnection $connection): string
{
if (!is_scalar($buffer)) {
$buffer = json_encode($buffer, JSON_UNESCAPED_UNICODE);
}
if (empty($connection->websocketType)) {
$connection->websocketType = static::BINARY_TYPE_BLOB;
}
if (ord($connection->websocketType) & 64) {
$buffer = static::deflate($connection, $buffer);
}
$firstByte = $connection->websocketType;
$len = strlen($buffer);
if ($len <= 125) {
$encodeBuffer = $firstByte . chr($len) . $buffer;
} else {
if ($len <= 65535) {
$encodeBuffer = $firstByte . chr(126) . pack("n", $len) . $buffer;
} else {
$encodeBuffer = $firstByte . chr(127) . pack("xxxxN", $len) . $buffer;
}
}
// Handshake not completed so temporary buffer websocket data waiting for send.
if (empty($connection->context->websocketHandshake)) {
if (empty($connection->context->tmpWebsocketData)) {
$connection->context->tmpWebsocketData = '';
}
// If buffer has already full then discard the current package.
if (strlen($connection->context->tmpWebsocketData) > $connection->maxSendBufferSize) {
if ($connection->onError) {
try {
($connection->onError)($connection, ConnectionInterface::SEND_FAIL, 'send buffer full and drop package');
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
}
return '';
}
$connection->context->tmpWebsocketData .= $encodeBuffer;
// Check buffer is full.
if ($connection->onBufferFull && $connection->maxSendBufferSize <= strlen($connection->context->tmpWebsocketData)) {
try {
($connection->onBufferFull)($connection);
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
}
// Return empty string.
return '';
}
return $encodeBuffer;
}
/**
* Websocket decode.
*
* @param string $buffer
* @param TcpConnection $connection
* @return string
*/
public static function decode(string $buffer, TcpConnection $connection): string
{
$firstByte = ord($buffer[0]);
$secondByte = ord($buffer[1]);
$len = $secondByte & 127;
$isFinFrame = (bool)($firstByte >> 7);
$rsv1 = 64 === ($firstByte & 64);
if ($len === 126) {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
} else {
if ($len === 127) {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
} else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}
}
$dataLength = strlen($data);
$masks = str_repeat($masks, (int)floor($dataLength / 4)) . substr($masks, 0, $dataLength % 4);
$decoded = $data ^ $masks;
if ($connection->context->websocketCurrentFrameLength) {
$connection->context->websocketDataBuffer .= $decoded;
if ($rsv1) {
return static::inflate($connection, $connection->context->websocketDataBuffer, $isFinFrame);
}
return $connection->context->websocketDataBuffer;
}
if ($connection->context->websocketDataBuffer !== '') {
$decoded = $connection->context->websocketDataBuffer . $decoded;
$connection->context->websocketDataBuffer = '';
}
if ($rsv1) {
return static::inflate($connection, $decoded, $isFinFrame);
}
return $decoded;
}
/**
* Inflate.
*
* @param TcpConnection $connection
* @param string $buffer
* @param bool $isFinFrame
* @return false|string
*/
protected static function inflate(TcpConnection $connection, string $buffer, bool $isFinFrame): bool|string
{
if (!isset($connection->context->inflator)) {
$connection->context->inflator = inflate_init(
ZLIB_ENCODING_RAW,
[
'level' => -1,
'memory' => 8,
'window' => 15,
'strategy' => ZLIB_DEFAULT_STRATEGY
]
);
}
if ($isFinFrame) {
$buffer .= "\x00\x00\xff\xff";
}
return inflate_add($connection->context->inflator, $buffer);
}
/**
* Deflate.
*
* @param TcpConnection $connection
* @param string $buffer
* @return false|string
*/
protected static function deflate(TcpConnection $connection, string $buffer): bool|string
{
if (!isset($connection->context->deflator)) {
$connection->context->deflator = deflate_init(
ZLIB_ENCODING_RAW,
[
'level' => -1,
'memory' => 8,
'window' => 15,
'strategy' => ZLIB_DEFAULT_STRATEGY
]
);
}
return substr(deflate_add($connection->context->deflator, $buffer), 0, -4);
}
/**
* Websocket handshake.
*
* @param string $buffer
* @param TcpConnection $connection
* @return int
*/
public static function dealHandshake(string $buffer, TcpConnection $connection): int
{
// HTTP protocol.
if (str_starts_with($buffer, 'GET')) {
// Find \r\n\r\n.
$headerEndPos = strpos($buffer, "\r\n\r\n");
if (!$headerEndPos) {
return 0;
}
$headerLength = $headerEndPos + 4;
// Get Sec-WebSocket-Key.
if (preg_match("/Sec-WebSocket-Key: *(.*?)\r\n/i", $buffer, $match)) {
$SecWebSocketKey = $match[1];
} else {
$connection->close(
"HTTP/1.0 400 Bad Request\r\nServer: workerman\r\n\r\n<div style=\"text-align:center\"><h1>WebSocket</h1><hr>workerman</div>", true);
return 0;
}
// Calculation websocket key.
$newKey = base64_encode(sha1($SecWebSocketKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
// Handshake response data.
$handshakeMessage = "HTTP/1.1 101 Switching Protocol\r\n"
. "Upgrade: websocket\r\n"
. "Sec-WebSocket-Version: 13\r\n"
. "Connection: Upgrade\r\n"
. "Sec-WebSocket-Accept: " . $newKey . "\r\n";
// Websocket data buffer.
$connection->context->websocketDataBuffer = '';
// Current websocket frame length.
$connection->context->websocketCurrentFrameLength = 0;
// Current websocket frame data.
$connection->context->websocketCurrentFrameBuffer = '';
// Consume handshake data.
$connection->consumeRecvBuffer($headerLength);
// Request from buffer
$request = new Request($buffer);
// Try to emit onWebSocketConnect callback.
$onWebsocketConnect = $connection->onWebSocketConnect ?? $connection->worker->onWebSocketConnect ?? false;
if ($onWebsocketConnect) {
try {
$onWebsocketConnect($connection, $request);
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
}
// blob or arraybuffer
if (empty($connection->websocketType)) {
$connection->websocketType = static::BINARY_TYPE_BLOB;
}
$hasServerHeader = false;
if ($connection->headers) {
foreach ($connection->headers as $header) {
if (stripos($header, 'Server:') === 0) {
$hasServerHeader = true;
}
$handshakeMessage .= "$header\r\n";
}
}
if (!$hasServerHeader) {
$handshakeMessage .= "Server: workerman\r\n";
}
$handshakeMessage .= "\r\n";
// Send handshake response.
$connection->send($handshakeMessage, true);
// Mark handshake complete.
$connection->context->websocketHandshake = true;
// Try to emit onWebSocketConnected callback.
$onWebsocketConnected = $connection->onWebSocketConnected ?? $connection->worker->onWebSocketConnected ?? false;
if ($onWebsocketConnected) {
try {
$onWebsocketConnected($connection, $request);
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
}
// There are data waiting to be sent.
if (!empty($connection->context->tmpWebsocketData)) {
$connection->send($connection->context->tmpWebsocketData, true);
$connection->context->tmpWebsocketData = '';
}
if (strlen($buffer) > $headerLength) {
return static::input(substr($buffer, $headerLength), $connection);
}
return 0;
}
// Bad websocket handshake request.
$connection->close(
"HTTP/1.0 400 Bad Request\r\nServer: workerman\r\n\r\n<div style=\"text-align:center\"><h1>400 Bad Request</h1><hr>workerman</div>", true);
return 0;
}
}
+470
View File
@@ -0,0 +1,470 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman\Protocols;
use Throwable;
use Workerman\Connection\AsyncTcpConnection;
use Workerman\Connection\ConnectionInterface;
use Workerman\Protocols\Http\Response;
use Workerman\Timer;
use Workerman\Worker;
use function base64_encode;
use function bin2hex;
use function explode;
use function floor;
use function ord;
use function pack;
use function preg_match;
use function sha1;
use function str_repeat;
use function strlen;
use function strpos;
use function substr;
use function trim;
use function unpack;
/**
* Websocket protocol for client.
*/
class Ws
{
/**
* Websocket blob type.
*
* @var string
*/
public const BINARY_TYPE_BLOB = "\x81";
/**
* Websocket arraybuffer type.
*
* @var string
*/
public const BINARY_TYPE_ARRAYBUFFER = "\x82";
/**
* Check the integrity of the package.
*
* @param string $buffer
* @param AsyncTcpConnection $connection
* @return int
*/
public static function input(string $buffer, AsyncTcpConnection $connection): int
{
if (empty($connection->context->handshakeStep)) {
Worker::safeEcho("recv data before handshake. Buffer:" . bin2hex($buffer) . "\n");
return -1;
}
// Recv handshake response
if ($connection->context->handshakeStep === 1) {
return self::dealHandshake($buffer, $connection);
}
$recvLen = strlen($buffer);
if ($recvLen < 2) {
return 0;
}
// Buffer websocket frame data.
if ($connection->context->websocketCurrentFrameLength) {
// We need more frame data.
if ($connection->context->websocketCurrentFrameLength > $recvLen) {
// Return 0, because it is not clear the full packet length, waiting for the frame of fin=1.
return 0;
}
} else {
$firstByte = ord($buffer[0]);
$secondByte = ord($buffer[1]);
$dataLen = $secondByte & 127;
$isFinFrame = $firstByte >> 7;
$masked = $secondByte >> 7;
if ($masked) {
Worker::safeEcho("frame masked so close the connection\n");
$connection->close();
return 0;
}
$opcode = $firstByte & 0xf;
switch ($opcode) {
case 0x0:
// Blob type.
case 0x1:
// Arraybuffer type.
case 0x2:
// Ping package.
case 0x9:
// Pong package.
case 0xa:
break;
// Close package.
case 0x8:
// Try to emit onWebSocketClose callback.
if (isset($connection->onWebSocketClose)) {
try {
($connection->onWebSocketClose)($connection);
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
} // Close connection.
else {
$connection->close();
}
return 0;
// Wrong opcode.
default :
Worker::safeEcho("error opcode $opcode and close websocket connection. Buffer:" . $buffer . "\n");
$connection->close();
return 0;
}
// Calculate packet length.
if ($dataLen === 126) {
if (strlen($buffer) < 4) {
return 0;
}
$pack = unpack('nn/ntotal_len', $buffer);
$currentFrameLength = $pack['total_len'] + 4;
} else if ($dataLen === 127) {
if (strlen($buffer) < 10) {
return 0;
}
$arr = unpack('n/N2c', $buffer);
$currentFrameLength = $arr['c1'] * 4294967296 + $arr['c2'] + 10;
} else {
$currentFrameLength = $dataLen + 2;
}
$totalPackageSize = strlen($connection->context->websocketDataBuffer) + $currentFrameLength;
if ($totalPackageSize > $connection->maxPackageSize) {
Worker::safeEcho("error package. package_length=$totalPackageSize\n");
$connection->close();
return 0;
}
if ($isFinFrame) {
if ($opcode === 0x9) {
if ($recvLen >= $currentFrameLength) {
$pingData = static::decode(substr($buffer, 0, $currentFrameLength), $connection);
$connection->consumeRecvBuffer($currentFrameLength);
$tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB;
$connection->websocketType = "\x8a";
if (isset($connection->onWebSocketPing)) {
try {
($connection->onWebSocketPing)($connection, $pingData);
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
} else {
$connection->send($pingData);
}
$connection->websocketType = $tmpConnectionType;
if ($recvLen > $currentFrameLength) {
return static::input(substr($buffer, $currentFrameLength), $connection);
}
}
return 0;
}
if ($opcode === 0xa) {
if ($recvLen >= $currentFrameLength) {
$pongData = static::decode(substr($buffer, 0, $currentFrameLength), $connection);
$connection->consumeRecvBuffer($currentFrameLength);
$tmpConnectionType = $connection->websocketType ?? static::BINARY_TYPE_BLOB;
$connection->websocketType = "\x8a";
// Try to emit onWebSocketPong callback.
if (isset($connection->onWebSocketPong)) {
try {
($connection->onWebSocketPong)($connection, $pongData);
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
}
$connection->websocketType = $tmpConnectionType;
if ($recvLen > $currentFrameLength) {
return static::input(substr($buffer, $currentFrameLength), $connection);
}
}
return 0;
}
return $currentFrameLength;
}
$connection->context->websocketCurrentFrameLength = $currentFrameLength;
}
// Received just a frame length data.
if ($connection->context->websocketCurrentFrameLength === $recvLen) {
self::decode($buffer, $connection);
$connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength);
$connection->context->websocketCurrentFrameLength = 0;
return 0;
} // The length of the received data is greater than the length of a frame.
elseif ($connection->context->websocketCurrentFrameLength < $recvLen) {
self::decode(substr($buffer, 0, $connection->context->websocketCurrentFrameLength), $connection);
$connection->consumeRecvBuffer($connection->context->websocketCurrentFrameLength);
$currentFrameLength = $connection->context->websocketCurrentFrameLength;
$connection->context->websocketCurrentFrameLength = 0;
// Continue to read next frame.
return self::input(substr($buffer, $currentFrameLength), $connection);
} // The length of the received data is less than the length of a frame.
else {
return 0;
}
}
/**
* Websocket encode.
*
* @param string $payload
* @param AsyncTcpConnection $connection
* @return string
* @throws Throwable
*/
public static function encode(string $payload, AsyncTcpConnection $connection): string
{
if (empty($connection->websocketType)) {
$connection->websocketType = self::BINARY_TYPE_BLOB;
}
if (empty($connection->context->handshakeStep)) {
static::sendHandshake($connection);
}
$maskKey = "\x00\x00\x00\x00";
$length = strlen($payload);
if (strlen($payload) < 126) {
$head = chr(0x80 | $length);
} elseif ($length < 0xFFFF) {
$head = chr(0x80 | 126) . pack("n", $length);
} else {
$head = chr(0x80 | 127) . pack("N", 0) . pack("N", $length);
}
$frame = $connection->websocketType . $head . $maskKey;
// append payload to frame:
$maskKey = str_repeat($maskKey, (int)floor($length / 4)) . substr($maskKey, 0, $length % 4);
$frame .= $payload ^ $maskKey;
if ($connection->context->handshakeStep === 1) {
// If buffer has already full then discard the current package.
if (strlen($connection->context->tmpWebsocketData) > $connection->maxSendBufferSize) {
if ($connection->onError) {
try {
($connection->onError)($connection, ConnectionInterface::SEND_FAIL, 'send buffer full and drop package');
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
}
return '';
}
$connection->context->tmpWebsocketData .= $frame;
// Check buffer is full.
if ($connection->onBufferFull && $connection->maxSendBufferSize <= strlen($connection->context->tmpWebsocketData)) {
try {
($connection->onBufferFull)($connection);
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
}
return '';
}
return $frame;
}
/**
* Websocket decode.
*
* @param string $bytes
* @param AsyncTcpConnection $connection
* @return string
*/
public static function decode(string $bytes, AsyncTcpConnection $connection): string
{
$dataLength = ord($bytes[1]);
if ($dataLength === 126) {
$decodedData = substr($bytes, 4);
} else if ($dataLength === 127) {
$decodedData = substr($bytes, 10);
} else {
$decodedData = substr($bytes, 2);
}
if ($connection->context->websocketCurrentFrameLength) {
$connection->context->websocketDataBuffer .= $decodedData;
return $connection->context->websocketDataBuffer;
}
if ($connection->context->websocketDataBuffer !== '') {
$decodedData = $connection->context->websocketDataBuffer . $decodedData;
$connection->context->websocketDataBuffer = '';
}
return $decodedData;
}
/**
* Send websocket handshake data.
*
* @param AsyncTcpConnection $connection
* @return void
* @throws Throwable
*/
public static function onConnect(AsyncTcpConnection $connection): void
{
static::sendHandshake($connection);
}
/**
* Clean
*
* @param AsyncTcpConnection $connection
*/
public static function onClose(AsyncTcpConnection $connection): void
{
$connection->context->handshakeStep = null;
$connection->context->websocketCurrentFrameLength = 0;
$connection->context->tmpWebsocketData = '';
$connection->context->websocketDataBuffer = '';
if (!empty($connection->context->websocketPingTimer)) {
Timer::del($connection->context->websocketPingTimer);
$connection->context->websocketPingTimer = null;
}
}
/**
* Send websocket handshake.
*
* @param AsyncTcpConnection $connection
* @return void
* @throws Throwable
*/
public static function sendHandshake(AsyncTcpConnection $connection): void
{
if (!empty($connection->context->handshakeStep)) {
return;
}
// Get Host.
$port = $connection->getRemotePort();
$host = $port === 80 || $port === 443 ? $connection->getRemoteHost() : $connection->getRemoteHost() . ':' . $port;
// Handshake header.
$connection->context->websocketSecKey = base64_encode(random_bytes(16));
$userHeader = $connection->headers ?? null;
$userHeaderStr = '';
if (!empty($userHeader)) {
foreach ($userHeader as $k => $v) {
$userHeaderStr .= "$k: $v\r\n";
}
$userHeaderStr = "\r\n" . trim($userHeaderStr);
}
$header = 'GET ' . $connection->getRemoteURI() . " HTTP/1.1\r\n" .
(!preg_match("/\nHost:/i", $userHeaderStr) ? "Host: $host\r\n" : '') .
"Connection: Upgrade\r\n" .
"Upgrade: websocket\r\n" .
(isset($connection->websocketOrigin) ? "Origin: " . $connection->websocketOrigin . "\r\n" : '') .
(isset($connection->websocketClientProtocol) ? "Sec-WebSocket-Protocol: " . $connection->websocketClientProtocol . "\r\n" : '') .
"Sec-WebSocket-Version: 13\r\n" .
"Sec-WebSocket-Key: " . $connection->context->websocketSecKey . $userHeaderStr . "\r\n\r\n";
$connection->send($header, true);
$connection->context->handshakeStep = 1;
$connection->context->websocketCurrentFrameLength = 0;
$connection->context->websocketDataBuffer = '';
$connection->context->tmpWebsocketData = '';
}
/**
* Websocket handshake.
*
* @param string $buffer
* @param AsyncTcpConnection $connection
* @return bool|int
*/
public static function dealHandshake(string $buffer, AsyncTcpConnection $connection): bool|int
{
$pos = strpos($buffer, "\r\n\r\n");
if ($pos) {
//checking Sec-WebSocket-Accept
if (preg_match("/Sec-WebSocket-Accept: *(.*?)\r\n/i", $buffer, $match)) {
if ($match[1] !== base64_encode(sha1($connection->context->websocketSecKey . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true))) {
Worker::safeEcho("Sec-WebSocket-Accept not match. Header:\n" . substr($buffer, 0, $pos) . "\n");
$connection->close();
return 0;
}
} else {
Worker::safeEcho("Sec-WebSocket-Accept not found. Header:\n" . substr($buffer, 0, $pos) . "\n");
$connection->close();
return 0;
}
// handshake complete
$connection->context->handshakeStep = 2;
$handshakeResponseLength = $pos + 4;
$buffer = substr($buffer, 0, $handshakeResponseLength);
$response = static::parseResponse($buffer);
// Try to emit onWebSocketConnect callback.
if (isset($connection->onWebSocketConnect)) {
try {
($connection->onWebSocketConnect)($connection, $response);
} catch (Throwable $e) {
Worker::stopAll(250, $e);
}
}
// Headbeat.
if (!empty($connection->websocketPingInterval)) {
$connection->context->websocketPingTimer = Timer::add($connection->websocketPingInterval, function () use ($connection) {
if (false === $connection->send(pack('H*', '898000000000'), true)) {
Timer::del($connection->context->websocketPingTimer);
$connection->context->websocketPingTimer = null;
}
});
}
$connection->consumeRecvBuffer($handshakeResponseLength);
if (!empty($connection->context->tmpWebsocketData)) {
$connection->send($connection->context->tmpWebsocketData, true);
$connection->context->tmpWebsocketData = '';
}
if (strlen($buffer) > $handshakeResponseLength) {
return self::input(substr($buffer, $handshakeResponseLength), $connection);
}
}
return 0;
}
/**
* Parse response.
*
* @param string $buffer
* @return Response
*/
protected static function parseResponse(string $buffer): Response
{
[$http_header, ] = explode("\r\n\r\n", $buffer, 2);
$header_data = explode("\r\n", $http_header);
[$protocol, $status, $phrase] = explode(' ', $header_data[0], 3);
$protocolVersion = substr($protocol, 5);
unset($header_data[0]);
$headers = [];
foreach ($header_data as $content) {
// \r\n\r\n
if (empty($content)) {
continue;
}
list($key, $value) = explode(':', $content, 2);
$value = trim($value);
$headers[$key] = $value;
}
return (new Response())->withStatus((int)$status, $phrase)->withHeaders($headers)->withProtocolVersion($protocolVersion);
}
}
+276
View File
@@ -0,0 +1,276 @@
<?php
/**
* This file is part of workerman.
*
* 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
*/
declare(strict_types=1);
namespace Workerman;
use RuntimeException;
use Throwable;
use Workerman\Events\EventInterface;
use Workerman\Events\Revolt;
use Workerman\Events\Swoole;
use Workerman\Events\Swow;
use function function_exists;
use function pcntl_alarm;
use function pcntl_signal;
use function time;
use const PHP_INT_MAX;
use const SIGALRM;
/**
* Timer.
*/
class Timer
{
/**
* Tasks that based on ALARM signal.
* [
* run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]],
* run_time => [[$func, $args, $persistent, time_interval],[$func, $args, $persistent, time_interval],..]],
* ..
* ]
*
* @var array
*/
protected static array $tasks = [];
/**
* Event
*
* @var ?EventInterface
*/
protected static ?EventInterface $event = null;
/**
* Timer id
*
* @var int
*/
protected static int $timerId = 0;
/**
* Timer status
* [
* timer_id1 => bool,
* timer_id2 => bool,
* ....................,
* ]
*
* @var array
*/
protected static array $status = [];
/**
* Init.
*
* @param EventInterface|null $event
* @return void
*/
public static function init(?EventInterface $event = null): void
{
if ($event) {
self::$event = $event;
return;
}
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGALRM, self::signalHandle(...), false);
}
}
/**
* Repeat.
*
* @param float $timeInterval
* @param callable $func
* @param array $args
* @return int
*/
public static function repeat(float $timeInterval, callable $func, array $args = []): int
{
return self::$event->repeat($timeInterval, $func, $args);
}
/**
* Delay.
*
* @param float $timeInterval
* @param callable $func
* @param array $args
* @return int
*/
public static function delay(float $timeInterval, callable $func, array $args = []): int
{
return self::$event->delay($timeInterval, $func, $args);
}
/**
* ALARM signal handler.
*
* @return void
*/
public static function signalHandle(): void
{
if (!self::$event) {
pcntl_alarm(1);
self::tick();
}
}
/**
* Add a timer.
*
* @param float $timeInterval
* @param callable $func
* @param null|array $args
* @param bool $persistent
* @return int
*/
public static function add(float $timeInterval, callable $func, ?array $args = [], bool $persistent = true): int
{
if ($timeInterval < 0) {
throw new RuntimeException('$timeInterval can not less than 0');
}
if ($args === null) {
$args = [];
}
if (self::$event) {
return $persistent ? self::$event->repeat($timeInterval, $func, $args) : self::$event->delay($timeInterval, $func, $args);
}
// If not workerman runtime just return.
if (!Worker::getAllWorkers()) {
throw new RuntimeException('Timer can only be used in workerman running environment');
}
if (empty(self::$tasks)) {
pcntl_alarm(1);
}
$runTime = time() + $timeInterval;
if (!isset(self::$tasks[$runTime])) {
self::$tasks[$runTime] = [];
}
self::$timerId = self::$timerId == PHP_INT_MAX ? 1 : ++self::$timerId;
self::$status[self::$timerId] = true;
self::$tasks[$runTime][self::$timerId] = [$func, (array)$args, $persistent, $timeInterval];
return self::$timerId;
}
/**
* Coroutine sleep.
*
* @param float $delay
* @return void
*/
public static function sleep(float $delay): void
{
switch (Worker::$eventLoopClass) {
// Fiber
case Revolt::class:
$suspension = \Revolt\EventLoop::getSuspension();
static::add($delay, function () use ($suspension) {
$suspension->resume();
}, null, false);
$suspension->suspend();
return;
// Swoole
case Swoole::class:
\Swoole\Coroutine\System::sleep($delay);
return;
// Swow
case Swow::class:
usleep((int)($delay * 1000 * 1000));
return;
}
throw new RuntimeException('Timer::sleep() require revolt/event-loop. Please run command "composer require revolt/event-loop" and restart workerman');
}
/**
* Tick.
*
* @return void
*/
protected static function tick(): void
{
if (empty(self::$tasks)) {
pcntl_alarm(0);
return;
}
$timeNow = time();
foreach (self::$tasks as $runTime => $taskData) {
if ($timeNow >= $runTime) {
foreach ($taskData as $index => $oneTask) {
$taskFunc = $oneTask[0];
$taskArgs = $oneTask[1];
$persistent = $oneTask[2];
$timeInterval = $oneTask[3];
try {
$taskFunc(...$taskArgs);
} catch (Throwable $e) {
Worker::safeEcho((string)$e);
}
if ($persistent && !empty(self::$status[$index])) {
$newRunTime = time() + $timeInterval;
if (!isset(self::$tasks[$newRunTime])) {
self::$tasks[$newRunTime] = [];
}
self::$tasks[$newRunTime][$index] = [$taskFunc, (array)$taskArgs, $persistent, $timeInterval];
}
}
unset(self::$tasks[$runTime]);
}
}
}
/**
* Remove a timer.
*
* @param int $timerId
* @return bool
*/
public static function del(int $timerId): bool
{
if (self::$event) {
return self::$event->offDelay($timerId);
}
foreach (self::$tasks as $runTime => $taskData) {
if (array_key_exists($timerId, $taskData)) {
unset(self::$tasks[$runTime][$timerId]);
}
}
if (array_key_exists($timerId, self::$status)) {
unset(self::$status[$timerId]);
}
return true;
}
/**
* Remove all timers.
*
* @return void
*/
public static function delAll(): void
{
self::$tasks = self::$status = [];
if (function_exists('pcntl_alarm')) {
pcntl_alarm(0);
}
self::$event?->deleteAllTimer();
}
}
File diff suppressed because it is too large Load Diff