├── .gitignore
├── tests
├── stubs
│ └── SwooleCoroutineStub.php
├── servers
│ ├── Swoole
│ │ └── server.php
│ └── Workerman
│ │ └── server.php
├── e2e
│ └── AdapterTest.php
└── unit
│ └── ClientTest.php
├── .github
├── workflows
│ ├── linter.yml
│ ├── codeql-analysis.yml
│ └── tests.yml
└── ISSUE_TEMPLATE
│ ├── documentation.yaml
│ ├── feature.yaml
│ └── bug.yaml
├── phpunit.xml
├── php-8.1.Dockerfile
├── php-8.2.Dockerfile
├── php-8.3.Dockerfile
├── php-8.0.Dockerfile
├── composer.json
├── LICENSE
├── docker-compose.yml
├── psalm.xml
├── README.md
├── CODE_OF_CONDUCT.md
├── src
└── WebSocket
│ ├── Adapter.php
│ ├── Adapter
│ ├── Workerman.php
│ └── Swoole.php
│ ├── Server.php
│ └── Client.php
├── CONTRIBUTING.md
└── composer.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | .phpunit*
3 | .idea
4 |
--------------------------------------------------------------------------------
/tests/stubs/SwooleCoroutineStub.php:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | ./tests/unit/
14 |
15 |
16 | ./tests/e2e/
17 |
18 |
19 |
--------------------------------------------------------------------------------
/php-8.1.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM composer:2.0 AS composer
2 |
3 | ARG TESTING=false
4 | ENV TESTING=$TESTING
5 |
6 | WORKDIR /usr/local/src/
7 |
8 | COPY composer.lock /usr/local/src/
9 | COPY composer.json /usr/local/src/
10 |
11 | RUN composer install \
12 | --ignore-platform-reqs \
13 | --optimize-autoloader \
14 | --no-plugins \
15 | --no-scripts \
16 | --prefer-dist
17 |
18 | FROM appwrite/utopia-base:php-8.1-0.1.0 AS final
19 |
20 | RUN docker-php-ext-install sockets pcntl
21 |
22 | COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor
23 |
24 | COPY ./src /usr/src/code/src
25 | COPY ./tests /usr/src/code/tests
26 |
27 | WORKDIR /usr/src/code
28 |
29 | CMD ["tail", "-f", "/dev/null"]
--------------------------------------------------------------------------------
/php-8.2.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM composer:2.0 AS composer
2 |
3 | ARG TESTING=false
4 | ENV TESTING=$TESTING
5 |
6 | WORKDIR /usr/local/src/
7 |
8 | COPY composer.lock /usr/local/src/
9 | COPY composer.json /usr/local/src/
10 |
11 | RUN composer install \
12 | --ignore-platform-reqs \
13 | --optimize-autoloader \
14 | --no-plugins \
15 | --no-scripts \
16 | --prefer-dist
17 |
18 | FROM appwrite/utopia-base:php-8.2-0.1.0 AS final
19 |
20 | RUN docker-php-ext-install sockets pcntl
21 |
22 | COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor
23 |
24 | COPY ./src /usr/src/code/src
25 | COPY ./tests /usr/src/code/tests
26 |
27 | WORKDIR /usr/src/code
28 |
29 | CMD ["tail", "-f", "/dev/null"]
--------------------------------------------------------------------------------
/php-8.3.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM composer:2.0 AS composer
2 |
3 | ARG TESTING=false
4 | ENV TESTING=$TESTING
5 |
6 | WORKDIR /usr/local/src/
7 |
8 | COPY composer.lock /usr/local/src/
9 | COPY composer.json /usr/local/src/
10 |
11 | RUN composer install \
12 | --ignore-platform-reqs \
13 | --optimize-autoloader \
14 | --no-plugins \
15 | --no-scripts \
16 | --prefer-dist
17 |
18 | FROM appwrite/utopia-base:php-8.3-0.1.0 AS final
19 |
20 | RUN docker-php-ext-install sockets pcntl
21 |
22 | COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor
23 |
24 | COPY ./src /usr/src/code/src
25 | COPY ./tests /usr/src/code/tests
26 |
27 | WORKDIR /usr/src/code
28 |
29 | CMD ["tail", "-f", "/dev/null"]
--------------------------------------------------------------------------------
/php-8.0.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM composer:2.0 AS composer
2 |
3 | ARG TESTING=false
4 | ENV TESTING=$TESTING
5 |
6 | WORKDIR /usr/local/src/
7 |
8 | COPY composer.lock /usr/local/src/
9 | COPY composer.json /usr/local/src/
10 |
11 | RUN composer install \
12 | --ignore-platform-reqs \
13 | --optimize-autoloader \
14 | --no-plugins \
15 | --no-scripts \
16 | --prefer-dist
17 |
18 | FROM appwrite/utopia-base:php-8.0-0.1.0 AS final
19 |
20 | RUN docker-php-ext-install sockets pcntl
21 |
22 | COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor
23 |
24 | COPY ./src /usr/src/code/src
25 | COPY ./tests /usr/src/code/tests
26 |
27 | WORKDIR /usr/src/code
28 |
29 | CMD ["tail", "-f", "/dev/null"]
30 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "utopia-php/websocket",
3 | "description": "A simple abstraction for WebSocket servers.",
4 | "type": "library",
5 | "keywords": ["php","framework", "upf", "utopia", "websocket"],
6 | "license": "MIT",
7 | "scripts": {
8 | "test": "./vendor/bin/phpunit",
9 | "lint": "./vendor/bin/pint --test --preset=psr12 src tests",
10 | "format": "./vendor/bin/pint --preset=psr12 src tests",
11 | "check": "./vendor/bin/phpstan analyse --level max src tests"
12 | },
13 | "minimum-stability": "stable",
14 | "autoload": {
15 | "psr-4": {"Utopia\\WebSocket\\": "src/WebSocket"}
16 | },
17 | "autoload-dev": {
18 | "psr-4": {"Utopia\\WebSocket\\Tests\\": "tests/e2e"}
19 | },
20 | "require": {
21 | "php": ">=8.0"
22 | },
23 | "require-dev": {
24 | "swoole/ide-helper": "5.1.2",
25 | "textalk/websocket": "1.5.2",
26 | "phpunit/phpunit": "^9.5.5",
27 | "workerman/workerman": "4.1.*",
28 | "phpstan/phpstan": "^1.12",
29 | "laravel/pint": "^1.15"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 utopia
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | tests:
3 | container_name: tests
4 | image: websocket-${PHP_VERSION:-8.3}-dev
5 | build:
6 | context: .
7 | dockerfile: php-${PHP_VERSION:-8.3}.Dockerfile
8 | volumes:
9 | - ./src:/usr/src/code/src
10 | - ./tests:/usr/src/code/tests
11 | - ./phpunit.xml:/usr/src/code/phpunit.xml
12 | networks:
13 | - websockets
14 |
15 | swoole:
16 | container_name: swoole
17 | image: websocket-${PHP_VERSION:-8.3}-dev
18 | command: php tests/servers/Swoole/server.php
19 | volumes:
20 | - ./src:/usr/src/code/src
21 | - ./tests:/usr/src/code/tests
22 | - ./phpunit.xml:/usr/src/code/phpunit.xml
23 | networks:
24 | - websockets
25 | ports:
26 | - "8001:80"
27 |
28 | workerman:
29 | container_name: workerman
30 | image: websocket-${PHP_VERSION:-8.3}-dev
31 | command: php tests/servers/Workerman/server.php start
32 | volumes:
33 | - ./src:/usr/src/code/src
34 | - ./tests:/usr/src/code/tests
35 | - ./phpunit.xml:/usr/src/code/phpunit.xml
36 | networks:
37 | - websockets
38 | ports:
39 | - "8002:80"
40 |
41 | networks:
42 | websockets:
43 | driver: bridge
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation.yaml:
--------------------------------------------------------------------------------
1 | name: "📚 Documentation"
2 | description: "Report an issue related to documentation"
3 | title: "📚 Documentation: "
4 | labels: [documentation]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Thanks for taking the time to fill out our documentation update request form 🙏
10 | - type: textarea
11 | id: issue-description
12 | validations:
13 | required: true
14 | attributes:
15 | label: "💭 Description"
16 | description: "A clear and concise description of what the issue is."
17 | placeholder: "Documentation should not ..."
18 | - type: checkboxes
19 | id: no-duplicate-issues
20 | attributes:
21 | label: "👀 Have you spent some time to check if this issue has been raised before?"
22 | description: "Have you Googled for a similar issue or checked our older issues for a similar bug?"
23 | options:
24 | - label: "I checked and didn't find similar issue"
25 | required: true
26 | - type: checkboxes
27 | id: read-code-of-conduct
28 | attributes:
29 | label: "🏢 Have you read the Code of Conduct?"
30 | options:
31 | - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)"
32 | required: true
33 |
34 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: "Tests"
2 |
3 | on: [pull_request]
4 | jobs:
5 | tests:
6 | name: Unit & E2E
7 | runs-on: ubuntu-latest
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | php-versions: [
12 | '8.0',
13 | '8.1',
14 | '8.2',
15 | '8.3'
16 | ]
17 |
18 | steps:
19 | - name: Checkout repository
20 | uses: actions/checkout@v3
21 | with:
22 | fetch-depth: 2
23 | submodules: recursive
24 |
25 | - run: git checkout HEAD^2
26 |
27 | - name: Set up Docker Buildx
28 | uses: docker/setup-buildx-action@v2
29 |
30 | - name: Build image
31 | uses: docker/build-push-action@v3
32 | with:
33 | context: .
34 | file: php-${{ matrix.php-versions }}.Dockerfile
35 | push: false
36 | tags: websocket-${{ matrix.php-versions }}-dev
37 | load: true
38 | outputs: type=cacheonly
39 | cache-from: type=gha,scope=${{ matrix.php-versions }}
40 | cache-to: type=gha,mode=max,scope=${{ matrix.php-versions }}
41 |
42 | - name: Start Containers
43 | run: |
44 | export PHP_VERSION=${{ matrix.php-versions }}
45 | docker compose up -d
46 | sleep 10
47 |
48 | - name: Run Tests
49 | run: docker compose exec tests vendor/bin/phpunit --debug
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature.yaml:
--------------------------------------------------------------------------------
1 | name: 🚀 Feature
2 | description: "Submit a proposal for a new feature"
3 | title: "🚀 Feature: "
4 | labels: [feature]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Thanks for taking the time to fill out our feature request form 🙏
10 | - type: textarea
11 | id: feature-description
12 | validations:
13 | required: true
14 | attributes:
15 | label: "🔖 Feature description"
16 | description: "A clear and concise description of what the feature is."
17 | placeholder: "You should add ..."
18 | - type: textarea
19 | id: pitch
20 | validations:
21 | required: true
22 | attributes:
23 | label: "🎤 Pitch"
24 | description: "Please explain why this feature should be implemented and how it would be used. Add examples, if applicable."
25 | placeholder: "In my use-case, ..."
26 | - type: checkboxes
27 | id: no-duplicate-issues
28 | attributes:
29 | label: "👀 Have you spent some time to check if this issue has been raised before?"
30 | description: "Have you Googled for a similar issue or checked our older issues for a similar bug?"
31 | options:
32 | - label: "I checked and didn't find similar issue"
33 | required: true
34 | - type: checkboxes
35 | id: read-code-of-conduct
36 | attributes:
37 | label: "🏢 Have you read the Code of Conduct?"
38 | options:
39 | - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)"
40 | required: true
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Utopia WebSocket
2 |
3 | [](https://travis-ci.com/utopia-php/websocket)
4 | 
5 | [](https://appwrite.io/discord)
6 |
7 | Utopia WebSocket is a simple and lite abstraction layer around a WebSocket server. This library is aiming to be as simple and easy to learn and use. This library is maintained by the [Appwrite team](https://appwrite.io).
8 |
9 | Although this library is part of the [Utopia Framework](https://github.com/utopia-php/framework) project it is dependency free and can be used as standalone with any other PHP project or framework.
10 |
11 | ## Getting Started
12 |
13 | Install using composer:
14 | ```bash
15 | composer require utopia-php/websocket
16 | ```
17 |
18 | Init in your application:
19 | ```php
20 | setPackageMaxLength(64000);
28 |
29 | $server = new WebSocket\Server($adapter);
30 | $server->onStart(function () {
31 | echo "Server started!";
32 | });
33 | $server->onWorkerStart(function (int $workerId) {
34 | echo "Worker {$workerId} started!";
35 | });
36 | $server->onOpen(function (int $connection, $request) {
37 | echo "Connection {$connection} established!";
38 | });
39 | $server->onMessage(function (int $connection, string $message) {
40 | echo "Message from {$connection}: {$message}";
41 | });
42 | $server->onClose(function (int $connection) {
43 | echo "Connection {$workerId} closed!";
44 | });
45 |
46 | $server->start();
47 | ```
48 |
49 | ## System Requirements
50 |
51 | Utopia Framework requires PHP 8.0 or later. We recommend using the latest PHP version whenever possible.
52 |
53 | ## Copyright and license
54 |
55 | The MIT License (MIT) [http://www.opensource.org/licenses/mit-license.php](http://www.opensource.org/licenses/mit-license.php)
56 |
--------------------------------------------------------------------------------
/tests/servers/Swoole/server.php:
--------------------------------------------------------------------------------
1 | setWorkerNumber(1); // Important for tests
11 |
12 | $server = new WebSocket\Server($adapter);
13 |
14 | $server
15 | ->onWorkerStart(function (int $workerId) {
16 | echo 'worker started ', $workerId, PHP_EOL;
17 | })
18 | ->onWorkerStop(function (int $workerId) {
19 | echo "worker stopped ", $workerId, PHP_EOL;
20 | })
21 | ->onOpen(function (int $connection, Request $request) {
22 | echo 'connected ', $connection, PHP_EOL;
23 | })
24 | ->onClose(function (int $connection) {
25 | echo 'disconnected ', $connection, PHP_EOL;
26 | })
27 | ->onMessage(function (int $connection, string $message) use ($server) {
28 | echo $message, PHP_EOL;
29 |
30 | switch ($message) {
31 | case 'ping':
32 | $server->send([$connection], 'pong');
33 | break;
34 | case 'pong':
35 | $server->send([$connection], 'ping');
36 | break;
37 | case 'broadcast':
38 | $server->send($server->getConnections(), 'broadcast');
39 | break;
40 | case 'disconnect':
41 | $server->send([$connection], 'disconnect');
42 | $server->close($connection, 1000);
43 | break;
44 | }
45 | })
46 | ->onRequest(function (Request $request, Response $response) use ($server) {
47 | echo 'HTTP request received: ', $request->server['request_uri'], PHP_EOL;
48 |
49 | if ($request->server['request_uri'] === '/health') {
50 | $response->header('Content-Type', 'application/json');
51 | $response->status(200);
52 | $response->end(json_encode(['status' => 'ok', 'message' => 'WebSocket server is running']));
53 | } elseif ($request->server['request_uri'] === '/info') {
54 | $response->header('Content-Type', 'application/json');
55 | $response->status(200);
56 | $response->end(json_encode([
57 | 'server' => 'Swoole WebSocket',
58 | 'connections' => count($server->getConnections()),
59 | 'timestamp' => time()
60 | ]));
61 | } else {
62 | $response->status(404);
63 | $response->end('Not Found');
64 | }
65 | })
66 | ->start();
67 |
--------------------------------------------------------------------------------
/tests/e2e/AdapterTest.php:
--------------------------------------------------------------------------------
1 | 10,
16 | ]);
17 | }
18 |
19 | public function setUp(): void
20 | {
21 | }
22 |
23 | public function testSwoole(): void
24 | {
25 | $this->testServer('swoole', 80);
26 | }
27 |
28 | public function testWorkerman(): void
29 | {
30 | $this->testServer('workerman', 80);
31 | }
32 |
33 | private function testServer(string $host, int $port): void
34 | {
35 | run(function () use ($host, $port) {
36 | $client = $this->getWebsocket($host, $port);
37 | $client->connect();
38 |
39 | $client->send('ping');
40 | $this->assertEquals('pong', $client->receive());
41 | $this->assertEquals(true, $client->isConnected());
42 |
43 | $clientA = $this->getWebsocket($host, $port);
44 | $clientA->connect();
45 | $clientB = $this->getWebsocket($host, $port);
46 | $clientB->connect();
47 |
48 | $clientA->send('ping');
49 | $this->assertEquals('pong', $clientA->receive());
50 | $clientB->send('pong');
51 | $this->assertEquals('ping', $clientB->receive());
52 |
53 | $clientA->send('broadcast');
54 | $this->assertEquals('broadcast', $client->receive());
55 | $this->assertEquals('broadcast', $clientA->receive());
56 | $this->assertEquals('broadcast', $clientB->receive());
57 |
58 | $clientB->send('broadcast');
59 | $this->assertEquals('broadcast', $client->receive());
60 | $this->assertEquals('broadcast', $clientA->receive());
61 | $this->assertEquals('broadcast', $clientB->receive());
62 |
63 | $clientA->close();
64 | $clientB->close();
65 |
66 | $client->send('disconnect');
67 | $this->assertEquals('disconnect', $client->receive());
68 |
69 | try {
70 | $client->receive();
71 | $this->fail('Expected RuntimeException was not thrown');
72 | } catch (\RuntimeException $e) {
73 | $this->assertStringContainsString('Failed to receive data:', $e->getMessage());
74 | }
75 | });
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tests/servers/Workerman/server.php:
--------------------------------------------------------------------------------
1 | setWorkerNumber(1); // Important for tests
11 |
12 | $server = new WebSocket\Server($adapter);
13 |
14 | $server
15 | ->onWorkerStart(function (int $workerId) {
16 | echo 'worker started ', $workerId, PHP_EOL;
17 | })
18 | ->onWorkerStop(function (int $workerId) {
19 | echo "worker stopped ", $workerId, PHP_EOL;
20 | })
21 | ->onOpen(function (int $connection, array $request) {
22 | echo 'connected ', $connection, PHP_EOL;
23 | })
24 | ->onClose(function (int $connection) {
25 | echo 'disconnected ', $connection, PHP_EOL;
26 | })
27 | ->onMessage(function (int $connection, string $message) use ($server) {
28 | echo $message, PHP_EOL;
29 |
30 | switch ($message) {
31 | case 'ping':
32 | $server->send([$connection], 'pong');
33 | break;
34 | case 'pong':
35 | $server->send([$connection], 'ping');
36 | break;
37 | case 'broadcast':
38 | $server->send($server->getConnections(), 'broadcast');
39 | break;
40 | case 'disconnect':
41 | $server->send([$connection], 'disconnect');
42 | $server->close($connection, 1000);
43 | break;
44 | }
45 | })
46 | ->onRequest(function (TcpConnection $connection, Request $request) use ($server) {
47 | $path = $request->path();
48 | if (!is_string($path)) {
49 | throw new \Exception('Invalid path ' . $path . ' for request: ' . json_encode($request, JSON_PRETTY_PRINT));
50 | }
51 | echo 'HTTP request received: ', $path, PHP_EOL;
52 |
53 | if ($path === '/health') {
54 | $connection->send('HTTP/1.1 200 OK' . "\r\n" .
55 | 'Content-Type: application/json' . "\r\n" .
56 | 'Connection: close' . "\r\n\r\n" .
57 | json_encode(['status' => 'ok', 'message' => 'WebSocket server is running']));
58 | } elseif ($path === '/info') {
59 | $connection->send('HTTP/1.1 200 OK' . "\r\n" .
60 | 'Content-Type: application/json' . "\r\n" .
61 | 'Connection: close' . "\r\n\r\n" .
62 | json_encode([
63 | 'server' => 'Workerman WebSocket',
64 | 'connections' => count($server->getConnections()),
65 | 'timestamp' => time()
66 | ]));
67 | } else {
68 | $connection->send('HTTP/1.1 404 Not Found' . "\r\n" .
69 | 'Connection: close' . "\r\n\r\n" .
70 | 'Not Found');
71 | }
72 | })
73 | ->start();
74 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yaml:
--------------------------------------------------------------------------------
1 | name: "🐛 Bug Report"
2 | description: "Submit a bug report to help us improve"
3 | title: "🐛 Bug Report: "
4 | labels: [bug]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Thanks for taking the time to fill out our bug report form 🙏
10 | - type: textarea
11 | id: steps-to-reproduce
12 | validations:
13 | required: true
14 | attributes:
15 | label: "👟 Reproduction steps"
16 | description: "How do you trigger this bug? Please walk us through it step by step."
17 | placeholder: "When I ..."
18 | - type: textarea
19 | id: expected-behavior
20 | validations:
21 | required: true
22 | attributes:
23 | label: "👍 Expected behavior"
24 | description: "What did you think would happen?"
25 | placeholder: "It should ..."
26 | - type: textarea
27 | id: actual-behavior
28 | validations:
29 | required: true
30 | attributes:
31 | label: "👎 Actual Behavior"
32 | description: "What did actually happen? Add screenshots, if applicable."
33 | placeholder: "It actually ..."
34 | - type: dropdown
35 | id: appwrite-websocket
36 | attributes:
37 | label: "🎲 Utopia Websocket version"
38 | description: "What version of Appwrite are you running?"
39 | options:
40 | - Version 0.10.x
41 | - Version 0.9.x
42 | - Version 0.8.x
43 | - Version 0.7.x
44 | - Version 0.6.x
45 | - Different version (specify in environment)
46 | validations:
47 | required: true
48 | - type: dropdown
49 | id: php-version
50 | attributes:
51 | label: "🐘 PHP Version"
52 | description: "What version of PHP are you running?"
53 | options:
54 | - PHP 7.3.x
55 | - PHP 7.4.x
56 | - Different version (specify in environment)
57 | validations:
58 | required: true
59 | - type: dropdown
60 | id: operating-system
61 | attributes:
62 | label: "💻 Operating system"
63 | description: "What OS is your server / device running on?"
64 | options:
65 | - Linux
66 | - MacOS
67 | - Windows
68 | - Something else
69 | validations:
70 | required: true
71 | - type: textarea
72 | id: enviromnemt
73 | validations:
74 | required: false
75 | attributes:
76 | label: "🧱 Your Environment"
77 | description: "Is your environment customized in any way?"
78 | placeholder: "I use Cloudflare for ..."
79 | - type: checkboxes
80 | id: no-duplicate-issues
81 | attributes:
82 | label: "👀 Have you spent some time to check if this issue has been raised before?"
83 | description: "Have you Googled for a similar issue or checked our older issues for a similar bug?"
84 | options:
85 | - label: "I checked and didn't find similar issue"
86 | required: true
87 | - type: checkboxes
88 | id: read-code-of-conduct
89 | attributes:
90 | label: "🏢 Have you read the Code of Conduct?"
91 | options:
92 | - label: "I have read the [Code of Conduct](https://github.com/appwrite/appwrite/blob/HEAD/CODE_OF_CONDUCT.md)"
93 | required: true
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to make participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity, expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at team@appwrite.io. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/src/WebSocket/Adapter.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 |
14 | protected array $config = [];
15 |
16 | public function __construct(string $host = '0.0.0.0', int $port = 80)
17 | {
18 | $this->host = $host;
19 | $this->port = $port;
20 | }
21 |
22 | /**
23 | * Starts the Server.
24 | * @return void
25 | */
26 | abstract public function start(): void;
27 |
28 | /**
29 | * Shuts down the Server.
30 | * @return void
31 | */
32 | abstract public function shutdown(): void;
33 |
34 | /**
35 | * Sends a message to passed connections.
36 | * @param array $connections Array of connection ID's.
37 | * @param string $message Message.
38 | * @return void
39 | */
40 | abstract public function send(array $connections, string $message): void;
41 |
42 | /**
43 | * Closes a connection.
44 | * @param int $connection Connection ID.
45 | * @param int $code Close Code.
46 | * @return void
47 | */
48 | abstract public function close(int $connection, int $code): void;
49 |
50 | /**
51 | * Is called when the Server starts.
52 | * @param callable $callback
53 | * @return self
54 | */
55 | abstract public function onStart(callable $callback): self;
56 |
57 | /**
58 | * Is called when a Worker starts.
59 | * @param callable $callback
60 | * @return self
61 | */
62 | abstract public function onWorkerStart(callable $callback): self;
63 |
64 | /**
65 | * Is called when a Worker stops.
66 | * @param callable $callback
67 | * @return self
68 | */
69 | abstract public function onWorkerStop(callable $callback): self;
70 |
71 | /**
72 | * Is called when a connection is established.
73 | * @param callable $callback
74 | * @return self
75 | */
76 | abstract public function onOpen(callable $callback): self;
77 |
78 | /**
79 | * Is called when a message is received.
80 | * @param callable $callback
81 | * @return self
82 | */
83 | abstract public function onMessage(callable $callback): self;
84 |
85 | /**
86 | * Is called when an HTTP request is received.
87 | * @param callable $callback
88 | * @return self
89 | */
90 | abstract public function onRequest(callable $callback): self;
91 |
92 | /**
93 | * Is called when a connection is closed.
94 | * @param callable $callback
95 | * @return self
96 | */
97 | abstract public function onClose(callable $callback): self;
98 |
99 | /**
100 | * Sets maximum package length in bytes.
101 | * @param int $bytes
102 | * @return Adapter
103 | */
104 | abstract public function setPackageMaxLength(int $bytes): self;
105 |
106 | /**
107 | * Enables/Disables compression.
108 | * @param bool $enabled
109 | * @return Adapter
110 | */
111 | abstract public function setCompressionEnabled(bool $enabled): self;
112 |
113 | /**
114 | * Sets the number of workers.
115 | * @param int $num
116 | * @return Adapter
117 | */
118 | abstract public function setWorkerNumber(int $num): self;
119 |
120 | /**
121 | * Returns the native server object from the Adapter.
122 | * @return mixed
123 | */
124 | abstract public function getNative(): mixed;
125 |
126 | /**
127 | * Returns all connections.
128 | *
129 | * @return array
130 | */
131 | abstract public function getConnections(): array;
132 | }
133 |
--------------------------------------------------------------------------------
/src/WebSocket/Adapter/Workerman.php:
--------------------------------------------------------------------------------
1 | server = new Worker("websocket://{$this->host}:{$this->port}");
29 | }
30 |
31 | public function start(): void
32 | {
33 | Worker::runAll();
34 | $callable = ($this->callbackOnStart);
35 | if (!is_callable($callable)) {
36 | throw new \Exception();
37 | }
38 | \call_user_func($callable);
39 | }
40 |
41 | public function shutdown(): void
42 | {
43 | Worker::stopAll();
44 | }
45 |
46 | public function send(array $connections, string $message): void
47 | {
48 | foreach ($connections as $connection) {
49 | TcpConnection::$connections[$connection]->send($message);
50 | }
51 | }
52 |
53 | public function close(int $connection, int $code): void
54 | {
55 | TcpConnection::$connections[$connection]->close();
56 | }
57 |
58 | public function onStart(callable $callback): self
59 | {
60 | $this->callbackOnStart = $callback;
61 |
62 | return $this;
63 | }
64 |
65 | public function onWorkerStart(callable $callback): self
66 | {
67 | $this->server->onWorkerStart = function (Worker $worker) use ($callback): void {
68 | call_user_func($callback, $worker->id);
69 | };
70 | return $this;
71 | }
72 |
73 | public function onWorkerStop(callable $callback): Adapter
74 | {
75 | $this->server->onWorkerStop = function (Worker $worker) use ($callback): void {
76 | call_user_func($callback, $worker->id);
77 | };
78 |
79 | return $this;
80 | }
81 |
82 | public function onOpen(callable $callback): self
83 | {
84 | $this->server->onConnect = function ($connection) use ($callback): void {
85 | $connection->onWebSocketConnect = function (TcpConnection $connection) use ($callback): void {
86 | /** @var array $_SERVER */
87 | call_user_func($callback, $connection->id, $_SERVER);
88 | };
89 | };
90 |
91 | return $this;
92 | }
93 |
94 | public function onMessage(callable $callback): self
95 | {
96 | $this->callbackOnMessage = $callback;
97 | $this->setupMessageHandler();
98 |
99 | return $this;
100 | }
101 |
102 | public function onClose(callable $callback): self
103 | {
104 | $this->server->onClose = function (TcpConnection $connection) use ($callback): void {
105 | call_user_func($callback, $connection->id);
106 | };
107 |
108 | return $this;
109 | }
110 |
111 | public function onRequest(callable $callback): self
112 | {
113 | $this->callbackOnRequest = $callback;
114 | $this->setupMessageHandler();
115 |
116 | return $this;
117 | }
118 |
119 | /**
120 | * Sets up the unified message handler that routes between WebSocket messages and HTTP requests
121 | */
122 | private function setupMessageHandler(): void
123 | {
124 | $this->server->onMessage = function (TcpConnection $connection, $data) {
125 | if ($data instanceof Request) {
126 | if (is_callable($this->callbackOnRequest)) {
127 | call_user_func($this->callbackOnRequest, $connection, $data);
128 | }
129 | } elseif (is_string($data)) {
130 | if (is_callable($this->callbackOnMessage)) {
131 | call_user_func($this->callbackOnMessage, $connection->id, $data);
132 | }
133 | }
134 | };
135 | }
136 |
137 | public function setPackageMaxLength(int $bytes): self
138 | {
139 | return $this;
140 | }
141 |
142 | public function setCompressionEnabled(bool $enabled): self
143 | {
144 | return $this;
145 | }
146 |
147 | public function setWorkerNumber(int $num): self
148 | {
149 | $this->server->count = $num;
150 |
151 | return $this;
152 | }
153 |
154 | public function getNative(): Worker
155 | {
156 | return $this->server;
157 | }
158 |
159 | public function getConnections(): array
160 | {
161 | return array_keys(TcpConnection::$connections);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | We would ❤️ for you to contribute to Utopia-php and help make it better! We want contributing to Utopia-php to be fun, enjoyable, and educational for anyone and everyone. All contributions are welcome, including issues, new docs as well as updates and tweaks, blog posts, workshops, and more.
4 |
5 | ## How to Start?
6 |
7 | If you are worried or don’t know where to start, check out our next section explaining what kind of help we could use and where can you get involved. You can reach out with questions to [Eldad Fux (@eldadfux)](https://twitter.com/eldadfux) or anyone from the [Appwrite team on Discord](https://discord.gg/GSeTUeA). You can also submit an issue, and a maintainer can guide you!
8 |
9 | ## Code of Conduct
10 |
11 | Help us keep Utopia-php open and inclusive. Please read and follow our [Code of Conduct](/CODE_OF_CONDUCT.md).
12 |
13 | ## Submit a Pull Request 🚀
14 |
15 | Branch naming convention is as following
16 |
17 | `TYPE-ISSUE_ID-DESCRIPTION`
18 |
19 | example:
20 |
21 | ```
22 | doc-548-submit-a-pull-request-section-to-contribution-guide
23 | ```
24 |
25 | When `TYPE` can be:
26 |
27 | - **feat** - is a new feature
28 | - **doc** - documentation only changes
29 | - **cicd** - changes related to CI/CD system
30 | - **fix** - a bug fix
31 | - **refactor** - code change that neither fixes a bug nor adds a feature
32 |
33 | **All PRs must include a commit message with the changes description!**
34 |
35 | For the initial start, fork the project and use git clone command to download the repository to your computer. A standard procedure for working on an issue would be to:
36 |
37 | 1. `git pull`, before creating a new branch, pull the changes from upstream. Your master needs to be up to date.
38 |
39 | ```
40 | $ git pull
41 | ```
42 |
43 | 2. Create new branch from `master` like: `doc-548-submit-a-pull-request-section-to-contribution-guide`
44 |
45 | ```
46 | $ git checkout -b [name_of_your_new_branch]
47 | ```
48 |
49 | 3. Work - commit - repeat ( be sure to be in your branch )
50 |
51 | 4. Push changes to GitHub
52 |
53 | ```
54 | $ git push origin [name_of_your_new_branch]
55 | ```
56 |
57 | 5. Submit your changes for review
58 | If you go to your repository on GitHub, you'll see a `Compare & pull request` button. Click on that button.
59 | 6. Start a Pull Request
60 | Now submit the pull request and click on `Create pull request`.
61 | 7. Get a code review approval/reject
62 | 8. After approval, merge your PR
63 | 9. GitHub will automatically delete the branch after the merge is done. (they can still be restored).
64 |
65 | ## Introducing New Features
66 |
67 | We would 💖 you to contribute to Utopia-php, but we would also like to make sure Utopia-php is as great as possible and loyal to its vision and mission statement 🙏.
68 |
69 | For us to find the right balance, please open an issue explaining your ideas before introducing a new pull request.
70 |
71 | This will allow the Utopia-php community to have sufficient discussion about the new feature value and how it fits in the product roadmap and vision.
72 |
73 | This is also important for the Utopia-php lead developers to be able to give technical input and different emphasis regarding the feature design and architecture. Some bigger features might need to go through our [RFC process](https://github.com/appwrite/rfc).
74 |
75 | ## Other Ways to Help
76 |
77 | Pull requests are great, but there are many other areas where you can help Utopia-php.
78 |
79 | ### Blogging & Speaking
80 |
81 | Blogging, speaking about, or creating tutorials about one of Utopia-php’s many features is great way to contribute and help our project grow.
82 |
83 | ### Presenting at Meetups
84 |
85 | Presenting at meetups and conferences about your Utopia-php projects. Your unique challenges and successes in building things with Utopia-php can provide great speaking material. We’d love to review your talk abstract/CFP, so get in touch with us if you’d like some help!
86 |
87 | ### Sending Feedbacks & Reporting Bugs
88 |
89 | Sending feedback is a great way for us to understand your different use cases of Utopia-php better. If you had any issues, bugs, or want to share about your experience, feel free to do so on our GitHub issues page or at our [Discord channel](https://discord.gg/GSeTUeA).
90 |
91 | ### Submitting New Ideas
92 |
93 | If you think Utopia-php could use a new feature, please open an issue on our GitHub repository, stating as much information as you can think about your new idea and it's implications. We would also use this issue to gather more information, get more feedback from the community, and have a proper discussion about the new feature.
94 |
95 | ### Improving Documentation
96 |
97 | Submitting documentation updates, enhancements, designs, or bug fixes. Spelling or grammar fixes will be very much appreciated.
98 |
99 | ### Helping Someone
100 |
101 | Searching for Utopia-php, GitHub or StackOverflow and helping someone else who needs help. You can also help by teaching others how to contribute to Utopia-php's repo!
102 |
--------------------------------------------------------------------------------
/src/WebSocket/Adapter/Swoole.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | private static array $connections = [];
23 |
24 | public function __construct(string $host = '0.0.0.0', int $port = 80)
25 | {
26 | parent::__construct($host, $port);
27 |
28 | $this->server = new Server($this->host, $this->port);
29 |
30 | // Set maximum connections to Swoole's limit of 1 Million
31 | $this->config['max_connection'] = 1_000_000;
32 | }
33 |
34 | public function start(): void
35 | {
36 | $this->server->set($this->config);
37 | $this->server->start();
38 | }
39 |
40 | public function shutdown(): void
41 | {
42 | $this->server->shutdown();
43 | }
44 |
45 | public function send(array $connections, string $message): void
46 | {
47 | foreach ($connections as $connection) {
48 | go(function () use ($connection, $message) {
49 | if ($this->server->exist($connection) && $this->server->isEstablished($connection)) {
50 | $this->server->push(
51 | $connection,
52 | $message,
53 | SWOOLE_WEBSOCKET_OPCODE_TEXT,
54 | SWOOLE_WEBSOCKET_FLAG_FIN | SWOOLE_WEBSOCKET_FLAG_COMPRESS
55 | );
56 | } else {
57 | $this->server->close($connection);
58 | }
59 | });
60 | }
61 | }
62 |
63 | public function close(int $connection, int $code): void
64 | {
65 | $this->server->close($connection);
66 | }
67 |
68 | public function onStart(callable $callback): self
69 | {
70 | $this->server->on('start', function () use ($callback) {
71 | call_user_func($callback);
72 |
73 | Process::signal('2', function () {
74 | $this->shutdown();
75 | });
76 | });
77 |
78 | return $this;
79 | }
80 |
81 | public function onWorkerStart(callable $callback): self
82 | {
83 | $this->server->on('workerStart', function (Server $server, int $workerId) use ($callback) {
84 | call_user_func($callback, $workerId);
85 | });
86 | return $this;
87 | }
88 |
89 | public function onWorkerStop(callable $callback): Adapter
90 | {
91 | $this->server->on('workerStop', function (Server $server, int $workerId) use ($callback) {
92 | call_user_func($callback, $workerId);
93 | });
94 |
95 | return $this;
96 | }
97 |
98 | public function onOpen(callable $callback): self
99 | {
100 | $this->server->on('open', function (Server $server, Request $request) use ($callback) {
101 | self::$connections[$request->fd] = true;
102 |
103 | call_user_func($callback, $request->fd, $request);
104 | });
105 |
106 | return $this;
107 | }
108 |
109 | public function onMessage(callable $callback): self
110 | {
111 | $this->server->on('message', function (Server $server, Frame $frame) use ($callback) {
112 | call_user_func($callback, $frame->fd, $frame->data);
113 | });
114 |
115 | return $this;
116 | }
117 |
118 | public function onClose(callable $callback): self
119 | {
120 | $this->server->on('close', function (Server $server, int $fd) use ($callback) {
121 | unset(self::$connections[$fd]);
122 |
123 | call_user_func($callback, $fd);
124 | });
125 |
126 | return $this;
127 | }
128 |
129 | public function onRequest(callable $callback): self
130 | {
131 | $this->server->on('request', function (Request $request, Response $response) use ($callback) {
132 | call_user_func($callback, $request, $response);
133 | });
134 |
135 | return $this;
136 | }
137 |
138 | public function setPackageMaxLength(int $bytes): self
139 | {
140 | $this->config['package_max_length'] = $bytes;
141 |
142 | return $this;
143 | }
144 |
145 | public function setCompressionEnabled(bool $enabled): self
146 | {
147 | $this->config['websocket_compression'] = $enabled;
148 |
149 | return $this;
150 | }
151 |
152 | public function setWorkerNumber(int $num): self
153 | {
154 | $this->config['worker_num'] = $num;
155 |
156 | return $this;
157 | }
158 |
159 | public function getNative(): Server
160 | {
161 | return $this->server;
162 | }
163 |
164 | public function getConnections(): array
165 | {
166 | return array_keys(self::$connections);
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/WebSocket/Server.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | protected array $errorCallbacks = [];
14 |
15 | protected Adapter $adapter;
16 |
17 | /**
18 | * Creates an instance of a WebSocket server.
19 | * @param Adapter $adapter
20 | */
21 | public function __construct(Adapter $adapter)
22 | {
23 | $this->adapter = $adapter;
24 | }
25 |
26 | /**
27 | * Starts the WebSocket server.
28 | * @return void
29 | */
30 | public function start(): void
31 | {
32 | try {
33 | $this->adapter->start();
34 | } catch (Throwable $error) {
35 | foreach ($this->errorCallbacks as $errorCallback) {
36 | $errorCallback($error, 'start');
37 | }
38 | }
39 | }
40 |
41 | /**
42 | * Shuts down the WebSocket server.
43 | * @return void
44 | */
45 | public function shutdown(): void
46 | {
47 | try {
48 | $this->adapter->shutdown();
49 | } catch (Throwable $error) {
50 | foreach ($this->errorCallbacks as $errorCallback) {
51 | $errorCallback($error, 'shutdown');
52 | }
53 | }
54 | }
55 |
56 | /**
57 | * Sends a message to passed connections.
58 | * @param array $connections Array of connection ID's.
59 | * @param string $message Message.
60 | * @return void
61 | */
62 | public function send(array $connections, string $message): void
63 | {
64 | try {
65 | $this->adapter->send($connections, $message);
66 | } catch (Throwable $error) {
67 | foreach ($this->errorCallbacks as $errorCallback) {
68 | $errorCallback($error, 'send');
69 | }
70 | }
71 | }
72 |
73 | /**
74 | * Closes a connection.
75 | *
76 | * @param int $connection Connection ID.
77 | * @param int $code Close Code.
78 | * @return void
79 | */
80 | public function close(int $connection, int $code): void
81 | {
82 | try {
83 | $this->adapter->close($connection, $code);
84 | } catch (Throwable $error) {
85 | foreach ($this->errorCallbacks as $errorCallback) {
86 | $errorCallback($error, 'close');
87 | }
88 | }
89 | }
90 |
91 | /**
92 | * Is called when the Server starts.
93 | * @param callable $callback
94 | * @return self
95 | */
96 | public function onStart(callable $callback): self
97 | {
98 | try {
99 | $this->adapter->onStart($callback);
100 | } catch (Throwable $error) {
101 | foreach ($this->errorCallbacks as $errorCallback) {
102 | $errorCallback($error, 'onStart');
103 | }
104 | }
105 |
106 | return $this;
107 | }
108 |
109 | /**
110 | * Is called when a Worker starts.
111 | * @param callable $callback
112 | * @return self
113 | */
114 | public function onWorkerStart(callable $callback): self
115 | {
116 | try {
117 | $this->adapter->onWorkerStart($callback);
118 | } catch (Throwable $error) {
119 | foreach ($this->errorCallbacks as $errorCallback) {
120 | $errorCallback($error, 'onWorkerStart');
121 | }
122 | }
123 |
124 | return $this;
125 | }
126 |
127 | /**
128 | * Is called when a Worker stops.
129 | * @param callable $callback
130 | * @return self
131 | */
132 | public function onWorkerStop(callable $callback): self
133 | {
134 | try {
135 | $this->adapter->onWorkerStop($callback);
136 | } catch (Throwable $error) {
137 | foreach ($this->errorCallbacks as $errorCallback) {
138 | $errorCallback($error, 'onWorkerStop');
139 | }
140 | }
141 |
142 | return $this;
143 | }
144 |
145 | /**
146 | * Is called when a connection is established.
147 | * @param callable $callback
148 | * @return self
149 | */
150 | public function onOpen(callable $callback): self
151 | {
152 | try {
153 | $this->adapter->onOpen($callback);
154 | } catch (Throwable $error) {
155 | foreach ($this->errorCallbacks as $errorCallback) {
156 | $errorCallback($error, 'onOpen');
157 | }
158 | }
159 |
160 | return $this;
161 | }
162 |
163 | /**
164 | * Is called when a message is received.
165 | * @param callable $callback
166 | * @return self
167 | */
168 | public function onMessage(callable $callback): self
169 | {
170 | try {
171 | $this->adapter->onMessage($callback);
172 | } catch (Throwable $error) {
173 | foreach ($this->errorCallbacks as $errorCallback) {
174 | $errorCallback($error, 'onMessage');
175 | }
176 | }
177 |
178 | return $this;
179 | }
180 |
181 | /**
182 | * Is called when a connection is closed.
183 | * @param callable $callback
184 | * @return self
185 | */
186 | public function onClose(callable $callback): self
187 | {
188 | try {
189 | $this->adapter->onClose($callback);
190 | } catch (Throwable $error) {
191 | foreach ($this->errorCallbacks as $errorCallback) {
192 | $errorCallback($error, 'onClose');
193 | }
194 | }
195 |
196 | return $this;
197 | }
198 |
199 | /**
200 | * Is called when an HTTP request is received.
201 | * @param callable $callback
202 | * @return self
203 | */
204 | public function onRequest(callable $callback): self
205 | {
206 | try {
207 | $this->adapter->onRequest($callback);
208 | } catch (Throwable $error) {
209 | foreach ($this->errorCallbacks as $errorCallback) {
210 | $errorCallback($error, 'onRequest');
211 | }
212 | }
213 |
214 | return $this;
215 | }
216 |
217 | /**
218 | * Returns all connections.
219 | *
220 | * @return array
221 | */
222 | public function getConnections(): array
223 | {
224 | return $this->adapter->getConnections();
225 | }
226 |
227 | /**
228 | * Register callback. Will be executed when error occurs.
229 | *
230 | * @param callable $callback
231 | * @return self
232 | */
233 | public function error(callable $callback): self
234 | {
235 | $this->errorCallbacks[] = $callback;
236 | return $this;
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/tests/unit/ClientTest.php:
--------------------------------------------------------------------------------
1 | client = new Client($this->testUrl);
16 | }
17 |
18 | public function testConstructorWithValidUrl(): void
19 | {
20 | $client = new Client($this->testUrl);
21 | $this->assertInstanceOf(Client::class, $client);
22 | }
23 |
24 | public function testConstructorWithInvalidUrl(): void
25 | {
26 | $this->expectException(\InvalidArgumentException::class);
27 | new Client('invalid-url');
28 | }
29 |
30 | public function testConstructorWithCustomOptions(): void
31 | {
32 | $options = [
33 | 'headers' => ['Authorization' => 'Bearer token'],
34 | 'timeout' => 60
35 | ];
36 | $client = new Client($this->testUrl, $options);
37 | $this->assertInstanceOf(Client::class, $client);
38 | }
39 |
40 | public function testEventHandlers(): void
41 | {
42 | $messageReceived = false;
43 | $this->client->onMessage(function ($data) use (&$messageReceived) {
44 | $messageReceived = true;
45 | });
46 |
47 | $closeReceived = false;
48 | $this->client->onClose(function () use (&$closeReceived) {
49 | $closeReceived = true;
50 | });
51 |
52 | $errorReceived = false;
53 | $this->client->onError(function ($error) use (&$errorReceived) {
54 | $errorReceived = true;
55 | });
56 |
57 | $openReceived = false;
58 | $this->client->onOpen(function () use (&$openReceived) {
59 | $openReceived = true;
60 | });
61 |
62 | $pingReceived = false;
63 | $this->client->onPing(function ($data) use (&$pingReceived) {
64 | $pingReceived = true;
65 | });
66 |
67 | $pongReceived = false;
68 | $this->client->onPong(function ($data) use (&$pongReceived) {
69 | $pongReceived = true;
70 | });
71 |
72 | // Verify that all handlers are properly set
73 | $this->assertIsCallable([$this->client, 'onMessage']);
74 | $this->assertIsCallable([$this->client, 'onClose']);
75 | $this->assertIsCallable([$this->client, 'onError']);
76 | $this->assertIsCallable([$this->client, 'onOpen']);
77 | $this->assertIsCallable([$this->client, 'onPing']);
78 | $this->assertIsCallable([$this->client, 'onPong']);
79 | }
80 |
81 | public function testIsConnected(): void
82 | {
83 | $this->assertFalse($this->client->isConnected());
84 | }
85 |
86 | public function testSendWithoutConnection(): void
87 | {
88 | $this->expectException(\RuntimeException::class);
89 | $this->expectExceptionMessage('Not connected to WebSocket server');
90 | $this->client->send('test message');
91 | }
92 |
93 | public function testReceiveWithoutConnection(): void
94 | {
95 | $this->expectException(\RuntimeException::class);
96 | $this->expectExceptionMessage('Not connected to WebSocket server');
97 | $this->client->receive();
98 | }
99 |
100 | public function testListen(): void
101 | {
102 | try {
103 | $messageReceived = false;
104 | $testMessage = 'Hello WebSocket!';
105 |
106 | $this->client->onMessage(function ($data) use (&$messageReceived, $testMessage) {
107 | $messageReceived = true;
108 | $this->assertEquals($testMessage, $data);
109 | });
110 |
111 | // Mock the client's recv method to simulate receiving a message
112 | $mockFrame = new \Swoole\WebSocket\Frame();
113 | $mockFrame->opcode = WEBSOCKET_OPCODE_TEXT;
114 | $mockFrame->data = $testMessage;
115 |
116 | $swooleClient = $this->getMockBuilder(\Swoole\Coroutine\Http\Client::class)
117 | ->disableOriginalConstructor()
118 | ->getMock();
119 | } catch (\Error $e) {
120 | if (strpos($e->getMessage(), 'enum_exists') !== false) {
121 | $this->markTestSkipped('Test skipped due to enum_exists compatibility issue');
122 | }
123 | throw $e;
124 | }
125 |
126 | $swooleClient->expects($this->exactly(2))
127 | ->method('recv')
128 | ->willReturnOnConsecutiveCalls($mockFrame, false);
129 |
130 | $swooleClient->errCode = SWOOLE_ERROR_CLIENT_NO_CONNECTION;
131 |
132 | // Use reflection to set the private properties
133 | $reflectionClass = new \ReflectionClass(Client::class);
134 |
135 | $connectedProperty = $reflectionClass->getProperty('connected');
136 | $connectedProperty->setAccessible(true);
137 | $connectedProperty->setValue($this->client, true);
138 |
139 | $clientProperty = $reflectionClass->getProperty('client');
140 | $clientProperty->setAccessible(true);
141 | $clientProperty->setValue($this->client, $swooleClient);
142 |
143 | $this->client->listen();
144 |
145 | $this->assertTrue($messageReceived);
146 | $this->assertFalse($this->client->isConnected());
147 | }
148 |
149 | public function testListenWithError(): void
150 | {
151 | try {
152 | $errorReceived = false;
153 | $this->client->onError(function ($error) use (&$errorReceived) {
154 | $errorReceived = true;
155 | $this->assertInstanceOf(\RuntimeException::class, $error);
156 | });
157 |
158 | $swooleClient = $this->getMockBuilder(\Swoole\Coroutine\Http\Client::class)
159 | ->disableOriginalConstructor()
160 | ->getMock();
161 | } catch (\Error $e) {
162 | if (strpos($e->getMessage(), 'enum_exists') !== false) {
163 | $this->markTestSkipped('Test skipped due to enum_exists compatibility issue');
164 | }
165 | throw $e;
166 | }
167 |
168 | $swooleClient->expects($this->once())
169 | ->method('recv')
170 | ->willReturn(false);
171 |
172 | $swooleClient->errCode = 1; // Some error code that's not SWOOLE_ERROR_CLIENT_NO_CONNECTION
173 | $swooleClient->errMsg = 'Test error';
174 |
175 | // Use reflection to set the private properties
176 | $reflectionClass = new \ReflectionClass(Client::class);
177 |
178 | $connectedProperty = $reflectionClass->getProperty('connected');
179 | $connectedProperty->setAccessible(true);
180 | $connectedProperty->setValue($this->client, true);
181 |
182 | $clientProperty = $reflectionClass->getProperty('client');
183 | $clientProperty->setAccessible(true);
184 | $clientProperty->setValue($this->client, $swooleClient);
185 |
186 | $this->client->listen();
187 |
188 | $this->assertTrue($errorReceived);
189 | $this->assertFalse($this->client->isConnected());
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/WebSocket/Client.php:
--------------------------------------------------------------------------------
1 | */
16 | private array $headers;
17 | private float $timeout;
18 |
19 | // Event handlers
20 | private ?\Closure $onMessage = null;
21 | private ?\Closure $onClose = null;
22 | private ?\Closure $onError = null;
23 | private ?\Closure $onOpen = null;
24 | private ?\Closure $onPing = null;
25 | private ?\Closure $onPong = null;
26 |
27 | /**
28 | * @param string $url
29 | * @param array{headers?: array, timeout?: float} $options
30 | */
31 | public function __construct(string $url, array $options = [])
32 | {
33 | $parsedUrl = parse_url($url);
34 | if ($parsedUrl === false) {
35 | throw new \InvalidArgumentException('Invalid WebSocket URL');
36 | }
37 |
38 | if (!isset($parsedUrl['host'])) {
39 | throw new \InvalidArgumentException('WebSocket URL must contain a host');
40 | }
41 |
42 | $this->host = $parsedUrl['host'];
43 | $this->port = $parsedUrl['port'] ?? (isset($parsedUrl['scheme']) && $parsedUrl['scheme'] === 'wss' ? 443 : 80);
44 | $this->path = $parsedUrl['path'] ?? '/';
45 | if (isset($parsedUrl['query'])) {
46 | $this->path .= '?' . $parsedUrl['query'];
47 | }
48 |
49 | $this->headers = $options['headers'] ?? [];
50 | $this->timeout = $options['timeout'] ?? 30;
51 | }
52 |
53 | public function connect(): void
54 | {
55 | $this->client = new SwooleClient($this->host, $this->port, $this->port === 443);
56 | $this->client->set([
57 | 'timeout' => $this->timeout,
58 | 'websocket_compression' => true,
59 | 'max_frame_size' => 32 * 1024 * 1024, // 32MB max frame size
60 | ]);
61 |
62 | if (!empty($this->headers)) {
63 | $this->client->setHeaders($this->headers);
64 | }
65 |
66 | $success = $this->client->upgrade($this->path);
67 |
68 | if (!$success) {
69 | $error = new \RuntimeException(
70 | "WebSocket connection failed: {$this->client->errCode} - {$this->client->errMsg}"
71 | );
72 | $this->emit('error', $error);
73 | throw $error;
74 | }
75 |
76 | $this->connected = true;
77 | $this->emit('open');
78 | }
79 |
80 | public function listen(): void
81 | {
82 | while ($this->connected) {
83 | try {
84 | $frame = $this->client->recv($this->timeout);
85 |
86 | if ($frame === false) {
87 | if ($this->client->errCode === SWOOLE_ERROR_CLIENT_NO_CONNECTION) {
88 | $this->handleClose();
89 | break;
90 | }
91 | if ($this->client->errCode === 110) {
92 | continue;
93 | }
94 | throw new \RuntimeException(
95 | "Failed to receive data: {$this->client->errCode} - {$this->client->errMsg}"
96 | );
97 | }
98 |
99 | if ($frame === "") {
100 | continue;
101 | }
102 |
103 | if ($frame instanceof Frame) {
104 | $this->handleFrame($frame);
105 | }
106 | } catch (\Throwable $e) {
107 | $this->emit('error', $e);
108 | $this->handleClose();
109 | break;
110 | }
111 | }
112 | }
113 |
114 | private function handleFrame(Frame $frame): void
115 | {
116 | switch ($frame->opcode) {
117 | case WEBSOCKET_OPCODE_TEXT:
118 | $this->emit('message', $frame->data);
119 | break;
120 | case WEBSOCKET_OPCODE_CLOSE:
121 | $this->handleClose();
122 | break;
123 | case WEBSOCKET_OPCODE_PING:
124 | $this->emit('ping', $frame->data);
125 | $this->client->push('', WEBSOCKET_OPCODE_PONG);
126 | break;
127 | case WEBSOCKET_OPCODE_PONG:
128 | $this->emit('pong', $frame->data);
129 | break;
130 | }
131 | }
132 |
133 | private function handleClose(): void
134 | {
135 | if ($this->connected) {
136 | $this->connected = false;
137 | $this->emit('close');
138 | $this->client->close();
139 | }
140 | }
141 |
142 | public function send(string $data): void
143 | {
144 | if (!$this->connected) {
145 | throw new \RuntimeException('Not connected to WebSocket server');
146 | }
147 |
148 | $success = $this->client->push($data);
149 |
150 | if ($success === false) {
151 | $error = new \RuntimeException(
152 | "Failed to send data: {$this->client->errCode} - {$this->client->errMsg}"
153 | );
154 | $this->emit('error', $error);
155 | throw $error;
156 | }
157 | }
158 |
159 | public function close(): void
160 | {
161 | $this->handleClose();
162 | }
163 |
164 | public function isConnected(): bool
165 | {
166 | return $this->connected;
167 | }
168 |
169 | // Event handling methods
170 | public function onMessage(\Closure $callback): self
171 | {
172 | $this->onMessage = $callback;
173 | return $this;
174 | }
175 |
176 | public function onClose(\Closure $callback): self
177 | {
178 | $this->onClose = $callback;
179 | return $this;
180 | }
181 |
182 | public function onError(\Closure $callback): self
183 | {
184 | $this->onError = $callback;
185 | return $this;
186 | }
187 |
188 | public function onOpen(\Closure $callback): self
189 | {
190 | $this->onOpen = $callback;
191 | return $this;
192 | }
193 |
194 | public function onPing(\Closure $callback): self
195 | {
196 | $this->onPing = $callback;
197 | return $this;
198 | }
199 |
200 | public function onPong(\Closure $callback): self
201 | {
202 | $this->onPong = $callback;
203 | return $this;
204 | }
205 |
206 | /**
207 | * @param string $event
208 | * @param mixed $data
209 | */
210 | private function emit(string $event, mixed $data = null): void
211 | {
212 | $handler = match ($event) {
213 | 'message' => $this->onMessage,
214 | 'close' => $this->onClose,
215 | 'error' => $this->onError,
216 | 'open' => $this->onOpen,
217 | 'ping' => $this->onPing,
218 | 'pong' => $this->onPong,
219 | default => null
220 | };
221 |
222 | if ($handler !== null) {
223 | $handler($data);
224 | }
225 | }
226 |
227 | public function receive(): ?string
228 | {
229 | if (!$this->connected) {
230 | throw new \RuntimeException('Not connected to WebSocket server');
231 | }
232 |
233 | $frame = $this->client->recv($this->timeout);
234 |
235 | if ($frame === false) {
236 | if ($this->client->errCode === SWOOLE_ERROR_CLIENT_NO_CONNECTION) {
237 | $this->handleClose();
238 | return null;
239 | }
240 | throw new \RuntimeException(
241 | "Failed to receive data: {$this->client->errCode} - {$this->client->errMsg}"
242 | );
243 | }
244 |
245 | if ($frame === "") {
246 | return null;
247 | }
248 |
249 | if ($frame instanceof Frame) {
250 | switch ($frame->opcode) {
251 | case WEBSOCKET_OPCODE_TEXT:
252 | $this->emit('message', $frame->data);
253 | return $frame->data;
254 | case WEBSOCKET_OPCODE_CLOSE:
255 | $this->handleClose();
256 | return null;
257 | case WEBSOCKET_OPCODE_PING:
258 | $this->emit('ping', $frame->data);
259 | $this->client->push('', WEBSOCKET_OPCODE_PONG);
260 | return null;
261 | case WEBSOCKET_OPCODE_PONG:
262 | $this->emit('pong', $frame->data);
263 | return null;
264 | }
265 | }
266 |
267 | return null;
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "c7a4fa828d98ef53ef4d74783e227ebf",
8 | "packages": [],
9 | "packages-dev": [
10 | {
11 | "name": "doctrine/instantiator",
12 | "version": "2.0.0",
13 | "source": {
14 | "type": "git",
15 | "url": "https://github.com/doctrine/instantiator.git",
16 | "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
17 | },
18 | "dist": {
19 | "type": "zip",
20 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
21 | "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
22 | "shasum": ""
23 | },
24 | "require": {
25 | "php": "^8.1"
26 | },
27 | "require-dev": {
28 | "doctrine/coding-standard": "^11",
29 | "ext-pdo": "*",
30 | "ext-phar": "*",
31 | "phpbench/phpbench": "^1.2",
32 | "phpstan/phpstan": "^1.9.4",
33 | "phpstan/phpstan-phpunit": "^1.3",
34 | "phpunit/phpunit": "^9.5.27",
35 | "vimeo/psalm": "^5.4"
36 | },
37 | "type": "library",
38 | "autoload": {
39 | "psr-4": {
40 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
41 | }
42 | },
43 | "notification-url": "https://packagist.org/downloads/",
44 | "license": [
45 | "MIT"
46 | ],
47 | "authors": [
48 | {
49 | "name": "Marco Pivetta",
50 | "email": "ocramius@gmail.com",
51 | "homepage": "https://ocramius.github.io/"
52 | }
53 | ],
54 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
55 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
56 | "keywords": [
57 | "constructor",
58 | "instantiate"
59 | ],
60 | "support": {
61 | "issues": "https://github.com/doctrine/instantiator/issues",
62 | "source": "https://github.com/doctrine/instantiator/tree/2.0.0"
63 | },
64 | "funding": [
65 | {
66 | "url": "https://www.doctrine-project.org/sponsorship.html",
67 | "type": "custom"
68 | },
69 | {
70 | "url": "https://www.patreon.com/phpdoctrine",
71 | "type": "patreon"
72 | },
73 | {
74 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
75 | "type": "tidelift"
76 | }
77 | ],
78 | "time": "2022-12-30T00:23:10+00:00"
79 | },
80 | {
81 | "name": "laravel/pint",
82 | "version": "v1.21.2",
83 | "source": {
84 | "type": "git",
85 | "url": "https://github.com/laravel/pint.git",
86 | "reference": "370772e7d9e9da087678a0edf2b11b6960e40558"
87 | },
88 | "dist": {
89 | "type": "zip",
90 | "url": "https://api.github.com/repos/laravel/pint/zipball/370772e7d9e9da087678a0edf2b11b6960e40558",
91 | "reference": "370772e7d9e9da087678a0edf2b11b6960e40558",
92 | "shasum": ""
93 | },
94 | "require": {
95 | "ext-json": "*",
96 | "ext-mbstring": "*",
97 | "ext-tokenizer": "*",
98 | "ext-xml": "*",
99 | "php": "^8.2.0"
100 | },
101 | "require-dev": {
102 | "friendsofphp/php-cs-fixer": "^3.72.0",
103 | "illuminate/view": "^11.44.2",
104 | "larastan/larastan": "^3.2.0",
105 | "laravel-zero/framework": "^11.36.1",
106 | "mockery/mockery": "^1.6.12",
107 | "nunomaduro/termwind": "^2.3",
108 | "pestphp/pest": "^2.36.0"
109 | },
110 | "bin": [
111 | "builds/pint"
112 | ],
113 | "type": "project",
114 | "autoload": {
115 | "psr-4": {
116 | "App\\": "app/",
117 | "Database\\Seeders\\": "database/seeders/",
118 | "Database\\Factories\\": "database/factories/"
119 | }
120 | },
121 | "notification-url": "https://packagist.org/downloads/",
122 | "license": [
123 | "MIT"
124 | ],
125 | "authors": [
126 | {
127 | "name": "Nuno Maduro",
128 | "email": "enunomaduro@gmail.com"
129 | }
130 | ],
131 | "description": "An opinionated code formatter for PHP.",
132 | "homepage": "https://laravel.com",
133 | "keywords": [
134 | "format",
135 | "formatter",
136 | "lint",
137 | "linter",
138 | "php"
139 | ],
140 | "support": {
141 | "issues": "https://github.com/laravel/pint/issues",
142 | "source": "https://github.com/laravel/pint"
143 | },
144 | "time": "2025-03-14T22:31:42+00:00"
145 | },
146 | {
147 | "name": "myclabs/deep-copy",
148 | "version": "1.13.0",
149 | "source": {
150 | "type": "git",
151 | "url": "https://github.com/myclabs/DeepCopy.git",
152 | "reference": "024473a478be9df5fdaca2c793f2232fe788e414"
153 | },
154 | "dist": {
155 | "type": "zip",
156 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
157 | "reference": "024473a478be9df5fdaca2c793f2232fe788e414",
158 | "shasum": ""
159 | },
160 | "require": {
161 | "php": "^7.1 || ^8.0"
162 | },
163 | "conflict": {
164 | "doctrine/collections": "<1.6.8",
165 | "doctrine/common": "<2.13.3 || >=3 <3.2.2"
166 | },
167 | "require-dev": {
168 | "doctrine/collections": "^1.6.8",
169 | "doctrine/common": "^2.13.3 || ^3.2.2",
170 | "phpspec/prophecy": "^1.10",
171 | "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
172 | },
173 | "type": "library",
174 | "autoload": {
175 | "files": [
176 | "src/DeepCopy/deep_copy.php"
177 | ],
178 | "psr-4": {
179 | "DeepCopy\\": "src/DeepCopy/"
180 | }
181 | },
182 | "notification-url": "https://packagist.org/downloads/",
183 | "license": [
184 | "MIT"
185 | ],
186 | "description": "Create deep copies (clones) of your objects",
187 | "keywords": [
188 | "clone",
189 | "copy",
190 | "duplicate",
191 | "object",
192 | "object graph"
193 | ],
194 | "support": {
195 | "issues": "https://github.com/myclabs/DeepCopy/issues",
196 | "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
197 | },
198 | "funding": [
199 | {
200 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
201 | "type": "tidelift"
202 | }
203 | ],
204 | "time": "2025-02-12T12:17:51+00:00"
205 | },
206 | {
207 | "name": "nikic/php-parser",
208 | "version": "v5.4.0",
209 | "source": {
210 | "type": "git",
211 | "url": "https://github.com/nikic/PHP-Parser.git",
212 | "reference": "447a020a1f875a434d62f2a401f53b82a396e494"
213 | },
214 | "dist": {
215 | "type": "zip",
216 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
217 | "reference": "447a020a1f875a434d62f2a401f53b82a396e494",
218 | "shasum": ""
219 | },
220 | "require": {
221 | "ext-ctype": "*",
222 | "ext-json": "*",
223 | "ext-tokenizer": "*",
224 | "php": ">=7.4"
225 | },
226 | "require-dev": {
227 | "ircmaxell/php-yacc": "^0.0.7",
228 | "phpunit/phpunit": "^9.0"
229 | },
230 | "bin": [
231 | "bin/php-parse"
232 | ],
233 | "type": "library",
234 | "extra": {
235 | "branch-alias": {
236 | "dev-master": "5.0-dev"
237 | }
238 | },
239 | "autoload": {
240 | "psr-4": {
241 | "PhpParser\\": "lib/PhpParser"
242 | }
243 | },
244 | "notification-url": "https://packagist.org/downloads/",
245 | "license": [
246 | "BSD-3-Clause"
247 | ],
248 | "authors": [
249 | {
250 | "name": "Nikita Popov"
251 | }
252 | ],
253 | "description": "A PHP parser written in PHP",
254 | "keywords": [
255 | "parser",
256 | "php"
257 | ],
258 | "support": {
259 | "issues": "https://github.com/nikic/PHP-Parser/issues",
260 | "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
261 | },
262 | "time": "2024-12-30T11:07:19+00:00"
263 | },
264 | {
265 | "name": "phar-io/manifest",
266 | "version": "2.0.4",
267 | "source": {
268 | "type": "git",
269 | "url": "https://github.com/phar-io/manifest.git",
270 | "reference": "54750ef60c58e43759730615a392c31c80e23176"
271 | },
272 | "dist": {
273 | "type": "zip",
274 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
275 | "reference": "54750ef60c58e43759730615a392c31c80e23176",
276 | "shasum": ""
277 | },
278 | "require": {
279 | "ext-dom": "*",
280 | "ext-libxml": "*",
281 | "ext-phar": "*",
282 | "ext-xmlwriter": "*",
283 | "phar-io/version": "^3.0.1",
284 | "php": "^7.2 || ^8.0"
285 | },
286 | "type": "library",
287 | "extra": {
288 | "branch-alias": {
289 | "dev-master": "2.0.x-dev"
290 | }
291 | },
292 | "autoload": {
293 | "classmap": [
294 | "src/"
295 | ]
296 | },
297 | "notification-url": "https://packagist.org/downloads/",
298 | "license": [
299 | "BSD-3-Clause"
300 | ],
301 | "authors": [
302 | {
303 | "name": "Arne Blankerts",
304 | "email": "arne@blankerts.de",
305 | "role": "Developer"
306 | },
307 | {
308 | "name": "Sebastian Heuer",
309 | "email": "sebastian@phpeople.de",
310 | "role": "Developer"
311 | },
312 | {
313 | "name": "Sebastian Bergmann",
314 | "email": "sebastian@phpunit.de",
315 | "role": "Developer"
316 | }
317 | ],
318 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
319 | "support": {
320 | "issues": "https://github.com/phar-io/manifest/issues",
321 | "source": "https://github.com/phar-io/manifest/tree/2.0.4"
322 | },
323 | "funding": [
324 | {
325 | "url": "https://github.com/theseer",
326 | "type": "github"
327 | }
328 | ],
329 | "time": "2024-03-03T12:33:53+00:00"
330 | },
331 | {
332 | "name": "phar-io/version",
333 | "version": "3.2.1",
334 | "source": {
335 | "type": "git",
336 | "url": "https://github.com/phar-io/version.git",
337 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
338 | },
339 | "dist": {
340 | "type": "zip",
341 | "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
342 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
343 | "shasum": ""
344 | },
345 | "require": {
346 | "php": "^7.2 || ^8.0"
347 | },
348 | "type": "library",
349 | "autoload": {
350 | "classmap": [
351 | "src/"
352 | ]
353 | },
354 | "notification-url": "https://packagist.org/downloads/",
355 | "license": [
356 | "BSD-3-Clause"
357 | ],
358 | "authors": [
359 | {
360 | "name": "Arne Blankerts",
361 | "email": "arne@blankerts.de",
362 | "role": "Developer"
363 | },
364 | {
365 | "name": "Sebastian Heuer",
366 | "email": "sebastian@phpeople.de",
367 | "role": "Developer"
368 | },
369 | {
370 | "name": "Sebastian Bergmann",
371 | "email": "sebastian@phpunit.de",
372 | "role": "Developer"
373 | }
374 | ],
375 | "description": "Library for handling version information and constraints",
376 | "support": {
377 | "issues": "https://github.com/phar-io/version/issues",
378 | "source": "https://github.com/phar-io/version/tree/3.2.1"
379 | },
380 | "time": "2022-02-21T01:04:05+00:00"
381 | },
382 | {
383 | "name": "phpstan/phpstan",
384 | "version": "1.12.23",
385 | "source": {
386 | "type": "git",
387 | "url": "https://github.com/phpstan/phpstan.git",
388 | "reference": "29201e7a743a6ab36f91394eab51889a82631428"
389 | },
390 | "dist": {
391 | "type": "zip",
392 | "url": "https://api.github.com/repos/phpstan/phpstan/zipball/29201e7a743a6ab36f91394eab51889a82631428",
393 | "reference": "29201e7a743a6ab36f91394eab51889a82631428",
394 | "shasum": ""
395 | },
396 | "require": {
397 | "php": "^7.2|^8.0"
398 | },
399 | "conflict": {
400 | "phpstan/phpstan-shim": "*"
401 | },
402 | "bin": [
403 | "phpstan",
404 | "phpstan.phar"
405 | ],
406 | "type": "library",
407 | "autoload": {
408 | "files": [
409 | "bootstrap.php"
410 | ]
411 | },
412 | "notification-url": "https://packagist.org/downloads/",
413 | "license": [
414 | "MIT"
415 | ],
416 | "description": "PHPStan - PHP Static Analysis Tool",
417 | "keywords": [
418 | "dev",
419 | "static analysis"
420 | ],
421 | "support": {
422 | "docs": "https://phpstan.org/user-guide/getting-started",
423 | "forum": "https://github.com/phpstan/phpstan/discussions",
424 | "issues": "https://github.com/phpstan/phpstan/issues",
425 | "security": "https://github.com/phpstan/phpstan/security/policy",
426 | "source": "https://github.com/phpstan/phpstan-src"
427 | },
428 | "funding": [
429 | {
430 | "url": "https://github.com/ondrejmirtes",
431 | "type": "github"
432 | },
433 | {
434 | "url": "https://github.com/phpstan",
435 | "type": "github"
436 | }
437 | ],
438 | "time": "2025-03-23T14:57:32+00:00"
439 | },
440 | {
441 | "name": "phpunit/php-code-coverage",
442 | "version": "9.2.32",
443 | "source": {
444 | "type": "git",
445 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
446 | "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5"
447 | },
448 | "dist": {
449 | "type": "zip",
450 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5",
451 | "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5",
452 | "shasum": ""
453 | },
454 | "require": {
455 | "ext-dom": "*",
456 | "ext-libxml": "*",
457 | "ext-xmlwriter": "*",
458 | "nikic/php-parser": "^4.19.1 || ^5.1.0",
459 | "php": ">=7.3",
460 | "phpunit/php-file-iterator": "^3.0.6",
461 | "phpunit/php-text-template": "^2.0.4",
462 | "sebastian/code-unit-reverse-lookup": "^2.0.3",
463 | "sebastian/complexity": "^2.0.3",
464 | "sebastian/environment": "^5.1.5",
465 | "sebastian/lines-of-code": "^1.0.4",
466 | "sebastian/version": "^3.0.2",
467 | "theseer/tokenizer": "^1.2.3"
468 | },
469 | "require-dev": {
470 | "phpunit/phpunit": "^9.6"
471 | },
472 | "suggest": {
473 | "ext-pcov": "PHP extension that provides line coverage",
474 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
475 | },
476 | "type": "library",
477 | "extra": {
478 | "branch-alias": {
479 | "dev-main": "9.2.x-dev"
480 | }
481 | },
482 | "autoload": {
483 | "classmap": [
484 | "src/"
485 | ]
486 | },
487 | "notification-url": "https://packagist.org/downloads/",
488 | "license": [
489 | "BSD-3-Clause"
490 | ],
491 | "authors": [
492 | {
493 | "name": "Sebastian Bergmann",
494 | "email": "sebastian@phpunit.de",
495 | "role": "lead"
496 | }
497 | ],
498 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
499 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
500 | "keywords": [
501 | "coverage",
502 | "testing",
503 | "xunit"
504 | ],
505 | "support": {
506 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
507 | "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
508 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32"
509 | },
510 | "funding": [
511 | {
512 | "url": "https://github.com/sebastianbergmann",
513 | "type": "github"
514 | }
515 | ],
516 | "time": "2024-08-22T04:23:01+00:00"
517 | },
518 | {
519 | "name": "phpunit/php-file-iterator",
520 | "version": "3.0.6",
521 | "source": {
522 | "type": "git",
523 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
524 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
525 | },
526 | "dist": {
527 | "type": "zip",
528 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
529 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
530 | "shasum": ""
531 | },
532 | "require": {
533 | "php": ">=7.3"
534 | },
535 | "require-dev": {
536 | "phpunit/phpunit": "^9.3"
537 | },
538 | "type": "library",
539 | "extra": {
540 | "branch-alias": {
541 | "dev-master": "3.0-dev"
542 | }
543 | },
544 | "autoload": {
545 | "classmap": [
546 | "src/"
547 | ]
548 | },
549 | "notification-url": "https://packagist.org/downloads/",
550 | "license": [
551 | "BSD-3-Clause"
552 | ],
553 | "authors": [
554 | {
555 | "name": "Sebastian Bergmann",
556 | "email": "sebastian@phpunit.de",
557 | "role": "lead"
558 | }
559 | ],
560 | "description": "FilterIterator implementation that filters files based on a list of suffixes.",
561 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
562 | "keywords": [
563 | "filesystem",
564 | "iterator"
565 | ],
566 | "support": {
567 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
568 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
569 | },
570 | "funding": [
571 | {
572 | "url": "https://github.com/sebastianbergmann",
573 | "type": "github"
574 | }
575 | ],
576 | "time": "2021-12-02T12:48:52+00:00"
577 | },
578 | {
579 | "name": "phpunit/php-invoker",
580 | "version": "3.1.1",
581 | "source": {
582 | "type": "git",
583 | "url": "https://github.com/sebastianbergmann/php-invoker.git",
584 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
585 | },
586 | "dist": {
587 | "type": "zip",
588 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
589 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
590 | "shasum": ""
591 | },
592 | "require": {
593 | "php": ">=7.3"
594 | },
595 | "require-dev": {
596 | "ext-pcntl": "*",
597 | "phpunit/phpunit": "^9.3"
598 | },
599 | "suggest": {
600 | "ext-pcntl": "*"
601 | },
602 | "type": "library",
603 | "extra": {
604 | "branch-alias": {
605 | "dev-master": "3.1-dev"
606 | }
607 | },
608 | "autoload": {
609 | "classmap": [
610 | "src/"
611 | ]
612 | },
613 | "notification-url": "https://packagist.org/downloads/",
614 | "license": [
615 | "BSD-3-Clause"
616 | ],
617 | "authors": [
618 | {
619 | "name": "Sebastian Bergmann",
620 | "email": "sebastian@phpunit.de",
621 | "role": "lead"
622 | }
623 | ],
624 | "description": "Invoke callables with a timeout",
625 | "homepage": "https://github.com/sebastianbergmann/php-invoker/",
626 | "keywords": [
627 | "process"
628 | ],
629 | "support": {
630 | "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
631 | "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
632 | },
633 | "funding": [
634 | {
635 | "url": "https://github.com/sebastianbergmann",
636 | "type": "github"
637 | }
638 | ],
639 | "time": "2020-09-28T05:58:55+00:00"
640 | },
641 | {
642 | "name": "phpunit/php-text-template",
643 | "version": "2.0.4",
644 | "source": {
645 | "type": "git",
646 | "url": "https://github.com/sebastianbergmann/php-text-template.git",
647 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
648 | },
649 | "dist": {
650 | "type": "zip",
651 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
652 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
653 | "shasum": ""
654 | },
655 | "require": {
656 | "php": ">=7.3"
657 | },
658 | "require-dev": {
659 | "phpunit/phpunit": "^9.3"
660 | },
661 | "type": "library",
662 | "extra": {
663 | "branch-alias": {
664 | "dev-master": "2.0-dev"
665 | }
666 | },
667 | "autoload": {
668 | "classmap": [
669 | "src/"
670 | ]
671 | },
672 | "notification-url": "https://packagist.org/downloads/",
673 | "license": [
674 | "BSD-3-Clause"
675 | ],
676 | "authors": [
677 | {
678 | "name": "Sebastian Bergmann",
679 | "email": "sebastian@phpunit.de",
680 | "role": "lead"
681 | }
682 | ],
683 | "description": "Simple template engine.",
684 | "homepage": "https://github.com/sebastianbergmann/php-text-template/",
685 | "keywords": [
686 | "template"
687 | ],
688 | "support": {
689 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
690 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
691 | },
692 | "funding": [
693 | {
694 | "url": "https://github.com/sebastianbergmann",
695 | "type": "github"
696 | }
697 | ],
698 | "time": "2020-10-26T05:33:50+00:00"
699 | },
700 | {
701 | "name": "phpunit/php-timer",
702 | "version": "5.0.3",
703 | "source": {
704 | "type": "git",
705 | "url": "https://github.com/sebastianbergmann/php-timer.git",
706 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
707 | },
708 | "dist": {
709 | "type": "zip",
710 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
711 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
712 | "shasum": ""
713 | },
714 | "require": {
715 | "php": ">=7.3"
716 | },
717 | "require-dev": {
718 | "phpunit/phpunit": "^9.3"
719 | },
720 | "type": "library",
721 | "extra": {
722 | "branch-alias": {
723 | "dev-master": "5.0-dev"
724 | }
725 | },
726 | "autoload": {
727 | "classmap": [
728 | "src/"
729 | ]
730 | },
731 | "notification-url": "https://packagist.org/downloads/",
732 | "license": [
733 | "BSD-3-Clause"
734 | ],
735 | "authors": [
736 | {
737 | "name": "Sebastian Bergmann",
738 | "email": "sebastian@phpunit.de",
739 | "role": "lead"
740 | }
741 | ],
742 | "description": "Utility class for timing",
743 | "homepage": "https://github.com/sebastianbergmann/php-timer/",
744 | "keywords": [
745 | "timer"
746 | ],
747 | "support": {
748 | "issues": "https://github.com/sebastianbergmann/php-timer/issues",
749 | "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
750 | },
751 | "funding": [
752 | {
753 | "url": "https://github.com/sebastianbergmann",
754 | "type": "github"
755 | }
756 | ],
757 | "time": "2020-10-26T13:16:10+00:00"
758 | },
759 | {
760 | "name": "phpunit/phpunit",
761 | "version": "9.6.22",
762 | "source": {
763 | "type": "git",
764 | "url": "https://github.com/sebastianbergmann/phpunit.git",
765 | "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c"
766 | },
767 | "dist": {
768 | "type": "zip",
769 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c",
770 | "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c",
771 | "shasum": ""
772 | },
773 | "require": {
774 | "doctrine/instantiator": "^1.5.0 || ^2",
775 | "ext-dom": "*",
776 | "ext-json": "*",
777 | "ext-libxml": "*",
778 | "ext-mbstring": "*",
779 | "ext-xml": "*",
780 | "ext-xmlwriter": "*",
781 | "myclabs/deep-copy": "^1.12.1",
782 | "phar-io/manifest": "^2.0.4",
783 | "phar-io/version": "^3.2.1",
784 | "php": ">=7.3",
785 | "phpunit/php-code-coverage": "^9.2.32",
786 | "phpunit/php-file-iterator": "^3.0.6",
787 | "phpunit/php-invoker": "^3.1.1",
788 | "phpunit/php-text-template": "^2.0.4",
789 | "phpunit/php-timer": "^5.0.3",
790 | "sebastian/cli-parser": "^1.0.2",
791 | "sebastian/code-unit": "^1.0.8",
792 | "sebastian/comparator": "^4.0.8",
793 | "sebastian/diff": "^4.0.6",
794 | "sebastian/environment": "^5.1.5",
795 | "sebastian/exporter": "^4.0.6",
796 | "sebastian/global-state": "^5.0.7",
797 | "sebastian/object-enumerator": "^4.0.4",
798 | "sebastian/resource-operations": "^3.0.4",
799 | "sebastian/type": "^3.2.1",
800 | "sebastian/version": "^3.0.2"
801 | },
802 | "suggest": {
803 | "ext-soap": "To be able to generate mocks based on WSDL files",
804 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
805 | },
806 | "bin": [
807 | "phpunit"
808 | ],
809 | "type": "library",
810 | "extra": {
811 | "branch-alias": {
812 | "dev-master": "9.6-dev"
813 | }
814 | },
815 | "autoload": {
816 | "files": [
817 | "src/Framework/Assert/Functions.php"
818 | ],
819 | "classmap": [
820 | "src/"
821 | ]
822 | },
823 | "notification-url": "https://packagist.org/downloads/",
824 | "license": [
825 | "BSD-3-Clause"
826 | ],
827 | "authors": [
828 | {
829 | "name": "Sebastian Bergmann",
830 | "email": "sebastian@phpunit.de",
831 | "role": "lead"
832 | }
833 | ],
834 | "description": "The PHP Unit Testing framework.",
835 | "homepage": "https://phpunit.de/",
836 | "keywords": [
837 | "phpunit",
838 | "testing",
839 | "xunit"
840 | ],
841 | "support": {
842 | "issues": "https://github.com/sebastianbergmann/phpunit/issues",
843 | "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
844 | "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22"
845 | },
846 | "funding": [
847 | {
848 | "url": "https://phpunit.de/sponsors.html",
849 | "type": "custom"
850 | },
851 | {
852 | "url": "https://github.com/sebastianbergmann",
853 | "type": "github"
854 | },
855 | {
856 | "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
857 | "type": "tidelift"
858 | }
859 | ],
860 | "time": "2024-12-05T13:48:26+00:00"
861 | },
862 | {
863 | "name": "psr/log",
864 | "version": "1.1.4",
865 | "source": {
866 | "type": "git",
867 | "url": "https://github.com/php-fig/log.git",
868 | "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
869 | },
870 | "dist": {
871 | "type": "zip",
872 | "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
873 | "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
874 | "shasum": ""
875 | },
876 | "require": {
877 | "php": ">=5.3.0"
878 | },
879 | "type": "library",
880 | "extra": {
881 | "branch-alias": {
882 | "dev-master": "1.1.x-dev"
883 | }
884 | },
885 | "autoload": {
886 | "psr-4": {
887 | "Psr\\Log\\": "Psr/Log/"
888 | }
889 | },
890 | "notification-url": "https://packagist.org/downloads/",
891 | "license": [
892 | "MIT"
893 | ],
894 | "authors": [
895 | {
896 | "name": "PHP-FIG",
897 | "homepage": "https://www.php-fig.org/"
898 | }
899 | ],
900 | "description": "Common interface for logging libraries",
901 | "homepage": "https://github.com/php-fig/log",
902 | "keywords": [
903 | "log",
904 | "psr",
905 | "psr-3"
906 | ],
907 | "support": {
908 | "source": "https://github.com/php-fig/log/tree/1.1.4"
909 | },
910 | "time": "2021-05-03T11:20:27+00:00"
911 | },
912 | {
913 | "name": "sebastian/cli-parser",
914 | "version": "1.0.2",
915 | "source": {
916 | "type": "git",
917 | "url": "https://github.com/sebastianbergmann/cli-parser.git",
918 | "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
919 | },
920 | "dist": {
921 | "type": "zip",
922 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
923 | "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
924 | "shasum": ""
925 | },
926 | "require": {
927 | "php": ">=7.3"
928 | },
929 | "require-dev": {
930 | "phpunit/phpunit": "^9.3"
931 | },
932 | "type": "library",
933 | "extra": {
934 | "branch-alias": {
935 | "dev-master": "1.0-dev"
936 | }
937 | },
938 | "autoload": {
939 | "classmap": [
940 | "src/"
941 | ]
942 | },
943 | "notification-url": "https://packagist.org/downloads/",
944 | "license": [
945 | "BSD-3-Clause"
946 | ],
947 | "authors": [
948 | {
949 | "name": "Sebastian Bergmann",
950 | "email": "sebastian@phpunit.de",
951 | "role": "lead"
952 | }
953 | ],
954 | "description": "Library for parsing CLI options",
955 | "homepage": "https://github.com/sebastianbergmann/cli-parser",
956 | "support": {
957 | "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
958 | "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
959 | },
960 | "funding": [
961 | {
962 | "url": "https://github.com/sebastianbergmann",
963 | "type": "github"
964 | }
965 | ],
966 | "time": "2024-03-02T06:27:43+00:00"
967 | },
968 | {
969 | "name": "sebastian/code-unit",
970 | "version": "1.0.8",
971 | "source": {
972 | "type": "git",
973 | "url": "https://github.com/sebastianbergmann/code-unit.git",
974 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
975 | },
976 | "dist": {
977 | "type": "zip",
978 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
979 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
980 | "shasum": ""
981 | },
982 | "require": {
983 | "php": ">=7.3"
984 | },
985 | "require-dev": {
986 | "phpunit/phpunit": "^9.3"
987 | },
988 | "type": "library",
989 | "extra": {
990 | "branch-alias": {
991 | "dev-master": "1.0-dev"
992 | }
993 | },
994 | "autoload": {
995 | "classmap": [
996 | "src/"
997 | ]
998 | },
999 | "notification-url": "https://packagist.org/downloads/",
1000 | "license": [
1001 | "BSD-3-Clause"
1002 | ],
1003 | "authors": [
1004 | {
1005 | "name": "Sebastian Bergmann",
1006 | "email": "sebastian@phpunit.de",
1007 | "role": "lead"
1008 | }
1009 | ],
1010 | "description": "Collection of value objects that represent the PHP code units",
1011 | "homepage": "https://github.com/sebastianbergmann/code-unit",
1012 | "support": {
1013 | "issues": "https://github.com/sebastianbergmann/code-unit/issues",
1014 | "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
1015 | },
1016 | "funding": [
1017 | {
1018 | "url": "https://github.com/sebastianbergmann",
1019 | "type": "github"
1020 | }
1021 | ],
1022 | "time": "2020-10-26T13:08:54+00:00"
1023 | },
1024 | {
1025 | "name": "sebastian/code-unit-reverse-lookup",
1026 | "version": "2.0.3",
1027 | "source": {
1028 | "type": "git",
1029 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
1030 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
1031 | },
1032 | "dist": {
1033 | "type": "zip",
1034 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
1035 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
1036 | "shasum": ""
1037 | },
1038 | "require": {
1039 | "php": ">=7.3"
1040 | },
1041 | "require-dev": {
1042 | "phpunit/phpunit": "^9.3"
1043 | },
1044 | "type": "library",
1045 | "extra": {
1046 | "branch-alias": {
1047 | "dev-master": "2.0-dev"
1048 | }
1049 | },
1050 | "autoload": {
1051 | "classmap": [
1052 | "src/"
1053 | ]
1054 | },
1055 | "notification-url": "https://packagist.org/downloads/",
1056 | "license": [
1057 | "BSD-3-Clause"
1058 | ],
1059 | "authors": [
1060 | {
1061 | "name": "Sebastian Bergmann",
1062 | "email": "sebastian@phpunit.de"
1063 | }
1064 | ],
1065 | "description": "Looks up which function or method a line of code belongs to",
1066 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
1067 | "support": {
1068 | "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
1069 | "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
1070 | },
1071 | "funding": [
1072 | {
1073 | "url": "https://github.com/sebastianbergmann",
1074 | "type": "github"
1075 | }
1076 | ],
1077 | "time": "2020-09-28T05:30:19+00:00"
1078 | },
1079 | {
1080 | "name": "sebastian/comparator",
1081 | "version": "4.0.8",
1082 | "source": {
1083 | "type": "git",
1084 | "url": "https://github.com/sebastianbergmann/comparator.git",
1085 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
1086 | },
1087 | "dist": {
1088 | "type": "zip",
1089 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
1090 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
1091 | "shasum": ""
1092 | },
1093 | "require": {
1094 | "php": ">=7.3",
1095 | "sebastian/diff": "^4.0",
1096 | "sebastian/exporter": "^4.0"
1097 | },
1098 | "require-dev": {
1099 | "phpunit/phpunit": "^9.3"
1100 | },
1101 | "type": "library",
1102 | "extra": {
1103 | "branch-alias": {
1104 | "dev-master": "4.0-dev"
1105 | }
1106 | },
1107 | "autoload": {
1108 | "classmap": [
1109 | "src/"
1110 | ]
1111 | },
1112 | "notification-url": "https://packagist.org/downloads/",
1113 | "license": [
1114 | "BSD-3-Clause"
1115 | ],
1116 | "authors": [
1117 | {
1118 | "name": "Sebastian Bergmann",
1119 | "email": "sebastian@phpunit.de"
1120 | },
1121 | {
1122 | "name": "Jeff Welch",
1123 | "email": "whatthejeff@gmail.com"
1124 | },
1125 | {
1126 | "name": "Volker Dusch",
1127 | "email": "github@wallbash.com"
1128 | },
1129 | {
1130 | "name": "Bernhard Schussek",
1131 | "email": "bschussek@2bepublished.at"
1132 | }
1133 | ],
1134 | "description": "Provides the functionality to compare PHP values for equality",
1135 | "homepage": "https://github.com/sebastianbergmann/comparator",
1136 | "keywords": [
1137 | "comparator",
1138 | "compare",
1139 | "equality"
1140 | ],
1141 | "support": {
1142 | "issues": "https://github.com/sebastianbergmann/comparator/issues",
1143 | "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
1144 | },
1145 | "funding": [
1146 | {
1147 | "url": "https://github.com/sebastianbergmann",
1148 | "type": "github"
1149 | }
1150 | ],
1151 | "time": "2022-09-14T12:41:17+00:00"
1152 | },
1153 | {
1154 | "name": "sebastian/complexity",
1155 | "version": "2.0.3",
1156 | "source": {
1157 | "type": "git",
1158 | "url": "https://github.com/sebastianbergmann/complexity.git",
1159 | "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
1160 | },
1161 | "dist": {
1162 | "type": "zip",
1163 | "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
1164 | "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
1165 | "shasum": ""
1166 | },
1167 | "require": {
1168 | "nikic/php-parser": "^4.18 || ^5.0",
1169 | "php": ">=7.3"
1170 | },
1171 | "require-dev": {
1172 | "phpunit/phpunit": "^9.3"
1173 | },
1174 | "type": "library",
1175 | "extra": {
1176 | "branch-alias": {
1177 | "dev-master": "2.0-dev"
1178 | }
1179 | },
1180 | "autoload": {
1181 | "classmap": [
1182 | "src/"
1183 | ]
1184 | },
1185 | "notification-url": "https://packagist.org/downloads/",
1186 | "license": [
1187 | "BSD-3-Clause"
1188 | ],
1189 | "authors": [
1190 | {
1191 | "name": "Sebastian Bergmann",
1192 | "email": "sebastian@phpunit.de",
1193 | "role": "lead"
1194 | }
1195 | ],
1196 | "description": "Library for calculating the complexity of PHP code units",
1197 | "homepage": "https://github.com/sebastianbergmann/complexity",
1198 | "support": {
1199 | "issues": "https://github.com/sebastianbergmann/complexity/issues",
1200 | "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
1201 | },
1202 | "funding": [
1203 | {
1204 | "url": "https://github.com/sebastianbergmann",
1205 | "type": "github"
1206 | }
1207 | ],
1208 | "time": "2023-12-22T06:19:30+00:00"
1209 | },
1210 | {
1211 | "name": "sebastian/diff",
1212 | "version": "4.0.6",
1213 | "source": {
1214 | "type": "git",
1215 | "url": "https://github.com/sebastianbergmann/diff.git",
1216 | "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
1217 | },
1218 | "dist": {
1219 | "type": "zip",
1220 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
1221 | "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
1222 | "shasum": ""
1223 | },
1224 | "require": {
1225 | "php": ">=7.3"
1226 | },
1227 | "require-dev": {
1228 | "phpunit/phpunit": "^9.3",
1229 | "symfony/process": "^4.2 || ^5"
1230 | },
1231 | "type": "library",
1232 | "extra": {
1233 | "branch-alias": {
1234 | "dev-master": "4.0-dev"
1235 | }
1236 | },
1237 | "autoload": {
1238 | "classmap": [
1239 | "src/"
1240 | ]
1241 | },
1242 | "notification-url": "https://packagist.org/downloads/",
1243 | "license": [
1244 | "BSD-3-Clause"
1245 | ],
1246 | "authors": [
1247 | {
1248 | "name": "Sebastian Bergmann",
1249 | "email": "sebastian@phpunit.de"
1250 | },
1251 | {
1252 | "name": "Kore Nordmann",
1253 | "email": "mail@kore-nordmann.de"
1254 | }
1255 | ],
1256 | "description": "Diff implementation",
1257 | "homepage": "https://github.com/sebastianbergmann/diff",
1258 | "keywords": [
1259 | "diff",
1260 | "udiff",
1261 | "unidiff",
1262 | "unified diff"
1263 | ],
1264 | "support": {
1265 | "issues": "https://github.com/sebastianbergmann/diff/issues",
1266 | "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
1267 | },
1268 | "funding": [
1269 | {
1270 | "url": "https://github.com/sebastianbergmann",
1271 | "type": "github"
1272 | }
1273 | ],
1274 | "time": "2024-03-02T06:30:58+00:00"
1275 | },
1276 | {
1277 | "name": "sebastian/environment",
1278 | "version": "5.1.5",
1279 | "source": {
1280 | "type": "git",
1281 | "url": "https://github.com/sebastianbergmann/environment.git",
1282 | "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
1283 | },
1284 | "dist": {
1285 | "type": "zip",
1286 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
1287 | "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
1288 | "shasum": ""
1289 | },
1290 | "require": {
1291 | "php": ">=7.3"
1292 | },
1293 | "require-dev": {
1294 | "phpunit/phpunit": "^9.3"
1295 | },
1296 | "suggest": {
1297 | "ext-posix": "*"
1298 | },
1299 | "type": "library",
1300 | "extra": {
1301 | "branch-alias": {
1302 | "dev-master": "5.1-dev"
1303 | }
1304 | },
1305 | "autoload": {
1306 | "classmap": [
1307 | "src/"
1308 | ]
1309 | },
1310 | "notification-url": "https://packagist.org/downloads/",
1311 | "license": [
1312 | "BSD-3-Clause"
1313 | ],
1314 | "authors": [
1315 | {
1316 | "name": "Sebastian Bergmann",
1317 | "email": "sebastian@phpunit.de"
1318 | }
1319 | ],
1320 | "description": "Provides functionality to handle HHVM/PHP environments",
1321 | "homepage": "http://www.github.com/sebastianbergmann/environment",
1322 | "keywords": [
1323 | "Xdebug",
1324 | "environment",
1325 | "hhvm"
1326 | ],
1327 | "support": {
1328 | "issues": "https://github.com/sebastianbergmann/environment/issues",
1329 | "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
1330 | },
1331 | "funding": [
1332 | {
1333 | "url": "https://github.com/sebastianbergmann",
1334 | "type": "github"
1335 | }
1336 | ],
1337 | "time": "2023-02-03T06:03:51+00:00"
1338 | },
1339 | {
1340 | "name": "sebastian/exporter",
1341 | "version": "4.0.6",
1342 | "source": {
1343 | "type": "git",
1344 | "url": "https://github.com/sebastianbergmann/exporter.git",
1345 | "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
1346 | },
1347 | "dist": {
1348 | "type": "zip",
1349 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
1350 | "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
1351 | "shasum": ""
1352 | },
1353 | "require": {
1354 | "php": ">=7.3",
1355 | "sebastian/recursion-context": "^4.0"
1356 | },
1357 | "require-dev": {
1358 | "ext-mbstring": "*",
1359 | "phpunit/phpunit": "^9.3"
1360 | },
1361 | "type": "library",
1362 | "extra": {
1363 | "branch-alias": {
1364 | "dev-master": "4.0-dev"
1365 | }
1366 | },
1367 | "autoload": {
1368 | "classmap": [
1369 | "src/"
1370 | ]
1371 | },
1372 | "notification-url": "https://packagist.org/downloads/",
1373 | "license": [
1374 | "BSD-3-Clause"
1375 | ],
1376 | "authors": [
1377 | {
1378 | "name": "Sebastian Bergmann",
1379 | "email": "sebastian@phpunit.de"
1380 | },
1381 | {
1382 | "name": "Jeff Welch",
1383 | "email": "whatthejeff@gmail.com"
1384 | },
1385 | {
1386 | "name": "Volker Dusch",
1387 | "email": "github@wallbash.com"
1388 | },
1389 | {
1390 | "name": "Adam Harvey",
1391 | "email": "aharvey@php.net"
1392 | },
1393 | {
1394 | "name": "Bernhard Schussek",
1395 | "email": "bschussek@gmail.com"
1396 | }
1397 | ],
1398 | "description": "Provides the functionality to export PHP variables for visualization",
1399 | "homepage": "https://www.github.com/sebastianbergmann/exporter",
1400 | "keywords": [
1401 | "export",
1402 | "exporter"
1403 | ],
1404 | "support": {
1405 | "issues": "https://github.com/sebastianbergmann/exporter/issues",
1406 | "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
1407 | },
1408 | "funding": [
1409 | {
1410 | "url": "https://github.com/sebastianbergmann",
1411 | "type": "github"
1412 | }
1413 | ],
1414 | "time": "2024-03-02T06:33:00+00:00"
1415 | },
1416 | {
1417 | "name": "sebastian/global-state",
1418 | "version": "5.0.7",
1419 | "source": {
1420 | "type": "git",
1421 | "url": "https://github.com/sebastianbergmann/global-state.git",
1422 | "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
1423 | },
1424 | "dist": {
1425 | "type": "zip",
1426 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
1427 | "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
1428 | "shasum": ""
1429 | },
1430 | "require": {
1431 | "php": ">=7.3",
1432 | "sebastian/object-reflector": "^2.0",
1433 | "sebastian/recursion-context": "^4.0"
1434 | },
1435 | "require-dev": {
1436 | "ext-dom": "*",
1437 | "phpunit/phpunit": "^9.3"
1438 | },
1439 | "suggest": {
1440 | "ext-uopz": "*"
1441 | },
1442 | "type": "library",
1443 | "extra": {
1444 | "branch-alias": {
1445 | "dev-master": "5.0-dev"
1446 | }
1447 | },
1448 | "autoload": {
1449 | "classmap": [
1450 | "src/"
1451 | ]
1452 | },
1453 | "notification-url": "https://packagist.org/downloads/",
1454 | "license": [
1455 | "BSD-3-Clause"
1456 | ],
1457 | "authors": [
1458 | {
1459 | "name": "Sebastian Bergmann",
1460 | "email": "sebastian@phpunit.de"
1461 | }
1462 | ],
1463 | "description": "Snapshotting of global state",
1464 | "homepage": "http://www.github.com/sebastianbergmann/global-state",
1465 | "keywords": [
1466 | "global state"
1467 | ],
1468 | "support": {
1469 | "issues": "https://github.com/sebastianbergmann/global-state/issues",
1470 | "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
1471 | },
1472 | "funding": [
1473 | {
1474 | "url": "https://github.com/sebastianbergmann",
1475 | "type": "github"
1476 | }
1477 | ],
1478 | "time": "2024-03-02T06:35:11+00:00"
1479 | },
1480 | {
1481 | "name": "sebastian/lines-of-code",
1482 | "version": "1.0.4",
1483 | "source": {
1484 | "type": "git",
1485 | "url": "https://github.com/sebastianbergmann/lines-of-code.git",
1486 | "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
1487 | },
1488 | "dist": {
1489 | "type": "zip",
1490 | "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
1491 | "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
1492 | "shasum": ""
1493 | },
1494 | "require": {
1495 | "nikic/php-parser": "^4.18 || ^5.0",
1496 | "php": ">=7.3"
1497 | },
1498 | "require-dev": {
1499 | "phpunit/phpunit": "^9.3"
1500 | },
1501 | "type": "library",
1502 | "extra": {
1503 | "branch-alias": {
1504 | "dev-master": "1.0-dev"
1505 | }
1506 | },
1507 | "autoload": {
1508 | "classmap": [
1509 | "src/"
1510 | ]
1511 | },
1512 | "notification-url": "https://packagist.org/downloads/",
1513 | "license": [
1514 | "BSD-3-Clause"
1515 | ],
1516 | "authors": [
1517 | {
1518 | "name": "Sebastian Bergmann",
1519 | "email": "sebastian@phpunit.de",
1520 | "role": "lead"
1521 | }
1522 | ],
1523 | "description": "Library for counting the lines of code in PHP source code",
1524 | "homepage": "https://github.com/sebastianbergmann/lines-of-code",
1525 | "support": {
1526 | "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
1527 | "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
1528 | },
1529 | "funding": [
1530 | {
1531 | "url": "https://github.com/sebastianbergmann",
1532 | "type": "github"
1533 | }
1534 | ],
1535 | "time": "2023-12-22T06:20:34+00:00"
1536 | },
1537 | {
1538 | "name": "sebastian/object-enumerator",
1539 | "version": "4.0.4",
1540 | "source": {
1541 | "type": "git",
1542 | "url": "https://github.com/sebastianbergmann/object-enumerator.git",
1543 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
1544 | },
1545 | "dist": {
1546 | "type": "zip",
1547 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
1548 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
1549 | "shasum": ""
1550 | },
1551 | "require": {
1552 | "php": ">=7.3",
1553 | "sebastian/object-reflector": "^2.0",
1554 | "sebastian/recursion-context": "^4.0"
1555 | },
1556 | "require-dev": {
1557 | "phpunit/phpunit": "^9.3"
1558 | },
1559 | "type": "library",
1560 | "extra": {
1561 | "branch-alias": {
1562 | "dev-master": "4.0-dev"
1563 | }
1564 | },
1565 | "autoload": {
1566 | "classmap": [
1567 | "src/"
1568 | ]
1569 | },
1570 | "notification-url": "https://packagist.org/downloads/",
1571 | "license": [
1572 | "BSD-3-Clause"
1573 | ],
1574 | "authors": [
1575 | {
1576 | "name": "Sebastian Bergmann",
1577 | "email": "sebastian@phpunit.de"
1578 | }
1579 | ],
1580 | "description": "Traverses array structures and object graphs to enumerate all referenced objects",
1581 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
1582 | "support": {
1583 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
1584 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
1585 | },
1586 | "funding": [
1587 | {
1588 | "url": "https://github.com/sebastianbergmann",
1589 | "type": "github"
1590 | }
1591 | ],
1592 | "time": "2020-10-26T13:12:34+00:00"
1593 | },
1594 | {
1595 | "name": "sebastian/object-reflector",
1596 | "version": "2.0.4",
1597 | "source": {
1598 | "type": "git",
1599 | "url": "https://github.com/sebastianbergmann/object-reflector.git",
1600 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
1601 | },
1602 | "dist": {
1603 | "type": "zip",
1604 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
1605 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
1606 | "shasum": ""
1607 | },
1608 | "require": {
1609 | "php": ">=7.3"
1610 | },
1611 | "require-dev": {
1612 | "phpunit/phpunit": "^9.3"
1613 | },
1614 | "type": "library",
1615 | "extra": {
1616 | "branch-alias": {
1617 | "dev-master": "2.0-dev"
1618 | }
1619 | },
1620 | "autoload": {
1621 | "classmap": [
1622 | "src/"
1623 | ]
1624 | },
1625 | "notification-url": "https://packagist.org/downloads/",
1626 | "license": [
1627 | "BSD-3-Clause"
1628 | ],
1629 | "authors": [
1630 | {
1631 | "name": "Sebastian Bergmann",
1632 | "email": "sebastian@phpunit.de"
1633 | }
1634 | ],
1635 | "description": "Allows reflection of object attributes, including inherited and non-public ones",
1636 | "homepage": "https://github.com/sebastianbergmann/object-reflector/",
1637 | "support": {
1638 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
1639 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
1640 | },
1641 | "funding": [
1642 | {
1643 | "url": "https://github.com/sebastianbergmann",
1644 | "type": "github"
1645 | }
1646 | ],
1647 | "time": "2020-10-26T13:14:26+00:00"
1648 | },
1649 | {
1650 | "name": "sebastian/recursion-context",
1651 | "version": "4.0.5",
1652 | "source": {
1653 | "type": "git",
1654 | "url": "https://github.com/sebastianbergmann/recursion-context.git",
1655 | "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
1656 | },
1657 | "dist": {
1658 | "type": "zip",
1659 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
1660 | "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
1661 | "shasum": ""
1662 | },
1663 | "require": {
1664 | "php": ">=7.3"
1665 | },
1666 | "require-dev": {
1667 | "phpunit/phpunit": "^9.3"
1668 | },
1669 | "type": "library",
1670 | "extra": {
1671 | "branch-alias": {
1672 | "dev-master": "4.0-dev"
1673 | }
1674 | },
1675 | "autoload": {
1676 | "classmap": [
1677 | "src/"
1678 | ]
1679 | },
1680 | "notification-url": "https://packagist.org/downloads/",
1681 | "license": [
1682 | "BSD-3-Clause"
1683 | ],
1684 | "authors": [
1685 | {
1686 | "name": "Sebastian Bergmann",
1687 | "email": "sebastian@phpunit.de"
1688 | },
1689 | {
1690 | "name": "Jeff Welch",
1691 | "email": "whatthejeff@gmail.com"
1692 | },
1693 | {
1694 | "name": "Adam Harvey",
1695 | "email": "aharvey@php.net"
1696 | }
1697 | ],
1698 | "description": "Provides functionality to recursively process PHP variables",
1699 | "homepage": "https://github.com/sebastianbergmann/recursion-context",
1700 | "support": {
1701 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
1702 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
1703 | },
1704 | "funding": [
1705 | {
1706 | "url": "https://github.com/sebastianbergmann",
1707 | "type": "github"
1708 | }
1709 | ],
1710 | "time": "2023-02-03T06:07:39+00:00"
1711 | },
1712 | {
1713 | "name": "sebastian/resource-operations",
1714 | "version": "3.0.4",
1715 | "source": {
1716 | "type": "git",
1717 | "url": "https://github.com/sebastianbergmann/resource-operations.git",
1718 | "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
1719 | },
1720 | "dist": {
1721 | "type": "zip",
1722 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
1723 | "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
1724 | "shasum": ""
1725 | },
1726 | "require": {
1727 | "php": ">=7.3"
1728 | },
1729 | "require-dev": {
1730 | "phpunit/phpunit": "^9.0"
1731 | },
1732 | "type": "library",
1733 | "extra": {
1734 | "branch-alias": {
1735 | "dev-main": "3.0-dev"
1736 | }
1737 | },
1738 | "autoload": {
1739 | "classmap": [
1740 | "src/"
1741 | ]
1742 | },
1743 | "notification-url": "https://packagist.org/downloads/",
1744 | "license": [
1745 | "BSD-3-Clause"
1746 | ],
1747 | "authors": [
1748 | {
1749 | "name": "Sebastian Bergmann",
1750 | "email": "sebastian@phpunit.de"
1751 | }
1752 | ],
1753 | "description": "Provides a list of PHP built-in functions that operate on resources",
1754 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
1755 | "support": {
1756 | "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
1757 | },
1758 | "funding": [
1759 | {
1760 | "url": "https://github.com/sebastianbergmann",
1761 | "type": "github"
1762 | }
1763 | ],
1764 | "time": "2024-03-14T16:00:52+00:00"
1765 | },
1766 | {
1767 | "name": "sebastian/type",
1768 | "version": "3.2.1",
1769 | "source": {
1770 | "type": "git",
1771 | "url": "https://github.com/sebastianbergmann/type.git",
1772 | "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
1773 | },
1774 | "dist": {
1775 | "type": "zip",
1776 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
1777 | "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
1778 | "shasum": ""
1779 | },
1780 | "require": {
1781 | "php": ">=7.3"
1782 | },
1783 | "require-dev": {
1784 | "phpunit/phpunit": "^9.5"
1785 | },
1786 | "type": "library",
1787 | "extra": {
1788 | "branch-alias": {
1789 | "dev-master": "3.2-dev"
1790 | }
1791 | },
1792 | "autoload": {
1793 | "classmap": [
1794 | "src/"
1795 | ]
1796 | },
1797 | "notification-url": "https://packagist.org/downloads/",
1798 | "license": [
1799 | "BSD-3-Clause"
1800 | ],
1801 | "authors": [
1802 | {
1803 | "name": "Sebastian Bergmann",
1804 | "email": "sebastian@phpunit.de",
1805 | "role": "lead"
1806 | }
1807 | ],
1808 | "description": "Collection of value objects that represent the types of the PHP type system",
1809 | "homepage": "https://github.com/sebastianbergmann/type",
1810 | "support": {
1811 | "issues": "https://github.com/sebastianbergmann/type/issues",
1812 | "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
1813 | },
1814 | "funding": [
1815 | {
1816 | "url": "https://github.com/sebastianbergmann",
1817 | "type": "github"
1818 | }
1819 | ],
1820 | "time": "2023-02-03T06:13:03+00:00"
1821 | },
1822 | {
1823 | "name": "sebastian/version",
1824 | "version": "3.0.2",
1825 | "source": {
1826 | "type": "git",
1827 | "url": "https://github.com/sebastianbergmann/version.git",
1828 | "reference": "c6c1022351a901512170118436c764e473f6de8c"
1829 | },
1830 | "dist": {
1831 | "type": "zip",
1832 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
1833 | "reference": "c6c1022351a901512170118436c764e473f6de8c",
1834 | "shasum": ""
1835 | },
1836 | "require": {
1837 | "php": ">=7.3"
1838 | },
1839 | "type": "library",
1840 | "extra": {
1841 | "branch-alias": {
1842 | "dev-master": "3.0-dev"
1843 | }
1844 | },
1845 | "autoload": {
1846 | "classmap": [
1847 | "src/"
1848 | ]
1849 | },
1850 | "notification-url": "https://packagist.org/downloads/",
1851 | "license": [
1852 | "BSD-3-Clause"
1853 | ],
1854 | "authors": [
1855 | {
1856 | "name": "Sebastian Bergmann",
1857 | "email": "sebastian@phpunit.de",
1858 | "role": "lead"
1859 | }
1860 | ],
1861 | "description": "Library that helps with managing the version number of Git-hosted PHP projects",
1862 | "homepage": "https://github.com/sebastianbergmann/version",
1863 | "support": {
1864 | "issues": "https://github.com/sebastianbergmann/version/issues",
1865 | "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
1866 | },
1867 | "funding": [
1868 | {
1869 | "url": "https://github.com/sebastianbergmann",
1870 | "type": "github"
1871 | }
1872 | ],
1873 | "time": "2020-09-28T06:39:44+00:00"
1874 | },
1875 | {
1876 | "name": "swoole/ide-helper",
1877 | "version": "5.1.2",
1878 | "source": {
1879 | "type": "git",
1880 | "url": "https://github.com/swoole/ide-helper.git",
1881 | "reference": "33ec7af9111b76d06a70dd31191cc74793551112"
1882 | },
1883 | "dist": {
1884 | "type": "zip",
1885 | "url": "https://api.github.com/repos/swoole/ide-helper/zipball/33ec7af9111b76d06a70dd31191cc74793551112",
1886 | "reference": "33ec7af9111b76d06a70dd31191cc74793551112",
1887 | "shasum": ""
1888 | },
1889 | "type": "library",
1890 | "notification-url": "https://packagist.org/downloads/",
1891 | "license": [
1892 | "Apache-2.0"
1893 | ],
1894 | "authors": [
1895 | {
1896 | "name": "Team Swoole",
1897 | "email": "team@swoole.com"
1898 | }
1899 | ],
1900 | "description": "IDE help files for Swoole.",
1901 | "support": {
1902 | "issues": "https://github.com/swoole/ide-helper/issues",
1903 | "source": "https://github.com/swoole/ide-helper/tree/5.1.2"
1904 | },
1905 | "time": "2024-02-01T22:28:11+00:00"
1906 | },
1907 | {
1908 | "name": "textalk/websocket",
1909 | "version": "1.5.2",
1910 | "source": {
1911 | "type": "git",
1912 | "url": "https://github.com/Textalk/websocket-php.git",
1913 | "reference": "b93249453806a2dd46495de46d76fcbcb0d8dee8"
1914 | },
1915 | "dist": {
1916 | "type": "zip",
1917 | "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/b93249453806a2dd46495de46d76fcbcb0d8dee8",
1918 | "reference": "b93249453806a2dd46495de46d76fcbcb0d8dee8",
1919 | "shasum": ""
1920 | },
1921 | "require": {
1922 | "php": "^7.2 | ^8.0",
1923 | "psr/log": "^1.0"
1924 | },
1925 | "require-dev": {
1926 | "php-coveralls/php-coveralls": "^2.0",
1927 | "phpunit/phpunit": "^8.0|^9.0",
1928 | "squizlabs/php_codesniffer": "^3.5"
1929 | },
1930 | "type": "library",
1931 | "autoload": {
1932 | "psr-4": {
1933 | "WebSocket\\": "lib"
1934 | }
1935 | },
1936 | "notification-url": "https://packagist.org/downloads/",
1937 | "license": [
1938 | "ISC"
1939 | ],
1940 | "authors": [
1941 | {
1942 | "name": "Fredrik Liljegren"
1943 | },
1944 | {
1945 | "name": "Sören Jensen",
1946 | "email": "soren@abicart.se"
1947 | }
1948 | ],
1949 | "description": "WebSocket client and server",
1950 | "support": {
1951 | "issues": "https://github.com/Textalk/websocket-php/issues",
1952 | "source": "https://github.com/Textalk/websocket-php/tree/1.5.2"
1953 | },
1954 | "time": "2021-02-12T15:39:23+00:00"
1955 | },
1956 | {
1957 | "name": "theseer/tokenizer",
1958 | "version": "1.2.3",
1959 | "source": {
1960 | "type": "git",
1961 | "url": "https://github.com/theseer/tokenizer.git",
1962 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
1963 | },
1964 | "dist": {
1965 | "type": "zip",
1966 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
1967 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
1968 | "shasum": ""
1969 | },
1970 | "require": {
1971 | "ext-dom": "*",
1972 | "ext-tokenizer": "*",
1973 | "ext-xmlwriter": "*",
1974 | "php": "^7.2 || ^8.0"
1975 | },
1976 | "type": "library",
1977 | "autoload": {
1978 | "classmap": [
1979 | "src/"
1980 | ]
1981 | },
1982 | "notification-url": "https://packagist.org/downloads/",
1983 | "license": [
1984 | "BSD-3-Clause"
1985 | ],
1986 | "authors": [
1987 | {
1988 | "name": "Arne Blankerts",
1989 | "email": "arne@blankerts.de",
1990 | "role": "Developer"
1991 | }
1992 | ],
1993 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
1994 | "support": {
1995 | "issues": "https://github.com/theseer/tokenizer/issues",
1996 | "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
1997 | },
1998 | "funding": [
1999 | {
2000 | "url": "https://github.com/theseer",
2001 | "type": "github"
2002 | }
2003 | ],
2004 | "time": "2024-03-03T12:36:25+00:00"
2005 | },
2006 | {
2007 | "name": "workerman/workerman",
2008 | "version": "v4.1.17",
2009 | "source": {
2010 | "type": "git",
2011 | "url": "https://github.com/walkor/workerman.git",
2012 | "reference": "bb9e4b0c3fc3931a2ae38a1fb7a3ac09246efae9"
2013 | },
2014 | "dist": {
2015 | "type": "zip",
2016 | "url": "https://api.github.com/repos/walkor/workerman/zipball/bb9e4b0c3fc3931a2ae38a1fb7a3ac09246efae9",
2017 | "reference": "bb9e4b0c3fc3931a2ae38a1fb7a3ac09246efae9",
2018 | "shasum": ""
2019 | },
2020 | "require": {
2021 | "php": ">=7.0"
2022 | },
2023 | "suggest": {
2024 | "ext-event": "For better performance. "
2025 | },
2026 | "type": "library",
2027 | "autoload": {
2028 | "psr-4": {
2029 | "Workerman\\": "./"
2030 | }
2031 | },
2032 | "notification-url": "https://packagist.org/downloads/",
2033 | "license": [
2034 | "MIT"
2035 | ],
2036 | "authors": [
2037 | {
2038 | "name": "walkor",
2039 | "email": "walkor@workerman.net",
2040 | "homepage": "http://www.workerman.net",
2041 | "role": "Developer"
2042 | }
2043 | ],
2044 | "description": "An asynchronous event driven PHP framework for easily building fast, scalable network applications.",
2045 | "homepage": "http://www.workerman.net",
2046 | "keywords": [
2047 | "asynchronous",
2048 | "event-loop"
2049 | ],
2050 | "support": {
2051 | "email": "walkor@workerman.net",
2052 | "forum": "http://wenda.workerman.net/",
2053 | "issues": "https://github.com/walkor/workerman/issues",
2054 | "source": "https://github.com/walkor/workerman",
2055 | "wiki": "http://doc.workerman.net/"
2056 | },
2057 | "funding": [
2058 | {
2059 | "url": "https://opencollective.com/workerman",
2060 | "type": "open_collective"
2061 | },
2062 | {
2063 | "url": "https://www.patreon.com/walkor",
2064 | "type": "patreon"
2065 | }
2066 | ],
2067 | "time": "2024-11-07T07:48:34+00:00"
2068 | }
2069 | ],
2070 | "aliases": [],
2071 | "minimum-stability": "stable",
2072 | "stability-flags": {},
2073 | "prefer-stable": false,
2074 | "prefer-lowest": false,
2075 | "platform": {
2076 | "php": ">=8.0"
2077 | },
2078 | "platform-dev": {},
2079 | "plugin-api-version": "2.6.0"
2080 | }
2081 |
--------------------------------------------------------------------------------