This commit is contained in:
2026-05-01 23:40:14 +08:00
commit b8f599a617
3867 changed files with 478663 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`.
+32
View File
@@ -0,0 +1,32 @@
{
"name": "workerman/coroutine",
"type": "library",
"license": "MIT",
"description": "Workerman coroutine",
"require": {
"php": ">=8.1",
"workerman/workerman": "^5.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();
+63
View File
@@ -0,0 +1,63 @@
<?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();
if (!$context) {
return $default;
}
$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 ? $data->getArrayCopy() : []);
}
/**
* @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();
}
}
}
@@ -0,0 +1,8 @@
<?php
namespace Workerman\Coroutine\Exception;
class PoolException extends \RuntimeException
{
}
+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 stdClass;
use Throwable;
use WeakMap;
use Workerman\Coroutine;
use Workerman\Coroutine\Exception\PoolException;
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 PoolException("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 PoolException('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 PoolException('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 PoolException('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);
}
}
+394
View File
@@ -0,0 +1,394 @@
<?php
namespace test;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
use ReflectionProperty;
use Workerman\Coroutine;
use Workerman\Coroutine\Exception\PoolException;
use Workerman\Coroutine\Pool;
use Psr\Log\LoggerInterface;
use ReflectionClass;
use stdClass;
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(PoolException::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->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)
{
$prop = new ReflectionProperty($object, $property);
return $prop->getValue($object);
}
private function setPrivateProperty($object, string $property, $value)
{
$prop = new ReflectionProperty($object, $property);
$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.');
}
}
+109
View File
@@ -0,0 +1,109 @@
<?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';
$phpunitDisplayOptions = [
'--colors=always',
'--display-deprecations',
'--display-phpunit-deprecations',
'--display-errors',
'--display-notices',
'--display-warnings',
'--display-incomplete',
'--display-skipped',
];
if (DIRECTORY_SEPARATOR === '/' || (!extension_loaded('swow') && !class_exists(Revolt\EventLoop::class))) {
create_test_worker(function () use ($phpunitDisplayOptions) {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
...$phpunitDisplayOptions,
__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 () use ($phpunitDisplayOptions) {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
...$phpunitDisplayOptions,
__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 () use ($phpunitDisplayOptions) {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
...$phpunitDisplayOptions,
...glob(__DIR__ . '/*Test.php')
]);
}, Fiber::class);
}
if (extension_loaded('Swoole')) {
create_test_worker(function () use ($phpunitDisplayOptions) {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
...$phpunitDisplayOptions,
...glob(__DIR__ . '/*Test.php')
]);
}, Swoole::class);
}
if (extension_loaded('Swow')) {
create_test_worker(function () use ($phpunitDisplayOptions) {
(new PHPUnit\TextUI\Application)->run([
__DIR__ . '/../vendor/bin/phpunit',
...$phpunitDisplayOptions,
...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": "^2.0 || ^3.0"
},
"suggest": {
"ext-event": "For better performance. "
},
"autoload": {
"psr-4": {
"Webman\\": "./src",
"support\\": "./src/support",
"Support\\": "./src/support",
"Support\\Bootstrap\\": "./src/support/bootstrap",
"Support\\Exception\\": "./src/support/exception",
"Support\\View\\": "./src/support/view"
},
"files": [
"./src/support/helpers.php"
]
},
"minimum-stability": "dev",
"prefer-stable": true
}
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use Workerman\Worker;
interface Bootstrap
{
/**
* onWorkerStart
*
* @param Worker|null $worker
* @return mixed
*/
public static function start(?Worker $worker);
}
+321
View File
@@ -0,0 +1,321 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use function array_replace_recursive;
use function array_reverse;
use function count;
use function explode;
use function in_array;
use function is_array;
use function is_dir;
use function is_file;
use function key;
use function str_replace;
class Config
{
/**
* @var array
*/
protected static $config = [];
/**
* @var string
*/
protected static $configPath = '';
/**
* @var bool
*/
protected static $loaded = false;
/**
* @var array Flat cache for repeated get() lookups.
*/
protected static $flatCache = [];
/**
* Load.
* @param string $configPath
* @param array $excludeFile
* @param string|null $key
* @return void
*/
public static function load(string $configPath, array $excludeFile = [], ?string $key = null)
{
static::$configPath = $configPath;
static::$flatCache = [];
if (!$configPath) {
return;
}
static::$loaded = false;
$config = static::loadFromDir($configPath, $excludeFile);
if (!$config) {
static::$loaded = true;
return;
}
if ($key !== null) {
foreach (array_reverse(explode('.', $key)) as $k) {
$config = [$k => $config];
}
}
static::$config = array_replace_recursive(static::$config, $config);
static::formatConfig();
static::$loaded = true;
}
/**
* This deprecated method will certainly be removed in the future.
* @param string $configPath
* @param array $excludeFile
* @return void
* @deprecated
*/
public static function reload(string $configPath, array $excludeFile = [])
{
static::load($configPath, $excludeFile);
}
/**
* Clear.
* @return void
*/
public static function clear()
{
static::$config = [];
static::$flatCache = [];
}
/**
* FormatConfig.
* @return void
*/
protected static function formatConfig()
{
$config = static::$config;
// Merge log config
foreach ($config['plugin'] ?? [] as $firm => $projects) {
if (isset($projects['app'])) {
foreach ($projects['log'] ?? [] as $key => $item) {
$config['log']["plugin.$firm.$key"] = $item;
}
}
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['log'] ?? [] as $key => $item) {
$config['log']["plugin.$firm.$name.$key"] = $item;
}
}
}
// Merge database config
foreach ($config['plugin'] ?? [] as $firm => $projects) {
if (isset($projects['app'])) {
foreach ($projects['database']['connections'] ?? [] as $key => $connection) {
$config['database']['connections']["plugin.$firm.$key"] = $connection;
}
}
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['database']['connections'] ?? [] as $key => $connection) {
$config['database']['connections']["plugin.$firm.$name.$key"] = $connection;
}
}
}
if (!empty($config['database']['connections'])) {
$config['database']['default'] = $config['database']['default'] ?? key($config['database']['connections']);
}
// Merge thinkorm config
foreach ($config['plugin'] ?? [] as $firm => $projects) {
if (isset($projects['app'])) {
foreach ($projects['thinkorm']['connections'] ?? [] as $key => $connection) {
$config['thinkorm']['connections']["plugin.$firm.$key"] = $connection;
}
foreach ($projects['think-orm']['connections'] ?? [] as $key => $connection) {
$config['think-orm']['connections']["plugin.$firm.$key"] = $connection;
}
}
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['thinkorm']['connections'] ?? [] as $key => $connection) {
$config['thinkorm']['connections']["plugin.$firm.$name.$key"] = $connection;
}
foreach ($project['think-orm']['connections'] ?? [] as $key => $connection) {
$config['think-orm']['connections']["plugin.$firm.$name.$key"] = $connection;
}
}
}
if (!empty($config['thinkorm']['connections'])) {
$config['thinkorm']['default'] = $config['thinkorm']['default'] ?? key($config['thinkorm']['connections']);
}
if (!empty($config['think-orm']['connections'])) {
$config['think-orm']['default'] = $config['think-orm']['default'] ?? key($config['think-orm']['connections']);
}
// Merge redis config
foreach ($config['plugin'] ?? [] as $firm => $projects) {
if (isset($projects['app'])) {
foreach ($projects['redis'] ?? [] as $key => $connection) {
$config['redis']["plugin.$firm.$key"] = $connection;
}
}
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['redis'] ?? [] as $key => $connection) {
$config['redis']["plugin.$firm.$name.$key"] = $connection;
}
}
}
static::$config = $config;
}
/**
* LoadFromDir.
* @param string $configPath
* @param array $excludeFile
* @return array
*/
public static function loadFromDir(string $configPath, array $excludeFile = []): array
{
$allConfig = [];
$dirIterator = new RecursiveDirectoryIterator($configPath, FilesystemIterator::FOLLOW_SYMLINKS);
$iterator = new RecursiveIteratorIterator($dirIterator);
foreach ($iterator as $file) {
/** var SplFileInfo $file */
if (is_dir($file) || $file->getExtension() != 'php' || in_array($file->getBaseName('.php'), $excludeFile)) {
continue;
}
$appConfigFile = $file->getPath() . '/app.php';
if (!is_file($appConfigFile)) {
continue;
}
$relativePath = str_replace($configPath . DIRECTORY_SEPARATOR, '', substr($file, 0, -4));
$explode = array_reverse(explode(DIRECTORY_SEPARATOR, $relativePath));
if (count($explode) >= 2) {
$appConfig = include $appConfigFile;
if (empty($appConfig['enable'])) {
continue;
}
}
$config = include $file;
foreach ($explode as $section) {
$tmp = [];
$tmp[$section] = $config;
$config = $tmp;
}
$allConfig = array_replace_recursive($allConfig, $config);
}
return $allConfig;
}
/**
* Get.
* @param string|null $key
* @param mixed $default
* @return mixed
*/
public static function get(?string $key = null, mixed $default = null)
{
if ($key === null) {
return static::$config;
}
if (isset(static::$flatCache[$key])) {
return static::$flatCache[$key];
}
$keyArray = explode('.', $key);
$value = static::$config;
$found = true;
foreach ($keyArray as $index) {
if (!isset($value[$index])) {
if (static::$loaded) {
return $default;
}
$found = false;
break;
}
$value = $value[$index];
}
if ($found) {
if (static::$loaded) {
static::$flatCache[$key] = $value;
if (count(static::$flatCache) > 1024) {
unset(static::$flatCache[key(static::$flatCache)]);
}
}
return $value;
}
return static::read($key, $default);
}
/**
* Read.
* @param string $key
* @param mixed $default
* @return mixed
*/
protected static function read(string $key, mixed $default = null)
{
$path = static::$configPath;
if ($path === '') {
return $default;
}
$keys = $keyArray = explode('.', $key);
foreach ($keyArray as $index => $section) {
unset($keys[$index]);
if (is_file($file = "$path/$section.php")) {
$config = include $file;
return static::find($keys, $config, $default);
}
if (!is_dir($path = "$path/$section")) {
return $default;
}
}
return $default;
}
/**
* Find.
* @param array $keyArray
* @param mixed $stack
* @param mixed $default
* @return array|mixed
*/
protected static function find(array $keyArray, $stack, $default)
{
if (!is_array($stack)) {
return $default;
}
$value = $stack;
foreach ($keyArray as $index) {
if (!isset($value[$index])) {
return $default;
}
$value = $value[$index];
}
return $value;
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace Webman;
use Psr\Container\ContainerInterface;
use Webman\Exception\NotFoundException;
use function array_key_exists;
use function class_exists;
/**
* Class Container
* @package Webman
*/
class Container implements ContainerInterface
{
/**
* @var array
*/
protected $instances = [];
/**
* @var array
*/
protected $definitions = [];
/**
* Get.
* @param string $name
* @return mixed
* @throws NotFoundException
*/
public function get(string $name)
{
if (!isset($this->instances[$name])) {
if (isset($this->definitions[$name])) {
$this->instances[$name] = call_user_func($this->definitions[$name], $this);
} else {
if (!class_exists($name)) {
throw new NotFoundException("Class '$name' not found");
}
$this->instances[$name] = new $name();
}
}
return $this->instances[$name];
}
/**
* Has.
* @param string $name
* @return bool
*/
public function has(string $name): bool
{
return array_key_exists($name, $this->instances)
|| array_key_exists($name, $this->definitions);
}
/**
* Make.
* @param string $name
* @param array $constructor
* @return mixed
* @throws NotFoundException
*/
public function make(string $name, array $constructor = [])
{
if (!class_exists($name)) {
throw new NotFoundException("Class '$name' not found");
}
return new $name(... array_values($constructor));
}
/**
* AddDefinitions.
* @param array $definitions
* @return $this
*/
public function addDefinitions(array $definitions): Container
{
$this->definitions = array_merge($this->definitions, $definitions);
return $this;
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Webman;
use Workerman\Coroutine\Context as WorkermanContext;
use Workerman\Coroutine\Utils\DestructionWatcher;
use Closure;
/**
* Class Context
* @package Webman
*/
class Context extends WorkermanContext
{
public static function onDestroy(Closure $closure): void
{
$obj = static::get('context.onDestroy');
if (!$obj) {
$obj = new \stdClass();
static::set('context.onDestroy', $obj);
}
DestructionWatcher::watch($obj, $closure);
}
}
@@ -0,0 +1,117 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Exception;
use RuntimeException;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
use function json_encode;
/**
* Class BusinessException
* @package support\exception
*/
class BusinessException extends RuntimeException
{
/**
* @var array
*/
protected $data = [];
/**
* @var bool
*/
protected $debug = false;
/**
* Render an exception into an HTTP response.
* @param Request $request
* @return Response|null
*/
public function render(Request $request): ?Response
{
if ($request->expectsJson()) {
$code = $this->getCode();
$json = ['code' => $code ?: 500, 'msg' => $this->getMessage(), 'data' => $this->data];
return new Response(200, ['Content-Type' => 'application/json'],
json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
return new Response(200, [], $this->getMessage());
}
/**
* Set data.
* @param array|null $data
* @return array|$this
*/
public function data(?array $data = null): array|static
{
if ($data === null) {
return $this->data;
}
$this->data = $data;
return $this;
}
/**
* Set debug.
* @param bool|null $value
* @return $this|bool
*/
public function debug(?bool $value = null): bool|static
{
if ($value === null) {
return $this->debug;
}
$this->debug = $value;
return $this;
}
/**
* Get data.
* @return array
*/
public function getData(): array
{
return $this->data;
}
/**
* Translate message.
* @param string $message
* @param array $parameters
* @param string|null $domain
* @param string|null $locale
* @return string
*/
protected function trans(string $message, array $parameters = [], ?string $domain = null, ?string $locale = null): string
{
$args = [];
foreach ($parameters as $key => $parameter) {
$args[":$key"] = $parameter;
}
try {
$message = trans($message, $args, $domain, $locale);
} catch (Throwable $e) {
}
foreach ($parameters as $key => $value) {
$message = str_replace(":$key", $value, $message);
}
return $message;
}
}
@@ -0,0 +1,121 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Exception;
use Psr\Log\LoggerInterface;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
use function json_encode;
use function nl2br;
use function trim;
/**
* Class Handler
* @package support\exception
*/
class ExceptionHandler implements ExceptionHandlerInterface
{
/**
* @var LoggerInterface
*/
protected $logger = null;
/**
* @var bool
*/
protected $debug = false;
/**
* @var array
*/
public $dontReport = [];
/**
* ExceptionHandler constructor.
* @param $logger
* @param $debug
*/
public function __construct($logger, $debug)
{
$this->logger = $logger;
$this->debug = $debug;
}
/**
* @param Throwable $exception
* @return void
*/
public function report(Throwable $exception)
{
if ($this->shouldntReport($exception)) {
return;
}
$logs = '';
if ($request = \request()) {
$logs = $request->getRealIp() . ' ' . $request->method() . ' ' . trim($request->fullUrl(), '/');
}
$this->logger->error($logs . PHP_EOL . $exception);
}
/**
* @param Request $request
* @param Throwable $exception
* @return Response
*/
public function render(Request $request, Throwable $exception): Response
{
if (method_exists($exception, 'render') && ($response = $exception->render($request))) {
return $response;
}
$code = $exception->getCode();
if ($request->expectsJson()) {
$json = ['code' => $code ?: 500, 'msg' => $this->debug ? $exception->getMessage() : 'Server internal error'];
$this->debug && $json['traces'] = (string)$exception;
return new Response(200, ['Content-Type' => 'application/json'],
json_encode($json, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
$error = $this->debug ? nl2br((string)$exception) : 'Server internal error';
return new Response(500, [], $error);
}
/**
* @param Throwable $e
* @return bool
*/
protected function shouldntReport(Throwable $e): bool
{
foreach ($this->dontReport as $type) {
if ($e instanceof $type) {
return true;
}
}
return false;
}
/**
* Compatible $this->_debug
*
* @param string $name
* @return bool|null
*/
public function __get(string $name)
{
if ($name === '_debug') {
return $this->debug;
}
return null;
}
}
@@ -0,0 +1,35 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Exception;
use Throwable;
use Webman\Http\Request;
use Webman\Http\Response;
interface ExceptionHandlerInterface
{
/**
* @param Throwable $exception
* @return mixed
*/
public function report(Throwable $exception);
/**
* @param Request $request
* @param Throwable $exception
* @return Response
*/
public function render(Request $request, Throwable $exception): Response;
}
@@ -0,0 +1,25 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Exception;
use RuntimeException;
/**
* Class FileException
* @package Webman\Exception
*/
class FileException extends RuntimeException
{
}
@@ -0,0 +1,25 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Exception;
use Psr\Container\NotFoundExceptionInterface;
/**
* Class NotFoundException
* @package Webman\Exception
*/
class NotFoundException extends \Exception implements NotFoundExceptionInterface
{
}
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use SplFileInfo;
use Webman\Exception\FileException;
use function chmod;
use function is_dir;
use function mkdir;
use function pathinfo;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use function strip_tags;
use function umask;
class File extends SplFileInfo
{
/**
* Move.
* @param string $destination
* @return File
*/
public function move(string $destination): File
{
set_error_handler(function ($type, $msg) use (&$error) {
$error = $msg;
});
$path = pathinfo($destination, PATHINFO_DIRNAME);
if (!is_dir($path) && !mkdir($path, 0777, true)) {
restore_error_handler();
throw new FileException(sprintf('Unable to create the "%s" directory (%s)', $path, strip_tags($error)));
}
if (!rename($this->getPathname(), $destination)) {
restore_error_handler();
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $destination, strip_tags($error)));
}
restore_error_handler();
@chmod($destination, 0666 & ~umask());
return new self($destination);
}
}
@@ -0,0 +1,235 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Finder;
use InvalidArgumentException;
use Webman\Config;
use function is_array;
use function is_dir;
use function preg_match;
use function preg_quote;
use function scandir;
use function str_starts_with;
/**
* ControllerFinder
*
* Discover controller files in main app and/or plugins.
*
* Scope examples:
* - null : main app only
* - '*' : main app + all enabled plugins
* - 'plugin.*' : all enabled plugins
* - 'plugin.xxx': single plugin (strict: throws when plugin directory/config missing)
*/
class ControllerFinder
{
/**
* Find controller files by scope.
*
* @param string|null $scope
* @return FileInfo[]
*/
public static function files(?string $scope = null): array
{
$roots = static::resolveRoots($scope);
if (!$roots) {
return [];
}
$resultsByPath = [];
foreach ($roots as $root) {
$dir = $root['dir'];
$suffix = $root['suffix'] ?? '';
$controllerFiles = static::findControllerFiles($dir, $suffix);
foreach ($controllerFiles as $file) {
$resultsByPath[$file->getPathname()] = $file;
}
}
return array_values($resultsByPath);
}
/**
* Resolve search roots by scope.
*
* @param string|null $scope
* @return array<int, array{dir: string, suffix: string}>
*/
protected static function resolveRoots(?string $scope): array
{
if ($scope === null) {
return static::mainAppRoots();
}
if ($scope === '*') {
return array_merge(static::mainAppRoots(), static::allPluginRoots());
}
if ($scope === 'plugin.*') {
return static::allPluginRoots();
}
if (str_starts_with($scope, 'plugin.')) {
$plugin = substr($scope, strlen('plugin.'));
if ($plugin === '' || $plugin === '*') {
throw new InvalidArgumentException("Invalid controller scope: $scope");
}
return static::singlePluginRoots($plugin);
}
throw new InvalidArgumentException("Invalid controller scope: $scope");
}
/**
* Main app roots.
*
* @return array<int, array{dir: string, suffix: string}>
*/
protected static function mainAppRoots(): array
{
$roots = [];
$appRoot = app_path();
if (is_dir($appRoot)) {
$roots[] = [
'dir' => $appRoot,
'suffix' => (string)Config::get('app.controller_suffix', ''),
];
}
return $roots;
}
/**
* Roots for all enabled plugins.
*
* Rule (A): if plugin app config is missing/empty, skip it silently.
*
* @return array<int, array{dir: string, suffix: string}>
*/
protected static function allPluginRoots(): array
{
$roots = [];
$pluginBase = base_path('plugin');
if (!is_dir($pluginBase)) {
return [];
}
foreach (scandir($pluginBase) ?: [] as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
if (!static::isValidIdentifier($entry)) {
continue;
}
$pluginDir = $pluginBase . DIRECTORY_SEPARATOR . $entry;
if (!is_dir($pluginDir)) {
continue;
}
// Only load enabled plugins (same semantics as Route::loadAnnotationRoutes()).
$pluginAppConfig = Config::get("plugin.$entry.app");
if (!$pluginAppConfig) {
continue;
}
$pluginAppDir = $pluginDir . DIRECTORY_SEPARATOR . 'app';
if (!is_dir($pluginAppDir)) {
continue;
}
$roots[] = [
'dir' => $pluginAppDir,
'suffix' => is_array($pluginAppConfig)
? (string)($pluginAppConfig['controller_suffix'] ?? '')
: (string)Config::get("plugin.$entry.app.controller_suffix", ''),
];
}
return $roots;
}
/**
* Roots for a single plugin (strict).
*
* @param string $plugin
* @return array<int, array{dir: string, suffix: string}>
*/
protected static function singlePluginRoots(string $plugin): array
{
if (!static::isValidIdentifier($plugin)) {
throw new InvalidArgumentException("Invalid plugin identifier: $plugin");
}
$pluginBase = base_path('plugin');
$pluginDir = $pluginBase . DIRECTORY_SEPARATOR . $plugin;
if (!is_dir($pluginDir)) {
throw new InvalidArgumentException("Plugin directory not found: $plugin");
}
$pluginAppConfig = Config::get("plugin.$plugin.app");
if (!$pluginAppConfig) {
throw new InvalidArgumentException("Plugin app config not found or empty: plugin.$plugin.app");
}
$pluginAppDir = $pluginDir . DIRECTORY_SEPARATOR . 'app';
if (!is_dir($pluginAppDir)) {
throw new InvalidArgumentException("Plugin app directory not found: plugin/$plugin/app");
}
return [[
'dir' => $pluginAppDir,
'suffix' => is_array($pluginAppConfig)
? (string)($pluginAppConfig['controller_suffix'] ?? '')
: (string)Config::get("plugin.$plugin.app.controller_suffix", ''),
]];
}
/**
* Find controller files.
*
* @param string $rootDir
* @param string $controllerSuffix
* @return FileInfo[]
*/
protected static function findControllerFiles(string $rootDir, string $controllerSuffix = ''): array
{
$controllerPathRegex = $controllerSuffix !== ''
? ('/(^|[\/\\\\])controller[\/\\\\].*' . preg_quote($controllerSuffix, '/') . '\.php$/i')
: '/(^|[\/\\\\])controller[\/\\\\].+\.php$/i';
$finder = Finder::in($rootDir)
->files()
->path($controllerPathRegex)
->hasAttributes(true)
->typeIn(['class'])
->psr4(true);
return $finder->find();
}
/**
* Is valid identifier (plugin name).
*
* @param string $name
* @return bool
*/
protected static function isValidIdentifier(string $name): bool
{
return $name !== '' && (bool)preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name);
}
}
@@ -0,0 +1,142 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Finder;
use Webman\File;
/**
* Class FileInfo
* @package Webman\Finder
*/
class FileInfo extends File
{
/**
* @var array PHP file meta info (hasAttributes, type, class, psr4, etc.)
*/
protected array $meta = [];
/**
* @var string Root directory this file belongs to
*/
protected string $rootDir = '';
// Root namespace is intentionally not stored.
/**
* Constructor.
* @param string $path
* @param array $meta
* @param string $rootDir
*/
public function __construct(string $path, array $meta = [], string $rootDir = '')
{
parent::__construct($path);
$this->meta = $meta;
$this->rootDir = $rootDir;
}
/**
* Get PHP file meta info.
* @return array
*/
public function meta(): array
{
return $this->meta;
}
/**
* Get declared class (FQCN) from meta.
* Example: "App\\Controller\\IndexController"
*
* @return string|null
*/
public function class(): ?string
{
$class = $this->meta['class'] ?? null;
if (!is_string($class) || $class === '') {
return null;
}
return ltrim($class, '\\');
}
/**
* Get declared short class name (without namespace).
* Example: "IndexController"
*
* @return string|null
*/
public function className(): ?string
{
$fqcn = $this->class();
if ($fqcn === null) {
return null;
}
$pos = strrpos($fqcn, '\\');
return $pos === false ? $fqcn : substr($fqcn, $pos + 1);
}
/**
* Get declared namespace.
* Example: "App\\Controller"
*
* @return string|null
*/
public function namespace(): ?string
{
$fqcn = $this->class();
if ($fqcn === null) {
return null;
}
$pos = strrpos($fqcn, '\\');
return $pos === false ? null : substr($fqcn, 0, $pos);
}
/**
* Set meta info.
* @param array $meta
* @return $this
*/
public function setMeta(array $meta): static
{
$this->meta = $meta;
return $this;
}
/**
* Get root directory.
* @return string
*/
public function rootDir(): string
{
return $this->rootDir;
}
/**
* Get relative pathname from root directory.
* @return string
*/
public function relativePathname(): string
{
if ($this->rootDir === '') {
return $this->getPathname();
}
$rootDir = rtrim(str_replace('\\', '/', $this->rootDir), '/');
$pathname = str_replace('\\', '/', $this->getPathname());
$rootLen = strlen($rootDir);
if (strncasecmp($pathname, $rootDir, $rootLen) === 0) {
return ltrim(substr($pathname, $rootLen), '/');
}
return $this->getPathname();
}
}
+905
View File
@@ -0,0 +1,905 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Finder;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RecursiveCallbackFilterIterator;
/**
* Class Finder
* File finder with PHP file caching support.
* @package Webman\Finder
*/
class Finder
{
/**
* @var string[] Root directories
*/
protected array $roots = [];
/**
* @var bool Only match files (not directories)
*/
protected bool $onlyFiles = false;
/**
* @var array File name patterns (glob or regex)
*/
protected array $names = [];
/**
* @var array Path patterns (regex)
*/
protected array $paths = [];
/**
* @var array Directories to exclude
*/
protected array $excludeDirs = ['vendor', 'runtime', '.git', 'storage', 'tests', 'node_modules'];
/**
* @var bool Enable PHP meta analysis
*/
protected bool $phpMetaEnabled = false;
/**
* @var bool|null Filter by hasAttributes
*/
protected ?bool $filterHasAttributes = null;
/**
* @var array|null Filter by type
*/
protected ?array $filterTypes = null;
/**
* @var bool|null Filter by psr4
*/
protected ?bool $filterPsr4 = null;
/**
* @var array<string, array> PHP file cache: [path => meta, ...]
*/
protected static array $phpCache = [];
/**
* @var array<string, bool> Cache dirty flags by root
*/
protected static array $cacheDirty = [];
/**
* @var string Cache directory
*/
protected static string $cacheDir = '';
/**
* Create a new Finder instance with root directories.
* @param string|array $dirs
* @return static
*/
public static function in(string|array $dirs): static
{
$instance = new static();
foreach ((array)$dirs as $dir) {
if (is_dir($dir)) {
$instance->roots[] = static::normalizePath($dir);
}
}
return $instance;
}
/**
* Create a new Finder instance.
* Use in() to set search directories.
* @return static
*/
public static function create(): static
{
return new static();
}
/**
* Set cache directory.
* @param string $dir
* @return void
*/
public static function setCacheDir(string $dir): void
{
static::$cacheDir = rtrim($dir, '/\\');
}
/**
* Get cache directory.
* @return string
*/
protected static function getCacheDir(): string
{
if (static::$cacheDir === '') {
static::$cacheDir = rtrim(runtime_path('cache/framework/finder'), '/\\');
if (!is_dir(static::$cacheDir)) {
@mkdir(static::$cacheDir, 0755, true);
}
}
return static::$cacheDir;
}
/**
* Only match files.
* @return $this
*/
public function files(): static
{
$this->onlyFiles = true;
return $this;
}
/**
* Filter by file name pattern (glob or regex).
* @param string|array $patterns
* @return $this
*/
public function name(string|array $patterns): static
{
$this->names = array_merge($this->names, (array)$patterns);
return $this;
}
/**
* Filter by path pattern (regex).
* @param string|array $patterns
* @return $this
*/
public function path(string|array $patterns): static
{
$this->paths = array_merge($this->paths, (array)$patterns);
return $this;
}
/**
* Exclude directories.
* @param string|array $dirs
* @return $this
*/
public function exclude(string|array $dirs): static
{
$this->excludeDirs = array_merge($this->excludeDirs, (array)$dirs);
return $this;
}
/**
* Set exclude directories (replace default).
* @param array $dirs
* @return $this
*/
public function excludeDirs(array $dirs): static
{
$this->excludeDirs = $dirs;
return $this;
}
/**
* Enable PHP meta analysis and caching.
* @return $this
*/
public function withPhpMeta(): static
{
$this->phpMetaEnabled = true;
return $this;
}
/**
* Whether any PHP meta filters are requested.
* @return bool
*/
protected function phpFiltersRequested(): bool
{
return $this->filterHasAttributes !== null
|| $this->filterTypes !== null
|| $this->filterPsr4 !== null;
}
/**
* Filter by hasAttributes.
* @param bool $value
* @return $this
*/
public function hasAttributes(bool $value): static
{
// PHP-specific filter: enable PHP meta automatically.
$this->phpMetaEnabled = true;
$this->filterHasAttributes = $value;
return $this;
}
/**
* Filter by type (class, interface, trait, enum, non_class).
* @param array $types
* @return $this
*/
public function typeIn(array $types): static
{
// PHP-specific filter: enable PHP meta automatically.
$this->phpMetaEnabled = true;
$this->filterTypes = $types;
return $this;
}
/**
* Filter by PSR-4 compliance.
* @param bool $value
* @return $this
*/
public function psr4(bool $value): static
{
// PHP-specific filter: enable PHP meta automatically.
$this->phpMetaEnabled = true;
$this->filterPsr4 = $value;
return $this;
}
/**
* Find files and return FileInfo array.
* @return FileInfo[]
*/
public function find(): array
{
$results = [];
$phpFiltersRequested = $this->phpFiltersRequested();
// phpMetaEnabled can be turned on explicitly (withPhpMeta) or implicitly (by PHP filters).
$phpMetaEnabled = $this->phpMetaEnabled || $phpFiltersRequested;
foreach ($this->roots as $rootDir) {
// Load cache for this root
if ($phpMetaEnabled) {
$this->loadCache($rootDir);
}
// Scan directory
$files = $this->scanDirectory($rootDir);
// Apply filters
foreach ($files as $filePath) {
// Apply name filter
if (!$this->matchesName($filePath)) {
continue;
}
// Apply path filter
if (!$this->matchesPath($filePath, $rootDir)) {
continue;
}
// Get or compute PHP meta
$meta = [];
if ($phpMetaEnabled) {
// If any PHP filters were requested, skip non-PHP files.
if ($phpFiltersRequested && !$this->isPhpFile($filePath)) {
continue;
}
if ($this->isPhpFile($filePath)) {
$meta = $this->getPhpMeta($filePath, $rootDir);
// Apply PHP meta filters
if ($phpFiltersRequested && !$this->matchesPhpFilters($meta, $filePath, $rootDir)) {
continue;
}
}
}
$results[] = new FileInfo($filePath, $meta, $rootDir);
}
// Save cache if dirty
if ($phpMetaEnabled) {
$this->saveCache($rootDir);
}
}
return $results;
}
/**
* Find files and return paths array.
* @return string[]
*/
public function findPaths(): array
{
return array_map(fn(FileInfo $f) => $f->getPathname(), $this->find());
}
/**
* Scan directory recursively.
* @param string $dir
* @return array
*/
protected function scanDirectory(string $dir): array
{
$files = [];
$excludeSet = array_flip($this->excludeDirs);
try {
$directoryIterator = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
$filterIterator = new RecursiveCallbackFilterIterator(
$directoryIterator,
function (\SplFileInfo $current) use ($excludeSet) {
if ($current->isDir()) {
return !isset($excludeSet[$current->getBasename()]);
}
return true;
}
);
$iterator = new RecursiveIteratorIterator($filterIterator, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $item) {
/** @var \SplFileInfo $item */
$basename = $item->getBasename();
// Skip excluded directories
if ($item->isDir()) {
continue;
}
// Skip if only files mode and not a file
if ($this->onlyFiles && !$item->isFile()) {
continue;
}
$files[] = static::normalizePath($item->getPathname());
}
} catch (\Throwable $e) {
// Ignore unreadable directories
}
return $files;
}
/**
* Check if file matches name patterns.
* @param string $filePath
* @return bool
*/
protected function matchesName(string $filePath): bool
{
if (empty($this->names)) {
return true;
}
$basename = basename($filePath);
foreach ($this->names as $pattern) {
if ($this->matchPattern($basename, $pattern)) {
return true;
}
}
return false;
}
/**
* Check if file matches path patterns.
* @param string $filePath
* @param string $rootDir
* @return bool
*/
protected function matchesPath(string $filePath, string $rootDir): bool
{
if (empty($this->paths)) {
return true;
}
// Use relative path for matching
$relativePath = $this->getRelativePath($filePath, $rootDir);
foreach ($this->paths as $pattern) {
if ($this->isRegex($pattern) && preg_match($pattern, $relativePath)) {
return true;
}
if (!$this->isRegex($pattern) && stripos($relativePath, $pattern) !== false) {
return true;
}
}
return false;
}
/**
* Match a pattern (glob or regex).
* @param string $value
* @param string $pattern
* @return bool
*/
protected function matchPattern(string $value, string $pattern): bool
{
// Regex pattern
if ($this->isRegex($pattern)) {
return (bool)preg_match($pattern, $value);
}
// Glob pattern
return fnmatch($pattern, $value, FNM_CASEFOLD);
}
/**
* Check if pattern is a regex.
* @param string $pattern
* @return bool
*/
protected function isRegex(string $pattern): bool
{
if ($pattern === '') {
return false;
}
$delimiter = $pattern[0];
if (!in_array($delimiter, ['/', '#', '~', '%'], true)) {
return false;
}
return (bool)preg_match('/^' . preg_quote($delimiter, '/') . '.*' . preg_quote($delimiter, '/') . '[imsxuADU]*$/', $pattern);
}
/**
* Check if file is a PHP file.
* @param string $filePath
* @return bool
*/
protected function isPhpFile(string $filePath): bool
{
return strtolower(pathinfo($filePath, PATHINFO_EXTENSION)) === 'php';
}
/**
* Get PHP file meta, using cache when possible.
* @param string $filePath
* @param string $rootDir
* @return array
*/
protected function getPhpMeta(string $filePath, string $rootDir): array
{
$cacheKey = $this->getCacheKey($rootDir);
$mtime = @filemtime($filePath);
// Check cache
if (isset(static::$phpCache[$cacheKey][$filePath])) {
$cached = static::$phpCache[$cacheKey][$filePath];
if (isset($cached['mtime']) && $cached['mtime'] === $mtime) {
return $cached;
}
}
// Compute new meta
$meta = $this->computePhpMeta($filePath, $mtime);
// Update cache
static::$phpCache[$cacheKey][$filePath] = $meta;
static::$cacheDirty[$cacheKey] = true;
return $meta;
}
/**
* Ensure psr4 is computed and cached in meta.
* Only computes once per (file, mtime) cache entry.
* @param string $filePath
* @param string $rootDir
* @param array $meta
* @return array Updated meta
*/
protected function ensurePsr4Cached(string $filePath, string $rootDir, array $meta): array
{
if (array_key_exists('psr4', $meta)) {
return $meta;
}
$psr4 = $this->checkPsr4($filePath, $rootDir, $meta['class'] ?? null);
$meta['psr4'] = $psr4;
$cacheKey = $this->getCacheKey($rootDir);
static::$phpCache[$cacheKey][$filePath] = $meta;
static::$cacheDirty[$cacheKey] = true;
return $meta;
}
/**
* Compute PHP file meta info.
* @param string $filePath
* @param int|false $mtime
* @return array
*/
protected function computePhpMeta(string $filePath, int|false $mtime): array
{
$meta = [
'mtime' => $mtime ?: 0,
'hasAttributes' => false,
'class' => null,
];
$code = @file_get_contents($filePath);
if ($code === false || $code === '') {
$meta['type'] = 'non_class';
return $meta;
}
// Fast check for attributes
$meta['hasAttributes'] = str_contains($code, '#[');
// Parse to get type and declared class
$parseResult = $this->parsePhpFile($code);
$meta['type'] = $parseResult['type'];
$meta['class'] = $parseResult['class'];
return $meta;
}
/**
* Parse PHP file to extract type and declared class.
* @param string $code
* @return array{type: string, class: string|null}
*/
protected function parsePhpFile(string $code): array
{
$result = [
'type' => 'non_class',
'class' => null,
];
try {
$tokens = token_get_all($code);
} catch (\Throwable $e) {
return $result;
}
$namespace = '';
$count = count($tokens);
$prevSignificant = null;
for ($i = 0; $i < $count; $i++) {
$token = $tokens[$i];
$id = is_array($token) ? $token[0] : null;
// Extract namespace
if ($id === T_NAMESPACE) {
$ns = '';
for ($j = $i + 1; $j < $count; $j++) {
$t = $tokens[$j];
if (is_array($t)) {
if ($t[0] === T_STRING || $t[0] === T_NS_SEPARATOR) {
$ns .= $t[1];
continue;
}
if (defined('T_NAME_QUALIFIED') && $t[0] === T_NAME_QUALIFIED) {
$ns .= $t[1];
continue;
}
if ($t[0] === T_WHITESPACE) {
continue;
}
} else {
if ($t === ';' || $t === '{') {
break;
}
}
}
$namespace = trim($ns, '\\');
continue;
}
// Detect class/interface/trait/enum
if ($id === T_CLASS || $id === T_INTERFACE || $id === T_TRAIT || (defined('T_ENUM') && $id === T_ENUM)) {
// Skip ::class usage
if ($prevSignificant === T_DOUBLE_COLON) {
$prevSignificant = null;
continue;
}
// Skip anonymous class
if ($id === T_CLASS && $prevSignificant === T_NEW) {
continue;
}
// Determine type
$type = match ($id) {
T_CLASS => 'class',
T_INTERFACE => 'interface',
T_TRAIT => 'trait',
default => defined('T_ENUM') && $id === T_ENUM ? 'enum' : 'class',
};
// Extract class name
for ($j = $i + 1; $j < $count; $j++) {
$t = $tokens[$j];
if (!is_array($t)) {
continue;
}
if ($t[0] === T_WHITESPACE) {
continue;
}
if ($t[0] === T_STRING) {
$className = $t[1];
$result['type'] = $type;
$result['class'] = $namespace !== '' ? ($namespace . '\\' . $className) : $className;
return $result; // Return first declaration only
}
break;
}
}
// Track previous significant token
if (is_array($token)) {
if ($id !== T_WHITESPACE && $id !== T_COMMENT && $id !== T_DOC_COMMENT) {
$prevSignificant = $id;
}
} else {
if (trim($token) !== '') {
$prevSignificant = $token;
}
}
}
return $result;
}
/**
* Check if meta matches PHP filters.
* @param array $meta
* @param string $filePath
* @param string $rootDir
* @return bool
*/
protected function matchesPhpFilters(array $meta, string $filePath, string $rootDir): bool
{
// Filter by hasAttributes
if ($this->filterHasAttributes !== null && $meta['hasAttributes'] !== $this->filterHasAttributes) {
return false;
}
// Filter by type
if ($this->filterTypes !== null && !in_array($meta['type'], $this->filterTypes, true)) {
return false;
}
// Filter by PSR-4
if ($this->filterPsr4 !== null) {
$meta = $this->ensurePsr4Cached($filePath, $rootDir, $meta);
$psr4Ok = $meta['psr4'];
if ($psr4Ok !== $this->filterPsr4) {
return false;
}
}
return true;
}
/**
* Check if file complies with PSR-4.
* @param string $filePath
* @param string $rootDir
* @param string|null $declaredClass
* @return bool
*/
protected function checkPsr4(string $filePath, string $rootDir, ?string $declaredClass): bool
{
if ($declaredClass === null) {
return false;
}
// Namespace-agnostic check:
// declared class must end with the PSR-4 relative class path derived from file path.
$relativePath = $this->getRelativePath($filePath, $rootDir);
if ($relativePath === '' || !str_ends_with($relativePath, '.php')) {
return false;
}
$relativeClassPath = substr($relativePath, 0, -4);
$relativeClassPath = str_replace('/', '\\', $relativeClassPath);
if (!$this->isValidPsr4ClassPath($relativeClassPath)) {
return false;
}
$declaredClass = ltrim($declaredClass, '\\');
$suffix = '\\' . $relativeClassPath;
return $declaredClass === $relativeClassPath || str_ends_with($declaredClass, $suffix);
}
/**
* Derive expected class name from file path (PSR-4 style).
* @param string $filePath
* @param string $rootDir
* @param string $rootNamespace
* @return string|null
*/
protected function classFromFile(string $filePath, string $rootDir, string $rootNamespace): ?string
{
$rootDir = rtrim(static::normalizePath($rootDir), '/');
$filePath = static::normalizePath($filePath);
$rootLen = strlen($rootDir);
if (strncasecmp($filePath, $rootDir, $rootLen) !== 0) {
return null;
}
$relative = ltrim(substr($filePath, $rootLen), '/');
if ($relative === '' || !str_ends_with($relative, '.php')) {
return null;
}
$relative = substr($relative, 0, -4); // Remove .php
$relative = str_replace('/', '\\', $relative);
if (!$this->isValidPsr4ClassPath($relative)) {
return null;
}
return rtrim($rootNamespace, '\\') . '\\' . $relative;
}
/**
* Check if relative class path is valid PSR-4.
* @param string $relativeClassPath
* @return bool
*/
protected function isValidPsr4ClassPath(string $relativeClassPath): bool
{
return (bool)preg_match('/^[A-Za-z_][A-Za-z0-9_]*(\\\\[A-Za-z_][A-Za-z0-9_]*)*$/', $relativeClassPath);
}
/**
* Get relative path from root directory.
* @param string $filePath
* @param string $rootDir
* @return string
*/
protected function getRelativePath(string $filePath, string $rootDir): string
{
$rootDir = rtrim(static::normalizePath($rootDir), '/');
$filePath = static::normalizePath($filePath);
$rootLen = strlen($rootDir);
if (strncasecmp($filePath, $rootDir, $rootLen) === 0) {
return ltrim(substr($filePath, $rootLen), '/');
}
return $filePath;
}
/**
* Get cache key for a root directory.
* @param string $rootDir
* @return string
*/
protected function getCacheKey(string $rootDir): string
{
return hash('sha256', $rootDir);
}
/**
* Get cache file path for a root directory.
* @param string $rootDir
* @return string
*/
protected function getCacheFile(string $rootDir): string
{
$cacheKey = $this->getCacheKey($rootDir);
return static::getCacheDir() . DIRECTORY_SEPARATOR . "php_files_{$cacheKey}.php";
}
/**
* Load cache for a root directory.
* @param string $rootDir
* @return void
*/
protected function loadCache(string $rootDir): void
{
$cacheKey = $this->getCacheKey($rootDir);
if (isset(static::$phpCache[$cacheKey])) {
return; // Already loaded
}
$cacheFile = $this->getCacheFile($rootDir);
if (is_file($cacheFile)) {
try {
$data = include $cacheFile;
if (is_array($data)) {
static::$phpCache[$cacheKey] = $data;
return;
}
} catch (\Throwable $e) {
// Ignore corrupted cache
}
}
static::$phpCache[$cacheKey] = [];
}
/**
* Save cache for a root directory.
* @param string $rootDir
* @return void
*/
protected function saveCache(string $rootDir): void
{
$cacheKey = $this->getCacheKey($rootDir);
if (empty(static::$cacheDirty[$cacheKey])) {
return;
}
$cacheFile = $this->getCacheFile($rootDir);
$cacheDir = dirname($cacheFile);
if (!is_dir($cacheDir)) {
@mkdir($cacheDir, 0755, true);
}
$data = static::$phpCache[$cacheKey] ?? [];
// Clean up deleted files
foreach ($data as $path => $meta) {
if (!is_file($path)) {
unset($data[$path]);
}
}
static::$phpCache[$cacheKey] = $data;
$content = "<?php\nreturn " . var_export($data, true) . ";\n";
$suffix = (string)getmypid();
try {
$suffix .= '.' . bin2hex(random_bytes(6));
} catch (\Throwable $e) {
$suffix .= '.' . uniqid('', true);
}
$tempFile = $cacheFile . '.tmp.' . $suffix;
// No locks: write temp file then atomic rename.
if (@file_put_contents($tempFile, $content) !== false) {
// On Windows, rename() fails if target exists. Use a backup to swap.
if (!@rename($tempFile, $cacheFile)) {
$backupFile = $cacheFile . '.bak.' . $suffix;
// Best-effort: move old cache away, then move new cache in.
@rename($cacheFile, $backupFile);
if (@rename($tempFile, $cacheFile)) {
@unlink($backupFile);
} else {
// Restore old cache if possible.
@rename($backupFile, $cacheFile);
@unlink($tempFile);
}
}
}
static::$cacheDirty[$cacheKey] = false;
}
/**
* Clear all caches.
* @return void
*/
public static function clearCache(): void
{
static::$phpCache = [];
static::$cacheDirty = [];
}
/**
* Normalize path separators.
* @param string $path
* @return string
*/
protected static function normalizePath(string $path): string
{
$realPath = realpath($path);
if ($realPath !== false) {
return str_replace('\\', '/', $realPath);
}
return str_replace('\\', '/', $path);
}
}
+409
View File
@@ -0,0 +1,409 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Http;
use Webman\Route\Route;
use function current;
use function filter_var;
use function ip2long;
use function is_array;
use function strpos;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_NO_PRIV_RANGE;
use const FILTER_FLAG_NO_RES_RANGE;
use const FILTER_VALIDATE_IP;
/**
* Class Request
* @package Webman\Http
*/
class Request extends \Workerman\Protocols\Http\Request
{
/**
* @var string
*/
public $plugin = null;
/**
* @var string
*/
public $app = null;
/**
* @var string
*/
public $controller = null;
/**
* @var string
*/
public $action = null;
/**
* @var Route
*/
public $route = null;
/**
* @var bool
*/
protected $isDirty = false;
/**
* @return mixed|null
*/
public function all()
{
return $this->get() + $this->post();
}
/**
* Input
* @param string $name
* @param mixed $default
* @return mixed
*/
public function input(string $name, mixed $default = null)
{
return $this->get($name, $this->post($name, $default));
}
/**
* Only
* @param array $keys
* @return array
*/
public function only(array $keys): array
{
$all = $this->all();
$result = [];
foreach ($keys as $key) {
if (isset($all[$key])) {
$result[$key] = $all[$key];
}
}
return $result;
}
/**
* Except
* @param array $keys
* @return mixed|null
*/
public function except(array $keys)
{
$all = $this->all();
foreach ($keys as $key) {
unset($all[$key]);
}
return $all;
}
/**
* File
* @param string|null $name
* @return UploadFile|UploadFile[]|null
*/
public function file(?string $name = null): array|null|UploadFile
{
$files = parent::file($name);
if (null === $files) {
return $name === null ? [] : null;
}
if ($name !== null) {
// Multi files
if (is_array(current($files))) {
return $this->parseFiles($files);
}
return $this->parseFile($files);
}
$uploadFiles = [];
foreach ($files as $name => $file) {
// Multi files
if (is_array(current($file))) {
$uploadFiles[$name] = $this->parseFiles($file);
} else {
$uploadFiles[$name] = $this->parseFile($file);
}
}
return $uploadFiles;
}
/**
* ParseFile
* @param array $file
* @return UploadFile
*/
protected function parseFile(array $file): UploadFile
{
return new UploadFile($file['tmp_name'], $file['name'], $file['type'], $file['error']);
}
/**
* ParseFiles
* @param array $files
* @return array
*/
protected function parseFiles(array $files): array
{
$uploadFiles = [];
foreach ($files as $key => $file) {
if (is_array(current($file))) {
$uploadFiles[$key] = $this->parseFiles($file);
} else {
$uploadFiles[$key] = $this->parseFile($file);
}
}
return $uploadFiles;
}
/**
* GetRemoteIp
* @return string
*/
public function getRemoteIp(): string
{
return $this->connection ? $this->connection->getRemoteIp() : '0.0.0.0';
}
/**
* GetRemotePort
* @return int
*/
public function getRemotePort(): int
{
return $this->connection ? $this->connection->getRemotePort() : 0;
}
/**
* GetLocalIp
* @return string
*/
public function getLocalIp(): string
{
return $this->connection ? $this->connection->getLocalIp() : '0.0.0.0';
}
/**
* GetLocalPort
* @return int
*/
public function getLocalPort(): int
{
return $this->connection ? $this->connection->getLocalPort() : 0;
}
/**
* GetRealIp
* @param bool $safeMode
* @return string
*/
public function getRealIp(bool $safeMode = true): string
{
$remoteIp = $this->getRemoteIp();
if ($safeMode && !static::isIntranetIp($remoteIp)) {
return $remoteIp;
}
$ip = $this->header('x-forwarded-for')
?? $this->header('x-real-ip')
?? $this->header('client-ip')
?? $this->header('x-client-ip')
?? $this->header('via')
?? $remoteIp;
if (is_string($ip)) {
$ip = current(explode(',', $ip));
}
return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : $remoteIp;
}
/**
* Url
* @return string
*/
public function url(): string
{
return '//' . $this->host() . $this->path();
}
/**
* FullUrl
* @return string
*/
public function fullUrl(): string
{
return '//' . $this->host() . $this->uri();
}
/**
* IsAjax
* @return bool
*/
public function isAjax(): bool
{
return $this->header('X-Requested-With') === 'XMLHttpRequest';
}
/**
* IsGet
* @return bool
*/
public function isGet(): bool
{
return $this->method() === 'GET';
}
/**
* IsPost
* @return bool
*/
public function isPost(): bool
{
return $this->method() === 'POST';
}
/**
* IsPjax
* @return bool
*/
public function isPjax(): bool
{
return (bool)$this->header('X-PJAX');
}
/**
* ExpectsJson
* @return bool
*/
public function expectsJson(): bool
{
return ($this->isAjax() && !$this->isPjax()) || $this->acceptJson();
}
/**
* AcceptJson
* @return bool
*/
public function acceptJson(): bool
{
return false !== strpos($this->header('accept', ''), 'json');
}
/**
* IsIntranetIp
* @param string $ip
* @return bool
*/
public static function isIntranetIp(string $ip): bool
{
// Not validate ip .
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
return false;
}
// Is intranet ip ? For IPv4, the result of false may not be accurate, so we need to check it manually later .
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return true;
}
// Manual check only for IPv4 .
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return false;
}
// Manual check .
$reservedIps = [
1681915904 => 1686110207, // 100.64.0.0 - 100.127.255.255
3221225472 => 3221225727, // 192.0.0.0 - 192.0.0.255
3221225984 => 3221226239, // 192.0.2.0 - 192.0.2.255
3227017984 => 3227018239, // 192.88.99.0 - 192.88.99.255
3323068416 => 3323199487, // 198.18.0.0 - 198.19.255.255
3325256704 => 3325256959, // 198.51.100.0 - 198.51.100.255
3405803776 => 3405804031, // 203.0.113.0 - 203.0.113.255
3758096384 => 4026531839, // 224.0.0.0 - 239.255.255.255
];
$ipLong = ip2long($ip);
foreach ($reservedIps as $ipStart => $ipEnd) {
if (($ipLong >= $ipStart) && ($ipLong <= $ipEnd)) {
return true;
}
}
return false;
}
/**
* Set get.
* @param array|string $input
* @param mixed $value
* @return Request
*/
public function setGet(array|string $input, mixed $value = null): Request
{
$this->isDirty = true;
$input = is_array($input) ? $input : array_merge($this->get(), [$input => $value]);
if (isset($this->data)) {
$this->data['get'] = $input;
} else {
$this->_data['get'] = $input;
}
return $this;
}
/**
* Set post.
* @param array|string $input
* @param mixed $value
* @return Request
*/
public function setPost(array|string $input, mixed $value = null): Request
{
$this->isDirty = true;
$input = is_array($input) ? $input : array_merge($this->post(), [$input => $value]);
if (isset($this->data)) {
$this->data['post'] = $input;
} else {
$this->_data['post'] = $input;
}
return $this;
}
/**
* Set header.
* @param array|string $input
* @param mixed $value
* @return Request
*/
public function setHeader(array|string $input, mixed $value = null): Request
{
$this->isDirty = true;
$input = is_array($input) ? $input : array_merge($this->header(), [$input => $value]);
if (isset($this->data)) {
$this->data['headers'] = $input;
} else {
$this->_data['headers'] = $input;
}
return $this;
}
/**
* Destroy
*/
public function destroy(): void
{
if ($this->isDirty) {
unset($this->data['get'], $this->data['post'], $this->data['headers']);
}
parent::destroy();
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Http;
use Throwable;
use Webman\App;
use function filemtime;
use function gmdate;
/**
* Class Response
* @package Webman\Http
*/
class Response extends \Workerman\Protocols\Http\Response
{
/**
* @var Throwable
*/
protected $exception = null;
/**
* File
* @param string $file
* @return $this
*/
public function file(string $file): Response
{
if ($this->notModifiedSince($file)) {
return $this->withStatus(304);
}
return $this->withFile($file);
}
/**
* Download
* @param string $file
* @param string $downloadName
* @return $this
*/
public function download(string $file, string $downloadName = ''): Response
{
$this->withFile($file);
if ($downloadName) {
// Sanitize to prevent header injection
$downloadName = str_replace(['"', "\r", "\n", "\0"], '', $downloadName);
$this->header('Content-Disposition', "attachment; filename=\"$downloadName\"");
}
return $this;
}
/**
* NotModifiedSince
* @param string $file
* @return bool
*/
protected function notModifiedSince(string $file): bool
{
$ifModifiedSince = App::request()->header('if-modified-since');
if ($ifModifiedSince === null || !is_file($file) || !($mtime = filemtime($file))) {
return false;
}
return $ifModifiedSince === gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
}
/**
* Exception
* @param Throwable|null $exception
* @return Throwable|null
*/
public function exception(?Throwable $exception = null): ?Throwable
{
if ($exception) {
$this->exception = $exception;
}
return $this->exception;
}
}
@@ -0,0 +1,111 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Http;
use Webman\File;
use function pathinfo;
/**
* Class UploadFile
* @package Webman\Http
*/
class UploadFile extends File
{
/**
* @var string
*/
protected $uploadName = null;
/**
* @var string
*/
protected $uploadMimeType = null;
/**
* @var int
*/
protected $uploadErrorCode = null;
/**
* UploadFile constructor.
*
* @param string $fileName
* @param string $uploadName
* @param string $uploadMimeType
* @param int $uploadErrorCode
*/
public function __construct(string $fileName, string $uploadName, string $uploadMimeType, int $uploadErrorCode)
{
$this->uploadName = $uploadName;
$this->uploadMimeType = $uploadMimeType;
$this->uploadErrorCode = $uploadErrorCode;
parent::__construct($fileName);
}
/**
* GetUploadName
* @return string
*/
public function getUploadName(): ?string
{
return $this->uploadName;
}
/**
* GetUploadMimeType
* @return string
*/
public function getUploadMimeType(): ?string
{
return $this->uploadMimeType;
}
/**
* GetUploadExtension
* @return string
*/
public function getUploadExtension(): string
{
return pathinfo($this->uploadName, PATHINFO_EXTENSION);
}
/**
* GetUploadErrorCode
* @return int
*/
public function getUploadErrorCode(): ?int
{
return $this->uploadErrorCode;
}
/**
* IsValid
* @return bool
*/
public function isValid(): bool
{
return $this->uploadErrorCode === UPLOAD_ERR_OK;
}
/**
* GetUploadMineType
* @return string
* @deprecated
*/
public function getUploadMineType(): ?string
{
return $this->uploadMimeType;
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace Webman;
class Install
{
const WEBMAN_PLUGIN = true;
/**
* @var array
*/
protected static $pathRelation = [
'start.php' => 'start.php',
'windows.php' => 'windows.php',
];
/**
* Install
* @return void
*/
public static function install()
{
static::installByRelation();
}
/**
* Uninstall
* @return void
*/
public static function uninstall()
{
}
/**
* InstallByRelation
* @return void
*/
public static function installByRelation()
{
foreach (static::$pathRelation as $source => $dest) {
$parentDir = base_path(dirname($dest));
if (!is_dir($parentDir)) {
mkdir($parentDir, 0777, true);
}
$sourceFile = __DIR__ . "/$source";
copy_dir($sourceFile, base_path($dest), true);
echo "Create $dest\r\n";
if (is_file($sourceFile)) {
@unlink($sourceFile);
}
}
if (is_file($file = base_path('support/helpers.php'))) {
file_put_contents($file, "<?php\n// This file is generated by Webman, please don't modify it.\n");
echo "Clear helpers.php\r\n";
}
}
}
+162
View File
@@ -0,0 +1,162 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use Closure;
use ReflectionAttribute;
use support\annotation\Middleware as MiddlewareAttribute;
use Webman\Route\Route;
use ReflectionClass;
use ReflectionMethod;
use RuntimeException;
use function array_merge;
use function array_reverse;
use function is_array;
use function method_exists;
class Middleware
{
/**
* @var array
*/
protected static $instances = [];
/**
* @var array Cache for controller middleware resolved via reflection/attributes.
*/
protected static $controllerMiddlewareCache = [];
/**
* @param mixed $allMiddlewares
* @param string $plugin
* @return void
*/
public static function load($allMiddlewares, string $plugin = '')
{
if (!is_array($allMiddlewares)) {
return;
}
foreach ($allMiddlewares as $appName => $middlewares) {
if (!is_array($middlewares)) {
throw new RuntimeException('Bad middleware config');
}
$pluginKey = $plugin;
$appKey = $appName;
if ($appKey === '@') {
$pluginKey = '';
} elseif (strpos($appKey, 'plugin.') !== false) {
$explode = explode('.', $appKey, 4);
$pluginKey = $explode[1];
$appKey = $explode[2] ?? '';
}
foreach ($middlewares as $className) {
if (method_exists($className, 'process')) {
static::$instances[$pluginKey][$appKey][] = [$className, 'process'];
} else {
// @todo Log
echo "middleware $className::process not exsits\n";
}
}
}
}
/**
* @param string $plugin
* @param string $appName
* @param string|array|Closure $controller
* @param Route|null $route
* @param bool $withGlobalMiddleware
* @return array
*/
public static function getMiddleware(string $plugin, string $appName, string|array|Closure $controller, Route|null $route, bool $withGlobalMiddleware = true): array
{
$isController = is_array($controller) && is_string($controller[0]);
$globalMiddleware = $withGlobalMiddleware ? static::$instances['']['@'] ?? [] : [];
$appGlobalMiddleware = $withGlobalMiddleware && isset(static::$instances[$plugin]['']) ? static::$instances[$plugin][''] : [];
$middlewares = $routeMiddlewares = [];
// Route middleware
if ($route) {
foreach (array_reverse($route->getMiddleware()) as $className) {
$routeMiddlewares[] = [$className, 'process'];
}
}
if ($isController && $controller[0] && class_exists($controller[0])) {
$cacheKey = $controller[0] . '::' . $controller[1];
if (isset(static::$controllerMiddlewareCache[$cacheKey])) {
$cached = static::$controllerMiddlewareCache[$cacheKey];
$middlewares = array_merge($cached['before_route'], $routeMiddlewares, $cached['after_route']);
} else {
$beforeRoute = [];
$afterRoute = [];
// Controller middleware annotation
$reflectionClass = new ReflectionClass($controller[0]);
self::prepareAttributeMiddlewares($beforeRoute, $reflectionClass);
// Controller middleware property
if ($reflectionClass->hasProperty('middleware')) {
$defaultProperties = $reflectionClass->getDefaultProperties();
$middlewaresClasses = $defaultProperties['middleware'];
foreach ((array)$middlewaresClasses as $className) {
$beforeRoute[] = [$className, 'process'];
}
}
// Method middleware annotation (route must be between controller and method)
if ($reflectionClass->hasMethod($controller[1])) {
self::prepareAttributeMiddlewares($afterRoute, $reflectionClass->getMethod($controller[1]));
}
$middlewares = array_merge($beforeRoute, $routeMiddlewares, $afterRoute);
static::$controllerMiddlewareCache[$cacheKey] = ['before_route' => $beforeRoute, 'after_route' => $afterRoute];
if (count(static::$controllerMiddlewareCache) > 1024) {
unset(static::$controllerMiddlewareCache[key(static::$controllerMiddlewareCache)]);
}
}
} else {
// Route middleware
$middlewares = array_merge($middlewares, $routeMiddlewares);
}
if ($appName === '') {
return array_reverse(array_merge($globalMiddleware, $appGlobalMiddleware, $middlewares));
}
$appMiddleware = static::$instances[$plugin][$appName] ?? [];
return array_reverse(array_merge($globalMiddleware, $appGlobalMiddleware, $appMiddleware, $middlewares));
}
/**
* @param array $middlewares
* @param ReflectionClass|ReflectionMethod $reflection
* @return void
*/
private static function prepareAttributeMiddlewares(array &$middlewares, ReflectionClass|ReflectionMethod $reflection): void
{
if ($reflection instanceof ReflectionClass && $parent_ref = $reflection->getParentClass()) {
self::prepareAttributeMiddlewares($middlewares, $parent_ref);
}
$middlewareAttributes = $reflection->getAttributes(MiddlewareAttribute::class, ReflectionAttribute::IS_INSTANCEOF);
foreach ($middlewareAttributes as $middlewareAttribute) {
$middlewareAttributeInstance = $middlewareAttribute->newInstance();
$middlewares = array_merge($middlewares, $middlewareAttributeInstance->getMiddlewares());
}
}
/**
* @return void
* @deprecated
*/
public static function container($_)
{
}
}
@@ -0,0 +1,30 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use Webman\Http\Request;
use Webman\Http\Response;
interface MiddlewareInterface
{
/**
* Process an incoming server request.
*
* Processes an incoming server request in order to produce a response.
* If unable to produce the response itself, it may delegate to the provided
* request handler to do so.
*/
public function process(Request $request, callable $handler): Response;
}
+848
View File
@@ -0,0 +1,848 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use FastRoute\Dispatcher\GroupCountBased;
use FastRoute\RouteCollector;
use FilesystemIterator;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use RuntimeException;
use support\annotation\Middleware as MiddlewareAttribute;
use support\annotation\route\DisableDefaultRoute;
use support\annotation\route\Route as RouteAttribute;
use support\annotation\route\RouteGroup as RouteGroupAttribute;
use Webman\Finder\FileInfo;
use Webman\Finder\ControllerFinder;
use Webman\Route\Route as RouteObject;
use function array_diff;
use function array_values;
use function class_exists;
use function explode;
use function FastRoute\simpleDispatcher;
use function in_array;
use function is_array;
use function is_callable;
use function is_file;
use function is_scalar;
use function is_string;
use function json_encode;
use function method_exists;
use function strpos;
/**
* Class Route
* @package Webman
*/
class Route
{
/**
* @var Route
*/
protected static $instance = null;
/**
* @var GroupCountBased
*/
protected static $dispatcher = null;
/**
* @var RouteCollector
*/
protected static $collector = null;
/**
* @var RouteObject[]
*/
protected static $fallbackRoutes = [];
/**
* @var array
*/
protected static $fallback = [];
/**
* @var array
*/
protected static $nameList = [];
/**
* @var string
*/
protected static $groupPrefix = '';
/**
* @var bool
*/
protected static $disabledDefaultRoutes = [];
/**
* @var array
*/
protected static $disabledDefaultRouteControllers = [];
/**
* @var array
*/
protected static $disabledDefaultRouteActions = [];
/**
* @var RouteObject[]
*/
protected static $allRoutes = [];
/**
* Index for conflict detection: ["METHOD path" => "callback string"]
* @var array<string, string>
*/
protected static array $methodPathIndex = [];
/**
* @var string|null
*/
protected static ?string $registeringSource = null;
/**
* @var RouteObject[]
*/
protected $routes = [];
/**
* @var Route[]
*/
protected $children = [];
/**
* Add GET route.
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function get(string $path, $callback): RouteObject
{
return static::addRoute('GET', $path, $callback);
}
/**
* Add POST route.
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function post(string $path, $callback): RouteObject
{
return static::addRoute('POST', $path, $callback);
}
/**
* Add PUT route.
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function put(string $path, $callback): RouteObject
{
return static::addRoute('PUT', $path, $callback);
}
/**
* Add PATCH route.
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function patch(string $path, $callback): RouteObject
{
return static::addRoute('PATCH', $path, $callback);
}
/**
* Add DELETE route.
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function delete(string $path, $callback): RouteObject
{
return static::addRoute('DELETE', $path, $callback);
}
/**
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function head(string $path, $callback): RouteObject
{
return static::addRoute('HEAD', $path, $callback);
}
/**
* Add HEAD route.
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function options(string $path, $callback): RouteObject
{
return static::addRoute('OPTIONS', $path, $callback);
}
/**
* Add OPTIONS route.
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function any(string $path, $callback): RouteObject
{
return static::addRoute(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], $path, $callback);
}
/**
* Add route.
* @param $method
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
public static function add($method, string $path, $callback): RouteObject
{
return static::addRoute($method, $path, $callback);
}
/**
* Add group.
* @param string|callable $path
* @param callable|null $callback
* @return static
*/
public static function group($path, ?callable $callback = null): Route
{
if ($callback === null) {
$callback = $path;
$path = '';
}
$previousGroupPrefix = static::$groupPrefix;
static::$groupPrefix = $previousGroupPrefix . $path;
$previousInstance = static::$instance;
$instance = static::$instance = new static;
static::$collector->addGroup($path, $callback);
static::$groupPrefix = $previousGroupPrefix;
static::$instance = $previousInstance;
if ($previousInstance) {
$previousInstance->addChild($instance);
}
return $instance;
}
/**
* Add resource.
* @param string $name
* @param string $controller
* @param array $options
* @return void
*/
public static function resource(string $name, string $controller, array $options = [])
{
$name = trim($name, '/');
if (is_array($options) && !empty($options)) {
$diffOptions = array_diff($options, ['index', 'create', 'store', 'update', 'show', 'edit', 'destroy', 'recovery']);
if (!empty($diffOptions)) {
foreach ($diffOptions as $action) {
static::any("/$name/{$action}[/{id}]", [$controller, $action])->name("$name.{$action}");
}
}
// 注册路由 由于顺序不同会导致路由无效 因此不适用循环注册
if (in_array('index', $options)) static::get("/$name", [$controller, 'index'])->name("$name.index");
if (in_array('create', $options)) static::get("/$name/create", [$controller, 'create'])->name("$name.create");
if (in_array('store', $options)) static::post("/$name", [$controller, 'store'])->name("$name.store");
if (in_array('update', $options)) static::put("/$name/{id}", [$controller, 'update'])->name("$name.update");
if (in_array('patch', $options)) static::patch("/$name/{id}", [$controller, 'patch'])->name("$name.patch");
if (in_array('show', $options)) static::get("/$name/{id}", [$controller, 'show'])->name("$name.show");
if (in_array('edit', $options)) static::get("/$name/{id}/edit", [$controller, 'edit'])->name("$name.edit");
if (in_array('destroy', $options)) static::delete("/$name/{id}", [$controller, 'destroy'])->name("$name.destroy");
if (in_array('recovery', $options)) static::put("/$name/{id}/recovery", [$controller, 'recovery'])->name("$name.recovery");
} else {
//为空时自动注册所有常用路由
if (method_exists($controller, 'index')) static::get("/$name", [$controller, 'index'])->name("$name.index");
if (method_exists($controller, 'create')) static::get("/$name/create", [$controller, 'create'])->name("$name.create");
if (method_exists($controller, 'store')) static::post("/$name", [$controller, 'store'])->name("$name.store");
if (method_exists($controller, 'update')) static::put("/$name/{id}", [$controller, 'update'])->name("$name.update");
if (method_exists($controller, 'patch')) static::patch("/$name/{id}", [$controller, 'patch'])->name("$name.patch");
if (method_exists($controller, 'show')) static::get("/$name/{id}", [$controller, 'show'])->name("$name.show");
if (method_exists($controller, 'edit')) static::get("/$name/{id}/edit", [$controller, 'edit'])->name("$name.edit");
if (method_exists($controller, 'destroy')) static::delete("/$name/{id}", [$controller, 'destroy'])->name("$name.destroy");
if (method_exists($controller, 'recovery')) static::put("/$name/{id}/recovery", [$controller, 'recovery'])->name("$name.recovery");
}
}
/**
* Get routes.
* @return RouteObject[]
*/
public static function getRoutes(): array
{
return static::$allRoutes;
}
/**
* Disable default route.
* @param array|string $plugin
* @param string|null $app
* @return bool
*/
public static function disableDefaultRoute(array|string $plugin = '', ?string $app = null): bool
{
// Is [controller action]
if (is_array($plugin)) {
$controllerAction = $plugin;
if (!isset($controllerAction[0]) || !is_string($controllerAction[0]) ||
!isset($controllerAction[1]) || !is_string($controllerAction[1])) {
return false;
}
$controller = $controllerAction[0];
$action = $controllerAction[1];
static::$disabledDefaultRouteActions[$controller][$action] = $action;
return true;
}
// Is plugin
if (is_string($plugin) && (preg_match('/^[a-zA-Z0-9_]+$/', $plugin) || $plugin === '')) {
if (!isset(static::$disabledDefaultRoutes[$plugin])) {
static::$disabledDefaultRoutes[$plugin] = [];
}
$app = $app ?? '*';
static::$disabledDefaultRoutes[$plugin][$app] = $app;
return true;
}
// Is controller
if (is_string($plugin) && class_exists($plugin)) {
static::$disabledDefaultRouteControllers[$plugin] = $plugin;
return true;
}
return false;
}
/**
* Is default route disabled.
* @param array|string $plugin
* @param string|null $app
* @return bool
*/
public static function isDefaultRouteDisabled(array|string $plugin = '', ?string $app = null): bool
{
// Is [controller action]
if (is_array($plugin)) {
if (!isset($plugin[0]) || !is_string($plugin[0]) ||
!isset($plugin[1]) || !is_string($plugin[1])) {
return false;
}
return isset(static::$disabledDefaultRouteActions[$plugin[0]][$plugin[1]]) || static::isDefaultRouteDisabledByAnnotation($plugin[0], $plugin[1]);
}
// Is plugin
if (is_string($plugin) && (preg_match('/^[a-zA-Z0-9_]+$/', $plugin) || $plugin === '')) {
$app = $app ?? '*';
return isset(static::$disabledDefaultRoutes[$plugin]['*']) || isset(static::$disabledDefaultRoutes[$plugin][$app]);
}
// Is controller
if (is_string($plugin) && class_exists($plugin)) {
return isset(static::$disabledDefaultRouteControllers[$plugin]);
}
return false;
}
/**
* Is default route disabled by annotation.
* @param string $controller
* @param string|null $action
* @return bool
*/
protected static function isDefaultRouteDisabledByAnnotation(string $controller, ?string $action = null): bool
{
if (class_exists($controller)) {
$reflectionClass = new ReflectionClass($controller);
if (static::isRefHasDefaultRouteDisabledAnnotation($reflectionClass)) {
return true;
}
if ($action && $reflectionClass->hasMethod($action)) {
$reflectionMethod = $reflectionClass->getMethod($action);
if ($reflectionMethod->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF)) {
return true;
}
}
}
return false;
}
/**
* Is reflection class has default route disabled annotation.
* @param ReflectionClass $reflectionClass
* @return bool
*/
protected static function isRefHasDefaultRouteDisabledAnnotation(ReflectionClass $reflectionClass): bool
{
$has = $reflectionClass->getAttributes(DisableDefaultRoute::class, ReflectionAttribute::IS_INSTANCEOF);
if ($has) {
return true;
}
if (method_exists($reflectionClass, 'getParentClass')) {
$parent = $reflectionClass->getParentClass();
if ($parent) {
return static::isRefHasDefaultRouteDisabledAnnotation($parent);
}
}
return false;
}
/**
* Add middleware.
* @param $middleware
* @return $this
*/
public function middleware($middleware): Route
{
foreach ($this->routes as $route) {
$route->middleware($middleware);
}
foreach ($this->getChildren() as $child) {
$child->middleware($middleware);
}
return $this;
}
/**
* Collect route.
* @param RouteObject $route
*/
public function collect(RouteObject $route)
{
$this->routes[] = $route;
}
/**
* Set by name.
* @param string $name
* @param RouteObject $instance
*/
public static function setByName(string $name, RouteObject $instance)
{
static::$nameList[$name] = $instance;
}
/**
* Get by name.
* @param string $name
* @return null|RouteObject
*/
public static function getByName(string $name): ?RouteObject
{
return static::$nameList[$name] ?? null;
}
/**
* Add child.
* @param Route $route
* @return void
*/
public function addChild(Route $route)
{
$this->children[] = $route;
}
/**
* Get children.
* @return Route[]
*/
public function getChildren()
{
return $this->children;
}
/**
* Dispatch.
* @param string $method
* @param string $path
* @return array
*/
public static function dispatch(string $method, string $path): array
{
return static::$dispatcher->dispatch($method, $path);
}
/**
* Convert to callable.
* @param string $path
* @param callable|mixed $callback
* @return callable|false|string[]
*/
public static function convertToCallable(string $path, $callback)
{
if (is_string($callback) && strpos($callback, '@')) {
$callback = explode('@', $callback, 2);
}
if (!is_array($callback)) {
if (!is_callable($callback)) {
$callStr = is_scalar($callback) ? $callback : 'Closure';
echo "Route $path $callStr is not callable\n";
return false;
}
} else {
$callback = array_values($callback);
if (!isset($callback[1]) || !class_exists($callback[0]) || !method_exists($callback[0], $callback[1])) {
echo "Route $path " . json_encode($callback) . " is not callable\n";
return false;
}
}
return $callback;
}
/**
* Add route.
* @param array|string $methods
* @param string $path
* @param callable|mixed $callback
* @return RouteObject
*/
protected static function addRoute($methods, string $path, $callback): RouteObject
{
$fullPath = static::$groupPrefix . $path;
foreach ((array)$methods as $method) {
$method = strtoupper((string)$method);
$key = $method . ' ' . $fullPath;
if (isset(static::$methodPathIndex[$key])) {
$old = static::$methodPathIndex[$key];
$new = static::callbackToString($callback);
$source = static::$registeringSource ? (' from ' . static::$registeringSource) : '';
throw new RuntimeException("Route conflict: [$key] already registered as $old, cannot register $new$source");
}
static::$methodPathIndex[$key] = static::callbackToString($callback);
}
$route = new RouteObject($methods, static::$groupPrefix . $path, $callback);
static::$allRoutes[] = $route;
if ($callback = static::convertToCallable($path, $callback)) {
static::$collector->addRoute($methods, $path, ['callback' => $callback, 'route' => $route]);
}
if (static::$instance) {
static::$instance->collect($route);
}
return $route;
}
/**
* Load.
* @param mixed $paths
* @return void
*/
public static function load($paths)
{
if (!is_array($paths)) {
return;
}
static::$dispatcher = null;
static::$collector = null;
static::$fallbackRoutes = [];
static::$fallback = [];
static::$nameList = [];
static::$disabledDefaultRoutes = [];
static::$disabledDefaultRouteControllers = [];
static::$disabledDefaultRouteActions = [];
static::$allRoutes = [];
static::$methodPathIndex = [];
static::$registeringSource = null;
static::$dispatcher = simpleDispatcher(function (RouteCollector $route) use ($paths) {
Route::setCollector($route);
foreach ($paths as $configPath) {
$routeConfigFile = $configPath . '/route.php';
if (is_file($routeConfigFile)) {
require_once $routeConfigFile;
}
if (!is_dir($pluginConfigPath = $configPath . '/plugin')) {
continue;
}
$dirIterator = new RecursiveDirectoryIterator($pluginConfigPath, FilesystemIterator::FOLLOW_SYMLINKS);
$iterator = new RecursiveIteratorIterator($dirIterator);
foreach ($iterator as $file) {
if ($file->getBaseName('.php') !== 'route') {
continue;
}
$appConfigFile = pathinfo($file, PATHINFO_DIRNAME) . '/app.php';
if (!is_file($appConfigFile)) {
continue;
}
$appConfig = include $appConfigFile;
if (empty($appConfig['enable'])) {
continue;
}
require_once $file;
}
}
static::loadAnnotationRoutes();
});
}
/**
* SetCollector.
* @param RouteCollector $route
* @return void
*/
public static function setCollector(RouteCollector $route)
{
static::$collector = $route;
}
/**
* Fallback.
* @param callable|mixed $callback
* @param string $plugin
* @return RouteObject
*/
public static function fallback(callable $callback, string $plugin = '')
{
$route = new RouteObject([], '', $callback);
static::$fallbackRoutes[$plugin] = $route;
return $route;
}
/**
* GetFallBack.
* @param string $plugin
* @param int $status
* @return callable|null
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws ReflectionException
*/
public static function getFallback(string $plugin = '', int $status = 404)
{
if (!isset(static::$fallback[$plugin])) {
$callback = null;
$route = static::$fallbackRoutes[$plugin] ?? null;
static::$fallback[$plugin] = $route ? App::getCallback($plugin, 'NOT_FOUND', $route->getCallback(), ['status' => $status], false, $route) : null;
}
return static::$fallback[$plugin];
}
/**
* Load annotation routes.
* @return void
*/
protected static function loadAnnotationRoutes(): void
{
$controllerFiles = ControllerFinder::files('*');
if (!$controllerFiles) {
return;
}
$routes = static::buildAnnotationRouteDefinitions($controllerFiles);
static::registerAnnotationRouteDefinitions($routes);
}
/**
* Build annotation route definitions.
* @param FileInfo[] $controllerFiles
* @return array<int,array{methods: string[], path: string, callback: array{0:string,1:string}, name: ?string, middlewares: array}>
*/
protected static function buildAnnotationRouteDefinitions(array $controllerFiles): array
{
$definitions = [];
foreach ($controllerFiles as $foundFile) {
$meta = $foundFile->meta();
$controllerClass = $meta['class'] ?? null;
if (!$controllerClass) {
continue;
}
$file = $foundFile->getPathname();
if (!class_exists($controllerClass)) {
require_once $file;
}
if (!class_exists($controllerClass)) {
continue;
}
$ref = new ReflectionClass($controllerClass);
if ($ref->isAbstract() || $ref->isInterface()) {
continue;
}
$prefix = '';
$groupAttrs = $ref->getAttributes(RouteGroupAttribute::class, ReflectionAttribute::IS_INSTANCEOF);
if ($groupAttrs) {
/** @var RouteGroupAttribute $group */
$group = $groupAttrs[0]->newInstance();
$prefix = static::normalizeRoutePrefix($group->prefix);
}
foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
if ($method->isConstructor() || $method->isDestructor()) {
continue;
}
if ($method->getDeclaringClass()->getName() !== $controllerClass) {
continue;
}
$routeAttrs = $method->getAttributes(RouteAttribute::class, ReflectionAttribute::IS_INSTANCEOF);
if (!$routeAttrs) {
continue;
}
foreach ($routeAttrs as $routeAttr) {
/** @var RouteAttribute $route */
$route = $routeAttr->newInstance();
if ($route->path === null) {
// Null path means "method restriction only" for default route, do not register.
continue;
}
$path = static::normalizeRoutePath($route->path, $controllerClass . '::' . $method->getName());
$fullPath = $prefix ? rtrim($prefix, '/') . $path : $path;
$methods = [];
foreach ($route->methods as $m) {
$methods[] = strtoupper((string)$m);
}
$definitions[] = [
'methods' => $methods,
'path' => $fullPath,
'callback' => [$controllerClass, $method->getName()],
'name' => $route->name,
];
}
}
}
return $definitions;
}
/**
* Collect middlewares from attributes.
* @param array<ReflectionAttribute> $attributes
* @return array
*/
protected static function collectMiddlewaresFromAttributes(array $attributes): array
{
$middlewares = [];
foreach ($attributes as $attribute) {
/** @var MiddlewareAttribute $instance */
$instance = $attribute->newInstance();
foreach ($instance->getMiddlewares() as $middleware) {
if (is_string($middleware)) {
$middlewares[] = $middleware;
continue;
}
if (is_array($middleware) && isset($middleware[0]) && is_string($middleware[0])) {
$middlewares[] = $middleware[0];
}
}
}
return $middlewares;
}
/**
* Register annotation route definitions.
* @param array $definitions
* @return void
*/
protected static function registerAnnotationRouteDefinitions(array $definitions): void
{
foreach ($definitions as $definition) {
static::$registeringSource = 'annotation ' . $definition['callback'][0] . '::' . $definition['callback'][1];
$route = static::add($definition['methods'], $definition['path'], $definition['callback']);
if (!empty($definition['name'])) {
$route->name($definition['name']);
}
if (!empty($definition['middlewares'])) {
$route->middleware($definition['middlewares']);
}
static::$registeringSource = null;
}
}
/**
* Normalize route prefix.
* @param string $prefix
* @return string
*/
protected static function normalizeRoutePrefix(string $prefix): string
{
$prefix = trim($prefix);
if ($prefix === '') {
return '';
}
if ($prefix[0] !== '/') {
$prefix = '/' . $prefix;
}
return rtrim($prefix, '/');
}
/**
* Normalize route path.
* @param string $path
* @param string $source
* @return string
*/
protected static function normalizeRoutePath(string $path, string $source): string
{
$path = trim($path);
if ($path === '' || $path[0] !== '/') {
throw new RuntimeException("Annotation route path must start with '/': $path ($source)");
}
return $path;
}
/**
* Callback to string.
* @param mixed $callback
* @return string
*/
protected static function callbackToString(mixed $callback): string
{
if (is_array($callback)) {
$callback = array_values($callback);
$class = $callback[0] ?? '';
$method = $callback[1] ?? '';
return $class && $method ? ($class . '::' . $method) : json_encode($callback);
}
if ($callback instanceof \Closure) {
return 'Closure';
}
if (is_string($callback)) {
return $callback;
}
return get_debug_type($callback);
}
/**
* @return void
* @deprecated
*/
public static function container()
{
}
}
+199
View File
@@ -0,0 +1,199 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Route;
use Webman\Route as Router;
use function array_merge;
use function count;
use function preg_replace_callback;
use function str_replace;
/**
* Class Route
* @package Webman
*/
class Route
{
/**
* @var string|null
*/
protected $name = null;
/**
* @var array
*/
protected $methods = [];
/**
* @var string
*/
protected $path = '';
/**
* @var callable
*/
protected $callback = null;
/**
* @var array
*/
protected $middlewares = [];
/**
* @var array
*/
protected $params = [];
/**
* Route constructor.
* @param array $methods
* @param string $path
* @param callable $callback
*/
public function __construct($methods, string $path, $callback)
{
$this->methods = (array)$methods;
$this->path = $path;
$this->callback = $callback;
}
/**
* Get name.
* @return string|null
*/
public function getName(): ?string
{
return $this->name ?? null;
}
/**
* Name.
* @param string $name
* @return $this
*/
public function name(string $name): Route
{
$this->name = $name;
Router::setByName($name, $this);
return $this;
}
/**
* Middleware.
* @param mixed $middleware
* @return $this|array
*/
public function middleware(mixed $middleware = null)
{
if ($middleware === null) {
return $this->middlewares;
}
$this->middlewares = array_merge($this->middlewares, is_array($middleware) ? array_reverse($middleware) : [$middleware]);
return $this;
}
/**
* GetPath.
* @return string
*/
public function getPath(): string
{
return $this->path;
}
/**
* GetMethods.
* @return array
*/
public function getMethods(): array
{
return $this->methods;
}
/**
* GetCallback.
* @return callable|null
*/
public function getCallback()
{
return $this->callback;
}
/**
* GetMiddleware.
* @return array
*/
public function getMiddleware(): array
{
return $this->middlewares;
}
/**
* Param.
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function param(?string $name = null, mixed $default = null)
{
if ($name === null) {
return $this->params;
}
return $this->params[$name] ?? $default;
}
/**
* SetParams.
* @param array $params
* @return $this
*/
public function setParams(array $params): Route
{
$this->params = array_merge($this->params, $params);
return $this;
}
/**
* Url.
* @param array $parameters
* @return string
*/
public function url(array $parameters = []): string
{
if (empty($parameters)) {
return $this->path;
}
$path = str_replace(['[', ']'], '', $this->path);
$path = preg_replace_callback('/\{(.*?)(?:\:[^\}]*?)*?\}/', function ($matches) use (&$parameters) {
if (!$parameters) {
return $matches[0];
}
if (isset($parameters[$matches[1]])) {
$value = $parameters[$matches[1]];
unset($parameters[$matches[1]]);
return $value;
}
$key = key($parameters);
if (is_int($key)) {
$value = $parameters[$key];
unset($parameters[$key]);
return $value;
}
return $matches[0];
}, $path);
return count($parameters) > 0 ? $path . '?' . http_build_query($parameters) : $path;
}
}
@@ -0,0 +1,26 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Session;
use Workerman\Protocols\Http\Session\FileSessionHandler as FileHandler;
/**
* Class FileSessionHandler
* @package Webman
*/
class FileSessionHandler extends FileHandler
{
}
@@ -0,0 +1,22 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Session;
use Workerman\Protocols\Http\Session\RedisClusterSessionHandler as RedisClusterHandler;
class RedisClusterSessionHandler extends RedisClusterHandler
{
}
@@ -0,0 +1,26 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman\Session;
use Workerman\Protocols\Http\Session\RedisSessionHandler as RedisHandler;
/**
* Class FileSessionHandler
* @package Webman
*/
class RedisSessionHandler extends RedisHandler
{
}
+44
View File
@@ -0,0 +1,44 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
use function array_diff;
use function array_map;
use function scandir;
/**
* Class Util
* @package Webman
*/
class Util
{
/**
* ScanDir.
* @param string $basePath
* @param bool $withBasePath
* @return array
*/
public static function scanDir(string $basePath, bool $withBasePath = true): array
{
if (!is_dir($basePath)) {
return [];
}
$paths = array_diff(scandir($basePath), array('.', '..')) ?: [];
return $withBasePath ? array_map(static function ($path) use ($basePath) {
return $basePath . DIRECTORY_SEPARATOR . $path;
}, $paths) : $paths;
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Webman;
interface View
{
/**
* Render.
* @param string $template
* @param array $vars
* @param string|null $app
* @return string
*/
public static function render(string $template, array $vars, ?string $app = null): string;
}
+167
View File
@@ -0,0 +1,167 @@
<?php
namespace support;
use Dotenv\Dotenv;
use RuntimeException;
use Throwable;
use Webman\Config;
use Webman\Util;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;
use function base_path;
use function call_user_func;
use function is_dir;
use function opcache_get_status;
use function opcache_invalidate;
use const DIRECTORY_SEPARATOR;
class App
{
/**
* Run.
* @return void
* @throws Throwable
*/
public static function run()
{
ini_set('display_errors', 'on');
error_reporting(E_ALL);
if (class_exists(Dotenv::class) && file_exists(run_path('.env'))) {
if (method_exists(Dotenv::class, 'createUnsafeImmutable')) {
Dotenv::createUnsafeImmutable(run_path())->load();
} else {
Dotenv::createMutable(run_path())->load();
}
}
if (!$appConfigFile = config_path('app.php')) {
throw new RuntimeException('Config file not found: app.php');
}
$appConfig = require $appConfigFile;
if ($timezone = $appConfig['default_timezone'] ?? '') {
date_default_timezone_set($timezone);
}
static::loadAllConfig(['route', 'container']);
if (!is_phar() && DIRECTORY_SEPARATOR === '\\' && empty(config('server.listen'))) {
echo "Please run 'php windows.php' on windows system." . PHP_EOL;
exit;
}
$errorReporting = config('app.error_reporting');
if (isset($errorReporting)) {
error_reporting($errorReporting);
}
$runtimeLogsPath = runtime_path() . DIRECTORY_SEPARATOR . 'logs';
if (!file_exists($runtimeLogsPath) || !is_dir($runtimeLogsPath)) {
if (!mkdir($runtimeLogsPath, 0777, true)) {
throw new RuntimeException("Failed to create runtime logs directory. Please check the permission.");
}
}
$runtimeViewsPath = runtime_path() . DIRECTORY_SEPARATOR . 'views';
if (!file_exists($runtimeViewsPath) || !is_dir($runtimeViewsPath)) {
if (!mkdir($runtimeViewsPath, 0777, true)) {
throw new RuntimeException("Failed to create runtime views directory. Please check the permission.");
}
}
Worker::$onMasterReload = function () {
if (function_exists('opcache_get_status')) {
if ($status = opcache_get_status()) {
if (isset($status['scripts']) && $scripts = $status['scripts']) {
foreach (array_keys($scripts) as $file) {
opcache_invalidate($file, true);
}
}
}
}
};
$config = config('server');
Worker::$pidFile = $config['pid_file'];
Worker::$stdoutFile = $config['stdout_file'];
Worker::$logFile = $config['log_file'];
Worker::$eventLoopClass = $config['event_loop'] ?? '';
TcpConnection::$defaultMaxPackageSize = $config['max_package_size'] ?? 10 * 1024 * 1024;
if (property_exists(Worker::class, 'statusFile')) {
Worker::$statusFile = $config['status_file'] ?? '';
}
if (property_exists(Worker::class, 'stopTimeout')) {
Worker::$stopTimeout = $config['stop_timeout'] ?? 2;
}
if ($config['listen'] ?? false) {
$worker = new Worker($config['listen'], $config['context'] ?? []);
$propertyMap = [
'name',
'count',
'user',
'group',
'reusePort',
'transport',
'protocol'
];
foreach ($propertyMap as $property) {
if (isset($config[$property])) {
$worker->$property = $config[$property];
}
}
$worker->onWorkerStart = function ($worker) {
require_once base_path() . '/support/bootstrap.php';
$app = new \Webman\App(config('app.request_class', Request::class), Log::channel('default'), app_path(), public_path());
$worker->onMessage = [$app, 'onMessage'];
call_user_func([$app, 'onWorkerStart'], $worker);
};
}
$windowsWithoutServerListen = is_phar() && DIRECTORY_SEPARATOR === '\\' && empty($config['listen']);
$process = config('process', []);
if ($windowsWithoutServerListen && $process) {
$processName = isset($process['webman']) ? 'webman' : key($process);
worker_start($processName, $process[$processName]);
} else if (DIRECTORY_SEPARATOR === '/') {
foreach (config('process', []) as $processName => $config) {
worker_start($processName, $config);
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['process'] ?? [] as $processName => $config) {
worker_start("plugin.$firm.$name.$processName", $config);
}
}
foreach ($projects['process'] ?? [] as $processName => $config) {
worker_start("plugin.$firm.$processName", $config);
}
}
}
Worker::runAll();
}
/**
* LoadAllConfig.
* @param array $excludes
* @return void
*/
public static function loadAllConfig(array $excludes = [])
{
Config::load(config_path(), $excludes);
$directory = base_path() . '/plugin';
foreach (Util::scanDir($directory, false) as $name) {
$dir = "$directory/$name/config";
if (is_dir($dir)) {
Config::load($dir, $excludes, "plugin.$name");
}
}
}
}
@@ -0,0 +1,47 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
use Webman\Config;
/**
* Class Container
* @package support
* @method static mixed get($name)
* @method static mixed make($name, array $parameters)
* @method static bool has($name)
*/
class Container
{
/**
* Instance
* @param string $plugin
* @return array|mixed|void|null
*/
public static function instance(string $plugin = '')
{
return Config::get($plugin ? "plugin.$plugin.container" : 'container');
}
/**
* @param string $name
* @param array $arguments
* @return mixed
*/
public static function __callStatic(string $name, array $arguments)
{
return static::instance()->{$name}(... $arguments);
}
}
@@ -0,0 +1,25 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
/**
* Class Context
* @package Webman
*/
class Context extends \Webman\Context
{
}
+143
View File
@@ -0,0 +1,143 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\FormattableHandlerInterface;
use Monolog\Handler\HandlerInterface;
use Monolog\Logger;
use function array_values;
use function config;
use function is_array;
/**
* Class Log
* @package support
*
* @method static void log($level, $message, array $context = [])
* @method static void debug($message, array $context = [])
* @method static void info($message, array $context = [])
* @method static void notice($message, array $context = [])
* @method static void warning($message, array $context = [])
* @method static void error($message, array $context = [])
* @method static void critical($message, array $context = [])
* @method static void alert($message, array $context = [])
* @method static void emergency($message, array $context = [])
*/
class Log
{
/**
* @var array
*/
protected static $instance = [];
/**
* Channel.
* @param string $name
* @return Logger
*/
public static function channel(string $name = 'default'): Logger
{
if (!isset(static::$instance[$name])) {
$config = config('log', [])[$name];
$handlers = self::handlers($config);
$processors = self::processors($config);
$logger = new Logger($name, $handlers, $processors);
if (method_exists($logger, 'useLoggingLoopDetection')) {
$logger->useLoggingLoopDetection(false);
}
static::$instance[$name] = $logger;
}
return static::$instance[$name];
}
/**
* Handlers.
* @param array $config
* @return array
*/
protected static function handlers(array $config): array
{
$handlerConfigs = $config['handlers'] ?? [[]];
$handlers = [];
foreach ($handlerConfigs as $value) {
$class = $value['class'] ?? [];
$constructor = $value['constructor'] ?? [];
$formatterConfig = $value['formatter'] ?? [];
$class && $handlers[] = self::handler($class, $constructor, $formatterConfig);
}
return $handlers;
}
/**
* Handler.
* @param string $class
* @param array $constructor
* @param array $formatterConfig
* @return HandlerInterface
*/
protected static function handler(string $class, array $constructor, array $formatterConfig): HandlerInterface
{
/** @var HandlerInterface $handler */
$handler = new $class(... array_values($constructor));
if ($handler instanceof FormattableHandlerInterface && $formatterConfig) {
$formatterClass = $formatterConfig['class'];
$formatterConstructor = $formatterConfig['constructor'];
/** @var FormatterInterface $formatter */
$formatter = new $formatterClass(... array_values($formatterConstructor));
$handler->setFormatter($formatter);
}
return $handler;
}
/**
* Processors.
* @param array $config
* @return array
*/
protected static function processors(array $config): array
{
$result = [];
if (!isset($config['processors']) && isset($config['processor'])) {
$config['processors'] = [$config['processor']];
}
foreach ($config['processors'] ?? [] as $value) {
if (is_array($value) && isset($value['class'])) {
$value = new $value['class'](... array_values($value['constructor'] ?? []));
}
$result[] = $value;
}
return $result;
}
/**
* @param string $name
* @param array $arguments
* @return mixed
*/
public static function __callStatic(string $name, array $arguments)
{
return static::channel()->{$name}(... $arguments);
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace support;
use function defined;
use function is_callable;
use function is_file;
use function method_exists;
class Plugin
{
/**
* Install.
* @param mixed $event
* @return void
*/
public static function install($event)
{
static::findHelper();
$psr4 = static::getPsr4($event);
foreach ($psr4 as $namespace => $path) {
$pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
if (!defined($pluginConst)) {
continue;
}
$installFunction = "\\{$namespace}Install::install";
if (is_callable($installFunction)) {
$installFunction(true);
}
}
}
/**
* Update.
* @param mixed $event
* @return void
*/
public static function update($event)
{
static::findHelper();
$psr4 = static::getPsr4($event);
foreach ($psr4 as $namespace => $path) {
$pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
if (!defined($pluginConst)) {
continue;
}
$updateFunction = "\\{$namespace}Install::update";
if (is_callable($updateFunction)) {
$updateFunction();
continue;
}
$installFunction = "\\{$namespace}Install::install";
if (is_callable($installFunction)) {
$installFunction(false);
}
}
}
/**
* Uninstall.
* @param mixed $event
* @return void
*/
public static function uninstall($event)
{
static::findHelper();
$psr4 = static::getPsr4($event);
foreach ($psr4 as $namespace => $path) {
$pluginConst = "\\{$namespace}Install::WEBMAN_PLUGIN";
if (!defined($pluginConst)) {
continue;
}
$uninstallFunction = "\\{$namespace}Install::uninstall";
if (is_callable($uninstallFunction)) {
$uninstallFunction();
}
}
}
/**
* Get psr-4 info
*
* @param mixed $event
* @return array
*/
protected static function getPsr4($event)
{
$operation = $event->getOperation();
$autoload = method_exists($operation, 'getPackage') ? $operation->getPackage()->getAutoload() : $operation->getTargetPackage()->getAutoload();
return $autoload['psr-4'] ?? [];
}
/**
* FindHelper.
* @return void
*/
protected static function findHelper()
{
// Plugin.php in webman
require_once __DIR__ . '/helpers.php';
}
}
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
/**
* Class Request
* @package support
*/
class Request extends \Webman\Http\Request
{
}
@@ -0,0 +1,24 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
/**
* Class Response
* @package support
*/
class Response extends \Webman\Http\Response
{
}
@@ -0,0 +1,108 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use Symfony\Component\Translation\Translator;
use Webman\Exception\NotFoundException;
use function basename;
use function config;
use function get_realpath;
use function pathinfo;
use function request;
use function substr;
/**
* Class Translation
* @package support
* @method static string trans(?string $id, array $parameters = [], string $domain = null, string $locale = null)
* @method static void setLocale(string $locale)
* @method static string getLocale()
*/
class Translation
{
/**
* @var Translator[]
*/
protected static $instance = [];
/**
* Instance.
* @param string $plugin
* @param array|null $config
* @return Translator
* @throws NotFoundException
*/
public static function instance(string $plugin = '', ?array $config = null): Translator
{
if (!isset(static::$instance[$plugin])) {
$config = $config ?? config($plugin ? "plugin.$plugin.translation" : 'translation', []);
$paths = (array)($config['path'] ?? []);
static::$instance[$plugin] = $translator = new Translator($config['locale']);
$translator->setFallbackLocales($config['fallback_locale']);
$classes = $config['loader'] ?? [
'Symfony\Component\Translation\Loader\PhpFileLoader' => [
'extension' => '.php',
'format' => 'phpfile'
],
'Symfony\Component\Translation\Loader\PoFileLoader' => [
'extension' => '.po',
'format' => 'pofile'
]
];
foreach ($paths as $path) {
// Phar support. Compatible with the 'realpath' function in the phar file.
if (!$translationsPath = get_realpath($path)) {
continue;
}
foreach ($classes as $class => $opts) {
$translator->addLoader($opts['format'], new $class);
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($translationsPath, FilesystemIterator::SKIP_DOTS));
$files = new RegexIterator($iterator, '/^.+' . preg_quote($opts['extension']) . '$/i', RegexIterator::GET_MATCH);
foreach ($files as $file) {
$file = $file[0];
$domain = basename($file, $opts['extension']);
$dirName = pathinfo($file, PATHINFO_DIRNAME);
$locale = substr(strrchr($dirName, DIRECTORY_SEPARATOR), 1);
if ($domain && $locale) {
$translator->addResource($opts['format'], $file, $locale, $domain);
}
}
}
}
}
return static::$instance[$plugin];
}
/**
* @param string $name
* @param array $arguments
* @return mixed
* @throws NotFoundException
*/
public static function __callStatic(string $name, array $arguments)
{
$request = request();
$plugin = $request->plugin ?? '';
return static::instance($plugin)->{$name}(... $arguments);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace support;
use function config;
use function request;
class View
{
/**
* Assign.
* @param mixed $name
* @param mixed $value
* @return void
*/
public static function assign($name, mixed $value = null)
{
$request = request();
$plugin = $request->plugin ?? '';
$handler = config($plugin ? "plugin.$plugin.view.handler" : 'view.handler');
$handler::assign($name, $value);
}
}
@@ -0,0 +1,13 @@
<?php
namespace support\annotation;
use Attribute;
/**
* @deprecated Use support\annotation\route\DisableDefaultRoute instead.
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class DisableDefaultRoute extends \support\annotation\route\DisableDefaultRoute
{
}
@@ -0,0 +1,41 @@
<?php
namespace support\annotation;
use Attribute;
/**
* Attach middlewares to routes/controllers/functions via attributes.
*
* Example:
* #[Middleware(AuthMiddleware::class, RateLimitMiddleware::class)]
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class Middleware
{
/**
* @var array
*/
protected array $middlewares = [];
/**
* @param mixed ...$middlewares Middleware class names.
*/
public function __construct(...$middlewares)
{
$this->middlewares = $middlewares;
}
/**
* Convert to webman middleware callable format: [MiddlewareClass, 'process'].
* @return array
*/
public function getMiddlewares(): array
{
$middlewares = [];
foreach ($this->middlewares as $middleware) {
$middlewares[] = [$middleware, 'process'];
}
return $middlewares;
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: ['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS'], ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Any extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'], $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'DELETE', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Delete extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'DELETE', $name);
}
}
@@ -0,0 +1,13 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Disable webman's default route mapping for a controller or action.
*/
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class DisableDefaultRoute
{
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'GET', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Get extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'GET', $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'HEAD', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Head extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'HEAD', $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'OPTIONS', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Options extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'OPTIONS', $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'PATCH', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Patch extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'PATCH', $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'POST', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Post extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'POST', $name);
}
}
@@ -0,0 +1,22 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Shortcut for #[Route(methods: 'PUT', ...)].
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Put extends Route
{
/**
* @param string|null $path Route path. Null means default-route method restriction only.
* @param string|null $name Route name
*/
public function __construct(?string $path = null, ?string $name = null)
{
parent::__construct($path, 'PUT', $name);
}
}
@@ -0,0 +1,40 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Define an explicit route, or restrict allowed HTTP methods for default route when path is null.
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Route
{
/**
* Route path. Null means "method restriction only" for default route.
*/
public ?string $path;
/**
* @var string[]
*/
public array $methods;
/**
* Route name for URL generation.
*/
public ?string $name;
/**
* @param string|null $path Route path, must start with "/". Null means "method restriction only" for default route.
* @param string|string[] $methods HTTP methods
* @param string|null $name Route name
*/
public function __construct(?string $path = null, array|string $methods = ['GET'], ?string $name = null)
{
$this->path = $path;
$this->methods = is_array($methods) ? $methods : [$methods];
$this->name = $name;
}
}
@@ -0,0 +1,26 @@
<?php
namespace support\annotation\route;
use Attribute;
/**
* Group routes by controller-level prefix.
*/
#[Attribute(Attribute::TARGET_CLASS)]
class RouteGroup
{
/**
* Prefix for all routes in this controller.
*/
public string $prefix;
/**
* @param string $prefix Route group prefix, e.g. "/api/v1"
*/
public function __construct(string $prefix = '')
{
$this->prefix = $prefix;
}
}
@@ -0,0 +1,139 @@
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use Dotenv\Dotenv;
use support\Log;
use Webman\Bootstrap;
use Webman\Config;
use Webman\Middleware;
use Webman\Route;
use Webman\Util;
use Workerman\Events\Select;
use Workerman\Worker;
$worker = $worker ?? null;
if (empty(Worker::$eventLoopClass)) {
Worker::$eventLoopClass = Select::class;
}
set_error_handler(function ($level, $message, $file = '', $line = 0) {
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
});
if ($worker) {
register_shutdown_function(function ($startTime) {
if (time() - $startTime <= 0.1) {
sleep(1);
}
}, time());
}
if (class_exists('Dotenv\Dotenv') && file_exists(base_path(false) . '/.env')) {
if (method_exists('Dotenv\Dotenv', 'createUnsafeMutable')) {
Dotenv::createUnsafeMutable(base_path(false))->load();
} else {
Dotenv::createMutable(base_path(false))->load();
}
}
Config::clear();
support\App::loadAllConfig(['route']);
if ($timezone = config('app.default_timezone')) {
date_default_timezone_set($timezone);
}
foreach (config('autoload.files', []) as $file) {
include_once $file;
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['autoload']['files'] ?? [] as $file) {
include_once $file;
}
}
foreach ($projects['autoload']['files'] ?? [] as $file) {
include_once $file;
}
}
Middleware::load(config('middleware', []));
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project) || $name === 'static') {
continue;
}
Middleware::load($project['middleware'] ?? []);
}
Middleware::load($projects['middleware'] ?? [], $firm);
if ($staticMiddlewares = config("plugin.$firm.static.middleware")) {
Middleware::load(['__static__' => $staticMiddlewares], $firm);
}
}
Middleware::load(['__static__' => config('static.middleware', [])]);
foreach (config('bootstrap', []) as $className) {
if (!class_exists($className)) {
$log = "Warning: Class $className setting in config/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
foreach (config('plugin', []) as $firm => $projects) {
foreach ($projects as $name => $project) {
if (!is_array($project)) {
continue;
}
foreach ($project['bootstrap'] ?? [] as $className) {
if (!class_exists($className)) {
$log = "Warning: Class $className setting in config/plugin/$firm/$name/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
}
foreach ($projects['bootstrap'] ?? [] as $className) {
/** @var string $className */
if (!class_exists($className)) {
$log = "Warning: Class $className setting in plugin/$firm/config/bootstrap.php not found\r\n";
echo $log;
Log::error($log);
continue;
}
/** @var Bootstrap $className */
$className::start($worker);
}
}
$directory = base_path() . '/plugin';
$paths = [config_path()];
foreach (Util::scanDir($directory) as $path) {
if (is_dir($path = "$path/config")) {
$paths[] = $path;
}
}
Route::load($paths);

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