Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Liform Bundle Laravel Package

rezozero/liform-bundle

Symfony bundle that converts Symfony form definitions into JSON Schema, letting front-end apps render forms from a single source of truth. LiFormBundle serializes form types, fields and options into portable JSON, keeping server-side and client-side forms in sync.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check most "Getting Started" sections bury: this is a Symfony bundle, not a Laravel package. There is no bundle kernel in Laravel, no config/bundles.php, no service container extension to auto-load. The bundle is thin glue around liform/liform, which in turn hard-depends on symfony/form. So "getting started" in a Laravel codebase means one of two things:

  1. If you have a Symfony app somewhere in the org — read the bundle, install it there, consume the schema over HTTP.
  2. In Laravel — you don't install the bundle. You install the library it wraps and treat the bundle as a reference implementation.

Where to look in the repo, in order:

  • src/LiformBundle.php and src/DependencyInjection/ — confirms how little the bundle does (registers a generator service, maybe a Twig extension/helper). This is your "there is nothing magic here" validation.
  • Resources/config/ — the service definitions tell you exactly which liform class to instantiate standalone.
  • tests/ fixtures — the fastest way to learn the output shape. Read expected schema JSON for a few form types before writing any code; it tells you the JSON Schema draft level and which constraints actually map.
  • Then the upstream liform/liform generator and its extension/visitor classes — that's where the real translation logic lives.

Minimal path in Laravel (Path A spike):

composer require liform/liform symfony/form
# pin exact versions; commit composer.lock

Do not attempt to boot the bundle or Symfony\Bundle\FrameworkBundle. Construct the factory yourself:

use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\Forms;

$this->app->singleton(FormFactoryInterface::class, fn () =>
    Forms::createFormFactoryBuilder()->getFormFactory()
);

Define a plain form type (no EntityType, no dynamic listeners):

class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $b, array $o): void
    {
        $b->add('name', TextType::class)
          ->add('price', NumberType::class)
          ->add('status', ChoiceType::class, [
              'choices' => ['draft' => 'draft', 'live' => 'live'],
          ]);
    }
}

Generate and return:

$form   = app(FormFactoryInterface::class)->create(ProductType::class);
$schema = (new LiformGenerator())->getSchema($form);

return response()->json($schema);

First real use case: one small, static admin form (settings or a simple CRUD) exposed at GET /api/schemas/{formKey} and rendered by a JSON Schema renderer. Do not start with your gnarliest entity-picker form — it will look bad and tell you nothing.

Implementation Patterns

Isolate at one seam, immediately. The single most important decision: every reference to Symfony classes lives behind one interface.

interface FormSchemaGenerator {
    /** @return array<string,mixed> */
    public function for(string $formKey): array;
}

Nothing else in the app imports Symfony\Component\Form\*. This makes swapping in a Laravel-native rules-to-schema translator later a one-file change, which matters given the maintenance posture.

Form catalogue, not dynamic registration. Define 3–5 form types as plain PHP classes keyed by formKey. Resist database-driven or config-driven form construction — Symfony Form's statefulness is exactly where the JSON Schema translation degrades.

Choices: precompute, don't query. For ChoiceType, feed static choices arrays. EntityType with a query_builder has no data at schema-generation time — you'll get an empty or exploded enum, or worse, a silent object type. If the real choices live in the DB, define the enum statically and hydrate labels client-side; document that the schema's enum is advisory.

Treat the output as a client hint, never as server authority. Symfony Validator constraints live outside the Form component; only the subset liform maps surfaces as minLength/pattern/enum/bounds/required. Your Laravel FormRequest rules remain the source of truth. A permissive schema means the frontend accepts input the server will 422 — plan the error UX for that path explicitly, because JSON Schema carries no validation messages.

Contract-test each form against a golden file. With zero upstream users to absorb the cost of finding regressions, this is your only safety net:

public function test_product_schema_is_stable(): void
{
    $actual = app(FormSchemaGenerator::class)->for('product');
    $this->assertJsonStringEqualsJsonFile(
        base_path('tests/fixtures/schemas/product.json'),
        json_encode($actual, JSON_PRETTY_PRINT)
    );
}

Review changes to those fixtures in code review like you'd review a migration — a schema diff is an API change.

Cache aggressively and version the response. Generation is cheap but not free, and it's pure for static form types. Cache per formKey, key by an app release/hash, and include a version field plus $id/$schema in the payload so consumers can detect staleness.

Emit through a controller endpoint, not templates. No Twig in Laravel, and embedding schemas in a Blade view bypasses HTTP caching and makes the contract invisible to the frontend team.

Document the draft level. Older liform output tracks draft-04 semantics. If your renderer assumes draft-07/2020-12 (if/then, dependentRequired, $defs), either normalize the output through your generator facade or pick a renderer that accepts draft-04. Decide once, write it down.

Add a custom liform extension rather than patching vendor when you need a bespoke field type or constraint mapping — the generator accepts extensions, and vendor patches will be lost on the next composer update.

Gotchas and Tips

The bundle will not boot in Laravel — do not try. Any attempt to register a Symfony kernel or service extension is a dead end. Register only the Form factory and the extensions you actually need.

Runtime state is silently dropped. PRE_SET_DATA / PRE_SUBMIT listeners, entity-aware form types, and collection prototypes with add/remove UI have no faithful schema representation. The generator walks the form definition, not a bound, event-dispatched instance. If your schema is missing something, it's usually because the logic lived in an event listener.

Data transformers flip the shape. A date string ↔ DateTime transformer, or an ID ↔ entity transformer, means the emitted schema describes the transformed shape, not the payload the browser posts. Always test your endpoint against a real request, not just the schema.

mapped => false, hidden fields, and CSRF tokens. Standalone (no HttpKernel/session), CSRF may be disabled or may emit a _token field depending on the factory config. Check the output and strip infrastructure fields in your facade before returning — don't let _token leak into a public API contract.

Custom field types fall back to string/object. guessType maps unknown types to something generic. Verify each custom type in your catalogue maps to the JSON Schema primitive you expect; add an explicit mapping in a custom extension when it doesn't.

Debugging recipe: dump($form->all()) and walk $form->getConfig() to see the tree the generator sees; diff that against the emitted properties. A missing schema field is nearly always (a) an unmapped/unmapped-by-guess type, (b) a listener-driven addition, or (c) an EntityType with no options. This is faster than reading liform's visitors.

Collection types: array items are generally expressed, but min/max entry counts, prototypes, and server-side Count/All constraints usually are not. Enforce them in Laravel and mirror the rule by hand in the frontend.

Dependency risk is the headline, not a footnote. 0 stars, 0 dependents, single-agency provenance. Assume no CVE triage and no Symfony 7 / PHP 8.3 release. Pin exact versions, commit the lock file, run composer audit on the transitive tree in CI, and be prepared to vendor or fork. The bundle's thinness means forking it costs hours, not weeks — treat that as the plan, not the fallback.

Don't put it on the critical path. Timebox a spike against 3–5 real forms. If mapped-constraint coverage is below roughly 80% (i.e., the schema can't actually render or validate the fields that matter), abandon it and take schema-first with opis/json-schema or a Laravel-rules-to-schema translator instead. The reusable takeaway is the pattern — one canonical form definition producing both server validation and a client-consumable schema — not the package.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment