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

Pho Laravel Package

danielstjules/pho

Pho is a lightweight BDD test framework for PHP inspired by Jasmine. It offers describe/it syntax, expressive expectations, spies, and before/after hooks, making it easy to write readable, behavior-driven specs for PHP projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package as a dev dependency; it ships a pho binary.

composer require --dev danielstjules/pho
vendor/bin/pho --help

By default Pho looks for a spec/ directory in the current working directory and runs files matching *Spec.php. A minimal, realistic first spec:

<?php
// spec/CartSpec.php

describe('Cart', function () {
    beforeEach(function () {
        $this->cart = new Cart();
    });

    it('starts empty', function () {
        expect($this->cart->count())->toBe(0);
    });

    it('sums line items', function () {
        $this->cart->add(new Item('book', 10));
        $this->cart->add(new Item('pen', 2));

        expect($this->cart->total())->toEqual(12);
    });
});

Run it:

vendor/bin/pho                                # default "spec" formatter
vendor/bin/pho --formatter dot                # CI-friendly one-liner output
vendor/bin/pho --filter "sums line items"     # isolate a single spec

Where to look first

  1. README.md — the assertion list is the part you'll actually reference daily (toBe, toEqual, toContain, toMatch, toThrow, toBeAnInstanceOf, toBeTruthy/toBeFalsy, toBeNull, toBeGreaterThan, .not chaining).
  2. spec/ in the repo itself — Pho tests itself with Pho; that's the best source of real usage patterns for hooks, nesting, and async.
  3. src/Matcher/ — the canonical list of matchers and how they're implemented if you need custom ones.
  4. src/Formatter/ and src/Reporter/ — extension points for output.

First use case: Pho is best for a greenfield library or service where you want Mocha/Jasmine-style readability (describe/it/beforeEach) and low ceremony. A very practical first adoption is characterization tests around a legacy class — the nesting and $this-shared fixture style makes it easy to add one it per behavior without a test-class-per-file tax.

Implementation Patterns

Mirror src/ in spec/. Keep spec/CartSpec.php next to src/Cart.php. The file name drives discovery, so a file that doesn't end in Spec.php silently never runs.

Hooks are scoped and nest. before/after run once per describe block; beforeEach/afterEach run per it. Ordering: outer beforeEach → inner beforeEach → spec → inner afterEach → outer afterEach. Use outer blocks for expensive setup (DB connection, container) and inner blocks for per-spec state.

describe('UserRepository', function () {
    before(function () {
        $this->pdo = new PDO('sqlite::memory:');
    });

    beforeEach(function () {
        $this->pdo->exec('DELETE FROM users');
        $this->repo = new UserRepository($this->pdo);
    });

    describe('#find', function () {
        it('returns null when missing', function () {
            expect($this->repo->find(1))->toBeNull();
        });
    });
});

Table-driven specs. There are no data providers, but describe/it are just function calls executed at registration time, so you can generate specs in a loop:

$cases = [
    'empty string' => ['', 0],
    'one word'     => ['hello', 1],
    'two words'    => ['hello world', 2],
];

describe('#wordCount', function () use ($cases) {
    foreach ($cases as $name => [$input, $expected]) {
        it("returns $expected for $name", function () use ($input, $expected) {
            expect(wordCount($input))->toBe($expected);
        });
    }
});

Note the use (...) — closures in Pho are not auto-capturing, so anything from the enclosing scope must be imported, and $this inside a nested closure is not the spec scope.

Async via $done. Declaring a $done parameter on the it closure tells Pho to wait; call it when finished. Do not declare it on synchronous specs.

it('resolves the deferred value', function ($done) {
    $client = new Client();
    $client->getAsync('/health', function ($response) use ($done) {
        expect($response->getStatus())->toBe(200);
        $done();
    });
});

Bootstrap autoloading. Point Pho at Composer's autoloader once, via .pho.yml in the project root:

bootstrap: vendor/autoload.php
spec_dir: spec
timeout: 3000
formatter: spec

Or per invocation: vendor/bin/pho --bootstrap vendor/autoload.php. Put shared helpers (factories, fakes, custom matcher registration) in a spec/bootstrap.php and require it from the bootstrap file.

Test doubles. Pho deliberately stops at assertions; there's no createMock() equivalent. Wire in the library you already know (Mockery is the least friction) from the bootstrap or a root beforeEach:

beforeEach(function () {
    $this->mockery = new \Mockery\Container();
    $this->mailer  = $this->mockery->mock(Mailer::class);
    $this->mailer->shouldReceive('send')->once();
});

afterEach(function () {
    $this->mockery->mockery_close();
});

CI integration. The binary exits non-zero on failure, so it's a drop-in for a Composer script:

"scripts": {
  "test": "pho --formatter dot"
}

Use --filter to shard by name, and set timeout in .pho.yml if your suite has genuinely slow specs — the default is aggressive enough that a slow integration spec will look like a hang.

Coexisting with PHPUnit. Nothing stops you from keeping PHPUnit for integration tests and using Pho for unit specs. Either run both from a Composer script, or split with two Composer scripts/two CI jobs. Don't try to share fixtures between the two beyond plain PHP classes.

Gotchas and Tips

This package is frozen at April 2017. Treat PHP 8 compatibility as unproven until you've run the suite on your target version. Before committing, do a one-file smoke test on the exact PHP you deploy on. If you're on PHP 8.x and it breaks, the realistic options are pinning the dev environment to PHP 7.x or migrating to Pest/PHPUnit — Pest in particular is a near-mechanical port of describe/it/expect, so don't sink time into patching Pho internals.

Focused specs are a CI landmine. fit/fdescribe (and xit/xdescribe for skipping) are handy locally, but a committed fdescribe will silently skip the rest of the file and still exit green. Grep for them in a pre-commit hook or CI step.

$this rebinding will bite you. Specs run with $this bound to a per-spec context object, which is why beforeEach assignments are visible in it. That magic breaks the moment you introduce an inner closure or an arrow function — fn() binds $this from the enclosing scope, not the spec context. Use function () {} and explicit use for anything nested.

$done misuse presents as a timeout, not an error. If you declare $done and forget to call it on an early-return path, Pho reports a timeout with no useful stack. Keep async specs tiny, and call $done() in finally-style fashion.

toBe is strict, toEqual is loose. expect('1')->toBe(1) fails; toEqual passes. Use toBe for scalars and identity, toEqual for value objects and numeric coercion. For floats, compare with a delta (toBeGreaterThan/toBeLessThan pairs) rather than toBe.

expect() is context-bound. Assertions outside an it/hook (e.g., inside a helper class, or in bootstrap) throw. If you want reusable assertion helpers, pass values in and assert at the call site, or write a helper that returns the value.

Shared $this state leaks between nested describe blocks. Because the context is shared up the nesting chain, a property set in one inner block can be visible in a sibling. Always re-initialize mutable fixtures in beforeEach, not before.

Bootstrap runs once per process, not per spec. Anything stateful there (a container, a static connection) persists across the whole run. That's a feature for speed and a footgun for isolation; reset it in beforeEach if specs mutate it.

Config discovery is cwd-relative. .pho.yml is picked up from where you invoke the binary. Running vendor/bin/pho from a subdirectory silently uses defaults and finds no specs — a common "why did zero tests run?" moment. Check the reported spec count.

Debugging tactics. Use the default spec formatter (not dot) when investigating failures — it prints the full nesting path and stack. var_dump/fwrite(STDERR, ...) inside it works fine since everything runs in-process; --filter on a distinctive spec name is faster than any debugger loop.

No coverage, no parallelism. There's no built-in coverage or worker pool. Coverage requires wrapping the process with Xdebug/phpdbg yourself, and specs execute serially — large suites get slow. For big projects this is the main reason to prefer a maintained runner.

Extension points are thin and undocumented. Custom matchers and formatters exist in src/Matcher and src/Formatter, but the interfaces aren't part of a stable public contract and there's no published guide. Inspect the built-ins, copy the shape, and keep customizations minimal — you're the maintainer once the package is abandoned.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums