Skip to content

toandv97/form-builder-api

Repository files navigation

Dynamic Form Builder API

REST API for defining dynamic forms, publishing active versions, and accepting submissions. The codebase is organized around Domain-Driven Design (DDD) with clear separation between domain rules, application workflows, infrastructure (persistence, cache, logging), and HTTP delivery.

Requirements

  • PHP ^8.1
  • Laravel 10
  • MySQL (see database/migrations)
  • Composer

Configure .env (copy from .env.example). For asynchronous submission processing and realistic caching in production, set QUEUE_CONNECTION to database or redis and run a queue worker; set CACHE_DRIVER to redis or another shared store if you run multiple app instances.

High-level architecture

flowchart TB
  subgraph presentation [Presentation - HTTP]
    Routes[routes/api.php]
    Controllers[Http/Controllers]
  end

  subgraph application [Application layer]
    Commands[Commands]
    Handlers[Handlers]
    Pipes[Pipes - Submission pipeline]
    Jobs[Jobs - Queue]
    Audit[Common/Audit]
  end

  subgraph domain [Domain layer]
    Entities[Entities]
    VOs[Value Objects]
    ReposIf[Repository interfaces]
    DomainEx[Domain exceptions]
    FieldTypes[Field types + Factory]
  end

  subgraph infrastructure [Infrastructure]
    Eloquent[Persistence/Repositories]
    Models[Persistence/Models]
    CacheSvc[Cache/FormCacheService]
    AuditLog[Logging/DatabaseAuditLogger]
    Providers[Providers/DomainServiceProvider]
  end

  Routes --> Controllers
  Controllers --> Handlers
  Controllers --> ReposIf
  Handlers --> Commands
  Handlers --> ReposIf
  Handlers --> Pipes
  Handlers --> Jobs
  Handlers --> Audit
  Handlers --> CacheSvc
  Pipes --> ReposIf
  Pipes --> FieldTypes
  Jobs --> ReposIf
  Audit --> AuditLog
  Providers -.bind.- ReposIf
  Providers -.to.- Eloquent
  Eloquent --> Models
  Entities --- ReposIf
Loading

Dependency rule: outer layers depend inward. The Domain layer has no Laravel imports; Application orchestrates use cases; Infrastructure implements technical details; HTTP is a thin adapter that validates input and maps responses.

Layer guide

Layer Path (PSR-4) Responsibility
Domain Domain\app/Domain/ Entities, value objects, domain exceptions, repository contracts, field-type contracts and strategies. Expresses business language and invariants.
Application Application\app/Application/ Commands, handlers, submission pipeline steps, queued jobs, cross-cutting application services (e.g. audit). No HTTP; coordinates domain + ports.
Infrastructure Infrastructure\app/Infrastructure/ Eloquent repositories, Eloquent models, cache helpers, database audit logger, service provider bindings.
Presentation App\Http\ Controllers, form requests, JSON resources. Translates HTTP ↔ application commands and repository calls.

Composer maps these roots explicitly (composer.json autoload.psr-4) so namespaces stay aligned with DDD boundaries.

Project folder structure

Relevant application code lives under app/. Standard Laravel folders (bootstrap/, config/, public/, resources/, routes/, storage/, tests/, vendor/) follow the framework defaults.

app/
├── Application/                 # Use cases (orchestration, no HTTP)
│   ├── Common/Audit/            # AuditLogService
│   ├── Form/
│   │   ├── Commands/
│   │   └── Handlers/
│   └── Submission/
│       ├── Commands/
│       ├── Handlers/
│       ├── Jobs/                # ProcessSubmissionJob (queue)
│       └── Pipes/               # Submission pipeline steps
├── Domain/                      # Business model (framework-agnostic)
│   ├── Field/
│   │   ├── FieldTypeRegistry.php # Resolves field type → strategy (namespace Domain\Field)
│   │   ├── Contracts/           # FieldTypeInterface
│   │   └── Types/               # TextField, SelectField, …
│   ├── Form/
│   │   ├── Entities/, Events/, Exceptions/
│   │   ├── Repositories/        # FormRepositoryInterface
│   │   └── ValueObjects/
│   └── Submission/
│       ├── Entities/, Exceptions/, Repositories/, ValueObjects/
├── Infrastructure/              # Adapters: DB, cache, logging
│   ├── Cache/                   # FormCacheService
│   ├── Logging/                 # DatabaseAuditLogger
│   ├── Persistence/
│   │   ├── Models/              # Eloquent models + traits
│   │   └── Repositories/        # Eloquent*Repository implementations
│   └── Providers/               # DomainServiceProvider
├── Http/
│   ├── Controllers/             # Public + Admin + Auth
│   ├── Middleware/, Requests/, Resources/
├── Models/                      # Laravel User (auth)
├── Providers/                 # App, Route, …
└── Exceptions/                # Global Handler

database/
├── factories/                   # UserFactory (default password: "password")
├── migrations/                  # forms, form_fields, form_versions, …
└── seeders/
    ├── DatabaseSeeder.php       # Calls UserSeeder, FormSeeder
    ├── UserSeeder.php
    └── FormSeeder.php

Bounded contexts (conceptual)

  • Form management: lifecycle of forms, fields, schema versioning, status (draft / active / archived), and published schema snapshots (FormVersion).
  • Submission intake: validate and transform answers against the active schema, persist a submission, then hand off async post-processing (e.g. notifications, integrations).

These map to Domain\Form, Domain\Submission, and matching Application\Form / Application\Submission modules.

Design patterns and decisions

Repository pattern

Domain defines FormRepositoryInterface and SubmissionRepositoryInterface. Implementations live in Infrastructure\Persistence\Repositories and are bound in App\Infrastructure\Providers\DomainServiceProvider. Controllers and handlers depend on interfaces, so persistence can be swapped or mocked without touching domain rules.

Command + handler (application services)

Each use case is expressed as a small command object (e.g. CreateFormCommand, SubmitFormCommand) handled by a dedicated handler (e.g. CreateFormHandler, SubmitFormHandler). This keeps controllers slim and makes behavior easy to locate and test.

Pipeline (submission flow)

SubmitFormHandler runs SubmitFormCommand through Laravel’s Pipeline with ordered pipes:

  1. EvaluateConditionsPipe — conditional show/hide logic
  2. ValidateDataPipe — dynamic validation using field types
  3. TransformDataPipe — normalize submitted values
  4. SaveSubmissionPipe — persist within the same DB transaction

After the pipeline completes, ProcessSubmissionJob is dispatched for asynchronous work.

Strategy / factory for field types

Domain\Field\Contracts\FieldTypeInterface is implemented per type (TextField, NumberField, SelectField, etc.). Domain\Field\FieldTypeRegistry resolves the correct strategy from the stored type string. Validation and transformation stay extensible without a large switch scattered through the codebase.

Value objects

Types such as FormStatus, FieldType, SchemaVersion, ValidationRules, ConditionalLogic, and SubmissionStatus wrap primitives and enforce valid combinations, reducing invalid states in entities.

Domain events (on the aggregate)

Domain\Form\Entities\Form records events like FormUpdated / FormDeleted and exposes pullDomainEvents(). This models something happened in the domain separately from Laravel’s event bus. Wiring pullDomainEvents() to application or infrastructure dispatchers is a natural next step if you need cross-service reactions.

Audit logging

Application\Common\Audit\AuditLogService sits in the application layer and delegates to Infrastructure\Logging\DatabaseAuditLogger, so auditing is a port with a concrete adapter rather than domain logic leaking into raw DB calls from controllers.

Caching

Infrastructure\Cache\FormCacheService wraps Laravel’s cache repository with a fixed TTL (1 hour) and key conventions:

  • form:schema:{id} — single form resolution (admin show, public showActive)
  • form:active:list — list of active forms for public listing

Invalidation is triggered from write paths after mutations: form update/delete and field add/update/delete handlers call invalidate($formId), which clears both the per-form key and the active list key so readers do not serve stale schemas.

Reads go through remember / rememberList; writes own consistency via invalidation (cache-aside pattern).

Queue

Application\Submission\Jobs\ProcessSubmissionJob implements ShouldQueue, receives a submissionId, loads the aggregate via SubmissionRepositoryInterface, and transitions status to processed or failed with logging and retries ($tries = 3). The HTTP submit endpoint responds 202 after persistence and dispatch, decoupling the client from slow side effects.

With QUEUE_CONNECTION=sync in .env.example, jobs run inline during the request (useful locally). Use a real queue driver and php artisan queue:work in production.

Exception handling

  • Domain / application-specific: Domain\Submission\Exceptions\SubmissionException carries structured validation errors; SubmissionController@submit catches it and returns 422 with an errors payload. Other failures may surface as RuntimeException with 400 in the same action.
  • Domain invariants: e.g. InvalidFormStatusTransitionException, FormNotActiveException, SubmissionNotFoundException live under Domain\…\Exceptions for clear semantics.
  • Global handler: App\Exceptions\Handler is still the default Laravel stub. Centralizing mapping from domain exceptions to HTTP status codes (e.g. in register() with renderable) is the recommended extension point so every controller does not duplicate try/catch.

Persistence notes

Eloquent models in app/Infrastructure/Persistence/Models are infrastructure; repositories map rows to domain entities (Form, Field, Submission) and back. Form schema versions are stored as snapshots (FormVersionModel) so submissions remain interpretable even if the live form later changes.

API surface

Routes are defined in routes/api.php:

  • Public: list/show active forms, submit to POST /api/forms/{id}/submit
  • Admin (Sanctum): CRUD forms and nested fields
  • Submissions: list and show

A Postman collection is available at form_builder.postman_collection.json.

Local development

composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan serve

Optional: php artisan queue:work when using a non-sync queue driver.

Database and seeding

  1. Configure the database in .env (DB_DATABASE, DB_USERNAME, DB_PASSWORD, DB_HOST). Create the empty database if it does not exist.

  2. Run migrations so tables match database/migrations/ (forms, form fields, versions, submissions, audit logs, users, etc.).

    php artisan migrate
  3. Seed demo data (optional but useful for trying the API and Postman collection):

    php artisan db:seed

    Or recreate the schema and seed in one step (destructive: drops all tables):

    php artisan migrate:fresh --seed

What the seeders do

Seeder Purpose
UserSeeder Creates 10 users via User::factory(). Emails are random; every factory user uses the same hashed password (password) as defined in database/factories/UserFactory.php. Use any seeded row’s email with password to obtain a Sanctum token from POST /api/login.
FormSeeder Inserts one active sample form, Customer Feedback Form, with fields (text, select, textarea with conditional logic, date), plus an initial form_versions row with a schema_snapshot so submission and public endpoints have a consistent version to read.

Tips

  • After seeding, list users in Tinker to copy an email: php artisan tinker then User::pluck('email');.
  • If you re-run db:seed without migrate:fresh, FormSeeder may insert duplicate sample forms (it does not clear existing rows). Prefer migrate:fresh --seed for a clean demo, or extend FormSeeder to guard against duplicates if you need idempotent seeds.

Summary

This project uses DDD layering, repository + command/handler use cases, a pipeline for submission, strategy/factory for field behavior, cache-aside for read-heavy form metadata, and a queued job for submission follow-up. Domain exceptions express business failures; HTTP mapping is partially in controllers today and can be consolidated in the global exception handler as the API grows.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors