Reading values in PHP
Every entity VX can hold fields for has a facade class with the same field() method:
php
$class = \VxFramework\Post::get( get_the_ID() );
$style = \VxFramework\Term::get( $term_id );
$user = \VxFramework\User::get( get_current_user_id() );Option pages have the same facade: \VxFramework\Option_Page::get( 'site_settings' ). So do comments: \VxFramework\Comment::get( $comment ) takes a comment or its id, such as the comment a wp_list_comments() callback receives. To collect comment fields on your comment form, see Comment forms.
field( $key ) returns a Field_Value, which you read in the shape you need:
php
echo esc_html( $class->field( 'tagline' )->string() );
$capacity = $class->field( 'capacity' )->int();
$is_free = $class->field( 'free_entry' )->bool();Reading a field that doesn't exist, or is empty, never throws. It returns an empty value, so ->string() gives '' and ->int() gives 0.
->int() drops decimals, and a Number or Stepper accepts them whatever its step. Read one with ->float(), or let ->value() return whichever of the two the stored value is.
php
if ( $class->field( 'tagline' )->filled() ) {
echo '<p class="tagline">' . esc_html( $class->field( 'tagline' )->string() ) . '</p>';
}The natural shape: value()
value() returns each field type's natural PHP shape, so you don't have to pick an accessor:
| Field types | value() returns |
|---|---|
| Text, Textarea, Email, Phone, URL, Color, Rich text, dates and times | string |
| Number, Stepper, Slider | int, float or null |
| Switcher | bool |
| Select, Radio Buttons, Button Group | [ 'value' => …, 'label' => … ] |
| Multiselect, Checkboxes | a list of [ 'value', 'label' ] |
| List | string[] |
| Image, Video, Audio, SVG | a Media_Value, or null |
| Gallery | Media_Value[] |
| Select Post, Select User, Taxonomy | a facade, or a list of them |
| Group, Code, Date range, Range slider | an associative array |
| Repeater | a list of associative arrays |
| Files | Media_Value[] |
| Post Relation, User Relation | a facade, or a list of them |
| Location | an associative array |
| Field Table, Content sections, Recurring Date | a list of associative arrays |
| Password, layout fields | null |
raw() gives the value exactly as stored, and the typed accessors below force a specific shape.
Choices
php
$class->field( 'price_range' )->option(); // 'mid' (the stored value)
$class->field( 'price_range' )->option_label(); // '$$' (the label)
$class->field( 'amenities' )->options(); // [ 'wifi', 'parking' ]
$class->field( 'amenities' )->option_labels(); // [ 'Wi-Fi', 'Parking' ]Labels come from the field's choices, so they match what dynamic tags show.
Media
php
$logo = $class->field( 'logo' )->image(); // ?Media_Value
echo esc_url( $logo?->url( 'medium' ) ); // a size, falling back to full
echo esc_attr( $logo?->alt() );
foreach ( $class->field( 'photos' )->gallery() as $photo ) {
echo wp_get_attachment_image( $photo->id(), 'large' );
}A Media_Value has id(), url( $size ), alt(), title(), caption(), description(), mime() and path().
Dates
php
$opens = $class->field( 'class_date' )->date(); // ?Date_Value
echo esc_html( $opens?->format( 'j F Y' ) );
$opens?->datetime(); // ?DateTimeImmutable
$opens?->timestamp();A stored date is the calendar date the editor picked, so it is read in the site's timezone (Settings → General): 2026-03-01 prints as 1 March wherever the site is.
Posts, users and terms
php
foreach ( $class->field( 'instructors' )->posts() as $instructor ) { // \VxFramework\Post[]
echo esc_html( $instructor->get_title() );
echo esc_html( $instructor->field( 'tagline' )->string() );
}
$manager = $class->field( 'organizer' )->user(); // ?\VxFramework\User
$ids = $class->field( 'instructors' )->ids(); // int[]
$styles = $class->field( 'style' )->terms(); // \VxFramework\Term[]Groups and rows
php
// Group: one record
$address = $class->field( 'address' )->group();
echo esc_html( $address?->field( 'city' )->string() );
// Repeater: a collection of rows
foreach ( $class->field( 'opening_hours' )->rows() as $row ) {
printf( '%s: %s–%s', esc_html( $row->field( 'day' )->string() ),
esc_html( $row->field( 'opens' )->string() ), esc_html( $row->field( 'closes' )->string() ) );
}
$days = $class->field( 'opening_hours' )->rows()->map( fn( $row ) => $row->field( 'day' )->string() );Collections also have first(), last(), count(), filter(), is_empty() and array access.
For Content sections, check each row's layout before reading its fields. A row's field() only sees its own layout's fields:
php
foreach ( $page->field( 'sections' )->rows() as $section ) {
if ( $section->layout() === 'hero' ) {
echo esc_html( $section->field( 'heading' )->string() );
}
}Formatting output
Escape values for where you print them, as with any WordPress data. For formatted fields, use the render helpers, which apply the same cleaning VX uses everywhere:
php
echo \VxFramework\render_rich_text( $class->field( 'description' )->string() );
echo \VxFramework\render_code( $class->field( 'embed' )->raw() ); // <pre><code>, escaped\VxFramework\render_markdown() renders a Markdown field as HTML. It's defined on every site, so a theme can call it unguarded; where nothing renders Markdown, it prints the text escaped, as paragraphs.
Each accepts the field's config array as a second argument, to respect per-field settings.
For a Location, \VxFramework\geo_component( $value, 'locality' ) returns one part of the address, and \VxFramework\geo_format( $value, '{street} {street_number}, {postcode} {locality}' ) builds a formatted string, dropping separators around empty parts.
These two exist only where the Location field does. In a theme that may run on a site without it, check first:
php
if ( function_exists( '\VxFramework\geo_format' ) ) {
echo esc_html( \VxFramework\geo_format( $class->field( 'venue' )->value(), '{locality}, {region_code}' ) );
}Finding posts
\VxFramework\Post::query( $args ) accepts the same arguments as get_posts() and returns \VxFramework\Post objects:
php
foreach ( \VxFramework\Post::query( [ 'post_type' => 'class', 'posts_per_page' => 10 ] ) as $class ) {
echo esc_html( $class->get_title() );
}Reading many posts
In a loop, reading the same field on every post costs a database query per post. Pass the list to \VxFramework\Post::prime() first, and the reads in the loop come from the cache:
php
$classes = \VxFramework\Post::query( [ 'post_type' => 'class', 'posts_per_page' => 20 ] );
\VxFramework\Post::prime( $classes );
foreach ( $classes as $class ) {
echo esc_html( $class->field( 'level' )->string() );
}prime() takes \VxFramework\Post objects, WP_Post objects or post IDs, such as a WP_Query's posts.
It also loads Post Relation fields in both directions, with one query per relation for the whole list rather than one per post.
Long-running jobs
Every post you read stays in memory until the request ends. A page reads a few dozen, but a WP-CLI command, an import or an index build that reads every post in one process keeps all of them. Release each batch when you are done with it:
php
$page = 1;
do {
$ids = get_posts( [ 'post_type' => 'class', 'fields' => 'ids', 'posts_per_page' => 500, 'paged' => $page++ ] );
\VxFramework\Post::prime( $ids );
foreach ( $ids as $id ) {
$level = \VxFramework\Post::get( $id )->field( 'level' )->string();
// ...
}
\VxFramework\Post::forget( $ids );
wp_cache_flush_runtime();
} while ( $ids !== [] );forget() takes the same list as prime(), and \VxFramework\Post::flush() releases every post at once. \VxFramework\Term and \VxFramework\User have both methods too. A released post reads as before: the next get() loads it again.
wp_cache_flush_runtime() empties WordPress's own in-memory cache, which holds the same posts and their meta. Without it, memory still grows with every batch.
Reading meta directly
Prefer the accessors. If you must read meta yourself: the field key is the meta key (no fieldset prefix); lists, groups, repeaters and media lists are stored as JSON strings; choice fields store values, not labels; and a Taxonomy field on a post is a normal term assignment, readable with get_the_terms().
Technical details for LLMs
- Facades:
\VxFramework\Post,\VxFramework\Term,\VxFramework\User(get(),query(),field(),has_field(),fields(),field_keys(),get_fieldset_values(),save_fieldset_values(); Post alsoget_id(),get_title(),get_link(),get_status(), and staticprime( $posts ), which firesvxframework/fields/primewith( 'post_type', $ids )). - Releasing: static
forget( $entities )andflush()on all three facades drop the cached facades and the field data loaded for them, then firevxframework/fields/forgetwith( $object_kind, $ids ):post_type,taxonomyoruser, andnullforflush().Post::force_get( $id )releases the post the same way before it reloads it. \VxFramework\Option_Page(get( $key ),field()) reads option page values.\VxFramework\Comment(get( $comment ),query( $get_comments_args ),field(),has_field(),fields(),field_keys(),get_fieldset_values(),save_fieldset_values(), staticvalidate_new(),forget(),flush(); object kindcomment_type). Server-side VX Script takes[ 'comment_id' => … ]for@comment(…).Field_Value:exists,key,type,label,config,raw,is_empty,filled,value,string,string_or_null,int,float,bool,url,option,option_label,options,option_labels,date,media,image,media_all,gallery,posts,post,users,user,terms,term,ids,group(→Field_Row),rows(→Field_CollectionofField_Row),count.value()shapes come from the same descriptor as dynamic data; a custom field type can overridephp_value( array $field, $raw )(non-null wins).- Content sections
value()returns[ { id, layout, layout_label, fields } ]like REST. \VxFramework\get()/\VxFramework\set()read and write VX configuration (vxframework_<group>options, resolvable groups merged with code), not field values.- Server-side VX Script:
\VxFramework\Modules\Dynamic_Data\Expression::render( $expr, [ 'post_id' => … ] )(string) andrender_nullable().