init
This commit is contained in:
52
.devcontainer/Dockerfile
Normal file
52
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
||||
# Dev Container Dockerfile
|
||||
#
|
||||
# @link https://www.hyperf.io
|
||||
# @document https://hyperf.wiki
|
||||
# @contact group@hyperf.io
|
||||
# @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
|
||||
FROM hyperf/hyperf:8.3-alpine-v3.19-swoole
|
||||
LABEL maintainer="Hyperf Developers <group@hyperf.io>" version="1.0" license="MIT" app.name="Hyperf"
|
||||
|
||||
##
|
||||
# ---------- env settings ----------
|
||||
##
|
||||
# --build-arg timezone=Asia/Shanghai
|
||||
ARG timezone
|
||||
|
||||
ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
|
||||
APP_ENV=dev \
|
||||
SCAN_CACHEABLE=(false)
|
||||
|
||||
# update
|
||||
RUN set -ex \
|
||||
# show php version and extensions
|
||||
&& php -v \
|
||||
&& php -m \
|
||||
&& php --ri swoole \
|
||||
# ---------- some config ----------
|
||||
&& cd /etc/php* \
|
||||
# - config PHP
|
||||
&& { \
|
||||
echo "upload_max_filesize=128M"; \
|
||||
echo "post_max_size=128M"; \
|
||||
echo "memory_limit=1G"; \
|
||||
echo "date.timezone=${TIMEZONE}"; \
|
||||
} | tee conf.d/99_overrides.ini \
|
||||
# - config timezone
|
||||
&& ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
|
||||
&& echo "${TIMEZONE}" > /etc/timezone \
|
||||
# ---------- clear works ----------
|
||||
&& rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
|
||||
&& echo -e "\033[42;37m Build Completed :).\033[0m\n"
|
||||
|
||||
WORKDIR /opt/www
|
||||
|
||||
# Composer Cache
|
||||
# COPY ./composer.* /opt/www/
|
||||
# RUN composer install --no-dev --no-scripts
|
||||
|
||||
COPY . /opt/www
|
||||
RUN composer install && php bin/hyperf.php
|
||||
|
||||
EXPOSE 9501
|
||||
7
.devcontainer/devcontainer.json
Normal file
7
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"build": {
|
||||
"context": "..",
|
||||
"dockerfile": "./Dockerfile"
|
||||
},
|
||||
"forwardPorts": [9501]
|
||||
}
|
||||
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
**
|
||||
!app/
|
||||
!bin/
|
||||
!config/
|
||||
!composer.*
|
||||
25
.env
Normal file
25
.env
Normal file
@@ -0,0 +1,25 @@
|
||||
APP_NAME=hyperf-
|
||||
APP_ENV=dev
|
||||
|
||||
DB_DRIVER=mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=hyperf_
|
||||
DB_USERNAME=hyperf_
|
||||
DB_PASSWORD=4cfDRXZSksn7npiP
|
||||
DB_CHARSET=utf8mb4
|
||||
DB_COLLATION=utf8mb4_unicode_ci
|
||||
DB_PREFIX=_
|
||||
|
||||
REDIS_HOST=localhost
|
||||
REDIS_AUTH=(null)
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
|
||||
JSON_RPC_HOST=0.0.0.0
|
||||
JSON_RPC_PORT=9610
|
||||
SWOOLE_WORKER_NUM=1
|
||||
|
||||
NACOS_HOST=192.168.28.199
|
||||
NACOS_PORT=8848
|
||||
NACOS_NAMESPACE=e42b853c-5195-478b-b5e3-6d49f6a45053
|
||||
22
.env.example
Normal file
22
.env.example
Normal file
@@ -0,0 +1,22 @@
|
||||
APP_NAME=hyperf-demo
|
||||
APP_ENV=dev
|
||||
|
||||
DB_DRIVER=mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=hyperf_demo
|
||||
DB_USERNAME=hyperf_demo
|
||||
DB_PASSWORD=4cfDRXZSksn7npiP
|
||||
DB_CHARSET=utf8mb4
|
||||
DB_COLLATION=utf8mb4_unicode_ci
|
||||
DB_PREFIX=dm_
|
||||
|
||||
REDIS_HOST=localhost
|
||||
REDIS_AUTH=(null)
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
|
||||
JSON_RPC_HOST=0.0.0.0
|
||||
JSON_RPC_PORT=9610
|
||||
|
||||
SWOOLE_WORKER_NUM=1
|
||||
54
.github/workflows/Dockerfile
vendored
Normal file
54
.github/workflows/Dockerfile
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# Default Dockerfile
|
||||
#
|
||||
# @link https://www.hyperf.io
|
||||
# @document https://hyperf.wiki
|
||||
# @contact group@hyperf.io
|
||||
# @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
|
||||
FROM hyperf/hyperf:8.3-alpine-v3.19-swoole
|
||||
LABEL maintainer="Hyperf Developers <group@hyperf.io>" version="1.0" license="MIT" app.name="Hyperf"
|
||||
|
||||
##
|
||||
# ---------- env settings ----------
|
||||
##
|
||||
# --build-arg timezone=Asia/Shanghai
|
||||
ARG timezone
|
||||
|
||||
ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
|
||||
APP_ENV=prod \
|
||||
SCAN_CACHEABLE=(true)
|
||||
|
||||
# update
|
||||
RUN set -ex \
|
||||
# show php version and extensions
|
||||
&& php -v \
|
||||
&& php -m \
|
||||
&& php --ri swoole \
|
||||
# ---------- some config ----------
|
||||
&& cd /etc/php* \
|
||||
# - config PHP
|
||||
&& { \
|
||||
echo "upload_max_filesize=128M"; \
|
||||
echo "post_max_size=128M"; \
|
||||
echo "memory_limit=1G"; \
|
||||
echo "date.timezone=${TIMEZONE}"; \
|
||||
} | tee conf.d/99_overrides.ini \
|
||||
# - config timezone
|
||||
&& ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
|
||||
&& echo "${TIMEZONE}" > /etc/timezone \
|
||||
# ---------- clear works ----------
|
||||
&& rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
|
||||
&& echo -e "\033[42;37m Build Completed :).\033[0m\n"
|
||||
|
||||
WORKDIR /opt/www
|
||||
|
||||
# Composer Cache
|
||||
# COPY ./composer.* /opt/www/
|
||||
# RUN composer install --no-dev --no-scripts
|
||||
|
||||
COPY . /opt/www
|
||||
RUN print "\n" | composer install -o && php bin/hyperf.php
|
||||
|
||||
EXPOSE 9501
|
||||
|
||||
ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"]
|
||||
12
.github/workflows/build.yml
vendored
Normal file
12
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Build Docker
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: cp -rf .github/workflows/Dockerfile . && docker build -t hyperf .
|
||||
25
.github/workflows/release.yml
vendored
Normal file
25
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
on:
|
||||
push:
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
name: Release
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
.buildpath
|
||||
.settings/
|
||||
.project
|
||||
*.patch
|
||||
.idea/
|
||||
.git/
|
||||
runtime/
|
||||
vendor/
|
||||
.phpintel/
|
||||
#.env
|
||||
.DS_Store
|
||||
.phpunit*
|
||||
*.cache
|
||||
.vscode/
|
||||
/phpstan.neon
|
||||
/phpunit.xml
|
||||
57
.gitlab-ci.yml
Normal file
57
.gitlab-ci.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
# usermod -aG docker gitlab-runner
|
||||
|
||||
stages:
|
||||
- build
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
PROJECT_NAME: hyperf
|
||||
REGISTRY_URL: registry-docker.org
|
||||
|
||||
build_test_docker:
|
||||
stage: build
|
||||
before_script:
|
||||
# - git submodule sync --recursive
|
||||
# - git submodule update --init --recursive
|
||||
script:
|
||||
- docker build . -t $PROJECT_NAME
|
||||
- docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:test
|
||||
- docker push $REGISTRY_URL/$PROJECT_NAME:test
|
||||
only:
|
||||
- test
|
||||
tags:
|
||||
- builder
|
||||
|
||||
deploy_test_docker:
|
||||
stage: deploy
|
||||
script:
|
||||
- docker stack deploy -c deploy.test.yml --with-registry-auth $PROJECT_NAME
|
||||
only:
|
||||
- test
|
||||
tags:
|
||||
- test
|
||||
|
||||
build_docker:
|
||||
stage: build
|
||||
before_script:
|
||||
# - git submodule sync --recursive
|
||||
# - git submodule update --init --recursive
|
||||
script:
|
||||
- docker build . -t $PROJECT_NAME
|
||||
- docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME
|
||||
- docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:latest
|
||||
- docker push $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME
|
||||
- docker push $REGISTRY_URL/$PROJECT_NAME:latest
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- builder
|
||||
|
||||
deploy_docker:
|
||||
stage: deploy
|
||||
script:
|
||||
- echo SUCCESS
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- builder
|
||||
92
.php-cs-fixer.php
Executable file
92
.php-cs-fixer.php
Executable file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
use PhpCsFixer\Config;
|
||||
use PhpCsFixer\Finder;
|
||||
|
||||
return (new Config())
|
||||
->setRiskyAllowed(true)
|
||||
->setRules([
|
||||
'@PSR2' => true,
|
||||
'@Symfony' => true,
|
||||
'@DoctrineAnnotation' => true,
|
||||
'@PhpCsFixer' => true,
|
||||
'header_comment' => [
|
||||
'comment_type' => 'PHPDoc',
|
||||
'header' => '', // $header,
|
||||
'separate' => 'none',
|
||||
'location' => 'after_declare_strict',
|
||||
],
|
||||
'array_syntax' => [
|
||||
'syntax' => 'short',
|
||||
],
|
||||
'list_syntax' => [
|
||||
'syntax' => 'short',
|
||||
],
|
||||
'concat_space' => [
|
||||
'spacing' => 'one',
|
||||
],
|
||||
'global_namespace_import' => [
|
||||
'import_classes' => true,
|
||||
'import_constants' => true,
|
||||
'import_functions' => null,
|
||||
],
|
||||
'blank_line_before_statement' => [
|
||||
'statements' => [
|
||||
'declare',
|
||||
],
|
||||
],
|
||||
'general_phpdoc_annotation_remove' => [
|
||||
'annotations' => [
|
||||
'author',
|
||||
],
|
||||
],
|
||||
'ordered_imports' => [
|
||||
'imports_order' => [
|
||||
'class', 'function', 'const',
|
||||
],
|
||||
'sort_algorithm' => 'alpha',
|
||||
],
|
||||
'single_line_comment_style' => [
|
||||
'comment_types' => [
|
||||
],
|
||||
],
|
||||
'yoda_style' => [
|
||||
'always_move_variable' => false,
|
||||
'equal' => false,
|
||||
'identical' => false,
|
||||
],
|
||||
'phpdoc_align' => [
|
||||
'align' => 'left',
|
||||
],
|
||||
'multiline_whitespace_before_semicolons' => [
|
||||
'strategy' => 'no_multi_line',
|
||||
],
|
||||
'constant_case' => [
|
||||
'case' => 'lower',
|
||||
],
|
||||
'class_attributes_separation' => true,
|
||||
'combine_consecutive_unsets' => true,
|
||||
'declare_strict_types' => true,
|
||||
'linebreak_after_opening_tag' => true,
|
||||
'lowercase_static_reference' => true,
|
||||
'no_useless_else' => true,
|
||||
'no_unused_imports' => true,
|
||||
'not_operator_with_successor_space' => true,
|
||||
'not_operator_with_space' => false,
|
||||
'ordered_class_elements' => true,
|
||||
'php_unit_strict' => false,
|
||||
'phpdoc_separation' => false,
|
||||
'single_quote' => true,
|
||||
'standardize_not_equals' => true,
|
||||
'multiline_comment_opening_closing' => true,
|
||||
'single_line_empty_body' => false,
|
||||
])
|
||||
->setFinder(
|
||||
Finder::create()
|
||||
->exclude('public')
|
||||
->exclude('runtime')
|
||||
->exclude('vendor')
|
||||
->in(__DIR__)
|
||||
)
|
||||
->setUsingCache(false);
|
||||
12
.phpstorm.meta.php
Normal file
12
.phpstorm.meta.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace PHPSTORM_META {
|
||||
// Reflect
|
||||
override(\Psr\Container\ContainerInterface::get(0), map(['' => '@']));
|
||||
override(\Hyperf\Context\Context::get(0), map(['' => '@']));
|
||||
override(\make(0), map(['' => '@']));
|
||||
override(\di(0), map(['' => '@']));
|
||||
override(\Hyperf\Support\make(0), map(['' => '@']));
|
||||
override(\Hyperf\Support\optional(0), type(0));
|
||||
override(\Hyperf\Tappable\tap(0), type(0));
|
||||
}
|
||||
54
Dockerfile
Normal file
54
Dockerfile
Normal file
@@ -0,0 +1,54 @@
|
||||
# Default Dockerfile
|
||||
#
|
||||
# @link https://www.hyperf.io
|
||||
# @document https://hyperf.wiki
|
||||
# @contact group@hyperf.io
|
||||
# @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
|
||||
FROM hyperf/hyperf:8.3-alpine-v3.19-swoole
|
||||
LABEL maintainer="Hyperf Developers <group@hyperf.io>" version="1.0" license="MIT" app.name="Hyperf"
|
||||
|
||||
##
|
||||
# ---------- env settings ----------
|
||||
##
|
||||
# --build-arg timezone=Asia/Shanghai
|
||||
ARG timezone
|
||||
|
||||
ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \
|
||||
APP_ENV=prod \
|
||||
SCAN_CACHEABLE=(true)
|
||||
|
||||
# update
|
||||
RUN set -ex \
|
||||
# show php version and extensions
|
||||
&& php -v \
|
||||
&& php -m \
|
||||
&& php --ri swoole \
|
||||
# ---------- some config ----------
|
||||
&& cd /etc/php* \
|
||||
# - config PHP
|
||||
&& { \
|
||||
echo "upload_max_filesize=128M"; \
|
||||
echo "post_max_size=128M"; \
|
||||
echo "memory_limit=1G"; \
|
||||
echo "date.timezone=${TIMEZONE}"; \
|
||||
} | tee conf.d/99_overrides.ini \
|
||||
# - config timezone
|
||||
&& ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
|
||||
&& echo "${TIMEZONE}" > /etc/timezone \
|
||||
# ---------- clear works ----------
|
||||
&& rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
|
||||
&& echo -e "\033[42;37m Build Completed :).\033[0m\n"
|
||||
|
||||
WORKDIR /opt/www
|
||||
|
||||
# Composer Cache
|
||||
# COPY ./composer.* /opt/www/
|
||||
# RUN composer install --no-dev --no-scripts
|
||||
|
||||
COPY . /opt/www
|
||||
RUN composer install --no-dev -o && php bin/hyperf.php
|
||||
|
||||
EXPOSE 9501
|
||||
|
||||
ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Hyperf
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
30
app/Controller/AbstractController.php
Normal file
30
app/Controller/AbstractController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Hyperf\Di\Annotation\Inject;
|
||||
use Hyperf\HttpServer\Contract\RequestInterface;
|
||||
use Hyperf\HttpServer\Contract\ResponseInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
abstract class AbstractController
|
||||
{
|
||||
#[Inject]
|
||||
protected ContainerInterface $container;
|
||||
|
||||
#[Inject]
|
||||
protected RequestInterface $request;
|
||||
|
||||
#[Inject]
|
||||
protected ResponseInterface $response;
|
||||
}
|
||||
27
app/Controller/IndexController.php
Normal file
27
app/Controller/IndexController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$user = $this->request->input('user', 'Hyperf');
|
||||
$method = $this->request->getMethod();
|
||||
|
||||
return [
|
||||
'method' => $method,
|
||||
'message' => "Hello {$user}.",
|
||||
];
|
||||
}
|
||||
}
|
||||
38
app/Exception/Handler/AppExceptionHandler.php
Normal file
38
app/Exception/Handler/AppExceptionHandler.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace App\Exception\Handler;
|
||||
|
||||
use Hyperf\Contract\StdoutLoggerInterface;
|
||||
use Hyperf\ExceptionHandler\ExceptionHandler;
|
||||
use Hyperf\HttpMessage\Stream\SwooleStream;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Throwable;
|
||||
|
||||
class AppExceptionHandler extends ExceptionHandler
|
||||
{
|
||||
public function __construct(protected StdoutLoggerInterface $logger)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Throwable $throwable, ResponseInterface $response)
|
||||
{
|
||||
$this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile()));
|
||||
$this->logger->error($throwable->getTraceAsString());
|
||||
return $response->withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream('Internal Server Error.'));
|
||||
}
|
||||
|
||||
public function isValid(Throwable $throwable): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
66
app/Listener/DbQueryExecutedListener.php
Normal file
66
app/Listener/DbQueryExecutedListener.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace App\Listener;
|
||||
|
||||
use Hyperf\Collection\Arr;
|
||||
use Hyperf\Database\Events\QueryExecuted;
|
||||
use Hyperf\Event\Annotation\Listener;
|
||||
use Hyperf\Event\Contract\ListenerInterface;
|
||||
use Hyperf\Logger\LoggerFactory;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
#[Listener]
|
||||
class DbQueryExecutedListener implements ListenerInterface
|
||||
{
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->logger = $container->get(LoggerFactory::class)->get('sql');
|
||||
}
|
||||
|
||||
public function listen(): array
|
||||
{
|
||||
return [
|
||||
QueryExecuted::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryExecuted $event
|
||||
*/
|
||||
public function process(object $event): void
|
||||
{
|
||||
if ($event instanceof QueryExecuted) {
|
||||
$sql = $event->sql;
|
||||
if (! Arr::isAssoc($event->bindings)) {
|
||||
$position = 0;
|
||||
foreach ($event->bindings as $value) {
|
||||
$position = strpos($sql, '?', $position);
|
||||
if ($position === false) {
|
||||
break;
|
||||
}
|
||||
$value = "'{$value}'";
|
||||
$sql = substr_replace($sql, $value, $position, 1);
|
||||
$position += strlen($value);
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info(sprintf('[%s] %s', $event->time, $sql));
|
||||
}
|
||||
}
|
||||
}
|
||||
35
app/Listener/ResumeExitCoordinatorListener.php
Normal file
35
app/Listener/ResumeExitCoordinatorListener.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace App\Listener;
|
||||
|
||||
use Hyperf\Command\Event\AfterExecute;
|
||||
use Hyperf\Coordinator\Constants;
|
||||
use Hyperf\Coordinator\CoordinatorManager;
|
||||
use Hyperf\Event\Annotation\Listener;
|
||||
use Hyperf\Event\Contract\ListenerInterface;
|
||||
|
||||
#[Listener]
|
||||
class ResumeExitCoordinatorListener implements ListenerInterface
|
||||
{
|
||||
public function listen(): array
|
||||
{
|
||||
return [
|
||||
AfterExecute::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function process(object $event): void
|
||||
{
|
||||
CoordinatorManager::until(Constants::WORKER_EXIT)->resume();
|
||||
}
|
||||
}
|
||||
31
bin/hyperf.php
Normal file
31
bin/hyperf.php
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
ini_set('display_errors', 'on');
|
||||
ini_set('display_startup_errors', 'on');
|
||||
ini_set('memory_limit', '1G');
|
||||
|
||||
error_reporting(E_ALL);
|
||||
|
||||
! defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));
|
||||
|
||||
require BASE_PATH . '/vendor/autoload.php';
|
||||
|
||||
! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', Hyperf\Engine\DefaultOption::hookFlags());
|
||||
|
||||
// Self-called anonymous function that creates its own scope and keep the global namespace clean.
|
||||
(function () {
|
||||
Hyperf\Di\ClassLoader::init();
|
||||
/** @var Psr\Container\ContainerInterface $container */
|
||||
$container = require BASE_PATH . '/config/container.php';
|
||||
|
||||
$application = $container->get(Hyperf\Contract\ApplicationInterface::class);
|
||||
$application->run();
|
||||
})();
|
||||
88
composer.json
Executable file
88
composer.json
Executable file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"name": "hyperf/hyperf-skeleton",
|
||||
"type": "project",
|
||||
"keywords": [
|
||||
"php",
|
||||
"swoole",
|
||||
"framework",
|
||||
"hyperf",
|
||||
"microservice",
|
||||
"middleware"
|
||||
],
|
||||
"description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.",
|
||||
"license": "Apache-2.0",
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"hyperf/cache": "~3.1.0",
|
||||
"hyperf/command": "~3.1.0",
|
||||
"hyperf/config": "~3.1.0",
|
||||
"hyperf/database": "~3.1.0",
|
||||
"hyperf/db-connection": "~3.1.0",
|
||||
"hyperf/engine": "^2.10",
|
||||
"hyperf/framework": "~3.1.0",
|
||||
"hyperf/guzzle": "~3.1.0",
|
||||
"hyperf/http-server": "~3.1.0",
|
||||
"hyperf/json-rpc": "~3.1.0",
|
||||
"hyperf/logger": "~3.1.0",
|
||||
"hyperf/memory": "~3.1.0",
|
||||
"hyperf/model-cache": "^3.1",
|
||||
"hyperf/paginator": "^3.1",
|
||||
"hyperf/process": "~3.1.0",
|
||||
"hyperf/redis": "~3.1.0",
|
||||
"hyperf/rpc": "~3.1.0",
|
||||
"hyperf/rpc-client": "~3.1.0",
|
||||
"hyperf/rpc-server": "~3.1.0",
|
||||
"hyperf/service-governance-nacos": "^3.1",
|
||||
"hyperf/validation": "^3.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"hyperf/devtool": "~3.1.0",
|
||||
"hyperf/testing": "~3.1.0",
|
||||
"hyperf/watcher": "^3.1",
|
||||
"mockery/mockery": "^1.0",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"swoole/ide-helper": "^5.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-openssl": "Required to use HTTPS.",
|
||||
"ext-json": "Required to use JSON.",
|
||||
"ext-pdo": "Required to use MySQL Client.",
|
||||
"ext-pdo_mysql": "Required to use MySQL Client.",
|
||||
"ext-redis": "Required to use Redis Client."
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Aether\\": "extend/Aether/PHP/Hyperf"
|
||||
},
|
||||
"files": []
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"HyperfTest\\": "./test/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"extra": [],
|
||||
"scripts": {
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"rm -rf runtime/container"
|
||||
],
|
||||
"test": "co-phpunit --prepend test/bootstrap.php --colors=always",
|
||||
"cs-fix": "php-cs-fixer fix $1",
|
||||
"analyse": "phpstan analyse --memory-limit 300M",
|
||||
"start": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"php ./bin/hyperf.php start"
|
||||
]
|
||||
}
|
||||
}
|
||||
10449
composer.lock
generated
Executable file
10449
composer.lock
generated
Executable file
File diff suppressed because it is too large
Load Diff
21
config/autoload/annotations.php
Normal file
21
config/autoload/annotations.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
return [
|
||||
'scan' => [
|
||||
'paths' => [
|
||||
BASE_PATH . '/app',
|
||||
],
|
||||
'ignore_annotations' => [
|
||||
'mixin',
|
||||
],
|
||||
],
|
||||
];
|
||||
13
config/autoload/aspects.php
Normal file
13
config/autoload/aspects.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
return [
|
||||
];
|
||||
19
config/autoload/cache.php
Normal file
19
config/autoload/cache.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
return [
|
||||
'default' => [
|
||||
'driver' => Hyperf\Cache\Driver\RedisDriver::class,
|
||||
'packer' => Hyperf\Codec\Packer\PhpSerializerPacker::class,
|
||||
'prefix' => 'c:',
|
||||
'skip_cache_results' => [],
|
||||
],
|
||||
];
|
||||
13
config/autoload/commands.php
Normal file
13
config/autoload/commands.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
return [
|
||||
];
|
||||
41
config/autoload/databases.php
Normal file
41
config/autoload/databases.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
use function Hyperf\Support\env;
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'driver' => env('DB_DRIVER', 'mysql'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'database' => env('DB_DATABASE', 'hyperf'),
|
||||
'port' => env('DB_PORT', 3306),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'collation' => env('DB_COLLATION', 'utf8_unicode_ci'),
|
||||
'prefix' => env('DB_PREFIX', ''),
|
||||
'pool' => [
|
||||
'min_connections' => 1,
|
||||
'max_connections' => 10,
|
||||
'connect_timeout' => 10.0,
|
||||
'wait_timeout' => 3.0,
|
||||
'heartbeat' => -1,
|
||||
'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60),
|
||||
],
|
||||
'commands' => [
|
||||
'gen:model' => [
|
||||
'path' => 'app/Model',
|
||||
'force_casts' => true,
|
||||
'inheritance' => 'Model',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
13
config/autoload/dependencies.php
Normal file
13
config/autoload/dependencies.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
return [
|
||||
];
|
||||
44
config/autoload/devtool.php
Normal file
44
config/autoload/devtool.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
return [
|
||||
'generator' => [
|
||||
'amqp' => [
|
||||
'consumer' => [
|
||||
'namespace' => 'App\\Amqp\\Consumer',
|
||||
],
|
||||
'producer' => [
|
||||
'namespace' => 'App\\Amqp\\Producer',
|
||||
],
|
||||
],
|
||||
'aspect' => [
|
||||
'namespace' => 'App\\Aspect',
|
||||
],
|
||||
'command' => [
|
||||
'namespace' => 'App\\Command',
|
||||
],
|
||||
'controller' => [
|
||||
'namespace' => 'App\\Controller',
|
||||
],
|
||||
'job' => [
|
||||
'namespace' => 'App\\Job',
|
||||
],
|
||||
'listener' => [
|
||||
'namespace' => 'App\\Listener',
|
||||
],
|
||||
'middleware' => [
|
||||
'namespace' => 'App\\Middleware',
|
||||
],
|
||||
'Process' => [
|
||||
'namespace' => 'App\\Processes',
|
||||
],
|
||||
],
|
||||
];
|
||||
22
config/autoload/exceptions.php
Normal file
22
config/autoload/exceptions.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
return [
|
||||
'handler' => [
|
||||
'http' => [
|
||||
Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class,
|
||||
App\Exception\Handler\AppExceptionHandler::class,
|
||||
],
|
||||
'jsonrpc-http' => [
|
||||
Aether\Exception\AppExceptionHandler::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
15
config/autoload/listeners.php
Normal file
15
config/autoload/listeners.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
return [
|
||||
Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler::class,
|
||||
Hyperf\Command\Listener\FailToHandleListener::class,
|
||||
];
|
||||
30
config/autoload/logger.php
Normal file
30
config/autoload/logger.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
return [
|
||||
'default' => [
|
||||
'handler' => [
|
||||
'class' => Monolog\Handler\StreamHandler::class,
|
||||
'constructor' => [
|
||||
'stream' => BASE_PATH . '/runtime/logs/hyperf.log',
|
||||
'level' => Monolog\Logger::DEBUG,
|
||||
],
|
||||
],
|
||||
'formatter' => [
|
||||
'class' => Monolog\Formatter\LineFormatter::class,
|
||||
'constructor' => [
|
||||
'format' => null,
|
||||
'dateFormat' => 'Y-m-d H:i:s',
|
||||
'allowInlineLineBreaks' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
15
config/autoload/middlewares.php
Normal file
15
config/autoload/middlewares.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
return [
|
||||
'http' => [
|
||||
],
|
||||
];
|
||||
13
config/autoload/processes.php
Normal file
13
config/autoload/processes.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
return [
|
||||
];
|
||||
29
config/autoload/redis.php
Normal file
29
config/autoload/redis.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
use function Hyperf\Support\env;
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'host' => env('REDIS_HOST', 'localhost'),
|
||||
'auth' => env('REDIS_AUTH', null),
|
||||
'port' => (int) env('REDIS_PORT', 6379),
|
||||
'db' => (int) env('REDIS_DB', 0),
|
||||
'pool' => [
|
||||
'min_connections' => 1,
|
||||
'max_connections' => 10,
|
||||
'connect_timeout' => 10.0,
|
||||
'wait_timeout' => 3.0,
|
||||
'heartbeat' => -1,
|
||||
'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60),
|
||||
],
|
||||
],
|
||||
];
|
||||
54
config/autoload/server.php
Normal file
54
config/autoload/server.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Hyperf\Framework\Bootstrap\PipeMessageCallback;
|
||||
use Hyperf\Framework\Bootstrap\WorkerExitCallback;
|
||||
use Hyperf\Framework\Bootstrap\WorkerStartCallback;
|
||||
use Hyperf\JsonRpc\HttpServer;
|
||||
use Hyperf\Server\Event;
|
||||
use Hyperf\Server\Server;
|
||||
use Swoole\Constant;
|
||||
|
||||
use function Hyperf\Support\env;
|
||||
|
||||
return [
|
||||
'mode' => SWOOLE_PROCESS,
|
||||
'servers' => [
|
||||
[
|
||||
'name' => 'jsonrpc-http',
|
||||
'type' => Server::SERVER_HTTP,
|
||||
'host' => env('JSON_RPC_HOST', '0.0.0.0'),
|
||||
'port' => (int) env('JSON_RPC_PORT', 9620),
|
||||
'sock_type' => SWOOLE_SOCK_TCP,
|
||||
'callbacks' => [
|
||||
Event::ON_REQUEST => [HttpServer::class, 'onRequest'],
|
||||
],
|
||||
'settings' => [
|
||||
'open_eof_split' => true,
|
||||
'package_eof' => "\r\n",
|
||||
'package_max_length' => 1024 * 1024 * 2,
|
||||
],
|
||||
'options' => [
|
||||
// Whether to enable request lifecycle event
|
||||
'enable_request_lifecycle' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
'settings' => [
|
||||
Constant::OPTION_ENABLE_COROUTINE => true,
|
||||
Constant::OPTION_WORKER_NUM => (int) env('SWOOLE_WORKER_NUM', swoole_cpu_num()), //swoole_cpu_num(),
|
||||
Constant::OPTION_PID_FILE => BASE_PATH . '/runtime/hyperf.pid',
|
||||
Constant::OPTION_OPEN_TCP_NODELAY => true,
|
||||
Constant::OPTION_MAX_COROUTINE => 100000,
|
||||
Constant::OPTION_OPEN_HTTP2_PROTOCOL => true,
|
||||
Constant::OPTION_MAX_REQUEST => 100000,
|
||||
Constant::OPTION_SOCKET_BUFFER_SIZE => 2 * 1024 * 1024,
|
||||
Constant::OPTION_BUFFER_OUTPUT_SIZE => 2 * 1024 * 1024,
|
||||
],
|
||||
'callbacks' => [
|
||||
Event::ON_WORKER_START => [WorkerStartCallback::class, 'onWorkerStart'],
|
||||
Event::ON_PIPE_MESSAGE => [PipeMessageCallback::class, 'onPipeMessage'],
|
||||
Event::ON_WORKER_EXIT => [WorkerExitCallback::class, 'onWorkerExit'],
|
||||
],
|
||||
];
|
||||
30
config/autoload/services.php
Normal file
30
config/autoload/services.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use function Hyperf\Support\env;
|
||||
|
||||
return [
|
||||
'enable' => [
|
||||
'discovery' => true,
|
||||
'register' => true,
|
||||
],
|
||||
'consumers' => [],
|
||||
'providers' => [],
|
||||
'drivers' => [
|
||||
'nacos' => [
|
||||
// nacos server url like https://nacos.hyperf.io, Priority is higher than host:port
|
||||
// 'url' => '',
|
||||
// The nacos host info
|
||||
'host' => env('NACOS_HOST', '127.0.0.1'),
|
||||
'port' => env('NACOS_PORT', 8848),
|
||||
// The nacos account info
|
||||
'username' => null,
|
||||
'password' => null,
|
||||
'guzzle' => [
|
||||
'config' => null,
|
||||
],
|
||||
'group_name' => 'api',
|
||||
'namespace_id' => env('NACOS_NAMESPACE', 'dev'),
|
||||
'heartbeat' => 5,
|
||||
],
|
||||
],
|
||||
];
|
||||
33
config/config.php
Normal file
33
config/config.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
use Hyperf\Contract\StdoutLoggerInterface;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
use function Hyperf\Support\env;
|
||||
|
||||
return [
|
||||
'app_name' => env('APP_NAME', 'skeleton'),
|
||||
'app_env' => env('APP_ENV', 'dev'),
|
||||
'scan_cacheable' => env('SCAN_CACHEABLE', false),
|
||||
StdoutLoggerInterface::class => [
|
||||
'log_level' => [
|
||||
LogLevel::ALERT,
|
||||
LogLevel::CRITICAL,
|
||||
LogLevel::DEBUG,
|
||||
LogLevel::EMERGENCY,
|
||||
LogLevel::ERROR,
|
||||
LogLevel::INFO,
|
||||
LogLevel::NOTICE,
|
||||
LogLevel::WARNING,
|
||||
],
|
||||
],
|
||||
];
|
||||
21
config/container.php
Normal file
21
config/container.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* Initialize a dependency injection container that implemented PSR-11 and return the container.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
use Hyperf\Context\ApplicationContext;
|
||||
use Hyperf\Di\Container;
|
||||
use Hyperf\Di\Definition\DefinitionSourceFactory;
|
||||
|
||||
$container = new Container((new DefinitionSourceFactory())());
|
||||
|
||||
return ApplicationContext::setContainer($container);
|
||||
18
config/routes.php
Normal file
18
config/routes.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
use Hyperf\HttpServer\Router\Router;
|
||||
|
||||
Router::addRoute(['GET', 'POST', 'HEAD'], '/', 'App\Controller\IndexController@index');
|
||||
|
||||
Router::get('/favicon.ico', function () {
|
||||
return '';
|
||||
});
|
||||
30
deploy.test.yml
Normal file
30
deploy.test.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
hyperf:
|
||||
image: $REGISTRY_URL/$PROJECT_NAME:test
|
||||
environment:
|
||||
- "APP_PROJECT=hyperf"
|
||||
- "APP_ENV=testing"
|
||||
ports:
|
||||
- "9501:9501"
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 5
|
||||
update_config:
|
||||
parallelism: 2
|
||||
delay: 5s
|
||||
order: start-first
|
||||
networks:
|
||||
- hyperf_net
|
||||
configs:
|
||||
- source: hyperf_v1.0
|
||||
target: /opt/www/.env
|
||||
configs:
|
||||
hyperf_v1.0:
|
||||
external: true
|
||||
networks:
|
||||
hyperf_net:
|
||||
external: true
|
||||
18
docker-compose.yml
Normal file
18
docker-compose.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
version: '3'
|
||||
services:
|
||||
hyperf-skeleton:
|
||||
container_name: hyperf-skeleton
|
||||
image: hyperf-skeleton
|
||||
build:
|
||||
context: .
|
||||
volumes:
|
||||
- ./:/opt/www
|
||||
ports:
|
||||
- "9610:9610"
|
||||
environment:
|
||||
- APP_ENV=dev
|
||||
- SCAN_CACHEABLE=false
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: hyperf-skeleton
|
||||
40
extend/Aether/PHP/Hyperf/AetherCrudInterface.php
Normal file
40
extend/Aether/PHP/Hyperf/AetherCrudInterface.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Aether;
|
||||
|
||||
interface AetherCrudInterface
|
||||
{
|
||||
/**
|
||||
* 列表
|
||||
* @return array
|
||||
*/
|
||||
public function list(): array;
|
||||
|
||||
/**
|
||||
* 查询
|
||||
* @param int $id
|
||||
* @return object
|
||||
*/
|
||||
public function detail(int $id): object;
|
||||
/**
|
||||
* 新增
|
||||
* @param array $data
|
||||
* @return int
|
||||
*/
|
||||
public function create(array $data): int;
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function update(int $id, array $data): bool;
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param int $id
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(int $id): bool;
|
||||
}
|
||||
318
extend/Aether/PHP/Hyperf/AetherCrudService.php
Normal file
318
extend/Aether/PHP/Hyperf/AetherCrudService.php
Normal file
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether;
|
||||
|
||||
use Aether\Contract\TreeableInterface;
|
||||
use Aether\Exception\BusinessException;
|
||||
use Hyperf\Database\Model\Builder;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 抽象CRUD服务基类,封装通用逻辑
|
||||
*/
|
||||
abstract class AetherCrudService extends AetherService implements AetherCrudInterface
|
||||
{
|
||||
protected array $search = [];
|
||||
|
||||
protected function getSearch(): array
|
||||
{
|
||||
return $this->search;
|
||||
}
|
||||
|
||||
protected array $ignoreSearchFields = [];
|
||||
|
||||
protected function getIgnoreSearchFields(): array
|
||||
{
|
||||
return $this->ignoreSearchFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前服务对应的模型实例(由子类实现)
|
||||
*/
|
||||
protected abstract function getModel(): AetherModel;
|
||||
|
||||
/**
|
||||
* 获取当前服务对应的验证器实例(由子类实现)
|
||||
*/
|
||||
protected abstract function getValidator(): AetherValidator;
|
||||
|
||||
|
||||
/**
|
||||
* 通用列表查询(支持分页和树形结构)
|
||||
*/
|
||||
public function list(array $params = []): array
|
||||
{
|
||||
$model = $this->getModel();
|
||||
$query = $model->newQuery();
|
||||
|
||||
// 通过模型配置自动应用所有搜索条件
|
||||
$this->applySearch($query, $params);
|
||||
|
||||
// 动态应用排序
|
||||
$sortConfig = $model->getSortConfig();
|
||||
if ($sortConfig) {
|
||||
$query->orderBy($sortConfig['field'], $sortConfig['direction']);
|
||||
}
|
||||
|
||||
$withDeleted = filter_var($params['with_deleted'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
if ($withDeleted) {
|
||||
$query->withTrashed();
|
||||
}
|
||||
|
||||
// 存在分页参数(page或size)则进行分页查询
|
||||
if (isset($params['page']) || isset($params['size'])) {
|
||||
$page = (int)($params['page'] ?? 1);
|
||||
$size = (int)($params['size'] ?? 10);
|
||||
// 确保分页参数合法性
|
||||
$page = max(1, $page);
|
||||
$size = max(1, min(100, $size)); // 限制最大页大小为100
|
||||
|
||||
$result = $query->paginate($size, ['*'], 'page', $page);
|
||||
return [
|
||||
'total' => $result->total(),
|
||||
'list' => $result->items()
|
||||
];
|
||||
}
|
||||
|
||||
// 无分页参数时返回完整数据集合
|
||||
$items = $query->get()->toArray();
|
||||
|
||||
// 若模型支持树形结构则构建树形,否则返回普通数组
|
||||
if ($model instanceof TreeableInterface) {
|
||||
return $model::buildTree($items, (int)($params['parent_id'] ?? 0));
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用详情查询
|
||||
*/
|
||||
public function detail(int $id): object
|
||||
{var_dump('detail');
|
||||
return $this->getModel()->findOrFailById($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用创建逻辑
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function create(array $data): int
|
||||
{
|
||||
// 数据验证(使用子类指定的验证器)
|
||||
$this->getValidator()->scene('create', $data)->check();
|
||||
|
||||
return $this->transaction(function () use ($data) {
|
||||
$model = $this->getModel()->createOne($data);
|
||||
$this->logger()->info('创建资源', [
|
||||
'id' => $model->id,
|
||||
'code' => $data['code'] ?? $model->code
|
||||
]);
|
||||
return $model->id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用更新逻辑
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function update(int $id, array $data): bool
|
||||
{
|
||||
$model = $this->getModel();
|
||||
$resource = $model->findById($id);
|
||||
$this->checkResourceExists($resource);
|
||||
|
||||
// 数据验证
|
||||
$this->getValidator()->scene('update', $data)->check();
|
||||
|
||||
// 钩子:处理更新时的特殊逻辑(如禁止自身为父级)
|
||||
$this->handleUpdateSpecialLogic($id, $data);
|
||||
|
||||
return $this->transaction(function () use ($id, $data) {
|
||||
$result = $this->getModel()->updateById($id, $data);
|
||||
$this->logger()->info('更新资源', [
|
||||
'id' => $id,
|
||||
'data' => $data
|
||||
]);
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用删除逻辑
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$model = $this->getModel();
|
||||
$resource = $model->findById($id);
|
||||
|
||||
$this->checkResourceExists($resource);
|
||||
|
||||
// 钩子:删除前检查(如子级存在性)
|
||||
$this->checkChildrenBeforeDelete($id);
|
||||
|
||||
return $this->transaction(function () use ($id) {
|
||||
$result = $this->getModel()->deleteById($id);
|
||||
$this->logger()->info('删除资源', ['id' => $id]);
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 钩子方法:更新时的特殊逻辑(子类可重写)
|
||||
*/
|
||||
protected function handleUpdateSpecialLogic(int $id, array &$data): void
|
||||
{
|
||||
// 通用逻辑:禁止将自身设为父级(适用于有parent_id的场景)
|
||||
if (isset($data['parent_id']) && $data['parent_id'] == $id) {
|
||||
throw new BusinessException('不能将自身设为父级', 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 钩子方法:删除前检查子级(子类可重写)
|
||||
*/
|
||||
protected function checkChildrenBeforeDelete(int $id): void
|
||||
{
|
||||
// 默认不检查,需要的子类重写
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型的$search配置,自动应用搜索条件到查询构建器
|
||||
*/
|
||||
public function applySearch(Builder $query, array $params): void
|
||||
{
|
||||
foreach ($this->search as $field => $rule) {
|
||||
// 跳过未传递的参数
|
||||
if (!isset($params[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $params[$field];
|
||||
$this->applySearchRule($query, $field, $value, $rule);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用单个搜索规则
|
||||
*/
|
||||
protected function applySearchRule(Builder $query, string $field, $value, $rule): void
|
||||
{
|
||||
// 处理规则格式(支持字符串简写或数组配置)
|
||||
$config = is_array($rule) ? $rule : ['type' => $rule];
|
||||
$type = $config['type'];
|
||||
|
||||
switch ($type) {
|
||||
case '=': // 精确匹配
|
||||
$query->where($field, $value);
|
||||
break;
|
||||
case 'like': // 模糊匹配
|
||||
$query->where($field, 'like', "%{$value}%");
|
||||
break;
|
||||
case 'between': // 范围查询(支持数组或两个参数)
|
||||
$values = is_array($value) ? $value : [$value, $params[$field . '_end'] ?? $value];
|
||||
$query->whereBetween($field, $values);
|
||||
break;
|
||||
case 'callback': // 自定义回调
|
||||
if (isset($config['handler']) && is_callable($config['handler'])) {
|
||||
call_user_func($config['handler'], $query, $value);
|
||||
}
|
||||
break;
|
||||
// 可扩展其他类型:in、>、< 等
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除恢复
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function restore(int $id): bool
|
||||
{
|
||||
$model = $this->getModel();
|
||||
// 必须使用withTrashed()才能查询到已删除记录
|
||||
$resource = $model->newQuery()->withTrashed()->find($id);
|
||||
$this->checkResourceExists($resource, '恢复的资源不存在');
|
||||
|
||||
return $this->transaction(function () use ($id) {
|
||||
$result = $this->getModel()->newQuery()->withTrashed()
|
||||
->where('id', $id)->restore();
|
||||
|
||||
$this->logger()->info('恢复软删除资源', ['id' => $id]);
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量软删除
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function batchDelete(array $ids): bool
|
||||
{
|
||||
if (empty($ids)) {
|
||||
throw new BusinessException('请选择要删除的记录', 400);
|
||||
}
|
||||
|
||||
$model = $this->getModel();
|
||||
$exists = $model->whereIn('id', $ids)->exists();
|
||||
if (!$exists) {
|
||||
throw new BusinessException('部分记录不存在', 404);
|
||||
}
|
||||
|
||||
return $this->transaction(function () use ($ids) {
|
||||
$result = $this->getModel()->whereIn('id', $ids)->delete();
|
||||
$this->logger()->info('批量软删除资源', ['ids' => $ids]);
|
||||
return $result > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量恢复软删除
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function batchRestore(array $ids): bool
|
||||
{
|
||||
if (empty($ids)) {
|
||||
throw new BusinessException('请选择要恢复的记录', 400);
|
||||
}
|
||||
|
||||
$model = $this->getModel();
|
||||
$exists = $model->newQuery()->withTrashed()
|
||||
->whereIn('id', $ids)
|
||||
->whereNotNull('deleted_at')
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
throw new BusinessException('部分记录不存在或未被删除', 404);
|
||||
}
|
||||
|
||||
return $this->transaction(function () use ($ids) {
|
||||
$result = $this->getModel()->newQuery()->withTrashed()
|
||||
->whereIn('id', $ids)->restore();
|
||||
|
||||
$this->logger()->info('批量恢复资源', ['ids' => $ids]);
|
||||
return $result > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 永久删除(物理删除)
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function forceDelete(int $id): bool
|
||||
{
|
||||
$model = $this->getModel();
|
||||
$resource = $model->newQuery()->withTrashed()->find($id);
|
||||
$this->checkResourceExists($resource, '要删除的资源不存在');
|
||||
|
||||
return $this->transaction(function () use ($id) {
|
||||
$result = $this->getModel()->newQuery()->withTrashed()
|
||||
->where('id', $id)->forceDelete();
|
||||
|
||||
$this->logger()->info('永久删除资源', ['id' => $id]);
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
}
|
||||
494
extend/Aether/PHP/Hyperf/AetherModel.php
Normal file
494
extend/Aether/PHP/Hyperf/AetherModel.php
Normal file
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether;
|
||||
|
||||
use Closure;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Hyperf\Context\ApplicationContext;
|
||||
use Hyperf\Contract\LengthAwarePaginatorInterface;
|
||||
use Hyperf\Database\Model\Builder;
|
||||
use Hyperf\Database\Model\Collection;
|
||||
use Hyperf\Database\Model\ModelNotFoundException;
|
||||
use Hyperf\DbConnection\Model\Model as HyperfModel;
|
||||
use Hyperf\DbConnection\Db;
|
||||
use Hyperf\HttpServer\Contract\RequestInterface;
|
||||
use Hyperf\ModelCache\Cacheable;
|
||||
use Hyperf\ModelCache\CacheableInterface;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Throwable;
|
||||
|
||||
abstract class AetherModel extends HyperfModel implements CacheableInterface
|
||||
{
|
||||
use Cacheable;
|
||||
// use AetherSoftDelete; 移除,由子类决定是否使用
|
||||
|
||||
/**
|
||||
* 批量赋值白名单.
|
||||
*/
|
||||
protected array $fillable = [];
|
||||
|
||||
/**
|
||||
* 时间戳格式.
|
||||
*/
|
||||
protected ?string $dateFormat = 'Y-m-d H:i:s';
|
||||
|
||||
/**
|
||||
* 搜索器规则配置.
|
||||
*/
|
||||
protected array $search = [];
|
||||
|
||||
/**
|
||||
* 获取器规则配置.
|
||||
*/
|
||||
protected array $append = [];
|
||||
|
||||
/**
|
||||
* 隐藏字段.
|
||||
*/
|
||||
protected array $hidden = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
// 'deleted_at' // 移除,由子类决定是否使用
|
||||
];
|
||||
|
||||
/**
|
||||
* 排序配置:
|
||||
* - false: 禁用排序
|
||||
* - 字符串: 排序字段(默认升序)
|
||||
* - 数组: ['field' => '字段名', 'direction' => 'asc/desc']
|
||||
*/
|
||||
protected string|array|bool $sortable = 'sort'; // 默认按sort字段升序
|
||||
|
||||
/**
|
||||
* 获取排序配置
|
||||
* @return array|null [field, direction] 或 null(禁用排序)
|
||||
*/
|
||||
public function getSortConfig(): ?array
|
||||
{
|
||||
if ($this->sortable === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理字符串配置(如 'sort' 或 'create_time')
|
||||
if (is_string($this->sortable)) {
|
||||
return [
|
||||
'field' => $this->sortable,
|
||||
'direction' => 'asc'
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
$this->bootBaseModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷创建.
|
||||
*/
|
||||
public static function createOne(array $data): AetherModel|Builder|HyperfModel
|
||||
{
|
||||
return static::query()->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷更新.
|
||||
*/
|
||||
public static function updateById(int $id, array $data): bool
|
||||
{
|
||||
return static::query()->where('id', $id)->update($data) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷删除指定ID的记录
|
||||
*
|
||||
* @param int $id 要删除的记录ID
|
||||
* @return bool 成功删除返回true
|
||||
* @throws ModelNotFoundException 当记录不存在时抛出
|
||||
* @throws Exception 当删除操作发生其他错误时抛出
|
||||
*/
|
||||
public static function deleteById(int $id): bool
|
||||
{
|
||||
if (!static::query()->where('id', $id)->exists()) {
|
||||
throw new ModelNotFoundException(sprintf(
|
||||
'找不到ID为 %d 的 %s 记录',
|
||||
$id,
|
||||
static::class
|
||||
));
|
||||
}
|
||||
return static::query()->where('id', $id)->delete() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷查找.
|
||||
* @param int $id
|
||||
* @return Builder|Builder[]|Collection|HyperfModel
|
||||
* @throws Exception 当删除操作发生其他错误时抛出
|
||||
* @throws ModelNotFoundException
|
||||
*/
|
||||
public static function findById(int $id): array|Builder|Collection|HyperfModel
|
||||
{
|
||||
$record = static::query()->find($id);
|
||||
if (is_null($record)) {
|
||||
throw new ModelNotFoundException(sprintf('找不到 ID 为 %d 的记录', $id));
|
||||
}
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷查找或失败.
|
||||
* @param int $id 要查找的记录ID
|
||||
* @return AetherModel 根据ID查找记录,不存在则抛出异常
|
||||
* 根据ID查找记录,不存在则抛出异常
|
||||
* @throws Exception 当删除操作发生其他错误时抛出
|
||||
* @throws ModelNotFoundException 当记录不存在时抛出
|
||||
*/
|
||||
public static function findOrFailById(int $id): static
|
||||
{
|
||||
$record = static::query()->find($id);
|
||||
if(is_null($record)) {
|
||||
throw new ModelNotFoundException(sprintf(
|
||||
'找不到ID为 %d 的 %s 记录',
|
||||
$id,
|
||||
static::class
|
||||
));
|
||||
}
|
||||
return $record;// static::query()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表.
|
||||
*/
|
||||
public static function getList(
|
||||
array $conditions = [],
|
||||
array $columns = ['*'],
|
||||
array $orders = []
|
||||
): Collection {
|
||||
$query = static::buildQuery($conditions, $orders);
|
||||
return $query->get($columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询列表.
|
||||
*/
|
||||
public static function getPageList(
|
||||
array $conditions = [],
|
||||
int $page = 1,
|
||||
int $pageSize = 10,
|
||||
array $columns = ['*'],
|
||||
array $orderBy = []
|
||||
): LengthAwarePaginatorInterface {
|
||||
// 直接通过静态方法链构建查询
|
||||
$query = static::query();
|
||||
|
||||
// 应用条件
|
||||
foreach ($conditions as $field => $value) {
|
||||
$query->where($field, $value);
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
foreach ($orderBy as $field => $direction) {
|
||||
$query->orderBy($field, $direction);
|
||||
}
|
||||
|
||||
// 执行分页
|
||||
return $query->paginate($pageSize, $columns, 'page', $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取器处理.
|
||||
*/
|
||||
public function getAttribute(string $key): mixed
|
||||
{
|
||||
$value = parent::getAttribute($key);
|
||||
|
||||
// 检查是否有自定义获取器
|
||||
$getterMethod = 'get' . ucfirst($key) . 'Attr';
|
||||
if (method_exists($this, $getterMethod)) {
|
||||
return $this->{$getterMethod}($value);
|
||||
}
|
||||
|
||||
// 应用获取器规则
|
||||
if (isset($this->append[$key])) {
|
||||
return $this->applyGetterRule($value, $this->append[$key]);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新.
|
||||
*/
|
||||
public static function batchUpdate(array $conditions, array $data): int
|
||||
{
|
||||
return static::query()->where($conditions)->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务处理.
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function transaction(Closure $closure): mixed
|
||||
{
|
||||
return Db::transaction($closure);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化模型.
|
||||
*/
|
||||
protected function bootBaseModel(): void
|
||||
{
|
||||
// 自动注册搜索器和获取器
|
||||
$this->registerSearchers();
|
||||
$this->registerGetters();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册搜索器.
|
||||
*/
|
||||
protected function registerSearchers(): void
|
||||
{
|
||||
// 为模型查询添加全局作用域,自动应用搜索规则
|
||||
static::addGlobalScope('auto_search', function (Builder $query) {
|
||||
$searchConditions = $this->getSearchConditions();
|
||||
if (empty($searchConditions)) {
|
||||
return;
|
||||
}
|
||||
// 应用搜索条件到查询
|
||||
$this->applySearch($query, $searchConditions);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取搜索条件.
|
||||
* @throws ContainerExceptionInterface
|
||||
* @throws NotFoundExceptionInterface
|
||||
*/
|
||||
protected function getSearchConditions(): array
|
||||
{
|
||||
$request = ApplicationContext::getContainer()->get(RequestInterface::class);
|
||||
$allParams = $request->all();
|
||||
|
||||
if (is_array($allParams) && count($allParams) === 1 && is_array($allParams[0])) {
|
||||
$allParams = $allParams[0];
|
||||
}
|
||||
|
||||
$allowedFields = array_keys($this->search);
|
||||
return array_intersect_key($allParams, array_flip($allowedFields));
|
||||
}
|
||||
|
||||
protected function registerGetters(): void
|
||||
{
|
||||
foreach ($this->append as $field => $rule) {
|
||||
// 为字段注册获取器回调
|
||||
$this->registerGetter($field, $rule);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为单个字段注册获取器.
|
||||
* @param string $field 字段名
|
||||
* @param array|Closure|string $rule 规则(类型/带参数的类型/闭包)
|
||||
*/
|
||||
protected function registerGetter(string $field, array|Closure|string $rule): void
|
||||
{
|
||||
// 生成获取器方法名(遵循 Hyperf 模型获取器规范:get{Field}Attr)
|
||||
$getterMethod = 'get' . ucfirst($field) . 'Attr';
|
||||
|
||||
// 若已存在自定义获取器方法,则不覆盖
|
||||
if (method_exists($this, $getterMethod)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 动态定义获取器方法
|
||||
$this->{$getterMethod} = function ($value) use ($rule) {
|
||||
// 闭包规则直接执行
|
||||
if ($rule instanceof Closure) {
|
||||
return $rule($value, $this); // 传入当前模型实例方便关联字段处理
|
||||
}
|
||||
|
||||
// 解析规则类型和参数
|
||||
if (is_array($rule)) {
|
||||
$type = $rule[0];
|
||||
$params = $rule[1] ?? [];
|
||||
} else {
|
||||
$type = $rule;
|
||||
$params = [];
|
||||
}
|
||||
|
||||
// 应用规则(复用/扩展 applyGetterRule 方法)
|
||||
return $this->applyGetterRule($value, $type, $params);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用获取器规则(支持参数).
|
||||
* @param mixed $value 字段原始值
|
||||
* @param string $type 规则类型
|
||||
* @param array $params 规则参数
|
||||
*/
|
||||
protected function applyGetterRule(mixed $value, string $type, array $params = []): mixed
|
||||
{
|
||||
return match ($type) {
|
||||
// 基础规则
|
||||
'date' => $value instanceof DateTime ? $value->format('Y-m-d') : $value,
|
||||
'datetime' => $value instanceof DateTime ? $value->format('Y-m-d H:i:s') : $value,
|
||||
'timestamp' => $value instanceof DateTime ? $value->getTimestamp() : $value,
|
||||
'boolean' => (bool) $value,
|
||||
'json' => is_string($value) ? json_decode($value, true) : $value,
|
||||
|
||||
// 参数化规则
|
||||
'number' => $this->formatNumber($value, $params), // 数字格式化
|
||||
'truncate' => $this->truncateString($value, $params), // 字符串截断
|
||||
'enum' => $this->mapEnum($value, $params), // 枚举映射
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
// 数字格式化(示例参数化实现)
|
||||
protected function formatNumber($value, array $params): string
|
||||
{
|
||||
$precision = $params['precision'] ?? 2; // 默认保留2位小数
|
||||
return number_format((float) $value, $precision);
|
||||
}
|
||||
|
||||
// 字符串截断(示例参数化实现)
|
||||
protected function truncateString($value, array $params): string
|
||||
{
|
||||
$length = $params['length'] ?? 20; // 默认截断到20字符
|
||||
$suffix = $params['suffix'] ?? '...'; // 省略符
|
||||
if (! is_string($value)) {
|
||||
$value = (string) $value;
|
||||
}
|
||||
return mb_strlen($value) > $length
|
||||
? mb_substr($value, 0, $length) . $suffix
|
||||
: $value;
|
||||
}
|
||||
|
||||
// 枚举映射(示例参数化实现)
|
||||
protected function mapEnum($value, array $params): mixed
|
||||
{
|
||||
$map = $params['map'] ?? []; // 枚举映射表,如 [1 => '男', 2 => '女']
|
||||
return $map[$value] ?? $value;
|
||||
}
|
||||
|
||||
// 修正 buildQuery 方法,避免使用 new static()
|
||||
protected static function buildQuery(array $conditions = [], array $orderBy = []): Builder
|
||||
{
|
||||
// 使用静态 query() 方法获取查询构建器
|
||||
$query = static::query();
|
||||
|
||||
// 处理搜索条件
|
||||
foreach ($conditions as $field => $value) {
|
||||
$query->where($field, $value);
|
||||
}
|
||||
|
||||
// 处理排序
|
||||
foreach ($orderBy as $field => $direction) {
|
||||
$query->orderBy($field, $direction);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用搜索条件.
|
||||
*/
|
||||
protected function applySearch(Builder $query, array $conditions): void
|
||||
{
|
||||
foreach ($conditions as $field => $value) {
|
||||
// 跳过非字符串的字段名(防止索引数组键导致的类型错误)
|
||||
if (!is_string($field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理嵌套关系查询(如:user.name)
|
||||
if (str_contains($field, '.')) {
|
||||
[$relation, $relationField] = explode('.', $field, 2);
|
||||
$query->whereHas($relation, function ($q) use ($relationField, $value) {
|
||||
$q->where($relationField, $value);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否有自定义搜索器方法
|
||||
$searchMethod = 'search' . ucfirst($field);
|
||||
if (method_exists($this, $searchMethod)) {
|
||||
$this->{$searchMethod}($query, $value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 应用搜索规则配置
|
||||
if (isset($this->search[$field])) {
|
||||
$this->applySearchRule($query, $field, $value, $this->search[$field]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 默认精确匹配(仅对$search中允许的字段生效,因已通过白名单过滤)
|
||||
$query->where($field, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用搜索规则.
|
||||
*/
|
||||
protected function applySearchRule(Builder $query, string $field, mixed $value, array|string $rule): void
|
||||
{
|
||||
if (is_array($rule)) {
|
||||
$type = $rule[0];
|
||||
$params = $rule[1] ?? [];
|
||||
} else {
|
||||
$type = $rule;
|
||||
$params = [];
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'like':
|
||||
$query->where($field, 'like', "%{$value}%");
|
||||
break;
|
||||
case 'like_left':
|
||||
$query->where($field, 'like', "%{$value}");
|
||||
break;
|
||||
case 'like_right':
|
||||
$query->where($field, 'like', "{$value}%");
|
||||
break;
|
||||
case 'in':
|
||||
$query->whereIn($field, (array) $value);
|
||||
break;
|
||||
case 'not_in':
|
||||
$query->whereNotIn($field, (array) $value);
|
||||
break;
|
||||
case 'gt':
|
||||
$query->where($field, '>', $value);
|
||||
break;
|
||||
case 'lt':
|
||||
$query->where($field, '<', $value);
|
||||
break;
|
||||
case 'gte':
|
||||
$query->where($field, '>=', $value);
|
||||
break;
|
||||
case 'lte':
|
||||
$query->where($field, '<=', $value);
|
||||
break;
|
||||
case 'between':
|
||||
$query->whereBetween($field, (array) $value);
|
||||
break;
|
||||
case 'null':
|
||||
$query->whereNull($field);
|
||||
break;
|
||||
case 'not_null':
|
||||
$query->whereNotNull($field);
|
||||
break;
|
||||
default:
|
||||
$query->where($field, $type, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
49
extend/Aether/PHP/Hyperf/AetherService.php
Normal file
49
extend/Aether/PHP/Hyperf/AetherService.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether;
|
||||
|
||||
use Aether\Exception\BusinessException;
|
||||
use Hyperf\Di\Annotation\Inject;
|
||||
use Hyperf\Logger\LoggerFactory;
|
||||
use Hyperf\DbConnection\Db;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
abstract class AetherService
|
||||
{
|
||||
#[Inject]
|
||||
protected LoggerFactory $loggerFactory;
|
||||
|
||||
/**
|
||||
* 获取当前服务日志器
|
||||
*/
|
||||
protected function logger(): LoggerInterface
|
||||
{
|
||||
// return $this->loggerFactory->get(substr(strrchr(static::class, '\\'), 1));
|
||||
// 提取类名(如ExamTypeService)作为日志通道名
|
||||
$className = substr(strrchr(static::class, '\\'), 1);
|
||||
return $this->loggerFactory->get($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务处理
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function transaction(callable $callback)
|
||||
{
|
||||
return Db::transaction($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查资源是否存在(不存在则抛出异常)
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function checkResourceExists(?object $resource, string $message = '资源不存在'): void
|
||||
{
|
||||
if (empty($resource)) {
|
||||
throw new BusinessException($message, BusinessException::RESOURCE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
134
extend/Aether/PHP/Hyperf/AetherValidator.php
Normal file
134
extend/Aether/PHP/Hyperf/AetherValidator.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether;
|
||||
|
||||
use Aether\Exception\ValidationFailedException;
|
||||
use Hyperf\Di\Annotation\Inject;
|
||||
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
|
||||
use Hyperf\Validation\Validator;
|
||||
use Hyperf\Context\ApplicationContext;
|
||||
|
||||
abstract class AetherValidator
|
||||
{
|
||||
#[Inject]
|
||||
protected ValidatorFactoryInterface $validationFactory;
|
||||
|
||||
/**
|
||||
* 当前场景名
|
||||
*/
|
||||
public ?string $currentScene = null;
|
||||
|
||||
/**
|
||||
* 待验证数据
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
/**
|
||||
* 自定义验证规则(子类可通过该属性注册,无需重写registerRules)
|
||||
* 格式:['规则名' => 闭包/类方法]
|
||||
*/
|
||||
protected array $customRules = [];
|
||||
|
||||
/**
|
||||
* 静态快捷验证方法(简化调用)
|
||||
*/
|
||||
public static function validate(string $scene, array $data = []): array
|
||||
{
|
||||
// return (new static())->scene($scene, $data)->check();
|
||||
// 从容器中获取当前类的实例(确保依赖注入生效)
|
||||
$instance = ApplicationContext::getContainer()->get(static::class);
|
||||
return $instance->scene($scene, $data)->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置验证场景和数据(支持链式调用)
|
||||
*/
|
||||
public function scene(string $scene, array $data = []): self
|
||||
{
|
||||
$this->currentScene = $scene;
|
||||
$this->data = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行验证(失败抛出异常)
|
||||
*/
|
||||
public function check(): array
|
||||
{
|
||||
if (empty($this->currentScene)) {
|
||||
throw new \RuntimeException('请先设置验证场景');
|
||||
}
|
||||
|
||||
$scenes = $this->scenes();
|
||||
if (!isset($scenes[$this->currentScene])) {
|
||||
throw new \RuntimeException("验证场景不存在:{$this->currentScene}");
|
||||
}
|
||||
|
||||
$sceneConfig = $scenes[$this->currentScene];
|
||||
return $this->validateData(
|
||||
$this->data,
|
||||
$sceneConfig['rules'],
|
||||
$sceneConfig['messages'] ?? [],
|
||||
$sceneConfig['attributes'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际执行验证的逻辑(重命名方法名更清晰)
|
||||
*/
|
||||
protected function validateData(array $data, array $rules, array $messages = [], array $attributes = []): array
|
||||
{
|
||||
$validator = $this->validationFactory->make($data, $rules, $messages, $attributes);
|
||||
$this->registerRules($validator);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new ValidationFailedException(
|
||||
$validator,
|
||||
$this->currentScene ?? '',
|
||||
$validator->errors()->first()
|
||||
);
|
||||
}
|
||||
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化验证错误信息(统一格式,供异常处理器复用)
|
||||
*/
|
||||
public function formatValidationErrors(Validator $validator): array
|
||||
{
|
||||
$errors = [];
|
||||
$failedRules = $validator->failed();
|
||||
$errorMessages = $validator->errors()->getMessages();
|
||||
$attributes = $validator->attributes();
|
||||
|
||||
foreach ($failedRules as $field => $rules) {
|
||||
$errors[] = [
|
||||
'field' => $field,
|
||||
'field_label' => $attributes[$field] ?? $field,
|
||||
'message' => $errorMessages[$field][0] ?? '',
|
||||
'rules' => array_keys($rules),
|
||||
'value' => $validator->getValue($field)
|
||||
];
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动注册自定义规则(优先使用$customRules属性)
|
||||
*/
|
||||
protected function registerRules(Validator $validator): void
|
||||
{
|
||||
foreach ($this->customRules as $ruleName => $rule) {
|
||||
$validator->extend($ruleName, $rule);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义场景验证规则(子类实现)
|
||||
*/
|
||||
abstract protected function scenes(): array;
|
||||
}
|
||||
24
extend/Aether/PHP/Hyperf/Contract/TreeableInterface.php
Normal file
24
extend/Aether/PHP/Hyperf/Contract/TreeableInterface.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Aether\Contract;
|
||||
|
||||
/**
|
||||
* 树形结构接口,标识模型支持树形功能
|
||||
*/
|
||||
interface TreeableInterface
|
||||
{
|
||||
/**
|
||||
* 构建树形结构
|
||||
* @param array $items 原始数据
|
||||
* @param int $parentId 根节点ID
|
||||
* @return array
|
||||
*/
|
||||
public static function buildTree(array $items, int $parentId = 0): array;
|
||||
|
||||
/**
|
||||
* 获取子节点ID集合
|
||||
* @param int $id 节点ID
|
||||
* @return array
|
||||
*/
|
||||
public function getChildIds(int $id): array;
|
||||
}
|
||||
136
extend/Aether/PHP/Hyperf/Exception/AetherExceptionHandler.php
Normal file
136
extend/Aether/PHP/Hyperf/Exception/AetherExceptionHandler.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether\Exception;
|
||||
|
||||
use Aether\AetherValidator;
|
||||
use Hyperf\Contract\StdoutLoggerInterface;
|
||||
use Hyperf\Database\Model\ModelNotFoundException; // 引入模型未找到异常
|
||||
use Hyperf\ExceptionHandler\ExceptionHandler;
|
||||
use Hyperf\HttpMessage\Stream\SwooleStream;
|
||||
use Hyperf\Validation\ValidationException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Throwable;
|
||||
use Hyperf\Context\Context;
|
||||
use function Hyperf\Support\env;
|
||||
|
||||
class AetherExceptionHandler extends ExceptionHandler
|
||||
{
|
||||
public function __construct(protected StdoutLoggerInterface $logger)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$requestId = Context::get('request_id', uniqid());
|
||||
$result = $this->formatErrorResponse($throwable, $requestId);
|
||||
|
||||
// 记录错误日志(包含完整堆栈)
|
||||
$this->logger->error(sprintf(
|
||||
'Exception [%s] | RequestId: %s | Message: %s in %s:%d',
|
||||
get_class($throwable),
|
||||
$requestId,
|
||||
$throwable->getMessage(),
|
||||
$throwable->getFile(),
|
||||
$throwable->getLine()
|
||||
));
|
||||
if (env('APP_ENV') === 'dev') {
|
||||
$this->logger->error($throwable->getTraceAsString());
|
||||
}
|
||||
|
||||
// 确保状态码合法(100-599)
|
||||
$statusCode = $result['code'] ?? 500;
|
||||
if ($statusCode < 100 || $statusCode >= 600) {
|
||||
$statusCode = 500;
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withStatus($statusCode)
|
||||
->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE)));
|
||||
}
|
||||
|
||||
private function formatErrorResponse(Throwable $throwable, string $requestId): array
|
||||
{
|
||||
// 模型未找到异常
|
||||
if ($throwable instanceof ModelNotFoundException) {
|
||||
return [
|
||||
'code' => 404, // 资源不存在标准状态码
|
||||
'message' => $throwable->getMessage() ?: '请求的资源不存在',
|
||||
'data' => env('APP_ENV') === 'dev' ? [
|
||||
'file' => $throwable->getFile(),
|
||||
'line' => $throwable->getLine()
|
||||
] : null,
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
// 自定义验证异常
|
||||
if ($throwable instanceof ValidationFailedException) {
|
||||
return $this->formatValidationError($throwable, $requestId);
|
||||
}
|
||||
|
||||
// 原生验证异常
|
||||
if ($throwable instanceof ValidationException) {
|
||||
return $this->formatNativeValidationError($throwable, $requestId);
|
||||
}
|
||||
|
||||
// 其他异常(如RuntimeException等)
|
||||
return [
|
||||
'code' => 600,
|
||||
'message' => env('APP_ENV') === 'dev' ? $throwable->getMessage() : '服务暂时不可用',
|
||||
'data' => env('APP_ENV') === 'dev' ? [
|
||||
'file' => $throwable->getFile(),
|
||||
'line' => $throwable->getLine(),
|
||||
'trace' => explode("\n", $throwable->getTraceAsString())
|
||||
] : null,
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
private function formatValidationError(ValidationFailedException $e, string $requestId): array
|
||||
{
|
||||
$validatorInstance = new class extends AetherValidator {
|
||||
protected function scenes(): array { return []; }
|
||||
};
|
||||
|
||||
return [
|
||||
'code' => 422,
|
||||
'message' => $e->getMessage(),
|
||||
'data' => [
|
||||
'errors' => $validatorInstance->formatValidationErrors($e->validator),
|
||||
'scene' => $e->getScene(),
|
||||
'validated_data' => env('APP_ENV') === 'dev' ? $e->validator->getData() : null
|
||||
],
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
private function formatNativeValidationError(ValidationException $e, string $requestId): array
|
||||
{
|
||||
$validatorInstance = new class extends AetherValidator {
|
||||
protected function scenes(): array { return []; }
|
||||
};
|
||||
|
||||
return [
|
||||
'code' => 422,
|
||||
'message' => '参数验证失败',
|
||||
'data' => [
|
||||
'errors' => $validatorInstance->formatValidationErrors($e->validator),
|
||||
'validated_data' => env('APP_ENV') === 'dev' ? $e->validator->getData() : null
|
||||
],
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
public function isValid(Throwable $throwable): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
119
extend/Aether/PHP/Hyperf/Exception/AppExceptionHandler.php
Normal file
119
extend/Aether/PHP/Hyperf/Exception/AppExceptionHandler.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether\Exception;
|
||||
|
||||
use Aether\AetherValidator;
|
||||
use Hyperf\Contract\StdoutLoggerInterface;
|
||||
use Hyperf\ExceptionHandler\ExceptionHandler;
|
||||
use Hyperf\HttpMessage\Stream\SwooleStream;
|
||||
use Hyperf\Validation\ValidationException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Throwable;
|
||||
use Hyperf\Context\Context;
|
||||
use function Hyperf\Support\env;
|
||||
|
||||
class AppExceptionHandler extends ExceptionHandler
|
||||
{
|
||||
public function __construct(protected StdoutLoggerInterface $logger)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$requestId = Context::get('request_id', uniqid());
|
||||
$result = $this->formatErrorResponse($throwable, $requestId);
|
||||
|
||||
$this->logger->error(sprintf(
|
||||
'Exception: %s[%s] in %s:%d',
|
||||
get_class($throwable),
|
||||
$throwable->getMessage(),
|
||||
$throwable->getFile(),
|
||||
$throwable->getLine()
|
||||
));
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withStatus($result['code'] ?? 500)
|
||||
->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一错误响应格式
|
||||
*/
|
||||
private function formatErrorResponse(Throwable $throwable, string $requestId): array
|
||||
{
|
||||
// 处理自定义验证异常
|
||||
if ($throwable instanceof ValidationFailedException) {
|
||||
return $this->formatValidationError($throwable, $requestId);
|
||||
}
|
||||
|
||||
// 处理原生验证异常
|
||||
if ($throwable instanceof ValidationException) {
|
||||
return $this->formatNativeValidationError($throwable, $requestId);
|
||||
}
|
||||
|
||||
// 处理其他异常
|
||||
return [
|
||||
'code' => 500,
|
||||
'message' => env('APP_ENV') === 'dev' ? $throwable->getMessage() : '服务暂时不可用',
|
||||
'data' => env('APP_ENV') === 'dev' ? [
|
||||
'file' => $throwable->getFile(),
|
||||
'line' => $throwable->getLine(),
|
||||
'trace' => explode("\n", $throwable->getTraceAsString())
|
||||
] : null,
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化自定义验证异常
|
||||
*/
|
||||
private function formatValidationError(ValidationFailedException $e, string $requestId): array
|
||||
{
|
||||
// 复用AetherValidator的错误格式化方法
|
||||
$validatorInstance = new class extends AetherValidator {
|
||||
protected function scenes(): array { return []; }
|
||||
};
|
||||
|
||||
return [
|
||||
'code' => 422,
|
||||
'message' => $e->getMessage(),
|
||||
'data' => [
|
||||
'errors' => $validatorInstance->formatValidationErrors($e->validator),
|
||||
'scene' => $e->getScene(), // 直接从异常获取场景
|
||||
'validated_data' => env('APP_ENV') === 'dev' ? $e->validator->getData() : null
|
||||
],
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化原生验证异常(保持格式一致)
|
||||
*/
|
||||
private function formatNativeValidationError(ValidationException $e, string $requestId): array
|
||||
{
|
||||
$validatorInstance = new class extends AetherValidator {
|
||||
protected function scenes(): array { return []; }
|
||||
};
|
||||
|
||||
return [
|
||||
'code' => 422,
|
||||
'message' => '参数验证失败',
|
||||
'data' => [
|
||||
'errors' => $validatorInstance->formatValidationErrors($e->validator),
|
||||
'validated_data' => env('APP_ENV') === 'dev' ? $e->validator->getData() : null
|
||||
],
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
public function isValid(Throwable $throwable): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
40
extend/Aether/PHP/Hyperf/Exception/BusinessException.php
Normal file
40
extend/Aether/PHP/Hyperf/Exception/BusinessException.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether\Exception;
|
||||
|
||||
use Hyperf\Server\Exception\ServerException;
|
||||
|
||||
class BusinessException extends ServerException
|
||||
{
|
||||
// 错误码常量(按业务模块划分)
|
||||
public const VALIDATION_ERROR = 400; // 参数验证失败
|
||||
public const AUTH_ERROR = 401; // 认证失败
|
||||
public const PERMISSION_DENY = 403; // 权限不足
|
||||
public const RESOURCE_NOT_FOUND = 404; // 资源不存在
|
||||
public const SCENE_NOT_FOUND = 400;
|
||||
|
||||
/**
|
||||
* 额外错误数据(如验证详情)
|
||||
*/
|
||||
protected ?array $errorData = null;
|
||||
|
||||
public function __construct(
|
||||
string $message,
|
||||
int $code = 500,
|
||||
?\Throwable $previous = null,
|
||||
?array $errorData = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->errorData = $errorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取额外错误数据
|
||||
*/
|
||||
public function getErrorData(): ?array
|
||||
{
|
||||
return $this->errorData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Aether\Exception;
|
||||
|
||||
use Hyperf\Validation\ValidationException;
|
||||
use Hyperf\Validation\Validator;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class ValidationFailedException extends ValidationException
|
||||
{
|
||||
/**
|
||||
* 验证场景
|
||||
*/
|
||||
protected string $scene;
|
||||
|
||||
public function __construct(
|
||||
Validator $validator,
|
||||
string $scene,
|
||||
string $message = '参数验证失败',
|
||||
?ResponseInterface $response = null // 新增response参数,符合父类要求
|
||||
) {
|
||||
// 父类构造函数仅接受 $validator 和 $response
|
||||
parent::__construct($validator, $response);
|
||||
// 单独设置消息(父类的 $message 为 protected 属性,可直接赋值)
|
||||
$this->message = $message;
|
||||
$this->scene = $scene;
|
||||
}
|
||||
|
||||
public function getScene(): string
|
||||
{
|
||||
return $this->scene;
|
||||
}
|
||||
}
|
||||
159
extend/Aether/PHP/Hyperf/Traits/AetherEnum.php
Normal file
159
extend/Aether/PHP/Hyperf/Traits/AetherEnum.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether\Traits;
|
||||
|
||||
use App\Notice\Enum\NoticeStatusEnum;
|
||||
use App\Notice\Model\NoticeStatsModel;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionClass;
|
||||
|
||||
trait AetherEnum
|
||||
{
|
||||
/**
|
||||
* 获取所有枚举值数组(严格保持定义顺序).
|
||||
* @return array<int|string> 枚举值集合
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
$values = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$values[] = $case->value;
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有枚举描述数组(与values()顺序一一对应).
|
||||
* @return array<string> 描述文本集合
|
||||
*/
|
||||
public static function descriptions(): array
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
$descriptions = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$descriptions[] = $case->description();
|
||||
}
|
||||
return $descriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值-描述映射数组(用于下拉选择等场景).
|
||||
* @return array<int|string, string> 键为枚举值,值为描述文本
|
||||
*/
|
||||
public static function valueMap(): array
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
$map = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$map[$case->value] = $case->description();
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据值获取枚举实例(严格模式).
|
||||
* @param int|string $value 枚举值
|
||||
* @return AetherEnum|NoticeStatsModel|NoticeStatusEnum 枚举实例
|
||||
*/
|
||||
public static function fromValue(int|string $value): self
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
// 检查值类型是否与枚举类型匹配(基于第一个case的类型)
|
||||
$firstCase = self::cases()[0] ?? null;
|
||||
if ($firstCase) {
|
||||
$expectedType = gettype($firstCase->value);
|
||||
$actualType = gettype($value);
|
||||
if ($expectedType !== $actualType) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'枚举值类型不匹配,%s期望%s类型,实际为%s',
|
||||
self::class,
|
||||
$expectedType,
|
||||
$actualType
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$enum = self::tryFrom($value);
|
||||
if (! $enum) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'无效的%s值: %s,允许值: %s',
|
||||
self::class,
|
||||
$value,
|
||||
implode(', ', self::values())
|
||||
));
|
||||
}
|
||||
return $enum;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据描述获取枚举实例(精确匹配).
|
||||
* @param string $description 描述文本
|
||||
* @return null|AetherEnum|NoticeStatsModel|NoticeStatusEnum 匹配的枚举实例,无匹配时返回null
|
||||
*/
|
||||
public static function fromDescription(string $description): ?self
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
foreach (self::cases() as $case) {
|
||||
if ($case->description() === $description) {
|
||||
return $case;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否为有效的枚举值(严格类型检查).
|
||||
* @param int|string $value 待检查的值
|
||||
* @return bool 是否有效
|
||||
*/
|
||||
public static function isValidValue(int|string $value): bool
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
foreach (self::cases() as $case) {
|
||||
if ($case->value === $value) { // 严格相等,避免类型松散匹配
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验枚举结构合法性(替代__init,私有静态方法).
|
||||
*/
|
||||
private static function validateEnumStructure(): void
|
||||
{
|
||||
// 检查当前类是否为枚举
|
||||
if (! (new ReflectionClass(self::class))->isEnum()) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'AetherEnum trait仅允许枚举类使用,%s不是枚举',
|
||||
self::class
|
||||
));
|
||||
}
|
||||
|
||||
// 检查枚举是否实现了description()方法
|
||||
if (! method_exists(self::class, 'description')) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'枚举类%s必须实现description()方法(返回字符串描述)',
|
||||
self::class
|
||||
));
|
||||
}
|
||||
|
||||
// 检查description()方法返回值是否为字符串
|
||||
$sampleCase = self::cases()[0] ?? null;
|
||||
if ($sampleCase && ! is_string($sampleCase->description())) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'枚举类%s的description()方法必须返回字符串',
|
||||
self::class
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
10
extend/Aether/PHP/Hyperf/Traits/AetherSearchable.php
Normal file
10
extend/Aether/PHP/Hyperf/Traits/AetherSearchable.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether\Traits;
|
||||
|
||||
trait AetherSearchable
|
||||
{
|
||||
|
||||
}
|
||||
24
extend/Aether/PHP/Hyperf/Traits/AetherSoftDelete.php
Normal file
24
extend/Aether/PHP/Hyperf/Traits/AetherSoftDelete.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Aether\Traits;
|
||||
|
||||
use Hyperf\Database\Model\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 通用软删除Trait,供需要软删除的模型使用
|
||||
*/
|
||||
trait AetherSoftDelete
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* 初始化软删除相关配置(自动隐藏deleted_at字段)
|
||||
*/
|
||||
protected function initializeAetherSoftDeletes(): void
|
||||
{
|
||||
// 自动将deleted_at添加到隐藏字段,避免序列化时暴露
|
||||
if (!in_array('deleted_at', $this->hidden, true)) {
|
||||
$this->hidden[] = 'deleted_at';
|
||||
}
|
||||
}
|
||||
}
|
||||
125
extend/Aether/PHP/Hyperf/Traits/AetherTree.php
Normal file
125
extend/Aether/PHP/Hyperf/Traits/AetherTree.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Aether\Traits;
|
||||
|
||||
use Aether\AetherModel;
|
||||
use Hyperf\Database\Model\Collection;
|
||||
use LogicException;
|
||||
|
||||
trait AetherTree
|
||||
{
|
||||
// 初始化时检查当前类是否继承AetherModel
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
|
||||
if (!$this instanceof AetherModel) {
|
||||
throw new LogicException(
|
||||
"使用AetherTree trait的类必须继承AetherModel,当前类: " . get_class($this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽象方法:获取父ID字段名(由子类实现)
|
||||
*/
|
||||
abstract protected function getParentIdField(): string;
|
||||
|
||||
/**
|
||||
* 抽象方法:获取排序字段名(由子类实现)
|
||||
*/
|
||||
abstract protected function getSortField(): string;
|
||||
|
||||
/**
|
||||
* 构建树形结构
|
||||
*/
|
||||
public static function buildTree($items, int $parentId = 0): array
|
||||
{
|
||||
$self = new static();
|
||||
$parentField = $self->getParentIdField();
|
||||
$sortField = $self->getSortField();
|
||||
|
||||
$items = $items instanceof Collection ? $items->toArray() : $items;
|
||||
$tree = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item[$parentField] == $parentId) {
|
||||
$children = static::buildTree($items, $item['id']);
|
||||
if (!empty($children)) {
|
||||
$item['children'] = $children;
|
||||
}
|
||||
$tree[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$self->sortTreeItems($tree, $sortField);
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 树形节点排序
|
||||
*/
|
||||
protected function sortTreeItems(array &$items, string $sortField): void
|
||||
{
|
||||
usort($items, function ($a, $b) use ($sortField) {
|
||||
$direction = $this->treeSortDirection ?? 'asc';
|
||||
return $direction === 'desc'
|
||||
? $b[$sortField] <=> $a[$sortField]
|
||||
: $a[$sortField] <=> $b[$sortField];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定节点的所有子节点ID
|
||||
*/
|
||||
public function getChildIds(int $id): array
|
||||
{
|
||||
$parentField = $this->getParentIdField();
|
||||
// 现在可以安全调用newQuery(),因为已通过继承检查
|
||||
$allItems = $this->newQuery()->get(['id', $parentField])->toArray();
|
||||
$ids = [$id];
|
||||
|
||||
$this->collectChildIds($allItems, $id, $parentField, $ids);
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集子节点ID
|
||||
*/
|
||||
private function collectChildIds(array $items, int $parentId, string $parentField, array &$ids): void
|
||||
{
|
||||
foreach ($items as $item) {
|
||||
if ($item[$parentField] == $parentId) {
|
||||
$ids[] = $item['id'];
|
||||
$this->collectChildIds($items, $item['id'], $parentField, $ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的完整路径
|
||||
*/
|
||||
public function getPath(int $id): array
|
||||
{
|
||||
$parentField = $this->getParentIdField();
|
||||
// 安全调用newQuery()
|
||||
$node = $this->newQuery()->find($id);
|
||||
if (!$node) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$path = [$node->toArray()];
|
||||
$parentId = $node[$parentField];
|
||||
|
||||
while ($parentId > 0) {
|
||||
$parent = $this->newQuery()->find($parentId);
|
||||
if (!$parent) {
|
||||
break;
|
||||
}
|
||||
array_unshift($path, $parent->toArray());
|
||||
$parentId = $parent[$parentField];
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
14
phpstan.neon.dist
Normal file
14
phpstan.neon.dist
Normal file
@@ -0,0 +1,14 @@
|
||||
# Magic behaviour with __get, __set, __call and __callStatic is not exactly static analyser-friendly :)
|
||||
# Fortunately, You can ignore it by the following config.
|
||||
#
|
||||
# vendor/bin/phpstan analyse app --memory-limit 200M -l 0
|
||||
#
|
||||
parameters:
|
||||
level: 0
|
||||
paths:
|
||||
- ./app
|
||||
- ./config
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
ignoreErrors:
|
||||
- '#Static call to instance method Hyperf\\HttpServer\\Router\\Router::[a-zA-Z0-9\\_]+\(\)#'
|
||||
- '#Static call to instance method Hyperf\\DbConnection\\Db::[a-zA-Z0-9\\_]+\(\)#'
|
||||
16
phpunit.xml.dist
Normal file
16
phpunit.xml.dist
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="./test/bootstrap.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
|
||||
<testsuites>
|
||||
<testsuite name="Tests">
|
||||
<directory suffix="Test.php">./test</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing" force="true"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">./app</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
27
test/Cases/ExampleTest.php
Normal file
27
test/Cases/ExampleTest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest\Cases;
|
||||
|
||||
use Hyperf\Testing\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
public function testExample()
|
||||
{
|
||||
$this->get('/')->assertOk()->assertSee('Hyperf');
|
||||
}
|
||||
}
|
||||
45
test/HttpTestCase.php
Normal file
45
test/HttpTestCase.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest;
|
||||
|
||||
use Hyperf\Testing\Client;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
use function Hyperf\Support\make;
|
||||
|
||||
/**
|
||||
* Class HttpTestCase.
|
||||
* @method get($uri, $data = [], $headers = [])
|
||||
* @method post($uri, $data = [], $headers = [])
|
||||
* @method json($uri, $data = [], $headers = [])
|
||||
* @method file($uri, $data = [], $headers = [])
|
||||
* @method request($method, $path, $options = [])
|
||||
*/
|
||||
abstract class HttpTestCase extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
public function __construct($name = null, array $data = [], $dataName = '')
|
||||
{
|
||||
parent::__construct($name, $data, $dataName);
|
||||
$this->client = make(Client::class);
|
||||
}
|
||||
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
return $this->client->{$name}(...$arguments);
|
||||
}
|
||||
}
|
||||
30
test/bootstrap.php
Normal file
30
test/bootstrap.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
ini_set('display_errors', 'on');
|
||||
ini_set('display_startup_errors', 'on');
|
||||
|
||||
error_reporting(E_ALL);
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
|
||||
Swoole\Runtime::enableCoroutine(true);
|
||||
|
||||
! defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));
|
||||
|
||||
require BASE_PATH . '/vendor/autoload.php';
|
||||
|
||||
! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', Hyperf\Engine\DefaultOption::hookFlags());
|
||||
|
||||
Hyperf\Di\ClassLoader::init();
|
||||
|
||||
$container = require BASE_PATH . '/config/container.php';
|
||||
|
||||
$container->get(Hyperf\Contract\ApplicationInterface::class);
|
||||
Reference in New Issue
Block a user