field('custom_name')->type('text')->label('Custom Label') * - Use presets: $schema->preset('name')->label('Override Label') * - Define types: $schema->type('WebSite')->fields(['name', 'url', 'description']) */ class SchemaBuilder { private static ?self $instance = null; private array $fieldDefinitions = []; private array $typeDefinitions = []; private array $typeGroups = []; private ?FieldBuilder $currentField = null; private ?TypeBuilder $currentType = null; public array $schemaTypes = [ 'WebSite' => 'Web Site', 'Organization' => 'Organization', 'LocalBusiness' => ' - Local Business', 'TattooParlor' => ' - - Tattoo Shop', 'HealthBusiness' => ' - - Health Business', 'FoodEstablishment' => ' - - Restaurant', 'WebPage' => 'Web Page', 'CollectionPage' => ' - Collection Page', 'DefinedTermSet' => ' - Glossary/Collection', 'FAQPage' => ' - FAQ Page', 'Person' => 'Person', 'CreativeWork' => 'Creative Work', 'DefinedTerm' => ' - Defined Term', 'VisualArtwork' => ' - Visual Artwork', 'Tattoo' => ' - - Tattoo', 'BeforeAfter' => ' - Before & After', 'Product' => 'Product', 'Event' => 'Event', ]; private array $metaFields = ['metaTitle', 'metaDescription', 'socialPreviewImage', 'twitterImage']; private array $defaultMetaValues = [ 'metaTitle' => '{{post_title}}', 'metaDescription' => '{{post_excerpt}}', 'socialPreviewImage' => '{{featured_image}}', 'twitterImage' => '' ]; public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->registerPresetFields(); $this->registerTypes(); $this->registerTypeGroups(); do_action(BASE . 'schema_builder_loaded', $this); } /** * Start defining a custom field */ public function field(string $name): FieldBuilder { $this->currentField = new FieldBuilder($this, $name); return $this->currentField; } /** * Start with a preset field (can be customized) */ public function preset(string $name): FieldBuilder { $presets = $this->getPresetDefinitions(); if (!isset($presets[$name])) { throw new \InvalidArgumentException("Unknown preset field: {$name}"); } $this->currentField = new FieldBuilder($this, $name, $presets[$name]); return $this->currentField; } /** * Start defining a schema type */ public function type(string $typeName): TypeBuilder { $this->currentType = new TypeBuilder($this, $typeName); return $this->currentType; } /** * Register a custom field definition */ public function registerField(string $fieldName, array $config): void { $this->fieldDefinitions[$fieldName] = $config; } /** * Register a custom type definition */ public function registerType(string $typeName, array $config): void { $this->typeDefinitions[$typeName] = $config; } /** * Get field definition */ public function getFieldDefinition(string $fieldName): ?array { $definitions = apply_filters(BASE . 'schema_field_definitions', $this->fieldDefinitions); return $definitions[$fieldName] ?? null; } /** * Get type definition */ public function getTypeDefinition(string $type): ?array { $definitions = $this->getTypeDefinitions(); return $definitions[$type] ?? null; } /** * Get all type definitions */ public function getTypeDefinitions(): array { return apply_filters(BASE . 'schema_type_definitions', $this->typeDefinitions); } public function getTypeGroups(): array { return $this->typeGroups; } public function getMetaFields(): array { return $this->metaFields; } public function getDefaultMetaValues(): array { return $this->defaultMetaValues; } /** * Get all fields for a specific type (with inheritance) */ public function getFieldsForType(string $type): array { $fields = []; $typeDefinition = $this->getTypeDefinition($type); if (!$typeDefinition) { return $fields; } $fields = array_merge($fields, $typeDefinition['fields'] ?? []); // Handle inheritance if (!empty($typeDefinition['extends'])) { $parentFields = $this->getFieldsForType($typeDefinition['extends']); $fields = array_unique(array_merge($parentFields, $fields)); } return $fields; } /** * Get Meta configuration for a schema type * This creates the form fields for the selected @type */ public function getMetaConfigForType(string $type): array { $fields = $this->getFieldsForType($type); $config = []; foreach ($fields as $fieldName) { $fieldDef = $this->getFieldDefinition($fieldName); if ($fieldDef) { // Use the field name as the key (this IS the schema property) $config[$fieldName] = $fieldDef; } } return $config; } /** * Get types organized by group for UI display */ public function getTypesByGroup(): array { $types = $this->getTypeDefinitions(); $grouped = []; foreach ($types as $typeName => $config) { $group = $config['group'] ?? 'general'; if (!isset($grouped[$group])) { $grouped[$group] = [ 'label' => $this->typeGroups[$group] ?? ucfirst($group), 'types' => [] ]; } $grouped[$group]['types'][$typeName] = $config['label'] ?? $typeName; } return $grouped; } /** * Register a type group */ public function registerGroup(string $key, string $label): void { $this->typeGroups[$key] = $label; } /** * Get post types for select options */ public static function getContentPostTypes(): array { $options = ['' => '-- Select Post Type --']; if (defined('JVB_CONTENT')) { foreach (JVB_CONTENT as $key => $config) { $options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key); } } return $options; } /** * Get taxonomies for select options */ public static function getContentTaxonomies(): array { $options = ['' => '-- Select Taxonomy --']; if (defined('JVB_TAXONOMY')) { foreach (JVB_TAXONOMY as $key => $config) { $options[jvbCheckBase($key)] = $config['plural'] ?? $config['singular'] ?? ucwords($key); } } return $options; } /** * Define preset fields that can be reused */ private function registerPresetFields(): void { // Special type selector field $this->field('type') ->type('select') ->label('Type') ->options(array_merge(['' => '-- Content Type'], $this->schemaTypes)); /************************************************************** * META FIELDS **************************************************************/ $this->field('metaTitle') ->type('text') ->label('Meta Title') ->description('Used in search results and when shared on social media. Leave blank to use default.'); $this->field('metaDescription') ->type('textarea') ->label('Meta Description') ->description('Brief description shown in search results and social previews.'); $this->field('socialPreviewImage') ->type('group') ->label('Social Preview Image') ->description('Image shown when shared on social media. Recommended: 1200x630px.') ->transformer('image_url_with_fallback') ->fields([ 'source_field' => [ 'type' => 'text', 'label' => 'Image Source Field', 'description' => 'Template field to get image from (e.g., {{post_thumbnail}}, {{custom_image_field}})', 'placeholder' => '{{post_thumbnail}}' ], 'fallback' => [ 'type' => 'upload', 'label' => 'Fallback Image', 'description' => 'Used when source field returns no image' ] ]); $this->field('twitterImage') ->type('group') ->label('Twitter Card Image') ->description('Separate image for Twitter. Falls back to main social image if empty.') ->transformer('image_url_with_fallback') ->fields([ 'source_field' => [ 'type' => 'text', 'label' => 'Image Source Field', 'placeholder' => '{{twitter_specific_image}}' ], 'fallback' => [ 'type' => 'upload', 'label' => 'Fallback Image' ] ]); /************************************************************** * QA FIELD FAQ **************************************************************/ $this->field('question') ->type('text') ->label('Question') ->description('Template for the question (e.g., {{post_title}})') ->default('{{post_title}}') ->transformer('text'); $this->field('answer') ->type('textarea') ->label('Answer') ->description('Template for the answer (e.g., {{post_content}})') ->default('{{post_content}}') ->transformer('text'); /************************************************************** * CORE IDENTITY FIELDS **************************************************************/ $this->field('name') ->type('text') ->label('Name') ->description('The name of the item') ->transformer('text'); $this->field('alternateName') ->type('repeater') ->label('Alternate Name(s)') ->description('Alternative names or nicknames') ->transformer('text_array') ->fields([ 'name' => [ 'type' => 'text', 'label' => 'Name' ] ]); $this->field('legalName') ->type('text') ->label('Legal Name') ->description('The official legal name') ->transformer('text'); $this->field('description') ->type('textarea') ->label('Description') ->description('A description of the item') ->transformer('text'); $this->field('disambiguatingDescription') ->type('textarea') ->label('Disambiguating Description') ->description('Brief clarification to distinguish from similar items') ->transformer('text'); $this->field('url') ->type('url') ->label('URL') ->description('Website URL') ->transformer('url'); $this->field('slogan') ->type('text') ->label('Slogan') ->description('A slogan or tagline') ->transformer('text'); /************************************************************** * BEFORE/AFTER FIELDS **************************************************************/ $this->field('about') ->type('reference') ->label('About') ->transformer('reference'); $this->field('temporalCoverage') ->type('text') ->label('Time Period') ->description('ISO 8601 format: 2024-01-10/2024-09-01') ->transformer('text'); $this->field('associatedMedia') ->type('repeater') ->label('Associated Media') ->transformer('image_object_array') ->fields([ 'image' => ['type' => 'image', 'label' => 'Image'], 'caption' => ['type' => 'text', 'label' => 'Caption'], 'position' => ['type' => 'number', 'label' => 'Position'], ]); $this->field('additionalProperty') ->type('repeater') ->label('Additional Properties') ->transformer('property_value_array') ->fields([ 'name' => ['type' => 'text', 'label' => 'Property Name'], 'value' => ['type' => 'text', 'label' => 'Value'], ]); /************************************************************** * IMAGE FIELDS **************************************************************/ $this->field('image') ->type('upload') ->label('Image') ->description('Primary image') ->transformer('image_object'); $this->field('logo') ->type('upload') ->label('Logo') ->transformer('image_object'); $this->field('photo') ->type('upload') ->label('Photo of Location') ->transformer('image_object'); $this->field('video') ->type('upload') ->label('Video') ->transformer('video_object'); /************************************************************** * LOCATION & CONTACT FIELDS **************************************************************/ $this->field('location') ->type('location') ->label('Location') ->description('Physical location with address and coordinates') ->transformer('location_complex'); $this->field('address') ->type('location') ->label('Address') ->description('Postal address') ->transformer('postal_address'); $this->field('geo') ->type('group') ->label('Geographic Coordinates') ->description('Latitude and longitude') ->transformer('geo_coordinates') ->fields([ 'latitude' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Latitude', ], 'longitude' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Longitude', ] ]); $this->field('telephone') ->type('text') ->label('Telephone') ->description('Phone number') ->transformer('text'); $this->field('faxNumber') ->type('text') ->label('Fax Number') ->transformer('text'); $this->field('email') ->type('email') ->label('Email') ->description('Email address') ->transformer('email'); $this->field('contactPoint') ->type('repeater') ->label('Contact Points') ->description('Additional contact methods') ->transformer('contact_point_array') ->fields([ 'contactType' => [ 'type' => 'text', 'label' => 'Contact Type', 'description' => 'e.g., customer service, sales', ], 'telephone' => [ 'type' => 'text', 'label' => 'Phone', ], 'email' => [ 'type' => 'email', 'label' => 'Email', ] ]); $this->field('potentialAction') ->type('repeater') ->label('Potential Actions') ->transformer('potential_action_array') ->fields([ 'action' => [ 'type' => 'radio', 'label' => 'Action', 'options' => [ 'searchAction' => 'Search Action', 'communicateAction' => 'Contact Action', 'scheduleAction' => 'Reserve Action', 'applyAction' => 'Estimate Action' ] ], 'name' => [ 'type' => 'text', 'label' => 'Name', ], 'target' => [ 'type' => 'url', 'label' => 'Action URL', ], 'description' => [ 'type' => 'textarea', 'label' => 'Description' ] ]) ->default([ [ 'action' => 'searchAction', 'target' => get_home_url(null, '/search/?s={query}') ] ]); /************************************************************** * HOURS & OPERATIONAL FIELDS **************************************************************/ $this->field('openingHours') ->type('group') ->label('Opening Hours') ->description('Business hours specification') ->transformer('opening_hours_specification') ->fields([ 'monday' => [ 'type' => 'group', 'label' => 'Monday', 'fields' => [ 'opens' => ['type' => 'time', 'label' => 'Opens'], 'closes' => ['type' => 'time', 'label' => 'Closes'] ] ], 'tuesday' => [ 'type' => 'group', 'label' => 'Tuesday', 'fields' => [ 'opens' => ['type' => 'time', 'label' => 'Opens'], 'closes' => ['type' => 'time', 'label' => 'Closes'] ] ], 'wednesday' => [ 'type' => 'group', 'label' => 'Wednesday', 'fields' => [ 'opens' => ['type' => 'time', 'label' => 'Opens'], 'closes' => ['type' => 'time', 'label' => 'Closes'] ] ], 'thursday' => [ 'type' => 'group', 'label' => 'Thursday', 'fields' => [ 'opens' => ['type' => 'time', 'label' => 'Opens'], 'closes' => ['type' => 'time', 'label' => 'Closes'] ] ], 'friday' => [ 'type' => 'group', 'label' => 'Friday', 'fields' => [ 'opens' => ['type' => 'time', 'label' => 'Opens'], 'closes' => ['type' => 'time', 'label' => 'Closes'] ] ], 'saturday' => [ 'type' => 'group', 'label' => 'Saturday', 'fields' => [ 'opens' => ['type' => 'time', 'label' => 'Opens'], 'closes' => ['type' => 'time', 'label' => 'Closes'] ] ], 'sunday' => [ 'type' => 'group', 'label' => 'Sunday', 'fields' => [ 'opens' => ['type' => 'time', 'label' => 'Opens'], 'closes' => ['type' => 'time', 'label' => 'Closes'] ] ], ]); $this->field('hasPart') ->type('repeater') ->label('Site Navigation') ->description('Main navigation menu items') ->transformer('navigation_array') ->fields([ 'name' => ['type' => 'text', 'label' => 'Link Text'], 'url' => ['type' => 'url', 'label' => 'URL'], 'description' => ['type' => 'textarea', 'label' => 'Description (optional)'], ]); $this->field('priceRange') ->type('text') ->label('Price Range') ->description('e.g., $$, $100-$500') ->transformer('text'); $this->field('currenciesAccepted') ->type('checkbox') ->label('Currencies Accepted') ->options(['CAD' => 'CAD', 'USD' => 'USD']) ->transformer('text_array'); $this->field('paymentAccepted') ->type('checkbox') ->label('Payment Methods') ->options([ 'Cash' => 'Cash', 'Credit Card' => 'Credit Card', 'Debit' => 'Debit', 'Google Pay' => 'Google Pay', 'Apple Pay' => 'Apple Pay', 'PayPal' => 'PayPal', 'Interac' => 'Interac', 'AMEX' => 'AMEX', ]) ->transformer('text_array'); /************************************************************** * ORGANIZATION & BUSINESS FIELDS **************************************************************/ $this->field('foundingDate') ->type('date') ->label('Founding Date') ->description('Date the organization was founded') ->transformer('date'); $this->field('dissolutionDate') ->type('date') ->label('Dissolution Date') ->description('Date the organization closed') ->transformer('date'); $this->field('founders') ->type('repeater') ->label('Founders') ->description('Name of founder(s)') ->transformer('person_array') ->fields([ 'name' => ['type' => 'text', 'label' => 'Name'], 'url' => ['type' => 'url', 'label' => 'URL'], ]); $this->field('numberOfEmployees') ->type('text') ->label('Number of Employees') ->transformer('number'); $this->field('taxID') ->type('text') ->label('Tax ID') ->description('Tax identification number') ->transformer('text'); $this->field('vatID') ->type('text') ->label('VAT ID') ->description('VAT registration number') ->transformer('text'); $this->field('duns') ->type('text') ->label('D-U-N-S Number') ->description('Dun & Bradstreet number') ->transformer('text'); /************************************************************** * SOCIAL & LINKS **************************************************************/ $this->field('sameAs') ->type('repeater') ->label('Social Media & Links') ->description('URLs to social profiles and related pages') ->transformer('url_array') ->fields([ 'url' => ['type' => 'url', 'label' => 'URL'] ]); /************************************************************** * AREA & GEOGRAPHY **************************************************************/ $this->field('areaServed') ->type('repeater') ->label('Area Served') ->description('Geographic areas served') ->transformer('text_array') ->fields([ 'name' => ['type' => 'text', 'label' => 'Location Name'], 'url' => ['type' => 'url', 'label' => 'Wikipedia Page'], ]); $this->field('hasMap') ->type('url') ->label('Map URL') ->description('Link to a map (e.g., Google Maps)') ->transformer('url'); /************************************************************** * AMENITIES & FEATURES **************************************************************/ $this->field('amenityFeature') ->type('checkbox') ->label('Amenity Features') ->description('Available facilities and features') ->transformer('text') ->options([ 'Wheelchair Accessible' => 'Wheelchair Accessible', 'Free Parking' => 'Free Parking', 'Private Rooms' => 'Private Rooms', 'Air Conditioning' => 'Air Conditioning', 'WiFi' => 'WiFi', 'Gender Neutral Restroom' => 'Gender Neutral Restroom', 'LGBTQ+ Friendly' => 'LGBTQ+ Friendly', 'Sterilization Room' => 'Sterilization Room', 'Refreshments Available' => 'Refreshments Available', 'Street Level Access' => 'Street Level Access', 'Single Use Needles' => 'Single Use Needles', 'Consultation Room' => 'Consultation Room', 'Aftercare Products Available' => 'Aftercare Products Available', 'Walk-Ins Welcome' => 'Walk-Ins Welcome', 'By Appointment' => 'By Appointment Only', ]); /************************************************************** * LANGUAGES **************************************************************/ $this->field('availableLanguage') ->type('repeater') ->label('Languages Available') ->description('Languages spoken or supported') ->transformer('language_array') ->fields([ 'language' => ['type' => 'text', 'label' => 'Language'] ]); $this->field('knowsLanguage') ->type('repeater') ->label('Languages Known') ->description('Languages the person knows') ->transformer('language_array') ->fields([ 'language' => ['type' => 'text', 'label' => 'Language'] ]); $this->field('inLanguage') ->type('radio') ->label('In Language') ->options([ 'en-CA' => 'English, Canadian', 'en-US' => 'English, American', 'fr-CA' => 'French, Canadian' ]) ->transformer('text'); /************************************************************** * RATINGS & REVIEWS **************************************************************/ $this->field('aggregateRating') ->type('group') ->label('Aggregate Rating') ->description('Overall rating and review count') ->transformer('aggregate_rating') ->fields([ 'ratingValue' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Rating Value', 'description' => 'Average rating (e.g., 4.5)', ], 'bestRating' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Best Rating', 'default' => 5, 'description' => 'Highest possible rating (e.g., 5)', ], 'worstRating' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Worst Rating', 'default' => 1, 'description' => 'Lowest possible rating (e.g., 1)', ], 'ratingCount' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Rating Count', 'description' => 'Total number of ratings', ], 'reviewCount' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Review Count', 'description' => 'Total number of reviews', ] ]); /************************************************************** * KEYWORDS & CATEGORIZATION **************************************************************/ $this->field('keywords') ->type('repeater') ->label('Keywords') ->description('Keywords or tags') ->transformer('text_array') ->fields([ 'keyword' => ['type' => 'text', 'label' => 'Keyword'] ]); /************************************************************** * PERSON FIELDS **************************************************************/ $this->field('givenName') ->type('text') ->label('First Name') ->transformer('text'); $this->field('familyName') ->type('text') ->label('Last Name') ->transformer('text'); $this->field('honorificPrefix') ->type('text') ->label('Honorific Prefix') ->description('e.g., Dr., Mr., Ms.') ->transformer('text'); $this->field('honorificSuffix') ->type('text') ->label('Honorific Suffix') ->description('e.g., PhD, MD') ->transformer('text'); $this->field('jobTitle') ->type('text') ->label('Job Title') ->transformer('text'); $this->field('birthDate') ->type('date') ->label('Birth Date') ->description('For public figures') ->transformer('date'); $this->field('gender') ->type('text') ->label('Gender') ->transformer('text'); /************************************************************** * CREATIVE WORK FIELDS **************************************************************/ $this->field('author') ->type('text') ->label('Author') ->description('Author name or reference') ->transformer('person_reference'); $this->field('creator') ->type('text') ->label('Creator') ->description('Creator name or reference') ->transformer('text'); $this->field('dateCreated') ->type('text') ->label('Date Created') ->transformer('text'); $this->field('datePublished') ->type('text') ->label('Date Published') ->default('{{post_date') ->transformer('text'); $this->field('dateModified') ->type('text') ->default('{{post_modified}}') ->label('Date Modified') ->transformer('text'); /************************************************************** * VISUAL ARTWORK FIELDS **************************************************************/ $this->field('artform') ->type('text') ->label('Art Form') ->description('e.g., Painting, Sculpture, Tattoo') ->transformer('text'); $this->field('artMedium') ->type('text') ->label('Art Medium') ->description('e.g., Oil, Watercolor, Ink') ->transformer('text'); $this->field('artworkSurface') ->type('text') ->label('Artwork Surface') ->description('e.g., Canvas, Paper, Skin') ->transformer('text'); $this->field('width') ->type('text') ->label('Width') ->description('Width with unit (e.g., 10cm, 5in)') ->transformer('dimension'); $this->field('height') ->type('text') ->label('Height') ->description('Height with unit (e.g., 15cm, 8in)') ->transformer('dimension'); /************************************************************** * EVENT FIELDS **************************************************************/ $this->field('startDate') ->type('text') ->default('{{start_date}}') ->label('Start Date/Time') ->transformer('text'); $this->field('endDate') ->type('text') ->default('{{end_date}}') ->label('End Date/Time') ->transformer('text'); $this->field('eventStatus') ->type('select') ->label('Event Status') ->options([ 'https://schema.org/EventScheduled' => 'Scheduled', 'https://schema.org/EventCancelled' => 'Cancelled', 'https://schema.org/EventPostponed' => 'Postponed', 'https://schema.org/EventRescheduled' => 'Rescheduled', ]) ->transformer('text'); $this->field('eventAttendanceMode') ->type('select') ->label('Attendance Mode') ->options([ 'https://schema.org/OfflineEventAttendanceMode' => 'In-Person', 'https://schema.org/OnlineEventAttendanceMode' => 'Online', 'https://schema.org/MixedEventAttendanceMode' => 'Mixed/Hybrid', ]) ->transformer('text'); /************************************************************** * PRODUCT FIELDS **************************************************************/ $this->field('brand') ->type('group') ->label('Brand') ->transformer('brand_object') ->fields([ 'type' => [ 'type' => 'select', 'label' => 'Brand Type', 'options' => [ 'text' => 'Text Only', 'organization' => 'Organization/Brand', ] ], 'name' => [ 'type' => 'text', 'label' => 'Brand Name', ], 'url' => [ 'type' => 'url', 'label' => 'Brand Website', 'condition' => [ 'field' => 'type', 'value' => 'organization' ] ], 'logo' => [ 'type' => 'upload', 'label' => 'Brand Logo', 'condition' => [ 'field' => 'type', 'value' => 'organization' ] ], ]); $this->field('sku') ->type('text') ->label('SKU') ->description('Stock Keeping Unit') ->transformer('text'); $this->field('gtin') ->type('text') ->label('GTIN') ->description('Global Trade Item Number') ->transformer('text'); /************************************************************** * SERVICES & OFFERS **************************************************************/ $this->field('hasOfferCatalog') ->type('group') ->label('Offer Catalog') ->transformer('offer_catalog_array') ->fields([ 'source' => [ 'type' => 'select', 'label' => 'Source', 'options' => [ 'auto' => 'Auto from post type', 'manual' => 'Manual entry', ] ], 'post_type' => [ 'type' => 'select', 'label' => 'Post Type', 'options' => self::getContentPostTypes(), 'condition' => ['field' => 'source', 'value' => 'auto'] ], 'group_by_taxonomy' => [ 'type' => 'true_false', 'label' => 'Group by category/taxonomy', 'condition' => ['field' => 'source', 'value' => 'auto'] ], 'taxonomy' => [ 'type' => 'select', 'label' => 'Taxonomy', 'options' => self::getContentTaxonomies(), 'condition' => ['field' => 'group_by_taxonomy', 'value' => '1'] ], 'manual_items' => [ 'type' => 'repeater', 'label' => 'Manual Offers', 'condition' => ['field' => 'source', 'value' => 'manual'], 'fields' => [ 'type' => ['type' => 'radio', 'label' => 'Type', 'options' => ['Service' => 'Service', 'Product' => 'Product']], 'name' => ['type' => 'text', 'label' => 'Offer Name'], 'description' => ['type' => 'textarea', 'label' => 'Description'], 'price' => ['type' => 'text', 'label' => 'Price'], ] ] ]); $this->field('knowsAbout') ->type('repeater') ->label('Areas of Expertise') ->description('Skills and specialties') ->transformer('text_array') ->fields([ 'topic' => ['type' => 'text', 'label' => 'Topic'] ]); /************************************************************** * CREDENTIALS & CERTIFICATIONS **************************************************************/ $this->field('hasCredential') ->type('repeater') ->label('Credentials / Certifications') ->description('Professional certifications') ->transformer('credential_array') ->fields([ 'credentialCategory' => ['type' => 'text', 'label' => 'Category'], 'name' => ['type' => 'text', 'label' => 'Name'], 'issuedBy' => ['type' => 'text', 'label' => 'Issued By'] ]); $this->field('award') ->type('repeater') ->label('Awards & Recognition') ->transformer('text_array') ->fields([ 'award' => ['type' => 'text', 'label' => 'Award'] ]); $this->field('serviceArea') ->type('repeater') ->label('Service Areas') ->description('Geographic areas served (cities, neighborhoods, or radius)') ->transformer('service_area_array') ->fields([ 'name' => ['type' => 'text', 'label' => 'Area Name'], 'type' => [ 'type' => 'select', 'label' => 'Type', 'options' => [ 'City' => 'City', 'AdministrativeArea' => 'Region/Province', 'GeoCircle' => 'Radius', ] ], 'radius' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Radius (km)'], ]); $this->field('makesOffer') ->type('group') ->label('Featured Offerings') ->transformer('offers_from_posts') ->fields([ 'source' => [ 'type' => 'select', 'label' => 'Source', 'options' => ['auto' => 'Auto from post type', 'manual' => 'Manual entry'] ], 'post_type' => [ 'type' => 'select', 'label' => 'Post Type', 'options' => self::getContentPostTypes(), 'condition' => ['field' => 'source', 'value' => 'auto'] ], 'limit' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Featured Count', 'default' => 5, 'condition' => ['field' => 'source', 'value' => 'auto'] ], 'manual_items' => [ 'type' => 'repeater', 'label' => 'Manual Offers', 'condition' => ['field' => 'source', 'value' => 'manual'], 'fields' => [ 'name' => ['type' => 'text', 'label' => 'Offer Name'], 'description' => ['type' => 'textarea', 'label' => 'Description'], 'price' => ['type' => 'text', 'label' => 'Price/Range'], ] ] ]); $this->field('hasMenu') ->type('group') ->label('Menu Items') ->description('Auto-populate from post type or enter manually') ->transformer('menu_from_posts') ->fields([ 'source' => [ 'type' => 'select', 'label' => 'Source', 'options' => ['auto' => 'Auto from post type', 'manual' => 'Manual entry'] ], 'post_type' => [ 'type' => 'select', 'label' => 'Post Type', 'options' => self::getContentPostTypes(), 'condition' => ['field' => 'source', 'value' => 'auto'] ], 'limit' => [ 'type' => 'text', 'subtype' => 'number', 'label' => 'Number of items', 'default' => 10, 'condition' => ['field' => 'source', 'value' => 'auto'] ], 'orderby' => [ 'type' => 'select', 'label' => 'Order By', 'options' => ['menu_order' => 'Menu Order', 'title' => 'Title', 'date' => 'Date'], 'condition' => ['field' => 'source', 'value' => 'auto'] ], 'manual_items' => [ 'type' => 'repeater', 'label' => 'Manual Items', 'condition' => ['field' => 'source', 'value' => 'manual'], 'fields' => [ 'name' => ['type' => 'text', 'label' => 'Item Name'], 'description' => ['type' => 'textarea', 'label' => 'Description'], 'price' => ['type' => 'text', 'label' => 'Price'], ] ] ]); /************************************************************** * FAQ FIELDS **************************************************************/ $this->field('faq') ->type('repeater') ->label('FAQ Items') ->description('Question and Answer pairs') ->transformer('faq_array') ->fields([ 'question' => ['type' => 'text', 'label' => 'Question'], 'answer' => ['type' => 'text', 'label' => 'Answer'] ]); /************************************************************** * FOOD & CUISINE **************************************************************/ $this->field('servesCuisine') ->type('repeater') ->label('Cuisine Types') ->description('Types of cuisine served') ->transformer('text_array') ->fields([ 'cuisine' => ['type' => 'text', 'label' => 'Cuisine Type', 'description' => 'e.g., Italian, Mexican, Vegan'] ]); $this->field('menu') ->type('url') ->label('Menu URL') ->description('Link to online menu') ->transformer('url'); /************************************************************** * PRODUCT/OFFER FIELDS **************************************************************/ $this->field('offers') ->type('group') ->label('Offer Details') ->description('Price and availability information') ->transformer('offer_object') ->fields([ 'price' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Price'], 'priceCurrency' => ['type' => 'text', 'label' => 'Currency', 'default' => 'USD'], 'availability' => [ 'type' => 'select', 'label' => 'Availability', 'options' => [ 'InStock' => 'In Stock', 'PreOrder' => 'Pre-Order', 'SoldOut' => 'Sold Out', 'OutOfStock' => 'Out of Stock', 'Discontinued' => 'Discontinued', ] ], 'validFrom' => ['type' => 'text', 'label' => 'Valid From', 'default' => '{{validFrom}}'], 'validThrough' => ['type' => 'text', 'label' => 'Valid Through', 'default' => '{{validTo}}'], ]); $this->field('mpn') ->type('text') ->label('Manufacturer Part Number') ->transformer('text'); /************************************************************** * BUSINESS POLICIES & FEATURES **************************************************************/ $this->field('isAccessibleForFree') ->type('true_false') ->label('Accessible For Free') ->description('Is this service/location accessible without payment?') ->transformer('boolean'); $this->field('smokingAllowed') ->type('true_false') ->label('Smoking Allowed') ->transformer('boolean'); $this->field('petsAllowed') ->type('select') ->label('Pets Allowed') ->options([ '' => 'Not specified', 'yes' => 'Yes', 'no' => 'No', ]) ->transformer('boolean'); /************************************************************** * ORGANIZATION RELATIONSHIPS **************************************************************/ $this->field('parentOrganization') ->type('group') ->label('Parent Organization') ->description('Organization this is a part of') ->transformer('organization_reference') ->fields([ 'name' => ['type' => 'text', 'label' => 'Organization Name'], 'url' => ['type' => 'url', 'label' => 'Website'], ]); $this->field('subOrganization') ->type('repeater') ->label('Sub-Organizations') ->description('Child organizations or departments') ->transformer('organization_reference_array') ->fields([ 'name' => ['type' => 'text', 'label' => 'Organization Name'], 'url' => ['type' => 'url', 'label' => 'Website'], ]); $this->field('employee') ->type('repeater') ->label('Employees') ->transformer('person_reference_array') ->fields([ 'name' => ['type' => 'text', 'label' => 'Name'], 'jobTitle' => ['type' => 'text', 'label' => 'Job Title'], ]); /************************************************************** * HOSPITALITY **************************************************************/ $this->field('checkinTime') ->type('time') ->label('Check-in Time') ->transformer('time'); $this->field('checkoutTime') ->type('time') ->label('Check-out Time') ->transformer('time'); $this->field('starRating') ->type('group') ->label('Star Rating') ->transformer('rating_object') ->fields([ 'ratingValue' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Rating', 'min' => 1, 'max' => 5] ]); /************************************************************** * REVIEW & RATING **************************************************************/ $this->field('review') ->type('repeater') ->label('Reviews') ->transformer('review_array') ->fields([ 'author' => ['type' => 'text', 'label' => 'Reviewer Name'], 'reviewRating' => ['type' => 'text', 'subtype' => 'number', 'label' => 'Rating', 'min' => 1, 'max' => 5], 'reviewBody' => ['type' => 'textarea', 'label' => 'Review Text'], 'datePublished' => ['type' => 'date', 'label' => 'Date'], ]); /************************************************************** * HEALTH & MEDICAL **************************************************************/ $this->field('medicalSpecialty') ->type('repeater') ->label('Medical Specialties') ->transformer('text_array') ->fields([ 'specialty' => ['type' => 'text', 'label' => 'Specialty'] ]); $this->field('healthcareService') ->type('repeater') ->label('Healthcare Services') ->transformer('text_array') ->fields([ 'service' => ['type' => 'text', 'label' => 'Service'] ]); /*************************************************************** ***************************************************************/ $this->field('termCode') ->type('text') ->label('Term Code') ->description('Unique identifier or code for this term') ->transformer('text'); $this->field('hasDefinedTerm') ->type('group') ->label('Defined Terms') ->description('Terms included in this glossary or collection') ->transformer('defined_terms_from_posts') ->fields([ 'source' => [ 'type' => 'select', 'label' => 'Source', 'options' => ['auto' => 'Auto from post type', 'manual' => 'Manual entry'] ], 'post_type' => [ 'type' => 'select', 'label' => 'Post Type', 'options' => self::getContentPostTypes(), 'condition' => ['field' => 'source', 'value' => 'auto'] ], 'taxonomy' => [ 'type' => 'select', 'label' => 'Filter by Taxonomy', 'options' => self::getContentTaxonomies(), 'condition' => ['field' => 'source', 'value' => 'auto'] ] ]); } /** * Get raw preset definitions (before filters) */ private function getPresetDefinitions(): array { return $this->fieldDefinitions; } /** * Define schema types */ private function registerTypes(): void { /************************************************************** * GENERAL / SITE-WIDE **************************************************************/ $this->type('WebSite') ->label('Website') ->group('general') ->fields([ 'name', 'description', 'url', 'inLanguage', 'potentialAction', 'hasPart', 'creator', ]); /************************************************************** * PAGE TYPES **************************************************************/ $this->type('WebPage') ->label('Web Page') ->group('page') ->fields([ 'type', 'name', 'description', 'url', 'image', 'datePublished', 'dateModified', 'author', ]); $this->type('CollectionPage') ->label('Collection Page') ->group('page') ->extends('WebPage'); $this->type('FAQPage') ->label('FAQ Page') ->group('page') ->extends('WebPage') ->addFields([ 'question', 'answer' ]); $this->type('Place') ->label('Place') ->group('general') ->fields([ 'type', 'name', 'description', 'url', 'image', 'geo', 'address', 'sameAs', ]); $this->type('City') ->label('City') ->group('general') ->extends('Place') ->addFields([ 'containedInPlace', ]); $this->field('containedInPlace') ->type('reference') ->label('Contained In') ->description('Parent place (province, country)') ->transformer('reference'); /************************************************************** * ORGANIZATION & BUSINESS **************************************************************/ $this->type('Organization') ->label('Organization') ->group('business') ->fields([ 'type', 'name', 'legalName', 'alternateName', 'description', 'url', 'logo', 'image', 'email', 'telephone', 'sameAs', 'founders', 'foundingDate', 'numberOfEmployees', 'taxID', 'vatID', 'duns', 'slogan', 'disambiguatingDescription', ]); $this->type('LocalBusiness') ->label('Local Business') ->group('business') ->extends('Organization') ->addFields([ 'location', 'openingHours', 'priceRange', 'currenciesAccepted', 'paymentAccepted', 'serviceArea', 'areaServed', 'hasMap', 'amenityFeature', 'availableLanguage', 'hasOfferCatalog', 'makesOffer', 'hasMenu', 'knowsAbout', 'hasCredential', 'aggregateRating', 'review', 'award', ]); $this->type('TattooParlor') ->label('Tattoo Parlor') ->group('business') ->extends('LocalBusiness') ->addFields([ 'makesOffer', 'hasOfferCatalog', 'award', ]); $this->type('HealthBusiness') ->label('Health Business') ->group('business') ->extends('LocalBusiness'); $this->type('FoodEstablishment') ->label('Food Establishment') ->group('business') ->extends('LocalBusiness') ->addFields([ 'hasMenu', 'servesCuisine', ]); $this->type('FoodTruck') ->label('Food Truck') ->group('business') ->extends('FoodEstablishment') ->addField('serviceArea'); $this->type('Store') ->label('Store / Shop') ->group('business') ->extends('LocalBusiness') ->addFields([ 'hasOfferCatalog', 'makesOffer', ]); $this->type('ProfessionalService') ->label('Professional Service') ->group('business') ->extends('LocalBusiness') ->addFields([ 'serviceArea', 'makesOffer', 'award', ]); /************************************************************** * PERSON **************************************************************/ $this->type('Person') ->label('Person') ->group('person') ->fields([ 'type', 'name', 'givenName', 'familyName', 'honorificPrefix', 'honorificSuffix', 'alternateName', 'description', 'image', 'url', 'email', 'telephone', 'sameAs', 'jobTitle', 'knowsLanguage', 'knowsAbout', 'award', 'hasCredential', 'birthDate', 'gender', ]); /************************************************************** * CREATIVE WORKS **************************************************************/ $this->type('CreativeWork') ->label('Creative Work') ->group('creative') ->fields([ 'type', 'name', 'description', 'image', 'author', 'creator', 'dateCreated', 'datePublished', 'dateModified', 'keywords', 'aggregateRating' ]); $this->type('DefinedTerm') ->label('Defined Term') ->group('creative') ->extends('CreativeWork') ->addFields([ 'termCode', // 'inDefinedTermSet', ]); $this->type('BeforeAfter') ->label('Before & After Case') ->group('creative') ->extends('CreativeWork') ->addFields([ 'about', 'temporalCoverage', 'hasPart', 'associatedMedia', 'additionalProperty', ]); $this->type('VisualArtwork') ->label('Visual Artwork') ->group('creative') ->extends('CreativeWork') ->addFields([ 'artform', 'artMedium', 'artworkSurface', 'width', 'height', ]); $this->type('Tattoo') ->label('Tattoo') ->group('creative') ->extends('VisualArtwork'); $this->type('Product') ->label('Product') ->group('creative') ->fields([ 'name', 'description', 'image', 'brand', 'sku', 'gtin', 'offers', 'aggregateRating', 'review', 'award', ]); /************************************************************** * EVENTS **************************************************************/ $this->type('Event') ->label('Event') ->group('event') ->fields([ 'type', 'name', 'description', 'image', 'startDate', 'endDate', 'location', 'eventStatus', 'eventAttendanceMode', ]); } /** * Define type groups for organization */ private function registerTypeGroups(): void { $this->typeGroups = [ 'general' => 'General', 'page' => 'Page Types', 'business' => 'Business & Organization', 'person' => 'People', 'creative' => 'Creative Works', 'event' => 'Events', ]; } }