├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── scripts │ └── setup-symfony-env.bash └── workflows │ ├── static-analysis.yml │ └── tests.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── Changelog.md ├── Dockerfile ├── LICENSE ├── bin └── console.php ├── composer.json ├── config └── services.php ├── console ├── docker-compose.yml ├── docs └── README.md ├── phpunit.xml.dist ├── psalm.xml ├── public └── .gitignore ├── src ├── Builders │ └── ClientBuilder.php ├── Collector │ └── Neo4jDataCollector.php ├── Decorators │ ├── SymfonyClient.php │ ├── SymfonyDriver.php │ ├── SymfonySession.php │ └── SymfonyTransaction.php ├── DependencyInjection │ ├── Configuration.php │ └── Neo4jExtension.php ├── Event │ ├── FailureEvent.php │ ├── PostRunEvent.php │ ├── PreRunEvent.php │ └── Transaction │ │ ├── PostTransactionBeginEvent.php │ │ ├── PostTransactionCommitEvent.php │ │ ├── PostTransactionRollbackEvent.php │ │ ├── PreTransactionBeginEvent.php │ │ ├── PreTransactionCommitEvent.php │ │ └── PreTransactionRollbackEvent.php ├── EventHandler.php ├── EventListener │ └── Neo4jProfileListener.php ├── Factories │ ├── ClientFactory.php │ ├── StopwatchEventNameFactory.php │ └── SymfonyDriverFactory.php ├── Neo4jBundle.php └── Resources │ ├── public │ ├── css │ │ └── neo4j.css │ ├── images │ │ └── neo4j.svg │ └── js │ │ └── neo4j.js │ └── views │ └── web_profiler.html.twig └── tests ├── App ├── Controller │ ├── TestController.php │ └── Twig │ │ ├── base.html.twig │ │ └── index.html.twig ├── TestKernel.php ├── config │ ├── ci │ │ └── default.yml │ ├── default.yml │ └── routes.yaml └── index.php ├── Application.php └── Functional ├── IntegrationTest.php └── ProfilerTest.php /.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 | 1. Create client with neo4j scheme 16 | 2. Open transaction 17 | 3. Commit transaction 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - Library version: [e.g. 2.0.8, use `composer show -i laudis/neo4j-php-client` and `composer show -i neo4j/neo4j-bundle` to find out] 28 | - Neo4j Version: [e.g. 4.2.1, aura, use `neo4j version` to find out] 29 | - PHP version: [e.g. 8.0.2, use `php -v` to find out] 30 | - OS: [e.g. Linux, 5.13.4-1-MANJARO, Windows 10] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.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 or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/scripts/setup-symfony-env.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | if [ -z "$1" ]; then 6 | echo "Please specify the Symfony version to install" 7 | exit 1 8 | fi 9 | 10 | echo "Installing Symfony version $1" 11 | 12 | # This is not required for CI, but it allows to test the script locally 13 | function cleanup { 14 | echo "Cleaning up" 15 | mv composer.origin.json composer.json 16 | } 17 | 18 | function install-specified-symfony-version { 19 | local symfony_version=$1 20 | cp composer.json composer.origin.json 21 | rm composer.lock || true 22 | rm -Rf vendor || true 23 | sed -i 's/\^5.4 || \^6.0 || \^7.0/\^'$symfony_version'/g' composer.json 24 | composer install 25 | } 26 | 27 | trap cleanup EXIT 28 | 29 | install-specified-symfony-version $1 30 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | jobs: 9 | php-cs-fixer: 10 | name: "Lint & Analyse" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: php-actions/composer@v6 15 | with: 16 | progress: yes 17 | php_version: 8.4 18 | version: 2 19 | - name: Lint & Analyse 20 | uses: php-actions/composer@v6 21 | env: 22 | PHP_CS_FIXER_IGNORE_ENV: "1" 23 | with: 24 | php_version: "8.4" 25 | version: 2 26 | command: check-cs 27 | 28 | - name: Run Psalm 29 | uses: php-actions/composer@v6 30 | with: 31 | php_version: "8.4" 32 | version: 2 33 | command: psalm 34 | 35 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | env: 13 | APP_ENV: ci 14 | 15 | strategy: 16 | max-parallel: 10 17 | matrix: 18 | php: [ '8.1', '8.2', '8.4' ] 19 | sf_version: [ '6.4', '7.2', '7.3' ] 20 | exclude: 21 | - php: 8.1 22 | sf_version: 7.2 23 | - php: 8.1 24 | sf_version: 7.3 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Validate composer-5.4.json 29 | run: composer validate --strict 30 | 31 | - name: Setup PHP ${{ matrix.php }} 32 | uses: shivammathur/setup-php@v2 33 | with: 34 | php-version: ${{ matrix.php }} 35 | 36 | - name: Override Symfony version 37 | run: composer run ci-symfony-install-version ${{ matrix.sf_version }} 38 | 39 | - uses: php-actions/phpunit@v3 40 | with: 41 | configuration: phpunit.xml.dist 42 | php_version: ${{ matrix.php }} 43 | memory_limit: 1024M 44 | version: 9 45 | bootstrap: vendor/autoload.php 46 | 47 | services: 48 | neo4j: 49 | image: neo4j:5.22 50 | env: 51 | NEO4J_AUTH: neo4j/testtest 52 | options: >- 53 | --hostname neo4j 54 | --health-cmd "wget -q --method=HEAD http://localhost:7474 || exit 1" 55 | --health-start-period "60s" 56 | --health-interval "30s" 57 | --health-timeout "15s" 58 | --health-retries "5" 59 | ports: 60 | - 7474:7474 61 | - 7687:7687 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /var/cache/* 2 | /vendor 3 | composer.lock 4 | .phpunit.result.cache 5 | .idea 6 | .php-cs-fixer.cache 7 | 8 | composer.origin.json 9 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | use PhpCsFixer\Config; 15 | use PhpCsFixerCustomFixers\Fixer\ConstructorEmptyBracesFixer; 16 | use PhpCsFixerCustomFixers\Fixer\IssetToArrayKeyExistsFixer; 17 | use PhpCsFixerCustomFixers\Fixer\MultilineCommentOpeningClosingAloneFixer; 18 | use PhpCsFixerCustomFixers\Fixer\MultilinePromotedPropertiesFixer; 19 | use PhpCsFixerCustomFixers\Fixer\PhpdocNoSuperfluousParamFixer; 20 | use PhpCsFixerCustomFixers\Fixer\PhpdocParamOrderFixer; 21 | use PhpCsFixerCustomFixers\Fixer\PhpUnitAssertArgumentsOrderFixer; 22 | use PhpCsFixerCustomFixers\Fixer\StringableInterfaceFixer; 23 | 24 | try { 25 | $finder = PhpCsFixer\Finder::create() 26 | ->in(__DIR__.'/src') 27 | ->in(__DIR__.'/config') 28 | ->in(__DIR__.'/tests'); 29 | } catch (Throwable $e) { 30 | echo $e->getMessage()."\n"; 31 | 32 | exit(1); 33 | } 34 | 35 | return (new Config()) 36 | ->setRiskyAllowed(true) 37 | ->setRules([ 38 | '@Symfony' => true, 39 | ]) 40 | ->setFinder($finder) 41 | ; 42 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. 4 | 5 | ## 0.5.0 6 | 7 | - Reworked to newer version of Client and driver 8 | - Changed config api to reflect newer client 9 | - Removed OGM support 10 | - Moved to modern bundle structure standards 11 | 12 | ## 0.4.2 13 | 14 | ### Added 15 | 16 | - Autowire support by register interfaces as aliases for default services. 17 | 18 | ### Fixed 19 | 20 | - Support for Symfony 4.2 21 | - Better query logging on exceptions 22 | 23 | ## 0.4.1 24 | 25 | ### Added 26 | 27 | - Support for DSN 28 | - Support for resettable data collectors 29 | 30 | ## 0.4.0 31 | 32 | ### Added 33 | 34 | - Support for Symfony 4 35 | 36 | ### Fixed 37 | 38 | - Updating the twig path for symfony flex 39 | - Register an autoloader for proxies to avoid issues when unserializing 40 | 41 | ## 0.3.0 42 | 43 | ### Added 44 | 45 | - Show exceptions in profiler 46 | 47 | ### Fixed 48 | 49 | - Typo in configuration "schema" => "scheme". 50 | - Bug where clients accidentally could share connections. 51 | 52 | ## 0.2.0 53 | 54 | ### Added 55 | 56 | * Support for BOLT 57 | * Test the bundle without OGM 58 | 59 | ### Changed 60 | 61 | * Made the graphaware/neo4j-php-ogm optional 62 | 63 | ### Fixed 64 | 65 | * Invalid alias whennot using the entity manager 66 | * Make sure query logger has default values when exception occour. 67 | 68 | ## 0.1.0 69 | 70 | First release 71 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.4-cli 2 | RUN apt-get update \ 3 | && apt-get install -y \ 4 | libzip-dev \ 5 | unzip \ 6 | git \ 7 | wget \ 8 | && docker-php-ext-install -j$(nproc) bcmath sockets \ 9 | && pecl install xdebug \ 10 | && docker-php-ext-enable xdebug \ 11 | && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 12 | 13 | RUN echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini 14 | RUN echo "xdebug.mode=debug,develop" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini 15 | 16 | WORKDIR /opt/project 17 | 18 | CMD ["php", "-S", "0.0.0.0:80", "-t", "/opt/project/tests/App"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /bin/console.php: -------------------------------------------------------------------------------- 1 | run(); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neo4j/neo4j-bundle", 3 | "description": "Symfony integration for Neo4j", 4 | "type": "symfony-bundle", 5 | "keywords": ["neo4j", "symfony", "bundle", "graph", "database", "cypher"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Ghlen Nagels", 10 | "email": "ghlen@nagels.tech" 11 | }, 12 | { 13 | "name": "Nabeel Parkar", 14 | "email": "nabeel@nagels.tech" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=8.1", 19 | "laudis/neo4j-php-client": "^3.3", 20 | "twig/twig": "^3.0", 21 | "ext-json": "*", 22 | "symfony/dependency-injection": "^6.4 || ^7.2", 23 | "symfony/config": "^6.4 || ^7.2" 24 | }, 25 | "require-dev": { 26 | "friendsofphp/php-cs-fixer": "^3.75", 27 | "kubawerlos/php-cs-fixer-custom-fixers": "^3.0", 28 | "matthiasnoback/symfony-dependency-injection-test": "^4.3 || ^5.0", 29 | "phpunit/phpunit": "^9.5", 30 | "psalm/plugin-phpunit": "^0.19.5", 31 | "psalm/plugin-symfony": "^5.0", 32 | "symfony/console": "^6.4 || ^7.2", 33 | "symfony/framework-bundle": "^6.4 || ^7.2", 34 | "symfony/http-kernel": "^6.4 || ^7.2", 35 | "symfony/routing": "^6.4 || ^7.2", 36 | "symfony/stopwatch": "^6.4 || ^7.2", 37 | "symfony/test-pack": "^1.1", 38 | "symfony/twig-bundle": "^6.4 || ^7.2", 39 | "symfony/uid": "^6.4 || ^7.2", 40 | "symfony/web-profiler-bundle": "^6.4 || ^7.2", 41 | "symfony/yaml": "^6.4 || ^7.2", 42 | "vimeo/psalm": "^6.11" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Neo4j\\Neo4jBundle\\": "src/" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Neo4j\\Neo4jBundle\\Tests\\": "tests/" 52 | } 53 | }, 54 | "config": { 55 | "sort-packages": true, 56 | "allow-plugins": { 57 | "php-http/discovery": false 58 | } 59 | }, 60 | "scripts": { 61 | "psalm": "APP_ENV=dev php bin/console.php cache:warmup && vendor/bin/psalm --show-info=true", 62 | "fix-cs": "vendor/bin/php-cs-fixer fix", 63 | "check-cs": "vendor/bin/php-cs-fixer fix --dry-run", 64 | "ci-symfony-install-version": "./.github/scripts/setup-symfony-env.bash" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | services(); 27 | 28 | $services->set('neo4j.client_factory', ClientFactory::class); 29 | 30 | $services->set(DriverConfiguration::class, DriverConfiguration::class) 31 | ->factory([DriverConfiguration::class, 'default']); 32 | $services->set(SessionConfiguration::class, SessionConfiguration::class); 33 | $services->set(TransactionConfiguration::class, TransactionConfiguration::class); 34 | $services->set(ClientBuilder::class, ClientBuilder::class) 35 | ->autowire(); 36 | 37 | $services->set('neo4j.client', SymfonyClient::class) 38 | ->factory([service('neo4j.client_factory'), 'create']) 39 | ->public(); 40 | 41 | $services->set('neo4j.driver', Driver::class) 42 | ->factory([service('neo4j.client'), 'getDriver']) 43 | ->public(); 44 | 45 | $services->set('neo4j.session', Session::class) 46 | ->factory([service('neo4j.driver'), 'createSession']) 47 | ->share(false) 48 | ->public(); 49 | 50 | $services->set('neo4j.transaction', TransactionInterface::class) 51 | ->factory([service('neo4j.session'), 'beginTransaction']) 52 | ->share(false) 53 | ->public(); 54 | 55 | $services->set(SymfonyDriverFactory::class, SymfonyDriverFactory::class) 56 | ->arg('$handler', service(EventHandler::class)) 57 | ->arg('$uuidFactory', service('uuid.factory')->nullOnInvalid()); 58 | 59 | $services->set(StopwatchEventNameFactory::class, StopwatchEventNameFactory::class); 60 | $services->set(EventHandler::class, EventHandler::class) 61 | ->arg('$dispatcher', service('event_dispatcher')->nullOnInvalid()) 62 | ->arg('$stopwatch', service('debug.stopwatch')->nullOnInvalid()) 63 | ->arg('$nameFactory', service(StopwatchEventNameFactory::class)); 64 | 65 | $services->set(StopwatchEventNameFactory::class); 66 | 67 | $services->set(DriverSetupManager::class, DriverSetupManager::class) 68 | ->arg('$formatter', service(SummarizedResultFormatter::class)) 69 | ->arg('$configuration', service(DriverConfiguration::class)); 70 | $services->set(SummarizedResultFormatter::class, SummarizedResultFormatter::class) 71 | ->factory([SummarizedResultFormatter::class, 'create']); 72 | 73 | $services->alias(ClientInterface::class, 'neo4j.client'); 74 | $services->alias(DriverInterface::class, 'neo4j.driver'); 75 | $services->alias(SessionInterface::class, 'neo4j.session'); 76 | $services->alias(TransactionInterface::class, 'neo4j.transaction'); 77 | 78 | $services->set('neo4j.subscriber', Neo4jProfileListener::class) 79 | ->tag('kernel.event_subscriber'); 80 | }; 81 | -------------------------------------------------------------------------------- /console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | networks: 2 | neo4j-symfony: 3 | 4 | services: 5 | app: 6 | user: root:${UID-1000}:${GID-1000} 7 | build: 8 | context: . 9 | ports: 10 | - ${DOCKER_HOST_APP_PORT:-8000}:80 11 | volumes: 12 | - ./:/opt/project 13 | environment: 14 | - NEO4J_HOST=neo4j 15 | - NEO4J_DATABASE=neo4j 16 | - NEO4J_PORT=7687 17 | - NEO4J_USER=neo4j 18 | - NEO4J_PASSWORD=testtest 19 | - XDEBUG_CONFIG="client_host=host.docker.internal log=/tmp/xdebug.log" 20 | working_dir: /opt/project 21 | extra_hosts: 22 | - "host.docker.internal:host-gateway" 23 | networks: 24 | - neo4j-symfony 25 | 26 | neo4j: 27 | image: neo4j:5.22 28 | ports: 29 | - ${DOCKER_HOST_NEO4J_HTTP_PORT:-7474}:7474 30 | - ${DOCKER_HOST_NEO4J_BOLT_PORT:-7687}:7687 31 | environment: 32 | - NEO4J_AUTH=neo4j/testtest 33 | 34 | # advertise “neo4j:7687” instead of localhost 35 | - NEO4J_server_default__advertised__address=${DEFAULT_ADDRESS-localhost} 36 | - NEO4J_server_bolt_advertised__address=${DEFAULT_ADDRESS-localhost}:7687 37 | networks: 38 | - neo4j-symfony 39 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Neo4j Symfony Bundle 2 | 3 | [![Latest Version](https://img.shields.io/github/release/neo4j-contrib/neo4j-symfony.svg?style=flat-square)](https://github.com/neo4j-contrib/neo4j-symfony/releases) 4 | [![Static Analysis](https://github.com/neo4j-php/neo4j-symfony/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/neo4j-php/neo4j-symfony/actions/workflows/static-analysis.yml)![License](https://img.shields.io/github/license/neo4j-php/neo4j-symfony) 5 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/neo4j-contrib/neo4j-symfony.svg?style=flat-square)](https://scrutinizer-ci.com/g/neo4j-contrib/neo4j-symfony) 6 | [![Quality Score](https://img.shields.io/scrutinizer/g/neo4j-contrib/neo4j-symfony.svg?style=flat-square)](https://scrutinizer-ci.com/g/neo4j-contrib/neo4j-symfony) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/neo4j/neo4j-bundle.svg?style=flat-square)](https://packagist.org/packages/neo4j/neo4j-bundle) 8 | 9 | 10 | Installation 11 | ============ 12 | 13 | Make sure Composer is installed globally, as explained in the 14 | [installation chapter](https://getcomposer.org/doc/00-intro.md) 15 | of the Composer documentation. 16 | 17 | Applications that don't use Symfony Flex 18 | ---------------------------------------- 19 | 20 | ### Step 1: Download the Bundle 21 | 22 | Open a command console, enter your project directory and execute the 23 | following command to download the latest stable version of this bundle: 24 | 25 | ```console 26 | $ composer require neo4j/neo4j-bundle 27 | ``` 28 | 29 | ### Step 2: Enable the Bundle 30 | 31 | Then, enable the bundle by adding it to the list of registered bundles 32 | in the `config/bundles.php` file of your project: 33 | 34 | ```php 35 | // config/bundles.php 36 | 37 | return [ 38 | // ... 39 | \Neo4j\Neo4jBundle\Neo4jBundle::class => ['all' => true], 40 | ]; 41 | ``` 42 | 43 | ## Documentation 44 | 45 | The bundle is a convenient way of registering services. We register `Drivers` and one 46 | `Clients`. You will always have alias for the default services: 47 | 48 | * neo4j.driver 49 | * neo4j.client 50 | 51 | 52 | ### Minimal configuration 53 | 54 | ```yaml 55 | neo4j: 56 | drivers: 57 | default: ~ 58 | ``` 59 | 60 | With the minimal configuration we have services named: 61 | * neo4j.driver.default 62 | * neo4j.client 63 | 64 | ### Full configuration example 65 | 66 | This example configures the client to contain two instances. 67 | 68 | ```yaml 69 | neo4j: 70 | profiling: true 71 | default_driver: high-availability 72 | drivers: 73 | - alias: high-availability 74 | dsn: 'neo4j://core1.mydomain.com:7687' 75 | authentication: 76 | type: 'oidc' 77 | token: '%neo4j.openconnect-id-token%' 78 | priority: 1 79 | # Overriding the alias makes it so that there is a backup server to use in case 80 | # the routing table cannot be fetched through the driver with a higher priority 81 | # but the same alias. 82 | # Once the table is fetched it will use that information to auto-route as usual. 83 | - alias: high-availability 84 | dsn: 'neo4j://core2.mydomain.com:7687' 85 | priority: 0 86 | authentication: 87 | type: 'oidc' 88 | token: '%neo4j.openconnect-id-token%' 89 | - alias: backup-instance 90 | dsn: 'bolt://localhost:7687' 91 | authentication: 92 | type: basic 93 | username: '%neo4j.backup-user%' 94 | password: '%neo4j.backup-pass%' 95 | ``` 96 | 97 | ## Testing 98 | 99 | ``` bash 100 | $ composer test 101 | ``` 102 | 103 | ## Example application 104 | 105 | See an example application at https://github.com/neo4j-examples/movies-symfony-php-bolt (legacy project) 106 | 107 | ## License 108 | 109 | The MIT License (MIT). Please see [License File](../LICENSE) for more information. 110 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests/Functional 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | var/cache/dev/Neo4j_Neo4jBundle_Tests_App_TestKernelDevDebugContainer.xml 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /src/Builders/ClientBuilder.php: -------------------------------------------------------------------------------- 1 | driverSetups->getLogger()); 37 | 38 | return $this->withParsedUrl($alias, $uri, $authentication, $priority ?? 0); 39 | } 40 | 41 | private function withParsedUrl(string $alias, Uri $uri, AuthenticateInterface $authentication, int $priority): self 42 | { 43 | $scheme = $uri->getScheme(); 44 | 45 | if (!in_array($scheme, self::SUPPORTED_SCHEMES, true)) { 46 | throw UnsupportedScheme::make($scheme, self::SUPPORTED_SCHEMES); 47 | } 48 | 49 | $tbr = clone $this; 50 | $tbr->driverSetups = $this->driverSetups->withSetup(new DriverSetup($uri, $authentication), $alias, $priority); 51 | 52 | return $tbr; 53 | } 54 | 55 | public function withDefaultDriver(string $alias): self 56 | { 57 | $tbr = clone $this; 58 | $tbr->driverSetups = $this->driverSetups->withDefault($alias); 59 | 60 | return $tbr; 61 | } 62 | 63 | public function build(): SymfonyClient 64 | { 65 | return new SymfonyClient( 66 | driverSetups: $this->driverSetups, 67 | defaultSessionConfiguration: $this->defaultSessionConfig, 68 | defaultTransactionConfiguration: $this->defaultTransactionConfig, 69 | factory: $this->driverFactory 70 | ); 71 | } 72 | 73 | public function withDefaultDriverConfiguration(DriverConfiguration $config): self 74 | { 75 | $tbr = clone $this; 76 | 77 | $tbr->driverSetups = $tbr->driverSetups->withDriverConfiguration($config); 78 | 79 | return $tbr; 80 | } 81 | 82 | public function withDefaultSessionConfiguration(SessionConfiguration $config): self 83 | { 84 | $tbr = clone $this; 85 | $tbr->defaultSessionConfig = $config; 86 | 87 | return $tbr; 88 | } 89 | 90 | public function withDefaultTransactionConfiguration(TransactionConfiguration $config): self 91 | { 92 | $tbr = clone $this; 93 | $tbr->defaultTransactionConfig = $config; 94 | 95 | return $tbr; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Collector/Neo4jDataCollector.php: -------------------------------------------------------------------------------- 1 | > | list, 21 | * } $data 22 | */ 23 | final class Neo4jDataCollector extends AbstractDataCollector 24 | { 25 | public function __construct( 26 | private readonly Neo4jProfileListener $subscriber, 27 | ) { 28 | } 29 | 30 | #[\Override] 31 | public function collect(Request $request, Response $response, ?\Throwable $exception = null): void 32 | { 33 | $t = $this; 34 | $profiledSummaries = $this->subscriber->getProfiledSummaries(); 35 | $successfulStatements = []; 36 | foreach ($profiledSummaries as $summary) { 37 | $statement = ['status' => 'success']; 38 | foreach ($summary as $key => $value) { 39 | if (!is_array($value) && !is_object($value)) { 40 | $statement[$key] = $value; 41 | continue; 42 | } 43 | 44 | $statement[$key] = $t->recursiveToArray($value); 45 | } 46 | $successfulStatements[] = $statement; 47 | } 48 | 49 | $failedStatements = array_map( 50 | static fn (array $x) => [ 51 | 'status' => 'failure', 52 | 'time' => $x['time'], 53 | 'timestamp' => $x['timestamp'], 54 | 'result' => [ 55 | 'statement' => $x['statement']?->toArray(), 56 | ], 57 | 'exception' => [ 58 | 'code' => $x['exception']->getErrors()[0]->getCode(), 59 | 'message' => $x['exception']->getErrors()[0]->getMessage(), 60 | 'classification' => $x['exception']->getErrors()[0]->getClassification(), 61 | 'category' => $x['exception']->getErrors()[0]->getCategory(), 62 | 'title' => $x['exception']->getErrors()[0]->getTitle(), 63 | ], 64 | 'alias' => $x['alias'], 65 | ], 66 | $this->subscriber->getProfiledFailures() 67 | ); 68 | 69 | $this->data['successful_statements_count'] = count($successfulStatements); 70 | $this->data['failed_statements_count'] = count($failedStatements); 71 | $mergedArray = array_merge($successfulStatements, $failedStatements); 72 | uasort( 73 | $mergedArray, 74 | static fn (array $a, array $b) => $a['start_time'] <=> ($b['timestamp'] ?? $b['start_time']) 75 | ); 76 | $this->data['statements'] = $mergedArray; 77 | } 78 | 79 | #[\Override] 80 | public function reset(): void 81 | { 82 | parent::reset(); 83 | $this->subscriber->reset(); 84 | } 85 | 86 | #[\Override] 87 | public function getName(): string 88 | { 89 | return 'neo4j'; 90 | } 91 | 92 | /** @api */ 93 | public function getStatements(): array 94 | { 95 | return $this->data['statements']; 96 | } 97 | 98 | public function getSuccessfulStatements(): array 99 | { 100 | return array_filter( 101 | $this->data['statements'], 102 | static fn (array $x) => 'success' === $x['status'] 103 | ); 104 | } 105 | 106 | public function getFailedStatements(): array 107 | { 108 | return array_filter( 109 | $this->data['statements'], 110 | static fn (array $x) => 'failure' === $x['status'] 111 | ); 112 | } 113 | 114 | /** @api */ 115 | public function getFailedStatementsCount(): array 116 | { 117 | return $this->data['failed_statements_count']; 118 | } 119 | 120 | /** @api */ 121 | public function getSuccessfulStatementsCount(): array 122 | { 123 | return $this->data['successful_statements_count']; 124 | } 125 | 126 | public function getQueryCount(): int 127 | { 128 | return count($this->data['statements']); 129 | } 130 | 131 | #[\Override] 132 | public static function getTemplate(): ?string 133 | { 134 | return '@Neo4j/web_profiler.html.twig'; 135 | } 136 | 137 | private function recursiveToArray(array|object $obj): mixed 138 | { 139 | if (is_array($obj)) { 140 | return array_map( 141 | fn (mixed $x): mixed => $this->recursiveToArray($x), 142 | $obj 143 | ); 144 | } 145 | 146 | if (method_exists($obj, 'toArray')) { 147 | return $obj->toArray(); 148 | } 149 | 150 | return $obj; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Decorators/SymfonyClient.php: -------------------------------------------------------------------------------- 1 | > 24 | */ 25 | private array $boundTransactions = []; 26 | 27 | /** 28 | * @var array 29 | */ 30 | private array $boundSessions = []; 31 | 32 | /** 33 | * @psalm-mutation-free 34 | */ 35 | public function __construct( 36 | private readonly DriverSetupManager $driverSetups, 37 | private readonly SessionConfiguration $defaultSessionConfiguration, 38 | private readonly TransactionConfiguration $defaultTransactionConfiguration, 39 | private readonly SymfonyDriverFactory $factory, 40 | ) { 41 | } 42 | 43 | public function getDefaultSessionConfiguration(): SessionConfiguration 44 | { 45 | return $this->defaultSessionConfiguration; 46 | } 47 | 48 | public function getDefaultTransactionConfiguration(): TransactionConfiguration 49 | { 50 | return $this->defaultTransactionConfiguration; 51 | } 52 | 53 | #[\Override] 54 | public function run(string $statement, iterable $parameters = [], ?string $alias = null): SummarizedResult 55 | { 56 | return $this->runStatement(Statement::create($statement, $parameters), $alias); 57 | } 58 | 59 | #[\Override] 60 | public function runStatement(Statement $statement, ?string $alias = null): SummarizedResult 61 | { 62 | return $this->runStatements([$statement], $alias)->first(); 63 | } 64 | 65 | private function getRunner(?string $alias = null): SymfonyTransaction|SymfonySession 66 | { 67 | $alias ??= $this->driverSetups->getDefaultAlias(); 68 | 69 | if ( 70 | array_key_exists($alias, $this->boundTransactions) 71 | && count($this->boundTransactions[$alias]) > 0 72 | ) { 73 | return $this->boundTransactions[$alias][array_key_last($this->boundTransactions[$alias])]; 74 | } 75 | 76 | return $this->getSession($alias); 77 | } 78 | 79 | private function getSession(?string $alias = null): SymfonySession 80 | { 81 | $alias ??= $this->driverSetups->getDefaultAlias(); 82 | 83 | if (array_key_exists($alias, $this->boundSessions)) { 84 | return $this->boundSessions[$alias]; 85 | } 86 | 87 | return $this->boundSessions[$alias] = $this->startSession($alias, $this->defaultSessionConfiguration); 88 | } 89 | 90 | #[\Override] 91 | public function runStatements(iterable $statements, ?string $alias = null): CypherList 92 | { 93 | $runner = $this->getRunner($alias); 94 | if ($runner instanceof SessionInterface) { 95 | return $runner->runStatements($statements, $this->defaultTransactionConfiguration); 96 | } 97 | 98 | return $runner->runStatements($statements); 99 | } 100 | 101 | #[\Override] 102 | public function beginTransaction(?iterable $statements = null, ?string $alias = null, ?TransactionConfiguration $config = null): SymfonyTransaction 103 | { 104 | $session = $this->getSession($alias); 105 | $config = $this->getTsxConfig($config); 106 | 107 | return $session->beginTransaction($statements, $config); 108 | } 109 | 110 | #[\Override] 111 | public function getDriver(?string $alias): SymfonyDriver 112 | { 113 | return $this->factory->createDriver( 114 | new Driver($this->driverSetups->getDriver($this->defaultSessionConfiguration, $alias)), 115 | $alias ?? $this->driverSetups->getDefaultAlias(), 116 | '', 117 | ); 118 | } 119 | 120 | private function startSession(?string $alias, SessionConfiguration $configuration): SymfonySession 121 | { 122 | return $this->factory->createSession( 123 | new Driver($this->driverSetups->getDriver($this->defaultSessionConfiguration, $alias)), 124 | $configuration, 125 | $alias ?? $this->driverSetups->getDefaultAlias(), 126 | '', 127 | ); 128 | } 129 | 130 | /** 131 | * @template HandlerResult 132 | * 133 | * @param callable(SymfonyTransaction):HandlerResult $tsxHandler 134 | * 135 | * @return HandlerResult 136 | */ 137 | #[\Override] 138 | public function writeTransaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null): mixed 139 | { 140 | if ($this->defaultSessionConfiguration->getAccessMode() === AccessMode::WRITE()) { 141 | $session = $this->getSession($alias); 142 | } else { 143 | $sessionConfig = $this->defaultSessionConfiguration->withAccessMode(AccessMode::WRITE()); 144 | $session = $this->startSession($alias, $sessionConfig); 145 | } 146 | 147 | return $session->writeTransaction($tsxHandler, $this->getTsxConfig($config)); 148 | } 149 | 150 | /** 151 | * @template HandlerResult 152 | * 153 | * @param callable(SymfonyTransaction):HandlerResult $tsxHandler 154 | * 155 | * @return HandlerResult 156 | */ 157 | #[\Override] 158 | public function readTransaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null): mixed 159 | { 160 | if ($this->defaultSessionConfiguration->getAccessMode() === AccessMode::READ()) { 161 | $session = $this->getSession($alias); 162 | } else { 163 | $sessionConfig = $this->defaultSessionConfiguration->withAccessMode(AccessMode::WRITE()); 164 | $session = $this->startSession($alias, $sessionConfig); 165 | } 166 | 167 | return $session->readTransaction($tsxHandler, $this->getTsxConfig($config)); 168 | } 169 | 170 | /** 171 | * @template HandlerResult 172 | * 173 | * @param callable(SymfonyTransaction):HandlerResult $tsxHandler 174 | * 175 | * @return HandlerResult 176 | */ 177 | #[\Override] 178 | public function transaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null) 179 | { 180 | return $this->writeTransaction($tsxHandler, $alias, $config); 181 | } 182 | 183 | #[\Override] 184 | public function verifyConnectivity(?string $driver = null): bool 185 | { 186 | return $this->driverSetups->verifyConnectivity($this->defaultSessionConfiguration, $driver); 187 | } 188 | 189 | #[\Override] 190 | public function hasDriver(string $alias): bool 191 | { 192 | return $this->driverSetups->hasDriver($alias); 193 | } 194 | 195 | #[\Override] 196 | public function bindTransaction(?string $alias = null, ?TransactionConfiguration $config = null): void 197 | { 198 | $alias ??= $this->driverSetups->getDefaultAlias(); 199 | 200 | $this->boundTransactions[$alias] ??= []; 201 | $this->boundTransactions[$alias][] = $this->beginTransaction(null, $alias, $config); 202 | } 203 | 204 | #[\Override] 205 | public function rollbackBoundTransaction(?string $alias = null, int $depth = 1): void 206 | { 207 | $this->popTransactions(static fn (SymfonyTransaction $tsx) => $tsx->rollback(), $alias, $depth); 208 | } 209 | 210 | /** 211 | * @param callable(SymfonyTransaction): void $handler 212 | * 213 | * @psalm-suppress ImpureFunctionCall 214 | */ 215 | private function popTransactions(callable $handler, ?string $alias = null, int $depth = 1): void 216 | { 217 | $alias ??= $this->driverSetups->getDefaultAlias(); 218 | 219 | if (!array_key_exists($alias, $this->boundTransactions)) { 220 | return; 221 | } 222 | 223 | while (0 !== count($this->boundTransactions[$alias]) && 0 !== $depth) { 224 | $tsx = array_pop($this->boundTransactions[$alias]); 225 | $handler($tsx); 226 | --$depth; 227 | } 228 | } 229 | 230 | #[\Override] 231 | public function commitBoundTransaction(?string $alias = null, int $depth = 1): void 232 | { 233 | $this->popTransactions(static fn (UnmanagedTransactionInterface $tsx) => $tsx->commit(), $alias, $depth); 234 | } 235 | 236 | private function getTsxConfig(?TransactionConfiguration $config): TransactionConfiguration 237 | { 238 | if (null !== $config) { 239 | return $this->defaultTransactionConfiguration->merge($config); 240 | } 241 | 242 | return $this->defaultTransactionConfiguration; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/Decorators/SymfonyDriver.php: -------------------------------------------------------------------------------- 1 | factory->createSession($this->driver, $config, $this->alias, $this->schema); 27 | } 28 | 29 | #[\Override] 30 | public function verifyConnectivity(?SessionConfiguration $config = null): bool 31 | { 32 | return $this->driver->verifyConnectivity(); 33 | } 34 | 35 | #[\Override] 36 | public function closeConnections(): void 37 | { 38 | $this->driver->closeConnections(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Decorators/SymfonySession.php: -------------------------------------------------------------------------------- 1 | runStatement($statement); 33 | } 34 | 35 | return CypherList::fromIterable($tbr); 36 | } 37 | 38 | #[\Override] 39 | public function runStatement(Statement $statement, ?TransactionConfiguration $config = null): SummarizedResult 40 | { 41 | return $this->handler->handleQuery( 42 | runHandler: fn (Statement $statement) => $this->session->runStatement($statement), 43 | statement: $statement, 44 | alias: $this->alias, 45 | scheme: $this->schema, 46 | transactionId: null 47 | ); 48 | } 49 | 50 | #[\Override] 51 | public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null): SummarizedResult 52 | { 53 | return $this->runStatement(new Statement($statement, $parameters)); 54 | } 55 | 56 | #[\Override] 57 | public function beginTransaction(?iterable $statements = null, ?TransactionConfiguration $config = null): SymfonyTransaction 58 | { 59 | return $this->factory->createTransaction( 60 | session: $this->session, 61 | config: $config, 62 | alias: $this->alias, 63 | schema: $this->schema 64 | ); 65 | } 66 | 67 | /** 68 | * @template HandlerResult 69 | * 70 | * @param callable(SymfonyTransaction):HandlerResult $tsxHandler 71 | * 72 | * @return HandlerResult 73 | * 74 | * @psalm-suppress ArgumentTypeCoercion 75 | */ 76 | #[\Override] 77 | public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) 78 | { 79 | return TransactionHelper::retry( 80 | fn () => $this->beginTransaction(config: $config), 81 | $tsxHandler 82 | ); 83 | } 84 | 85 | /** 86 | * @template HandlerResult 87 | * 88 | * @param callable(SymfonyTransaction):HandlerResult $tsxHandler 89 | * 90 | * @return HandlerResult 91 | */ 92 | #[\Override] 93 | public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) 94 | { 95 | // TODO: create read transaction here. 96 | return $this->writeTransaction($tsxHandler, $config); 97 | } 98 | 99 | /** 100 | * @template HandlerResult 101 | * 102 | * @param callable(SymfonyTransaction):HandlerResult $tsxHandler 103 | * 104 | * @return HandlerResult 105 | */ 106 | #[\Override] 107 | public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null) 108 | { 109 | return $this->writeTransaction($tsxHandler, $config); 110 | } 111 | 112 | #[\Override] 113 | public function getLastBookmark(): Bookmark 114 | { 115 | return $this->session->getLastBookmark(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Decorators/SymfonyTransaction.php: -------------------------------------------------------------------------------- 1 | runStatement(new Statement($statement, $parameters)); 29 | } 30 | 31 | #[\Override] 32 | public function runStatement(Statement $statement): SummarizedResult 33 | { 34 | return $this->handler->handleQuery(fn (Statement $statement) => $this->tsx->runStatement($statement), 35 | $statement, 36 | $this->alias, 37 | $this->scheme, 38 | $this->transactionId 39 | ); 40 | } 41 | 42 | /** 43 | * @psalm-suppress InvalidReturnStatement 44 | */ 45 | #[\Override] 46 | public function runStatements(iterable $statements): CypherList 47 | { 48 | $tbr = []; 49 | foreach ($statements as $statement) { 50 | $tbr[] = $this->runStatement($statement); 51 | } 52 | 53 | return CypherList::fromIterable($tbr); 54 | } 55 | 56 | #[\Override] 57 | public function commit(iterable $statements = []): CypherList 58 | { 59 | $results = $this->runStatements($statements); 60 | 61 | $this->handler->handleTransactionAction( 62 | TransactionState::COMMITTED, 63 | $this->transactionId, 64 | fn () => $this->tsx->commit(), 65 | $this->alias, 66 | $this->scheme, 67 | ); 68 | 69 | return $results; 70 | } 71 | 72 | #[\Override] 73 | public function rollback(): void 74 | { 75 | $this->handler->handleTransactionAction( 76 | TransactionState::ROLLED_BACK, 77 | $this->transactionId, 78 | fn () => $this->tsx->commit(), 79 | $this->alias, 80 | $this->scheme, 81 | ); 82 | } 83 | 84 | #[\Override] 85 | public function isRolledBack(): bool 86 | { 87 | return $this->tsx->isRolledBack(); 88 | } 89 | 90 | #[\Override] 91 | public function isCommitted(): bool 92 | { 93 | return $this->tsx->isCommitted(); 94 | } 95 | 96 | #[\Override] 97 | public function isFinished(): bool 98 | { 99 | return $this->tsx->isFinished(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 51 | * } 52 | * 53 | * @psalm-suppress PossiblyNullReference 54 | * @psalm-suppress PossiblyUndefinedMethod 55 | * @psalm-suppress UndefinedInterfaceMethod 56 | */ 57 | final class Configuration implements ConfigurationInterface 58 | { 59 | #[\Override] 60 | public function getConfigTreeBuilder(): TreeBuilder 61 | { 62 | $treeBuilder = new TreeBuilder('neo4j'); 63 | 64 | $treeBuilder->getRootNode() 65 | ->fixXmlConfig('driver') 66 | ->children() 67 | ->append($this->decorateDriverConfig()) 68 | ->append($this->decorateSessionConfig()) 69 | ->append($this->decorateTransactionConfig()) 70 | ->scalarNode('min_log_level') 71 | ->info('Minimum severity the driver will log. Follows Psr LogLevel. Default is "error".') 72 | ->defaultValue(LogLevel::DEBUG) 73 | ->end() 74 | ->scalarNode('default_driver') 75 | ->info('The default driver to use. Default is the first configured driver.') 76 | ->end() 77 | ->arrayNode('drivers') 78 | ->info( 79 | 'List of drivers to use. If no drivers are configured the default driver will try to open a bolt connection without authentication on localhost over port 7687' 80 | ) 81 | ->arrayPrototype() 82 | ->fixXmlConfig('driver') 83 | ->children() 84 | ->scalarNode('alias') 85 | ->info('The alias for this driver. Default is "default".') 86 | ->defaultValue('default') 87 | ->end() 88 | ->scalarNode('profiling') 89 | ->info('Enable profiling for requests on this driver. If no value is provided the default value will be equal to the kernel.debug parameter.') 90 | ->defaultValue(null) 91 | ->end() 92 | ->scalarNode('dsn') 93 | ->info('The DSN for the driver. Default is "bolt://localhost:7687".') 94 | ->defaultValue('bolt://localhost:7687') 95 | ->end() 96 | ->arrayNode('authentication') 97 | ->info('The authentication for the driver') 98 | ->children() 99 | ->enumNode('type') 100 | ->info('The type of authentication') 101 | ->values(['basic', 'kerberos', 'dsn', 'none', 'oid']) 102 | ->end() 103 | ->scalarNode('username')->end() 104 | ->scalarNode('password')->end() 105 | ->scalarNode('token')->end() 106 | ->end() 107 | ->end() 108 | ->scalarNode('priority') 109 | ->info('The priority of this when trying to fall back on the same alias. Default is 0') 110 | ->end() 111 | ->end() 112 | ->end() 113 | ->end() 114 | ->end(); 115 | 116 | return $treeBuilder; 117 | } 118 | 119 | private function decorateSessionConfig(): ArrayNodeDefinition 120 | { 121 | return (new ArrayNodeDefinition('default_session_config')) 122 | ->info('The default configuration for every session') 123 | ->children() 124 | ->scalarNode('fetch_size') 125 | ->info('The amount of rows that are being fetched at once in the result cursor') 126 | ->end() 127 | ->enumNode('access_mode') 128 | ->values(['read', 'write', null]) 129 | ->info('The default access mode for every session. Default is WRITE.') 130 | ->end() 131 | ->scalarNode('database') 132 | ->info('Select the standard database to use. Default is value is null, meaning the preconfigured database by the server is used (usually a database called neo4j).') 133 | ->end() 134 | ->end(); 135 | } 136 | 137 | private function decorateDriverConfig(): ArrayNodeDefinition 138 | { 139 | return (new ArrayNodeDefinition('default_driver_config')) 140 | ->info('The default configuration for every driver') 141 | ->children() 142 | ->scalarNode('acquire_connection_timeout') 143 | ->info(sprintf( 144 | 'The default timeout for acquiring a connection from the connection pool. Default is %s seconds. Note that this is different from the transaction timeout.', 145 | DriverConfiguration::DEFAULT_ACQUIRE_CONNECTION_TIMEOUT 146 | )) 147 | ->end() 148 | ->scalarNode('user_agent') 149 | ->info('The default user agent this driver. Default is "neo4j-php-client/%client-version-numer%".') 150 | ->end() 151 | ->scalarNode('pool_size') 152 | ->info(sprintf( 153 | 'The default maximum number of connections in the connection pool. Default is %s. Connections are lazily created and closed.', 154 | DriverConfiguration::DEFAULT_POOL_SIZE 155 | )) 156 | ->end() 157 | ->arrayNode('ssl') 158 | ->info('The SSL configuration for this driver') 159 | ->children() 160 | ->enumNode('mode') 161 | ->values(['enable', 'disable', 'from_url', 'enable_with_self_signed', null]) 162 | ->info('The SSL mode for this driver') 163 | ->end() 164 | ->booleanNode('verify_peer') 165 | ->info('Verify the peer certificate. Default is true.') 166 | ->end() 167 | ->end() 168 | ->end() 169 | ->end(); 170 | } 171 | 172 | private function decorateTransactionConfig(): ArrayNodeDefinition 173 | { 174 | return (new ArrayNodeDefinition('default_transaction_config')) 175 | ->info('The default configuration for every transaction') 176 | ->children() 177 | ->scalarNode('timeout') 178 | ->info( 179 | 'The default transaction timeout. If null is provided it will fall back tot he preconfigured timeout on the server' 180 | ) 181 | ->end() 182 | ->end(); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/DependencyInjection/Neo4jExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 33 | 34 | $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config')); 35 | $loader->load('services.php'); 36 | 37 | $defaultAlias = $mergedConfig['default_driver'] ?? $mergedConfig['drivers'][0]['alias'] ?? 'default'; 38 | $container->setDefinition('neo4j.event_handler', new Definition(EventHandler::class)) 39 | ->setAutowired(true) 40 | ->addTag('neo4j.event_handler') 41 | ->setArgument(1, $defaultAlias); 42 | 43 | $container->getDefinition('neo4j.client_factory') 44 | ->setArgument('$driverConfig', $mergedConfig['default_driver_config'] ?? null) 45 | ->setArgument('$sessionConfig', $mergedConfig['default_session_config'] ?? null) 46 | ->setArgument('$transactionConfig', $mergedConfig['default_transaction_config'] ?? null) 47 | ->setArgument('$connections', $mergedConfig['drivers'] ?? []) 48 | ->setArgument('$defaultDriver', $mergedConfig['default_driver'] ?? null) 49 | ->setArgument('$builder', new Reference(ClientBuilder::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) 50 | ->setArgument('$logLevel', $mergedConfig['min_log_level'] ?? 'debug') 51 | ->setArgument('$logger', new Reference(LoggerInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) 52 | ->setAbstract(false); 53 | 54 | $container->getDefinition('neo4j.driver') 55 | ->setArgument(0, $defaultAlias); 56 | 57 | foreach ($mergedConfig['drivers'] as $driverConfig) { 58 | $container 59 | ->setDefinition( 60 | 'neo4j.driver.'.$driverConfig['alias'], 61 | (new Definition(DriverInterface::class)) 62 | ->setFactory([new Reference('neo4j.client'), 'getDriver']) 63 | ->setArgument(0, $driverConfig['alias']) 64 | ) 65 | ->setPublic(true); 66 | 67 | $container 68 | ->setDefinition( 69 | 'neo4j.session.'.$driverConfig['alias'], 70 | (new Definition(SessionInterface::class)) 71 | ->setFactory([new Reference('neo4j.driver.'.$driverConfig['alias']), 'createSession']) 72 | ->setShared(false) 73 | ) 74 | ->setPublic(true); 75 | } 76 | 77 | $enabledProfiles = []; 78 | foreach ($mergedConfig['drivers'] as $driver) { 79 | if (true === $driver['profiling'] || (null === $driver['profiling'] && $container->getParameter( 80 | 'kernel.debug' 81 | ))) { 82 | $enabledProfiles[] = $driver['alias']; 83 | } 84 | } 85 | 86 | if (0 !== count($enabledProfiles)) { 87 | $container->setDefinition( 88 | 'neo4j.data_collector', 89 | (new Definition(Neo4jDataCollector::class)) 90 | ->setAutowired(true) 91 | ->addTag('data_collector', [ 92 | 'id' => 'neo4j', 93 | 'priority' => 500, 94 | ]) 95 | ); 96 | 97 | $container->setAlias(Neo4jProfileListener::class, 'neo4j.subscriber'); 98 | 99 | $container->setDefinition( 100 | 'neo4j.subscriber', 101 | (new Definition(Neo4jProfileListener::class)) 102 | ->setArgument(0, $enabledProfiles) 103 | ->addTag('kernel.event_subscriber') 104 | ); 105 | } 106 | 107 | return $container; 108 | } 109 | 110 | #[\Override] 111 | public function getConfiguration(array $config, ContainerBuilder $container): Configuration 112 | { 113 | return new Configuration(); 114 | } 115 | 116 | #[\Override] 117 | public function getAlias(): string 118 | { 119 | return 'neo4j'; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Event/FailureEvent.php: -------------------------------------------------------------------------------- 1 | statement; 30 | } 31 | 32 | public function getException(): Neo4jException 33 | { 34 | return $this->exception; 35 | } 36 | 37 | /** @api */ 38 | public function disableException(): void 39 | { 40 | $this->shouldThrowException = false; 41 | } 42 | 43 | public function shouldThrowException(): bool 44 | { 45 | return $this->shouldThrowException; 46 | } 47 | 48 | public function getTime(): \DateTimeInterface 49 | { 50 | return $this->time; 51 | } 52 | 53 | public function getAlias(): ?string 54 | { 55 | return $this->alias; 56 | } 57 | 58 | public function getScheme(): ?string 59 | { 60 | return $this->scheme; 61 | } 62 | 63 | public function getTransactionId(): ?string 64 | { 65 | return $this->transactionId; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Event/PostRunEvent.php: -------------------------------------------------------------------------------- 1 | result; 26 | } 27 | 28 | public function getTime(): \DateTimeInterface 29 | { 30 | return $this->time; 31 | } 32 | 33 | public function getAlias(): ?string 34 | { 35 | return $this->alias; 36 | } 37 | 38 | public function getScheme(): ?string 39 | { 40 | return $this->scheme; 41 | } 42 | 43 | public function getTransactionId(): ?string 44 | { 45 | return $this->transactionId; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Event/PreRunEvent.php: -------------------------------------------------------------------------------- 1 | statement; 27 | } 28 | 29 | public function getTime(): \DateTimeInterface 30 | { 31 | return $this->time; 32 | } 33 | 34 | public function getAlias(): ?string 35 | { 36 | return $this->alias; 37 | } 38 | 39 | public function getScheme(): ?string 40 | { 41 | return $this->scheme; 42 | } 43 | 44 | public function getTransactionId(): ?string 45 | { 46 | return $this->transactionId; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Event/Transaction/PostTransactionBeginEvent.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 34 | } 35 | 36 | /** 37 | * @template T 38 | * 39 | * @param callable(Statement):T $runHandler 40 | * 41 | * @return T 42 | */ 43 | public function handleQuery(callable $runHandler, Statement $statement, string $alias, string $scheme, ?string $transactionId): SummarizedResult 44 | { 45 | $stopwatchName = $this->nameFactory->createQueryEventName($alias, $transactionId); 46 | 47 | $time = new \DateTimeImmutable(); 48 | $event = new PreRunEvent( 49 | alias: $alias, 50 | statement: $statement, 51 | time: $time, 52 | scheme: $scheme, 53 | transactionId: $transactionId 54 | ); 55 | 56 | $this->dispatcher?->dispatch($event, PreRunEvent::EVENT_ID); 57 | 58 | $runHandler = static fn (): mixed => $runHandler($statement); 59 | $result = $this->handleAction( 60 | runHandler: $runHandler, 61 | alias: $alias, 62 | scheme: $scheme, 63 | stopwatchName: $stopwatchName, 64 | transactionId: $transactionId, 65 | statement: $statement 66 | ); 67 | 68 | $event = new PostRunEvent( 69 | alias: $alias, 70 | result: $result->getSummary(), 71 | time: $time, 72 | scheme: $scheme, 73 | transactionId: $transactionId 74 | ); 75 | 76 | $this->dispatcher?->dispatch( 77 | $event, 78 | PostRunEvent::EVENT_ID 79 | ); 80 | 81 | return $result; 82 | } 83 | 84 | /** 85 | * @template T 86 | * 87 | * @param callable():T $runHandler 88 | * 89 | * @return T 90 | */ 91 | public function handleTransactionAction( 92 | TransactionState $nextTransactionState, 93 | string $transactionId, 94 | callable $runHandler, 95 | string $alias, 96 | string $scheme, 97 | ): mixed { 98 | $stopWatchName = $this->nameFactory->createTransactionEventName($alias, $transactionId, $nextTransactionState); 99 | 100 | [ 101 | 'preEvent' => $preEvent, 102 | 'preEventId' => $preEventId, 103 | 'postEvent' => $postEvent, 104 | 'postEventId' => $postEventId, 105 | ] = $this->createPreAndPostEventsAndIds( 106 | nextTransactionState: $nextTransactionState, 107 | alias: $alias, 108 | scheme: $scheme, 109 | transactionId: $transactionId 110 | ); 111 | 112 | $this->dispatcher?->dispatch($preEvent, $preEventId); 113 | $result = $this->handleAction(runHandler: $runHandler, alias: $alias, scheme: $scheme, stopwatchName: $stopWatchName, transactionId: $transactionId, statement: null); 114 | $this->dispatcher?->dispatch($postEvent, $postEventId); 115 | 116 | return $result; 117 | } 118 | 119 | /** 120 | * @template T 121 | * 122 | * @param callable():T $runHandler 123 | * 124 | * @return T 125 | */ 126 | private function handleAction(callable $runHandler, string $alias, string $scheme, string $stopwatchName, ?string $transactionId, ?Statement $statement): mixed 127 | { 128 | try { 129 | $this->stopwatch?->start($stopwatchName, 'database'); 130 | $result = $runHandler(); 131 | $this->stopwatch?->stop($stopwatchName); 132 | 133 | return $result; 134 | } catch (Neo4jException $e) { 135 | $this->stopwatch?->stop($stopwatchName); 136 | $event = new FailureEvent( 137 | alias: $alias, 138 | statement: $statement, 139 | exception: $e, 140 | time: new \DateTimeImmutable('now'), 141 | scheme: $scheme, 142 | transactionId: $transactionId 143 | ); 144 | 145 | $this->dispatcher?->dispatch($event, FailureEvent::EVENT_ID); 146 | 147 | throw $e; 148 | } 149 | } 150 | 151 | /** 152 | * @return array{'preEvent': object, 'preEventId': string, 'postEvent': object, 'postEventId': string} 153 | */ 154 | private function createPreAndPostEventsAndIds( 155 | TransactionState $nextTransactionState, 156 | string $alias, 157 | string $scheme, 158 | string $transactionId, 159 | ): array { 160 | [$preEvent, $preEventId] = match ($nextTransactionState) { 161 | TransactionState::ACTIVE => [ 162 | new PreTransactionBeginEvent( 163 | alias: $alias, 164 | time: new \DateTimeImmutable(), 165 | scheme: $scheme, 166 | transactionId: $transactionId, 167 | ), 168 | PreTransactionBeginEvent::EVENT_ID, 169 | ], 170 | TransactionState::ROLLED_BACK => [ 171 | new PreTransactionRollbackEvent( 172 | alias: $alias, 173 | time: new \DateTimeImmutable(), 174 | scheme: $scheme, 175 | transactionId: $transactionId, 176 | ), 177 | PreTransactionRollbackEvent::EVENT_ID, 178 | ], 179 | TransactionState::COMMITTED => [ 180 | new PreTransactionCommitEvent( 181 | alias: $alias, 182 | time: new \DateTimeImmutable(), 183 | scheme: $scheme, 184 | transactionId: $transactionId, 185 | ), 186 | PreTransactionCommitEvent::EVENT_ID, 187 | ], 188 | TransactionState::TERMINATED => throw new \UnexpectedValueException('TERMINATED is not a valid transaction state at this point'), 189 | }; 190 | [$postEvent, $postEventId] = match ($nextTransactionState) { 191 | TransactionState::ACTIVE => [ 192 | new PostTransactionBeginEvent( 193 | alias: $alias, 194 | time: new \DateTimeImmutable(), 195 | scheme: $scheme, 196 | transactionId: $transactionId, 197 | ), 198 | PostTransactionBeginEvent::EVENT_ID, 199 | ], 200 | TransactionState::ROLLED_BACK => [ 201 | new PostTransactionRollbackEvent( 202 | alias: $alias, 203 | time: new \DateTimeImmutable(), 204 | scheme: $scheme, 205 | transactionId: $transactionId, 206 | ), 207 | PostTransactionRollbackEvent::EVENT_ID, 208 | ], 209 | TransactionState::COMMITTED => [ 210 | new PostTransactionCommitEvent( 211 | alias: $alias, 212 | time: new \DateTimeImmutable(), 213 | scheme: $scheme, 214 | transactionId: $transactionId, 215 | ), 216 | PostTransactionCommitEvent::EVENT_ID, 217 | ], 218 | TransactionState::TERMINATED => throw new \UnexpectedValueException('TERMINATED is not a valid transaction state at this point'), 219 | }; 220 | 221 | return [ 222 | 'preEvent' => $preEvent, 223 | 'preEventId' => $preEventId, 224 | 'postEvent' => $postEvent, 225 | 'postEventId' => $postEventId, 226 | ]; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/EventListener/Neo4jProfileListener.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | private array $profiledSummaries = []; 27 | 28 | /** 29 | * @var list 36 | */ 37 | private array $profiledFailures = []; 38 | 39 | /** 40 | * @param list $enabledProfiles 41 | */ 42 | public function __construct(private readonly array $enabledProfiles = []) 43 | { 44 | } 45 | 46 | #[\Override] 47 | public static function getSubscribedEvents(): array 48 | { 49 | return [ 50 | PostRunEvent::EVENT_ID => 'onPostRun', 51 | FailureEvent::EVENT_ID => 'onFailure', 52 | ]; 53 | } 54 | 55 | public function onPostRun(PostRunEvent $event): void 56 | { 57 | if (in_array($event->getAlias(), $this->enabledProfiles)) { 58 | $time = $event->getTime(); 59 | $result = $event->getResult(); 60 | $end_time = $time->getTimestamp() + $result->getResultAvailableAfter() + $result->getResultConsumedAfter(); 61 | $this->profiledSummaries[] = [ 62 | 'result' => $event->getResult(), 63 | 'alias' => $event->getAlias(), 64 | 'time' => $time->format('Y-m-d H:i:s'), 65 | 'start_time' => $time->getTimestamp(), 66 | 'end_time' => $end_time, 67 | ]; 68 | } 69 | } 70 | 71 | public function onFailure(FailureEvent $event): void 72 | { 73 | if (in_array($event->getAlias(), $this->enabledProfiles)) { 74 | $time = $event->getTime(); 75 | $this->profiledFailures[] = [ 76 | 'exception' => $event->getException(), 77 | 'statement' => $event->getStatement(), 78 | 'alias' => $event->getAlias(), 79 | 'time' => $time->format('Y-m-d H:i:s'), 80 | 'timestamp' => $time->getTimestamp(), 81 | ]; 82 | } 83 | } 84 | 85 | public function getProfiledSummaries(): array 86 | { 87 | return $this->profiledSummaries; 88 | } 89 | 90 | /** 91 | * @return list 98 | */ 99 | public function getProfiledFailures(): array 100 | { 101 | return $this->profiledFailures; 102 | } 103 | 104 | #[\Override] 105 | public function reset(): void 106 | { 107 | $this->profiledFailures = []; 108 | $this->profiledSummaries = []; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Factories/ClientFactory.php: -------------------------------------------------------------------------------- 1 | $connections 36 | */ 37 | public function __construct( 38 | private readonly ?array $driverConfig, 39 | private readonly ?array $sessionConfig, 40 | private readonly ?array $transactionConfig, 41 | private readonly array $connections, 42 | private readonly ?string $defaultDriver, 43 | private readonly ?string $logLevel, 44 | private readonly ?LoggerInterface $logger, 45 | private ClientBuilder $builder, 46 | ) { 47 | } 48 | 49 | public function create(): SymfonyClient 50 | { 51 | $this->builder = $this->builder->withDefaultDriverConfiguration( 52 | $this->makeDriverConfig($this->logLevel, $this->logger) 53 | ); 54 | 55 | $this->builder = $this->builder->withDefaultSessionConfiguration($this->makeSessionConfig()); 56 | 57 | $this->builder = $this->builder->withDefaultTransactionConfiguration($this->makeTransactionConfig()); 58 | 59 | foreach ($this->connections as $connection) { 60 | $this->builder = $this->builder->withDriver( 61 | $connection['alias'], 62 | $connection['dsn'], 63 | $this->createAuth($connection['authentication'] ?? null, $connection['dsn']), 64 | $connection['priority'] ?? null 65 | ); 66 | } 67 | 68 | if (null !== $this->defaultDriver) { 69 | $this->builder = $this->builder->withDefaultDriver($this->defaultDriver); 70 | } 71 | 72 | return $this->builder->build(); 73 | } 74 | 75 | private function makeDriverConfig(?string $logLevel = null, ?LoggerInterface $logger = null): DriverConfiguration 76 | { 77 | return new DriverConfiguration( 78 | userAgent: $this->driverConfig['user_agent'] ?? null, 79 | sslConfig: $this->makeSslConfig($this->driverConfig['ssl'] ?? null), 80 | maxPoolSize: $this->driverConfig['pool_size'] ?? null, 81 | cache: null, 82 | acquireConnectionTimeout: $this->driverConfig['acquire_connection_timeout'] ?? null, 83 | semaphore: null, 84 | logLevel: $logLevel, 85 | logger: $logger, 86 | ); 87 | } 88 | 89 | private function makeSessionConfig(): SessionConfiguration 90 | { 91 | return new SessionConfiguration( 92 | database: $this->sessionConfig['database'] ?? null, 93 | fetchSize: $this->sessionConfig['fetch_size'] ?? null, 94 | accessMode: match ($this->sessionConfig['access_mode'] ?? null) { 95 | 'write', null => AccessMode::WRITE(), 96 | 'read' => AccessMode::READ(), 97 | }, 98 | ); 99 | } 100 | 101 | private function makeTransactionConfig(): TransactionConfiguration 102 | { 103 | return new TransactionConfiguration( 104 | timeout: $this->transactionConfig['timeout'] ?? null 105 | ); 106 | } 107 | 108 | /** 109 | * @param DriverAuthenticationArray|null $auth 110 | */ 111 | private function createAuth(?array $auth, string $dsn): AuthenticateInterface 112 | { 113 | if (null === $auth) { 114 | return Authenticate::fromUrl(Uri::create($dsn)); 115 | } 116 | 117 | return match ($auth['type'] ?? null) { 118 | 'basic' => Authenticate::basic( 119 | $auth['username'] ?? throw new \InvalidArgumentException('Missing username for basic authentication'), 120 | $auth['password'] ?? throw new \InvalidArgumentException('Missing password for basic authentication') 121 | ), 122 | 'kerberos' => Authenticate::kerberos( 123 | $auth['token'] ?? throw new \InvalidArgumentException('Missing token for kerberos authentication') 124 | ), 125 | 'dsn', null => Authenticate::fromUrl(Uri::create($dsn)), 126 | 'none' => Authenticate::disabled(), 127 | 'oid' => Authenticate::oidc( 128 | $auth['token'] ?? throw new \InvalidArgumentException('Missing token for oid authentication') 129 | ), 130 | }; 131 | } 132 | 133 | /** 134 | * @param SslConfigArray|null $ssl 135 | */ 136 | private function makeSslConfig(?array $ssl): SslConfiguration 137 | { 138 | if (null === $ssl) { 139 | return new SslConfiguration( 140 | mode: SslMode::DISABLE(), 141 | verifyPeer: false, 142 | ); 143 | } 144 | 145 | return new SslConfiguration( 146 | mode: match ($ssl['mode'] ?? null) { 147 | null, 'disable' => SslMode::DISABLE(), 148 | 'enable' => SslMode::ENABLE(), 149 | 'from_url' => SslMode::FROM_URL(), 150 | 'enable_with_self_signed' => SslMode::ENABLE_WITH_SELF_SIGNED(), 151 | }, 152 | verifyPeer: !(($ssl['verify_peer'] ?? true) === false), 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Factories/StopwatchEventNameFactory.php: -------------------------------------------------------------------------------- 1 | nextTransactionStateToAction($nextTransactionState) 29 | ); 30 | } 31 | 32 | private function nextTransactionStateToAction(TransactionState $state): string 33 | { 34 | return match ($state) { 35 | TransactionState::COMMITTED => 'commit', 36 | TransactionState::ACTIVE => 'begin', 37 | TransactionState::ROLLED_BACK => 'rollback', 38 | TransactionState::TERMINATED => 'error', 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Factories/SymfonyDriverFactory.php: -------------------------------------------------------------------------------- 1 | generateTransactionId(); 27 | 28 | $handler = fn (): SymfonyTransaction => new SymfonyTransaction( 29 | tsx: $session->beginTransaction(config: $config), 30 | handler: $this->handler, 31 | alias: $alias, 32 | scheme: $schema, 33 | transactionId: $tranactionId 34 | ); 35 | 36 | return $this->handler->handleTransactionAction( 37 | nextTransactionState: TransactionState::ACTIVE, 38 | transactionId: $tranactionId, 39 | runHandler: $handler, 40 | alias: $alias, 41 | scheme: $schema, 42 | ); 43 | } 44 | 45 | public function createSession( 46 | Driver $driver, 47 | ?SessionConfiguration $config, 48 | string $alias, 49 | string $schema, 50 | ): SymfonySession { 51 | return new SymfonySession( 52 | session: $driver->createSession($config), 53 | handler: $this->handler, 54 | factory: $this, 55 | alias: $alias, 56 | schema: $schema, 57 | ); 58 | } 59 | 60 | public function createDriver( 61 | Driver $driver, 62 | string $alias, 63 | string $schema, 64 | ): SymfonyDriver { 65 | return new SymfonyDriver( 66 | $driver, 67 | $this, 68 | $alias, 69 | $schema, 70 | ); 71 | } 72 | 73 | private function generateTransactionId(): string 74 | { 75 | if ($this->uuidFactory) { 76 | return $this->uuidFactory->create()->toRfc4122(); 77 | } 78 | 79 | $data = random_bytes(16); 80 | 81 | // Set the version to 4 (UUID v4) 82 | $data[6] = chr((ord($data[6]) & 0x0F) | 0x40); 83 | 84 | // Set the variant to RFC 4122 (10xx) 85 | $data[8] = chr((ord($data[8]) & 0x3F) | 0x80); 86 | 87 | // Format the UUID as 8-4-4-4-12 hexadecimal characters 88 | return sprintf( 89 | '%08s-%04s-%04s-%04s-%12s', 90 | bin2hex(substr($data, 0, 4)), 91 | bin2hex(substr($data, 4, 2)), 92 | bin2hex(substr($data, 6, 2)), 93 | bin2hex(substr($data, 8, 2)), 94 | bin2hex(substr($data, 10, 6)) 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Neo4jBundle.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Resources/public/js/neo4j.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j-php/neo4j-symfony/94dbd253de151e781405b7ef9be7b3d2f92b2349/src/Resources/public/js/neo4j.js -------------------------------------------------------------------------------- /src/Resources/views/web_profiler.html.twig: -------------------------------------------------------------------------------- 1 | {# templates/data_collector/template.html.twig #} 2 | {% extends '@WebProfiler/Profiler/layout.html.twig' %} 3 | 4 | {% block toolbar %} 5 | {% if collector.queryCount > 0 %} 6 | {% set icon %} 7 | {# {{ include('@Neo4j/public/images/neo4j.svg') }}#} 8 | {{ collector.queryCount }} 9 | stmt. 10 | {% endset %} 11 | 12 | {% set text %} 13 |
14 | Query count 15 | {{ collector.queryCount }} 16 |
17 | {% if collector.failedStatements|length %} 18 |
19 | Failed queries 20 | {{ collector.failedStatements|length }} 21 |
22 | {% endif %} 23 |
24 | Total time 25 | {# {{ '%0.2f'|format(collector.time) }}ms#} 26 |
27 | 28 | {% endset %} 29 | {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': profiler_url, 'status': collector.failedStatements|length ? 'red' : '' } %} 30 | {% endif %} 31 | {% endblock %} 32 | 33 | {#{% block head %}#} 34 | {# #} 35 | {# #} 36 | {# {{ parent() }}#} 37 | {#{% endblock %}#} 38 | 39 | {% block menu %} 40 | {# This left-hand menu appears when using the full-screen profiler. #} 41 | 42 | 43 | {# {{ include('@Neo4j/Icon/neo4j.svg') }}#} 44 | 45 | Neo4j 46 | {% if collector.failedStatements|length %} 47 | 48 | {{ collector.failedStatements|length }} 49 | 50 | {% endif %} 51 | 52 | {% endblock %} 53 | 54 | {% block panel %} 55 |

Neo4j Bundle

56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | {% for idx, statement in collector.statements %} 72 | 73 | {% set status = statement.status|default('unknown') %} 74 | {% set start_time = statement.start_time|default(null) %} 75 | {% set end_time = statement.end_time|default(null) %} 76 | 77 | 78 | 79 | 103 | 104 | {% if status is same as('success') %} 105 | 106 | 107 | {% else %} 108 | 109 | 110 | {% endif %} 111 | 112 | {% if status is same as('success') %} 113 | 114 | {% else %} 115 | 116 | {% endif %} 117 | 118 | {% endfor %} 119 | 120 |
#StatusQueryQuery TimeQuery TypeDatabaseExecuted AtException Title
{{ idx + 1 }}{{ statement.status }} 80 |
81 | {{ statement.result.statement.text|default('') }} 82 |
83 |
84 | View details 85 |
86 | 87 |
88 |
89 | Parameters: {{ statement.result.statement.parameters|default([])|json_encode }} 90 |
91 |
92 | Alias: {{ statement.alias|default('N/A') }} 93 |
94 | {# TODO: Add scheme. Scheme is currently private in the underlying driver #} 95 | {#
#} 96 | {# Scheme: {{ statement.scheme|default('N/A') }}#} 97 | {#
#} 98 |
99 | Statistics: {{ statement|json_encode }} 100 |
101 |
102 |
{% if status is same as('success') %}{% if start_time is not null and end_time is not null %}{{ '%0.2f'|format(end_time - start_time) }}ms{% endif %}{% else %}N/A{% endif %}{{ statement.result.queryType }}{{ statement.result.databaseInfo.name }}N/AN/A{{ statement.time }}N/A{{ statement.exception.title }}
121 | 122 | {% endblock %} 123 | 124 | -------------------------------------------------------------------------------- /tests/App/Controller/TestController.php: -------------------------------------------------------------------------------- 1 | run('MATCH (n {foo: $bar}) RETURN n', ['bar' => 'baz']); 20 | try { 21 | $client->run('MATCH (n) {x: $x}', ['x' => 1]); 22 | } catch (\Exception) { 23 | $this->logger->warning('Detected failed statement'); 24 | } 25 | 26 | return $this->render('index.html.twig'); 27 | } 28 | 29 | public function runOnSession(SessionInterface $session): Response 30 | { 31 | $session->run('MATCH (n {foo: $bar}) RETURN n', ['bar' => 'baz']); 32 | try { 33 | $session->run('MATCH (n) {x: $x}', ['x' => 1]); 34 | } catch (\Exception) { 35 | $this->logger->warning('Detected failed statement'); 36 | } 37 | 38 | return $this->render('index.html.twig'); 39 | } 40 | 41 | public function runOnTransaction(SessionInterface $session): Response 42 | { 43 | $tsx = $session->beginTransaction(); 44 | 45 | $tsx->run('MATCH (n {foo: $bar}) RETURN n', ['bar' => 'baz']); 46 | try { 47 | $tsx->run('MATCH (n) {x: $x}', ['x' => 1]); 48 | } catch (\Exception) { 49 | $this->logger->warning('Detected failed statement'); 50 | } finally { 51 | $tsx->rollback(); 52 | } 53 | 54 | return $this->render('index.html.twig'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/App/Controller/Twig/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}My Application{% endblock %} 6 | {% block stylesheets %} 7 | {# Include your stylesheets here #} 8 | {% endblock %} 9 | 10 | 11 |
12 | {# Include your navigation/header here #} 13 |
14 | 15 |
16 | {% block body %}{% endblock %} 17 |
18 | 19 |
20 | {# Include your footer here #} 21 |
22 | 23 | {% block javascripts %} 24 | {# Include your JavaScripts here #} 25 | {% endblock %} 26 | 27 | -------------------------------------------------------------------------------- /tests/App/Controller/Twig/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}Hello Neo4j{% endblock %} 4 | 5 | {% block body %} 6 |

Hello Neo4j

7 | {% endblock %} -------------------------------------------------------------------------------- /tests/App/TestKernel.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/config/default.yml'); 31 | if ('ci' === $this->environment) { 32 | $loader->load(__DIR__.'/config/ci/default.yml'); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/App/config/ci/default.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | neo4j.dsn.badname: bolt://localhostt 3 | neo4j.dsn.test: neo4j://neo4j:testtest@localhost 4 | neo4j.dsn.auth: neo4j://localhost 5 | -------------------------------------------------------------------------------- /tests/App/config/default.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | Neo4j\Neo4jBundle\Tests\App\Controller\TestController: 4 | public: true 5 | autoconfigure: true 6 | autowire: true 7 | tags: ['controller.service_arguments'] 8 | Symfony\Component\HttpKernel\Profiler\Profiler: '@profiler' 9 | Symfony\Component\HttpKernel\EventListener\ProfilerListener: '@profiler_listener' 10 | 11 | framework: 12 | secret: test 13 | test: true 14 | profiler: { enabled: true, collect: true } 15 | router: 16 | resource: '%kernel.project_dir%/tests/App/config/routes.yaml' 17 | type: 'yaml' 18 | 19 | twig: 20 | debug: "%kernel.debug%" 21 | paths: 22 | - '%kernel.project_dir%/tests/App/Controller/Twig' 23 | 24 | web_profiler: 25 | toolbar: true 26 | intercept_redirects: false 27 | 28 | parameters: 29 | neo4j.dsn.badname: bolt://localhost 30 | neo4j.dsn.secret: neo4j://neo4j:secret@localhost:7688 31 | neo4j.dsn.test: neo4j://neo4j:testtest@neo4j 32 | neo4j.dsn.auth: neo4j://neo4j 33 | neo4j.dsn.simple: bolt://test:test@localhost 34 | 35 | neo4j: 36 | min_log_level: warning 37 | default_driver: neo4j-test 38 | default_driver_config: 39 | acquire_connection_timeout: 10 40 | user_agent: "Neo4j Symfony Bundle/testing" 41 | pool_size: 256 42 | ssl: 43 | mode: disable 44 | verify_peer: false 45 | default_session_config: 46 | fetch_size: 999 47 | access_mode: read 48 | database: neo4j 49 | default_transaction_config: 50 | timeout: 40 51 | 52 | drivers: 53 | - alias: neo4j_undefined_configs 54 | dsn: "%neo4j.dsn.badname%" 55 | 56 | - alias: neo4j-enforced-defaults 57 | dsn: "%neo4j.dsn.badname%" 58 | priority: null 59 | 60 | - alias: neo4j-partly-enforced-defaults 61 | dsn: "%neo4j.dsn.secret%" 62 | 63 | - alias: neo4j-simple 64 | dsn: "%neo4j.dsn.simple%" 65 | 66 | - alias: neo4j-fallback-mechanism 67 | priority: 100 68 | dsn: "%neo4j.dsn.badname%" 69 | 70 | - alias: neo4j-fallback-mechanism 71 | priority: 1000 72 | dsn: "%neo4j.dsn.badname%" 73 | 74 | - alias: neo4j-test 75 | dsn: "%neo4j.dsn.test%" 76 | 77 | - alias: neo4j-auth 78 | dsn: "%neo4j.dsn.auth%" 79 | authentication: 80 | type: basic 81 | username: neo4j 82 | password: testtest 83 | -------------------------------------------------------------------------------- /tests/App/config/routes.yaml: -------------------------------------------------------------------------------- 1 | run-on-client: 2 | path: /client 3 | controller: Neo4j\Neo4jBundle\Tests\App\Controller\TestController::runOnClient 4 | run-on-session: 5 | path: /session 6 | controller: Neo4j\Neo4jBundle\Tests\App\Controller\TestController::runOnSession 7 | run-on-transaction: 8 | path: /transaction 9 | controller: Neo4j\Neo4jBundle\Tests\App\Controller\TestController::runOnTransaction 10 | 11 | web_profiler_wdt: 12 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 13 | prefix: /_wdt 14 | web_profiler_profiler: 15 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 16 | prefix: /_profiler4 -------------------------------------------------------------------------------- /tests/App/index.php: -------------------------------------------------------------------------------- 1 | boot(); 13 | $request = Request::createFromGlobals(); 14 | $response = $kernel->handle($request); 15 | if ($kernel->getContainer()->has('profiler')) { 16 | /** @var Symfony\Component\HttpKernel\Profiler\Profiler $profiler */ 17 | $profiler = $kernel->getContainer()->get('profiler'); 18 | $profile = $profiler->collect($request, $response); 19 | if (null === $profile) { 20 | error_log('Profiler token was not generated!!!'); 21 | } else { 22 | error_log('Profiler token: '.$profile->getToken()); 23 | } 24 | } else { 25 | error_log('Profiler service not found in container'); 26 | } 27 | $response->send(); 28 | $kernel->terminate($request, $response); 29 | -------------------------------------------------------------------------------- /tests/Application.php: -------------------------------------------------------------------------------- 1 | assertTrue($container->has('neo4j.client')); 46 | $client = $container->get('neo4j.client'); 47 | $this->assertInstanceOf(ClientInterface::class, $client); 48 | 49 | $this->assertTrue($container->has(ClientInterface::class)); 50 | $this->assertInstanceOf(ClientInterface::class, $client); 51 | 52 | $this->assertSame($client, $container->get('neo4j.client')); 53 | } 54 | 55 | public function testDriver(): void 56 | { 57 | static::bootKernel(); 58 | $container = static::getContainer(); 59 | 60 | $this->assertTrue($container->has('neo4j.driver')); 61 | $driver = $container->get('neo4j.driver'); 62 | $this->assertInstanceOf(DriverInterface::class, $driver); 63 | 64 | $this->assertTrue($container->has(DriverInterface::class)); 65 | $this->assertInstanceOf(DriverInterface::class, $driver); 66 | 67 | $this->assertSame($driver, $container->get('neo4j.driver')); 68 | } 69 | 70 | public function testDefaultDsn(): void 71 | { 72 | static::bootKernel(); 73 | $container = static::getContainer(); 74 | 75 | /** 76 | * @var ClientInterface $client 77 | */ 78 | $client = $container->get('neo4j.client'); 79 | /** 80 | * @var Neo4jDriver $driver 81 | */ 82 | $driver = $client->getDriver('default'); 83 | 84 | $driver = $this->getPrivateProperty($driver, 'driver'); 85 | $driver = $this->getPrivateProperty($driver, 'driver'); 86 | 87 | $uri = $this->getPrivateProperty($driver, 'parsedUrl'); 88 | 89 | $this->assertSame($uri->getScheme(), 'neo4j'); 90 | } 91 | 92 | public function testDsn(): void 93 | { 94 | static::bootKernel(); 95 | $container = static::getContainer(); 96 | 97 | $this->expectException(\RuntimeException::class); 98 | $this->expectExceptionMessageMatches( 99 | "/Cannot connect to any server on alias: neo4j_undefined_configs with Uris: \('bolt:\/\/(localhost|localhostt)'\)/" 100 | ); 101 | 102 | /** 103 | * @var ClientInterface $client 104 | */ 105 | $client = $container->get('neo4j.client'); 106 | $client->getDriver('neo4j_undefined_configs'); 107 | } 108 | 109 | public function testDriverAuthentication(): void 110 | { 111 | static::bootKernel(); 112 | $container = static::getContainer(); 113 | 114 | /** 115 | * @var ClientInterface $client 116 | */ 117 | $client = $container->get('neo4j.client'); 118 | /** @var Neo4jDriver $driver */ 119 | $driver = $client->getDriver('neo4j-auth'); 120 | 121 | $driver = $this->getPrivateProperty($driver, 'driver'); 122 | $driver = $this->getPrivateProperty($driver, 'driver'); 123 | 124 | /** @var Neo4jConnectionPool $pool */ 125 | $pool = $this->getPrivateProperty($driver, 'pool'); 126 | /** @var ConnectionRequestData $data */ 127 | $data = $this->getPrivateProperty($pool, 'data'); 128 | $auth = $data->getAuth(); 129 | /** @var string $username */ 130 | $username = $this->getPrivateProperty($auth, 'username'); 131 | /** @var string $password */ 132 | $password = $this->getPrivateProperty($auth, 'password'); 133 | 134 | $this->assertSame($username, 'neo4j'); 135 | $this->assertSame($password, 'testtest'); 136 | } 137 | 138 | public function testDefaultDriverConfig(): void 139 | { 140 | static::bootKernel(); 141 | $container = static::getContainer(); 142 | 143 | /** 144 | * @var ClientInterface $client 145 | */ 146 | $client = $container->get('neo4j.client'); 147 | /** @var Neo4jDriver $driver */ 148 | $driver = $client->getDriver('default'); 149 | 150 | $driver = $this->getPrivateProperty($driver, 'driver'); 151 | $driver = $this->getPrivateProperty($driver, 'driver'); 152 | 153 | /** @var Neo4jConnectionPool $pool */ 154 | $pool = $this->getPrivateProperty($driver, 'pool'); 155 | /** @var SingleThreadedSemaphore $semaphore */ 156 | $semaphore = $this->getPrivateProperty($pool, 'semaphore'); 157 | /** @var int $max */ 158 | $max = $this->getPrivateProperty($semaphore, 'max'); 159 | 160 | // default_driver_config.pool_size 161 | $this->assertSame($max, 256); 162 | 163 | /** @var ConnectionRequestData $data */ 164 | $data = $this->getPrivateProperty($pool, 'data'); 165 | 166 | $this->assertSame($data->getUserAgent(), 'Neo4j Symfony Bundle/testing'); 167 | 168 | /** @var SslConfiguration $sslConfig */ 169 | $sslConfig = $this->getPrivateProperty($data, 'config'); 170 | /** @var SslMode $sslMode */ 171 | $sslMode = $this->getPrivateProperty($sslConfig, 'mode'); 172 | /** @var bool $verifyPeer */ 173 | $verifyPeer = $this->getPrivateProperty($sslConfig, 'verifyPeer'); 174 | 175 | $this->assertSame($sslMode, SslMode::DISABLE()); 176 | $this->assertFalse($verifyPeer); 177 | } 178 | 179 | public function testDefaultSessionConfig(): void 180 | { 181 | static::bootKernel(); 182 | $container = static::getContainer(); 183 | 184 | /** 185 | * @var ClientInterface $client 186 | */ 187 | $client = $container->get('neo4j.client'); 188 | $sessionConfig = $client->getDefaultSessionConfiguration(); 189 | 190 | $this->assertSame($sessionConfig->getFetchSize(), 999); 191 | } 192 | 193 | public function testDefaultTransactionConfig(): void 194 | { 195 | static::bootKernel(); 196 | $container = static::getContainer(); 197 | 198 | /** 199 | * @var ClientInterface $client 200 | */ 201 | $client = $container->get('neo4j.client'); 202 | $transactionConfig = $client->getDefaultTransactionConfiguration(); 203 | 204 | $this->assertSame($transactionConfig->getTimeout(), 40.0); 205 | } 206 | 207 | public function testDriverAndSessionTags(): void 208 | { 209 | static::bootKernel(); 210 | $container = static::getContainer(); 211 | 212 | $this->assertTrue($container->has('neo4j.driver.neo4j-simple')); 213 | $this->assertTrue($container->has('neo4j.driver.neo4j-test')); 214 | 215 | $this->assertTrue($container->has('neo4j.session.neo4j-simple')); 216 | $this->assertTrue($container->has('neo4j.session.neo4j-test')); 217 | } 218 | 219 | public function testPriority(): void 220 | { 221 | static::bootKernel(); 222 | $container = static::getContainer(); 223 | 224 | /** 225 | * @var ClientInterface $client 226 | */ 227 | $client = $container->get('neo4j.client'); 228 | /** @var DriverSetupManager $drivers */ 229 | $drivers = $this->getPrivateProperty($client, 'driverSetups'); 230 | /** @var array<\SplPriorityQueue> $fallbackDriverQueue */ 231 | $driverSetups = $this->getPrivateProperty($drivers, 'driverSetups'); 232 | /** @var \SplPriorityQueue $fallbackDriverQueue */ 233 | $fallbackDriverQueue = $driverSetups['neo4j-fallback-mechanism']; 234 | $fallbackDriverQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); 235 | /** @var array{data: DriverSetup, priority: int} $extractedValue */ 236 | $extractedValue = $fallbackDriverQueue->extract(); 237 | 238 | $this->assertSame($extractedValue['priority'], 1000); 239 | } 240 | 241 | public function testDefaultLogLevel(): void 242 | { 243 | static::bootKernel(); 244 | $container = static::getContainer(); 245 | 246 | /** 247 | * @var SymfonyClient $client 248 | */ 249 | $client = $container->get('neo4j.client'); 250 | /** @var Neo4jDriver $driver */ 251 | $driver = $client->getDriver('default'); 252 | 253 | $driver = $this->getPrivateProperty($driver, 'driver'); 254 | $driver = $this->getPrivateProperty($driver, 'driver'); 255 | 256 | /** @var Neo4jConnectionPool $pool */ 257 | $pool = $this->getPrivateProperty($driver, 'pool'); 258 | $level = $pool->getLogger()?->getLevel(); 259 | 260 | $this->assertSame('warning', $level); 261 | } 262 | 263 | private function getPrivateProperty(object $object, string $property): mixed 264 | { 265 | $reflection = new \ReflectionClass($object); 266 | $property = $reflection->getProperty($property); 267 | 268 | return $property->getValue($object); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /tests/Functional/ProfilerTest.php: -------------------------------------------------------------------------------- 1 | enableProfiler(); 21 | 22 | $client->request('GET', '/client'); 23 | 24 | if ($profile = $client->getProfile()) { 25 | /** @var Neo4jDataCollector $collector */ 26 | $collector = $profile->getCollector('neo4j'); 27 | $this->assertEquals( 28 | 2, 29 | $collector->getQueryCount() 30 | ); 31 | $successfulStatements = $collector->getSuccessfulStatements(); 32 | $failedStatements = $collector->getFailedStatements(); 33 | $this->assertCount(1, $successfulStatements); 34 | $this->assertCount(1, $failedStatements); 35 | } 36 | } 37 | 38 | public function testProfilerOnSession(): void 39 | { 40 | $client = static::createClient(); 41 | $client->enableProfiler(); 42 | 43 | $client->request('GET', '/session'); 44 | 45 | if ($profile = $client->getProfile()) { 46 | /** @var Neo4jDataCollector $collector */ 47 | $collector = $profile->getCollector('neo4j'); 48 | $this->assertEquals( 49 | 2, 50 | $collector->getQueryCount() 51 | ); 52 | $successfulStatements = $collector->getSuccessfulStatements(); 53 | $failedStatements = $collector->getFailedStatements(); 54 | $this->assertCount(1, $successfulStatements); 55 | $this->assertCount(1, $failedStatements); 56 | } 57 | } 58 | 59 | public function testProfilerOnTransaction(): void 60 | { 61 | $client = static::createClient(); 62 | $client->enableProfiler(); 63 | 64 | $client->request('GET', '/transaction'); 65 | 66 | if ($profile = $client->getProfile()) { 67 | /** @var Neo4jDataCollector $collector */ 68 | $collector = $profile->getCollector('neo4j'); 69 | $this->assertEquals( 70 | 2, 71 | $collector->getQueryCount() 72 | ); 73 | $successfulStatements = $collector->getSuccessfulStatements(); 74 | $failedStatements = $collector->getFailedStatements(); 75 | $this->assertCount(1, $successfulStatements); 76 | $this->assertCount(1, $failedStatements); 77 | } 78 | } 79 | } 80 | --------------------------------------------------------------------------------