Deck by PromptPHP provides a clean, expressive API for loading, rendering, and managing versioned AI prompts in your Laravel application. Prompts are stored as plain files on disk and organised by name, version, and role. They are accessed through the Deck facade.
A prompt can contain multiple roles (system, user, assistant, developer, tool, or any custom role you define). Each role's content supports {{ $variable }} interpolation, and the entire prompt can be converted into a messages array ready to send to OpenAI, Anthropic, or any chat-completion API.
use PromptPHP\Deck\Facades\Deck;
$prompt = Deck::get('order-summary');
$prompt->system(['tone' => 'friendly']);
// "You are a friendly AI assistant..."
$prompt->user(['input' => $request->message]);
// "Summarise the following order: ..."
The Deck facade is the primary entry point for loading prompts. It delegates to the PromptManager singleton registered by the service provider:
use PromptPHP\Deck\Facades\Deck;
// Load the active version.
$prompt = Deck::get('order-summary');
// Load a specific version.
$prompt = Deck::get('order-summary', 2);
The get method returns a PromptTemplate instance. If no version is specified, the active version is resolved automatically.
You can also inject the PromptManager directly via Laravel's service container:
use PromptPHP\Deck\PromptManager;
class OrderController extends Controller
{
public function __construct(protected PromptManager $prompts) {}
public function summarise(Request $request)
{
$prompt = $this->prompts->get('order-summary');
// ...
}
}
The PromptManager is registered as a singleton, so the same instance is reused throughout the request lifecycle.
Use the active method to explicitly load the active version:
$prompt = Deck::active('order-summary');
$prompt->version(); // e.g. 3
This is equivalent to calling get() without a version number.
Pass a version number as the second argument to get to load a specific version, regardless of which version is currently active:
$prompt = Deck::get('order-summary', 1);
$prompt->version(); // 1
If the version does not exist, an InvalidVersionException is thrown.
Once you have a PromptTemplate instance, you can render any role's content with variable interpolation.
The most expressive way to render a role is to call it as a method directly on the prompt instance. This uses PHP's __call magic method and works for any role — not just system and user:
$prompt = Deck::get('code-reviewer');
// Render the system role.
$prompt->system(['tone' => 'professional']);
// Render the user role.
$prompt->user(['input' => $code]);
// Render custom roles.
$prompt->assistant(['context' => $history]);
$prompt->developer(['task' => 'review']);
$prompt->tool(['name' => 'search']);
When called without arguments, the content is returned with placeholders left intact:
$prompt->system(); // "You are a {{ $tone }} AI assistant..."
role methodIf the role name is dynamic or stored in a variable, use the explicit role method:
$roleName = 'assistant';
$content = $prompt->role($roleName, ['context' => $history]);
This is functionally identical to the magic method approach. If the role does not exist, an empty string is returned.
To retrieve a role's content without variable interpolation, use the raw method:
$template = $prompt->raw('system');
// "You are a {{ $tone }} AI assistant specialized in..."
This is useful when you need to inspect the template, store it, or perform your own interpolation logic.
The roles method returns an array of all role names defined in the prompt:
$prompt->roles();
// ['system', 'user', 'assistant']
Roles are discovered automatically from the files in the version directory. Any file matching the configured extension (e.g. .md) becomes a role — the filename (without extension) is the role name.
Use the has method to check whether a specific role exists before attempting to render it:
if ($prompt->has('assistant')) {
$content = $prompt->assistant(['context' => $history]);
}
Each prompt version can carry metadata (stored in metadata.json at the version level). Access it via the metadata method:
$prompt->metadata();
// ['description' => 'Summarises customer orders', 'variables' => ['tone', 'input'], ...]
Metadata is an associative array. If no metadata.json exists in the version directory, an empty array is returned.
$prompt->name(); // 'order-summary'
$prompt->version(); // 2
The toMessages method builds a messages array compatible with OpenAI, Anthropic, and other chat-completion APIs. It renders every role with the given variables and returns them in definition order:
$messages = Deck::get('chat-agent')->toMessages([
'tone' => 'helpful',
'input' => $userMessage,
]);
// [
// ['role' => 'system', 'content' => 'You are a helpful AI assistant...'],
// ['role' => 'user', 'content' => 'Please help me with: ...'],
// ['role' => 'assistant', 'content' => 'Based on the context...'],
// ]
This array can be passed directly to any AI API client:
// OpenAI
$response = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => $messages,
]);
// Anthropic
$response = Anthropic::messages()->create([
'model' => 'claude-3-sonnet',
'messages' => $messages,
]);
Pass a second argument to limit which roles are included and in what order:
$messages = $prompt->toMessages(
['tone' => 'concise'],
['system', 'user'] // only include these two roles
);
Roles specified in the filter that don't exist in the prompt are silently skipped.
Deck by PromptPHP supports two placeholder syntaxes within prompt files:
| Syntax | Example |
|---|---|
| Spaced (recommended) | {{ $tone }} |
| Compact | {{tone}} |
Both are replaced when you render a role with variables:
// Template: "You are a {{ $tone }} AI assistant. Task: {{task}}"
$prompt->system(['tone' => 'friendly', 'task' => 'summarise']);
// "You are a friendly AI assistant. Task: summarise"
Values are cast to strings via PHP's (string) cast, so you can pass any scalar or stringable value:
$prompt->system([
'name' => 'Alice', // string
'count' => 42, // int → "42"
'score' => 3.14, // float → "3.14"
'price' => '$100.00', // string with special characters
]);
Placeholders that are not matched by the provided variables are left intact. This lets you render in stages or identify unfilled variables:
$prompt->system(['tone' => 'friendly']);
// "You are a friendly AI assistant. Your role is {{ $role }}."
Prompts are versioned using directory-based versioning. Each version lives in its own sub-directory (v1/, v2/, etc.) inside the prompt folder:
resources/prompts/
└── order-summary/
├── v1/
│ ├── system.md
│ └── user.md
├── v2/
│ ├── system.md
│ ├── user.md
│ └── assistant.md
└── metadata.json
Each version directory can contain:
system.md, user.md, assistant.md)metadata.json for version-level metadataRetrieve all versions for a prompt programmatically:
$versions = Deck::versions('order-summary');
// [
// ['version' => 1, 'path' => '...', 'metadata' => [...]],
// ['version' => 2, 'path' => '...', 'metadata' => [...]],
// ]
Versions are returned sorted in ascending order. Each entry includes the version number, the absolute path to the version directory, and any metadata from that version's metadata.json.
Or use the Artisan command:
php artisan prompt:list --all
See Artisan Commands — prompt:list for details.
Set a specific version as the active version:
Deck::activate('order-summary', 2);
When database tracking is enabled, this updates the prompt_versions table — setting is_active = false on all versions of that prompt, then is_active = true on the specified version.
When tracking is disabled, it writes the active_version key to the prompt's root metadata.json file.
Or use the Artisan command:
php artisan prompt:activate order-summary 2
# or
php artisan prompt:activate order-summary v2
See Artisan Commands — prompt:activate for details.
When you call Deck::get('name') without a version, the active version is resolved in this priority order:
is_active = true in the prompt_versions table for that prompt name.active_version key from the prompt's root metadata.json file.v1/ and v3/ exist, version 3 is used).If no versions exist at all, an InvalidVersionException is thrown.
When caching is enabled, loaded prompts are stored in your configured cache store to avoid repeated filesystem reads:
// config/deck.php
'cache' => [
'enabled' => env('DECK_CACHE_ENABLED', true),
'store' => env('DECK_CACHE_STORE', 'file'),
'ttl' => 3600, // seconds
'prefix' => 'deck:',
],
The cache key follows the pattern {prefix}{name}.v{version}. Prompts are cached on first load and served from cache on subsequent requests until the TTL expires.
Caching is automatically disabled when APP_DEBUG=true to ensure file changes are picked up immediately during development.
See Configuration — Cache for the full reference.
When database tracking is enabled, you can log prompt executions for performance monitoring, A/B testing, and audit trails:
Deck::track('order-summary', 2, [
'input' => ['message' => 'Summarise order #1234'],
'output' => 'Your order contains...',
'tokens' => 150,
'latency' => 234.5,
'cost' => 0.002,
'model' => 'gpt-4o',
'provider' => 'openai',
'feedback' => ['rating' => 5],
]);
All fields in the data array are optional. Records are inserted into the prompt_executions table. If tracking is disabled, the track method is a safe no-op.
See Tracking & Performance for comprehensive documentation on the tracking system, database schema, and Eloquent models.
The PromptTemplate class implements Laravel's Arrayable contract. Call toArray() to get a serialisable representation:
$prompt->toArray();
// [
// 'name' => 'order-summary',
// 'version' => 2,
// 'roles' => ['system' => '...', 'user' => '...'],
// 'metadata' => ['description' => '...'],
// ]
This is useful for caching, logging, debugging, or passing prompt data to queued jobs.
How can I help you explore Laravel packages today?