Skip to Content
FrontendCrud V2Crud GeneratorGenerator v2Metadata API Usage

Metadata API Usage

This page documents every metadata key the generator reads, where it reads it from, and what it produces out of it.


1. Metadata Entities

To get the list of all the metadata entities the generator calls {{BASE_URL}}/{{API_VERSION}}/metadata/entities and reads data.entities, which is the array of FQCNs.

This entity path is the folder key for everything:

config/versioned/json/{entityPath}/{version}/ config/versioned/tsx/{entityPath}/{version}/

This API is only called by the …All commands (generateJson all, generateTsx all, validateAll). For a single entity we pass --entity=AccountingBundle/Account and the FQCN is built locally by encodeEntityName(), so there is no need to call the entities list.

See also: Commands & CLI Flags.


2. Metadata Entity

For one entity we are not calling /metadata/entity/{FQCN} once but 4 times with different query strings, because each call returns a different set of fields.

1st call — no query string

From this one we take: fields, unfiltered_fields, virtual_properties, displayable_properties, resourceName, class_object_data.tableName, class_object_data.implemented_interfaces, availableSerializationGroups, writable_properties, updatable_properties, writable_required_properties / required_properties, apiFilters and operations.

2nd call — Show groups

?groups[]=… — from this we merge fields + virtual_properties and it becomes our showFields.

3rd call — Listing groups

?groups[]=… — same thing, fields + virtual_properties merged, and it becomes our listingFields.

4th call — ?requiredOnly=true

From this we take required_properties and fields to build the requiredFields.

The 1st and 4th calls are running in parallel, and after that the 2nd and 3rd are running in parallel, because they need availableSerializationGroups which comes from the 1st call.

Whatever comes from these calls is saved as it is in data.json. After that the TSX layer is only reading data.json — the API is never called again during generateTsx.


3. Serialization Groups

First we fetch availableSerializationGroups and then createEntitySerialization() in utils/functions.js decides which groups to send.

The convention is {bundle}{Entity}{Suffix}, with the first letter in lowercase and the bundle without its Bundle suffix … so AccountingBundle/Account + Show becomes accountingAccountShow.

If this group is not present in availableSerializationGroups then it falls back in this order:

{entity}{Suffix} → {bundle}{Suffix} → listing → show

After that a few more groups are appended if they exist:

  • Show: show, standardShow
  • Listing: standardListing, tagTaggingList, id, listing, {entity}tagging{Suffix}, {entity}taggingList, {group}tagging{Suffix}

The final query string is built in the array style:

?groups[]=accountingAccountShow&groups[]=show&groups[]=standardShow

The old ?group=a,b style is still there in the code but commented, for the CRUDs which are not migrated yet.

The matching is case-insensitive, so tagtagginglist written in the code is still matching tagTaggingList coming from the API.


4. Resource Name

resourceName is the main source of truth for the generator as well. It is printed in the tsx as const resource: string = 'accounts'.

There are a few actions which are switched directly on the resourceName:

resourceNameConditionAction
userscrud-link
usersaccountCrudsend-invite
dashboard_reportsreport
dashboard_widgetswidget
dataanalysis_datacollectorsdata-collector
dataanalysis_dataprocessorsdata-processor
templatestemplate
ticketsticketCrudall the ticket actions

users is also the only resource where we keep the slug field in the quick forms.


5. Class Objects

class_object_data has many keys but the generator is only reading two of them.

  • tableName — used by the frontend for the customProperties (/${apiVersion}/custom_properties?tableName=${tableName}). We just take it from the API and print it in the config as const tableName: string = '…'. See Custom Properties.
  • implemented_interfaces — the biggest deciding factor in the whole generation. They are mapped in ImplementedInterfaceMapping (src/templates/template-generator.js) and also printed in the tsx as the implementedInterfaces object.

Interfaces and their impact

InterfaceImpact in the generated config
TaggableThe tag filter is added in filterFields.all and a SELECT_ASYNC tag filter is generated in the tsx
DisableableThe enabled filter is added (default true) and the disable action as well, but only if disabledAt is also present in updatable_properties
SoftDeleteabledelete action, but this one is used only when operations is not answering it already
Prioritizablepriority-up / priority-down actions and the Priority section on the show page
NestedEntityInterface (treeview)tree-up / tree-down, the Position section and the tree fields handling
TranslatableThe Translatable section on the show page with title and description
Printableprint-template action
Emailableemail-template action
SequenceableNot read from the entity itself but from the target entity — more about it in point 12
Sluggable, Defaultable, Timestampable, StringableJust passed to the tsx implementedInterfaces object, no behaviour is attached to them for now

For the backend side of these interfaces see Entity Interfaces & Behaviors, and for the template side see Entity Interfaces & Template.

used_traits, role_prefix, parentRole, shortname and defaultDataSkeletonUri are not read by the generator at all.


6. Operations

operations is the main deciding factor, exactly as mentioned in the backend doc. We store it as it is in data.json and then it is interpreted in createVersionedChangesTemplate().

The numbered objects like "0": {"method": …, "url": …, "groups": {…}} are the ones we read. The generator loops on all of them and checks the method: POST → add, PUT/PATCH → update, DELETE → delete. The old boolean style (operations.add === false) is also still supported.

  • From this we build isNotCreatable which is true when there is no POST, or when writable_properties is explicitly empty. In that case formFields.sections is generated empty and the duplicate, quick-duplicate and default-value actions are removed.
  • isNotEditable is true when there is no PUT/PATCH, or when the updatable properties are explicitly empty. In that case edit, quick-edit and inline-edit are removed and the edit modal fields stay empty.
  • hasDeleteable is taken from the DELETE method, and only if operations is not there at all we fall back on the SoftDeleteable interface.
  • If operations is null or missing we assume everything is allowed, otherwise an older backend would silently lose all its forms.
  • The url and the groups (normalization / denormalization) inside each operation are stored but we are not using them, because the groups are already decided from availableSerializationGroups as explained in point 3.

7. Required Properties

required_properties is coming from the ?requiredOnly=true call and we use it to build the requiredFields … we take each name and look for it in fields first, and if it is not found then in virtual_properties, just to get the complete metadata of that field.

writable_required_properties (with required_properties as fallback) is the one which fills formFields.required, so it decides which inputs are marked as required: true.

There is one deviation here. The boolean fields are removed from formFields.required even if the API says they are required. It is a temporary workaround for the frontend issue phpreaction-frontend-crud-react-v2#838 .


8. Displayable Properties

displayable_properties is used for only one decision today, to check if uniqueId exists or not.

If it exists then uniqueId is placed in listingFields.selected and id is moved to hideSelected, otherwise we keep showing id.


9. Writable Properties

writable_properties is filtering the add form and the add modal fields.

One important thing: an empty array is not the same as a missing key. If the API returns it as empty we treat it as “this entity can’t be created”.

A virtual property can come inside a form only if it is also present in writable_properties, and after that the required/optional split is done with required_properties.


10. Updatable Properties

updatable_properties is filtering the edit modal fields.

  • If the key is not present at all we fall back on writable_properties.
  • Same as above, an empty array means “this entity can’t be edited”.
  • It is also checked for the disable action … the Disableable interface is not enough, disabledAt must also be present in updatable_properties.

11. Unfiltered Fields

unfiltered_fields is majorly used to generate the sideboxes, which is still matching the original intention even if the API is returning relational and non-relational fields together.

We only pick the entries where the type is MANY_TO_MANY or ONE_TO_MANY and they become sideboxFields.all as {fieldName, targetResource, targetEntity}. For the name we prefer inversedBy, then mappedBy, and at the end fieldName.

The same keys are then excluded from the show and form sections, otherwise the same relation was getting rendered twice (generator issue frontend-components-crud-react-generator#258  — sidebox fields excluded from showFields, referenced in src/core/version-manager.js:876).


12. Virtual Properties

virtual_properties is merged inside the show and listing field pools on every serialization call.

toString is always available … even if the API doesn’t return it we force it as {fieldName: 'toString', type: 'string'}, and on the show page it is placed first in the Identification section as the identifier of the record.

As said above these can’t be created/edited directly, so a virtual field can only reach a form through writable_properties / updatable_properties.

showFields.sequencable is the only place where the generator is reading another entity’s metadata. For each MANY_TO_ONE field it takes the targetEntity, converts it to the entity path, opens the latest data.json of that entity and checks if it implements Sequenceable. If that entity is not generated yet then the flag is simply skipped, so generating the full bundle twice gives a better result than generating it once.


13. Writable Required Properties

writable_required_properties is taken as it is from the API and it is the primary source for the required form fields. The generator is not calculating the intersection by itself.

required_properties is only used as a fallback when this key is missing.


14. API Filters

apiFilters is used for the filters on the listing page and for the sorting. extractFromApiFilters() is looping on every filter class and on the properties of every filter entry.

  • Sorting — we use PHPReaction\Api\Filter\OrderFiltergobal.properties (the API’s spelling) and it becomes sortableFields in data.json and then listingFields.sortable.
  • The dotted paths are splitted, so translations.title is displayed as title and we keep sortableKeys: { title: 'translations.title' } so that the frontend always sends the correct sort key. For defaultOrder we take the first available one between priority: asc, title: asc and id: desc.
  • Search — we use ApiPlatform\Doctrine\Orm\Filter\SearchFilter → it fills filterFields.searchMethods and the strategies are ordered as partial, start, end, exact. When the properties is an array (relation filters) then no search method is stored for it.
  • Ranges — we use RangeFilter and DateFilter and their properties go to filterFields.range.
  • For TranslationSearchFilter, and also for any property starting with translations., we keep the base field as translatable and we skip it from the normal fields list.
  • The text fields are removed from the filters list, because a textarea is not really a filter input.
  • By default these filters are enabled: id, type, tag, enabled, plus whatever the legacy scraper is giving us as listing filters for that entity.

15. Fields

fields is used to generate the fields metadata in the generator, however from each field object we are reading only a few keys.

KeyUsage
fieldNameThe key in the tsx, and also the source of the label (capitalize() → “Account Name”)
typeIt decides the ColumnTypeEnum through FieldTypeGeneration() and also the relation detection
formatIt refines the type, money → CURRENCY and percent → PERCENT
nullablePrinted on the boolean inputs
targetEntityThe FQCN of the relation target, used for the sequencable lookup and the sidebox
targetEntityResourceNametargetResourceAsync on the async selects, and the sidebox target
targetEntityResourceIriprefixOnSubmit on the async selects
inversedBy / mappedByThe preferred name of the sidebox field
constraintsOnly the presence is checked, as a tiebreaker when we have to choose a fallback sortable column

A few things about the type mapping: the 4 relation types → SELECT_ASYNC, text → TEXTAREA (and directives → SQL_QUERY), date/datetime (with _immutable also) → DATE/DATETIME, integer/smallint → NUMBER, decimal/float → DECIMAL/FLOAT, json/json_document → JSON. On the strings we also check the name, like icon, color, *Phone*, *Email*, *url*, *password*. And there is a hardcoded list of money fields (total, amount, price …) which forces CURRENCY.


16. What the generator is producing at the end

/metadata/entities and the 4 /metadata/entity/{FQCN} calls are producing config/versioned/json/{Entity}/vN/data.json which is the raw snapshot of the API.

The keys of data.json, in order:

entity resourceName tableName implementedInterfaces fields showFields listingFields unfilteredFields virtualProperties displayableProperties requiredFields requiredProperties writableProperties updatableProperties operations availableSerializationGroups apiFilters sortableFields _generatedAt _generationMode

After that changes.json is generated with all the defaults calculated from the keys mentioned above, and then it is customized by hand — see the changes.json Reference.

index.tsx is generated only from data.json + changes.json, without touching the API.

On the regeneration, the API driven arrays (sortable, all, required, fields, range) are merged with the previous version values, but selected, hide and order are considered as the user’s choice and they are kept as they are. More in Workflow & Versioning.

bundleCrud is not coming from the API, we pass it ourselves with --bundleCrud, but it ends up in the generated config as the FQCN Bundle (fqcn_bui.Bundle) and the entity simple name goes as the Unit (fqcn_bui.Unit). From these two we derive fqcn_bui_listing, fqcn_bui_create, fqcn_bui_edit and fqcn_bui_show. It is also used in a couple of places to switch the actions, like accountCrud + userssend-invite and ticketCrud + tickets → the ticket actions.


17 Not used by the generator

  • From the entity metadata: used_traits, role_prefix, parentRole, shortname, defaultDataSkeletonUri, database_required_properties, hidden_from_serialization_properties, hidden_oneToMany_relations_properties, the top level constraints and success.
  • From operations: the url and the groups of each nested object.
  • From fields: everything listed in 15.12.

There are many other things that have been skipped for now, which are either internal to the generator or not coming from the API at all.

Last updated on