PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / trunk
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings vtrunk
7.2.2 7.2.1 7.2 7.1.2 7.1.1 7.1 7.0.4 7.0.6 7.0.7 6.3.8 6.3.7 6.3.6 6.3.5 6.3.4 6.3.3 6.3.1 trunk 5.7.3 5.7.5 5.8.1 5.8.2 5.8.3 5.8.4 5.8.6 6.0.4 All 37 releases
mlsimport / includes / standalone / class-mlsimport-page-block-search-fields.php

class-mlsimport-page-block-search-fields.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings trunk, at includes/standalone/class-mlsimport-page-block-search-fields.php

240 lines 11.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Standalone (theme_id 990) search-field catalog + classifier.
4 *
5 * The single source of truth for "what can a search form filter on". A field is
6 * only offered if the fast listings table can actually filter it, which keeps
7 * search off slow meta lookups. Two groups, in this display order:
8 *
9 * 1. taxonomies — every standalone taxonomy (§7). City/State/Zip/Type/Status
10 * are backed by a fast column and matched by the term *name*; Area/County/
11 * Label/Features have no column and are matched by a term-join on *slug*.
12 * 2. columns — the fast-table fact columns, EXCEPT the geo coordinates
13 * (latitude/longitude) and the internal identity columns.
14 *
15 * Each entry also carries how it renders and which query param(s) it submits, so
16 * the Gutenberg field picker, the front-end form and the WHERE-builder all read
17 * one definition. Pure data + lookup: no WordPress, no DB.
18 *
19 * @package Mlsimport
20 */
21
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 /**
27 * Classifies and describes the fields a standalone search form may filter by.
28 */
29 class Mlsimport_Page_Block_Search_Fields {
30
31 /**
32 * Taxonomy search fields, in display order. Mirrors the standalone taxonomy
33 * registry (§7). Per field:
34 * label editor/front-end label.
35 * tax the taxonomy slug whose terms populate the control.
36 * match 'column-in' (fast column IN names) or 'term-join'
37 * (wp_term_relationships subquery on slug).
38 * multi true — every plugin taxonomy is a multi-select.
39 * value the option value the control submits: 'name' or 'slug'.
40 * join for term-join fields only: 'and' (a listing must carry every chosen
41 * term — amenities, ADR-0004) or 'or' (any chosen term — locations,
42 * labels, which a listing only holds one of).
43 */
44 private const TAXONOMY_FIELDS = array(
45 'property_type' => array( 'label' => 'Property Type', 'tax' => 'mlsimport_property_type', 'match' => 'column-in', 'multi' => true, 'value' => 'name' ),
46 'listing_type' => array( 'label' => 'Listing Type', 'tax' => 'mlsimport_listing_type', 'match' => 'column-in', 'multi' => true, 'value' => 'name' ),
47 'status' => array( 'label' => 'Status', 'tax' => 'mlsimport_status', 'match' => 'column-in', 'multi' => true, 'value' => 'name' ),
48 'city' => array( 'label' => 'City', 'tax' => 'mlsimport_city', 'match' => 'column-in', 'multi' => true, 'value' => 'name' ),
49 'area' => array( 'label' => 'Area', 'tax' => 'mlsimport_area', 'match' => 'term-join', 'multi' => true, 'value' => 'slug', 'join' => 'or' ),
50 'county' => array( 'label' => 'County', 'tax' => 'mlsimport_county', 'match' => 'term-join', 'multi' => true, 'value' => 'slug', 'join' => 'or' ),
51 'state' => array( 'label' => 'State', 'tax' => 'mlsimport_state', 'match' => 'column-in', 'multi' => true, 'value' => 'name' ),
52 'zip' => array( 'label' => 'ZIP Code', 'tax' => 'mlsimport_zip', 'match' => 'column-in', 'multi' => true, 'value' => 'name' ),
53 'high_school_district' => array( 'label' => 'High School District', 'tax' => 'mlsimport_high_school_district', 'match' => 'term-join', 'multi' => true, 'value' => 'slug', 'join' => 'or' ),
54 'features' => array( 'label' => 'Features', 'tax' => 'mlsimport_feature', 'match' => 'term-join', 'multi' => true, 'value' => 'slug', 'join' => 'and' ),
55 'label' => array( 'label' => 'Label', 'tax' => 'mlsimport_label', 'match' => 'term-join', 'multi' => true, 'value' => 'slug', 'join' => 'or' ),
56 );
57
58 /**
59 * Fast-table column search fields, in display order — the property facts the
60 * operator searches by. Latitude/longitude (geo) and the internal identity
61 * columns are deliberately absent. Per field:
62 * label editor/front-end label.
63 * control 'range' (min/max number pair), 'number', 'date' or 'text'.
64 * params the query param(s) the control submits. A range submits two.
65 */
66 private const COLUMN_FIELDS = array(
67 // One text box that stands in for the four separate location taxonomies: the
68 // visitor types (or picks from the autocomplete) a city, area, county or ZIP
69 // and the WHERE-builder ORs the value across all four plus the FULLTEXT
70 // search_text column, which already carries UnparsedAddress — so a street
71 // address typed in full matches too, with no address column to maintain.
72 'location' => array( 'label' => 'Location', 'control' => 'location', 'params' => array( 'location' ) ),
73 'price' => array( 'label' => 'Price', 'control' => 'range', 'params' => array( 'price_min', 'price_max' ) ),
74 'beds_baths' => array( 'label' => 'Beds & Baths', 'control' => 'beds_baths', 'params' => array( 'beds', 'baths' ) ),
75 'sqft' => array( 'label' => 'Living Area (sq ft)', 'control' => 'range', 'params' => array( 'sqft_min', 'sqft_max' ) ),
76 'lot' => array( 'label' => 'Lot Size', 'control' => 'range', 'params' => array( 'lot_min', 'lot_max' ) ),
77 'year' => array( 'label' => 'Year Built', 'control' => 'range', 'params' => array( 'year_min', 'year_max' ) ),
78 'list_date' => array( 'label' => 'Listed After', 'control' => 'date', 'params' => array( 'list_date_min' ) ),
79 'garage' => array( 'label' => 'Garage Spaces', 'control' => 'number', 'params' => array( 'garage_min' ) ),
80 'stories' => array( 'label' => 'Stories', 'control' => 'number', 'params' => array( 'stories' ) ),
81 'hoa' => array( 'label' => 'Max HOA Fee', 'control' => 'number', 'params' => array( 'hoa_max' ) ),
82 'dom' => array( 'label' => 'Max Days on Market', 'control' => 'number', 'params' => array( 'dom_max' ) ),
83 'subdivision' => array( 'label' => 'Subdivision', 'control' => 'text', 'params' => array( 'subdivision' ) ),
84 // The public MLS number off a sign or flyer (RESO ListingId, e.g. TB8541851):
85 // an exact match on the listing_id column, so it lands on that one listing.
86 'mls_number' => array( 'label' => 'MLS #', 'control' => 'text', 'params' => array( 'listing_id' ) ),
87 );
88
89 /**
90 * Classify a search field by how the fast table can filter it.
91 *
92 * @param string $field Field key (e.g. 'price', 'city', 'features').
93 * @return string 'taxonomy', 'scalar', or '' when the field is not searchable.
94 */
95 public static function classify( string $field ): string {
96 if ( isset( self::TAXONOMY_FIELDS[ $field ] ) ) {
97 return 'taxonomy';
98 }
99 if ( isset( self::COLUMN_FIELDS[ $field ] ) ) {
100 return 'scalar';
101 }
102 return '';
103 }
104
105 /**
106 * Filter a configured field list to only the searchable fields, preserving the
107 * builder's order. The guard the search form builder applies to its repeater.
108 *
109 * @param string[] $fields Requested field keys, in display order.
110 * @return string[] The subset that is searchable.
111 */
112 public static function allowed( array $fields ): array {
113 $out = array();
114 // Keep a field only if it classifies as taxonomy or scalar; drop unknowns.
115 foreach ( $fields as $field ) {
116 if ( '' !== self::classify( (string) $field ) ) {
117 $out[] = (string) $field;
118 }
119 }
120 return $out;
121 }
122
123 /**
124 * Every searchable field key, taxonomies then columns — the order the field
125 * picker and the full search form offer them.
126 *
127 * @return string[]
128 */
129 public static function catalog(): array {
130 return array_merge( array_keys( self::TAXONOMY_FIELDS ), array_keys( self::COLUMN_FIELDS ) );
131 }
132
133 /**
134 * The field picker's options: field key => label, taxonomies then columns.
135 *
136 * @return array<string,string>
137 */
138 public static function labels(): array {
139 $out = array();
140 // Taxonomy fields first (they lead the picker), then the column fields.
141 foreach ( self::TAXONOMY_FIELDS as $key => $def ) {
142 $out[ $key ] = $def['label'];
143 }
144 foreach ( self::COLUMN_FIELDS as $key => $def ) {
145 $out[ $key ] = $def['label'];
146 }
147 return $out;
148 }
149
150 /**
151 * The field keys that render as a dual-handle range slider. The search form's
152 * per-row "slider minimum/maximum" overrides only apply to these, so the editor
153 * can show those two controls conditionally instead of on every row.
154 *
155 * @return string[]
156 */
157 public static function range_fields(): array {
158 $out = array();
159 // A range control is the only one with a configurable floor and ceiling.
160 foreach ( self::COLUMN_FIELDS as $key => $def ) {
161 if ( 'range' === $def['control'] ) {
162 $out[] = $key;
163 }
164 }
165 return $out;
166 }
167
168 /**
169 * The full definition for one field, normalized with its group, or null when
170 * the field is not searchable. Taxonomy fields gain group=taxonomy + tax/match/
171 * multi/value; column fields gain group=column + control/params.
172 *
173 * @param string $field Field key.
174 * @return array|null
175 */
176 public static function definition( string $field ) {
177 if ( isset( self::TAXONOMY_FIELDS[ $field ] ) ) {
178 return array_merge( array( 'key' => $field, 'group' => 'taxonomy' ), self::TAXONOMY_FIELDS[ $field ] );
179 }
180 if ( isset( self::COLUMN_FIELDS[ $field ] ) ) {
181 return array_merge( array( 'key' => $field, 'group' => 'column' ), self::COLUMN_FIELDS[ $field ] );
182 }
183 return null;
184 }
185
186 /**
187 * The search field that filters a given taxonomy, as { key, value }, or null
188 * when the taxonomy has no search field. Lets a taxonomy term archive pre-filter
189 * the grid (and pre-select the search form) by its queried term: 'key' is the
190 * query param, 'value' says whether that param carries the term name or slug.
191 *
192 * @param string $taxonomy Taxonomy slug.
193 * @return array{key:string,value:string}|null
194 */
195 public static function field_for_taxonomy( string $taxonomy ) {
196 // Reverse-lookup: find the search field whose 'tax' matches this taxonomy.
197 foreach ( self::TAXONOMY_FIELDS as $key => $def ) {
198 if ( $def['tax'] === $taxonomy ) {
199 return array( 'key' => $key, 'value' => $def['value'] );
200 }
201 }
202 return null;
203 }
204
205 /**
206 * The term-join taxonomy fields: query param => { tax, join }. The listings
207 * query resolves each submitted slug to a term_taxonomy_id and filters by them
208 * per the join mode ('and' = carry every term, 'or' = carry any). Lets the query
209 * stay in sync with the catalog without re-declaring the join set.
210 *
211 * @return array<string,array{tax:string,join:string}>
212 */
213 public static function term_join_params(): array {
214 $out = array();
215 // Collect only the term-join fields; column-in fields filter via a fast column.
216 foreach ( self::TAXONOMY_FIELDS as $key => $def ) {
217 if ( 'term-join' === $def['match'] ) {
218 $out[ $key ] = array(
219 'tax' => $def['tax'],
220 // Default to 'or'; only an explicit 'and' requires every chosen term.
221 'join' => isset( $def['join'] ) && 'and' === $def['join'] ? 'and' : 'or',
222 );
223 }
224 }
225 return $out;
226 }
227
228 /**
229 * The taxonomies the combined Location field reaches beyond the fast city/zip
230 * columns — area and county, which have no column of their own. One list, read
231 * by both the autocomplete suggestion source and the WHERE-builder's location
232 * clause, so the two can never offer and match different things.
233 *
234 * @return string[] Taxonomy slugs.
235 */
236 public static function location_taxonomies(): array {
237 return array( self::TAXONOMY_FIELDS['area']['tax'], self::TAXONOMY_FIELDS['county']['tax'] );
238 }
239 }
240