暂存
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
composer.lock
|
||||
vendor
|
||||
.idea
|
||||
tests/.phpunit.result.cache
|
||||
tests/workerman.log
|
||||
Vendored
+21
@@ -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.
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# Workerman coroutine library
|
||||
|
||||
This is Workerman's coroutine library, which includes `Coroutine` `Channel` `Barrier` `Parallel` `Pool`.
|
||||
+32
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
{
|
||||
// 模拟关闭连接
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+1328
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Webman;
|
||||
|
||||
use Workerman\Worker;
|
||||
|
||||
interface Bootstrap
|
||||
{
|
||||
/**
|
||||
* onWorkerStart
|
||||
*
|
||||
* @param Worker|null $worker
|
||||
* @return mixed
|
||||
*/
|
||||
public static function start(?Worker $worker);
|
||||
}
|
||||
+321
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Webman\Exception;
|
||||
|
||||
use Throwable;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
|
||||
interface ExceptionHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @param Throwable $exception
|
||||
* @return mixed
|
||||
*/
|
||||
public function report(Throwable $exception);
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Throwable $exception
|
||||
* @return Response
|
||||
*/
|
||||
public function render(Request $request, Throwable $exception): Response;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Webman\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class FileException
|
||||
* @package Webman\Exception
|
||||
*/
|
||||
class FileException extends RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Webman\Exception;
|
||||
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
/**
|
||||
* Class NotFoundException
|
||||
* @package Webman\Exception
|
||||
*/
|
||||
class NotFoundException extends \Exception implements NotFoundExceptionInterface
|
||||
{
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Webman;
|
||||
|
||||
use SplFileInfo;
|
||||
use Webman\Exception\FileException;
|
||||
use function chmod;
|
||||
use function is_dir;
|
||||
use function mkdir;
|
||||
use function pathinfo;
|
||||
use function restore_error_handler;
|
||||
use function set_error_handler;
|
||||
use function sprintf;
|
||||
use function strip_tags;
|
||||
use function umask;
|
||||
|
||||
class File extends SplFileInfo
|
||||
{
|
||||
|
||||
/**
|
||||
* Move.
|
||||
* @param string $destination
|
||||
* @return File
|
||||
*/
|
||||
public function move(string $destination): File
|
||||
{
|
||||
set_error_handler(function ($type, $msg) use (&$error) {
|
||||
$error = $msg;
|
||||
});
|
||||
$path = pathinfo($destination, PATHINFO_DIRNAME);
|
||||
if (!is_dir($path) && !mkdir($path, 0777, true)) {
|
||||
restore_error_handler();
|
||||
throw new FileException(sprintf('Unable to create the "%s" directory (%s)', $path, strip_tags($error)));
|
||||
}
|
||||
if (!rename($this->getPathname(), $destination)) {
|
||||
restore_error_handler();
|
||||
throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $destination, strip_tags($error)));
|
||||
}
|
||||
restore_error_handler();
|
||||
@chmod($destination, 0666 & ~umask());
|
||||
return new self($destination);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Webman\Route;
|
||||
|
||||
use Webman\Route as Router;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function preg_replace_callback;
|
||||
use function str_replace;
|
||||
|
||||
/**
|
||||
* Class Route
|
||||
* @package Webman
|
||||
*/
|
||||
class Route
|
||||
{
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $name = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $methods = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $path = '';
|
||||
|
||||
/**
|
||||
* @var callable
|
||||
*/
|
||||
protected $callback = null;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $middlewares = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $params = [];
|
||||
|
||||
/**
|
||||
* Route constructor.
|
||||
* @param array $methods
|
||||
* @param string $path
|
||||
* @param callable $callback
|
||||
*/
|
||||
public function __construct($methods, string $path, $callback)
|
||||
{
|
||||
$this->methods = (array)$methods;
|
||||
$this->path = $path;
|
||||
$this->callback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name.
|
||||
* @return string|null
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Name.
|
||||
* @param string $name
|
||||
* @return $this
|
||||
*/
|
||||
public function name(string $name): Route
|
||||
{
|
||||
$this->name = $name;
|
||||
Router::setByName($name, $this);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware.
|
||||
* @param mixed $middleware
|
||||
* @return $this|array
|
||||
*/
|
||||
public function middleware(mixed $middleware = null)
|
||||
{
|
||||
if ($middleware === null) {
|
||||
return $this->middlewares;
|
||||
}
|
||||
$this->middlewares = array_merge($this->middlewares, is_array($middleware) ? array_reverse($middleware) : [$middleware]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetPath.
|
||||
* @return string
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetMethods.
|
||||
* @return array
|
||||
*/
|
||||
public function getMethods(): array
|
||||
{
|
||||
return $this->methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetCallback.
|
||||
* @return callable|null
|
||||
*/
|
||||
public function getCallback()
|
||||
{
|
||||
return $this->callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* GetMiddleware.
|
||||
* @return array
|
||||
*/
|
||||
public function getMiddleware(): array
|
||||
{
|
||||
return $this->middlewares;
|
||||
}
|
||||
|
||||
/**
|
||||
* Param.
|
||||
* @param string|null $name
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function param(?string $name = null, mixed $default = null)
|
||||
{
|
||||
if ($name === null) {
|
||||
return $this->params;
|
||||
}
|
||||
return $this->params[$name] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* SetParams.
|
||||
* @param array $params
|
||||
* @return $this
|
||||
*/
|
||||
public function setParams(array $params): Route
|
||||
{
|
||||
$this->params = array_merge($this->params, $params);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Url.
|
||||
* @param array $parameters
|
||||
* @return string
|
||||
*/
|
||||
public function url(array $parameters = []): string
|
||||
{
|
||||
if (empty($parameters)) {
|
||||
return $this->path;
|
||||
}
|
||||
$path = str_replace(['[', ']'], '', $this->path);
|
||||
$path = preg_replace_callback('/\{(.*?)(?:\:[^\}]*?)*?\}/', function ($matches) use (&$parameters) {
|
||||
if (!$parameters) {
|
||||
return $matches[0];
|
||||
}
|
||||
if (isset($parameters[$matches[1]])) {
|
||||
$value = $parameters[$matches[1]];
|
||||
unset($parameters[$matches[1]]);
|
||||
return $value;
|
||||
}
|
||||
$key = key($parameters);
|
||||
if (is_int($key)) {
|
||||
$value = $parameters[$key];
|
||||
unset($parameters[$key]);
|
||||
return $value;
|
||||
}
|
||||
return $matches[0];
|
||||
}, $path);
|
||||
return count($parameters) > 0 ? $path . '?' . http_build_query($parameters) : $path;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Webman\Session;
|
||||
|
||||
use Workerman\Protocols\Http\Session\FileSessionHandler as FileHandler;
|
||||
|
||||
/**
|
||||
* Class FileSessionHandler
|
||||
* @package Webman
|
||||
*/
|
||||
class FileSessionHandler extends FileHandler
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Webman\Session;
|
||||
|
||||
use Workerman\Protocols\Http\Session\RedisClusterSessionHandler as RedisClusterHandler;
|
||||
|
||||
class RedisClusterSessionHandler extends RedisClusterHandler
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Webman\Session;
|
||||
|
||||
use Workerman\Protocols\Http\Session\RedisSessionHandler as RedisHandler;
|
||||
|
||||
/**
|
||||
* Class FileSessionHandler
|
||||
* @package Webman
|
||||
*/
|
||||
class RedisSessionHandler extends RedisHandler
|
||||
{
|
||||
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Webman;
|
||||
|
||||
use function array_diff;
|
||||
use function array_map;
|
||||
use function scandir;
|
||||
|
||||
/**
|
||||
* Class Util
|
||||
* @package Webman
|
||||
*/
|
||||
class Util
|
||||
{
|
||||
/**
|
||||
* ScanDir.
|
||||
* @param string $basePath
|
||||
* @param bool $withBasePath
|
||||
* @return array
|
||||
*/
|
||||
public static function scanDir(string $basePath, bool $withBasePath = true): array
|
||||
{
|
||||
if (!is_dir($basePath)) {
|
||||
return [];
|
||||
}
|
||||
$paths = array_diff(scandir($basePath), array('.', '..')) ?: [];
|
||||
return $withBasePath ? array_map(static function ($path) use ($basePath) {
|
||||
return $basePath . DIRECTORY_SEPARATOR . $path;
|
||||
}, $paths) : $paths;
|
||||
}
|
||||
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* This file is part of webman.
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* For full copyright and license information, please see the MIT-LICENSE.txt
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @author walkor<walkor@workerman.net>
|
||||
* @copyright walkor<walkor@workerman.net>
|
||||
* @link http://www.workerman.net/
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
|
||||
namespace Webman;
|
||||
|
||||
interface View
|
||||
{
|
||||
/**
|
||||
* Render.
|
||||
* @param string $template
|
||||
* @param array $vars
|
||||
* @param string|null $app
|
||||
* @return string
|
||||
*/
|
||||
public static function render(string $template, array $vars, ?string $app = null): string;
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+13
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -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
Reference in New Issue
Block a user