承接 survos/field-bundle 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

survos/field-bundle

Composer 安装命令:

composer require survos/field-bundle

包简介

Universal field/property metadata attributes for search, grid, and AI — #[Field], Widget enum, and FieldReader.

README 文档

README

Universal field/property metadata for Symfony — declare once, consume everywhere.

The problem

Property metadata is scattered across attributes with overlapping concerns:

Attribute Owner Covers
#[ApiProperty] api-platform OpenAPI description, example
#[With] symfony/ai JSON Schema constraints for LLMs
#[ORM\Column] doctrine Storage type
#[ApiFilter] api-platform Server-side filter declaration

None of them answer: how should this property be displayed and filtered in a grid, search panel, or UX-search widget?

The solution

#[Field] declares display and search behavior once, orthogonally to the other attributes:

use Survos\FieldBundle\Attribute\Field;
use Survos\FieldBundle\Enum\Widget;

class Tenant
{
    #[Field(searchable: true, sortable: true, order: 10)]
    public string $name = '';

    #[Field(filterable: true, widget: Widget::Select, facet: true, order: 20)]
    public string $status = '';

    #[Field(sortable: true, format: 'date', order: 30)]
    public \DateTimeImmutable $createdAt;
}

Attribute lanes — no overlap

#[With(description: 'Execution status', enum: ['pending', 'done', 'failed'])]  // LLM schema
#[ApiProperty('Current execution status')]                                       // OpenAPI
#[Field(filterable: true, widget: Widget::Select, facet: true)]                 // display/search
public AiTaskStatus $status;

Installation

composer require survos/field-bundle

Attributes

The bundle provides five PHP attributes covering properties, entities, and controllers.

#[Field] — property / method level

use Survos\FieldBundle\Attribute\Field;
use Survos\FieldBundle\Enum\Widget;

#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD)]
class Field
{
    public function __construct(
        ?string $transKey   = null,   // translation key override (looked up in 'fields' domain)
        bool    $searchable = false,  // include in full-text search
        bool    $sortable   = false,  // allow ordering
        bool    $filterable = false,  // expose a filter control
        ?Widget $widget     = null,   // filter UI widget; inferred from PHP type when null
        bool    $facet      = false,  // include in facet panel (sidebar, searchList, refinements)
        bool    $visible    = true,   // show by default (false = hidden but toggleable)
        int     $order      = 100,    // column display position (lower = further left)
        ?string $width      = null,   // CSS width hint, e.g. '8rem'
        ?string $format     = null,   // display format: 'date', 'datetime', 'currency', etc.
    ) {}
}

Widget inference — when widget is null, FieldDescriptor::resolvedWidget() infers from PHP type:

PHP type Inferred widget
bool Widget::Boolean
int, float Widget::Range
\DateTimeInterface Widget::Date
backed enum Widget::Select
string Widget::Text

Widget is only inferred when filterable: true. Non-filterable fields return null.

BrowsabilityWidget::Select and Widget::Boolean are "browsable" (render as selectable lists in ColumnControl / SearchBuilder / facet panels). Widget::Range, Widget::Date, and Widget::Text are filterable but not browsable — they render as input controls.

#[EntityMeta] — class level

Class-level metadata for admin UI, dashboard cards, and menu auto-registration.

use Survos\FieldBundle\Attribute\EntityMeta;

#[EntityMeta(
    icon: 'mdi:building',
    group: 'Content',
    order: 10,
    label: 'Tenant',
    description: 'A workspace that can own projects and members.',
    adminBrowsable: true,
)]
class Tenant { ... }

Parameters:

Parameter Type Default Description
icon string null UX icon name, e.g. 'mdi:building', 'tabler:user'
iconClass string null CSS class for the icon, e.g. 'text-primary'
order int 100 Position within the group (lower = first)
group string 'General' Section/submenu header in admin nav
label string null Human-readable label; defaults to short class name
description string null One-line description for dashboard cards
adminBrowsable bool true Include in admin navbar and dashboard

Discovered at compile time by EntityMetaPass, which scans all Doctrine entity directories.

Twig globals — every #[EntityMeta] entity is exposed as a Twig global keyed by APP_ENTITY_{SHORTNAME} (upper-snake of short class name). Use this to avoid class strings in templates:

{# Instead of constant('App\\Entity\\Song') #}
<twig:api_grid :class="APP_ENTITY_SONG" ... />

{# Iterate all registered entities #}
{% for descriptor in ENTITY_META.all %}
    {{ descriptor.label }}: {{ descriptor.class }}
{% endfor %}

#[RouteIdentity] — class level

Declares how an entity identifies itself in URLs. This is fundamental to Survos navigation: entities generate their own route parameters with getRp(), controllers resolve typed entity arguments from those same parameters, and templates link with path('route_name', entity.rp).

This replaces the legacy UNIQUE_PARAMETERS const pattern from survos/core-bundle and avoids repeating #[MapEntity] mappings on every controller method.

use Survos\FieldBundle\Attribute\RouteIdentity;

// Simple: single field
#[RouteIdentity(field: 'code')]
class Tenant implements RouteParametersInterface
{
    use RouteIdentityTrait;
    #[ORM\Column] public string $code;
}

// Nested: child entity walks the parent chain automatically
#[RouteIdentity(field: 'code', parents: ['tenant'], key: 'projectCode')]
class Project implements RouteParametersInterface
{
    use RouteIdentityTrait;
    #[ORM\ManyToOne] public Tenant $tenant;
    #[ORM\Column]    public string $code;
}

// $project->getRp() → ['tenantId' => 'acme', 'projectCode' => 'photo-archive']
// No manual merge — the parent chain resolves automatically.

Parameters:

Parameter Type Description
field string Property name or getter to read (e.g. 'code'$entity->code or $entity->getCode())
parents string[] Property names of associations to walk for parent route params
key string Override the URL parameter key (defaults to {lcfirst(ShortName)}Id)

RouteIdentityTrait implements getRp(), getUniqueIdentifiers(), and erp() for the entity. Pair with implements RouteParametersInterface from survos/core-bundle.

Navigation Contract

For every navigable Doctrine entity, use this pattern:

use Survos\CoreBundle\Entity\RouteParametersInterface;
use Survos\FieldBundle\Attribute\RouteIdentity;
use Survos\FieldBundle\Entity\RouteIdentityTrait;

#[RouteIdentity(field: 'id')]
class Image implements RouteParametersInterface
{
    use RouteIdentityTrait;

    #[ORM\Id]
    #[ORM\Column(length: 26)]
    public string $id;
}

Then name the route parameter after the generated key. The default key is {lcfirst(shortClassName)}Id, so Image becomes imageId, Item becomes itemId, and GalleryImage becomes galleryImageId.

#[Route('/image/{imageId}')]
final class ImageController extends AbstractController
{
    #[Route('/show', name: 'image_show')]
    public function show(Image $image): array
    {
        return ['image' => $image];
    }
}

Templates should not rebuild route parameters manually:

<a href="{{ path('image_show', image.rp) }}">{{ image }}</a>

For a custom route key, declare it on the entity and use that key in the route:

#[RouteIdentity(field: 'code', key: 'intakeCode', parents: ['tenant'])]
class Intake implements RouteParametersInterface
{
    use RouteIdentityTrait;
}

#[Route('/{tenantId}/i/{intakeCode}')]
final class IntakeController extends AbstractController
{
    #[Route('/show', name: 'intake_show')]
    public function show(Intake $intake): array
    {
        return ['intake' => $intake];
    }
}

The entity is the single source of truth for route identity:

  • entity.rp generates URL parameters.
  • RouteIdentityValueResolver resolves controller arguments.
  • Menus, labels, redirects, and templates all use the same contract.
  • If the route parameter name does not match the identity key, typed entity resolution will not run.

Migration from old pattern:

// Before (core-bundle)
class Owner implements RouteParametersInterface
{
    use RouteParametersTrait;
    public const array UNIQUE_PARAMETERS = ['ownerId' => 'code'];
}

// After (field-bundle)
#[RouteIdentity(field: 'code')]
class Owner implements RouteParametersInterface
{
    use RouteIdentityTrait;
}

#[RouteMeta] — method level

Metadata for individual controller actions. Powers sitemap generation, AI introspection, breadcrumbs, nav, and OpenAPI projection.

use Survos\FieldBundle\Attribute\RouteMeta;
use Survos\FieldBundle\Enum\Audience;
use Survos\FieldBundle\Enum\Purpose;

#[Route('/tenant/{tenantId}', name: 'tenant_show')]
#[RouteMeta(
    description: 'Public overview page for a tenant.',
    entity: Tenant::class,
    purpose: Purpose::Show,
    audience: Audience::Public,
    sitemap: true,
    changefreq: 'weekly',
)]
public function show(Tenant $tenant): array { ... }

Key parameters:

Parameter Type Description
description string Required. Dev-facing English prose. Used for AI, OpenAPI, dashboards.
entity class-string Primary entity this route operates on
purpose Purpose What the route does (List, Show, New, Edit, Delete, Export, Custom)
audience Audience Who it's for (Public, Authenticated, Admin, Api, Internal)
sitemap bool Include in sitemap.xml (defaults to true for Public routes)
changefreq string sitemap <changefreq>: always|daily|weekly|monthly|…
priority float sitemap <priority>: 0.0–1.0
tags string[] Free-form labels: ['admin', 'export', 'beta', …]
parents string[] Route names for breadcrumb parents

#[ControllerMeta] — class level

Class-level defaults for #[RouteMeta]. Avoids repeating entity:, audience:, and tags: on every action.

use Survos\FieldBundle\Attribute\ControllerMeta;

#[Route('/tenant/{tenantId}')]
#[ControllerMeta(entity: Tenant::class, audience: Audience::Authenticated)]
final class TenantController extends AbstractController
{
    #[Route('', name: 'tenant_show')]
    #[RouteMeta(description: 'Tenant detail page', purpose: Purpose::Show, audience: Audience::Public)]
    public function show(Tenant $tenant): array { ... }

    // Inherits entity: Tenant::class, audience: Audience::Authenticated from ControllerMeta
    #[Route('/edit', name: 'tenant_edit')]
    #[RouteMeta(description: 'Edit tenant settings', purpose: Purpose::Edit)]
    public function edit(Tenant $tenant, Request $request): array|RedirectResponse { ... }
}

RouteMetaPass merges class-level ControllerMeta defaults under each method's #[RouteMeta]. The method always wins for any field it sets explicitly; ControllerMeta fills the gaps.

FieldReader — reading descriptors at runtime

FieldReader is the main service for consuming #[Field] metadata programmatically. Inject it anywhere:

use Survos\FieldBundle\Service\FieldReader;
use Survos\FieldBundle\Model\FieldDescriptor;

class MyService
{
    public function __construct(private readonly FieldReader $fieldReader) {}

    public function buildSearchConfig(string $class): array
    {
        $descriptors = $this->fieldReader->getDescriptors($class);

        return [
            'searchable' => array_map(fn (FieldDescriptor $d) => $d->name,
                array_filter($descriptors, fn ($d) => $d->searchable)),
            'sortable'   => array_map(fn (FieldDescriptor $d) => $d->name,
                array_filter($descriptors, fn ($d) => $d->sortable)),
        ];
    }

    // Get a single property descriptor
    public function getLabel(string $class, string $property): string
    {
        $d = $this->fieldReader->getDescriptor($class, $property);
        return $d?->getFallbackLabel() ?? $property;
    }
}

FieldDescriptor properties

Property Type Source
name string Property/method name
type string PHP type (e.g. 'string', 'int', 'App\Enum\Status')
transKey ?string #[Field(transKey:)] or null
description ?string #[With], #[ApiProperty], or null
example mixed #[With], #[ApiProperty], or null
searchable bool #[Field] or #[ApiFilter(SearchFilter)]
sortable bool #[Field] or #[ApiFilter(OrderFilter)]
filterable bool #[Field] or #[ApiFilter]
widget ?Widget #[Field(widget:)] or inferred
facet bool #[Field(facet:)]
visible bool #[Field(visible:)]
order int #[Field(order:)]
width ?string #[Field(width:)]
format ?string #[Field(format:)]
enum scalar[] Backed enum cases, or #[With(enum:)]
minimum int|float #[With(minimum:)] or #[Range] constraint
maximum int|float #[With(maximum:)] or #[Range] constraint
maxLength ?int #[Length(max:)] constraint
pattern ?string #[Regex(pattern:)] constraint
required bool #[NotBlank] constraint
isUrl bool #[Url] constraint
isEmail bool #[Email] constraint

Key methods:

$d->getTranslationKey()     // transKey ?? name
$d->getTranslationDomain()  // 'fields' (always)
$d->getFallbackLabel()      // TitleCase of name, e.g. 'accountType' → 'Account Type'
$d->resolvedWidget()        // widget ?? inferred from type (null when not filterable)
$d->inputType()             // HTML input type: 'email'|'url'|'number'|'datetime-local'|'text'

Progressive enhancement sources

FieldReader enriches descriptors when optional packages are present:

Source Package What it adds
#[Field] (this bundle) All display/search settings
Symfony validation symfony/validator required, isUrl, isEmail, minimum, maximum, maxLength, pattern
#[With] symfony/ai-platform description, example, enum, minimum, maximum
#[ApiProperty] api-platform/core description, example
#[ApiFilter] on class api-platform/core searchable, sortable, filterable (fallback when no #[Field])
#[MeiliIndex] on class survos/meili-bundle searchable, sortable, filterable (synthesized fallback)
PHP reflection (always) type, backed enum cases

Fallback synthesis — properties with no #[Field] but referenced in #[ApiFilter] or #[MeiliIndex] get a synthesized descriptor so the grid still shows them correctly. Add #[Field] to take explicit control.

Widget mapping across consumers

Widget ColumnControl (api-grid) Meilisearch (meili-bundle) UX-Search
Text search input searchable SearchBox
Select searchList dropdown RefinementList RefinementList
Range Min/Max number inputs RangeSlider RangeSlider
Date (future) NumericMenu DateRangePicker
Boolean searchList dropdown Toggle ToggleRefinement

Zero required dependencies

#[Field] and Widget have no external dependencies — just PHP 8.4. FieldReader enhances output progressively based on what packages are installed.

Consumers

Bundle What it uses
survos/api-grid-bundle FieldReader::getDescriptors() → column sortable/searchable/browsable/width/widget
survos/grid-bundle DataTables column config
survos/meili-bundle Meilisearch searchable/filterable/sortable/facet index settings
survos/inspection-bundle Unified FieldDescriptor DTO for Twig templates and admin tooling

Further reading

  • docs/CONTROLLERS.md — Survos controller naming convention: XxxController (entity) vs XxxListController (collection). Covers #[RouteMeta], #[ControllerMeta], #[RouteIdentity], entity injection, and testing patterns.

survos/field-bundle 适用场景与选型建议

survos/field-bundle 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 447 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 04 月 26 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「symfony」 「metadata」 「search」 「grid」 「datatables」 「field」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 survos/field-bundle 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 survos/field-bundle 我们能提供哪些服务?
定制开发 / 二次开发

基于 survos/field-bundle 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 447
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 48
  • 依赖项目数: 11
  • 推荐数: 3

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-04-26