├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── db.44.2204.yml │ ├── db.50.2204.yml │ └── no-db.2204.yml ├── .gitignore ├── .well-known └── funding-manifest-urls ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Bolt.php ├── autoload.php ├── connection │ ├── AConnection.php │ ├── IConnection.php │ ├── PStreamSocket.php │ ├── Socket.php │ └── StreamSocket.php ├── enum │ ├── Message.php │ ├── ServerState.php │ └── Signature.php ├── error │ ├── BoltException.php │ ├── ConnectException.php │ ├── ConnectionTimeoutException.php │ ├── PackException.php │ └── UnpackException.php ├── helpers │ ├── CacheProvider.php │ └── FileCache.php ├── packstream │ ├── Bytes.php │ ├── IPackDictionaryGenerator.php │ ├── IPackListGenerator.php │ ├── IPacker.php │ ├── IUnpacker.php │ └── v1 │ │ ├── Packer.php │ │ └── Unpacker.php └── protocol │ ├── AProtocol.php │ ├── IStructure.php │ ├── Response.php │ ├── V1.php │ ├── V2.php │ ├── V3.php │ ├── V4.php │ ├── V4_1.php │ ├── V4_2.php │ ├── V4_3.php │ ├── V4_4.php │ ├── V5.php │ ├── V5_1.php │ ├── V5_2.php │ ├── V5_3.php │ ├── V5_4.php │ ├── V5_6.php │ ├── V5_7.php │ ├── V5_8.php │ ├── v1 │ ├── AckFailureMessage.php │ ├── AvailableStructures.php │ ├── DiscardAllMessage.php │ ├── InitMessage.php │ ├── PullAllMessage.php │ ├── ResetMessage.php │ ├── RunMessage.php │ ├── ServerStateTransition.php │ └── structures │ │ ├── Date.php │ │ ├── DateTime.php │ │ ├── DateTimeZoneId.php │ │ ├── Duration.php │ │ ├── LocalDateTime.php │ │ ├── LocalTime.php │ │ ├── Node.php │ │ ├── Path.php │ │ ├── Point2D.php │ │ ├── Point3D.php │ │ ├── Relationship.php │ │ ├── Time.php │ │ └── UnboundRelationship.php │ ├── v3 │ ├── BeginMessage.php │ ├── CommitMessage.php │ ├── GoodbyeMessage.php │ ├── HelloMessage.php │ ├── RollbackMessage.php │ ├── RunMessage.php │ └── ServerStateTransition.php │ ├── v4 │ ├── DiscardMessage.php │ ├── PullMessage.php │ └── ServerStateTransition.php │ ├── v4_1 │ └── HelloMessage.php │ ├── v4_3 │ ├── AvailableStructures.php │ └── RouteMessage.php │ ├── v4_4 │ └── RouteMessage.php │ ├── v5 │ ├── AvailableStructures.php │ └── structures │ │ ├── DateTime.php │ │ ├── DateTimeZoneId.php │ │ ├── Node.php │ │ ├── Relationship.php │ │ └── UnboundRelationship.php │ ├── v5_1 │ ├── HelloMessage.php │ ├── LogoffMessage.php │ ├── LogonMessage.php │ └── ServerStateTransition.php │ ├── v5_3 │ └── HelloMessage.php │ └── v5_4 │ └── TelemetryMessage.php └── tests ├── BoltTest.php ├── PerformanceTest.php ├── TestLayer.php ├── connection └── ConnectionTest.php ├── error └── ErrorsTest.php ├── helpers ├── FileCacheTest.php └── lock.php ├── packstream └── v1 │ ├── BytesTest.php │ ├── PackerTest.php │ ├── UnpackerTest.php │ └── generators │ ├── DictionaryGenerator.php │ ├── ListGenerator.php │ └── RandomDataGenerator.php ├── protocol ├── ProtocolLayer.php ├── V1Test.php ├── V2Test.php ├── V3Test.php ├── V4Test.php ├── V4_1Test.php ├── V4_2Test.php ├── V4_3Test.php ├── V4_4Test.php ├── V5Test.php ├── V5_1Test.php ├── V5_2Test.php ├── V5_3Test.php ├── V5_4Test.php ├── V5_6Test.php ├── V5_7Test.php └── V5_8Test.php └── structures ├── StructureLayer.php ├── v1 ├── DateTimeTrait.php ├── DateTimeZoneIdTrait.php └── StructuresTest.php ├── v4_3 └── StructuresTest.php └── v5 └── StructuresTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: stefanak-michal 4 | ko_fi: michalstefanak 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Desktop (please complete the following information):** 20 | - OS: [e.g. Win 10 Pro] 21 | - PHP Version [e.g. 7.1.22] 22 | - Neo4j Version [e.g. 4.1.7] 23 | - Bolt Library Version [e.g. v1.2 or master] 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/db.44.2204.yml: -------------------------------------------------------------------------------- 1 | name: Tests with Neo4j 4.4 on PHP^8 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | db-tests-44-2204: 9 | runs-on: ubuntu-22.04 10 | name: "Running Integration tests for PHP ${{ matrix.php-version }} on Neo4j ${{ matrix.neo4j-version }}" 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | neo4j-version: ['4.4'] 15 | php-version: ['8.1', '8.2', '8.3', '8.4'] 16 | 17 | services: 18 | neo4j: 19 | image: neo4j:${{ matrix.neo4j-version }} 20 | env: 21 | NEO4J_AUTH: neo4j/nothing 22 | NEO4JLABS_PLUGINS: '["apoc-core"]' 23 | ports: 24 | - 7687:7687 25 | - 7474:7474 26 | options: >- 27 | --health-cmd "wget http://localhost:7474 || exit 1" 28 | 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | 33 | - name: Setup PHP 34 | uses: shivammathur/setup-php@v2 35 | with: 36 | php-version: ${{ matrix.php-version }} 37 | extensions: mbstring, sockets 38 | coverage: xdebug 39 | ini-values: max_execution_time=0 40 | 41 | - name: Install dependencies 42 | run: composer install --no-progress 43 | 44 | - name: Test with phpunit 45 | env: 46 | GDB_USERNAME: neo4j 47 | GDB_PASSWORD: nothing 48 | run: vendor/bin/phpunit --configuration phpunit.xml --testsuite "Database" 49 | -------------------------------------------------------------------------------- /.github/workflows/db.50.2204.yml: -------------------------------------------------------------------------------- 1 | name: Tests with Neo4j^5 on PHP^8 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | db-tests-50-2204: 9 | runs-on: ubuntu-22.04 10 | name: "Running Integration tests for PHP ${{ matrix.php-version }} on Neo4j ${{ matrix.neo4j-version }}" 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | neo4j-version: ['5.4', '5.6', '5.8', '5.12', '5.13', '5.23', '5.26'] 15 | php-version: ['8.1', '8.2', '8.3', '8.4'] 16 | 17 | services: 18 | neo4j: 19 | image: neo4j:${{ matrix.neo4j-version }} 20 | env: 21 | NEO4J_AUTH: neo4j/nothing123 22 | NEO4J_PLUGINS: '["apoc"]' 23 | ports: 24 | - 7687:7687 25 | - 7474:7474 26 | options: >- 27 | --health-cmd "wget http://localhost:7474 || exit 1" 28 | 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | 33 | - name: Setup PHP 34 | uses: shivammathur/setup-php@v2 35 | with: 36 | php-version: ${{ matrix.php-version }} 37 | extensions: mbstring, sockets 38 | coverage: xdebug 39 | ini-values: max_execution_time=0 40 | 41 | - name: Install dependencies 42 | run: composer install --no-progress 43 | 44 | - name: Test with phpunit 45 | env: 46 | GDB_USERNAME: neo4j 47 | GDB_PASSWORD: nothing123 48 | run: vendor/bin/phpunit --configuration phpunit.xml --testsuite "Database" 49 | -------------------------------------------------------------------------------- /.github/workflows/no-db.2204.yml: -------------------------------------------------------------------------------- 1 | name: Tests without a database PHP^8 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | no-db-tests-2204: 9 | runs-on: ubuntu-22.04 10 | name: "Running Tests for PHP ${{ matrix.php-version }}" 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | php-version: ['8.1', '8.2', '8.3', '8.4'] 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php-version }} 24 | extensions: mbstring, sockets 25 | coverage: xdebug 26 | ini-values: max_execution_time=0 27 | 28 | - name: Install dependencies 29 | run: composer install --no-progress 30 | 31 | - name: Test with phpunit 32 | run: vendor/bin/phpunit --configuration phpunit.xml --testsuite "NoDatabase" 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | /index.php 4 | cert/ 5 | /vendor/ 6 | *.lock 7 | *.cache 8 | phpunit.dev.xml 9 | temp/ 10 | tmp/ 11 | -------------------------------------------------------------------------------- /.well-known/funding-manifest-urls: -------------------------------------------------------------------------------- 1 | https://github.com/stefanak-michal/stefanak-michal/blob/main/funding.json 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | devrel@neo4j.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | We'd love for you to contribute to our source code and to make php Bolt library even better than it is today! Here are the guidelines we'd like you to follow. 3 | 4 | ## Code of Conduct 5 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://github.com/neo4j-php/Bolt/blob/master/CODE_OF_CONDUCT.md). 6 | 7 | ## Did you find a bug? 8 | Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/neo4j-php/Bolt/issues?q=is%3Aissue). 9 | 10 | If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/neo4j-php/Bolt/issues/new/choose). Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring. 11 | 12 | ## Did you write a piece of code? 13 | Open a new GitHub pull request with the patch. Don't forget to run phpunit when you make changes. 14 | 15 | Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 16 | 17 | ## Clean code 18 | We like to keep the code unified and clean as possible. 19 | 20 | - Follow PHP Standards Recommendations (PSR) 21 | - Avoid import with `use function ;` 22 | - Use type specification for method arguments 23 | - Set methods return type 24 | - Add descriptive method annotations if needed 25 | - Don't use @deprecated annotation 26 | - Follow Bolt specification by [official documentation](https://www.neo4j.com/docs/bolt/current/) 27 | 28 | \ 29 | PHP Bolt library is a volunteer effort. We are thankful for your interest or contribution. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Michal Štefaňák 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 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stefanak-michal/bolt", 3 | "description": "PHP library to provide connectivity to graph database over TCP socket with Bolt specification", 4 | "keywords": ["neo4j", "bolt", "socket", "database"], 5 | "homepage": "https://github.com/neo4j-php/Bolt", 6 | "type": "library", 7 | "readme": "README.md", 8 | "license": "MIT", 9 | "minimum-stability": "stable", 10 | "require": { 11 | "php": "^8.1", 12 | "ext-mbstring": "*", 13 | "ext-curl": "*", 14 | "psr/simple-cache": "^3.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^9" 18 | }, 19 | "support": { 20 | "issues": "https://github.com/neo4j-php/Bolt/issues", 21 | "source": "https://github.com/neo4j-php/Bolt", 22 | "docs": "https://www.neo4j.com/docs/bolt/current/" 23 | }, 24 | "funding": [ 25 | { 26 | "type": "ko-fi", 27 | "url": "https://ko-fi.com/michalstefanak" 28 | } 29 | ], 30 | "authors": [ 31 | { 32 | "name": "Michal Stefanak", 33 | "role": "Developer", 34 | "homepage": "https://www.linkedin.com/in/michalstefanak/" 35 | } 36 | ], 37 | "autoload": { 38 | "psr-4": { 39 | "Bolt\\": "src/" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Bolt\\tests\\": "tests/" 45 | } 46 | }, 47 | "suggest": { 48 | "laudis/neo4j-php-client": "Neo4j-PHP-Client is the most advanced PHP Client for Neo4j", 49 | "stefanak-michal/neo4j-bolt-wrapper": "Wrapper for Neo4j PHP Bolt library to simplify usage.", 50 | "ext-sockets": "Needed when using Socket connection class", 51 | "ext-openssl": "Needed when using StreamSocket connection class with SSL" 52 | }, 53 | "scripts": { 54 | "test": [ 55 | "@putenv XDEBUG_MODE=debug", 56 | "Composer\\Config::disableProcessTimeout", 57 | "phpunit" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/BoltTest.php 6 | ./tests/connection 7 | ./tests/error 8 | ./tests/packstream 9 | ./tests/structures 10 | ./tests/PerformanceTest.php 11 | 12 | 13 | ./tests/protocol 14 | ./tests/helpers/FileCacheTest.php 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/autoload.php: -------------------------------------------------------------------------------- 1 | ip, FILTER_VALIDATE_URL)) { 21 | $scheme = parse_url($this->ip, PHP_URL_SCHEME); 22 | if (!empty($scheme)) { 23 | $this->ip = str_replace($scheme . '://', '', $this->ip); 24 | } 25 | } 26 | } 27 | 28 | /** 29 | * Print buffer as HEX 30 | */ 31 | protected function printHex(string $str, string $prefix = 'C: '): void 32 | { 33 | $str = implode(unpack('H*', $str)); 34 | echo '
' . $prefix;
35 |         foreach (str_split($str, 8) as $chunk) {
36 |             echo implode(' ', str_split($chunk, 2));
37 |             echo '    ';
38 |         }
39 |         echo '
' . PHP_EOL; 40 | } 41 | 42 | public function getIp(): string 43 | { 44 | return $this->ip; 45 | } 46 | 47 | public function getPort(): int 48 | { 49 | return $this->port; 50 | } 51 | 52 | public function getTimeout(): float 53 | { 54 | return $this->timeout; 55 | } 56 | 57 | public function setTimeout(float $timeout): void 58 | { 59 | $this->timeout = $timeout; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/connection/IConnection.php: -------------------------------------------------------------------------------- 1 | stream]; 50 | $write = $except = null; 51 | if (stream_select($read, $write, $except, 0) === 1) { 52 | do { 53 | $r = fread($this->stream, 1024); 54 | } while ($r !== false && mb_strlen($r) == 1024); 55 | } 56 | 57 | return $result; 58 | } 59 | 60 | public function getIdentifier(): string 61 | { 62 | if (empty($this->identifier)) 63 | $this->identifier = str_replace(':', '_', stream_socket_get_name($this->stream, false)) . '_' . str_replace(':', '_', stream_socket_get_name($this->stream, true)); 64 | return $this->identifier; 65 | } 66 | 67 | public function disconnect(): void 68 | { 69 | if (is_resource($this->stream)) { 70 | stream_socket_shutdown($this->stream, STREAM_SHUT_RDWR); 71 | fclose($this->stream); 72 | unset($this->stream); 73 | CacheProvider::get()->delete($this->getIdentifier()); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/connection/Socket.php: -------------------------------------------------------------------------------- 1 | socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 32 | if ($this->socket === false) { 33 | throw new ConnectException('Cannot create socket'); 34 | } 35 | 36 | if (socket_set_block($this->socket) === false) { 37 | throw new ConnectException('Cannot set socket into blocking mode'); 38 | } 39 | 40 | socket_set_option($this->socket, SOL_TCP, TCP_NODELAY, 1); 41 | socket_set_option($this->socket, SOL_SOCKET, SO_KEEPALIVE, 1); 42 | $this->configureTimeout(); 43 | 44 | $conn = @socket_connect($this->socket, $this->ip, $this->port); 45 | if (!$conn) { 46 | $code = socket_last_error($this->socket); 47 | throw new ConnectException(socket_strerror($code), $code); 48 | } 49 | 50 | return true; 51 | } 52 | 53 | public function write(string $buffer): void 54 | { 55 | if ($this->socket === false) { 56 | throw new ConnectException('Not initialized socket'); 57 | } 58 | 59 | if (Bolt::$debug) 60 | $this->printHex($buffer); 61 | 62 | $size = mb_strlen($buffer, '8bit'); 63 | while (0 < $size) { 64 | $sent = @socket_write($this->socket, $buffer, $size); 65 | if ($sent === false) 66 | $this->throwConnectException(); 67 | $buffer = mb_strcut($buffer, $sent, null, '8bit'); 68 | $size -= $sent; 69 | } 70 | } 71 | 72 | public function read(int $length = 2048): string 73 | { 74 | if ($this->socket === false) 75 | throw new ConnectException('Not initialized socket'); 76 | 77 | $output = ''; 78 | $t = microtime(true); 79 | do { 80 | if (mb_strlen($output, '8bit') == 0 && $this->timeout > 0 && (microtime(true) - $t) >= $this->timeout) 81 | throw new ConnectionTimeoutException('Read from connection reached timeout after ' . $this->timeout . ' seconds.'); 82 | $readed = @socket_read($this->socket, $length - mb_strlen($output, '8bit')); 83 | if ($readed === false) 84 | $this->throwConnectException(); 85 | $output .= $readed; 86 | } while (mb_strlen($output, '8bit') < $length); 87 | 88 | if (Bolt::$debug) 89 | $this->printHex($output, 'S: '); 90 | 91 | return $output; 92 | } 93 | 94 | public function disconnect(): void 95 | { 96 | if ($this->socket !== false) { 97 | @socket_shutdown($this->socket); 98 | @socket_close($this->socket); 99 | } 100 | } 101 | 102 | public function setTimeout(float $timeout): void 103 | { 104 | parent::setTimeout($timeout); 105 | $this->configureTimeout(); 106 | } 107 | 108 | private function configureTimeout(): void 109 | { 110 | if ($this->socket === false) 111 | return; 112 | $timeoutSeconds = floor($this->timeout); 113 | $microSeconds = floor(($this->timeout - $timeoutSeconds) * 1000000); 114 | $timeoutOption = ['sec' => $timeoutSeconds, 'usec' => $microSeconds]; 115 | socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, $timeoutOption); 116 | socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, $timeoutOption); 117 | } 118 | 119 | /** 120 | * @throws ConnectException 121 | * @throws ConnectionTimeoutException 122 | */ 123 | private function throwConnectException(): void 124 | { 125 | $code = socket_last_error($this->socket); 126 | if (in_array($code, self::POSSIBLE_TIMEOUTS_CODES)) { 127 | throw new ConnectionTimeoutException('Connection timeout reached after ' . $this->timeout . ' seconds.'); 128 | } elseif ($code !== 0) { 129 | throw new ConnectException(socket_strerror($code), $code); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/connection/StreamSocket.php: -------------------------------------------------------------------------------- 1 | sslContextOptions = $options; 35 | } 36 | 37 | public function connect(): bool 38 | { 39 | $context = stream_context_create([ 40 | 'socket' => [ 41 | 'tcp_nodelay' => true, 42 | ], 43 | 'ssl' => $this->sslContextOptions 44 | ]); 45 | 46 | $this->stream = @stream_socket_client('tcp://' . $this->ip . ':' . $this->port, $errno, $errstr, $this->timeout, $this->connectionFlags, $context); 47 | 48 | if ($this->stream === false) { 49 | throw new ConnectException($errstr, $errno); 50 | } 51 | 52 | if (!stream_set_blocking($this->stream, true)) { 53 | throw new ConnectException('Cannot set socket into blocking mode'); 54 | } 55 | 56 | if (!empty($this->sslContextOptions)) { 57 | if (stream_socket_enable_crypto($this->stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT) !== true) { 58 | throw new ConnectException('Enable encryption error'); 59 | } 60 | } 61 | 62 | $this->configureTimeout(); 63 | 64 | return true; 65 | } 66 | 67 | public function write(string $buffer): void 68 | { 69 | if (Bolt::$debug) 70 | $this->printHex($buffer); 71 | 72 | $size = mb_strlen($buffer, '8bit'); 73 | 74 | $time = microtime(true); 75 | while (0 < $size) { 76 | $sent = fwrite($this->stream, $buffer); 77 | 78 | if ($sent === false) { 79 | if (microtime(true) - $time >= $this->timeout) 80 | throw new ConnectionTimeoutException('Connection timeout reached after ' . $this->timeout . ' seconds.'); 81 | else 82 | throw new ConnectException('Write error'); 83 | } 84 | 85 | $buffer = mb_strcut($buffer, $sent, null, '8bit'); 86 | $size -= $sent; 87 | } 88 | } 89 | 90 | public function read(int $length = 2048): string 91 | { 92 | $output = ''; 93 | $t = microtime(true); 94 | do { 95 | if (mb_strlen($output, '8bit') == 0 && $this->timeout > 0 && (microtime(true) - $t) >= $this->timeout) 96 | throw new ConnectionTimeoutException('Read from connection reached timeout after ' . $this->timeout . ' seconds.'); 97 | 98 | $readed = stream_get_contents($this->stream, $length - mb_strlen($output, '8bit')); 99 | 100 | if (stream_get_meta_data($this->stream)['timed_out'] ?? false) 101 | throw new ConnectionTimeoutException('Stream connection timed out after ' . $this->timeout . ' seconds.'); 102 | if ($readed === false) 103 | throw new ConnectException('Read error'); 104 | 105 | $output .= $readed; 106 | } while (mb_strlen($output, '8bit') < $length); 107 | 108 | if (Bolt::$debug) 109 | $this->printHex($output, 'S: '); 110 | 111 | return $output; 112 | } 113 | 114 | public function disconnect(): void 115 | { 116 | if (is_resource($this->stream)) { 117 | stream_socket_shutdown($this->stream, STREAM_SHUT_RDWR); 118 | unset($this->stream); 119 | } 120 | } 121 | 122 | /** 123 | * @throws ConnectException 124 | */ 125 | public function setTimeout(float $timeout): void 126 | { 127 | parent::setTimeout($timeout); 128 | $this->configureTimeout(); 129 | } 130 | 131 | /** 132 | * @throws ConnectException 133 | */ 134 | protected function configureTimeout(): void 135 | { 136 | if (is_resource($this->stream)) { 137 | $timeout = (int)floor($this->timeout); 138 | if (!stream_set_timeout($this->stream, $timeout, (int)floor(($this->timeout - $timeout) * 1000000))) { 139 | throw new ConnectException('Cannot set timeout on stream'); 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/enum/Message.php: -------------------------------------------------------------------------------- 1 | signal will set the connection in the DEFUNCT server state. 35 | */ 36 | case DEFUNCT; 37 | 38 | /** 39 | * The READY state can handle the request messages RUN and BEGIN and receive a query. 40 | */ 41 | case READY; 42 | 43 | /** 44 | * When STREAMING, a result is available for streaming from server to client. 45 | * This result must be fully consumed or discarded by a client before the server can re-enter the READY state and allow any further queries to be executed. 46 | */ 47 | case STREAMING; 48 | 49 | /** 50 | * When transaction started 51 | */ 52 | case TX_READY; 53 | 54 | /** 55 | * When TX_STREAMING, a result is available for streaming from server to client. This result must be fully consumed or discarded by a client before the server can transition to the TX_READY state. 56 | */ 57 | case TX_STREAMING; 58 | 59 | /** 60 | * When FAILED, a connection is in a temporarily unusable state. This is generally as the result of encountering a recoverable error. 61 | * This mode ensures that only one failure can exist at a time, preventing cascading issues from batches of work. 62 | */ 63 | case FAILED; 64 | 65 | /** 66 | * This state occurs between the server receiving the jump-ahead and the queued RESET message, (the RESET message triggers an ). 67 | * Most incoming messages are ignored when the server are in an INTERRUPTED state, with the exception of the RESET that allows transition back to READY. 68 | * The signal will set the connection in the INTERRUPTED server state. 69 | */ 70 | case INTERRUPTED; 71 | 72 | /** 73 | * Connection has been established and metadata has been sent back from the HELLO message or a LOGOFF message was received whilst in ready state. Ready to accept a LOGON message with authentication information. 74 | */ 75 | case AUTHENTICATION; 76 | } 77 | -------------------------------------------------------------------------------- /src/enum/Signature.php: -------------------------------------------------------------------------------- 1 | tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-bolt-filecache' . DIRECTORY_SEPARATOR; 27 | 28 | if (!file_exists($this->tempDir)) { 29 | mkdir(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-bolt-filecache', recursive: true); 30 | } 31 | // dotted directory to hold "time-to-live" informations 32 | if (!file_exists($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR)) { 33 | mkdir($this->tempDir . '.ttl'); 34 | } 35 | 36 | register_shutdown_function([$this, 'shutdown']); 37 | } 38 | 39 | private function shutdown(): void 40 | { 41 | foreach ($this->handles as $handle) { 42 | flock($handle, LOCK_UN); 43 | fclose($handle); 44 | } 45 | $this->handles = []; 46 | } 47 | 48 | /** 49 | * Validate cache key if it does conform to allowed characters 50 | */ 51 | private function validateKey(string $key): void 52 | { 53 | if (!preg_match('/^[\w\.]+$/i', $key)) { 54 | throw new class($key) extends \Exception implements InvalidArgumentException { 55 | protected $message; 56 | 57 | public function __construct(string $key) 58 | { 59 | $this->message = "Invalid cache key: $key. Allowed characters are A-Za-z0-9_."; 60 | } 61 | }; 62 | } 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | public function get(string $key, mixed $default = null): mixed 69 | { 70 | $this->validateKey($key); 71 | 72 | if (array_key_exists($key, $this->handles)) { 73 | rewind($this->handles[$key]); 74 | return @unserialize(stream_get_contents($this->handles[$key]), ['allowed_classes' => false]); 75 | } 76 | 77 | if ($this->has($key)) { 78 | $data = file_get_contents($this->tempDir . $key); 79 | if (!empty($data)) { 80 | return @unserialize($data, ['allowed_classes' => false]); 81 | } 82 | } 83 | 84 | return $default; 85 | } 86 | 87 | /** 88 | * @inheritDoc 89 | */ 90 | public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool 91 | { 92 | $this->validateKey($key); 93 | 94 | if ($ttl) { 95 | is_writable($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR) && file_put_contents( 96 | $this->tempDir . '.ttl' . DIRECTORY_SEPARATOR . $key, 97 | $ttl instanceof \DateInterval ? (new \DateTime())->add($ttl)->getTimestamp() : $ttl 98 | ); 99 | } 100 | 101 | if (array_key_exists($key, $this->handles)) { 102 | ftruncate($this->handles[$key], 0); 103 | rewind($this->handles[$key]); 104 | return fwrite($this->handles[$key], serialize($value)) !== false; 105 | } 106 | 107 | return is_writable($this->tempDir) && file_put_contents($this->tempDir . $key, serialize($value)) !== false; 108 | } 109 | 110 | /** 111 | * @inheritDoc 112 | */ 113 | public function delete(string $key): bool 114 | { 115 | $this->validateKey($key); 116 | 117 | if ($this->has($key)) { 118 | if (file_exists($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR . $key)) { 119 | @unlink($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR . $key); 120 | } 121 | return @unlink($this->tempDir . $key); 122 | } 123 | 124 | return true; 125 | } 126 | 127 | /** 128 | * @inheritDoc 129 | */ 130 | public function clear(): bool 131 | { 132 | array_map('unlink', glob($this->tempDir . '*.*')); 133 | array_map('unlink', glob($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR . '*.*')); 134 | return true; 135 | } 136 | 137 | /** 138 | * @inheritDoc 139 | */ 140 | public function getMultiple(iterable $keys, mixed $default = null): iterable 141 | { 142 | foreach ($keys as $key) { 143 | yield $this->get($key, $default); 144 | } 145 | } 146 | 147 | /** 148 | * @inheritDoc 149 | */ 150 | public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool 151 | { 152 | foreach ($values as $key => $value) { 153 | $this->set($key, $value, $ttl); 154 | } 155 | return true; 156 | } 157 | 158 | /** 159 | * @inheritDoc 160 | */ 161 | public function deleteMultiple(iterable $keys): bool 162 | { 163 | foreach ($keys as $key) { 164 | $this->delete($key); 165 | } 166 | return true; 167 | } 168 | 169 | /** 170 | * @inheritDoc 171 | */ 172 | public function has(string $key): bool 173 | { 174 | $this->validateKey($key); 175 | 176 | // remove file when is expired 177 | if ( 178 | file_exists($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR . $key) 179 | && intval(file_get_contents($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR . $key)) < time() 180 | ) { 181 | @unlink($this->tempDir . '.ttl' . DIRECTORY_SEPARATOR . $key); 182 | @unlink($this->tempDir . $key); 183 | } 184 | 185 | return file_exists($this->tempDir . $key) && is_file($this->tempDir . $key); 186 | } 187 | 188 | /** 189 | * Lock a key to prevent other processes from modifying it 190 | */ 191 | public function lock(string $key): bool 192 | { 193 | $this->validateKey($key); 194 | 195 | $handle = @fopen($this->tempDir . $key, 'c+'); 196 | if ($handle === false) return false; 197 | $this->handles[$key] = $handle; 198 | return flock($this->handles[$key], LOCK_EX); 199 | } 200 | 201 | /** 202 | * Unlock a key 203 | */ 204 | public function unlock(string $key): void 205 | { 206 | $this->validateKey($key); 207 | 208 | if (array_key_exists($key, $this->handles)) { 209 | flock($this->handles[$key], LOCK_UN); 210 | fclose($this->handles[$key]); 211 | unset($this->handles[$key]); 212 | } 213 | } 214 | 215 | public function __destruct() 216 | { 217 | $this->shutdown(); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/packstream/Bytes.php: -------------------------------------------------------------------------------- 1 | bytes); 24 | } 25 | 26 | public function offsetGet($offset): ?string 27 | { 28 | return $this->bytes[$offset] ?? null; 29 | } 30 | 31 | public function offsetSet($offset, $value): void 32 | { 33 | if ($offset === null) 34 | $this->bytes[] = $value; 35 | else 36 | $this->bytes[$offset] = $value; 37 | } 38 | 39 | public function offsetUnset($offset): void 40 | { 41 | unset($this->bytes[$offset]); 42 | } 43 | 44 | public function count(): int 45 | { 46 | return count($this->bytes); 47 | } 48 | 49 | public function __toString(): string 50 | { 51 | return implode($this->bytes); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/packstream/IPackDictionaryGenerator.php: -------------------------------------------------------------------------------- 1 | classFQN] 18 | */ 19 | public function __construct(array $structuresLt = []); 20 | 21 | /** 22 | * Pack message 23 | * @throws PackException 24 | */ 25 | public function pack(int $signature, mixed ...$params): iterable; 26 | } 27 | -------------------------------------------------------------------------------- /src/packstream/IUnpacker.php: -------------------------------------------------------------------------------- 1 | classFQN] 18 | */ 19 | public function __construct(array $structuresLt = []); 20 | 21 | /** 22 | * Unpack message 23 | * @throws UnpackException 24 | */ 25 | public function unpack(string $msg): mixed; 26 | 27 | /** 28 | * Get unpacked message status signature 29 | */ 30 | public function getSignature(): int; 31 | } 32 | -------------------------------------------------------------------------------- /src/protocol/AProtocol.php: -------------------------------------------------------------------------------- 1 | packer = new $packerClass($this->packStructuresLt ?? []); 55 | 56 | $unpackerClass = "\\Bolt\\packstream\\v" . $packStreamVersion . "\\Unpacker"; 57 | if (!class_exists($unpackerClass)) { 58 | throw new UnpackException('Requested PackStream version (' . $packStreamVersion . ') not yet implemented'); 59 | } 60 | $this->unpacker = new $unpackerClass($this->unpackStructuresLt ?? []); 61 | } 62 | 63 | /** 64 | * Write to connection 65 | * @throws ConnectException 66 | */ 67 | protected function write(iterable $generator): void 68 | { 69 | $this->writeCalls++; 70 | foreach ($generator as $buffer) { 71 | $this->connection->write($buffer); 72 | } 73 | } 74 | 75 | /** 76 | * Read from connection 77 | * @throws BoltException 78 | */ 79 | protected function read(?Signature &$signature = Signature::NONE): array 80 | { 81 | $msg = ''; 82 | while (true) { 83 | $header = $this->connection->read(2); 84 | if ($msg !== '' && ord($header[0]) == 0x00 && ord($header[1]) == 0x00) 85 | break; 86 | $length = unpack('n', $header)[1] ?? 0; 87 | $msg .= $this->connection->read($length); 88 | } 89 | 90 | $output = []; 91 | if (!empty($msg)) { 92 | $output = $this->unpacker->unpack($msg); 93 | $s = $this->unpacker->getSignature(); 94 | $signature = Signature::from($s); 95 | 96 | if ($signature == Signature::IGNORED) { 97 | // Ignored doesn't have any response content 98 | $output = []; 99 | } 100 | } 101 | 102 | return $output; 103 | } 104 | 105 | /** 106 | * Returns the bolt protocol version as a string. 107 | */ 108 | public function getVersion(): string 109 | { 110 | if (preg_match("/V([\d_]+)$/", static::class, $match)) { 111 | return str_replace('_', '.', $match[1]); 112 | } 113 | 114 | trigger_error('Protocol version class name is not valid', E_USER_ERROR); 115 | } 116 | 117 | /** 118 | * Read responses from host output buffer. 119 | */ 120 | public function getResponses(): \Iterator 121 | { 122 | while (count($this->pipelinedMessages) > 0) { 123 | yield $this->getResponse(); 124 | } 125 | } 126 | 127 | /** 128 | * Read one response from host output buffer 129 | */ 130 | public function getResponse(): Response 131 | { 132 | $serverState = $this->serverState; 133 | 134 | $message = reset($this->pipelinedMessages); 135 | if ($message === false) 136 | throw new ConnectException('No response waiting to be consumed'); 137 | 138 | $methodName = '_' . ( 139 | str_contains($message->name, '_') 140 | ? preg_replace("/_([a-z])/", "strtoupper('$1')", strtolower($message->name)) 141 | : strtolower($message->name) 142 | ); 143 | if (method_exists($this, $methodName)) { 144 | /** @var Response $response */ 145 | $response = $this->$methodName()->current(); 146 | } else { 147 | $content = $this->read($signature); 148 | $response = new Response($message, $signature, $content); 149 | } 150 | 151 | if ($response->signature != Signature::RECORD) 152 | array_shift($this->pipelinedMessages); 153 | 154 | foreach (($this->serverStateTransition ?? []) as $transition) { 155 | if ($transition[0] === $serverState && $transition[1] === $response->message && $transition[2] === $response->signature) { 156 | $this->serverState = $transition[3]; 157 | if (in_array($response->message, [Message::PULL, Message::DISCARD], true) 158 | && $response->signature === Signature::SUCCESS 159 | && (($response->content['has_more'] ?? false) || $this->openStreams)) 160 | $this->serverState = $this->serverState === ServerState::TX_READY ? ServerState::TX_STREAMING : ServerState::STREAMING; 161 | if ($transition[3] === ServerState::DEFUNCT) 162 | $this->connection->disconnect(); 163 | break; 164 | } 165 | } 166 | 167 | return $response; 168 | } 169 | 170 | public function __destruct() 171 | { 172 | if (getenv('BOLT_ANALYTICS_OPTOUT')) { 173 | return; 174 | } 175 | 176 | if (method_exists(CacheProvider::get(), 'lock')) { 177 | CacheProvider::get()->lock('analytics'); 178 | } 179 | 180 | // update analytics data 181 | $data = (array)CacheProvider::get()->get('analytics'); 182 | $data[strtotime('today')] = [ 183 | 'queries' => ($data[strtotime('today')]['queries'] ?? 0) + $this->writeCalls, 184 | 'sessions' => ($data[strtotime('today')]['sessions'] ?? 0) + 1 185 | ]; 186 | CacheProvider::get()->set('analytics', $data); 187 | 188 | if (method_exists(CacheProvider::get(), 'unlock')) { 189 | CacheProvider::get()->unlock('analytics'); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/protocol/IStructure.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x0E)); 21 | $this->pipelinedMessages[] = Message::ACK_FAILURE; 22 | return $this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/protocol/v1/AvailableStructures.php: -------------------------------------------------------------------------------- 1 | Date::class, 33 | 0x54 => Time::class, 34 | 0x74 => LocalTime::class, 35 | 0x46 => DateTime::class, 36 | 0x66 => DateTimeZoneId::class, 37 | 0x64 => LocalDateTime::class, 38 | 0x45 => Duration::class, 39 | 0x58 => Point2D::class, 40 | 0x59 => Point3D::class, 41 | ]; 42 | 43 | protected array $unpackStructuresLt = [ 44 | 0x4E => Node::class, 45 | 0x52 => Relationship::class, 46 | 0x72 => UnboundRelationship::class, 47 | 0x50 => Path::class, 48 | 0x44 => Date::class, 49 | 0x54 => Time::class, 50 | 0x74 => LocalTime::class, 51 | 0x46 => DateTime::class, 52 | 0x66 => DateTimeZoneId::class, 53 | 0x64 => LocalDateTime::class, 54 | 0x45 => Duration::class, 55 | 0x58 => Point2D::class, 56 | 0x59 => Point3D::class, 57 | ]; 58 | } 59 | -------------------------------------------------------------------------------- /src/protocol/v1/DiscardAllMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x2F)); 20 | $this->pipelinedMessages[] = Message::DISCARD_ALL; 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/protocol/v1/InitMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x01, $userAgent, $authToken)); 20 | $this->pipelinedMessages[] = Message::INIT; 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/protocol/v1/PullAllMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x3F)); 21 | $this->pipelinedMessages[] = Message::PULL_ALL; 22 | return $this; 23 | } 24 | 25 | /** 26 | * Read PULL_ALL response 27 | * @throws BoltException 28 | */ 29 | protected function _pullAll(): iterable 30 | { 31 | do { 32 | $content = $this->read($signature); 33 | yield new Response(Message::PULL_ALL, $signature, $content); 34 | } while ($signature == Signature::RECORD); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/protocol/v1/ResetMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x0F)); 20 | $this->pipelinedMessages[] = Message::RESET; 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/protocol/v1/RunMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x10, $query, (object)$parameters)); 20 | $this->pipelinedMessages[] = Message::RUN; 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/protocol/v1/ServerStateTransition.php: -------------------------------------------------------------------------------- 1 | days) . ' days +0000', 0)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/DateTime.php: -------------------------------------------------------------------------------- 1 | utc_nanoseconds = (seconds * 1000000000) + nanoseconds - (tx_offset_minutes * 60 * 1000000000) 16 | * 17 | * @author Michal Stefanak 18 | * @link https://github.com/neo4j-php/Bolt 19 | * @link https://www.neo4j.com/docs/bolt/current/bolt/structure-semantics/#structure-legacy-datetime 20 | * @package Bolt\protocol\v1\structures 21 | */ 22 | class DateTime implements IStructure 23 | { 24 | /** 25 | * @param int $seconds seconds since the adjusted Unix epoch. This is not UTC 26 | * @param int $nanoseconds 27 | * @param int $tz_offset_seconds specifies the offset in seconds from UTC 28 | */ 29 | public function __construct( 30 | public readonly int $seconds, 31 | public readonly int $nanoseconds, 32 | public readonly int $tz_offset_seconds 33 | ) 34 | { 35 | } 36 | 37 | public function __toString(): string 38 | { 39 | $datetime = sprintf("%d", $this->seconds - $this->tz_offset_seconds) 40 | . '.' . substr(sprintf("%09d", $this->nanoseconds), 0, 6); 41 | return \DateTime::createFromFormat('U.u', $datetime, new \DateTimeZone('UTC')) 42 | ->setTimezone(new \DateTimeZone(sprintf("%+'05d", $this->tz_offset_seconds / 3600 * 100))) 43 | ->format('Y-m-d\TH:i:s.uP'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/DateTimeZoneId.php: -------------------------------------------------------------------------------- 1 | utc_nanoseconds = (seconds * 1000000000) + nanoseconds - get_offset_in_nanoseconds(tz_id) 16 | * 17 | * @author Michal Stefanak 18 | * @link https://github.com/neo4j-php/Bolt 19 | * @link https://www.neo4j.com/docs/bolt/current/bolt/structure-semantics/#structure-legacy-datetimezoneid 20 | * @package Bolt\protocol\v1\structures 21 | */ 22 | class DateTimeZoneId implements IStructure 23 | { 24 | /** 25 | * @param int $seconds seconds since the adjusted Unix epoch. This is not UTC 26 | * @param int $nanoseconds 27 | * @param string $tz_id identifier for a specific time zone 28 | */ 29 | public function __construct( 30 | public readonly int $seconds, 31 | public readonly int $nanoseconds, 32 | public readonly string $tz_id 33 | ) 34 | { 35 | } 36 | 37 | public function __toString(): string 38 | { 39 | $datetime = sprintf("%d", $this->seconds) 40 | . '.' . substr(sprintf("%09d", $this->nanoseconds), 0, 6); 41 | return \DateTime::createFromFormat('U.u', $datetime, new \DateTimeZone($this->tz_id)) 42 | ->format('Y-m-d\TH:i:s.u') . '[' . $this->tz_id . ']'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/Duration.php: -------------------------------------------------------------------------------- 1 | months / 12); 33 | if (!empty($years)) 34 | $output .= $years . 'Y'; 35 | if (!empty($this->months % 12)) 36 | $output .= ($this->months % 12) . 'M'; 37 | if (!empty($this->days)) 38 | $output .= $this->days . 'D'; 39 | 40 | $time = ''; 41 | $hours = floor($this->seconds / 3600); 42 | if (!empty($hours)) 43 | $time .= $hours . 'H'; 44 | $minutes = floor($this->seconds % 3600 / 60); 45 | if (!empty($minutes)) 46 | $time .= $minutes . 'M'; 47 | 48 | $seconds = rtrim(sprintf("%d", $this->seconds % 3600 % 60) 49 | . '.' . substr(sprintf("%09d", $this->nanoseconds), 0, 6), '0.'); 50 | if (!empty($seconds)) 51 | $time .= $seconds . 'S'; 52 | 53 | if (!empty($time)) 54 | $output .= 'T' . $time; 55 | 56 | return $output; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/LocalDateTime.php: -------------------------------------------------------------------------------- 1 | seconds) 34 | . '.' . substr(sprintf("%09d", $this->nanoseconds), 0, 6); 35 | return \DateTime::createFromFormat('U.u', $datetime) 36 | ->format('Y-m-d\TH:i:s.u'); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/LocalTime.php: -------------------------------------------------------------------------------- 1 | nanoseconds); 30 | $seconds = substr($value, 0, -9); 31 | if (empty($seconds)) 32 | $seconds = '0'; 33 | $fraction = substr($value, -9, 6); 34 | 35 | return \DateTime::createFromFormat('U.u', $seconds . '.' . $fraction) 36 | ->format('H:i:s.u'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/Node.php: -------------------------------------------------------------------------------- 1 | $this->id, 30 | 'labels' => $this->labels, 31 | 'properties' => $this->properties 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/Path.php: -------------------------------------------------------------------------------- 1 | json_decode(reset($this->nodes), true), 35 | 'end' => json_decode(end($this->nodes), true), 36 | 'segments' => [], 37 | 'length' => count($this->ids) - 1 38 | ]; 39 | 40 | for ($i = 0; $i < count($this->nodes) - 1; $i++) { 41 | $obj['segments'][] = [ 42 | 'start' => json_decode($this->nodes[$i], true), 43 | 'relationship' => array_merge(json_decode($this->rels[$i], true), ['start' => $this->nodes[$i]->id, 'end' => $this->nodes[$i + 1]->id()]), 44 | 'end' => json_decode($this->nodes[$i + 1], true) 45 | ]; 46 | } 47 | 48 | return json_encode($obj); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/Point2D.php: -------------------------------------------------------------------------------- 1 | srid . ', ' . 'x: ' . $this->x . ', y: ' . $this->y . '})'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/Point3D.php: -------------------------------------------------------------------------------- 1 | srid . ', ' . 'x: ' . $this->x . ', y: ' . $this->y . ', z: ' . $this->z . '})'; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/Relationship.php: -------------------------------------------------------------------------------- 1 | $this->id, 32 | 'start' => $this->startNodeId, 33 | 'end' => $this->endNodeId, 34 | 'type' => $this->type, 35 | 'properties' => $this->properties 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/Time.php: -------------------------------------------------------------------------------- 1 | utc_nanoseconds = nanoseconds - (tz_offset_seconds * 1000000000) 15 | * 16 | * @author Michal Stefanak 17 | * @link https://github.com/neo4j-php/Bolt 18 | * @link https://www.neo4j.com/docs/bolt/current/bolt/structure-semantics/#structure-time 19 | * @package Bolt\protocol\v1\structures 20 | */ 21 | class Time implements IStructure 22 | { 23 | /** 24 | * @param int $nanoseconds nanoseconds since midnight. This time is not UTC 25 | * @param int $tz_offset_seconds offset in seconds from UTC 26 | */ 27 | public function __construct( 28 | public readonly int $nanoseconds, 29 | public readonly int $tz_offset_seconds 30 | ) 31 | { 32 | } 33 | 34 | public function __toString(): string 35 | { 36 | $value = sprintf("%09d", $this->nanoseconds - $this->tz_offset_seconds * 1e9); 37 | $seconds = substr($value, 0, -9); 38 | if (empty($seconds)) 39 | $seconds = '0'; 40 | $fraction = substr($value, -9, 6); 41 | 42 | return \DateTime::createFromFormat('U.u', $seconds . '.' . $fraction, new \DateTimeZone('UTC')) 43 | ->setTimezone(new \DateTimeZone(sprintf("%+'05d", $this->tz_offset_seconds / 3600 * 100))) 44 | ->format('H:i:s.uP'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/protocol/v1/structures/UnboundRelationship.php: -------------------------------------------------------------------------------- 1 | $this->id, 30 | 'type' => $this->type, 31 | 'properties' => $this->properties 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/protocol/v3/BeginMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x11, (object)$extra)); 20 | $this->pipelinedMessages[] = Message::BEGIN; 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/protocol/v3/CommitMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x12)); 21 | $this->pipelinedMessages[] = Message::COMMIT; 22 | return $this; 23 | } 24 | 25 | /** 26 | * Read COMMIT response 27 | * @return iterable 28 | * @throws BoltException 29 | */ 30 | protected function _commit(): iterable 31 | { 32 | $this->openStreams = 0; 33 | $content = $this->read($signature); 34 | yield new Response(Message::COMMIT, $signature, $content); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/protocol/v3/GoodbyeMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x02)); 20 | $this->connection->disconnect(); 21 | $this->serverState = ServerState::DEFUNCT; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/protocol/v3/HelloMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x01, $extra)); 21 | $this->pipelinedMessages[] = Message::HELLO; 22 | return $this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/protocol/v3/RollbackMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x13)); 21 | $this->pipelinedMessages[] = Message::ROLLBACK; 22 | return $this; 23 | } 24 | 25 | /** 26 | * Read ROLLBACK response 27 | * @return iterable 28 | * @throws BoltException 29 | */ 30 | protected function _rollback(): iterable 31 | { 32 | $this->openStreams = 0; 33 | $content = $this->read($signature); 34 | yield new Response(Message::ROLLBACK, $signature, $content); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/protocol/v3/RunMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack( 21 | 0x10, 22 | $query, 23 | (object)$parameters, 24 | (object)$extra 25 | )); 26 | $this->pipelinedMessages[] = Message::RUN; 27 | return $this; 28 | } 29 | 30 | /** 31 | * Read RUN response 32 | * @return iterable 33 | * @throws BoltException 34 | */ 35 | protected function _run(): iterable 36 | { 37 | $content = $this->read($signature); 38 | if (array_key_exists('qid', $content)) 39 | $this->openStreams++; 40 | yield new Response(Message::RUN, $signature, $content); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/protocol/v3/ServerStateTransition.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x2F, $extra)); 25 | $this->pipelinedMessages[] = Message::DISCARD; 26 | return $this; 27 | } 28 | 29 | /** 30 | * Read DISCARD response 31 | * @return iterable 32 | * @throws BoltException 33 | */ 34 | protected function _discard(): iterable 35 | { 36 | $content = $this->read($signature); 37 | if (!($content['has_more'] ?? false) && $this->openStreams) 38 | $this->openStreams = $signature === Signature::SUCCESS ? $this->openStreams - 1 : 0; 39 | yield new Response(Message::DISCARD, $signature, $content); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/protocol/v4/PullMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x3F, $extra)); 24 | $this->pipelinedMessages[] = Message::PULL; 25 | return $this; 26 | } 27 | 28 | /** 29 | * Read PULL responses 30 | * @return iterable 31 | * @throws BoltException 32 | */ 33 | protected function _pull(): iterable 34 | { 35 | do { 36 | $content = $this->read($signature); 37 | if (!($content['has_more'] ?? false) && $this->openStreams) { 38 | if ($signature === Signature::SUCCESS) 39 | $this->openStreams--; 40 | elseif ($signature === Signature::FAILURE) 41 | $this->openStreams = 0; 42 | } 43 | yield new Response(Message::PULL, $signature, $content); 44 | } while ($signature == Signature::RECORD); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/protocol/v4/ServerStateTransition.php: -------------------------------------------------------------------------------- 1 | __hello($extra); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/protocol/v4_3/AvailableStructures.php: -------------------------------------------------------------------------------- 1 | Date::class, 37 | 0x54 => Time::class, 38 | 0x74 => LocalTime::class, 39 | 0x46 => DateTime::class, 40 | 0x49 => v5_DateTime::class, 41 | 0x66 => DateTimeZoneId::class, 42 | 0x69 => v5_DateTimeZoneId::class, 43 | 0x64 => LocalDateTime::class, 44 | 0x45 => Duration::class, 45 | 0x58 => Point2D::class, 46 | 0x59 => Point3D::class, 47 | ]; 48 | 49 | protected array $unpackStructuresLt = [ 50 | 0x4E => Node::class, 51 | 0x52 => Relationship::class, 52 | 0x72 => UnboundRelationship::class, 53 | 0x50 => Path::class, 54 | 0x44 => Date::class, 55 | 0x54 => Time::class, 56 | 0x74 => LocalTime::class, 57 | 0x46 => DateTime::class, 58 | 0x49 => v5_DateTime::class, 59 | 0x66 => DateTimeZoneId::class, 60 | 0x69 => v5_DateTimeZoneId::class, 61 | 0x64 => LocalDateTime::class, 62 | 0x45 => Duration::class, 63 | 0x58 => Point2D::class, 64 | 0x59 => Point3D::class, 65 | ]; 66 | } 67 | -------------------------------------------------------------------------------- /src/protocol/v4_3/RouteMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x66, (object)$routing, $bookmarks, $db)); 20 | $this->pipelinedMessages[] = Message::ROUTE; 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/protocol/v4_4/RouteMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x66, (object)$routing, $bookmarks, (object)$extra)); 21 | $this->pipelinedMessages[] = Message::ROUTE; 22 | return $this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/protocol/v5/AvailableStructures.php: -------------------------------------------------------------------------------- 1 | Date::class, 35 | 0x54 => Time::class, 36 | 0x74 => LocalTime::class, 37 | 0x49 => DateTime::class, 38 | 0x69 => DateTimeZoneId::class, 39 | 0x64 => LocalDateTime::class, 40 | 0x45 => Duration::class, 41 | 0x58 => Point2D::class, 42 | 0x59 => Point3D::class, 43 | ]; 44 | 45 | protected array $unpackStructuresLt = [ 46 | 0x4E => Node::class, 47 | 0x52 => Relationship::class, 48 | 0x72 => UnboundRelationship::class, 49 | 0x50 => Path::class, 50 | 0x44 => Date::class, 51 | 0x54 => Time::class, 52 | 0x74 => LocalTime::class, 53 | 0x49 => DateTime::class, 54 | 0x69 => DateTimeZoneId::class, 55 | 0x64 => LocalDateTime::class, 56 | 0x45 => Duration::class, 57 | 0x58 => Point2D::class, 58 | 0x59 => Point3D::class, 59 | ]; 60 | } 61 | -------------------------------------------------------------------------------- /src/protocol/v5/structures/DateTime.php: -------------------------------------------------------------------------------- 1 | seconds) . '.' . substr(sprintf("%09d", $this->nanoseconds), 0, 6); 24 | return \DateTime::createFromFormat('U.u', $datetime, new \DateTimeZone('UTC')) 25 | ->setTimezone(new \DateTimeZone(sprintf("%+'05d", $this->tz_offset_seconds / 3600 * 100))) 26 | ->format('Y-m-d\TH:i:s.uP'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/protocol/v5/structures/DateTimeZoneId.php: -------------------------------------------------------------------------------- 1 | seconds) . '.' . substr(sprintf("%09d", $this->nanoseconds), 0, 6); 24 | return \DateTime::createFromFormat('U.u', $datetime, new \DateTimeZone('UTC')) 25 | ->setTimezone(new \DateTimeZone($this->tz_id)) 26 | ->format('Y-m-d\TH:i:s.u') . '[' . $this->tz_id . ']'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/protocol/v5/structures/Node.php: -------------------------------------------------------------------------------- 1 | $this->id, 32 | 'labels' => $this->labels, 33 | 'properties' => $this->properties, 34 | 'element_id' => $this->element_id 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/protocol/v5/structures/Relationship.php: -------------------------------------------------------------------------------- 1 | $this->id, 36 | 'start' => $this->startNodeId, 37 | 'end' => $this->endNodeId, 38 | 'type' => $this->type, 39 | 'properties' => $this->properties, 40 | 'element_id' => $this->element_id, 41 | 'start_node_element_id' => $this->start_node_element_id, 42 | 'end_node_element_id' => $this->end_node_element_id 43 | ]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/protocol/v5/structures/UnboundRelationship.php: -------------------------------------------------------------------------------- 1 | $this->id, 32 | 'type' => $this->type, 33 | 'properties' => $this->properties, 34 | 'element_id' => $this->element_id 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/protocol/v5_1/HelloMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x01, (object)$extra)); 25 | $this->pipelinedMessages[] = Message::HELLO; 26 | return $this; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/protocol/v5_1/LogoffMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x6B)); 20 | $this->pipelinedMessages[] = Message::LOGOFF; 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/protocol/v5_1/LogonMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x6A, (object)$auth)); 20 | $this->pipelinedMessages[] = Message::LOGON; 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/protocol/v5_1/ServerStateTransition.php: -------------------------------------------------------------------------------- 1 | 'php-bolt/' . \Composer\InstalledVersions::getPrettyVersion('stefanak-michal/bolt'), 25 | 'platform' => php_uname(), 26 | 'language' => 'PHP/' . phpversion(), 27 | 'language_details' => 'null' 28 | ]; 29 | 30 | return $this->__hello($extra); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/protocol/v5_4/TelemetryMessage.php: -------------------------------------------------------------------------------- 1 | write($this->packer->pack(0x54, $api)); 20 | $this->pipelinedMessages[] = Message::TELEMETRY; 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/BoltTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Sockets extension not available'); 23 | 24 | $conn = new \Bolt\connection\Socket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687, 3); 25 | $this->assertInstanceOf(\Bolt\connection\Socket::class, $conn); 26 | 27 | $bolt = new Bolt($conn); 28 | $this->assertInstanceOf(Bolt::class, $bolt); 29 | 30 | $protocol = $bolt->setProtocolVersions($this->getCompatibleBoltVersion())->build(); 31 | $this->assertInstanceOf(AProtocol::class, $protocol); 32 | 33 | $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); 34 | 35 | if (method_exists($protocol, 'goodbye')) 36 | $protocol->goodbye(); 37 | } 38 | 39 | public function testAura(): void 40 | { 41 | $conn = new \Bolt\connection\StreamSocket('neo4j+s://demo.neo4jlabs.com'); 42 | $conn->setSslContextOptions([ 43 | 'verify_peer' => true 44 | ]); 45 | $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); 46 | 47 | $bolt = new Bolt($conn); 48 | $this->assertInstanceOf(Bolt::class, $bolt); 49 | 50 | $protocol = $bolt->setProtocolVersions($this->getCompatibleBoltVersion('https://demo.neo4jlabs.com:7473'))->build(); 51 | $this->assertInstanceOf(AProtocol::class, $protocol); 52 | 53 | $this->sayHello($protocol, 'movies', 'movies'); 54 | 55 | if (method_exists($protocol, 'goodbye')) 56 | $protocol->goodbye(); 57 | } 58 | 59 | public function testHello(): AProtocol 60 | { 61 | $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687); 62 | $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); 63 | 64 | $bolt = new Bolt($conn); 65 | $this->assertInstanceOf(Bolt::class, $bolt); 66 | 67 | $protocol = $bolt->setProtocolVersions($this->getCompatibleBoltVersion())->build(); 68 | $this->assertInstanceOf(AProtocol::class, $protocol); 69 | 70 | $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); 71 | 72 | return $protocol; 73 | } 74 | 75 | /** 76 | * @depends testHello 77 | */ 78 | public function testPull(AProtocol $protocol): void 79 | { 80 | $protocol 81 | ->run('RETURN 1 AS num, 2 AS cnt', [], ['mode' => 'r']) 82 | ->pull(); 83 | 84 | $this->assertArrayHasKey('fields', $protocol->getResponse()->content); 85 | 86 | $res = $protocol->getResponse()->content; 87 | $this->assertEquals(1, $res[0] ?? 0); 88 | $this->assertEquals(2, $res[1] ?? 0); 89 | $protocol->getResponse(); // last success message 90 | } 91 | 92 | /** 93 | * @depends testHello 94 | */ 95 | public function testDiscard(AProtocol $protocol): void 96 | { 97 | $gen = $protocol 98 | ->run('MATCH (a:Test) RETURN *', [], ['mode' => 'r']) 99 | ->discard() 100 | ->getResponses(); 101 | 102 | foreach ($gen as $response) { 103 | $this->assertEquals(Signature::SUCCESS, $response->signature); 104 | } 105 | } 106 | 107 | /** 108 | * @depends testHello 109 | * @throws Exception 110 | */ 111 | public function testTransaction(AProtocol $protocol): void 112 | { 113 | if (version_compare($protocol->getVersion(), 3, '<')) { 114 | $this->markTestSkipped('Old Neo4j version does not support transactions'); 115 | } 116 | 117 | $res = iterator_to_array( 118 | $protocol 119 | ->begin() 120 | ->run('CREATE (a:Test) RETURN a, ID(a)') 121 | ->pull() 122 | ->rollback() 123 | ->getResponses(), 124 | false 125 | ); 126 | 127 | $id = $res[2]->content[1]; 128 | $this->assertIsInt($id); 129 | 130 | $res = iterator_to_array( 131 | $protocol 132 | ->run('MATCH (a:Test) WHERE ID(a) = ' 133 | . (version_compare($protocol->getVersion(), 4, '<') ? '{a}' : '$a') 134 | . ' RETURN COUNT(a)', [ 135 | 'a' => $id 136 | ]) 137 | ->pull() 138 | ->getResponses(), 139 | false 140 | ); 141 | 142 | $this->assertEquals(0, $res[1]->content[0]); 143 | } 144 | 145 | /** 146 | * @depends testHello 147 | */ 148 | public function testRoute(AProtocol $protocol): void 149 | { 150 | if (version_compare($protocol->getVersion(), 4.3, '>=')) { 151 | $response = $protocol 152 | ->route([ 153 | 'address' => ($GLOBALS['NEO_HOST'] ?? '127.0.0.1') . ':' . ($GLOBALS['NEO_PORT'] ?? 7687) 154 | ]) 155 | ->getResponse(); 156 | $this->assertEquals(Signature::SUCCESS, $response->signature); 157 | } else { 158 | $this->markTestSkipped('Old Neo4j version does not support route message'); 159 | } 160 | } 161 | 162 | /** 163 | * @depends testHello 164 | */ 165 | public function testReset(AProtocol $protocol): void 166 | { 167 | $response = $protocol 168 | ->reset() 169 | ->getResponse(); 170 | $this->assertEquals(Signature::SUCCESS, $response->signature); 171 | } 172 | 173 | /** 174 | * @large 175 | * @depends testHello 176 | * @throws Exception 177 | */ 178 | public function testChunking(AProtocol $protocol): void 179 | { 180 | $gen = $protocol 181 | ->begin() 182 | ->run('CREATE (a:Test) RETURN ID(a)') 183 | ->pull() 184 | ->getResponses(); 185 | $id = iterator_to_array($gen, false)[2]->content[0]; 186 | 187 | $data = []; 188 | while (strlen(serialize($data)) < 65535 * 2) { 189 | $data[base64_encode(random_bytes(32))] = base64_encode(random_bytes(128)); 190 | $gen = $protocol 191 | ->run('MATCH (a:Test) WHERE ID(a) = $id SET a += $data RETURN a', [ 192 | 'id' => $id, 193 | 'data' => (object)$data 194 | ]) 195 | ->pull() 196 | ->getResponses(); 197 | $result = iterator_to_array($gen, false); 198 | $this->assertInstanceOf(\Bolt\protocol\v1\structures\Node::class, $result[1]->content[0]); 199 | $this->assertCount(count($data), $result[1]->content[0]->properties); 200 | } 201 | 202 | $protocol->rollback(); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /tests/PerformanceTest.php: -------------------------------------------------------------------------------- 1 | setProtocolVersions($this->getCompatibleBoltVersion())->build(); 25 | 26 | $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); 27 | 28 | //prevent multiple runs at once 29 | while (true) { 30 | $protocol->run('MATCH (n:Test50k) RETURN count(n)')->getResponse(); 31 | /** @var Response $response */ 32 | $response = $protocol->pull()->getResponse(); 33 | if ($response->signature !== Signature::RECORD) 34 | $this->markTestSkipped(); 35 | $protocol->getResponse(); 36 | if ($response->content[0] > 0) { 37 | sleep(60); 38 | } else { 39 | iterator_to_array($protocol->run('CREATE (n:Test50k)')->pull()->getResponses(), false); 40 | break; 41 | } 42 | } 43 | 44 | $generator = new RandomDataGenerator($amount); 45 | /** @var Response $response */ 46 | $response = $protocol 47 | ->run('UNWIND $x as x RETURN x', ['x' => $generator]) 48 | ->getResponse(); 49 | 50 | if ($response->signature !== Signature::SUCCESS) 51 | $this->markTestIncomplete('[' . $response->content['code'] . '] ' . $response->content['message']); 52 | 53 | $count = 0; 54 | /** @var Response $response */ 55 | foreach ($protocol->pull()->getResponses() as $response) { 56 | if ($response->signature === Signature::RECORD) 57 | $count++; 58 | } 59 | 60 | $protocol->run('MATCH (n:Test50k) DELETE n')->getResponses(); 61 | $this->assertEquals($amount, $count); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/TestLayer.php: -------------------------------------------------------------------------------- 1 | assertEquals(Signature::SUCCESS, $protocol->init('bolt-php', [ 41 | 'scheme' => 'basic', 42 | 'principal' => $name, 43 | 'credentials' => $password 44 | ])->getResponse()->signature); 45 | } elseif (method_exists($protocol, 'logon')) { 46 | $this->assertEquals(Signature::SUCCESS, $protocol->hello()->getResponse()->signature); 47 | $this->assertEquals(Signature::SUCCESS, $protocol->logon([ 48 | 'scheme' => 'basic', 49 | 'principal' => $name, 50 | 'credentials' => $password 51 | ])->getResponse()->signature); 52 | } else { 53 | $this->assertEquals(Signature::SUCCESS, $protocol->hello([ 54 | 'user_agent' => 'bolt-php', 55 | 'scheme' => 'basic', 56 | 'principal' => $name, 57 | 'credentials' => $password, 58 | ])->getResponse()->signature); 59 | } 60 | } 61 | 62 | /** 63 | * Choose the right bolt version by Neo4j version 64 | * Neo4j version is received by HTTP request on browser port 65 | * @param string|null $url 66 | * @return float|int 67 | * @link https://neo4j.com/docs/http-api/current/endpoints/#discovery-api 68 | */ 69 | protected function getCompatibleBoltVersion(string $url = null): float|int 70 | { 71 | $json = file_get_contents($url ?? $GLOBALS['NEO_BROWSER'] ?? ('http://' . ($GLOBALS['NEO_HOST'] ?? 'localhost') . ':7474/')); 72 | $decoded = json_decode($json, true); 73 | if (json_last_error() !== JSON_ERROR_NONE) 74 | $this->markTestIncomplete('Not able to obtain Neo4j version through HTTP'); 75 | 76 | $neo4jVersion = $decoded['neo4j_version']; 77 | 78 | if (version_compare($neo4jVersion, '5.26', '>=')) 79 | return 5.8; 80 | if (version_compare($neo4jVersion, '5.23', '>=')) 81 | return 5.6; 82 | if (version_compare($neo4jVersion, '5.13', '>=')) 83 | return 5.4; 84 | if (version_compare($neo4jVersion, '5.9', '>=')) 85 | return 5.3; 86 | if (version_compare($neo4jVersion, '5.7', '>=')) 87 | return 5.2; 88 | if (version_compare($neo4jVersion, '5.5', '>=')) 89 | return 5.1; 90 | if (version_compare($neo4jVersion, '5.0', '>=')) 91 | return 5; 92 | if (version_compare($neo4jVersion, '4.4', '>=')) 93 | return 4.4; 94 | if (version_compare($neo4jVersion, '4.3', '>=')) 95 | return 4.3; 96 | if (version_compare($neo4jVersion, '4.2', '>=')) 97 | return 4.2; 98 | if (version_compare($neo4jVersion, '4.1', '>=')) 99 | return 4.1; 100 | if (version_compare($neo4jVersion, '4', '>=')) 101 | return 4; 102 | if (version_compare($neo4jVersion, '3.5', '>=')) 103 | return 3; 104 | if (version_compare($neo4jVersion, '3.4', '>=')) 105 | return 2; 106 | return 1; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/connection/ConnectionTest.php: -------------------------------------------------------------------------------- 1 | [StreamSocket::class], 27 | Socket::class => [Socket::class], 28 | ]; 29 | } 30 | 31 | /** 32 | * @dataProvider provideConnections 33 | */ 34 | public function testMillisecondTimeout(string $alias): void 35 | { 36 | $conn = $this->getConnection($alias); 37 | $conn->setTimeout(1.5); 38 | $protocol = (new Bolt($conn))->setProtocolVersions($this->getCompatibleBoltVersion())->build(); 39 | $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); 40 | $this->expectException(ConnectionTimeoutException::class); 41 | $protocol 42 | ->run('FOREACH ( i IN range(1,10000) | MERGE (d:Day {day: i}) )') 43 | ->getResponse(); 44 | } 45 | 46 | /** 47 | * @dataProvider provideConnections 48 | */ 49 | public function testLongNoTimeout(string $alias): void 50 | { 51 | $conn = $this->getConnection($alias); 52 | $protocol = (new Bolt($conn))->setProtocolVersions($this->getCompatibleBoltVersion())->build(); 53 | $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); 54 | $conn->setTimeout(200); 55 | $protocol 56 | ->run('CALL apoc.util.sleep(150000)', [], ['mode' => 'r', 'tx_timeout' => 120000]) 57 | ->getResponse(); 58 | } 59 | 60 | /** 61 | * @dataProvider provideConnections 62 | */ 63 | public function testSecondsTimeout(string $alias): void 64 | { 65 | $conn = $this->getConnection($alias); 66 | $conn->setTimeout(1); 67 | $protocol = (new Bolt($conn))->setProtocolVersions($this->getCompatibleBoltVersion())->build(); 68 | $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); 69 | $this->expectException(ConnectionTimeoutException::class); 70 | $protocol 71 | ->run('FOREACH ( i IN range(1,10000) | MERGE (d:Day {day: i}) )') 72 | ->getResponse(); 73 | } 74 | 75 | /** 76 | * @dataProvider provideConnections 77 | */ 78 | public function testTimeoutRecoverAndReset(string $alias): void 79 | { 80 | $conn = $this->getConnection($alias); 81 | $protocol = (new Bolt($conn))->setProtocolVersions($this->getCompatibleBoltVersion())->build(); 82 | $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); 83 | 84 | $conn->setTimeout(1.5); 85 | $time = microtime(true); 86 | try { 87 | iterator_to_array( 88 | $protocol 89 | ->run('FOREACH ( i IN range(1,10000) | MERGE (d:Day {day: i}) )') 90 | ->pull() 91 | ->getResponses(), 92 | false); 93 | $this->fail('No timeout error triggered'); 94 | } catch (ConnectionTimeoutException) { 95 | $newTime = microtime(true); 96 | $this->assertGreaterThanOrEqual(1.0, $newTime - $time); 97 | } 98 | 99 | $conn->setTimeout(15.0); 100 | $response = $protocol 101 | ->reset() 102 | ->getResponse(); 103 | 104 | $this->assertEquals(Signature::FAILURE, $response->signature); 105 | $protocol = (new Bolt($conn))->setProtocolVersions($this->getCompatibleBoltVersion())->build(); 106 | $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); 107 | 108 | $conn->setTimeout(1.5); 109 | $time = microtime(true); 110 | try { 111 | $protocol 112 | ->run('FOREACH ( i IN range(1,10000) | MERGE (d:Day {day: i}) )') 113 | ->getResponse(); 114 | $this->fail('No timeout error triggered'); 115 | } catch (ConnectionTimeoutException) { 116 | $newTime = microtime(true); 117 | $this->assertGreaterThanOrEqual(1.0, $newTime - $time); 118 | } 119 | } 120 | 121 | private function getConnection(string $class): IConnection 122 | { 123 | return new $class($GLOBALS['NEO_HOST'] ?? '127.0.0.1', (int)($GLOBALS['NEO_PORT'] ?? 7687), 1); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/error/ErrorsTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); 20 | $this->expectException(\Bolt\error\ConnectException::class); 21 | $conn->connect(); 22 | } 23 | 24 | public function testPackException1(): void 25 | { 26 | $packer = new \Bolt\packstream\v1\Packer(); 27 | $this->assertInstanceOf(\Bolt\packstream\v1\Packer::class, $packer); 28 | $this->expectException(\Bolt\error\PackException::class); 29 | foreach ($packer->pack(0x00, fopen('php://input', 'r')) as $chunk) { 30 | $this->markTestIncomplete(); 31 | } 32 | } 33 | 34 | public function testPackException2(): void 35 | { 36 | $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687); 37 | $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); 38 | 39 | $bolt = new \Bolt\Bolt($conn); 40 | $this->assertInstanceOf(\Bolt\Bolt::class, $bolt); 41 | 42 | $this->expectException(\Bolt\error\PackException::class); 43 | $bolt->setPackStreamVersion(2); 44 | $bolt->build(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/helpers/FileCacheTest.php: -------------------------------------------------------------------------------- 1 | cache = new FileCache(); 15 | } 16 | 17 | public function testConstruct(): void 18 | { 19 | $this->assertDirectoryExists(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-bolt-filecache'); 20 | $this->assertDirectoryExists(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-bolt-filecache' . DIRECTORY_SEPARATOR . '.ttl'); 21 | } 22 | 23 | public function testGetAndSet(): void 24 | { 25 | $key = uniqid('key_', true); 26 | $value = uniqid('value_', true); 27 | $this->assertTrue($this->cache->set($key, $value)); 28 | $this->assertEquals($value, $this->cache->get($key)); 29 | } 30 | 31 | public function testDelete(): void 32 | { 33 | $key = uniqid('key_', true); 34 | $value = uniqid('value_', true); 35 | $this->assertTrue($this->cache->set($key, $value)); 36 | $this->assertTrue($this->cache->delete($key)); 37 | $this->assertNull($this->cache->get($key)); 38 | } 39 | 40 | public function testClear(): void 41 | { 42 | $this->assertTrue($this->cache->set(uniqid('key_', true), uniqid('value_', true))); 43 | $this->assertTrue($this->cache->set(uniqid('key_', true), uniqid('value_', true))); 44 | $this->assertTrue($this->cache->clear()); 45 | $this->assertNull($this->cache->get(uniqid('key_', true))); 46 | $this->assertNull($this->cache->get(uniqid('key_', true))); 47 | } 48 | 49 | public function testGetMultiple(): void 50 | { 51 | $key1 = uniqid('key1_', true); 52 | $key2 = uniqid('key2_', true); 53 | $value1 = uniqid('value1_', true); 54 | $value2 = uniqid('value2_', true); 55 | $this->assertTrue($this->cache->set($key1, $value1)); 56 | $this->assertTrue($this->cache->set($key2, $value2)); 57 | $result = iterator_to_array($this->cache->getMultiple([$key1, $key2])); 58 | $this->assertEquals([$value1, $value2], $result); 59 | } 60 | 61 | public function testSetMultiple(): void 62 | { 63 | $values = [ 64 | uniqid('key1_', true) => uniqid('value1_', true), 65 | uniqid('key2_', true) => uniqid('value2_', true) 66 | ]; 67 | $this->assertTrue($this->cache->setMultiple($values)); 68 | foreach ($values as $key => $value) { 69 | $this->assertEquals($value, $this->cache->get($key)); 70 | } 71 | } 72 | 73 | public function testDeleteMultiple(): void 74 | { 75 | $key1 = uniqid('key1_', true); 76 | $key2 = uniqid('key2_', true); 77 | $value1 = uniqid('value1_', true); 78 | $value2 = uniqid('value2_', true); 79 | $this->assertTrue($this->cache->set($key1, $value1)); 80 | $this->assertTrue($this->cache->set($key2, $value2)); 81 | $this->assertTrue($this->cache->deleteMultiple([$key1, $key2])); 82 | $this->assertNull($this->cache->get($key1)); 83 | $this->assertNull($this->cache->get($key2)); 84 | } 85 | 86 | public function testHas(): void 87 | { 88 | $key = uniqid('key_', true); 89 | $value = uniqid('value_', true); 90 | $this->assertTrue($this->cache->set($key, $value)); 91 | $this->assertTrue($this->cache->has($key)); 92 | $this->assertTrue($this->cache->delete($key)); 93 | $this->assertFalse($this->cache->has($key)); 94 | } 95 | 96 | public function testLockAndUnlock(): void 97 | { 98 | $key = uniqid('key_', true); 99 | $value = uniqid('value_', true); 100 | $this->assertTrue($this->cache->set($key, $value)); 101 | $this->assertTrue($this->cache->lock($key)); 102 | $this->cache->unlock($key); 103 | 104 | $reflection = new \ReflectionClass($this->cache); 105 | $property = $reflection->getProperty('handles'); 106 | $property->setAccessible(true); 107 | $handles = $property->getValue($this->cache); 108 | $this->assertFalse(array_key_exists($key, $handles)); 109 | } 110 | 111 | public function testLockingMechanism(): void 112 | { 113 | $key = 'test_lock_key'; 114 | $this->assertTrue($this->cache->delete($key)); 115 | 116 | $descriptorspec = array( 117 | 0 => array("pipe", "r"), 118 | 1 => array("pipe", "w"), 119 | ); 120 | 121 | $t = microtime(true); 122 | // run another script in background 123 | $proc = proc_open('php ' . __DIR__ . DIRECTORY_SEPARATOR . 'lock.php', $descriptorspec, $pipes); 124 | // wait to make sure another script is running and it locked the key 125 | sleep(1); 126 | proc_close($proc); 127 | 128 | if ($this->cache->lock($key)) { 129 | $this->assertGreaterThan(3.0, microtime(true) - $t); 130 | $this->assertEquals(123, $this->cache->get($key)); 131 | $this->cache->unlock($key); 132 | } 133 | } 134 | 135 | public function testShutdown(): void 136 | { 137 | $key = 'test_lock_key'; 138 | $this->assertTrue($this->cache->delete($key)); 139 | 140 | $descriptorspec = array( 141 | 0 => array("pipe", "r"), 142 | 1 => array("pipe", "w"), 143 | ); 144 | 145 | $t = microtime(true); 146 | // Run another script in background 147 | $proc = proc_open('php ' . __DIR__ . DIRECTORY_SEPARATOR . 'lock.php', $descriptorspec, $pipes); 148 | $pid = proc_get_status($proc)['pid']; 149 | sleep(1); 150 | 151 | // Terminate the process 152 | if (strncasecmp(PHP_OS, 'WIN', 3) === 0) { 153 | exec("taskkill /F /PID $pid /T"); 154 | } else { 155 | exec("kill -9 $pid"); 156 | } 157 | 158 | proc_close($proc); 159 | 160 | $this->assertLessThan(3.0, microtime(true) - $t); 161 | $this->assertTrue($this->cache->has($key)); 162 | $this->assertNull($this->cache->get($key)); 163 | $this->assertTrue($this->cache->lock($key)); 164 | $this->cache->unlock($key); 165 | } 166 | 167 | public function testInvalidKey(): void 168 | { 169 | $this->expectException(\Psr\SimpleCache\InvalidArgumentException::class); 170 | $this->expectExceptionMessage('Invalid cache key: invalid key!. Allowed characters are A-Za-z0-9_.'); 171 | $this->cache->set('invalid key!', 'value'); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tests/helpers/lock.php: -------------------------------------------------------------------------------- 1 | lock($key)) { 11 | sleep(3); 12 | $cache->set($key, 123); 13 | $cache->unlock($key); 14 | } 15 | -------------------------------------------------------------------------------- /tests/packstream/v1/BytesTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); 20 | 21 | $bolt = new Bolt($conn); 22 | $this->assertInstanceOf(Bolt::class, $bolt); 23 | 24 | $protocol = $bolt->setProtocolVersions($this->getCompatibleBoltVersion())->build(); 25 | $this->assertInstanceOf(AProtocol::class, $protocol); 26 | 27 | $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); 28 | 29 | return $protocol; 30 | } 31 | 32 | /** 33 | * @depends testInit 34 | * @dataProvider providerBytes 35 | */ 36 | public function testBytes(Bytes $arr, AProtocol $protocol) 37 | { 38 | $res = iterator_to_array( 39 | $protocol 40 | ->run('RETURN $arr', ['arr' => $arr]) 41 | ->pull() 42 | ->getResponses(), 43 | false 44 | ); 45 | $this->assertEquals($arr, $res[1]->content[0]); 46 | } 47 | 48 | public function providerBytes(): \Generator 49 | { 50 | foreach ([1, 200, 60000, 70000] as $size) { 51 | $arr = new Bytes(); 52 | while (count($arr) < $size) { 53 | $arr[] = pack('H', mt_rand(0, 255)); 54 | } 55 | yield 'bytes: ' . count($arr) => [$arr]; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/packstream/v1/UnpackerTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); 23 | 24 | $bolt = new Bolt($conn); 25 | $this->assertInstanceOf(Bolt::class, $bolt); 26 | 27 | $protocol = $bolt->setProtocolVersions($this->getCompatibleBoltVersion())->build(); 28 | $this->assertInstanceOf(AProtocol::class, $protocol); 29 | 30 | $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); 31 | 32 | $conn->setTimeout(60 * 10); 33 | return $protocol; 34 | } 35 | 36 | /** 37 | * @depends testInit 38 | */ 39 | public function testNull(AProtocol $protocol): void 40 | { 41 | $gen = $protocol 42 | ->run('RETURN null', [], ['mode' => 'r']) 43 | ->pull() 44 | ->getResponses(); 45 | 46 | /** @var Response $response */ 47 | foreach ($gen as $response) { 48 | if ($response->signature == Signature::RECORD) 49 | $this->assertNull($response->content[0]); 50 | } 51 | } 52 | 53 | /** 54 | * @depends testInit 55 | */ 56 | public function testBoolean(AProtocol $protocol): void 57 | { 58 | $gen = $protocol 59 | ->run('RETURN true, false', [], ['mode' => 'r']) 60 | ->pull() 61 | ->getResponses(); 62 | 63 | /** @var Response $response */ 64 | foreach ($gen as $response) { 65 | if ($response->signature == Signature::RECORD) { 66 | $this->assertTrue($response->content[0]); 67 | $this->assertFalse($response->content[1]); 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * @depends testInit 74 | */ 75 | public function testInteger(AProtocol $protocol): void 76 | { 77 | $gen = $protocol 78 | ->run('RETURN -16, 0, 127, -17, -128, 128, 32767, 32768, 2147483647, 2147483648, 9223372036854775807, -129, -32768, -32769, -2147483648, -2147483649, -9223372036854775808', [], ['mode' => 'r']) 79 | ->pull() 80 | ->getResponses(); 81 | 82 | /** @var Response $response */ 83 | foreach ($gen as $response) { 84 | if ($response->signature == Signature::RECORD) { 85 | foreach ([-16, 0, 127, -17, -128, 128, 32767, 32768, 2147483647, 2147483648, 9223372036854775807, -129, -32768, -32769, -2147483648, -2147483649, -9223372036854775808] as $i => $value) { 86 | $this->assertEquals($value, $response->content[$i]); 87 | } 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * @depends testInit 94 | */ 95 | public function testFloat(AProtocol $protocol): void 96 | { 97 | for ($i = 0; $i < 10; $i++) { 98 | $num = mt_rand(-mt_getrandmax(), mt_getrandmax()) / mt_getrandmax(); 99 | 100 | $gen = $protocol 101 | ->run('RETURN ' . $num, [], ['mode' => 'r']) 102 | ->pull() 103 | ->getResponses(); 104 | 105 | /** @var Response $response */ 106 | foreach ($gen as $response) { 107 | if ($response->signature == Signature::RECORD) { 108 | $this->assertEqualsWithDelta($num, $response->content[0], 0.000001); 109 | } 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * @depends testInit 116 | * @dataProvider stringProvider 117 | */ 118 | public function testString(string $str, AProtocol $protocol): void 119 | { 120 | $gen = $protocol 121 | ->run('RETURN "' . str_replace(['\\', '"'], ['\\\\', '\\"'], $str) . '" AS a', [], ['mode' => 'r']) 122 | ->pull() 123 | ->getResponses(); 124 | 125 | /** @var Response $response */ 126 | foreach ($gen as $response) { 127 | if ($response->signature == Signature::RECORD) { 128 | $this->assertEquals($str, $response->content[0]); 129 | } 130 | } 131 | } 132 | 133 | public function stringProvider(): \Generator 134 | { 135 | foreach ([0, 10, 200, 60000, 200000] as $length) 136 | yield 'string length: ' . $length => [$this->randomString($length)]; 137 | } 138 | 139 | private function randomString(int $length): string 140 | { 141 | $str = ''; 142 | while (strlen($str) < $length) 143 | $str .= chr(mt_rand(32, 126)); 144 | return $str; 145 | } 146 | 147 | /** 148 | * @depends testInit 149 | * @dataProvider listProvider 150 | */ 151 | public function testList(int $size, AProtocol $protocol): void 152 | { 153 | $gen = $protocol 154 | ->run('RETURN range(0, ' . $size . ') AS a', [], ['mode' => 'r']) 155 | ->pull() 156 | ->getResponses(); 157 | 158 | /** @var Response $response */ 159 | foreach ($gen as $response) { 160 | if ($response->signature == Signature::RECORD) { 161 | $this->assertEquals(range(0, $size), $response->content[0]); 162 | } 163 | } 164 | } 165 | 166 | public function listProvider(): \Generator 167 | { 168 | foreach ([0, 10, 200, 60000, 200000] as $size) 169 | yield 'list size: ' . $size => [$size]; 170 | } 171 | 172 | /** 173 | * @depends testInit 174 | * @dataProvider dictionaryProvider 175 | */ 176 | public function testDictionary(string $query, int $size, AProtocol $protocol): void 177 | { 178 | $gen = $protocol 179 | ->run($query, [], ['mode' => 'r']) 180 | ->pull() 181 | ->getResponses(); 182 | 183 | /** @var Response $response */ 184 | foreach ($gen as $response) { 185 | if ($response->signature == Signature::RECORD) { 186 | $this->assertCount($size, $response->content[0]); 187 | } elseif ($response->signature == Signature::FAILURE) { 188 | $this->markTestIncomplete(print_r($response->content, true)); 189 | } 190 | } 191 | } 192 | 193 | public function dictionaryProvider(): \Generator 194 | { 195 | foreach ([0, 10, 200, 20000, 70000] as $size) { 196 | yield 'dictionary size: ' . $size => ['RETURN apoc.map.fromLists(toStringList(range(1, ' . $size . ')), range(1, ' . $size . '))', $size]; 197 | } 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /tests/packstream/v1/generators/DictionaryGenerator.php: -------------------------------------------------------------------------------- 1 | position = 0; 24 | } 25 | 26 | public function current(): mixed 27 | { 28 | return array_values($this->array)[$this->position]; 29 | } 30 | 31 | public function key(): string|int 32 | { 33 | return array_keys($this->array)[$this->position]; 34 | } 35 | 36 | public function next(): void 37 | { 38 | ++$this->position; 39 | } 40 | 41 | public function valid(): bool 42 | { 43 | return array_key_exists($this->position, array_values($this->array)); 44 | } 45 | 46 | public function count(): int 47 | { 48 | return count($this->array); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/packstream/v1/generators/ListGenerator.php: -------------------------------------------------------------------------------- 1 | position = 0; 24 | } 25 | 26 | public function current(): mixed 27 | { 28 | return $this->array[$this->position]; 29 | } 30 | 31 | public function key(): int 32 | { 33 | return $this->position; 34 | } 35 | 36 | public function next(): void 37 | { 38 | ++$this->position; 39 | } 40 | 41 | public function valid(): bool 42 | { 43 | return array_key_exists($this->position, $this->array); 44 | } 45 | 46 | public function count(): int 47 | { 48 | return count($this->array); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/packstream/v1/generators/RandomDataGenerator.php: -------------------------------------------------------------------------------- 1 | rows = $rows; 21 | } 22 | 23 | public function current(): array 24 | { 25 | return [bin2hex(random_bytes(0x20)) => bin2hex(random_bytes(0x800))]; 26 | } 27 | 28 | public function next(): void 29 | { 30 | ++$this->count; 31 | } 32 | 33 | public function key(): int 34 | { 35 | return $this->count; 36 | } 37 | 38 | public function valid(): bool 39 | { 40 | return $this->count < $this->rows; 41 | } 42 | 43 | public function rewind(): void 44 | { 45 | $this->count = 0; 46 | } 47 | 48 | public function count(): int 49 | { 50 | return $this->rows; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/protocol/ProtocolLayer.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(AConnection::class) 45 | ->disableOriginalConstructor(); 46 | call_user_func([$mockBuilder, method_exists($mockBuilder, 'onlyMethods') ? 'onlyMethods' : 'setMethods'], ['__construct', 'write', 'read', 'connect', 'disconnect']); 47 | $connection = $mockBuilder->getMock(); 48 | 49 | $connection 50 | ->method('write') 51 | ->with( 52 | $this->callback(function ($buffer) { 53 | if (bin2hex($buffer) == '0000') 54 | return true; 55 | 56 | //skip write buffer check 57 | if (empty(self::$writeBuffer)) 58 | return true; 59 | 60 | $i = self::$writeIndex; 61 | self::$writeIndex++; 62 | if (self::$writeIndex >= count(self::$writeBuffer)) 63 | self::$writeIndex = 0; 64 | 65 | //verify expected buffer 66 | return hex2bin(str_replace(' ', '', self::$writeBuffer[$i] ?? '')) === $buffer; 67 | }) 68 | ); 69 | 70 | $connection 71 | ->method('read') 72 | ->will($this->returnCallback([$this, 'readCallback'])); 73 | 74 | /** @var AConnection $connection */ 75 | return $connection; 76 | } 77 | 78 | /** 79 | * Mocked Socket read method 80 | */ 81 | public function readCallback(int $length = 2048): string 82 | { 83 | if (empty(self::$readBuffer)) { 84 | $params = array_shift(self::$readArray); 85 | $gen = self::$packer->pack(...$params); 86 | foreach ($gen as $s) { 87 | self::$readBuffer .= mb_strcut($s, 2, null, '8bit'); 88 | } 89 | 90 | self::$readBuffer = pack('n', mb_strlen(self::$readBuffer, '8bit')) . self::$readBuffer . chr(0x00) . chr(0x00); 91 | } 92 | 93 | $output = mb_strcut(self::$readBuffer, 0, $length, '8bit'); 94 | self::$readBuffer = mb_strcut(self::$readBuffer, mb_strlen($output, '8bit'), null, '8bit'); 95 | return $output; 96 | } 97 | 98 | /** 99 | * Reset mockup AConnetion variables 100 | */ 101 | protected function setUp(): void 102 | { 103 | if (!getenv('BOLT_ANALYTICS_OPTOUT') && is_writable(sys_get_temp_dir() . DIRECTORY_SEPARATOR)) { 104 | if (!file_exists(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-bolt-analytics' . DIRECTORY_SEPARATOR)) { 105 | mkdir(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'php-bolt-analytics', recursive: true); 106 | } 107 | } 108 | 109 | self::$readBuffer = ''; 110 | self::$readArray = []; 111 | self::$writeIndex = 0; 112 | self::$writeBuffer = []; 113 | 114 | self::$packer = new Packer(); 115 | } 116 | 117 | protected function checkFailure(Response $response): void 118 | { 119 | $this->assertEquals(Signature::FAILURE, $response->signature); 120 | $this->assertEquals('some error message', $response->content['message']); 121 | $this->assertEquals('Neo.ClientError.Statement.SyntaxError', $response->content['code']); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/protocol/V2Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 20 | $this->assertInstanceOf(V2::class, $cls); 21 | return $cls; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/protocol/V4Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 20 | $this->assertInstanceOf(V4::class, $cls); 21 | return $cls; 22 | } 23 | 24 | /** 25 | * @depends test__construct 26 | */ 27 | public function testPull(V4 $cls): void 28 | { 29 | self::$readArray = [ 30 | [0x71, (object)[]], 31 | [0x70, (object)[]], 32 | [0x7F, (object)['message' => 'some error message', 'code' => 'Neo.ClientError.Statement.SyntaxError']], 33 | [0x7E, (object)[]] 34 | ]; 35 | self::$writeBuffer = [ 36 | '00 01 b1', 37 | '00 01 3f', 38 | '00 01 a2', 39 | '00 02 81 6e', 40 | '00 01 ff', 41 | '00 04 83 71 69 64', 42 | '00 01 ff', 43 | ]; 44 | 45 | $cls->serverState = ServerState::STREAMING; 46 | $res = iterator_to_array($cls->pull(['n' => -1, 'qid' => -1])->getResponses(), false); 47 | $this->assertIsArray($res); 48 | $this->assertCount(2, $res); 49 | $this->assertEquals(ServerState::READY, $cls->serverState); 50 | 51 | $cls->serverState = ServerState::STREAMING; 52 | $responses = iterator_to_array($cls->pull(['n' => -1, 'qid' => -1])->getResponses(), false); 53 | $this->checkFailure($responses[0]); 54 | $this->assertEquals(ServerState::FAILED, $cls->serverState); 55 | 56 | $cls->serverState = ServerState::INTERRUPTED; 57 | $responses = iterator_to_array($cls->pull(['n' => -1, 'qid' => -1])->getResponses(), false); 58 | $this->assertEquals(Signature::IGNORED, $responses[0]->signature); 59 | $this->assertEquals(ServerState::INTERRUPTED, $cls->serverState); 60 | } 61 | 62 | /** 63 | * @depends test__construct 64 | */ 65 | public function testDiscard(V4 $cls): void 66 | { 67 | self::$readArray = [ 68 | [0x70, (object)[]], 69 | [0x7F, (object)['message' => 'some error message', 'code' => 'Neo.ClientError.Statement.SyntaxError']], 70 | [0x7E, (object)[]] 71 | ]; 72 | self::$writeBuffer = [ 73 | '0001b1', 74 | '00012f', 75 | '0001a2', 76 | '0002816e', 77 | '0001ff', 78 | '000483716964', 79 | '0001ff', 80 | ]; 81 | 82 | $cls->serverState = ServerState::STREAMING; 83 | $this->assertEquals(Signature::SUCCESS, $cls->discard(['n' => -1, 'qid' => -1])->getResponse()->signature); 84 | $this->assertEquals(ServerState::READY, $cls->serverState); 85 | 86 | $cls->serverState = ServerState::STREAMING; 87 | $response = $cls->discard(['n' => -1, 'qid' => -1])->getResponse(); 88 | $this->checkFailure($response); 89 | $this->assertEquals(ServerState::FAILED, $cls->serverState); 90 | 91 | $cls->serverState = ServerState::INTERRUPTED; 92 | $response = $cls->discard(['n' => -1, 'qid' => -1])->getResponse(); 93 | $this->assertEquals(Signature::IGNORED, $response->signature); 94 | $this->assertEquals(ServerState::INTERRUPTED, $cls->serverState); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/protocol/V4_1Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 20 | $this->assertInstanceOf(V4_1::class, $cls); 21 | return $cls; 22 | } 23 | 24 | /** 25 | * @depends test__construct 26 | */ 27 | public function testHello(V4_1 $cls): void 28 | { 29 | self::$readArray = [ 30 | [0x70, (object)[]], 31 | [0x7F, (object)['message' => 'some error message', 'code' => 'Neo.ClientError.Statement.SyntaxError']] 32 | ]; 33 | self::$writeBuffer = [ 34 | '0001b1', 35 | '000101', 36 | '0001a4', 37 | '000b8a757365725f6167656e74', 38 | '000988626f6c742d706870', 39 | '000786736368656d65', 40 | '0006856261736963', 41 | '000a897072696e636970616c', 42 | '00058475736572', 43 | '000c8b63726564656e7469616c73', 44 | '00098870617373776f7264', 45 | ]; 46 | 47 | $cls->serverState = ServerState::CONNECTED; 48 | $this->assertEquals(Signature::SUCCESS, $cls->hello([ 49 | 'user_agent' => 'bolt-php', 50 | 'scheme' => 'basic', 51 | 'principal' => 'user', 52 | 'credentials' => 'password', 53 | ])->getResponse()->signature); 54 | $this->assertEquals(ServerState::READY, $cls->serverState); 55 | 56 | $cls->serverState = ServerState::CONNECTED; 57 | $response = $cls->hello([ 58 | 'user_agent' => 'bolt-php', 59 | 'scheme' => 'basic', 60 | 'principal' => 'user', 61 | 'credentials' => 'password', 62 | ])->getResponse(); 63 | $this->checkFailure($response); 64 | $this->assertEquals(ServerState::DEFUNCT, $cls->serverState); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/protocol/V4_2Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 19 | $this->assertInstanceOf(V4_2::class, $cls); 20 | return $cls; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /tests/protocol/V4_3Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 20 | $this->assertInstanceOf(V4_3::class, $cls); 21 | return $cls; 22 | } 23 | 24 | /** 25 | * @depends test__construct 26 | */ 27 | public function testRoute(V4_3 $cls): void 28 | { 29 | self::$readArray = [ 30 | [0x70, (object)[]], 31 | [0x7F, (object)['message' => 'some error message', 'code' => 'Neo.ClientError.Statement.SyntaxError']], 32 | [0x7E, (object)[]] 33 | ]; 34 | self::$writeBuffer = [ 35 | '0001b3', 36 | '000166', 37 | '0001a1', 38 | '00088761646472657373', 39 | '000f8e6c6f63616c686f73743a37363837', 40 | '000190', 41 | '0001c0', 42 | ]; 43 | 44 | $cls->serverState = ServerState::READY; 45 | $this->assertEquals(Signature::SUCCESS, $cls->route(['address' => 'localhost:7687'])->getResponse()->signature); 46 | $this->assertEquals(ServerState::READY, $cls->serverState); 47 | 48 | $cls->serverState = ServerState::READY; 49 | $response = $cls->route(['address' => 'localhost:7687'])->getResponse(); 50 | $this->checkFailure($response); 51 | $this->assertEquals(ServerState::FAILED, $cls->serverState); 52 | 53 | $cls->serverState = ServerState::INTERRUPTED; 54 | $response = $cls->route(['address' => 'localhost:7687'])->getResponse(); 55 | $this->assertEquals(Signature::IGNORED, $response->signature); 56 | $this->assertEquals(ServerState::INTERRUPTED, $cls->serverState); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /tests/protocol/V4_4Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 20 | $this->assertInstanceOf(V4_4::class, $cls); 21 | return $cls; 22 | } 23 | 24 | /** 25 | * @depends test__construct 26 | */ 27 | public function testRoute(V4_4 $cls): void 28 | { 29 | self::$readArray = [ 30 | [0x70, (object)[]], 31 | [0x7F, (object)['message' => 'some error message', 'code' => 'Neo.ClientError.Statement.SyntaxError']], 32 | [0x7E, (object)[]] 33 | ]; 34 | self::$writeBuffer = [ 35 | '0001b3', 36 | '000166', 37 | '0001a1', 38 | '00088761646472657373', 39 | '000f8e6c6f63616c686f73743a37363837', 40 | '000190', 41 | '0001a1', 42 | '0003826462', 43 | '0001c0', 44 | ]; 45 | 46 | $cls->serverState = ServerState::READY; 47 | $this->assertEquals(Signature::SUCCESS, $cls->route(['address' => 'localhost:7687'], [], ['db' => null])->getResponse()->signature); 48 | $this->assertEquals(ServerState::READY, $cls->serverState); 49 | 50 | $cls->serverState = ServerState::READY; 51 | $response = $cls->route(['address' => 'localhost:7687'], [], ['db' => null])->getResponse(); 52 | $this->checkFailure($response); 53 | $this->assertEquals(ServerState::FAILED, $cls->serverState); 54 | 55 | $cls->serverState = ServerState::INTERRUPTED; 56 | $response = $cls->route(['address' => 'localhost:7687'], [], ['db' => null])->getResponse(); 57 | $this->assertEquals(Signature::IGNORED, $response->signature); 58 | $this->assertEquals(ServerState::INTERRUPTED, $cls->serverState); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /tests/protocol/V5Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 19 | $this->assertInstanceOf(V5::class, $cls); 20 | return $cls; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/protocol/V5_1Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 20 | $this->assertInstanceOf(V5_1::class, $cls); 21 | return $cls; 22 | } 23 | 24 | /** 25 | * @depends test__construct 26 | */ 27 | public function testHello(V5_1 $cls): void 28 | { 29 | self::$readArray = [ 30 | [0x70, (object)[]], 31 | [0x7F, (object)['message' => 'some error message', 'code' => 'Neo.ClientError.Statement.SyntaxError']] 32 | ]; 33 | self::$writeBuffer = [ 34 | '0001b1', 35 | '000101', 36 | '0001a1', 37 | '000b8a757365725f6167656e74', 38 | '000988626f6c742d706870', 39 | ]; 40 | 41 | $cls->serverState = ServerState::NEGOTIATION; 42 | $this->assertEquals(Signature::SUCCESS, $cls->hello()->getResponse()->signature); 43 | $this->assertEquals(ServerState::AUTHENTICATION, $cls->serverState); 44 | 45 | $cls->serverState = ServerState::NEGOTIATION; 46 | $response = $cls->hello()->getResponse(); 47 | $this->checkFailure($response); 48 | $this->assertEquals(ServerState::DEFUNCT, $cls->serverState); 49 | } 50 | 51 | /** 52 | * @depends test__construct 53 | */ 54 | public function testLogon(V5_1 $cls): void 55 | { 56 | self::$readArray = [ 57 | [0x70, (object)[]], 58 | [0x7F, (object)['message' => 'some error message', 'code' => 'Neo.ClientError.Statement.SyntaxError']] 59 | ]; 60 | self::$writeBuffer = [ 61 | '0001b1', 62 | '00016a', 63 | '0001a3', 64 | '000786736368656d65', 65 | '0006856261736963', 66 | '000a897072696e636970616c', 67 | '00058475736572', 68 | '000c8b63726564656e7469616c73', 69 | '00098870617373776f7264', 70 | ]; 71 | 72 | $cls->serverState = ServerState::AUTHENTICATION; 73 | $this->assertEquals(Signature::SUCCESS, $cls->logon([ 74 | 'scheme' => 'basic', 75 | 'principal' => 'user', 76 | 'credentials' => 'password' 77 | ])->getResponse()->signature); 78 | $this->assertEquals(ServerState::READY, $cls->serverState); 79 | 80 | $cls->serverState = ServerState::AUTHENTICATION; 81 | $response = $cls->logon([ 82 | 'scheme' => 'basic', 83 | 'principal' => 'user', 84 | 'credentials' => 'password' 85 | ])->getResponse(); 86 | $this->checkFailure($response); 87 | $this->assertEquals(ServerState::DEFUNCT, $cls->serverState); 88 | } 89 | 90 | /** 91 | * @depends test__construct 92 | */ 93 | public function testLogoff(V5_1 $cls): void 94 | { 95 | self::$readArray = [ 96 | [0x70, (object)[]], 97 | [0x7F, (object)['message' => 'some error message', 'code' => 'Neo.ClientError.Statement.SyntaxError']] 98 | ]; 99 | self::$writeBuffer = [ 100 | '0001b0', 101 | '00016b', 102 | ]; 103 | 104 | $cls->serverState = ServerState::READY; 105 | $this->assertEquals(Signature::SUCCESS, $cls->logoff()->getResponse()->signature); 106 | $this->assertEquals(ServerState::AUTHENTICATION, $cls->serverState); 107 | 108 | $cls->serverState = ServerState::READY; 109 | $response = $cls->logoff()->getResponse(); 110 | $this->checkFailure($response); 111 | $this->assertEquals(ServerState::FAILED, $cls->serverState); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/protocol/V5_2Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 17 | $this->assertInstanceOf(V5_2::class, $cls); 18 | return $cls; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/protocol/V5_3Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 20 | $this->assertInstanceOf(V5_3::class, $cls); 21 | return $cls; 22 | } 23 | 24 | /** 25 | * @depends test__construct 26 | */ 27 | public function testHello(V5_3 $cls): void 28 | { 29 | self::$readArray = [ 30 | [0x70, (object)[]], 31 | [0x7F, (object)['message' => 'some error message', 'code' => 'Neo.ClientError.Statement.SyntaxError']] 32 | ]; 33 | 34 | $cls->serverState = ServerState::NEGOTIATION; 35 | $this->assertEquals(Signature::SUCCESS, $cls->hello()->getResponse()->signature); 36 | $this->assertEquals(ServerState::AUTHENTICATION, $cls->serverState); 37 | 38 | $cls->serverState = ServerState::NEGOTIATION; 39 | $response = $cls->hello()->getResponse(); 40 | $this->checkFailure($response); 41 | $this->assertEquals(ServerState::DEFUNCT, $cls->serverState); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/protocol/V5_4Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 17 | $this->assertInstanceOf(V5_4::class, $cls); 18 | return $cls; 19 | } 20 | 21 | /** 22 | * @depends test__construct 23 | */ 24 | public function testTelemetry(V5_4 $cls): void 25 | { 26 | // todo 27 | $this->markTestSkipped(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/protocol/V5_6Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 17 | $this->assertInstanceOf(V5_6::class, $cls); 18 | return $cls; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/protocol/V5_7Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 17 | $this->assertInstanceOf(V5_7::class, $cls); 18 | return $cls; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/protocol/V5_8Test.php: -------------------------------------------------------------------------------- 1 | mockConnection()); 17 | $this->assertInstanceOf(V5_8::class, $cls); 18 | return $cls; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/structures/StructureLayer.php: -------------------------------------------------------------------------------- 1 | randomTimestamp(); 25 | yield 'ts: ' . $ts => [$ts]; 26 | } 27 | } 28 | 29 | public function providerTimestampTimezone(): \Generator 30 | { 31 | for ($i = 0; $i < self::$iterations; $i++) { 32 | $tz = \DateTimeZone::listIdentifiers()[array_rand(\DateTimeZone::listIdentifiers())]; 33 | $ts = $this->randomTimestamp($tz); 34 | yield 'ts: ' . $ts . ' tz: ' . $tz => [$ts, $tz]; 35 | } 36 | } 37 | 38 | private function randomTimestamp(string $timezone = '+0000'): int 39 | { 40 | try { 41 | $zone = new \DateTimeZone($timezone); 42 | $start = new \DateTime(date('Y-m-d H:i:s', strtotime('-10 years', 0)), $zone); 43 | $end = new \DateTime(date('Y-m-d H:i:s', strtotime('+10 years', 0)), $zone); 44 | return rand($start->getTimestamp(), $end->getTimestamp()); 45 | } catch (Exception) { 46 | return strtotime('now ' . $timezone); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/structures/v1/DateTimeTrait.php: -------------------------------------------------------------------------------- 1 | format('Y-m-d\TH:i:s.uP'); 23 | 24 | //unpack 25 | $res = iterator_to_array( 26 | $protocol->run('RETURN datetime($date)', [ 27 | 'date' => $datetime 28 | ], ['mode' => 'r']) 29 | ->pull() 30 | ->getResponses(), 31 | false 32 | ); 33 | $dateTimeStructure = $res[1]->content[0]; 34 | 35 | $this->assertInstanceOf($this->expectedDateTimeClass, $dateTimeStructure); 36 | $this->assertEquals($datetime, (string)$dateTimeStructure, 'unpack ' . $datetime . ' != ' . $dateTimeStructure); 37 | 38 | //pack 39 | $res = iterator_to_array( 40 | $protocol 41 | ->run('RETURN toString($date)', [ 42 | 'date' => $dateTimeStructure 43 | ], ['mode' => 'r']) 44 | ->pull() 45 | ->getResponses(), 46 | false 47 | ); 48 | 49 | // neo4j returns fraction of seconds not padded with zeros ... zero timezone offset returns as Z 50 | $datetime = preg_replace(["/\.?0+(.\d{2}:\d{2})$/", "/\+00:00$/"], ['$1', 'Z'], $datetime); 51 | $this->assertEquals($datetime, $res[1]->content[0], 'pack ' . $datetime . ' != ' . $res[1]->content[0]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/structures/v1/DateTimeZoneIdTrait.php: -------------------------------------------------------------------------------- 1 | format('Y-m-d\TH:i:s.u') . '[' . $timezone . ']'; 27 | 28 | //unpack 29 | $res = iterator_to_array( 30 | $protocol 31 | ->run('RETURN datetime($dt)', [ 32 | 'dt' => $datetime 33 | ], ['mode' => 'r']) 34 | ->pull() 35 | ->getResponses(), 36 | false 37 | ); 38 | 39 | /** @var Response $response */ 40 | foreach ($res as $response) { 41 | if ($response->signature == Signature::FAILURE) { 42 | throw new Exception($response->content['message']); 43 | } 44 | } 45 | 46 | $dateTimeZoneIdStructure = $res[1]->content[0]; 47 | 48 | $this->assertInstanceOf($this->expectedDateTimeZoneIdClass, $dateTimeZoneIdStructure); 49 | $this->assertEquals($datetime, (string)$dateTimeZoneIdStructure, 'unpack ' . $datetime . ' != ' . $dateTimeZoneIdStructure); 50 | 51 | //pack 52 | $res = iterator_to_array( 53 | $protocol 54 | ->run('RETURN toString($dt)', [ 55 | 'dt' => $dateTimeZoneIdStructure 56 | ], ['mode' => 'r']) 57 | ->pull() 58 | ->getResponses(), 59 | false 60 | ); 61 | 62 | // neo4j returns fraction of seconds not padded with zeros ... also contains timezone offset before timezone id 63 | $datetime = preg_replace("/\.?0+\[/", '[', $datetime); 64 | $dateTimeZoneIdStructure = preg_replace("/([+\-]\d{2}:\d{2}|Z)\[/", '[', $res[1]->content[0]); 65 | $this->assertEquals($datetime, $dateTimeZoneIdStructure, 'pack ' . $datetime . ' != ' . $dateTimeZoneIdStructure); 66 | } catch (Exception $e) { 67 | if (str_starts_with($e->getMessage(), 'Invalid value for TimeZone: Text \'' . $timezone . '\'')) { 68 | $protocol->reset()->getResponse(); 69 | $this->markTestSkipped('Test skipped because database is missing timezone ID ' . $timezone); 70 | } else { 71 | $this->markTestIncomplete($e->getMessage()); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/structures/v4_3/StructuresTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); 35 | 36 | $bolt = new Bolt($conn); 37 | $this->assertInstanceOf(Bolt::class, $bolt); 38 | 39 | $bolt->setProtocolVersions(4.4, 4.3); 40 | /** @var AProtocol|V4_4|V4_3 $protocol */ 41 | $protocol = $bolt->build(); 42 | $this->assertInstanceOf(AProtocol::class, $protocol); 43 | 44 | /** @var Response $helloResponse */ 45 | $helloResponse = $protocol->hello([ 46 | 'user_agent' => 'bolt-php', 47 | 'scheme' => 'basic', 48 | 'principal' => $GLOBALS['NEO_USER'], 49 | 'credentials' => $GLOBALS['NEO_PASS'], 50 | 'patch_bolt' => ['utc'] 51 | ])->getResponse(); 52 | $this->assertEquals(Signature::SUCCESS, $helloResponse->signature); 53 | 54 | if (version_compare($protocol->getVersion(), '5', '>=') || version_compare($protocol->getVersion(), '4.3', '<')) { 55 | $this->markTestSkipped('You are not running Neo4j version with patch_bolt support.'); 56 | } 57 | 58 | if (($helloResponse->content['patch_bolt'] ?? null) !== ['utc']) { 59 | $this->markTestSkipped('Currently used Neo4j version does not support patch_bolt.'); 60 | } 61 | 62 | return $protocol; 63 | } 64 | 65 | private string $expectedDateTimeClass = DateTime::class; 66 | use DateTimeTrait; 67 | 68 | private string $expectedDateTimeZoneIdClass = DateTimeZoneId::class; 69 | use DateTimeZoneIdTrait; 70 | } 71 | -------------------------------------------------------------------------------- /tests/structures/v5/StructuresTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); 31 | 32 | $bolt = new Bolt($conn); 33 | $this->assertInstanceOf(Bolt::class, $bolt); 34 | 35 | $protocol = $bolt->build(); 36 | $this->assertInstanceOf(AProtocol::class, $protocol); 37 | 38 | if (version_compare($protocol->getVersion(), '5', '<')) { 39 | $this->markTestSkipped('Tests available only for version 5 and higher.'); 40 | } 41 | 42 | $this->sayHello($protocol, $GLOBALS['NEO_USER'], $GLOBALS['NEO_PASS']); 43 | 44 | return $protocol; 45 | } 46 | 47 | private string $expectedDateTimeClass = DateTime::class; 48 | use DateTimeTrait; 49 | 50 | private string $expectedDateTimeZoneIdClass = DateTimeZoneId::class; 51 | use DateTimeZoneIdTrait; 52 | 53 | /** 54 | * @depends testInit 55 | */ 56 | public function testNode(AProtocol $protocol) 57 | { 58 | $protocol->begin()->getResponse(); 59 | 60 | //unpack 61 | $res = iterator_to_array( 62 | $protocol 63 | ->run('CREATE (a:Test { param1: 123 }) RETURN a, ID(a), elementId(a)') 64 | ->pull() 65 | ->getResponses(), 66 | false 67 | ); 68 | $this->assertInstanceOf(Node::class, $res[1]->content[0]); 69 | 70 | $this->assertEquals($res[1]->content[1], $res[1]->content[0]->id); 71 | $this->assertEquals($res[1]->content[2], $res[1]->content[0]->element_id); 72 | $this->assertEquals(['Test'], $res[1]->content[0]->labels); 73 | $this->assertEquals(['param1' => 123], $res[1]->content[0]->properties); 74 | 75 | //pack not supported 76 | 77 | $protocol->rollback()->getResponse(); 78 | } 79 | 80 | /** 81 | * @depends testInit 82 | */ 83 | public function testPath(AProtocol $protocol) 84 | { 85 | $protocol->begin()->getResponse(); 86 | 87 | //unpack 88 | $res = iterator_to_array( 89 | $protocol 90 | ->run('CREATE p=(:Test)-[r:HAS { param1: 123 }]->(:Test) RETURN p, ID(r), elementId(r)') 91 | ->pull() 92 | ->getResponses(), 93 | false 94 | ); 95 | $this->assertInstanceOf(Path::class, $res[1]->content[0]); 96 | 97 | foreach ($res[1]->content[0]->rels as $rel) { 98 | $this->assertInstanceOf(UnboundRelationship::class, $rel); 99 | 100 | $this->assertEquals($res[1]->content[1], $rel->id); 101 | $this->assertEquals($res[1]->content[2], $rel->element_id); 102 | $this->assertEquals('HAS', $rel->type); 103 | $this->assertEquals(['param1' => 123], $rel->properties); 104 | } 105 | 106 | //pack not supported 107 | 108 | $protocol->rollback()->getResponse(); 109 | } 110 | 111 | /** 112 | * @depends testInit 113 | */ 114 | public function testRelationship(AProtocol $protocol) 115 | { 116 | $protocol->begin()->getResponse(); 117 | 118 | //unpack 119 | $res = iterator_to_array( 120 | $protocol 121 | ->run('CREATE (a:Test)-[rel:HAS { param1: 123 }]->(b:Test) RETURN rel, ID(rel), elementId(rel), ID(a), ID(b), elementId(a), elementId(b)') 122 | ->pull() 123 | ->getResponses(), 124 | false 125 | ); 126 | $this->assertInstanceOf(Relationship::class, $res[1]->content[0]); 127 | 128 | $this->assertEquals($res[1]->content[1], $res[1]->content[0]->id); 129 | $this->assertEquals($res[1]->content[2], $res[1]->content[0]->element_id); 130 | $this->assertEquals('HAS', $res[1]->content[0]->type); 131 | $this->assertEquals(['param1' => 123], $res[1]->content[0]->properties); 132 | $this->assertEquals($res[1]->content[3], $res[1]->content[0]->startNodeId); 133 | $this->assertEquals($res[1]->content[4], $res[1]->content[0]->endNodeId); 134 | $this->assertEquals($res[1]->content[5], $res[1]->content[0]->start_node_element_id); 135 | $this->assertEquals($res[1]->content[6], $res[1]->content[0]->end_node_element_id); 136 | 137 | //pack not supported 138 | 139 | $protocol->rollback()->getResponse(); 140 | } 141 | } 142 | --------------------------------------------------------------------------------