laravel-forms maintained by logicoforms
Laravel Forms
A Typeform-style forms package for Laravel with logic branching, an AI form builder, and a full UI out of the box.
Requirements
- PHP 8.2+
- Laravel 12+
- GD extension (for OG image generation)
- An AI provider API key (OpenAI, Anthropic, etc.) if using the AI builder
Installation
composer require logicoforms/laravel-forms
The service provider is auto-discovered. Publish the config and run migrations:
php artisan vendor:publish --tag=forms-config
php artisan migrate
If the laravel/ai migration doesn't run automatically, run it manually:
php artisan migrate --path=vendor/laravel/ai/database/migrations
Local path install (development)
Add the path repository to your app's composer.json:
{
"repositories": [
{
"type": "path",
"url": "../packages/laravel-forms"
}
],
"require": {
"logicoforms/laravel-forms": "@dev"
}
}
Then run composer update logicoforms/laravel-forms.
Environment Variables
Add these to your .env file. All are optional — sensible defaults are used when omitted.
# ── General ──────────────────────────────────────────────────────
FORMS_OWNER_MODEL="App\Models\User" # Model that owns forms
FORMS_API_PREFIX=api # Prefix for public API routes
FORMS_API_RATE_LIMIT=60,1 # API throttle (requests,minutes)
# ── URLs ─────────────────────────────────────────────────────────
FORMS_URL_EDIT=/logico/forms/{id}/edit # Edit URL pattern (used by AI builder)
FORMS_URL_PUBLIC=/f/{slug} # Public form URL pattern
# ── Branding ─────────────────────────────────────────────────────
FORMS_BRAND_NAME="My App" # Shown in nav & OG images
FORMS_BRAND_DOMAIN=myapp.com # Shown in OG images
FORMS_BRAND_TAGLINE="Smart forms for everyone"
FORMS_BRAND_LOGO_URL=/logo.svg # Logo URL for nav
# ── AI Provider Keys (required for AI builder) ──────────────────
OPENAI_API_KEY= # Required if using OpenAI
ANTHROPIC_API_KEY= # Required if using Anthropic
# ── AI Builder ───────────────────────────────────────────────────
FORMS_AI_BUILDER_PROVIDER= # e.g. openai, anthropic (auto-detected if blank)
FORMS_AI_BUILDER_MODEL=gpt-5.2 # Model for form generation
FORMS_AI_BUILDER_TIMEOUT=120 # Max seconds per AI request
FORMS_AI_BUILDER_ENFORCE_QUALITY=true # Reject lazy branching patterns
# ── AI Critic (optional quality reviewer) ────────────────────────
FORMS_AI_CRITIC_ENABLED=true # Enable AI quality review
FORMS_AI_CRITIC_MODE=on_pass # When to run critic
FORMS_AI_CRITIC_PROVIDER= # Provider (defaults to builder provider)
FORMS_AI_CRITIC_MODEL=gpt-4o # Critic model
FORMS_AI_CRITIC_TIMEOUT=25 # Critic timeout in seconds
FORMS_AI_CRITIC_MAX_ATTEMPTS=2 # Max retry attempts
Note: The AI builder requires the
laravel/aipackage. Make sure you also configure your AI provider's API key inlaravel/ai's config (e.g.OPENAI_API_KEYorANTHROPIC_API_KEY).
Setup
1. Implement FormOwner on your User model
use Logicoforms\Forms\Contracts\FormOwner;
use Logicoforms\Forms\Models\Form;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Authenticatable implements FormOwner
{
public function forms(): HasMany
{
return $this->hasMany(Form::class, 'created_by');
}
}
2. Configure (optional)
Edit config/forms.php after publishing. Key options:
return [
// The model that owns forms
'owner_model' => App\Models\User::class,
// URL patterns used by the AI builder when generating links
'urls' => [
'edit' => '/forms/{id}/edit',
'public' => '/f/{slug}',
],
// Set to false to define your own web routes
'register_web_routes' => true,
// Middleware for authenticated routes
'auth_middleware' => ['web', 'auth'],
// Branding shown in nav and OG images
'brand' => [
'name' => 'My App',
'domain' => 'myapp.com',
'tagline' => 'Smart forms for everyone',
],
// Override view partials (or set to null to disable)
'views' => [
'nav' => 'forms::partials.nav', // or your own: 'partials.my-nav'
'analytics' => null, // e.g. 'partials.gtag'
],
// AI builder settings (requires laravel/ai)
'ai_builder' => [
'provider' => null, // auto-detected from laravel/ai config
'model' => 'gpt-4o',
'timeout' => 120,
],
];
3. Implement FormLimiter (optional)
By default the package uses NullFormLimiter which imposes no limits. To add plan-based restrictions, create your own implementation:
use Logicoforms\Forms\Contracts\FormLimiter;
class AppFormLimiter implements FormLimiter
{
public function maxResponsesPerForm($owner): ?int
{
return null; // unlimited
}
public function canCreateForm($owner): bool
{
return $owner->forms()->count() < 100;
}
public function canRemoveBranding($owner): bool
{
return false;
}
public function canUseCustomThemes($owner): bool
{
return true;
}
public function canAccessAiBuilder($owner): bool
{
return true;
}
public function hasAiCredits($owner): bool
{
return true;
}
public function deductAiCredit($owner): void
{
// no-op
}
public function getAiCreditBalance($owner): ?int
{
return null; // unlimited
}
public function formLimitRedirectUrl(): ?string
{
return null; // shows generic error
}
}
Register it in your AppServiceProvider:
use Logicoforms\Forms\Contracts\FormLimiter;
public function register(): void
{
$this->app->singleton(FormLimiter::class, AppFormLimiter::class);
}
What's included
Routes
The package registers both API and web routes automatically.
Web routes (configurable via register_web_routes):
| Method | URI | Description |
|---|---|---|
| GET | /f/{slug} |
Public form view |
| GET | /f/{slug}/og-image.png |
OG image |
| GET | /logico/forms |
Forms list |
| GET | /logico/forms/create |
Create form |
| GET | /logico/forms/{form}/edit |
Form editor |
| GET | /logico/forms/{form} |
Responses viewer |
| GET | /logico/forms/{form}/logic-tree |
Logic tree visualizer |
| GET | /logico/forms/templates |
Template gallery |
| GET | /logico/forms/ai-builder |
AI form builder |
API routes (always registered):
| Method | URI | Description |
|---|---|---|
| POST | /api/forms/{form}/sessions |
Start a session |
| POST | /api/forms/{form}/sessions/{uuid}/answers |
Submit an answer |
| POST | /api/forms/{form}/sessions/{uuid}/complete |
Complete session |
Models
Form— title, slug, status, theme, end screenFormQuestion— type, text, help text, settings, orderingQuestionOption— label, value, image URLQuestionLogic— operator, value, next question routingFormSession— tracks a respondent's progressFormAnswer— stores individual answersFormThemePreset— reusable theme configurations
Question types
text, email, number, select, radio, checkbox, rating, picture_choice, opinion_scale
Logic branching
Each question can have logic rules that route respondents to different questions based on their answers. Supported operators: equals, not_equals, is, is_not, greater_than, less_than, contains, not_contains, begins_with, ends_with, always, and more.
AI builder
The AI builder creates forms from natural language descriptions. It generates questions, options, and logic rules automatically. Requires laravel/ai and an API key for your chosen provider.
Set up the queue worker for AI builder to function:
php artisan queue:work
Events
The package dispatches events instead of logging directly, so you can listen to them in your app:
| Event | Payload |
|---|---|
FormCreated |
Form $form, array $metadata |
FormUpdated |
Form $form |
FormDeleted |
int $formId, string $formTitle |
QuestionChanged |
Form $form, string $action, array $metadata |
AiBuilderUsed |
array $metadata |
Views
All views are published under the forms namespace. Override any view by publishing:
php artisan vendor:publish --tag=forms-views
This copies views to resources/views/vendor/forms/ where you can customize them.
Customizing the navigation
The dashboard includes a configurable nav partial. Set forms.views.nav in config to point to your own Blade partial:
'views' => [
'nav' => 'partials.my-nav', // your app's nav
],
Set it to null to disable the nav entirely.
Querying Forms & Responses
All package models are available for direct use. Import from Logicoforms\Forms\Models\*.
Get a user's forms
use Logicoforms\Forms\Models\Form;
$forms = $user->forms;
// Only published
$published = $user->forms()->where('status', 'published')->get();
// With question and session counts
$forms = Form::where('created_by', $user->id)
->withCount(['questions', 'sessions'])
->latest()
->get();
Get form questions with options
$form = Form::with('questions.options')->find($formId);
foreach ($form->questions as $question) {
echo $question->question_text; // "How likely are you to recommend us?"
echo $question->type; // radio, text, rating, etc.
foreach ($question->options as $option) {
echo $option->label; // "Very likely"
echo $option->value; // "very_likely"
}
}
Get completed responses for a form
use Logicoforms\Forms\Models\FormSession;
$sessions = FormSession::where('form_id', $formId)
->where('is_completed', true)
->with('answers.question')
->latest()
->get();
foreach ($sessions as $session) {
echo $session->session_uuid;
echo $session->created_at;
foreach ($session->answers as $answer) {
echo $answer->question->question_text;
echo $answer->answer_value; // string or array (for checkbox)
}
}
Get answers for a specific question
use Logicoforms\Forms\Models\FormAnswer;
$answers = FormAnswer::where('question_id', $questionId)
->whereHas('session', fn ($q) => $q->where('is_completed', true))
->pluck('answer_value');
Count responses
$form = Form::withCount([
'sessions',
'sessions as completed_count' => fn ($q) => $q->where('is_completed', true),
])->find($formId);
echo $form->sessions_count; // total started
echo $form->completed_count; // total completed
Aggregate answers (averages, breakdowns)
// Average rating
$avg = FormAnswer::where('question_id', $ratingQuestionId)
->whereHas('session', fn ($q) => $q->where('is_completed', true))
->get()
->avg(fn ($a) => (float) $a->answer_value);
// Option breakdown for radio/select
$breakdown = FormAnswer::where('question_id', $radioQuestionId)
->whereHas('session', fn ($q) => $q->where('is_completed', true))
->get()
->countBy(fn ($a) => $a->answer_value);
// Returns: ['very_likely' => 12, 'not_likely' => 3, ...]
Filter responses by date
$sessions = FormSession::where('form_id', $formId)
->where('is_completed', true)
->whereBetween('created_at', [$startDate, $endDate])
->with('answers')
->get();
Export-friendly: all responses as rows
$form = Form::with('questions')->find($formId);
$questions = $form->questions;
$rows = FormSession::where('form_id', $formId)
->where('is_completed', true)
->with('answers')
->get()
->map(function ($session) use ($questions) {
$row = [
'uuid' => $session->session_uuid,
'completed_at' => $session->updated_at,
];
foreach ($questions as $q) {
$answer = $session->answers->firstWhere('question_id', $q->id);
$val = $answer?->answer_value;
$row[$q->question_text] = is_array($val) ? implode(', ', $val) : $val;
}
return $row;
});
Claude Code Skill
The package ships with a Claude Code skill that lets you create forms, add logic, review quality, export responses, and debug issues — all from the CLI.
Setup
Copy the skill into your project's .claude/skills/ directory:
mkdir -p .claude/skills/forms
cp vendor/logicoforms/laravel-forms/.claude/skills/forms/SKILL.md .claude/skills/forms/SKILL.md
Or if you're using a path repository (development):
mkdir -p .claude/skills/forms
cp ../packages/laravel-forms/.claude/skills/forms/SKILL.md .claude/skills/forms/SKILL.md
Usage
Once installed, use /forms followed by what you want to do:
/forms create a customer feedback survey with NPS branching
/forms review form 3 for logic issues
/forms add branching to form 5 based on the role question
/forms export responses for form 3 as CSV
/forms debug why form 3 never completes
/forms show me the routes this package registers
The skill knows the full package API — models, logic engine, routes, events, config, and common debugging patterns.
Testing
cd packages/laravel-forms
composer install
./vendor/bin/phpunit
License
MIT