diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..8ccdf51
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,98 @@
+name: CI
+
+on:
+ pull_request: ~
+
+jobs:
+ run:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ php:
+ - '8.1'
+ - '8.2'
+ - '8.3'
+ coverage: ['none']
+ symfony-versions:
+ - '4.4.*'
+ - '5.4.*'
+ - '6.0.*'
+ - '6.2.*'
+ - '7.0.*'
+ exclude:
+ - php: '8.1'
+ symfony-versions: '7.0.*'
+ include:
+ - php: '7.4'
+ symfony-versions: '^4.4'
+ coverage: 'none'
+ - php: '7.4'
+ symfony-versions: '^5.4'
+ coverage: 'none'
+ - php: '8.0'
+ symfony-versions: '^4.4'
+ coverage: 'none'
+ - php: '8.0'
+ symfony-versions: '^5.4'
+ coverage: 'none'
+ - php: '8.2'
+ coverage: 'xdebug'
+ symfony-versions: '^7.0'
+ description: 'Log Code Coverage'
+
+ name: PHP ${{ matrix.php }} Symfony ${{ matrix.symfony-versions }} ${{ matrix.description }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - uses: actions/cache@v4
+ with:
+ path: ~/.composer/cache/files
+ key: ${{ matrix.php }}-${{ matrix.symfony-versions }}
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ coverage: ${{ matrix.coverage }}
+
+ - name: Add PHPUnit matcher
+ run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
+
+ - name: Set composer cache directory
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
+
+ - name: Cache composer
+ uses: actions/cache@v4
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.symfony-versions }}-composer-${{ hashFiles('composer.json') }}
+ restore-keys: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.symfony-versions }}-composer
+
+ - name: Update Symfony version
+ if: matrix.symfony-versions != ''
+ run: |
+ composer require symfony/config:${{ matrix.symfony-versions }} --no-update --no-scripts
+ composer require symfony/dependency-injection:${{ matrix.symfony-versions }} --no-update --no-scripts
+ composer require symfony/http-kernel:${{ matrix.symfony-versions }} --no-update --no-scripts
+
+ - name: Install dependencies
+ run: composer install
+
+ - name: Run PHPUnit tests
+ run: composer phpunit
+ if: matrix.coverage == 'none'
+
+ - name: PHPUnit tests and Log Code coverage
+ run: vendor/bin/phpunit --coverage-clover=coverage.xml
+ if: matrix.coverage == 'xdebug'
+
+ - name: Run codecov
+ uses: codecov/codecov-action@v4.0.1
+ if: matrix.coverage == 'xdebug'
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: './coverage.xml'
+ fail_ci_if_error: true
diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml
new file mode 100644
index 0000000..248d5d0
--- /dev/null
+++ b/.github/workflows/security.yaml
@@ -0,0 +1,24 @@
+on:
+ pull_request:
+ push:
+ branches: [ main, develop ]
+
+jobs:
+ security-checker:
+ name: Security checker
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+
+ - name: Install dependencies
+ run: composer install --no-progress --no-interaction --prefer-dist
+
+ - name: Download local-php-security-checker
+ run: curl -s -L -o local-php-security-checker https://github.com/fabpot/local-php-security-checker/releases/download/v1.0.0/local-php-security-checker_1.0.0_linux_amd64
+
+ - name: Run local-php-security-checker
+ run: chmod +x local-php-security-checker && ./local-php-security-checker
diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml
new file mode 100644
index 0000000..c54cc67
--- /dev/null
+++ b/.github/workflows/static-analysis.yaml
@@ -0,0 +1,55 @@
+name: Code style and static analysis
+
+on:
+ pull_request:
+ push:
+ branches: [ main, develop ]
+
+jobs:
+ php-cs-fixer:
+ name: PHP-CS-Fixer
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+
+ - name: Install dependencies
+ run: composer install --no-progress --no-interaction --prefer-dist
+
+ - name: Run script
+ run: composer code-style
+
+ phpstan:
+ name: PHPStan
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+
+ - name: Install dependencies
+ run: composer install --no-progress --no-interaction --prefer-dist
+
+ - name: Run script
+ run: composer phpstan
+
+ composer-validate:
+ name: Composer validate
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+
+ - name: Install dependencies
+ run: composer install --no-progress --no-interaction --prefer-dist
+
+ - name: Run script
+ run: composer composer-validate
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1d073d6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/.idea/
+/vendor/
+/composer.lock
+.DS_Store
+/.phpunit.result.cache
diff --git a/.infrastructure/docker/Dockerfile b/.infrastructure/docker/Dockerfile
new file mode 100644
index 0000000..22e32ad
--- /dev/null
+++ b/.infrastructure/docker/Dockerfile
@@ -0,0 +1,10 @@
+ARG from_image
+
+FROM $from_image
+
+ENV XDEBUG_MODE=off
+
+RUN apk add --no-cache bash linux-headers $PHPIZE_DEPS \
+ && pecl install xdebug \
+ && docker-php-ext-enable xdebug \
+ && curl --silent https://getcomposer.org/composer-stable.phar -o /usr/bin/composer && chmod a+x /usr/bin/composer
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..f96e33e
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+yozhef@macpaw.com.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7aab1b6
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 MacPaw Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 25a9e6d..d47c4bc 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,38 @@
-# behat-orm-context
-Behat ORM context Bundle
+# Behat ORM Context Bundle
+
+| Version | Build Status | Code Coverage |
+|:---------:|:---------------------------------------------------------:|:------------------------------------------------------------------------:|
+| `master` | [![CI][master Build Status Image]][master Build Status] | [![Coverage Status][master Code Coverage Image]][master Code Coverage] |
+| `develop` | [![CI][develop Build Status Image]][develop Build Status] | [![Coverage Status][develop Code Coverage Image]][develop Code Coverage] |
+
+Behat context for testing Doctrine ORM integration.
+
+## Description
+
+This bundle provides a Behat context for testing your application's interaction with a database using Doctrine ORM.
+
+## Installation
+
+See the [installation instructions](docs/install.md).
+
+## Features
+
+The bundle provides several Behat step definitions for ORM testing:
+
+* [See X entities in repository](docs/ORMContext/see-count-in-repository.md) - Check if the count of entities in the repository matches expected
+* [See entity with ID](docs/ORMContext/see-entity-in-repository-with-id.md) - Check if an entity with a specific ID exists
+* [See entity with properties](docs/ORMContext/see-entity-in-repository-with-properties.md) - Check if an entity with specific properties exists
+
+## License
+
+This bundle is released under the MIT license. See the included [LICENSE](LICENSE) file for more information.
+
+[master Build Status]: https://github.com/macpaw/behat-orm-context/actions?query=workflow%3ACI+branch%3Amaster
+[master Build Status Image]: https://github.com/macpaw/behat-orm-context/workflows/CI/badge.svg?branch=master
+[develop Build Status]: https://github.com/macpaw/behat-orm-context/actions?query=workflow%3ACI+branch%3Adevelop
+[develop Build Status Image]: https://github.com/macpaw/behat-orm-context/workflows/CI/badge.svg?branch=develop
+[master Code Coverage]: https://codecov.io/gh/macpaw/behat-orm-context/branch/master
+[master Code Coverage Image]: https://img.shields.io/codecov/c/github/macpaw/behat-orm-context/master?logo=codecov
+[develop Code Coverage]: https://codecov.io/gh/macpaw/behat-orm-context/branch/develop
+[develop Code Coverage Image]: https://img.shields.io/codecov/c/github/macpaw/behat-orm-context/develop?logo=codecov
+
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..8d26322
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,19 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | ------------------ |
+| 0.x.x | :white_check_mark: |
+| 1.x.x | :white_check_mark: |
+
+## Reporting a Bug
+
+Report security bugs by emailing the lead maintainer at yozhef@macpaw.com or create [Issues](https://github.com/MacPaw/BehatMessengerContext/issues)
+
+## Reporting a Vulnerability
+
+Please report (suspected) security vulnerabilities to
+**[yozhef@macpaw.com](mailto:yozhef@macpaw.com)**. You will receive a response from
+us within 48 hours. If the issue is confirmed, we will release a patch as soon
+as possible depending on complexity but historically within a few days.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..8967c9e
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,79 @@
+{
+ "name": "macpaw/behat-orm-context",
+ "description": "Behat Context for testing Symfony ORM",
+ "type": "symfony-bundle",
+ "license": "MIT",
+ "keywords": [
+ "MacPaw",
+ "symfony",
+ "behat",
+ "BDD",
+ "Context",
+ "ORM"
+ ],
+ "authors": [
+ {
+ "name": "Vladislav Hanziuk",
+ "email": "ganzyukv@macpaw.com",
+ "homepage": "https://macpaw.com/",
+ "role": "Software Engineer"
+ },
+ {
+ "name": "Serhii Donii",
+ "email": "serhii.donii@macpaw.com",
+ "homepage": "https://macpaw.com/",
+ "role": "Software Engineer"
+ },
+ {
+ "name": "Yozhef Hisem",
+ "email": "hisemjo@gmail.com",
+ "homepage": "https://macpaw.com/",
+ "role": "Software Engineer"
+ }
+ ],
+ "require": {
+ "ext-json": "*",
+ "php": "^7.4 || ^8.0",
+ "behat/behat": "^3.0",
+ "symfony/config": "^4.4 || ^5.4 || ^6.0 || ^7.0",
+ "symfony/dependency-injection": "^4.4 || ^5.4.34 || ^6.0 || ^7.0.2",
+ "symfony/http-kernel": "^4.4 || ^5.4 || ^6.0 || ^7.0",
+ "macpaw/similar-arrays": "^1.0",
+ "doctrine/orm": "^2.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.12",
+ "phpunit/phpunit": "^9.6",
+ "slevomat/coding-standard": "^7.2",
+ "squizlabs/php_codesniffer": "^3.12"
+ },
+ "autoload": {
+ "psr-4": {
+ "BehatOrmContext\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "BehatOrmContext\\Tests\\": "tests"
+ }
+ },
+ "scripts": {
+ "composer-validate": "composer validate",
+ "phpstan": "./vendor/bin/phpstan analyse -l max",
+ "code-style": "./vendor/bin/phpcs",
+ "code-style-fix": "./vendor/bin/phpcbf",
+ "phpunit": "./vendor/bin/phpunit",
+ "phpunit-html-coverage": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html=coverage",
+ "dev-checks": [
+ "composer validate",
+ "@phpstan",
+ "@code-style",
+ "@phpunit"
+ ]
+ },
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
+ }
+}
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..cf844e5
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,14 @@
+version: '3.9'
+
+services:
+ php80:
+ build:
+ context: .infrastructure
+ dockerfile: docker/Dockerfile
+ args:
+ from_image: php:8.0-fpm-alpine
+ working_dir: /app
+ environment:
+ PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/app/vendor/bin"
+ volumes:
+ - ./:/app
diff --git a/docs/ORMContext/see-count-in-repository.md b/docs/ORMContext/see-count-in-repository.md
new file mode 100644
index 0000000..0db7c27
--- /dev/null
+++ b/docs/ORMContext/see-count-in-repository.md
@@ -0,0 +1,26 @@
+### I See X Entities in Repository
+
+#### Step Definition:
+
+This step checks if the database contains exactly the specified number of entities for a given entity class.
+
+#### Gherkin Example:
+
+```gherkin
+And I see 5 entities "App\Entity\User"
+```
+
+#### Description:
+
+This step allows you to verify that a specific number of entities exist in the database. It executes a count query on the database for the specified entity class and verifies that the result matches the expected count.
+
+#### Parameters:
+
+- `count`: The expected number of entities
+- `entityClass`: The fully-qualified class name of the entity to check
+
+#### Use Cases:
+
+- Verifying that data setup was successful
+- Checking that operations have created/deleted the expected number of records
+- Validating database state after data manipulation steps
\ No newline at end of file
diff --git a/docs/ORMContext/see-entity-in-repository-with-id.md b/docs/ORMContext/see-entity-in-repository-with-id.md
new file mode 100644
index 0000000..2b88155
--- /dev/null
+++ b/docs/ORMContext/see-entity-in-repository-with-id.md
@@ -0,0 +1,26 @@
+### I See Entity with Specific ID
+
+#### Step Definition:
+
+This step checks if the database contains an entity of a specific type with the given ID.
+
+#### Gherkin Example:
+
+```gherkin
+And I see entity "App\Entity\Product" with id "abc123"
+```
+
+#### Description:
+
+This step allows you to verify that a specific entity exists in the database by its ID. It performs a count query with a condition on the ID field and expects exactly one matching entity.
+
+#### Parameters:
+
+- `entityClass`: The fully-qualified class name of the entity to check
+- `id`: The ID value to match against the entity's ID field
+
+#### Use Cases:
+
+- Verifying that a specific record exists after creation
+- Confirming entity persistence during a multi-step process
+- Checking that an entity with a specific identifier is still present
\ No newline at end of file
diff --git a/docs/ORMContext/see-entity-in-repository-with-properties.md b/docs/ORMContext/see-entity-in-repository-with-properties.md
new file mode 100644
index 0000000..1187bb2
--- /dev/null
+++ b/docs/ORMContext/see-entity-in-repository-with-properties.md
@@ -0,0 +1,40 @@
+### I See Entity with Specific Properties
+
+#### Step Definition:
+
+This step checks if the database contains an entity of a specific type with the given properties.
+
+#### Gherkin Example:
+
+```gherkin
+Then I see entity "App\Entity\User" with properties:
+ """
+ {
+ "email": "user@example.com",
+ "firstName": "John",
+ "lastName": "Doe",
+ "active": true
+ }
+ """
+```
+
+#### Description:
+
+This step allows you to verify that a specific entity exists in the database by matching multiple property values. It performs a count query with conditions on each specified property and expects exactly one matching entity.
+
+#### Parameters:
+
+- `entityClass`: The fully-qualified class name of the entity to check
+- `properties`: A JSON string containing key-value pairs representing entity properties to match
+
+#### Use Cases:
+
+- Verifying that an entity with specific field values exists
+- Checking that entity properties match expected values after operations
+- Validating complex entity state with multiple property conditions
+- Testing business logic that modifies entity properties
+
+#### Notes:
+
+- Properties with `null` values are queried using `IS NULL` condition
+- All other properties are matched using equality
\ No newline at end of file
diff --git a/docs/install.md b/docs/install.md
new file mode 100644
index 0000000..ee3e510
--- /dev/null
+++ b/docs/install.md
@@ -0,0 +1,70 @@
+# Installation
+
+## Step 1: Download the Bundle
+
+Open a command console, enter your project directory and execute:
+
+### Applications that use Symfony Flex
+
+```bash
+ composer require --dev macpaw/behat-orm-context
+```
+
+### Applications that don't use Symfony Flex
+
+Open a command console, enter your project directory and execute the following command to download the latest stable
+version of this bundle:
+
+```bash
+ composer require --dev macpaw/behat-orm-context
+```
+
+This command requires you to have Composer installed globally, as explained
+in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
+of the Composer documentation.
+
+Then, enable the bundle by adding it to the list of registered bundles
+in the `app/AppKernel.php` file of your project:
+
+```php
+ ['test' => true],
+ );
+
+ // ...
+ }
+
+ // ...
+}
+```
+
+## Step 2: Configure Behat
+
+Go to `behat.yml`:
+
+```yaml
+# ...
+contexts:
+ - BehatOrmContext\Context\OrmContext
+# ...
+```
+
+## Configuration
+
+By default, the bundle has the following configuration:
+
+```yaml
+behat_orm_context:
+ # Currently no specific configuration options are available
+```
+
+You can override it manually in your `config/packages/test/behat_orm_context.yaml`.
\ No newline at end of file
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..a3ac003
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,45 @@
+
+
+ The coding standard of Behat ORM Context package
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ src/
+ tests/
+
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..fbb2748
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,6 @@
+parameters:
+ excludes_analyse:
+ paths:
+ - src
+ level: max
+ checkExplicitMixed: false
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..9aacc0e
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,25 @@
+
+
+
+
+ tests
+
+
+
+
+ ./src
+
+
+
+
+
+
diff --git a/src/BehatOrmContextBundle.php b/src/BehatOrmContextBundle.php
new file mode 100644
index 0000000..4529143
--- /dev/null
+++ b/src/BehatOrmContextBundle.php
@@ -0,0 +1,11 @@
+manager = $manager;
+ }
+
+ /**
+ * @And I see :count entities :entityClass
+ */
+ public function andISeeInRepository(int $count, string $entityClass): void
+ {
+ $this->seeInRepository($count, $entityClass);
+ }
+
+ /**
+ * @Then I see :count entities :entityClass
+ */
+ public function thenISeeInRepository(int $count, string $entityClass): void
+ {
+ $this->seeInRepository($count, $entityClass);
+ }
+
+ /**
+ * @And I see entity :entity with id :id
+ */
+ public function andISeeEntityInRepositoryWithId(string $entityClass, string $id): void
+ {
+ $this->seeInRepository(1, $entityClass, ['id' => $id]);
+ }
+
+ /**
+ * @Then I see entity :entity with id :id
+ */
+ public function thenISeeEntityInRepositoryWithId(string $entityClass, string $id): void
+ {
+ $this->seeInRepository(1, $entityClass, ['id' => $id]);
+ }
+
+ /**
+ * @Then I see entity :entity with properties:
+ */
+ public function andISeeEntityInRepositoryWithProperties(string $entityClass, PyStringNode $string): void
+ {
+ $expectedProperties = json_decode(trim($string->getRaw()), true, 512, JSON_THROW_ON_ERROR);
+ $this->seeInRepository(1, $entityClass, $expectedProperties);
+ }
+
+ /**
+ * @param array $params
+ *
+ * @throws NonUniqueResultException
+ * @throws NoResultException
+ */
+ private function seeInRepository(int $count, string $entityClass, ?array $params = null): void
+ {
+ $query = $this->manager->createQueryBuilder()
+ ->from($entityClass, 'e')
+ ->select('count(e)');
+
+ if (null !== $params) {
+ foreach ($params as $columnName => $columnValue) {
+ if ($columnValue === null) {
+ $query->andWhere(sprintf('e.%s IS NULL', $columnName));
+ } else {
+ $query->andWhere(sprintf('e.%s = :%s', $columnName, $columnName))
+ ->setParameter($columnName, $columnValue);
+ }
+ }
+ }
+
+ $realCount = $query->getQuery()
+ ->getSingleScalarResult();
+
+ if ($count !== $realCount) {
+ throw new RuntimeException(
+ sprintf('Real count is %d, not %d', $realCount, $count),
+ );
+ }
+ }
+}
diff --git a/src/DependencyInjection/BehatOrmContextExtension.php b/src/DependencyInjection/BehatOrmContextExtension.php
new file mode 100644
index 0000000..5cfb72c
--- /dev/null
+++ b/src/DependencyInjection/BehatOrmContextExtension.php
@@ -0,0 +1,24 @@
+> $configs
+ *
+ * {@inheritdoc}
+ */
+ public function load(array $configs, ContainerBuilder $container): void
+ {
+ $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
+ $loader->load('orm_context.xml');
+ }
+}
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
new file mode 100644
index 0000000..c0f9c7b
--- /dev/null
+++ b/src/DependencyInjection/Configuration.php
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/tests/DependencyInjection/BehatOrmContextExtensionTest.php b/tests/DependencyInjection/BehatOrmContextExtensionTest.php
new file mode 100644
index 0000000..2fb15dd
--- /dev/null
+++ b/tests/DependencyInjection/BehatOrmContextExtensionTest.php
@@ -0,0 +1,35 @@
+load([], $container);
+
+ self::assertInstanceOf(Extension::class, $extension);
+
+ self::assertTrue($container->has(OrmContext::class));
+ }
+
+ public function testOrmContextIsCorrectlyDefined(): void
+ {
+ $extension = new BehatOrmContextExtension();
+ $container = new ContainerBuilder();
+ $extension->load([], $container);
+
+ $definition = $container->getDefinition(OrmContext::class);
+ self::assertSame(OrmContext::class, $definition->getClass());
+ }
+}
diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php
new file mode 100644
index 0000000..298dded
--- /dev/null
+++ b/tests/DependencyInjection/ConfigurationTest.php
@@ -0,0 +1,25 @@
+processConfiguration($configuration, []);
+
+ self::assertSame([], $configs);
+ }
+}
diff --git a/tests/Unit/Context/ORMContextTest.php b/tests/Unit/Context/ORMContextTest.php
new file mode 100644
index 0000000..f721a9d
--- /dev/null
+++ b/tests/Unit/Context/ORMContextTest.php
@@ -0,0 +1,177 @@
+createContext('App\Entity\SomeEntity', self::COUNT);
+ $context->andISeeInRepository(self::COUNT, 'App\Entity\SomeEntity');
+ }
+
+ public function testAndISeeCountInRepositoryFailed(): void
+ {
+ $context = $this->createContext('App\Entity\SomeEntity', self::COUNT);
+ self::expectException(RuntimeException::class);
+ $context->andISeeInRepository(self::COUNT + 1, 'App\Entity\SomeEntity');
+ }
+
+ public function testThenISeeCountInRepository(): void
+ {
+ $context = $this->createContext('App\Entity\SomeEntity', self::COUNT);
+ $context->thenISeeInRepository(self::COUNT, 'App\Entity\SomeEntity');
+ }
+
+ public function testThenISeeCountInRepositoryFailed(): void
+ {
+ $context = $this->createContext('App\Entity\SomeEntity', self::COUNT);
+ self::expectException(RuntimeException::class);
+ $context->thenISeeInRepository(self::COUNT + 1, 'App\Entity\SomeEntity');
+ }
+
+ public function testThenISeeCountInRepositoryWithId(): void
+ {
+ $context = $this->createContext(
+ 'App\Entity\SomeEntity',
+ 1,
+ ['id' => self::UUID],
+ );
+ $context->thenISeeEntityInRepositoryWithId(
+ 'App\Entity\SomeEntity',
+ self::UUID,
+ );
+ }
+
+ public function testThenISeeCountInRepositoryWithIdFailed(): void
+ {
+ $context = $this->createContext(
+ 'App\Entity\SomeEntity',
+ 1,
+ ['id' => self::UUID],
+ );
+ $context->andISeeEntityInRepositoryWithId(
+ 'App\Entity\SomeEntity',
+ self::UUID,
+ );
+ }
+
+ public function testThenISeeEntityInRepositoryWithProperties(): void
+ {
+ $context = $this->createContext(
+ 'App\Entity\SomeEntity',
+ 1,
+ [
+ 'id' => self::UUID,
+ 'someProperty' => 'someValue',
+ 'otherProperty' => 'otherValue',
+ ],
+ );
+ $context->andISeeEntityInRepositoryWithProperties(
+ 'App\Entity\SomeEntity',
+ new PyStringNode([
+ <<<'PSN'
+ {
+ "id": "e809639f-011a-4ae0-9ae3-8fcb460fe950",
+ "someProperty": "someValue",
+ "otherProperty": "otherValue"
+ }
+ PSN
+ ], 1),
+ );
+ }
+
+ public function testThenISeeEntityInRepositoryWithPropertyNull(): void
+ {
+ $context = $this->createContext(
+ 'App\Entity\SomeEntity',
+ 1,
+ [
+ 'id' => self::UUID,
+ 'someProperty' => null,
+ ],
+ );
+ $context->andISeeEntityInRepositoryWithProperties(
+ 'App\Entity\SomeEntity',
+ new PyStringNode([
+ <<<'PSN'
+ {
+ "id": "e809639f-011a-4ae0-9ae3-8fcb460fe950",
+ "someProperty": null
+ }
+ PSN
+ ], 1),
+ );
+ }
+
+ private function createContext(
+ string $entityName,
+ int $count = 1,
+ ?array $properties = null
+ ): ORMContext {
+ $queryMock = $this->getMockBuilder(Query::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $queryMock->expects(self::once())
+ ->method('getSingleScalarResult')
+ ->willReturn($count);
+
+ $entityManagerMock = $this->getMockBuilder(EntityManagerInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $queryBuilderMock = $this->getMockBuilder(QueryBuilder::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $queryBuilderMock->expects(self::once())
+ ->method('from')
+ ->with(
+ $entityName,
+ 'e',
+ )->willReturn($queryBuilderMock);
+
+ if (null !== $properties) {
+ foreach ($properties as $name => $value) {
+ $queryBuilderMock->expects(self::exactly(count($properties)))
+ ->method('andWhere')
+ ->willReturnSelf();
+ $setParametersCount = count(array_filter($properties, function ($value) {
+ return !is_null($value);
+ }));
+ $queryBuilderMock->expects(self::exactly($setParametersCount))
+ ->method('setParameter')
+ ->willReturnSelf();
+ }
+ }
+
+ $queryBuilderMock->expects(self::once())
+ ->method('select')
+ ->with('count(e)')
+ ->willReturn($queryBuilderMock);
+
+ $queryBuilderMock->expects(self::once())
+ ->method('getQuery')
+ ->willReturn($queryMock);
+
+ $entityManagerMock->expects(self::once())
+ ->method('createQueryBuilder')
+ ->willReturn($queryBuilderMock);
+
+ return new ORMContext($entityManagerMock);
+ }
+}