├── .github ├── dependabot.yml └── workflows │ ├── build-swoole.yml │ ├── coding_style_checks.yml │ ├── static_analysis.yml │ ├── syntax_checks.yml │ └── unit_tests.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── .phplint.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── THIRD-PARTY-NOTICES ├── composer.json ├── docker-compose.yml ├── examples ├── bootstrap.php ├── coroutine │ ├── batch.php │ ├── greeter.txt │ ├── map.php │ └── short_name.php ├── curl │ └── write_func.php ├── fastcgi │ ├── greeter │ │ ├── call.php │ │ ├── client.php │ │ └── greeter.php │ ├── proxy │ │ └── wordpress.php │ └── var │ │ ├── client.php │ │ └── var.php ├── functions │ └── table.php ├── mysqli │ ├── base.php │ └── io_failure.php ├── pdo │ ├── base.php │ └── io_failure.php ├── redis │ └── base.php ├── service │ ├── consul.php │ └── nacos.php ├── string │ └── mbstring.php └── thread │ └── pool.php ├── phpstan.neon.dist ├── phpunit.xml.dist ├── src ├── __init__.php ├── alias.php ├── alias_ns.php ├── constants.php ├── core │ ├── ArrayObject.php │ ├── ConnectionPool.php │ ├── Constant.php │ ├── Coroutine │ │ ├── Barrier.php │ │ ├── FastCGI │ │ │ ├── Client.php │ │ │ ├── Client │ │ │ │ └── Exception.php │ │ │ └── Proxy.php │ │ ├── Http │ │ │ ├── ClientProxy.php │ │ │ └── functions.php │ │ ├── Server.php │ │ ├── Server │ │ │ └── Connection.php │ │ ├── WaitGroup.php │ │ └── functions.php │ ├── Curl │ │ ├── Exception.php │ │ └── Handler.php │ ├── Database │ │ ├── DetectsLostConnections.php │ │ ├── MysqliConfig.php │ │ ├── MysqliException.php │ │ ├── MysqliPool.php │ │ ├── MysqliProxy.php │ │ ├── MysqliStatementProxy.php │ │ ├── ObjectProxy.php │ │ ├── PDOConfig.php │ │ ├── PDOPool.php │ │ ├── PDOProxy.php │ │ ├── PDOStatementProxy.php │ │ ├── RedisConfig.php │ │ └── RedisPool.php │ ├── Exception │ │ └── ArrayKeyNotExists.php │ ├── FastCGI.php │ ├── FastCGI │ │ ├── FrameParser.php │ │ ├── HttpRequest.php │ │ ├── HttpResponse.php │ │ ├── Message.php │ │ ├── Record.php │ │ ├── Record │ │ │ ├── AbortRequest.php │ │ │ ├── BeginRequest.php │ │ │ ├── Data.php │ │ │ ├── EndRequest.php │ │ │ ├── GetValues.php │ │ │ ├── GetValuesResult.php │ │ │ ├── Params.php │ │ │ ├── Stderr.php │ │ │ ├── Stdin.php │ │ │ ├── Stdout.php │ │ │ └── UnknownType.php │ │ ├── Request.php │ │ └── Response.php │ ├── Http │ │ └── Status.php │ ├── MultibyteStringObject.php │ ├── NameResolver.php │ ├── NameResolver │ │ ├── Cluster.php │ │ ├── Consul.php │ │ ├── Exception.php │ │ ├── Nacos.php │ │ └── Redis.php │ ├── ObjectProxy.php │ ├── Process │ │ └── Manager.php │ ├── Server │ │ ├── Admin.php │ │ └── Helper.php │ ├── StringObject.php │ └── Thread │ │ ├── Pool.php │ │ └── Runnable.php ├── ext │ ├── curl.php │ └── sockets.php ├── functions.php ├── std │ └── exec.php └── vendor_init.php └── tests ├── DatabaseTestCase.php ├── HookFlagsTrait.php ├── TestThread.php ├── bootstrap.php ├── unit ├── ArrayObjectTest.php ├── Coroutine │ ├── BarrierTest.php │ ├── FunctionTest.php │ ├── HttpFunctionTest.php │ └── WaitGroupTest.php ├── Curl │ └── HandlerTest.php ├── Database │ ├── PDOPoolTest.php │ └── PDOStatementProxyTest.php ├── FastCGI │ ├── FrameParserTest.php │ ├── HttpRequestTest.php │ ├── HttpResponseTest.php │ ├── Record │ │ ├── AbortRequestTest.php │ │ ├── BeginRequestTest.php │ │ ├── DataTest.php │ │ ├── EndRequestTest.php │ │ ├── GetValuesResultTest.php │ │ ├── GetValuesTest.php │ │ ├── ParamsTest.php │ │ ├── StderrTest.php │ │ ├── StdinTest.php │ │ ├── StdoutTest.php │ │ └── UnknownTypeTest.php │ └── RecordTest.php ├── FunctionTest.php ├── MultibyteStringObjectTest.php ├── NameResolverTest.php ├── ObjectProxyTest.php ├── Process │ └── ProcessManagerTest.php ├── StringObjectTest.php └── Thread │ └── PoolTest.php └── www ├── README.md ├── header0.php ├── header1.php ├── header2.php ├── status0.php ├── status1.php ├── status2.php └── status3.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/build-swoole.yml: -------------------------------------------------------------------------------- 1 | name: Build Swoole 2 | 3 | on: [ push, pull_request, workflow_dispatch ] 4 | 5 | jobs: 6 | build-swoole: 7 | runs-on: ubuntu-22.04 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | php: ["8.1", "8.2", "8.3", "8.4"] 12 | 13 | name: PHP ${{ matrix.php }} - Swoole 14 | 15 | steps: 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{ matrix.php }} 20 | tools: composer:v2, phpize 21 | coverage: none 22 | 23 | - name: Download Latest Swoole 24 | run: | 25 | set -ex 26 | mkdir swoole 27 | curl -sfL https://github.com/swoole/swoole-src/archive/master.tar.gz -o swoole.tar.gz 28 | tar xfz swoole.tar.gz --strip-components=1 -C swoole 29 | 30 | - name: Checkout Source Code of Swoole Library 31 | uses: actions/checkout@v4 32 | with: 33 | path: './swoole/library' 34 | 35 | - name: Build Swoole 36 | run: | 37 | set -ex 38 | 39 | cd swoole 40 | composer install -d ./tools -n -q --no-progress 41 | 42 | cat ext-src/php_swoole_library.h | grep '/* $Id:' # the commit # of Swoole Library used in Swoole. 43 | php ./tools/build-library.php 44 | cat ext-src/php_swoole_library.h | grep '/* $Id:' # the commit # of current Swoole Library. 45 | 46 | phpize 47 | ./configure 48 | make -j$(nproc) 49 | sudo make install 50 | 51 | echo "extension=swoole" | sudo tee "$(php-config --ini-dir)/ext-swoole.ini" 52 | 53 | - name: Check Swoole Installation 54 | run: php -v && php --ri swoole 55 | -------------------------------------------------------------------------------- /.github/workflows/coding_style_checks.yml: -------------------------------------------------------------------------------- 1 | name: Coding Style Checks 2 | 3 | on: [ push, pull_request, workflow_dispatch ] 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-22.04 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Run Coding Style Checks 14 | run: docker run -q --rm -v "$(pwd):/project" -w /project -i jakzal/phpqa:php8.3-alpine php-cs-fixer fix -q --dry-run 15 | -------------------------------------------------------------------------------- /.github/workflows/static_analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | 3 | on: [ push, pull_request, workflow_dispatch ] 4 | 5 | jobs: 6 | static-analysis: 7 | runs-on: ubuntu-22.04 8 | container: 9 | image: phpswoole/swoole:6.0-php8.3 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup PHP And Swoole 16 | run: | 17 | set -e 18 | echo "swoole.enable_library=Off" >> /usr/local/etc/php/conf.d/docker-php-ext-swoole.ini 19 | composer require -n -q --no-progress -- phpstan/phpstan=~2.0 20 | 21 | - name: Run Static Analysis 22 | run: ./vendor/bin/phpstan analyse --no-progress --memory-limit 2G 23 | -------------------------------------------------------------------------------- /.github/workflows/syntax_checks.yml: -------------------------------------------------------------------------------- 1 | name: Syntax Checks 2 | 3 | on: [ push, pull_request, workflow_dispatch ] 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-22.04 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | php: ["8.0", "8.1", "8.2", "8.3", "8.4"] 12 | 13 | name: Syntax Checks Under PHP ${{ matrix.php }} 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Run Syntax Checks 20 | run: docker run -q --rm -v "$(pwd):/project" -w /project -i jakzal/phpqa:php${{ matrix.php }} phplint 21 | -------------------------------------------------------------------------------- /.github/workflows/unit_tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: [ push, pull_request, workflow_dispatch ] 4 | 5 | jobs: 6 | # Run unit tests with Swoole 6.0+. 7 | unit-tests: 8 | runs-on: ubuntu-22.04 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | image-tag-prefix: ["", "6.0-"] 13 | php: ["8.1", "8.2", "8.3", "8.4"] 14 | 15 | name: Image phpswoole/swoole:${{ matrix.image-tag-prefix }}php${{ matrix.php }} 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Start Docker Containers 22 | uses: hoverkraft-tech/compose-action@v2.2.0 23 | env: 24 | IMAGE_TAG_PREFIX: ${{ matrix.image-tag-prefix }} 25 | PHP_VERSION: ${{ matrix.php }} 26 | 27 | - name: Prepare Test Environment 28 | run: | 29 | docker compose exec -T app php -v 30 | docker compose exec -T app php --ri swoole 31 | docker compose exec -T app composer install -n -q --no-progress 32 | sleep 40s 33 | 34 | - name: Run Unit Tests 35 | run: docker compose exec -T app composer test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Composer files. 2 | /composer.lock 3 | /composer.phar 4 | /vendor/* 5 | 6 | # Ignore PHPUnit files. 7 | /.phpunit.cache 8 | /.phpunit.result.cache 9 | /phpunit.xml 10 | 11 | # Ignore PHPStan files. 12 | /phpstan.neon 13 | 14 | # Ignore test output. 15 | /.phplint-cache 16 | /.phplint.cache/* 17 | 18 | # Ignore project files. 19 | /Dockerfile.bak 20 | /html/* 21 | /.idea 22 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 13 | ->setRules([ 14 | '@DoctrineAnnotation' => true, 15 | '@PhpCsFixer' => true, 16 | '@PSR2' => true, 17 | '@Symfony' => true, 18 | 'align_multiline_comment' => ['comment_type' => 'all_multiline'], 19 | 'array_syntax' => ['syntax' => 'short'], 20 | 'binary_operator_spaces' => ['operators' => ['=' => 'align', '=>' => 'align', ]], 21 | 'blank_line_after_namespace' => true, 22 | 'blank_line_before_statement' => ['statements' => ['declare']], 23 | 'class_attributes_separation' => true, 24 | 'concat_space' => ['spacing' => 'one'], 25 | 'constant_case' => ['case' => 'lower'], 26 | 'combine_consecutive_unsets' => true, 27 | 'declare_strict_types' => true, 28 | 'fully_qualified_strict_types' => ['phpdoc_tags' => []], 29 | 'general_phpdoc_annotation_remove' => ['annotations' => ['author']], 30 | 'header_comment' => ['comment_type' => 'PHPDoc', 'header' => $header, 'location' => 'after_open', 'separate' => 'bottom'], 31 | 'increment_style' => ['style' => 'post'], 32 | 'lambda_not_used_import' => false, 33 | 'linebreak_after_opening_tag' => true, 34 | 'list_syntax' => ['syntax' => 'short'], 35 | 'lowercase_static_reference' => true, 36 | 'multiline_comment_opening_closing' => true, 37 | 'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'], 38 | 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true, 'allow_unused_params' => true, 'remove_inheritdoc' => false], 39 | 'no_unused_imports' => true, 40 | 'no_useless_else' => true, 41 | 'no_useless_return' => true, 42 | 'not_operator_with_space' => false, 43 | 'not_operator_with_successor_space' => false, 44 | 'nullable_type_declaration_for_default_null_value' => ['use_nullable_type_declaration' => true], 45 | 'php_unit_strict' => false, 46 | 'phpdoc_align' => ['align' => 'left'], 47 | 'phpdoc_annotation_without_dot' => false, 48 | 'phpdoc_no_empty_return' => false, 49 | 'phpdoc_types_order' => ['sort_algorithm' => 'none', 'null_adjustment' => 'always_last'], 50 | 'phpdoc_separation' => false, 51 | 'phpdoc_summary' => false, 52 | 'ordered_class_elements' => true, 53 | 'ordered_imports' => ['imports_order' => ['class', 'function', 'const'], 'sort_algorithm' => 'alpha'], 54 | 'ordered_types' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 55 | 'single_line_comment_style' => ['comment_types' => []], 56 | 'single_line_comment_spacing' => false, 57 | 'single_line_empty_body' => false, 58 | 'single_quote' => true, 59 | 'standardize_increment' => false, 60 | 'standardize_not_equals' => true, 61 | 'yoda_style' => ['always_move_variable' => false, 'equal' => false, 'identical' => false], 62 | ]) 63 | ->setFinder( 64 | PhpCsFixer\Finder::create() 65 | ->exclude(['html', 'vendor']) 66 | ->in(__DIR__) 67 | ) 68 | ->setUsingCache(false); 69 | -------------------------------------------------------------------------------- /.phplint.yml: -------------------------------------------------------------------------------- 1 | path: 2 | - examples/ 3 | - src/ 4 | - tests/ 5 | jobs: 10 6 | extensions: 7 | - php 8 | exclude: 9 | - vendor 10 | warning: true 11 | memory-limit: -1 12 | no-cache: false 13 | log-json: false 14 | log-junit: false 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG IMAGE_TAG_PREFIX="" 2 | ARG PHP_VERSION=8.3 3 | 4 | FROM phpswoole/swoole:${IMAGE_TAG_PREFIX}php${PHP_VERSION} 5 | 6 | RUN set -ex \ 7 | && apt update \ 8 | && apt install -y libaio-dev libc-ares-dev libaio1 supervisor wget git --no-install-recommends \ 9 | && wget -nv https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linuxx64.zip \ 10 | && unzip instantclient-basiclite-linuxx64.zip && rm -rf META-INF instantclient-basiclite-linuxx64.zip \ 11 | && wget -nv https://download.oracle.com/otn_software/linux/instantclient/instantclient-sdk-linuxx64.zip \ 12 | && unzip instantclient-sdk-linuxx64.zip && rm -rf META-INF instantclient-sdk-linuxx64.zip \ 13 | && mv instantclient_*_* ./instantclient \ 14 | && rm ./instantclient/sdk/include/ldap.h \ 15 | && echo DISABLE_INTERRUPT=on > ./instantclient/network/admin/sqlnet.ora \ 16 | && mv ./instantclient /usr/local/ \ 17 | && echo '/usr/local/instantclient' > /etc/ld.so.conf.d/oracle-instantclient.conf \ 18 | && ldconfig \ 19 | && export ORACLE_HOME=instantclient,/usr/local/instantclient \ 20 | && apt install -y sqlite3 libsqlite3-dev libpq-dev --no-install-recommends \ 21 | && docker-php-ext-install mysqli pdo_pgsql pdo_sqlite \ 22 | && docker-php-ext-enable mysqli pdo_pgsql pdo_sqlite \ 23 | && pecl channel-update pecl \ 24 | && if [ "$(php -r 'echo version_compare(PHP_VERSION, "8.4.0", "<") ? "old" : "new";')" = "old" ] ; then docker-php-ext-install pdo_oci; else pecl install pdo_oci-stable; fi \ 25 | && docker-php-ext-enable pdo_oci \ 26 | && git clone https://github.com/swoole/swoole-src.git \ 27 | && cd ./swoole-src \ 28 | && phpize \ 29 | && ./configure --enable-openssl \ 30 | --enable-sockets \ 31 | --enable-mysqlnd \ 32 | --enable-swoole-curl \ 33 | --enable-cares \ 34 | --enable-swoole-pgsql \ 35 | --with-swoole-oracle=instantclient,/usr/local/instantclient \ 36 | --enable-swoole-sqlite \ 37 | && make -j$(cat /proc/cpuinfo | grep processor | wc -l) \ 38 | && make install \ 39 | && docker-php-ext-enable swoole \ 40 | && echo "swoole.enable_library=off" >> /usr/local/etc/php/conf.d/docker-php-ext-swoole.ini \ 41 | && php -m \ 42 | && php --ri swoole \ 43 | && { \ 44 | echo '[supervisord]'; \ 45 | echo 'user = root'; \ 46 | echo ''; \ 47 | echo '[program:wordpress]'; \ 48 | echo 'command = php /var/www/examples/fastcgi/proxy/wordpress.php'; \ 49 | echo 'user = root'; \ 50 | echo 'autostart = true'; \ 51 | echo 'stdout_logfile=/proc/self/fd/1'; \ 52 | echo 'stdout_logfile_maxbytes=0'; \ 53 | echo 'stderr_logfile=/proc/self/fd/1'; \ 54 | echo 'stderr_logfile_maxbytes=0'; \ 55 | } > /etc/supervisor/service.d/wordpress.conf 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swoole Library 2 | 3 | [![Twitter](https://badgen.net/badge/icon/twitter?icon=twitter&label)](https://twitter.com/phpswoole) 4 | [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.swoole.dev) 5 | [![Library Status](https://github.com/swoole/library/workflows/Unit%20Tests/badge.svg)](https://github.com/swoole/library/actions) 6 | [![License](https://img.shields.io/badge/license-apache2-blue.svg)](LICENSE) 7 | 8 | ## Dockerized Local Development 9 | 10 | First, run one of the following commands to install development packages using _Composer_: 11 | 12 | ```bash 13 | docker run --rm -v "$(pwd)":/var/www -ti phpswoole/swoole composer update -n 14 | 15 | # or, use the official Composer Docker image: 16 | docker run --rm -v "$(pwd)":/app -ti composer update -n --ignore-platform-reqs 17 | 18 | # or, use the local Composer if installed: 19 | composer update -n --ignore-platform-reqs 20 | ``` 21 | 22 | Next, you need to start Docker containers: 23 | 24 | ```bash 25 | docker compose up -d 26 | ``` 27 | 28 | Alternatively, if you need to rebuild some Docker image and to restart the containers: 29 | 30 | ```bash 31 | docker compose build --progress plain --no-cache 32 | docker compose up -d --force-recreate 33 | ``` 34 | 35 | Now you can create a `bash` session in the `app` container: 36 | 37 | ```bash 38 | docker compose exec app bash 39 | ``` 40 | 41 | And run commands inside the container: 42 | 43 | ```bash 44 | composer test 45 | ``` 46 | 47 | Or you can run commands directly inside the `app` container: 48 | 49 | ```bash 50 | docker compose exec app composer test 51 | ``` 52 | 53 | ## Examples 54 | 55 | Once you have Docker containers started (as discussed in previous section), you can use commands like following to run 56 | examples under folder [examples](https://github.com/swoole/library/tree/master/examples). 57 | 58 | ### Examples of Database Connection Pool 59 | 60 | ```bash 61 | docker compose exec app php examples/mysqli/base.php 62 | docker compose exec app php examples/pdo/base.php 63 | docker compose exec app php examples/redis/base.php 64 | ``` 65 | 66 | ### Examples of FastCGI Calls 67 | 68 | There is a fantastic example showing how to use Swoole as a proxy to serve a WordPress website using PHP-FPM. Just 69 | open URL _http://127.0.0.1_ in the browser and check what you see there. Source code of the example can be 70 | found [here](https://github.com/swoole/library/blob/master/examples/fastcgi/proxy/wordpress.php). 71 | 72 | Here are some more examples to make FastCGI calls to PHP-FPM: 73 | 74 | ```bash 75 | docker compose exec app php examples/fastcgi/greeter/call.php 76 | docker compose exec app php examples/fastcgi/greeter/client.php 77 | docker compose exec app php examples/fastcgi/proxy/base.php 78 | docker compose exec app php examples/fastcgi/var/client.php 79 | ``` 80 | 81 | ## Third Party Libraries 82 | 83 | Here are all the third party libraries used in this project: 84 | 85 | * The FastCGI part is derived from Composer package [lisachenko/protocol-fcgi](https://github.com/lisachenko/protocol-fcgi). 86 | 87 | You can find the licensing information of these third party libraries [here](https://github.com/swoole/library/blob/master/THIRD-PARTY-NOTICES). 88 | 89 | ## License 90 | 91 | This project follows the [Apache 2 license](https://github.com/swoole/library/blob/master/LICENSE). 92 | -------------------------------------------------------------------------------- /THIRD-PARTY-NOTICES: -------------------------------------------------------------------------------- 1 | Swoole Library uses third-party libraries or other resources that may 2 | be distributed under licenses different than the Swoole project. 3 | 4 | In the event that we accidentally failed to list a required notice, 5 | please bring it to our attention through any of the ways detailed here : 6 | 7 | team@swoole.com 8 | 9 | The attached notices are provided for information only. 10 | 11 | 1) License Notice for Composer Package "lisachenko/protocol-fcgi" 12 | ----------------------------------------------------------------- 13 | 14 | https://github.com/lisachenko/protocol-fcgi/blob/master/LICENSE 15 | 16 | Copyright (c) 2015 Lisachenko Alexander 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy 19 | of this software and associated documentation files (the "Software"), to deal 20 | in the Software without restriction, including without limitation the rights 21 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | copies of the Software, and to permit persons to whom the Software is 23 | furnished to do so, subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in 26 | all copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 34 | THE SOFTWARE. 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swoole/library", 3 | "description": "Library of Swoole", 4 | "keywords": [ 5 | "swoole", 6 | "library" 7 | ], 8 | "license": "Apache-2.0", 9 | "authors": [ 10 | { 11 | "name": "Swoole Team", 12 | "email": "team@swoole.com" 13 | } 14 | ], 15 | "support": { 16 | "issues": "https://github.com/swoole/library" 17 | }, 18 | "require": { 19 | "php": ">=8.1", 20 | "ext-swoole": ">=5.1", 21 | "nikic/php-parser": "^5.2" 22 | }, 23 | "require-dev": { 24 | "ext-sockets": "*", 25 | "ext-json": "*", 26 | "ext-redis": "*", 27 | "ext-curl": "*", 28 | "phpunit/phpunit": "~10.0 || ~11.0", 29 | "swoole/ide-helper": "dev-master" 30 | }, 31 | "suggest": { 32 | "ext-mysqli": "Required to use mysqli database", 33 | "ext-pdo": "Required to use pdo database", 34 | "ext-redis": "Required to use redis database, and the required version is greater than or equal to 3.1.3", 35 | "ext-curl": "Required to use http client" 36 | }, 37 | "autoload": { 38 | "files": [ 39 | "src/constants.php", 40 | "src/core/Coroutine/functions.php", 41 | "src/core/Coroutine/Http/functions.php", 42 | "src/std/exec.php", 43 | "src/ext/curl.php", 44 | "src/ext/sockets.php", 45 | "src/functions.php", 46 | "src/alias.php", 47 | "src/alias_ns.php", 48 | "src/vendor_init.php" 49 | ], 50 | "psr-4": { 51 | "Swoole\\": "src/core" 52 | } 53 | }, 54 | "autoload-dev": { 55 | "classmap": [ 56 | "tests/DatabaseTestCase.php", 57 | "tests/HookFlagsTrait.php" 58 | ] 59 | }, 60 | "config": { 61 | "discard-changes": true 62 | }, 63 | "scripts": { 64 | "test": "/usr/bin/env php -d swoole.enable_library=Off ./vendor/bin/phpunit", 65 | "post-install-cmd": [ 66 | "rm -rf ./vendor/swoole/ide-helper/src/swoole_library ./vendor/swoole/ide-helper/output/swoole_library" 67 | ], 68 | "post-update-cmd": [ 69 | "rm -rf ./vendor/swoole/ide-helper/src/swoole_library ./vendor/swoole/ide-helper/output/swoole_library" 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: . 5 | args: 6 | - IMAGE_TAG_PREFIX 7 | - PHP_VERSION 8 | platform: linux/amd64 9 | links: 10 | - consul 11 | - mysql 12 | - nacos 13 | - oracle 14 | - php-fpm 15 | - pgsql 16 | - redis 17 | - wordpress 18 | environment: 19 | DISABLE_DEFAULT_SERVER: 1 20 | GITHUB_ACTIONS: "yes" 21 | ports: 22 | - "80:80" 23 | volumes: 24 | - .:/var/www 25 | - wordpress:/var/www/html 26 | 27 | php-fpm: 28 | image: php:8.4-fpm 29 | volumes: 30 | - .:/var/www 31 | 32 | wordpress: 33 | image: wordpress:php8.4-fpm 34 | links: 35 | - mysql 36 | environment: 37 | WORDPRESS_DB_HOST: mysql 38 | WORDPRESS_DB_USER: username 39 | WORDPRESS_DB_PASSWORD: password 40 | WORDPRESS_DB_NAME: test 41 | WORDPRESS_TABLE_PREFIX: wp_ 42 | volumes: 43 | - type: volume 44 | source: wordpress 45 | target: /var/www/html 46 | volume: 47 | nocopy: false 48 | 49 | mysql: 50 | image: mysql:8 51 | environment: 52 | MYSQL_DATABASE: test 53 | MYSQL_USER: username 54 | MYSQL_PASSWORD: password 55 | MYSQL_ROOT_PASSWORD: password 56 | 57 | pgsql: 58 | image: postgres:17-alpine 59 | environment: 60 | POSTGRES_DB: test 61 | POSTGRES_USER: username 62 | POSTGRES_PASSWORD: password 63 | 64 | oracle: 65 | image: gvenzl/oracle-xe:slim 66 | platform: linux/amd64 67 | environment: 68 | ORACLE_PASSWORD: oracle 69 | 70 | redis: 71 | image: redis:7.2 72 | 73 | nacos: 74 | image: nacos/nacos-server 75 | platform: linux/amd64 76 | environment: 77 | MODE: standalone 78 | PREFER_HOST_MODE: hostname 79 | 80 | consul: 81 | image: consul:1.15 82 | command: 83 | consul agent -dev -client=0.0.0.0 84 | 85 | volumes: 86 | wordpress: 87 | -------------------------------------------------------------------------------- /examples/bootstrap.php: -------------------------------------------------------------------------------- 1 | SWOOLE_HOOK_ALL]); 19 | 20 | Coroutine\run(function () { 21 | $use = microtime(true); 22 | $results = batch([ 23 | 'gethostbyname' => fn () => gethostbyname('localhost'), 24 | 'file_get_contents' => fn () => file_get_contents(__DIR__ . '/greeter.txt'), 25 | 'sleep' => function () { 26 | sleep(1); 27 | return true; 28 | }, 29 | 'usleep' => function () { 30 | usleep(1000); 31 | return true; 32 | }, 33 | ], 0.1); 34 | $use = microtime(true) - $use; 35 | echo "Use {$use}s, Result:\n"; 36 | var_dump($results); 37 | }); 38 | echo "Done\n"; 39 | -------------------------------------------------------------------------------- /examples/coroutine/greeter.txt: -------------------------------------------------------------------------------- 1 | Hello Swoole -------------------------------------------------------------------------------- /examples/coroutine/map.php: -------------------------------------------------------------------------------- 1 | SWOOLE_HOOK_ALL]); 19 | 20 | function fatorial(int $n): int 21 | { 22 | return array_product(range($n, 1)); 23 | } 24 | 25 | Coroutine\run(function () { 26 | $use = microtime(true); 27 | 28 | $results = map([2, 3, 4], 'fatorial'); // 2 6 24 29 | 30 | $use = microtime(true) - $use; 31 | echo "Use {$use}s, Result:\n"; 32 | var_dump($results); 33 | }); 34 | echo "Done\n"; 35 | -------------------------------------------------------------------------------- /examples/coroutine/short_name.php: -------------------------------------------------------------------------------- 1 | 'Swoole'] 23 | ); 24 | echo "Result: {$result}\n"; 25 | } catch (Client\Exception $exception) { 26 | echo "Error: {$exception->getMessage()}\n"; 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /examples/fastcgi/greeter/client.php: -------------------------------------------------------------------------------- 1 | withScriptFilename(__DIR__ . '/greeter.php') 23 | ->withMethod('POST') 24 | ->withBody(['who' => 'Swoole']) 25 | ; 26 | $response = $client->execute($request); 27 | echo "Result: {$response->getBody()}\n"; 28 | } catch (Client\Exception $exception) { 29 | echo "Error: {$exception->getMessage()}\n"; 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /examples/fastcgi/greeter/greeter.php: -------------------------------------------------------------------------------- 1 | set([ 23 | Constant::OPTION_WORKER_NUM => swoole_cpu_num() * 2, 24 | Constant::OPTION_HTTP_PARSE_COOKIE => false, 25 | Constant::OPTION_HTTP_PARSE_POST => false, 26 | Constant::OPTION_DOCUMENT_ROOT => $documentRoot, 27 | Constant::OPTION_ENABLE_STATIC_HANDLER => true, 28 | Constant::OPTION_STATIC_HANDLER_LOCATIONS => ['/wp-admin', '/wp-content', '/wp-includes'], 29 | ]); 30 | $proxy = new Proxy('wordpress:9000', $documentRoot); 31 | $server->on('request', function (Request $request, Response $response) use ($proxy, $documentRoot) { 32 | // Requests to /wp-login.php, /wp-signup.php, etc should not be processed using /index.php. 33 | if (!is_readable($documentRoot . $request->server['path_info'])) { 34 | $request->server['path_info'] = '/index.php'; 35 | } 36 | $proxy->pass($request, $response); 37 | }); 38 | $server->start(); 39 | -------------------------------------------------------------------------------- /examples/fastcgi/var/client.php: -------------------------------------------------------------------------------- 1 | withDocumentRoot(__DIR__) 23 | ->withScriptFilename(__DIR__ . '/var.php') 24 | ->withScriptName('var.php') 25 | ->withMethod('POST') 26 | ->withUri('/var?foo=bar&bar=char') 27 | ->withHeader('X-Foo', 'bar') 28 | ->withHeader('X-Bar', 'char') 29 | ->withBody(['foo' => 'bar', 'bar' => 'char']) 30 | ; 31 | $response = $client->execute($request); 32 | echo "Result: \n{$response->getBody()}"; 33 | } catch (Client\Exception $exception) { 34 | echo "Error: {$exception->getMessage()}\n"; 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /examples/fastcgi/var/var.php: -------------------------------------------------------------------------------- 1 | withHost(MYSQL_SERVER_HOST) 27 | ->withPort(MYSQL_SERVER_PORT) 28 | // ->withUnixSocket('/tmp/mysql.sock') 29 | ->withDbName(MYSQL_SERVER_DB) 30 | ->withCharset('utf8mb4') 31 | ->withUsername(MYSQL_SERVER_USER) 32 | ->withPassword(MYSQL_SERVER_PWD) 33 | ); 34 | for ($n = N; $n--;) { 35 | Coroutine::create(function () use ($pool) { 36 | $mysqli = $pool->get(); 37 | $statement = $mysqli->prepare('SELECT ? + ?'); 38 | if (!$statement) { 39 | throw new RuntimeException('Prepare failed'); 40 | } 41 | $a = mt_rand(1, 100); 42 | $b = mt_rand(1, 100); 43 | if (!$statement->bind_param('dd', $a, $b)) { 44 | throw new RuntimeException('Bind param failed'); 45 | } 46 | if (!$statement->execute()) { 47 | throw new RuntimeException('Execute failed'); 48 | } 49 | if (!$statement->bind_result($result)) { 50 | throw new RuntimeException('Bind result failed'); 51 | } 52 | if (!$statement->fetch()) { 53 | throw new RuntimeException('Fetch failed'); 54 | } 55 | if ($a + $b !== (int) $result) { 56 | throw new RuntimeException('Bad result'); 57 | } 58 | while ($statement->fetch()) { 59 | continue; 60 | } 61 | $pool->put($mysqli); 62 | }); 63 | } 64 | }); 65 | $s = microtime(true) - $s; 66 | echo 'Use ' . $s . 's for ' . N . ' queries' . PHP_EOL; 67 | -------------------------------------------------------------------------------- /examples/mysqli/io_failure.php: -------------------------------------------------------------------------------- 1 | withHost(MYSQL_SERVER_HOST) 27 | ->withPort(MYSQL_SERVER_PORT) 28 | // ->withUnixSocket('/tmp/mysql.sock') 29 | ->withDbName(MYSQL_SERVER_DB) 30 | ->withCharset('utf8mb4') 31 | ->withUsername(MYSQL_SERVER_USER) 32 | ->withPassword(MYSQL_SERVER_PWD) 33 | ); 34 | Coroutine::create(function () use ($pool) { 35 | $killer = $pool->get(); 36 | while (true) { 37 | $processList = $killer->query('show processlist'); 38 | $processList = $processList->fetch_all(MYSQLI_ASSOC); 39 | $processList = array_filter($processList, fn (array $value) => $value['db'] === 'test' && $value['Info'] != 'show processlist'); 40 | foreach ($processList as $process) { 41 | $killer->query("KILL {$process['Id']}"); 42 | } 43 | Coroutine::sleep(0.1); 44 | } 45 | }); 46 | /* record and show success count */ 47 | $success = 0; 48 | Coroutine::create(function () use (&$success) { 49 | while (true) { 50 | echo "Success: {$success}" . PHP_EOL; 51 | Coroutine::sleep(1); 52 | } 53 | }); 54 | for ($c = C; $c--;) { 55 | Coroutine::create(function () use ($pool, &$success) { 56 | while (true) { 57 | $mysqli = $pool->get(); 58 | $statement = $mysqli->prepare('SELECT ? + ?'); 59 | if (!$statement) { 60 | throw new RuntimeException('Prepare failed'); 61 | } 62 | $a = mt_rand(1, 100); 63 | $b = mt_rand(1, 100); 64 | if (!$statement->bind_param('dd', $a, $b)) { 65 | throw new RuntimeException('Bind param failed'); 66 | } 67 | if (!$statement->execute()) { 68 | throw new RuntimeException('Execute failed'); 69 | } 70 | if (!$statement->bind_result($result)) { 71 | throw new RuntimeException('Bind result failed'); 72 | } 73 | if (!$statement->fetch()) { 74 | throw new RuntimeException('Fetch failed'); 75 | } 76 | if ($a + $b !== (int) $result) { 77 | throw new RuntimeException('Bad result'); 78 | } 79 | while ($statement->fetch()) { 80 | continue; 81 | } 82 | $pool->put($mysqli); 83 | $success++; 84 | co::sleep(mt_rand(100, 1000) / 1000); 85 | } 86 | }); 87 | } 88 | }); 89 | -------------------------------------------------------------------------------- /examples/pdo/base.php: -------------------------------------------------------------------------------- 1 | withHost(MYSQL_SERVER_HOST) 27 | ->withPort(MYSQL_SERVER_PORT) 28 | // ->withUnixSocket('/tmp/mysql.sock') 29 | ->withDbName(MYSQL_SERVER_DB) 30 | ->withCharset('utf8mb4') 31 | ->withUsername(MYSQL_SERVER_USER) 32 | ->withPassword(MYSQL_SERVER_PWD) 33 | ); 34 | for ($n = N; $n--;) { 35 | Coroutine::create(function () use ($pool) { 36 | $pdo = $pool->get(); 37 | $statement = $pdo->prepare('SELECT ? + ?'); 38 | if (!$statement) { 39 | throw new RuntimeException('Prepare failed'); 40 | } 41 | $a = mt_rand(1, 100); 42 | $b = mt_rand(1, 100); 43 | $result = $statement->execute([$a, $b]); 44 | if (!$result) { 45 | throw new RuntimeException('Execute failed'); 46 | } 47 | $result = $statement->fetchAll(); 48 | if ($a + $b !== (int) $result[0][0]) { 49 | throw new RuntimeException('Bad result'); 50 | } 51 | $pool->put($pdo); 52 | }); 53 | } 54 | }); 55 | $s = microtime(true) - $s; 56 | echo 'Use ' . $s . 's for ' . N . ' queries' . PHP_EOL; 57 | -------------------------------------------------------------------------------- /examples/pdo/io_failure.php: -------------------------------------------------------------------------------- 1 | new PDO( 26 | 'mysql:' . 27 | 'host=' . MYSQL_SERVER_HOST . ';' . 28 | 'port=' . MYSQL_SERVER_PWD . ';' . 29 | 'dbname=' . MYSQL_SERVER_DB . ';' . 30 | 'charset=utf8mb4', 31 | MYSQL_SERVER_USER, 32 | MYSQL_SERVER_PWD 33 | ); 34 | /* connection killer */ 35 | Coroutine::create(function () use ($constructor) { 36 | $pdo = $constructor(); 37 | while (true) { 38 | $processList = $pdo->query('show processlist'); 39 | $processList->execute(); 40 | $processList = $processList->fetchAll(); 41 | $processList = array_filter($processList, fn (array $value) => $value['db'] === 'test' && $value['Info'] != 'show processlist'); 42 | foreach ($processList as $process) { 43 | $pdo->exec("KILL {$process['Id']}"); 44 | } 45 | Coroutine::sleep(0.1); 46 | } 47 | }); 48 | /* connection pool */ 49 | $pool = new ConnectionPool($constructor, 8, PDOProxy::class); 50 | /* record and show success count */ 51 | $success = 0; 52 | Coroutine::create(function () use (&$success) { 53 | while (true) { 54 | echo "Success: {$success}" . PHP_EOL; 55 | Coroutine::sleep(1); 56 | } 57 | }); 58 | /* clients */ 59 | for ($c = C; $c--;) { 60 | Coroutine::create(function () use ($pool, &$success) { 61 | /* @var $pdo PDO */ 62 | while (true) { 63 | $pdo = $pool->get(); 64 | $statement = $pdo->prepare('SELECT 1 + 1'); 65 | $ret = $statement->execute(); 66 | if ($ret !== true) { 67 | throw new RuntimeException('Execute failed'); 68 | } 69 | $ret = $statement->fetchAll(); 70 | if ($ret[0][0] !== '2') { 71 | throw new RuntimeException('Fetch failed'); 72 | } 73 | $success++; 74 | $pool->put($pdo); 75 | co::sleep(mt_rand(100, 1000) / 1000); 76 | } 77 | }); 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /examples/redis/base.php: -------------------------------------------------------------------------------- 1 | withHost(REDIS_SERVER_HOST) 27 | ->withPort(REDIS_SERVER_PORT) 28 | ->withAuth('') 29 | ->withDbIndex(0) 30 | ->withTimeout(1) 31 | ); 32 | for ($n = N; $n--;) { 33 | Coroutine::create(function () use ($pool) { 34 | $redis = $pool->get(); 35 | $result = $redis->set('foo', 'bar'); 36 | if (!$result) { 37 | throw new RuntimeException('Set failed'); 38 | } 39 | $result = $redis->get('foo'); 40 | if ($result !== 'bar') { 41 | throw new RuntimeException('Get failed'); 42 | } 43 | $pool->put($redis); 44 | }); 45 | } 46 | }); 47 | $s = microtime(true) - $s; 48 | echo 'Use ' . $s . 's for ' . (N * 2) . ' queries' . PHP_EOL; 49 | -------------------------------------------------------------------------------- /examples/service/consul.php: -------------------------------------------------------------------------------- 1 | join('test_service', '127.0.0.1', 9502); 21 | var_dump($c->resolve(SERVICE_NAME)); 22 | }); 23 | -------------------------------------------------------------------------------- /examples/service/nacos.php: -------------------------------------------------------------------------------- 1 | join(SERVICE_NAME, '127.0.0.1', 9502)); 21 | var_dump($c->join(SERVICE_NAME, '127.0.0.1', 9501)); 22 | 23 | go(function () use ($c) { 24 | while (true) { 25 | sleep(1); 26 | var_dump($c->join(SERVICE_NAME, '127.0.0.1', 9501)); 27 | } 28 | }); 29 | var_dump($c->resolve(SERVICE_NAME)); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/string/mbstring.php: -------------------------------------------------------------------------------- 1 | substr(0)); 16 | var_dump((string) $str->substr(2, 2)); 17 | var_dump($str->contains('中国')); 18 | var_dump($str->contains('美国')); 19 | var_dump($str->startsWith('我')); 20 | var_dump($str->endsWith('不是')); 21 | var_dump($str->length()); 22 | -------------------------------------------------------------------------------- /examples/thread/pool.php: -------------------------------------------------------------------------------- 1 | withAutoloader(dirname(__DIR__, 2) . '/vendor/autoload.php') 22 | ->withClassDefinitionFile(__DIR__ . '/TestThread.php') 23 | ->withArguments([uniqid(), $map]) 24 | ->start() 25 | ; 26 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 5 3 | paths: 4 | - ./src 5 | ignoreErrors: 6 | - '#Instantiated class Swoole\\Thread(\\.+)? not found\.#' 7 | - '#Access to property \$.+ on an unknown class Swoole\\Thread(\\.+)?\.#' 8 | - '#Property Swoole\\Thread(\\.+)?::\$.+ has unknown class Swoole\\Thread(\\.+)? as its type\.#' 9 | - '#Call to method .+\(\) on an unknown class Swoole\\Thread(\\.+)?\.#' 10 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | tests/unit 10 | tests/unit/Thread 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/__init__.php: -------------------------------------------------------------------------------- 1 | 'swoole', 13 | 'checkFileChange' => !getenv('SWOOLE_LIBRARY_DEV'), 14 | 'output' => getenv('SWOOLE_DIR') . '/ext-src/php_swoole_library.h', 15 | 'stripComments' => false, 16 | /* Notice: Sort by dependency */ 17 | 'files' => [ 18 | # # 19 | 'constants.php', 20 | # # 21 | 'std/exec.php', 22 | # # 23 | 'core/Constant.php', 24 | 'core/StringObject.php', 25 | 'core/MultibyteStringObject.php', 26 | 'core/Exception/ArrayKeyNotExists.php', 27 | 'core/ArrayObject.php', 28 | 'core/ObjectProxy.php', 29 | 'core/Coroutine/WaitGroup.php', 30 | 'core/Coroutine/Server.php', 31 | 'core/Coroutine/Server/Connection.php', 32 | 'core/Coroutine/Barrier.php', 33 | 'core/Coroutine/Http/ClientProxy.php', 34 | 'core/Coroutine/Http/functions.php', 35 | # # 36 | 'core/ConnectionPool.php', 37 | 'core/Database/ObjectProxy.php', 38 | 'core/Database/MysqliConfig.php', 39 | 'core/Database/MysqliException.php', 40 | 'core/Database/MysqliPool.php', 41 | 'core/Database/MysqliProxy.php', 42 | 'core/Database/MysqliStatementProxy.php', 43 | 'core/Database/DetectsLostConnections.php', 44 | 'core/Database/PDOConfig.php', 45 | 'core/Database/PDOPool.php', 46 | 'core/Database/PDOProxy.php', 47 | 'core/Database/PDOStatementProxy.php', 48 | 'core/Database/RedisConfig.php', 49 | 'core/Database/RedisPool.php', 50 | # # 51 | 'core/Http/Status.php', 52 | # # 53 | 'core/Curl/Exception.php', 54 | 'core/Curl/Handler.php', 55 | # # 56 | 'core/FastCGI.php', 57 | 'core/FastCGI/Record.php', 58 | 'core/FastCGI/Record/Params.php', 59 | 'core/FastCGI/Record/AbortRequest.php', 60 | 'core/FastCGI/Record/BeginRequest.php', 61 | 'core/FastCGI/Record/Data.php', 62 | 'core/FastCGI/Record/EndRequest.php', 63 | 'core/FastCGI/Record/GetValues.php', 64 | 'core/FastCGI/Record/GetValuesResult.php', 65 | 'core/FastCGI/Record/Stdin.php', 66 | 'core/FastCGI/Record/Stdout.php', 67 | 'core/FastCGI/Record/Stderr.php', 68 | 'core/FastCGI/Record/UnknownType.php', 69 | 'core/FastCGI/FrameParser.php', 70 | 'core/FastCGI/Message.php', 71 | 'core/FastCGI/Request.php', 72 | 'core/FastCGI/Response.php', 73 | 'core/FastCGI/HttpRequest.php', 74 | 'core/FastCGI/HttpResponse.php', 75 | 'core/Coroutine/FastCGI/Client.php', 76 | 'core/Coroutine/FastCGI/Client/Exception.php', 77 | 'core/Coroutine/FastCGI/Proxy.php', 78 | # # 79 | 'core/Process/Manager.php', 80 | # # 81 | 'core/Server/Admin.php', 82 | 'core/Server/Helper.php', 83 | # # 84 | 'core/NameResolver.php', 85 | 'core/NameResolver/Exception.php', 86 | 'core/NameResolver/Cluster.php', 87 | 'core/NameResolver/Redis.php', 88 | 'core/NameResolver/Nacos.php', 89 | 'core/NameResolver/Consul.php', 90 | # # 91 | 'core/Thread/Pool.php', 92 | 'core/Thread/Runnable.php', 93 | # # 94 | 'core/Coroutine/functions.php', 95 | # # 96 | 'ext/curl.php', 97 | 'ext/sockets.php', 98 | # # 99 | 'functions.php', 100 | 'alias.php', 101 | 'alias_ns.php', 102 | ], 103 | ]; 104 | -------------------------------------------------------------------------------- /src/alias.php: -------------------------------------------------------------------------------- 1 | pool = new Channel($this->size = $size); 32 | $this->constructor = $constructor; 33 | } 34 | 35 | public function fill(): void 36 | { 37 | while ($this->size > $this->num) { 38 | $this->make(); 39 | } 40 | } 41 | 42 | /** 43 | * Get a connection from the pool. 44 | * 45 | * @param float $timeout > 0 means waiting for the specified number of seconds. other means no waiting. 46 | * @return mixed|false Returns a connection object from the pool, or false if the pool is full and the timeout is reached. 47 | */ 48 | public function get(float $timeout = -1) 49 | { 50 | if ($this->pool === null) { 51 | throw new \RuntimeException('Pool has been closed'); 52 | } 53 | if ($this->pool->isEmpty() && $this->num < $this->size) { 54 | $this->make(); 55 | } 56 | return $this->pool->pop($timeout); 57 | } 58 | 59 | public function put($connection): void 60 | { 61 | if ($this->pool === null) { 62 | return; 63 | } 64 | if ($connection !== null) { 65 | $this->pool->push($connection); 66 | } else { 67 | /* connection broken */ 68 | $this->num -= 1; 69 | $this->make(); 70 | } 71 | } 72 | 73 | public function close(): void 74 | { 75 | $this->pool->close(); 76 | $this->pool = null; 77 | $this->num = 0; 78 | } 79 | 80 | protected function make(): void 81 | { 82 | $this->num++; 83 | try { 84 | if ($this->proxy) { 85 | $connection = new $this->proxy($this->constructor); 86 | } else { 87 | $constructor = $this->constructor; 88 | $connection = $constructor(); 89 | } 90 | } catch (\Throwable $throwable) { 91 | $this->num--; 92 | throw $throwable; 93 | } 94 | $this->put($connection); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/core/Coroutine/Barrier.php: -------------------------------------------------------------------------------- 1 | timer !== -1) { 29 | Timer::clear($this->timer); 30 | if (isset(self::$cancel_list[$this->cid])) { 31 | unset(self::$cancel_list[$this->cid]); 32 | return; 33 | } 34 | } 35 | if ($this->cid !== -1 && $this->cid !== Coroutine::getCid()) { 36 | Coroutine::resume($this->cid); 37 | } else { 38 | self::$cancel_list[$this->cid] = true; 39 | } 40 | } 41 | 42 | public static function make(): self 43 | { 44 | return new self(); 45 | } 46 | 47 | /** 48 | * @param-out null $barrier 49 | */ 50 | public static function wait(Barrier &$barrier, float $timeout = -1): void 51 | { 52 | if ($barrier->cid !== -1) { 53 | throw new Exception('The barrier is waiting, cannot wait again.'); 54 | } 55 | $cid = Coroutine::getCid(); 56 | $barrier->cid = $cid; 57 | if ($timeout > 0 && ($timeout_ms = (int) ($timeout * 1000)) > 0) { 58 | $barrier->timer = Timer::after($timeout_ms, function () use ($cid) { 59 | self::$cancel_list[$cid] = true; 60 | Coroutine::resume($cid); 61 | }); 62 | } 63 | $barrier = null; 64 | if (!isset(self::$cancel_list[$cid])) { 65 | Coroutine::yield(); 66 | } else { 67 | unset(self::$cancel_list[$cid]); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/core/Coroutine/FastCGI/Client/Exception.php: -------------------------------------------------------------------------------- 1 | headers = $headers ?? []; 23 | $this->cookies = $cookies ?? []; 24 | } 25 | 26 | public function getBody(): string 27 | { 28 | return $this->body; 29 | } 30 | 31 | public function getStatusCode(): int 32 | { 33 | return $this->statusCode; 34 | } 35 | 36 | public function getHeaders(): array 37 | { 38 | return $this->headers; 39 | } 40 | 41 | public function getCookies(): array 42 | { 43 | return $this->cookies; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/core/Coroutine/Server/Connection.php: -------------------------------------------------------------------------------- 1 | socket = $conn; 23 | } 24 | 25 | public function recv(float $timeout = 0) 26 | { 27 | return $this->socket->recvPacket($timeout); 28 | } 29 | 30 | public function send(string $data) 31 | { 32 | return $this->socket->sendAll($data); 33 | } 34 | 35 | public function close(): bool 36 | { 37 | return $this->socket->close(); 38 | } 39 | 40 | public function exportSocket(): Socket 41 | { 42 | return $this->socket; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/Coroutine/WaitGroup.php: -------------------------------------------------------------------------------- 1 | chan = new Channel(1); 25 | if ($delta > 0) { 26 | $this->add($delta); 27 | } 28 | } 29 | 30 | public function add(int $delta = 1): void 31 | { 32 | if ($this->waiting) { 33 | throw new \BadMethodCallException('WaitGroup misuse: add called concurrently with wait'); 34 | } 35 | $count = $this->count + $delta; 36 | if ($count < 0) { 37 | throw new \InvalidArgumentException('WaitGroup misuse: negative counter'); 38 | } 39 | $this->count = $count; 40 | } 41 | 42 | public function done(): void 43 | { 44 | $count = $this->count - 1; 45 | if ($count < 0) { 46 | throw new \BadMethodCallException('WaitGroup misuse: negative counter'); 47 | } 48 | $this->count = $count; 49 | if ($count === 0 && $this->waiting) { 50 | $this->chan->push(true); 51 | } 52 | } 53 | 54 | public function wait(float $timeout = -1): bool 55 | { 56 | if ($this->waiting) { 57 | throw new \BadMethodCallException('WaitGroup misuse: reused before previous wait has returned'); 58 | } 59 | if ($this->count > 0) { 60 | $this->waiting = true; 61 | $done = $this->chan->pop($timeout); 62 | $this->waiting = false; 63 | return $done; 64 | } 65 | return true; 66 | } 67 | 68 | public function count(): int 69 | { 70 | return $this->count; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/core/Coroutine/functions.php: -------------------------------------------------------------------------------- 1 | set(['hook_flags' => SWOOLE_HOOK_ALL]); 22 | } 23 | $s->add($fn, ...$args); 24 | return $s->start(); 25 | } 26 | 27 | function go(callable $fn, ...$args) 28 | { 29 | return Coroutine::create($fn, ...$args); 30 | } 31 | 32 | function defer(callable $fn) 33 | { 34 | Coroutine::defer($fn); 35 | } 36 | 37 | function batch(array $tasks, float $timeout = -1): array 38 | { 39 | $wg = new WaitGroup(count($tasks)); 40 | foreach ($tasks as $id => $task) { 41 | Coroutine::create(function () use ($wg, &$tasks, $id, $task) { 42 | $tasks[$id] = null; 43 | $tasks[$id] = $task(); 44 | $wg->done(); 45 | }); 46 | } 47 | $wg->wait($timeout); 48 | return $tasks; 49 | } 50 | 51 | function parallel(int $n, callable $fn): void 52 | { 53 | $count = $n; 54 | $wg = new WaitGroup($n); 55 | while ($count--) { 56 | Coroutine::create(function () use ($fn, $wg) { 57 | $fn(); 58 | $wg->done(); 59 | }); 60 | } 61 | $wg->wait(); 62 | } 63 | 64 | /** 65 | * Applies the callback to the elements of the given list. 66 | * 67 | * The callback function takes on two parameters. The list parameter's value being the first, and the key/index second. 68 | * Each callback runs in a new coroutine, allowing the list to be processed in parallel. 69 | * 70 | * @param array $list A list of key/value paired input data. 71 | * @param callable $fn The callback function to apply to each item on the list. The callback takes on two parameters. 72 | * The list parameter's value being the first, and the key/index second. 73 | * @param float $timeout > 0 means waiting for the specified number of seconds. other means no waiting. 74 | * @return array Returns an array containing the results of applying the callback function to the corresponding value 75 | * and key of the list (used as arguments for the callback). The returned array will preserve the keys of 76 | * the list. 77 | */ 78 | function map(array $list, callable $fn, float $timeout = -1): array 79 | { 80 | $wg = new WaitGroup(count($list)); 81 | foreach ($list as $id => $elem) { 82 | Coroutine::create(function () use ($wg, &$list, $id, $elem, $fn): void { 83 | $list[$id] = null; 84 | $list[$id] = $fn($elem, $id); 85 | $wg->done(); 86 | }); 87 | } 88 | $wg->wait($timeout); 89 | return $list; 90 | } 91 | 92 | function deadlock_check() 93 | { 94 | $all_coroutines = Coroutine::listCoroutines(); 95 | $count = Coroutine::stats()['coroutine_num']; 96 | echo "\n===================================================================", 97 | "\n [FATAL ERROR]: all coroutines (count: {$count}) are asleep - deadlock!", 98 | "\n===================================================================\n"; 99 | 100 | $options = Coroutine::getOptions(); 101 | if (empty($options['deadlock_check_disable_trace'])) { 102 | $index = 0; 103 | $limit = empty($options['deadlock_check_limit']) ? 32 : intval($options['deadlock_check_limit']); 104 | $depth = empty($options['deadlock_check_depth']) ? 32 : intval($options['deadlock_check_depth']); 105 | foreach ($all_coroutines as $cid) { 106 | echo "\n [Coroutine-{$cid}]"; 107 | echo "\n--------------------------------------------------------------------\n"; 108 | echo Coroutine::printBackTrace($cid, DEBUG_BACKTRACE_IGNORE_ARGS, $depth); 109 | echo "\n"; 110 | $index++; 111 | // limit the number of maximum outputs 112 | if ($index >= $limit) { 113 | break; 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/core/Curl/Exception.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | private const ERROR_MESSAGES = [ 20 | 'server has gone away', 21 | 'no connection to the server', 22 | 'Lost connection', 23 | 'is dead or not enabled', 24 | 'Error while sending', 25 | 'decryption failed or bad record mac', 26 | 'server closed the connection unexpectedly', 27 | 'SSL connection has been closed unexpectedly', 28 | 'Error writing data to the connection', 29 | 'Resource deadlock avoided', 30 | 'Transaction() on null', 31 | 'child connection forced to terminate due to client_idle_limit', 32 | 'query_wait_timeout', 33 | 'reset by peer', 34 | 'Physical connection is not usable', 35 | 'TCP Provider: Error code 0x68', 36 | 'ORA-03113', 37 | 'ORA-03114', 38 | 'Packets out of order. Expected', 39 | 'Adaptive Server connection failed', 40 | 'Communication link failure', 41 | 'connection is no longer usable', 42 | 'Login timeout expired', 43 | 'SQLSTATE[HY000] [2002] Connection refused', 44 | 'running with the --read-only option so it cannot execute this statement', 45 | 'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.', 46 | 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again', 47 | 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known', 48 | 'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for', 49 | 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected', 50 | 'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.', 51 | 'Temporary failure in name resolution', 52 | 'SQLSTATE[08S01]: Communication link failure', 53 | 'SQLSTATE[08006] [7] could not connect to server: Connection refused Is the server running on host', 54 | 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: No route to host', 55 | 'The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior.', 56 | 'SQLSTATE[08006] [7] could not translate host name', 57 | 'TCP Provider: Error code 0x274C', 58 | 'SQLSTATE[HY000] [2002] No such file or directory', 59 | 'Reason: Server is in script upgrade mode. Only administrator can connect at this time.', 60 | 'Unknown $curl_error_code: 77', 61 | 'SQLSTATE[08006] [7] SSL error: sslv3 alert unexpected message', 62 | 'SQLSTATE[08006] [7] unrecognized SSL error code:', 63 | 'SQLSTATE[HY000] [2002] No connection could be made because the target machine actively refused it', 64 | 'Broken pipe', 65 | // PDO::prepare(): Send of 77 bytes failed with errno=110 Operation timed out 66 | // SSL: Handshake timed out 67 | // SSL: Operation timed out 68 | // SSL: Connection timed out 69 | // SQLSTATE[HY000] [2002] Connection timed out 70 | 'timed out', 71 | 'Error reading result', 72 | ]; 73 | 74 | public static function causedByLostConnection(\Throwable $e): bool 75 | { 76 | $message = $e->getMessage(); 77 | foreach (self::ERROR_MESSAGES as $needle) { 78 | if (mb_strpos($message, $needle) !== false) { 79 | return true; 80 | } 81 | } 82 | 83 | return false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/core/Database/MysqliConfig.php: -------------------------------------------------------------------------------- 1 | host; 35 | } 36 | 37 | public function withHost(string $host): self 38 | { 39 | $this->host = $host; 40 | return $this; 41 | } 42 | 43 | public function getPort(): int 44 | { 45 | return $this->port; 46 | } 47 | 48 | public function getUnixSocket(): ?string 49 | { 50 | return $this->unixSocket ?? null; 51 | } 52 | 53 | public function withUnixSocket(?string $unixSocket): self 54 | { 55 | $this->unixSocket = $unixSocket; 56 | return $this; 57 | } 58 | 59 | public function withPort(int $port): self 60 | { 61 | $this->port = $port; 62 | return $this; 63 | } 64 | 65 | public function getDbname(): string 66 | { 67 | return $this->dbname; 68 | } 69 | 70 | public function withDbname(string $dbname): self 71 | { 72 | $this->dbname = $dbname; 73 | return $this; 74 | } 75 | 76 | public function getCharset(): string 77 | { 78 | return $this->charset; 79 | } 80 | 81 | public function withCharset(string $charset): self 82 | { 83 | $this->charset = $charset; 84 | return $this; 85 | } 86 | 87 | public function getUsername(): string 88 | { 89 | return $this->username; 90 | } 91 | 92 | public function withUsername(string $username): self 93 | { 94 | $this->username = $username; 95 | return $this; 96 | } 97 | 98 | public function getPassword(): string 99 | { 100 | return $this->password; 101 | } 102 | 103 | public function withPassword(string $password): self 104 | { 105 | $this->password = $password; 106 | return $this; 107 | } 108 | 109 | public function getOptions(): array 110 | { 111 | return $this->options; 112 | } 113 | 114 | public function withOptions(array $options): self 115 | { 116 | $this->options = $options; 117 | return $this; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/core/Database/MysqliException.php: -------------------------------------------------------------------------------- 1 | config->getOptions() as $option => $value) { 28 | $mysqli->set_opt($option, $value); 29 | } 30 | $mysqli->real_connect( 31 | $this->config->getHost(), 32 | $this->config->getUsername(), 33 | $this->config->getPassword(), 34 | $this->config->getDbname(), 35 | $this->config->getPort(), 36 | $this->config->getUnixSocket() 37 | ); 38 | if ($mysqli->connect_errno) { 39 | throw new MysqliException($mysqli->connect_error, $mysqli->connect_errno); 40 | } 41 | $mysqli->set_charset($this->config->getCharset()); 42 | return $mysqli; 43 | }, $size, MysqliProxy::class); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/core/Database/MysqliProxy.php: -------------------------------------------------------------------------------- 1 | constructor = $constructor; 45 | } 46 | 47 | public function __call(string $name, array $arguments) 48 | { 49 | for ($n = 3; $n--;) { 50 | $ret = @$this->__object->{$name}(...$arguments); 51 | if ($ret === false) { 52 | /* non-IO method */ 53 | if (!preg_match(static::IO_METHOD_REGEX, $name)) { 54 | break; 55 | } 56 | /* no more chances or non-IO failures */ 57 | if (!in_array($this->__object->errno, static::IO_ERRORS, true) || ($n === 0)) { 58 | throw new MysqliException($this->__object->error, $this->__object->errno); 59 | } 60 | $this->reconnect(); 61 | continue; 62 | } 63 | if (strcasecmp($name, 'prepare') === 0) { 64 | $ret = new MysqliStatementProxy($ret, $arguments[0], $this); 65 | } elseif (strcasecmp($name, 'stmt_init') === 0) { 66 | $ret = new MysqliStatementProxy($ret, null, $this); 67 | } 68 | break; 69 | } 70 | /* @noinspection PhpUndefinedVariableInspection */ 71 | return $ret; 72 | } 73 | 74 | public function getRound(): int 75 | { 76 | return $this->round; 77 | } 78 | 79 | public function reconnect(): void 80 | { 81 | $constructor = $this->constructor; 82 | parent::__construct($constructor()); 83 | $this->round++; 84 | /* restore context */ 85 | if (!empty($this->charsetContext)) { 86 | $this->__object->set_charset($this->charsetContext); 87 | } 88 | foreach ($this->setOptContext as $opt => $val) { 89 | $this->__object->set_opt($opt, $val); 90 | } 91 | if (!empty($this->changeUserContext)) { 92 | $this->__object->change_user(...$this->changeUserContext); 93 | } 94 | } 95 | 96 | public function options(int $option, $value): bool 97 | { 98 | $this->setOptContext[$option] = $value; 99 | return $this->__object->options($option, $value); 100 | } 101 | 102 | public function set_opt(int $option, $value): bool 103 | { 104 | return $this->options($option, $value); 105 | } 106 | 107 | public function set_charset(string $charset): bool 108 | { 109 | $this->charsetContext = $charset; 110 | return $this->__object->set_charset($charset); 111 | } 112 | 113 | public function change_user(string $user, string $password, ?string $database): bool 114 | { 115 | $this->changeUserContext = [$user, $password, $database]; 116 | return $this->__object->change_user($user, $password, $database); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/core/Database/MysqliStatementProxy.php: -------------------------------------------------------------------------------- 1 | queryString = $queryString; 37 | $this->parent = $parent; 38 | $this->parentRound = $parent->getRound(); 39 | } 40 | 41 | public function __call(string $name, array $arguments) 42 | { 43 | for ($n = 3; $n--;) { 44 | $ret = @$this->__object->{$name}(...$arguments); 45 | if ($ret === false) { 46 | /* non-IO method */ 47 | if (!preg_match(static::IO_METHOD_REGEX, $name)) { 48 | break; 49 | } 50 | /* no more chances or non-IO failures or in transaction */ 51 | if (!in_array($this->__object->errno, $this->parent::IO_ERRORS, true) || ($n === 0)) { 52 | throw new MysqliException($this->__object->error, $this->__object->errno); 53 | } 54 | if ($this->parent->getRound() === $this->parentRound) { 55 | /* if not equal, parent has reconnected */ 56 | $this->parent->reconnect(); 57 | } 58 | $parent = $this->parent->__getObject(); 59 | $this->__object = $this->queryString ? @$parent->prepare($this->queryString) : @$parent->stmt_init(); 60 | if ($this->__object === false) { 61 | throw new MysqliException($parent->error, $parent->errno); 62 | } 63 | if (!empty($this->bindParamContext)) { 64 | $this->__object->bind_param($this->bindParamContext[0], ...$this->bindParamContext[1]); 65 | } 66 | if (!empty($this->bindResultContext)) { 67 | $this->__object->bind_result($this->bindResultContext); 68 | } 69 | foreach ($this->attrSetContext as $attr => $value) { 70 | $this->__object->attr_set($attr, $value); 71 | } 72 | continue; 73 | } 74 | if (strcasecmp($name, 'prepare') === 0) { 75 | $this->queryString = $arguments[0]; 76 | } 77 | break; 78 | } 79 | /* @noinspection PhpUndefinedVariableInspection */ 80 | return $ret; 81 | } 82 | 83 | public function attr_set($attr, $mode): bool 84 | { 85 | $this->attrSetContext[$attr] = $mode; 86 | return $this->__object->attr_set($attr, $mode); 87 | } 88 | 89 | public function bind_param($types, &...$arguments): bool 90 | { 91 | $this->bindParamContext = [$types, $arguments]; 92 | return $this->__object->bind_param($types, ...$arguments); 93 | } 94 | 95 | public function bind_result(&...$arguments): bool 96 | { 97 | $this->bindResultContext = $arguments; 98 | return $this->__object->bind_result(...$arguments); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/core/Database/ObjectProxy.php: -------------------------------------------------------------------------------- 1 | driver; 39 | } 40 | 41 | public function withDriver(string $driver): self 42 | { 43 | $this->driver = $driver; 44 | return $this; 45 | } 46 | 47 | public function getHost(): string 48 | { 49 | return $this->host; 50 | } 51 | 52 | public function withHost(string $host): self 53 | { 54 | $this->host = $host; 55 | return $this; 56 | } 57 | 58 | public function getPort(): int 59 | { 60 | return $this->port; 61 | } 62 | 63 | public function hasUnixSocket(): bool 64 | { 65 | return !empty($this->unixSocket); 66 | } 67 | 68 | public function getUnixSocket(): ?string 69 | { 70 | return $this->unixSocket ?? null; 71 | } 72 | 73 | public function withUnixSocket(?string $unixSocket): self 74 | { 75 | $this->unixSocket = $unixSocket; 76 | return $this; 77 | } 78 | 79 | public function withPort(int $port): self 80 | { 81 | $this->port = $port; 82 | return $this; 83 | } 84 | 85 | public function getDbname(): string 86 | { 87 | return $this->dbname; 88 | } 89 | 90 | public function withDbname(string $dbname): self 91 | { 92 | $this->dbname = $dbname; 93 | return $this; 94 | } 95 | 96 | public function getCharset(): string 97 | { 98 | return $this->charset; 99 | } 100 | 101 | public function withCharset(string $charset): self 102 | { 103 | $this->charset = $charset; 104 | return $this; 105 | } 106 | 107 | public function getUsername(): string 108 | { 109 | return $this->username; 110 | } 111 | 112 | public function withUsername(string $username): self 113 | { 114 | $this->username = $username; 115 | return $this; 116 | } 117 | 118 | public function getPassword(): string 119 | { 120 | return $this->password; 121 | } 122 | 123 | public function withPassword(string $password): self 124 | { 125 | $this->password = $password; 126 | return $this; 127 | } 128 | 129 | public function getOptions(): array 130 | { 131 | return $this->options; 132 | } 133 | 134 | public function withOptions(array $options): self 135 | { 136 | $this->options = $options; 137 | return $this; 138 | } 139 | 140 | /** 141 | * Returns the list of available drivers 142 | * 143 | * @return string[] 144 | */ 145 | public static function getAvailableDrivers(): array 146 | { 147 | return [ 148 | self::DRIVER_MYSQL, 149 | ]; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/core/Database/PDOPool.php: -------------------------------------------------------------------------------- 1 | config->getDriver(); 26 | if ($driver === 'sqlite') { 27 | return new \PDO($this->createDSN('sqlite')); 28 | } 29 | 30 | return new \PDO($this->createDSN($driver), $this->config->getUsername(), $this->config->getPassword(), $this->config->getOptions()); 31 | }, $size, PDOProxy::class); 32 | } 33 | 34 | /** 35 | * Get a PDO connection from the pool. The PDO connection (a PDO object) is wrapped in a PDOProxy object returned. 36 | * 37 | * @param float $timeout > 0 means waiting for the specified number of seconds. other means no waiting. 38 | * @return PDOProxy|false Returns a PDOProxy object from the pool, or false if the pool is full and the timeout is reached. 39 | * {@inheritDoc} 40 | */ 41 | public function get(float $timeout = -1) 42 | { 43 | /* @var \Swoole\Database\PDOProxy|false $pdo */ 44 | $pdo = parent::get($timeout); 45 | if ($pdo === false) { 46 | return false; 47 | } 48 | 49 | $pdo->reset(); 50 | 51 | return $pdo; 52 | } 53 | 54 | /** 55 | * @purpose create DSN 56 | * @throws \Exception 57 | */ 58 | private function createDSN(string $driver): string 59 | { 60 | switch ($driver) { 61 | case 'mysql': 62 | if ($this->config->hasUnixSocket()) { 63 | $dsn = "mysql:unix_socket={$this->config->getUnixSocket()};dbname={$this->config->getDbname()};charset={$this->config->getCharset()}"; 64 | } else { 65 | $dsn = "mysql:host={$this->config->getHost()};port={$this->config->getPort()};dbname={$this->config->getDbname()};charset={$this->config->getCharset()}"; 66 | } 67 | break; 68 | case 'pgsql': 69 | $dsn = 'pgsql:host=' . ($this->config->hasUnixSocket() ? $this->config->getUnixSocket() : $this->config->getHost()) . ";port={$this->config->getPort()};dbname={$this->config->getDbname()}"; 70 | break; 71 | case 'oci': 72 | $dsn = 'oci:dbname=' . ($this->config->hasUnixSocket() ? $this->config->getUnixSocket() : $this->config->getHost()) . ':' . $this->config->getPort() . '/' . $this->config->getDbname() . ';charset=' . $this->config->getCharset(); 73 | break; 74 | case 'sqlite': 75 | // There are three types of SQLite databases: databases on disk, databases in memory, and temporary 76 | // databases (which are deleted when the connections are closed). It doesn't make sense to use 77 | // connection pool for the latter two types of databases, because each connection connects to a 78 | //different in-memory or temporary SQLite database. 79 | if ($this->config->getDbname() === '') { 80 | throw new \Exception('Connection pool in Swoole does not support temporary SQLite databases.'); 81 | } 82 | if ($this->config->getDbname() === ':memory:') { 83 | throw new \Exception('Connection pool in Swoole does not support creating SQLite databases in memory.'); 84 | } 85 | $dsn = 'sqlite:' . $this->config->getDbname(); 86 | break; 87 | default: 88 | throw new \Exception('Unsupported Database Driver:' . $driver); 89 | } 90 | return $dsn; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/core/Database/PDOProxy.php: -------------------------------------------------------------------------------- 1 | __object->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); 35 | $this->constructor = $constructor; 36 | } 37 | 38 | public function __call(string $name, array $arguments) 39 | { 40 | try { 41 | $ret = $this->__object->{$name}(...$arguments); 42 | } catch (\PDOException $e) { 43 | if (!$this->__object->inTransaction() && DetectsLostConnections::causedByLostConnection($e)) { 44 | $this->reconnect(); 45 | $ret = $this->__object->{$name}(...$arguments); 46 | } else { 47 | throw $e; 48 | } 49 | } 50 | 51 | if (strcasecmp($name, 'beginTransaction') === 0) { 52 | $this->inTransaction++; 53 | } 54 | 55 | if ((strcasecmp($name, 'commit') === 0 || strcasecmp($name, 'rollback') === 0) && $this->inTransaction > 0) { 56 | $this->inTransaction--; 57 | } 58 | 59 | if ((strcasecmp($name, 'prepare') === 0) || (strcasecmp($name, 'query') === 0)) { 60 | $ret = new PDOStatementProxy($ret, $this); 61 | } 62 | 63 | return $ret; 64 | } 65 | 66 | public function getRound(): int 67 | { 68 | return $this->round; 69 | } 70 | 71 | public function reconnect(): void 72 | { 73 | $constructor = $this->constructor; 74 | parent::__construct($constructor()); 75 | $this->__object->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); 76 | $this->round++; 77 | /* restore context */ 78 | foreach ($this->setAttributeContext as $attribute => $value) { 79 | $this->__object->setAttribute($attribute, $value); 80 | } 81 | } 82 | 83 | public function setAttribute(int $attribute, $value): bool 84 | { 85 | $this->setAttributeContext[$attribute] = $value; 86 | return $this->__object->setAttribute($attribute, $value); 87 | } 88 | 89 | public function inTransaction(): bool 90 | { 91 | return $this->inTransaction > 0; 92 | } 93 | 94 | public function reset(): void 95 | { 96 | $this->inTransaction = 0; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/core/Database/PDOStatementProxy.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 43 | $this->parentRound = $parent->getRound(); 44 | } 45 | 46 | public function __call(string $name, array $arguments) 47 | { 48 | try { 49 | $ret = $this->__object->{$name}(...$arguments); 50 | } catch (\PDOException $e) { 51 | if (!$this->parent->inTransaction() && DetectsLostConnections::causedByLostConnection($e)) { 52 | if ($this->parent->getRound() === $this->parentRound) { 53 | /* if not equal, parent has reconnected */ 54 | $this->parent->reconnect(); 55 | } 56 | $parent = $this->parent->__getObject(); 57 | $this->__object = $parent->prepare($this->__object->queryString); 58 | 59 | foreach ($this->setAttributeContext as $attribute => $value) { 60 | $this->__object->setAttribute($attribute, $value); 61 | } 62 | if (!empty($this->setFetchModeContext)) { 63 | $this->__object->setFetchMode(...$this->setFetchModeContext); 64 | } 65 | foreach ($this->bindParamContext as $param => $item) { 66 | $this->__object->bindParam($param, ...$item); 67 | } 68 | foreach ($this->bindColumnContext as $column => $item) { 69 | $this->__object->bindColumn($column, ...$item); 70 | } 71 | foreach ($this->bindValueContext as $value => $item) { 72 | $this->__object->bindParam($value, ...$item); 73 | } 74 | $ret = $this->__object->{$name}(...$arguments); 75 | } else { 76 | throw $e; 77 | } 78 | } 79 | 80 | return $ret; 81 | } 82 | 83 | public function setAttribute(int $attribute, $value): bool 84 | { 85 | $this->setAttributeContext[$attribute] = $value; 86 | return $this->__object->setAttribute($attribute, $value); 87 | } 88 | 89 | /** 90 | * Set the default fetch mode for this statement. 91 | * 92 | * @see https://www.php.net/manual/en/pdostatement.setfetchmode.php 93 | */ 94 | public function setFetchMode(int $mode, ...$params): bool 95 | { 96 | $this->setFetchModeContext = func_get_args(); 97 | return $this->__object->setFetchMode(...$this->setFetchModeContext); 98 | } 99 | 100 | public function bindParam($parameter, &$variable, $data_type = \PDO::PARAM_STR, $length = 0, $driver_options = null): bool 101 | { 102 | $this->bindParamContext[$parameter] = [$variable, $data_type, $length, $driver_options]; 103 | return $this->__object->bindParam($parameter, $variable, $data_type, $length, $driver_options); 104 | } 105 | 106 | public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null): bool 107 | { 108 | $this->bindColumnContext[$column] = [$param, $type, $maxlen, $driverdata]; 109 | return $this->__object->bindColumn($column, $param, $type, $maxlen, $driverdata); 110 | } 111 | 112 | public function bindValue($parameter, $value, $data_type = \PDO::PARAM_STR): bool 113 | { 114 | $this->bindValueContext[$parameter] = [$value, $data_type]; 115 | return $this->__object->bindValue($parameter, $value, $data_type); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/core/Database/RedisConfig.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | protected array $options = []; 36 | 37 | public function getHost(): string 38 | { 39 | return $this->host; 40 | } 41 | 42 | public function withHost(string $host): self 43 | { 44 | $this->host = $host; 45 | return $this; 46 | } 47 | 48 | public function getPort(): int 49 | { 50 | return $this->port; 51 | } 52 | 53 | public function withPort(int $port): self 54 | { 55 | $this->port = $port; 56 | return $this; 57 | } 58 | 59 | public function getTimeout(): float 60 | { 61 | return $this->timeout; 62 | } 63 | 64 | public function withTimeout(float $timeout): self 65 | { 66 | $this->timeout = $timeout; 67 | return $this; 68 | } 69 | 70 | public function getReserved(): string 71 | { 72 | return $this->reserved; 73 | } 74 | 75 | public function withReserved(string $reserved): self 76 | { 77 | $this->reserved = $reserved; 78 | return $this; 79 | } 80 | 81 | public function getRetryInterval(): int 82 | { 83 | return $this->retry_interval; 84 | } 85 | 86 | public function withRetryInterval(int $retry_interval): self 87 | { 88 | $this->retry_interval = $retry_interval; 89 | return $this; 90 | } 91 | 92 | public function getReadTimeout(): float 93 | { 94 | return $this->read_timeout; 95 | } 96 | 97 | public function withReadTimeout(float $read_timeout): self 98 | { 99 | $this->read_timeout = $read_timeout; 100 | return $this; 101 | } 102 | 103 | public function getAuth(): string 104 | { 105 | return $this->auth; 106 | } 107 | 108 | public function withAuth(string $auth): self 109 | { 110 | $this->auth = $auth; 111 | return $this; 112 | } 113 | 114 | public function getDbIndex(): int 115 | { 116 | return $this->dbIndex; 117 | } 118 | 119 | public function withDbIndex(int $dbIndex): self 120 | { 121 | $this->dbIndex = $dbIndex; 122 | return $this; 123 | } 124 | 125 | /** 126 | * Add a configurable option. 127 | */ 128 | public function withOption(int $option, mixed $value): self 129 | { 130 | $this->options[$option] = $value; 131 | return $this; 132 | } 133 | 134 | /** 135 | * Add/override configurable options. 136 | * 137 | * @param array $options 138 | */ 139 | public function setOptions(array $options): self 140 | { 141 | $this->options = $options; 142 | return $this; 143 | } 144 | 145 | /** 146 | * Get configurable options. 147 | * 148 | * @return array 149 | */ 150 | public function getOptions(): array 151 | { 152 | return $this->options; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/core/Database/RedisPool.php: -------------------------------------------------------------------------------- 1 | config->getHost(), 30 | $this->config->getPort(), 31 | ]; 32 | if ($this->config->getTimeout() !== 0.0) { 33 | $arguments[] = $this->config->getTimeout(); 34 | } 35 | if ($this->config->getRetryInterval() !== 0) { 36 | /* reserved should always be NULL */ 37 | $arguments[] = null; 38 | $arguments[] = $this->config->getRetryInterval(); 39 | } 40 | if ($this->config->getReadTimeout() !== 0.0) { 41 | $arguments[] = $this->config->getReadTimeout(); 42 | } 43 | $redis->connect(...$arguments); 44 | if ($this->config->getAuth()) { 45 | $redis->auth($this->config->getAuth()); 46 | } 47 | if ($this->config->getDbIndex() !== 0) { 48 | $redis->select($this->config->getDbIndex()); 49 | } 50 | 51 | /* Set Redis options. */ 52 | foreach ($this->config->getOptions() as $key => $value) { 53 | $redis->setOption($key, $value); 54 | } 55 | 56 | return $redis; 57 | }, $size); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/core/Exception/ArrayKeyNotExists.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | protected static array $classMapping = [ 38 | FastCGI::BEGIN_REQUEST => BeginRequest::class, 39 | FastCGI::ABORT_REQUEST => AbortRequest::class, 40 | FastCGI::END_REQUEST => EndRequest::class, 41 | FastCGI::PARAMS => Params::class, 42 | FastCGI::STDIN => Stdin::class, 43 | FastCGI::STDOUT => Stdout::class, 44 | FastCGI::STDERR => Stderr::class, 45 | FastCGI::DATA => Data::class, 46 | FastCGI::GET_VALUES => GetValues::class, 47 | FastCGI::GET_VALUES_RESULT => GetValuesResult::class, 48 | FastCGI::UNKNOWN_TYPE => UnknownType::class, 49 | ]; 50 | 51 | /** 52 | * Checks if the buffer contains a valid frame to parse 53 | */ 54 | public static function hasFrame(string $binaryBuffer): bool 55 | { 56 | $bufferLength = strlen($binaryBuffer); 57 | if ($bufferLength < FastCGI::HEADER_LEN) { 58 | return false; 59 | } 60 | 61 | /** @phpstan-var false|array{version: int, type: int, requestId: int, contentLength: int, paddingLength: int} */ 62 | $fastInfo = unpack(FastCGI::HEADER_FORMAT, $binaryBuffer); 63 | if ($fastInfo === false) { 64 | throw new \RuntimeException('Can not unpack data from the binary buffer'); 65 | } 66 | if ($bufferLength < FastCGI::HEADER_LEN + $fastInfo['contentLength'] + $fastInfo['paddingLength']) { 67 | return false; 68 | } 69 | 70 | return true; 71 | } 72 | 73 | /** 74 | * Parses a frame from the binary buffer 75 | * 76 | * @return Record One of the corresponding FastCGI record 77 | */ 78 | public static function parseFrame(string &$binaryBuffer): Record 79 | { 80 | $bufferLength = strlen($binaryBuffer); 81 | if ($bufferLength < FastCGI::HEADER_LEN) { 82 | throw new \RuntimeException('Not enough data in the buffer to parse'); 83 | } 84 | /** @phpstan-var false|array{version: int, type: int, requestId: int, contentLength: int, paddingLength: int} */ 85 | $recordHeader = unpack(FastCGI::HEADER_FORMAT, $binaryBuffer); 86 | if ($recordHeader === false) { 87 | throw new \RuntimeException('Can not unpack data from the binary buffer'); 88 | } 89 | $recordType = $recordHeader['type']; 90 | if (!isset(self::$classMapping[$recordType])) { 91 | throw new \DomainException("Invalid FastCGI record type {$recordType} received"); 92 | } 93 | 94 | /** @var Record $className */ 95 | $className = self::$classMapping[$recordType]; 96 | $record = $className::unpack($binaryBuffer); 97 | 98 | $offset = FastCGI::HEADER_LEN + $record->getContentLength() + $record->getPaddingLength(); 99 | $binaryBuffer = substr($binaryBuffer, $offset); 100 | 101 | return $record; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/core/FastCGI/Message.php: -------------------------------------------------------------------------------- 1 | params[$name] ?? null; 25 | } 26 | 27 | public function withParam(string $name, string $value): static 28 | { 29 | $this->params[$name] = $value; 30 | return $this; 31 | } 32 | 33 | public function withoutParam(string $name): static 34 | { 35 | unset($this->params[$name]); 36 | return $this; 37 | } 38 | 39 | public function getParams(): array 40 | { 41 | return $this->params; 42 | } 43 | 44 | public function withParams(array $params): static 45 | { 46 | $this->params = $params; 47 | return $this; 48 | } 49 | 50 | public function withAddedParams(array $params): static 51 | { 52 | $this->params = $params + $this->params; 53 | return $this; 54 | } 55 | 56 | public function getBody(): string 57 | { 58 | return $this->body; 59 | } 60 | 61 | public function withBody(string|\Stringable $body): self 62 | { 63 | $this->body = (string) $body; 64 | return $this; 65 | } 66 | 67 | public function getError(): string 68 | { 69 | return $this->error; 70 | } 71 | 72 | public function withError(string $error): static 73 | { 74 | $this->error = $error; 75 | return $this; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/core/FastCGI/Record/AbortRequest.php: -------------------------------------------------------------------------------- 1 | type = FastCGI::ABORT_REQUEST; 25 | $this->setRequestId($requestId); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/core/FastCGI/Record/BeginRequest.php: -------------------------------------------------------------------------------- 1 | type = FastCGI::BEGIN_REQUEST; 49 | $this->role = $role; 50 | $this->flags = $flags; 51 | $this->reserved1 = $reserved; 52 | $this->setContentData($this->packPayload()); 53 | } 54 | 55 | /** 56 | * Returns the role 57 | * 58 | * The role component sets the role the Web server expects the application to play. 59 | * The currently-defined roles are: 60 | * FCGI_RESPONDER 61 | * FCGI_AUTHORIZER 62 | * FCGI_FILTER 63 | */ 64 | public function getRole(): int 65 | { 66 | return $this->role; 67 | } 68 | 69 | /** 70 | * Returns the flags 71 | * 72 | * The flags component contains a bit that controls connection shutdown. 73 | * 74 | * flags & FCGI_KEEP_CONN: 75 | * If zero, the application closes the connection after responding to this request. 76 | * If not zero, the application does not close the connection after responding to this request; 77 | * the Web server retains responsibility for the connection. 78 | */ 79 | public function getFlags(): int 80 | { 81 | return $this->flags; 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | * @param static $self 87 | */ 88 | protected static function unpackPayload(Record $self, string $binaryData): void 89 | { 90 | assert($self instanceof self); // @phpstan-ignore function.alreadyNarrowedType,instanceof.alwaysTrue 91 | 92 | /** @phpstan-var false|array{role: int, flags: int, reserved: string} */ 93 | $payload = unpack('nrole/Cflags/a5reserved', $binaryData); 94 | if ($payload === false) { 95 | throw new \RuntimeException('Can not unpack data from the binary buffer'); 96 | } 97 | [ 98 | $self->role, 99 | $self->flags, 100 | $self->reserved1, 101 | ] = array_values($payload); 102 | } 103 | 104 | /** {@inheritdoc} */ 105 | protected function packPayload(): string 106 | { 107 | return pack( 108 | 'nCa5', 109 | $this->role, 110 | $this->flags, 111 | $this->reserved1 112 | ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/core/FastCGI/Record/Data.php: -------------------------------------------------------------------------------- 1 | type = FastCGI::DATA; 27 | $this->setContentData($contentData); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/core/FastCGI/Record/EndRequest.php: -------------------------------------------------------------------------------- 1 | type = FastCGI::END_REQUEST; 51 | $this->protocolStatus = $protocolStatus; 52 | $this->appStatus = $appStatus; 53 | $this->reserved1 = $reserved; 54 | $this->setContentData($this->packPayload()); 55 | } 56 | 57 | /** 58 | * Returns app status 59 | * 60 | * The appStatus component is an application-level status code. Each role documents its usage of appStatus. 61 | */ 62 | public function getAppStatus(): int 63 | { 64 | return $this->appStatus; 65 | } 66 | 67 | /** 68 | * Returns the protocol status 69 | * 70 | * The possible protocolStatus values are: 71 | * FCGI_REQUEST_COMPLETE: normal end of request. 72 | * FCGI_CANT_MPX_CONN: rejecting a new request. 73 | * This happens when a Web server sends concurrent requests over one connection to an application that is 74 | * designed to process one request at a time per connection. 75 | * FCGI_OVERLOADED: rejecting a new request. 76 | * This happens when the application runs out of some resource, e.g. database connections. 77 | * FCGI_UNKNOWN_ROLE: rejecting a new request. 78 | * This happens when the Web server has specified a role that is unknown to the application. 79 | */ 80 | public function getProtocolStatus(): int 81 | { 82 | return $this->protocolStatus; 83 | } 84 | 85 | /** 86 | * {@inheritdoc} 87 | * @param static $self 88 | */ 89 | protected static function unpackPayload(Record $self, string $binaryData): void 90 | { 91 | assert($self instanceof self); // @phpstan-ignore function.alreadyNarrowedType,instanceof.alwaysTrue 92 | 93 | /** @phpstan-var false|array{appStatus: int, protocolStatus: int, reserved: string} */ 94 | $payload = unpack('NappStatus/CprotocolStatus/a3reserved', $binaryData); 95 | if ($payload === false) { 96 | throw new \RuntimeException('Can not unpack data from the binary buffer'); 97 | } 98 | [ 99 | $self->appStatus, 100 | $self->protocolStatus, 101 | $self->reserved1, 102 | ] = array_values($payload); 103 | } 104 | 105 | /** {@inheritdoc} */ 106 | protected function packPayload(): string 107 | { 108 | return pack( 109 | 'NCa3', 110 | $this->appStatus, 111 | $this->protocolStatus, 112 | $this->reserved1 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/core/FastCGI/Record/GetValues.php: -------------------------------------------------------------------------------- 1 | $keys 44 | */ 45 | public function __construct(array $keys) 46 | { 47 | parent::__construct(array_fill_keys($keys, '')); 48 | $this->type = FastCGI::GET_VALUES; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/core/FastCGI/Record/GetValuesResult.php: -------------------------------------------------------------------------------- 1 | $values 42 | */ 43 | public function __construct(array $values) 44 | { 45 | parent::__construct($values); 46 | $this->type = FastCGI::GET_VALUES_RESULT; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/core/FastCGI/Record/Stderr.php: -------------------------------------------------------------------------------- 1 | type = FastCGI::STDERR; 27 | $this->setContentData($contentData); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/core/FastCGI/Record/Stdin.php: -------------------------------------------------------------------------------- 1 | type = FastCGI::STDIN; 27 | $this->setContentData($contentData); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/core/FastCGI/Record/Stdout.php: -------------------------------------------------------------------------------- 1 | type = FastCGI::STDOUT; 27 | $this->setContentData($contentData); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/core/FastCGI/Record/UnknownType.php: -------------------------------------------------------------------------------- 1 | type = FastCGI::UNKNOWN_TYPE; 40 | $this->type1 = $type; 41 | $this->reserved1 = $reserved; 42 | $this->setContentData($this->packPayload()); 43 | } 44 | 45 | /** 46 | * Returns the unrecognized type 47 | */ 48 | public function getUnrecognizedType(): int 49 | { 50 | return $this->type1; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | * @param static $self 56 | */ 57 | public static function unpackPayload(Record $self, string $binaryData): void 58 | { 59 | assert($self instanceof self); // @phpstan-ignore function.alreadyNarrowedType,instanceof.alwaysTrue 60 | 61 | /** @phpstan-var false|array{type: int, reserved: string} */ 62 | $payload = unpack('Ctype/a7reserved', $binaryData); 63 | if ($payload === false) { 64 | throw new \RuntimeException('Can not unpack data from the binary buffer'); 65 | } 66 | [$self->type1, $self->reserved1] = array_values($payload); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | protected function packPayload(): string 73 | { 74 | return pack( 75 | 'Ca7', 76 | $this->type1, 77 | $this->reserved1 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/core/FastCGI/Request.php: -------------------------------------------------------------------------------- 1 | getBody(); 26 | $beginRequestFrame = new BeginRequest(FastCGI::RESPONDER, $this->keepConn ? FastCGI::KEEP_CONN : 0); 27 | $paramsFrame = new Params($this->getParams()); 28 | $paramsEofFrame = new Params([]); 29 | if (empty($body)) { 30 | $message = "{$beginRequestFrame}{$paramsFrame}{$paramsEofFrame}"; 31 | } else { 32 | $stdinList = []; 33 | while (true) { 34 | $stdinList[] = $stdin = new Stdin($body); 35 | $stdinLength = $stdin->getContentLength(); 36 | if ($stdinLength === strlen($body)) { 37 | break; 38 | } 39 | $body = substr($body, $stdinLength); 40 | } 41 | $stdinList[] = new Stdin(''); 42 | $stdin = implode('', $stdinList); 43 | $message = "{$beginRequestFrame}{$paramsFrame}{$paramsEofFrame}{$stdin}"; 44 | } 45 | return $message; 46 | } 47 | 48 | public function getKeepConn(): bool 49 | { 50 | return $this->keepConn; 51 | } 52 | 53 | public function withKeepConn(bool $keepConn): self 54 | { 55 | $this->keepConn = $keepConn; 56 | return $this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/core/FastCGI/Response.php: -------------------------------------------------------------------------------- 1 | $records 22 | */ 23 | public function __construct(array $records) 24 | { 25 | if (!static::verify($records)) { 26 | throw new \InvalidArgumentException('Bad records'); 27 | } 28 | 29 | $body = $error = ''; 30 | foreach ($records as $record) { 31 | if ($record instanceof Stdout) { 32 | if ($record->getContentLength() > 0) { 33 | $body .= $record->getContentData(); 34 | } 35 | } elseif ($record instanceof Stderr) { 36 | if ($record->getContentLength() > 0) { 37 | $error .= $record->getContentData(); 38 | } 39 | } 40 | } 41 | $this->withBody($body)->withError($error); 42 | } 43 | 44 | /** 45 | * @param array $records 46 | */ 47 | protected static function verify(array $records): bool 48 | { 49 | return !empty($records) && $records[array_key_last($records)] instanceof EndRequest; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/core/MultibyteStringObject.php: -------------------------------------------------------------------------------- 1 | string); 19 | } 20 | 21 | public function indexOf(string $needle, int $offset = 0, ?string $encoding = null): false|int 22 | { 23 | return mb_strpos($this->string, $needle, $offset, $encoding); 24 | } 25 | 26 | public function lastIndexOf(string $needle, int $offset = 0, ?string $encoding = null): false|int 27 | { 28 | return mb_strrpos($this->string, $needle, $offset, $encoding); 29 | } 30 | 31 | public function pos(string $needle, int $offset = 0, ?string $encoding = null): false|int 32 | { 33 | return mb_strpos($this->string, $needle, $offset, $encoding); 34 | } 35 | 36 | public function rpos(string $needle, int $offset = 0, ?string $encoding = null): false|int 37 | { 38 | return mb_strrpos($this->string, $needle, $offset, $encoding); 39 | } 40 | 41 | public function ipos(string $needle, int $offset = 0, ?string $encoding = null): int|false 42 | { 43 | return mb_stripos($this->string, $needle, $offset, $encoding); 44 | } 45 | 46 | /** 47 | * @see https://www.php.net/mb_substr 48 | */ 49 | public function substr(int $start, ?int $length = null, ?string $encoding = null): static 50 | { 51 | return new static(mb_substr($this->string, $start, $length, $encoding)); // @phpstan-ignore new.static 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | * @see https://www.php.net/mb_str_split 57 | */ 58 | public function chunk(int $length = 1): ArrayObject 59 | { 60 | return static::detectArrayType(mb_str_split($this->string, $length)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/core/NameResolver.php: -------------------------------------------------------------------------------- 1 | checkServerUrl($url); 30 | } 31 | 32 | abstract public function join(string $name, string $ip, int $port, array $options = []): bool; 33 | 34 | abstract public function leave(string $name, string $ip, int $port): bool; 35 | 36 | abstract public function getCluster(string $name): ?Cluster; 37 | 38 | public function withFilter(callable $fn): self 39 | { 40 | $this->filter_fn = $fn; 41 | return $this; 42 | } 43 | 44 | public function getFilter() 45 | { 46 | return $this->filter_fn; 47 | } 48 | 49 | public function hasFilter(): bool 50 | { 51 | return !empty($this->filter_fn); 52 | } 53 | 54 | /** 55 | * return string: final result, non-empty string must be a valid IP address, 56 | * and an empty string indicates name lookup failed, and lookup operation will not continue. 57 | * return Cluster: has multiple nodes and failover is possible 58 | * return false or null: try another name resolver 59 | * @return Cluster|false|string|null 60 | */ 61 | public function lookup(string $name) 62 | { 63 | if ($this->hasFilter() and ($this->getFilter())($name) !== true) { 64 | return null; 65 | } 66 | $cluster = $this->getCluster($name); 67 | // lookup failed, terminate execution 68 | if ($cluster == null) { 69 | return ''; 70 | } 71 | // only one node, cannot retry 72 | if ($cluster->count() == 1) { 73 | return $cluster->pop(); 74 | } 75 | return $cluster; 76 | } 77 | 78 | /** 79 | * !!! The host MUST BE IP ADDRESS 80 | */ 81 | protected function checkServerUrl(string $url) 82 | { 83 | $info = parse_url($url); 84 | if (empty($info['scheme']) or empty($info['host'])) { 85 | throw new \RuntimeException("invalid url parameter '{$url}'"); 86 | } 87 | if (!filter_var($info['host'], FILTER_VALIDATE_IP)) { 88 | $info['ip'] = gethostbyname($info['host']); 89 | if (!filter_var($info['ip'], FILTER_VALIDATE_IP)) { 90 | throw new \RuntimeException("Failed to resolve host '{$info['host']}'"); 91 | } 92 | } else { 93 | $info['ip'] = $info['host']; 94 | } 95 | $baseUrl = $info['scheme'] . '://' . $info['ip']; 96 | if (!empty($info['port'])) { 97 | $baseUrl .= ":{$info['port']}"; 98 | } 99 | if (!empty($info['path'])) { 100 | $baseUrl .= rtrim($info['path'], '/'); 101 | } 102 | $this->baseUrl = $baseUrl; 103 | $this->info = $info; 104 | } 105 | 106 | protected function checkResponse(ClientProxy $response): bool 107 | { 108 | if ($response->getStatusCode() === Status::OK) { 109 | return true; 110 | } 111 | 112 | throw new Exception('Http Body: ' . $response->getBody(), $response->getStatusCode()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/core/NameResolver/Cluster.php: -------------------------------------------------------------------------------- 1 | 65535) { 29 | throw new Exception("Bad Port [{$port}]"); 30 | } 31 | if ($weight < 0 or $weight > 100) { 32 | throw new Exception("Bad Weight [{$weight}]"); 33 | } 34 | $this->nodes[] = ['host' => $host, 'port' => $port, 'weight' => $weight]; 35 | } 36 | 37 | /** 38 | * @return false|string 39 | */ 40 | public function pop() 41 | { 42 | if (empty($this->nodes)) { 43 | return false; 44 | } 45 | $index = array_rand($this->nodes, 1); 46 | $node = $this->nodes[$index]; 47 | unset($this->nodes[$index]); 48 | return $node; 49 | } 50 | 51 | public function count(): int 52 | { 53 | return count($this->nodes); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/core/NameResolver/Consul.php: -------------------------------------------------------------------------------- 1 | $this->getServiceId($name, $ip, $port), 33 | 'Name' => $this->prefix . $name, 34 | 'Address' => $ip, 35 | 'Port' => $port, 36 | 'EnableTagOverride' => false, 37 | 'Weights' => [ 38 | 'Passing' => $weight, 39 | 'Warning' => 1, 40 | ], 41 | ]; 42 | $url = $this->baseUrl . '/v1/agent/service/register'; 43 | $r = request($url, 'PUT', json_encode($data, JSON_THROW_ON_ERROR)); 44 | return $this->checkResponse($r); 45 | } 46 | 47 | public function leave(string $name, string $ip, int $port): bool 48 | { 49 | $url = $this->baseUrl . '/v1/agent/service/deregister/' . $this->getServiceId( 50 | $name, 51 | $ip, 52 | $port 53 | ); 54 | $r = request($url, 'PUT'); 55 | return $this->checkResponse($r); 56 | } 57 | 58 | public function enableMaintenanceMode(string $name, string $ip, int $port): bool 59 | { 60 | $url = $this->baseUrl . '/v1/agent/service/maintenance/' . $this->getServiceId( 61 | $name, 62 | $ip, 63 | $port 64 | ); 65 | $r = request($url, 'PUT'); 66 | return $this->checkResponse($r); 67 | } 68 | 69 | public function getCluster(string $name): ?Cluster 70 | { 71 | $url = $this->baseUrl . '/v1/catalog/service/' . $this->prefix . $name; 72 | $r = get($url); 73 | if (!$this->checkResponse($r)) { 74 | return null; 75 | } 76 | $list = json_decode($r->getBody(), null, 512, JSON_THROW_ON_ERROR); 77 | if (empty($list)) { 78 | return null; 79 | } 80 | $cluster = new Cluster(); 81 | foreach ($list as $li) { 82 | $cluster->add($li->ServiceAddress, $li->ServicePort, $li->ServiceWeights->Passing); 83 | } 84 | return $cluster; 85 | } 86 | 87 | private function getServiceId(string $name, string $ip, int $port): string 88 | { 89 | return $this->prefix . $name . "_{$ip}:{$port}"; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/core/NameResolver/Exception.php: -------------------------------------------------------------------------------- 1 | prefix . $name; 38 | 39 | $url = $this->baseUrl . '/nacos/v1/ns/instance?' . http_build_query($params); 40 | $r = Coroutine\Http\post($url, []); 41 | return $this->checkResponse($r); 42 | } 43 | 44 | /** 45 | * @throws Coroutine\Http\Client\Exception|Exception 46 | */ 47 | public function leave(string $name, string $ip, int $port): bool 48 | { 49 | $params['port'] = $port; 50 | $params['ip'] = $ip; 51 | $params['serviceName'] = $this->prefix . $name; 52 | 53 | $url = $this->baseUrl . '/nacos/v1/ns/instance?' . http_build_query($params); 54 | $r = Coroutine\Http\request($this->baseUrl . '/nacos/v1/ns/instance?' . http_build_query($params), 'DELETE'); 55 | return $this->checkResponse($r); 56 | } 57 | 58 | /** 59 | * @throws Coroutine\Http\Client\Exception|Exception|\Swoole\Exception 60 | */ 61 | public function getCluster(string $name): ?Cluster 62 | { 63 | $params['serviceName'] = $this->prefix . $name; 64 | 65 | $url = $this->baseUrl . '/nacos/v1/ns/instance/list?' . http_build_query($params); 66 | $r = Coroutine\Http\get($url); 67 | if (!$this->checkResponse($r)) { 68 | return null; 69 | } 70 | $result = json_decode($r->getBody(), null, 512, JSON_THROW_ON_ERROR); 71 | if (empty($result)) { 72 | return null; 73 | } 74 | $cluster = new Cluster(); 75 | foreach ($result->hosts as $node) { 76 | $cluster->add($node->ip, $node->port, $node->weight); 77 | } 78 | return $cluster; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/core/NameResolver/Redis.php: -------------------------------------------------------------------------------- 1 | serverHost = $this->info['ip']; 33 | $this->serverPort = $this->info['port'] ?? 6379; 34 | } 35 | 36 | public function join(string $name, string $ip, int $port, array $options = []): bool 37 | { 38 | if (($redis = $this->connect()) === false) { 39 | return false; 40 | } 41 | if ($redis->sAdd($this->prefix . $name, $ip . ':' . $port) === false) { 42 | return false; 43 | } 44 | return true; 45 | } 46 | 47 | public function leave(string $name, string $ip, int $port): bool 48 | { 49 | if (($redis = $this->connect()) === false) { 50 | return false; 51 | } 52 | if ($redis->sRem($this->prefix . $name, $ip . ':' . $port) === false) { 53 | return false; 54 | } 55 | return true; 56 | } 57 | 58 | public function getCluster(string $name): ?Cluster 59 | { 60 | if (($redis = $this->connect()) === false) { 61 | return null; 62 | } 63 | $members = $redis->sMembers($this->prefix . $name); 64 | if (empty($members)) { 65 | return null; 66 | } 67 | $cluster = new Cluster(); 68 | foreach ($members as $m) { 69 | [$host, $port] = explode(':', $m); 70 | $cluster->add($host, intval($port)); 71 | } 72 | return $cluster; 73 | } 74 | 75 | protected function connect() 76 | { 77 | $redis = new \Redis(); 78 | if ($redis->connect($this->serverHost, $this->serverPort) === false) { 79 | return false; 80 | } 81 | return $redis; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/core/ObjectProxy.php: -------------------------------------------------------------------------------- 1 | __object = $object; 22 | } 23 | 24 | public function __getObject() 25 | { 26 | return $this->__object; 27 | } 28 | 29 | public function __get(string $name) 30 | { 31 | return $this->__object->{$name}; 32 | } 33 | 34 | public function __set(string $name, $value): void 35 | { 36 | $this->__object->{$name} = $value; 37 | } 38 | 39 | public function __isset($name) 40 | { 41 | return isset($this->__object->{$name}); 42 | } 43 | 44 | public function __unset(string $name): void 45 | { 46 | unset($this->__object->{$name}); 47 | } 48 | 49 | public function __call(string $name, array $arguments) 50 | { 51 | return $this->__object->{$name}(...$arguments); 52 | } 53 | 54 | public function __invoke(...$arguments) 55 | { 56 | /** @var mixed $object */ 57 | $object = $this->__object; 58 | return $object(...$arguments); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/core/Process/Manager.php: -------------------------------------------------------------------------------- 1 | setIPCType($ipcType)->setMsgQueueKey($msgQueueKey); 43 | } 44 | 45 | public function add(callable $func, bool $enableCoroutine = false): self 46 | { 47 | $this->addBatch(1, $func, $enableCoroutine); 48 | return $this; 49 | } 50 | 51 | public function addBatch(int $workerNum, callable $func, bool $enableCoroutine = false): self 52 | { 53 | for ($i = 0; $i < $workerNum; $i++) { 54 | $this->startFuncMap[] = [$func, $enableCoroutine]; 55 | } 56 | return $this; 57 | } 58 | 59 | public function start(): void 60 | { 61 | $this->pool = new Pool(count($this->startFuncMap), $this->ipcType, $this->msgQueueKey, false); 62 | 63 | $this->pool->on(Constant::EVENT_WORKER_START, function (Pool $pool, int $workerId) { 64 | [$func, $enableCoroutine] = $this->startFuncMap[$workerId]; 65 | if ($enableCoroutine) { 66 | run($func, $pool, $workerId); 67 | } else { 68 | $func($pool, $workerId); 69 | } 70 | }); 71 | 72 | $this->pool->start(); 73 | } 74 | 75 | public function setIPCType(int $ipcType): self 76 | { 77 | $this->ipcType = $ipcType; 78 | return $this; 79 | } 80 | 81 | public function getIPCType(): int 82 | { 83 | return $this->ipcType; 84 | } 85 | 86 | public function setMsgQueueKey(int $msgQueueKey): self 87 | { 88 | $this->msgQueueKey = $msgQueueKey; 89 | return $this; 90 | } 91 | 92 | public function getMsgQueueKey(): int 93 | { 94 | return $this->msgQueueKey; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/core/Thread/Runnable.php: -------------------------------------------------------------------------------- 1 | running = $running; 26 | $this->id = $index; 27 | } 28 | 29 | abstract public function run(array $args): void; 30 | 31 | protected function isRunning(): bool 32 | { 33 | return $this->running->get() === 1; 34 | } 35 | 36 | protected function shutdown(): void 37 | { 38 | $this->running->set(0); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ext/curl.php: -------------------------------------------------------------------------------- 1 | setOpt($opt, $value); 22 | } 23 | 24 | function swoole_curl_setopt_array(Swoole\Curl\Handler $obj, $array): bool 25 | { 26 | foreach ($array as $k => $v) { 27 | if ($obj->setOpt($k, $v) !== true) { 28 | return false; 29 | } 30 | } 31 | return true; 32 | } 33 | 34 | function swoole_curl_exec(Swoole\Curl\Handler $obj) 35 | { 36 | return $obj->exec(); 37 | } 38 | 39 | function swoole_curl_getinfo(Swoole\Curl\Handler $obj, int $opt = 0) 40 | { 41 | $info = $obj->getInfo(); 42 | if (is_array($info) and $opt) { 43 | return match ($opt) { 44 | CURLINFO_EFFECTIVE_URL => $info['url'], 45 | CURLINFO_HTTP_CODE => $info['http_code'], 46 | CURLINFO_CONTENT_TYPE => $info['content_type'], 47 | CURLINFO_REDIRECT_COUNT => $info['redirect_count'], 48 | CURLINFO_REDIRECT_URL => $info['redirect_url'], 49 | CURLINFO_TOTAL_TIME => $info['total_time'], 50 | CURLINFO_STARTTRANSFER_TIME => $info['starttransfer_time'], 51 | CURLINFO_SIZE_DOWNLOAD => $info['size_download'], 52 | CURLINFO_SPEED_DOWNLOAD => $info['speed_download'], 53 | CURLINFO_REDIRECT_TIME => $info['redirect_time'], 54 | CURLINFO_HEADER_SIZE => $info['header_size'], 55 | CURLINFO_PRIMARY_IP => $info['primary_ip'], 56 | CURLINFO_PRIVATE => $info['private'], 57 | default => null, 58 | }; 59 | } 60 | return $info; 61 | } 62 | 63 | function swoole_curl_errno(Swoole\Curl\Handler $obj): int 64 | { 65 | return $obj->errno(); 66 | } 67 | 68 | function swoole_curl_error(Swoole\Curl\Handler $obj): string 69 | { 70 | return $obj->error(); 71 | } 72 | 73 | function swoole_curl_reset(Swoole\Curl\Handler $obj) 74 | { 75 | return $obj->reset(); 76 | } 77 | 78 | function swoole_curl_close(Swoole\Curl\Handler $obj): void 79 | { 80 | $obj->close(); 81 | } 82 | 83 | function swoole_curl_multi_getcontent(Swoole\Curl\Handler $obj) 84 | { 85 | return $obj->getContent(); 86 | } 87 | -------------------------------------------------------------------------------- /src/std/exec.php: -------------------------------------------------------------------------------- 1 | withHost(MYSQL_SERVER_HOST) 54 | ->withPort(MYSQL_SERVER_PORT) 55 | ->withDbName(MYSQL_SERVER_DB) 56 | ->withCharset('utf8mb4') 57 | ->withUsername(MYSQL_SERVER_USER) 58 | ->withPassword(MYSQL_SERVER_PWD) 59 | ; 60 | 61 | return new MysqliPool($config, $size); 62 | } 63 | 64 | protected static function getPdoMysqlPool(int $size = ConnectionPool::DEFAULT_SIZE): PDOPool 65 | { 66 | $config = (new PDOConfig()) 67 | ->withHost(MYSQL_SERVER_HOST) 68 | ->withPort(MYSQL_SERVER_PORT) 69 | ->withDbName(MYSQL_SERVER_DB) 70 | ->withCharset('utf8mb4') 71 | ->withUsername(MYSQL_SERVER_USER) 72 | ->withPassword(MYSQL_SERVER_PWD) 73 | ; 74 | 75 | return new PDOPool($config, $size); 76 | } 77 | 78 | protected static function getPdoPgsqlPool(int $size = ConnectionPool::DEFAULT_SIZE): PDOPool 79 | { 80 | $config = (new PDOConfig()) 81 | ->withDriver('pgsql') 82 | ->withHost(PGSQL_SERVER_HOST) 83 | ->withPort(PGSQL_SERVER_PORT) 84 | ->withDbName(PGSQL_SERVER_DB) 85 | ->withUsername(PGSQL_SERVER_USER) 86 | ->withPassword(PGSQL_SERVER_PWD) 87 | ; 88 | 89 | return new PDOPool($config, $size); 90 | } 91 | 92 | protected static function getPdoOraclePool(int $size = ConnectionPool::DEFAULT_SIZE): PDOPool 93 | { 94 | $config = (new PDOConfig()) 95 | ->withDriver('oci') 96 | ->withHost(ORACLE_SERVER_HOST) 97 | ->withPort(ORACLE_SERVER_PORT) 98 | ->withDbName(ORACLE_SERVER_DB) 99 | ->withCharset('AL32UTF8') 100 | ->withUsername(ORACLE_SERVER_USER) 101 | ->withPassword(ORACLE_SERVER_PWD) 102 | ; 103 | 104 | return new PDOPool($config, $size); 105 | } 106 | 107 | protected static function getPdoSqlitePool(int $size = ConnectionPool::DEFAULT_SIZE): PDOPool 108 | { 109 | $config = (new PDOConfig())->withDriver('sqlite')->withDbname(static::$sqliteDatabaseFile); 110 | 111 | return new PDOPool($config, $size); 112 | } 113 | 114 | protected static function getRedisPool(int $size = ConnectionPool::DEFAULT_SIZE): RedisPool 115 | { 116 | $config = (new RedisConfig())->withHost(REDIS_SERVER_HOST)->withPort(REDIS_SERVER_PORT); 117 | 118 | return new RedisPool($config, $size); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/HookFlagsTrait.php: -------------------------------------------------------------------------------- 1 | incr('thread', 1); 22 | 23 | for ($i = 0; $i < 5; $i++) { 24 | usleep(10000); 25 | $map->incr('sleep'); 26 | } 27 | 28 | if ($map['sleep'] > 50) { 29 | $this->shutdown(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | SWOOLE_LOG_INFO, 21 | Constant::OPTION_TRACE_FLAGS => 0, 22 | ]); 23 | 24 | if (!defined('MYSQL_SERVER_HOST')) { 25 | define('MYSQL_SERVER_HOST', 'mysql'); 26 | define('MYSQL_SERVER_PORT', 3306); 27 | define('MYSQL_SERVER_USER', 'username'); 28 | define('MYSQL_SERVER_PWD', 'password'); 29 | define('MYSQL_SERVER_DB', 'test'); 30 | } 31 | 32 | if (!defined('PGSQL_SERVER_HOST')) { 33 | define('PGSQL_SERVER_HOST', 'pgsql'); 34 | define('PGSQL_SERVER_PORT', 5432); 35 | define('PGSQL_SERVER_USER', 'username'); 36 | define('PGSQL_SERVER_PWD', 'password'); 37 | define('PGSQL_SERVER_DB', 'test'); 38 | } 39 | 40 | if (!defined('ORACLE_SERVER_HOST')) { 41 | define('ORACLE_SERVER_HOST', 'oracle'); 42 | define('ORACLE_SERVER_PORT', 1521); 43 | define('ORACLE_SERVER_USER', 'system'); 44 | define('ORACLE_SERVER_PWD', 'oracle'); 45 | define('ORACLE_SERVER_DB', 'xe'); 46 | } 47 | 48 | if (!defined('REDIS_SERVER_HOST')) { 49 | define('REDIS_SERVER_HOST', 'redis'); 50 | define('REDIS_SERVER_PORT', 6379); 51 | } 52 | 53 | if (getenv('GITHUB_ACTIONS')) { 54 | define('CONSUL_AGENT_URL', 'http://consul:8500'); 55 | define('NACOS_SERVER_URL', 'http://nacos:8848'); 56 | define('REDIS_SERVER_URL', 'tcp://redis:6379'); 57 | define('GITHUB_ACTIONS', true); 58 | } else { 59 | define('CONSUL_AGENT_URL', 'http://127.0.0.1:8500'); 60 | define('NACOS_SERVER_URL', 'http://127.0.0.1:8848'); 61 | define('REDIS_SERVER_URL', 'tcp://127.0.0.1:6379'); 62 | define('GITHUB_ACTIONS', false); 63 | } 64 | 65 | // This points to folder ./tests/www under root directory of the project. 66 | const DOCUMENT_ROOT = '/var/www/tests/www'; 67 | -------------------------------------------------------------------------------- /tests/unit/Coroutine/HttpFunctionTest.php: -------------------------------------------------------------------------------- 1 | fun1(); 35 | }); 36 | 37 | Coroutine::create(function () { 38 | $this->fun2(); 39 | }); 40 | }); 41 | } 42 | 43 | public function testPost(): void 44 | { 45 | run(function () { 46 | $this->fun3(); 47 | }); 48 | } 49 | 50 | public function testCurlGet(): void 51 | { 52 | swoole_library_set_option(Constant::OPTION_HTTP_CLIENT_DRIVER, 'curl'); 53 | $this->fun1(); 54 | $this->fun2(); 55 | } 56 | 57 | public function testCurlPost(): void 58 | { 59 | swoole_library_set_option(Constant::OPTION_HTTP_CLIENT_DRIVER, 'curl'); 60 | $this->fun3(); 61 | } 62 | 63 | public function testStreamGet(): void 64 | { 65 | swoole_library_set_option(Constant::OPTION_HTTP_CLIENT_DRIVER, 'stream'); 66 | $this->fun1(); 67 | $this->fun2(); 68 | } 69 | 70 | public function testStreamPost(): void 71 | { 72 | swoole_library_set_option(Constant::OPTION_HTTP_CLIENT_DRIVER, 'stream'); 73 | $this->fun3(); 74 | } 75 | 76 | private function fun1(): void 77 | { 78 | self::assertSame(200, get('http://httpbin.org')->getStatusCode(), 'Test HTTP GET without query strings.'); 79 | } 80 | 81 | private function fun2(): void 82 | { 83 | $data = get('http://httpbin.org/get?hello=world'); 84 | $body = json_decode($data->getBody(), null, 512, JSON_THROW_ON_ERROR); 85 | self::assertSame('httpbin.org', $body->headers->Host); 86 | self::assertSame('world', $body->args->hello); 87 | } 88 | 89 | private function fun3(): void 90 | { 91 | $random_data = base64_encode(random_bytes(128)); 92 | $data = post('http://httpbin.org/post?hello=world', ['random_data' => $random_data]); 93 | $body = json_decode($data->getBody(), null, 512, JSON_THROW_ON_ERROR); 94 | self::assertSame('httpbin.org', $body->headers->Host); 95 | self::assertSame('world', $body->args->hello); 96 | self::assertSame($random_data, $body->form->random_data); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/unit/Coroutine/WaitGroupTest.php: -------------------------------------------------------------------------------- 1 | done(); 33 | }); 34 | } 35 | $this->assertEquals($N, $wg->count(), 'Four active coroutines in sleeping state (not yet finished execution).'); 36 | 37 | $wg->wait(); 38 | 39 | self::assertEqualsWithDelta(microtime(true), $st + 0.525, 0.025, 'The four coroutines take about 0.50 to 0.55 second in total to finish.'); 40 | $this->assertEquals(0, $wg->count(), 'All four coroutines have finished execution.'); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/unit/Database/PDOStatementProxyTest.php: -------------------------------------------------------------------------------- 1 | get()->query("SHOW TABLES like 'NON_EXISTING_TABLE_NAME'")->fetch(\PDO::FETCH_ASSOC), 30 | 'FALSE is returned if no results found.' 31 | ); 32 | }); 33 | } 34 | 35 | public static function dataSetFetchMode(): array 36 | { 37 | return [ 38 | [ 39 | [ 40 | ['col1' => '1', 'col2' => '2'], 41 | ['col1' => '3', 'col2' => '4'], 42 | ['col1' => '5', 'col2' => '6'], 43 | ], 44 | [\PDO::FETCH_ASSOC], 45 | 'Test the fetch mode "PDO::FETCH_ASSOC"', 46 | ], 47 | [ 48 | [ 49 | '2', 50 | '4', 51 | '6', 52 | ], 53 | [\PDO::FETCH_COLUMN, 1], 54 | 'Test the fetch mode "PDO::FETCH_COLUMN"', 55 | ], 56 | [ 57 | [ 58 | (object) ['col1' => '1', 'col2' => '2'], 59 | (object) ['col1' => '3', 'col2' => '4'], 60 | (object) ['col1' => '5', 'col2' => '6'], 61 | ], 62 | [\PDO::FETCH_CLASS, \stdClass::class], 63 | 'Test the fetch mode "PDO::FETCH_CLASS"', 64 | ], 65 | ]; 66 | } 67 | 68 | #[DataProvider('dataSetFetchMode')] 69 | public function testSetFetchMode(array $expected, array $args, string $message): void 70 | { 71 | Coroutine\run(function () use ($expected, $args, $message) { 72 | $stmt = self::getPdoMysqlPool()->get()->query( 73 | 'SELECT 74 | * 75 | FROM ( 76 | SELECT 1 as col1, 2 as col2 77 | UNION SELECT 3, 4 78 | UNION SELECT 5, 6 79 | ) `table1`' 80 | ); 81 | $stmt->setFetchMode(...$args); 82 | self::assertEquals($expected, $stmt->fetchAll(), $message); 83 | }); 84 | } 85 | 86 | public function testBindParam(): void 87 | { 88 | Coroutine\run(function () { 89 | $stmt = self::getPdoMysqlPool()->get()->prepare('SHOW TABLES like ?'); 90 | $table = 'NON_EXISTING_TABLE_NAME'; 91 | $stmt->bindParam(1, $table, \PDO::PARAM_STR); 92 | $stmt->execute(); 93 | self::assertIsArray($stmt->fetchAll()); 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/FrameParserTest.php: -------------------------------------------------------------------------------- 1 | assertFalse(FrameParser::hasFrame($incompletePacket)); 30 | 31 | /** @var string $completePacket */ 32 | $completePacket = hex2bin('01010001000800000001010000000000'); 33 | $this->assertTrue(FrameParser::hasFrame($completePacket)); 34 | } 35 | 36 | public function testParsingFrame(): void 37 | { 38 | // one FCGI_BEGIN request with two empty FCGI_PARAMS request 39 | /** @var string $dataStream */ 40 | $dataStream = hex2bin('0101000100080000000101000000000001040001000000000104000100000000'); 41 | $bufferSize = strlen($dataStream); 42 | $this->assertEquals(32, $bufferSize); 43 | 44 | // consume FCGI_BEGIN request 45 | $record = FrameParser::parseFrame($dataStream); 46 | $this->assertInstanceOf(BeginRequest::class, $record); 47 | $recordSize = strlen((string) $record); 48 | $this->assertEquals(16, $recordSize); 49 | 50 | $this->assertEquals($bufferSize - $recordSize, strlen($dataStream)); 51 | 52 | // consume first FCGI_PARAMS request 53 | $record = FrameParser::parseFrame($dataStream); 54 | $this->assertInstanceOf(Params::class, $record); 55 | 56 | // consume second FCGI_PARAMS request 57 | $record = FrameParser::parseFrame($dataStream); 58 | $this->assertInstanceOf(Params::class, $record); 59 | 60 | $this->assertEquals(0, strlen($dataStream)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/HttpRequestTest.php: -------------------------------------------------------------------------------- 1 | withScriptFilename(DOCUMENT_ROOT . '/header0.php')->withKeepConn(false); 37 | for ($i = 0; $i < 2; $i++) { 38 | self::assertSame(200, $client->execute($request)->getStatusCode(), 'Status code should always be 200 when HTTP header keep-alive is turned off.'); 39 | } 40 | 41 | $request->withKeepConn(true); 42 | for ($i = 0; $i < 2; $i++) { 43 | self::assertSame(200, $client->execute($request)->getStatusCode(), 'Status code should always be 200 when HTTP header keep-alive is turned on.'); 44 | } 45 | } 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/Record/AbortRequestTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(FastCGI::ABORT_REQUEST, $request->getType()); 30 | $this->assertEquals(1, $request->getRequestId()); 31 | 32 | $this->assertSame(self::$rawMessage, bin2hex((string) $request)); 33 | } 34 | 35 | public function testUnpacking(): void 36 | { 37 | /** @var string $binaryData */ 38 | $binaryData = hex2bin(self::$rawMessage); 39 | $request = AbortRequest::unpack($binaryData); 40 | $this->assertEquals(FastCGI::ABORT_REQUEST, $request->getType()); 41 | $this->assertEquals(1, $request->getRequestId()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/Record/BeginRequestTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(FastCGI::BEGIN_REQUEST, $request->getType()); 30 | $this->assertEquals(FastCGI::RESPONDER, $request->getRole()); 31 | $this->assertEquals(FastCGI::KEEP_CONN, $request->getFlags()); 32 | 33 | $this->assertSame(self::$rawMessage, bin2hex((string) $request)); 34 | } 35 | 36 | public function testUnpacking(): void 37 | { 38 | /** @var string $binaryData */ 39 | $binaryData = hex2bin(self::$rawMessage); 40 | $request = BeginRequest::unpack($binaryData); 41 | 42 | $this->assertEquals(FastCGI::BEGIN_REQUEST, $request->getType()); 43 | $this->assertEquals(FastCGI::RESPONDER, $request->getRole()); 44 | $this->assertEquals(FastCGI::KEEP_CONN, $request->getFlags()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/Record/DataTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('test', $request->getContentData()); 30 | $this->assertEquals(FastCGI::DATA, $request->getType()); 31 | $this->assertSame(self::$rawMessage, bin2hex((string) $request)); 32 | } 33 | 34 | public function testUnpacking(): void 35 | { 36 | /** @var string $binaryData */ 37 | $binaryData = hex2bin(self::$rawMessage); 38 | $request = Data::unpack($binaryData); 39 | $this->assertEquals(FastCGI::DATA, $request->getType()); 40 | $this->assertEquals('test', $request->getContentData()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/Record/EndRequestTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(FastCGI::END_REQUEST, $request->getType()); 30 | $this->assertEquals(FastCGI::REQUEST_COMPLETE, $request->getProtocolStatus()); 31 | $this->assertEquals(100, $request->getAppStatus()); 32 | 33 | $this->assertSame(self::$rawMessage, bin2hex((string) $request)); 34 | } 35 | 36 | public function testUnpacking(): void 37 | { 38 | /** @var string $binaryData */ 39 | $binaryData = hex2bin(self::$rawMessage); 40 | $request = EndRequest::unpack($binaryData); 41 | 42 | $this->assertEquals(FastCGI::END_REQUEST, $request->getType()); 43 | $this->assertEquals(FastCGI::REQUEST_COMPLETE, $request->getProtocolStatus()); 44 | $this->assertEquals(100, $request->getAppStatus()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/Record/GetValuesResultTest.php: -------------------------------------------------------------------------------- 1 | '1']); 29 | $this->assertEquals(FastCGI::GET_VALUES_RESULT, $request->getType()); 30 | $this->assertEquals(['FCGI_MPXS_CONNS' => '1'], $request->getValues()); 31 | 32 | $this->assertSame(self::$rawMessage, bin2hex((string) $request)); 33 | } 34 | 35 | public function testUnpacking(): void 36 | { 37 | /** @var string $binaryData */ 38 | $binaryData = hex2bin(self::$rawMessage); 39 | $request = GetValuesResult::unpack($binaryData); 40 | 41 | $this->assertEquals(FastCGI::GET_VALUES_RESULT, $request->getType()); 42 | $this->assertEquals(['FCGI_MPXS_CONNS' => '1'], $request->getValues()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/Record/GetValuesTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(FastCGI::GET_VALUES, $request->getType()); 30 | $this->assertEquals(['FCGI_MPXS_CONNS' => ''], $request->getValues()); 31 | 32 | $this->assertSame(self::$rawMessage, bin2hex((string) $request)); 33 | } 34 | 35 | public function testUnpacking(): void 36 | { 37 | /** @var string $binaryData */ 38 | $binaryData = hex2bin(self::$rawMessage); 39 | $request = GetValues::unpack($binaryData); 40 | 41 | $this->assertEquals(FastCGI::GET_VALUES, $request->getType()); 42 | $this->assertEquals(['FCGI_MPXS_CONNS' => ''], $request->getValues()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/Record/ParamsTest.php: -------------------------------------------------------------------------------- 1 | '/home/test.php', 34 | 'GATEWAY_INTERFACE' => 'CGI/1.1', 35 | 'SERVER_SOFTWARE' => 'PHP/Protocol-FCGI', 36 | ]; 37 | 38 | public function testPacking(): void 39 | { 40 | $request = new Params(self::$params); 41 | $this->assertEquals(FastCGI::PARAMS, $request->getType()); 42 | $this->assertEquals(self::$params, $request->getValues()); 43 | 44 | $this->assertSame(preg_replace('/\s+/', '', self::$rawMessage), bin2hex((string) $request)); 45 | } 46 | 47 | public function testUnpacking(): void 48 | { 49 | $oneLineData = preg_replace('/\s+/', '', self::$rawMessage) ?? ''; 50 | $binaryData = hex2bin($oneLineData); 51 | if ($binaryData === false) { 52 | throw new \ValueError('Invalid binary string format'); 53 | } 54 | $request = Params::unpack($binaryData); 55 | 56 | $this->assertEquals(FastCGI::PARAMS, $request->getType()); 57 | $this->assertEquals(self::$params, $request->getValues()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/Record/StderrTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('test', $request->getContentData()); 30 | $this->assertEquals(FastCGI::STDERR, $request->getType()); 31 | $this->assertSame(self::$rawMessage, bin2hex((string) $request)); 32 | } 33 | 34 | public function testUnpacking(): void 35 | { 36 | /** @var string $binaryData */ 37 | $binaryData = hex2bin(self::$rawMessage); 38 | $request = Stderr::unpack($binaryData); 39 | $this->assertEquals(FastCGI::STDERR, $request->getType()); 40 | $this->assertEquals('test', $request->getContentData()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/Record/StdinTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('test', $request->getContentData()); 30 | $this->assertEquals(FastCGI::STDIN, $request->getType()); 31 | $this->assertSame(self::$rawMessage, bin2hex((string) $request)); 32 | } 33 | 34 | public function testUnpacking(): void 35 | { 36 | /** @var string $binaryData */ 37 | $binaryData = hex2bin(self::$rawMessage); 38 | $request = Stdin::unpack($binaryData); 39 | $this->assertEquals(FastCGI::STDIN, $request->getType()); 40 | $this->assertEquals('test', $request->getContentData()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/Record/StdoutTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('test', $request->getContentData()); 30 | $this->assertEquals(FastCGI::STDOUT, $request->getType()); 31 | $this->assertSame(self::$rawMessage, bin2hex((string) $request)); 32 | } 33 | 34 | public function testUnpacking(): void 35 | { 36 | /** @var string $binaryData */ 37 | $binaryData = hex2bin(self::$rawMessage); 38 | $request = Stdout::unpack($binaryData); 39 | $this->assertEquals(FastCGI::STDOUT, $request->getType()); 40 | $this->assertEquals('test', $request->getContentData()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/Record/UnknownTypeTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(FastCGI::UNKNOWN_TYPE, $request->getType()); 30 | $this->assertEquals(42, $request->getUnrecognizedType()); 31 | 32 | $this->assertSame(self::$rawMessage, bin2hex((string) $request)); 33 | } 34 | 35 | public function testUnpacking(): void 36 | { 37 | /** @var string $binaryData */ 38 | $binaryData = hex2bin(self::$rawMessage); 39 | $request = UnknownType::unpack($binaryData); 40 | 41 | $this->assertEquals(FastCGI::UNKNOWN_TYPE, $request->getType()); 42 | $this->assertEquals(42, $request->getUnrecognizedType()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/unit/FastCGI/RecordTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(FastCGI::VERSION_1, $record->getVersion()); 35 | $this->assertEquals(FastCGI::BEGIN_REQUEST, $record->getType()); 36 | $this->assertEquals(1, $record->getRequestId()); 37 | $this->assertEquals(8, $record->getContentLength()); 38 | $this->assertEquals(0, $record->getPaddingLength()); 39 | 40 | // Check payload data 41 | $this->assertEquals(hex2bin('0001010000000000'), $record->getContentData()); 42 | } 43 | 44 | public function testPackingPacket(): void 45 | { 46 | $record = new Record(); 47 | $record->setRequestId(5); 48 | $record->setContentData('12345'); 49 | $packet = (string) $record; 50 | 51 | $this->assertEquals($packet, hex2bin('010b0005000503003132333435000000')); 52 | $result = Record::unpack($packet); 53 | $this->assertEquals(FastCGI::UNKNOWN_TYPE, $result->getType()); 54 | $this->assertEquals(5, $result->getRequestId()); 55 | $this->assertEquals('12345', $result->getContentData()); 56 | } 57 | 58 | /** 59 | * Padding size should resize the packet size to the 8 bytes boundary for optimal performance 60 | */ 61 | public function testAutomaticCalculationOfPaddingLength(): void 62 | { 63 | $record = new Record(); 64 | $record->setContentData('12345'); 65 | $this->assertEquals(3, $record->getPaddingLength()); 66 | 67 | $record->setContentData('12345678'); 68 | $this->assertEquals(0, $record->getPaddingLength()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/unit/FunctionTest.php: -------------------------------------------------------------------------------- 1 | uniqid()]; 28 | swoole_library_set_options($options); 29 | $this->assertEquals($options, swoole_library_get_options()); 30 | } 31 | 32 | public function testOption(): void 33 | { 34 | $option = uniqid(); 35 | swoole_library_set_option(__METHOD__, $option); 36 | $this->assertEquals($option, swoole_library_get_option(__METHOD__)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/unit/MultibyteStringObjectTest.php: -------------------------------------------------------------------------------- 1 | length(); 27 | $this->assertEquals(strlen($str), $length); 28 | } 29 | 30 | public function testIndexOf(): void 31 | { 32 | $this->assertEquals(swoole_mbstring('hello swoole and hello world')->indexOf('swoole'), 6); 33 | } 34 | 35 | public function testLastIndexOf(): void 36 | { 37 | $this->assertEquals(swoole_mbstring('hello swoole and hello world')->lastIndexOf('hello'), 17); 38 | } 39 | 40 | public function testPos(): void 41 | { 42 | $this->assertEquals(swoole_mbstring('hello swoole and hello world')->pos('and'), 13); 43 | } 44 | 45 | public function testRPos(): void 46 | { 47 | $this->assertEquals(swoole_mbstring('hello swoole and hello world')->rpos('hello'), 17); 48 | } 49 | 50 | public function testIPos(): void 51 | { 52 | $this->assertEquals(swoole_mbstring('hello swoole AND hello world')->ipos('and'), 13); 53 | } 54 | 55 | public function testSubstr(): void 56 | { 57 | $this->assertEquals(swoole_mbstring('hello swoole and hello world') 58 | ->substr(4, 8)->toString(), 'o swoole'); 59 | } 60 | 61 | public function chunk(): void 62 | { 63 | $r = swoole_mbstring('hello swoole and hello world')->chunk(5)->toArray(); 64 | $expectResult = [ 65 | 0 => 'hello', 66 | 1 => ' swoo', 67 | 2 => 'le an', 68 | 3 => 'd hel', 69 | 4 => 'lo wo', 70 | 5 => 'rld', 71 | ]; 72 | $this->assertEquals($expectResult, $r); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/unit/NameResolverTest.php: -------------------------------------------------------------------------------- 1 | fun1($ns); 31 | } 32 | 33 | public function testConsul(): void 34 | { 35 | swoole_library_set_option('http_client_driver', 'curl'); 36 | $ns = new NameResolver\Consul(CONSUL_AGENT_URL); 37 | $this->fun1($ns); 38 | } 39 | 40 | public function testNacos(): void 41 | { 42 | if (GITHUB_ACTIONS) { 43 | $this->markTestSkipped('Nacos is not available.'); 44 | } 45 | swoole_library_set_option('http_client_driver', 'curl'); 46 | $ns = new NameResolver\Nacos(NACOS_SERVER_URL); 47 | $this->fun1($ns); 48 | } 49 | 50 | public function testLookup(): void 51 | { 52 | if (!function_exists('swoole_name_resolver_lookup')) { 53 | $this->markTestSkipped('Swoole v4.9 or later is required.'); 54 | } 55 | $count = 0; 56 | $ns = new NameResolver\Redis(REDIS_SERVER_URL); 57 | $ns->withFilter(function ($name) use (&$count) { 58 | $count++; 59 | return swoole_string($name)->endsWith('.service'); 60 | }); 61 | swoole_name_resolver_add($ns); 62 | $domain = 'localhost'; 63 | $this->assertEquals(swoole_name_resolver_lookup($domain, new NameResolver\Context()), gethostbyname($domain)); 64 | $this->assertEquals(1, $count); 65 | $this->assertTrue(swoole_name_resolver_remove($ns)); 66 | } 67 | 68 | public function testRedisCo(): void 69 | { 70 | run(function () { 71 | $ns = new NameResolver\Redis(REDIS_SERVER_URL); 72 | $this->fun1($ns); 73 | }); 74 | } 75 | 76 | public function testConsulCo(): void 77 | { 78 | run(function () { 79 | $ns = new NameResolver\Consul(CONSUL_AGENT_URL); 80 | $this->fun1($ns); 81 | }); 82 | } 83 | 84 | public function testNacosCo(): void 85 | { 86 | if (GITHUB_ACTIONS) { 87 | $this->markTestSkipped('Nacos is not available.'); 88 | } 89 | run(function () { 90 | $ns = new NameResolver\Nacos(NACOS_SERVER_URL); 91 | $this->fun1($ns); 92 | }); 93 | } 94 | 95 | private function fun1(NameResolver $ns): void 96 | { 97 | $service_name = uniqid() . '.service'; 98 | $ip = '127.0.0.1'; 99 | $port = random_int(10000, 65536); 100 | $this->assertTrue($ns->join($service_name, $ip, $port)); 101 | 102 | $rs = $ns->getCluster($service_name); 103 | $this->assertEquals(1, $rs->count()); 104 | $node = $rs->pop(); 105 | $this->assertNotEmpty($node); 106 | $this->assertEquals($ip, $node['host']); 107 | $this->assertEquals($port, $node['port']); 108 | 109 | $this->assertTrue($ns->leave($service_name, $ip, $port)); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/unit/ObjectProxyTest.php: -------------------------------------------------------------------------------- 1 | |null}> 30 | */ 31 | public static function dataDatabaseObjectProxy(): array 32 | { 33 | return [ 34 | [[self::class, 'getMysqliPool'], \mysqli::class, MysqliProxy::class], 35 | [[self::class, 'getPdoMysqlPool'], \PDO::class, PDOProxy::class], 36 | [[self::class, 'getPdoOraclePool'], \PDO::class, PDOProxy::class], 37 | [[self::class, 'getPdoPgsqlPool'], \PDO::class, PDOProxy::class], 38 | [[self::class, 'getPdoSqlitePool'], \PDO::class, PDOProxy::class], 39 | [[self::class, 'getRedisPool'], \Redis::class], 40 | ]; 41 | } 42 | 43 | /** 44 | * @param class-string $expectedObjectClass 45 | * @param class-string|null $expectedProxyClass 46 | */ 47 | #[DataProvider('dataDatabaseObjectProxy')] 48 | public function testDatabaseObjectProxy(callable $callback, string $expectedObjectClass, ?string $expectedProxyClass = null): void 49 | { 50 | Coroutine\run(function () use ($callback, $expectedObjectClass, $expectedProxyClass): void { 51 | $pool = $callback(); 52 | self::assertInstanceOf(ConnectionPool::class, $pool); 53 | /** @var ConnectionPool $pool */ 54 | $conn = $pool->get(); 55 | 56 | if (is_null($expectedProxyClass)) { // Proxy class not in use? 57 | self::assertInstanceOf($expectedObjectClass, $conn); 58 | } else { 59 | self::assertInstanceOf($expectedProxyClass, $conn); 60 | self::assertInstanceOf($expectedObjectClass, $conn->__getObject()); 61 | } 62 | }); 63 | } 64 | 65 | /** 66 | * @return array> 67 | */ 68 | public static function dataUncloneableDatabaseProxyObject(): array 69 | { 70 | return [ 71 | [[self::class, 'getMysqliPool']], 72 | [[self::class, 'getPdoMysqlPool']], 73 | [[self::class, 'getPdoOraclePool']], 74 | [[self::class, 'getPdoPgsqlPool']], 75 | [[self::class, 'getPdoSqlitePool']], 76 | ]; 77 | } 78 | 79 | #[Depends('testDatabaseObjectProxy')] 80 | #[DataProvider('dataUncloneableDatabaseProxyObject')] 81 | public function testUncloneableDatabaseProxyObject(callable $callback): void 82 | { 83 | Coroutine\run(function () use ($callback): void { 84 | /** @var ConnectionPool $pool */ 85 | $pool = $callback(); 86 | try { 87 | clone $pool->get(); 88 | } catch (\Error $e) { 89 | if ($e->getMessage() != 'Trying to clone an uncloneable database proxy object') { 90 | throw $e; 91 | } 92 | } 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/unit/Process/ProcessManagerTest.php: -------------------------------------------------------------------------------- 1 | add(function (Pool $pool, int $workerId) use ($atomic) { 31 | $this->assertEquals(0, $workerId); 32 | usleep(100000); 33 | $atomic->wakeup(); 34 | }); 35 | 36 | $pm->add(function (Pool $pool, int $workerId) use ($atomic) { 37 | $this->assertEquals(1, $workerId); 38 | $atomic->wait(1.5); 39 | $pool->shutdown(); 40 | }); 41 | 42 | $pm->start(); 43 | } 44 | 45 | public function testAddDisableCoroutine(): void 46 | { 47 | $pm = new ProcessManager(); 48 | 49 | $pm->add(function (Pool $pool, int $workerId) { 50 | $this->assertEquals(-1, Coroutine::getCid()); 51 | $pool->shutdown(); 52 | }); 53 | 54 | $pm->start(); 55 | } 56 | 57 | public function testAddEnableCoroutine(): void 58 | { 59 | $pm = new ProcessManager(); 60 | 61 | $pm->add(function (Pool $pool, int $workerId) { 62 | $this->assertGreaterThanOrEqual(1, Coroutine::getCid()); 63 | $pool->shutdown(); 64 | }, true); 65 | 66 | $pm->start(); 67 | } 68 | 69 | public function testAddBatch(): void 70 | { 71 | $pm = new ProcessManager(); 72 | 73 | $pm->addBatch(2, function (Pool $pool, int $workerId) { 74 | if ($workerId == 1) { 75 | $pool->shutdown(); 76 | } 77 | }); 78 | 79 | $pm->start(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/unit/Thread/PoolTest.php: -------------------------------------------------------------------------------- 1 | withClassDefinitionFile(dirname(__DIR__, 2) . '/TestThread.php') 30 | ->withArguments(uniqid(), $map) 31 | ->start() 32 | ; 33 | 34 | $this->assertEquals($map['sleep'], 65); 35 | $this->assertEquals($map['thread'], 13); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/www/README.md: -------------------------------------------------------------------------------- 1 | This directory contains PHP-FPM scripts to test the FastCGI client/proxy. 2 | -------------------------------------------------------------------------------- /tests/www/header0.php: -------------------------------------------------------------------------------- 1 | ; rel="https://api.w.org/"'); // Set a default header from WordPress. 13 | 14 | echo "Hello world!\n"; 15 | -------------------------------------------------------------------------------- /tests/www/header1.php: -------------------------------------------------------------------------------- 1 |