2025.3.11

This commit is contained in:
2025-03-11 13:34:12 +08:00
commit 0b95eb8bf0
2161 changed files with 219203 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
composer.lock
vendor
.idea
tests/.phpunit.result.cache
tests/workerman.log
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 workerman
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.
+3
View File
@@ -0,0 +1,3 @@
# Workerman coroutine library
This is Workerman's coroutine library, which includes `Coroutine` `Channel` `Barrier` `Parallel` `Pool`.
+31
View File
@@ -0,0 +1,31 @@
{
"name": "workerman/coroutine",
"type": "library",
"license": "MIT",
"description": "Workerman coroutine",
"require": {
"php": ">=8.1"
},
"autoload": {
"psr-4": {
"Workerman\\Coroutine\\": "src",
"Workerman\\": "src"
}
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"psr/log": "*"
},
"autoload-dev": {
"psr-4": {
"Workerman\\Coroutine\\": "src",
"Workerman\\": "src",
"tests\\": "tests"
}
},
"scripts": {
"test": "php tests/start.php start"
},
"minimum-stability": "dev",
"prefer-stable": true
}
+65
View File
@@ -0,0 +1,65 @@
<?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\Coroutine;
use Workerman\Coroutine\Barrier\BarrierInterface;
use Workerman\Events\Swoole;
use Workerman\Events\Swow;
use Workerman\Worker;
/**
* Class Barrier
*/
class Barrier implements BarrierInterface
{
/**
* @var string
*/
protected static string $driver;
/**
* Get driver.
*
* @return string
*/
protected static function getDriver(): string
{
return static::$driver ??= match (Worker::$eventLoopClass) {
Swoole::class => Barrier\Swoole::class,
Swow::class => Barrier\Swow::class,
default=> Barrier\Fiber::class,
};
}
/**
* @inheritDoc
*/
public static function wait(object &$barrier, int $timeout = -1): void
{
static::getDriver()::wait($barrier, $timeout);
}
/**
* @inheritDoc
*/
public static function create(): object
{
return static::getDriver()::create();
}
}
@@ -0,0 +1,39 @@
<?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\Coroutine\Barrier;
/**
* Interface BarrierInterface
*/
interface BarrierInterface
{
/**
* Wait for the barrier to be released.
*
* @param object $barrier
* @param int $timeout
* @return void
*/
public static function wait(object &$barrier, int $timeout = -1): void;
/**
* Create a new barrier instance.
*
* @return BarrierInterface
*/
public static function create(): object;
}
+85
View File
@@ -0,0 +1,85 @@
<?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\Coroutine\Barrier;
use Revolt\EventLoop;
use RuntimeException;
use Workerman\Coroutine\Utils\DestructionWatcher;
use Workerman\Timer;
use Fiber as BaseFiber;
use Workerman\Worker;
/**
* Class Fiber
*/
class Fiber implements BarrierInterface
{
/**
* @inheritDoc
*/
public static function wait(object &$barrier, int $timeout = -1): void
{
$coroutine = BaseFiber::getCurrent();
$resumed = false;
$timerId = null;
if ($timeout > 0 && $coroutine) {
$timerId = Timer::delay($timeout, function() use ($coroutine, &$resumed) {
if (!$resumed) {
$resumed = true;
$coroutine->resume();
}
});
}
$coroutine && DestructionWatcher::watch($barrier, function() use ($coroutine, &$resumed, &$timerId) {
if (!$resumed) {
$resumed = true;
if ($timerId !== null) {
Timer::del($timerId);
}
// In PHP 8.4.0 and earlier,
// switching fibers during the execution of an object's destructor method is not allowed,
// so we implemented a delay.
if ($coroutine instanceof BaseFiber) {
Timer::delay(0.00001, function() use ($coroutine) {
$coroutine->resume();
});
return;
}
EventLoop::defer(function () use ($coroutine) {
$coroutine->resume();
});
}
});
$barrier = null;
$coroutine && BaseFiber::suspend();
}
/**
* @inheritDoc
*/
public static function create(): object
{
if (!Worker::isRunning()) {
throw new RuntimeException('Fiber barrier only support in workerman runtime');
}
return new self();
}
}
+39
View File
@@ -0,0 +1,39 @@
<?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\Coroutine\Barrier;
use Swoole\Coroutine\Barrier as SwooleBarrier;
class Swoole implements BarrierInterface
{
/**
* @inheritDoc
*/
public static function wait(object &$barrier, int $timeout = -1): void
{
SwooleBarrier::wait($barrier, $timeout);
}
/**
* @inheritDoc
*/
public static function create(): object
{
return SwooleBarrier::make();
}
}
+39
View File
@@ -0,0 +1,39 @@
<?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\Coroutine\Barrier;
use Swow\Sync\WaitReference;
class Swow implements BarrierInterface
{
/**
* @inheritDoc
*/
public static function wait(object &$barrier, int $timeout = -1): void
{
WaitReference::wait($barrier, $timeout);
}
/**
* @inheritDoc
*/
public static function create(): object
{
return new WaitReference();
}
}
+114
View File
@@ -0,0 +1,114 @@
<?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\Coroutine;
use InvalidArgumentException;
use Workerman\Coroutine\Channel\ChannelInterface;
use Workerman\Coroutine\Channel\Memory as ChannelMemory;
use Workerman\Coroutine\Channel\Swoole as ChannelSwoole;
use Workerman\Coroutine\Channel\Swow as ChannelSwow;
use Workerman\Coroutine\Channel\Fiber as ChannelFiber;
use Workerman\Events\Fiber;
use Workerman\Events\Swoole;
use Workerman\Events\Swow;
use Workerman\Worker;
/**
* Class Channel
*/
class Channel implements ChannelInterface
{
/**
* @var ChannelInterface
*/
protected ChannelInterface $driver;
/**
* Channel constructor.
*
* @param int $capacity
*/
public function __construct(int $capacity = 1)
{
if ($capacity < 1) {
throw new InvalidArgumentException("The capacity must be greater than 0");
}
$this->driver = match (Worker::$eventLoopClass) {
Swoole::class => new ChannelSwoole($capacity),
Swow::class => new ChannelSwow($capacity),
Fiber::class => new ChannelFiber($capacity),
default => new ChannelMemory($capacity),
};
}
/**
* @inheritDoc
*/
public function push(mixed $data, float $timeout = -1): bool
{
return $this->driver->push($data, $timeout);
}
/**
* @inheritDoc
*/
public function pop(float $timeout = -1): mixed
{
return $this->driver->pop($timeout);
}
/**
* @inheritDoc
*/
public function length(): int
{
return $this->driver->length();
}
/**
* @inheritDoc
*/
public function getCapacity(): int
{
return $this->driver->getCapacity();
}
/**
* @inheritDoc
*/
public function hasConsumers(): bool
{
return $this->driver->hasConsumers();
}
/**
* @inheritDoc
*/
public function hasProducers(): bool
{
return $this->driver->hasProducers();
}
/**
* @inheritDoc
*/
public function close(): void
{
$this->driver->close();
}
}
@@ -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\Coroutine\Channel;
/**
* ChannelInterface
*/
interface ChannelInterface
{
/**
* Push data to channel.
*
* @param mixed $data
* @param float $timeout
* @return bool
*/
public function push(mixed $data, float $timeout = -1): bool;
/**
* Pop data from channel.
*
* @param float $timeout
* @return mixed
*/
public function pop(float $timeout = -1): mixed;
/**
* Get the length of channel.
*
* @return int
*/
public function length(): int;
/**
* Get the capacity of channel.
*
* @return int
*/
public function getCapacity(): int;
/**
* Check if there are consumers waiting to pop data from the channel.
*
* @return bool
*/
public function hasConsumers(): bool;
/**
* Check if there are producers waiting to push data to the channel.
*
* @return bool
*/
public function hasProducers(): bool;
/**
* Close the channel.
*
* @return void
*/
public function close(): void;
}
+252
View File
@@ -0,0 +1,252 @@
<?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\Coroutine\Channel;
use Fiber as BaseFiber;
use RuntimeException;
use Workerman\Timer;
use WeakMap;
use Workerman\Worker;
/**
* Channel
*/
class Fiber implements ChannelInterface
{
/**
* @var array
*/
private array $queue = [];
/**
* @var WeakMap
*/
private WeakMap $waitingPush;
/**
* @var WeakMap
*/
private WeakMap $waitingPop;
/**
* @var int
*/
private int $capacity;
/**
* @var bool
*/
private bool $closed = false;
/**
* Constructor
*
* @param int $capacity
*/
public function __construct(int $capacity = 1)
{
$this->capacity = $capacity;
$this->waitingPush = new WeakMap();
$this->waitingPop = new WeakMap();
}
/**
* @inheritDoc
*/
public function push(mixed $data, float $timeout = -1): bool
{
if ($this->closed) {
return false;
}
if (count($this->queue) >= $this->capacity) {
if ($timeout == 0) {
return false;
}
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
throw new RuntimeException("Fiber::getCurrent() returned null. Ensure this method is called within a Fiber context.");
}
$this->waitingPush[$fiber] = true;
$timedOut = false;
$timerId = null;
if ($timeout > 0 && Worker::isRunning()) {
$timerId = Timer::delay($timeout, function () use ($fiber, &$timedOut) {
$timedOut = true;
if ($fiber->isSuspended()) {
unset($this->waitingPush[$fiber]);
$fiber->resume(false);
}
});
}
BaseFiber::suspend();
unset($this->waitingPush[$fiber]);
if (!$timedOut && $timerId) {
Timer::del($timerId);
}
if ($timedOut) {
return false;
}
// If the channel is closed while waiting, return false.
if ($this->closed) {
return false;
}
}
foreach ($this->waitingPop as $popFiber => $_) {
unset($this->waitingPop[$popFiber]);
if ($popFiber->isSuspended()) {
$popFiber->resume($data);
return true;
}
}
$this->queue[] = $data;
return true;
}
/**
* @inheritDoc
*/
public function pop(float $timeout = -1): mixed
{
if ($this->closed && empty($this->queue)) {
return false;
}
if (empty($this->queue)) {
if ($timeout == 0) {
return false;
}
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
throw new RuntimeException("Fiber::getCurrent() returned null. Ensure this method is called within a Fiber context.");
}
$this->waitingPop[$fiber] = true;
$timedOut = false;
$timerId = null;
if ($timeout > 0) {
Worker::isRunning() && $timerId = Timer::delay($timeout, function () use ($fiber, &$timedOut) {
$timedOut = true;
if ($fiber->isSuspended()) {
unset($this->waitingPop[$fiber]);
$fiber->resume(false);
}
});
}
$data = BaseFiber::suspend();
unset($this->waitingPop[$fiber]);
if (!$timedOut && $timerId !== null) {
Timer::del($timerId);
}
if ($timedOut) {
return false;
}
if ($data === false && $this->closed) {
return false;
}
return $data;
}
$value = array_shift($this->queue);
foreach ($this->waitingPush as $pushFiber => $_) {
unset($this->waitingPush[$pushFiber]);
if ($pushFiber->isSuspended()) {
$pushFiber->resume();
break;
}
}
return $value;
}
/**
* @inheritDoc
*/
public function length(): int
{
return count($this->queue);
}
/**
* @inheritDoc
*/
public function getCapacity(): int
{
return $this->capacity;
}
/**
* @inheritDoc
*/
public function hasConsumers(): bool
{
return count($this->waitingPop) > 0;
}
/**
* @inheritDoc
*/
public function hasProducers(): bool
{
return count($this->waitingPush) > 0;
}
/**
* @inheritDoc
*/
public function close(): void
{
$this->closed = true;
foreach ($this->waitingPush as $fiber => $_) {
unset($this->waitingPush[$fiber]);
if ($fiber->isSuspended()) {
$fiber->resume(false);
}
}
$this->waitingPush = new WeakMap();
foreach ($this->waitingPop as $fiber => $_) {
unset($this->waitingPop[$fiber]);
if ($fiber->isSuspended()) {
$fiber->resume(false);
}
}
$this->waitingPop = new WeakMap();
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Workerman\Coroutine\Channel;
class Memory implements ChannelInterface
{
private array $data = [];
private int $capacity;
private bool $closed = false;
public function __construct(int $capacity = 0)
{
$this->capacity = $capacity;
}
public function push(mixed $data, float $timeout = -1): bool
{
if ($this->closed) {
return false;
}
if ($this->capacity > 0 && count($this->data) >= $this->capacity) {
// Channel is full
return false;
}
$this->data[] = $data;
return true;
}
public function pop(float $timeout = -1): mixed
{
if (count($this->data) > 0) {
return array_shift($this->data);
}
return false;
}
public function length(): int
{
return count($this->data);
}
public function getCapacity(): int
{
return $this->capacity;
}
/**
* @inheritDoc
*/
public function hasConsumers(): bool
{
return false;
}
/**
* @inheritDoc
*/
public function hasProducers(): bool
{
return false;
}
public function close(): void
{
$this->closed = true;
}
}
+98
View File
@@ -0,0 +1,98 @@
<?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\Coroutine\Channel;
use Swoole\Coroutine\Channel;
/**
* Class Swoole
*/
class Swoole implements ChannelInterface
{
/**
* @var Channel
*/
protected Channel $channel;
/**
* Constructor.
*
* @param int $capacity
*/
public function __construct(protected int $capacity = 1)
{
$this->channel = new Channel($capacity);
}
/**
* @inheritDoc
*/
public function push(mixed $data, float $timeout = -1): bool
{
return $this->channel->push($data, $timeout);
}
/**
* @inheritDoc
*/
public function pop(float $timeout = -1): mixed
{
return $this->channel->pop($timeout);
}
/**
* @inheritDoc
*/
public function length(): int
{
return $this->channel->length();
}
/**
* @inheritDoc
*/
public function getCapacity(): int
{
return $this->channel->capacity;
}
/**
* @inheritDoc
*/
public function hasConsumers(): bool
{
return $this->channel->stats()['consumer_num'] > 0;
}
/**
* @inheritDoc
*/
public function hasProducers(): bool
{
return $this->channel->stats()['producer_num'] > 0;
}
/**
* @inheritDoc
*/
public function close(): void
{
$this->channel->close();
}
}
+108
View File
@@ -0,0 +1,108 @@
<?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\Coroutine\Channel;
use Swow\Channel;
use Throwable;
/**
* Class Swow
*/
class Swow implements ChannelInterface
{
/**
* @var Channel
*/
protected Channel $channel;
/**
* Constructor.
*
* @param int $capacity
*/
public function __construct(protected int $capacity = 1)
{
$this->channel = new Channel($capacity);
}
/**
* @inheritDoc
*/
public function push(mixed $data, float $timeout = -1): bool
{
try {
$this->channel->push($data, $timeout == -1 ? -1 : (int)($timeout * 1000));
} catch (Throwable) {
return false;
}
return true;
}
/**
* @inheritDoc
*/
public function pop(float $timeout = -1): mixed
{
try {
return $this->channel->pop($timeout == -1 ? -1 : (int)($timeout * 1000));
} catch (Throwable) {
return false;
}
}
/**
* @inheritDoc
*/
public function length(): int
{
return $this->channel->getLength();
}
/**
* @inheritDoc
*/
public function getCapacity(): int
{
return $this->channel->getCapacity();
}
/**
* @inheritDoc
*/
public function hasConsumers(): bool
{
return $this->channel->hasConsumers();
}
/**
* @inheritDoc
*/
public function hasProducers(): bool
{
return $this->channel->hasProducers();
}
/**
* @inheritDoc
*/
public function close(): void
{
$this->channel->close();
}
}
+90
View File
@@ -0,0 +1,90 @@
<?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\Coroutine;
use ArrayObject;
use Workerman\Coroutine\Context\ContextInterface;
use Workerman\Events\Swoole;
use Workerman\Events\Swow;
use Workerman\Worker;
/**
* Class Context
*/
class Context implements ContextInterface
{
/**
* @var class-string<ContextInterface>
*/
protected static string $driver;
/**
* @inheritDoc
*/
public static function get(?string $name = null, mixed $default = null): mixed
{
return static::$driver::get($name, $default);
}
/**
* @inheritDoc
*/
public static function set(string $name, $value): void
{
static::$driver::set($name, $value);
}
/**
* @inheritDoc
*/
public static function has(string $name): bool
{
return static::$driver::has($name);
}
/**
* @inheritDoc
*/
public static function reset(?ArrayObject $data = null): void
{
static::$driver::reset($data);
}
/**
* @inheritDoc
*/
public static function destroy(): void
{
static::$driver::destroy();
}
/**
* @return void
*/
public static function initDriver(): void
{
static::$driver ??= match (Worker::$eventLoopClass) {
Swoole::class => Context\Swoole::class,
Swow::class => Context\Swow::class,
default=> Context\Fiber::class,
};
}
}
Context::initDriver();
@@ -0,0 +1,50 @@
<?php
namespace Workerman\Coroutine\Context;
use ArrayObject;
/**
* Interface ContextInterface
*/
interface ContextInterface
{
/**
* Get the value from the context with the specified name.
* If the name does not exist, return the default value.
*
* @param string|null $name The name of the value to get.
* @param mixed $default The default value to return if the name does not exist.
* @return mixed The value from the context or the default value.
*/
public static function get(?string $name = null, mixed $default = null): mixed;
/**
* Set the value in the context with the specified name.
*
* @param string $name The name of the value to set.
* @param mixed $value The value to set.
*/
public static function set(string $name, mixed $value): void;
/**
* Check if the specified name exists in the context.
*
* @param string $name The name to check.
* @return bool True if the name exists, otherwise false.
*/
public static function has(string $name): bool;
/**
* Initialize the context with an array of data.
*
* @param ArrayObject|null $data The array of data to initialize the context.
*/
public static function reset(?ArrayObject $data = null): void;
/**
* Destroy the context.
*/
public static function destroy(): void;
}
+107
View File
@@ -0,0 +1,107 @@
<?php
namespace Workerman\Coroutine\Context;
use ArrayObject;
use WeakMap;
use Fiber as BaseFiber;
/**
* Class Fiber
*/
class Fiber implements ContextInterface
{
/**
* @var WeakMap
*/
private static WeakMap $contexts;
/**
* @var ArrayObject
*/
private static ArrayObject $nonFiberContext;
/**
* @inheritDoc
*/
public static function get(?string $name = null, mixed $default = null): mixed
{
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
return $name !== null ? (static::$nonFiberContext[$name] ?? $default) : static::$nonFiberContext;
}
if ($name === null) {
return static::$contexts[$fiber] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
}
return static::$contexts[$fiber][$name] ?? $default;
}
/**
* @inheritDoc
*/
public static function set(string $name, $value): void
{
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
static::$nonFiberContext[$name] = $value;
return;
}
static::$contexts[$fiber] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
static::$contexts[$fiber][$name] = $value;
}
/**
* @inheritDoc
*/
public static function has(string $name): bool
{
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
return static::$nonFiberContext->offsetExists($name);
}
return isset(static::$contexts[$fiber]) && static::$contexts[$fiber]->offsetExists($name);
}
/**
* @inheritDoc
*/
public static function reset(?ArrayObject $data = null): void
{
if ($data) {
$data->setFlags(ArrayObject::ARRAY_AS_PROPS);
} else {
$data = new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
}
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
static::$nonFiberContext = $data;
return;
}
static::$contexts[$fiber] = $data;
}
/**
* @inheritDoc
*/
public static function destroy(): void
{
$fiber = BaseFiber::getCurrent();
if ($fiber === null) {
static::$nonFiberContext = new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
return;
}
unset(static::$contexts[$fiber]);
}
/**
* Initialize the weakMap.
*/
public static function initContext(): void
{
static::$contexts = new WeakMap();
static::$nonFiberContext = new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
}
}
Fiber::initContext();
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace Workerman\Coroutine\Context;
use ArrayObject;
use Swoole\Coroutine;
class Swoole implements ContextInterface
{
/**
* @inheritDoc
*/
public static function get(?string $name = null, mixed $default = null): mixed
{
$context = Coroutine::getContext();
$context->setFlags(ArrayObject::ARRAY_AS_PROPS);
if ($name === null) {
return $context;
}
return $context[$name] ?? $default;
}
/**
* @inheritDoc
*/
public static function set(string $name, $value): void
{
Coroutine::getContext()[$name] = $value;
}
/**
* @inheritDoc
*/
public static function has(string $name): bool
{
$context = Coroutine::getContext();
return $context->offsetExists($name);
}
/**
* @inheritDoc
*/
public static function reset(?ArrayObject $data = null): void
{
$context = Coroutine::getContext();
$context->setFlags(ArrayObject::ARRAY_AS_PROPS);
$context->exchangeArray($data ?: []);
}
/**
* @inheritDoc
*/
public static function destroy(): void
{
$context = Coroutine::getContext();
$context->exchangeArray([]);
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace Workerman\Coroutine\Context;
use ArrayObject;
use Swow\Coroutine;
use WeakMap;
class Swow implements ContextInterface
{
/**
* @var WeakMap
*/
public static WeakMap $contexts;
/**
* @inheritDoc
*/
public static function get(?string $name = null, mixed $default = null): mixed
{
$fiber = Coroutine::getCurrent();
if ($name === null) {
static::$contexts[$fiber] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
return static::$contexts[$fiber];
}
return static::$contexts[$fiber][$name] ?? $default;
}
/**
* @inheritDoc
*/
public static function set(string $name, $value): void
{
$coroutine = Coroutine::getCurrent();
static::$contexts[$coroutine] ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS);
static::$contexts[$coroutine][$name] = $value;
}
/**
* @inheritDoc
*/
public static function has(string $name): bool
{
$fiber = Coroutine::getCurrent();
return isset(static::$contexts[$fiber]) && static::$contexts[$fiber]->offsetExists($name);
}
/**
* @inheritDoc
*/
public static function reset(?ArrayObject $data = null): void
{
$coroutine = Coroutine::getCurrent();
$data->setFlags(ArrayObject::ARRAY_AS_PROPS);
static::$contexts[$coroutine] = $data;
}
/**
* @inheritDoc
*/
public static function destroy(): void
{
unset(static::$contexts[Coroutine::getCurrent()]);
}
/**
* Initialize the weakMap.
*
* @return void
*/
public static function initContext(): void
{
self::$contexts = new WeakMap();
}
}
Swow::initContext();
+129
View File
@@ -0,0 +1,129 @@
<?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 Workerman\Coroutine\Coroutine\CoroutineInterface;
use Workerman\Coroutine\Coroutine\Fiber;
use Workerman\Worker;
use Workerman\Coroutine\Coroutine\Swoole as SwooleCoroutine;
use Workerman\Coroutine\Coroutine\Swow as SwowCoroutine;
use Workerman\Events\Swoole as SwooleEvent;
use Workerman\Events\Swow as SwowEvent;
/**
* Class Coroutine
*/
class Coroutine implements CoroutineInterface
{
/**
* @var class-string<CoroutineInterface>
*/
protected static string $driverClass;
/**
* @var CoroutineInterface
*/
public CoroutineInterface $driver;
/**
* Coroutine constructor.
*
* @param callable $callable
*/
public function __construct(callable $callable)
{
$this->driver = new static::$driverClass($callable);
}
/**
* @inheritDoc
*/
public static function create(callable $callable, ...$args): CoroutineInterface
{
return static::$driverClass::create($callable, ...$args);
}
/**
* @inheritDoc
*/
public function start(mixed ...$args): mixed
{
return $this->driver->start(...$args);
}
/**
* @inheritDoc
*/
public function resume(mixed ...$args): mixed
{
return $this->driver->resume(...$args);
}
/**
* @inheritDoc
*/
public function id(): int
{
return $this->driver->id();
}
/**
* @inheritDoc
*/
public static function defer(callable $callable): void
{
static::$driverClass::defer($callable);
}
/**
* @inheritDoc
*/
public static function suspend(mixed $value = null): mixed
{
return static::$driverClass::suspend($value);
}
/**
* @inheritDoc
*/
public static function getCurrent(): CoroutineInterface
{
return static::$driverClass::getCurrent();
}
/**
* @inheritDoc
*/
public static function isCoroutine(): bool
{
return static::$driverClass::isCoroutine();
}
/**
* @return void
*/
public static function init(): void
{
static::$driverClass = match (Worker::$eventLoopClass ?? null) {
SwooleEvent::class => SwooleCoroutine::class,
SwowEvent::class => SwowCoroutine::class,
default => Fiber::class,
};
}
}
Coroutine::init();
@@ -0,0 +1,90 @@
<?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\Coroutine\Coroutine;
use Fiber;
use Swow\Coroutine as SwowCoroutine;
/**
* Interface CoroutineInterface
*/
interface CoroutineInterface
{
/**
* Create a coroutine.
*
* @param callable $callable
* @param ...$data
* @return CoroutineInterface
*/
public static function create(callable $callable, ...$data): CoroutineInterface;
/**
* Start a coroutine.
*
* @param mixed ...$args
* @return mixed
*/
public function start(mixed ...$args): mixed;
/**
* Resume a coroutine.
*
* @param mixed ...$args
* @return mixed
*/
public function resume(mixed ...$args): mixed;
/**
* Get the id of the coroutine.
*
* @return int
*/
public function id(): int;
/**
* Register a callable to be executed when the current fiber is destroyed
*
* @param callable $callable
* @return void
*/
public static function defer(callable $callable): void;
/**
* Yield the coroutine.
*
* @param mixed|null $value
* @return mixed
*/
public static function suspend(mixed $value = null): mixed;
/**
* Get the current coroutine.
*
* @return CoroutineInterface|Fiber|SwowCoroutine|static
*/
public static function getCurrent(): CoroutineInterface|Fiber|SwowCoroutine|static;
/**
* Check if the current coroutine is in a coroutine.
*
* @return bool
*/
public static function isCoroutine(): bool;
}
+154
View File
@@ -0,0 +1,154 @@
<?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\Coroutine\Coroutine;
use Fiber as BaseFiber;
use RuntimeException;
use WeakMap;
use Workerman\Coroutine\Utils\DestructionWatcher;
/**
* Class Fiber
*/
class Fiber implements CoroutineInterface
{
/**
* @var BaseFiber|null
*/
private ?BaseFiber $fiber;
/**
* @var WeakMap
*/
private static WeakMap $instances;
/**
* @var int
*/
private int $id;
/**
* @param callable|null $callable
*/
public function __construct(?callable $callable = null)
{
static $id = 0;
$this->id = ++$id;
if ($callable) {
$callable = function(...$args) use ($callable) {
try {
$callable(...$args);
} finally {
$this->fiber = null;
}
};
$this->fiber = new BaseFiber($callable);
self::$instances[$this->fiber] = $this;
}
}
/**
* @inheritDoc
*/
public static function create(callable $callable, ...$args): CoroutineInterface
{
$fiber = new Fiber($callable);
$fiber->start(...$args);
return $fiber;
}
/**
* @inheritDoc
*/
public function start(mixed ...$args): mixed
{
return $this->fiber->start(...$args);
}
/**
* @inheritDoc
*/
public function resume(mixed ...$args): mixed
{
return $this->fiber->resume(...$args);
}
/**
* @inheritDoc
*/
public static function suspend(mixed $value = null): mixed
{
return BaseFiber::suspend($value);
}
/**
* @inheritDoc
*/
public function id(): int
{
return $this->id;
}
/**
* @inheritDoc
*/
public static function defer(callable $callable): void
{
$baseFiber = BaseFiber::getCurrent();
if ($baseFiber === null) {
throw new RuntimeException('Cannot defer outside of a fiber.');
}
DestructionWatcher::watch($baseFiber, $callable);
}
/**
* @inheritDoc
*/
public static function getCurrent(): CoroutineInterface
{
if (!$baseFiber = BaseFiber::getCurrent()) {
throw new RuntimeException('Not in fiber context');
}
if (!isset(self::$instances[$baseFiber])) {
$fiber = new Fiber();
$fiber->fiber = $baseFiber;
self::$instances[$baseFiber] = $fiber;
}
return self::$instances[$baseFiber];
}
/**
* @inheritDoc
*/
public static function isCoroutine(): bool
{
return BaseFiber::getCurrent() !== null;
}
/**
* Initialize the fiber.
*
* @return void
*/
public static function init(): void
{
self::$instances = new WeakMap();
}
}
Fiber::init();
+148
View File
@@ -0,0 +1,148 @@
<?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\Coroutine\Coroutine;
use RuntimeException;
use Swoole\Coroutine;
use WeakReference;
class Swoole implements CoroutineInterface
{
/**
* @var array
*/
private static array $instances = [];
/**
* @var int
*/
private int $id = 0;
/**
* @var callable|null
*/
private $callable;
/**
* Coroutine constructor.
*
* @param callable|null $callable
*/
public function __construct(?callable $callable = null)
{
$this->callable = $callable;
}
/**
* @inheritDoc
*/
public static function create(callable $callable, ...$args): CoroutineInterface
{
$id = Coroutine::create($callable, ...$args);
if (isset(self::$instances[$id]) && $coroutine = self::$instances[$id]->get()) {
return $coroutine;
}
$coroutine = new self($callable);
$coroutine->id = $id;
self::$instances[$id] = WeakReference::create($coroutine);
return $coroutine;
}
/**
* @inheritDoc
*/
public function start(mixed ...$args): CoroutineInterface
{
if ($this->id) {
throw new RuntimeException('Coroutine has already started');
}
$this->id = Coroutine::create($this->callable, ...$args);
$this->callable = null;
if (isset(self::$instances[$this->id]) && $coroutine = self::$instances[$this->id]->get()) {
return $coroutine;
}
self::$instances[$this->id] = WeakReference::create($this);
return $this;
}
/**
* @inheritDoc
*/
public function resume(mixed ...$args): mixed
{
return Coroutine::resume($this->id, ...$args);
}
/**
* @inheritDoc
*/
public function id(): int
{
return $this->id;
}
/**
* @inheritDoc
*/
public static function defer(callable $callable): void
{
Coroutine::defer($callable);
}
/**
* @inheritDoc
*/
public static function suspend(mixed $value = null): mixed
{
return Coroutine::suspend($value);
}
/**
* @inheritDoc
*/
public static function getCurrent(): CoroutineInterface
{
$id = Coroutine::getCid();
if ($id === -1) {
throw new RuntimeException('Not in coroutine');
}
if (!isset(self::$instances[$id])) {
$coroutine = new self();
$coroutine->id = $id;
self::$instances[$id] = WeakReference::create($coroutine);
}
return self::$instances[$id]->get();
}
/**
* @inheritDoc
*/
public static function isCoroutine(): bool
{
return Coroutine::getCid() > 0;
}
/**
* Destructor.
*/
public function __destruct()
{
unset(self::$instances[$this->id]);
}
}
+91
View File
@@ -0,0 +1,91 @@
<?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\Coroutine\Coroutine;
use Swow\Coroutine;
/**
* Class Swow
*/
class Swow extends Coroutine implements CoroutineInterface
{
/**
* @var array
*/
private array $callbacks = [];
/**
* @inheritDoc
*/
public static function defer(callable $callable): void
{
$coroutine = static::getCurrent();
$coroutine->callbacks[] = $callable;
}
/**
* @inheritDoc
*/
public static function create(callable $callable, ...$args): CoroutineInterface
{
return static::run($callable, ...$args);
}
/**
* @inheritDoc
*/
public function start(mixed ...$args): mixed
{
return $this->resume(...$args);
}
/**
* @inheritDoc
*/
public function id(): int
{
return $this->getId();
}
/**
* @inheritDoc
*/
public static function suspend(mixed $value = null): mixed
{
return Coroutine::yield($value);
}
/**
* @inheritDoc
*/
public static function isCoroutine(): bool
{
return true;
}
/**
* Destructor.
*/
public function __destruct()
{
foreach (array_reverse($this->callbacks) as $callable) {
$callable();
}
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
/**
* This file is part of workerman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
declare(strict_types=1);
namespace Workerman\Coroutine;
use RuntimeException;
/**
* Class Locker
*/
class Locker
{
/**
* @var Channel[]
*/
protected static array $channels = [];
/**
* Lock.
*
* @param string $key
* @return bool
*/
public static function lock(string $key): bool
{
if (!isset(static::$channels[$key])) {
static::$channels[$key] = new Channel(1);
}
return static::$channels[$key]->push(true);
}
/**
* Unlock.
*
* @param string $key
* @return bool
*/
public static function unlock(string $key): bool
{
if ($channel = static::$channels[$key] ?? null) {
// Must check hasProducers before pop, because pop in swow will wake up the producer, leading to inaccurate judgment.
$hasProducers = $channel->hasProducers();
$result = $channel->pop();
if (!$hasProducers) {
$channel->close();
unset(static::$channels[$key]);
}
return $result;
}
throw new RuntimeException("Unlock failed, because the key $key is not locked");
}
}
+109
View File
@@ -0,0 +1,109 @@
<?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\Coroutine;
use Throwable;
use Workerman\Coroutine;
/**
* Class Parallel
*/
class Parallel
{
/**
* @var Channel|null
*/
protected ?Channel $channel = null;
/**
* @var array
*/
protected array $callbacks = [];
/**
* @var array
*/
protected array $results = [];
/**
* @var array
*/
protected array $exceptions = [];
/**
* Constructor.
*
* @param int $concurrent
*/
public function __construct(int $concurrent = -1)
{
if ($concurrent > 0) {
$this->channel = new Channel($concurrent);
}
}
/**
* Add a coroutine.
*
* @param callable $callable
* @param string|null $key
* @return void
*/
public function add(callable $callable, ?string $key = null): void
{
if ($key === null) {
$this->callbacks[] = $callable;
} else {
$this->callbacks[$key] = $callable;
}
}
/**
* Wait all coroutines complete and return results.
*
* @return array
*/
public function wait(): array
{
$barrier = Barrier::create();
foreach ($this->callbacks as $key => $callback) {
$this->channel?->push(true);
Coroutine::create(function () use ($callback, $key, $barrier) {
try {
$this->results[$key] = $callback();
} catch (Throwable $throwable) {
$this->exceptions[$key] = $throwable;
} finally {
$this->channel?->pop();
}
});
}
Barrier::wait($barrier);
return $this->results;
}
/**
* Get failed results.
*
* @return array
*/
public function getExceptions(): array
{
return $this->exceptions;
}
}
+386
View File
@@ -0,0 +1,386 @@
<?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\Coroutine;
use Closure;
use Psr\Log\LoggerInterface;
use RuntimeException;
use stdClass;
use Throwable;
use WeakMap;
use Workerman\Coroutine;
use Workerman\Coroutine\Utils\DestructionWatcher;
use Workerman\Timer;
use Workerman\Worker;
/**
* Class Pool
*/
class Pool implements PoolInterface
{
/**
* @var Channel
*/
protected Channel $channel;
/**
* @var int
*/
protected int $minConnections = 1;
/**
* @var WeakMap
*/
protected WeakMap $connections;
/**
* @var ?object
*/
protected ?object $nonCoroutineConnection = null;
/**
* @var WeakMap
*/
protected WeakMap $lastUsedTimes;
/**
* @var WeakMap
*/
protected WeakMap $lastHeartbeatTimes;
/**
* @var Closure|null
*/
protected ?Closure $connectionCreateHandler = null;
/**
* @var Closure|null
*/
protected ?Closure $connectionDestroyHandler = null;
/**
* @var Closure|null
*/
protected ?Closure $connectionHeartbeatHandler = null;
/**
* @var float
*/
protected float $idleTimeout = 60;
/**
* @var float
*/
protected float $heartbeatInterval = 50;
/**
* @var float
*/
protected float $waitTimeout = 10;
/**
* @var LoggerInterface|Closure|null
*/
protected LoggerInterface|Closure|null $logger = null;
/**
* @var array|string[]
*/
private array $configurableProperties = [
'minConnections',
'idleTimeout',
'heartbeatInterval',
'waitTimeout',
];
/**
* Constructor.
*
* @param int $maxConnections
* @param array $config
*/
public function __construct(protected int $maxConnections = 1, protected array $config = [])
{
foreach ($config as $key => $value) {
$camelCaseKey = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $key))));
if (in_array($camelCaseKey, $this->configurableProperties, true)) {
$this->$camelCaseKey = $value;
}
}
$this->channel = new Channel($maxConnections);
$this->lastUsedTimes = new WeakMap();
$this->lastHeartbeatTimes = new WeakMap();
$this->connections = new WeakMap();
if (Worker::isRunning()) {
Timer::repeat(1, function () {
$this->checkConnections();
});
}
}
/**
* Set the connection creator.
*
* @param callable $connectionCreateHandler
* @return $this
*/
public function setConnectionCreator(callable $connectionCreateHandler): self
{
$this->connectionCreateHandler = $connectionCreateHandler;
return $this;
}
/**
* Set the connection closer.
*
* @param callable $connectionDestroyHandler
* @return $this
*/
public function setConnectionCloser(callable $connectionDestroyHandler): self
{
$this->connectionDestroyHandler = $connectionDestroyHandler;
return $this;
}
/**
* Set the connection heartbeat checker.
*
* @param callable $connectionHeartbeatHandler
* @return $this
*/
public function setHeartbeatChecker(callable $connectionHeartbeatHandler): self
{
$this->connectionHeartbeatHandler = $connectionHeartbeatHandler;
return $this;
}
/**
* Get connection.
*
* @return object
* @throws Throwable
*/
public function get(): object
{
if (!Coroutine::isCoroutine()) {
if (!$this->nonCoroutineConnection) {
$this->nonCoroutineConnection = $this->createConnection();
}
return $this->nonCoroutineConnection;
}
$num = $this->channel->length();
if ($num === 0 && $this->getConnectionCount() < $this->maxConnections) {
return $this->createConnection();
}
$connection = $this->channel->pop($this->waitTimeout);
if (!$connection) {
throw new RuntimeException("Failed to get a connection from the pool within the wait timeout ($this->waitTimeout seconds). The connection pool is exhausted.");
}
$this->lastUsedTimes[$connection] = time();
return $connection;
}
/**
* Put connection to pool.
*
* @param object $connection
* @return void
* @throws Throwable
*/
public function put(object $connection): void
{
// This connection does not belong to the connection pool.
// It may have been closed by $this->closeConnection($connection).
if (!isset($this->connections[$connection])) {
throw new RuntimeException('The connection does not belong to the connection pool.');
}
if ($connection === $this->nonCoroutineConnection) {
return;
}
try {
$this->channel->push($connection);
} catch (Throwable $throwable) {
$this->closeConnection($connection);
throw $throwable;
}
}
/**
* Check if the connection is valid.
*
* @param $connection
* @return bool
*/
protected function isValidConnection($connection): bool
{
return is_object($connection);
}
/**
* Create connection.
*
* @return object
* @throws Throwable
*/
public function createConnection(): object
{
if ($this->getConnectionCount() >= $this->maxConnections) {
throw new RuntimeException('CreateConnection failed, maximum connection limit reached.');
}
// Create a placeholder to ensure the correct value of getConnectionCount().
$placeholder = new stdClass;
$this->connections[$placeholder] = 0;
try {
// Coroutines will switch here, so we need $placeholder to ensure the correct value of getConnectionCount().
$connection = ($this->connectionCreateHandler)();
if (!$this->isValidConnection($connection)) {
throw new RuntimeException('CreateConnection failed, expected a connection object, but got ' . gettype($connection) . '.');
}
unset($this->connections[$placeholder]);
$this->connections[$connection] = $this->lastUsedTimes[$connection] = $this->lastHeartbeatTimes[$connection] = time();
} catch (Throwable $throwable) {
unset($this->connections[$placeholder]);
throw $throwable;
}
return $connection;
}
/**
* Close the connection and remove the connection from the connection pool.
*
* @param object $connection
* @return void
*/
public function closeConnection(object $connection): void
{
if (!isset($this->connections[$connection])) {
return;
}
// Mark this connection as no longer belonging to the connection pool.
unset($this->lastUsedTimes[$connection], $this->lastHeartbeatTimes[$connection], $this->connections[$connection]);
if ($this->nonCoroutineConnection === $connection) {
$this->nonCoroutineConnection = null;
}
if (!$this->connectionDestroyHandler) {
return;
}
try {
($this->connectionDestroyHandler)($connection);
} catch (Throwable $throwable) {
$this->log($throwable);
}
}
/**
* Cleanup idle connections.
*
* @return void
*/
protected function checkConnections(): void
{
$num = $this->channel->length();
$time = time();
for($i = $num; $i > 0; $i--) {
$connection = $this->channel->pop(0.001);
if (!$connection) {
return;
}
$lastUsedTime = $this->lastUsedTimes[$connection];
if ($time - $lastUsedTime > $this->idleTimeout && $this->channel->length() >= $this->minConnections) {
$this->closeConnection($connection);
continue;
}
$this->trySendHeartbeat($connection) && $this->channel->push($connection);
}
if ($this->nonCoroutineConnection) {
$this->trySendHeartbeat($this->nonCoroutineConnection);
}
}
/**
* Try to send heartbeat.
*
* @param $connection
* @return bool
*/
private function trySendHeartbeat($connection): bool
{
$lastHeartbeatTime = $this->lastHeartbeatTimes[$connection] ?? 0;
$time = time();
if ($this->connectionHeartbeatHandler && $time - $lastHeartbeatTime >= $this->heartbeatInterval) {
try {
($this->connectionHeartbeatHandler)($connection);
$this->lastHeartbeatTimes[$connection] = $time;
} catch (Throwable $throwable) {
$this->log($throwable);
$this->closeConnection($connection);
return false;
}
}
return true;
}
/**
* Get the number of connections in the connection pool.
*
* @return int
*/
public function getConnectionCount(): int
{
return count($this->connections);
}
/**
* Close connections.
*
* @return void
*/
public function closeConnections(): void
{
$num = $this->channel->length();
for ($i = $num; $i > 0; $i--) {
$connection = $this->channel->pop(0.001);
if (!$connection) {
return;
}
$this->closeConnection($connection);
}
$this->nonCoroutineConnection && $this->closeConnection($this->nonCoroutineConnection);
}
/**
* Log.
*
* @param $message
* @return void
*/
protected function log($message): void
{
if (!$this->logger) {
echo $message . PHP_EOL;
return;
}
if ($this->logger instanceof Closure) {
($this->logger)($message);
return;
}
$this->logger->info((string)$message);
}
}
+69
View File
@@ -0,0 +1,69 @@
<?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\Coroutine;
/**
* Interface PoolInterface
*/
interface PoolInterface
{
/**
* Get a connection from the pool.
*
* @return mixed
*/
public function get(): mixed;
/**
* Put a connection back to the pool.
*
* @param object $connection
* @return void
*/
public function put(object $connection): void;
/**
* Create a connection.
*
* @return object
*/
public function createConnection(): object;
/**
* Close the connection and remove the connection from the connection pool.
*
* @param object $connection
* @return void
*/
public function closeConnection(object $connection): void;
/**
* Get the number of connections in the connection pool.
*
* @return int
*/
public function getConnectionCount(): int;
/**
* Close connections in the connection pool.
*
* @return void
*/
public function closeConnections(): void;
}
@@ -0,0 +1,67 @@
<?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\Coroutine\Utils;
use WeakMap;
class DestructionWatcher
{
/**
* @var WeakMap
*/
protected static WeakMap $objects;
/**
* @var callable[]
*/
protected array $callbacks = [];
/**
* DestructionWatcher constructor.
*
* @param callable|null $callback
*/
public function __construct(?callable $callback = null)
{
if ($callback) {
$this->callbacks[] = $callback;
}
}
/**
* DestructionWatcher destructor.
*/
public function __destruct()
{
foreach (array_reverse($this->callbacks) as $callback) {
$callback();
}
}
/**
* Watch object destruction.
*
* @param object $object
* @param callable $callback
* @return void
*/
public static function watch(object $object, callable $callback): void
{
static::$objects ??= new WeakMap();
static::$objects[$object] ??= new static();
static::$objects[$object]->callbacks[] = $callback;
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
/**
* @author workbunny/Chaz6chez
* @email chaz6chez1993@outlook.com
*/
declare(strict_types=1);
namespace Workerman\Coroutine;
use BadMethodCallException;
use Workerman\Worker;
use Workerman\Coroutine\Coroutine\CoroutineInterface;
use Workerman\Coroutine\WaitGroup\Fiber as FiberWaitGroup;
use Workerman\Coroutine\WaitGroup\Swoole as SwooleWaitGroup;
use Workerman\Coroutine\WaitGroup\Swow as SwowWaitGroup;
use Workerman\Coroutine\WaitGroup\WaitGroupInterface;
use Workerman\Events\Swoole;
use Workerman\Events\Swow;
/**
* @method bool add(int $delta = 1)
* @method bool done()
* @method int count()
* @method bool wait(int|float $timeout = -1)
*/
class WaitGroup
{
/**
* @var class-string<CoroutineInterface>
*/
protected static string $driverClass;
/**
* @var WaitGroupInterface
*/
protected WaitGroupInterface $driver;
/**
* 构造方法
*/
public function __construct()
{
$this->driver = new (self::driverClass());
}
/**
* Get driver class.
*
* @return class-string<CoroutineInterface>
*/
protected static function driverClass(): string
{
return static::$driverClass ??= match (Worker::$eventLoopClass ?? null) {
Swoole::class => SwooleWaitGroup::class,
Swow::class => SwowWaitGroup::class,
default => FiberWaitGroup::class,
};
}
/**
* 代理调用WaitGroupInterface方法
*
* @codeCoverageIgnore 系统魔术方法,忽略覆盖
* @param string $name
* @param array $arguments
* @return mixed
*/
public function __call(string $name, array $arguments): mixed
{
if (!method_exists($this->driver, $name)) {
throw new BadMethodCallException("Method $name not exists. ");
}
return $this->driver->$name(...$arguments);
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* @author workbunny/Chaz6chez
* @email chaz6chez1993@outlook.com
*/
declare(strict_types=1);
namespace Workerman\Coroutine\WaitGroup;
use Workerman\Coroutine\Channel\Fiber as Channel;
class Fiber implements WaitGroupInterface
{
/** @var int */
protected int $count;
/**
* @var Channel
*/
protected Channel $channel;
public function __construct()
{
$this->count = 0;
$this->channel = new Channel(1);
}
/** @inheritdoc */
public function add(int $delta = 1): bool
{
$this->count += max($delta, 1);
return true;
}
/** @inheritdoc */
public function done(): bool
{
$this->count--;
if ($this->count <= 0) {
$this->channel->push(true);
}
return true;
}
/** @inheritdoc */
public function count(): int
{
return $this->count;
}
/** @inheritdoc */
public function wait(int|float $timeout = -1): bool
{
if ($this->count() > 0) {
return $this->channel->pop($timeout);
}
return true;
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
/**
* @author workbunny/Chaz6chez
* @email chaz6chez1993@outlook.com
*/
declare(strict_types=1);
namespace Workerman\Coroutine\WaitGroup;
use Swoole\Coroutine\WaitGroup;
use Throwable;
/**
* Class Swoole
*/
class Swoole implements WaitGroupInterface
{
/** @var WaitGroup */
protected WaitGroup $waitGroup;
public function __construct()
{
$this->waitGroup = new WaitGroup();
}
/** @inheritdoc */
public function add(int $delta = 1): bool
{
$this->waitGroup->add(max($delta, 1));
return true;
}
/** @inheritdoc */
public function done(): bool
{
if ($this->count() > 0) {
$this->waitGroup->done();
}
return true;
}
/** @inheritdoc */
public function count(): int
{
return $this->waitGroup->count();
}
/** @inheritdoc */
public function wait(int|float $timeout = -1): bool
{
try {
$this->waitGroup->wait(max($timeout, $timeout > 0 ? 0.001 : -1));
return true;
} catch (Throwable) {
return false;
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* @author workbunny/Chaz6chez
* @email chaz6chez1993@outlook.com
*/
declare(strict_types=1);
namespace Workerman\Coroutine\WaitGroup;
use Swow\Sync\WaitGroup;
use Throwable;
class Swow implements WaitGroupInterface
{
/** @var WaitGroup */
protected WaitGroup $waitGroup;
/** @var int count */
protected int $count;
public function __construct()
{
$this->waitGroup = new WaitGroup();
$this->count = 0;
}
/** @inheritdoc */
public function add(int $delta = 1): bool
{
$this->waitGroup->add($delta = max($delta, 1));
$this->count += $delta;
return true;
}
/** @inheritdoc */
public function done(): bool
{
if ($this->count() > 0) {
$this->count--;
$this->waitGroup->done();
}
return true;
}
/** @inheritdoc */
public function count(): int
{
return $this->count;
}
/** @inheritdoc */
public function wait(int|float $timeout = -1): bool
{
try {
$this->waitGroup->wait($timeout > 0 ? (int) ($timeout * 1000) : $timeout);
return true;
} catch (Throwable) {
return false;
}
}
}
@@ -0,0 +1,42 @@
<?php
/**
* @author workbunny/Chaz6chez
* @email chaz6chez1993@outlook.com
*/
declare(strict_types=1);
namespace Workerman\Coroutine\WaitGroup;
interface WaitGroupInterface
{
/**
* Increment count
*
* @param int $delta
* @return bool
*/
public function add(int $delta = 1): bool;
/**
* Complete count
*
* @return bool
*/
public function done(): bool;
/**
* Return count
*
* @return int
*/
public function count(): int;
/**
* Wait
*
* @param int|float $timeout second
* @return bool timeout:false success:true
*/
public function wait(int|float $timeout = -1): bool;
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace Swow;
class Coroutine
{
public function resume(mixed ...$args): mixed
{
// Stub for PHPStorm
return null;
}
public static function getCurrent(): static
{
// Stub for PHPStorm
return new Coroutine;
}
public static function yield (mixed ...$args) : mixed
{
// Stub for PHPStorm
return null;
}
public function getId() : int
{
// Stub for PHPStorm
return 0;
}
public static function run(callable $callable , mixed ... $args): static
{
// Stub for PHPStorm
return new Coroutine;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine\Barrier;
use Workerman\Coroutine;
use Workerman\Timer;
/**
* Class FiberBarrierTest
*
* Tests for the Fiber Barrier implementation.
*/
class BarrierTest extends TestCase
{
/**
* Test that the barrier is set to null after calling wait.
*/
public function testWaitSetsBarrierToNull()
{
$barrier = Barrier::create();
$results = [0];
Coroutine::create(function () use ($barrier, &$results) {
Timer::sleep(0.1);
$results[] = 1;
});
Coroutine::create(function () use ($barrier, &$results) {
Timer::sleep(0.2);
$results[] = 2;
});
Coroutine::create(function () use ($barrier, &$results) {
Timer::sleep(0.3);
$results[] = 3;
});
Barrier::wait($barrier);
$this->assertNull($barrier, 'Barrier should be null after wait is called.');
$this->assertEquals([0, 1, 2, 3], $results, 'All coroutines should have been executed.');
}
}
+313
View File
@@ -0,0 +1,313 @@
<?php
declare(strict_types=1);
namespace tests;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionException;
use stdClass;
use Workerman\Coroutine\Channel;
use PHPUnit\Framework\Attributes\DataProvider;
use Workerman\Coroutine\Channel\Memory;
use Workerman\Coroutine;
class ChannelTest extends TestCase
{
/**
* Test initializing channel with valid capacity.
*/
public function testInitializeWithValidCapacity()
{
$channel = new Channel(1);
$this->assertInstanceOf(Channel::class, $channel);
$this->assertEquals(1, $channel->getCapacity());
}
/**
* Test initializing channel with invalid capacities.
*/
#[DataProvider('invalidCapacitiesProvider')]
public function testInitializeWithInvalidCapacity($capacity)
{
$this->expectException(InvalidArgumentException::class);
new Channel($capacity);
}
/**
* Data provider for invalid capacities.
*/
public static function invalidCapacitiesProvider(): array
{
return [
[0],
[-1],
[-100]
];
}
/**
* Test pushing and popping data.
*/
public function testPushAndPop()
{
$channel = new Channel(2);
$data1 = 'test data 1';
$data2 = 'test data 2';
// Push data into the channel
$this->assertTrue($channel->push($data1));
$this->assertTrue($channel->push($data2));
// Verify the length of the channel
$this->assertEquals(2, $channel->length());
// Pop data from the channel
$this->assertEquals($data1, $channel->pop());
$this->assertEquals($data2, $channel->pop());
}
/**
* Test pushing data when the channel is full.
* @throws ReflectionException
*/
public function testPushWhenFull()
{
// Memory driver does not support push with timeout
if ($this->driverIsMemory()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
$this->assertTrue($channel->push('data1'));
$timeout = 0.5;
// Attempt to push when the channel is full with a timeout
$startTime = microtime(true);
$this->assertFalse($channel->push('data2', $timeout));
$elapsedTime = microtime(true) - $startTime;
// Verify that the push operation timed out
$this->assertTrue(0.1 > abs($elapsedTime - $timeout));
}
/**
* Test popping data when the channel is empty.
* @throws ReflectionException
*/
public function testPopWhenEmpty()
{
// Memory driver does not support push with timeout
if ($this->driverIsMemory()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
// Attempt to pop when the channel is empty with a timeout
$startTime = microtime(true);
$this->assertFalse($channel->pop(0.1));
$elapsedTime = microtime(true) - $startTime;
// Verify that the pop operation timed out
$this->assertGreaterThanOrEqual(0.09, $elapsedTime);
}
/**
* Test closing the channel and its effects.
*/
public function testCloseChannel()
{
$channel = new Channel(1);
$this->assertTrue($channel->push('data'));
// Close the channel
$channel->close();
// Attempt to push after closing
$this->assertFalse($channel->push('new data'));
// Pop the remaining data
$this->assertEquals('data', $channel->pop());
// Attempt to pop after channel is empty and closed
$this->assertFalse($channel->pop());
}
/**
* Test that push and pop return false when channel is closed.
*/
public function testPushAndPopReturnFalseWhenClosed()
{
$channel = new Channel(1);
$channel->close();
$this->assertFalse($channel->push('data'));
$this->assertFalse($channel->pop());
}
/**
* Test the length and capacity methods.
*/
public function testLengthAndCapacity()
{
$channel = new Channel(5);
$this->assertEquals(0, $channel->length());
$this->assertEquals(5, $channel->getCapacity());
$channel->push('data1');
$channel->push('data2');
$this->assertEquals(2, $channel->length());
}
/**
* Test pushing and popping with different data types.
*/
#[DataProvider('dataTypesProvider')]
public function testPushAndPopWithDifferentDataTypes($data)
{
$channel = new Channel(1);
$this->assertTrue($channel->push($data));
$this->assertSame($data, $channel->pop());
}
/**
* Data provider for different data types.
*/
public static function dataTypesProvider(): array
{
return [
['string'],
[123],
[123.456],
[true],
[false],
[null],
[[]],
[['key' => 'value']],
[new stdClass()],
[fopen('php://memory', 'r')],
];
}
/**
* Test pushing to a closed channel immediately returns false.
*/
public function testPushToClosedChannel()
{
$channel = new Channel(1);
$channel->close();
$this->assertFalse($channel->push('data', 0));
}
/**
* Test popping from a closed and empty channel immediately returns false.
*/
public function testPopFromClosedAndEmptyChannel()
{
$channel = new Channel(1);
$channel->close();
$this->assertFalse($channel->pop(0));
}
/**
* @return bool
* @throws ReflectionException
*/
protected function driverIsMemory(): bool
{
$reflectionClass = new ReflectionClass(Channel::class);
$instance = $reflectionClass->newInstance();
$property = $reflectionClass->getProperty('driver');
$driverValue = $property->getValue($instance);
return $driverValue instanceof Memory;
}
/**
* 测试 hasConsumers 当没有消费者时返回 false
*/
public function testHasConsumersWhenNoConsumers()
{
if (!Coroutine::isCoroutine()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
$this->assertFalse($channel->hasConsumers());
$channel->close();
}
/**
* 测试 hasConsumers 当有消费者等待时返回 true
* @throws ReflectionException
*/
public function testHasConsumersWhenConsumersWaiting()
{
if ($this->driverIsMemory()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
$sync = new Channel(1);
Coroutine::create(function () use ($channel, $sync) {
$sync->push(true);
$channel->pop();
});
$sync->pop();
$this->assertTrue($channel->hasConsumers());
Coroutine::create(function () use ($channel) {
$channel->push('data');
});
$channel->close();
}
/**
* 测试 hasProducers 当没有生产者时返回 false
* @throws ReflectionException
*/
public function testHasProducersWhenNoProducers()
{
if ($this->driverIsMemory()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
$this->assertFalse($channel->hasProducers());
$channel->close();
}
/**
* 测试 hasProducers 当有生产者等待时返回 true
* @throws ReflectionException
*/
public function testHasProducersWhenProducersWaiting()
{
if ($this->driverIsMemory()) {
$this->assertTrue(true);
return;
}
$channel = new Channel(1);
$channel->push('data1');
$sync = new Channel(1);
Coroutine::create(function () use ($channel, $sync) {
$sync->push(true);
$channel->push('data2');
});
$sync->pop();
$this->assertTrue($channel->hasProducers());
$channel->pop();
$channel->close();
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
namespace tests;
use ArrayObject;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine\Context;
use Workerman\Coroutine;
// Now, the test cases
class ContextTest extends TestCase
{
public function testContextSetAndGetWithinCoroutine()
{
Coroutine::create(function () {
$key = 'testContextSetAndGetWithinCoroutine';
Context::set($key, 'value');
$this->assertEquals('value', Context::get($key));
});
}
public function testContextGet()
{
Context::reset(new ArrayObject(['not_exist' => 'value']));
$key = 'testContextGet';
Context::reset(new ArrayObject([$key => 'value']));
$context = Context::get();
$this->assertArrayNotHasKey('not_exist', $context);
$this->assertObjectNotHasProperty('not_exist', $context);
$this->assertArrayHasKey($key, $context);
$this->assertObjectHasProperty($key, $context);
$this->assertEquals('value', $context[$key]);
$this->assertEquals('value', $context->$key);
$this->assertInstanceOf('ArrayObject', $context);
unset($context[$key]);
$this->assertNull(Context::get($key));
$context[$key] = 'value';
$this->assertEquals('value', Context::get($key));
unset($context->$key);
$this->assertNull(Context::get($key));
$context->$key = 'value';
$this->assertEquals('value', Context::get($key));
}
public function testContextIsolationBetweenCoroutines()
{
$values = [];
Coroutine::create(function () use (&$values) {
Context::set('key', 'value1');
$values[] = Context::get('key');
// Ensure the value is not available after coroutine ends
Context::destroy();
});
Coroutine::create(function () use (&$values) {
Context::set('key', 'value2');
$values[] = Context::get('key');
// Ensure the value is not available after coroutine ends
Context::destroy();
});
$this->assertEquals(['value1', 'value2'], $values);
}
public function testContextDestroyedAfterCoroutineEnds()
{
Coroutine::create(function () {
Context::set('key', 'value');
$this->assertTrue(Context::has('key'));
// Simulate coroutine end and context destruction
Context::destroy();
});
// After coroutine ends, the context should be destroyed
// Need to simulate this by trying to access context outside coroutine
$this->assertNull(Context::get('key'));
$this->assertFalse(Context::has('key'));
}
public function testContextHasMethod()
{
Coroutine::create(function () {
$this->assertFalse(Context::has('key'));
Context::set('key', 'value');
$this->assertTrue(Context::has('key'));
});
}
public function testContextResetMethod()
{
Coroutine::create(function () {
Context::reset(new ArrayObject(['key3' => 'value1']));
Context::reset(new ArrayObject(['key1' => 'value1', 'key2' => 'value2']));
$this->assertEquals('value1', Context::get('key1'));
$this->assertEquals('value2', Context::get('key2'));
// Test that other keys are not set
$this->assertNull(Context::get('key3'));
});
}
public function testContextDataNotSharedBetweenCoroutines()
{
$result = [];
Coroutine::create(function () use (&$result) {
Context::set('counter', 1);
$result[] = Context::get('counter');
Context::destroy();
});
Coroutine::create(function () use (&$result) {
$this->assertNull(Context::get('counter'));
Context::set('counter', 2);
$result[] = Context::get('counter');
Context::destroy();
});
$this->assertEquals([1, 2], $result);
}
public function testContextDefaultValues()
{
Coroutine::create(function () {
$this->assertEquals('default', Context::get('non_existing_key', 'default'));
});
}
public function testContextSetOverrideValue()
{
Coroutine::create(function () {
Context::set('key', 'initial');
$this->assertEquals('initial', Context::get('key'));
Context::set('key', 'overridden');
$this->assertEquals('overridden', Context::get('key'));
});
}
public function testContextMultipleKeys()
{
Coroutine::create(function () {
Context::set('key1', 'value1');
Context::set('key2', 'value2');
$this->assertEquals('value1', Context::get('key1'));
$this->assertEquals('value2', Context::get('key2'));
});
}
public function testContextPersistenceWithinCoroutine()
{
Coroutine::create(function () {
Context::set('key', 'value');
// Simulate asynchronous operation within coroutine
$this->someAsyncOperation(function () {
$this->assertEquals('value', Context::get('key'));
});
// Context should persist throughout the coroutine
$this->assertEquals('value', Context::get('key'));
});
}
private function someAsyncOperation(callable $callback)
{
// Simulate async operation
$callback();
}
}
+184
View File
@@ -0,0 +1,184 @@
<?php
namespace tests;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine;
use Workerman\Coroutine\Coroutine\CoroutineInterface;
use Workerman\Events\Swoole;
use Workerman\Worker;
class CoroutineTest extends TestCase
{
public function testCreateReturnsCoroutineInterface()
{
$callable = function() {};
$coroutine = Coroutine::create($callable);
$this->assertInstanceOf(CoroutineInterface::class, $coroutine);
}
public function testStartExecutesCoroutine()
{
$value = null;
Coroutine::create(function() use (&$value) {
$value = 'started';
});
$this->assertEquals('started', $value);
}
public function testSuspendAndResumeCoroutine()
{
if (Worker::$eventLoopClass === Swoole::class) {
// Swoole does not support suspend and resume
$this->assertTrue(true);
return;
}
$value = [];
$coroutine = Coroutine::create(function() use (&$value) {
$value[] = 'before suspend';
$resumedValue = Coroutine::suspend();
$value[] = 'after resume';
$value[] = $resumedValue;
});
$this->assertEquals(['before suspend'], $value);
$coroutine->resume('resumed data');
unset($coroutine);
gc_collect_cycles();
$this->assertEquals(['before suspend', 'after resume', 'resumed data'], $value);
}
public function testGetCurrentReturnsCurrentCoroutine()
{
$currentCoroutine = null;
$coroutine = Coroutine::create(function() use (&$currentCoroutine) {
$currentCoroutine = Coroutine::getCurrent();
});
$this->assertSame($coroutine, $currentCoroutine);
}
public function testCoroutineIdIsInteger()
{
$coroutine = Coroutine::create(function() {});
$id = $coroutine->id();
$this->assertIsInt($id);
}
public function testDeferExecutesAfterCoroutineDestruction()
{
$value = [];
$coroutine = Coroutine::create(function() use (&$value) {
Coroutine::defer(function() use (&$value) {
$value[] = 'defer1';
});
Coroutine::defer(function() use (&$value) {
$value[] = 'defer2';
});
$value[] = 'before suspend';
Coroutine::suspend();
$value[] = 'after resume';
});
$this->assertEquals(['before suspend'], $value);
$coroutine->resume();
unset($coroutine);
gc_collect_cycles();
$this->assertEquals(['before suspend', 'after resume', 'defer2', 'defer1'], $value);
}
public function testMultipleCoroutines()
{
$sequence = [];
$coroutine1 = Coroutine::create(function() use (&$sequence) {
$sequence[] = 'coroutine1 start';
Coroutine::suspend();
$sequence[] = 'coroutine1 resumed';
});
$coroutine2 = Coroutine::create(function() use (&$sequence) {
$sequence[] = 'coroutine2 start';
Coroutine::suspend();
$sequence[] = 'coroutine2 resumed';
});
$this->assertEquals(['coroutine1 start', 'coroutine2 start'], $sequence);
$coroutine1->resume();
$coroutine2->resume();
$this->assertEquals(
['coroutine1 start', 'coroutine2 start', 'coroutine1 resumed', 'coroutine2 resumed'],
$sequence
);
}
public function testCoroutineWithArguments()
{
$result = null;
$coroutine = new Coroutine(function($a, $b) use (&$result) {
$result = $a + $b;
});
$coroutine->start(2, 3);
$this->assertEquals(5, $result);
}
public function testSuspendReturnsValue()
{
if (Worker::$eventLoopClass === Swoole::class) {
// Swoole does not support suspend and resume
$this->assertTrue(true);
return;
}
$coroutine = new Coroutine(function() {
$valueFromResume = Coroutine::suspend('first suspend');
Coroutine::suspend($valueFromResume);
});
$first_suspend = $coroutine->start();
$this->assertEquals('first suspend', $first_suspend);
$result = $coroutine->resume('value from resume');
$this->assertEquals('value from resume', $result);
}
public function testNestedCoroutines()
{
$sequence = [];
$coroutine = Coroutine::create(function() use (&$sequence) {
$sequence[] = 'outer start';
$inner = Coroutine::create(function() use (&$sequence) {
$sequence[] = 'inner start';
Coroutine::suspend();
$sequence[] = 'inner resumed';
});
Coroutine::suspend();
$sequence[] = 'outer resumed';
$inner->resume();
$sequence[] = 'outer end';
});
$this->assertEquals(['outer start', 'inner start'], $sequence);
$coroutine->resume();
$this->assertEquals(['outer start', 'inner start', 'outer resumed', 'inner resumed', 'outer end'], $sequence);
}
/*public function testCoroutineExceptionHandling()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Test exception');
Coroutine::create(function() {
throw new \Exception('Test exception');
});
}*/
public function testDeferOrder()
{
$value = [];
$coroutine = Coroutine::create(function() use (&$value) {
Coroutine::defer(function() use (&$value) {
$value[] = 'defer1';
});
Coroutine::defer(function() use (&$value) {
$value[] = 'defer2';
});
$value[] = 'coroutine body';
});
unset($coroutine);
// Force garbage collection
gc_collect_cycles();
$this->assertEquals(['coroutine body', 'defer2', 'defer1'], $value);
}
}
+346
View File
@@ -0,0 +1,346 @@
<?php
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine\Channel\Fiber as Channel;
use Workerman\Timer;
use Fiber as BaseFiber;
class FiberChannelTest extends TestCase
{
/**
* Test basic push and pop operations.
*/
public function testBasicPushPop()
{
$channel = new Channel();
$fiber = new BaseFiber(function() use ($channel) {
$channel->push('test data');
});
$fiber->start();
$this->assertEquals('test data', $channel->pop());
}
/**
* Test that pop will block until data is available or timeout occurs.
*/
public function testPopWithTimeout()
{
$channel = new Channel();
$fiber = new BaseFiber(function() use ($channel) {
$result = $channel->pop(0.5);
$this->assertFalse($result);
});
$startTime = microtime(true);
$fiber->start();
// Allow time for the fiber to suspend and wait
Timer::sleep(0.2); // 200 ms
// Ensure that the fiber is still waiting (not timed out yet)
$this->assertTrue($fiber->isSuspended());
// Wait until the timeout should have occurred
Timer::sleep(0.4); // 400 ms
$endTime = microtime(true);
$this->assertTrue($fiber->isTerminated());
$this->assertGreaterThanOrEqual(0.5, $endTime - $startTime);
}
/**
* Test that push will block when capacity is reached and timeout occurs.
*/
public function testPushWithTimeout()
{
$channel = new Channel(1);
$this->assertTrue($channel->push('data1'));
$fiber = new BaseFiber(function() use ($channel) {
$result = $channel->push('data2', 0.5);
$this->assertFalse($result);
});
$startTime = microtime(true);
$fiber->start();
// Allow time for the fiber to suspend and wait
Timer::sleep(0.2); // 200 ms
// Ensure that the fiber is still waiting (not timed out yet)
$this->assertTrue($fiber->isSuspended());
// Wait until the timeout should have occurred
Timer::sleep(0.4); // 400 ms
$endTime = microtime(true);
$this->assertTrue($fiber->isTerminated());
$this->assertGreaterThanOrEqual(0.5, $endTime - $startTime);
}
/**
* Test that push returns false immediately if capacity is full and timeout is zero.
*/
public function testPushNonBlockingWhenFull()
{
$channel = new Channel(1);
$this->assertTrue($channel->push('data1'));
$result = $channel->push('data2', 0);
$this->assertFalse($result);
}
/**
* Test that pop returns false immediately if the channel is empty and timeout is zero.
*/
public function testPopNonBlockingWhenEmpty()
{
$channel = new Channel();
$result = $channel->pop(0);
$this->assertFalse($result);
}
/**
* Test closing the channel.
*/
public function testCloseChannel()
{
$channel = new Channel();
$channel->close();
$this->assertFalse($channel->push('data'));
$this->assertFalse($channel->pop());
}
/**
* Test that waiting pushers and poppers are resumed when the channel is closed.
*/
public function testWaitersAreResumedOnClose()
{
$channelPush = new Channel(1);
$channelPop = new Channel(1);
$pushFiber = new BaseFiber(function() use ($channelPush) {
$channelPush->push('data', 1);
$result = $channelPush->push('data', 1);
$this->assertFalse($result);
});
$popFiber = new BaseFiber(function() use ($channelPop) {
$result = $channelPop->pop(1);
$this->assertFalse($result);
});
$pushFiber->start();
$popFiber->start();
// Allow time for fibers to suspend
Timer::sleep(0.1); // 100 ms
// Close the channel to resume fibers
$channelPush->close();
$channelPop->close();
// Allow time for fibers to process after resuming
Timer::sleep(0.1); // 100 ms
$this->assertTrue($pushFiber->isTerminated());
$this->assertTrue($popFiber->isTerminated());
}
/**
* Test that length and getCapacity methods return correct values.
*/
public function testLengthAndCapacity()
{
$capacity = 2;
$channel = new Channel($capacity);
$this->assertEquals(0, $channel->length());
$this->assertEquals($capacity, $channel->getCapacity());
$channel->push('data1');
$this->assertEquals(1, $channel->length());
$channel->push('data2');
$this->assertEquals(2, $channel->length());
$channel->pop();
$this->assertEquals(1, $channel->length());
$channel->pop();
$this->assertEquals(0, $channel->length());
}
/**
* Test pushing to a closed channel.
*/
public function testPushToClosedChannel()
{
$channel = new Channel();
$channel->close();
$result = $channel->push('data');
$this->assertFalse($result);
}
/**
* Test popping from a closed channel.
*/
public function testPopFromClosedChannel()
{
$channel = new Channel();
$channel->push('data');
$channel->close();
$this->assertEquals('data', $channel->pop());
$this->assertFalse($channel->pop());
}
/**
* Test multiple push and pop operations with fibers.
*/
public function testMultiplePushPopWithFibers()
{
$channel = new Channel(2);
$results = [];
$producerFiber = new BaseFiber(function() use ($channel) {
$channel->push('data1');
$channel->push('data2');
$channel->push('data3');
});
$consumerFiber = new BaseFiber(function() use ($channel, &$results) {
$results[] = $channel->pop();
$results[] = $channel->pop();
$results[] = $channel->pop();
});
$producerFiber->start();
$consumerFiber->start();
// Allow time for fibers to execute
usleep(500000); // 500 ms
$this->assertEquals(['data1', 'data2', 'data3'], $results);
}
/**
* Test that fibers are properly blocked and resumed in push and pop operations.
*/
public function testFiberBlockingAndResuming()
{
$channel = new Channel(1);
$pushFiber = new BaseFiber(function() use ($channel) {
$channel->push('data1');
$channel->push('data2');
$channel->push('data3');
});
$popFiber = new BaseFiber(function() use ($channel) {
$this->assertEquals('data1', $channel->pop());
$this->assertEquals('data2', $channel->pop());
$this->assertEquals('data3', $channel->pop());
});
$pushFiber->start();
$popFiber->start();
// Allow time for fibers to execute
Timer::sleep(0.5); // 500 ms
$this->assertTrue($pushFiber->isTerminated());
$this->assertTrue($popFiber->isTerminated());
}
/**
* Test that pushing data after capacity is reached blocks until space is available.
*/
public function testPushBlocksWhenFull()
{
$channel = new Channel(1);
$channel->push('data1');
$pushFiber = new BaseFiber(function() use ($channel) {
$channel->push('data2');
});
$popFiber = new BaseFiber(function() use ($channel) {
Timer::sleep(0.2); // Wait before popping
$this->assertEquals('data1', $channel->pop());
});
$pushFiber->start();
$popFiber->start();
// Allow time for fibers to execute
Timer::sleep(0.5); // 500 ms
$this->assertTrue($pushFiber->isTerminated());
$this->assertTrue($popFiber->isTerminated());
}
/**
* Test that popping data from an empty channel blocks until data is available.
*/
public function testPopBlocksWhenEmpty()
{
$channel = new Channel();
$popFiber = new BaseFiber(function() use ($channel) {
$this->assertEquals('data1', $channel->pop());
});
$pushFiber = new BaseFiber(function() use ($channel) {
Timer::sleep(0.2); // Wait before pushing
$channel->push('data1');
});
$popFiber->start();
$pushFiber->start();
// Allow time for fibers to execute
Timer::sleep(0.5); // 500 ms
$this->assertTrue($pushFiber->isTerminated());
$this->assertTrue($popFiber->isTerminated());
}
/**
* Test pushing and popping with zero timeout.
*/
public function testPushPopWithZeroTimeout()
{
$channel = new Channel(1);
$this->assertTrue($channel->push('data1'));
$result = $channel->push('data2', 0);
$this->assertFalse($result);
$result = $channel->pop(0);
$this->assertEquals('data1', $result);
$result = $channel->pop(0);
$this->assertFalse($result);
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine\Locker;
use RuntimeException;
use Workerman\Coroutine;
use ReflectionClass;
use Workerman\Timer;
class LockerTest extends TestCase
{
public function testLock()
{
$key = 'testLock';
Locker::lock($key);
$timeStart = microtime(true);
$timeDiff2 = 0;
Coroutine::create(function () use ($key, $timeStart, &$timeDiff2) {
$this->assertChannelExists($key);
Locker::lock($key);
$timeDiff = microtime(true) - $timeStart;
$this->assertGreaterThan($timeDiff2, $timeDiff);
Locker::unlock($key);
});
usleep(100000);
$timeDiff2 = microtime(true) - $timeStart;
Locker::unlock($key);
}
public function testLockAndUnlock()
{
$key = 'testLockAndUnlock';
$this->assertTrue(Locker::lock($key));
$this->assertTrue(Locker::unlock($key));
$this->assertChannelRemoved($key);
}
public function testUnlockWithoutLockThrowsException()
{
$this->expectException(RuntimeException::class);
Locker::unlock('non_existent_key');
}
public function testRelockAfterUnlock()
{
$key = 'testRelockAfterUnlock';
Locker::lock($key);
Locker::unlock($key);
$this->assertTrue(Locker::lock($key));
Locker::unlock($key);
$this->assertChannelRemoved($key);
}
public function testMultipleCoroutinesLocking()
{
$key = 'testMultipleCoroutinesLocking';
$results = [];
Coroutine::create(function () use ($key, &$results) {
Coroutine::create(function () use ($key, &$results) {
Locker::lock($key);
$results[] = 'A';
Timer::sleep(0.1);
usleep(100000);
Locker::unlock($key);
});
Coroutine::create(function () use ($key, &$results) {
Timer::sleep(0.05);
Locker::lock($key);
$results[] = 'B';
Locker::unlock($key);
});
Coroutine::create(function () use ($key, &$results) {
Timer::sleep(0.05);
Locker::lock($key);
$results[] = 'C';
Locker::unlock($key);
});
});
Timer::sleep(0.3);
$this->assertEquals(['A', 'B', 'C'], $results);
$this->assertChannelRemoved($key);
}
public function testChannelRemainsWhenWaiting()
{
$key = 'testChannelRemainsWhenWaiting';
Locker::lock($key);
Coroutine::create(function () use ($key) {
Coroutine::create(function () use ($key) {
Locker::lock($key);
Locker::unlock($key);
});
Locker::unlock($key);
$this->assertChannelRemoved($key);
});
}
private function assertChannelExists(string $key): void
{
$channels = $this->getChannels();
$this->assertArrayHasKey($key, $channels, "Channel for key '$key' should exist");
}
private function assertChannelRemoved(string $key): void
{
$channels = $this->getChannels();
$this->assertArrayNotHasKey($key, $channels, "Channel for key '$key' should be removed");
}
private function getChannels(): array
{
$reflector = new ReflectionClass(Locker::class);
$property = $reflector->getProperty('channels');
return $property->getValue();
}
}
+302
View File
@@ -0,0 +1,302 @@
<?php
namespace tests;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine\Parallel;
use Workerman\Coroutine;
use Workerman\Timer;
/**
* Test cases for the Workerman\Coroutine\Parallel class.
*/
class ParallelTest extends TestCase
{
/**
* Test that callables are added and executed, and results are collected properly.
*/
public function testAddAndWait()
{
$parallel = new Parallel();
$parallel->add(function () {
// Simulate some work.
Timer::sleep(0.01);
return 1;
}, 'task1');
$parallel->add(function () {
// Simulate some work.
Timer::sleep(0.005);
return 2;
}, 'task2');
$results = $parallel->wait();
$this->assertEquals(['task1' => 1, 'task2' => 2], $results);
}
/**
* Test that exceptions thrown in callables are caught and can be retrieved.
*/
public function testExceptions()
{
$parallel = new Parallel();
$parallel->add(function () {
throw new \Exception('Test exception');
}, 'task_with_exception');
$parallel->add(function () {
return 'normal result';
}, 'normal_task');
$results = $parallel->wait();
$exceptions = $parallel->getExceptions();
// Check that the normal task result is present.
$this->assertEquals(['normal_task' => 'normal result'], $results);
// Check that the exception is captured for the failing task.
$this->assertArrayHasKey('task_with_exception', $exceptions);
$this->assertInstanceOf(\Exception::class, $exceptions['task_with_exception']);
$this->assertEquals('Test exception', $exceptions['task_with_exception']->getMessage());
}
/**
* Test concurrency control by limiting the number of concurrent tasks.
*/
public function testConcurrencyLimit()
{
$concurrentLimit = 2;
$parallel = new Parallel($concurrentLimit);
$startTimes = [];
$endTimes = [];
for ($i = 0; $i < 5; $i++) {
$parallel->add(function () use (&$startTimes, &$endTimes, $i) {
$startTimes[$i] = microtime(true);
// Simulate some work.
Timer::sleep(0.1); // 100 milliseconds
$endTimes[$i] = microtime(true);
return $i;
}, "task{$i}");
}
$parallel->wait();
// Since we limited concurrency to 2, tasks should finish in batches.
// We'll check that at no point more than $concurrentLimit tasks were running simultaneously.
// Collect start and end times into an array of intervals.
$intervals = [];
for ($i = 0; $i < 5; $i++) {
$intervals[] = ['start' => $startTimes[$i], 'end' => $endTimes[$i]];
}
// Check the maximum number of overlapping intervals does not exceed the concurrency limit.
$maxConcurrent = $this->getMaxConcurrentIntervals($intervals);
$this->assertLessThanOrEqual($concurrentLimit, $maxConcurrent);
}
/**
* Helper function to determine the maximum number of overlapping intervals.
*
* @param array $intervals
* @return int
*/
private function getMaxConcurrentIntervals(array $intervals)
{
$events = [];
foreach ($intervals as $interval) {
$events[] = ['time' => $interval['start'], 'type' => 'start'];
$events[] = ['time' => $interval['end'], 'type' => 'end'];
}
// Sort events by time, 'start' before 'end' if times are equal.
usort($events, function ($a, $b) {
if ($a['time'] == $b['time']) {
return $a['type'] === 'start' ? -1 : 1;
}
return $a['time'] < $b['time'] ? -1 : 1;
});
$maxConcurrent = 0;
$currentConcurrent = 0;
foreach ($events as $event) {
if ($event['type'] === 'start') {
$currentConcurrent++;
if ($currentConcurrent > $maxConcurrent) {
$maxConcurrent = $currentConcurrent;
}
} else {
$currentConcurrent--;
}
}
return $maxConcurrent;
}
/**
* Test that callables are executed in parallel when no concurrency limit is set.
*/
public function testParallelExecutionWithoutConcurrencyLimit()
{
$parallel = new Parallel();
$startTimes = [];
$endTimes = [];
$parallel->add(function () use (&$startTimes, &$endTimes) {
$startTimes[] = microtime(true);
Timer::sleep(0.1); // 100 milliseconds
$endTimes[] = microtime(true);
return 'task1';
}, 'task1');
$parallel->add(function () use (&$startTimes, &$endTimes) {
$startTimes[] = microtime(true);
Timer::sleep(0.1);// 100 milliseconds
$endTimes[] = microtime(true);
return 'task2';
}, 'task2');
$parallel->wait();
// Calculate total elapsed time.
$totalTime = max($endTimes) - min($startTimes);
// The total time should be approximately the duration of one task, not the sum of both.
$this->assertLessThan(0.2, $totalTime);
}
/**
* Test adding callables without specifying keys and ensure results are correctly indexed.
*/
public function testAddWithoutKeys()
{
$parallel = new Parallel();
$parallel->add(function () {
return 'result1';
});
$parallel->add(function () {
return 'result2';
});
$results = $parallel->wait();
// Since no keys were specified, indices should be 0 and 1.
$this->assertEquals(['result1', 'result2'], $results);
}
/**
* Test that the Parallel class can handle a large number of tasks.
*/
public function testLargeNumberOfTasks()
{
$parallel = new Parallel();
$taskCount = 100;
for ($i = 0; $i < $taskCount; $i++) {
$parallel->add(function () use ($i) {
return $i * $i;
}, "task{$i}");
}
$results = $parallel->wait();
// Verify that all tasks have been completed and results are correct.
for ($i = 0; $i < $taskCount; $i++) {
$this->assertEquals($i * $i, $results["task{$i}"]);
}
}
/**
* Test that adding a non-callable throws a TypeError.
*/
public function testAddNonCallable()
{
$this->expectException(\TypeError::class);
$parallel = new Parallel();
$parallel->add('not a callable');
}
/**
* Test that the wait method can be called multiple times safely.
*/
public function testMultipleWaitCalls()
{
$parallel = new Parallel();
$parallel->add(function () {
return 'first call';
}, 'task1');
$resultsFirst = $parallel->wait();
$this->assertEquals(['task1' => 'first call'], $resultsFirst);
// Add another task after first wait.
$parallel->add(function () {
return 'second call';
}, 'task2');
$resultsSecond = $parallel->wait();
// Since the callbacks array is not cleared after wait, results should include both tasks.
$this->assertEquals(['task1' => 'first call', 'task2' => 'second call'], $resultsSecond);
}
/**
* Test that the class properly handles empty tasks (no callables added).
*/
public function testNoTasks()
{
$parallel = new Parallel();
$results = $parallel->wait();
$this->assertEmpty($results);
}
/**
* Test that the class handles tasks that return null.
*/
public function testTasksReturningNull()
{
$parallel = new Parallel();
$parallel->add(function () {
// No return statement, implicitly returns null.
}, 'nullTask');
$results = $parallel->wait();
$this->assertArrayHasKey('nullTask', $results);
$this->assertNull($results['nullTask']);
}
/**
* Test defer can be used in tasks.
*/
public function testWithDefer()
{
$parallel = new Parallel();
$results = [];
$parallel->add(function () use (&$results) {
Coroutine::defer(function () use (&$results) {
$results[] = 'defer1';
});
});
$parallel->wait();
$this->assertEquals(['defer1'], $results);
}
}
+398
View File
@@ -0,0 +1,398 @@
<?php
namespace test;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use Workerman\Coroutine;
use Workerman\Coroutine\Pool;
use Psr\Log\LoggerInterface;
use ReflectionClass;
use stdClass;
use RuntimeException;
use Exception;
use Workerman\Events\Event;
use Workerman\Events\Select;
use Workerman\Timer;
use Workerman\Worker;
class PoolTest extends TestCase
{
public function testConstructorWithConfig()
{
$config = [
'min_connections' => 2,
'idle_timeout' => 30,
'heartbeat_interval' => 10,
'wait_timeout' => 5,
];
$pool = new Pool(10, $config);
$this->assertEquals(10, $this->getPrivateProperty($pool, 'maxConnections'));
$this->assertEquals(2, $this->getPrivateProperty($pool, 'minConnections'));
$this->assertEquals(30, $this->getPrivateProperty($pool, 'idleTimeout'));
$this->assertEquals(10, $this->getPrivateProperty($pool, 'heartbeatInterval'));
$this->assertEquals(5, $this->getPrivateProperty($pool, 'waitTimeout'));
}
public function testSetConnectionCreator()
{
$pool = new Pool(5);
$connectionCreator = function () {
return new stdClass();
};
$pool->setConnectionCreator($connectionCreator);
$this->assertSame($connectionCreator, $this->getPrivateProperty($pool, 'connectionCreateHandler'));
}
public function testSetConnectionCloser()
{
$pool = new Pool(5);
$connectionCloser = function ($conn) {
// Close connection.
};
$pool->setConnectionCloser($connectionCloser);
$this->assertSame($connectionCloser, $this->getPrivateProperty($pool, 'connectionDestroyHandler'));
}
public function testGetConnection()
{
$pool = new Pool(5);
$connectionMock = $this->createMock(stdClass::class);
// 设置连接创建器
$pool->setConnectionCreator(function () use ($connectionMock) {
return $connectionMock;
});
$connection = $pool->get();
$this->assertSame($connectionMock, $connection);
$this->assertEquals(1, $this->getCurrentConnections($pool));
// 检查 WeakMap 是否更新
$connections = $this->getPrivateProperty($pool, 'connections');
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
$this->assertTrue($connections->offsetExists($connection));
$this->assertTrue($lastUsedTimes->offsetExists($connection));
$this->assertTrue($lastHeartbeatTimes->offsetExists($connection));
}
public function testPutConnection()
{
$pool = new Pool(5);
$connectionMock = $this->createMock(stdClass::class);
$pool->setConnectionCreator(function () use ($connectionMock) {
return $connectionMock;
});
$connection = $pool->get();
$pool->put($connection);
if (Coroutine::isCoroutine()) {
$channel = $this->getPrivateProperty($pool, 'channel');
$this->assertEquals(1, $channel->length());
}
$this->assertEquals(1, $pool->getConnectionCount());
}
public function testPutConnectionDoesNotBelong()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('The connection does not belong to the connection pool.');
$pool = new Pool(5);
$connection = new stdClass();
$pool->put($connection);
}
public function testCreateConnection()
{
$pool = new Pool(5);
$connectionMock = $this->createMock(stdClass::class);
$pool->setConnectionCreator(function () use ($connectionMock) {
return $connectionMock;
});
$connection = $pool->createConnection();
$this->assertSame($connectionMock, $connection);
// 确保 currentConnections 增加
$this->assertEquals(1, $this->getCurrentConnections($pool));
// 检查 WeakMap 是否更新
$connections = $this->getPrivateProperty($pool, 'connections');
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
$this->assertTrue($connections->offsetExists($connection));
$this->assertTrue($lastUsedTimes->offsetExists($connection));
$this->assertTrue($lastHeartbeatTimes->offsetExists($connection));
}
public function testCreateMaxConnections()
{
if (in_array(Worker::$eventLoopClass, [Select::class, Event::class])) {
$this->assertTrue(true);
return;
}
$maxConnections = 2;
$pool = new Pool($maxConnections);
$pool->setConnectionCreator(function () {
Timer::sleep(0.01);
return $this->createMock(stdClass::class);
});
$connections = [];
for ($i = 0; $i < 3; $i++) {
Coroutine::create(function () use ($pool, &$connections) {
$connections[] = $pool->get();
});
}
Timer::sleep(0.1);
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
$this->assertCount($maxConnections, $lastUsedTimes);
$this->assertCount($maxConnections, $lastHeartbeatTimes);
foreach ($connections as $connection) {
$pool->put($connection);
}
}
public function testCreateConnectionThrowsException()
{
$pool = new Pool(5);
$pool->setConnectionCreator(function () {
throw new Exception('Failed to create connection');
});
$this->expectException(Exception::class);
$this->expectExceptionMessage('Failed to create connection');
try {
$pool->createConnection();
} finally {
// 确保 currentConnections 减少
$this->assertEquals(0, $this->getCurrentConnections($pool));
}
}
public function testCloseConnection()
{
$pool = new Pool(5);
$connection = $this->createMock(ConnectionMock::class);
// 模拟连接属于连接池
$connections = $this->getPrivateProperty($pool, 'connections');
$connections[$connection] = time();
$connection->expects($this->once())->method('close');
$pool->setConnectionCloser(function ($conn) {
$conn->close();
});
$pool->closeConnection($connection);
// 确保 currentConnections 减少
$this->assertEquals(0, $this->getCurrentConnections($pool));
// 确保连接从 WeakMap 中移除
$this->assertFalse($connections->offsetExists($connection));
}
public function testCloseConnections()
{
$maxConnections = 5;
$pool = new Pool($maxConnections);
$pool->setConnectionCreator(function () {
$connection = $this->createMock(ConnectionMock::class);
$connection->expects($this->once())->method('close');
return $connection;
});
$pool->setConnectionCloser(function ($conn) {
$conn->close();
});
$connections = [];
for ($i = 0; $i < $maxConnections; $i++) {
$connections[] = $pool->get();
}
$this->assertEquals(Coroutine::isCoroutine() ? $maxConnections : 1, $this->getCurrentConnections($pool));
$pool->closeConnections();
$this->assertEquals(Coroutine::isCoroutine() ? $maxConnections : 0, $this->getCurrentConnections($pool));
if (!Coroutine::isCoroutine()) {
return;
}
foreach ($connections as $connection) {
$pool->put($connection);
}
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
$pool->closeConnections();
$this->assertEquals(0, $this->getCurrentConnections($pool));
$connections = [];
for ($i = 0; $i < $maxConnections; $i++) {
$connections[] = $pool->get();
}
$this->assertEquals($maxConnections, $this->getCurrentConnections($pool));
foreach ($connections as $connection) {
$pool->put($connection);
}
$pool->closeConnections();
unset($connections);
$this->assertEquals(0, $this->getCurrentConnections($pool));
}
public function testCloseConnectionWithExceptionInDestroyHandler()
{
$pool = new Pool(5);
$connection = $this->createMock(stdClass::class);
// 模拟连接属于连接池
$connections = $this->getPrivateProperty($pool, 'connections');
$connections[$connection] = time();
$exception = new Exception('Error closing connection');
$pool->setConnectionCloser(function ($conn) use ($exception) {
throw $exception;
});
// 设置日志记录器
$loggerMock = $this->createMock(LoggerInterface::class);
$loggerMock->expects($this->once())
->method('info')
->with($this->stringContains('Error closing connection'));
$this->setPrivateProperty($pool, 'logger', $loggerMock);
$pool->closeConnection($connection);
// 确保 currentConnections 减少
$this->assertEquals(0, $this->getCurrentConnections($pool));
// 确保连接从 WeakMap 中移除
$this->assertFalse($connections->offsetExists($connection));
}
public function testHeartbeatChecker()
{
$pool = $this->getMockBuilder(Pool::class)
->setConstructorArgs([5])
->onlyMethods(['closeConnection'])
->getMock();
$connection = $this->createMock(stdClass::class);
// 设置连接心跳检测器
$pool->setHeartbeatChecker(function ($conn) {
// 模拟心跳检测
});
// 模拟连接在通道中
$channel = $this->getPrivateProperty($pool, 'channel');
$channel->push($connection);
// 设置连接的上次使用时间和心跳时间
$connections = $this->getPrivateProperty($pool, 'connections');
$connections[$connection] = time();
$lastUsedTimes = $this->getPrivateProperty($pool, 'lastUsedTimes');
$lastUsedTimes[$connection] = time();
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
$lastHeartbeatTimes[$connection] = time() - 100; // 超过心跳间隔
// 调用受保护的 checkConnections 方法
$reflectedMethod = new ReflectionMethod($pool, 'checkConnections');
$reflectedMethod->setAccessible(true);
$reflectedMethod->invoke($pool);
// 检查心跳时间是否更新
$lastHeartbeatTimes = $this->getPrivateProperty($pool, 'lastHeartbeatTimes');
$this->assertGreaterThan(time() - 2, $lastHeartbeatTimes[$connection]);
}
public function testConnectionDestroyedWithoutReturn()
{
$pool = new Pool(5);
// 设置连接创建器
$pool->setConnectionCreator(function () {
return new stdClass;
});
// 获取初始的 currentConnections
$initialConnections = $this->getCurrentConnections($pool);
// 从连接池获取一个连接
$connection = $pool->get();
// 检查 currentConnections 是否增加
$this->assertEquals(Coroutine::isCoroutine() ? $initialConnections + 1 : 1, $this->getCurrentConnections($pool));
// 不归还连接,并销毁连接对象
unset($connection);
// 检查 currentConnections 是否减少
$this->assertEquals(Coroutine::isCoroutine() ? $initialConnections : 1, $this->getCurrentConnections($pool));
}
private function getPrivateProperty($object, string $property)
{
$reflection = new ReflectionClass($object);
$prop = $reflection->getProperty($property);
$prop->setAccessible(true);
return $prop->getValue($object);
}
private function setPrivateProperty($object, string $property, $value)
{
$reflection = new ReflectionClass($object);
$prop = $reflection->getProperty($property);
$prop->setAccessible(true);
$prop->setValue($object, $value);
}
private function getCurrentConnections($object): int
{
return $object->getConnectionCount();
}
}
// 定义 ConnectionMock 类用于测试
class ConnectionMock
{
public function close()
{
// 模拟关闭连接
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace tests;
use PHPUnit\Framework\TestCase;
use Workerman\Coroutine;
use Workerman\Timer;
use Workerman\Coroutine\WaitGroup;
/**
* Class WaitGroupTest
*
* Tests for the Fiber WaitGroup implementation.
*/
class WaitGroupTest extends TestCase
{
public function testWaitWaitGroupDone()
{
$waitGroup = new WaitGroup();
$this->assertEquals(0, $waitGroup->count());
$results = [0];
$this->assertTrue($waitGroup->add());
Coroutine::create(function () use ($waitGroup, &$results) {
try {
Timer::sleep(0.1);
$results[] = 1;
} finally {
$this->assertTrue($waitGroup->done());
}
});
$this->assertTrue($waitGroup->add());
Coroutine::create(function () use ($waitGroup, &$results) {
try {
Timer::sleep(0.2);
$results[] = 2;
} finally {
$this->assertTrue($waitGroup->done());
}
});
$this->assertTrue($waitGroup->add());
Coroutine::create(function () use ($waitGroup, &$results) {
try {
Timer::sleep(0.3);
$results[] = 3;
} finally {
$this->assertTrue($waitGroup->done());
}
});
$this->assertTrue($waitGroup->wait());
$this->assertEquals(0, $waitGroup->count(), 'WaitGroup count should be 0 after wait is called.');
$this->assertEquals([0, 1, 2, 3], $results, 'All coroutines should have been executed.');
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
use Workerman\Events\Event;
use Workerman\Events\Select;
use Workerman\Events\Swow;
use Workerman\Events\Swoole;
use Workerman\Events\Fiber;
use Workerman\Timer;
use Workerman\Worker;
require_once __DIR__ . '/../vendor/autoload.php';
if (DIRECTORY_SEPARATOR === '/' || (!extension_loaded('swow') && !class_exists(Revolt\EventLoop::class))) {
create_test_worker(function () {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
'--colors=always',
__DIR__ . '/ChannelTest.php',
__DIR__ . '/PoolTest.php',
__DIR__ . '/BarrierTest.php',
__DIR__ . '/ContextTest.php',
__DIR__ . '/WaitGroupTest.php',
]);
}, Select::class);
}
if (extension_loaded('event')) {
create_test_worker(function () {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
'--colors=always',
__DIR__ . '/ChannelTest.php',
__DIR__ . '/PoolTest.php',
__DIR__ . '/BarrierTest.php',
__DIR__ . '/ContextTest.php',
__DIR__ . '/WaitGroupTest.php',
]);
}, Event::class);
}
if (class_exists(Revolt\EventLoop::class) && (DIRECTORY_SEPARATOR === '/' || !extension_loaded('swow'))) {
create_test_worker(function () {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
'--colors=always',
...glob(__DIR__ . '/*Test.php')
]);
}, Fiber::class);
}
if (extension_loaded('Swoole')) {
create_test_worker(function () {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
'--colors=always',
...glob(__DIR__ . '/*Test.php')
]);
}, Swoole::class);
}
if (extension_loaded('Swow')) {
create_test_worker(function () {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
'--colors=always',
...glob(__DIR__ . '/*Test.php')
]);
}, Swow::class);
}
function create_test_worker(Closure $callable, $eventLoopClass): void
{
$worker = new Worker();
$worker->eventLoop = $eventLoopClass;
$worker->onWorkerStart = function () use ($callable, $eventLoopClass) {
$fp = fopen(__FILE__, 'r+');
flock($fp, LOCK_EX);
echo PHP_EOL . PHP_EOL. PHP_EOL . '[TEST EVENT-LOOP: ' . basename(str_replace('\\', '/', $eventLoopClass)) . ']' . PHP_EOL;
try {
$callable();
} catch (Throwable $e) {
echo $e;
} finally {
flock($fp, LOCK_UN);
}
Timer::repeat(1, function () use ($fp) {
if (flock($fp, LOCK_EX | LOCK_NB)) {
if(function_exists('posix_kill')) {
posix_kill(posix_getppid(), SIGINT);
} else {
Worker::stopAll();
}
}
});
};
}
Worker::runAll();
@@ -0,0 +1,5 @@
composer.lock
vendor
vendor/
.idea
.idea/
+5
View File
@@ -0,0 +1,5 @@
# webman-framework
Note: This repository is the core code of the webman framework. If you want to build an application using webman, visit the main [webman](https://github.com/walkor/webman) repository.
## LICENSE
MIT
+52
View File
@@ -0,0 +1,52 @@
{
"name": "workerman/webman-framework",
"type": "library",
"keywords": [
"high performance",
"http service"
],
"homepage": "https://www.workerman.net",
"license": "MIT",
"description": "High performance HTTP Service Framework.",
"authors": [
{
"name": "walkor",
"email": "walkor@workerman.net",
"homepage": "https://www.workerman.net",
"role": "Developer"
}
],
"support": {
"email": "walkor@workerman.net",
"issues": "https://github.com/walkor/webman/issues",
"forum": "https://wenda.workerman.net/",
"wiki": "https://doc.workerman.net/",
"source": "https://github.com/walkor/webman-framework"
},
"require": {
"php": ">=8.1",
"ext-json": "*",
"workerman/workerman": "^5.1 || dev-master",
"nikic/fast-route": "^1.3",
"psr/container": ">=1.0",
"psr/log": "^3.0"
},
"suggest": {
"ext-event": "For better performance. "
},
"autoload": {
"psr-4": {
"Webman\\": "./src",
"support\\": "./src/support",
"Support\\": "./src/support",
"Support\\Bootstrap\\": "./src/support/bootstrap",
"Support\\Exception\\": "./src/support/exception",
"Support\\View\\": "./src/support/view"
},
"files": [
"./src/support/helpers.php"
]
},
"minimum-stability": "dev",
"prefer-stable": true
}
@@ -0,0 +1,10 @@
<?php
namespace Webman\Annotation;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class DisableDefaultRoute
{
}
@@ -0,0 +1,25 @@
<?php
namespace Webman\Annotation;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class Middleware
{
protected array $middlewares = [];
public function __construct(...$middlewares)
{
$this->middlewares = $middlewares;
}
public function getMiddlewares(): array
{
$middlewares = [];
foreach ($this->middlewares as $middleware) {
$middlewares[] = [$middleware, 'process'];
}
return $middlewares;
}
}
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use Workerman\Worker;
interface Bootstrap
{
/**
* onWorkerStart
*
* @param Worker|null $worker
* @return mixed
*/
public static function start(?Worker $worker);
}
+296
View File
@@ -0,0 +1,296 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use function array_replace_recursive;
use function array_reverse;
use function count;
use function explode;
use function in_array;
use function is_array;
use function is_dir;
use function is_file;
use function key;
use function str_replace;
class Config
{
/**
* @var array
*/
protected static $config = [];
/**
* @var string
*/
protected static $configPath = '';
/**
* @var bool
*/
protected static $loaded = false;
/**
* Load.
* @param string $configPath
* @param array $excludeFile
* @param string|null $key
* @return void
*/
public static function load(string $configPath, array $excludeFile = [], ?string $key = null)
{
static::$configPath = $configPath;
if (!$configPath) {
return;
}
static::$loaded = false;
$config = static::loadFromDir($configPath, $excludeFile);
if (!$config) {
static::$loaded = true;
return;
}
if ($key !== null) {
foreach (array_reverse(explode('.', $key)) as $k) {
$config = [$k => $config];
}
}
static::$config = array_replace_recursive(static::$config, $config);
static::formatConfig();
static::$loaded = true;
}
/**
* This deprecated method will certainly be removed in the future.
* @param string $configPath
* @param array $excludeFile
* @return void
* @deprecated
*/
public static function reload(string $configPath, array $excludeFile = [])
{
static::load($configPath, $excludeFile);
}
/**
* Clear.
* @return void
*/
public static function clear()
{
static::$config = [];
}
/**
* FormatConfig.
* @return void
*/
protected static function formatConfig()
{
$config = static::$config;
// Merge log config
foreach ($config['plugin'] ?? [] as $firm => $projects) {
if (isset($projects['app'])) {
foreach ($projects['log'] ?? [] as $key => $item) {
$config['log']["plugin.$firm.$key"] = $item;
}
}
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['log'] ?? [] as $key => $item) {
$config['log']["plugin.$firm.$name.$key"] = $item;
}
}
}
// Merge database config
foreach ($config['plugin'] ?? [] as $firm => $projects) {
if (isset($projects['app'])) {
foreach ($projects['database']['connections'] ?? [] as $key => $connection) {
$config['database']['connections']["plugin.$firm.$key"] = $connection;
}
}
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['database']['connections'] ?? [] as $key => $connection) {
$config['database']['connections']["plugin.$firm.$name.$key"] = $connection;
}
}
}
if (!empty($config['database']['connections'])) {
$config['database']['default'] = $config['database']['default'] ?? key($config['database']['connections']);
}
// Merge thinkorm config
foreach ($config['plugin'] ?? [] as $firm => $projects) {
if (isset($projects['app'])) {
foreach ($projects['thinkorm']['connections'] ?? [] as $key => $connection) {
$config['thinkorm']['connections']["plugin.$firm.$key"] = $connection;
}
}
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['thinkorm']['connections'] ?? [] as $key => $connection) {
$config['thinkorm']['connections']["plugin.$firm.$name.$key"] = $connection;
}
}
}
if (!empty($config['thinkorm']['connections'])) {
$config['thinkorm']['default'] = $config['thinkorm']['default'] ?? key($config['thinkorm']['connections']);
}
// Merge redis config
foreach ($config['plugin'] ?? [] as $firm => $projects) {
if (isset($projects['app'])) {
foreach ($projects['redis'] ?? [] as $key => $connection) {
$config['redis']["plugin.$firm.$key"] = $connection;
}
}
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['redis'] ?? [] as $key => $connection) {
$config['redis']["plugin.$firm.$name.$key"] = $connection;
}
}
}
static::$config = $config;
}
/**
* LoadFromDir.
* @param string $configPath
* @param array $excludeFile
* @return array
*/
public static function loadFromDir(string $configPath, array $excludeFile = []): array
{
$allConfig = [];
$dirIterator = new RecursiveDirectoryIterator($configPath, FilesystemIterator::FOLLOW_SYMLINKS);
$iterator = new RecursiveIteratorIterator($dirIterator);
foreach ($iterator as $file) {
/** var SplFileInfo $file */
if (is_dir($file) || $file->getExtension() != 'php' || in_array($file->getBaseName('.php'), $excludeFile)) {
continue;
}
$appConfigFile = $file->getPath() . '/app.php';
if (!is_file($appConfigFile)) {
continue;
}
$relativePath = str_replace($configPath . DIRECTORY_SEPARATOR, '', substr($file, 0, -4));
$explode = array_reverse(explode(DIRECTORY_SEPARATOR, $relativePath));
if (count($explode) >= 2) {
$appConfig = include $appConfigFile;
if (empty($appConfig['enable'])) {
continue;
}
}
$config = include $file;
foreach ($explode as $section) {
$tmp = [];
$tmp[$section] = $config;
$config = $tmp;
}
$allConfig = array_replace_recursive($allConfig, $config);
}
return $allConfig;
}
/**
* Get.
* @param string|null $key
* @param mixed $default
* @return mixed
*/
public static function get(?string $key = null, mixed $default = null)
{
if ($key === null) {
return static::$config;
}
$keyArray = explode('.', $key);
$value = static::$config;
$found = true;
foreach ($keyArray as $index) {
if (!isset($value[$index])) {
if (static::$loaded) {
return $default;
}
$found = false;
break;
}
$value = $value[$index];
}
if ($found) {
return $value;
}
return static::read($key, $default);
}
/**
* Read.
* @param string $key
* @param mixed $default
* @return mixed
*/
protected static function read(string $key, mixed $default = null)
{
$path = static::$configPath;
if ($path === '') {
return $default;
}
$keys = $keyArray = explode('.', $key);
foreach ($keyArray as $index => $section) {
unset($keys[$index]);
if (is_file($file = "$path/$section.php")) {
$config = include $file;
return static::find($keys, $config, $default);
}
if (!is_dir($path = "$path/$section")) {
return $default;
}
}
return $default;
}
/**
* Find.
* @param array $keyArray
* @param mixed $stack
* @param mixed $default
* @return array|mixed
*/
protected static function find(array $keyArray, $stack, $default)
{
if (!is_array($stack)) {
return $default;
}
$value = $stack;
foreach ($keyArray as $index) {
if (!isset($value[$index])) {
return $default;
}
$value = $value[$index];
}
return $value;
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace Webman;
use Psr\Container\ContainerInterface;
use Webman\Exception\NotFoundException;
use function array_key_exists;
use function class_exists;
/**
* Class Container
* @package Webman
*/
class Container implements ContainerInterface
{
/**
* @var array
*/
protected $instances = [];
/**
* @var array
*/
protected $definitions = [];
/**
* Get.
* @param string $name
* @return mixed
* @throws NotFoundException
*/
public function get(string $name)
{
if (!isset($this->instances[$name])) {
if (isset($this->definitions[$name])) {
$this->instances[$name] = call_user_func($this->definitions[$name], $this);
} else {
if (!class_exists($name)) {
throw new NotFoundException("Class '$name' not found");
}
$this->instances[$name] = new $name();
}
}
return $this->instances[$name];
}
/**
* Has.
* @param string $name
* @return bool
*/
public function has(string $name): bool
{
return array_key_exists($name, $this->instances)
|| array_key_exists($name, $this->definitions);
}
/**
* Make.
* @param string $name
* @param array $constructor
* @return mixed
* @throws NotFoundException
*/
public function make(string $name, array $constructor = [])
{
if (!class_exists($name)) {
throw new NotFoundException("Class '$name' not found");
}
return new $name(... array_values($constructor));
}
/**
* AddDefinitions.
* @param array $definitions
* @return $this
*/
public function addDefinitions(array $definitions): Container
{
$this->definitions = array_merge($this->definitions, $definitions);
return $this;
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Webman;
use Workerman\Coroutine\Context as WorkermanContext;
use Workerman\Coroutine\Utils\DestructionWatcher;
use Closure;
/**
* Class Context
* @package Webman
*/
class Context extends WorkermanContext
{
public static function onDestroy(Closure $closure): void
{
$obj = static::get('context.onDestroy');
if (!$obj) {
$obj = new \stdClass();
static::set('context.onDestroy', $obj);
}
DestructionWatcher::watch($obj, $closure);
}
}
@@ -0,0 +1,117 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Exception;
use RuntimeException;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
use function json_encode;
/**
* Class BusinessException
* @package support\exception
*/
class BusinessException extends RuntimeException
{
/**
* @var array
*/
protected $data = [];
/**
* @var bool
*/
protected $debug = false;
/**
* Render an exception into an HTTP response.
* @param Request $request
* @return Response|null
*/
public function render(Request $request): ?Response
{
if ($request->expectsJson()) {
$code = $this->getCode();
$json = ['code' => $code ?: 500, 'msg' => $this->getMessage(), 'data' => $this->data];
return new Response(200, ['Content-Type' => 'application/json'],
json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
return new Response(200, [], $this->getMessage());
}
/**
* Set data.
* @param array|null $data
* @return array|$this
*/
public function data(?array $data = null): array|static
{
if ($data === null) {
return $this->data;
}
$this->data = $data;
return $this;
}
/**
* Set debug.
* @param bool|null $value
* @return $this|bool
*/
public function debug(?bool $value = null): bool|static
{
if ($value === null) {
return $this->debug;
}
$this->debug = $value;
return $this;
}
/**
* Get data.
* @return array
*/
public function getData(): array
{
return $this->data;
}
/**
* Translate message.
* @param string $message
* @param array $parameters
* @param string|null $domain
* @param string|null $locale
* @return string
*/
protected function trans(string $message, array $parameters = [], ?string $domain = null, ?string $locale = null): string
{
$args = [];
foreach ($parameters as $key => $parameter) {
$args[":$key"] = $parameter;
}
try {
$message = trans($message, $args, $domain, $locale);
} catch (Throwable $e) {
}
foreach ($parameters as $key => $value) {
$message = str_replace(":$key", $value, $message);
}
return $message;
}
}
@@ -0,0 +1,121 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Exception;
use Psr\Log\LoggerInterface;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
use function json_encode;
use function nl2br;
use function trim;
/**
* Class Handler
* @package support\exception
*/
class ExceptionHandler implements ExceptionHandlerInterface
{
/**
* @var LoggerInterface
*/
protected $logger = null;
/**
* @var bool
*/
protected $debug = false;
/**
* @var array
*/
public $dontReport = [];
/**
* ExceptionHandler constructor.
* @param $logger
* @param $debug
*/
public function __construct($logger, $debug)
{
$this->logger = $logger;
$this->debug = $debug;
}
/**
* @param Throwable $exception
* @return void
*/
public function report(Throwable $exception)
{
if ($this->shouldntReport($exception)) {
return;
}
$logs = '';
if ($request = \request()) {
$logs = $request->getRealIp() . ' ' . $request->method() . ' ' . trim($request->fullUrl(), '/');
}
$this->logger->error($logs . PHP_EOL . $exception);
}
/**
* @param Request $request
* @param Throwable $exception
* @return Response
*/
public function render(Request $request, Throwable $exception): Response
{
if (method_exists($exception, 'render') && ($response = $exception->render($request))) {
return $response;
}
$code = $exception->getCode();
if ($request->expectsJson()) {
$json = ['code' => $code ?: 500, 'msg' => $this->debug ? $exception->getMessage() : 'Server internal error'];
$this->debug && $json['traces'] = (string)$exception;
return new Response(200, ['Content-Type' => 'application/json'],
json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
$error = $this->debug ? nl2br((string)$exception) : 'Server internal error';
return new Response(500, [], $error);
}
/**
* @param Throwable $e
* @return bool
*/
protected function shouldntReport(Throwable $e): bool
{
foreach ($this->dontReport as $type) {
if ($e instanceof $type) {
return true;
}
}
return false;
}
/**
* Compatible $this->_debug
*
* @param string $name
* @return bool|null
*/
public function __get(string $name)
{
if ($name === '_debug') {
return $this->debug;
}
return null;
}
}
@@ -0,0 +1,35 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Exception;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
interface ExceptionHandlerInterface
{
/**
* @param Throwable $exception
* @return mixed
*/
public function report(Throwable $exception);
/**
* @param Request $request
* @param Throwable $exception
* @return Response
*/
public function render(Request $request, Throwable $exception): Response;
}
@@ -0,0 +1,25 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Exception;
use RuntimeException;
/**
* Class FileException
* @package Webman\Exception
*/
class FileException extends RuntimeException
{
}
@@ -0,0 +1,25 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Exception;
use Psr\Container\NotFoundExceptionInterface;
/**
* Class NotFoundException
* @package Webman\Exception
*/
class NotFoundException extends \Exception implements NotFoundExceptionInterface
{
}
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use SplFileInfo;
use Webman\Exception\FileException;
use function chmod;
use function is_dir;
use function mkdir;
use function pathinfo;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use function strip_tags;
use function umask;
class File extends SplFileInfo
{
/**
* Move.
* @param string $destination
* @return File
*/
public function move(string $destination): File
{
set_error_handler(function ($type, $msg) use (&$error) {
$error = $msg;
});
$path = pathinfo($destination, PATHINFO_DIRNAME);
if (!is_dir($path) && !mkdir($path, 0777, true)) {
restore_error_handler();
throw new FileException(sprintf('Unable to create the "%s" directory (%s)', $path, strip_tags($error)));
}
if (!rename($this->getPathname(), $destination)) {
restore_error_handler();
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $destination, strip_tags($error)));
}
restore_error_handler();
@chmod($destination, 0666 & ~umask());
return new self($destination);
}
}
+409
View File
@@ -0,0 +1,409 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Http;
use Webman\Route\Route;
use function current;
use function filter_var;
use function ip2long;
use function is_array;
use function strpos;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_NO_PRIV_RANGE;
use const FILTER_FLAG_NO_RES_RANGE;
use const FILTER_VALIDATE_IP;
/**
* Class Request
* @package Webman\Http
*/
class Request extends \Workerman\Protocols\Http\Request
{
/**
* @var string
*/
public $plugin = null;
/**
* @var string
*/
public $app = null;
/**
* @var string
*/
public $controller = null;
/**
* @var string
*/
public $action = null;
/**
* @var Route
*/
public $route = null;
/**
* @var bool
*/
protected $isDirty = false;
/**
* @return mixed|null
*/
public function all()
{
return $this->get() + $this->post();
}
/**
* Input
* @param string $name
* @param mixed $default
* @return mixed
*/
public function input(string $name, mixed $default = null)
{
return $this->get($name, $this->post($name, $default));
}
/**
* Only
* @param array $keys
* @return array
*/
public function only(array $keys): array
{
$all = $this->all();
$result = [];
foreach ($keys as $key) {
if (isset($all[$key])) {
$result[$key] = $all[$key];
}
}
return $result;
}
/**
* Except
* @param array $keys
* @return mixed|null
*/
public function except(array $keys)
{
$all = $this->all();
foreach ($keys as $key) {
unset($all[$key]);
}
return $all;
}
/**
* File
* @param string|null $name
* @return UploadFile|UploadFile[]|null
*/
public function file(?string $name = null): array|null|UploadFile
{
$files = parent::file($name);
if (null === $files) {
return $name === null ? [] : null;
}
if ($name !== null) {
// Multi files
if (is_array(current($files))) {
return $this->parseFiles($files);
}
return $this->parseFile($files);
}
$uploadFiles = [];
foreach ($files as $name => $file) {
// Multi files
if (is_array(current($file))) {
$uploadFiles[$name] = $this->parseFiles($file);
} else {
$uploadFiles[$name] = $this->parseFile($file);
}
}
return $uploadFiles;
}
/**
* ParseFile
* @param array $file
* @return UploadFile
*/
protected function parseFile(array $file): UploadFile
{
return new UploadFile($file['tmp_name'], $file['name'], $file['type'], $file['error']);
}
/**
* ParseFiles
* @param array $files
* @return array
*/
protected function parseFiles(array $files): array
{
$uploadFiles = [];
foreach ($files as $key => $file) {
if (is_array(current($file))) {
$uploadFiles[$key] = $this->parseFiles($file);
} else {
$uploadFiles[$key] = $this->parseFile($file);
}
}
return $uploadFiles;
}
/**
* GetRemoteIp
* @return string
*/
public function getRemoteIp(): string
{
return $this->connection ? $this->connection->getRemoteIp() : '0.0.0.0';
}
/**
* GetRemotePort
* @return int
*/
public function getRemotePort(): int
{
return $this->connection ? $this->connection->getRemotePort() : 0;
}
/**
* GetLocalIp
* @return string
*/
public function getLocalIp(): string
{
return $this->connection ? $this->connection->getLocalIp() : '0.0.0.0';
}
/**
* GetLocalPort
* @return int
*/
public function getLocalPort(): int
{
return $this->connection ? $this->connection->getLocalPort() : 0;
}
/**
* GetRealIp
* @param bool $safeMode
* @return string
*/
public function getRealIp(bool $safeMode = true): string
{
$remoteIp = $this->getRemoteIp();
if ($safeMode && !static::isIntranetIp($remoteIp)) {
return $remoteIp;
}
$ip = $this->header('x-forwarded-for')
?? $this->header('x-real-ip')
?? $this->header('client-ip')
?? $this->header('x-client-ip')
?? $this->header('via')
?? $remoteIp;
if (is_string($ip)) {
$ip = current(explode(',', $ip));
}
return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : $remoteIp;
}
/**
* Url
* @return string
*/
public function url(): string
{
return '//' . $this->host() . $this->path();
}
/**
* FullUrl
* @return string
*/
public function fullUrl(): string
{
return '//' . $this->host() . $this->uri();
}
/**
* IsAjax
* @return bool
*/
public function isAjax(): bool
{
return $this->header('X-Requested-With') === 'XMLHttpRequest';
}
/**
* IsGet
* @return bool
*/
public function isGet(): bool
{
return $this->method() === 'GET';
}
/**
* IsPost
* @return bool
*/
public function isPost(): bool
{
return $this->method() === 'POST';
}
/**
* IsPjax
* @return bool
*/
public function isPjax(): bool
{
return (bool)$this->header('X-PJAX');
}
/**
* ExpectsJson
* @return bool
*/
public function expectsJson(): bool
{
return ($this->isAjax() && !$this->isPjax()) || $this->acceptJson();
}
/**
* AcceptJson
* @return bool
*/
public function acceptJson(): bool
{
return false !== strpos($this->header('accept', ''), 'json');
}
/**
* IsIntranetIp
* @param string $ip
* @return bool
*/
public static function isIntranetIp(string $ip): bool
{
// Not validate ip .
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
return false;
}
// Is intranet ip ? For IPv4, the result of false may not be accurate, so we need to check it manually later .
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return true;
}
// Manual check only for IPv4 .
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return false;
}
// Manual check .
$reservedIps = [
1681915904 => 1686110207, // 100.64.0.0 - 100.127.255.255
3221225472 => 3221225727, // 192.0.0.0 - 192.0.0.255
3221225984 => 3221226239, // 192.0.2.0 - 192.0.2.255
3227017984 => 3227018239, // 192.88.99.0 - 192.88.99.255
3323068416 => 3323199487, // 198.18.0.0 - 198.19.255.255
3325256704 => 3325256959, // 198.51.100.0 - 198.51.100.255
3405803776 => 3405804031, // 203.0.113.0 - 203.0.113.255
3758096384 => 4026531839, // 224.0.0.0 - 239.255.255.255
];
$ipLong = ip2long($ip);
foreach ($reservedIps as $ipStart => $ipEnd) {
if (($ipLong >= $ipStart) && ($ipLong <= $ipEnd)) {
return true;
}
}
return false;
}
/**
* Set get.
* @param array|string $input
* @param mixed $value
* @return Request
*/
public function setGet(array|string $input, mixed $value = null): Request
{
$this->isDirty = true;
$input = is_array($input) ? $input : array_merge($this->get(), [$input => $value]);
if (isset($this->data)) {
$this->data['get'] = $input;
} else {
$this->_data['get'] = $input;
}
return $this;
}
/**
* Set post.
* @param array|string $input
* @param mixed $value
* @return Request
*/
public function setPost(array|string $input, mixed $value = null): Request
{
$this->isDirty = true;
$input = is_array($input) ? $input : array_merge($this->post(), [$input => $value]);
if (isset($this->data)) {
$this->data['post'] = $input;
} else {
$this->_data['post'] = $input;
}
return $this;
}
/**
* Set header.
* @param array|string $input
* @param mixed $value
* @return Request
*/
public function setHeader(array|string $input, mixed $value = null): Request
{
$this->isDirty = true;
$input = is_array($input) ? $input : array_merge($this->header(), [$input => $value]);
if (isset($this->data)) {
$this->data['headers'] = $input;
} else {
$this->_data['headers'] = $input;
}
return $this;
}
/**
* Destroy
*/
public function destroy(): void
{
if ($this->isDirty) {
unset($this->data['get'], $this->data['post'], $this->data['headers']);
}
parent::destroy();
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Http;
use Throwable;
use Webman\App;
use function filemtime;
use function gmdate;
/**
* Class Response
* @package Webman\Http
*/
class Response extends \Workerman\Protocols\Http\Response
{
/**
* @var Throwable
*/
protected $exception = null;
/**
* File
* @param string $file
* @return $this
*/
public function file(string $file): Response
{
if ($this->notModifiedSince($file)) {
return $this->withStatus(304);
}
return $this->withFile($file);
}
/**
* Download
* @param string $file
* @param string $downloadName
* @return $this
*/
public function download(string $file, string $downloadName = ''): Response
{
$this->withFile($file);
if ($downloadName) {
$this->header('Content-Disposition', "attachment; filename=\"$downloadName\"");
}
return $this;
}
/**
* NotModifiedSince
* @param string $file
* @return bool
*/
protected function notModifiedSince(string $file): bool
{
$ifModifiedSince = App::request()->header('if-modified-since');
if ($ifModifiedSince === null || !is_file($file) || !($mtime = filemtime($file))) {
return false;
}
return $ifModifiedSince === gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
}
/**
* Exception
* @param Throwable|null $exception
* @return Throwable|null
*/
public function exception(?Throwable $exception = null): ?Throwable
{
if ($exception) {
$this->exception = $exception;
}
return $this->exception;
}
}
@@ -0,0 +1,111 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Http;
use Webman\File;
use function pathinfo;
/**
* Class UploadFile
* @package Webman\Http
*/
class UploadFile extends File
{
/**
* @var string
*/
protected $uploadName = null;
/**
* @var string
*/
protected $uploadMimeType = null;
/**
* @var int
*/
protected $uploadErrorCode = null;
/**
* UploadFile constructor.
*
* @param string $fileName
* @param string $uploadName
* @param string $uploadMimeType
* @param int $uploadErrorCode
*/
public function __construct(string $fileName, string $uploadName, string $uploadMimeType, int $uploadErrorCode)
{
$this->uploadName = $uploadName;
$this->uploadMimeType = $uploadMimeType;
$this->uploadErrorCode = $uploadErrorCode;
parent::__construct($fileName);
}
/**
* GetUploadName
* @return string
*/
public function getUploadName(): ?string
{
return $this->uploadName;
}
/**
* GetUploadMimeType
* @return string
*/
public function getUploadMimeType(): ?string
{
return $this->uploadMimeType;
}
/**
* GetUploadExtension
* @return string
*/
public function getUploadExtension(): string
{
return pathinfo($this->uploadName, PATHINFO_EXTENSION);
}
/**
* GetUploadErrorCode
* @return int
*/
public function getUploadErrorCode(): ?int
{
return $this->uploadErrorCode;
}
/**
* IsValid
* @return bool
*/
public function isValid(): bool
{
return $this->uploadErrorCode === UPLOAD_ERR_OK;
}
/**
* GetUploadMineType
* @return string
* @deprecated
*/
public function getUploadMineType(): ?string
{
return $this->uploadMimeType;
}
}
+78
View File
@@ -0,0 +1,78 @@
<?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();
static::removeLaravelDbFromBootstrap();
}
/**
* 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";
}
}
/**
* @return void
*/
protected static function removeLaravelDbFromBootstrap(): void
{
$file = base_path('config/bootstrap.php');
if (!is_file($file)) {
return;
}
$pattern = '/^\s*support\\\\bootstrap\\\\LaravelDb::class,?\s*?\r?\n/m';
$content = file_get_contents($file);
if (preg_match($pattern, $content)) {
$updatedContent = preg_replace($pattern, '', $content);
file_put_contents($file, $updatedContent);
}
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use Closure;
use ReflectionAttribute;
use Webman\Route\Route;
use ReflectionClass;
use ReflectionMethod;
use RuntimeException;
use function array_merge;
use function array_reverse;
use function is_array;
use function method_exists;
class Middleware
{
/**
* @var array
*/
protected static $instances = [];
/**
* @param mixed $allMiddlewares
* @param string $plugin
* @return void
*/
public static function load($allMiddlewares, string $plugin = '')
{
if (!is_array($allMiddlewares)) {
return;
}
foreach ($allMiddlewares as $appName => $middlewares) {
if (!is_array($middlewares)) {
throw new RuntimeException('Bad middleware config');
}
if ($appName === '@') {
$plugin = '';
}
if (strpos($appName, 'plugin.') !== false) {
$explode = explode('.', $appName, 4);
$plugin = $explode[1];
$appName = $explode[2] ?? '';
}
foreach ($middlewares as $className) {
if (method_exists($className, 'process')) {
static::$instances[$plugin][$appName][] = [$className, 'process'];
} else {
// @todo Log
echo "middleware $className::process not exsits\n";
}
}
}
}
/**
* @param string $plugin
* @param string $appName
* @param string|array|Closure $controller
* @param Route|null $route
* @param bool $withGlobalMiddleware
* @return array
*/
public static function getMiddleware(string $plugin, string $appName, string|array|Closure $controller, Route|null $route, bool $withGlobalMiddleware = true): array
{
$isController = is_array($controller) && is_string($controller[0]);
$globalMiddleware = $withGlobalMiddleware ? static::$instances['']['@'] ?? [] : [];
$appGlobalMiddleware = $withGlobalMiddleware && isset(static::$instances[$plugin]['']) ? static::$instances[$plugin][''] : [];
$middlewares = $routeMiddlewares = [];
// Route middleware
if ($route) {
foreach (array_reverse($route->getMiddleware()) as $className) {
$routeMiddlewares[] = [$className, 'process'];
}
}
if ($isController && $controller[0] && class_exists($controller[0])) {
// Controller middleware annotation
$reflectionClass = new ReflectionClass($controller[0]);
self::prepareAttributeMiddlewares($middlewares, $reflectionClass);
// Controller middleware property
if ($reflectionClass->hasProperty('middleware')) {
$defaultProperties = $reflectionClass->getDefaultProperties();
$middlewaresClasses = $defaultProperties['middleware'];
foreach ((array)$middlewaresClasses as $className) {
$middlewares[] = [$className, 'process'];
}
}
// Route middleware
$middlewares = array_merge($middlewares, $routeMiddlewares);
// Method middleware annotation
if ($reflectionClass->hasMethod($controller[1])) {
self::prepareAttributeMiddlewares($middlewares, $reflectionClass->getMethod($controller[1]));
}
} else {
// Route middleware
$middlewares = array_merge($middlewares, $routeMiddlewares);
}
if ($appName === '') {
return array_reverse(array_merge($globalMiddleware, $appGlobalMiddleware, $middlewares));
}
$appMiddleware = static::$instances[$plugin][$appName] ?? [];
return array_reverse(array_merge($globalMiddleware, $appGlobalMiddleware, $appMiddleware, $middlewares));
}
/**
* @param array $middlewares
* @param ReflectionClass|ReflectionMethod $reflection
* @return void
*/
private static function prepareAttributeMiddlewares(array &$middlewares, ReflectionClass|ReflectionMethod $reflection): void
{
$middlewareAttributes = $reflection->getAttributes(Annotation\Middleware::class, ReflectionAttribute::IS_INSTANCEOF);
foreach ($middlewareAttributes as $middlewareAttribute) {
$middlewareAttributeInstance = $middlewareAttribute->newInstance();
$middlewares = array_merge($middlewares, $middlewareAttributeInstance->getMiddlewares());
}
}
/**
* @return void
* @deprecated
*/
public static function container($_)
{
}
}
@@ -0,0 +1,30 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use Webman\Http\Request;
use Webman\Http\Response;
interface MiddlewareInterface
{
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
*/
public function process(Request $request, callable $handler): Response;
}
+572
View File
@@ -0,0 +1,572 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use FastRoute\Dispatcher\GroupCountBased;
use FastRoute\RouteCollector;
use FilesystemIterator;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use Webman\Annotation\DisableDefaultRoute;
use Webman\Route\Route as RouteObject;
use function array_diff;
use function array_values;
use function class_exists;
use function explode;
use function FastRoute\simpleDispatcher;
use function in_array;
use function is_array;
use function is_callable;
use function is_file;
use function is_scalar;
use function is_string;
use function json_encode;
use function method_exists;
use function strpos;
/**
* Class Route
* @package Webman
*/
class Route
{
/**
* @var Route
*/
protected static $instance = null;
/**
* @var GroupCountBased
*/
protected static $dispatcher = null;
/**
* @var RouteCollector
*/
protected static $collector = null;
/**
* @var RouteObject[]
*/
protected static $fallbackRoutes = [];
/**
* @var array
*/
protected static $fallback = [];
/**
* @var array
*/
protected static $nameList = [];
/**
* @var string
*/
protected static $groupPrefix = '';
/**
* @var bool
*/
protected static $disabledDefaultRoutes = [];
/**
* @var array
*/
protected static $disabledDefaultRouteControllers = [];
/**
* @var array
*/
protected static $disabledDefaultRouteActions = [];
/**
* @var RouteObject[]
*/
protected static $allRoutes = [];
/**
* @var RouteObject[]
*/
protected $routes = [];
/**
* @var Route[]
*/
protected $children = [];
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function get(string $path, $callback): RouteObject
{
return static::addRoute('GET', $path, $callback);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function post(string $path, $callback): RouteObject
{
return static::addRoute('POST', $path, $callback);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function put(string $path, $callback): RouteObject
{
return static::addRoute('PUT', $path, $callback);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function patch(string $path, $callback): RouteObject
{
return static::addRoute('PATCH', $path, $callback);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function delete(string $path, $callback): RouteObject
{
return static::addRoute('DELETE', $path, $callback);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function head(string $path, $callback): RouteObject
{
return static::addRoute('HEAD', $path, $callback);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function options(string $path, $callback): RouteObject
{
return static::addRoute('OPTIONS', $path, $callback);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function any(string $path, $callback): RouteObject
{
return static::addRoute(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], $path, $callback);
}
/**
* @param $method
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function add($method, string $path, $callback): RouteObject
{
return static::addRoute($method, $path, $callback);
}
/**
* @param string|callable $path
* @param callable|null $callback
* @return static
*/
public static function group($path, ?callable $callback = null): Route
{
if ($callback === null) {
$callback = $path;
$path = '';
}
$previousGroupPrefix = static::$groupPrefix;
static::$groupPrefix = $previousGroupPrefix . $path;
$previousInstance = static::$instance;
$instance = static::$instance = new static;
static::$collector->addGroup($path, $callback);
static::$groupPrefix = $previousGroupPrefix;
static::$instance = $previousInstance;
if ($previousInstance) {
$previousInstance->addChild($instance);
}
return $instance;
}
/**
* @param string $name
* @param string $controller
* @param array $options
* @return void
*/
public static function resource(string $name, string $controller, array $options = [])
{
$name = trim($name, '/');
if (is_array($options) && !empty($options)) {
$diffOptions = array_diff($options, ['index', 'create', 'store', 'update', 'show', 'edit', 'destroy', 'recovery']);
if (!empty($diffOptions)) {
foreach ($diffOptions as $action) {
static::any("/$name/{$action}[/{id}]", [$controller, $action])->name("$name.{$action}");
}
}
// 注册路由 由于顺序不同会导致路由无效 因此不适用循环注册
if (in_array('index', $options)) static::get("/$name", [$controller, 'index'])->name("$name.index");
if (in_array('create', $options)) static::get("/$name/create", [$controller, 'create'])->name("$name.create");
if (in_array('store', $options)) static::post("/$name", [$controller, 'store'])->name("$name.store");
if (in_array('update', $options)) static::put("/$name/{id}", [$controller, 'update'])->name("$name.update");
if (in_array('patch', $options)) static::patch("/$name/{id}", [$controller, 'patch'])->name("$name.patch");
if (in_array('show', $options)) static::get("/$name/{id}", [$controller, 'show'])->name("$name.show");
if (in_array('edit', $options)) static::get("/$name/{id}/edit", [$controller, 'edit'])->name("$name.edit");
if (in_array('destroy', $options)) static::delete("/$name/{id}", [$controller, 'destroy'])->name("$name.destroy");
if (in_array('recovery', $options)) static::put("/$name/{id}/recovery", [$controller, 'recovery'])->name("$name.recovery");
} else {
//为空时自动注册所有常用路由
if (method_exists($controller, 'index')) static::get("/$name", [$controller, 'index'])->name("$name.index");
if (method_exists($controller, 'create')) static::get("/$name/create", [$controller, 'create'])->name("$name.create");
if (method_exists($controller, 'store')) static::post("/$name", [$controller, 'store'])->name("$name.store");
if (method_exists($controller, 'update')) static::put("/$name/{id}", [$controller, 'update'])->name("$name.update");
if (method_exists($controller, 'patch')) static::patch("/$name/{id}", [$controller, 'patch'])->name("$name.patch");
if (method_exists($controller, 'show')) static::get("/$name/{id}", [$controller, 'show'])->name("$name.show");
if (method_exists($controller, 'edit')) static::get("/$name/{id}/edit", [$controller, 'edit'])->name("$name.edit");
if (method_exists($controller, 'destroy')) static::delete("/$name/{id}", [$controller, 'destroy'])->name("$name.destroy");
if (method_exists($controller, 'recovery')) static::put("/$name/{id}/recovery", [$controller, 'recovery'])->name("$name.recovery");
}
}
/**
* @return RouteObject[]
*/
public static function getRoutes(): array
{
return static::$allRoutes;
}
/**
* disableDefaultRoute.
*
* @param array|string $plugin
* @param string|null $app
* @return bool
*/
public static function disableDefaultRoute(array|string $plugin = '', ?string $app = null): bool
{
// Is [controller action]
if (is_array($plugin)) {
$controllerAction = $plugin;
if (!isset($controllerAction[0]) || !is_string($controllerAction[0]) ||
!isset($controllerAction[1]) || !is_string($controllerAction[1])) {
return false;
}
$controller = $controllerAction[0];
$action = $controllerAction[1];
static::$disabledDefaultRouteActions[$controller][$action] = $action;
return true;
}
// Is plugin
if (is_string($plugin) && (preg_match('/^[a-zA-Z0-9_]+$/', $plugin) || $plugin === '')) {
if (!isset(static::$disabledDefaultRoutes[$plugin])) {
static::$disabledDefaultRoutes[$plugin] = [];
}
$app = $app ?? '*';
static::$disabledDefaultRoutes[$plugin][$app] = $app;
return true;
}
// Is controller
if (is_string($plugin) && class_exists($plugin)) {
static::$disabledDefaultRouteControllers[$plugin] = $plugin;
return true;
}
return false;
}
/**
* @param array|string $plugin
* @param string|null $app
* @return bool
*/
public static function isDefaultRouteDisabled(array|string $plugin = '', ?string $app = null): bool
{
// Is [controller action]
if (is_array($plugin)) {
if (!isset($plugin[0]) || !is_string($plugin[0]) ||
!isset($plugin[1]) || !is_string($plugin[1])) {
return false;
}
return isset(static::$disabledDefaultRouteActions[$plugin[0]][$plugin[1]]) || static::isDefaultRouteDisabledByAnnotation($plugin[0], $plugin[1]);
}
// Is plugin
if (is_string($plugin) && (preg_match('/^[a-zA-Z0-9_]+$/', $plugin) || $plugin === '')) {
$app = $app ?? '*';
return isset(static::$disabledDefaultRoutes[$plugin]['*']) || isset(static::$disabledDefaultRoutes[$plugin][$app]);
}
// Is controller
if (is_string($plugin) && class_exists($plugin)) {
return isset(static::$disabledDefaultRouteControllers[$plugin]);
}
return false;
}
/**
* @param string $controller
* @param string|null $action
* @return bool
*/
protected static function isDefaultRouteDisabledByAnnotation(string $controller, ?string $action = null): bool
{
if (class_exists($controller)) {
$reflectionClass = new ReflectionClass($controller);
if ($reflectionClass->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF)) {
return true;
}
if ($action && $reflectionClass->hasMethod($action)) {
$reflectionMethod = $reflectionClass->getMethod($action);
if ($reflectionMethod->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF)) {
return true;
}
}
}
return false;
}
/**
* @param $middleware
* @return $this
*/
public function middleware($middleware): Route
{
foreach ($this->routes as $route) {
$route->middleware($middleware);
}
foreach ($this->getChildren() as $child) {
$child->middleware($middleware);
}
return $this;
}
/**
* @param RouteObject $route
*/
public function collect(RouteObject $route)
{
$this->routes[] = $route;
}
/**
* @param string $name
* @param RouteObject $instance
*/
public static function setByName(string $name, RouteObject $instance)
{
static::$nameList[$name] = $instance;
}
/**
* @param string $name
* @return null|RouteObject
*/
public static function getByName(string $name): ?RouteObject
{
return static::$nameList[$name] ?? null;
}
/**
* @param Route $route
* @return void
*/
public function addChild(Route $route)
{
$this->children[] = $route;
}
/**
* @return Route[]
*/
public function getChildren()
{
return $this->children;
}
/**
* @param string $method
* @param string $path
* @return array
*/
public static function dispatch(string $method, string $path): array
{
return static::$dispatcher->dispatch($method, $path);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return callable|false|string[]
*/
public static function convertToCallable(string $path, $callback)
{
if (is_string($callback) && strpos($callback, '@')) {
$callback = explode('@', $callback, 2);
}
if (!is_array($callback)) {
if (!is_callable($callback)) {
$callStr = is_scalar($callback) ? $callback : 'Closure';
echo "Route $path $callStr is not callable\n";
return false;
}
} else {
$callback = array_values($callback);
if (!isset($callback[1]) || !class_exists($callback[0]) || !method_exists($callback[0], $callback[1])) {
echo "Route $path " . json_encode($callback) . " is not callable\n";
return false;
}
}
return $callback;
}
/**
* @param array|string $methods
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
protected static function addRoute($methods, string $path, $callback): RouteObject
{
$route = new RouteObject($methods, static::$groupPrefix . $path, $callback);
static::$allRoutes[] = $route;
if ($callback = static::convertToCallable($path, $callback)) {
static::$collector->addRoute($methods, $path, ['callback' => $callback, 'route' => $route]);
}
if (static::$instance) {
static::$instance->collect($route);
}
return $route;
}
/**
* Load.
* @param mixed $paths
* @return void
*/
public static function load($paths)
{
if (!is_array($paths)) {
return;
}
static::$dispatcher = simpleDispatcher(function (RouteCollector $route) use ($paths) {
Route::setCollector($route);
foreach ($paths as $configPath) {
$routeConfigFile = $configPath . '/route.php';
if (is_file($routeConfigFile)) {
require_once $routeConfigFile;
}
if (!is_dir($pluginConfigPath = $configPath . '/plugin')) {
continue;
}
$dirIterator = new RecursiveDirectoryIterator($pluginConfigPath, FilesystemIterator::FOLLOW_SYMLINKS);
$iterator = new RecursiveIteratorIterator($dirIterator);
foreach ($iterator as $file) {
if ($file->getBaseName('.php') !== 'route') {
continue;
}
$appConfigFile = pathinfo($file, PATHINFO_DIRNAME) . '/app.php';
if (!is_file($appConfigFile)) {
continue;
}
$appConfig = include $appConfigFile;
if (empty($appConfig['enable'])) {
continue;
}
require_once $file;
}
}
});
}
/**
* SetCollector.
* @param RouteCollector $route
* @return void
*/
public static function setCollector(RouteCollector $route)
{
static::$collector = $route;
}
/**
* Fallback.
* @param callable|mixed $callback
* @param string $plugin
* @return void
*/
public static function fallback(callable $callback, string $plugin = '')
{
$route = new RouteObject([], '', $callback);
static::$fallbackRoutes[$plugin] = $route;
return $route;
}
/**
* GetFallBack.
* @param string $plugin
* @param int $status
* @return callable|null
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws ReflectionException
*/
public static function getFallback(string $plugin = '', int $status = 404)
{
if (!isset(static::$fallback[$plugin])) {
$callback = null;
$route = static::$fallbackRoutes[$plugin] ?? null;
static::$fallback[$plugin] = $route ? App::getCallback($plugin, 'NOT_FOUND', $route->getCallback(), ['status' => $status], false, $route) : null;
}
return static::$fallback[$plugin];
}
/**
* @return void
* @deprecated
*/
public static function container()
{
}
}
+199
View File
@@ -0,0 +1,199 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Route;
use Webman\Route as Router;
use function array_merge;
use function count;
use function preg_replace_callback;
use function str_replace;
/**
* Class Route
* @package Webman
*/
class Route
{
/**
* @var string|null
*/
protected $name = null;
/**
* @var array
*/
protected $methods = [];
/**
* @var string
*/
protected $path = '';
/**
* @var callable
*/
protected $callback = null;
/**
* @var array
*/
protected $middlewares = [];
/**
* @var array
*/
protected $params = [];
/**
* Route constructor.
* @param array $methods
* @param string $path
* @param callable $callback
*/
public function __construct($methods, string $path, $callback)
{
$this->methods = (array)$methods;
$this->path = $path;
$this->callback = $callback;
}
/**
* Get name.
* @return string|null
*/
public function getName(): ?string
{
return $this->name ?? null;
}
/**
* Name.
* @param string $name
* @return $this
*/
public function name(string $name): Route
{
$this->name = $name;
Router::setByName($name, $this);
return $this;
}
/**
* Middleware.
* @param mixed $middleware
* @return $this|array
*/
public function middleware(mixed $middleware = null)
{
if ($middleware === null) {
return $this->middlewares;
}
$this->middlewares = array_merge($this->middlewares, is_array($middleware) ? array_reverse($middleware) : [$middleware]);
return $this;
}
/**
* GetPath.
* @return string
*/
public function getPath(): string
{
return $this->path;
}
/**
* GetMethods.
* @return array
*/
public function getMethods(): array
{
return $this->methods;
}
/**
* GetCallback.
* @return callable|null
*/
public function getCallback()
{
return $this->callback;
}
/**
* GetMiddleware.
* @return array
*/
public function getMiddleware(): array
{
return $this->middlewares;
}
/**
* Param.
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function param(?string $name = null, mixed $default = null)
{
if ($name === null) {
return $this->params;
}
return $this->params[$name] ?? $default;
}
/**
* SetParams.
* @param array $params
* @return $this
*/
public function setParams(array $params): Route
{
$this->params = array_merge($this->params, $params);
return $this;
}
/**
* Url.
* @param array $parameters
* @return string
*/
public function url(array $parameters = []): string
{
if (empty($parameters)) {
return $this->path;
}
$path = str_replace(['[', ']'], '', $this->path);
$path = preg_replace_callback('/\{(.*?)(?:\:[^\}]*?)*?\}/', function ($matches) use (&$parameters) {
if (!$parameters) {
return $matches[0];
}
if (isset($parameters[$matches[1]])) {
$value = $parameters[$matches[1]];
unset($parameters[$matches[1]]);
return $value;
}
$key = key($parameters);
if (is_int($key)) {
$value = $parameters[$key];
unset($parameters[$key]);
return $value;
}
return $matches[0];
}, $path);
return count($parameters) > 0 ? $path . '?' . http_build_query($parameters) : $path;
}
}
@@ -0,0 +1,26 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Session;
use Workerman\Protocols\Http\Session\FileSessionHandler as FileHandler;
/**
* Class FileSessionHandler
* @package Webman
*/
class FileSessionHandler extends FileHandler
{
}
@@ -0,0 +1,22 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Session;
use Workerman\Protocols\Http\Session\RedisClusterSessionHandler as RedisClusterHandler;
class RedisClusterSessionHandler extends RedisClusterHandler
{
}
@@ -0,0 +1,26 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Session;
use Workerman\Protocols\Http\Session\RedisSessionHandler as RedisHandler;
/**
* Class FileSessionHandler
* @package Webman
*/
class RedisSessionHandler extends RedisHandler
{
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use function array_diff;
use function array_map;
use function scandir;
/**
* Class Util
* @package Webman
*/
class Util
{
/**
* ScanDir.
* @param string $basePath
* @param bool $withBasePath
* @return array
*/
public static function scanDir(string $basePath, bool $withBasePath = true): array
{
if (!is_dir($basePath)) {
return [];
}
$paths = array_diff(scandir($basePath), array('.', '..')) ?: [];
return $withBasePath ? array_map(static function ($path) use ($basePath) {
return $basePath . DIRECTORY_SEPARATOR . $path;
}, $paths) : $paths;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
interface View
{
/**
* Render.
* @param string $template
* @param array $vars
* @param string|null $app
* @return string
*/
public static function render(string $template, array $vars, ?string $app = null): string;
}
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env php
<?php
chdir(__DIR__);
require_once __DIR__ . '/vendor/autoload.php';
support\App::run();
+167
View File
@@ -0,0 +1,167 @@
<?php
namespace support;
use Dotenv\Dotenv;
use RuntimeException;
use Throwable;
use Webman\Config;
use Webman\Util;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;
use function base_path;
use function call_user_func;
use function is_dir;
use function opcache_get_status;
use function opcache_invalidate;
use const DIRECTORY_SEPARATOR;
class App
{
/**
* Run.
* @return void
* @throws Throwable
*/
public static function run()
{
ini_set('display_errors', 'on');
error_reporting(E_ALL);
if (class_exists(Dotenv::class) && file_exists(run_path('.env'))) {
if (method_exists(Dotenv::class, 'createUnsafeImmutable')) {
Dotenv::createUnsafeImmutable(run_path())->load();
} else {
Dotenv::createMutable(run_path())->load();
}
}
if (!$appConfigFile = config_path('app.php')) {
throw new RuntimeException('Config file not found: app.php');
}
$appConfig = require $appConfigFile;
if ($timezone = $appConfig['default_timezone'] ?? '') {
date_default_timezone_set($timezone);
}
static::loadAllConfig(['route', 'container']);
if (!is_phar() && DIRECTORY_SEPARATOR === '\\' && empty(config('server.listen'))) {
echo "Please run 'php windows.php' on windows system." . PHP_EOL;
exit;
}
$errorReporting = config('app.error_reporting');
if (isset($errorReporting)) {
error_reporting($errorReporting);
}
$runtimeLogsPath = runtime_path() . DIRECTORY_SEPARATOR . 'logs';
if (!file_exists($runtimeLogsPath) || !is_dir($runtimeLogsPath)) {
if (!mkdir($runtimeLogsPath, 0777, true)) {
throw new RuntimeException("Failed to create runtime logs directory. Please check the permission.");
}
}
$runtimeViewsPath = runtime_path() . DIRECTORY_SEPARATOR . 'views';
if (!file_exists($runtimeViewsPath) || !is_dir($runtimeViewsPath)) {
if (!mkdir($runtimeViewsPath, 0777, true)) {
throw new RuntimeException("Failed to create runtime views directory. Please check the permission.");
}
}
Worker::$onMasterReload = function () {
if (function_exists('opcache_get_status')) {
if ($status = opcache_get_status()) {
if (isset($status['scripts']) && $scripts = $status['scripts']) {
foreach (array_keys($scripts) as $file) {
opcache_invalidate($file, true);
}
}
}
}
};
$config = config('server');
Worker::$pidFile = $config['pid_file'];
Worker::$stdoutFile = $config['stdout_file'];
Worker::$logFile = $config['log_file'];
Worker::$eventLoopClass = $config['event_loop'] ?? '';
TcpConnection::$defaultMaxPackageSize = $config['max_package_size'] ?? 10 * 1024 * 1024;
if (property_exists(Worker::class, 'statusFile')) {
Worker::$statusFile = $config['status_file'] ?? '';
}
if (property_exists(Worker::class, 'stopTimeout')) {
Worker::$stopTimeout = $config['stop_timeout'] ?? 2;
}
if ($config['listen'] ?? false) {
$worker = new Worker($config['listen'], $config['context']);
$propertyMap = [
'name',
'count',
'user',
'group',
'reusePort',
'transport',
'protocol'
];
foreach ($propertyMap as $property) {
if (isset($config[$property])) {
$worker->$property = $config[$property];
}
}
$worker->onWorkerStart = function ($worker) {
require_once base_path() . '/support/bootstrap.php';
$app = new \Webman\App(config('app.request_class', Request::class), Log::channel('default'), app_path(), public_path());
$worker->onMessage = [$app, 'onMessage'];
call_user_func([$app, 'onWorkerStart'], $worker);
};
}
$windowsWithoutServerListen = is_phar() && DIRECTORY_SEPARATOR === '\\' && empty($config['listen']);
$process = config('process', []);
if ($windowsWithoutServerListen && $process) {
$processName = isset($process['webman']) ? 'webman' : key($process);
worker_start($processName, $process[$processName]);
} else if (DIRECTORY_SEPARATOR === '/') {
foreach (config('process', []) as $processName => $config) {
worker_start($processName, $config);
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['process'] ?? [] as $processName => $config) {
worker_start("plugin.$firm.$name.$processName", $config);
}
}
foreach ($projects['process'] ?? [] as $processName => $config) {
worker_start("plugin.$firm.$processName", $config);
}
}
}
Worker::runAll();
}
/**
* LoadAllConfig.
* @param array $excludes
* @return void
*/
public static function loadAllConfig(array $excludes = [])
{
Config::load(config_path(), $excludes);
$directory = base_path() . '/plugin';
foreach (Util::scanDir($directory, false) as $name) {
$dir = "$directory/$name/config";
if (is_dir($dir)) {
Config::load($dir, $excludes, "plugin.$name");
}
}
}
}
@@ -0,0 +1,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
{
}
+139
View File
@@ -0,0 +1,139 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\FormattableHandlerInterface;
use Monolog\Handler\HandlerInterface;
use Monolog\Logger;
use function array_values;
use function config;
use function is_array;
/**
* Class Log
* @package support
*
* @method static void log($level, $message, array $context = [])
* @method static void debug($message, array $context = [])
* @method static void info($message, array $context = [])
* @method static void notice($message, array $context = [])
* @method static void warning($message, array $context = [])
* @method static void error($message, array $context = [])
* @method static void critical($message, array $context = [])
* @method static void alert($message, array $context = [])
* @method static void emergency($message, array $context = [])
*/
class Log
{
/**
* @var array
*/
protected static $instance = [];
/**
* Channel.
* @param string $name
* @return Logger
*/
public static function channel(string $name = 'default'): Logger
{
if (!isset(static::$instance[$name])) {
$config = config('log', [])[$name];
$handlers = self::handlers($config);
$processors = self::processors($config);
static::$instance[$name] = new Logger($name, $handlers, $processors);
}
return static::$instance[$name];
}
/**
* Handlers.
* @param array $config
* @return array
*/
protected static function handlers(array $config): array
{
$handlerConfigs = $config['handlers'] ?? [[]];
$handlers = [];
foreach ($handlerConfigs as $value) {
$class = $value['class'] ?? [];
$constructor = $value['constructor'] ?? [];
$formatterConfig = $value['formatter'] ?? [];
$class && $handlers[] = self::handler($class, $constructor, $formatterConfig);
}
return $handlers;
}
/**
* Handler.
* @param string $class
* @param array $constructor
* @param array $formatterConfig
* @return HandlerInterface
*/
protected static function handler(string $class, array $constructor, array $formatterConfig): HandlerInterface
{
/** @var HandlerInterface $handler */
$handler = new $class(... array_values($constructor));
if ($handler instanceof FormattableHandlerInterface && $formatterConfig) {
$formatterClass = $formatterConfig['class'];
$formatterConstructor = $formatterConfig['constructor'];
/** @var FormatterInterface $formatter */
$formatter = new $formatterClass(... array_values($formatterConstructor));
$handler->setFormatter($formatter);
}
return $handler;
}
/**
* Processors.
* @param array $config
* @return array
*/
protected static function processors(array $config): array
{
$result = [];
if (!isset($config['processors']) && isset($config['processor'])) {
$config['processors'] = [$config['processor']];
}
foreach ($config['processors'] ?? [] as $value) {
if (is_array($value) && isset($value['class'])) {
$value = new $value['class'](... array_values($value['constructor'] ?? []));
}
$result[] = $value;
}
return $result;
}
/**
* @param string $name
* @param array $arguments
* @return mixed
*/
public static function __callStatic(string $name, array $arguments)
{
return static::channel()->{$name}(... $arguments);
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace support;
use function defined;
use function is_callable;
use function is_file;
use function method_exists;
class Plugin
{
/**
* Install.
* @param mixed $event
* @return void
*/
public static function install($event)
{
static::findHelper();
$psr4 = static::getPsr4($event);
foreach ($psr4 as $namespace => $path) {
$pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
if (!defined($pluginConst)) {
continue;
}
$installFunction = "\\{$namespace}Install::install";
if (is_callable($installFunction)) {
$installFunction(true);
}
}
}
/**
* Update.
* @param mixed $event
* @return void
*/
public static function update($event)
{
static::findHelper();
$psr4 = static::getPsr4($event);
foreach ($psr4 as $namespace => $path) {
$pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
if (!defined($pluginConst)) {
continue;
}
$updateFunction = "\\{$namespace}Install::update";
if (is_callable($updateFunction)) {
$updateFunction();
continue;
}
$installFunction = "\\{$namespace}Install::install";
if (is_callable($installFunction)) {
$installFunction(false);
}
}
}
/**
* Uninstall.
* @param mixed $event
* @return void
*/
public static function uninstall($event)
{
static::findHelper();
$psr4 = static::getPsr4($event);
foreach ($psr4 as $namespace => $path) {
$pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
if (!defined($pluginConst)) {
continue;
}
$uninstallFunction = "\\{$namespace}Install::uninstall";
if (is_callable($uninstallFunction)) {
$uninstallFunction();
}
}
}
/**
* Get psr-4 info
*
* @param mixed $event
* @return array
*/
protected static function getPsr4($event)
{
$operation = $event->getOperation();
$autoload = method_exists($operation, 'getPackage') ? $operation->getPackage()->getAutoload() : $operation->getTargetPackage()->getAutoload();
return $autoload['psr-4'] ?? [];
}
/**
* FindHelper.
* @return void
*/
protected static function findHelper()
{
// Plugin.php in webman
require_once __DIR__ . '/helpers.php';
}
}
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
/**
* Class Request
* @package support
*/
class Request extends \Webman\Http\Request
{
}
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
/**
* Class Response
* @package support
*/
class Response extends \Webman\Http\Response
{
}
@@ -0,0 +1,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 = $config['loader'] ?? [
'Symfony\Component\Translation\Loader\PhpFileLoader' => [
'extension' => '.php',
'format' => 'phpfile'
],
'Symfony\Component\Translation\Loader\PoFileLoader' => [
'extension' => '.po',
'format' => 'pofile'
]
];
foreach ($paths as $path) {
// Phar support. Compatible with the 'realpath' function in the phar file.
if (!$translationsPath = get_realpath($path)) {
throw new NotFoundException("File {$path} not found");
}
foreach ($classes as $class => $opts) {
$translator->addLoader($opts['format'], new $class);
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($translationsPath, FilesystemIterator::SKIP_DOTS));
$files = new RegexIterator($iterator, '/^.+' . preg_quote($opts['extension']) . '$/i', RegexIterator::GET_MATCH);
foreach ($files as $file) {
$file = $file[0];
$domain = basename($file, $opts['extension']);
$dirName = pathinfo($file, PATHINFO_DIRNAME);
$locale = substr(strrchr($dirName, DIRECTORY_SEPARATOR), 1);
if ($domain && $locale) {
$translator->addResource($opts['format'], $file, $locale, $domain);
}
}
}
}
}
return static::$instance[$plugin];
}
/**
* @param string $name
* @param array $arguments
* @return mixed
* @throws NotFoundException
*/
public static function __callStatic(string $name, array $arguments)
{
$request = request();
$plugin = $request->plugin ?? '';
return static::instance($plugin)->{$name}(... $arguments);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
use function config;
use function request;
class View
{
/**
* Assign.
* @param mixed $name
* @param mixed $value
* @return void
*/
public static function assign($name, mixed $value = null)
{
$request = request();
$plugin = $request->plugin ?? '';
$handler = config($plugin ? "plugin.$plugin.view.handler" : 'view.handler');
$handler::assign($name, $value);
}
}
@@ -0,0 +1,10 @@
<?php
namespace support\annotation;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class DisableDefaultRoute extends \Webman\Annotation\DisableDefaultRoute
{
}
@@ -0,0 +1,11 @@
<?php
namespace support\annotation;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class Middleware extends \Webman\Annotation\Middleware
{
}
@@ -0,0 +1,139 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Dotenv\Dotenv;
use support\Log;
use Webman\Bootstrap;
use Webman\Config;
use Webman\Middleware;
use Webman\Route;
use Webman\Util;
use Workerman\Events\Select;
use Workerman\Worker;
$worker = $worker ?? null;
if (empty(Worker::$eventLoopClass)) {
Worker::$eventLoopClass = Select::class;
}
set_error_handler(function ($level, $message, $file = '', $line = 0) {
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
});
if ($worker) {
register_shutdown_function(function ($startTime) {
if (time() - $startTime <= 0.1) {
sleep(1);
}
}, time());
}
if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) {
if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) {
Dotenv::createUnsafeMutable(base_path(false))->load();
} else {
Dotenv::createMutable(base_path(false))->load();
}
}
Config::clear();
support\App::loadAllConfig(['route']);
if ($timezone = config('app.default_timezone')) {
date_default_timezone_set($timezone);
}
foreach (config('autoload.files', []) as $file) {
include_once $file;
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['autoload']['files'] ?? [] as $file) {
include_once $file;
}
}
foreach ($projects['autoload']['files'] ?? [] as $file) {
include_once $file;
}
}
Middleware::load(config('middleware', []));
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project) || $name === 'static') {
continue;
}
Middleware::load($project['middleware'] ?? []);
}
Middleware::load($projects['middleware'] ?? [], $firm);
if ($staticMiddlewares = config("plugin.$firm.static.middleware")) {
Middleware::load(['__static__' => $staticMiddlewares], $firm);
}
}
Middleware::load(['__static__' => config('static.middleware', [])]);
foreach (config('bootstrap', []) as $className) {
if (!class_exists($className)) {
$log = "Warning: Class $className setting in config/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['bootstrap'] ?? [] as $className) {
if (!class_exists($className)) {
$log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
}
foreach ($projects['bootstrap'] ?? [] as $className) {
/** @var string $className */
if (!class_exists($className)) {
$log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
}
$directory = base_path() . '/plugin';
$paths = [config_path()];
foreach (Util::scanDir($directory) as $path) {
if (is_dir($path = "$path/config")) {
$paths[] = $path;
}
}
Route::load($paths);
@@ -0,0 +1,61 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support\bootstrap;
use Webman\Bootstrap;
use Workerman\Protocols\Http;
use Workerman\Protocols\Http\Session as SessionBase;
use Workerman\Worker;
use function config;
use function property_exists;
/**
* Class Session
* @package support
*/
class Session implements Bootstrap
{
/**
* @param Worker|null $worker
* @return void
*/
public static function start(?Worker $worker)
{
$config = config('session');
if (property_exists(SessionBase::class, 'name')) {
SessionBase::$name = $config['session_name'];
} else {
Http::sessionName($config['session_name']);
}
SessionBase::handlerClass($config['handler'], $config['config'][$config['type']]);
$map = [
'auto_update_timestamp' => 'autoUpdateTimestamp',
'cookie_lifetime' => 'cookieLifetime',
'gc_probability' => 'gcProbability',
'cookie_path' => 'cookiePath',
'http_only' => 'httpOnly',
'same_site' => 'sameSite',
'lifetime' => 'lifetime',
'domain' => 'domain',
'secure' => 'secure',
];
foreach ($map as $key => $name) {
if (isset($config[$key]) && property_exists(SessionBase::class, $name)) {
SessionBase::${$name} = $config[$key];
}
}
}
}
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support\exception;
/**
* Class BusinessException
* @package support\exception
*/
class BusinessException extends \Webman\Exception\BusinessException
{
}
@@ -0,0 +1,43 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support\exception;
use Throwable;
use Webman\Exception\ExceptionHandler;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\Exception\BusinessException;
/**
* Class Handler
* @package support\exception
*/
class Handler extends ExceptionHandler
{
public $dontReport = [
BusinessException::class,
];
public function report(Throwable $exception)
{
parent::report($exception);
}
public function render(Request $request, Throwable $exception): Response
{
return parent::render($request, $exception);
}
}
@@ -0,0 +1,24 @@
<?php
namespace support\exception;
use Throwable;
class InputTypeException extends PageNotFoundException
{
/**
* @var string
*/
protected $template = '/app/view/400';
/**
* InputTypeException constructor.
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = 'Input :parameter must be of type :exceptType, :actualType given', int $code = 400, ?Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,24 @@
<?php
namespace support\exception;
use Throwable;
class InputValueException extends PageNotFoundException
{
/**
* @var string
*/
protected $template = '/app/view/400';
/**
* InputTypeException constructor.
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = 'Input :parameter is invalid.', int $code = 400, ?Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}
@@ -0,0 +1,46 @@
<?php
namespace support\exception;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
class MissingInputException extends PageNotFoundException
{
/**
* @var string
*/
protected $template = '/app/view/400';
/**
* MissingInputException constructor.
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = 'Missing input parameter :parameter', int $code = 400, ?Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
/**
* Render an exception into an HTTP response.
* @param Request $request
* @return Response|null
* @throws Throwable
*/
public function render(Request $request): ?Response
{
$code = $this->getCode() ?: 404;
$debug = config($request->plugin ? "plugin.$request->plugin.app.debug" : 'app.debug');
$data = $debug ? $this->data : ['parameter' => ''];
$message = $this->trans($this->getMessage(), $data);
if ($request->expectsJson()) {
$json = ['code' => $code, 'msg' => $message, 'data' => $data];
return new Response(200, ['Content-Type' => 'application/json'],
json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
return new Response($code, [], $this->html($message));
}
}
@@ -0,0 +1,20 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support\exception;
class NotFoundException extends BusinessException
{
}
@@ -0,0 +1,93 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support\exception;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
class PageNotFoundException extends NotFoundException
{
/**
* @var string
*/
protected $template = '/app/view/404';
/**
* PageNotFoundException constructor.
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = '404 Not Found', int $code = 404, ?Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
/**
* Render an exception into an HTTP response.
* @param Request $request
* @return Response|null
* @throws Throwable
*/
public function render(Request $request): ?Response
{
$code = $this->getCode() ?: 404;
$data = $this->data;
$message = $this->trans($this->getMessage(), $data);
if ($request->expectsJson()) {
$json = ['code' => $code, 'msg' => $message, 'data' => $data];
return new Response(200, ['Content-Type' => 'application/json'],
json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
return new Response($code, [], $this->html($message));
}
/**
* Get the HTML representation of the exception.
* @param string $message
* @return string
* @throws Throwable
*/
protected function html(string $message): string
{
$message = htmlspecialchars($message);
if (is_file(base_path("$this->template.html"))) {
return raw_view($this->template, ['message' => $message])->rawBody();
}
return <<<EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>$message</title>
<style>
.center {
text-align: center;
}
</style>
</head>
<body>
<h1 class="center">$message</h1>
<hr>
<div class="center">webman</div>
</body>
</html>
EOF;
}
}
@@ -0,0 +1,672 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\Container;
use support\Request;
use support\Response;
use support\Translation;
use support\view\Blade;
use support\view\Raw;
use support\view\ThinkPHP;
use support\view\Twig;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;
use Webman\App;
use Webman\Config;
use Webman\Route;
use Workerman\Protocols\Http\Session;
use Workerman\Worker;
/**
* Get the base path of the application
*/
if (!defined('BASE_PATH')) {
if (!$basePath = Phar::running()) {
$basePath = getcwd();
while ($basePath !== dirname($basePath)) {
if (is_dir("$basePath/vendor") && is_file("$basePath/start.php")) {
break;
}
$basePath = dirname($basePath);
}
if ($basePath === dirname($basePath)) {
$basePath = __DIR__ . '/../../../../../';
}
}
define('BASE_PATH', realpath($basePath) ?: $basePath);
}
if (!function_exists('run_path')) {
/**
* return the program execute directory
* @param string $path
* @return string
*/
function run_path(string $path = ''): string
{
static $runPath = '';
if (!$runPath) {
$runPath = is_phar() ? dirname(Phar::running(false)) : BASE_PATH;
}
return path_combine($runPath, $path);
}
}
if (!function_exists('base_path')) {
/**
* if the param $path equal false,will return this program current execute directory
* @param string|false $path
* @return string
*/
function base_path($path = ''): string
{
if (false === $path) {
return run_path();
}
return path_combine(BASE_PATH, $path);
}
}
if (!function_exists('app_path')) {
/**
* App path
* @param string $path
* @return string
*/
function app_path(string $path = ''): string
{
return path_combine(BASE_PATH . DIRECTORY_SEPARATOR . 'app', $path);
}
}
if (!function_exists('public_path')) {
/**
* Public path
* @param string $path
* @param string|null $plugin
* @return string
*/
function public_path(string $path = '', ?string $plugin = null): string
{
static $publicPaths = [];
$plugin = $plugin ?? '';
if (isset($publicPaths[$plugin])) {
$publicPath = $publicPaths[$plugin];
} else {
$prefix = $plugin ? "plugin.$plugin." : '';
$pathPrefix = $plugin ? 'plugin' . DIRECTORY_SEPARATOR . $plugin . DIRECTORY_SEPARATOR : '';
$publicPath = \config("{$prefix}app.public_path", run_path("{$pathPrefix}public"));
if (count($publicPaths) > 32) {
$publicPaths = [];
}
$publicPaths[$plugin] = $publicPath;
}
return $path === '' ? $publicPath : path_combine($publicPath, $path);
}
}
if (!function_exists('config_path')) {
/**
* Config path
* @param string $path
* @return string
*/
function config_path(string $path = ''): string
{
return path_combine(BASE_PATH . DIRECTORY_SEPARATOR . 'config', $path);
}
}
if (!function_exists('runtime_path')) {
/**
* Runtime path
* @param string $path
* @return string
*/
function runtime_path(string $path = ''): string
{
static $runtimePath = '';
if (!$runtimePath) {
$runtimePath = \config('app.runtime_path') ?: run_path('runtime');
}
return path_combine($runtimePath, $path);
}
}
if (!function_exists('path_combine')) {
/**
* Generate paths based on given information
* @param string $front
* @param string $back
* @return string
*/
function path_combine(string $front, string $back): string
{
return $front . ($back ? (DIRECTORY_SEPARATOR . ltrim($back, DIRECTORY_SEPARATOR)) : $back);
}
}
if (!function_exists('response')) {
/**
* Response
* @param int $status
* @param array $headers
* @param string $body
* @return Response
*/
function response(string $body = '', int $status = 200, array $headers = []): Response
{
return new Response($status, $headers, $body);
}
}
if (!function_exists('json')) {
/**
* Json response
* @param $data
* @param int $options
* @return Response
*/
function json($data, int $options = JSON_UNESCAPED_UNICODE): 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);
}
}
@@ -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();
}
}

Some files were not shown because too many files have changed in this diff Show More