Watcher.php<?php namespace think\swoole; use think\swoole\watcher\Driver; /** * @mixin Driver */ class Watcher extends \think\Manager { protected $namespace = '\\think\\swoole\\watcher\\'; protected function getConfig(string $name, $default = null) { return $this->app->config->get('swoole.hot_update.' . $name, $default); } protected function resolveParams($name): array { return [ $this->getConfig('include', []), $this->getConfig('exclude', []), $this->getConfig('name', []), ]; } public function getDefaultDriver() { return $this->getConfig('type', 'scan'); } } Websocket.php<?php namespace think\swoole; use Swoole\Http\Response; use think\Event; use think\swoole\websocket\Pusher; use think\swoole\websocket\Room; /** * Class Websocket */ class Websocket { /** * @var \think\App */ protected $app; /** * @var Room */ protected $room; /** * Scoket sender's fd. * * @var string */ protected $sender; /** @var Event */ protected $event; /** @var Response */ protected $client; /** * Websocket constructor. * * @param \think\App $app * @param Room $room * @param Event $event */ public function __construct(\think\App $app, Room $room, Event $event) { $this->app = $app; $this->room = $room; $this->event = $event; } /** * @return Pusher */ protected function makePusher() { return $this->app->invokeClass(Pusher::class); } public function to(...$values) { return $this->makePusher()->to(...$values); } public function push($data) { $this->makePusher()->to($this->getSender())->push($data); } public function emit(string $event, ...$data) { $this->makePusher()->to($this->getSender())->emit($event, ...$data); } /** * Join sender to multiple rooms. * * @param string|integer|array $rooms * * @return $this */ public function join($rooms): self { $rooms = is_string($rooms) || is_int($rooms) ? func_get_args() : $rooms; $this->room->add($this->getSender(), $rooms); return $this; } /** * Make sender leave multiple rooms. * * @param array|string|integer $rooms * * @return $this */ public function leave($rooms = []): self { $rooms = is_string($rooms) || is_int($rooms) ? func_get_args() : $rooms; $this->room->delete($this->getSender(), $rooms); return $this; } public function isEstablished() { return !!$this->client; } /** * Close current connection. */ public function close() { if ($this->client) { $this->client->close(); } } /** * @param Response $response */ public function setClient($response) { $this->client = $response; } /** * Set sender fd. * * @param string * * @return $this */ public function setSender(string $fd) { $this->sender = $fd; return $this; } /** * Get current sender fd. */ public function getSender() { return $this->sender; } } App.php<?php namespace think\swoole; class App extends \think\App { protected $inConsole = true; public function setInConsole($inConsole = true) { $this->inConsole = $inConsole; } public function runningInConsole(): bool { return $this->inConsole; } public function clearInstances() { $this->instances = []; } } Service.php<?php // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK IT ] // +---------------------------------------------------------------------- // | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: yunwuxin <448901948@qq.com> // +---------------------------------------------------------------------- namespace think\swoole; use think\swoole\command\RpcInterface; use think\swoole\command\Server as ServerCommand; class Service extends \think\Service { public function boot() { $this->commands( ServerCommand::class, RpcInterface::class, ); } } Pool.php<?php namespace think\swoole; use Smf\ConnectionPool\ConnectionPool; use think\helper\Arr; class Pool { protected $pools = []; /** * @param string $name * @param ConnectionPool $pool * * @return Pool */ public function add(string $name, ConnectionPool $pool) { $pool->init(); $this->pools[$name] = $pool; return $this; } /** * @param string $name * * @return ConnectionPool */ public function get(string $name) { return $this->pools[$name] ?? null; } public function close(string $key) { return $this->pools[$key]->close(); } /** * @return array */ public function getAll() { return $this->pools; } public function closeAll() { foreach ($this->pools as $pool) { $pool->close(); } } /** * @param string $key * * @return ConnectionPool */ public function __get($key) { return $this->get($key); } public static function pullPoolConfig(&$config) { return [ 'minActive' => Arr::pull($config, 'min_active', 0), 'maxActive' => Arr::pull($config, 'max_active', 10), 'maxWaitTime' => Arr::pull($config, 'max_wait_time', 5), 'maxIdleTime' => Arr::pull($config, 'max_idle_time', 20), 'idleCheckInterval' => Arr::pull($config, 'idle_check_interval', 10), ]; } } Http.php<?php namespace think\swoole; /** * Class Http * @package think\swoole * @property $request */ class Http extends \think\Http { protected function loadMiddleware(): void { } protected function loadRoutes(): void { } } Sandbox.php<?php namespace think\swoole; use Closure; use InvalidArgumentException; use ReflectionObject; use RuntimeException; use Swoole\Coroutine; use think\App; use think\Config; use think\Container; use think\Event; use think\swoole\App as SwooleApp; use think\swoole\concerns\ModifyProperty; use think\swoole\contract\ResetterInterface; use think\swoole\coroutine\Context; use think\swoole\resetters\ClearInstances; use think\swoole\resetters\ResetConfig; use think\swoole\resetters\ResetEvent; use think\swoole\resetters\ResetModel; use think\swoole\resetters\ResetPaginator; use think\swoole\resetters\ResetService; use Throwable; class Sandbox { use ModifyProperty; /** * The app containers in different coroutine environment. * * @var SwooleApp[] */ protected $snapshots = []; /** @var SwooleApp */ protected $app; /** @var Config */ protected $config; /** @var Event */ protected $event; /** @var ResetterInterface[] */ protected $resetters = []; protected $services = []; public function __construct(Container $app) { $this->setBaseApp($app); $this->initialize(); } public function setBaseApp(Container $app) { $this->app = $app; return $this; } public function getBaseApp() { return $this->app; } protected function initialize() { Container::setInstance(function () { return $this->getApplication(); }); $this->setInitialConfig(); $this->setInitialServices(); $this->setInitialEvent(); $this->setInitialResetters(); return $this; } public function run(Closure $callable) { $this->init(); try { $this->getApplication()->invoke($callable, [$this]); } catch (Throwable $e) { throw $e; } finally { $this->clear(); } } public function init() { $app = $this->getApplication(true); $this->setInstance($app); $this->resetApp($app); } public function clear() { if ($app = $this->getSnapshot()) { $app->clearInstances(); unset($this->snapshots[$this->getSnapshotId()]); } Context::clear(); $this->setInstance($this->getBaseApp()); } public function getApplication($init = false) { $snapshot = $this->getSnapshot($init); if ($snapshot instanceof Container) { return $snapshot; } if ($init) { $snapshot = clone $this->getBaseApp(); $this->setSnapshot($snapshot); return $snapshot; } throw new InvalidArgumentException('The app object has not been initialized'); } protected function getSnapshotId($init = false) { if ($init) { Coroutine::getContext()->offsetSet('#root', true); return Coroutine::getCid(); } else { $cid = Coroutine::getCid(); while (!Coroutine::getContext($cid)->offsetExists('#root')) { $cid = Coroutine::getPcid($cid); if ($cid < 1) { break; } } return $cid; } } /** * Get current snapshot. * @return App|null */ public function getSnapshot($init = false) { return $this->snapshots[$this->getSnapshotId($init)] ?? null; } public function setSnapshot(Container $snapshot) { $this->snapshots[$this->getSnapshotId()] = $snapshot; return $this; } public function setInstance(Container $app) { $app->instance('app', $app); $app->instance(Container::class, $app); $reflectObject = new ReflectionObject($app); $reflectProperty = $reflectObject->getProperty('services'); $reflectProperty->setAccessible(true); $services = $reflectProperty->getValue($app); foreach ($services as $service) { $this->modifyProperty($service, $app); } } /** * Set initial config. */ protected function setInitialConfig() { $this->config = clone $this->getBaseApp()->config; } protected function setInitialEvent() { $this->event = clone $this->getBaseApp()->event; } /** * Get config snapshot. */ public function getConfig() { return $this->config; } public function getEvent() { return $this->event; } public function getServices() { return $this->services; } protected function setInitialServices() { $app = $this->getBaseApp(); $services = $this->config->get('swoole.services', []); foreach ($services as $service) { if (class_exists($service) && !in_array($service, $this->services)) { $serviceObj = new $service($app); $this->services[$service] = $serviceObj; } } } /** * Initialize resetters. */ protected function setInitialResetters() { $app = $this->getBaseApp(); $resetters = [ ClearInstances::class, ResetConfig::class, ResetEvent::class, ResetService::class, ResetModel::class, ResetPaginator::class, ]; $resetters = array_merge($resetters, $this->config->get('swoole.resetters', [])); foreach ($resetters as $resetter) { $resetterClass = $app->make($resetter); if (!$resetterClass instanceof ResetterInterface) { throw new RuntimeException("{$resetter} must implement " . ResetterInterface::class); } $this->resetters[$resetter] = $resetterClass; } } /** * Reset Application. * * @param App $app */ protected function resetApp(App $app) { foreach ($this->resetters as $resetter) { $resetter->handle($app, $this); } } } Manager.php<?php // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK IT ] // +---------------------------------------------------------------------- // | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: yunwuxin <448901948@qq.com> // +---------------------------------------------------------------------- namespace think\swoole; use think\swoole\concerns\InteractsWithHttp; use think\swoole\concerns\InteractsWithPools; use think\swoole\concerns\InteractsWithQueue; use think\swoole\concerns\InteractsWithRpcClient; use think\swoole\concerns\InteractsWithRpcServer; use think\swoole\concerns\InteractsWithServer; use think\swoole\concerns\InteractsWithSwooleTable; use think\swoole\concerns\InteractsWithTracing; use think\swoole\concerns\WithApplication; use think\swoole\concerns\WithContainer; /** * Class Manager */ class Manager { use InteractsWithServer, InteractsWithSwooleTable, InteractsWithHttp, InteractsWithPools, InteractsWithRpcClient, InteractsWithRpcServer, InteractsWithQueue, InteractsWithTracing, WithContainer, WithApplication; /** * Initialize. */ protected function initialize(): void { $this->prepareTables(); $this->preparePools(); $this->prepareHttp(); $this->prepareRpcServer(); $this->prepareQueue(); $this->prepareRpcClient(); $this->prepareTracing(); } } Middleware.php<?php namespace think\swoole; use Closure; use InvalidArgumentException; use think\App; use think\Pipeline; class Middleware { /** * 中间件执行队列 * @var array */ protected $queue = []; /** * @var App */ protected $app; public function __construct(App $app, $middlewares = []) { $this->app = $app; foreach ($middlewares as $middleware) { $this->queue[] = $this->buildMiddleware($middleware); } } public static function make(App $app, $middlewares = []) { return new self($app, $middlewares); } /** * 调度管道 * @return Pipeline */ public function pipeline() { return (new Pipeline()) ->through(array_map(function ($middleware) { return function ($request, $next) use ($middleware) { [$call, $params] = $middleware; if (is_array($call) && is_string($call[0])) { $call = [$this->app->make($call[0]), $call[1]]; } return call_user_func($call, $request, $next, ...$params); }; }, $this->queue)); } /** * 解析中间件 * @param mixed $middleware * @return array */ protected function buildMiddleware($middleware): array { if (is_array($middleware)) { [$middleware, $params] = $middleware; } if ($middleware instanceof Closure) { return [$middleware, $params ?? []]; } if (!is_string($middleware)) { throw new InvalidArgumentException('The middleware is invalid'); } return [[$middleware, 'handle'], $params ?? []]; } } helpers.php<?php namespace { if (!function_exists('swoole_cpu_num')) { function swoole_cpu_num(): int { return 1; } } if (!defined('SWOOLE_SOCK_TCP')) { define('SWOOLE_SOCK_TCP', 1); } if (!defined('SWOOLE_PROCESS')) { define('SWOOLE_PROCESS', 3); } if (!defined('SWOOLE_HOOK_ALL')) { define('SWOOLE_HOOK_ALL', 1879048191); } } namespace think\swoole\helper { use think\swoole\response\File; function download(string $filename, string $name = '', $disposition = File::DISPOSITION_ATTACHMENT): File { $response = new File($filename, $disposition); if ($name) { $response->setContentDisposition($disposition, $name); } return $response; } function file(string $filename) { return new File($filename); } } Table.php<?php // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK IT ] // +---------------------------------------------------------------------- // | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: yunwuxin <448901948@qq.com> // +---------------------------------------------------------------------- namespace think\swoole; use Swoole\Table as SwooleTable; class Table { public const TYPE_INT = 1; public const TYPE_STRING = 3; public const TYPE_FLOAT = 2; /** * Registered swoole tables. * * @var array */ protected $tables = []; /** * Add a swoole table to existing tables. * * @param string $name * @param SwooleTable $table * * @return Table */ public function add(string $name, SwooleTable $table) { $this->tables[$name] = $table; return $this; } /** * Get a swoole table by its name from existing tables. * * @param string $name * * @return SwooleTable $table */ public function get(string $name) { return $this->tables[$name] ?? null; } /** * Get all existing swoole tables. * * @return array */ public function getAll() { return $this->tables; } /** * Dynamically access table. * * @param string $key * * @return SwooleTable */ public function __get(string $key) { return $this->get($key); } } commandServer.php<?php // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK IT ] // +---------------------------------------------------------------------- // | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: liu21st <liu21st@gmail.com> // +---------------------------------------------------------------------- namespace think\swoole\command; use think\console\Command; use think\console\input\Option; use think\swoole\Manager; class Server extends Command { public function configure() { $this->setName('swoole') ->addOption( 'env', 'E', Option::VALUE_REQUIRED, 'Environment name', '' ) ->setDescription('Swoole Server for ThinkPHP'); } public function handle(Manager $manager) { $this->checkEnvironment(); $this->output->writeln('Starting swoole server...'); $this->output->writeln('You can exit with <info>`CTRL-C`</info>'); $envName = $this->input->getOption('env'); $manager->start($envName); } /** * 检查环境 */ protected function checkEnvironment() { if (!extension_loaded('swoole')) { $this->output->error('Can\'t detect Swoole extension installed.'); exit(1); } if (!version_compare(swoole_version(), '4.6.0', 'ge')) { $this->output->error('Your Swoole version must be higher than `4.6.0`.'); exit(1); } } } RpcInterface.php<?php namespace think\swoole\command; use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\Dumper; use Nette\PhpGenerator\Helpers; use Nette\PhpGenerator\PhpFile; use think\console\Command; use think\helper\Arr; use think\swoole\contract\rpc\ParserInterface; use think\swoole\rpc\client\Gateway; use think\swoole\rpc\client\Service; use think\swoole\rpc\JsonParser; use function Swoole\Coroutine\run; class RpcInterface extends Command { public function configure() { $this->setName('rpc:interface') ->setDescription('Generate Rpc Service Interfaces'); } public function handle() { run(function () { $file = new PhpFile; $file->addComment('This file is auto-generated.'); $file->setStrictTypes(); $services = []; $clients = $this->app->config->get('swoole.rpc.client', []); foreach ($clients as $name => $config) { $parserClass = Arr::get($config, 'parser', JsonParser::class); /** @var ParserInterface $parser */ $parser = new $parserClass; $gateway = new Gateway($config, $parser, Arr::get($config, 'tries', 2)); $result = $gateway->getServices(); $namespace = $file->addNamespace("rpc\\contract\\{$name}"); $namespace->addUse(Service::class); foreach ($result as $interface => $methods) { $services[$name][] = $namespace->getName() . "\\{$interface}"; $class = $namespace->addInterface($interface); $class->addExtend(Service::class); foreach ($methods as $methodName => ['parameters' => $parameters, 'returnType' => $returnType, 'comment' => $comment]) { $method = $class->addMethod($methodName) ->setVisibility(ClassType::VISIBILITY_PUBLIC) ->setComment(Helpers::unformatDocComment($comment)) ->setReturnType($returnType); foreach ($parameters as $parameter) { if ($parameter['type'] && (class_exists($parameter['type']) || interface_exists($parameter['type']))) { $namespace->addUse($parameter['type']); } $param = $method->addParameter($parameter['name']) ->setType($parameter['type']); if (array_key_exists('default', $parameter)) { $param->setDefaultValue($parameter['default']); } if (array_key_exists('nullable', $parameter)) { $param->setNullable(); } } } } } $dumper = new Dumper(); $services = 'return ' . $dumper->dump($services) . ';'; file_put_contents($this->app->getBasePath() . 'rpc.php', $file . $services); $this->output->writeln('<info>Succeed!</info>'); }); } } concernsWithApplication.php<?php namespace think\swoole\concerns; use Closure; use think\App; use think\swoole\App as SwooleApp; use think\swoole\Manager; use think\swoole\pool\Cache; use think\swoole\pool\Db; use think\swoole\Sandbox; use Throwable; /** * Trait WithApplication * @package think\swoole\concerns * @property App $container */ trait WithApplication { /** * @var SwooleApp */ protected $app; protected function prepareApplication(string $envName) { if (!$this->app instanceof SwooleApp) { $this->app = new SwooleApp($this->container->getRootPath()); $this->app->setEnvName($envName); $this->app->bind(SwooleApp::class, App::class); $this->app->bind(Manager::class, $this); //绑定连接池 if ($this->getConfig('pool.db.enable', true)) { $this->app->bind('db', Db::class); $this->app->resolving(Db::class, function (Db $db) { $db->setLog($this->container->log); }); } if ($this->getConfig('pool.cache.enable', true)) { $this->app->bind('cache', Cache::class); } $this->app->initialize(); $this->app->instance('request', $this->container->request); $this->prepareConcretes(); } } /** * 预加载 */ protected function prepareConcretes() { $defaultConcretes = ['db', 'cache', 'event']; $concretes = array_merge($defaultConcretes, $this->getConfig('concretes', [])); foreach ($concretes as $concrete) { if ($this->app->has($concrete)) { $this->app->make($concrete); } } } public function getApplication() { return $this->app; } /** * 获取沙箱 * @return Sandbox */ protected function getSandbox() { return $this->app->make(Sandbox::class); } /** * 在沙箱中执行 * @param Closure $callable */ public function runInSandbox(Closure $callable) { try { $this->getSandbox()->run($callable); } catch (Throwable $e) { $this->logServerError($e); } } } InteractsWithRpcServer.php<?php namespace think\swoole\concerns; use Swoole\Coroutine\Server; use Swoole\Coroutine\Server\Connection; use think\App; use think\swoole\contract\rpc\ParserInterface; use think\swoole\Pool; use think\swoole\rpc\Error; use think\swoole\rpc\File; use think\swoole\rpc\JsonParser; use think\swoole\rpc\Packer; use think\swoole\rpc\server\Dispatcher; use Throwable; /** * Trait InteractsWithRpc * @package think\swoole\concerns * @property App $app * @property App $container * @method Pool getPools() */ trait InteractsWithRpcServer { protected function createRpcServer() { $this->bindRpcParser(); $this->bindRpcDispatcher(); $host = $this->getConfig('rpc.server.host', '0.0.0.0'); $port = $this->getConfig('rpc.server.port', 9000); $server = new Server($host, $port, false, true); $server->handle(function (Connection $conn) { $this->runInSandbox(function (App $app, Dispatcher $dispatcher) use ($conn) { $files = []; while (true) { //接收数据 $data = $conn->recv(); if ($data === '' || $data === false) { break; } begin: if (!isset($handler)) { try { [$handler, $data] = Packer::unpack($data); } catch (Throwable $e) { //错误的包头 $result = Error::make(Dispatcher::INVALID_REQUEST, $e->getMessage()); $this->runWithBarrier(function () use ($dispatcher, $app, $conn, $result) { $dispatcher->dispatch($app, $conn, $result); }); break; } } $result = $handler->write($data); if (!empty($result)) { $handler = null; if ($result instanceof File) { $files[] = $result; } else { $this->runWithBarrier(function () use ($dispatcher, $app, $conn, $result, $files) { $dispatcher->dispatch($app, $conn, $result, $files); }); $files = []; } } if (!empty($data)) { goto begin; } } $conn->close(); }); }); $server->start(); } protected function prepareRpcServer() { if ($this->getConfig('rpc.server.enable', false)) { $workerNum = $this->getConfig('rpc.server.worker_num', swoole_cpu_num()); $this->addBatchWorker($workerNum, [$this, 'createRpcServer'], 'rpc server'); } } protected function bindRpcDispatcher() { $services = $this->getConfig('rpc.server.services', []); $middleware = $this->getConfig('rpc.server.middleware', []); $this->app->make(Dispatcher::class, [$services, $middleware]); } protected function bindRpcParser() { $parserClass = $this->getConfig('rpc.server.parser', JsonParser::class); $this->app->bind(ParserInterface::class, $parserClass); $this->app->make(ParserInterface::class); } } InteractsWithSwooleTable.php<?php // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK IT ] // +---------------------------------------------------------------------- // | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: yunwuxin <448901948@qq.com> // +---------------------------------------------------------------------- namespace think\swoole\concerns; use Swoole\Table as SwooleTable; use think\App; use think\swoole\Table; /** * Trait InteractsWithSwooleTable * * @property App $container * @property App $app */ trait InteractsWithSwooleTable { /** * @var Table */ protected $currentTable; /** * Register customized swoole tables. */ protected function prepareTables() { $this->currentTable = new Table(); $this->registerTables(); $this->onEvent('workerStart', function () { $this->app->instance(Table::class, $this->currentTable); foreach ($this->currentTable->getAll() as $name => $table) { $this->app->instance("swoole.table.{$name}", $table); } }); } /** * Register user-defined swoole tables. */ protected function registerTables() { $tables = $this->container->make('config')->get('swoole.tables', []); foreach ($tables as $key => $value) { $table = new SwooleTable($value['size']); $columns = $value['columns'] ?? []; foreach ($columns as $column) { if (isset($column['size'])) { $table->column($column['name'], $column['type'], $column['size']); } else { $table->column($column['name'], $column['type']); } } $table->create(); $this->currentTable->add($key, $table); } } } InteractsWithWebsocket.php<?php namespace think\swoole\concerns; use Swoole\Atomic; use Swoole\Coroutine; use Swoole\Coroutine\Channel; use Swoole\Http\Request; use Swoole\Http\Response; use Swoole\WebSocket\CloseFrame; use Swoole\WebSocket\Frame; use think\App; use think\helper\Str; use think\swoole\contract\websocket\HandlerInterface; use think\swoole\contract\websocket\RoomInterface; use think\swoole\Middleware; use think\swoole\Websocket; use think\swoole\websocket\message\PushMessage; use think\swoole\websocket\Room; /** * Trait InteractsWithWebsocket * @package think\swoole\concerns * * @property App $app * @property App $container */ trait InteractsWithWebsocket { /** * @var RoomInterface */ protected $wsRoom; /** * @var Channel[] */ protected $wsMessageChannel = []; protected $wsEnable = false; /** @var Atomic */ protected $wsIdAtomic; /** * "onHandShake" listener. * * @param Request $req * @param Response $res */ public function onHandShake($req, $res) { $this->runInSandbox(function (App $app, Websocket $websocket, HandlerInterface $handler) use ($req, $res) { $res->upgrade(); $websocket->setClient($res); $request = $this->prepareRequest($req); $request = $this->setRequestThroughMiddleware($app, $request); $fd = $this->wsIdAtomic->add(); $this->wsMessageChannel[$fd] = new Channel(1); Coroutine::create(function () use ($res, $fd) { //推送消息 while ($message = $this->wsMessageChannel[$fd]->pop()) { $res->push($message); } }); try { $id = "{$this->workerId}.{$fd}"; $websocket->setSender($id); $websocket->join($id); $this->runWithBarrier(function () use ($request, $handler) { $handler->onOpen($request); }); $this->runWithBarrier(function () use ($handler, $res) { $cid = Coroutine::getCid(); $messages = 0; $wait = false; $frame = null; while (true) { /** @var Frame|false|string $recv */ $recv = $res->recv(); if ($recv === '' || $recv === false || $recv instanceof CloseFrame) { break; } if (empty($frame)) { $frame = new Frame(); $frame->opcode = $recv->opcode; $frame->flags = $recv->flags; $frame->fd = $recv->fd; $frame->finish = false; } $frame->data .= $recv->data; $frame->finish = $recv->finish; if ($frame->finish) { Coroutine::create(function () use (&$wait, &$messages, $cid, $frame, $handler) { ++$messages; Coroutine::defer(function () use (&$wait, &$messages, $cid) { --$messages; if ($wait) { Coroutine::resume($cid); } }); $handler->onMessage($frame); }); $frame = null; } } //等待消息执行完毕 while ($messages > 0) { $wait = true; Coroutine::yield(); } }); //关闭连接 $res->close(); $this->runWithBarrier(function () use ($handler) { $handler->onClose(); }); } finally { // leave all rooms $websocket->leave(); if (isset($this->wsMessageChannel[$fd])) { $this->wsMessageChannel[$fd]->close(); unset($this->wsMessageChannel[$fd]); } $websocket->setClient(null); } }); } /** * @param App $app * @param \think\Request $request * @return \think\Request */ protected function setRequestThroughMiddleware(App $app, \think\Request $request) { $app->instance('request', $request); return Middleware::make($app, $this->getConfig('websocket.middleware', [])) ->pipeline() ->send($request) ->then(function ($request) { return $request; }); } /** * Prepare settings if websocket is enabled. */ protected function prepareWebsocket() { $this->prepareWebsocketIdAtomic(); $this->prepareWebsocketRoom(); $this->onEvent('message', function ($message) { if ($message instanceof PushMessage) { if (isset($this->wsMessageChannel[$message->fd])) { $this->wsMessageChannel[$message->fd]->push($message->data); } } }); $this->onEvent('workerStart', function () { $this->bindWebsocketRoom(); $this->bindWebsocketHandler(); $this->prepareWebsocketListener(); }); } protected function prepareWebsocketIdAtomic() { $this->wsIdAtomic = new Atomic(); } /** * Prepare websocket room. */ protected function prepareWebsocketRoom() { $this->wsRoom = $this->container->make(Room::class); $this->wsRoom->prepare(); } protected function prepareWebsocketListener() { $listeners = $this->getConfig('websocket.listen', []); foreach ($listeners as $event => $listener) { $this->app->event->listen('swoole.websocket.' . Str::studly($event), $listener); } $subscribers = $this->getConfig('websocket.subscribe', []); foreach ($subscribers as $subscriber) { $this->app->event->observe($subscriber, 'swoole.websocket.'); } } /** * Prepare websocket handler for onOpen and onClose callback */ protected function bindWebsocketHandler() { $handlerClass = $this->getConfig('websocket.handler'); $this->app->bind(HandlerInterface::class, $handlerClass); } /** * Bind room instance to app container. */ protected function bindWebsocketRoom(): void { $this->app->instance(Room::class, $this->wsRoom); } } WithRpcClient.php<?php namespace think\swoole\concerns; use think\App; use think\console\Command; use think\console\Input; use think\console\Output; use think\helper\Arr; use think\swoole\rpc\client\Gateway; use think\swoole\rpc\client\Proxy; use think\swoole\rpc\JsonParser; use Throwable; use function Swoole\Coroutine\run; /** * 在命令行里使用RPC * @property App $app */ trait WithRpcClient { public function __construct() { if (!($this instanceof Command)) { throw new \RuntimeException('Trait `WithRpcClient` only can be used in Command'); } parent::__construct(); } protected function execute(Input $input, Output $output) { $this->bindRpcInterface(); run(function () { $this->app->invoke([$this, 'handle']); }); } protected function bindRpcInterface() { //引入rpc接口文件 if (file_exists($rpc = $this->app->getBasePath() . 'rpc.php')) { $rpcServices = (array) include $rpc; //绑定rpc接口 try { foreach ($rpcServices as $name => $abstracts) { $config = $this->app->config->get("swoole.rpc.client.{$name}", []); $parserClass = Arr::pull($config, 'parser', JsonParser::class); $tries = Arr::pull($config, 'tries', 2); $middleware = Arr::pull($config, 'middleware', []); $parser = $this->app->make($parserClass); $gateway = new Gateway($config, $parser, $tries); foreach ($abstracts as $abstract) { $this->app->bind($abstract, function (App $app) use ($middleware, $gateway, $name, $abstract) { return $app->invokeClass(Proxy::getClassName($name, $abstract), [$gateway, $middleware]); }); } } } catch (Throwable $e) { } } } } ModifyProperty.php<?php namespace think\swoole\concerns; use ReflectionObject; trait ModifyProperty { protected function modifyProperty($object, $value, $property = 'app') { $reflectObject = new ReflectionObject($object); if ($reflectObject->hasProperty($property)) { $reflectProperty = $reflectObject->getProperty($property); $reflectProperty->setAccessible(true); $reflectProperty->setValue($object, $value); } } } InteractsWithServer.php<?php namespace think\swoole\concerns; use Swoole\Constant; use Swoole\Coroutine; use Swoole\Event; use Swoole\Process; use Swoole\Process\Pool; use Swoole\Runtime; use think\App; use think\swoole\Watcher; /** * Trait InteractsWithServer * @package think\swoole\concerns * @property App $container */ trait InteractsWithServer { /** * @var array */ protected $startFuncMap = []; protected $workerId; /** @var Pool */ protected $pool; public function addBatchWorker(int $workerNum, callable $func, $name = null) { for ($i = 0; $i < $workerNum; $i++) { $this->addWorker($func, $name ? "{$name} #{$i}" : null); } return $this; } public function addWorker(callable $func, $name = null): self { $this->startFuncMap[] = [$func, $name]; return $this; } /** * 启动服务 * @param string $envName 环境变量标识 */ public function start(string $envName): void { $this->setProcessName('manager process'); $this->initialize(); $this->triggerEvent('init'); //热更新 if ($this->getConfig('hot_update.enable', false)) { $this->addHotUpdateProcess(); } $pool = new Pool(count($this->startFuncMap), SWOOLE_IPC_UNIXSOCK, 0, true); $pool->on(Constant::EVENT_WORKER_START, function ($pool, $workerId) use ($envName) { Runtime::enableCoroutine(); $this->pool = $pool; $this->workerId = $workerId; [$func, $name] = $this->startFuncMap[$workerId]; if ($name) { $this->setProcessName($name); } Process::signal(SIGTERM, function () { $this->pool->getProcess()->exit(); }); /** @var Coroutine\Socket $socket */ $socket = $this->pool->getProcess()->exportSocket(); //启动消息监听 Event::add($socket, function (Coroutine\Socket $socket) { $recv = $socket->recv(); $message = unserialize($recv); $this->triggerEvent('message', $message); }); $this->clearCache(); $this->prepareApplication($envName); $this->triggerEvent(Constant::EVENT_WORKER_START); $func($pool, $workerId); }); $pool->start(); } public function getWorkerId() { return $this->workerId; } /** * 获取当前工作进程池对象 * @return Pool */ public function getPool() { return $this->pool; } public function sendMessage($workerId, $message) { if ($workerId === $this->workerId) { $this->triggerEvent('message', $message); } else { /** @var Process $process */ $process = $this->pool->getProcess($workerId); $socket = $process->exportSocket(); $socket->send(serialize($message)); } } public function runWithBarrier(callable $func, ...$params) { $channel = new Coroutine\Channel(1); Coroutine::create(function (...$params) use ($channel, $func) { Coroutine::defer(function () use ($channel) { $channel->close(); }); call_user_func_array($func, $params); }, ...$params); $channel->pop(); } /** * 热更新 */ protected function addHotUpdateProcess() { $this->addWorker(function (Process\Pool $pool) { $watcher = $this->container->make(Watcher::class); $watcher->watch(function () use ($pool) { Process::kill($pool->master_pid, SIGUSR1); }); }, 'hot update'); } /** * 清除apc、op缓存 */ protected function clearCache() { if (extension_loaded('apc')) { apc_clear_cache(); } if (extension_loaded('Zend OPcache')) { opcache_reset(); } } /** * Set process name. * * @param $process */ protected function setProcessName($process) { $appName = $this->container->config->get('app.name', 'ThinkPHP'); $name = sprintf('swoole: %s process for %s', $process, $appName); @cli_set_process_title($name); } } WithMiddleware.php<?php namespace think\swoole\concerns; trait WithMiddleware { /** * 中间件 * @var array */ protected $middleware = []; protected function middleware($middleware, ...$params) { $options = []; $this->middleware[] = [ 'middleware' => [$middleware, $params], 'options' => &$options, ]; return new class($options) { protected $options; public function __construct(array &$options) { $this->options = &$options; } public function only($methods) { $this->options['only'] = is_array($methods) ? $methods : func_get_args(); return $this; } public function except($methods) { $this->options['except'] = is_array($methods) ? $methods : func_get_args(); return $this; } }; } } InteractsWithHttp.php<?php namespace think\swoole\concerns; use Swoole\Coroutine\Http\Server; use Swoole\Http\Request; use Swoole\Http\Response; use Swoole\Http\Status; use think\App; use think\Cookie; use think\Event; use think\exception\Handle; use think\helper\Arr; use think\helper\Str; use think\Http; use think\swoole\App as SwooleApp; use think\swoole\Http as SwooleHttp; use think\swoole\response\File as FileResponse; use Throwable; use function substr; /** * Trait InteractsWithHttp * @package think\swoole\concerns * @property App $app * @property App $container */ trait InteractsWithHttp { use InteractsWithWebsocket, ModifyProperty; public function createHttpServer() { $this->preloadHttp(); $host = $this->getConfig('http.host'); $port = $this->getConfig('http.port'); $options = $this->getConfig('http.options', []); $server = new Server($host, $port, false, true); $server->set($options); $server->handle('/', function (Request $req, Response $res) { if ($this->wsEnable && $this->isWebsocketRequest($req)) { $this->onHandShake($req, $res); } else { $this->onRequest($req, $res); } }); $server->start(); } protected function preloadHttp() { $http = $this->app->http; $this->app->invokeMethod([$http, 'loadMiddleware'], [], true); if ($this->app->config->get('app.with_route', true)) { $this->app->invokeMethod([$http, 'loadRoutes'], [], true); $route = clone $this->app->route; $this->modifyProperty($route, null); unset($this->app->route); $this->app->resolving(SwooleHttp::class, function ($http, App $app) use ($route) { $newRoute = clone $route; $this->modifyProperty($newRoute, $app); $app->instance('route', $newRoute); }); } $middleware = clone $this->app->middleware; $this->modifyProperty($middleware, null); unset($this->app->middleware); $this->app->resolving(SwooleHttp::class, function ($http, App $app) use ($middleware) { $newMiddleware = clone $middleware; $this->modifyProperty($newMiddleware, $app); $app->instance('middleware', $newMiddleware); }); unset($this->app->http); $this->app->bind(Http::class, SwooleHttp::class); } protected function isWebsocketRequest(Request $req) { $header = $req->header; return strcasecmp(Arr::get($header, 'connection', ''), 'upgrade') === 0 && strcasecmp(Arr::get($header, 'upgrade', ''), 'websocket') === 0; } protected function prepareHttp() { if ($this->getConfig('http.enable', true)) { $this->wsEnable = $this->getConfig('websocket.enable', false); if ($this->wsEnable) { $this->prepareWebsocket(); } $workerNum = $this->getConfig('http.worker_num', swoole_cpu_num()); $this->addBatchWorker($workerNum, [$this, 'createHttpServer'], 'http server'); } } /** * "onRequest" listener. * * @param Request $req * @param Response $res */ public function onRequest($req, $res) { $this->runWithBarrier([$this, 'runInSandbox'], function (Http $http, Event $event, SwooleApp $app) use ($req, $res) { $app->setInConsole(false); $request = $this->prepareRequest($req); try { $response = $this->handleRequest($http, $request); } catch (Throwable $e) { $response = $this->app ->make(Handle::class) ->render($request, $e); } $this->setCookie($res, $app->cookie); $this->sendResponse($res, $request, $response); }); } protected function handleRequest(Http $http, $request) { $level = ob_get_level(); ob_start(); $response = $http->run($request); $content = $response->getContent(); if (ob_get_level() == 0) { ob_start(); } $http->end($response); if (ob_get_length() > 0) { $response->content(ob_get_contents() . $content); } while (ob_get_level() > $level) { ob_end_clean(); } return $response; } protected function prepareRequest(Request $req) { $header = $req->header ?: []; $server = $req->server ?: []; foreach ($header as $key => $value) { $server['http_' . str_replace('-', '_', $key)] = $value; } // 重新实例化请求对象 处理swoole请求数据 /** @var \think\Request $request */ $request = $this->app->make('request', [], true); return $request ->withHeader($header) ->withServer($server) ->withGet($req->get ?: []) ->withPost($req->post ?: []) ->withCookie($req->cookie ?: []) ->withFiles($this->getFiles($req)) ->withInput($req->rawContent()) ->setBaseUrl($req->server['request_uri']) ->setUrl($req->server['request_uri'] . (!empty($req->server['query_string']) ? '?' . $req->server['query_string'] : '')) ->setPathinfo(ltrim($req->server['path_info'], '/')); } protected function getFiles(Request $req) { if (empty($req->files)) { return []; } return array_map(function ($file) { if (!Arr::isAssoc($file)) { $files = []; foreach ($file as $f) { $files['name'][] = $f['name']; $files['type'][] = $f['type']; $files['tmp_name'][] = $f['tmp_name']; $files['error'][] = $f['error']; $files['size'][] = $f['size']; } return $files; } return $file; }, $req->files); } protected function setCookie(Response $res, Cookie $cookie) { foreach ($cookie->getCookie() as $name => $val) { [$value, $expire, $option] = $val; $res->cookie($name, $value, $expire, $option['path'], $option['domain'], (bool) $option['secure'], (bool) $option['httponly'], $option['samesite']); } } protected function setHeader(Response $res, array $headers) { foreach ($headers as $key => $val) { $res->header($key, $val); } } protected function setStatus(Response $res, $code) { $res->status($code, Status::getReasonPhrase($code)); } protected function sendResponse(Response $res, \think\Request $request, \think\Response $response) { switch (true) { case $response instanceof FileResponse: $this->sendFile($res, $request, $response); break; default: $this->sendContent($res, $response); } } protected function sendFile(Response $res, \think\Request $request, FileResponse $response) { $ifNoneMatch = $request->header('If-None-Match'); $ifRange = $request->header('If-Range'); $code = $response->getCode(); $file = $response->getFile(); $eTag = $response->getHeader('ETag'); $lastModified = $response->getHeader('Last-Modified'); $fileSize = $file->getSize(); $offset = 0; $length = -1; if ($ifNoneMatch == $eTag) { $code = 304; } elseif (!$ifRange || $ifRange === $eTag || $ifRange === $lastModified) { $range = $request->header('Range', ''); if (Str::startsWith($range, 'bytes=')) { [$start, $end] = explode('-', substr($range, 6), 2) + [0]; $end = ('' === $end) ? $fileSize - 1 : (int) $end; if ('' === $start) { $start = $fileSize - $end; $end = $fileSize - 1; } else { $start = (int) $start; } if ($start <= $end) { $end = min($end, $fileSize - 1); if ($start < 0 || $start > $end) { $code = 416; $response->header([ 'Content-Range' => sprintf('bytes */%s', $fileSize), ]); } elseif ($end - $start < $fileSize - 1) { $length = $end < $fileSize ? $end - $start + 1 : -1; $offset = $start; $code = 206; $response->header([ 'Content-Range' => sprintf('bytes %s-%s/%s', $start, $end, $fileSize), 'Content-Length' => $end - $start + 1, ]); } } } } $this->setStatus($res, $code); $this->setHeader($res, $response->getHeader()); if ($code >= 200 && $code < 300 && $length !== 0) { $res->sendfile($file->getPathname(), $offset, $length); } else { $res->end(); } } protected function sendContent(Response $res, \think\Response $response) { // 由于开启了 Transfer-Encoding: chunked，根据 HTTP 规范，不再需要设置 Content-Length $response->header(['Content-Length' => null]); $this->setStatus($res, $response->getCode()); $this->setHeader($res, $response->getHeader()); $content = $response->getContent(); if ($content) { $contentSize = strlen($content); $chunkSize = 8192; if ($contentSize > $chunkSize) { $sendSize = 0; do { if (!$res->write(substr($content, $sendSize, $chunkSize))) { break; } } while (($sendSize += $chunkSize) < $contentSize); } else { $res->write($content); } } $res->end(); } } InteractsWithQueue.php<?php namespace think\swoole\concerns; use Swoole\Process; use Swoole\Timer; use think\helper\Arr; use think\queue\event\JobFailed; use think\queue\Worker; trait InteractsWithQueue { protected function createQueueWorkers() { $workers = $this->getConfig('queue.workers', []); foreach ($workers as $queue => $options) { if (strpos($queue, '@') !== false) { [$queue, $connection] = explode('@', $queue); } else { $connection = null; } $workerNum = Arr::get($options, 'worker_num', 1); $this->addBatchWorker($workerNum, function (Process\Pool $pool) use ($options, $connection, $queue) { $delay = Arr::get($options, 'delay', 0); $sleep = Arr::get($options, 'sleep', 3); $tries = Arr::get($options, 'tries', 0); $timeout = Arr::get($options, 'timeout', 60); /** @var Worker $worker */ $worker = $this->app->make(Worker::class); while (true) { $timer = Timer::after($timeout * 1000, function () use ($pool) { $pool->getProcess()->exit(); }); $this->runWithBarrier([$this, 'runInSandbox'], function () use ($connection, $queue, $delay, $sleep, $tries, $worker) { $worker->runNextJob($connection, $queue, $delay, $sleep, $tries); }); Timer::clear($timer); } }, "queue [$queue]"); } } public function prepareQueue() { if ($this->getConfig('queue.enable', false)) { $this->listenForEvents(); $this->createQueueWorkers(); } } /** * 注册事件 */ protected function listenForEvents() { $this->container->event->listen(JobFailed::class, function (JobFailed $event) { $this->logFailedJob($event); }); } /** * 记录失败任务 * @param JobFailed $event */ protected function logFailedJob(JobFailed $event) { $this->container['queue.failer']->log( $event->connection, $event->job->getQueue(), $event->job->getRawBody(), $event->exception ); } } InteractsWithRpcConnector.php<?php namespace think\swoole\concerns; use Generator; use Swoole\Coroutine\Client; use think\swoole\exception\RpcClientException; use think\swoole\rpc\File; use think\swoole\rpc\Packer; use think\swoole\rpc\Protocol; trait InteractsWithRpcConnector { abstract protected function runWithClient($callback); protected function recv(Client $client, callable $decoder) { $handler = null; $file = null; while ($data = $client->recv()) { begin: if (empty($handler)) { [$handler, $data] = Packer::unpack($data); } $response = $handler->write($data); if (!empty($response)) { $handler = null; if ($response instanceof File) { $file = $response; } else { $result = $decoder($response); if ($result === Protocol::FILE) { $result = $file; } return $result; } } if (!empty($data)) { goto begin; } } if ($data === '') { throw new RpcClientException('Connection is closed. ' . $client->errMsg, $client->errCode); } if ($data === false) { throw new RpcClientException('Error receiving data, errno=' . $client->errCode . ' errmsg=' . swoole_strerror($client->errCode), $client->errCode); } } public function sendAndRecv($data, callable $decoder) { if (!$data instanceof Generator) { $data = [$data]; } return $this->runWithClient(function (Client $client) use ($decoder, $data) { try { foreach ($data as $string) { if (!empty($string)) { if ($client->send($string) === false) { throw new RpcClientException('Send data failed. ' . $client->errMsg, $client->errCode); } } } return $this->recv($client, $decoder); } catch (RpcClientException $e) { $client->close(); throw $e; } }); } } InteractsWithPools.php<?php namespace think\swoole\concerns; use Smf\ConnectionPool\ConnectionPool; use Smf\ConnectionPool\Connectors\ConnectorInterface; use think\App; use think\helper\Arr; use think\swoole\Pool; /** * Trait InteractsWithRpc * @package think\swoole\concerns * @property App $app */ trait InteractsWithPools { /** * @return Pool */ public function getPools() { return $this->app->make(Pool::class); } protected function preparePools() { $createPools = function () { /** @var Pool $pool */ $pools = $this->getPools(); foreach ($this->getConfig('pool', []) as $name => $config) { $type = Arr::pull($config, 'type'); if ($type && is_subclass_of($type, ConnectorInterface::class)) { $pool = new ConnectionPool( Pool::pullPoolConfig($config), $this->app->make($type), $config ); $pools->add($name, $pool); //注入到app $this->app->instance("swoole.pool.{$name}", $pool); } } }; $this->onEvent('workerStart', $createPools); } } InteractsWithRpcClient.php<?php namespace think\swoole\concerns; use Smf\ConnectionPool\ConnectionPool; use think\App; use think\swoole\Pool; use think\swoole\pool\Client; use think\swoole\rpc\client\Connector; use think\swoole\rpc\client\Gateway; use think\swoole\rpc\client\Proxy; use think\swoole\rpc\JsonParser; use Throwable; /** * Trait InteractsWithRpcClient * @package think\swoole\concerns * @property App $app * @property App $container * @method Pool getPools() */ trait InteractsWithRpcClient { protected function prepareRpcClient() { $this->onEvent('workerStart', function () { $this->bindRpcClientPool(); $this->bindRpcInterface(); }); } protected function bindRpcInterface() { //引入rpc接口文件 if (file_exists($rpc = $this->container->getBasePath() . 'rpc.php')) { $rpcServices = (array) include $rpc; //绑定rpc接口 try { foreach ($rpcServices as $name => $abstracts) { $parserClass = $this->getConfig("rpc.client.{$name}.parser", JsonParser::class); $tries = $this->getConfig("rpc.client.{$name}.tries", 2); $middleware = $this->getConfig("rpc.client.{$name}.middleware", []); $parser = $this->getApplication()->make($parserClass); $gateway = new Gateway($this->createRpcConnector($name), $parser, $tries); foreach ($abstracts as $abstract) { $this->getApplication() ->bind($abstract, function (App $app) use ($middleware, $gateway, $name, $abstract) { return $app->invokeClass(Proxy::getClassName($name, $abstract), [$gateway, $middleware]); }); } } } catch (Throwable $e) { } } } protected function bindRpcClientPool() { if (!empty($clients = $this->getConfig('rpc.client'))) { //创建client连接池 foreach ($clients as $name => $config) { $pool = new ConnectionPool( Pool::pullPoolConfig($config), new Client(), $config ); $this->getPools()->add("rpc.client.{$name}", $pool); } } } protected function createRpcConnector($name) { $pool = $this->getPools()->get("rpc.client.{$name}"); return new class($pool) implements Connector { use InteractsWithRpcConnector; protected $pool; public function __construct(ConnectionPool $pool) { $this->pool = $pool; } protected function runWithClient($callback) { /** @var \Swoole\Coroutine\Client $client */ $client = $this->pool->borrow(); try { return $callback($client); } finally { $this->pool->return($client); } } }; } } WithContainer.php<?php namespace think\swoole\concerns; use think\App; use think\console\Output; use think\exception\Handle; use Throwable; trait WithContainer { /** * @var App */ protected $container; /** * Manager constructor. * @param App $container */ public function __construct(App $container) { $this->container = $container; } protected function getContainer() { return $this->container; } /** * 获取配置 * @param string $name * @param null $default * @return mixed */ public function getConfig(string $name, $default = null) { return $this->container->config->get("swoole.{$name}", $default); } /** * 触发事件 * @param string $event * @param null $params */ public function triggerEvent(string $event, $params = null): void { $this->container->event->trigger("swoole.{$event}", $params); } /** * 监听事件 * @param string $event * @param $listener * @param bool $first */ public function onEvent(string $event, $listener, bool $first = false): void { $this->container->event->listen("swoole.{$event}", $listener, $first); } /** * Log server error. * * @param Throwable $e */ public function logServerError(Throwable $e) { /** @var Handle $handle */ $handle = $this->container->make(Handle::class); $handle->renderForConsole(new Output(), $e); $handle->report($e); } } InteractsWithTracing.php<?php namespace think\swoole\concerns; use Smf\ConnectionPool\ConnectionPool; use Smf\ConnectionPool\Connectors\PhpRedisConnector; use Swoole\Coroutine; use think\helper\Arr; use think\swoole\coroutine\Context; use think\swoole\Pool; use think\tracing\reporter\RedisReporter; use think\tracing\Tracer; /** * 链路追踪上报进程 */ trait InteractsWithTracing { protected function prepareTracing() { if (class_exists(Tracer::class)) { $tracers = $this->container->config->get('tracing.tracers'); $hasAsync = false; foreach ($tracers as $name => $tracer) { if (Arr::get($tracer, 'async', false)) { $this->addWorker(function () use ($name) { $tracer = $this->app->make(Tracer::class)->tracer($name); $tracer->report(); }, "tracing [{$name}]"); $hasAsync = true; } } if ($hasAsync) { $this->onEvent('workerStart', function () { $this->bindTracingRedisPool(); $this->bindTracingRedisReporter(); }); } } } protected function bindTracingRedisReporter() { $this->getApplication()->bind(RedisReporter::class, function ($name) { $pool = $this->getPools()->get("tracing.redis"); $redis = Context::rememberData('tracing.redis', function () use ($pool) { $redis = $pool->borrow(); Coroutine::defer(function () use ($pool, $redis) { $pool->return($redis); }); return $redis; }); return new RedisReporter($name, $redis); }); } protected function bindTracingRedisPool() { $config = $this->container->config->get('tracing.redis'); $pool = new ConnectionPool( Pool::pullPoolConfig($config), new PhpRedisConnector(), $config ); $this->getPools()->add("tracing.redis", $pool); } } configswoole.php<?php use think\swoole\websocket\socketio\Handler; return [ 'http' => [ 'enable' => true, 'host' => '0.0.0.0', 'port' => 80, 'worker_num' => swoole_cpu_num(), 'options' => [], ], 'websocket' => [ 'enable' => false, 'handler' => Handler::class, 'ping_interval' => 25000, 'ping_timeout' => 60000, 'room' => [ 'type' => 'table', 'table' => [ 'room_rows' => 8192, 'room_size' => 2048, 'client_rows' => 4096, 'client_size' => 2048, ], 'redis' => [ 'host' => '127.0.0.1', 'port' => 6379, 'max_active' => 3, 'max_wait_time' => 5, ], ], 'listen' => [], 'subscribe' => [], ], 'rpc' => [ 'server' => [ 'enable' => false, 'host' => '0.0.0.0', 'port' => 9000, 'worker_num' => swoole_cpu_num(), 'services' => [], ], 'client' => [], ], //队列 'queue' => [ 'enable' => false, 'workers' => [], ], 'hot_update' => [ 'enable' => env('APP_DEBUG', false), 'name' => ['*.php'], 'include' => [app_path()], 'exclude' => [], ], //连接池 'pool' => [ 'db' => [ 'enable' => true, 'max_active' => 3, 'max_wait_time' => 5, ], 'cache' => [ 'enable' => true, 'max_active' => 3, 'max_wait_time' => 5, ], //自定义连接池 ], 'tables' => [], //每个worker里需要预加载以共用的实例 'concretes' => [], //重置器 'resetters' => [], //每次请求前需要清空的实例 'instances' => [], //每次请求前需要重新执行的服务 'services' => [], ]; contractwebsocketHandlerInterface.php<?php namespace think\swoole\contract\websocket; use Swoole\WebSocket\Frame; use think\Request; interface HandlerInterface { /** * "onOpen" listener. * * @param Request $request */ public function onOpen(Request $request); /** * "onMessage" listener. * * @param Frame $frame */ public function onMessage(Frame $frame); /** * "onClose" listener. */ public function onClose(); public function encodeMessage($message); } RoomInterface.php<?php namespace think\swoole\contract\websocket; interface RoomInterface { /** * Rooms key * * @const string */ public const ROOMS_KEY = 'rooms'; /** * Descriptors key * * @const string */ public const DESCRIPTORS_KEY = 'fds'; /** * Do some init stuffs before workers started. * * @return RoomInterface */ public function prepare(): RoomInterface; /** * Add multiple socket fds to a room. * * @param string fd * @param array|string $roomNames */ public function add($fd, $roomNames); /** * Delete multiple socket fds from a room. * * @param string fd * @param array|string $roomNames */ public function delete($fd, $roomNames); /** * Get all sockets by a room key. * * @param string room * * @return array */ public function getClients(string $room); /** * Get all rooms by a fd. * * @param string fd * * @return array */ public function getRooms($fd); } ResetterInterface.php<?php namespace think\swoole\contract; use think\App; use think\swoole\Sandbox; interface ResetterInterface { /** * "handle" function for resetting app. * * @param \think\App $app * @param Sandbox $sandbox */ public function handle(App $app, Sandbox $sandbox); } rpcParserInterface.php<?php namespace think\swoole\contract\rpc; use think\swoole\rpc\Protocol; interface ParserInterface { const EOF = "\r\n\r\n"; /** * @param Protocol $protocol * * @return string */ public function encode(Protocol $protocol): string; /** * @param string $string * * @return Protocol */ public function decode(string $string): Protocol; /** * @param string $string * * @return mixed */ public function decodeResponse(string $string); /** * @param mixed $result * * @return string */ public function encodeResponse($result): string; } coroutineContext.php<?php namespace think\swoole\coroutine; use ArrayObject; use Closure; use Swoole\Coroutine; class Context { /** * 获取协程上下文 * @param int $cid * @return Coroutine\Context */ public static function get($cid = 0) { return Coroutine::getContext($cid); } public static function getDataObject() { $context = self::get(); if (!isset($context['#data'])) { $context['#data'] = new ArrayObject(); } return $context['#data']; } /** * 获取当前协程临时数据 * @param string $key * @param null $default * @return mixed|null */ public static function getData(string $key, $default = null) { if (self::hasData($key)) { return self::getDataObject()->offsetGet($key); } return $default; } /** * 判断是否存在临时数据 * @param string $key * @return bool */ public static function hasData(string $key) { return self::getDataObject()->offsetExists($key); } /** * 写入临时数据 * @param string $key * @param $value */ public static function setData(string $key, $value) { self::getDataObject()->offsetSet($key, $value); } /** * 删除数据 * @param string $key */ public static function removeData(string $key) { if (self::hasData($key)) { self::getDataObject()->offsetUnset($key); } } /** * 如果不存在则写入数据 * @param string $key * @param $value * @return mixed|null */ public static function rememberData(string $key, $value) { if (self::hasData($key)) { return self::getData($key); } if ($value instanceof Closure) { // 获取缓存数据 $value = $value(); } self::setData($key, $value); return $value; } /** * @internal * 清空数据 */ public static function clear() { self::getDataObject()->exchangeArray([]); } /** * 获取当前协程ID * @return mixed */ public static function getCoroutineId() { return Coroutine::getuid(); } } exceptionRpcResponseException.php<?php namespace think\swoole\exception; use Exception; use think\swoole\rpc\Error; class RpcResponseException extends Exception { protected $error; public function __construct(Error $error) { parent::__construct($error->getMessage(), $error->getCode()); $this->error = $error; } public function getError() { return $this->error; } } RpcClientException.php<?php namespace think\swoole\exception; use Exception; class RpcClientException extends Exception { } middlewareTraceRpcServer.php<?php namespace think\swoole\middleware; use think\swoole\rpc\Protocol; use think\tracing\Tracer; use Throwable; use const OpenTracing\Formats\TEXT_MAP; use const OpenTracing\Tags\ERROR; use const OpenTracing\Tags\SPAN_KIND; use const OpenTracing\Tags\SPAN_KIND_RPC_SERVER; class TraceRpcServer { protected $tracer; public function __construct(Tracer $tracer) { $this->tracer = $tracer; } public function handle(Protocol $protocol, $next) { $context = $this->tracer->extract(TEXT_MAP, $protocol->getContext()); $scope = $this->tracer->startActiveSpan( 'rpc.server:' . $protocol->getInterface() . '@' . $protocol->getMethod(), [ 'child_of' => $context, 'tags' => [ SPAN_KIND => SPAN_KIND_RPC_SERVER, ], ] ); $span = $scope->getSpan(); try { return $next($protocol); } catch (Throwable $e) { $span->setTag(ERROR, $e); throw $e; } finally { $scope->close(); $this->tracer->flush(); } } } TraceRpcClient.php<?php namespace think\swoole\middleware; use think\swoole\exception\RpcResponseException; use think\swoole\rpc\Protocol; use think\tracing\Tracer; use Throwable; use const OpenTracing\Formats\TEXT_MAP; use const OpenTracing\Tags\ERROR; use const OpenTracing\Tags\SPAN_KIND; use const OpenTracing\Tags\SPAN_KIND_RPC_CLIENT; class TraceRpcClient { protected $tracer; public function __construct(Tracer $tracer) { $this->tracer = $tracer; } public function handle(Protocol $protocol, $next) { $scope = $this->tracer->startActiveSpan( 'rpc.client:' . $protocol->getInterface() . '@' . $protocol->getMethod(), [ 'tags' => [ SPAN_KIND => SPAN_KIND_RPC_CLIENT, ], ] ); $span = $scope->getSpan(); $context = $protocol->getContext(); $this->tracer->inject($span->getContext(), TEXT_MAP, $context); $protocol->setContext($context); try { return $next($protocol); } catch (Throwable $e) { if (!$e instanceof RpcResponseException) { $span->setTag(ERROR, $e); } throw $e; } finally { $scope->close(); } } } InteractsWithVarDumper.php<?php namespace think\swoole\middleware; use Closure; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Symfony\Component\VarDumper\VarDumper; use think\Request; use think\Response; class InteractsWithVarDumper { /** * @param Request $request * @param Closure $next * @return Response */ public function handle($request, Closure $next) { if (class_exists(VarDumper::class)) { $cloner = new VarCloner(); $dumper = new HtmlDumper(); $prevHandler = VarDumper::setHandler(function ($var) use ($dumper, $cloner) { $dumper->dump($cloner->cloneVar($var)); }); /** @var Response $response */ $response = $next($request); VarDumper::setHandler($prevHandler); return $response; } return $next($request); } } poolproxyConnection.php<?php namespace think\swoole\pool\proxy; use Psr\SimpleCache\CacheInterface; use think\db\BaseQuery; use think\db\ConnectionInterface; use think\DbManager; use think\swoole\pool\Proxy; /** * Class Connection * @package think\swoole\pool\db * @property ConnectionInterface $handler */ class Connection extends Proxy implements ConnectionInterface { /** * 获取当前连接器类对应的Query类 * @access public * @return string */ public function getQueryClass(): string { return $this->__call(__FUNCTION__, func_get_args()); } /** * 连接数据库方法 * @access public * @param array $config 接参数 * @param integer $linkNum 连接序号 * @return mixed */ public function connect(array $config = [], $linkNum = 0) { return $this->__call(__FUNCTION__, func_get_args()); } /** * 设置当前的数据库Db对象 * @access public * @param DbManager $db * @return void */ public function setDb(DbManager $db) { return $this->__call(__FUNCTION__, func_get_args()); } /** * 设置当前的缓存对象 * @access public * @param CacheInterface $cache * @return void */ public function setCache(CacheInterface $cache) { return $this->__call(__FUNCTION__, func_get_args()); } /** * 获取数据库的配置参数 * @access public * @param string $config 配置名称 * @return mixed */ public function getConfig(string $config = '') { return $this->__call(__FUNCTION__, func_get_args()); } /** * 关闭数据库（或者重新连接） * @access public */ public function close() { return $this->__call(__FUNCTION__, func_get_args()); } /** * 查找单条记录 * @access public * @param BaseQuery $query 查询对象 * @return array */ public function find(BaseQuery $query): array { return $this->__call(__FUNCTION__, func_get_args()); } /** * 查找记录 * @access public * @param BaseQuery $query 查询对象 * @return array */ public function select(BaseQuery $query): array { return $this->__call(__FUNCTION__, func_get_args()); } /** * 插入记录 * @access public * @param BaseQuery $query 查询对象 * @param boolean $getLastInsID 返回自增主键 * @return mixed */ public function insert(BaseQuery $query, bool $getLastInsID = false) { return $this->__call(__FUNCTION__, func_get_args()); } /** * 批量插入记录 * @access public * @param BaseQuery $query 查询对象 * @param mixed $dataSet 数据集 * @return integer * @throws \Exception * @throws \Throwable */ public function insertAll(BaseQuery $query, array $dataSet = []): int { return $this->__call(__FUNCTION__, func_get_args()); } /** * 更新记录 * @access public * @param BaseQuery $query 查询对象 * @return integer */ public function update(BaseQuery $query): int { return $this->__call(__FUNCTION__, func_get_args()); } /** * 删除记录 * @access public * @param BaseQuery $query 查询对象 * @return int */ public function delete(BaseQuery $query): int { return $this->__call(__FUNCTION__, func_get_args()); } /** * 得到某个字段的值 * @access public * @param BaseQuery $query 查询对象 * @param string $field 字段名 * @param mixed $default 默认值 * @return mixed */ public function value(BaseQuery $query, string $field, $default = null) { return $this->__call(__FUNCTION__, func_get_args()); } /** * 得到某个列的数组 * @access public * @param BaseQuery $query 查询对象 * @param string|array $column 字段名 多个字段用逗号分隔 * @param string $key 索引 * @return array */ public function column(BaseQuery $query, $column, string $key = ''): array { return $this->__call(__FUNCTION__, func_get_args()); } /** * 执行数据库事务 * @access public * @param callable $callback 数据操作方法回调 * @return mixed * @throws \Throwable */ public function transaction(callable $callback) { return $this->__call(__FUNCTION__, func_get_args()); } /** * 启动事务 * @access public * @return void * @throws \Exception */ public function startTrans() { return $this->__call(__FUNCTION__, func_get_args()); } /** * 用于非自动提交状态下面的查询提交 * @access public * @return void */ public function commit() { return $this->__call(__FUNCTION__, func_get_args()); } /** * 事务回滚 * @access public * @return void */ public function rollback() { return $this->__call(__FUNCTION__, func_get_args()); } /** * 获取最近一次查询的sql语句 * @access public * @return string */ public function getLastSql(): string { return $this->__call(__FUNCTION__, func_get_args()); } public function table($table) { return $this->__call(__FUNCTION__, func_get_args()); } public function name($name) { return $this->__call(__FUNCTION__, func_get_args()); } } Store.php<?php namespace think\swoole\pool\proxy; use Psr\SimpleCache\CacheInterface; use think\contract\CacheHandlerInterface; use think\swoole\pool\Proxy; class Store extends Proxy implements CacheHandlerInterface, CacheInterface { /** * @inheritDoc */ public function has($name) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @inheritDoc */ public function get($name, $default = null) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @inheritDoc */ public function set($name, $value, $expire = null) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @inheritDoc */ public function inc(string $name, int $step = 1) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @inheritDoc */ public function dec(string $name, int $step = 1) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @inheritDoc */ public function delete($name) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @inheritDoc */ public function clear() { return $this->__call(__FUNCTION__, func_get_args()); } /** * @inheritDoc */ public function clearTag(array $keys) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @inheritDoc */ public function getMultiple($keys, $default = null) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @inheritDoc */ public function setMultiple($values, $ttl = null) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @inheritDoc */ public function deleteMultiple($keys) { return $this->__call(__FUNCTION__, func_get_args()); } /** * @inheritDoc */ public function tag($name) { return $this->__call(__FUNCTION__, func_get_args()); } } Cache.php<?php namespace think\swoole\pool; use think\swoole\pool\proxy\Store; class Cache extends \think\Cache { protected function createDriver(string $name) { return new Store(function () use ($name) { return parent::createDriver($name); }, $this->app->config->get('swoole.pool.cache', [])); } } Connector.php<?php namespace think\swoole\pool; use Smf\ConnectionPool\Connectors\ConnectorInterface; class Connector implements ConnectorInterface { protected $connector; public function __construct($connector) { $this->connector = $connector; } public function connect(array $config) { return call_user_func($this->connector, $config); } public function disconnect($connection) { } public function isConnected($connection): bool { return !property_exists($connection, Proxy::KEY_DISCONNECTED); } public function reset($connection, array $config) { } public function validate($connection): bool { return true; } } Db.php<?php namespace think\swoole\pool; use think\Config; use think\db\ConnectionInterface; use think\swoole\pool\proxy\Connection; /** * Class Db * @package think\swoole\pool * @property Config $config */ class Db extends \think\Db { protected function createConnection(string $name): ConnectionInterface { return new Connection(new class(function () use ($name) { return parent::createConnection($name); }) extends Connector { public function disconnect($connection) { if ($connection instanceof ConnectionInterface) { $connection->close(); } } }, $this->config->get('swoole.pool.db', [])); } } Proxy.php<?php namespace think\swoole\pool; use Closure; use Exception; use RuntimeException; use Smf\ConnectionPool\ConnectionPool; use Smf\ConnectionPool\Connectors\ConnectorInterface; use Swoole\Coroutine; use think\swoole\coroutine\Context; use think\swoole\Pool; use Throwable; abstract class Proxy { const KEY_RELEASED = '__released'; const KEY_DISCONNECTED = '__disconnected'; protected $pool; /** * Proxy constructor. * @param Closure|ConnectorInterface $connector * @param array $config */ public function __construct($connector, $config, array $connectionConfig = []) { if ($connector instanceof Closure) { $connector = new Connector($connector); } $this->pool = new ConnectionPool( Pool::pullPoolConfig($config), $connector, $connectionConfig ); $this->pool->init(); } protected function getPoolConnection() { return Context::rememberData('connection.' . spl_object_id($this), function () { $connection = $this->pool->borrow(); $connection->{static::KEY_RELEASED} = false; Coroutine::defer(function () use ($connection) { //自动释放 $this->releaseConnection($connection); }); return $connection; }); } protected function releaseConnection($connection) { if ($connection->{static::KEY_RELEASED}) { return; } $connection->{static::KEY_RELEASED} = true; $this->pool->return($connection); } public function release() { $connection = $this->getPoolConnection(); $this->releaseConnection($connection); } public function __call($method, $arguments) { $connection = $this->getPoolConnection(); if ($connection->{static::KEY_RELEASED}) { throw new RuntimeException('Connection already has been released!'); } try { return $connection->{$method}(...$arguments); } catch (Exception|Throwable $e) { $connection->{static::KEY_DISCONNECTED} = true; throw $e; } } } Client.php<?php namespace think\swoole\pool; use Smf\ConnectionPool\Connectors\ConnectorInterface; use think\helper\Arr; class Client implements ConnectorInterface { /** * Connect to the specified Server and returns the connection resource * @param array $config * @return \Swoole\Coroutine\Client */ public function connect(array $config) { $client = new \Swoole\Coroutine\Client(SWOOLE_SOCK_TCP); $host = Arr::pull($config, 'host'); $port = Arr::pull($config, 'port'); $timeout = Arr::pull($config, 'timeout', 5); $client->set($config); $client->connect($host, $port, $timeout); return $client; } /** * Disconnect and free resources * @param \Swoole\Coroutine\Client $connection * @return mixed */ public function disconnect($connection) { $connection->close(); } /** * Whether the connection is established * @param \Swoole\Coroutine\Client $connection * @return bool */ public function isConnected($connection): bool { return $connection->isConnected() && $connection->peek() !== ''; } /** * Reset the connection * @param \Swoole\Coroutine\Client $connection * @param array $config * @return mixed */ public function reset($connection, array $config) { } /** * Validate the connection * * @param \Swoole\Coroutine\Client $connection * @return bool */ public function validate($connection): bool { return $connection instanceof \Swoole\Coroutine\Client; } } resettersResetModel.php<?php namespace think\swoole\resetters; use think\App; use think\Model; use think\swoole\contract\ResetterInterface; use think\swoole\Sandbox; class ResetModel implements ResetterInterface { public function handle(App $app, Sandbox $sandbox) { if (class_exists(Model::class)) { Model::setInvoker(function (...$args) use ($sandbox) { return $sandbox->getApplication()->invoke(...$args); }); } } } ResetConfig.php<?php namespace think\swoole\resetters; use think\App; use think\swoole\contract\ResetterInterface; use think\swoole\Sandbox; class ResetConfig implements ResetterInterface { public function handle(App $app, Sandbox $sandbox) { $app->instance('config', clone $sandbox->getConfig()); return $app; } } ClearInstances.php<?php namespace think\swoole\resetters; use think\App; use think\swoole\contract\ResetterInterface; use think\swoole\Sandbox; class ClearInstances implements ResetterInterface { public function handle(App $app, Sandbox $sandbox) { $instances = ['log', 'session', 'view', 'response', 'cookie']; $instances = array_merge($instances, $sandbox->getConfig()->get('swoole.instances', [])); foreach ($instances as $instance) { $app->delete($instance); } return $app; } } ResetEvent.php<?php namespace think\swoole\resetters; use think\App; use think\swoole\concerns\ModifyProperty; use think\swoole\contract\ResetterInterface; use think\swoole\Sandbox; /** * Class ResetEvent * @package think\swoole\resetters */ class ResetEvent implements ResetterInterface { use ModifyProperty; public function handle(App $app, Sandbox $sandbox) { $event = clone $sandbox->getEvent(); $this->modifyProperty($event, $app); $app->instance('event', $event); return $app; } } ResetService.php<?php namespace think\swoole\resetters; use think\App; use think\swoole\concerns\ModifyProperty; use think\swoole\contract\ResetterInterface; use think\swoole\Sandbox; /** * Class ResetService * @package think\swoole\resetters */ class ResetService implements ResetterInterface { use ModifyProperty; /** * "handle" function for resetting app. * * @param App $app * @param Sandbox $sandbox */ public function handle(App $app, Sandbox $sandbox) { foreach ($sandbox->getServices() as $service) { $this->modifyProperty($service, $app); if (method_exists($service, 'register')) { $service->register(); } if (method_exists($service, 'boot')) { $app->invoke([$service, 'boot']); } } } } ResetPaginator.php<?php namespace think\swoole\resetters; use think\App; use think\Paginator; use think\swoole\contract\ResetterInterface; use think\swoole\Sandbox; class ResetPaginator implements ResetterInterface { public function handle(App $app, Sandbox $sandbox) { Paginator::currentPathResolver(function () use ($sandbox) { return $sandbox->getApplication()->request->baseUrl(); }); Paginator::currentPageResolver(function ($varPage = 'page') use ($sandbox) { $page = $sandbox->getApplication()->request->param($varPage); if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) { return (int) $page; } return 1; }); } } responseFile.php<?php namespace think\swoole\response; use DateTime; use RuntimeException; use SplFileInfo; use think\Response; class File extends Response { public const DISPOSITION_ATTACHMENT = 'attachment'; public const DISPOSITION_INLINE = 'inline'; protected $header = [ 'Content-Type' => 'application/octet-stream', 'Accept-Ranges' => 'bytes', ]; /** * @var SplFileInfo */ protected $file; public function __construct($file, string $contentDisposition = null, bool $autoEtag = true, bool $autoLastModified = true, bool $autoContentType = true) { $this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified, $autoContentType); } public function getFile() { return $this->file; } public function setFile($file, string $contentDisposition = null, bool $autoEtag = true, bool $autoLastModified = true, bool $autoContentType = true) { if (!$file instanceof SplFileInfo) { $file = new SplFileInfo((string) $file); } if (!$file->isReadable()) { throw new RuntimeException('File must be readable.'); } $this->header['Content-Length'] = $file->getSize(); $this->file = $file; if ($autoEtag) { $this->setAutoEtag(); } if ($autoLastModified) { $this->setAutoLastModified(); } if ($contentDisposition) { $this->setContentDisposition($contentDisposition); } if ($autoContentType) { $this->setAutoContentType(); } return $this; } public function setAutoContentType() { $finfo = finfo_open(FILEINFO_MIME_TYPE); $mimeType = finfo_file($finfo, $this->file->getPathname()); if ($mimeType) { $this->header['Content-Type'] = $mimeType; } } public function setContentDisposition(string $disposition, string $filename = '') { if ('' === $filename) { $filename = $this->file->getFilename(); } $this->header['Content-Disposition'] = "{$disposition}; filename=\"{$filename}\""; return $this; } public function setAutoLastModified() { $date = DateTime::createFromFormat('U', $this->file->getMTime()); return $this->lastModified($date->format('D, d M Y H:i:s') . ' GMT'); } public function setAutoEtag() { $eTag = "W/\"" . sha1_file($this->file->getPathname()) . "\""; return $this->eTag($eTag); } protected function sendData(string $data): void { readfile($this->file->getPathname()); } } rpcProtocol.php<?php namespace think\swoole\rpc; class Protocol { const ACTION_INTERFACE = '@action_interface'; const FILE = '@file'; /** * @var string */ private $interface = ''; /** * @var string */ private $method = ''; /** * @var array */ private $params = []; /** * @var array */ private $context = []; /** * Replace constructor * * @param string $interface * @param string $method * @param array $params * @param array $context * @return Protocol */ public static function make(string $interface, string $method, array $params, array $context = []) { $instance = new static(); $instance->interface = $interface; $instance->method = $method; $instance->params = $params; $instance->context = $context; return $instance; } /** * @return string */ public function getInterface(): string { return $this->interface; } /** * @return string */ public function getMethod(): string { return $this->method; } /** * @return array */ public function getParams(): array { return $this->params; } /** * @return array */ public function getContext(): array { return $this->context; } /** * @param string $interface */ public function setInterface(string $interface): void { $this->interface = $interface; } /** * @param string $method */ public function setMethod(string $method): void { $this->method = $method; } /** * @param array $params */ public function setParams(array $params): void { $this->params = $params; } /** * @param array $context */ public function setContext(array $context): void { $this->context = $context; } } File.php<?php namespace think\swoole\rpc; use Throwable; class File extends \think\File { public function __destruct() { //销毁时删除临时文件 try { if (file_exists($this->getPathname())) { unlink($this->getPathname()); } } catch (Throwable $e) { } } } serverDispatcher.php<?php namespace think\swoole\rpc\server; use ReflectionClass; use ReflectionException; use ReflectionMethod; use ReflectionNamedType; use RuntimeException; use Swoole\Coroutine\Server\Connection; use think\App; use think\swoole\contract\rpc\ParserInterface; use think\swoole\Middleware; use think\swoole\rpc\Error; use think\swoole\rpc\Packer; use think\swoole\rpc\Protocol; use think\swoole\rpc\Sendfile; use Throwable; class Dispatcher { use Sendfile; /** * Parser error */ const PARSER_ERROR = -32700; /** * Invalid Request */ const INVALID_REQUEST = -32600; /** * Method not found */ const METHOD_NOT_FOUND = -32601; /** * Invalid params */ const INVALID_PARAMS = -32602; /** * Internal error */ const INTERNAL_ERROR = -32603; protected $parser; protected $services = []; protected $middleware = []; public function __construct(ParserInterface $parser, $services, $middleware = []) { $this->parser = $parser; $this->prepareServices($services); $this->middleware = $middleware; } /** * 获取服务接口 * @param $services * @throws ReflectionException */ protected function prepareServices($services) { foreach ($services as $className) { $reflectionClass = new ReflectionClass($className); $interfaces = $reflectionClass->getInterfaceNames(); if (!empty($interfaces)) { foreach ($interfaces as $interface) { $this->services[class_basename($interface)] = [ 'interface' => $interface, 'class' => $className, ]; } } else { $this->services[class_basename($className)] = [ 'interface' => $className, 'class' => $className, ]; } } } /** * 获取接口信息 * @return array */ protected function getInterfaces() { $interfaces = []; foreach ($this->services as $key => ['interface' => $interface]) { $interfaces[$key] = $this->getMethods($interface); } return $interfaces; } protected function getMethods($interface) { $methods = []; $reflection = new ReflectionClass($interface); foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { if ($method->isConstructor() || $method->isDestructor()) { continue; } $returnType = $method->getReturnType(); if ($returnType instanceof ReflectionNamedType) { $returnType = $returnType->getName(); } $methods[$method->getName()] = [ 'parameters' => $this->getParameters($method), 'returnType' => $returnType, 'comment' => $method->getDocComment(), ]; } return $methods; } protected function getParameters(ReflectionMethod $method) { $parameters = []; foreach ($method->getParameters() as $parameter) { $type = $parameter->getType(); if ($type instanceof ReflectionNamedType) { $type = $type->getName(); } $param = [ 'name' => $parameter->getName(), 'type' => $type, ]; if ($parameter->isOptional()) { $param['default'] = $parameter->getDefaultValue(); } if ($parameter->allowsNull()) { $param['nullable'] = true; } $parameters[] = $param; } return $parameters; } /** * 调度 * @param App $app * @param Connection $conn * @param string|Error $data * @param array $files */ public function dispatch(App $app, Connection $conn, $data, $files = []) { try { switch (true) { case $data instanceof Error: $result = $data; break; case $data === Protocol::ACTION_INTERFACE: $result = $this->getInterfaces(); break; default: $protocol = $this->parser->decode($data); $result = $this->dispatchWithMiddleware($app, $protocol, $files); } } catch (Throwable $e) { $result = Error::make($e->getCode(), $e->getMessage()); } //传输文件 if ($result instanceof \think\File) { foreach ($this->fread($result) as $string) { if (!empty($string)) { $conn->send($string); } } $result = Protocol::FILE; } $data = $this->parser->encodeResponse($result); $conn->send(Packer::pack($data)); } protected function dispatchWithMiddleware(App $app, Protocol $protocol, $files) { $interface = $protocol->getInterface(); $method = $protocol->getMethod(); $params = $protocol->getParams(); //文件参数 foreach ($params as $index => $param) { if ($param === Protocol::FILE) { $params[$index] = array_shift($files); } } $service = $this->services[$interface] ?? null; if (empty($service)) { throw new RuntimeException( sprintf('Service %s is not founded!', $interface), self::METHOD_NOT_FOUND ); } $instance = $app->make($service['class']); $middlewares = array_merge($this->middleware, $this->getServiceMiddlewares($instance, $method)); return Middleware::make($app, $middlewares) ->pipeline() ->send($protocol) ->then(function () use ($instance, $method, $params) { return call_user_func_array([$instance, $method], $params); }); } protected function getServiceMiddlewares($service, $method) { $middlewares = []; $class = new ReflectionClass($service); if ($class->hasProperty('middleware')) { $reflectionProperty = $class->getProperty('middleware'); $reflectionProperty->setAccessible(true); foreach ($reflectionProperty->getValue($service) as $key => $val) { if (!is_int($key)) { $middleware = $key; $options = $val; } elseif (isset($val['middleware'])) { $middleware = $val['middleware']; $options = $val['options'] ?? []; } else { $middleware = $val; $options = []; } if ((isset($options['only']) && !in_array($method, (array) $options['only'])) || (!empty($options['except']) && in_array($method, (array) $options['except']))) { continue; } if (is_string($middleware) && strpos($middleware, ':')) { $middleware = explode(':', $middleware); if (count($middleware) > 1) { $middleware = [$middleware[0], array_slice($middleware, 1)]; } } $middlewares[] = $middleware; } } return $middlewares; } } Channel.php<?php namespace think\swoole\rpc\server; use Swoole\Coroutine; use think\swoole\rpc\packer\Buffer; use think\swoole\rpc\packer\File; class Channel { protected $header; protected $queue; public function __construct($handler) { $this->queue = new Coroutine\Channel(1); Coroutine::create(function () use ($handler) { $this->queue->push($handler); }); } /** * @return File|Buffer */ public function pop() { return $this->queue->pop(); } public function push($handle) { return $this->queue->push($handle); } public function close() { return $this->queue->close(); } } Packer.php<?php namespace think\swoole\rpc; use RuntimeException; use think\swoole\rpc\packer\Buffer; use think\swoole\rpc\packer\File; class Packer { public const HEADER_SIZE = 8; public const HEADER_STRUCT = 'Nlength/Ntype'; public const HEADER_PACK = 'NN'; public const TYPE_BUFFER = 0; public const TYPE_FILE = 1; public static function pack($data, $type = self::TYPE_BUFFER) { return pack(self::HEADER_PACK, strlen($data), $type) . $data; } /** * @param $data * @return array<Buffer|File|string> */ public static function unpack($data) { $header = unpack(self::HEADER_STRUCT, substr($data, 0, self::HEADER_SIZE)); if ($header === false) { throw new RuntimeException('Invalid Header'); } switch ($header['type']) { case Packer::TYPE_BUFFER: $handler = new Buffer($header['length']); break; case Packer::TYPE_FILE: $handler = new File($header['length']); break; default: throw new RuntimeException("unsupported data type: [{$header['type']}"); } $data = substr($data, self::HEADER_SIZE); return [$handler, $data]; } } Sendfile.php<?php namespace think\swoole\rpc; trait Sendfile { protected function fread(\think\File $file) { try { $handle = fopen($file->getPathname(), 'rb'); yield pack(Packer::HEADER_PACK, $file->getSize(), Packer::TYPE_FILE); while (!feof($handle)) { yield fread($handle, 8192); } } finally { fclose($handle); } } } Error.php<?php namespace think\swoole\rpc; class Error implements \JsonSerializable { /** * @var int */ protected $code = 0; /** * @var string */ protected $message = ''; /** * @var mixed */ protected $data; /** * @param int $code * @param string $message * @param mixed $data * * @return Error */ public static function make(int $code, string $message, $data = null): self { $instance = new static(); $instance->code = $code; $instance->message = $message; $instance->data = $data; return $instance; } /** * @return int */ public function getCode(): int { return $this->code; } /** * @return string */ public function getMessage(): string { return $this->message; } /** * @return mixed */ public function getData() { return $this->data; } public function jsonSerialize() { return [ 'code' => $this->code, 'message' => $this->message, 'data' => $this->data, ]; } } JsonParser.php<?php namespace think\swoole\rpc; use Exception; use think\swoole\contract\rpc\ParserInterface; use think\swoole\rpc\server\Dispatcher; class JsonParser implements ParserInterface { /** * Json-rpc version */ const VERSION = '2.0'; const DELIMITER = "@"; /** * @param Protocol $protocol * * @return string */ public function encode(Protocol $protocol): string { $interface = $protocol->getInterface(); $methodName = $protocol->getMethod(); $method = $interface . self::DELIMITER . $methodName; $data = [ 'jsonrpc' => self::VERSION, 'method' => $method, 'params' => $protocol->getParams(), 'context' => $protocol->getContext(), 'id' => '', ]; return json_encode($data, JSON_UNESCAPED_UNICODE); } /** * @param string $string * * @return Protocol */ public function decode(string $string): Protocol { $data = json_decode($string, true); $error = json_last_error(); if ($error != JSON_ERROR_NONE) { throw new Exception( sprintf('Data(%s) is not json format!', $string), Dispatcher::PARSER_ERROR ); } $method = $data['method'] ?? ''; $params = $data['params'] ?? []; $context = $data['context'] ?? []; if (empty($method)) { throw new Exception( sprintf('Method(%s) cant not be empty!', $string), Dispatcher::INVALID_PARAMS ); } $methodAry = explode(self::DELIMITER, $method); if (count($methodAry) < 2) { throw new Exception( sprintf('Method(%s) is bad format!', $method), Dispatcher::INVALID_PARAMS ); } [$interfaceClass, $methodName] = $methodAry; if (empty($interfaceClass) || empty($methodName)) { throw new Exception( sprintf('Interface(%s) or Method(%s) can not be empty!', $interfaceClass, $method), Dispatcher::INVALID_PARAMS ); } return Protocol::make($interfaceClass, $methodName, $params, $context); } /** * @param string $string * * @return mixed */ public function decodeResponse(string $string) { $data = json_decode($string, true); if (array_key_exists('result', $data)) { return $data['result']; } $code = $data['error']['code'] ?? 0; $message = $data['error']['message'] ?? ''; $data = $data['error']['data'] ?? null; return Error::make($code, $message, $data); } /** * @param mixed $result * * @return string */ public function encodeResponse($result): string { $data = [ 'jsonrpc' => self::VERSION, 'id' => '', ]; if ($result instanceof Error) { $data['error'] = $result; } else { $data['result'] = $result; } return json_encode($data); } } clientService.php<?php namespace think\swoole\rpc\client; interface Service { public function withContext($context): self; } Connector.php<?php namespace think\swoole\rpc\client; use Generator; interface Connector { /** * @param Generator|string $data * @param callable $decoder * @return string */ public function sendAndRecv($data, callable $decoder); } Proxy.php<?php namespace think\swoole\rpc\client; use InvalidArgumentException; use Nette\PhpGenerator\Factory; use Nette\PhpGenerator\PhpNamespace; use ReflectionClass; use think\App; use think\swoole\Middleware; use think\swoole\rpc\Protocol; abstract class Proxy implements Service { protected $interface; /** @var Gateway */ protected $gateway; /** @var App */ protected $app; protected $middleware = []; protected $context = []; final public function __construct(App $app, Gateway $gateway, $middleware) { $this->app = $app; $this->gateway = $gateway; $this->middleware = $middleware; } final protected function proxyCall($method, $params) { $protocol = Protocol::make($this->interface, $method, $params, $this->context); return Middleware::make($this->app, $this->middleware) ->pipeline() ->send($protocol) ->then(function (Protocol $protocol) { return $this->gateway->call($protocol); }); } final public function withContext($context): self { $this->context = $context; return $this; } final public static function getClassName($client, $interface) { if (!interface_exists($interface)) { throw new InvalidArgumentException( sprintf('%s must be exist interface!', $interface) ); } $proxyName = class_basename($interface) . "Service"; $className = "rpc\\service\\{$client}\\{$proxyName}"; if (!class_exists($className, false)) { $namespace = new PhpNamespace("rpc\\service\\{$client}"); $namespace->addUse(Proxy::class); $namespace->addUse($interface); $class = $namespace->addClass($proxyName); $class->setExtends(Proxy::class); $class->addImplement($interface); $class->addProperty('interface', class_basename($interface)); $reflection = new ReflectionClass($interface); foreach ($reflection->getMethods() as $methodRef) { if ($methodRef->getDeclaringClass()->name == Service::class) { continue; } $method = (new Factory)->fromMethodReflection($methodRef); $body = "\$this->proxyCall('{$methodRef->getName()}', func_get_args());"; if ($method->getReturnType() != 'void') { $body = "return {$body}"; } $method->setBody($body); $class->addMember($method); } eval($namespace); } return $className; } } Gateway.php<?php namespace think\swoole\rpc\client; use Closure; use Swoole\Coroutine\Client; use think\File; use think\helper\Arr; use think\swoole\concerns\InteractsWithRpcConnector; use think\swoole\contract\rpc\ParserInterface; use think\swoole\exception\RpcClientException; use think\swoole\exception\RpcResponseException; use think\swoole\rpc\Error; use think\swoole\rpc\Packer; use think\swoole\rpc\Protocol; use think\swoole\rpc\Sendfile; class Gateway { use Sendfile; /** @var Connector */ protected $connector; /** @var ParserInterface */ protected $parser; protected $tries; /** * Gateway constructor. * @param Connector|array $connector * @param ParserInterface $parser */ public function __construct($connector, ParserInterface $parser, $tries = 2) { if (is_array($connector)) { $connector = $this->createDefaultConnector($connector); } $this->connector = $connector; $this->parser = $parser; $this->tries = $tries; } protected function encodeData(Protocol $protocol) { $params = $protocol->getParams(); //有文件,先传输 foreach ($params as $index => $param) { if ($param instanceof File) { yield from $this->fread($param); $params[$index] = Protocol::FILE; } } $protocol->setParams($params); $data = $this->parser->encode($protocol); yield Packer::pack($data); } protected function decodeResponse($response) { $result = $this->parser->decodeResponse($response); if ($result instanceof Error) { throw new RpcResponseException($result); } return $result; } protected function sendAndRecv($data) { return $this->connector->sendAndRecv($data, Closure::fromCallable([$this, 'decodeResponse'])); } public function call(Protocol $protocol) { if ($this->tries > 1) { $result = backoff(function () use ($protocol) { try { return $this->sendAndRecv($this->encodeData($protocol)); } catch (RpcResponseException $e) { return $e; } }, $this->tries); if ($result instanceof RpcResponseException) { throw $result; } return $result; } else { return $this->sendAndRecv($this->encodeData($protocol)); } } public function getServices() { return $this->sendAndRecv(Packer::pack(Protocol::ACTION_INTERFACE)); } protected function createDefaultConnector($config) { return new class($config) implements Connector { use InteractsWithRpcConnector; /** @var Client */ protected $client; protected $config; /** * constructor. * @param [] $config */ public function __construct($config) { $this->config = $config; } protected function isConnected(): bool { return $this->client && $this->client->isConnected() && $this->client->peek() !== ''; } protected function getClient() { if (!$this->isConnected()) { $client = new Client(SWOOLE_SOCK_TCP); $config = $this->config; $host = Arr::pull($config, 'host'); $port = Arr::pull($config, 'port'); $timeout = Arr::pull($config, 'timeout', 5); $client->set($config); if (!$client->connect($host, $port, $timeout)) { throw new RpcClientException( sprintf('Connect failed host=%s port=%d', $host, $port) ); } $this->client = $client; } return $this->client; } protected function runWithClient($callback) { return $callback($this->getClient()); } }; } } packerFile.php<?php namespace think\swoole\rpc\packer; class File { protected $name; protected $handle; protected $length; public function __construct($length) { $this->length = $length; } public function write(&$data) { if (!$this->handle) { $this->name = tempnam(sys_get_temp_dir(), 'swoole_rpc_'); $this->handle = fopen($this->name, 'ab'); } $size = fstat($this->handle)['size']; $string = substr($data, 0, $this->length - $size); fwrite($this->handle, $string); if (strlen($data) >= $this->length - $size) { fclose($this->handle); $data = substr($data, $this->length - $size); return new \think\swoole\rpc\File($this->name); } else { $data = ''; } } } Buffer.php<?php namespace think\swoole\rpc\packer; class Buffer { protected $data = ''; protected $length; public function __construct($length) { $this->length = $length; } public function write(&$data) { $size = strlen($this->data); $string = substr($data, 0, $this->length - $size); $this->data .= $string; if (strlen($data) >= $this->length - $size) { $data = substr($data, $this->length - $size); return $this->data; } else { $data = ''; } } } watcherDriver.php<?php namespace think\swoole\watcher; interface Driver { public function watch(callable $callback); } Find.php<?php namespace think\swoole\watcher; use InvalidArgumentException; use Swoole\Coroutine\System; use Swoole\Timer; use think\helper\Str; class Find implements Driver { protected $name; protected $directory; protected $exclude; public function __construct($directory, $exclude, $name) { $ret = System::exec('which find'); if (empty($ret['output'])) { throw new InvalidArgumentException('find not exists.'); } $ret = System::exec('find --help', true); if (Str::contains($ret['output'] ?? '', 'BusyBox')) { throw new InvalidArgumentException('find version not support.'); } $this->directory = $directory; $this->exclude = $exclude; $this->name = $name; } public function watch(callable $callback) { $ms = 2000; $seconds = ceil(($ms + 1000) / 1000); $minutes = sprintf('-%.2f', $seconds / 60); $dest = implode(' ', $this->directory); $name = empty($this->name) ? '' : ' \( ' . join(' -o ', array_map(fn ($v) => "-name \"{$v}\"", $this->name)) . ' \)'; $notName = ''; $notPath = ''; if (!empty($this->exclude)) { $excludeDirs = $excludeFiles = []; foreach ($this->exclude as $directory) { $directory = rtrim($directory, '/'); if (is_dir($directory)) { $excludeDirs[] = $directory; } else { $excludeFiles[] = $directory; } } if (!empty($excludeFiles)) { $notPath = ' -not \( ' . join(' -and ', array_map(fn ($v) => "-name \"{$v}\"", $excludeFiles)) . ' \)'; } if (!empty($excludeDirs)) { $notPath = ' -not \( ' . join(' -and ', array_map(fn ($v) => "-path \"{$v}/*\"", $excludeDirs)) . ' \)'; } } $command = "find {$dest}{$name}{$notName}{$notPath} -mmin {$minutes} -type f -print"; Timer::tick($ms, function () use ($callback, $command) { $ret = System::exec($command); if ($ret['code'] === 0 && strlen($ret['output'])) { $stdout = trim($ret['output']); if (!empty($stdout)) { call_user_func($callback); } } }); } } Scan.php<?php namespace think\swoole\watcher; use Swoole\Timer; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; class Scan implements Driver { protected $finder; protected $files = []; public function __construct($directory, $exclude, $name) { $this->finder = new Finder(); $this->finder ->files() ->name($name) ->in(array_filter((array) $directory, function ($dir) { return is_dir($dir); })) ->exclude($exclude); } protected function findFiles() { $files = []; /** @var SplFileInfo $f */ foreach ($this->finder as $f) { $files[$f->getRealpath()] = $f->getMTime(); } return $files; } public function watch(callable $callback) { $this->files = $this->findFiles(); Timer::tick(2000, function () use ($callback) { $files = $this->findFiles(); foreach ($files as $path => $time) { if (empty($this->files[$path]) || $this->files[$path] != $time) { call_user_func($callback); break; } } $this->files = $files; }); } } websocketmiddlewareSessionInit.php<?php namespace think\swoole\websocket\middleware; use Closure; use think\App; use think\Request; use think\Response; use think\Session; class SessionInit { /** @var App */ protected $app; /** @var Session */ protected $session; public function __construct(App $app, Session $session) { $this->app = $app; $this->session = $session; } /** * Session初始化 * @access public * @param Request $request * @param Closure $next * @return Response */ public function handle($request, Closure $next) { // Session初始化 $varSessionId = $this->app->config->get('session.var_session_id'); $cookieName = $this->session->getName(); if ($varSessionId && $request->request($varSessionId)) { $sessionId = $request->request($varSessionId); } else { $sessionId = $request->cookie($cookieName); } if ($sessionId) { $this->session->setId($sessionId); } $this->session->init(); $request->withSession($this->session); return $next($request); } } Event.php<?php namespace think\swoole\websocket; class Event { public $type; public $data; public function __construct($type, $data) { $this->type = $type; $this->data = $data; } } roomRedis.php<?php namespace think\swoole\websocket\room; use InvalidArgumentException; use Redis as PHPRedis; use Smf\ConnectionPool\ConnectionPool; use Smf\ConnectionPool\Connectors\PhpRedisConnector; use think\helper\Arr; use think\swoole\contract\websocket\RoomInterface; use think\swoole\Manager; use think\swoole\Pool; /** * Class RedisRoom */ class Redis implements RoomInterface { /** * @var array */ protected $config; /** * @var string */ protected $prefix = 'swoole:'; /** @var Manager */ protected $manager; /** @var ConnectionPool */ protected $pool; /** * RedisRoom constructor. * * @param Manager $manager * @param array $config */ public function __construct(Manager $manager, array $config) { $this->manager = $manager; $this->config = $config; if ($prefix = Arr::get($this->config, 'prefix')) { $this->prefix = $prefix; } } /** * @return RoomInterface */ public function prepare(): RoomInterface { $this->initData(); $this->prepareRedis(); return $this; } protected function prepareRedis() { $this->manager->onEvent('workerStart', function () { $config = $this->config; $this->pool = new ConnectionPool( Pool::pullPoolConfig($config), new PhpRedisConnector(), $config ); $this->manager->getPools()->add('websocket.room', $this->pool); }); } protected function initData() { $connector = new PhpRedisConnector(); $connection = $connector->connect($this->config); if (count($keys = $connection->keys("{$this->prefix}*"))) { $connection->del($keys); } $connector->disconnect($connection); } /** * Add multiple socket fds to a room. * * @param string $fd * @param array|string $roomNames */ public function add($fd, $roomNames) { $rooms = is_array($roomNames) ? $roomNames : [$roomNames]; $this->addValue($fd, $rooms, RoomInterface::DESCRIPTORS_KEY); foreach ($rooms as $room) { $this->addValue($room, [$fd], RoomInterface::ROOMS_KEY); } } /** * Delete multiple socket fds from a room. * * @param string fd * @param array|string rooms */ public function delete($fd, $rooms) { $rooms = is_array($rooms) ? $rooms : [$rooms]; $rooms = count($rooms) ? $rooms : $this->getRooms($fd); $this->removeValue($fd, $rooms, RoomInterface::DESCRIPTORS_KEY); foreach ($rooms as $room) { $this->removeValue($room, [$fd], RoomInterface::ROOMS_KEY); } } protected function runWithRedis(\Closure $callable) { $redis = $this->pool->borrow(); try { return $callable($redis); } finally { $this->pool->return($redis); } } /** * Add value to redis. * * @param $key * @param array $values * @param string $table * * @return $this */ protected function addValue($key, array $values, string $table) { $this->checkTable($table); $redisKey = $this->getKey($key, $table); $this->runWithRedis(function (PHPRedis $redis) use ($redisKey, $values) { $pipe = $redis->multi(PHPRedis::PIPELINE); foreach ($values as $value) { $pipe->sadd($redisKey, $value); } $pipe->exec(); }); return $this; } /** * Remove value from redis. * * @param $key * @param array $values * @param string $table * * @return $this */ protected function removeValue($key, array $values, string $table) { $this->checkTable($table); $redisKey = $this->getKey($key, $table); $this->runWithRedis(function (PHPRedis $redis) use ($redisKey, $values) { $pipe = $redis->multi(PHPRedis::PIPELINE); foreach ($values as $value) { $pipe->srem($redisKey, $value); } $pipe->exec(); }); return $this; } /** * Get all sockets by a room key. * * @param string room * * @return array */ public function getClients(string $room) { return $this->getValue($room, RoomInterface::ROOMS_KEY) ?? []; } /** * Get all rooms by a fd. * * @param string fd * * @return array */ public function getRooms($fd) { return $this->getValue($fd, RoomInterface::DESCRIPTORS_KEY) ?? []; } /** * Check table for rooms and descriptors. * * @param string $table */ protected function checkTable(string $table) { if (!in_array($table, [RoomInterface::ROOMS_KEY, RoomInterface::DESCRIPTORS_KEY])) { throw new InvalidArgumentException("Invalid table name: `{$table}`."); } } /** * Get value. * * @param string $key * @param string $table * * @return array */ protected function getValue(string $key, string $table) { $this->checkTable($table); return $this->runWithRedis(function (PHPRedis $redis) use ($table, $key) { return $redis->smembers($this->getKey($key, $table)); }); } /** * Get key. * * @param string $key * @param string $table * * @return string */ protected function getKey(string $key, string $table) { return "{$this->prefix}{$table}:{$key}"; } } Table.php<?php namespace think\swoole\websocket\room; use InvalidArgumentException; use Swoole\Table as SwooleTable; use think\swoole\contract\websocket\RoomInterface; class Table implements RoomInterface { /** * @var array */ protected $config = [ 'room_rows' => 8192, 'room_size' => 2048, 'client_rows' => 4096, 'client_size' => 2048, ]; /** * @var SwooleTable */ protected $rooms; /** * @var SwooleTable */ protected $fds; /** * TableRoom constructor. * * @param array $config */ public function __construct(array $config) { $this->config = array_merge($this->config, $config); } /** * Do some init stuffs before workers started. * * @return RoomInterface */ public function prepare(): RoomInterface { $this->initRoomsTable(); $this->initFdsTable(); return $this; } /** * Add multiple socket fds to a room. * * @param string fd * @param array|string $roomNames */ public function add($fd, $roomNames) { $rooms = $this->getRooms($fd); $roomNames = is_array($roomNames) ? $roomNames : [$roomNames]; foreach ($roomNames as $room) { $fds = $this->getClients($room); if (in_array($fd, $fds)) { continue; } $fds[] = $fd; $rooms[] = $room; $this->setClients($room, $fds); } $this->setRooms($fd, $rooms); } /** * Delete multiple socket fds from a room. * * @param string fd * @param array|string $roomNames */ public function delete($fd, $roomNames = []) { $allRooms = $this->getRooms($fd); $roomNames = is_array($roomNames) ? $roomNames : [$roomNames]; $rooms = count($roomNames) ? $roomNames : $allRooms; $removeRooms = []; foreach ($rooms as $room) { $fds = $this->getClients($room); if (!in_array($fd, $fds)) { continue; } $this->setClients($room, array_values(array_diff($fds, [$fd]))); $removeRooms[] = $room; } $this->setRooms($fd, collect($allRooms)->diff($removeRooms)->values()->toArray()); } /** * Get all sockets by a room key. * * @param string room * * @return array */ public function getClients(string $room) { return $this->getValue($room, RoomInterface::ROOMS_KEY) ?? []; } /** * Get all rooms by a fd. * * @param string fd * * @return array */ public function getRooms($fd) { return $this->getValue($fd, RoomInterface::DESCRIPTORS_KEY) ?? []; } /** * @param string $room * @param array $fds * * @return $this */ protected function setClients(string $room, array $fds) { return $this->setValue($room, $fds, RoomInterface::ROOMS_KEY); } /** * @param string $fd * @param array $rooms * * @return $this */ protected function setRooms($fd, array $rooms) { return $this->setValue($fd, $rooms, RoomInterface::DESCRIPTORS_KEY); } /** * Init rooms table */ protected function initRoomsTable(): void { $this->rooms = new SwooleTable($this->config['room_rows']); $this->rooms->column('value', SwooleTable::TYPE_STRING, $this->config['room_size']); $this->rooms->create(); } /** * Init descriptors table */ protected function initFdsTable() { $this->fds = new SwooleTable($this->config['client_rows']); $this->fds->column('value', SwooleTable::TYPE_STRING, $this->config['client_size']); $this->fds->create(); } /** * Set value to table * * @param $key * @param array $value * @param string $table * * @return $this */ public function setValue($key, array $value, string $table) { $this->checkTable($table); if (empty($value)) { $this->$table->del($key); } else { $this->$table->set($key, ['value' => json_encode($value)]); } return $this; } /** * Get value from table * * @param string $key * @param string $table * * @return array|mixed */ public function getValue(string $key, string $table) { $this->checkTable($table); $value = $this->$table->get($key); return $value ? json_decode($value['value'], true) : []; } /** * Check table for exists * * @param string $table */ protected function checkTable(string $table) { if (!property_exists($this, $table) || !$this->$table instanceof SwooleTable) { throw new InvalidArgumentException("Invalid table name: `{$table}`."); } } } Handler.php<?php namespace think\swoole\websocket; use Swoole\WebSocket\Frame; use think\Event; use think\Request; use think\swoole\contract\websocket\HandlerInterface; use think\swoole\websocket\Event as WsEvent; class Handler implements HandlerInterface { protected $event; public function __construct(Event $event) { $this->event = $event; } /** * "onOpen" listener. * * @param Request $request */ public function onOpen(Request $request) { $this->event->trigger('swoole.websocket.Open', $request); } /** * "onMessage" listener. * * @param Frame $frame */ public function onMessage(Frame $frame) { $this->event->trigger('swoole.websocket.Message', $frame); $this->event->trigger('swoole.websocket.Event', $this->decode($frame->data)); } /** * "onClose" listener. */ public function onClose() { $this->event->trigger('swoole.websocket.Close'); } protected function decode($payload) { $data = json_decode($payload, true); return new WsEvent($data['type'] ?? null, $data['data'] ?? null); } public function encodeMessage($message) { if ($message instanceof WsEvent) { return json_encode([ 'type' => $message->type, 'data' => $message->data, ]); } return $message; } } Room.php<?php namespace think\swoole\websocket; use think\Manager; use think\swoole\websocket\room\Table; /** * Class Room * @package think\swoole\websocket * @mixin Table */ class Room extends Manager { protected $namespace = "\\think\\swoole\\websocket\\room\\"; protected function resolveConfig(string $name) { return $this->app->config->get("swoole.websocket.room.{$name}", []); } /** * 默认驱动 * @return string|null */ public function getDefaultDriver() { return $this->app->config->get('swoole.websocket.room.type', 'table'); } } messagePushMessage.php<?php namespace think\swoole\websocket\message; class PushMessage { public $fd; public $data; public function __construct($fd, $data) { $this->fd = $fd; $this->data = $data; } } socketioEnginePacket.php<?php namespace think\swoole\websocket\socketio; class EnginePacket { /** * Engine.io packet type `open`. */ const OPEN = 0; /** * Engine.io packet type `close`. */ const CLOSE = 1; /** * Engine.io packet type `ping`. */ const PING = 2; /** * Engine.io packet type `pong`. */ const PONG = 3; /** * Engine.io packet type `message`. */ const MESSAGE = 4; /** * Engine.io packet type 'upgrade' */ const UPGRADE = 5; /** * Engine.io packet type `noop`. */ const NOOP = 6; public $type; public $data = ''; public function __construct($type, $data = '') { $this->type = $type; $this->data = $data; } public static function open($payload) { return new static(self::OPEN, $payload); } public static function pong($payload = '') { return new static(self::PONG, $payload); } public static function ping() { return new static(self::PING); } public static function message($payload) { return new static(self::MESSAGE, $payload); } public static function fromString(string $packet) { return new static(substr($packet, 0, 1), substr($packet, 1) ?? ''); } public function toString() { return $this->type . $this->data; } } Handler.php<?php namespace think\swoole\websocket\socketio; use Exception; use Swoole\Timer; use Swoole\Websocket\Frame; use think\Config; use think\Event; use think\Request; use think\swoole\contract\websocket\HandlerInterface; use think\swoole\Websocket; use think\swoole\websocket\Event as WsEvent; class Handler implements HandlerInterface { /** @var Config */ protected $config; protected $event; protected $websocket; protected $eio; protected $pingTimeoutTimer = 0; protected $pingIntervalTimer = 0; protected $pingInterval; protected $pingTimeout; public function __construct(Event $event, Config $config, Websocket $websocket) { $this->event = $event; $this->config = $config; $this->websocket = $websocket; $this->pingInterval = $this->config->get('swoole.websocket.ping_interval', 25000); $this->pingTimeout = $this->config->get('swoole.websocket.ping_timeout', 60000); } /** * "onOpen" listener. * * @param Request $request */ public function onOpen(Request $request) { $this->eio = $request->param('EIO'); $payload = json_encode( [ 'sid' => base64_encode(uniqid()), 'upgrades' => [], 'pingInterval' => $this->pingInterval, 'pingTimeout' => $this->pingTimeout, ] ); $this->push(EnginePacket::open($payload)); $this->event->trigger('swoole.websocket.Open', $request); if ($this->eio < 4) { $this->resetPingTimeout($this->pingInterval + $this->pingTimeout); $this->onConnect(); } else { $this->schedulePing(); } } /** * "onMessage" listener. * * @param Frame $frame */ public function onMessage(Frame $frame) { $enginePacket = EnginePacket::fromString($frame->data); $this->event->trigger('swoole.websocket.Message', $enginePacket); $this->resetPingTimeout($this->pingInterval + $this->pingTimeout); switch ($enginePacket->type) { case EnginePacket::MESSAGE: $packet = Packet::fromString($enginePacket->data); switch ($packet->type) { case Packet::CONNECT: $this->onConnect($packet->data); break; case Packet::EVENT: $type = array_shift($packet->data); $data = $packet->data; $result = $this->event->trigger('swoole.websocket.Event', new WsEvent($type, $data)); if ($packet->id !== null) { $responsePacket = Packet::create(Packet::ACK, [ 'id' => $packet->id, 'nsp' => $packet->nsp, 'data' => $result, ]); $this->push($responsePacket); } break; case Packet::DISCONNECT: $this->event->trigger('swoole.websocket.Disconnect'); $this->websocket->close(); break; default: $this->websocket->close(); break; } break; case EnginePacket::PING: $this->push(EnginePacket::pong($enginePacket->data)); break; case EnginePacket::PONG: $this->schedulePing(); break; default: $this->websocket->close(); break; } } /** * "onClose" listener. */ public function onClose() { Timer::clear($this->pingTimeoutTimer); Timer::clear($this->pingIntervalTimer); $this->event->trigger('swoole.websocket.Close'); } protected function onConnect($data = null) { try { $this->event->trigger('swoole.websocket.Connect', $data); $packet = Packet::create(Packet::CONNECT); if ($this->eio >= 4) { $packet->data = ['sid' => base64_encode(uniqid())]; } } catch (Exception $exception) { $packet = Packet::create(Packet::CONNECT_ERROR, [ 'data' => ['message' => $exception->getMessage()], ]); } $this->push($packet); } protected function resetPingTimeout($timeout) { Timer::clear($this->pingTimeoutTimer); $this->pingTimeoutTimer = Timer::after($timeout, function () { $this->websocket->close(); }); } protected function schedulePing() { Timer::clear($this->pingIntervalTimer); $this->pingIntervalTimer = Timer::after($this->pingInterval, function () { $this->push(EnginePacket::ping()); $this->resetPingTimeout($this->pingTimeout); }); } public function encodeMessage($message) { if ($message instanceof WsEvent) { $message = Packet::create(Packet::EVENT, [ 'data' => array_merge([$message->type], $message->data), ]); } if ($message instanceof Packet) { $message = EnginePacket::message($message->toString()); } if ($message instanceof EnginePacket) { $message = $message->toString(); } return $message; } protected function push($data) { $this->websocket->push($data); } } Packet.php<?php namespace think\swoole\websocket\socketio; /** * Class Packet */ class Packet { /** * Socket.io packet type `connect`. */ const CONNECT = 0; /** * Socket.io packet type `disconnect`. */ const DISCONNECT = 1; /** * Socket.io packet type `event`. */ const EVENT = 2; /** * Socket.io packet type `ack`. */ const ACK = 3; /** * Socket.io packet type `connect_error`. */ const CONNECT_ERROR = 4; /** * Socket.io packet type 'binary event' */ const BINARY_EVENT = 5; /** * Socket.io packet type `binary ack`. For acks with binary arguments. */ const BINARY_ACK = 6; public $type; public $nsp = '/'; public $data = null; public $id = null; public function __construct(int $type) { $this->type = $type; } public static function create($type, array $decoded = []) { $new = new static($type); $new->id = $decoded['id'] ?? null; if (isset($decoded['nsp'])) { $new->nsp = $decoded['nsp'] ?: '/'; } else { $new->nsp = '/'; } $new->data = $decoded['data'] ?? null; return $new; } public function toString() { $str = '' . $this->type; if ($this->nsp && '/' !== $this->nsp) { $str .= $this->nsp . ','; } if ($this->id !== null) { $str .= $this->id; } if (null !== $this->data) { $str .= json_encode($this->data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } return $str; } public static function fromString(string $str) { $i = 0; $packet = new Packet((int) substr($str, 0, 1)); // look up namespace (if any) if ('/' === substr($str, $i + 1, 1)) { $nsp = ''; while (++$i) { $c = substr($str, $i, 1); if (',' === $c) { break; } $nsp .= $c; if ($i === strlen($str)) { break; } } $packet->nsp = $nsp; } else { $packet->nsp = '/'; } // look up id $next = substr($str, $i + 1, 1); if ('' !== $next && is_numeric($next)) { $id = ''; while (++$i) { $c = substr($str, $i, 1); if (null == $c || !is_numeric($c)) { --$i; break; } $id .= substr($str, $i, 1); if ($i === strlen($str)) { break; } } $packet->id = intval($id); } // look up json data if (substr($str, ++$i, 1)) { $packet->data = json_decode(substr($str, $i), true); } return $packet; } } Pusher.php<?php namespace think\swoole\websocket; use think\swoole\contract\websocket\HandlerInterface; use think\swoole\Manager; use think\swoole\websocket\message\PushMessage; /** * Class Pusher */ class Pusher { /** @var Room */ protected $room; /** @var Manager */ protected $manager; /** @var HandlerInterface */ protected $handler; protected $to = []; public function __construct(Manager $manager, Room $room, HandlerInterface $handler) { $this->manager = $manager; $this->room = $room; $this->handler = $handler; } public function to(...$values) { foreach ($values as $value) { if (is_array($value)) { $this->to(...$value); } elseif (!in_array($value, $this->to)) { $this->to[] = $value; } } return $this; } /** * Push message to related descriptors * @param $data * @return void */ public function push($data): void { $fds = []; foreach ($this->to as $room) { $clients = $this->room->getClients($room); if (!empty($clients)) { $fds = array_merge($fds, $clients); } } foreach (array_unique($fds) as $fd) { [$workerId, $fd] = explode('.', $fd); $data = $this->handler->encodeMessage($data); $this->manager->sendMessage((int) $workerId, new PushMessage((int) $fd, $data)); } } public function emit(string $event, ...$data): void { $this->push(new Event($event, $data)); } } 目录 ├── Watcher.php ├── Websocket.php ├── App.php ├── Service.php ├── Pool.php ├── Http.php ├── Sandbox.php ├── Manager.php ├── Middleware.php ├── helpers.php ├── Table.php └── command ├── Server.php ├── RpcInterface.php └── concerns ├── WithApplication.php ├── InteractsWithRpcServer.php ├── InteractsWithSwooleTable.php ├── InteractsWithWebsocket.php ├── WithRpcClient.php ├── ModifyProperty.php ├── InteractsWithServer.php ├── WithMiddleware.php ├── InteractsWithHttp.php ├── InteractsWithQueue.php ├── InteractsWithRpcConnector.php ├── InteractsWithPools.php ├── InteractsWithRpcClient.php ├── WithContainer.php ├── InteractsWithTracing.php └── config ├── swoole.php └── contract └── websocket ├── HandlerInterface.php ├── RoomInterface.php ├── ResetterInterface.php └── rpc ├── ParserInterface.php └── coroutine ├── Context.php └── exception ├── RpcResponseException.php ├── RpcClientException.php └── middleware ├── TraceRpcServer.php ├── TraceRpcClient.php ├── InteractsWithVarDumper.php └── pool └── proxy ├── Connection.php ├── Store.php ├── Cache.php ├── Connector.php ├── Db.php ├── Proxy.php ├── Client.php └── resetters ├── ResetModel.php ├── ResetConfig.php ├── ClearInstances.php ├── ResetEvent.php ├── ResetService.php ├── ResetPaginator.php └── response ├── File.php └── rpc ├── Protocol.php ├── File.php └── server ├── Dispatcher.php ├── Channel.php ├── Packer.php ├── Sendfile.php ├── Error.php ├── JsonParser.php └── client ├── Service.php ├── Connector.php ├── Proxy.php ├── Gateway.php └── packer ├── File.php ├── Buffer.php └── watcher ├── Driver.php ├── Find.php ├── Scan.php └── websocket └── middleware ├── SessionInit.php ├── Event.php └── room ├── Redis.php ├── Table.php ├── Handler.php ├── Room.php └── message ├── PushMessage.php └── socketio ├── EnginePacket.php ├── Handler.php ├── Packet.php ├── Pusher.php
使用方法:php think code 表 模块儿 路径codesys.php<?php use app\code\command\Controller; use think\Console; $app = app(); if ($app->request->isCli()) { // 动态注册操作指令 Console::starting(function (Console $console) { $console->addCommand(Controller::class); }); } else { }tplindex_search.html 换行位置 条件搜索 换行位置 url()}{/literal}" autocomplete="off" class="layui-form layui-form-pane form-search" method="get" onsubmit="return false"> {foreach $info['columns'] as $k=>$v}换行位置 换行位置 {$v['COLUMN_COMMENT']} 换行位置 {if in_array($v['COLUMN_NAME'],$info['search']['like'])} 换行位置 换行位置 换行位置 {elseif in_array($v['COLUMN_NAME'],$info['search']['equal']) /} 换行位置 -- 全部 -- 换行位置 {literal}{foreach ['0','1'] as $k=>$v}{/literal}换行位置 {literal}{if $k.'' eq input('{/literal}{$v['COLUMN_NAME']}{literal}')}{/literal}换行位置 {literal}{$v}{/literal} 换行位置 {literal}{else}{/literal}换行位置 {literal}{$v}{/literal} 换行位置 {literal}{/if}{/literal}{literal}{/foreach}{/literal}换行位置 {elseif in_array($v['COLUMN_NAME'],$info['search']['between']) /} 换行位置 换行位置 {else /} 换行位置 换行位置 换行位置 {/if} 换行位置 {/foreach} 换行位置  搜 索 换行位置 换行位置 换行位置 换行位置 form.html url()}{/literal}" autocomplete="off" class="layui-form layui-card" data-auto="true" method="post">换行位置 换行位置 {foreach $info['columns'] as $k=>$v} {if $v['COLUMN_NAME']===$info['pk']} {literal}{notempty name='vo.id'} {/notempty}{/literal}换行位置 {else/} {if in_array($v['COLUMN_NAME'],$info['add']['input'])} 换行位置 {$v['COLUMN_COMMENT']}换行位置 {:ucwords($v['COLUMN_NAME'])}换行位置 换行位置 提示，请填写{$v['COLUMN_COMMENT']}换行位置 换行位置 {elseif in_array($v['COLUMN_NAME'],$info['add']['time']) /}换行位置 换行位置 {$v['COLUMN_COMMENT']}换行位置 {:ucwords($v['COLUMN_NAME'])}换行位置 换行位置 换行位置 提示，请填写{$v['COLUMN_COMMENT']}换行位置 换行位置 {elseif in_array($v['COLUMN_NAME'],$info['add']['select']) /} 换行位置 {$v['COLUMN_COMMENT']}换行位置 {:ucwords($v['COLUMN_NAME'])}换行位置 换行位置 -- 全部 -- 换行位置 {literal}{foreach ['0','1'] as $k=>$v}{/literal}换行位置 {literal}{if $k.'' eq input('{/literal}{$v['COLUMN_NAME']}{literal}')}{/literal} {literal}{$v}{/literal} 换行位置 {literal}{else}{/literal} {literal}{$v}{/literal} 换行位置 {literal}{/if}{/literal}{literal}{/foreach}{/literal} 换行位置 提示，请填写{$v['COLUMN_COMMENT']}换行位置 换行位置 {elseif in_array($v['COLUMN_NAME'],$info['add']['text']) /} 换行位置 {$v['COLUMN_COMMENT']}换行位置 {:ucwords($v['COLUMN_NAME'])}换行位置 {literal}{$vo.{/literal}{$v['COLUMN_NAME']}{literal}|default=""}{/literal} 换行位置 提示，请填写{$v['COLUMN_COMMENT']}换行位置 换行位置 {elseif in_array($v['COLUMN_NAME'],$info['add']['pic']) /} 换行位置 {$v['COLUMN_COMMENT']}换行位置 {:ucwords($v['COLUMN_NAME'])}换行位置 {literal} {/literal}换行位置 {literal} {/literal}换行位置 换行位置 {else /} 换行位置 {$v['COLUMN_COMMENT']}换行位置 {:ucwords($v['COLUMN_NAME'])}换行位置 换行位置 提示，请填写{$v['COLUMN_COMMENT']}换行位置 换行位置 {/if} {/if} {/foreach} 换行位置 换行位置 换行位置 保存数据 换行位置 取消编辑 换行位置 换行位置 换行位置 {block name='script'} {literal} {/literal} {/block} controller.php<>?php namespace {$info["namespaceController"]}; use {$info["namespaceModel"]}\{$info['modelName']} as Model{$info['modelName']};换行位置 use think\admin\Controller; /** * {$info["text"]}管理 * Class {$info['controllerName']}换行位置 * @package {$info["namespaceController"]}\{$info['controllerName']}换行位置 */ class {$info['controllerName']} extends Controller { /** * {$info["text"]}管理 * @auth true * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ public function index() { $this->title = '{$info["text"]}管理'; $query = Model{$info['modelName']}::mQuery(); $query->like('{:implode(",",$info["search"]["like"])}')->equal('{:implode(",",$info["search"]["equal"])}')->dateBetween('{:implode(",",$info["search"]["between"])}'); $query->order('id desc')->page(); } /** * 添加{$info["text"]}换行位置 * @auth true */ public function add() { Model{$info['modelName']}::mForm('form'); } /** * 列表数据处理 * @param array $data */ protected function _index_page_filter(array &$data) { } /** * 表单数据处理 * @param array $data */ protected function _form_filter(array &$data) { if ($this->request->isGet()) { }else{ } } /** * 编辑{$info["text"]}换行位置 * @auth true */ public function edit() { Model{$info['modelName']}::mForm('form'); } /** * 修改{$info["text"]}状态 * @auth true */ public function state() { Model{$info['modelName']}::mSave($this->_vali([ 'status.in:0,1' => '状态值范围异常！', 'status.require' => '状态值不能为空！', ])); } /** * 删除{$info["text"]}换行位置 * @auth true */ public function remove(){ Model{$info['modelName']}::mDelete(); } }index.html{literal}{extend name="../../admin/view/main"} {block name="button"} 换行位置 添加{/literal}{$info['text']}{literal} 换行位置 换行位置 换行位置 删除{/literal}{$info['text']}{literal} 换行位置 换行位置 {/block} {block name='content'} {include file='{/literal}{:strtolower($info['controllerName'])}{literal}/index_search'} {notempty name='list'} {/literal}{literal} {/literal}换行位置 {foreach $info['columns'] as $k=>$v} {$v['COLUMN_COMMENT']} 换行位置 {/foreach} {literal} 操作 {/literal} {literal}{/notempty} {foreach $list as $key=>$vo}{/literal} 换行位置 {foreach $info['columns'] as $k=>$v} {if in_array($v['COLUMN_NAME'],$info['add']['pic']) /} 换行位置 {else /} {literal}{$vo.{/literal}{$v['COLUMN_NAME']}{literal}|default=''}{/literal} 换行位置 {/if} {/foreach} {literal} 换行位置 换行位置 编 辑换行位置 换行位置 换行位置 删 除换行位置 换行位置 {/foreach} 换行位置 {empty name='list'}没有记录哦{else}{$pagehtml|raw|default=''}{/empty} {/block} {block name='script'}换行位置 换行位置 {/block}{/literal} model.php<>?php namespace {$info["namespaceModel"]}; use think\admin\Model; /** * * Class {$info['modelName']}换行位置 * @package app\data\model */ class {$info['modelName']} extends Model { }commandController.php<?php namespace app\code\command; use think\admin\Command; use think\console\Input; use think\console\input\Argument; use think\console\Output; use think\View; /** * 生成代码 * Class Controller * @package app\data\command */ class Controller extends Command { protected function configure() { $this->setName('code'); $this->addArgument('table', Argument::OPTIONAL, '表名', ''); $this->addArgument('moduleName', Argument::OPTIONAL, '模块', ''); $this->addArgument('path', Argument::OPTIONAL, '路径', ''); $this->setDescription('生成代码', 'table moduleName path'); } //驼峰命名转下划线命名 public function toUnderScore($str, $separatist = '/') { $dstr = preg_replace_callback('/([A-Z]+)/', function ($matchs) { return '/' . strtolower($matchs[0]); }, $str); return str_replace('/', $separatist, trim(preg_replace('/\/{2,}/', '/', $dstr), '/')); } /** * @param Input $input * @param Output $output * @return void * @throws \think\admin\Exception * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ protected function execute(Input $input, Output $output) { [$table, $moduleName, $path] = [$input->getArgument('table'), $input->getArgument('moduleName'), $input->getArgument('path')]; if (empty($table)){ $this->setQueueError("示例:php think code 表 模块儿 路径");die;} $info = $this->getInfo($table, $moduleName, $path); $view = new View($this->app); $contentController = $view->fetch(__DIR__ . '/../tpl/controller.php', [ 'info' => $info ]); $contentController = str_replace('<>?php', '<?php', $contentController); $contentController = str_replace('换行位置', '', $contentController); $contentModel = $view->fetch(__DIR__ . '/../tpl/model.php', [ 'info' => $info ]); $contentModel = str_replace('<>?php', '<?php', $contentModel); $contentModel = str_replace('换行位置', '', $contentModel); $contentViewSearch = $view->fetch(__DIR__ . '/../tpl/index_search.html', [ 'info' => $info ]); $contentViewSearch = str_replace('换行位置', '', $contentViewSearch); $contentViewForm = $view->fetch(__DIR__ . '/../tpl/form.html', [ 'info' => $info ]); $contentViewForm = str_replace('换行位置', '', $contentViewForm); $contentViewIndex = $view->fetch(__DIR__ . '/../tpl/index.html', [ 'info' => $info ]); $contentViewIndex = str_replace('换行位置', '', $contentViewIndex); $pathController = __DIR__ . '/../../../' . str_replace('\\', '/', $info['namespaceController']); $pathModel = __DIR__ . '/../../../' . str_replace('\\', '/', $info['namespaceModel']); if ($path) { $pathView = $pathController . '/../../../' . $path . '/view/' . $moduleName . '/' .strtolower($info['controllerName']); } else { $pathView = $pathController . '/../view/' . strtolower($info['controllerName']); } $this->mkdirs($pathController); $this->mkdirs($pathModel); $this->mkdirs($pathView); var_dump($pathView . '/index.html'); if (!file_exists($pathController . '/' . $info['controllerName'] . '.php')) file_put_contents($pathController . '/' . $info['controllerName'] . '.php', $contentController); else echo $pathController . '/' . $info['controllerName'] . '.php,文件已存在'; if (!file_exists($pathModel . '/' . $info['modelName'] . '.php')) file_put_contents($pathModel . '/' . $info['modelName'] . '.php', $contentModel); else echo $pathModel . '/' . $info['modelName'] . '.php,文件已存在'; if (!file_exists($pathView . '/index.html')) file_put_contents($pathView . '/index.html', $contentViewIndex); else echo $pathView . '/index.html,文件已存在'; if (!file_exists($pathView . '/index_search.html')) file_put_contents($pathView . '/index_search.html', $contentViewSearch); else echo $pathView . '/index_search.html,文件已存在'; if (!file_exists($pathView . '/form.html')) file_put_contents($pathView . '/form.html', $contentViewForm); else echo $pathView . '/form.html,文件已存在'; $this->setQueueSuccess("生成代码成功"); } protected function mkdirs($dir, $mode = 0777) { if (is_dir($dir) || @mkdir($dir, $mode)) return TRUE; if (!$this->mkdirs(dirname($dir), $mode)) return FALSE; return @mkdir($dir, $mode); } protected function getInfo($table, $moduleName, $path) { $database = $this->app->config->get('database.connections.mysql.database'); $data = $this->app->db->query("select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA='${database}' and TABLE_NAME='${table}'"); $tableColumn = $this->app->db->query("select COLUMN_NAME,DATA_TYPE,COLUMN_COMMENT from information_schema.COLUMNS where TABLE_SCHEMA='$database' and table_name = '$table'"); if (count($data) == 0) { $this->setQueueError("生成代码的数据表{$table}不存在"); } $rtn = []; $pk = $this->app->db->name($table)->getPk(); $rtn['pk'] = $pk ? $pk : 'id'; $rtn['search'] = [ 'like' => [], 'equal' => [], 'between' => [], ]; $rtn['add'] = [ 'pic' => [], 'select' => [], 'input' => [], 'text' => [], 'time' => [], ]; $rtn['columns'] = $tableColumn; $columnTypeLike = [ 'varchar', 'char', ]; $columnTypeEqual = [ 'tinyint', ]; $columnTypeText = [ 'text', 'longtext', ]; $columnTypeBetween = [ 'date', 'datetime', 'timestamp', ]; foreach ($rtn['columns'] as $k => $v) { if (in_array($v['DATA_TYPE'], $columnTypeLike)) { $rtn['search']['like'][] = $v['COLUMN_NAME']; if (strpos($v['COLUMN_NAME'], 'pic') || strpos($v['COLUMN_NAME'], 'img') || strpos($v['COLUMN_NAME'], 'image') || strpos($v['COLUMN_NAME'], 'avatar')) { $rtn['add']['pic'][] = $v['COLUMN_NAME']; } else { $rtn['add']['input'][] = $v['COLUMN_NAME']; } } else if (in_array($v['DATA_TYPE'], $columnTypeEqual)) { $rtn['search']['equal'][] = $v['COLUMN_NAME']; $rtn['add']['select'][] = $v['COLUMN_NAME']; } else if (in_array($v['DATA_TYPE'], $columnTypeBetween)) { $rtn['search']['between'][] = $v['COLUMN_NAME']; $rtn['add']['time'][] = $v['COLUMN_NAME']; } else if (in_array($v['DATA_TYPE'], $columnTypeText)) { $rtn['add']['text'][] = $v['COLUMN_NAME']; } else { $rtn['add']['input'][] = $v['COLUMN_NAME']; } } // 处理控制器、模型 $tableArr = explode('_', $table); $rtn['moduleName'] = $moduleName; if (count($tableArr) == 1) { $rtn['controllerName'] = ucwords($tableArr[0]); $rtn['modelName'] = ucwords($tableArr[0]); } if (count($tableArr) == 2) { $rtn['controllerName'] = ucwords($tableArr[1]); $rtn['modelName'] = ucwords($tableArr[0]) . ucwords($tableArr[1]); } if (count($tableArr) == 3) { $rtn['controllerName'] = ucwords($tableArr[1]) . $tableArr[2]; $rtn['modelName'] = ucwords($tableArr[0]) . ucwords($tableArr[1]) . ucwords($tableArr[2]); } if (count($tableArr) == 4) { $rtn['controllerName'] = ucwords($tableArr[1]) . $tableArr[2] . $tableArr[3]; $rtn['modelName'] = ucwords($tableArr[1]) . ucwords($tableArr[2]) . ucwords($tableArr[3]); } if ($path) { $rtn['namespaceModel'] = "app\\" . $path . "\model"; $rtn['namespaceController'] = "app\\" . $path . "\\controller\\" . $moduleName; } else { $rtn['namespaceModel'] = "app\\" . $moduleName . "\model"; $rtn['namespaceController'] = "app\\" . $moduleName . "\\controller"; } $tableInfo = $this->app->db->query("show table status where name='$table'"); $comment = explode('-', $tableInfo[0]['Comment']); $rtn['text'] = count($comment) > 1 ? $comment[1] : $comment[0]; // 判断节点是否存在 $node = $path ? $path . '/' . strtolower($moduleName).'.'.strtolower($rtn['controllerName']) . '/index' : strtolower($moduleName).'/'.strtolower($rtn['controllerName']) . '/index'; $nodeData = $this->app->db->table('system_menu')->where('node', $node)->where('url', $node)->find(); if (!$nodeData) { $rootData = $this->app->db->table('system_menu')->where('url', 'like', "%$node%")->find(); if ($rootData) { $this->app->db->table('system_menu')->insert([ 'pid' => $rootData['pid'], 'title' => $rtn['text'] . '管理', 'icon' => '', 'node' => $node, 'url' => $node, 'params' => '', 'target' => '_self', 'sort' => 0, 'status' => 1, ]); } else { $rootData = $this->app->db->table('system_menu')->where('pid', '=', 0)->where('title', $comment[0] . '管理')->where('node', '')->find(); if (!$rootData) { $rootData = $this->app->db->table('system_menu')->insertGetId([ 'pid' => 0, 'title' => $comment[0] . '管理', 'icon' => '', 'node' => '', 'url' => '', 'params' => '', 'target' => '_self', 'sort' => 0, 'status' => 1, ]); } $this->app->db->table('system_menu')->insert([ 'pid' => $rootData['id'], 'title' => $rtn['text'] . '管理', 'icon' => '', 'node' => $node, 'url' => $node, 'params' => '', 'target' => '_self', 'sort' => 0, 'status' => 1, ]); } } // $this->setQueueError('调试'); return $rtn; } } 目录 └── code ├── sys.php └── tpl ├── index_search.html ├── form.html ├── controller.php ├── index.html ├── model.php └── command ├── Controller.php
数据列表操作启用、禁用 禁 用 激 活 多选表格头 表格行 排序表头 刷 新 表行 url()}" data-loading="false" data-value="id#{$vo.id};action#sort;sort#{value}" value="{$vo.sort}"> 爬虫 标题:{{=d.title}} 链接:{{=d.href}} 操作:删除 angluar操作例子 序号 是否正确 答案内容 添加 正确 错误 保存数据 取消编辑
xdebug 配置php.inixdebug 2.0配置[xdebug] xdebug.remote_enable=On xdebug.remote_autostart=Off xdebug.remote_handler=dbgp xdebug.remote_host=192.168.1.105 (开发环境地址) xdebug.remote_port=10010 (开发环境端口) xdebug.idekey=PHPSTORMxdebug 3.0配置[xdebug] # xdebug.remote_enable=On xdebug.start_with_request=yes xdebug.discover_client_host=true xdebug.remote_cookie_expire_time = 3600 xdebug.remote_autostart=Off xdebug.remote_handler=dbgp xdebug.client_host=192.168.101.4 xdebug.client_port=10010 xdebug.idekey=PHPSTORM xdebug.mode = debug配置完一定要重启fpmphpstorm 配置Break at first line in PHP Scripts Off 禁用 不然调试一直弹出 Languages Frameworks -> php -> debug -> xdebug prot 10010 Languages Frameworks -> php -> debug -> xdebug -> DBGp Proxy# DBGp Proxy key:PHPSTORM host:ti001.lan.baidu120.cc port:10010Languages Frameworks -> php -> debug -> xdebug -> servers# xdebug -> servers name:ti001.lan.baidu120.cc 访问域名 host:ti001.lan.baidu120.cc 访问域名 port:80 域名端口号 use maping path D:\docker.dev\www\dev.ti001.com /www/wwwroot/dev.ti001.comEdit configurations -> php remote debug# Edit configurations -> php remote debug name:ti001.lan.baidu120.cc On filter debug connection by ide key server:ti001.lan.baidu120.cc IDE key: PHPSTORMpostman 配置Cookie:XDEBUG_SESSION=PHPSTORM;
clienttcp_client.php<?php // 连接 swoole tcp 服务 $client = new swoole_client(SWOOLE_SOCK_TCP); if(!$client->connect("127.0.0.1", 9501)) { echo "连接失败"; exit; } /// php cli常量 fwrite(STDOUT, "请输入消息:"); $msg = trim(fgets(STDIN)); // 发送消息给 tcp server服务器 $client->send($msg); // 接受来自server 的数据 $result = $client->recv(); echo $result;coroutineredis.php<?php $http = new swoole_http_server('0.0.0.0', 8001); $http->on('request', function($request, $response) { // 获取redis 里面 的key的内容， 然后输出浏览器 $redis = new Swoole\Coroutine\Redis(); $redis->connect('127.0.0.1', 6379); $value = $redis->get($request->get['a']); $response->header("Content-Type", "text/plain"); $response->end($value); }); $http->start();io1.log20180306 18:08:4320180306 18:10:0720180306 18:10:0920180306 18:10:0920180306 18:10:39 20180306 18:10:401.txttest-iomysql.php<?php class AysMysql { /** * @var string */ public $dbSource = ""; /** * mysql的配置 * @var array */ public $dbConfig = []; public function __construct() { //new swoole_mysql; $this->dbSource = new Swoole\Mysql; $this->dbConfig = [ 'host' => '127.0.0.1', 'port' => 5123, 'user' => 'root', 'password' => 123456, 'database' => 'swoole', 'charset' => 'utf8', ]; } public function update() { } public function add() { } /** * mysql 执行逻辑 * @param $id * @param $username * @return bool */ public function execute($id, $username) { // connect $this->dbSource->connect($this->dbConfig, function($db, $result) use($id, $username) { echo "mysql-connect".PHP_EOL; if($result === false) { var_dump($db->connect_error); // todo } //$sql = "select * from test where id=1"; $sql = "update test set `username` = '".$username."' where id=".$id; // insert into // query (add select update delete) $db->query($sql, function($db, $result){ // select => result返回的是 查询的结果内容 if($result === false) { // todo var_dump($db->error); }elseif($result === true) {// add update delete // todo var_dump($db->affected_rows); }else { print_r($result); } $db->close(); }); }); return true; } } $obj = new AysMysql(); $flag = $obj->execute(1, 'singwa-111112'); var_dump($flag).PHP_EOL; echo "start".PHP_EOL; for($i=0; $i<900000;$i++) { echo $i.PHP_EOL; } // 详情页 -》mysql(阅读数) -》msyql 文章 +1 -》页面数据呈现出来read.php<?php /** * 读取文件 * __DIR__ */ $result = Swoole\Async::readfile(__DIR__."/1.txt", function($filename, $fileContent) { echo "filename:".$filename.PHP_EOL; // \n \r\n echo "content:".$fileContent.PHP_EOL; }); var_dump($result); echo "start".PHP_EOL;redis.php<?php $redisClient = new swoole_redis;// Swoole\Redis $redisClient->connect('127.0.0.1', 6379, function(swoole_redis $redisClient, $result) { echo "connect".PHP_EOL; var_dump($result); // 同步 redis (new Redis())->set('key',2); /*$redisClient->set('singwa_1', time(), function(swoole_redis $redisClient, $result) { var_dump($result); });*/ /*$redisClient->get('singwa_1', function(swoole_redis $redisClient, $result) { var_dump($result); $redisClient->close(); });*/ $redisClient->keys('*gw*', function(swoole_redis $redisClient, $result) { var_dump($result); $redisClient->close(); }); }); echo "start".PHP_EOL;write.php<?php $content = date("Ymd H:i:s").PHP_EOL; swoole_async_writefile(__DIR__."/1.log", $content, function($filename){ // todo echo "success".PHP_EOL; }, FILE_APPEND); // file_put_contents(); echo "start".PHP_EOL;memorytable.php<?php // 创建内存表 $table = new swoole_table(1024); // 内存表增加一列 $table->column('id', $table::TYPE_INT, 4); $table->column('name', $table::TYPE_STRING, 64); $table->column('age', $table::TYPE_INT, 3); $table->create(); $table->set('singwa_imooc', ['id' => 1, 'name'=> 'singwa', 'age' => 30]); // 另外一种方案 $table['singwa_imooc_2'] = [ 'id' => 2, 'name' => 'singwa2', 'age' => 31, ]; $table->decr('singwa_imooc_2', 'age', 2); print_r($table['singwa_imooc_2']); echo "delete start:".PHP_EOL; $table->del('singwa_imooc_2'); print_r($table['singwa_imooc_2']);processcurl.php<?php echo "process-start-time:".date("Ymd H:i:s"); $workers = []; $urls = [ 'http://baidu.com', 'http://sina.com.cn', 'http://qq.com', 'http://baidu.com?search=singwa', 'http://baidu.com?search=singwa2', 'http://baidu.com?search=imooc', ]; for($i = 0; $i < 6; $i++) { // 子进程 $process = new swoole_process(function(swoole_process $worker) use($i, $urls) { // curl $content = curlData($urls[$i]); //echo $content.PHP_EOL; $worker->write($content.PHP_EOL); }, true); $pid = $process->start(); $workers[$pid] = $process; } foreach($workers as $process) { echo $process->read(); } /** * 模拟请求URL的内容 1s * @param $url * @return string */ function curlData($url) { // curl file_get_contents sleep(1); return $url . "success".PHP_EOL; } echo "process-end-time:".date("Ymd H:i:s");process.php<?php $process = new swoole_process(function(swoole_process $pro) { // todo // php redis.php $pro->exec("/home/work/study/soft/php/bin/php", [__DIR__.'/../server/http_server.php']); }, false); $pid = $process->start(); echo $pid . PHP_EOL; swoole_process::wait();serveraccess.loghttp_server.php<?php $http = new swoole_http_server("0.0.0.0", 8811); $http->set( [ 'enable_static_handler' => true, 'document_root' => "/home/work/hdtocs/swoole_mooc/data", ] ); $http->on('request', function($request, $response) { //print_r($request->get); $content = [ 'date:' => date("Ymd H:i:s"), 'get:' => $request->get, 'post:' => $request->post, 'header:' => $request->header, ]; swoole_async_writefile(__DIR__."/access.log", json_encode($content).PHP_EOL, function($filename){ // todo }, FILE_APPEND); $response->cookie("singwa", "xsssss", time() + 1800); $response->end("sss". json_encode($request->get)); }); $http->start();tcp.php<?php //创建Server对象，监听 127.0.0.1:9501端口 $serv = new swoole_server("127.0.0.1", 9501); $serv->set([ 'worker_num' => 6 , // worker进程数 cpu 1-4 'max_request' => 10000, ]); //监听连接进入事件 /** * $fd 客户端连接的唯一标示 * $reactor_id 线程id */ $serv->on('connect', function ($serv, $fd, $reactor_id) { echo "Client: {$reactor_id} - {$fd}-Connect.\n"; }); //监听数据接收事件 $serv->on('receive', function ($serv, $fd, $reactor_id, $data) { $serv->send($fd, "Server: {$reactor_id} - {$fd}".$data); }); //监听连接关闭事件 $serv->on('close', function ($serv, $fd) { echo "Client: Close.\n"; }); //启动服务器 $serv->start();ws.php<?php class Ws { CONST HOST = "0.0.0.0"; CONST PORT = 8812; public $ws = null; public function __construct() { $this->ws = new swoole_websocket_server("0.0.0.0", 8812); $this->ws->set( [ 'worker_num' => 2, 'task_worker_num' => 2, ] ); $this->ws->on("open", [$this, 'onOpen']); $this->ws->on("message", [$this, 'onMessage']); $this->ws->on("task", [$this, 'onTask']); $this->ws->on("finish", [$this, 'onFinish']); $this->ws->on("close", [$this, 'onClose']); $this->ws->start(); } /** * 监听ws连接事件 * @param $ws * @param $request */ public function onOpen($ws, $request) { var_dump($request->fd); if($request->fd == 1) { // 每2秒执行 swoole_timer_tick(2000, function($timer_id){ echo "2s: timerId:{$timer_id}\n"; }); } } /** * 监听ws消息事件 * @param $ws * @param $frame */ public function onMessage($ws, $frame) { echo "ser-push-message:{$frame->data}\n"; // todo 10s $data = [ 'task' => 1, 'fd' => $frame->fd, ]; //$ws->task($data); swoole_timer_after(5000, function() use($ws, $frame) { echo "5s-after\n"; $ws->push($frame->fd, "server-time-after:"); }); $ws->push($frame->fd, "server-push:".date("Y-m-d H:i:s")); } /** * @param $serv * @param $taskId * @param $workerId * @param $data */ public function onTask($serv, $taskId, $workerId, $data) { print_r($data); // 耗时场景 10s sleep(10); return "on task finish"; // 告诉worker } /** * @param $serv * @param $taskId * @param $data */ public function onFinish($serv, $taskId, $data) { echo "taskId:{$taskId}\n"; echo "finish-data-sucess:{$data}\n"; } /** * close * @param $ws * @param $fd */ public function onClose($ws, $fd) { echo "clientid:{$fd}\n"; } } $obj = new Ws();ws_server.php<?php $server = new swoole_websocket_server("0.0.0.0", 8812); //$server->set([]); $server->set( [ 'enable_static_handler' => true, 'document_root' => "/home/work/hdtocs/swoole_mooc/data", ] ); //监听websocket连接打开事件 $server->on('open', 'onOpen'); function onOpen($server, $request) { print_r($request->fd); } // 监听ws消息事件 $server->on('message', function (swoole_websocket_server $server, $frame) { echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n"; $server->push($frame->fd, "singwa-push-secesss"); }); $server->on('close', function ($ser, $fd) { echo "client {$fd} closed\n"; }); $server->start();目录 └── client ├── tcp_client.php └── coroutine ├── redis.php └── io ├── 1.log ├── 1.txt ├── mysql.php ├── read.php ├── redis.php ├── write.php └── memory ├── table.php └── process ├── curl.php ├── process.php └── server ├── access.log ├── http_server.php ├── tcp.php ├── ws.php ├── ws_server.php
class Img { private static $instance; /** * 初始化 * @access public * @param array $options 参数 * @return Img */ public static function instance($options = []) { if (is_null(self::$instance)) { self::$instance = new static($options); } return self::$instance; } /** * 调整 头像大小 * @param $imgSource * @param $maxwidth * @param $maxheight * @return resource */ public function resizeImage(&$imgSource, $maxwidth, $maxheight) { $width = imagesx($imgSource); $height = imagesx($imgSource); $bgSource = imagecreatetruecolor($maxwidth, $maxheight); // 创建一个真彩色图像 我把它理解为创建了一个画布 imagecopyresampled($bgSource, $imgSource, 0, 0, 0, 0, $maxwidth, $maxheight, $width, $height); return $bgSource; } /** * 获取图片资源 * @param $imgPath * return resource images */ public function getSource($imgPath) { $ext = pathinfo($imgPath); switch ($ext['extension']) { case 'jpg': $imgSource = imagecreatefromjpeg($imgPath); break; case 'png': $imgSource = imagecreatefrompng($imgPath); break; case 'jpeg': $imgSource = imagecreatefromjpeg($imgPath); break; case 'wbmp': $imgSource = imagecreatefromwbmp($imgPath); break; case 'gif': $imgSource = imagecreatefromgif($imgPath); break; } return $imgSource; } /** * 处理成圆图片,如果图片不是正方形就取最小边的圆半径,从左边开始剪切成圆形 * * @param $imgSource * @param null $width * @param null $height * @param bool $return * @return resource */ public function circular(&$imgSource) { $w = imagesx($imgSource); $h = imagesy($imgSource); $w = min($w, $h); $h = $w; $returnSource = imagecreatetruecolor($w, $h); //这一句一定要有 imagesavealpha($returnSource, true); //拾取一个完全透明的颜色,最后一个参数127为全透明 $bg = imagecolorallocatealpha($returnSource, 255, 255, 255, 127); imagefill($returnSource, 0, 0, $bg); $r = $w / 2; //圆半径 for ($x = 0; $x < $w; $x++) { for ($y = 0; $y < $h; $y++) { $rgbColor = imagecolorat($imgSource, $x, $y); if (((($x - $r) * ($x - $r) + ($y - $r) * ($y - $r)) < ($r * $r))) { imagesetpixel($returnSource, $x, $y, $rgbColor); } } } return $returnSource; } /** * 合并图片. * @param string $bgSource * @param string $smSource * @param int $imgX * @param int $imgY * @return string */ public function mergeImages(&$bgSource = '', &$smSource = '', $imgX = 0, $imgY = 0) { // 获取点图片的宽高 $point_w = imagesx($smSource); $point_h = imagesy($smSource); // 重点：png透明用这个函数 imagecopy($bgSource, $smSource, $imgX, $imgY, 0, 0, $point_w, $point_h); return $bgSource; } /** * 合并文字 * @param string $bgSource * @param int $fontInt * @param $text * @param int $fontSize * @param string $textX * @param int $textY * @return string */ public function mergeText(&$bgSource = '', $fontInt = 1, $text, $fontSize = 40, $textX = '', $textY = 0) { // 背景宽度 $width = imagesx($bgSource); $font = ROOT_PATH . ($fontInt == 1 ? 'public/assets/fonts/AktivGroteskCNBold.ttf' : 'public/assets/fonts/Dengb.ttf');//字体 $fontBox = imagettfbbox($fontSize, 0, $font, $text);//文字水平居中实质 $white = imagecolorallocate($bgSource, 51, 51, 51);//字体颜色 RGB $textX = $textX ?: ceil(($width - $fontBox[2]) / 2); $circleSize = 0; //旋转角度 imagefttext($bgSource, $fontSize, $circleSize, $textX, $textY, $white, $font, $text); return $bgSource; } }
数组随机取function array_random ($number, $ary) { // @number 需要取多少个元素 // @ary 原数组 $final = []; while (count($final) < $number) { $element = $ary[array_rand($ary, 1)]; in_array($element, $final) OR $final[] = $element; } return $final; } // $number 要小于 数组长度 $number < count($ary); $final = array_random(3, $ary); $ary = ['php', 'java', 'python', 'c#', 'c++', 'ruby']; print_r($final);
1.Connection$redis = new Redis(); $redis->connect('127.0.0.1',6379,1);//短链接，本地host，端口为6379，超过1秒放弃链接 $redis->open('127.0.0.1',6379,1);//短链接(同上) $redis->pconnect('127.0.0.1',6379,1);//长链接，本地host，端口为6379，超过1秒放弃链接 $redis->popen('127.0.0.1',6379,1);//长链接(同上) $redis->auth('password');//登录验证密码，返回【true | false】 $redis->select(0);//选择redis库,0~15 共16个库 $redis->close();//释放资源 $redis->ping(); //检查是否还再链接,[+pong] $redis->ttl('key');//查看失效时间[-1 | timestamps] $redis->persist('key');//移除失效时间[ 1 | 0] $redis->sort('key',[$array]);//返回或保存给定列表、集合、有序集合key中经过排序的元素，$array为参数limit等！【配合$array很强大】 [array|false]2.共性的运算归类$redis->expire('key',10);//设置失效时间[true | false] $redis->move('key',15);//把当前库中的key移动到15库中[0|1] //string $redis->strlen('key');//获取当前key的长度 $redis->append('key','string');//把string追加到key现有的value中[追加后的个数] $redis->incr('key');//自增1，如不存在key,赋值为1(只对整数有效,存储以10进制64位，redis中为str)[new_num | false] $redis->incrby('key',$num);//自增$num,不存在为赋值,值需为整数[new_num | false] $redis->decr('key');//自减1，[new_num | false] $redis->decrby('key',$num);//自减$num，[ new_num | false] $redis->setex('key',10,'value');//key=value，有效期为10秒[true] //list $redis->llen('key');//返回列表key的长度,不存在key返回0， [ len | 0] //set $redis->scard('key');//返回集合key的基数(集合中元素的数量)。[num | 0] $redis->sMove('key1', 'key2', 'member');//移动，将member元素从key1集合移动到key2集合。[1 | 0] //Zset $redis->zcard('key');//返回集合key的基数(集合中元素的数量)。[num | 0] $redis->zcount('key',0,-1);//返回有序集key中，score值在min和max之间(默认包括score值等于min或max)的成员。[num | 0] //hash $redis->hexists('key','field');//查看hash中是否存在field,[1 | 0] $redis->hincrby('key','field',$int_num);//为哈希表key中的域field的值加上量(+|-)num,[new_num | false] $redis->hlen('key');//返回哈希表key中域的数量。[ num | 0]3.Server$redis->dbSize();//返回当前库中的key的个数 $redis->flushAll();//清空整个redis[总true] $redis->flushDB();//清空当前redis库[总true] $redis->save();//同步??把数据存储到磁盘-dump.rdb[true] $redis->bgsave();//异步？？把数据存储到磁盘-dump.rdb[true] $redis->info();//查询当前redis的状态 [verson:2.4.5....] $redis->lastSave();//上次存储时间key的时间[timestamp] $redis->watch('key','keyn');//监视一个(或多个) key ，如果在事务执行之前这个(或这些) key 被其他命令所改动，那么事务将被打断 [true] $redis->unwatch('key','keyn');//取消监视一个(或多个) key [true] $redis->multi(Redis::MULTI);//开启事务，事务块内的多条命令会按照先后顺序被放进一个队列当中，最后由 EXEC 命令在一个原子时间内执行。 $redis->multi(Redis::PIPELINE);//开启管道，事务块内的多条命令会按照先后顺序被放进一个队列当中，最后由 EXEC 命令在一个原子时间内执行。 $redis->exec();//执行所有事务块内的命令，；【事务块内所有命令的返回值，按命令执行的先后顺序排列，当操作被打断时，返回空值 false】4.String，键值对，创建更新同操作$redis->setOption(Redis::OPT_PREFIX,'hf_');//设置表前缀为hf_ $redis->set('key',1);//设置key=aa value=1 [true] $redis->mset($arr);//设置一个或多个键值[true] $redis->setnx('key','value');//key=value,key存在返回false[|true] $redis->get('key');//获取key [value] $redis->mget($arr);//(string|arr),返回所查询键的值 $redis->del($key_arr);//(string|arr)删除key，支持数组批量删除【返回删除个数】 $redis->delete($key_str,$key2,$key3);//删除keys,[del_num] $redis->getset('old_key','new_value');//先获得key的值，然后重新赋值,[old_value | false]5.List栈的结构,注意表头表尾,创建更新分开操作$redis->lpush('key','value');//增，只能将一个值value插入到列表key的表头，不存在就创建 [列表的长度 |false] $redis->rpush('key','value');//增，只能将一个值value插入到列表key的表尾 [列表的长度 |false] $redis->lInsert('key', Redis::AFTER, 'value', 'new_value');//增，将值value插入到列表key当中，位于值value之前或之后。[new_len | false] $redis->lpushx('key','value');//增，只能将一个值value插入到列表key的表头，不存在不创建 [列表的长度 |false] $redis->rpushx('key','value');//增，只能将一个值value插入到列表key的表尾，不存在不创建 [列表的长度 |false] $redis->lpop('key');//删，移除并返回列表key的头元素,[被删元素 | false] $redis->rpop('key');//删，移除并返回列表key的尾元素,[被删元素 | false] $redis->lrem('key','value',0);//删，根据参数count的值，移除列表中与参数value相等的元素count=(0|-n表头向尾|+n表尾向头移除n个value) [被移除的数量 | 0] $redis->ltrim('key',start,end);//删，列表修剪，保留(start,end)之间的值 [true|false] $redis->lset('key',index,'new_v');//改，从表头数，将列表key下标为第index的元素的值为new_v, [true | false] $redis->lindex('key',index);//查，返回列表key中，下标为index的元素[value|false] $redis->lrange('key',0,-1);//查，(start,stop|0,-1)返回列表key中指定区间内的元素，区间以偏移量start和stop指定。[array|false]6.Set，没有重复的member，创建更新同操作$redis->sadd('key','value1','value2','valuen');//增，改，将一个或多个member元素加入到集合key当中，已经存在于集合的member元素将被忽略。[insert_num] $redis->srem('key','value1','value2','valuen');//删，移除集合key中的一个或多个member元素，不存在的member元素会被忽略 [del_num | false] $redis->smembers('key');//查，返回集合key中的所有成员 [array | ''] $redis->sismember('key','member');//判断member元素是否是集合key的成员 [1 | 0] $redis->spop('key');//删，移除并返回集合中的一个随机元素 [member | false] $redis->srandmember('key');//查，返回集合中的一个随机元素 [member | false] $redis->sinter('key1','key2','keyn');//查，返回所有给定集合的交集 [array | false] $redis->sunion('key1','key2','keyn');//查，返回所有给定集合的并集 [array | false] $redis->sdiff('key1','key2','keyn');//查，返回所有给定集合的差集 [array | false]7.Zset，没有重复的member，有排序顺序,创建更新同操作$redis->zAdd('key',$score1,$member1,$scoreN,$memberN);//增，改，将一个或多个member元素及其score值加入到有序集key当中。[num | 0] $redis->zrem('key','member1','membern');//删，移除有序集key中的一个或多个成员，不存在的成员将被忽略。[del_num | 0] $redis->zscore('key','member');//查,通过值反拿权 [num | null] $redis->zrange('key',$start,$stop);//查，通过(score从小到大)【排序名次范围】拿member值，返回有序集key中，【指定区间内】的成员 [array | null] $redis->zrevrange('key',$start,$stop);//查，通过(score从大到小)【排序名次范围】拿member值，返回有序集key中，【指定区间内】的成员 [array | null] $redis->zrangebyscore('key',$min,$max[,$config]);//查，通过scroe权范围拿member值，返回有序集key中，指定区间内的(从小到大排)成员[array | null] $redis->zrevrangebyscore('key',$max,$min[,$config]);//查，通过scroe权范围拿member值，返回有序集key中，指定区间内的(从大到小排)成员[array | null] $redis->zrank('key','member');//查，通过member值查(score从小到大)排名结果中的【member排序名次】[order | null] $redis->zrevrank('key','member');//查，通过member值查(score从大到小)排名结果中的【member排序名次】[order | null] $redis->ZINTERSTORE();//交集 $redis->ZUNIONSTORE();//差集8.Hash，表结构，创建更新同操作$redis->hset('key','field','value');//增，改，将哈希表key中的域field的值设为value,不存在创建,存在就覆盖【1 | 0】 $redis->hget('key','field');//查，取值【value|false】 $arr = array('one'=>1,2,3);$arr2 = array('one',0,1); $redis->hmset('key',$arr);//增，改，设置多值$arr为(索引|关联)数组,$arr[key]=field, [ true ] $redis->hmget('key',$arr2);//查，获取指定下标的field，[$arr | false] $redis->hgetall('key');//查，返回哈希表key中的所有域和值。[当key不存在时，返回一个空表] $redis->hkeys('key');//查，返回哈希表key中的所有域。[当key不存在时，返回一个空表] $redis->hvals('key');//查，返回哈希表key中的所有值。[当key不存在时，返回一个空表] $redis->hdel('key',$arr2);//删，删除指定下标的field,不存在的域将被忽略,[num | false]
MongoDB的PHP驱动提供了一些核心类来操作MongoDB，总的来说MongoDB命令行中有的功能，它都可以实现，而且参数的格式基本相似。PHP7以前的版本和PHP7之后的版本对MongoDB的操作有所不同，本文主要以PHP7以前版本为例讲解PHP对MongoDB的各种操作，最后再简单说明一下PHP7以后版本对MongoDB的操作。PHP5一、数据插入//insert() //参数1：一个数组或对象 //参数2：扩展选项 // fsync：默认为false，若为true则mongo在确认数据插入成功之前将会强制把数据写入硬盘 // j：默认为false，若为true则mongo在确认数据插入成功之前将会强制把数据写入日志 // w：默认为1，写操作会被（主）服务器确认，若为0则将不会得到确认，使用复制集时设置为n用于确保主服务器将数据修改成功复制到n个节点后再确认 // wtimeout：默认为10000（毫秒），用于指定服务器等待接收确认的时间 // timeout：指定客户端需要等待服务器响应的超时时间（毫秒） $mongo = new MongoClient('mongodb://localhost:27017'); $db = $mongo->mf;//选择数据库 $collection = $db->friend;//选择文档集合 $doc = [//定义一个文档，即一个数组 'First Name' => 'Jet', 'Last Name' => 'Wu', 'Age' => 26, 'Phone' => '110', 'Address' => [ 'Country' => 'China', 'City' => 'Shen Zhen' ], 'E-Mail' => [ '123456@qq.com', '666666@sina.com', '8888888@qq.com', '77887788@qq.com' ] ]; $res = $collection->insert($doc);//向集合中插入一个文档 echo ' '; print_r($res);//$res['ok']=1表示插入成功二、数据查询查询单个文档：//findOne() //参数1：搜索条件 //参数2：指定返回字段，array('fieldname' => true, 'fieldname2' => true)。_id字段总会返回，除非在第二个参数显式加入'_id'=>false。不设置则返回所有字段 $mongo = new MongoClient('mongodb://localhost:27017'); $db = $mongo->mf; $collection = $db->friend; $one = $collection->findOne(['First Name' => 'Jet']); echo ' '; print_r($one);//返回一个数组，查不到数据则返回NULL查询多个文档：//find() //参数1：搜索条件 //参数2：指定返回字段，array('fieldname' => true, 'fieldname2' => true)。_id字段总会返回，除非显式设置为false不返回。不设置则返回所有字段 $mongo = new MongoClient('mongodb://localhost:27017'); $db = $mongo->mf; $collection = $db->friend; $cursor = $collection->find(['Address.Country' => 'China']);//使用点操作符查找数组元素 echo ' '; while($doc = $cursor->getNext()) {//循环读取每个匹配的文档 print_r($doc); }使用各种条件操作符定义查询：//mongodb分别使用$lt、$lte、$eq、$gte、$gt、$ne表示<、<=、=、>=、>、<>，用于整数字段查询 $mongo = new MongoClient('mongodb://localhost:27017'); $db = $mongo->mf; $collection = $db->friend; $cursor = $collection->find(['Age' => ['$gt' => 30]]); echo ' '; while($doc = $cursor->getNext()) { print_r($doc); }//查询某个字段的所有不重复的值 $res = $collection->distinct('Age');//$in：匹配多个值中任意一个 $cursor = $collection->find(['Address.Country' => ['$in' => ['China', 'USA']]]); //$all：匹配多个值中所有值（用于数组字段查询） $cursor = $collection->find(['E-Mail' => ['$all' => ['123456@qq.com', '77887788@qq.com']]]); //$or：或查询 $cursor = $collection->find(['$or' => [['First Name' => 'Jet'], ['Address.Country' => 'USA']]]); //$slice：获取数组字段中指定数目的元素，位于find()函数第二个参数中 $cursor = $collection->find(['First Name' => 'Jet'], ['E-Mail' => ['$slice' => 2]]);//只返回前两个email $cursor = $collection->find(['First Name' => 'Jet'], ['E-Mail' => ['$slice' => -2]]);//只返回最后两个email $cursor = $collection->find(['First Name' => 'Jet'], ['E-Mail' => ['$slice' => [1, 2]]]);//忽略第一个，返回接下来两个 //$exists：根据某个字段是否有设置值进行查询 $cursor = $collection->find(['Hobby' => ['$exists' => false]]);//查找Hobby字段未设置值的文档 //正则表达式查询 $cursor = $collection->find(['First Name' => new MongoRegex('/^Je/i')]);//查找First Name字段以Je开头的文档，忽略大小写差异使用MongoCursor类提供的其他函数：//排序：1升序，-1降序 $cursor->sort(['Age' => 1]); //忽略前n个匹配的文档 $cursor->skip(1); //只返回前n个匹配的文档（limit()与skip()结合使用可实现数据分页功能） $cursor->limit(1); //匹配文档的总数 $cursor->count(); //指定查询索引 $cursor->hint(['Last Name' => -1]);//若索引不存在则会报错聚集查询：对数据进行分组统计//聚合查询：对数据进行分组统计 $mongo = new MongoClient('mongodb://localhost:27017'); $db = $mongo->mf; $collection = $db->friend; $res = $collection->aggregate([ '$group' => [ '_id' => '$Address.Country',//分组字段，注意要加上“$”，这里是根据数组字段某个元素值进行分组 'total' => ['$sum' => 1],//求总和，表示每匹配一个文档总和就加1 'maxAge' => ['$max' => '$Age'],//分组中Age字段最大值 'minAge' => ['$min' => '$Age']//分组中Age字段最小值 ] ]); echo ' '; print_r($res);//返回一个数组，$ret['result']为数组，存放统计结果 //存在其它操作的聚合查询：多个操作之间执行先后顺序取决于它们位置的先后顺序 //聚合查询中的所有操作，包括'$group'在内，都是可选的。 $mongo = new MongoClient('mongodb://localhost:27017'); $db = $mongo->mf; $collection = $db->friend; $res = $collection->aggregate([ [//过滤条件：只对符合条件的原始文档进行聚合运算，若是放在'$group'之后则是只返回符合条件的结果文档 '$match' => ['Age' => ['$gt' => 30]] ], [//指定分组字段、统计字段 '$group' => [ '_id' => '$Address.Country', 'totalAge' => ['$sum' => '$Age']//计算各个分组Age字段总和 ] ], //以下操作若是放在'$group'之前则在聚合前作用于原始文档，若放在'$group'之后则在聚合后作用于结果文档 ['$unwind' => '$E-Mail'],//将包含有某个数组类型字段的文档拆分成多个文档，每个文档的同名字段的值为数组中的一个值。 ['$project' => ['myAge' => '$Age', 'First Name' => '$First Name']],//指定返回字段，可以对字段进行重命名，格式：返回字段名 => $原来字段名 ['$skip' => 2],//跳过指定数量的文档 ['$limit' => 2],//只返回指定数量的文档 ['$sort' => ['totalAge' => 1]]//排序 ]); echo ' '; print_r($res);三、数据修改//update() //参数1：更新条件，指定更新的目标对象。 //参数2：指定用于更新匹配记录的对象。 //参数3：扩展选项组。 // upsert：若设置为true，当没有匹配文档的时候会创建一个新的文档。 // multiple：默认为false，若设置为true，匹配文档将全部被更新。 // fsync：若设置为true，w参数将被覆盖为0，数据将在更新结果返回前同步到磁盘。 // w：默认为1；若设置为0，更新操作将不会得到确认；使用复制集时可设置为n，确保主服务器在将修改复制到n个节点后才确认该更新操作 // j：默认为false，若设置为true，数据将在更新结果返回之前写入到日志中。 // wtimeout：默认为10000（毫秒），用于指定服务器等待接收确认的时间 // timeout：指定客户端需要等待服务器响应的超时时间（毫秒） //注意：若不使用任何修改操作符，则匹配文档将直接被整个替换为参数2指定的对象。 //$inc：增加特定键的值，若字段不存在则新建字段并赋值 $mongo = new MongoClient('mongodb://localhost:27017'); $db = $mongo->mf; $collection = $db->friend; $res = $collection->update(['First Name' => 'Jet'], ['$inc' => ['Age' => 2]]); echo ' '; print_r($res);//$res['ok']=1表示修改成功，$res['nModified']表示修改的文档数量 //$set：重置特定键的值，若字段不存在则新建字段并赋值 $res = $collection->update(['First Name' => 'Jet'], ['$set' => ['Hobby' => 'pingpong']]); //$unset：删除字段 $res = $collection->update(['First Name' => 'Jet'], ['$unset' => ['Hobby' => 1]]); //$rename：重命名字段，若字段不存在则不进行任何操作 $res = $collection->update(['First Name' => 'Jet'], ['$rename' => ['Hobby' => 'hobby', 'Age' => 'age']]); //注意：如果文档中已经使用了指定名称的字段，则该字段将会被删除，然后再进行重命名操作。 //$setOnInsert：设置了upsert为true，并且发生了插入操作的时候，将某个字段设置为特定的 $res = $collection->update(['First Name' => 'jet'], ['$setOnInsert' => ['lang' => 'English']], ['upsert' => true]); //$push：向指定字段添加一个值（作用于数组字段），若字段不存在会先创建字段，若字段值不是数组会报错 $res = $collection->update(['First Name' => 'Jet'], ['$push' => ['E-Mail' => '123123@qq.com']]); //$push：向指定字段添加多个值（作用于数组字段），若字段不存在会先创建字段，若字段值不是数组会报错 $res = $collection->update(['First Name' => 'Jet'], ['$pushAll' => ['E-Mail' => ['666@qq.com', '8888888@qq.com']]]); //使用$push和$each向某个字段添加多个值（作用于数组字段），若字段不存在会先创建字段，若字段值不是数组会报错 $res = $collection->update(['First Name' => 'Jet'], ['$push' => ['E-Mail' => ['$each' => ['123123@qq.com', '666@qq.com']]]]); //$addToSet：将数据添加到数组中（只在目标数组没有该数据的时候才将数据添加到数组中） $res = $collection->update(['First Name' => 'Jet'], ['$addToSet' => ['E-Mail' => '123123@qq.com']]); $res = $collection->update(['First Name' => 'Jet'], ['$addToSet' => ['E-Mail' => ['$each' => ['123123@qq.com', '666@qq.com']]]]); //$pop：从数组中删除一个元素，-1表示删除第一个元素，1表示删除最后一个元素（其实负数都删除第一个元素，0或正数都删除最后一个元素） $res = $collection->update(['First Name' => 'Jet'], ['$pop' => ['E-Mail' => 1]]); //$pull：删除数组中所有指定值 $res = $collection->update(['First Name' => 'Jet'], ['$pull' => ['E-Mail' => '123123@qq.com']]); //$pullAll：删除数组中多个元素的所有值 $res = $collection->update(['First Name' => 'Jet'], ['$pullAll' => ['E-Mail' => ['123123@qq.com', '666@qq.com']]]); //save() //参数1：希望保存的信息数组 //参数2：扩展选项 // fsync：若设置为true，w参数将被覆盖为0，数据将在更新结果返回前同步到磁盘。 // w：默认为1；若设置为0，更新操作将不会得到确认；使用复制集时可设置为n，确保主服务器在将修改复制到n个节点后才确认该更新操作 // j：默认为false，若设置为true，数据将在更新结果返回之前写入到日志中。 // wtimeout：默认为10000（毫秒），用于指定服务器等待接收确认的时间 // timeout：指定客户端需要等待服务器响应的超时时间（毫秒） //注意：若已存在则更新，若不存在则插入；更新时使用参数1指定的信息数组替换整个文档。 //若想更新则应该在参数1中指定_id键的值。 $mongo = new MongoClient('mongodb://localhost:27017'); $db = $mongo->mf; $collection = $db->friend; $doc = [//定义一个文档，即一个数组 'First Name' => 'Jet', 'Last Name' => 'Wu', 'Age' => 26, 'Phone' => '110', 'Address' => [ 'Country' => 'China', 'City' => 'Shen Zhen' ], 'E-Mail' => [ '123456@qq.com', '666666@sina.com', '8888888@qq.com', '77887788@qq.com' ] ]; $res = $collection->save($doc); echo ' '; print_r($res);//$res['ok']=1表示操作成功，$res['updatedExisting']=1表示更新，$res['upserted']=1表示插入 //findAndModify() //参数1：指定查询条件 //参数2：指定用于更新文档的信息 //参数3：可选，指定希望返回的字段 //参数4：扩展选项 // sort：以特定顺序对匹配文档进行排序 // remove：若设置为true，第一个匹配文档将被删除 // update：若设置为true，将在被选择的文档上执行更新操作 // new：默认为false，若设置为true则返回更新后的文档，否则返回更新前的文档 // upsert：若设置为true，没有找到匹配文档的时候将插入一个新的文档 $mongo = new MongoClient('mongodb://localhost:27017'); $db = $mongo->mf; $collection = $db->friend; $res = $collection->findAndModify(['First Name' => 'Jet'], ['$push' => ['E-Mail' => '111@qq.com']]); echo ' '; print_r($res);四、数据删除//remove() //参数1：查询条件 //参数2：扩展选项 // justOne：若设置为true，则最多只有一个匹配的文档将被删除 // fsync：若设置为true，w参数将被覆盖为0，数据将在更新结果返回前同步到磁盘。 // w：默认为1；若设置为0，更新操作将不会得到确认；使用复制集时可设置为n，确保主服务器在将修改复制到n个节点后才确认该更新操作 // j：默认为false，若设置为true，数据将在更新结果返回之前写入到日志中。 // wtimeout：默认为10000（毫秒），用于指定服务器等待接收确认的时间 // timeout：指定客户端需要等待服务器响应的超时时间（毫秒） $mongo = new MongoClient('mongodb://localhost:27017'); $db = $mongo->mf; $collection = $db->friend; $res = $collection->remove(['First Name' => 'jet']); echo ' '; print_r($res);//$res['n']表示删除了几个文档以上是PHP7以前版本的MongoDB操作，下面简单介绍PHP7以后版本的操作。PHP7一、数据插入$manager = new MongoDB\Driver\Manager('mongodb://localhost:27017'); $bulk = new MongoDB\Driver\BulkWrite; $bulk->insert(['name' => 'JetWu5', 'age' => 26]); $bulk->insert(['name' => 'JetWu6', 'age' => 26]); $writeConcern = new MongoDB\Driver\WriteConcern(MongoDB\Driver\WriteConcern::MAJORITY, 1000);//可选，修改确认 $res = $manager->executeBulkWrite('wjt.friend', $bulk, $writeConcern); echo ' '; print_r($res);二、数据查询$manager = new MongoDB\Driver\Manager('mongodb://localhost:27017'); $query = new MongoDB\Driver\Query(['age' => 24], ['sort' => ['age' => 1]]); $cursor = $manager->executeQuery('wjt.friend', $query); $data = []; foreach($cursor as $doc) { $data[] = $doc; } echo ' '; print_r($data);三、数据修改$manager = new MongoDB\Driver\Manager('mongodb://localhost:27017'); $bulk = new MongoDB\Driver\BulkWrite; $bulk->update( ['name' => 'JetWu5'], ['$set' => ['age' => 30, 'promise' => 'always smile!']] ); $writeConcern = new MongoDB\Driver\WriteConcern(MongoDB\Driver\WriteConcern::MAJORITY, 1000);//可选，修改确认 $res = $manager->executeBulkWrite('wjt.friend', $bulk, $writeConcern); echo ' '; print_r($res);四、数据删除$manager = new MongoDB\Driver\Manager('mongodb://localhost:27017'); $bulk = new MongoDB\Driver\BulkWrite; $bulk->delete(['name' => 'JetWu3']); $bulk->delete(['name' => 'JetWu4']); $writeConcern = new MongoDB\Driver\WriteConcern(MongoDB\Driver\WriteConcern::MAJORITY, 1000);//可选，修改确认 $res = $manager->executeBulkWrite('wjt.friend', $bulk, $writeConcern); echo ' '; print_r($res);
nginx配置location / { #以下跨域设置 if ( $request_method = OPTIONS ) { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD; add_header Access-Control-Allow-Headers Origin,X-Requested-With,Content-Type,Accept,Authorization; return 200; } #以上跨域设置 try_files $uri $uri/ /index.php?$args; }php配置header("Access-Control-Allow-Origin: *"); // 允许所有来源访问 header("Access-Control-Allow-Methods: GET, POST,PUT,DELETE,HEAD,OPTIONS,TRACE,CONNECT"); // 允许访问的方式 header("Access-Control-Allow-Headers: Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With");// 允许自定义的头部参数HTTP/1.1协议中共定义了八种方法（有时也叫“动作”）来表明Request-URI指定的资源的不同操作方式：OPTIONS返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送'*'的请求来测试服务器的功能性。HEAD向服务器索要与GET请求相一致的响应，只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下，就可以获取包含在响应消息头中的元信息。GET向特定的资源发出请求。注意：GET方法不应当被用于产生“副作用”的操作中。POST向指定资源提交数据进行处理请求（例如提交表单或者上传文件）。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。PUT向指定资源位置上传其最新内容。DELETE请求服务器删除Request-URI所标识的资源。TRACE回显服务器收到的请求，主要用于测试或诊断。CONNECTHTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。