init
This commit is contained in:
Vendored
+29
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
+1047
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
+35
@@ -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
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace support;
|
||||
|
||||
use function defined;
|
||||
use function is_callable;
|
||||
use function is_file;
|
||||
use function method_exists;
|
||||
|
||||
class Plugin
|
||||
{
|
||||
/**
|
||||
* Install.
|
||||
* @param mixed $event
|
||||
* @return void
|
||||
*/
|
||||
public static function install($event)
|
||||
{
|
||||
static::findHelper();
|
||||
$psr4 = static::getPsr4($event);
|
||||
foreach ($psr4 as $namespace => $path) {
|
||||
$pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
|
||||
if (!defined($pluginConst)) {
|
||||
continue;
|
||||
}
|
||||
$installFunction = "\\{$namespace}Install::install";
|
||||
if (is_callable($installFunction)) {
|
||||
$installFunction(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update.
|
||||
* @param mixed $event
|
||||
* @return void
|
||||
*/
|
||||
public static function update($event)
|
||||
{
|
||||
static::findHelper();
|
||||
$psr4 = static::getPsr4($event);
|
||||
foreach ($psr4 as $namespace => $path) {
|
||||
$pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
|
||||
if (!defined($pluginConst)) {
|
||||
continue;
|
||||
}
|
||||
$updateFunction = "\\{$namespace}Install::update";
|
||||
if (is_callable($updateFunction)) {
|
||||
$updateFunction();
|
||||
continue;
|
||||
}
|
||||
$installFunction = "\\{$namespace}Install::install";
|
||||
if (is_callable($installFunction)) {
|
||||
$installFunction(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall.
|
||||
* @param mixed $event
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstall($event)
|
||||
{
|
||||
static::findHelper();
|
||||
$psr4 = static::getPsr4($event);
|
||||
foreach ($psr4 as $namespace => $path) {
|
||||
$pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
|
||||
if (!defined($pluginConst)) {
|
||||
continue;
|
||||
}
|
||||
$uninstallFunction = "\\{$namespace}Install::uninstall";
|
||||
if (is_callable($uninstallFunction)) {
|
||||
$uninstallFunction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get psr-4 info
|
||||
*
|
||||
* @param mixed $event
|
||||
* @return array
|
||||
*/
|
||||
protected static function getPsr4($event)
|
||||
{
|
||||
$operation = $event->getOperation();
|
||||
$autoload = method_exists($operation, 'getPackage') ? $operation->getPackage()->getAutoload() : $operation->getTargetPackage()->getAutoload();
|
||||
return $autoload['psr-4'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* FindHelper.
|
||||
* @return void
|
||||
*/
|
||||
protected static function findHelper()
|
||||
{
|
||||
// Plugin.php in webman
|
||||
require_once __DIR__ . '/helpers.php';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+10
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -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);
|
||||
}
|
||||
}
|
||||
+24
@@ -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);
|
||||
}
|
||||
}
|
||||
+46
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
+93
@@ -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;
|
||||
}
|
||||
// feat:custom worker class [default: Workerman\Worker]
|
||||
$class = is_a($class = $config['workerClass'] ?? '', Worker::class, true) ? $class : Worker::class;
|
||||
$worker = new $class($config['listen'] ?? null, $config['context'] ?? []);
|
||||
$properties = [
|
||||
'count',
|
||||
'user',
|
||||
'group',
|
||||
'reloadable',
|
||||
'reusePort',
|
||||
'transport',
|
||||
'protocol',
|
||||
'eventLoop',
|
||||
];
|
||||
$worker->name = $processName;
|
||||
foreach ($properties as $property) {
|
||||
if (isset($config[$property])) {
|
||||
$worker->$property = $config[$property];
|
||||
}
|
||||
}
|
||||
|
||||
$worker->onWorkerStart = function ($worker) use ($config) {
|
||||
require_once base_path('/support/bootstrap.php');
|
||||
if (isset($config['handler'])) {
|
||||
if (!class_exists($config['handler'])) {
|
||||
echo "process error: class {$config['handler']} not exists\r\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$instance = Container::make($config['handler'], $config['constructor'] ?? []);
|
||||
worker_bind($worker, $instance);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_realpath')) {
|
||||
/**
|
||||
* Get realpath
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
*/
|
||||
function get_realpath(string $filePath): string
|
||||
{
|
||||
if (strpos($filePath, 'phar://') === 0) {
|
||||
return $filePath;
|
||||
} else {
|
||||
return realpath($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('is_phar')) {
|
||||
/**
|
||||
* Is phar
|
||||
* @return bool
|
||||
*/
|
||||
function is_phar(): bool
|
||||
{
|
||||
return class_exists(Phar::class, false) && Phar::running();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('template_inputs')) {
|
||||
/**
|
||||
* Get template vars
|
||||
* @param mixed $template
|
||||
* @param array $vars
|
||||
* @param string|null $app
|
||||
* @param string|null $plugin
|
||||
* @return array
|
||||
*/
|
||||
function template_inputs(mixed $template, array $vars, ?string $app, ?string $plugin): array
|
||||
{
|
||||
$request = \request();
|
||||
$plugin = $plugin === null ? ($request->plugin ?? '') : $plugin;
|
||||
if (is_array($template)) {
|
||||
$vars = $template;
|
||||
$template = null;
|
||||
}
|
||||
if ($template === null && $controller = $request->controller) {
|
||||
$controllerSuffix = config($plugin ? "plugin.$plugin.app.controller_suffix" : "app.controller_suffix", '');
|
||||
$controllerName = $controllerSuffix !== '' ? substr($controller, 0, -strlen($controllerSuffix)) : $controller;
|
||||
$path = str_replace(['controller', 'Controller', '\\'], ['view', 'view', '/'], $controllerName);
|
||||
$path = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $path));
|
||||
$action = $request->action;
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
|
||||
foreach ($backtrace as $backtraceItem) {
|
||||
if (!isset($backtraceItem['class']) || !isset($backtraceItem['function'])) {
|
||||
continue;
|
||||
}
|
||||
if ($backtraceItem['class'] === App::class) {
|
||||
break;
|
||||
}
|
||||
if (preg_match('/\\\\controller\\\\/i', $backtraceItem['class'])) {
|
||||
$action = $backtraceItem['function'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
$actionFileBaseName = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $action));
|
||||
$template = "/$path/$actionFileBaseName";
|
||||
}
|
||||
return [$template, $vars, $app, $plugin];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cpu_count')) {
|
||||
/**
|
||||
* Get cpu count
|
||||
* @return int
|
||||
*/
|
||||
function cpu_count(): int
|
||||
{
|
||||
// Windows does not support the number of processes setting.
|
||||
if (DIRECTORY_SEPARATOR === '\\') {
|
||||
return 1;
|
||||
}
|
||||
$count = 4;
|
||||
if (is_callable('shell_exec')) {
|
||||
if (strtolower(PHP_OS) === 'darwin') {
|
||||
$count = (int)shell_exec('sysctl -n machdep.cpu.core_count');
|
||||
} else {
|
||||
try {
|
||||
$count = (int)shell_exec('nproc');
|
||||
} catch (\Throwable $ex) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
return $count > 0 ? $count : 4;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('input')) {
|
||||
/**
|
||||
* Get request parameters, if no parameter name is passed, an array of all values is returned, default values is supported
|
||||
* @param string|null $param param's name
|
||||
* @param mixed $default default value
|
||||
* @return mixed
|
||||
*/
|
||||
function input(?string $param = null, mixed $default = null): mixed
|
||||
{
|
||||
return is_null($param) ? request()->all() : request()->input($param, $default);
|
||||
}
|
||||
}
|
||||
|
||||
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
@@ -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.
|
||||
Vendored
+403
@@ -0,0 +1,403 @@
|
||||
# Workerman
|
||||
[](https://gitter.im/walkor/Workerman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=body_badge)
|
||||
[](https://packagist.org/packages/workerman/workerman)
|
||||
[](https://packagist.org/packages/workerman/workerman)
|
||||
[](https://packagist.org/packages/workerman/workerman)
|
||||
[](https://packagist.org/packages/workerman/workerman)
|
||||
[](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
@@ -0,0 +1,6 @@
|
||||
# Security Policy
|
||||
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please contact by email walkor@workerman.net
|
||||
+59
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
+209
@@ -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();
|
||||
+55
@@ -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);
|
||||
}
|
||||
}
|
||||
+166
@@ -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;
|
||||
}
|
||||
}
|
||||
+117
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
+2708
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user