├── .gitignore ├── function ├── functions.php └── shortcut.php ├── engine ├── base │ ├── Openly.php │ ├── QuietException.php │ ├── ResponseException.php │ ├── TraitModel.php │ ├── SwooleLock.php │ ├── BaseException.php │ └── Lock.php ├── rpc │ ├── RpcMatrix.php │ └── RpcHost.php ├── model │ ├── validator │ │ ├── TypeFilter.php │ │ ├── TypeAssignment.php │ │ ├── TypeEnding.php │ │ ├── assignment │ │ │ └── DefaultValidator.php │ │ ├── checker │ │ │ ├── EmailValidator.php │ │ │ ├── UrlValidator.php │ │ │ ├── IpValidator.php │ │ │ ├── BitsetValidator.php │ │ │ ├── RegularValidator.php │ │ │ ├── SetValidator.php │ │ │ ├── StringValidator.php │ │ │ └── NumberValidator.php │ │ ├── TypeChecker.php │ │ ├── filter │ │ │ └── FilterValidator.php │ │ ├── ending │ │ │ ├── RequiredValidator.php │ │ │ └── UniqueValidator.php │ │ └── ValidatorProperty.php │ └── ModelException.php ├── db │ ├── entity │ │ ├── SchemaAbstract.php │ │ ├── SchemaException.php │ │ ├── DbField.php │ │ └── FieldType.php │ ├── query │ │ └── builder │ │ │ ├── schema │ │ │ ├── PartitionSchema.php │ │ │ ├── LimitSchema.php │ │ │ ├── WithSchema.php │ │ │ ├── DeleteSchema.php │ │ │ ├── UnionSchema.php │ │ │ ├── HavingSchema.php │ │ │ ├── SelectModifierSchema.php │ │ │ ├── GroupSchema.php │ │ │ ├── UpdateSchema.php │ │ │ ├── InsertSelectSchema.php │ │ │ ├── WindowSchema.php │ │ │ ├── FrameSchema.php │ │ │ ├── OrderSchema.php │ │ │ ├── TableSchema.php │ │ │ ├── SelectSchema.php │ │ │ └── JoinSchema.php │ │ │ ├── statement │ │ │ ├── InsertSelectStatement.php │ │ │ └── InsertStatement.php │ │ │ └── RawBuilder.php │ ├── proxy │ │ ├── TransactionException.php │ │ └── SimpleTransaction.php │ ├── Shortcut.php │ ├── connector │ │ ├── DbEachIterator.php │ │ ├── DbPoolProductionConfig.php │ │ └── DbPool.php │ └── active │ │ ├── DceExtendColumn.php │ │ ├── DceModifyRecord.php │ │ ├── ActiveException.php │ │ ├── DceExternalRecord.php │ │ └── CacheEngine.php ├── loader │ ├── Decorator.php │ └── attr │ │ ├── Constructor.php │ │ ├── Singleton.php │ │ ├── Sington.php │ │ └── SingMatrix.php ├── i18n │ ├── TextMapping.php │ └── Locale.php ├── cache │ ├── CacheException.php │ ├── CacheClearable.php │ └── engine │ │ └── RedisCache.php ├── project │ ├── ProjectException.php │ ├── render │ │ ├── RawRenderer.php │ │ ├── RenderException.php │ │ ├── JsonRenderer.php │ │ ├── JsonpRenderer.php │ │ ├── template │ │ │ ├── status.php │ │ │ └── exception.php │ │ └── XmlRenderer.php │ ├── node │ │ ├── NodeException.php │ │ └── NodeArgument.php │ ├── request │ │ ├── RouterException.php │ │ ├── RequestException.php │ │ ├── RawRequest.php │ │ ├── RequestManager.php │ │ └── Pageable.php │ ├── session │ │ ├── SessionException.php │ │ ├── CookieCgi.php │ │ └── Cookie.php │ ├── Project.php │ └── ProjectManager.php ├── sharding │ ├── middleware │ │ ├── Middleware.php │ │ ├── data_processor │ │ │ ├── DataProcessor.php │ │ │ ├── DbWriteProcessor.php │ │ │ └── DbReadEachIterator.php │ │ ├── DirectiveParser.php │ │ └── MiddlewareException.php │ ├── id_generator │ │ ├── client │ │ │ ├── IdgClientIncrement.php │ │ │ ├── IdgClientRpc.php │ │ │ └── IdgClientTime.php │ │ ├── bridge │ │ │ ├── IdgRequestRpc.php │ │ │ ├── IdgRequestInterface.php │ │ │ ├── IdgStorageRedis.php │ │ │ ├── IdgRequestLocal.php │ │ │ ├── IdgStorage.php │ │ │ ├── IdgStorageFile.php │ │ │ └── IdgBatch.php │ │ ├── server │ │ │ ├── IdgServerRpc.php │ │ │ └── IdgServerProducer.php │ │ └── IdgException.php │ └── parser │ │ ├── StatementParser.php │ │ ├── mysql │ │ ├── MysqlCompareExpressionParser.php │ │ ├── MysqlStatementParser.php │ │ ├── list │ │ │ ├── MysqlGroupByParser.php │ │ │ ├── MysqlOrderByParser.php │ │ │ └── MysqlColumnParser.php │ │ ├── statement │ │ │ ├── MysqlColumnItemParser.php │ │ │ ├── MysqlWhenParser.php │ │ │ └── MysqlOrderByConditionParser.php │ │ ├── MysqlListParser.php │ │ └── MysqlValueParser.php │ │ └── StatementParserException.php ├── config │ ├── ConfigLibInterface.php │ └── ConfigException.php ├── storage │ └── redis │ │ ├── RedisProxySimple.php │ │ ├── RedisPoolProductionConfig.php │ │ ├── RedisPool.php │ │ ├── RedisConnector.php │ │ └── RedisProxyPool.php ├── log │ ├── SimpleLogger.php │ ├── FileOutput.php │ ├── MatrixOutput.php │ ├── MatrixLogger.php │ └── HttpOutput.php ├── pool │ ├── ArrayChannel.php │ ├── CoroutineChannel.php │ ├── TcpPoolProductionConfig.php │ ├── PoolException.php │ ├── ChannelAbstract.php │ ├── TcpPool.php │ └── PoolProduct.php └── run.php ├── drunk ├── units.php ├── debug │ ├── output │ │ ├── CliDebug.php │ │ └── LogDebug.php │ └── DebugException.php └── ArrayTree.php ├── project ├── dce │ ├── service │ │ ├── server │ │ │ ├── RpcServerApi.php │ │ │ ├── ConnectionException.php │ │ │ ├── Connection.php │ │ │ └── ServerApi.php │ │ ├── cron │ │ │ ├── CrontabBasic.php │ │ │ ├── CrontabCoroutine.php │ │ │ └── TaskIterator.php │ │ └── extender │ │ │ ├── Extender.php │ │ │ └── ExtenderException.php │ └── controller │ │ ├── ShardingController.php │ │ ├── RpcController.php │ │ ├── UtilityController.php │ │ ├── CrontabController.php │ │ └── DceController.php ├── http │ ├── service │ │ ├── HttpException.php │ │ └── CookieSwoole.php │ ├── config │ │ ├── config.php │ │ └── nodes.php │ └── controller │ │ └── HttpServerController.php ├── tcp │ ├── service │ │ ├── TcpException.php │ │ ├── RawRequestUdp.php │ │ └── RawRequestTcp.php │ ├── config │ │ ├── config.php │ │ └── nodes.php │ └── controller │ │ └── TcpServerController.php └── websocket │ ├── service │ └── WebsocketException.php │ ├── config │ ├── config.php │ └── nodes.php │ └── controller │ └── WebsocketServerController.php ├── LICENSE ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .ignore 3 | .ignore-git 4 | vendor 5 | composer.lock 6 | -------------------------------------------------------------------------------- /function/functions.php: -------------------------------------------------------------------------------- 1 | shardingQuery(); 14 | } 15 | 16 | abstract protected function shardingQuery(): void; 17 | } -------------------------------------------------------------------------------- /engine/sharding/middleware/data_processor/DataProcessor.php: -------------------------------------------------------------------------------- 1 | sourceData[] = $resultData; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/client/IdgClientIncrement.php: -------------------------------------------------------------------------------- 1 | check($this->getValue()); 12 | if ($result instanceof ValidatorException) { 13 | throw $result; 14 | } 15 | return true; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /project/websocket/service/WebsocketException.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'host' => '0.0.0.0', 10 | 'port' => 20460, 11 | 'service' => '\\http\\service\\HttpServer', 12 | ], 13 | 'swoole_http' => [ 14 | 'enable_static_handler' => true, 15 | 'document_root' => APP_WWW, 16 | ], 17 | '#extends' => [ 18 | APP_COMMON . 'config/http.php', 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /engine/base/ResponseException.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'host' => '0.0.0.0', 10 | 'port' => 20461, 11 | 'service' => '\\websocket\\service\\WebsocketServer', 12 | ], 13 | 'swoole_websocket' => [ 14 | // 'enable_static_handler' => true, 15 | // 'document_root' => APP_WWW, 16 | ], 17 | '#extends' => [ // 扩展用户自定义的配置 18 | APP_COMMON . 'config/websocket.php', 19 | ] 20 | ]; 21 | -------------------------------------------------------------------------------- /drunk/debug/output/CliDebug.php: -------------------------------------------------------------------------------- 1 | format($dataFormatted); 16 | echo $content; 17 | $this->logStorage && $this->logStorage->push($this->getPath(), $content); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/server/IdgServerRpc.php: -------------------------------------------------------------------------------- 1 | idGenerator->newServer()->register($tag); 11 | } 12 | 13 | public static function generate(string $tag): IdgBatch { 14 | return Dce::$config->idGenerator->newServer()->generate($tag); 15 | } 16 | } -------------------------------------------------------------------------------- /project/dce/controller/RpcController.php: -------------------------------------------------------------------------------- 1 | implode("\n", $output), 'code' => $code]; 14 | } 15 | 16 | protected static function sleep(): void { 17 | sleep(self::getIntervalSeconds()); 18 | } 19 | } -------------------------------------------------------------------------------- /engine/model/validator/assignment/DefaultValidator.php: -------------------------------------------------------------------------------- 1 | getProperty('default')->value ?? null); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/bridge/IdgRequestInterface.php: -------------------------------------------------------------------------------- 1 | request->cli['--type'] ?? $this->request->cli['-t'] ?? 'file'; 17 | Dce::$cache->{$cacheType}->clear(); 18 | $this->print('缓存清除完毕'); 19 | } 20 | } -------------------------------------------------------------------------------- /engine/project/node/NodeException.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'host' => '0.0.0.0', 10 | 'port' => 20462, 11 | 'mode' => SWOOLE_PROCESS, 12 | 'sock_type' => SWOOLE_SOCK_TCP, 13 | 'service' => '\\tcp\\service\\TcpServer', 14 | 'extra_ports' => [ 15 | ['host' => '0.0.0.0', 'port' => 20463, 'sock_type' => SWOOLE_SOCK_UDP], // 同时监听20463端口的Udp服务 16 | ], 17 | ], 18 | '#extends' => [ // 扩展用户自定义的配置 19 | APP_COMMON . 'config/tcp.php', 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /engine/project/request/RouterException.php: -------------------------------------------------------------------------------- 1 | array; 18 | } 19 | 20 | public function arrayify(bool $withWrap = true): array { 21 | $array = $this->array; 22 | $array['children'] = $this->childrenArrayify(); 23 | return $withWrap ? $array : $array['children']; 24 | } 25 | } -------------------------------------------------------------------------------- /engine/sharding/middleware/data_processor/DbWriteProcessor.php: -------------------------------------------------------------------------------- 1 | sourceData[0]) && throw new MiddlewareException(MiddlewareException::INSERT_FAILED_NO_ID); 14 | return $this->sourceData[0]; 15 | } 16 | 17 | public function queryGetAffectedCount(): int { 18 | return array_sum($this->sourceData); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /project/dce/service/cron/CrontabCoroutine.php: -------------------------------------------------------------------------------- 1 | parent::run($task, $now)); 14 | } 15 | 16 | /** @inheritDoc */ 17 | protected static function exec(string $command): array { 18 | return System::exec($command); 19 | } 20 | 21 | protected static function sleep(): void { 22 | System::sleep(self::getIntervalSeconds()); 23 | } 24 | } -------------------------------------------------------------------------------- /engine/storage/redis/RedisProxySimple.php: -------------------------------------------------------------------------------- 1 | redis = $pool[$key]; 17 | } else { 18 | $pool[$key] = $this->redis = (new RedisConnector(Dce::$config->redis))->getRedis(); 19 | } 20 | parent::__construct($index, $noSerialize); 21 | } 22 | } -------------------------------------------------------------------------------- /engine/db/proxy/TransactionException.php: -------------------------------------------------------------------------------- 1 | config(); 15 | $topic = self::applyTime($topic, time()); 16 | $config['console'] && LogManager::console($content ? "$topic\n" . self::slim($content) : "$topic", prefix: ''); 17 | ($logFile = LogManager::standardConfigLogfile($config)) && LogManager::write($logFile, $content ? "$topic\n$content" : $topic); 18 | } 19 | } -------------------------------------------------------------------------------- /engine/pool/ArrayChannel.php: -------------------------------------------------------------------------------- 1 | channel, $object); 15 | } 16 | 17 | /** @inheritDoc */ 18 | public function pop(): mixed { 19 | return array_pop($this->channel); 20 | } 21 | 22 | public function isEmpty(): bool { 23 | return empty($this->channel); 24 | } 25 | 26 | public function length(): int { 27 | return count($this->channel); 28 | } 29 | } -------------------------------------------------------------------------------- /engine/model/validator/checker/EmailValidator.php: -------------------------------------------------------------------------------- 1 | addError($this->getGeneralError(null, lang(ValidatorException::INVALID_EMAIL))); 20 | } 21 | 22 | return $this->getError(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /engine/model/validator/TypeChecker.php: -------------------------------------------------------------------------------- 1 | getValue(); 12 | if (! empty($value)) { 13 | $result = $this->check($value); 14 | if ($result instanceof ValidatorException) { 15 | throw $result; 16 | } 17 | } 18 | return true; 19 | } 20 | 21 | /** 22 | * @param mixed $value 23 | * @return ValidatorException|null 24 | */ 25 | abstract protected function check(mixed $value):? ValidatorException; 26 | } 27 | -------------------------------------------------------------------------------- /engine/model/validator/checker/UrlValidator.php: -------------------------------------------------------------------------------- 1 | addError($this->getGeneralError(null, lang(ValidatorException::INVALID_URL))); 20 | } 21 | 22 | return $this->getError(); 23 | } 24 | } -------------------------------------------------------------------------------- /engine/model/validator/filter/FilterValidator.php: -------------------------------------------------------------------------------- 1 | getProperty('regexp')) { 19 | $value = preg_replace($regexp->value, '', $value); 20 | } else if ($keyword = $this->getProperty('keyword')) { 21 | $value = str_replace($keyword->value, '', $value); 22 | } 23 | return $value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /engine/log/FileOutput.php: -------------------------------------------------------------------------------- 1 | genPath($path); 17 | ! is_dir($fileDir = dirname($filePath)) && mkdir($fileDir, 0766, true); 18 | $content .= "\n"; 19 | $logMethod === LogMethod::Prepend && $content .= file_get_contents($filePath) ?: ''; 20 | file_put_contents($filePath, $content, ($logMethod === LogMethod::Append ? FILE_APPEND : 0) | LOCK_EX); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /drunk/debug/output/LogDebug.php: -------------------------------------------------------------------------------- 1 | data = $data; 19 | return $this; 20 | } 21 | 22 | protected function output(array $dataFormatted): void { 23 | ! $this->logStorage && throw new DebugException(DebugException::SET_STORAGE_FIRST); 24 | $content = $this->format($dataFormatted); 25 | $this->logStorage->push($this->getPath(), $content); // 储存调试内容 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /engine/db/query/builder/schema/LimitSchema.php: -------------------------------------------------------------------------------- 1 | 0) { 14 | $this->pushCondition($limit, false); 15 | if ($offset > 0) { 16 | $this->pushCondition($offset, true); 17 | } 18 | } else { 19 | // 否则清空Limit条件 20 | $this->pushCondition(false); 21 | } 22 | } 23 | 24 | public function __toString(): string { 25 | return implode(',', $this->getConditions()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /function/shortcut.php: -------------------------------------------------------------------------------- 1 | getProperty('isIp4')->value ? FILTER_FLAG_IPV4 : FILTER_FLAG_IPV6)) { 21 | $this->addError($this->getGeneralError(null, lang(ValidatorException::INVALID_IP))); 22 | } 23 | 24 | return $this->getError(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /engine/run.php: -------------------------------------------------------------------------------- 1 | statement = trim($statement); 19 | $this->statementLength = mb_strlen($statement); 20 | $this->offset = & $offset; 21 | } 22 | 23 | protected static function isMinus(string $word): bool { 24 | return '-' === $word; 25 | } 26 | 27 | abstract public function toArray(): array; 28 | 29 | abstract public function __toString(): string; 30 | } 31 | -------------------------------------------------------------------------------- /drunk/debug/DebugException.php: -------------------------------------------------------------------------------- 1 | channel = new Channel($capacity); 17 | } 18 | 19 | public function push($object): bool { 20 | return $this->channel->push($object); 21 | } 22 | 23 | public function pop(): mixed { 24 | return $this->channel->pop(); 25 | } 26 | 27 | public function isEmpty(): bool { 28 | return $this->channel->isEmpty(); 29 | } 30 | 31 | public function length(): int { 32 | return $this->channel->length(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /engine/project/render/JsonRenderer.php: -------------------------------------------------------------------------------- 1 | header('Content-Type', 'application/json; charset=utf-8'); 18 | } 19 | 20 | /** @inheritDoc */ 21 | protected function rendering(Controller $controller, mixed $data): string { 22 | return json_encode(false === $data ? ($controller->getAllAssignedStatus() ?: new stdClass) : $data, JSON_UNESCAPED_UNICODE); 23 | } 24 | } -------------------------------------------------------------------------------- /engine/config/ConfigException.php: -------------------------------------------------------------------------------- 1 | getProperty('set', lang(ValidatorException::SET_REQUIRED)); 18 | if ($set && ($bits = array_reduce($set->value, fn($tb, $b) => $tb | $b, 0)) && (($value | $bits) !== $bits)) { 19 | $this->addError($this->getGeneralError($set->error, lang(ValidatorException::VALUE_NOT_IN_BITSET))); 20 | } 21 | 22 | return $this->getError(); 23 | } 24 | } -------------------------------------------------------------------------------- /engine/project/session/CookieCgi.php: -------------------------------------------------------------------------------- 1 | 'cli', 10 | 'path' => 'tcp', 11 | ], 12 | [ 13 | 'path' => 'start', 14 | 'name' => '启动服务', 15 | 'controller' => 'TcpServerController->start', 16 | ], 17 | [ 18 | 'path' => 'stop', 19 | 'name' => '停止服务', 20 | 'controller' => 'TcpServerController->stop', 21 | 'enable_coroutine' => true, 22 | ], 23 | [ 24 | 'path' => 'reload', 25 | 'name' => '重启服务', 26 | 'controller' => 'TcpServerController->reload', 27 | 'enable_coroutine' => true, 28 | ], 29 | [ 30 | 'path' => 'status', 31 | 'name' => '状态信息', 32 | 'controller' => 'TcpServerController->status', 33 | 'enable_coroutine' => true, 34 | ], 35 | ]; 36 | -------------------------------------------------------------------------------- /project/http/config/nodes.php: -------------------------------------------------------------------------------- 1 | 'cli', 10 | 'path' => 'http', 11 | ], 12 | [ 13 | 'path' => 'start', 14 | 'name' => '启动服务', 15 | 'controller' => 'HttpServerController->start', 16 | ], 17 | [ 18 | 'path' => 'stop', 19 | 'name' => '停止服务', 20 | 'controller' => 'HttpServerController->stop', 21 | 'enable_coroutine' => true, 22 | ], 23 | [ 24 | 'path' => 'reload', 25 | 'name' => '重启服务', 26 | 'controller' => 'HttpServerController->reload', 27 | 'enable_coroutine' => true, 28 | ], 29 | [ 30 | 'path' => 'status', 31 | 'name' => '状态信息', 32 | 'controller' => 'HttpServerController->status', 33 | 'enable_coroutine' => true, 34 | ], 35 | ]; 36 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/client/IdgClientRpc.php: -------------------------------------------------------------------------------- 1 | idGenerator->newClient($tag)->generate($tag, $uid, $geneTag); 16 | } 17 | 18 | public static function batchGenerate(string $tag, int $count, int|string $uid = 0, string|null $geneTag = null): array { 19 | return Dce::$config->idGenerator->newClient($tag)->batchGenerate($tag, $count, $uid, $geneTag); 20 | } 21 | 22 | public static function getClient(string $tag): IdGenerator { 23 | return Dce::$config->idGenerator->newClient($tag); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /engine/project/node/NodeArgument.php: -------------------------------------------------------------------------------- 1 | setProperties($properties); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /project/dce/controller/CrontabController.php: -------------------------------------------------------------------------------- 1 | run($this->rawRequest->remainingPaths[0] ?? '', time()); 22 | } 23 | 24 | #[Node] 25 | public function status(): void { 26 | $this->print(Crontab::inst()->showLog(true)); 27 | } 28 | 29 | #[Node] 30 | public function history(): void { 31 | $this->print(Crontab::inst()->showLog(false)); 32 | } 33 | } -------------------------------------------------------------------------------- /project/websocket/config/nodes.php: -------------------------------------------------------------------------------- 1 | 'cli', 10 | 'path' => 'websocket', 11 | ], 12 | [ 13 | 'path' => 'start', 14 | 'name' => '启动服务', 15 | 'controller' => 'WebsocketServerController->start', 16 | ], 17 | [ 18 | 'path' => 'stop', 19 | 'name' => '停止服务', 20 | 'controller' => 'WebsocketServerController->stop', 21 | 'enable_coroutine' => true, 22 | ], 23 | [ 24 | 'path' => 'reload', 25 | 'name' => '重启服务', 26 | 'controller' => 'WebsocketServerController->reload', 27 | 'enable_coroutine' => true, 28 | ], 29 | [ 30 | 'path' => 'status', 31 | 'name' => '状态信息', 32 | 'controller' => 'WebsocketServerController->status', 33 | 'enable_coroutine' => true, 34 | ], 35 | ]; 36 | -------------------------------------------------------------------------------- /engine/model/validator/ending/RequiredValidator.php: -------------------------------------------------------------------------------- 1 | getProperty('allowEmpty'); 18 | if (! $allowEmpty->value) { 19 | $this->addError($this->getGeneralError(null, lang(ValidatorException::CANNOT_BE_EMPTY))); 20 | } else if (false === $value) { 21 | $this->addError($this->getGeneralError(null, lang(ValidatorException::REQUIRED_MISSING))); 22 | } 23 | } 24 | return $this->getError(); 25 | } 26 | } -------------------------------------------------------------------------------- /engine/log/MatrixOutput.php: -------------------------------------------------------------------------------- 1 | root = preg_replace('/\/+?$/', '/', $this->root); 20 | } 21 | 22 | /** 23 | * 拼储存路径 24 | * @param string $path 25 | * @return string 26 | */ 27 | protected function genPath(string $path): string { 28 | return $this->root . ltrim($path, '/'); 29 | } 30 | 31 | /** 32 | * 压入调试内容 33 | * @param string $path 34 | * @param string $content 35 | * @param LogMethod $logMethod 36 | */ 37 | abstract public function push(string $path, string $content, LogMethod $logMethod = LogMethod::Append): void; 38 | } 39 | -------------------------------------------------------------------------------- /engine/db/query/builder/schema/WithSchema.php: -------------------------------------------------------------------------------- 1 | self::tableWrapThrow($c), $columns); 11 | $recursive ??= count($select->getUnionSchema()->getConditions()) === 1; 12 | $this->pushCondition([$recursive ? 'RECURSIVE ' : '', $name, $columns ? '(' . implode(', ', $columns) . ')' : '', $select]); 13 | $this->mergeParams($select->getParams()); 14 | } 15 | 16 | public function __toString(): string { 17 | return implode(', ', array_map(fn($c) => "$c[0]$c[1]$c[2] AS ($c[3])", $this->getConditions())); 18 | } 19 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "idrunk/dce", 3 | "description": "DCE, A network programming framework with sharding middleware", 4 | "keywords": ["dce", "swoole", "websocket", "sharding", "rcr", "rpc", "pool"], 5 | "homepage": "https://drunkce.com", 6 | "type": "library", 7 | "license": "MulanPSL-2.0", 8 | "authors": [ 9 | { 10 | "name": "Drunk", 11 | "homepage": "http://idrunk.net" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=8.1", 16 | "swoole/ide-helper": "@dev" 17 | }, 18 | "require-dev": { 19 | "fakerphp/faker": "^1.13", 20 | "vimeo/psalm": "^4.9" 21 | }, 22 | "suggest": { 23 | "ext-pdo": "*", 24 | "ext-curl": "*", 25 | "ext-posix": "*", 26 | "ext-sockets": "*", 27 | "ext-redis": "*", 28 | "ext-memcache": "*", 29 | "ext-memcached": "*" 30 | }, 31 | "autoload": { 32 | "files": ["engine/run.php"] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /engine/db/query/builder/schema/DeleteSchema.php: -------------------------------------------------------------------------------- 1 | pushCondition($table); 23 | } 24 | } 25 | } 26 | 27 | public function __toString(): string { 28 | return implode(',', $this->getConditions()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /engine/log/MatrixLogger.php: -------------------------------------------------------------------------------- 1 | $targetLength ? '...' : ''); 19 | } 20 | 21 | /** 22 | * @param LoggerType $type 23 | * @param string $topic 24 | * @param string|null $content 25 | */ 26 | abstract public function push(LoggerType $type, string $topic, string|null $content = null): void; 27 | 28 | public static function inst(): static { 29 | return Singleton::gen(static::class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /engine/project/render/JsonpRenderer.php: -------------------------------------------------------------------------------- 1 | header('Content-Type', 'application/javascript; charset=utf-8'); 18 | } 19 | 20 | /** @inheritDoc */ 21 | protected function rendering(Controller $controller, mixed $data): string { 22 | $callback = $controller->request->get[$controller->request->node->jsonpCallback]; 23 | $response = json_encode(false === $data ? ($controller->getAllAssignedStatus() ?: new stdClass) : $data, JSON_UNESCAPED_UNICODE); 24 | return "{$callback}({$response})"; 25 | } 26 | } -------------------------------------------------------------------------------- /engine/db/query/builder/schema/UnionSchema.php: -------------------------------------------------------------------------------- 1 | pushCondition([$statement, $isAll]); 17 | $this->mergeParams($statement->getParams()); 18 | $this->logHasSubQuery(true); 19 | } 20 | 21 | public function __toString(): string { 22 | $conditions = []; 23 | foreach ($this->getConditions() as [$statement, $isAll]) { 24 | $conditions[] = ($isAll ? 'UNION ALL ' : 'UNION ' ) . sprintf($statement instanceof RawBuilder ? '%s' : '(%s)', $statement); 25 | } 26 | return implode(' ', $conditions); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /engine/log/HttpOutput.php: -------------------------------------------------------------------------------- 1 | genPath($path); 14 | $content .= "\n"; 15 | $ch = curl_init(); 16 | curl_setopt($ch, CURLOPT_URL, $url); 17 | if ('https' === strtolower(parse_url($url, PHP_URL_SCHEME))) { 18 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); 19 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); 20 | } 21 | curl_setopt($ch, CURLOPT_POST, 1); 22 | curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: text/plain', 'Dce-Debug: 1', "Log-Type: $logMethod->value"]); 23 | curl_setopt($ch, CURLOPT_POSTFIELDS, $content); 24 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 25 | curl_exec($ch); 26 | } 27 | } -------------------------------------------------------------------------------- /engine/db/query/builder/schema/HavingSchema.php: -------------------------------------------------------------------------------- 1 | whereSchema = new WhereSchema(); 18 | } 19 | 20 | public function addCondition (string|array|RawBuilder|WhereSchema $column, string|float|RawBuilder|SelectStatement $operator, string|float|RawBuilder|SelectStatement $value) { 21 | $this->whereSchema->addCondition($column, $operator, $value, 'AND'); 22 | $this->mergeConditions($this->whereSchema->getConditions()); 23 | $this->mergeParams($this->whereSchema->getParams()); 24 | } 25 | 26 | public function __toString(): string { 27 | return (string) $this->whereSchema; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/bridge/IdgStorageRedis.php: -------------------------------------------------------------------------------- 1 | prefix = $prefix; 18 | $this->index = $index; 19 | } 20 | 21 | /** @inheritDoc */ 22 | protected function genKey(string $tag, string $prefix = ''): string { 23 | return "$prefix$this->prefix:$tag"; 24 | } 25 | 26 | /** @inheritDoc */ 27 | public function load(string $tag): IdgBatch|null { 28 | return RedisProxy::new($this->index)->get($this->genKey($tag)) ?: null; 29 | } 30 | 31 | /** @inheritDoc */ 32 | public function save(string $tag, IdgBatch $batch): void { 33 | RedisProxy::new($this->index)->set($this->genKey($tag), $batch); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /engine/pool/TcpPoolProductionConfig.php: -------------------------------------------------------------------------------- 1 | matchWithProperties($config, ['host', 'port']); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /project/dce/service/extender/Extender.php: -------------------------------------------------------------------------------- 1 | cli->input($value); 35 | } 36 | 37 | protected function print(string $value, string $suffix = "\n"): void { 38 | $value = date('[H:i:s] ') . $value; 39 | $this->cli->print($value, $suffix); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/bridge/IdgRequestLocal.php: -------------------------------------------------------------------------------- 1 | server = $server; 17 | } 18 | 19 | /** 20 | * 注意, 实际环境中您需要自己实现request接口, 集群环境中服务端应该部署在远程服务器上, 您应该基于RPC等方式实现接口来实现注册及生成ID池 21 | * @param string $tag 22 | * @return IdgBatch 23 | * @throws IdgException 24 | */ 25 | public function register(string $tag): IdgBatch { 26 | return $this->server->register($tag); 27 | } 28 | 29 | /** 30 | * @param string $tag 31 | * @return IdgBatch 32 | * @throws IdgException 33 | */ 34 | public function generate(string $tag): IdgBatch { 35 | return $this->server->generate($tag); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /engine/model/validator/checker/RegularValidator.php: -------------------------------------------------------------------------------- 1 | getProperty('regexp', lang(ValidatorException::REGEXP_REQUIRED)); 22 | if ($regexp) { 23 | if (! Char::isRegexp($regexp->value)) { 24 | $this->addError(lang(ValidatorException::INVALID_REGEXP)); 25 | } else if (! preg_match($regexp->value, $value)) { 26 | $this->addError($this->getGeneralError($regexp->error, lang(ValidatorException::INVALID_INPUT))); 27 | } 28 | } 29 | 30 | return $this->getError(); 31 | } 32 | } -------------------------------------------------------------------------------- /engine/storage/redis/RedisPoolProductionConfig.php: -------------------------------------------------------------------------------- 1 | matchWithProperties($config, ['host', 'port']); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /engine/project/session/Cookie.php: -------------------------------------------------------------------------------- 1 | format($modifier); 22 | } 23 | $this->pushCondition($upperModifier); 24 | } 25 | $this->modifier = $upperModifier; 26 | } 27 | 28 | public function __toString(): string { 29 | return $this->modifier; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/IdgException.php: -------------------------------------------------------------------------------- 1 | $propertyName = $value; 25 | return $this; 26 | } 27 | 28 | public function setProperties(array $properties): static { 29 | foreach ($properties as $name => $property) 30 | $this->setProperty($name, $property); 31 | return $this; 32 | } 33 | 34 | public function arrayify(): array { 35 | $properties = get_object_vars($this); 36 | foreach ($properties as $k => $property) 37 | if (! is_scalar($property)) unset($properties[$k]); 38 | return $properties; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /engine/model/validator/checker/SetValidator.php: -------------------------------------------------------------------------------- 1 | getProperty('set', lang(ValidatorException::SET_REQUIRED)); 29 | $type = $this->getProperty('type'); 30 | if ($set && ! (($type->value ?? 0) & self::TYPE_KEY_EXISTS ? key_exists($value, $set->value) : in_array($value, $set->value))) { 31 | $this->addError($this->getGeneralError($set->error, lang(ValidatorException::VALUE_NOT_IN_SET))); 32 | } 33 | 34 | return $this->getError(); 35 | } 36 | } -------------------------------------------------------------------------------- /engine/project/request/RequestException.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | <?=$status ? '✔️' : '❌'?> <?=$message ?? ''?> 19 | 38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /engine/db/Shortcut.php: -------------------------------------------------------------------------------- 1 | table($tableName, $alias); 19 | } 20 | return $query; 21 | } 22 | } 23 | 24 | if (! function_exists('raw')) { 25 | /** 26 | * 实例化一个原始SQL语句对象 27 | * @param string $sql 28 | * @param bool|array $autoParenthesis 29 | * @param array $params 30 | * @return \dce\db\query\builder\RawBuilder 31 | * @throws \dce\db\query\QueryException 32 | */ 33 | function raw(string $sql, bool|array $autoParenthesis = true, array $params = []): dce\db\query\builder\RawBuilder { 34 | return new dce\db\query\builder\RawBuilder($sql, $autoParenthesis, $params); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /engine/base/SwooleLock.php: -------------------------------------------------------------------------------- 1 | ', '<', '>=', '<=']; 20 | 21 | public function toArray(): array { 22 | return [ 23 | 'type' => 'compare_expression', 24 | 'name' => $this->operator, 25 | 'left' => $this->left->toArray(), 26 | 'right' => $this->right->toArray(), 27 | ]; 28 | } 29 | 30 | public function __toString(): string { 31 | return "$this->left$this->operator$this->right"; 32 | } 33 | 34 | public static function build(MysqlParser $left, string $operator, MysqlParser $right): self { 35 | $instance = new self(''); 36 | $instance->left = $left; 37 | $instance->operator = $operator; 38 | $instance->right = $right; 39 | return $instance; 40 | } 41 | } -------------------------------------------------------------------------------- /engine/base/BaseException.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | 18 | <?=($code ?? 0) >= 500 ? '💥' : '💢'?> <?=$message ?? ''?> 19 | 38 | 39 | 40 |
41 |
42 | = 500 ? '💥' : '💢'?> 43 | 44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /engine/storage/redis/RedisPool.php: -------------------------------------------------------------------------------- 1 | getRedis(); 26 | } 27 | 28 | public function fetch(): Redis { 29 | return $this->get(); 30 | } 31 | 32 | public static function inst(string ... $identities): static { 33 | return parent::getInstance(RedisPoolProductionConfig::class, ... $identities); 34 | } 35 | 36 | protected function retryable(Throwable $throwable): bool { 37 | return $throwable instanceof RedisException && preg_match(self::REGEXP_CONNECTION_LOST, $throwable->getMessage()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /project/http/service/CookieSwoole.php: -------------------------------------------------------------------------------- 1 | rawRequest = $requestHttpSwoole; 16 | } 17 | 18 | /** @inheritDoc */ 19 | public function get(string $key): mixed { 20 | return $this->rawRequest->getRaw()->cookie[$key] ?? null; 21 | } 22 | 23 | /** @inheritDoc */ 24 | public function set(string $key, string $value = "", int $expire = 0, string $path = "", string $domain = "", bool $secure = false, bool $httpOnly = false): void { 25 | $this->rawRequest->getResponse()->cookie($key, $value, $expire, $path, $domain, $secure, $httpOnly); 26 | } 27 | 28 | /** @inheritDoc */ 29 | public function delete(string $key): void { 30 | $this->rawRequest->getResponse()->cookie($key, null); 31 | } 32 | 33 | /** @inheritDoc */ 34 | public function getAll(string $key): array { 35 | return $this->rawRequest->getRaw()->cookie ?? []; 36 | } 37 | } -------------------------------------------------------------------------------- /engine/pool/ChannelAbstract.php: -------------------------------------------------------------------------------- 1 | 0 && $this->setCapacity($capacity); 17 | } 18 | 19 | /** 20 | * 可能某些池有容量上限, 可通过计数与此值比对是否超上限, 若无上限, 则无需调用此参数 (实例配置实例带有实例上限数, 该处已做过一次超限拦截) 21 | * @param int $capacity 22 | */ 23 | public function setCapacity(int $capacity): void { 24 | $this->capacity = $capacity; 25 | } 26 | 27 | /** 28 | * @param mixed $object 29 | * @return bool 30 | */ 31 | abstract public function push(mixed $object): bool; 32 | 33 | /** 34 | * @return mixed 35 | */ 36 | abstract public function pop(): mixed; 37 | 38 | abstract public function isEmpty(): bool; 39 | 40 | abstract public function length(): int; 41 | 42 | public static function autoNew(int $capacity = 0): static { 43 | return Sington::new(SwooleUtility::inSwoole() ? CoroutineChannel::class : ArrayChannel::class, $capacity); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /engine/db/connector/DbEachIterator.php: -------------------------------------------------------------------------------- 1 | statement = $statement; 25 | $this->count = $statement->rowCount(); 26 | $this->decorator = $decorator; 27 | } 28 | 29 | public function valid() { 30 | return $this->offset < $this->count; 31 | } 32 | 33 | public function rewind() { 34 | $this->offset = 0; 35 | } 36 | 37 | public function next() { 38 | $this->offset ++; 39 | } 40 | 41 | public function key() { 42 | return $this->offset; 43 | } 44 | 45 | public function current() { 46 | $data = $this->statement->fetch(PDO::FETCH_ASSOC); 47 | if ($this->decorator) { 48 | $data = call_user_func($this->decorator, $data); 49 | } 50 | return $data; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /engine/sharding/middleware/data_processor/DbReadEachIterator.php: -------------------------------------------------------------------------------- 1 | result = $result; 21 | $this->decorator = $decorator; 22 | } 23 | 24 | public function valid() { 25 | return $this->validBool; 26 | } 27 | 28 | public function rewind() { 29 | reset($this->result); 30 | $this->validBool = true; 31 | } 32 | 33 | public function next() { 34 | if (false === next($this->result)) { 35 | $this->validBool = false; 36 | } 37 | } 38 | 39 | public function key() { 40 | return key($this->result); 41 | } 42 | 43 | public function current() { 44 | $data = current($this->result); 45 | if ($this->decorator) { 46 | $data = call_user_func($this->decorator, $data); 47 | } 48 | return $data; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/bridge/IdgStorage.php: -------------------------------------------------------------------------------- 1 | procLock($this->genKey($tag, 'didg:')); 19 | } 20 | 21 | /** 22 | * 解进程锁 23 | * @param string $tag 24 | */ 25 | public function unlock(string $tag): void { 26 | Dce::$lock->procUnlock($this->genKey($tag, 'didg:')); 27 | } 28 | 29 | /** 30 | * 生成缓存键/文件名 31 | * @param string $tag 32 | * @param string $prefix 33 | * @return string 34 | */ 35 | abstract protected function genKey(string $tag, string $prefix = ''): string; 36 | 37 | /** 38 | * 加载配置数据 39 | * @param string $tag 40 | * @return IdgBatch|null 41 | */ 42 | abstract public function load(string $tag): IdgBatch|null; 43 | 44 | /** 45 | * 储存配置数据 46 | * @param string $tag 47 | * @param IdgBatch $batch 48 | */ 49 | abstract public function save(string $tag, IdgBatch $batch): void; 50 | } 51 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/bridge/IdgStorageFile.php: -------------------------------------------------------------------------------- 1 | dataDir = $dataDir; 14 | if (! file_exists($this->dataDir)) { 15 | mkdir($this->dataDir, 0777, true); 16 | } 17 | } 18 | 19 | /** @inheritDoc */ 20 | protected function genKey(string $tag, string $prefix = ''): string { 21 | return "{$prefix}{$this->dataDir}{$tag}.php"; 22 | } 23 | 24 | /** @inheritDoc */ 25 | public function load(string $tag): IdgBatch|null { 26 | $content = @file_get_contents($this->genKey($tag)); 27 | if ($content) { 28 | $batch = unserialize($content); 29 | if ($batch instanceof IdgBatch) { 30 | return $batch; 31 | } 32 | } 33 | return null; 34 | } 35 | 36 | /** @inheritDoc */ 37 | public function save(string $tag, IdgBatch $batch): void { 38 | $content = serialize($batch); 39 | file_put_contents($this->genKey($tag), $content); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /engine/db/active/DceExtendColumn.php: -------------------------------------------------------------------------------- 1 | 16 | * create table dce_extend_column ( 17 | * table_id tinyint unsigned not null, 18 | * primary_id bigint unsigned not null, 19 | * column_id tinyint unsigned not null, 20 | * value varbinary(255) not null, 21 | * primary key (table_id, primary_id, column_id) 22 | * ); 23 | * 24 | */ 25 | class DceExtendColumn extends DceExternalRecord { 26 | #[Property(id: ExternalPropertyId::COLUMN_ID), DbField(FieldType::Smallint, primary: true)] 27 | public int $columnId; 28 | #[Property(id: ExternalPropertyId::VALUE), DbField(FieldType::Varchar, 255)] 29 | public string $value; 30 | 31 | public static function getColumnProperty(): Property { 32 | return static::getPropertyById(ExternalPropertyId::ColumnId->value); 33 | } 34 | public static function getValueProperty(): Property { 35 | return static::getPropertyById(ExternalPropertyId::Value->value); 36 | } 37 | } -------------------------------------------------------------------------------- /engine/db/connector/DbPoolProductionConfig.php: -------------------------------------------------------------------------------- 1 | matchWithProperties($config, ['host', 'dbName']); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /engine/db/active/DceModifyRecord.php: -------------------------------------------------------------------------------- 1 | 16 | * create table dce_modify_record ( 17 | * table_id tinyint unsigned not null, 18 | * primary_id bigint unsigned not null, 19 | * version smallint unsigned not null, 20 | * original_record json not null, 21 | * primary key (table_id, primary_id, version) 22 | * ); 23 | * 24 | */ 25 | class DceModifyRecord extends DceExternalRecord { 26 | #[Property(id: ExternalPropertyId::VERSION), DbField(FieldType::Smallint, primary: true)] 27 | public int $version; 28 | #[Property(id: ExternalPropertyId::ORIGINAL_RECORD), DbField(FieldType::Json)] 29 | public string $originalRecord; 30 | 31 | public static function getVersionProperty(): Property { 32 | return static::getPropertyById(ExternalPropertyId::Version->value); 33 | } 34 | public static function getOriginalRecordProperty(): Property { 35 | return static::getPropertyById(ExternalPropertyId::OriginalRecord->value); 36 | } 37 | } -------------------------------------------------------------------------------- /engine/storage/redis/RedisConnector.php: -------------------------------------------------------------------------------- 1 | redis = new Redis(); 20 | if ($config['password'] ?? false) { 21 | $this->redis->auth($config['password']); 22 | } 23 | if ( 24 | $persistent 25 | ? ! $this->redis->pconnect($config['host'], $config['port'], self::CONNECT_TIMEOUT) 26 | : ! $this->redis->connect($config['host'], $config['port'], self::CONNECT_TIMEOUT) 27 | ) { 28 | throw new PoolException(PoolException::CONNECT_REDIS_FAILED); 29 | } 30 | if ($config['index'] > 0) { 31 | $this->redis->select($config['index']); 32 | } 33 | $this->redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); 34 | $this->redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); 35 | } 36 | 37 | public function getRedis(): Redis { 38 | return $this->redis; 39 | } 40 | } -------------------------------------------------------------------------------- /engine/sharding/parser/StatementParserException.php: -------------------------------------------------------------------------------- 1 | __NAMESPACE__ . '\statement\MysqlCaseParser', 19 | self::$whenStmt => __NAMESPACE__ . '\statement\MysqlWhenParser', 20 | default => false, 21 | }; 22 | } 23 | 24 | public static function build(string $statement, int & $offset, string|null $statementName = null): static|null { 25 | if (is_subclass_of(static::class, self::class)) { 26 | $class = static::class; 27 | } else { 28 | $class = self::detect($statementName); 29 | } 30 | if ($class) { 31 | $instance = new $class($statement, $offset); 32 | if ($instance instanceof self) { 33 | $instance->parse(); 34 | return $instance; 35 | } 36 | } 37 | return null; 38 | } 39 | 40 | abstract protected function parse(): void; 41 | } 42 | -------------------------------------------------------------------------------- /project/tcp/controller/TcpServerController.php: -------------------------------------------------------------------------------- 1 | request->config->tcp['service']; 20 | if (! is_a($serverClass, TcpServer::class, true)) { 21 | throw new ConfigException(ConfigException::TCP_SERVICE_INVALID); 22 | } 23 | // 构造函数内会挂载RPC客户端, 所以整个公共的呗 24 | $this->server = new $serverClass(); 25 | } 26 | 27 | public function start() { 28 | $this->server->start($this->request->pureCli); 29 | } 30 | 31 | public function stop() { 32 | RpcServerApi::stop(); 33 | $this->print('Tcp server was stopped.'); 34 | } 35 | 36 | public function reload() { 37 | RpcServerApi::reload(); 38 | $this->print('Tcp server was reloaded.'); 39 | } 40 | 41 | public function status() { 42 | $status = RpcServerApi::status(); 43 | $status = json_encode($status, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); 44 | $this->print($status); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /engine/model/validator/checker/StringValidator.php: -------------------------------------------------------------------------------- 1 | addError($this->getGeneralError(null, lang(ValidatorException::INVALID_STRING))); 24 | } else { 25 | $min = $this->getProperty('min'); 26 | $max = $this->getProperty('max'); 27 | if ($min || $max) { 28 | $length = mb_strlen($value); 29 | 30 | if ($min && $length < $min->value) { 31 | $this->addError($this->getGeneralError($min->error, lang(ValidatorException::CANNOT_LESS_THAN))); 32 | } 33 | 34 | if ($max && $length > $max->value) { 35 | $this->addError($this->getGeneralError($max->error, lang(ValidatorException::CANNOT_MORE_THAN))); 36 | } 37 | } 38 | } 39 | 40 | return $this->getError(); 41 | } 42 | } -------------------------------------------------------------------------------- /engine/db/entity/DbField.php: -------------------------------------------------------------------------------- 1 | setDefault($default); 37 | '' !== $comment && $this->setComment($comment); 38 | $type && $this->setType($type, $length, $unsigned, $precision); 39 | $primary && $this->setPrimary(); 40 | $null && $this->setNull(); 41 | $increment && $this->setIncrement(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /project/http/controller/HttpServerController.php: -------------------------------------------------------------------------------- 1 | request->config->http['service'] ?? ''; 20 | if (! is_a($serverClass, HttpServer::class, true)) { 21 | throw new ConfigException(ConfigException::HTTP_SERVICE_INVALID); 22 | } 23 | // 在这里初始化是因为需要准备RpcClient 24 | $this->server = new $serverClass(); 25 | } 26 | 27 | public function start() { 28 | $this->server->start($this->request->pureCli); 29 | } 30 | 31 | public function stop() { 32 | RpcServerApi::stop(); 33 | $this->print('Http server was stopped.'); 34 | } 35 | 36 | public function reload() { 37 | RpcServerApi::reload(); 38 | $this->print('Http server was reloaded.'); 39 | } 40 | 41 | public function status() { 42 | $status = RpcServerApi::status(); 43 | $status = json_encode($status, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); 44 | $this->print($status); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /project/websocket/controller/WebsocketServerController.php: -------------------------------------------------------------------------------- 1 | request->config->websocket['service']; 19 | if (! is_a($serverClass, WebsocketServer::class, true)) { 20 | throw new ConfigException(ConfigException::WEBSOCKET_SERVICE_INVALID); 21 | } 22 | // 构造函数内会挂载RPC客户端, 所以整个公共的呗 23 | $this->server = new $serverClass(); 24 | } 25 | 26 | public function start() { 27 | $this->server->start($this->request->pureCli); 28 | } 29 | 30 | public function stop() { 31 | RpcServerApi::stop(); 32 | $this->print('Websocket server was stopped.'); 33 | } 34 | 35 | public function reload() { 36 | RpcServerApi::reload(); 37 | $this->print('Websocket server was reloaded.'); 38 | } 39 | 40 | public function status() { 41 | $status = RpcServerApi::status(); 42 | $status = json_encode($status, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); 43 | $this->print($status); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/bridge/IdgBatch.php: -------------------------------------------------------------------------------- 1 | value]; 22 | } 23 | /** @return Property[] */ 24 | public static function getForeignKeyProperties(): array { 25 | return array_map(fn($pid) => self::getPropertyById($pid), static::getForeignKeyPropertyIds()); 26 | } 27 | public static function getForeignKeyPropertyByIndex(int $index): Property { 28 | return static::getPropertyById(static::getForeignKeyPropertyIds()[$index]); 29 | } 30 | public static function getForeignKeyValues(): array { 31 | return array_reduce(self::getForeignKeyProperties(), fn($map, $p) => $map + [$p->field->getName() => $this->{$p->name}], []); 32 | } 33 | public static function getTableProperty(): Property { 34 | return static::getPropertyById(ExternalPropertyId::TableId->value); 35 | } 36 | } -------------------------------------------------------------------------------- /engine/model/validator/ValidatorProperty.php: -------------------------------------------------------------------------------- 1 | format($modelPropertyLabel, $name); 27 | } 28 | } else if (! is_array($value)) { 29 | $value = [$value]; 30 | } 31 | $this->value = $value[0]; 32 | if (isset($value[1])) { 33 | $errCode = $value[2] ?? 0; 34 | // 如果配置了多语种映射或者异常码, 则实例化为Language, 否则为string 35 | $this->error = is_array($value[1]) || $errCode ? new Language($value[1], $errCode) : $value[1]; 36 | } 37 | } 38 | 39 | public function __toString(): string { 40 | return Utility::printable($this->value); 41 | } 42 | } -------------------------------------------------------------------------------- /engine/db/query/builder/schema/GroupSchema.php: -------------------------------------------------------------------------------- 1 | pushCondition($column); 31 | $this->mergeParams($column->getParams()); 32 | } else if (is_string($column) && $column = self::tableWrap($column)) { 33 | $this->pushCondition($column); 34 | } else { 35 | throw (new QueryException(QueryException::GROUP_COLUMN_INVALID))->format(self::printable($column)); 36 | } 37 | } 38 | } 39 | 40 | public function __toString(): string { 41 | return implode(',', $this->getConditions()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /engine/cache/CacheClearable.php: -------------------------------------------------------------------------------- 1 | getMeta($key, true); 20 | if ($meta && $meta['expiry'] > 0 && $meta['expiry'] + $meta['update_time'] < $time) { 21 | $meta = null; 22 | $this->del($key); 23 | // 如果当前的过期了, 则尝试清除其他过期的 24 | foreach ($this->listMeta() as $key => $data) { 25 | if ($data['expiry'] > 0 && $data['expiry'] + $data['update_time'] < $time) { 26 | $this->del([$key]); 27 | } 28 | } 29 | } 30 | return $meta['data'] ?? false; 31 | } 32 | 33 | /** 34 | * 列出缓存元 35 | * @return array 36 | */ 37 | abstract public function listMeta(): array; 38 | 39 | /** 40 | * 取缓存元信息 41 | * @param string|array $key 42 | * @param bool $loadData 43 | * @return array|false 44 | */ 45 | #[ArrayShape([ 46 | 'data' => 'mixed', 47 | 'expiry' => 'int', 48 | 'create_time' => 'int', 49 | 'update_time' => 'int', 50 | ])] 51 | abstract public function getMeta(string|array $key, bool $loadData = false): array|false; 52 | } -------------------------------------------------------------------------------- /engine/sharding/parser/mysql/list/MysqlGroupByParser.php: -------------------------------------------------------------------------------- 1 | offset < $this->statementLength) { 18 | $this->conditions[] = $this->parseWithOffset(); 19 | $nextSeparator = $this->preParseOperator(); 20 | if (! in_array($nextSeparator, self::$paramSeparators)) { 21 | break; 22 | } 23 | } 24 | } 25 | 26 | public function addItem(MysqlParser $item): void { 27 | $this->conditions[] = $item; 28 | } 29 | 30 | public function toArray(): array { 31 | $conditionsToArray = []; 32 | foreach ($this->conditions as $condition) { 33 | $conditionsToArray[] = $condition->toArray(); 34 | } 35 | return [ 36 | 'type' => 'list', 37 | 'class' => 'group', 38 | 'conditions' => $conditionsToArray, 39 | ]; 40 | } 41 | 42 | public function __toString(): string { 43 | $columns = implode(',', $this->conditions); 44 | return $columns; 45 | } 46 | 47 | protected static function queueProperty(): string { 48 | return 'conditions'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /engine/db/active/CacheEngine.php: -------------------------------------------------------------------------------- 1 | $modelClass 17 | * @return array 18 | */ 19 | public static function encodeKeys(array $data, string $modelClass): array { 20 | return array_reduce(array_keys($data), fn($m, $k) => $m + [$modelClass::getProperty($k)->id, $data[$k]], []); 21 | } 22 | 23 | /** 24 | * @param array $data 25 | * @param class-string $modelClass 26 | * @return array 27 | */ 28 | public static function decodeKeys(array $data, string $modelClass): array { 29 | return array_reduce(array_keys($data), fn($m, $k) => $m + [$modelClass::getPropertyById($k)->name => $data[$k]], []); 30 | } 31 | 32 | public static function save(string $cacheKey, array $data): void { 33 | $redis = RedisProxy::new(Dce::$config->cache['redis']['index'], true); 34 | $redis->hMSet($cacheKey, $data); 35 | $redis->expire($cacheKey, 604800); 36 | } 37 | 38 | public static function load(string $cacheKey): array { 39 | return RedisProxy::new(Dce::$config->cache['redis']['index'], true)->hGetAll($cacheKey); 40 | } 41 | 42 | public static function delete(string $cacheKey): void { 43 | RedisProxy::new(Dce::$config->cache['redis']['index'])->del($cacheKey); 44 | } 45 | } -------------------------------------------------------------------------------- /engine/rpc/RpcHost.php: -------------------------------------------------------------------------------- 1 | setProperties($properties); 41 | } 42 | 43 | /** 44 | * 设置鉴权方案 45 | * @param string $password 46 | * @param array $ipWhiteList 47 | * @param bool $needNative 48 | * @param bool $needLocal 49 | * @return $this 50 | */ 51 | public function setAuth(string $password, array $ipWhiteList = [], bool $needNative = false, bool $needLocal = false): self { 52 | $this->needNative = $needNative; 53 | $this->needLocal = $needLocal; 54 | $this->ipWhiteList = $ipWhiteList; 55 | $this->password = $password; 56 | return $this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /engine/project/request/RawRequest.php: -------------------------------------------------------------------------------- 1 | rawData; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /engine/storage/redis/RedisProxyPool.php: -------------------------------------------------------------------------------- 1 | setConfigs(Dce::$config->redis); 19 | $this->redis = self::$pool->fetch(); 20 | 21 | $barrier = Barrier::make(); 22 | $exceptions = []; 23 | PdoDbConnector::registerCoroutineAutoReleaseOrHandle( 24 | self::$pool->retryableContainer(function() use($index, $noSerialize) { 25 | parent::__construct($index, $noSerialize); 26 | PdoDbConnector::registerCoroutineAutoReleaseOrHandle(Coroutine::getCid(), false, 'redis'); 27 | }, $exceptions, $barrier), type: 'redis' 28 | ); 29 | Barrier::wait($barrier); 30 | if ($exceptions) { 31 | self::$pool = null; 32 | throw array_pop($exceptions); 33 | } 34 | } 35 | 36 | public function __destruct() { 37 | $redis = $this->redis; 38 | parent::__destruct(); 39 | // 必须最后put,否则高并发下,put回了pool但redis属性未释放,可能导致下述异常: 40 | // Socket#76 has already been bound to another coroutine#19, reading of the same socket in coroutine#24 at the same time is not allowed 41 | self::$pool?->put($redis); 42 | } 43 | } -------------------------------------------------------------------------------- /project/dce/controller/DceController.php: -------------------------------------------------------------------------------- 1 | print("\n你正在cli模式以空路径请求Dce接口"); 16 | } 17 | 18 | #[Node('empty/connection', ['websocket', 'tcp', 'udp'])] 19 | public function connection() { 20 | $this->assign('info', '恭喜!服务端收到了你的消息并给你作出了回应'); 21 | $this->response(); 22 | } 23 | 24 | #[Node('empty/http/ajax')] 25 | public function ajax() { 26 | $this->assign('info', '请求成功,祝你愉快 (*^▽^*)'); 27 | } 28 | 29 | #[Node('empty/http', ['get', 'post', 'put', 'delete', 'options', 'head'])] 30 | public function http() { 31 | $this->response(' 32 | 33 | 34 | 35 | 欢迎使用Dce (*´▽`)ノノ 36 | 37 | 38 |

欢迎使用Dce (*´▽`)ノノ

39 |

这是Dce默认HTTP响应页,当你看到它时,表示你的Dce框架成功运行。

40 |

本页面仅在你未配置根路径节点时才会显示,如果你配置了根节点,则会渲染显示该节点控制器的响应内容。

41 |

你可以通过`omissible_path`属性实现根节点效果,参考下述示例。

42 |
43 | return [
44 |     [
45 |         "path" => "home",
46 |         "omissible_path" => true,
47 |         "controller" => "IndexController->index",
48 |     ],
49 | ];
50 | 
51 |

上述配置定义了名为`home`的项目的节点配置,通过设置`omissible_path`为`true`实现可省略路径访问,即你可以通过`http://127.0.0.1/`路径请求`IndexController->index`控制器方法

52 | 53 | '); 54 | } 55 | } -------------------------------------------------------------------------------- /engine/db/entity/FieldType.php: -------------------------------------------------------------------------------- 1 | 0 ? $length : (match ($this) { 39 | self::Int => [10, 11], 40 | self::Tinyint => [3, 4], 41 | self::Smallint => [5, 6], 42 | self::Mediumint => [8, 8], 43 | self::Bigint => [20, 20], 44 | default => [0, 0], 45 | })[(int) ! $isUnsigned]; 46 | } 47 | 48 | public function isNumeric(): bool { 49 | return in_array($this, [self::Int, self::Tinyint, self::Smallint, self::Mediumint, self::Bigint, self::Decimal, self::Float, self::Double]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /engine/db/query/builder/statement/InsertSelectStatement.php: -------------------------------------------------------------------------------- 1 | tableSchema = $tableSchema; 19 | $this->insertSelectSchema = $insertSelectSchema; 20 | $this->ignoreOrReplace = $ignoreOrReplace; 21 | $this->valid(); 22 | $this->mergeParams($this->insertSelectSchema->getParams()); 23 | $this->logHasSubQuery($this->insertSelectSchema->hasSubQuery()); 24 | } 25 | 26 | public function __toString(): string { 27 | $insertSql = $this->ignoreOrReplace ? 'INSERT IGNORE' : (null === $this->ignoreOrReplace ? 'INSERT' : 'REPLACE'); 28 | $sql = "{$insertSql} INTO {$this->tableSchema} {$this->insertSelectSchema}"; 29 | return $sql; 30 | } 31 | 32 | protected function valid(): void { 33 | if ($this->tableSchema->isEmpty()) { 34 | throw new QueryException(QueryException::INSERT_TABLE_NOT_SPECIFIED); 35 | } 36 | if ($this->insertSelectSchema->isEmpty()) { 37 | throw new QueryException(QueryException::SELECT_TABLE_NOT_SPECIFIED); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /engine/sharding/parser/mysql/statement/MysqlColumnItemParser.php: -------------------------------------------------------------------------------- 1 | field = $this->parseWithOffset(); 21 | if (! $this->field instanceof MysqlFieldParser || ! in_array($this->field->field, self::$columnWildcards)) { 22 | // 非通配符才可能有别名 23 | $this->alias = $this->preParseAlias(); 24 | } 25 | } 26 | 27 | /** 28 | * 提取聚合函数 29 | * @return MysqlFunctionParser[] 30 | */ 31 | public function extractAggregates(): array { 32 | $aggregates = []; 33 | $aggregates = array_merge($aggregates, $this->field->extractAggregates()); 34 | return $aggregates; 35 | } 36 | 37 | public function toArray(): array { 38 | return [ 39 | 'type' => 'statement', 40 | 'statement' => 'column', 41 | 'field' => $this->field->toArray(), 42 | 'alias' => $this->alias, 43 | ]; 44 | } 45 | 46 | public function __toString(): string { 47 | $field = $this->field; 48 | if ($this->alias) { 49 | $field .= " AS {$this->alias}"; 50 | } 51 | return $field; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /project/dce/service/server/Connection.php: -------------------------------------------------------------------------------- 1 | initialNode = $initialNode; 27 | $this->session = $session; 28 | $swRequest && $this->swRequest = $swRequest; 29 | return $this; 30 | } 31 | 32 | public function onRequest(): bool { 33 | return RequestManager::current()?->fd === $this->fd; 34 | } 35 | 36 | public function destroy(): void { 37 | Sington::destroy(self::class, $this->fd); 38 | } 39 | 40 | public static function from(int $fd, ServerMatrix $server = null): self { 41 | $instance = Sington::generated(self::class, $fd); 42 | // 即便前个fd的Connection对象未被清除也没关系,因为后续setProps时会覆盖掉旧的属性 43 | is_string($instance) && $instance = Sington::logInstance($instance, new self($fd, $server)); 44 | return $instance; 45 | } 46 | 47 | public static function exists(int $fd): self|null { 48 | return is_string($instance = Sington::generated(self::class, $fd)) ? null : $instance; 49 | } 50 | } -------------------------------------------------------------------------------- /engine/db/query/builder/schema/UpdateSchema.php: -------------------------------------------------------------------------------- 1 | columns = $columns = array_keys($data); 19 | 20 | foreach ($this->columns as $k=>$column) { 21 | $columnName = self::tableWrap($column); 22 | ! $columnName && throw (new QueryException(QueryException::COLUMN_INVALID))->format($column); 23 | $this->columns[$k] = $columnName; 24 | } 25 | 26 | foreach ($columns as $k=>$column) { 27 | $value = $data[$column] ?? null; 28 | if ($value instanceof RawBuilder) { 29 | $this->sqlPackage[] = "{$this->columns[$k]}=$value"; 30 | $this->mergeParams($value->getParams()); 31 | } else if ($value instanceof SelectStatement) { 32 | $this->sqlPackage[] = "{$this->columns[$k]}=($value)"; 33 | $this->mergeParams($value->getParams()); 34 | $this->logHasSubQuery(true); 35 | } else { 36 | $this->sqlPackage[] = "{$this->columns[$k]}=?"; 37 | $this->pushParam($value); 38 | } 39 | } 40 | 41 | $this->pushCondition($data); 42 | } 43 | 44 | public function __toString(): string { 45 | return implode(',', $this->sqlPackage); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /engine/sharding/parser/mysql/statement/MysqlWhenParser.php: -------------------------------------------------------------------------------- 1 | when = $this->parseWithOffset(null); 21 | $then = $this->preParseWord(); 22 | if (! $then || 'THEN' !== strtoupper($then)) { 23 | throw new StatementParserException(StatementParserException::THEN_MISSING); 24 | } 25 | $this->then = $this->parseWithOffset(null); 26 | } 27 | 28 | /** 29 | * 提取聚合函数 30 | * @return MysqlFunctionParser[] 31 | */ 32 | public function extractAggregates(): array { 33 | $aggregates = []; 34 | $aggregates = array_merge($aggregates, $this->when->extractAggregates()); 35 | $aggregates = array_merge($aggregates, $this->then->extractAggregates()); 36 | return $aggregates; 37 | } 38 | 39 | public function toArray(): array { 40 | return [ 41 | 'type' => 'statement', 42 | 'statement' => 'when', 43 | 'when' => $this->when->toArray(), 44 | 'then' => $this->then->toArray(), 45 | ]; 46 | } 47 | 48 | public function __toString(): string { 49 | $when = "WHEN {$this->when} THEN {$this->then}"; 50 | return $when; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /engine/sharding/parser/mysql/list/MysqlOrderByParser.php: -------------------------------------------------------------------------------- 1 | offset < $this->statementLength) { 19 | $this->conditions[] = MysqlOrderByConditionParser::build($this->statement, $this->offset); 20 | $nextSeparator = $this->preParseOperator(); 21 | if (! in_array($nextSeparator, self::$paramSeparators)) { 22 | break; 23 | } 24 | } 25 | } 26 | 27 | public function addItem(MysqlParser $item): void { 28 | if ($item instanceof MysqlOrderByConditionParser) { 29 | $this->conditions[] = $item; 30 | } 31 | } 32 | 33 | public function toArray(): array { 34 | $conditionsToArray = []; 35 | foreach ($this->conditions as $condition) { 36 | $conditionsToArray[] = $condition->toArray(); 37 | } 38 | return [ 39 | 'type' => 'list', 40 | 'class' => 'order', 41 | 'conditions' => $conditionsToArray, 42 | ]; 43 | } 44 | 45 | public function __toString(): string { 46 | $columns = implode(',', $this->conditions); 47 | return $columns; 48 | } 49 | 50 | protected static function queueProperty(): string { 51 | return 'conditions'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /engine/project/render/XmlRenderer.php: -------------------------------------------------------------------------------- 1 | header('Content-Type', 'text/xml; charset=utf-8'); 18 | } 19 | 20 | /** @inheritDoc */ 21 | protected function rendering(Controller $controller, mixed $data): string { 22 | return self::arrayToXml(false === $data ? $controller->getAllAssignedStatus() : $data); 23 | } 24 | 25 | /** 26 | * 简单数组转XML 27 | * @param array $data 28 | * @param SimpleXMLElement|null $xmlElement 29 | * @return string|null 30 | */ 31 | private static function arrayToXml(array $data, SimpleXMLElement|null $xmlElement = null): string|null { 32 | $isTopLevel = false; 33 | if (null === $xmlElement) { 34 | $isTopLevel = true; 35 | $xmlElement = new SimpleXMLElement(''); 36 | } 37 | foreach ($data as $key => $value) { 38 | if (is_int($key)){ 39 | $key = "item"; 40 | } 41 | if (is_array($value)) { 42 | $childXmlElement = $xmlElement->addChild($key); 43 | self::arrayToXml($value, $childXmlElement); 44 | } else { 45 | $xmlElement->addChild($key, htmlspecialchars($value)); 46 | } 47 | } 48 | return $isTopLevel ? $xmlElement->asXML(): null; 49 | } 50 | } -------------------------------------------------------------------------------- /engine/db/query/builder/schema/InsertSelectSchema.php: -------------------------------------------------------------------------------- 1 | format(self::printable($column)); 25 | } 26 | $this->columns[] = $columnsName; 27 | } 28 | } 29 | if (! ($isSelect = $selectStatement instanceof SelectStatement) && ! $selectStatement instanceof RawBuilder) { 30 | throw (new QueryException(QueryException::SELECT_STRUCT_INVALID))->format(self::printable($selectStatement)); 31 | } 32 | $this->mergeParams($selectStatement->getParams()); 33 | $this->pushCondition($selectStatement); 34 | $this->logHasSubQuery($isSelect); 35 | } 36 | 37 | public function getColumns() { 38 | return $this->columns; 39 | } 40 | 41 | public function __toString(): string { 42 | $columnsSql = empty($this->columns) ? '' : '(' .implode(',', $this->columns). ') '; 43 | $selectSql = current($this->getConditions()); 44 | return "{$columnsSql}{$selectSql}"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /engine/db/query/builder/schema/WindowSchema.php: -------------------------------------------------------------------------------- 1 | setPartition($partition); 23 | $this->mergeParams($parts[1]->getParams()); 24 | } 25 | if ($order) { 26 | $parts[2] = new OrderSchema(); 27 | $parts[2]->addOrder($order, null, false); 28 | $this->mergeParams($parts[2]->getParams()); 29 | } 30 | if ($frame) { 31 | $parts[3] = new FrameSchema(); 32 | $parts[3]->setFrame($frame); 33 | $this->mergeParams($parts[3]->getParams()); 34 | } 35 | $this->pushCondition([$name, $parts]); 36 | } 37 | 38 | public function __toString(): string { 39 | return implode(', ', array_map(fn($c) => "$c[0] AS (" . 40 | implode(' ', array_filter([$c[1][0], $c[1][1] ? "PARTITION BY {$c[1][1]}" : false, $c[1][2] ? "ORDER BY {$c[1][2]}" : false, $c[1][3]])) 41 | . ')', $this->getConditions())); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /engine/db/query/builder/schema/FrameSchema.php: -------------------------------------------------------------------------------- 1 | 18 | * string 字符串型frame,将会进行正则校验,有效值如:ROWS UNBOUNDED PRECEDING; ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING 19 | * RawBuilder 不会进行正则校验,如:raw('ROWS UNBOUNDED PRECEDING', false); raw('ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING', false) 20 | * 21 | * @throws QueryException 22 | */ 23 | public function setFrame(string|RawBuilder $frame): void { 24 | if (is_string($frame)) { 25 | ! preg_match(self::VALID_REG, $frame) && throw new QueryException(QueryException::WINDOW_FRAME_INVALID); 26 | $this->pushCondition($frame); 27 | } else { 28 | $this->pushCondition($frame); 29 | $this->mergeParams($frame->getParams()); 30 | } 31 | } 32 | 33 | public function __toString(): string { 34 | return (string) $this->getConditions()[0]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /engine/model/validator/ending/UniqueValidator.php: -------------------------------------------------------------------------------- 1 | model instanceof ActiveRecord) { 18 | if (! $value) return null; 19 | $combined = $this->getProperty('combined'); 20 | $combinedNames = $combined->value ?? []; 21 | $whereConditions = [[$this->model::toDbKey($this->modelPropertyName), '=', $value]]; 22 | foreach ($combinedNames as $combinedName) 23 | $whereConditions[] = [$combinedName, '=', $this->model->{$this->model::toModelKey($combinedName)}]; 24 | $rows = $this->model::query()->where($whereConditions)->limit(2)->select(); 25 | if (! empty($rows)) { // 如果找到了记录 26 | if ($this->model->isCreateByQuery()) { // 如果创建自查询, 且如果查询到的数据大于两条, 或查出的主键与当前主键不同, 则表示有重复 27 | if (count($rows) > 1 || ! empty(array_diff_assoc($rows[0]->getPkValues(), $this->model->getPkValues()))) 28 | $this->addError($this->getGeneralError($combined->error ?? null, lang(ValidatorException::CANNOT_REPEAT))); 29 | } else { // 如果非创建自查询, 则为插入, 则只要有查到即为重复的 30 | $this->addError($this->getGeneralError($combined->error ?? null, lang(ValidatorException::CANNOT_REPEAT))); 31 | } 32 | } 33 | } else { 34 | $this->addError(lang(ValidatorException::NOT_SUPPORT_UNIQUE)); 35 | } 36 | 37 | return $this->getError(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /engine/db/proxy/SimpleTransaction.php: -------------------------------------------------------------------------------- 1 | proxy) && throw new TransactionException(TransactionException::REPEATED_OPEN); 23 | } 24 | 25 | /** 26 | * 按代理匹配事务实例 27 | * @param DbProxy $proxy 28 | * @return static|null 29 | */ 30 | private static function proxyMatch(DbProxy $proxy): static|null { 31 | foreach (self::$pond as $transaction) 32 | if ($transaction->proxy === $proxy) return $transaction; 33 | return null; 34 | } 35 | 36 | /** 37 | * 实例化一个事务对象,并递增进入计数 38 | * @param SimpleDbProxy $proxy 39 | * @return static 40 | */ 41 | public static function begin(SimpleDbProxy $proxy): self { 42 | $instance = self::proxyMatch($proxy) ?? new self($proxy); 43 | $instance->entries ++; 44 | return $instance; 45 | } 46 | 47 | /** 48 | * 检测并尝试开启连接事务 49 | * @param SimpleDbProxy $proxy 50 | * @param DbConnector $connector 51 | */ 52 | public static function tryBegin(SimpleDbProxy $proxy, DbConnector $connector): void { 53 | $transaction = self::proxyMatch($proxy); 54 | if ($transaction && ! isset($transaction->connector)) { 55 | $transaction->markConnector($connector); 56 | $connector->begin(); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /engine/sharding/middleware/MiddlewareException.php: -------------------------------------------------------------------------------- 1 | %s > id_column'])] 19 | public const CONFIG_ID_COLUMN_EMPTY = 1801; 20 | 21 | #[Language(['缺少分库配置 %s > %s > sharding_column'])] 22 | public const CONFIG_SHARDING_COLUMN_EMPTY = 1802; 23 | 24 | #[Language(['分库配置错误, 未配置 %s 表内容切分依据字段'])] 25 | public const CONFIG_TABLE_SHARDING_RULE_EMPTY = 1803; 26 | 27 | // 运行时异常 28 | #[Language(['非按模分库禁止连表查询'])] 29 | public const NO_MOD_SHARDING_NO_JOINT = 1820; 30 | 31 | #[Language(['未开启分库连表查询 (可以设置allow_joint=true开启)'])] 32 | public const ALLOW_JOINT_NOT_OPEN = 1821; 33 | 34 | #[Language(['分库配置错误, 未在储存数据中找到作为ID基因的字段 %s'])] 35 | public const GENE_COLUMN_NOT_FOUND = 1822; 36 | 37 | #[Language(['禁止更新分库依据字段,若允许请开启 cross_update 参数。该操作较危险,请谨慎。(建议自己实现跨库迁移功能)'])] 38 | public const OPEN_CROSS_UPDATE_TIP = 1823; 39 | 40 | #[Language(['记录插入失败,获取新ID失败'])] 41 | public const INSERT_FAILED_NO_ID = 1824; 42 | 43 | #[Language(['分库插入错误,未配置idTag时需手动指定待插入ID %s'])] 44 | public const INSERT_ID_NOT_SPECIFIED = 1825; 45 | 46 | #[Language(['分库依据字段 %s 未指定有效值'])] 47 | public const SHARDING_COLUMN_NOT_SPECIFIED = 1826; 48 | 49 | #[Language(['表 %s 中不存在分表依据字段 %s,或该字段有无效值,无法进行迁移更新'])] 50 | public const SHARDING_VALUE_NOT_SPECIFIED = 1827; 51 | 52 | #[Language(['跨库更新表 %s 时需指定idColumn字段 %s 的值'])] 53 | public const UP_ID_VALUE_NOT_SPECIFIED = 1828; 54 | 55 | #[Language(['跨库更新表 %s 时需指定shardingColumn字段 %s 的值'])] 56 | public const UP_SHARDING_VALUE_NOT_SPECIFIED = 1829; 57 | } 58 | -------------------------------------------------------------------------------- /engine/sharding/parser/mysql/statement/MysqlOrderByConditionParser.php: -------------------------------------------------------------------------------- 1 | field = $this->parseWithOffset(); 22 | $sort = $this->preParseWord(); 23 | if ($sort) { 24 | $sortUpper = strtoupper($sort); 25 | if (in_array($sortUpper, ['DESC', 'ASC'])) { 26 | $this->sort = $sortUpper; 27 | } else { 28 | $this->offset -= mb_strlen($sort); 29 | } 30 | } 31 | if ('ASC' !== $this->sort) { 32 | $this->isAsc = false; 33 | } 34 | } 35 | 36 | /** 37 | * 提取聚合函数 38 | * @return MysqlFunctionParser[] 39 | */ 40 | public function extractAggregates(): array { 41 | $aggregates = []; 42 | $aggregates = array_merge($aggregates, $this->field->extractAggregates()); 43 | return $aggregates; 44 | } 45 | 46 | public function toArray(): array { 47 | return [ 48 | 'type' => 'statement', 49 | 'statement' => 'order', 50 | 'field' => $this->field->toArray(), 51 | 'sort' => $this->sort, 52 | 'is_asc' => $this->isAsc, 53 | ]; 54 | } 55 | 56 | public function __toString(): string { 57 | $order = $this->field; 58 | if (! $this->isAsc) { 59 | $order .= "{$this->field} DESC"; 60 | } 61 | return $order; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/client/IdgClientTime.php: -------------------------------------------------------------------------------- 1 | timeId)) { 23 | throw new IdgException(IdgException::BASE_TIME_ID_MISSING); 24 | } 25 | } 26 | 27 | /** 28 | * 生成TimeId 29 | * @param IdgBatch $batch 30 | * @param int $base 31 | * @param int $nextBit 32 | * @return int 33 | */ 34 | protected static function generateBatch(IdgBatch $batch, int $base, int $nextBit): int { 35 | $base = parent::generateBatch($batch, $base, $nextBit); 36 | // uid左移service比特位 (非bc库还是够用100年的吧) 37 | $base += $batch->timeId << ($nextBit + $batch->batchBitWidth); 38 | return $base; 39 | } 40 | 41 | /** 42 | * 解析TimeId 43 | * @param int $id 44 | * @return IdgBatch 45 | */ 46 | public function parse(int $id): IdgBatch { 47 | $batch = parent::parse($id); 48 | $unParsedId = $batch->batchId; 49 | if (! empty($this->batch->batchBitWidth)) { 50 | $batch->batchId = self::parsePart($unParsedId, $this->batch->batchBitWidth); 51 | } 52 | // TimeId为解析后剩余部分 53 | $batch->timeId = $unParsedId; 54 | return $batch; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /engine/base/Lock.php: -------------------------------------------------------------------------------- 1 | procLockInit(); 14 | $this->distributedLockInit(); 15 | } 16 | 17 | /** 18 | * 跨进程协程锁初始化 (实例化跨进程对象) 19 | */ 20 | protected function procLockInit(): void {} 21 | 22 | /** 23 | * 加跨进程协程锁 (悲观, 自旋, 不可重入) 24 | * @param string $identification 锁标识 25 | * @param int $maximum 跨进程可重入次数 26 | * @return bool 27 | */ 28 | public function procLock(string $identification, int $maximum = 1): bool { 29 | return true; 30 | } 31 | 32 | /** 33 | * 解跨进程协程锁 34 | * @param string $identification 35 | */ 36 | public function procUnlock(string $identification): void {} 37 | 38 | /** 39 | * 加协程锁 (悲观, 自旋, 不可重入) 40 | * @param string $identification 锁标识 41 | * @param int $maximum 跨进程可重入次数 42 | * @return bool 43 | */ 44 | public function coLock(string $identification, int $maximum = 1): bool { 45 | return true; 46 | } 47 | 48 | /** 49 | * 解协程锁 50 | * @param string $identification 51 | */ 52 | public function coUnlock(string $identification): void {} 53 | 54 | /** 55 | * 分布式锁初始化 56 | */ 57 | protected function distributedLockInit(): void {} 58 | 59 | /** 60 | * 分布式锁加锁 61 | * @return bool 62 | */ 63 | public function distributedLock(): bool { 64 | return true; 65 | } 66 | 67 | /** 68 | * 分布式锁解锁 69 | */ 70 | public function distributedUnlock(): void {} 71 | 72 | /** 73 | * 实例化一个并发锁对象 74 | * @return static 75 | */ 76 | public static function init(): static { 77 | $lockClass = Dce::$config->lockClass ?? (SwooleUtility::inSwoole() ? SwooleLock::class : self::class); 78 | return new $lockClass; 79 | } 80 | } -------------------------------------------------------------------------------- /engine/db/query/builder/schema/OrderSchema.php: -------------------------------------------------------------------------------- 1 | format(self::printable($condition[0] ?? '')); 26 | $order = strtoupper(trim($condition[1] ?? '')); 27 | $this->pushCondition($column . (in_array($order, ['ASC', 'DESC']) ? " {$order}" : '')); 28 | $isRaw && $this->mergeParams($column->getParams()); 29 | } else if (($isRaw = $condition instanceof RawBuilder) || is_string($condition) && $column = self::tableWrap($condition)) { 30 | $this->pushCondition($isRaw ? $condition : $column); 31 | $isRaw && $this->mergeParams($condition->getParams()); 32 | } else { 33 | throw (new QueryException(QueryException::ORDER_BY_INVALID))->format(self::printable($condition)); 34 | } 35 | } 36 | } 37 | 38 | public function __toString(): string { 39 | return implode(',', $this->getConditions()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /engine/sharding/parser/mysql/MysqlListParser.php: -------------------------------------------------------------------------------- 1 | extractAggregates()); 23 | return $aggregates; 24 | } 25 | 26 | public function current(): MysqlParser { 27 | return $this->getQueue()[$this->queueOffset]; 28 | } 29 | 30 | public function next(): void { 31 | $this->queueOffset ++; 32 | } 33 | 34 | public function key(): int { 35 | return $this->queueOffset; 36 | } 37 | 38 | public function valid(): bool { 39 | return $this->queueOffset < count($this->getQueue()); 40 | } 41 | 42 | public function rewind(): void { 43 | $this->queueOffset = 0; 44 | } 45 | 46 | public function delItem(int $offset, int $length = 1): void { 47 | array_splice($this->getQueue(), $offset, $length); 48 | } 49 | 50 | /** 51 | * @return MysqlParser[] 52 | */ 53 | private function & getQueue(): array { 54 | $property = static::queueProperty(); 55 | return $this->$property; 56 | } 57 | 58 | public static function build(string $statement, int & $offset = 0): static { 59 | $instance = new static($statement, $offset); 60 | $instance->parse(); 61 | return $instance; 62 | } 63 | 64 | abstract protected function parse(): void; 65 | 66 | abstract public function addItem(MysqlParser $item): void; 67 | 68 | abstract protected static function queueProperty(): string; 69 | } 70 | -------------------------------------------------------------------------------- /engine/db/query/builder/schema/TableSchema.php: -------------------------------------------------------------------------------- 1 | tablePack($tableName); 17 | $this->mergeParams($params); 18 | $this->pushCondition([ 19 | 'table' => $tableSql, 20 | 'alias' => self::tableWrap($alias), 21 | ]); 22 | return $this; 23 | } 24 | 25 | public function tablePack(string|RawBuilder|SelectStatement $table) { 26 | $params = []; 27 | if (is_string($table) && $tableName = self::tableWrap($table)) { 28 | $tableSql = $tableName; 29 | } else if ($table instanceof RawBuilder) { 30 | $tableSql = "$table"; 31 | $params = $table->getParams(); 32 | } else if ($table instanceof SelectStatement) { 33 | $tableSql = "($table)"; 34 | $params = $table->getParams(); 35 | $this->logHasSubQuery(true); 36 | } else { 37 | throw new QueryException(QueryException::TABLE_INVALID); 38 | } 39 | return [$tableSql, $params]; 40 | } 41 | 42 | public function __toString(): string { 43 | $tableParts = []; 44 | foreach ($this->getConditions() as $table) { 45 | $tableParts[] = $table['table'] . ($table['alias'] ? " AS {$table['alias']}" : ''); 46 | } 47 | $tableSql = implode(',', $tableParts); 48 | return $tableSql; 49 | } 50 | 51 | public function getName(): string { 52 | return trim($this->getConditions()[0]['table'] ?? '', ' `'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /engine/model/validator/checker/NumberValidator.php: -------------------------------------------------------------------------------- 1 | addError($this->getGeneralError(null, lang(ValidatorException::INVALID_NUMBER))); 30 | } else { 31 | $negative = $this->getProperty('negative'); 32 | if (! ($negative->value ?? null) && $value < 0) { 33 | $this->addError($this->getGeneralError($negative->error ?? null, lang(ValidatorException::CANNOT_BE_NEGATIVE))); 34 | } 35 | 36 | $decimal = $this->getProperty('decimal'); 37 | if (! ($decimal->value ?? null) && ! ctype_digit(ltrim((string) $value, '-'))) { 38 | $this->addError($this->getGeneralError($decimal->error ?? null, lang(ValidatorException::CANNOT_BE_FLOAT))); 39 | } 40 | 41 | $min = $this->getProperty('min'); 42 | if ($min && $value < $min->value) { 43 | $this->addError($this->getGeneralError($min->error, lang(ValidatorException::CANNOT_SMALL_THAN))); 44 | } 45 | 46 | $max = $this->getProperty('max'); 47 | if ($max && $value > $max->value) { 48 | $this->addError($this->getGeneralError($max->error, lang(ValidatorException::CANNOT_LARGE_THAN))); 49 | } 50 | } 51 | 52 | return $this->getError(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /engine/db/query/builder/RawBuilder.php: -------------------------------------------------------------------------------- 1 | $placeholder) { 25 | if ('?' === $placeholder) { 26 | $isNamingPlaceholder = false; 27 | } else { 28 | $isQuestionPlaceholder = false; 29 | $k = $placeholder; 30 | $statement = preg_replace("/$placeholder\b/", '?', $statement, 1); 31 | } 32 | if (! key_exists($k, $params) && (($k = ltrim($k, ':')) && ! key_exists($k, $params))) { 33 | throw (new QueryException(QueryException::PLACEHOLDER_NOT_MATCH))->format($k); 34 | } 35 | $convertedParams[] = $params[$k]; 36 | } 37 | if (! $isQuestionPlaceholder && ! $isNamingPlaceholder) { 38 | throw new QueryException(QueryException::NOT_ALLOW_MIXED_PLACEHOLDER); 39 | } 40 | $this->mergeParams($convertedParams); 41 | } 42 | $this->statement = $statement; 43 | $this->autoParenthesis = $autoParenthesis; 44 | } 45 | 46 | protected function valid(): void {} 47 | 48 | public function __toString(): string { 49 | return sprintf($this->autoParenthesis ? '(%s)' : '%s', $this->statement); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /engine/pool/TcpPool.php: -------------------------------------------------------------------------------- 1 | port ?? 0) < 1 ? SWOOLE_SOCK_UNIX_STREAM : SWOOLE_SOCK_TCP); 19 | if (! $connection->connect($config->host, $config->port ?? 0, $config->timeout ?? -1)) { 20 | throw new PoolException($connection->errMsg, $connection->errCode); 21 | } 22 | $this->initTick($connection); 23 | return $connection; 24 | } 25 | 26 | private function initTick(Client $connection): void { 27 | $this->tickMapping[spl_object_id($connection)] = Timer::tick(30000, function() use($connection) { 28 | $lastFetchTime = $this->getProduct($connection)->lastFetch ?? 0; 29 | if (time() - $lastFetchTime >= 28) { 30 | // 如果到了闹铃点, 且距上次取连接已过了闹铃间隔时间(未过则不必发, 因为取连接则肯定用来send过业务数据了, send数据能覆盖ping功能), 则发送ping包 31 | $connection->send(0); 32 | } 33 | }); 34 | } 35 | 36 | public function destroyProduct(object $object): void { 37 | $objId = spl_object_id($object); 38 | Timer::clear($this->tickMapping[$objId]); 39 | unset($this->tickMapping[$objId]); 40 | } 41 | 42 | /** @inheritDoc */ 43 | public function fetch(array $config = []): Client { 44 | return $this->get($config); 45 | } 46 | 47 | /** 48 | * 取池子实例 49 | * @param string ...$identities 50 | * @return static 51 | */ 52 | public static function inst(string ... $identities): static { 53 | return parent::getInstance(TcpPoolProductionConfig::class, ... $identities); 54 | } 55 | 56 | protected function retryable(Throwable $throwable): bool { 57 | return false; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /engine/project/Project.php: -------------------------------------------------------------------------------- 1 | path, ProjectManager::$systemProjectRoot) && $this->isSystematic = true; 34 | } 35 | 36 | public function setNodeTree(NodeTree $nodeTree) { 37 | $this->nodeTree = $nodeTree; 38 | } 39 | 40 | /** 41 | * 取项目配置, 若配置为初始化过, 则先初始化 42 | * @return DceConfig 43 | */ 44 | public function getConfig(): DceConfig { 45 | if (! isset($this->config)) { 46 | $this->config = ConfigManager::getProjectConfig($this); 47 | } 48 | return $this->config; 49 | } 50 | 51 | /** 52 | * 取未合并公共配置的项目配置 53 | * @return DceConfig 54 | */ 55 | public function getPureConfig(): DceConfig { 56 | if (! isset($this->pureConfig)) { 57 | $this->pureConfig = ConfigManager::getPureProjectConfig($this); 58 | } 59 | return $this->pureConfig; 60 | } 61 | 62 | /** 63 | * 取项目根节点 64 | * @return Node 65 | */ 66 | public function getRootNode(): Node { 67 | return $this->nodeTree->getFirstNode(); 68 | } 69 | 70 | /** 71 | * 扩展属性 72 | * @param string $key 73 | * @param mixed $value 74 | */ 75 | public function extendProperty(string $key, mixed $value) { 76 | $this->extra[$key] = $value; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /engine/loader/attr/SingMatrix.php: -------------------------------------------------------------------------------- 1 | |callable $class 17 | * @param mixed ...$args 18 | * @return T 19 | */ 20 | public static function gen(string|callable $class, mixed ... $args): mixed { 21 | $isCallable = is_callable($class); 22 | $id = static::genInstanceId($isCallable ? array_shift($args) : $class, $args); 23 | return key_exists($id, static::$mapping) ? static::$mapping[$id] : static::logInstance($id, $isCallable ? call_user_func($class) : self::new($class, ... $args)); 24 | } 25 | 26 | /** 27 | * @template T 28 | * @param string $id 29 | * @param T $instance 30 | * @return T 31 | */ 32 | public static function logInstance(string $id, mixed $instance): mixed { 33 | return static::$mapping[$id] = $instance; 34 | } 35 | 36 | /** 37 | * @template T 38 | * @param class-string $class 39 | * @param mixed ...$args 40 | * @return T|string 41 | */ 42 | public static function generated(string $class, mixed ... $args): mixed { 43 | $id = static::genInstanceId($class, $args); 44 | return key_exists($id, static::$mapping) ? static::$mapping[$id] : $id; 45 | } 46 | 47 | public static function destroy(string $class, mixed ... $args): void { 48 | unset(static::$mapping[static::genInstanceId($class, $args)]); 49 | } 50 | 51 | protected abstract static function genInstanceId(string $class, array $args): string; 52 | 53 | /** 54 | * @template T 55 | * @param class-string $class 56 | * @param mixed ...$args 57 | * @return T 58 | */ 59 | public static function new(string $class, mixed ... $args): mixed { 60 | return new $class(... $args); 61 | } 62 | } -------------------------------------------------------------------------------- /engine/cache/engine/RedisCache.php: -------------------------------------------------------------------------------- 1 | config['index'])->get(self::genKey($key)); 21 | } 22 | 23 | /** @inheritDoc */ 24 | public function exists(string|array $key): bool { 25 | return RedisProxy::new($this->config['index'])->exists(self::genKey($key)); 26 | } 27 | 28 | /** @inheritDoc */ 29 | public function set(array|string $key, mixed $value, int $expiry = null): bool { 30 | return RedisProxy::new($this->config['index'])->set(self::genKey($key), $value, $expiry); 31 | } 32 | 33 | public function touch(array|string $key, int $expiry = 0): bool { 34 | return RedisProxy::new($this->config['index'])->expire(self::genKey($key), $expiry); 35 | } 36 | 37 | /** @inheritDoc */ 38 | public function inc(array|string $key, float $value = 1): int|float|false { 39 | return RedisProxy::new($this->config['index'])->incrBy(self::genKey($key), $value); 40 | } 41 | 42 | /** @inheritDoc */ 43 | public function dec(array|string $key, float $value = 1): int|float|false { 44 | return RedisProxy::new($this->config['index'])->decrBy(self::genKey($key), $value); 45 | } 46 | 47 | /** @inheritDoc */ 48 | public function del(array|string $key): bool { 49 | return !! RedisProxy::new($this->config['index'])->del(self::genKey($key)); 50 | } 51 | 52 | /** @inheritDoc */ 53 | public function clear(): void { 54 | $redis = RedisProxy::new($this->config['index']); 55 | // 直接删掉和当前主机应用绑定的, 几乎都是缓存, 非缓存的全服型数据不会与应用ID绑定 56 | foreach ($redis->keys(Dce::getId() . ':*') as $key) $redis->del($key); 57 | } 58 | } -------------------------------------------------------------------------------- /project/dce/service/server/ServerApi.php: -------------------------------------------------------------------------------- 1 | stop(); 29 | } 30 | 31 | /** 32 | * 重载服务 33 | */ 34 | public static function reload(): void { 35 | static::$server->reload(); 36 | } 37 | 38 | /** 39 | * 获取服务状态 40 | * @return array 41 | */ 42 | public static function status(): array { 43 | return static::$server->status(); 44 | } 45 | 46 | /** 47 | * 向Tcp客户端推送消息 48 | * @param int $fd 49 | * @param mixed $value 50 | * @param string|false $path 51 | * @return bool 52 | */ 53 | public static function send(int $fd, mixed $value, string|false $path): bool { 54 | return static::$server->send($fd, $value, $path); 55 | } 56 | 57 | /** 58 | * 向Udp客户端推送消息 59 | * @param string $host 60 | * @param int $port 61 | * @param mixed $value 62 | * @param string|false $path 63 | * @return bool 64 | */ 65 | public static function sendTo(string $host, int $port, mixed $value, string|false $path): bool { 66 | return static::$server->sendTo($host, $port, $value, $path); 67 | } 68 | 69 | /** 70 | * 向Websocket客户端推送消息 71 | * @param int $fd 72 | * @param mixed $value 73 | * @param string|false $path 74 | * @return bool 75 | */ 76 | public static function push(int $fd, mixed $value, string|false $path): bool { 77 | return self::$server->push($fd, $value, $path); 78 | } 79 | } -------------------------------------------------------------------------------- /project/tcp/service/RawRequestUdp.php: -------------------------------------------------------------------------------- 1 | clientInfo['ip'] = $this->clientInfo['address']; 23 | $this->clientInfo['packet'] = $packet; 24 | } 25 | 26 | /** @inheritDoc */ 27 | public function getServer(): ServerMatrix { 28 | return $this->server; 29 | } 30 | 31 | /** @inheritDoc */ 32 | public function getRaw(): array { 33 | return $this->clientInfo; 34 | } 35 | 36 | /** @inheritDoc */ 37 | public function init(): void { 38 | ['path' => $this->path, 'requestId' => $this->requestId, 'data' => $this->rawData, 'dataParsed' => $this->dataParsed] = $this->unPack($this->clientInfo['packet']); 39 | } 40 | 41 | /** @inheritDoc */ 42 | public function supplementRequest(Request $request): void { 43 | $request->rawData = $this->rawData; 44 | $request->request = is_array($this->dataParsed) ? $this->dataParsed : []; 45 | } 46 | 47 | /** @inheritDoc */ 48 | public function response(mixed $data, string|false $path): bool { 49 | LogManager::response($this, $data); 50 | return $this->server->sendTo($this->clientInfo['address'], $this->clientInfo['port'], $data, $path . (isset($this->requestId) ? self::REQUEST_SEPARATOR . $this->requestId : '')); 51 | } 52 | 53 | /** @inheritDoc */ 54 | public function getClientInfo(): array { 55 | $this->clientInfo['request'] = "$this->method :{$this->clientInfo['server_port']}/$this->path#" . ($this->requestId ?? ''); 56 | return $this->clientInfo; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /engine/sharding/parser/mysql/list/MysqlColumnParser.php: -------------------------------------------------------------------------------- 1 | preParseModifier()) && $this->modifiers[] = $modifier; 22 | 23 | while ($this->offset < $this->statementLength) { 24 | $this->columns[] = MysqlColumnItemParser::build($this->statement, $this->offset); 25 | $nextSeparator = $this->preParseOperator(); 26 | if (! in_array($nextSeparator, self::$paramSeparators)) break; 27 | } 28 | } 29 | 30 | /** @return MysqlColumnItemParser */ 31 | public function current(): MysqlColumnItemParser { 32 | return parent::current(); 33 | } 34 | 35 | public function addItem(MysqlParser $item): void { 36 | $item instanceof MysqlColumnItemParser && $this->columns[] = $item; 37 | } 38 | 39 | public function toArray(): array { 40 | $columnsToArray = []; 41 | foreach ($this->columns as $column) 42 | $columnsToArray[] = $column->toArray(); 43 | return [ 44 | 'type' => 'list', 45 | 'class' => 'column', 46 | 'modifiers' => $this->modifiers, 47 | 'columns' => $columnsToArray, 48 | ]; 49 | } 50 | 51 | public function __toString(): string { 52 | $modifiers = implode(' ', $this->modifiers); 53 | $columns = implode(',', $this->columns); 54 | $modifiers && $columns = "{$modifiers} {$columns}"; 55 | return $columns; 56 | } 57 | 58 | protected static function queueProperty(): string { 59 | return 'columns'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /project/dce/service/cron/TaskIterator.php: -------------------------------------------------------------------------------- 1 | $supplier 15 | * @param callable(T, string|int, int): void $consumer 16 | * @param callable(T, string|int): void $progressLogger 17 | * @param string|int $refCursor 18 | */ 19 | public static function batchIterate(callable $supplier, callable $consumer, callable $progressLogger, string|int & $refCursor): void { 20 | $prevHash = null; 21 | // 若取到与前一轮一样的数据,则表示已取完,需退出循环 22 | while (($calcList = call_user_func_array($supplier, [$refCursor])) && $prevHash !== $currentHash = json_encode($calcList[$lastKey = array_key_last($calcList)])) { 23 | $prevHash = $currentHash; 24 | call_user_func_array($consumer, [$calcList, & $refCursor, $lastKey]); 25 | call_user_func_array($progressLogger, [$calcList[$lastKey], & $refCursor]); 26 | } 27 | } 28 | 29 | /** 30 | * @template T 31 | * @param callable(string|int): list $supplier 32 | * @param callable(T, string|int): void $consumer 33 | * @param callable(T, string|int): void $progressLogger 34 | * @param string|int $refCursor 35 | * @param int $stepModToLog 36 | */ 37 | public static function batchStepIterate(callable $supplier, callable $consumer, callable $progressLogger, string|int & $refCursor, int $stepModToLog = 16): void { 38 | self::batchIterate($supplier, function(array $calcList, string|int & $refCursor, int $lastKey) use($consumer, $progressLogger, $stepModToLog) { 39 | foreach ($calcList as $k => $item) { 40 | call_user_func_array($consumer, [$item, & $refCursor]); 41 | (($k && ! ($k % $stepModToLog)) && $k !== $lastKey) && call_user_func_array($progressLogger, [$item, & $refCursor]); 42 | } 43 | }, $progressLogger, $refCursor); 44 | } 45 | } -------------------------------------------------------------------------------- /engine/db/query/builder/schema/SelectSchema.php: -------------------------------------------------------------------------------- 1 | $column) { 23 | $isAutoRaw && is_string($column) && $column = new RawBuilder($column, false); 24 | $this->addColumn($column, is_int($alias) ? null : $alias); 25 | } 26 | } 27 | 28 | public function addColumn(string|RawBuilder $column, string|null $alias = null) { 29 | $columnName = $column; 30 | if (! ($isRaw = $column instanceof RawBuilder) && ! is_numeric($column)) 31 | if (! $columnName = self::tableWrap($column, true)) 32 | throw (new QueryException(QueryException::SELECT_COLUMN_INVALID))->format(self::printable($column)); 33 | $columnName .= $alias ? " $alias" : ''; 34 | if (! in_array($columnName, $this->getConditions())) { 35 | $this->pushCondition($columnName); 36 | $isRaw && $this->mergeParams($column->getParams()); 37 | } 38 | } 39 | 40 | /** 41 | * 将查询字段转为查询结果的数组键名 42 | * @param string|RawBuilder $column 43 | * @return mixed 44 | */ 45 | public function columnToKey(string|RawBuilder $column) { 46 | if (! is_string($column) || ! $columnParts = self::tableNameParse($column)) return $column; 47 | [, $columnName, $alias] = $columnParts; 48 | return $alias ?: $columnName; 49 | } 50 | 51 | public function __toString(): string { 52 | return implode(',', $this->getConditions()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DCE 2 | 3 | Dce是一款基于PHP8开发的网络编程框架,支持传统Cgi式Web编程及命令行工具编程,也支持Swoole下常驻内存式Web编程与长连接服务器编程,并且设计了一套通用的RCR架构处理所有类型网络编程,让你的应用项目保持清晰整洁,助你轻松编写出易复用、好维护的代码。 4 | 5 | ![RCR架构流程图](https://drunkce.com/assets/img/rcr.728a5b53.svg) 6 | 7 | Dce还有很多特色功能,其中最为重量级的是 ~~分库中间件~~,可以让你轻松实现分库查询。除此之外还提供有负载均衡连接池、远程过程调用、ID生成器、并发锁、会话管理器等特色功能,这些功能依赖Swoole,Dce作者也强烈推荐你在Swoole下使用Dce,配合其多进程协程模式,可以将你的服务器性能发挥到极致。 8 | 9 | 当然,除了上述功能外,如模型、校验器、缓存器、事件、查询器、活动记录等这些常规的功能模块也必不可能缺少。 10 | 11 | 12 | 13 | ### 开始使用 14 | 15 | 16 | #### 获取 17 | 18 | ```shell 19 | composer create-project idrunk/dce-app:@dev 20 | ``` 21 | 22 | 取用例 23 | ```shell 24 | composer create-project idrunk/dce-app:dev-sample dce-sample 25 | ``` 26 | 27 | 28 | #### 使用命令行工具 29 | 30 | 执行一个空命令 31 | ```shell 32 | ./dce 33 | # 或者在windows下执行: 34 | .\dce.bat 35 | # 将响应: 36 | # 37 | # 你正在cli模式以空路径请求Dce接口 38 | ``` 39 | 40 | 在有Swoole的Linux下启动Websocket服务器 41 | ```shell 42 | dce websocket start 43 | # 将响应 44 | # Websocket server started with 0.0.0.0:20461. 45 | ``` 46 | 47 | JS连接Websocket服务 48 | ```js 49 | const ws = new WebSocket('ws://127.0.0.1:20461'); 50 | ws.onopen = () => ws.send(''); 51 | ws.onmessage = msg => console.log(msg.data); 52 | // 若连接成功,将在控制台打印出下述消息 53 | /* 54 | ;{"data":{"info":"恭喜!服务端收到了你的消息并给你作出了回应"}} 55 | */ 56 | ``` 57 | 58 | 59 | #### 使用Redis连接池 60 | 61 | RedisProxy会自动根据环境判定从实例池取还是新建连接,若使用实例池,则会自动取还。 62 | ```php 63 | RedisProxy::new()->set('homepage', 'https://drunkce.com'); 64 | ``` 65 | 66 | 67 | #### 数据库查询 68 | 69 | 分库查询需要进行分库规则配置,但查询方法和普通查询没区别,所以下述示例也适用于分库查询。 70 | ```php 71 | // 查一条 72 | $row = db('member')->where('mid', 4100001221441)->find(); 73 | // db方法为实例化查询器的快捷方法 74 | 75 | // 简单联合查询 76 | $list = db('member', 'm')->join('member_role', 'mr', 'mr.mid = m.mid')->select(); 77 | 78 | // 较复杂的嵌套条件查询 79 | $list = db('member')->where([ 80 | ['is_deleted', 0], 81 | ['register_time', 'between', ['2021-01-01', '2021-01-31 23:59:59']], 82 | [ 83 | ['level', '>', 60], 84 | 'or', 85 | ['vip', '>', 1], 86 | ], 87 | ['not exists', raw('select 1 from member_banned where mid = member.mid')], 88 | ])->select(); 89 | ``` 90 | 91 |
92 | 93 | 通过上述的简介,相信你对Dce已经有了一个初步认识,Dce的玩法远不止这些,你可以[点击这里](https://drunkce.com/guide/)继续深入了解。 -------------------------------------------------------------------------------- /engine/db/connector/DbPool.php: -------------------------------------------------------------------------------- 1 | connect($config->dbName, $config->host, $config->dbUser, $config->dbPassword, $config->dbPort, false); 26 | return $connector; 27 | } 28 | 29 | public function fetch(): DbConnector { 30 | return $this->get(); 31 | } 32 | 33 | /** 34 | * @param string ...$identities 35 | * @return static 36 | */ 37 | public static function inst(string ... $identities): static { 38 | $inst = parent::getInstance(DbPoolProductionConfig::class, ... $identities); 39 | $inst->dbAlias ??= $identities[0]; 40 | return $inst; 41 | } 42 | 43 | protected function retryable(Throwable $throwable): Throwable|bool { 44 | // 是否连接丢失的异常 45 | $result = $throwable instanceof PDOException && in_array($throwable->errorInfo[1] ?? 0, self::PdoDisconnectedCodes); 46 | 47 | // 若当前库开启了分库事务,且事务中成功发送过请求,则禁止重试连接 48 | if ($result && false !== Structure::arraySearch(function($shardingAlias) { 49 | $transaction = ShardingTransaction::aliasMatch($shardingAlias); 50 | $transaction && $transaction->clearBounds(); 51 | return $transaction->uses ?? 0 > 1; 52 | }, array_unique(array_column(isset(Dce::$config->sharding) 53 | ? Dce::$config->sharding->filter(fn($c) => key_exists($this->dbAlias, $c->mapping)) : [], 'alias'))) 54 | ) $result = new PoolException(PoolException::DISCONNECTED_TRANSACTION_ACTIVATED, previous: $throwable); 55 | 56 | return $result; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /engine/db/query/builder/schema/JoinSchema.php: -------------------------------------------------------------------------------- 1 | addTable($tableName, $alias); 18 | if ($on instanceof WhereSchema) { 19 | $whereSchema = $on; 20 | } else { 21 | if (! is_array($on)) { 22 | if (is_string($on)) { 23 | $on = [new RawBuilder($on)]; 24 | } else if ($on instanceof RawBuilder) { 25 | $on = [$on]; 26 | } else { 27 | $on = [[$on]]; 28 | } 29 | } 30 | $whereSchema = new WhereSchema(); 31 | $whereSchema->addCondition($on, false, false, 'AND'); 32 | } 33 | $typeUpper = strtoupper(trim($type)); 34 | if (! in_array($typeUpper, ['INNER', 'LEFT', 'RIGHT'])) { 35 | throw (new QueryException(QueryException::INNER_TYPE_INVALID))->format($typeUpper); 36 | } 37 | $this->mergeParams($tableSchema->getParams()); 38 | $this->mergeParams($whereSchema->getParams()); 39 | $this->pushCondition([ 40 | 'tableSchema' => $tableSchema, 41 | 'whereSchema' => $whereSchema, 42 | 'joinType' => $typeUpper, 43 | ]); 44 | } 45 | 46 | public function __toString(): string { 47 | $joinConditionSqlPack = []; 48 | foreach ($this->getConditions() as $condition) { 49 | $tableSchema = $condition['tableSchema']; 50 | $whereSchema = $condition['whereSchema']; 51 | $joinType = $condition['joinType']; 52 | $joinConditionSqlPack[] = "{$joinType} JOIN {$tableSchema}" . ($whereSchema->isEmpty() ? '' : " ON {$whereSchema}"); 53 | } 54 | return implode(' ', $joinConditionSqlPack); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /engine/i18n/Locale.php: -------------------------------------------------------------------------------- 1 | lang = $this->parseLang($request); 25 | $this->country = $this->parseCountry($request); 26 | } 27 | 28 | /** 29 | * 根据请求解析当前语言码 30 | * @param Request|null $request 31 | * @return string 32 | */ 33 | private function parseLang(Request|null $request): string { 34 | return isset(Dce::$config->app['lang_parse']) 35 | ? call_user_func(Dce::$config->app['lang_parse'], $request) 36 | : $request->request['lang'] ?? (isset($request->cookie) 37 | ? $request->cookie->get('lang') ?: Dce::$config->app['lang'] 38 | : $request->rawRequest->cookie['lang'] ?? Dce::$config->app['lang'] 39 | ); 40 | } 41 | 42 | /** 43 | * 解析用户国家码, 可以配置国家吗解析器解析, 也可以在控制器中手动解析再赋值到request->locale上 44 | * @param Request|null $request 45 | * @return string 46 | */ 47 | private function parseCountry(Request|null $request): string { 48 | return isset(Dce::$config->app['country_parse']) 49 | ? call_user_func(Dce::$config->app['country_parse'], $request) 50 | : Dce::$config->app['country']; 51 | } 52 | 53 | /** 54 | * 取当前请求客户端本地化参数 55 | * @return static 56 | */ 57 | public static function client(): self { 58 | $request = RequestManager::current(); 59 | return $request->locale ?? self::server(); 60 | } 61 | 62 | /** 63 | * 取服务器本地化参数 64 | * @return static 65 | */ 66 | public static function server(): self { 67 | static $instance; 68 | if (null === $instance) { 69 | $instance = new self; 70 | $instance->lang = Dce::$config->app['lang']; 71 | $instance->country = Dce::$config->app['country']; 72 | } 73 | return $instance; 74 | } 75 | } -------------------------------------------------------------------------------- /engine/db/query/builder/statement/InsertStatement.php: -------------------------------------------------------------------------------- 1 | tableSchema = $tableSchema; 20 | $this->insertSchema = $insertSchema; 21 | $this->updateOrIgnore = $updateOrIgnore || is_array($updateOrIgnore) ?: $updateOrIgnore; 22 | $this->valid(); 23 | $this->updateOrIgnore && $this->setConflictUpdate(is_array($updateOrIgnore) ? $updateOrIgnore : []); 24 | $this->mergeParams($this->insertSchema->getParams()); 25 | $this->logHasSubQuery($this->insertSchema->hasSubQuery()); 26 | } 27 | 28 | private function setConflictUpdate(array $excludeColumns): void { 29 | $this->updateColumns = array_filter($this->insertSchema->getColumns(), fn($c) => ! in_array($c, $excludeColumns), ARRAY_FILTER_USE_KEY); 30 | ! $this->updateColumns && throw new QueryException(QueryException::CONFLICT_UPDATE_COLUMNS_CANNOT_BE_EMPTY); 31 | } 32 | 33 | public function isBatch(): bool { 34 | return $this->insertSchema->isBatchInsert(); 35 | } 36 | 37 | public function getUpdateOrIgnore(): bool|null { 38 | return $this->updateOrIgnore; 39 | } 40 | 41 | public function __toString(): string { 42 | $insertSql = $this->updateOrIgnore === false ? 'INSERT IGNORE' : 'INSERT'; 43 | $updateSql = $this->updateOrIgnore ? ' AS EXCLUDED ON DUPLICATE KEY UPDATE ' . implode(', ', array_map(fn($c) => "$c=EXCLUDED.$c", $this->updateColumns)) : ''; 44 | return "$insertSql INTO $this->tableSchema $this->insertSchema$updateSql"; 45 | } 46 | 47 | protected function valid(): void { 48 | $this->tableSchema->isEmpty() && throw new QueryException(QueryException::INSERT_TABLE_NOT_SPECIFIED); 49 | $this->insertSchema->isEmpty() && throw new QueryException(QueryException::NO_INSERT_DATA); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /engine/project/ProjectManager.php: -------------------------------------------------------------------------------- 1 | projectPaths ?? [] 29 | ] as $path 30 | ) { 31 | ! is_dir($path) && throw (new ProjectException(ProjectException::PROJECT_PATH_INVALID))->format($path); 32 | $lastPathChar = substr($path, -1); 33 | if ($lastPathChar === '/') { // 以斜杠结尾的, 将扫描该路径下的目录作为项目 34 | // 应用目录下所有子目录均视为项目 35 | $projectPaths = [... $projectPaths, ... glob($path . '*', GLOB_ONLYDIR) ?: []]; 36 | } else { // 非以斜杠结尾的, 则直接视为项目目录 37 | $projectPaths[] = $path; 38 | } 39 | } 40 | foreach ($projectPaths as $projectPath) { 41 | $projectName = pathinfo($projectPath, PATHINFO_FILENAME); 42 | $projects[$projectName] = new Project($projectName, realpath($projectPath) . '/'); 43 | // 预加载项目类库 44 | Loader::prepareProject($projects[$projectName]); 45 | } 46 | self::$projects = $projects; 47 | } 48 | 49 | /** 50 | * 取指定项目 51 | * @param string $projectName 52 | * @return Project|null 53 | */ 54 | public static function get(string $projectName): Project|null { 55 | return self::$projects[$projectName] ?? null; 56 | } 57 | 58 | /** 59 | * 取所有项目 60 | * @param bool|null $justSystematic 61 | * @return Project[] 62 | */ 63 | public static function getAll(bool $justSystematic = null): array { 64 | return null === $justSystematic ? self::$projects : array_filter(self::$projects, fn($project) => $project->isSystematic === $justSystematic); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /engine/sharding/parser/mysql/MysqlValueParser.php: -------------------------------------------------------------------------------- 1 | value = $value; 29 | } 30 | 31 | public function toArray(): array { 32 | return [ 33 | 'type' => 'value', 34 | 'value' => $this->value, 35 | ]; 36 | } 37 | 38 | public function __toString(): string { 39 | $value = $this->isNumeric ? $this->value : "'{$this->value}'"; 40 | return $value; 41 | } 42 | 43 | public static function build(string $statement, int|null & $offset, string $operator): self|null { 44 | $instance = new self($statement, $offset); 45 | if ($instance->detect($operator)) { 46 | // 解析引号字符串 47 | $string = $instance->parseString($operator); 48 | $instance->parse($string); 49 | } else if (self::isMinus($operator)) { 50 | // 解析负数 51 | $value = $instance->preParseWord(); 52 | if (! is_numeric($value)) { 53 | throw new StatementParserException(StatementParserException::INVALID_NUMBER_AFTER_MINUS); 54 | } 55 | $instance->isNumeric = true; 56 | $instance->parse(- $value); 57 | } else { 58 | return null; 59 | } 60 | return $instance; 61 | } 62 | 63 | public static function buildByWord(string $statement, int|null & $offset, string $value): self|null { 64 | $instance = new self($statement, $offset); 65 | if ($instance->detectByWord($value)) { 66 | $instance->parse($value); 67 | $instance->isNumeric = true; 68 | return $instance; 69 | } 70 | return null; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /engine/project/request/RequestManager.php: -------------------------------------------------------------------------------- 1 | [] 请求映射表 */ 19 | private static array $requestMapping = []; 20 | 21 | /** 22 | * 实例化请求器并路由 23 | * @param class-string $rawRequestClass 24 | * @param mixed ...$rawRequestParams 25 | * @return Request 26 | * @throws RequestException 27 | * @throws Throwable 28 | */ 29 | public static function route(string $rawRequestClass, mixed ... $rawRequestParams): Request { 30 | $rawRequest = new $rawRequestClass(... $rawRequestParams); 31 | $rawRequest->init(); 32 | $request = new Request($rawRequest); 33 | self::logMapping($request); 34 | $request->route(); 35 | return $request; 36 | } 37 | 38 | /** 尝试清理过期请求的映射记录 */ 39 | private static function logMapping(Request $request): void { 40 | self::$requestMapping[$request->id] = WeakReference::create($request); 41 | // 每隔一定轮数清理一次过期请求的残留映射记录 42 | if (! (count(self::$requestMapping) % self::CLEAR_MOD)) { 43 | foreach (self::$requestMapping as $k => $requestRef) 44 | if (! $requestRef->get()) unset(self::$requestMapping[$k]); 45 | } 46 | } 47 | 48 | /** 49 | * 取当前请求ID 50 | * - CGI模式时, 一个进程处理一个请求, 控制器间不会干扰, 无需为请求分配唯一ID, 返回0 51 | * - 非携程模式时, 请求间不会并行执行控制器, 也不会互相干扰, 无需唯一ID, 返回0或-1 52 | * - Swoole协程模式时, 请求控制器会并行执行, 可能会相互干扰产生污染, 但他们拥有不同的根协程ID, 可以以此作为其ID隔离出安全沙盒 53 | * @return int 54 | */ 55 | public static function currentId(): int { 56 | $requestId = 0; 57 | if (SwooleUtility::inSwoole() && ($requestId = Coroutine::getCid()) > 0) { 58 | while (($pcid = Coroutine::getPcid($requestId)) > 0) 59 | $requestId = $pcid; 60 | } 61 | return $requestId; 62 | } 63 | 64 | /** 65 | * 取当前的请求对象 66 | * @return Request|null 67 | */ 68 | public static function current(): Request|null { 69 | return ($requestRef = self::$requestMapping[self::currentId()] ?? null) ? $requestRef->get() : null; 70 | } 71 | } -------------------------------------------------------------------------------- /project/tcp/service/RawRequestTcp.php: -------------------------------------------------------------------------------- 1 | isConnecting = true; 30 | $this->connection = $data; 31 | $data = null; 32 | } else { 33 | $this->connection = Connection::from($fd); 34 | } 35 | $this->raw = [ 36 | 'fd' => $fd, 37 | 'reactor_id' => $reactorId, 38 | 'data' => $data, 39 | ]; 40 | } 41 | 42 | /** @inheritDoc */ 43 | public function getServer(): ServerMatrix { 44 | return $this->server; 45 | } 46 | 47 | /** @inheritDoc */ 48 | public function getRaw(): array { 49 | return $this->raw; 50 | } 51 | 52 | /** @inheritDoc */ 53 | public function init(): void { 54 | ['path' => $this->path, 'requestId' => $this->requestId, 'data' => $this->rawData, 'dataParsed' => $this->dataParsed] = $this->isConnecting 55 | ? ['path' => $this->connection->initialNode->pathFormat, 'requestId' => null, 'data' => '', 'dataParsed' => null] : $this->unPack($this->raw['data']); 56 | } 57 | 58 | /** @inheritDoc */ 59 | public function supplementRequest(Request $request): void { 60 | $request->fd = $this->fd; 61 | $request->rawData = $this->rawData; 62 | $request->request = is_array($this->dataParsed) ? $this->dataParsed : []; 63 | $request->session = $this->connection->session; 64 | } 65 | 66 | /** @inheritDoc */ 67 | public function response(mixed $data, string|false $path): bool { 68 | LogManager::response($this, $data); 69 | return $this->server->send($this->raw['fd'], $data, $path . (isset($this->requestId) ? self::REQUEST_SEPARATOR . $this->requestId : '')); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /engine/project/request/Pageable.php: -------------------------------------------------------------------------------- 1 | */ 20 | public array $list = []; 21 | 22 | public function __construct( 23 | public array $filters = [], 24 | int $page = 0, 25 | int $pageSize = 0, 26 | ){ 27 | $pageSize < 1 && $pageSize = $filters['pageSize'] ?? 0; 28 | $pageSize < 1 && $pageSize = 20; 29 | $this->pageSize = $pageSize; 30 | $page < 1 && $page = $filters['page'] ?? 0; 31 | $page < 1 ? $this->setOffset($filters['offset'] ?? 0) : $this->setPage($page); 32 | } 33 | 34 | public function setPage(int $page): void { 35 | $this->page = $page; 36 | $this->offset = ($page - 1) * $this->pageSize; 37 | } 38 | 39 | public function setOffset(int $offset): void { 40 | $this->page = floor($offset / $this->pageSize) + 1; 41 | $this->offset = $offset; 42 | } 43 | 44 | public function setTotal(int $total): void { 45 | $this->total = $total; 46 | $this->totalPage = ceil($total / $this->pageSize); 47 | } 48 | 49 | /** @param array $list */ 50 | public function setList(array $list): void { 51 | $this->list = $list; 52 | } 53 | 54 | public function setExt(string $key, mixed $value): void { 55 | $this->extAttrs[$key] = $value; 56 | } 57 | 58 | public function setAttr(string $key, mixed $value): void { 59 | $this->rootAttrs[$key] = $value; 60 | } 61 | 62 | public function getAttr(string $key): mixed { 63 | return $this->rootAttrs[$key]; 64 | } 65 | 66 | public function hasAttr(string $key): bool { 67 | return key_exists($key, $this->rootAttrs); 68 | } 69 | 70 | public function build(array $list = []): array { 71 | $resp = $this->rootAttrs; 72 | isset($this->page) && $resp['page'] = $this->page; 73 | isset($this->total) && $resp['total'] = $this->total; 74 | $resp['list'] = $list ?: array_map(fn($ar) => $ar->extract(), $this->list); 75 | return $resp; 76 | } 77 | } -------------------------------------------------------------------------------- /engine/pool/PoolProduct.php: -------------------------------------------------------------------------------- 1 | mapping[$id])) { 40 | throw new PoolException(PoolException::INVALID_CHANNEL_INSTANCE); 41 | } 42 | return $this->mapping[$id]; 43 | } 44 | 45 | /** 46 | * 映射标记 47 | * @param object $object 48 | * @param PoolProductionConfig $config 49 | * @param ChannelAbstract $channel 50 | */ 51 | public function set(object $object, PoolProductionConfig $config, ChannelAbstract $channel): void { 52 | $id = spl_object_id($object); 53 | if (! key_exists($id, $this->mapping)) { 54 | $this->mapping[$id] = self::new()->setProperties([ 55 | 'product_ref' => WeakReference::create($object), 56 | 'config' => $config, 57 | 'channel' => $channel, 58 | ]); 59 | } 60 | } 61 | 62 | /** 63 | * 更新实例最后取出时间 64 | * @param object $object 65 | * @throws PoolException 66 | */ 67 | public function refresh(object $object): void { 68 | $this->get($object)->lastFetch = time(); 69 | } 70 | 71 | /** 72 | * 清除失效没回收的实例 73 | * @param PoolProductionConfig $config 74 | */ 75 | public function clear(PoolProductionConfig $config): void { 76 | foreach ($this->mapping as $id => $item) { 77 | if ($config === $item->config && ! $item->productRef->get()) { 78 | // 销毁实例减产量 79 | $item->config->destroy(); 80 | // 销毁实例退销量, (因为只有在生产消费率与生产率满时才进到此, 所以必定是有消费了未退还的实例, 所以此处可以安全退还) 81 | $item->config->return(); 82 | // 销毁实例相关映射 83 | unset($this->mapping[$id]); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /engine/sharding/id_generator/server/IdgServerProducer.php: -------------------------------------------------------------------------------- 1 | batchCount)) { 21 | // 如果未配置单次批量ID数, 则默认为8192 22 | $batch->batchCount = self::$default_batch_count; 23 | } 24 | // 如果未初始化过, 则初始化新批起始值为1 (自增型ID从1开始递增), 否则为前次截至值+1 25 | $batch->batchFrom = ($batch->batchTo ?? 0) + 1; 26 | $batch->batchTo = $batch->batchFrom + $batch->batchCount - 1; 27 | } 28 | 29 | /** 30 | * TimeId池生成器 31 | * @param IdgBatch $batch 32 | * @throws IdgException 33 | */ 34 | protected function produceTime(IdgBatch $batch): void { 35 | $time = time(); 36 | if (! isset($batch->timeId)) { 37 | $batch->timeId = $time; 38 | } 39 | if (! isset($batch->batchBitWidth)) { 40 | throw new IdgException(IdgException::BATCH_BIT_WIDTH_MISSING); 41 | } 42 | if (! isset($batch->batchCount)) { 43 | $batch->batchCount = self::$default_batch_count; 44 | } 45 | $maxBatchId = (1 << $batch->batchBitWidth) - 1; // 计算最大批次ID 46 | if (! isset($batch->batchTo)) { // 如果未初始化过, 则初始化新批起始值为0 47 | $batch->batchFrom = 0; 48 | } else if ($time > $batch->timeId) { // 如果距前次生产已过1秒 49 | $batch->timeId = $time; // 记录新的时间戳 50 | $batch->batchFrom = 0; // 初始化新批起始值为0 51 | } else { 52 | $batch->batchFrom += $batch->batchCount; // 初始化新批起始值为0 53 | if ($batch->batchFrom > $maxBatchId) { // 如果ID起始值超出范围, 则表示同一秒内生产了过多的ID 54 | $microTime = microtime(1); 55 | $nextLess = 1000000 * (ceil($microTime) - $microTime) + 777; // 算出距下一秒的剩余微妙数 56 | usleep($nextLess); // 阻塞到下一秒继续生产 (您也可以将单秒ID池调大) 57 | $this->produceTime($batch); 58 | return; 59 | } 60 | } 61 | $batch->batchTo = $batch->batchFrom + $batch->batchCount - 1; 62 | if ($batch->batchTo > $maxBatchId) { // 如果截止ID超出范围, 则重定义为最大有效ID 63 | $batch->batchTo = $maxBatchId; 64 | } 65 | } 66 | } 67 | --------------------------------------------------------------------------------