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

Module Symfony Laravel Package

codeception/module-symfony

Codeception module for Symfony: boots the kernel in functional tests, exposes the service container and Symfony services, and integrates Doctrine data cleanup, mailer and profiler assertions so you can write expressive end-to-end tests. Official module maintained by the Codeception team.

View on GitHub
Deep Wiki
Context7

Getting Started

Verdict up front: this is the module you reach for when you want functional acceptance tests against a real Symfony kernel — forms with CSRF, security voters, Doctrine, Mailer, Messenger, the profiler — without standing up HTTP infrastructure. If you only need HTTP-level smoke tests, plain PhpBrowser/REST is lighter. If you need "did the app do the right thing internally," this is the tool.

Install (Codeception 5 / Symfony 5.4–8.1, PHP ^8.2):

composer require --dev codeception/module-symfony codeception/codeception
vendor/bin/codecept init acceptance   # or bootstrap manually

Minimal tests/Acceptance.suite.yml:

actor: AcceptanceTester
modules:
  enabled:
    - Symfony:
        app_path: 'src'        # 'app' is the Symfony 2/3 default — wrong for modern apps
        environment: 'test'
    - Doctrine2:
        depends: Symfony       # REQUIRED: Doctrine2 borrows the kernel from Symfony
        cleanup: true
    - \Helper\Acceptance

Then vendor/bin/codecept build to regenerate the Actor with the module's methods on it.

Where to look first:

  1. tests/Support/AcceptanceTester.php — after build, autocompletion shows every method the module gives you. This is your real documentation surface.
  2. The module README's method list, grouped by concern: Forms, Doctrine, Mailer, Session, Security, Events, Twig, Messenger, Console.
  3. Your config/packages/test/*.yaml — half of the module's power is gated on framework.test: true, framework.profiler, and a mailer transport set to null:///test.

First use case — a login + form submission + DB assertion:

class RegistrationCest
{
    public function registerANewUser(AcceptanceTester $I): void
    {
        $I->amOnRoute('app_register');                    // route-aware, no hardcoded URLs
        $I->submitSymfonyForm('registration_form', [      // uses the real Symfony Form, CSRF intact
            'registration_form[email]'    => '[email protected]',
            'registration_form[plainPassword]' => 's3cret!',
        ]);

        $I->seeResponseCodeIs(302);
        $I->seePageRedirectsTo('/dashboard');
        $I->seeFormHasErrors(false);

        $user = $I->grabEntityFromRepository(User::class, ['email' => '[email protected]']);
        $I->assertSame('ROLE_USER', $user->getRoles()[0]);

        $I->seeEmailIsSent(1);
        $I->assertEmailSubjectContains($I->grabLastSentEmail(), 'Welcome');
    }
}

That single test exercises routing, form validation, CSRF, Doctrine persistence, and transactional email — which is exactly the day-to-day value proposition.

Implementation Patterns

1. Forms: use submitSymfonyForm, not submitForm

submitSymfonyForm('form_name', $values) goes through FormInterface, so CSRF tokens, name-prefixed field keys, and data transformers behave like production. Raw $I->submitForm() (InnerBrowser) posts raw arrays and will fail on any CSRF-protected form. Follow with targeted assertions rather than DOM scraping:

$I->submitSymfonyForm('checkout', $payload);
$I->seeFormHasErrors(true);
$I->seeFormErrorMessage('email', 'This value is not a valid email address.');
$I->dontSeeFormErrors();
$I->seeFormErrorMessages(['email' => '...', 'zip' => '...']); // plural form for multi-field checks

2. Data setup: Doctrine2 module, not fixtures

Configure Doctrine2 with depends: Symfony and cleanup: true, then seed per-test:

$I->haveInRepository(Product::class, ['sku' => 'ABC', 'price' => 999]);
$I->seeNumRecords(1, Product::class, ['sku' => 'ABC']);
$product = $I->grabEntityFromRepository(Product::class, ['sku' => 'ABC']);
$repo = $I->grabRepository(Product::class);

Prefer in-test seeding over shared fixture files — it keeps each Cest self-describing and avoids ordering dependencies. Keep one tests/_data fixture set only for genuinely global data (roles, reference tables).

3. Services: mock what's external, grab what's internal

Anything crossing the process boundary (HTTP clients, payment SDKs, mailers, clocks) should be swapped, and the swap must survive the kernel reboot between requests. Newer releases give you exactly that:

$I->mockService('http_client', new MockHttpClient($responses));  // survives reboots
$I->amOnPage('/checkout');
// ...
$I->unmockService('http_client');

$container = $I->grabContainer();                 // test container: private services visible
$clock     = $I->grabService('clock');            // via the module's service helper

For anything the DI container doesn't own, persistPermanentService() pins an object across requests; use it sparingly — it's the escape hatch, not the pattern.

4. Authentication: assert, don't UI-login every time

A full login flow through the form is slow. For Cests that don't test login itself:

  • Log the user in programmatically (the kernel's loginUser(), or a session/token injected via a persisted service), then
  • assert with $I->seeAuthentication(), $I->dontSeeAuthentication(), $I->seeUserHasRoles(['ROLE_ADMIN']), and for "remember me" the dedicated seeRememberedAuthentication() / dontSeeRememberedAuthentication() (these also verify the remember-me cookie, not just the token).

Teardown helpers: $I->logout() (alias of logoutProgrammatically()), $I->goToLogoutPath() when you want to test the real logout route.

5. Authorization: run the real voters

seeUserIsGranted('EDIT', $post) executes the app's own voters with a subject — this catches the class of bug where a controller forgets denyAccessUnlessGranted() without you having to reverse-engineer HTTP responses.

$I->seeUserIsGranted('EDIT', $post);
$I->dontSeeUserIsGranted('DELETE', $post);

6. Mail: assertion families, and the RawMessage migration

Grab-then-assert is the workflow:

$I->seeEmailIsSent(2);
$email = $I->grabLastSentEmail();                 // declared ?RawMessage since 3.10.0
$I->assertEmailAddressContains($email, 'to', '[email protected]');
$I->assertEmailSubjectContains($email, 'Welcome');
$I->assertEmailHtmlBodyContains($email, 'Confirm your account');
$I->assertEmailAttachmentCount($email, 1);

If you need Email-specific accessors (getTo(), getSubject()), narrow explicitly: assert($email instanceof Email);. Better: migrate to the purpose-built assertEmail* methods, which is why they were added.

7. Async/Messenger: assert the queue, then consume in-process

$I->seeMessageDispatched(SendInvoiceEmail::class);           // count / class-level
$I->seeDispatchedMessageCount(SendInvoiceEmail::class, 1);
$I->seeMessengerTransportContains('async', SendInvoiceEmail::class);
$I->seeMessengerQueueCount(1, 'async');
$I->consumeMessengerMessages('async');                        // handler runs inside the test
$I->seeEmailIsSent(1);

This lets you cover the "message dispatched → handler did its job" chain inside one test instead of relying on a worker.

8. N+1 and performance guards

Two cheap regression guards that pay for themselves on listing pages:

$I->seeNumQueriesIsLessThan(10);
$I->dontSeeDuplicateQueries();
$I->seeRequestTimeIsLessThan(500);   // ms, profiler-based

Drop these in a dedicated PerformanceCest for your hot routes rather than sprinkling them everywhere — thresholds are brittle if they're not in one place.

9. Console commands, two eras

runSymfonyConsoleCommand('app:import', ['--dry-run' => true]) still works everywhere. On Symfony 8.1+, runCommand() returns an ExecutionResult with status code, stdout, and stderr separated — assert on it directly:

$result = $I->runCommand('app:import');
$I->assertCommandIsSuccessful();
$I->assertCommandResultEquals("3 rows imported\n");

10. Structural/workflow conventions that hold up

  • One Cest per user journey, not per controller. RegistrationCest, CheckoutCest.
  • Assertions on the outcome (DB row, queued message, sent email, voter result) beat assertions on HTML where both are possible; use DOM assertions for what the user sees (see, seeLink, seeElement).
  • Keep a tests/Acceptance/_support/Helper/Acceptance.php with domain-level $I->createOrderFor($user)-style actions so Cests read like scenarios.
  • Split suites: Acceptance (kernel, full stack), a slimmer Smoke (fewer modules) for the long-tail routes, and PHPUnit for units. Symfony-module tests are the slowest tier — treat them accordingly.

Gotchas and Tips

The kernel reboots between requests. This is the source of most "why did my service lose its state?" confusion. Anything you put in memory in request 1 is gone in request 2 unless it's persisted (persistPermanentService, mockService) or serialized into the session/repository. Don't build tests that depend on in-memory accumulators across amOnPage() calls.

A failed flush() poisons the EntityManager. Once Doctrine closes the EM, every later query in that test throws. Use $I->resetDoctrineManager() (reopens if closed, clears if open) at the start of the follow-up step, or in _before. Also note the 3.10.x fix restoring Doctrine connections after kernel shutdown — if you're pinned below 3.10.1 and see listeners losing security context after a reboot, upgrade.

Profiler-gated assertions. seeEventTriggered, seeOrphanEvent, seeCurrentTemplateIs, and the request-time assertion all read from the profiler. If the profiler isn't enabled in your test env (it often isn't, for speed), these fail with confusing messages rather than "profiler not enabled." Check framework.profiler.collect: true in config/packages/test/framework.yaml first.

Email assertions and redirects. seeEmailIsSent/grabSentEmails rely on the profiler for the request that sent the mail. Once you stopFollowingRedirects(), the module can't navigate onward to inspect that request's profiler data — there are documented restrictions here. Practical rule: read your emails before you continue navigating, or structure the Cest so the mail-sending request is the last one you assert on.

Mailer must be the Symfony Mailer. SwiftMailer support was removed in 2.0.0 and the mailer config option with it. Your test env needs framework.mailer.dsn: 'null://null' (or a test transport) plus framework.test: true; otherwise grabSentEmails() returns an empty collection with no complaint.

Private services and the container error message. Older versions threw a bare ServiceNotFoundException for private services. Use grabContainer() (test container) rather than grabService() for anything private — grabService() still works for public services and gives a friendlier error, but the test container is the correct answer for modern Symfony where almost nothing is public.

app_path is the #1 first-run failure. Default is app (Symfony 2/3 layout). For Symfony 4+ set app_path: 'src' and, if your kernel isn't App\Kernel, set kernel_class explicitly. Symptom: the module can't boot the kernel and every test errors identically.

grabLastSentEmail() return type widened to ?RawMessage. A 3.10.0 change that will break static analysis or Email-specific method calls even though the runtime object is still an Email for mailer emails. Narrow with instanceof or switch to the header/subject assertions.

Version pinning matters more than usual here. 3.x means Codeception 5. The 2.x line is Codeception 4. The 3.9.x → 3.11.x span added a lot of methods (Doctrine, services, security, Messenger, Console) — if your team's docs mention mockService or consumeMessengerMessages and the test fails with "method does not exist," you're on an old lock. Run codecept build after every composer update.

codecept build after any suite/module config change. The Actor's method signatures are generated. A stale Actor is the second-most-common cause of "the method exists in the README but not in my IDE."

Debugging workflow: vendor/bin/codecept run Acceptance --debug shows step timings and the kernel boot/reboot boundaries; --steps shows the resolved selector for DOM assertions. When a form assertion fails, dump the crawler ($I->grabPageSource() or $I->seeInCurrentUrl()) before assuming the form name is wrong — the usual culprit is the wrong name attribute, which is not the same as the form class name.

Extension points, in order of leverage:

  1. Actor helper methods (tests/_support/Helper/Acceptance.php) — wrap multi-step domain flows; the $I in Cests then reads like business language.
  2. Custom assertion traits — the module itself is built from traits (FormAssertionsTrait, MailerAssertionsTrait, etc.); if your app has a domain you assert repeatedly (e.g. a billing ledger), write your own trait and enable it via the helper.
  3. _before/_after hooks in Cests for resetDoctrineManager(), unmockService(), and cache warming — cheaper and more visible than module config.
  4. Environment config (config/packages/test/) — the right place to change mailer DSN, profiler depth, and cache pools; resist doing it in code.

When not to use it: for pure JSON API contract testing, module-rest + PhpBrowser is faster and produces clearer failures. For browser behaviour (JS, scroll, focus), you need WebDriver/Playwright. The Symfony module's sweet spot is server-rendered flows where the interesting assertions are inside the app.

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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel