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' ).
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.
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 )). \VxFramework\Option_Page(get( $key ),field()) reads option page values.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().