Skip to content

Type Inference ​

Pest BDD uses PHP type hints to automatically determine how to extract parameters from step text. This eliminates the need for explicit type annotations in your patterns.

The Core Concept ​

Traditional BDD tools require you to specify types in patterns:

php
// Behat style - types in pattern
/** @Given I have :count items */
public function haveItems($count) { }  // $count is string!

Pest BDD infers types from PHP:

php
// Pest BDD - types from PHP
#[Given('I have {count} items')]
public function haveItems(int $count): void { }  // $count is int!

Type to Regex Mapping ​

When Pest BDD encounters a placeholder {name}, it:

  1. Finds the corresponding method parameter
  2. Gets the parameter's type hint
  3. Generates an appropriate regex pattern
PHP TypeGenerated RegexMatches
string"[^"]*""quoted text"
int-?\d+42, -5, 0
float-?\d*\.?\d+3.14, .5, -2.5
booltrue|false|yes|no|1|0true, yes, 1
Other(treated as injection)N/A

String Parameters ​

Strings are matched within double quotes:

php
#[Given('a user {name} exists')]
public function userExists(string $name): void
{
    // Pattern: a user (?P<name>"[^"]*") exists
}

Gherkin:

gherkin
Given a user "John Doe" exists
# $name = "John Doe"

Given a user "Alice O'Connor" exists
# $name = "Alice O'Connor"

Given a user "" exists
# $name = "" (empty string)

Why Quotes? ​

Quoted strings:

  • Clearly delimit the value in natural language
  • Support spaces and special characters
  • Match Gherkin conventions

Integer Parameters ​

Integers match digits with optional negative sign:

php
#[Given('there are {count} items')]
public function itemsExist(int $count): void
{
    // Pattern: there are (?P<count>-?\d+) items
}

Gherkin:

gherkin
Given there are 5 items
# $count = 5

Given there are 100 items
# $count = 100

Given there are -3 items
# $count = -3

Given there are 0 items
# $count = 0

Float Parameters ​

Floats match decimal numbers:

php
#[Given('the price is {amount}')]
public function priceIs(float $amount): void
{
    // Pattern: the price is (?P<amount>-?\d*\.?\d+)
}

Gherkin:

gherkin
Given the price is 99.99
# $amount = 99.99

Given the price is .5
# $amount = 0.5

Given the price is 100
# $amount = 100.0

Given the price is -25.50
# $amount = -25.5

Boolean Parameters ​

Booleans match several common formats:

php
#[Given('notifications are {enabled}')]
public function notificationsEnabled(bool $enabled): void
{
    // Pattern: notifications are (?P<enabled>true|false|yes|no|1|0)
}

Gherkin:

gherkin
Given notifications are true
# $enabled = true

Given notifications are yes
# $enabled = true

Given notifications are 1
# $enabled = true

Given notifications are false
# $enabled = false

Given notifications are no
# $enabled = false

Given notifications are 0
# $enabled = false

Non-Builtin Types ​

When a parameter type isn't a builtin (string, int, float, bool), it's treated as an injection candidate rather than a pattern parameter:

php
#[When('the user places an order')]
public function placeOrder(User $user): Order
{
    // $user is NOT extracted from text
    // $user is INJECTED from context
    return Order::create(['user_id' => $user->id]);
}

See Auto-Injection for details.

Mixed Parameters ​

Combine extracted and injected parameters:

php
#[When('the user buys {quantity} of {product}')]
public function buy(User $user, int $quantity, string $product): void
{
    // $user - injected from context
    // $quantity - extracted as int
    // $product - extracted as string
}

Gherkin:

gherkin
When the user buys 3 of "Widget"
# Extracts: quantity=3, product="Widget"
# Injects: user from context

Parameter Order Flexibility ​

Injected parameters can be in any position:

php
// All equivalent - $user is injected regardless of position
public function buy(User $user, int $quantity, string $product): void { }
public function buy(int $quantity, User $user, string $product): void { }
public function buy(int $quantity, string $product, User $user): void { }

Type Casting ​

Extracted values are cast to their target types:

String Processing ​

Quotes are stripped from strings:

php
// Input: "Hello World"
// Result: Hello World (no quotes)

Integer Casting ​

php
// Input: "42"
// Result: 42 (int)

// Input: "-5"
// Result: -5 (int)

Float Casting ​

php
// Input: "3.14"
// Result: 3.14 (float)

// Input: ".5"
// Result: 0.5 (float)

Boolean Casting ​

php
// Input: "true", "yes", "1"
// Result: true (bool)

// Input: "false", "no", "0"
// Result: false (bool)

Compiled Patterns ​

For performance, patterns are compiled once and reused:

php
// CompiledStepDefinition.php
class CompiledStepDefinition
{
    public string $regex;
    public array $typeHints;

    public static function compile(StepDefinition $def): self
    {
        $compiled = new self();

        // Extract type hints from reflection
        $compiled->typeHints = PatternMatcher::extractTypeHints(
            $def->pattern,
            $def->method
        );

        // Build regex once
        $compiled->regex = PatternMatcher::buildRegex(
            $def->pattern,
            $compiled->typeHints
        );

        return $compiled;
    }

    public function match(string $text): ?array
    {
        if (preg_match($this->regex, $text, $matches)) {
            return $this->castValues($matches);
        }
        return null;
    }
}

Advanced Patterns ​

Multiple Parameters of Same Type ​

php
#[Given('I transfer {amount} from {source} to {target}')]
public function transfer(float $amount, string $source, string $target): void
{
    // amount: float
    // source: string
    // target: string
}
gherkin
Given I transfer 100.50 from "Checking" to "Savings"

Optional Parameters with Defaults ​

php
#[Given('a user exists')]
public function userExists(string $name = 'Default User'): User
{
    return User::factory()->create(['name' => $name]);
}

#[Given('a user {name} exists')]
public function namedUserExists(string $name): User
{
    return User::factory()->create(['name' => $name]);
}

The first pattern has no placeholders, so it uses the default. The second extracts the name.

Nullable Types ​

php
#[When('I search for {query}')]
public function search(?string $query, User $user): void
{
    // $query extracted from pattern (required)
    // $user injected from context (nullable if not found)
}

Debugging Type Inference ​

Check Pattern Compilation ​

If a pattern isn't matching, verify the generated regex:

php
// Temporary debug
$matcher = new PatternMatcher();
$typeHints = ['count' => 'int'];
$regex = $matcher->buildRegex('I have {count} items', $typeHints);
dump($regex);  // /^I have (?P<count>-?\d+) items$/u

Common Issues ​

Issue: String not matching

gherkin
Given a user John exists  # Missing quotes!

Fix:

gherkin
Given a user "John" exists  # Add quotes

Issue: Wrong type hint

php
#[Given('there are {count} items')]
public function items(string $count): void  // Should be int!

Fix:

php
#[Given('there are {count} items')]
public function items(int $count): void

Issue: Placeholder name mismatch

php
#[Given('I have {count} items')]
public function items(int $total): void  // count ≠ total

Fix:

php
#[Given('I have {count} items')]
public function items(int $count): void  // Names match

Static Method Type Inference ​

Type inference works identically for static methods:

php
class User extends Model
{
    #[Given('a user {name} with {age} years exists')]
    public static function createWithAge(string $name, int $age): self
    {
        return self::create(['name' => $name, 'age' => $age]);
    }
}
gherkin
Given a user "John" with 25 years exists
# Extracted: name="John", age=25
# Returns: User instance

Static methods can also combine extracted and injected parameters:

php
class Order extends Model
{
    #[When('user places order for {amount}')]
    public static function createForUser(User $user, float $amount): self
    {
        // $user - injected from context
        // $amount - extracted from pattern
        return self::create([
            'user_id' => $user->id,
            'total' => $amount,
        ]);
    }
}

See Static Methods for more details.

Released under the MIT License.