Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Behat Orm Context Laravel Package

macpaw/behat-orm-context

Behat ORM Context by MacPaw provides Doctrine ORM-aware Behat contexts for acceptance testing in PHP projects. It helps manage entities, fixtures, and database state, keeping scenarios isolated and readable while reducing boilerplate around test data setup and cleanup.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle that ships pre-built Behat contexts for Doctrine ORM work: steps to create/persist entities from Gherkin tables, look up the "current" entity, and assert what's in the database. The point is to stop hand-writing I have a User named "bob" with email "..." steps in every project.

Install and wire it up (all in require-dev):

composer require --dev macpaw/behat-orm-context:^2.0

Register the bundle in config/bundles.php for the test environment only — this must not load in prod:

return [
    // ...
    MacPaw\BehatOrmContext\BehatOrmContextBundle::class => ['test' => true],
];

Behat contexts are not autowired by Symfony by default. Unless the bundle ships its own Behat extension, you need friends-of-behat/symfony-extension so the kernel (and therefore the EntityManager) is available to your context:

# behat.yml
default:
  extensions:
    FriendsOfBehat\SymfonyExtension:
      bootstrap: tests/bootstrap.php
      kernel:
        class: App\Kernel
        environment: test
        debug: true
  suites:
    default:
      contexts:
        - MacPaw\BehatOrmContext\Context\OrmContext   # name it exactly as in src/
        - App\Tests\Behat\FeatureContext

v2.0.0 compatibility floor (check this first): PHP ^8.1, Symfony 6.4–8 (anything below 6.4 is unsupported), Doctrine ORM 2–3, Behat 3. If your app is still on Symfony 6.0–6.3 you cannot take this release at all — plan that upgrade first, or stay on 1.2.0 and pin it.

Good news for upgraders: the public API is unchanged in 2.0. Step definitions, the config surface, and --definitions output are identical to 1.x, so moving from 1.2 is a composer.json bump, not a feature-file rewrite. If you're on an older Symfony and want the wider Doctrine support, that's the real reason to chase the upgrade.

First thing to do after installing: run

vendor/bin/behat --definitions

That prints the authoritative list of steps the context registers. Given the package's adoption level (2 stars, 0 dependents) and thin public docs, --definitions plus vendor/macpaw/behat-orm-context/src/ is your real documentation. Read the context class top to bottom once — it's a small file, and you'll immediately see which method names and config keys exist.

First use case — a smoke scenario that proves the wiring works:

Feature: Users can be created

  Scenario: Persisting a user
    Given the following "User" entities exist:
      | name | email           |
      | bob  | [email protected] |
    Then there should be 1 "User" entity

If that runs green against a real test database, the container, the mapping config, and the EntityManager are all hooked up correctly. Do this before you port over any existing features.

Implementation Patterns

1. Entity name resolution is the core config decision. Most ORM contexts map a short Gherkin name ("User") to a FQCN, either via a bundle config map or via a convention over your Doctrine mapping. Decide this early: an explicit map is boring but survives refactors and namespace moves; convention-based lookup breaks silently the day you introduce App\Domain\User next to App\Legacy\User. Prefer the explicit map.

# illustrative — confirm key names against src/ before copying
macpaw_behat_orm_context:
  entities:
    User: App\Domain\User\User
    Order: App\Domain\Order\Order

2. Put domain-specific steps in your context, not theirs. The package gives you persistence and assertion primitives. The business vocabulary — "a user registered yesterday", "an order with two line items" — belongs in your FeatureContext or a UserSteps trait. Keep the distinction sharp: the bundle talks to Doctrine, your traits talk to your domain.

3. Share entity state via your FeatureContext, not across contexts. If a Given persists an entity and a later Then needs to check it through the HTTP/API layer, get the identifier out of the ORM context and into shared storage. Behat contexts are independent objects by default; a SharedStorage-style object injected into both is the clean way:

final class FeatureContext implements Context
{
    public function __construct(
        private SharedStorage $storage,
        private EntityManagerInterface $em,
    ) {}
}

4. Combine with Mink for real end-to-end. The typical split: ORM context creates fixtures and asserts DB state, Mink drives the browser, HTTP client context hits the API. Use ORM Givens to set up state (fast, precise), Mink to interact, and ORM Thens only for side effects the UI doesn't show (queue rows, audit log entries, denormalized counters).

5. Isolate scenarios with transactional rollback. If the context flushes to the database, scenarios will leak state into each other and you'll get order-dependent failures that take a day to diagnose. Either wrap every scenario in a transaction (dama/doctrine-test-bundle) or add an explicit purge in @BeforeScenario for the tables the context touches. Do this on day one, not when the flakiness starts.

6. In CI, keep the schema cheap. The bundle requires no services — its own tests mock the EntityManager — but your suite needs a real DB. SQLite in-memory plus a schema-created-once bootstrap is usually enough for ORM-heavy suites; use MySQL/Postgres if your queries use vendor-specific SQL.

7. Table transforms are your friend. Gherkin tables are strings; entities have types, enums, DateTimeImmutable, relations. Write a small normalizer (or a TransformArgument context) that converts "now", "+1 day", enum names, and foreign-key-by-name lookups into real values, so feature files stay readable and step methods stay simple.

8. Target a Doctrine version deliberately, then test the pair you actually ship. 2.0 supports ORM 2 and 3 and DBAL 2 and 4 — the JSON-column handling is the piece that was made version-aware internally. That breadth is convenient, but it means bugs can be version-specific. Pin both the bundle and your ORM in composer.json, and if your suite exercises JSON columns or typed fields through the context, add a job or a documented note for the ORM major you run in CI, so a future bump doesn't surprise you.

Gotchas and Tips

  • v2.0.0 drops everything below Symfony 6.4. The floor didn't move in this release (it was already 6.4 in 1.2), but 2.0 makes the ceiling explicit and much higher: Symfony 8 is now supported, along with Doctrine ORM 3 and DBAL 4. That removes the most common "can't adopt" objection — but only if your test stack can follow. Check friends-of-behat/symfony-extension's supported Symfony range before you celebrate; on Symfony 7/8 that extension is far more likely to be your blocker than this bundle.
  • Doctrine ORM 3 changes are yours to worry about, not the bundle's. If you subclass the shipped context and override persist/lookup methods, audit your overrides for ORM 3 removals (annotations gone in favour of attributes, removed DBAL 3-era execute helpers, proxy behaviour). The bundle's own code is compatible; your FeatureContext traits are not automatically.
  • PHP ^8.1 floor, and the package pins config.platform.php to 8.1.0 with composer.lock gitignored. The dev tooling now reflects that: PHPUnit ^10.5 || ^11.0 (9 dropped) where 11 needs PHP 8.2, PHPCS ^4.0, Rector ^1 || ^2, Slevomat ^8, PHPStan ^2 with phpstan-doctrine ^2. Useful signal — static analysis now understands Doctrine metadata, and the run-phpunit.sh wrapper always passes PHPUnit 10+ runner-warning flags, so upstream CI is strict. It also means their CI never resolves against your PHP 8.3 lockfile. If you hit a resolution conflict, composer why-not is your first command.
  • Step definition collisions are the #1 startup failure. Behat throws on duplicate step text. Run vendor/bin/behat --definitions -vvv and grep for your existing custom steps before registering the context. Common clashes: the following ... exist, there should be N ....
  • Contexts need the container. If you see "Cannot autowire" or a null EntityManager, the FriendsOfBehat\SymfonyExtension isn't bootstrapping the kernel in the test environment, or the context is listed in behat.yml but not registered as a service in services_test.yaml. Behat's own exception messages here are unhelpful — check the extension config first.
  • flush() vs. transactions. If the context flushes per step, @BeforeScenario transaction rollback from dama/doctrine-test-bundle still works, but any test that asserts on auto-increment IDs will be brittle. Assert on business keys (email, slug, reference) rather than IDs.
  • Silent no-op persistence. A missing cascade: persist or a mis-typed entity name in the config map can produce a step that "succeeds" but writes nothing. When a Then fails unexpectedly, put die() (or a dump()) in the persist step once to confirm it's actually reaching Doctrine — faster than reading stack traces.
  • Debug loop: --dry-run to check step matching without executing, --append-snippets to generate missing definitions, --strict to turn undefined/pending steps into failures (you want this in CI), -vvv for the kernel boot and query log.
  • Extension points: the useful one is subclassing the shipped context and overriding the persist/lookup methods to add your own field normalization or ID strategy. Register your subclass in behat.yml instead of the vendor class — you keep upstream fixes for the step definitions and only own the parts you changed. Avoid editing vendor/.
  • Be honest about the trade-off. With 0 dependents and 2 stars, you are still the de facto maintainer of your integration. But 2.0 is a genuinely reassuring release: it widens to the current Symfony/Doctrine/Behat majors without touching the public API, and it moved its whole dev toolchain (PHPUnit 10/11, PHPCS 4, Rector 2, PHPStan 2 + phpstan-doctrine) to current majors rather than letting it rot. That's the behaviour of a maintained package. Still budget an afternoon to read src/ end to end, pin an exact version, and be prepared to drop the bundle for a ~150-line custom trait if it fights your domain model. The primitives are simple enough that replacing it is cheap; the value is in not writing them yourself on day one.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect