Skip to content

Feature request: auto-convert string values to BackedEnum instances when tool handler parameters are typed as enums #1016

@Ahrengot

Description

@Ahrengot

Summary

When a tool parameter is declared with withEnumParameter() and the handler method types that parameter as a BackedEnum subclass, Prism forwards the raw string value from the model straight into call_user_func. PHP's strict typing rejects the call, the TypeError is caught, and the model receives a validation error instead of a successful tool invocation. I'd like Prism to cast the string to the enum before invoking the handler, analogous to Laravel's route-model binding of enums.

Reproduction

use Prism\Prism\Tool;

enum Classification: string
{
    case A = 'A';
    case B = 'B';
    case C = 'C';
}

class ClassifyTool extends Tool
{
    public function __construct()
    {
        $this->as('classify')
            ->for('Classify something')
            ->withEnumParameter(
                name: 'target',
                description: 'Target classification',
                options: array_map(fn (Classification $c) => $c->value, Classification::cases()),
            )
            ->using($this);
    }

    public function __invoke(Classification $target): string
    {
        return $target->value;
    }
}

The model sends {"target": "B"}. "B" is a string; $target expects Classification. The call fails.

Current behavior

The TypeError is caught by Tool::handle() (around the call_user_func($this->fn, ...$args) line) and turned into a ToolError sent back to the model:

Parameter validation error: Type mismatch. Expected: [target (EnumSchema, required)]. Received: {"target":"B"}. Please provide correct parameter types and names.

The schema-declared type (EnumSchema with string options) and the PHP handler signature (Classification) disagree, and the gap is silently pushed onto the caller. Current workaround is to type the handler parameter as string and call Classification::from(\$value) manually inside the handler.

Proposed behavior

Before calling the handler, inspect each handler parameter via reflection. If the type is a BackedEnum subclass and the corresponding argument is a string or int, convert it with \$enumClass::from(\$value). If the schema for that parameter was declared with withEnumParameter, the developer has already told Prism the options map to enum cases — the cast is unambiguous.

\$reflection = new ReflectionFunction(Closure::fromCallable(\$this->fn));

foreach (\$reflection->getParameters() as \$index => \$param) {
    \$type = \$param->getType();
    if (! \$type instanceof ReflectionNamedType) {
        continue;
    }

    \$typeName = \$type->getName();
    if (! is_subclass_of(\$typeName, BackedEnum::class)) {
        continue;
    }

    \$key = array_key_exists(\$param->getName(), \$args) ? \$param->getName() : \$index;
    \$value = \$args[\$key] ?? null;

    if (is_string(\$value) || is_int(\$value)) {
        \$args[\$key] = \$typeName::from(\$value);
    }
}

Version and related work

Happy to open a PR with tests if the maintainers are open to the change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions