├── .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 | [](https://twitter.com/phpswoole)
4 | [](https://discord.swoole.dev)
5 | [](https://github.com/swoole/library/actions)
6 | [](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 |