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

Friendly Contexts Laravel Package

knplabs/friendly-contexts

FriendlyContexts is a set of reusable Behat contexts for Symfony applications. It offers ready-made steps for Doctrine entities, fixtures, services, and browser testing, cutting boilerplate so teams can write clear, expressive acceptance tests faster.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: this package was archived in 2016 and its last release predates Laravel 5.4. It is a Symfony2/Doctrine/Mink-era Behat extension, not a Laravel package. composer require --dev knplabs/friendly-contexts will resolve on PHP 5.6–7.0 with Symfony 2/3 and Guzzle 5/6; on a modern Laravel app (PHP 8.1+, Symfony 6/7) it will fail dependency resolution outright. Treat it as a source of reusable context code you vendor in and patch, not a drop-in dependency. That framing drives everything below.

1. Decide what you're actually harvesting. Roughly half the package is Symfony-coupled and useless to you:

Context Laravel usefulness
ApiContext + traits High — Guzzle-driven HTTP steps
JsonContext High — JSON pointer assertions
FilesystemContext Medium — fixture/temp-file steps
TableContext + Table\* Medium — the Table: transform is nice, entity persistence is Doctrine-only
ORMContext / DoctrineContext / FixturesContext None — assumes EntityManager and Alice/Doctrine bundles
SymfonyContext / WebContext / MailerContext None — kernel boot, Mink/Selenium, Swiftmailer MessageLoggerListener

2. Where to look first. Clone the repo and read, in this order:

  • src/Knp/FriendlyContexts/Extension.php — the config keys and default service wiring; this tells you what the YAML knob names actually are (faker_locale, base_url, files_path, Doctrine namespace settings).
  • src/Knp/FriendlyContexts/Context/ — the concrete contexts; each is a thin class composed from a trait.
  • src/Knp/FriendlyContexts/Context/Traits/ — where the step definitions actually live.
  • features/ — the package's own Behat suite is the only real documentation of the Table: DSL.

3. Install inside your own namespace. Copy the handful of files you want into tests/Behat/Context/ (or a small private Composer package with "type": "path"), keeping the Knp\FriendlyContexts namespace if you want to copy traits verbatim, or re-namespace them to App\Tests\Behat\. Add behat/behat and behat/mink-extension yourself — those still install fine.

4. Minimal behat.yml after vendoring:

default:
  suites:
    api:
      contexts: [App\Tests\Behat\Context\FeatureContext]
  extensions:
    Knp\FriendlyContexts\Extension:
      faker_locale: en_US
      base_url: http://127.0.0.1:8000
      files_path: tests/Behat/fixtures

If you skip the extension block entirely, you can still use the traits — the extension only exists to inject the Guzzle client, Faker generator, and filesystem helper into contexts implementing InitializedContext. Wiring those by hand in a @BeforeSuite hook is often simpler than fighting the extension.

5. First use case. A JSON API regression suite against a Laravel route. Write one context that composes ApiContextTrait + JsonContextTrait, boot the Laravel kernel in @BeforeSuite, and run vendor/bin/behat --definitions to confirm the inherited steps registered. That single feature file — GET /api/v1/widgets returns 200 with a data array — is the smallest thing that proves the vendored traits work under your framework.

Implementation Patterns

Trait composition, not inheritance. The package's central idea is that a context is assembled from capability traits (ApiContextTrait, JsonContextTrait, FilesystemContextTrait) plus a RawContext base that stores injected services. Keep that shape in your Laravel port:

class FeatureContext extends RawContext
{
    use ApiContextTrait;
    use JsonContextTrait;

    /** @BeforeSuite */
    public static function bootApp(): void
    {
        $app = require __DIR__.'/../../../bootstrap/app.php';
        $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
    }

    /** @BeforeScenario @db */
    public function beginTransaction(): void
    {
        app('db')->beginTransaction();
    }

    /** @AfterScenario @db */
    public function rollback(): void
    {
        app('db')->rollBack();
    }
}

The transaction-per-scenario hook is the piece the package never gives you (Doctrine users got it from doctrine/doctrine-fixtures-bundle), and it's the single most important addition for a Laravel app.

Replace the entity factory, keep the Table DSL. TableContext gives you steps like:

Given there are users:
  | name  | email             |
  | Alice | [email protected] |

Internally that calls an EntityFactory that resolves a Doctrine entity by class name, applies Faker defaults to unlisted columns, and persists via EntityManager. Swap that factory for an Eloquent one and the DSL is genuinely pleasant:

class EloquentEntityFactory implements EntityFactoryInterface
{
    public function create(string $entity, array $attributes): object
    {
        $class = 'App\\Models\\'.Str::studly($entity);

        return $class::factory()->create($attributes);
    }
}

Map step names (there are :count :entity, there is a :entity) to your factory and you get readable fixture setup that reuses your existing model factories — no duplicate fixture layer.

API + JSON assertion flow. ApiContextTrait gives you I send a GET request to :url / the response status code should be :code; JsonContextTrait adds JSON-path assertions and stores the last response. Typical scenario:

Scenario: Listing widgets requires auth
  When I send a GET request to "/api/v1/widgets"
  Then the response status code should be 401
  And the JSON node "message" should exist

Because the Guzzle client is injected, you can add a @BeforeScenario that sets default headers with Authorization: Bearer <token> from a Sanctum/Passport call — cheaper and more stable than actingAs() in a browser-driven flow.

Deterministic data. Pin Faker with a fixed seed per suite so generated columns don't drift between runs. In your ported factory: $faker->seed(1337) in @BeforeSuite. This is what makes the table DSL's implicit "fill the missing columns" behaviour safe to assert against.

Filesystem fixtures. FilesystemContextTrait gives you temp-dir creation, file writes, and cleanup steps. Use it for UploadedFile tests — point the step at a path under tests/Behat/fixtures and clean the destination in @AfterScenario so repeated local runs don't accumulate junk under storage/.

Coexistence, not replacement. Keep Pest/PHPUnit for unit and feature tests with RefreshDatabase; use Behat + this port for the handful of suites where business-readable Gherkin earns its keep (auth flows, pricing rules, cross-service contracts). Run it as a separate CI job against a booted php artisan serve or an in-process HttpKernel adapter — don't try to share a database connection strategy with your PHPUnit suite.

Reuse the transforms. The Table / Row value objects under Table\ are framework-agnostic. Even if you delete every context, these are worth keeping if you write your own step definitions that take multi-line tables.

Gotchas and Tips

Archived means no upstream fixes. Every bug you hit is yours. Budget for it: the traits assume PHP 5.x array syntax in places, use array() style, and reference GuzzleHttp\Client APIs (getBody()->getContents(), json()) that changed across Guzzle 5→6→7. Verify each trait against your installed Guzzle before trusting it.

Doctrine leakage is deep. ORMContext, FixturesContext, and the entity resolver all call getManager() / getRepository() / getMetadataFactory(). Don't try to shim an EntityManager around Eloquent — you'll spend more time than porting the three traits you actually need. Rip out the Doctrine tier wholesale.

Swiftmailer, not Symfony Mailer, and definitely not Laravel Mail. MailerContext hooks MessageLoggerListener from swiftmailer/swiftmailer, which is EOL and removed from Symfony. For Laravel, assert on Mail::fake() / Mail::assertSent() from a @AfterScenario hook instead, or use Mailhog/Mailpit + an HTTP assertion. Do not port MailerContext.

Step ambiguity is silent-ish. Behat picks the first matching definition across contexts and only warns with --strict. With two dozen traits composing into one context, a duplicated pattern (e.g. I send a GET request to :url) is easy to create. Run vendor/bin/behat --definitions -vvv and eyeball for duplicates whenever you add a trait.

Snippets generate into the wrong place. Undefined-step snippets print by default; add --no-snippets in CI so missing steps fail rather than silently passing. Turn snippets on locally, off in pipeline.

No database isolation out of the box. The package never wraps scenarios in transactions (it relied on doctrine:fixtures reloading the schema). Without the @BeforeScenario/@AfterScenario transaction hook above, data from scenario 3 will break scenario 7 in ways that look like application bugs. This is the #1 debugging trap when porting.

Config key names are not validated. The extension reads YAML keys loosely; a typo like baseurl: instead of base_url: yields a null URL and a confusing Guzzle "no host" exception at request time. When something is null, first suspect the YAML.

files_path is relative to the Behat run directory, not the project root. It resolves against getcwd(), so running Behat from a subdirectory silently changes the fixture root. Always invoke from the project root.

Faker's implicit column filling surprises people. Given | name | email | against a users table, Faker fills created_at, password, and anything else non-nullable — including columns you wanted to control. Add | password | secret | explicitly, or override defaults in your Eloquent factory.

Extension points worth knowing. If you do keep the extension: implement InitializedContext to receive the container, TaggedContext to scope steps to @api-style tags, and override EntityFactory in Extension::load() (or bind your own in a small subclass) rather than editing vendored code. Everything else is copy-paste-and-own.

Debugging toolkit.

  • — vendor/bin/behat --definitions=App\\Tests\\Behat lists every step your context exposes.
  • — -vvv shows the resolved config and the actual URL Guzzle hit.
  • — Dump the last response body in a failing step with fwrite(STDERR, $this->getLastResponse()->getBody()) — Behat swallows exceptions otherwise.
  • — --format=progress for CI, pretty locally.

PHP version pins. If you must install the original package to read it via Composer rather than git, add "config": {"platform": {"php": "7.0.33"}} to a throwaway project. Never in your application's composer.json — it will silently downgrade unrelated dependencies.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package