├── .github └── workflows │ └── php.yml ├── .gitignore ├── .php-cs-fixer.php ├── LICENSE ├── README.md ├── bin ├── release.sh ├── split-linux.sh ├── split.sh ├── splitsh-lite └── splitsh-lite-linux ├── composer.json ├── phpstan.neon ├── phpunit.xml ├── src ├── aop │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Aop.php │ │ ├── AstManager.php │ │ ├── Attribute │ │ └── AspectConfig.php │ │ ├── Collector │ │ ├── AbstractCollector.php │ │ ├── AspectCollector.php │ │ └── PropertyAttributeCollector.php │ │ ├── Composer.php │ │ ├── Contract │ │ ├── AspectInterface.php │ │ ├── CollectorInterface.php │ │ └── PropertyAttribute.php │ │ ├── Exception │ │ └── PropertyHandleException.php │ │ ├── Metadata.php │ │ ├── ProceedingJoinPoint.php │ │ ├── PropertyHandler.php │ │ ├── PropertyHandlerVisitor.php │ │ ├── ProxyHandler.php │ │ └── ProxyHandlerVisitor.php ├── cache │ ├── LICENSE │ ├── README.md │ ├── composer.json │ ├── src │ │ ├── Cache.php │ │ ├── CacheException.php │ │ ├── Contract │ │ │ └── CacheDriverInterface.php │ │ ├── Driver │ │ │ ├── AbstractDriver.php │ │ │ ├── ApcuDriver.php │ │ │ ├── FileDriver.php │ │ │ ├── MemcachedDriver.php │ │ │ └── RedisDriver.php │ │ └── InvalidArgumentException.php │ └── tests │ │ └── CacheTest.php ├── config │ ├── LICENSE │ ├── README.md │ ├── composer.json │ ├── src │ │ ├── Contract │ │ │ └── ConfigInterface.php │ │ └── Repository.php │ └── tests │ │ └── RepositoryTest.php ├── database │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Contract │ │ └── ConfigInterface.php │ │ ├── Database.php │ │ ├── Event │ │ └── QueryExecuted.php │ │ ├── PDOConfig.php │ │ └── Query.php ├── di │ ├── .phpstorm.meta.php │ ├── LICENSE │ ├── README.md │ ├── composer.json │ ├── src │ │ ├── Container.php │ │ ├── Context.php │ │ ├── Exception │ │ │ ├── ContainerException.php │ │ │ └── NotFoundException.php │ │ ├── Reflection.php │ │ └── helpers.php │ └── tests │ │ ├── Bar.php │ │ ├── ContainerTest.php │ │ ├── ContextTest.php │ │ ├── Foo.php │ │ └── FooInterface.php ├── event │ ├── LICENSE │ ├── README.md │ ├── composer.json │ ├── src │ │ ├── Contract │ │ │ └── EventListenerInterface.php │ │ ├── EventDispatcher.php │ │ ├── EventListener.php │ │ └── ListenerProvider.php │ └── tests │ │ ├── EventDispatcherTest.php │ │ ├── Events │ │ ├── BarEvent.php │ │ └── FooEvent.php │ │ ├── ListenerProviderTest.php │ │ └── Listeners │ │ ├── BarListener.php │ │ └── FooListener.php ├── foundation │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Cache │ │ └── Aspect │ │ │ └── Cacheable.php │ │ ├── Config │ │ └── Attribute │ │ │ └── Config.php │ │ ├── Console │ │ ├── Attribute │ │ │ └── Command.php │ │ └── Collector │ │ │ └── CommandCollector.php │ │ ├── Di │ │ └── Attribute │ │ │ └── Inject.php │ │ ├── Event │ │ ├── Attribute │ │ │ └── Listen.php │ │ └── Collector │ │ │ └── ListenerCollector.php │ │ └── Routing │ │ ├── Attribute │ │ ├── Controller.php │ │ ├── DeleteMapping.php │ │ ├── GetMapping.php │ │ ├── PatchMapping.php │ │ ├── PostMapping.php │ │ ├── PutMapping.php │ │ └── RequestMapping.php │ │ └── Collector │ │ └── RouteCollector.php ├── http-message │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Bag │ │ ├── CookieBag.php │ │ ├── FileBag.php │ │ ├── HeaderBag.php │ │ ├── ParameterBag.php │ │ └── ServerBag.php │ │ ├── Contract │ │ └── StatusCodeInterface.php │ │ ├── Cookie.php │ │ ├── Exception │ │ └── HttpException.php │ │ ├── Message.php │ │ ├── Request.php │ │ ├── Response.php │ │ ├── ServerRequest.php │ │ ├── Stream │ │ ├── FileStream.php │ │ └── StandardStream.php │ │ ├── UploadedFile.php │ │ ├── Uri.php │ │ └── helpers.php ├── http-server │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Exception │ │ ├── MethodNotAllowedException.php │ │ └── NotFoundException.php │ │ └── RequestHandler.php ├── http-throttle │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Handlers │ │ ├── CounterFixed.php │ │ ├── CounterSlider.php │ │ ├── LeakyBucket.php │ │ ├── ThrottleAbstract.php │ │ └── TokenBucket.php │ │ ├── RateLimitException.php │ │ └── ThrottleMiddleware.php ├── json-rpc │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Attribute │ │ └── RpcService.php │ │ ├── Client.php │ │ ├── Event │ │ └── RpcCalled.php │ │ ├── Message │ │ ├── Error.php │ │ ├── Request.php │ │ └── Response.php │ │ ├── Server.php │ │ └── ServiceCollector.php ├── pipeline │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Abort.php │ │ └── Context.php ├── pool │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── BasePool.php │ │ ├── BasePoolItem.php │ │ └── Contract │ │ ├── PoolInterface.php │ │ └── PoolItemInterface.php ├── redis │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Client.php │ │ ├── Connector │ │ ├── BaseConnector.php │ │ ├── BasePoolConnector.php │ │ └── SwoolePoolConnector.php │ │ └── Contract │ │ └── ConnectorInterface.php ├── routing │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Exception │ │ ├── MethodNotAllowedException.php │ │ └── RouteNotFoundException.php │ │ ├── Route.php │ │ ├── RouteCollection.php │ │ ├── Router.php │ │ └── UrlMatcher.php ├── session │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Handler │ │ ├── FileHandler.php │ │ └── RedisHandler.php │ │ ├── Session.php │ │ └── SessionException.php ├── swoole │ ├── LICENSE │ ├── README.md │ ├── composer.json │ ├── src │ │ ├── Context.php │ │ ├── Exception │ │ │ └── ParallelExecutionException.php │ │ ├── Parallel.php │ │ └── Table │ │ │ ├── Exception │ │ │ ├── DuplicateKeyException.php │ │ │ └── ModelNotFoundException.php │ │ │ ├── Manager.php │ │ │ └── Model.php │ └── tests │ │ └── Concerns │ │ └── UserTable.php ├── utils │ ├── LICENSE │ ├── README.md │ ├── composer.json │ ├── src │ │ ├── Arr.php │ │ ├── BC.php │ │ ├── Collection.php │ │ ├── Composer.php │ │ ├── Contract │ │ │ ├── Arrayable.php │ │ │ ├── CanBeEscapedWhenCastToString.php │ │ │ ├── DeferringDisplayableValue.php │ │ │ ├── Enumerable.php │ │ │ ├── Htmlable.php │ │ │ ├── Jsonable.php │ │ │ ├── MessageBag.php │ │ │ ├── MessageProvider.php │ │ │ ├── PackerInterface.php │ │ │ ├── ValidatedData.php │ │ │ └── Xmlable.php │ │ ├── Exception │ │ │ ├── FileNotFoundException.php │ │ │ ├── ItemNotFoundException.php │ │ │ └── MultipleItemsFoundException.php │ │ ├── Filesystem.php │ │ ├── Fluent.php │ │ ├── LazyCollection.php │ │ ├── Macroable.php │ │ ├── MessageBag.php │ │ ├── NamespacedItemResolver.php │ │ ├── Optional.php │ │ ├── Packer │ │ │ ├── JsonPacker.php │ │ │ └── PhpSerializePacker.php │ │ ├── Pipeline.php │ │ ├── Pluralizer.php │ │ ├── Proxy │ │ │ ├── HigherOrderCollectionProxy.php │ │ │ ├── HigherOrderTapProxy.php │ │ │ └── HigherOrderWhenProxy.php │ │ ├── Str.php │ │ ├── Stringable.php │ │ ├── Traits │ │ │ ├── AutoFillProperties.php │ │ │ ├── Conditionable.php │ │ │ ├── EnumeratesValues.php │ │ │ └── Tappable.php │ │ ├── ValidatedInput.php │ │ ├── Xml.php │ │ └── helpers.php │ └── tests │ │ └── BCTest.php ├── validation │ └── src │ │ ├── RuleInterface.php │ │ └── Rules │ │ └── In.php ├── var-dumper │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ │ ├── Adapter │ │ └── HyperfDumperHandler.php │ │ ├── Dumper.php │ │ ├── DumperHandler.php │ │ └── helpers.php ├── view │ ├── LICENSE │ ├── README.md │ ├── composer.json │ ├── publish │ │ └── view.php │ └── src │ │ ├── Contract │ │ └── ViewEngineInterface.php │ │ ├── Engine │ │ ├── Blade │ │ │ └── Compiler.php │ │ ├── BladeEngine.php │ │ └── PhpEngine.php │ │ ├── Exception │ │ └── ViewNotExistException.php │ │ ├── Renderer.php │ │ └── ViewFactory.php └── watcher │ ├── LICENSE │ ├── README.md │ ├── composer.json │ └── src │ ├── Contract │ └── DriverInterface.php │ ├── Driver │ ├── FindDriver.php │ └── InotifyDriver.php │ └── Watcher.php └── tests └── bootstrap.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: '8.2' 24 | tools: composer 25 | 26 | - name: Validate composer.json and composer.lock 27 | run: composer validate --strict 28 | 29 | - name: Cache Composer packages 30 | id: composer-cache 31 | uses: actions/cache@v3 32 | with: 33 | path: vendor 34 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 35 | restore-keys: | 36 | ${{ runner.os }}-php- 37 | 38 | - name: Install dependencies 39 | run: composer install --prefer-dist --no-progress 40 | 41 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 42 | # Docs: https://getcomposer.org/doc/articles/scripts.md 43 | 44 | - name: Run test suite 45 | run: composer test 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | .phpunit.cache 4 | .phpunit.result.cache 5 | composer.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Max 3 |

4 | 5 |

轻量 • 简单 • 快速

6 | 7 |

8 | 9 | 10 | 11 | 12 |

13 | 14 | 这是一款组件包,提供了一些web开发常用的组件,而且大部分组件都是可以独立使用的。参与开发可以直接向该包提交代码,将会同步至相应的包。 15 | 16 | ## 主要组件 17 | 18 | - 符合 Psr7 的 [next/http-message](https://github.com/next-laboratory/http-message) HTTP消息 19 | - 符合 Psr7 的 [next/routing](https://github.com/next-laboratory/routing) 路由组件 20 | - 符合 Psr11 的 [next/di](https://github.com/next-laboratory/di) 依赖注入容器 21 | - 符合 Psr14 的 [next/event](https://github.com/next-laboratory/event) 事件 22 | - 符合 Psr15 的 [next/http-server](https://github.com/next-laboratory/http-server) HTTP服务 23 | - 符合 Psr16 的 [next/cache](https://github.com/next-laboratory/cache),支持 File,Memcached,Redis,APC [可扩展] 24 | - AOP [next/aop](https://github.com/next-laboratory/aop) 25 | - database [next/database](https://github.com/next-laboratory/database) 26 | - session [next/session](https://github.com/next-laboratory/session) 27 | 28 | ## 贡献一览 29 | 30 | [![Contributor over time](https://contributor-overtime-api.apiseven.com/contributors-svg?chart=contributorOverTime&repo=next-laboratory/next)](https://contributor-overtime-api.apiseven.com/contributors-svg?chart=contributorOverTime&repo=next-laboratory/next) 31 | 32 | 欢迎有兴趣的朋友参与开发 33 | 34 | ## 致谢 35 | 36 | 感谢PHP最好用IDE: PHPStorm 37 | -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if (( "$#" == 0 )) 6 | then 7 | echo "Tag has to be provided" 8 | 9 | exit 1 10 | fi 11 | 12 | NOW=$(date +%s) 13 | CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 14 | VERSION=$1 15 | BASEPATH=$(cd `dirname $0`; cd ../src/; pwd) 16 | 17 | # Always prepend with "v" 18 | #if [[ $VERSION != v* ]] 19 | #then 20 | # VERSION="v$VERSION" 21 | #fi 22 | 23 | if [ -z $2 ] ; then 24 | repos=$(ls $BASEPATH) 25 | else 26 | repos=${@:2} 27 | fi 28 | 29 | for REMOTE in $repos 30 | do 31 | echo "" 32 | echo "" 33 | echo "Cloning $REMOTE"; 34 | TMP_DIR="/tmp/nextphp-split" 35 | REMOTE_URL="git@github.com:next-laboratory/$REMOTE.git" 36 | 37 | rm -rf $TMP_DIR; 38 | mkdir $TMP_DIR; 39 | 40 | ( 41 | cd $TMP_DIR; 42 | 43 | git clone $REMOTE_URL . 44 | git checkout "$CURRENT_BRANCH"; 45 | 46 | if [[ $(git log --pretty="%d" -n 1 | grep tag --count) -eq 0 ]]; then 47 | echo "Releasing $REMOTE" 48 | git tag $VERSION 49 | git push origin --tags 50 | fi 51 | ) 52 | done 53 | 54 | TIME=$(echo "$(date +%s) - $NOW" | bc) 55 | 56 | printf "Execution time: %f seconds" $TIME 57 | -------------------------------------------------------------------------------- /bin/split-linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | CURRENT_BRANCH="master" 7 | BASEPATH=$(cd `dirname $0`; cd ../src/; pwd) 8 | REPOS=$@ 9 | 10 | function split() 11 | { 12 | SHA1=`./bin/splitsh-lite-linux --prefix=$1` 13 | git push $2 "$SHA1:refs/heads/$CURRENT_BRANCH" -f 14 | } 15 | 16 | function remote() 17 | { 18 | git remote add $1 $2 || true 19 | } 20 | 21 | git pull origin $CURRENT_BRANCH 22 | 23 | if [[ $# -eq 0 ]]; then 24 | REPOS=$(ls $BASEPATH) 25 | fi 26 | 27 | for REPO in $REPOS ; do 28 | remote $REPO git@github.com:next-laboratory/$REPO.git 29 | 30 | split "src/$REPO" $REPO 31 | done 32 | -------------------------------------------------------------------------------- /bin/split.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 7 | BASEPATH=$(cd `dirname $0`; cd ../src/; pwd) 8 | REPOS=$@ 9 | 10 | function split() 11 | { 12 | SHA1=`./bin/splitsh-lite --prefix=$1` 13 | git push $2 "$SHA1:refs/heads/$CURRENT_BRANCH" -f 14 | } 15 | 16 | function remote() 17 | { 18 | git remote add $1 $2 || true 19 | } 20 | 21 | git pull origin $CURRENT_BRANCH 22 | 23 | if [[ $# -eq 0 ]]; then 24 | REPOS=$(ls $BASEPATH) 25 | fi 26 | 27 | for REPO in $REPOS ; do 28 | remote $REPO git@github.com:next-laboratory/$REPO.git 29 | 30 | split "src/$REPO" $REPO 31 | done 32 | -------------------------------------------------------------------------------- /bin/splitsh-lite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-laboratory/next/e284de7d30f202ceac161371d20c48c5afedd03b/bin/splitsh-lite -------------------------------------------------------------------------------- /bin/splitsh-lite-linux: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/next-laboratory/next/e284de7d30f202ceac161371d20c48c5afedd03b/bin/splitsh-lite-linux -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/next", 3 | "license": "Apache-2.0", 4 | "description": "一款支持swoole, workerman, FPM环境的框架的组件化的轻量`PHP`框架。", 5 | "homepage": "https://github.com/next-laboratory/next", 6 | "keywords": [ 7 | "nextphp", 8 | "flexible", 9 | "php framework" 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "Next\\Aop\\": "src/aop/src", 14 | "Next\\Cache\\": "src/cache/src", 15 | "Next\\Config\\": "src/config/src", 16 | "Next\\Database\\": "src/database/src", 17 | "Next\\Di\\": "src/di/src", 18 | "Next\\Event\\": "src/event/src", 19 | "Next\\Foundation\\": "src/foundation/src", 20 | "Next\\Http\\Message\\": "src/http-message/src", 21 | "Next\\Http\\Server\\": "src/http-server/src", 22 | "Next\\Routing\\": "src/routing/src", 23 | "Next\\Session\\": "src/session/src", 24 | "Next\\Utils\\": "src/utils/src", 25 | "Next\\VarDumper\\": "src/var-dumper/src" 26 | }, 27 | "files": [ 28 | "src/di/src/helpers.php", 29 | "src/utils/src/helpers.php", 30 | "src/var-dumper/src/helpers.php" 31 | ] 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Tests\\": "tests/", 36 | "Next\\Event\\Tests\\": "src/event/tests/" 37 | } 38 | }, 39 | "authors": [ 40 | { 41 | "name": "chengyao", 42 | "email": "chengyao0320@foxmail.com" 43 | } 44 | ], 45 | "require": { 46 | "php": "^8.2", 47 | "psr/container": "^2.0", 48 | "psr/simple-cache": "^1.0", 49 | "psr/event-dispatcher": "^1.0", 50 | "psr/http-message": "^2.0", 51 | "psr/http-server-middleware": "^1.0", 52 | "psr/http-server-handler": "^1.0", 53 | "psr/log": "^3.0", 54 | "symfony/var-dumper": "^7.0", 55 | "voku/portable-ascii": "^2.0", 56 | "workerman/workerman": "^4.1", 57 | "hyperf/exception-handler": "^3.1", 58 | "league/commonmark": "^2.4", 59 | "ramsey/uuid": "^4.7" 60 | }, 61 | "require-dev": { 62 | "friendsofphp/php-cs-fixer": "*", 63 | "phpstan/phpstan": "*", 64 | "doctrine/inflector": "*", 65 | "pestphp/pest": "^2.34" 66 | }, 67 | "replace": { 68 | "next/aop": "*", 69 | "next/cache": "*", 70 | "next/config": "*", 71 | "next/database": "*", 72 | "next/di": "*", 73 | "next/event": "*", 74 | "next/http-message": "*", 75 | "next/http-server": "*", 76 | "next/foundation": "*", 77 | "next/routing": "*", 78 | "next/session": "*", 79 | "next/utils": "*", 80 | "next/var-dumper": "*" 81 | }, 82 | "scripts": { 83 | "test": "@php ./vendor/bin/phpunit -c phpunit.xml --colors=always", 84 | "cs-fix": "@php ./vendor/bin/php-cs-fixer fix $1", 85 | "analyse": "@php ./vendor/bin/phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon" 86 | }, 87 | "config": { 88 | "allow-plugins": { 89 | "pestphp/pest-plugin": true 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | # Magic behaviour with __get, __set, __call and __callStatic is not exactly static analyser-friendly :) 2 | # Fortunately, You can ingore it by the following config. 3 | # 4 | # vendor/bin/phpstan analyse app --memory-limit 200M -l 0 5 | # 6 | parameters: 7 | reportUnmatchedIgnoredErrors: false 8 | ignoreErrors: 9 | - '#Unsafe usage of new static\(\)#' 10 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src/aop 6 | ./src/cache 7 | ./src/config 8 | ./src/di 9 | ./src/event 10 | ./src/http-message 11 | ./src/http-server 12 | ./src/routing 13 | ./src/session 14 | ./src/utils 15 | 16 | 17 | 18 | 19 | ./src/aop/tests 20 | ./src/cache/tests 21 | ./src/config/tests 22 | ./src/di/tests 23 | ./src/event/tests 24 | ./src/http-message/tests 25 | ./src/http-server/tests 26 | ./src/routing/tests 27 | ./src/session/tests 28 | ./src/utils/tests 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/aop/README.md: -------------------------------------------------------------------------------- 1 | # !!! 不能生产使用,仅作为研究用途 2 | 3 | 一款简单Aop实现。支持常驻内存型PHP应用。可以方便接入nextphp, Swoole,WebMan等框架。 4 | 5 | # 环境要求 6 | 7 | ``` 8 | php 8.2 9 | 开启passthru函数 10 | ``` 11 | 12 | # 安装 13 | 14 | ```shell 15 | composer require next/aop 16 | ``` 17 | 18 | # 使用,以下以webman为例 19 | 20 | ## 修改start.php文件 21 | 22 | ```php 23 | Aop::init( 24 | [__DIR__ . '/../app'], 25 | [ 26 | \Next\Aop\Collector\PropertyAttributeCollector::class, 27 | \Next\Aop\Collector\AspectCollector::class, 28 | ], 29 | __DIR__ . '/../runtime/aop', 30 | ); 31 | ``` 32 | 33 | * paths 注解扫描路径 34 | * collectors 注解收集器 35 | - \Next\Aop\Collector\AspectCollector::class 切面收集器,取消后不能使用切面 36 | - \Next\Aop\Collector\PropertyAttributeCollector::class 属性注解收集器,取消后不支持属性自动注入 37 | * runtimeDir 运行时,生成的代理类和代理类地图会被缓存到这里 38 | 39 | ## 编写切面类,实现AspectInterface接口 40 | 41 | ```php 42 | proceed(); // 直接调用被代理的方法 56 | // $result = $joinPoint->process(); // 继续执行其他切面逻辑 57 | echo 'after'; 58 | return $result; 59 | } 60 | } 61 | ``` 62 | 63 | 修改方法添加切面注解 64 | 65 | ```php 66 | 87 | 注意上面添加了两个注解,属性和方法注解的作用分别为注入属性和切入方法,可以直接在控制器中打印属性$request发现已经被注入了,切面注解可以有多个,会按照顺序执行。具体实现可以参考这两个类,注意这里的Inject注解并不是从webman容器中获取实例,所以使用的话需要重新定义Inject以保证单例 88 | 89 | 你也可以使用`AspectConfig`注解类配置要切入的类,例如上面的切面类 90 | 91 | ```php 92 | process(); 108 | echo 'after'; 109 | return $result; 110 | } 111 | } 112 | 113 | ``` 114 | 115 | 那么`BaconQrCode\Writer`类的`writeFile`方法将会被切入,该注解可以传递第三个参数数组,作为该切面构造函数的参数 116 | 117 | ## 启动 118 | 119 | ```shell 120 | php start.php start 121 | ``` 122 | 123 | 打开浏览器打开~~对应页面~~ 124 | 125 | ## 控制台输出内容为 126 | 127 | ``` 128 | before--controller--after 129 | ``` -------------------------------------------------------------------------------- /src/aop/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/aop", 3 | "description": "An AOP package that is resident in in-memory PHP frameworks, such as nextphp, WebMan.", 4 | "homepage": "https://github.com/next-laboratory/aop", 5 | "license": "Apache-2.0", 6 | "autoload": { 7 | "psr-4": { 8 | "Next\\Aop\\": "src/" 9 | } 10 | }, 11 | "authors": [ 12 | { 13 | "name": "chengyao", 14 | "email": "chengyao0320@foxmail.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "^8.2", 19 | "next/di": "^0.1", 20 | "nikic/php-parser": "^5.1", 21 | "symfony/finder": "^7.0", 22 | "symfony/string": "^7.0" 23 | }, 24 | "extra": { 25 | "branch-alias": { 26 | "dev-master": "0.1.x-dev" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/aop/src/AstManager.php: -------------------------------------------------------------------------------- 1 | parser = (new ParserFactory())->createForHostVersion(); 28 | } 29 | 30 | public function getNodes(string $realpath) 31 | { 32 | return $this->parser->parse(file_get_contents($realpath)); 33 | } 34 | 35 | public function getClassesByRealPath(string $realpath): array 36 | { 37 | $classes = []; 38 | foreach ($this->getNodes($realpath) as $stmt) { 39 | if ($stmt instanceof Namespace_) { 40 | $namespace = $stmt->name->toCodeString(); 41 | foreach ($stmt->stmts as $subStmt) { 42 | // TODO 不支持Trait 43 | if ($subStmt instanceof Class_) { 44 | $classes[] = $namespace . '\\' . $subStmt->name->toString(); 45 | } 46 | } 47 | } 48 | } 49 | return $classes; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/aop/src/Attribute/AspectConfig.php: -------------------------------------------------------------------------------- 1 | getMethods() as $reflectionMethod) { 40 | if (! $reflectionMethod->isConstructor()) { 41 | self::$container[$class][$reflectionMethod->getName()][] = $attribute; 42 | } 43 | } 44 | } elseif ($attribute instanceof AspectConfig) { 45 | $reflectionClass = Reflection::class($attribute->class); 46 | $annotation = new $class(...$attribute->params); 47 | $methods = $attribute->methods; 48 | if ($methods === '*') { 49 | foreach ($reflectionClass->getMethods() as $reflectionMethod) { 50 | if (! $reflectionMethod->isConstructor()) { 51 | self::$container[$attribute->class][$reflectionMethod->getName()][] = $annotation; 52 | } 53 | } 54 | } else { 55 | foreach ((array) $methods as $method) { 56 | self::$container[$attribute->class][$method][] = $annotation; 57 | } 58 | } 59 | Aop::addClass($attribute->class, $reflectionClass->getFileName()); 60 | } 61 | } 62 | 63 | /** 64 | * 返回某个类方法的切面. 65 | */ 66 | public static function getMethodAspects(string $class, string $method): array 67 | { 68 | return self::$container[$class][$method] ?? []; 69 | } 70 | 71 | /** 72 | * 返回被收集过的类. 73 | * 74 | * @return string[] 75 | */ 76 | public static function getCollectedClasses(): array 77 | { 78 | return array_keys(self::$container); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/aop/src/Collector/PropertyAttributeCollector.php: -------------------------------------------------------------------------------- 1 | $aspects 23 | */ 24 | public function __construct( 25 | protected array $aspects, 26 | protected \Closure $callback, 27 | public string $class, 28 | public string $method, 29 | public \ArrayObject $parameters, 30 | ) {} 31 | 32 | public function process() 33 | { 34 | if ($aspect = array_shift($this->aspects)) { 35 | return $aspect->process($this); 36 | } 37 | 38 | return $this->proceed(); 39 | } 40 | 41 | /** 42 | * 执行代理方法. 43 | */ 44 | public function proceed(): mixed 45 | { 46 | return call_user_func_array($this->callback, $this->parameters->getArrayCopy()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/aop/src/PropertyHandler.php: -------------------------------------------------------------------------------- 1 | __propertyHandled) { 23 | foreach (PropertyAttributeCollector::getByClass(self::class) as $property => $attributes) { 24 | foreach ($attributes as $attribute) { 25 | $attribute->handle($this, $property); 26 | } 27 | } 28 | $this->__propertyHandled = true; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/aop/src/ProxyHandler.php: -------------------------------------------------------------------------------- 1 | fn(JoinPoint $joinPoint) => $aspect->process($joinPoint, $stack), 29 | // fn(JoinPoint $joinPoint) => $joinPoint->process() 30 | // ); 31 | $args = new \ArrayObject(); 32 | $methodParameters = Reflection::methodParameterNames($class, $method); 33 | foreach ($parameters as $key => $parameter) { 34 | $args->offsetSet($methodParameters[$key], $parameter); 35 | } 36 | 37 | $aspects = AspectCollector::getMethodAspects($class, $method); 38 | return (new ProceedingJoinPoint($aspects, $callback, $class, $method, $args))->process(); 39 | // return $pipeline(new JoinPoint($class, $method, $funcArgs, $callback)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/aop/src/ProxyHandlerVisitor.php: -------------------------------------------------------------------------------- 1 | stmts = array_merge( 44 | [new TraitUse([new Name('\Next\Aop\ProxyHandler')])], 45 | $node->stmts 46 | ); 47 | } 48 | if ($node instanceof ClassMethod) { 49 | $methodName = $node->name->toString(); 50 | if ($methodName === '__construct') { 51 | $this->metadata->hasConstructor = true; 52 | return; 53 | } 54 | if (AspectCollector::getMethodAspects($this->metadata->className, $methodName)) { 55 | $methodCall = new Node\Expr\StaticCall( 56 | new Name('self'), 57 | '__callViaProxy', 58 | [ 59 | new Arg(new Function_()), 60 | new Arg(new Closure([ 61 | 'params' => $node->getParams(), 62 | 'stmts' => $node->stmts, 63 | ])), 64 | new Arg(new FuncCall(new Name('func_get_args'))), 65 | ] 66 | ); 67 | $returnType = $node->getReturnType(); 68 | if ($returnType instanceof Identifier && $returnType->name === 'void') { 69 | $node->stmts = [new Expression($methodCall)]; 70 | } else { 71 | if ($node->returnsByRef()) { 72 | $valueRef = new Variable('__returnValueRef_' . ByteString::fromRandom(16)); 73 | $node->stmts = [new Expression(new Assign($valueRef, $methodCall)), new Return_($valueRef)]; 74 | } else { 75 | $node->stmts = [new Return_($methodCall)]; 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/cache/README.md: -------------------------------------------------------------------------------- 1 |

2 | Max 3 |

4 | 5 |

轻量 • 简单 • 快速

6 | 7 |

8 | 9 | 10 |

11 | 12 | # 起步 13 | 14 | 符合Psr16的缓存组件,支持File,Memcached,Redis,Apcu驱动。协程环境下需要自定义驱动 15 | 16 | ## 安装 17 | 18 | ``` 19 | composer require next/cache 20 | ``` 21 | 22 | ## 使用 23 | 24 | ```php 25 | set('stat', 12, 10); 32 | //读取缓存 33 | var_dump($cache->get('stat')); 34 | 35 | ``` 36 | -------------------------------------------------------------------------------- /src/cache/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/cache", 3 | "type": "library", 4 | "license": "Apache-2.0", 5 | "homepage": "https://github.com/next-laboratory/cache", 6 | "description": "A cache component based on PSR16 specification which supports File,Redis,Memcached and APC.", 7 | "keywords": [ 8 | "cache", 9 | "nextphp" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "chengyao", 14 | "email": "chengyao0320@foxmail.com", 15 | "homepage": "https://www.chengyao.xyz" 16 | } 17 | ], 18 | "autoload": { 19 | "psr-4": { 20 | "Next\\Cache\\": "src/" 21 | } 22 | }, 23 | "autoload-dev": { 24 | "psr-4": { 25 | "Next\\Cache\\Tests\\": "tests/" 26 | } 27 | }, 28 | "require": { 29 | "php": "^8.2", 30 | "next/utils": "~0.1", 31 | "psr/simple-cache": "^1.0" 32 | }, 33 | "extra": { 34 | "branch-alias": { 35 | "dev-master": "0.1.x-dev" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/cache/src/CacheException.php: -------------------------------------------------------------------------------- 1 | get($key) + $step; 21 | $this->set($key, $value); 22 | return $value; 23 | } 24 | 25 | public function decrement(string $key, int $step = 1): bool|int 26 | { 27 | return $this->increment($key, -$step); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/cache/src/Driver/ApcuDriver.php: -------------------------------------------------------------------------------- 1 | memcached = new \Memcached(); 21 | } 22 | 23 | public function addServer($host = '127.0.0.1', $port = 11211, $weight = 0): void 24 | { 25 | $this->memcached->addServer($host, $port, $weight); 26 | } 27 | 28 | public function delete($key): bool 29 | { 30 | return $this->memcached->delete($key); 31 | } 32 | 33 | public function set($key, $value, $ttl = null): bool 34 | { 35 | return $this->memcached->set($key, serialize($value), (int) $ttl); 36 | } 37 | 38 | public function has($key): bool 39 | { 40 | $status = $this->memcached->get($key); 41 | return $status !== false && ! is_null($status); 42 | } 43 | 44 | public function clear(): bool 45 | { 46 | return $this->memcached->flush(); 47 | } 48 | 49 | public function get(string $key): mixed 50 | { 51 | return $this->memcached->get($key); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/cache/src/Driver/RedisDriver.php: -------------------------------------------------------------------------------- 1 | redis = new \Redis(); 34 | if ($this->redis->connect($host, $port, $timeout, $persistentId, $retryInterval, $readTimeout, $context)) { 35 | $this->redis->select($database); 36 | if ($password) { 37 | $this->redis->auth($password); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * @param mixed $key 44 | * @throws \RedisException 45 | */ 46 | public function delete($key): bool 47 | { 48 | return (bool) $this->redis->del($this->normalizeKey($key)); 49 | } 50 | 51 | /** 52 | * @param mixed $key 53 | * @throws \RedisException 54 | */ 55 | public function has($key): bool 56 | { 57 | return (bool) $this->redis->exists($this->normalizeKey($key)); 58 | } 59 | 60 | /** 61 | * @throws \RedisException 62 | */ 63 | public function clear(): bool 64 | { 65 | return $this->redis->eval(<<<'LUA' 66 | local keys = redis.call('keys', ARGV[1]) 67 | for _, key in ipairs(keys) do 68 | redis.call('del', key) 69 | end 70 | LUA 71 | , [$this->normalizeKey('*')]); 72 | } 73 | 74 | /** 75 | * @throws \RedisException 76 | */ 77 | public function get(string $key): mixed 78 | { 79 | return $this->redis->get($this->normalizeKey($key)); 80 | } 81 | 82 | /** 83 | * @throws \RedisException 84 | */ 85 | public function set(string $key, mixed $value, ?int $ttl = null): bool 86 | { 87 | return $this->redis->set($this->normalizeKey($key), $value, $ttl); 88 | } 89 | 90 | protected function normalizeKey($id) 91 | { 92 | $key = 'cache:' . $id; 93 | if ($this->cachePrefix) { 94 | $key = $this->cachePrefix . ':' . $key; 95 | } 96 | 97 | return $key; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/cache/src/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | fileCache = new Cache(new FileDriver($tmpDir . '/cache')); 36 | } 37 | 38 | /** 39 | * @throws InvalidArgumentException 40 | */ 41 | public function testSet() 42 | { 43 | $this->assertTrue($this->fileCache->set('foo', 'bar')); 44 | } 45 | 46 | /** 47 | * @throws InvalidArgumentException 48 | */ 49 | public function testHas() 50 | { 51 | $this->assertTrue($this->fileCache->has('foo')); 52 | } 53 | 54 | /** 55 | * @throws InvalidArgumentException 56 | */ 57 | public function testGet() 58 | { 59 | $this->assertEquals('bar', $this->fileCache->get('foo')); 60 | } 61 | 62 | /** 63 | * @throws InvalidArgumentException 64 | */ 65 | public function testGetMultiple() 66 | { 67 | $multiValues = $this->fileCache->getMultiple(['foo']); 68 | $this->assertArrayHasKey('foo', $multiValues); 69 | $this->assertEquals('bar', $multiValues['foo']); 70 | } 71 | 72 | /** 73 | * @throws InvalidArgumentException 74 | */ 75 | public function testDelete() 76 | { 77 | $this->fileCache->delete('foo'); 78 | $this->assertFalse($this->fileCache->has('foo')); 79 | } 80 | 81 | /** 82 | * @throws InvalidArgumentException 83 | */ 84 | public function testGetDefault() 85 | { 86 | $this->assertEquals('bio', $this->fileCache->get('foo', 'bio')); 87 | } 88 | 89 | /** 90 | * @throws InvalidArgumentException 91 | */ 92 | public function testRemember() 93 | { 94 | $this->fileCache->remember('bar', function () { 95 | return 'foo'; 96 | }); 97 | $this->assertEquals('foo', $this->fileCache->get('bar')); 98 | } 99 | 100 | /** 101 | * @throws InvalidArgumentException 102 | */ 103 | public function testIncrement() 104 | { 105 | $this->fileCache->delete('count'); 106 | $this->fileCache->increment('count'); 107 | $this->assertEquals(1, $this->fileCache->get('count')); 108 | } 109 | 110 | /** 111 | * @throws InvalidArgumentException 112 | */ 113 | public function testDecrement() 114 | { 115 | $this->fileCache->decrement('count'); 116 | $this->assertEquals(0, $this->fileCache->get('count')); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/config/README.md: -------------------------------------------------------------------------------- 1 | 这是一款独立的组件包,可以使用点语法来获取加载的配置 2 | 3 | ```php 4 | $repository = new \Next\Config\Repository(); 5 | $repository->set('app', [ 6 | 'debug' => false, 7 | ]); 8 | // 获取配置 9 | $repository->get('app.debug'); 10 | ``` 11 | 12 | > 注意:$repository示例应该保持单例,避免重复加载,加载配置的规则如下 13 | 14 | 例如对于app.php配置文件内容如下 15 | 16 | ```php 17 | return [ 18 | 'debug' => true, 19 | ]; 20 | ``` 21 | 22 | 加载后会按照文件名作为外层数组的键,因此获取配置需要使用`$repository->get('app.debug'')`,支持使用点语法。 23 | -------------------------------------------------------------------------------- /src/config/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/config", 3 | "license": "Apache-2.0", 4 | "authors": [ 5 | { 6 | "name": "chengyao", 7 | "email": "chengyao0320@foxmail.com" 8 | } 9 | ], 10 | "autoload": { 11 | "psr-4": { 12 | "Next\\Config\\": "src/" 13 | } 14 | }, 15 | "autoload-dev": { 16 | "psr-4": { 17 | "Next\\Config\\Tests\\": "tests/" 18 | } 19 | }, 20 | "require": { 21 | "php": "^8.2", 22 | "next/utils": "~0.1" 23 | }, 24 | "extra": { 25 | "branch-alias": { 26 | "dev-master": "0.1.x-dev" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/config/src/Contract/ConfigInterface.php: -------------------------------------------------------------------------------- 1 | repository = new Repository([ 28 | 'app' => [ 29 | 'debug' => true, 30 | 'name' => 'nextphp', 31 | ], 32 | 'cache' => [ 33 | 'driver' => 'file', 34 | ], 35 | ]); 36 | } 37 | 38 | public function testGet() 39 | { 40 | $this->assertTrue($this->repository->get('app.debug')); 41 | $this->assertEquals(['driver' => 'file'], $this->repository->get('cache')); 42 | } 43 | 44 | public function testSet() 45 | { 46 | $this->repository->set('cookie.name', 'nextphp'); 47 | $this->assertEquals('nextphp', $this->repository->get('cookie.name')); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/database/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | Max 5 |

6 | 7 |

轻量 • 简单 • 快速

8 | 9 |

10 | 11 | 12 |

13 | 14 | > 简单高效操作数据库,不支持swoole协程 15 | 16 | ### 使用示例 17 | 18 | ```php 19 | $db = new \Next\Database\Database(new \Next\Database\PDOConfig()); 20 | 21 | $query = $db->query(); // 实例化,建立连接 22 | $query->select('select * from users'); 23 | $query->selectOne('select * from users limit 1'); 24 | $query->delete('delete from users where id = 1'); 25 | 26 | //... 27 | ``` 28 | 29 | -------------------------------------------------------------------------------- /src/database/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/database", 3 | "type": "library", 4 | "license": "Apache-2.0", 5 | "homepage": "https://github.com/next-laboratory/database", 6 | "authors": [ 7 | { 8 | "name": "chengyao", 9 | "email": "chengyao0320@foxmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Next\\Database\\": "src/" 15 | } 16 | }, 17 | "require": { 18 | "php": "^8.2", 19 | "ext-pdo": "*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/database/src/Contract/ConfigInterface.php: -------------------------------------------------------------------------------- 1 | config->getDSN(), 29 | $this->config->getUser(), 30 | $this->config->getPassword(), 31 | $this->config->getOptions() 32 | ), 33 | $this->eventDispatcher 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/database/src/Event/QueryExecuted.php: -------------------------------------------------------------------------------- 1 | DSN)) { 30 | $this->DSN = sprintf('%s:host=%s;port=%s;', $driver, $host, $port); 31 | if (! empty($database)) { 32 | $this->DSN .= 'dbname=' . $database . ';'; 33 | } 34 | if (! empty($unixSocket)) { 35 | $this->DSN .= 'unix_socket=' . $unixSocket . ';'; 36 | } 37 | } 38 | } 39 | 40 | public function getDSN(): string 41 | { 42 | return $this->DSN; 43 | } 44 | 45 | public function getUser(): string 46 | { 47 | return $this->user; 48 | } 49 | 50 | public function getPassword(): string 51 | { 52 | return $this->password; 53 | } 54 | 55 | public function getOptions(): array 56 | { 57 | return $this->options; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/di/.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | 环境要求 PHP >= 8.0 6 | 7 | ```shell 8 | composer require next/di 9 | ``` 10 | 11 | # 使用 12 | 13 | 获取容器实例,注意不要直接实例化 14 | 15 | ```php 16 | $container = \Next\Di\Context::getContainer(); 17 | $container = container(); 18 | ``` 19 | 20 | 绑定类和别名,之后所有容器接口都可以使用TestInterface::class标识来获取Test::class实例 21 | 22 | ```php 23 | $container->bind(TestInterface::class, Test::class); 24 | ``` 25 | 26 | 实例化 27 | 28 | ```php 29 | $container->make(Test::class); 30 | ``` 31 | 32 | 获取对象 33 | 34 | ```php 35 | $container->get(Test::class); 36 | ``` 37 | 38 | 调用方法 39 | ```php 40 | $conatiner->call(callable $callable, array $arguments = []); 41 | ``` 42 | 43 | > 注意:所有需要传参的api均需要关联数组,数组的键为参数的名字 44 | -------------------------------------------------------------------------------- /src/di/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/di", 3 | "description": "A lightweight dependency injection tool, container based on the PSR-11 specification.", 4 | "homepage": "https://github.com/next-laboratory/di", 5 | "license": "Apache-2.0", 6 | "keywords": [ 7 | "container", 8 | "nextphp", 9 | "dependence injection" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "chengyao", 14 | "email": "chengyao0320@foxmail.com" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "Next\\Di\\": "src/" 20 | }, 21 | "files": [ 22 | "src/helpers.php" 23 | ] 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Next\\Di\\Tests\\": "tests/" 28 | } 29 | }, 30 | "require": { 31 | "php": "^8.2", 32 | "psr/container": "^1.0|^2.0" 33 | }, 34 | "extra": { 35 | "branch-alias": { 36 | "dev-master": "0.1.x-dev" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/di/src/Context.php: -------------------------------------------------------------------------------- 1 | set(ContainerInterface::class, self::$container); 30 | self::$container->set(Container::class, self::$container); 31 | } 32 | return self::$container; 33 | } 34 | 35 | public static function setContainer(ContainerInterface $container): void 36 | { 37 | self::$container = $container; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/di/src/Exception/ContainerException.php: -------------------------------------------------------------------------------- 1 | call($callback, $arguments); 40 | } 41 | } 42 | 43 | if (function_exists('make') === false) { 44 | /** 45 | * @template T 46 | * 47 | * @param class-string $id 48 | * 49 | * @return T 50 | * @throws NotFoundException 51 | * @throws ContainerExceptionInterface|ReflectionException 52 | */ 53 | function make(string $id, array $parameters = []) 54 | { 55 | return container()->make($id, $parameters); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/di/tests/Bar.php: -------------------------------------------------------------------------------- 1 | container = Context::getContainer(); 30 | } 31 | 32 | public function testBind() 33 | { 34 | $this->container->bind(FooInterface::class, Foo::class); 35 | $this->assertEquals($this->container->getBinding(FooInterface::class), Foo::class); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/di/tests/ContextTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(Context::hasContainer()); 34 | } 35 | 36 | public function testGetContainer() 37 | { 38 | $this->assertTrue(Context::getContainer() instanceof ContainerInterface); 39 | } 40 | 41 | public function testSetContainer() 42 | { 43 | Context::setContainer(new Container()); 44 | $this->assertTrue(Context::getContainer() instanceof ContainerInterface); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/di/tests/Foo.php: -------------------------------------------------------------------------------- 1 | user = false; 29 | } 30 | 31 | /** 32 | * 监听器优先级 33 | * 如果一个事件被多个监听器监听,那么执行顺序可以通过该方法调整 34 | * 优先级数字越大,优先级越高,越先执行 35 | * @return int 36 | */ 37 | public function getPriority(): int 38 | { 39 | return 0; 40 | } 41 | } 42 | ``` 43 | 44 | > 如果你不需要调整优先级,可以直接继承`Next\Event\EventListener`类 45 | 46 | ### 需要创建一个事件类 47 | 48 | ```php 49 | class UserRegistered 50 | { 51 | public $user; 52 | 53 | public function __construct($user) 54 | { 55 | $this->user = $user; 56 | } 57 | } 58 | ``` 59 | 60 | ### 实例化`ListenerProvider`, 使用`addListener`添加监听器 61 | 62 | ```php 63 | $listenerProvider = new ListenerProvider(); 64 | $listenerProvider->addListener(new UserStatusListener()); 65 | ``` 66 | 67 | ### 实例化调度器,给构造函数传入`ListenerProvider`实例 68 | 69 | ```php 70 | $dispatcher = new \Next\Event\EventDispatcher($listenerProvider); 71 | ``` 72 | 73 | ### 事件调度 74 | 75 | ```php 76 | $user = User::find(1); 77 | 78 | $event = $dispatcher->dispatch(new UserRegistered($user)); 79 | ``` 80 | 81 | ## 可终止事件 82 | 83 | > 事件实现`StoppableEventInterface`接口中的`isPropagationStopped`方法,并且返回true,则不会触发该事件之后的事件 84 | 85 | ```php 86 | class UserRegistered implements StoppableEventInterface 87 | { 88 | public $user; 89 | 90 | public function __construct($user) 91 | { 92 | $this->user = $user; 93 | } 94 | 95 | public function isPropagationStopped() : bool 96 | { 97 | return true; 98 | } 99 | } 100 | ``` 101 | -------------------------------------------------------------------------------- /src/event/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/event", 3 | "license": "Apache-2.0", 4 | "description": "Event based on the PSR-14 specification", 5 | "homepage": "https://github.com/next-laboratory/event.git", 6 | "keywords": [ 7 | "event", 8 | "psr-14" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "chengyao", 13 | "email": "chengyao0320@foxmail.com" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "Next\\Event\\": "src/" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "Next\\Event\\Tests\\": "tests/" 24 | } 25 | }, 26 | "require": { 27 | "php": "^8.2", 28 | "psr/event-dispatcher": "^1.0" 29 | }, 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "0.1.x-dev" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/event/src/Contract/EventListenerInterface.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function listen(): iterable; 20 | 21 | public function process(object $event): void; 22 | 23 | public function getPriority(): int; 24 | } 25 | -------------------------------------------------------------------------------- /src/event/src/EventDispatcher.php: -------------------------------------------------------------------------------- 1 | listenerProvider->getListenersForEvent($event) as $listener) { 37 | $listener->process($event); 38 | if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { 39 | break; 40 | } 41 | } 42 | return $event; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/event/src/EventListener.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | protected array $events = []; 23 | 24 | /** 25 | * 已经注册的监听器. 26 | * 27 | * @var array 28 | */ 29 | protected array $listeners = []; 30 | 31 | /** 32 | * 注册单个事件监听. 33 | */ 34 | public function addListener(EventListenerInterface ...$eventListeners): void 35 | { 36 | if (empty($eventListeners)) { 37 | return; 38 | } 39 | foreach ($eventListeners as $eventListener) { 40 | $listenerClass = $eventListener::class; 41 | if (! isset($this->listeners[$listenerClass])) { 42 | $this->listeners[$listenerClass] = $eventListener; 43 | foreach ($eventListener->listen() as $event) { 44 | $this->events[$event][] = $eventListener; 45 | } 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * @return iterable 52 | */ 53 | public function getListenersForEvent(object $event): iterable 54 | { 55 | $listeners = $this->events[$event::class] ?? []; 56 | $splPriorityQueue = new \SplPriorityQueue(); 57 | foreach ($listeners as $listener) { 58 | $splPriorityQueue->insert($listener, $listener->getPriority()); 59 | } 60 | 61 | return $splPriorityQueue; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/event/tests/EventDispatcherTest.php: -------------------------------------------------------------------------------- 1 | addListener(new FooListener(), new BarListener()); 34 | $this->eventDispatcher = new EventDispatcher($listenerProvider); 35 | } 36 | 37 | public function testDispatch() 38 | { 39 | $fooEvent = $this->eventDispatcher->dispatch(new FooEvent()); 40 | 41 | $this->assertEquals(FooListener::class, $fooEvent->value); 42 | } 43 | 44 | public function testStoppableEventDispatch() 45 | { 46 | $barEvent = $this->eventDispatcher->dispatch(new BarEvent()); 47 | 48 | $this->assertEquals(2, $barEvent->value); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/event/tests/Events/BarEvent.php: -------------------------------------------------------------------------------- 1 | listenerProvider = new ListenerProvider(); 31 | } 32 | 33 | public function testAddListeners() 34 | { 35 | $this->listenerProvider->addListener(new FooListener(), new BarListener()); 36 | $listeners = $this->listenerProvider->getListenersForEvent(new FooEvent()); 37 | $listenerClasses = []; 38 | foreach ($listeners as $listener) { 39 | $listenerClasses[] = $listener::class; 40 | } 41 | $this->assertEquals([BarListener::class, FooListener::class], $listenerClasses); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/event/tests/Listeners/BarListener.php: -------------------------------------------------------------------------------- 1 | value = self::class; 33 | break; 34 | case $event instanceof BarEvent: 35 | $event->value = 2; 36 | } 37 | } 38 | 39 | public function getPriority(): int 40 | { 41 | return 10; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/event/tests/Listeners/FooListener.php: -------------------------------------------------------------------------------- 1 | value = self::class; 33 | break; 34 | case $event instanceof BarEvent: 35 | $event->value = 1; 36 | break; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/foundation/README.md: -------------------------------------------------------------------------------- 1 | Foundation -------------------------------------------------------------------------------- /src/foundation/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/foundation", 3 | "autoload": { 4 | "psr-4": { 5 | "Next\\Foundation\\": "src/" 6 | } 7 | }, 8 | "authors": [ 9 | { 10 | "name": "chengyao", 11 | "email": "chengyao0320@foxmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.2", 16 | "next/aop": "~0.1" 17 | }, 18 | "extra": { 19 | "branch-alias": { 20 | "dev-master": "0.1.x-dev" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/foundation/src/Cache/Aspect/Cacheable.php: -------------------------------------------------------------------------------- 1 | remember($this->getKey($joinPoint), fn () => $joinPoint->process(), $this->ttl); 36 | } 37 | 38 | protected function getKey(ProceedingJoinPoint $joinPoint): string 39 | { 40 | $key = $this->key ?? ($joinPoint->class . ':' . $joinPoint->method . ':' . serialize(array_filter($joinPoint->parameters->getArrayCopy(), fn ($item) => ! is_object($item)))); 41 | return $this->prefix ? ($this->prefix . ':' . $key) : $key; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/foundation/src/Config/Attribute/Config.php: -------------------------------------------------------------------------------- 1 | setValue($object, $this->getValue()); 37 | } catch (\Throwable $e) { 38 | throw new PropertyHandleException('Property assign failed. ' . $e->getMessage()); 39 | } 40 | } 41 | 42 | /** 43 | * 获取配置值 44 | * 45 | * @throws ContainerExceptionInterface 46 | * @throws \ReflectionException 47 | */ 48 | protected function getValue() 49 | { 50 | return make(Repository::class)->get($this->key, $this->default); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/foundation/src/Console/Attribute/Command.php: -------------------------------------------------------------------------------- 1 | getType()) && $type = $type->getName()) || $type = $this->id) { 34 | $reflectionProperty->setValue($object, $this->getBinding($type)); 35 | } 36 | } catch (\Throwable $e) { 37 | throw new PropertyHandleException('Property assign failed. ' . $e->getMessage()); 38 | } 39 | } 40 | 41 | /** 42 | * @throws ContainerExceptionInterface 43 | * @throws \ReflectionException 44 | */ 45 | protected function getBinding(string $type): object 46 | { 47 | return make($type); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/foundation/src/Event/Attribute/Listen.php: -------------------------------------------------------------------------------- 1 | addListener(make($class)); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/foundation/src/Routing/Attribute/Controller.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public array $methods = ['DELETE']; 21 | } 22 | -------------------------------------------------------------------------------- /src/foundation/src/Routing/Attribute/GetMapping.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public array $methods = ['GET', 'HEAD']; 21 | } 22 | -------------------------------------------------------------------------------- /src/foundation/src/Routing/Attribute/PatchMapping.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public array $methods = ['PATCH']; 21 | } 22 | -------------------------------------------------------------------------------- /src/foundation/src/Routing/Attribute/PostMapping.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public array $methods = ['POST']; 21 | } 22 | -------------------------------------------------------------------------------- /src/foundation/src/Routing/Attribute/PutMapping.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public array $methods = ['PUT']; 21 | } 22 | -------------------------------------------------------------------------------- /src/foundation/src/Routing/Attribute/RequestMapping.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public array $methods = ['GET', 'HEAD', 'POST']; 21 | 22 | /** 23 | * @param string $path 路径 24 | * @param array $methods 方法 25 | * @param array $middlewares 中间件 26 | */ 27 | public function __construct( 28 | public string $path = '/', 29 | array $methods = [], 30 | public array $middlewares = [], 31 | ) { 32 | if (! empty($methods)) { 33 | $this->methods = $methods; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/foundation/src/Routing/Collector/RouteCollector.php: -------------------------------------------------------------------------------- 1 | make(RouteCollection::class); 43 | $router = new Router($attribute->prefix, $attribute->patterns, middlewares: $attribute->middlewares, routeCollection: $routeCollection); 44 | self::$router = $router; 45 | self::$class = $class; 46 | } 47 | } 48 | 49 | /** 50 | * @throws NotFoundException 51 | */ 52 | public static function collectMethod(string $class, string $method, object $attribute): void 53 | { 54 | if ($attribute instanceof RequestMapping && self::$class === $class && ! is_null(self::$router)) { 55 | self::$router->request($attribute->path, [$class, $method], $attribute->methods)->withMiddleware(...$attribute->middlewares); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/http-message/README.md: -------------------------------------------------------------------------------- 1 | 一款基于Psr7的HTTP-Message 2 | -------------------------------------------------------------------------------- /src/http-message/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/http-message", 3 | "license": "Apache-2.0", 4 | "home-page": "https://github.com/next-laboratory/http-message", 5 | "autoload": { 6 | "psr-4": { 7 | "Next\\Http\\Message\\": "src/" 8 | }, 9 | "files": [ 10 | "src/helpers.php" 11 | ] 12 | }, 13 | "authors": [ 14 | { 15 | "name": "chengyao", 16 | "email": "chengyao0320@foxmail.com" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.2", 21 | "ext-fileinfo": "*", 22 | "psr/http-message": "^2.0" 23 | }, 24 | "extra": { 25 | "branch-alias": { 26 | "dev-master": "0.1.x-dev" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/http-message/src/Bag/CookieBag.php: -------------------------------------------------------------------------------- 1 | parameters = array_change_key_case($parameters, CASE_UPPER); 21 | } 22 | 23 | public function get(string $key, $default = null): mixed 24 | { 25 | return parent::get(strtoupper($key), $default); 26 | } 27 | 28 | public function set(string $key, $value) 29 | { 30 | parent::set(strtoupper($key), $value); 31 | } 32 | 33 | public function has(string $key): bool 34 | { 35 | return parent::has(strtoupper($key)); 36 | } 37 | 38 | public function remove(string $key) 39 | { 40 | parent::remove(strtoupper($key)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/http-message/src/Bag/HeaderBag.php: -------------------------------------------------------------------------------- 1 | has($key)) { 21 | return $this->parameters[$this->map[strtoupper($key)]]; 22 | } 23 | return $default; 24 | } 25 | 26 | public function set(string $key, $value) 27 | { 28 | $uppercaseKey = strtoupper($key); 29 | if (isset($this->map[$key])) { 30 | $this->parameters[$this->map[$key]] = $this->formatValue($value); 31 | } else { 32 | $this->map[$uppercaseKey] = $key; 33 | $this->parameters[$key] = $this->formatValue($value); 34 | } 35 | } 36 | 37 | public function has(string $key): bool 38 | { 39 | return isset($this->map[strtoupper($key)]); 40 | } 41 | 42 | public function remove(string $key) 43 | { 44 | if ($this->has($key)) { 45 | $uppercaseKey = strtoupper($key); 46 | $key = $this->map[$uppercaseKey]; 47 | unset($this->parameters[$key], $this->map[$uppercaseKey]); 48 | } 49 | } 50 | 51 | public function replace(array $parameters = []) 52 | { 53 | $this->parameters = []; 54 | $this->map = []; 55 | foreach ($parameters as $key => $value) { 56 | $this->map[strtoupper($key)] = $key; 57 | $this->parameters[$key] = $this->formatValue($value); 58 | } 59 | } 60 | 61 | public function add(string $key, $value) 62 | { 63 | $uppercaseKey = strtoupper($key); 64 | if (isset($this->map[$uppercaseKey])) { 65 | array_push($this->parameters[$this->map[$uppercaseKey]], ...(array) $value); 66 | } else { 67 | $this->map[$uppercaseKey] = $key; 68 | $this->parameters[$key] = $this->formatValue($value); 69 | } 70 | } 71 | 72 | /** 73 | * @return array|string[] 74 | */ 75 | protected function formatValue($value): array 76 | { 77 | if (is_scalar($value)) { 78 | $value = [(string) $value]; 79 | } 80 | if (! is_array($value)) { 81 | throw new \InvalidArgumentException('The given header cannot be set.'); 82 | } 83 | 84 | return array_values($value); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/http-message/src/Bag/ParameterBag.php: -------------------------------------------------------------------------------- 1 | replace($parameters); 21 | } 22 | 23 | public function get(string $key, $default = null) 24 | { 25 | return $this->parameters[$key] ?? $default; 26 | } 27 | 28 | public function set(string $key, $value) 29 | { 30 | $this->parameters[$key] = $value; 31 | } 32 | 33 | public function has(string $key): bool 34 | { 35 | return isset($this->parameters[$key]); 36 | } 37 | 38 | public function remove(string $key) 39 | { 40 | unset($this->parameters[$key]); 41 | } 42 | 43 | public function replace(array $parameters = []) 44 | { 45 | $this->parameters = $parameters; 46 | } 47 | 48 | public function all(): array 49 | { 50 | return $this->parameters; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/http-message/src/Exception/HttpException.php: -------------------------------------------------------------------------------- 1 | statusCode; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/http-message/src/Request.php: -------------------------------------------------------------------------------- 1 | uri = $uri instanceof UriInterface ? $uri : new Uri($uri); 33 | $this->formatBody($body); 34 | $this->headers = new HeaderBag($headers); 35 | } 36 | 37 | public function getRequestTarget(): string 38 | { 39 | if ($this->requestTarget === '/') { 40 | return $this->uri->getPath() . $this->uri->getQuery(); 41 | } 42 | return '/'; 43 | } 44 | 45 | public function withRequestTarget($requestTarget): RequestInterface 46 | { 47 | if ($requestTarget === $this->requestTarget) { 48 | return $this; 49 | } 50 | $new = clone $this; 51 | $new->requestTarget = $requestTarget; 52 | 53 | return $new; 54 | } 55 | 56 | public function getMethod(): string 57 | { 58 | return $this->method; 59 | } 60 | 61 | public function withMethod($method): RequestInterface 62 | { 63 | if ($method === $this->method) { 64 | return $this; 65 | } 66 | $new = clone $this; 67 | $new->method = $method; 68 | 69 | return $new; 70 | } 71 | 72 | public function getUri(): UriInterface 73 | { 74 | return $this->uri; 75 | } 76 | 77 | public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface 78 | { 79 | if ($uri === $this->uri) { 80 | return $this; 81 | } 82 | $new = clone $this; 83 | if ($preserveHost === true) { 84 | $uri = $uri->withHost($this->getHeaderLine('Host')); 85 | } 86 | $new->uri = $uri; 87 | 88 | return $new; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/http-server/README.md: -------------------------------------------------------------------------------- 1 | 符合Psr7规范的兼容多容器的 Http Server 2 | 3 | # 设计思路 4 | 5 | request -> kernel -> response 6 | 7 | # 使用 8 | 9 | 新建类继承`\Next\Http\Server\Kernel`类 10 | 11 | ```php 12 | class HttpKernel extends Kernel 13 | { 14 | // 全局中间件 15 | protected array $middlewares = []; 16 | 17 | // 注册路由 18 | protected function map(Router $router) 19 | { 20 | $router->get('/', 'IndexController@index'); 21 | } 22 | } 23 | ``` 24 | 25 | 然后使用容器实例化`HttpKernel`类 26 | 27 | ```php 28 | $kernel = \Next\Di\Context::getContainer->make(HttpKernel::class); 29 | 30 | // 获取一个PsrServerRequest 31 | $request = \Next\Http\Message\ServerRequest::createFromGlobals(); 32 | 33 | // 返回PsrResponse 34 | $response = $kernel->handle($request); 35 | 36 | // 发送响应 37 | (new \Next\Http\Server\FPMResponseEmitter())->emit($response); 38 | 39 | ``` 40 | 41 | > 框架内置三种环境的ResponseEmitter,均可以自定义 42 | 43 | # 示例 44 | 45 | > FPM 环境 46 | 47 | ```php 48 | (function() { 49 | $loader = require_once '../vendor/autoload.php'; 50 | /** @var Kernel $kernel */ 51 | $kernel = Context::getContainer()->make(Kernel::class); 52 | $response = $kernel->handle(ServerRequest::createFromGlobals()); 53 | (new FPMResponseEmitter())->emit($response); 54 | })(); 55 | ``` 56 | 57 | 你还可以通过继承Kernel类的方式来改写其中的某些方法或者放入全局中间件 58 | -------------------------------------------------------------------------------- /src/http-server/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/http-server", 3 | "license": "Apache-2.0", 4 | "homepage": "https://github.com/next-laboratory/http-server", 5 | "autoload": { 6 | "psr-4": { 7 | "Next\\Http\\Server\\": "src/" 8 | } 9 | }, 10 | "authors": [ 11 | { 12 | "name": "chengyao", 13 | "email": "chengyao0320@foxmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.2", 18 | "next/http-message": "^0.1", 19 | "psr/http-server-middleware": "^1.0", 20 | "psr/http-server-handler": "^1.0" 21 | }, 22 | "extra": { 23 | "branch-alias": { 24 | "dev-master": "0.1.x-dev" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/http-server/src/Exception/MethodNotAllowedException.php: -------------------------------------------------------------------------------- 1 | */ 15 | protected array $middlewares = []; 16 | protected RequestHandlerInterface $handler; 17 | 18 | public function handle(ServerRequestInterface $request): ResponseInterface 19 | { 20 | if ($middleware = current($this->middlewares)) { 21 | next($this->middlewares); 22 | return $middleware->process($request, $this); 23 | } 24 | 25 | if (!isset($this->handler)) { 26 | throw new RuntimeException('Handler has not bee set'); 27 | } 28 | 29 | return $this->handler->handle($request); 30 | } 31 | 32 | public function withHandler(RequestHandlerInterface $handler): static 33 | { 34 | if ($handler instanceof self) { 35 | throw new InvalidArgumentException('Handler must not be an instance of RequestHandler'); 36 | } 37 | $this->handler = $handler; 38 | 39 | return $this; 40 | } 41 | 42 | public function withMiddleware(MiddlewareInterface ...$middlewares): static 43 | { 44 | array_push($this->middlewares, ...$middlewares); 45 | 46 | return $this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/http-throttle/README.md: -------------------------------------------------------------------------------- 1 | 开发中,本包提取自thinkphp 2 | -------------------------------------------------------------------------------- /src/http-throttle/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "max/http-throttle", 3 | "autoload": { 4 | "psr-4": { 5 | "Next\\Http\\Throttle\\": "src/" 6 | } 7 | }, 8 | "authors": [ 9 | { 10 | "name": "chengyao", 11 | "email": "chengyao0320@foxmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.0", 16 | "psr/http-message": "^1.0" 17 | }, 18 | "extra": { 19 | "branch-alias": { 20 | "dev-master": "1.x-dev" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/http-throttle/src/Handlers/CounterFixed.php: -------------------------------------------------------------------------------- 1 | get($key, 0); 28 | $now = (int) $micronow; 29 | $wait_reset_seconds = $duration - $now % $duration; // 距离下次重置还有n秒时间 30 | $this->waitSeconds = $wait_reset_seconds % $duration + 1; 31 | $this->currentRequests = $cur_requests; 32 | 33 | if ($cur_requests < $max_requests) { // 允许访问 34 | $cache->set($key, $this->currentRequests + 1, $wait_reset_seconds); 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/http-throttle/src/Handlers/CounterSlider.php: -------------------------------------------------------------------------------- 1 | get($key, []); 29 | $now = (int) $micronow; 30 | // 移除过期的请求的记录 31 | $history = array_values(array_filter($history, function ($val) use ($now, $duration) { 32 | return $val >= $now - $duration; 33 | })); 34 | 35 | $this->currentRequests = count($history); 36 | if ($this->currentRequests < $max_requests) { 37 | // 允许访问 38 | $history[] = $now; 39 | $cache->set($key, $history, $duration); 40 | return true; 41 | } 42 | 43 | if ($history) { 44 | $waitSeconds = $duration - ($now - $history[0]) + 1; 45 | $this->waitSeconds = max($waitSeconds, 0); 46 | } 47 | 48 | return false; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/http-throttle/src/Handlers/LeakyBucket.php: -------------------------------------------------------------------------------- 1 | get($key, 0); // 最近一次请求 29 | $rate = (float) $duration / $max_requests; // 平均 n 秒一个请求 30 | if ($micronow - $last_time < $rate) { 31 | $this->currentRequests = 1; 32 | $this->waitSeconds = ceil($rate - ($micronow - $last_time)); 33 | return false; 34 | } 35 | 36 | $cache->set($key, $micronow, $duration); 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/http-throttle/src/Handlers/ThrottleAbstract.php: -------------------------------------------------------------------------------- 1 | waitSeconds; 45 | } 46 | 47 | /** 48 | * 当前已有的请求数. 49 | */ 50 | public function getCurRequests(): int 51 | { 52 | return $this->currentRequests; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/http-throttle/src/Handlers/TokenBucket.php: -------------------------------------------------------------------------------- 1 | get($key, null); 35 | $store_num = $cache->get($assist_key, null); 36 | 37 | if ($last_time === null || $store_num === null) { // 首次访问 38 | $cache->set($key, $micronow, $duration); 39 | $cache->set($assist_key, $max_requests - 1, $duration); 40 | return true; 41 | } 42 | 43 | $create_num = floor(($micronow - $last_time) * $rate); // 推算生成的 token 数 44 | $token_left = (int) min($max_requests, $store_num + $create_num); // 当前剩余 tokens 数量 45 | 46 | if ($token_left < 1) { 47 | $tmp = (int) ceil($duration / $max_requests); 48 | $this->waitSeconds = $tmp - (int) ($micronow - $last_time) % $tmp; 49 | return false; 50 | } 51 | $this->currentRequests = $max_requests - $token_left; 52 | $cache->set($key, $micronow, $duration); 53 | $cache->set($assist_key, $token_left - 1, $duration); 54 | return true; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/http-throttle/src/RateLimitException.php: -------------------------------------------------------------------------------- 1 | server->serveHttp($request); 75 | } 76 | } 77 | ``` 78 | 79 | 如上新建了一个控制器,路由地址为/jsonrpc,启动服务 80 | 81 | > 服务端必须使用swoole/workerman等常驻内存的环境 82 | 83 | ### 测试请求 84 | 85 | > GET 127.0.0.1:8989/jsonrpc 86 | ```json 87 | { 88 | "jsonrpc": "2.0", 89 | "id": 123, 90 | "method": "calc.sum", 91 | "params": { 92 | "a": 1, 93 | "b": 2 94 | } 95 | } 96 | ``` 97 | 98 | 响应 99 | 100 | ```json 101 | { 102 | "jsonrpc": "2.0", 103 | "result": 3, 104 | "id": 123 105 | } 106 | ``` 107 | 108 | ## 客户端 109 | 110 | 请求 111 | 112 | ```php 113 | $client = new Client('http://127.0.0.1:8989'); 114 | $response = $client->call(new Request('calc.sum', ['a' => 1, 'b' => 2])); 115 | dump($response); 116 | ``` 117 | 118 | 响应 119 | 120 | ``` 121 | ^ array:3 [ 122 | "jsonrpc" => "2.0" 123 | "result" => 3 124 | "id" => "d562e03439af8a69d71ee833b8972bbd" 125 | ] 126 | ``` 127 | 128 | 如果要发送通知可以使用notify方法 129 | -------------------------------------------------------------------------------- /src/json-rpc/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "max/json-rpc", 3 | "autoload": { 4 | "psr-4": { 5 | "Next\\JsonRpc\\": "src/" 6 | } 7 | }, 8 | "authors": [ 9 | { 10 | "name": "chengyao", 11 | "email": "chengyao0320@foxmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.0", 16 | "max/http-message": "dev-master", 17 | "max/di": "dev-master" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/json-rpc/src/Attribute/RpcService.php: -------------------------------------------------------------------------------- 1 | client = new GzClient(['base_uri' => $this->uri]); 29 | } 30 | 31 | /** 32 | * @throws GuzzleException 33 | * @throws Exception 34 | */ 35 | public function call(Request $request, string $requestMethod = 'GET') 36 | { 37 | if (!$request->hasId()) { 38 | $request->setId(md5(uniqid())); 39 | } 40 | return Response::createFromPsrResponse($this->sendRequest($request, $requestMethod)); 41 | } 42 | 43 | /** 44 | * @throws GuzzleException 45 | */ 46 | public function sendRequest(Request $request, string $requestMethod = 'GET'): ResponseInterface 47 | { 48 | return $this->client->request($requestMethod, '/', ['json' => $request]); 49 | } 50 | 51 | /** 52 | * @throws GuzzleException 53 | */ 54 | public function notify(Request $request, string $requestMethod = 'GET'): void 55 | { 56 | $this->sendRequest($request, $requestMethod); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/json-rpc/src/Event/RpcCalled.php: -------------------------------------------------------------------------------- 1 | getHeaderLine('Content-Type'), 'application/json')) { 31 | throw new InvalidArgumentException('Invalid Request', -32600); 32 | } 33 | $body = $request->getBody()->getContents(); 34 | $parts = json_decode($body, true); 35 | if (!isset($parts['jsonrpc'], $parts['method'])) { 36 | throw new InvalidArgumentException('Parse error', -32700); 37 | } 38 | return new static($parts['method'], $parts['params'] ?? [], $parts['id'] ?? null, $parts['jsonrpc']); 39 | } 40 | 41 | public function getMethod(): string 42 | { 43 | return $this->method; 44 | } 45 | 46 | public function getParams(): array 47 | { 48 | return $this->params; 49 | } 50 | 51 | public function getId(): mixed 52 | { 53 | return $this->id; 54 | } 55 | 56 | public function hasId(): bool 57 | { 58 | return isset($this->id); 59 | } 60 | 61 | public function getJsonrpc(): string 62 | { 63 | return $this->jsonrpc; 64 | } 65 | 66 | public function setMethod(string $method): void 67 | { 68 | $this->method = $method; 69 | } 70 | 71 | public function setParams(array $params): void 72 | { 73 | $this->params = $params; 74 | } 75 | 76 | public function setId(mixed $id): void 77 | { 78 | $this->id = $id; 79 | } 80 | 81 | public function setJsonrpc(string $jsonrpc): void 82 | { 83 | $this->jsonrpc = $jsonrpc; 84 | } 85 | 86 | public function jsonSerialize() 87 | { 88 | return get_object_vars($this); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/json-rpc/src/Message/Response.php: -------------------------------------------------------------------------------- 1 | getHeaderLine('Content-Type'), 'application/json')) { 30 | throw new \Exception('Invalid Response', -32600); 31 | } 32 | $body = $response->getBody()->getContents(); 33 | return json_decode($body, true); 34 | } 35 | 36 | public function getResult(): mixed 37 | { 38 | return $this->result; 39 | } 40 | 41 | public function getId(): mixed 42 | { 43 | return $this->id; 44 | } 45 | 46 | public function getError(): ?Error 47 | { 48 | return $this->error; 49 | } 50 | 51 | public function getJsonrpc(): string 52 | { 53 | return $this->jsonrpc; 54 | } 55 | 56 | public function jsonSerialize() 57 | { 58 | return array_filter(get_object_vars($this)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/json-rpc/src/ServiceCollector.php: -------------------------------------------------------------------------------- 1 | name; 29 | make(Server::class)->register($service, $class); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/pipeline/README.md: -------------------------------------------------------------------------------- 1 | pipeline -------------------------------------------------------------------------------- /src/pipeline/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "max/pipeline", 3 | "license": "Apache-2.0", 4 | "autoload": { 5 | "psr-4": { 6 | "Next\\Pipeline\\": "src/" 7 | } 8 | }, 9 | "authors": [ 10 | { 11 | "name": "chengyao", 12 | "email": "chengyao0320@foxmail.com" 13 | } 14 | ], 15 | "require": { 16 | "php": "^8.0||^8.1||^8.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/pipeline/src/Abort.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | protected array $handlers = []; 27 | 28 | final public function withHandlers(callable ...$handlers): static 29 | { 30 | if (!empty($handlers)) { 31 | array_push($this->handlers, ...$handlers); 32 | } 33 | 34 | return $this; 35 | } 36 | 37 | final public function next(): void 38 | { 39 | if (count($this->handlers) === 0) { 40 | throw new RuntimeException('There is no handler that can be executed'); 41 | } 42 | array_shift($this->handlers)($this); 43 | } 44 | 45 | /** 46 | * @throws Abort 47 | */ 48 | final public function abort(string $message = '', int $code = 0) 49 | { 50 | throw new Abort($message, $code); 51 | } 52 | 53 | final public function setValues(array $values): void 54 | { 55 | $this->values = $values; 56 | } 57 | 58 | final public function setValue(string $key, mixed $value): void 59 | { 60 | $this->values[$key] = $value; 61 | } 62 | 63 | final public function hasValue(string $key): bool 64 | { 65 | return isset($this->values[$key]); 66 | } 67 | 68 | final public function getValue(string $key): mixed 69 | { 70 | return $this->values[$key] ?? null; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/pool/README.md: -------------------------------------------------------------------------------- 1 | Pool 2 | -------------------------------------------------------------------------------- /src/pool/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "max/pool", 3 | "autoload": { 4 | "psr-4": { 5 | "Next\\Pool\\": "src/" 6 | } 7 | }, 8 | "authors": [ 9 | { 10 | "name": "chengyao", 11 | "email": "chengyao0320@foxmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pool/src/BasePool.php: -------------------------------------------------------------------------------- 1 | isOpen) { 18 | $this->splQueue = new SplQueue(); 19 | $this->isOpen = true; 20 | } else { 21 | throw new RuntimeException('Pool is opened'); 22 | } 23 | } 24 | 25 | public function get() 26 | { 27 | $this->isOpen(); 28 | $isMaximum = $this->currentSize >= $this->getPoolCapacity(); 29 | if ($this->splQueue->isEmpty() && $isMaximum) { 30 | throw new RuntimeException('Too many connections'); 31 | } 32 | if (!$isMaximum) { 33 | $this->splQueue->enqueue($this->newPoolItem()); 34 | $this->currentSize++; 35 | } 36 | return $this->splQueue->dequeue(); 37 | } 38 | 39 | public function release($poolItem) 40 | { 41 | $this->isOpen(); 42 | if ($this->splQueue->count() < $this->getPoolCapacity()) { 43 | $this->splQueue->enqueue($poolItem); 44 | } 45 | } 46 | 47 | public function discard($poolItem) 48 | { 49 | $this->isOpen(); 50 | $this->currentSize--; 51 | } 52 | 53 | protected function isOpen() 54 | { 55 | if (!$this->isOpen) { 56 | throw new RuntimeException('Pool is not opened'); 57 | } 58 | } 59 | 60 | public function close() 61 | { 62 | $this->isOpen(); 63 | $this->splQueue = new SplQueue(); 64 | $this->isOpen = false; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/pool/src/BasePoolItem.php: -------------------------------------------------------------------------------- 1 | failed) { 27 | throw new Exception('Object unavailable'); 28 | } 29 | return $this->object->{$name}(...$arguments); 30 | } catch (Throwable $e) { 31 | $this->failed = true; 32 | throw $e; 33 | } 34 | } 35 | 36 | public function __destruct() 37 | { 38 | if ($this->failed) { 39 | $this->pool->discard($this); 40 | } else { 41 | $this->pool->release($this); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/pool/src/Contract/PoolInterface.php: -------------------------------------------------------------------------------- 1 | exists('maxphp'); 16 | // 执行多条 17 | $redis->wrap(function(Redis $redis) { 18 | if(!$redis->exists('maxphp')) { 19 | $redis->set('maxphp', 'good'); 20 | } 21 | }); 22 | ``` 23 | 24 | 执行多条命令时需要使用wrap方法,方法接收一个接收\Redis实例的闭包,在闭包内通过调用\Redis实例的方法实现。 25 | -------------------------------------------------------------------------------- /src/redis/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "max/redis", 3 | "autoload": { 4 | "psr-4": { 5 | "Next\\Redis\\": "src/" 6 | } 7 | }, 8 | "authors": [ 9 | { 10 | "name": "chengyao", 11 | "email": "chengyao0320@foxmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.0", 16 | "max/pool": "dev-master", 17 | "ext-redis": "*" 18 | }, 19 | "require-dev": { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/redis/src/Client.php: -------------------------------------------------------------------------------- 1 | wrap(function ($redis) use ($name, $arguments) { 36 | return $redis->{$name}(...$arguments); 37 | }); 38 | } 39 | 40 | public function multi(Closure $callback, int $mode = Redis::MULTI) 41 | { 42 | return $this->wrap(function (Redis $redis) use ($callback, $mode) { 43 | try { 44 | $redis = $redis->multi($mode); 45 | $result = $callback($redis); 46 | $redis->exec(); 47 | return $result; 48 | } catch (Throwable $e) { 49 | $redis->discard(); 50 | throw $e; 51 | } 52 | }); 53 | } 54 | 55 | /** 56 | * @param string|string[] $key 57 | * @throws Throwable 58 | */ 59 | public function watch(string|array $key, Closure $callback) 60 | { 61 | return $this->wrap(function (Redis $redis) use ($callback, $key) { 62 | $redis->watch($key); 63 | return $callback($redis); 64 | }); 65 | } 66 | 67 | /** 68 | * @throws Throwable 69 | */ 70 | public function wrap(Closure $callable) 71 | { 72 | try { 73 | $redis = $this->connector->get(); 74 | return $callable($redis); 75 | } catch (Throwable $e) { 76 | $redis = null; 77 | throw $e; 78 | } finally { 79 | $this->connector->release($redis); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/redis/src/Connector/BaseConnector.php: -------------------------------------------------------------------------------- 1 | connect( 38 | $this->host, 39 | $this->port, 40 | $this->timeout, 41 | $this->reserved, 42 | $this->retryInterval, 43 | $this->readTimeout 44 | ); 45 | $redis->select($this->database); 46 | $this->auth && $redis->auth($this->auth); 47 | return $redis; 48 | } 49 | 50 | public function release($connection) 51 | { 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/redis/src/Connector/BasePoolConnector.php: -------------------------------------------------------------------------------- 1 | open(); 24 | } 25 | 26 | public function getPoolCapacity(): int 27 | { 28 | return $this->poolSize; 29 | } 30 | 31 | /** 32 | * @throws RedisException 33 | */ 34 | public function newPoolItem() 35 | { 36 | $redis = new Redis(); 37 | $redis->pconnect( 38 | $this->host, 39 | $this->port, 40 | $this->timeout, 41 | $this->reserved, 42 | $this->retryInterval, 43 | $this->readTimeout 44 | ); 45 | $redis->select($this->database); 46 | $this->auth && $redis->auth($this->auth); 47 | return $redis; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/redis/src/Connector/SwoolePoolConnector.php: -------------------------------------------------------------------------------- 1 | withHost($this->host) 35 | ->withPort($this->port) 36 | ->withTimeout($this->timeout) 37 | ->withReadTimeout($this->readTimeout) 38 | ->withRetryInterval($this->retryInterval) 39 | ->withReserved($this->reserved) 40 | ->withDbIndex($this->database) 41 | ->withAuth($this->auth); 42 | $this->pool = new RedisPool($redisConfig, $this->poolSize); 43 | $this->pool->fill(); 44 | } 45 | 46 | public function get() 47 | { 48 | return $this->pool->get(); 49 | } 50 | 51 | public function release($connection) 52 | { 53 | $this->pool->put($connection); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/redis/src/Contract/ConnectorInterface.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected array $routes = []; 25 | 26 | /** 27 | * 添加一个路由. 28 | */ 29 | public function add(Route $route): Route 30 | { 31 | foreach ($route->getMethods() as $method) { 32 | $this->routes[$method][] = $route; 33 | } 34 | return $route; 35 | } 36 | 37 | /** 38 | * 全部. 39 | * 40 | * @return array 41 | */ 42 | public function all(): array 43 | { 44 | return $this->routes; 45 | } 46 | 47 | /** 48 | * @return Route[] 49 | */ 50 | public function list(string $method): array 51 | { 52 | return $this->routes[$method] 53 | ?? throw new MethodNotAllowedException(StatusCodeInterface::STATUS_METHOD_NOT_ALLOWED, 'Method not allowed: ' . $method); 54 | } 55 | 56 | public function count(): int 57 | { 58 | return \count($this->routes); 59 | } 60 | 61 | /** 62 | * @return \ArrayIterator 63 | */ 64 | public function getIterator(): \ArrayIterator 65 | { 66 | return new \ArrayIterator($this->routes); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/routing/src/UrlMatcher.php: -------------------------------------------------------------------------------- 1 | getUri()->getPath(), '/'); 32 | $method = $request->getMethod(); 33 | return $this->match($method, $path); 34 | } 35 | 36 | /** 37 | * 使用请求方法和请求path来匹配路由. 38 | */ 39 | public function match(string $method, string $path): Route 40 | { 41 | foreach ($this->routeCollection->list($method) as $route) { 42 | if (($compiledPath = $route->getCompiledPath()) && \preg_match($compiledPath, $path, $match)) { 43 | $matchedRoute = clone $route; 44 | if (!empty($match)) { 45 | foreach ($route->getParameters() as $key => $value) { 46 | if (\array_key_exists($key, $match)) { 47 | $matchedRoute->setParameter($key, $match[$key]); 48 | } 49 | } 50 | } 51 | return $matchedRoute; 52 | } 53 | } 54 | 55 | throw new NotFoundException(StatusCodeInterface::STATUS_NOT_FOUND, 'Not Found'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/session/README.md: -------------------------------------------------------------------------------- 1 | ## Session组件,支持File和Redis(不支持协程) Handler,可以自定义SesssionHandler 2 | 3 | ```php 4 | composer require next/session 5 | ``` 6 | 7 | ```php 8 | $sessionHandler = new \Next\Session\Handler\FileHandler(); 9 | 10 | $session = new \Next\Session\Session($sessionHandler); 11 | 12 | $session->start(null); // 如果为null则创建id 13 | $session->set('foo', 'bar'); 14 | $session->get('foo'); 15 | $session->save(); 16 | $session->close(); 17 | 18 | $sessionId = $session->getId(); 19 | ``` 20 | -------------------------------------------------------------------------------- /src/session/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/session", 3 | "license": "Apache-2.0", 4 | "homepage": "https://github.com/next-laboratory/session", 5 | "authors": [ 6 | { 7 | "name": "chengyao", 8 | "email": "chengyao0320@foxmail.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "Next\\Session\\": "src/" 14 | } 15 | }, 16 | "require": { 17 | "php": "^8.2" 18 | }, 19 | "extra": { 20 | "branch-alias": { 21 | "dev-master": "0.1.x-dev" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/session/src/Handler/RedisHandler.php: -------------------------------------------------------------------------------- 1 | redis->del($id); 41 | } catch (\RedisException) { 42 | return false; 43 | } 44 | } 45 | 46 | public function gc(int $max_lifetime): false|int 47 | { 48 | return 1; 49 | } 50 | 51 | /** 52 | * @throws \RedisException 53 | */ 54 | public function open(string $path, string $name): bool 55 | { 56 | $this->redis = new \Redis(); 57 | if ($this->redis->connect( 58 | $this->host, 59 | $this->port, 60 | $this->timeout, 61 | $this->persistentId, 62 | $this->retryInterval, 63 | $this->readTimeout, 64 | $this->context 65 | )) { 66 | $this->redis->select($this->database); 67 | if ($this->password) { 68 | $this->redis->auth($this->password); 69 | } 70 | } 71 | 72 | return false; 73 | } 74 | 75 | public function read(string $id): false|string 76 | { 77 | try { 78 | if ($data = $this->redis->get($this->normalizeId($id))) { 79 | return (string) $data; 80 | } 81 | return false; 82 | } catch (\RedisException) { 83 | return false; 84 | } 85 | } 86 | 87 | public function write(string $id, string $data): bool 88 | { 89 | try { 90 | return (bool) $this->redis->set($this->normalizeId($id), $data, $this->sessionTTL); 91 | } catch (\RedisException) { 92 | return false; 93 | } 94 | } 95 | 96 | protected function normalizeId(string $id): string 97 | { 98 | $key = 'session:' . $id; 99 | if ($this->sessionPrefix) { 100 | $key = $this->sessionPrefix . ':' . $key; 101 | } 102 | 103 | return $key; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/session/src/SessionException.php: -------------------------------------------------------------------------------- 1 | =4.6" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/swoole/src/Context.php: -------------------------------------------------------------------------------- 1 | 0) { 32 | self::for($cid)[$key] = $item; 33 | } else { 34 | self::$container[$key] = $item; 35 | } 36 | } 37 | 38 | public static function delete(string $key = ''): void 39 | { 40 | if (($cid = self::getCid()) > 0) { 41 | if (!empty($key)) { 42 | unset(self::for($cid)[$key]); 43 | } 44 | } else { 45 | if ($key) { 46 | unset(self::$container[$key]); 47 | } else { 48 | self::$container = []; 49 | } 50 | } 51 | } 52 | 53 | public static function has(string $key): bool 54 | { 55 | if (($cid = self::getCid()) > 0) { 56 | return isset(self::for($cid)[$key]); 57 | } 58 | return isset(self::$container[$key]); 59 | } 60 | 61 | public static function for(?int $cid = null): ?SwooleContext 62 | { 63 | return Coroutine::getContext($cid); 64 | } 65 | 66 | protected static function getCid(): int 67 | { 68 | if (class_exists('Swoole\Coroutine')) { 69 | return Coroutine::getCid(); 70 | } 71 | return -1; 72 | } 73 | 74 | public static function inCoroutine(): bool 75 | { 76 | return self::getCid() >= 0; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/swoole/src/Exception/ParallelExecutionException.php: -------------------------------------------------------------------------------- 1 | results; 23 | } 24 | 25 | public function setResults(array $results) 26 | { 27 | $this->results = $results; 28 | } 29 | 30 | public function getThrowables(): array 31 | { 32 | return $this->throwables; 33 | } 34 | 35 | public function setThrowables(array $throwables): array 36 | { 37 | return $this->throwables = $throwables; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/swoole/src/Table/Exception/DuplicateKeyException.php: -------------------------------------------------------------------------------- 1 | create(); 39 | static::$tables[$tableName] = $table; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/swoole/tests/Concerns/UserTable.php: -------------------------------------------------------------------------------- 1 | 'json']; 12 | } -------------------------------------------------------------------------------- /src/utils/README.md: -------------------------------------------------------------------------------- 1 | 这个包中的大部分代码均来自于laravel, 感谢laravel. 使用这个包让你在不使用laravel的场景下使用laravel中强大的辅助函数或类 -------------------------------------------------------------------------------- /src/utils/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/utils", 3 | "type": "library", 4 | "license": "Apache-2.0", 5 | "homepage": "https://github.com/next-laboratory/utils", 6 | "authors": [ 7 | { 8 | "name": "chengyao", 9 | "email": "chengyao0320@foxmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "files": [ 14 | "src/helpers.php" 15 | ], 16 | "psr-4": { 17 | "Next\\Utils\\": "src/" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "Next\\Utils\\Tests\\": "tests/" 23 | } 24 | }, 25 | "require": { 26 | "php": "^8.2", 27 | "symfony/finder": "*", 28 | "ramsey/uuid": "*", 29 | "symfony/mime": "*", 30 | "voku/portable-ascii": "*", 31 | "league/commonmark": "*", 32 | "doctrine/inflector": "*" 33 | }, 34 | "suggest": { 35 | "ext-bcmath": "*" 36 | }, 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "0.1.x-dev" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/src/Composer.php: -------------------------------------------------------------------------------- 1 | bindTo(null, static::class); 45 | } 46 | 47 | return $macro(...$parameters); 48 | } 49 | 50 | /** 51 | * Dynamically handle calls to the class. 52 | * 53 | * @return mixed 54 | * @throws \BadMethodCallException 55 | */ 56 | public function __call(string $method, array $parameters) 57 | { 58 | if (! static::hasMacro($method)) { 59 | throw new \BadMethodCallException(sprintf( 60 | 'Method %s::%s does not exist.', 61 | static::class, 62 | $method 63 | )); 64 | } 65 | 66 | $macro = static::$macros[$method]; 67 | 68 | if ($macro instanceof \Closure) { 69 | $macro = $macro->bindTo($this, static::class); 70 | } 71 | 72 | return $macro(...$parameters); 73 | } 74 | 75 | /** 76 | * Register a custom macro. 77 | */ 78 | public static function macro(string $name, callable|object $macro): void 79 | { 80 | static::$macros[$name] = $macro; 81 | } 82 | 83 | /** 84 | * Mix another object into the class. 85 | * 86 | * @throws \ReflectionException 87 | */ 88 | public static function mixin(object $mixin, bool $replace = true): void 89 | { 90 | $methods = (new \ReflectionClass($mixin))->getMethods( 91 | \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED 92 | ); 93 | 94 | foreach ($methods as $method) { 95 | if ($replace || ! static::hasMacro($method->name)) { 96 | $method->setAccessible(true); 97 | static::macro($method->name, $method->invoke($mixin)); 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * Checks if macro is registered. 104 | */ 105 | public static function hasMacro(string $name): bool 106 | { 107 | return isset(static::$macros[$name]); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/utils/src/Optional.php: -------------------------------------------------------------------------------- 1 | value)) { 39 | return $this->value->{$key} ?? null; 40 | } 41 | return null; 42 | } 43 | 44 | /** 45 | * Dynamically check a property exists on the underlying object. 46 | * 47 | * @return bool 48 | */ 49 | public function __isset(mixed $name) 50 | { 51 | if (is_object($this->value)) { 52 | return isset($this->value->{$name}); 53 | } 54 | 55 | if (is_array($this->value) || $this->value instanceof \ArrayObject) { 56 | return isset($this->value[$name]); 57 | } 58 | 59 | return false; 60 | } 61 | 62 | /** 63 | * Dynamically pass a method to the underlying object. 64 | * 65 | * @return mixed 66 | */ 67 | public function __call(string $method, array $parameters) 68 | { 69 | if (static::hasMacro($method)) { 70 | return $this->macroCall($method, $parameters); 71 | } 72 | 73 | if (is_object($this->value)) { 74 | return $this->value->{$method}(...$parameters); 75 | } 76 | } 77 | 78 | #[\ReturnTypeWillChange] 79 | public function offsetExists($key) 80 | { 81 | return Arr::accessible($this->value) && Arr::exists($this->value, $key); 82 | } 83 | 84 | #[\ReturnTypeWillChange] 85 | public function offsetGet($key) 86 | { 87 | return Arr::get($this->value, $key); 88 | } 89 | 90 | #[\ReturnTypeWillChange] 91 | public function offsetSet($key, $value) 92 | { 93 | if (Arr::accessible($this->value)) { 94 | $this->value[$key] = $value; 95 | } 96 | } 97 | 98 | #[\ReturnTypeWillChange] 99 | public function offsetUnset($key) 100 | { 101 | if (Arr::accessible($this->value)) { 102 | unset($this->value[$key]); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/utils/src/Packer/JsonPacker.php: -------------------------------------------------------------------------------- 1 | method = $method; 43 | $this->collection = $collection; 44 | } 45 | 46 | /** 47 | * Proxy accessing an attribute onto the collection items. 48 | * 49 | * @param string $key 50 | * 51 | * @return mixed 52 | */ 53 | public function __get($key) 54 | { 55 | return $this->collection->{$this->method}(function ($value) use ($key) { 56 | return is_array($value) ? $value[$key] : $value->{$key}; 57 | }); 58 | } 59 | 60 | /** 61 | * Proxy a method call onto the collection items. 62 | * 63 | * @param string $method 64 | * @param array $parameters 65 | * 66 | * @return mixed 67 | */ 68 | public function __call($method, $parameters) 69 | { 70 | return $this->collection->{$this->method}(function ($value) use ($method, $parameters) { 71 | return $value->{$method}(...$parameters); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/src/Proxy/HigherOrderTapProxy.php: -------------------------------------------------------------------------------- 1 | target = $target; 35 | } 36 | 37 | /** 38 | * Dynamically pass method calls to the target. 39 | * 40 | * @param string $method 41 | * @param array $parameters 42 | * @return mixed 43 | */ 44 | public function __call($method, $parameters) 45 | { 46 | $this->target->{$method}(...$parameters); 47 | 48 | return $this->target; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/src/Proxy/HigherOrderWhenProxy.php: -------------------------------------------------------------------------------- 1 | target = $target; 52 | } 53 | 54 | /** 55 | * Proxy accessing an attribute onto the target. 56 | * 57 | * @param string $key 58 | * @return mixed 59 | */ 60 | public function __get($key) 61 | { 62 | if (! $this->hasCondition) { 63 | $condition = $this->target->{$key}; 64 | 65 | return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition); 66 | } 67 | 68 | return $this->condition 69 | ? $this->target->{$key} 70 | : $this->target; 71 | } 72 | 73 | /** 74 | * Proxy a method call on the target. 75 | * 76 | * @param string $method 77 | * @param array $parameters 78 | * @return mixed 79 | */ 80 | public function __call($method, $parameters) 81 | { 82 | if (! $this->hasCondition) { 83 | $condition = $this->target->{$method}(...$parameters); 84 | 85 | return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition); 86 | } 87 | 88 | return $this->condition 89 | ? $this->target->{$method}(...$parameters) 90 | : $this->target; 91 | } 92 | 93 | /** 94 | * Set the condition on the proxy. 95 | * 96 | * @param bool $condition 97 | * @return $this 98 | */ 99 | public function condition($condition) 100 | { 101 | [$this->condition, $this->hasCondition] = [$condition, true]; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Indicate that the condition should be negated. 108 | * 109 | * @return $this 110 | */ 111 | public function negateConditionOnCapture() 112 | { 113 | $this->negateConditionOnCapture = true; 114 | 115 | return $this; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/utils/src/Traits/AutoFillProperties.php: -------------------------------------------------------------------------------- 1 | $value) { 22 | if ($force || property_exists($this, $key)) { 23 | $this->{$key} = $value; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/src/Traits/Conditionable.php: -------------------------------------------------------------------------------- 1 | condition($value); 44 | } 45 | 46 | if ($value) { 47 | return $callback($this, $value) ?? $this; 48 | } 49 | if ($default) { 50 | return $default($this, $value) ?? $this; 51 | } 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * Apply the callback if the given "value" is (or resolves to) falsy. 58 | * 59 | * @template TUnlessParameter 60 | * @template TUnlessReturnType 61 | * 62 | * @param null|mixed $value 63 | * @param null|(callable($this, TUnlessParameter): TUnlessReturnType) $callback 64 | * @param null|(callable($this, TUnlessParameter): TUnlessReturnType) $default 65 | * 66 | * @return $this|TUnlessReturnType 67 | */ 68 | public function unless($value = null, ?callable $callback = null, ?callable $default = null) 69 | { 70 | $value = $value instanceof \Closure ? $value($this) : $value; 71 | 72 | if (func_num_args() === 0) { 73 | return (new HigherOrderWhenProxy($this))->negateConditionOnCapture(); 74 | } 75 | 76 | if (func_num_args() === 1) { 77 | return (new HigherOrderWhenProxy($this))->condition(! $value); 78 | } 79 | 80 | if (! $value) { 81 | return $callback($this, $value) ?? $this; 82 | } 83 | if ($default) { 84 | return $default($this, $value) ?? $this; 85 | } 86 | 87 | return $this; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/utils/src/Traits/Tappable.php: -------------------------------------------------------------------------------- 1 | toArray(); 26 | } else { 27 | $data = (array) $data; 28 | } 29 | if ($parentNode === null) { 30 | $xml = new \SimpleXMLElement('' . "<{$root}>"); 31 | } else { 32 | $xml = $parentNode; 33 | } 34 | foreach ($data as $key => $value) { 35 | if (is_array($value)) { 36 | self::toXml($value, $xml->addChild($key)); 37 | } else { 38 | if (is_numeric($key)) { 39 | $xml->addChild('item' . $key, (string) $value); 40 | } else { 41 | $xml->addChild($key, (string) $value); 42 | } 43 | } 44 | } 45 | return trim($xml->asXML()); 46 | } 47 | 48 | public static function toArray($xml) 49 | { 50 | // For PHP 8.0, libxml_disable_entity_loader() has been deprecated. 51 | // As libxml 2.9.0 is now required, external entity loading is guaranteed to be disabled by default. 52 | // And this function is no longer needed to protect against XXE attacks, unless the (still vulnerable). LIBXML_NOENT is used. 53 | // In that case, it is recommended to refactor the code using libxml_set_external_entity_loader() to suppress loading of external entities. 54 | if (\PHP_VERSION_ID < 80000) { 55 | $disableLibxmlEntityLoader = libxml_disable_entity_loader(true); 56 | $respObject = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOERROR); 57 | libxml_disable_entity_loader($disableLibxmlEntityLoader); 58 | } else { 59 | $respObject = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOERROR); 60 | } 61 | 62 | if ($respObject === false) { 63 | throw new \InvalidArgumentException('Syntax error.'); 64 | } 65 | 66 | return json_decode(json_encode($respObject), true); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/utils/tests/BCTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(BC::class, $b); 36 | $this->assertEquals($b->toString(), $value); 37 | } 38 | 39 | /** 40 | * @throws InvalidArgumentException 41 | * @throws ExpectationFailedException 42 | */ 43 | public function testAdd() 44 | { 45 | $b = BC::new(1.23); 46 | $b = $b->add(2.34, 2); 47 | $this->assertEquals(3.57, $b->toString()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/validation/src/RuleInterface.php: -------------------------------------------------------------------------------- 1 | value = $value; 22 | } 23 | 24 | public function failure(): string 25 | { 26 | return $this->message; 27 | } 28 | 29 | public function valid(): bool 30 | { 31 | if (!$valid = in_array($this->value, $this->haystack)) { 32 | $this->message = '验证不通过'; 33 | } 34 | 35 | return $valid; 36 | } 37 | } -------------------------------------------------------------------------------- /src/var-dumper/README.md: -------------------------------------------------------------------------------- 1 | var-dumper 适配包,用来将变量打印到浏览器 2 | 3 | # 安装 4 | 5 | ```shell 6 | composer require next/var-dumper 7 | ``` 8 | 9 | # 使用 10 | 11 | ## hyperf 12 | 13 | 修改`app/config/autoload/exceptions.php` 14 | 15 | ```php 16 | [ 22 | 'http' => [ 23 | Next\VarDumper\Adapter\HyperfDumperHandler::class, 24 | Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, 25 | App\Exception\Handler\AppExceptionHandler::class, 26 | ], 27 | ], 28 | ]; 29 | 30 | ``` 31 | 32 | ## webman 33 | 34 | 建立新的异常处理类 35 | 36 | ```php 37 | \App\ExceptionHandler::class, 68 | ]; 69 | ``` 70 | 71 | ## 其他框架可参考webman配置,引入DumperHandler,将异常转为响应即可 72 | 73 | # 打印 74 | 75 | ```php 76 | d($request); 77 | ``` 78 | -------------------------------------------------------------------------------- /src/var-dumper/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next/var-dumper", 3 | "license": "Apache-2.0", 4 | "homepage": "https://github.com/next-laboratory/var-dumper", 5 | "autoload": { 6 | "psr-4": { 7 | "Next\\VarDumper\\": "src/" 8 | }, 9 | "files": [ 10 | "src/helpers.php" 11 | ] 12 | }, 13 | "authors": [ 14 | { 15 | "name": "chengyao", 16 | "email": "chengyao0320@foxmail.com" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=7.4", 21 | "symfony/var-dumper": "^7.0" 22 | }, 23 | "extra": { 24 | "branch-alias": { 25 | "dev-master": "0.1.x-dev" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/var-dumper/src/Adapter/HyperfDumperHandler.php: -------------------------------------------------------------------------------- 1 | stopPropagation(); 32 | 33 | return $response->withBody(new SwooleStream(self::convertToHtml($e))); 34 | } 35 | 36 | public function isValid(\Throwable $e): bool 37 | { 38 | return $e instanceof Dumper; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/var-dumper/src/Dumper.php: -------------------------------------------------------------------------------- 1 | vars = $vars; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/var-dumper/src/DumperHandler.php: -------------------------------------------------------------------------------- 1 | addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); 25 | foreach ($abort->vars as $var) { 26 | (new HtmlDumper())->dump($cloner->cloneVar($var)); 27 | } 28 | return (string) ob_get_clean(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/var-dumper/src/helpers.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Max 5 |

6 | 7 |

轻量 • 简单 • 快速

8 | 9 |

10 | 11 | 12 |

13 | 14 | `MaxPHP`视图组件,支持`Blade`,可扩展驱动。 可以独立使用! 15 | 16 | # 安装 17 | 18 | ``` 19 | composer require max/view 20 | ``` 21 | 22 | # 使用 23 | 24 | > Blade引擎支持的语法如下 25 | 26 | - {{}} 27 | - {{-- --}} 28 | - {!! !!} 29 | - @extends 30 | - @yield 31 | - @php 32 | - @include 33 | - @if 34 | - @unless 35 | - @empty 36 | - @isset 37 | - @foreach 38 | - @for 39 | - @switch 40 | - @section 41 | 42 | > 如果使用`extends` + `yield` + `section`, 务必保证子模板中除了`extends` 之外的所有代码均被`section` 包裹 43 | 44 | ## 配置文件 45 | 46 | 安装完成后框架会自动将配置文件`view.php`移动到根包的`config`目录下,如果创建失败,可以手动创建。文件内容如下: 47 | 48 | ```php 49 | '\Next\View\Engine\BladeEngine', 53 | 'config' => [ 54 | // 模板目录 55 | 'path' => __DIR__ . '/../views/', 56 | // 编译和缓存目录 57 | 'compile_dir' => __DIR__ . '/../runtime/cache/views/compile', 58 | // 模板缓存 59 | 'cache' => false, 60 | // 模板后缀 61 | 'suffix' => '.blade.php', 62 | ], 63 | ]; 64 | 65 | ``` 66 | 67 | ## 使用 68 | 69 | ```php 70 | // 如果你使用maxphp的容器实例化该类,则不需要传入任何参数,只需要添加相应配置文件即可。 71 | $viewFactory = new ViewFactory($config); 72 | $renderer = $viewFactory->getRenderer(); 73 | 74 | // 如果你没有使用maxphp, 则需要实例化renderer, 传入对应的驱动 75 | $renderer = new \Next\View\Renderer(new \Next\View\Engine\BladeEngine($options)); 76 | 77 | $renderer->assign('key', 'value'); 78 | $renderer->render('index', ['key2' => 'value2']); 79 | ``` 80 | 81 | ## 自定义引擎 82 | 83 | 自定义引擎必须实现`ViewEngineInterface`接口 84 | -------------------------------------------------------------------------------- /src/view/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "max/view", 3 | "license": "Apache-2.0", 4 | "homepage": "https://github.com/marxphp/view", 5 | "description": "A simple blade view engine.", 6 | "authors": [ 7 | { 8 | "name": "chengyao", 9 | "email": "chengyao0320@foxmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.0", 14 | "max/utils": "dev-master", 15 | "max/config": "dev-master" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Next\\View\\": "src/" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/view/publish/view.php: -------------------------------------------------------------------------------- 1 | 'Next\View\Engine\BladeEngine', 14 | 'config' => [ 15 | // 模板目录 16 | 'path' => __DIR__ . '/../views/', 17 | // 编译和缓存目录 18 | 'compileDir' => __DIR__ . '/../runtime/cache/views/', 19 | // 模板缓存 20 | 'cache' => false, 21 | // 模板后缀 22 | 'suffix' => '.blade.php', 23 | ], 24 | // 'engine' => 'Next\View\Engine\PhpEngine', 25 | // 'config' => [ 26 | // // 模板目录 27 | // 'path' => __DIR__ . '/../views/', 28 | // // 模板后缀 29 | // 'suffix' => '.blade.php', 30 | // ], 31 | ]; 32 | -------------------------------------------------------------------------------- /src/view/src/Contract/ViewEngineInterface.php: -------------------------------------------------------------------------------- 1 | path; 37 | } 38 | 39 | public function isCache(): bool 40 | { 41 | return $this->cache; 42 | } 43 | 44 | public function getSuffix(): string 45 | { 46 | return $this->suffix; 47 | } 48 | 49 | public function getCompileDir(): string 50 | { 51 | return $this->compileDir; 52 | } 53 | 54 | /** 55 | * @throws ViewNotExistException 56 | */ 57 | public function render(string $template, array $arguments = []) 58 | { 59 | $this->renderView($template, $arguments); 60 | } 61 | 62 | /** 63 | * @throws ViewNotExistException 64 | */ 65 | protected function renderView(): void 66 | { 67 | extract(\func_get_arg(1)); 68 | include (new Compiler($this))->compile(\func_get_arg(0)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/view/src/Engine/PhpEngine.php: -------------------------------------------------------------------------------- 1 | fillProperties($config); 28 | } 29 | 30 | public function render(string $template, array $arguments = []): void 31 | { 32 | $this->renderView($template, $arguments); 33 | } 34 | 35 | protected function renderView(): void 36 | { 37 | extract(func_get_arg(1)); 38 | include $this->findViewFile(func_get_arg(0)); 39 | } 40 | 41 | protected function findViewFile(string $view): string 42 | { 43 | return sprintf('%s/%s%s', rtrim($this->path, '/'), trim($view, '/'), $this->suffix); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/view/src/Exception/ViewNotExistException.php: -------------------------------------------------------------------------------- 1 | arguments[$name] = $value; 27 | } 28 | 29 | public function render(string $template, array $arguments = []): string 30 | { 31 | ob_start(); 32 | echo (string) $this->engine->render($template, array_merge($this->arguments, $arguments)); 33 | return (string) ob_get_clean(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/view/src/ViewFactory.php: -------------------------------------------------------------------------------- 1 | engine); 29 | } 30 | 31 | public function render(string $template, array $arguments = []): string 32 | { 33 | return $this->getRenderer()->render($template, $arguments); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/watcher/README.md: -------------------------------------------------------------------------------- 1 | 开发中 2 | -------------------------------------------------------------------------------- /src/watcher/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "max/watcher", 3 | "license": "Apache-2.0", 4 | "autoload": { 5 | "psr-4": { 6 | "Next\\Watcher\\": "src/" 7 | } 8 | }, 9 | "authors": [ 10 | { 11 | "name": "chengyao", 12 | "email": "chengyao0320@foxmail.com" 13 | } 14 | ], 15 | "require": { 16 | "max/utils": "dev-master" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/watcher/src/Contract/DriverInterface.php: -------------------------------------------------------------------------------- 1 | findFiles() as $file) { 22 | $this->last[$file->getRealPath()] = $file->getMTime(); 23 | } 24 | } 25 | 26 | public function watch(): void 27 | { 28 | while (true) { 29 | usleep($this->interval); 30 | $currentFiles = []; 31 | $modified = $added = []; 32 | foreach ($this->findFiles() as $file) { 33 | $realPath = $file->getRealPath(); 34 | $fileMTime = $file->getMTime(); 35 | if (!isset($this->last[$realPath])) { 36 | $added[] = $realPath; 37 | $this->last[$realPath] = $fileMTime; 38 | } else { 39 | if ($this->last[$realPath] != $fileMTime) { 40 | $modified[] = $realPath; 41 | $this->last[$realPath] = $fileMTime; 42 | } 43 | } 44 | 45 | $currentFiles[$realPath] = $fileMTime; 46 | } 47 | $deleted = array_diff_key($this->last, $currentFiles); 48 | $this->last = array_diff_key($this->last, $deleted); 49 | $deleted = array_keys($deleted); 50 | clearstatcache(); 51 | if (!empty($modified) || !empty($added) || !empty($deleted)) { 52 | ($this->callback)($added, $modified, $deleted); 53 | } 54 | } 55 | } 56 | 57 | protected function findFiles(): Finder 58 | { 59 | return Finder::create()->in($this->dirs)->name($this->pattern)->files(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/watcher/src/Watcher.php: -------------------------------------------------------------------------------- 1 | writeLine('Watching filesystem'); 27 | 28 | $this->driver->watch(); 29 | } 30 | 31 | public function writeLine(string $message): void 32 | { 33 | printf("\033[33m [INFO] \033[0m %s\n", $message); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |