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 Laravel Package

rogerdsh/liform

Liform is a lightweight form-building package for Laravel. Define forms in PHP, render them in Blade, and handle validation with minimal boilerplate—keeping markup consistent and controllers clean. A simple fit for admin panels and small apps.

View on GitHub
Deep Wiki
Context7

Getting Started

First, a caveat you should act on before writing any code: this package has 0 stars, 0 dependents, and no published documentation. The name, namespace (Liform\) and description strongly suggest it is a re-publish/fork of limenius/liform, the long-standing "Symfony Forms → JSON Schema" library. Treat the README (if any) as aspirational and the source as the spec. Before adopting, diff it against upstream limenius/liform — you may be better off requiring the upstream package directly, or vendoring this one at a pinned commit.

1. Install

composer require rogerdsh/liform

If Packagist has no usable release (likely), pin the repo instead:

{
  "repositories": [
    { "type": "vcs", "url": "https://github.com/RogerDS/Liform" }
  ],
  "require": {
    "rogerdsh/liform": "dev-master#<full-commit-sha>"
  }
}

Never float on dev-master for this one — there is no maintainer signal and no test/dependents safety net.

2. Where to look first (in this order)

  1. src/Resolver.php — the public entrypoint. Everything hangs off it: setForm(), setDefaultTransformers(), addTransformer(), transform().
  2. src/Transformer/FormTransformer.php — how type: object, properties, and required are produced.
  3. src/Transformer/ConstraintTransformer.php (or similarly named) — the Symfony Validator constraint → JSON Schema keyword mapping. This is where 80% of your surprises will live (NotBlank → minLength, Regex → pattern, etc.).
  4. tests/ — golden-schema fixtures are the real documentation for this class of library.
  5. composer.json — check the symfony/form constraint. If it's ^3.4|^4.0 and you're on Symfony 6/7, expect breakage from FormConfigBuilder/getBlockPrefix changes.

3. Minimal working example

use Liform\Resolver;

// Build the form the same way your controller does. Use null data —
// the schema describes the shape, not the values.
$form = $formFactory->create(ProductType::class, null, [
    'csrf_protection' => false,
]);

$resolver = new Resolver();
$resolver->setForm($form);
$resolver->setDefaultTransformers();

$schema = $resolver->transform();

return new JsonResponse($schema);

If a bundle/service definition is shipped, inject the resolver rather than new-ing it:

public function schema(Resolver $resolver, FormFactoryInterface $forms): JsonResponse
{
    $resolver->setForm($forms->create(ProductType::class));
    $resolver->setDefaultTransformers();

    return $this->json($resolver->transform());
}

4. First use case to prove out

A single GET /api/schemas/product endpoint feeding a client-side form renderer (react-jsonschema-form, @rjsf/core, or ajv for pre-submit validation). Keep it narrow: one form type, flat fields, no collections, no entity types. That exercises the whole pipeline (form build → transformer → constraint mapping) with the least surface area, and tells you within an hour whether the package is viable for you.

Implementation Patterns

Thin service wrapper, not raw resolver calls

Do not scatter new Resolver() through controllers. Wrap it once so you have one place to patch when you inevitably fork a transformer:

final class SchemaGenerator
{
    public function __construct(private FormFactoryInterface $forms) {}

    public function forType(string $formType, array $options = []): array
    {
        $form = $this->forms->create($formType, null, $options + [
            'csrf_protection' => false,
        ]);

        $resolver = new Resolver();
        $resolver->setForm($form);
        $resolver->setDefaultTransformers();

        return $resolver->transform();
    }
}

Cache aggressively — schemas are static

A schema depends only on the form type class + options, not on the request. It is a pure function output, and it is slow (it builds a full form tree and runs every transformer, including constraint reflection). Cache it:

$schema = $this->cache->get($key = 'schema.'.$formType, function () use ($formType) {
    return $this->schemaGenerator->forType($formType);
});

Invalidate on deploy (cache:clear in CI) or key on a build hash. Do not key on user input — the schema doesn't vary by user, which is also why you should be careful what you put in it (see Gotchas).

Constraint-driven schema is the whole value proposition

The reason to use this over hand-writing JSON Schema is that your Symfony constraints become the client-side contract. Lean into it:

$builder->add('email', EmailType::class)
    ->add('age', IntegerType::class, [
        'constraints' => [new Assert\Range(['min' => 18, 'max' => 120])],
    ])
    ->add('sku', TextType::class, [
        'constraints' => [new Assert\Regex(['pattern' => '/^[A-Z]{3}-\d{4}$/'])],
    ]);

→ format: email, minimum/maximum, pattern. Write an integration test asserting those keywords appear, because the mapping table is the part most likely to be incomplete in a low-traffic fork.

Custom transformers for domain types

Symfony's EntityType/ChoiceType with a query builder and FileType will not produce meaningful JSON Schema on their own. Register a transformer that intercepts your form type and emits a scalar:

$resolver->setForm($form);
$resolver->setDefaultTransformers();
$resolver->addTransformer(new CategoryTransformer()); // your class, implements the package's transformer interface

Look at how the package's own FieldTransformer implements the interface and mirror that signature exactly — the constructor/transform signature is the integration point, and it varies between upstream versions.

Nested forms and collections

For CollectionType, the output should be {"type": "array", "items": {...}}. Verify it does that rather than emitting properties: ["0", "1", ...] from a built prototype. If it's wrong, the fix is usually in FormTransformer recursing into $child->getConfig()->getType()->getInnerType().

Snapshot tests as regression guard

Because there's no upstream you can rely on to not break you, put a golden-file test around every form type you expose:

public function testProductSchemaUnchanged(): void
{
    $schema = $this->generator->forType(ProductType::class);
    $this->assertJsonStringEqualsJsonFile(__DIR__.'/fixtures/product.json', json_encode($schema));
}

This turns any accidental form-type edit (someone adds a field) into a deliberate, reviewable schema diff.

Frontend contract concerns

Decide early whether clients consume the schema only for rendering or also for validation. If validation, you need the dialect to match your validator (draft-07 for most ajv setups; older limenius emits draft-04). Pin ajv to the matching meta-schema or you'll get "unknown keyword" noise.

Gotchas and Tips

Zero-traffic dependency risk is the headline gotcha. 0 stars / 0 dependents / score 0 means: no issue tracker activity, no security response, no Symfony-version upkeep. Plan for one of these:

  • Fork it into app/src/Liform (or a private VCS) and own it. It's a few thousand lines and mostly pure functions — this is genuinely the low-risk path.
  • Or use limenius/liform upstream, which at least has history and existing forks you can search for fixes.
  • Or generate JSON Schema by hand from the form type and skip the dependency entirely — for <10 forms this is often cheaper than debugging a black box.

required will not mean what you think it means. Symfony's required option is a rendering hint (HTML5 required attribute), not a validation rule. NotBlank/NotNull constraints are the real thing. A schema transformer that reads the required option will emit a JSON Schema that rejects "" on the client while the server accepts it (or vice versa). Test with an empty-string payload specifically.

data_class and data transformers leak. If you pass an entity as form data, or a model transformer is registered, transformation can throw or produce garbage. Always call setForm with a form built against null data. If a transformer forces a value, you'll see it as a default in the schema — sometimes useful, often a stale snapshot of a database row in an API response.

ChoiceType with a lazy/dynamic choices callback freezes the enum at schema-build time. If choices come from a DB, your cached schema becomes wrong the moment a category is added. Either bust the cache on writes, or override that field with an enum sourced at request time and keep the static schema free of enum.

File and entity fields need suppression. FileType has no JSON Schema representation and EntityType will try to resolve a class to a schema. Override those to {"type": "string", "format": "binary"} and {"type": "integer"} respectively, or exclude them from the form passed to the resolver.

Symfony version drift is your most likely failure mode. The package likely targets Symfony 3.4/4.x Form internals. On Symfony 6/7 watch for: return types on buildForm, getBlockPrefix removal, FormConfigInterface::getType() returning ResolvedFormTypeInterface with a deprecated getInnerType(). Errors surface as fatal TypeErrors deep inside a transformer, not as friendly exceptions.

Debugging is by var_dump, not stack traces. Transformer exceptions tend to lose context. Wrap and dump the form tree first, then the transform:

try {
    $schema = $resolver->transform();
} catch (\Throwable $e) {
    dump($form->all());            // what children actually exist
    dump(get_class($e), $e->getMessage());
    throw $e;
}

Transformer ordering matters. setDefaultTransformers() establishes the chain; if you addTransformer() before it, yours may be clobbered, and vice versa it may shadow built-ins globally. Add custom transformers after defaults and scope them tightly (match on a specific form type, not a generic FormType).

Extension points to lean on. The resolver's transformer registry and each transformer's per-field output are your two levers. You almost never need to touch FormTransformer's object/properties assembly — you need to intercept at the field level. That keeps your patch small when you next upgrade.

Don't expose more than you intend. The schema is built from the same form used for writes. If that form has admin-only or internal fields, the schema endpoint leaks their existence, labels, and constraints. Either maintain a separate *SchemaType per endpoint or strip fields from the array before returning it — the latter is brittle, the former is the pattern worth copying.

Internationalization surprises. Labels and title/description are pulled via the translator at build time. If your schema is cached, users switching locale get the wrong-language schema. Cache per-locale, or strip titles from the response and let the client localize.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core