PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.2.1
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.2.1
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
← All changes | enviroment/ResidenceClass.php +62 -404 7.0.4 → 7.2.1 View file →
@@ -2,13 +2,12 @@
2 2 /**
3 3 * ResidenceClass — theme adapter for the WpResidence (WpEstate family) WordPress theme.
4 4 *
5 5 * One of the plugin's theme adapters (enviroment/ directory). Maps RESO-standard MLS
6 - * property data onto WpResidence's post type ('estate_property') and post-meta keys
7 - * (property_*, wpestate_*). Responsibilities mirror the other adapters: declare post
8 - * types, save the gallery, map selected "extra" MLS fields to post meta / taxonomy,
9 - * set hardcoded defaults on newly inserted properties, and sync selected MLS fields
10 - * into the theme's custom-fields registry.
6 + * property data onto WpResidence's post type ('estate_property') and post-meta
7 + * keys (property_*, wpestate_*). Stored Listing Write owns shared selection and
8 + * persistence; this adapter keeps only WpResidence gallery representation,
9 + * theme field projection, creation defaults, and custom-field registry support.
11 10 *
12 11 * @package MLSImport
13 12 */
14 13 // Abort if the file is accessed directly outside of WordPress.
@@ -17,9 +16,9 @@
17 16 }
18 17
19 18
20 19 /**
21 - * Description of ResidenceClass
20 + * Persist genuine WpResidence variation behind the Stored adapter seam.
22 21 *
23 22 * @author mlsimport
24 23 */
25 24 class ResidenceClass {
@@ -45,434 +44,92 @@
45 44 public function get_property_post_type() {
46 45 // WpResidence stores listings in the 'estate_property' post type.
47 46 return 'estate_property';
48 47 }
49 -
50 48
51 -
52 49 /**
53 - * return custom post field
50 + * Return the WpResidence property post type to Stored Listing Write.
54 51 *
55 - * The post type(s) WpResidence uses for agents, agencies, and developers.
56 - *
57 - * @since 1.0.0
58 - * @access protected
59 - * @var string $plugin_name
60 - * @return array The agent/agency/developer post-type slugs.
52 + * @return string Property post-type slug.
61 53 */
62 - public function get_agent_post_type() {
63 - // WpResidence splits contacts across three post types.
64 - return array('estate_agent','estate_agency','estate_developer');
54 + public function property_post_type(): string {
55 + return 'estate_property';
65 56 }
66 57
67 -
68 58 /**
69 - * image save
59 + * Persist WpResidence's one-array gallery representation.
70 60 *
71 - * No-op for WpResidence: single featured/gallery images are handled elsewhere;
72 - * the gallery is saved in one batch by enviroment_image_save_gallery().
73 - *
74 - * @since 1.0.0
75 - * @access protected
76 - * @var string $plugin_name
77 - * @param int $property_id The property post ID.
78 - * @param int $attach_id The image attachment ID (unused).
61 + * @param int $property_id Managed Listing post ID.
62 + * @param array<int, int> $attachment_ids Final ordered attachment IDs.
63 + * @return bool Whether the gallery representation was written.
79 64 */
80 - public function enviroment_image_save( $property_id, $attach_id ) {
81 - // Intentionally does nothing for this theme.
82 - return;
65 + public function write_gallery( int $property_id, array $attachment_ids ): bool {
66 + update_post_meta( $property_id, 'wpestate_property_gallery', $attachment_ids );
67 + return true;
83 68 }
84 69
85 -
86 -
87 70 /**
88 - * image save
71 + * Persist only WpResidence-specific fields, defaults, and agent linkage.
89 72 *
90 - * Save the full gallery for a property in the theme's gallery meta.
73 + * Shared selection, normalization, mappings, Rooms, and order have already
74 + * been resolved by Stored Listing Write. Existing listings skip the defaults
75 + * and Assigned Agent so later task changes cannot reassign them.
91 76 *
92 - * @since 1.0.0
93 - * @access protected
94 - * @var string $plugin_name
95 - * @param int $property_id The property post ID.
96 - * @param array $post_attachments Ordered array of attachment IDs.
77 + * @param int $property_id Managed Listing post ID.
78 + * @param array<string, mixed> $property Raw incoming property.
79 + * @param array<string, mixed> $context Prepared fields and creation choices.
80 + * @return bool Whether the theme projection completed.
97 81 */
98 - public function enviroment_image_save_gallery( $property_id, $post_attachments ) {
99 - // NB: $gallery_meta (CSV form) is built but the array is what gets stored.
100 - $gallery_meta = implode(',', $post_attachments);
101 - // WpResidence reads the gallery from the wpestate_property_gallery meta.
102 - update_post_meta($property_id, 'wpestate_property_gallery', $post_attachments);
82 + public function write_theme_projection( int $property_id, array $property, array $context ): bool {
83 + foreach ( (array) ( $context['fields'] ?? array() ) as $field ) {
84 + // GitHub issue #286: 'listingkey' is the same case-insensitive meta row
85 + // as the listing identity, and a WPResidence custom field the edit
86 + // screen saves blank. The identity is never projected as theme meta.
87 + if ( 'listingkey' === strtolower( (string) $field['field'] ) ) {
88 + continue;
89 + }
90 + update_post_meta( $property_id, strtolower( (string) $field['field'] ), (string) $field['value'] );
91 + }
92 + if ( empty( $context['is_new'] ) ) {
93 + return true;
94 + }
103 95
96 + update_post_meta( $property_id, 'prop_featured', 0 );
97 + update_post_meta( $property_id, 'page_custom_zoom', 16 );
98 + // Country comes from the feed itself (Centris sends "CA"); the helper
99 + // falls back to the historical "United States" default when absent.
100 + update_post_meta( $property_id, 'property_country', mlsimport_property_country( $property ) );
101 + update_post_meta( $property_id, 'property_agent', (int) ( $context['assigned_agent_id'] ?? 0 ) );
102 + update_post_meta( $property_id, 'property_page_desing_local', '' );
103 + foreach ( array( 'header_transparent', 'page_show_adv_search', 'sidebar_agent_option', 'local_pgpr_slider_type', 'local_pgpr_content_type', 'sidebar_select', 'sidebar_option' ) as $key ) {
104 + update_post_meta( $property_id, $key, 'global' );
105 + }
106 + update_post_meta( $property_id, 'header_type', 0 );
107 + if ( function_exists( 'wpestate_update_hiddent_address_single' ) ) {
108 + wpestate_update_hiddent_address_single( $property_id );
109 + }
110 + return true;
104 111 }
105 112
106 113
107 114
108 -
109 -
110 - /**
111 - * Format extra meta values into a safe string representation.
112 - *
113 - * Collapses arrays (JSON-encoding nested arrays, trimming scalars) into a
114 - * comma-separated string; scalars are trimmed with comma spacing normalized.
115 - *
116 - * @param mixed $meta_value Raw meta value (scalar or array).
117 - * @return string Comma-separated, normalized string form.
118 - */
119 - private function normalizeExtraMetaValue($meta_value) {
120 - // Array values are flattened into a comma-separated list.
121 - if (is_array($meta_value)) {
122 - $normalized = array();
123 -
124 - // Walk each element, encoding nested arrays and trimming scalars.
125 - foreach ($meta_value as $value) {
126 - if (is_array($value)) {
127 - // Nested array: prefer wp_json_encode, fall back to json_encode.
128 - if (function_exists('wp_json_encode')) {
129 - $encoded = wp_json_encode($value);
130 - } else {
131 - $encoded = json_encode($value);
132 - }
133 -
134 - // Keep only a successful encoding.
135 - if (false !== $encoded && null !== $encoded) {
136 - $normalized[] = $encoded;
137 - }
138 - } else {
139 - // Scalar: trim and skip empties.
140 - $value = trim((string) $value);
141 - if ('' !== $value) {
142 - $normalized[] = $value;
143 - }
144 - }
145 - }
146 -
147 - // Nothing usable collected -> empty string.
148 - if (empty($normalized)) {
149 - return '';
150 - }
151 -
152 - // Join collected pieces with commas.
153 - return implode(', ', $normalized);
154 - }
155 -
156 - // Scalar input: trim and normalize whitespace around commas.
157 - return preg_replace('/\s*,\s*/', ', ', trim((string) $meta_value));
158 - }
159 -
160 - /**
161 - * Format Rooms extra meta entries into a single string.
162 - *
163 - * Turns the RESO "Rooms" collection into a pipe-separated human-readable summary
164 - * (e.g. "Kitchen: Main, 10 x 12 ft | Bedroom: Upper").
165 - *
166 - * @param array $rooms Array of room detail arrays (RESO Rooms structure).
167 - * @param int $property_id Property post ID (unused; kept for signature parity).
168 - * @return string Pipe-separated room summaries, or '' when nothing usable.
169 - */
170 - private function formatRoomsExtraMeta($rooms, $property_id) {
171 - // Guard: nothing to format.
172 - if (!is_array($rooms) || empty($rooms)) {
173 - return '';
174 - }
175 -
176 -
177 - $formatted_rooms = array();
178 -
179 - // Build a summary line for each room entry.
180 - foreach ($rooms as $index => $room_details) {
181 - // Skip malformed (non-array) entries.
182 - if (!is_array($room_details)) {
183 - continue;
184 - }
185 -
186 - // Pull the RESO room fields, trimming each to a string.
187 - $room_type = isset($room_details['RoomType']) ? trim((string) $room_details['RoomType']) : '';
188 - $room_level = isset($room_details['RoomLevel']) ? trim((string) $room_details['RoomLevel']) : '';
189 - $room_length = isset($room_details['RoomLength']) ? trim((string) $room_details['RoomLength']) : '';
190 - $room_width = isset($room_details['RoomWidth']) ? trim((string) $room_details['RoomWidth']) : '';
191 - $room_units = isset($room_details['RoomLengthWidthUnits']) ? trim((string) $room_details['RoomLengthWidthUnits']) : '';
192 -
193 - // A room with no type is not worth listing.
194 - if ($room_type === '') {
195 - continue;
196 - }
197 -
198 - $details = array();
199 - // Include the floor/level when present.
200 - if ($room_level !== '') {
201 - $details[] = $room_level;
202 - }
203 -
204 - // Compose the dimension string from length and/or width.
205 - $dimension = '';
206 - if ($room_length !== '' && $room_width !== '') {
207 - $dimension = $room_length . ' x ' . $room_width;
208 - } elseif ($room_length !== '') {
209 - $dimension = $room_length;
210 - } elseif ($room_width !== '') {
211 - $dimension = $room_width;
212 - }
213 -
214 - // Append units to the dimension, or use units alone if no dimension.
215 - if ($dimension !== '') {
216 - if ($room_units !== '') {
217 - $dimension .= ' ' . $room_units;
218 - }
219 - $details[] = $dimension;
220 - } elseif ($room_units !== '') {
221 - $details[] = $room_units;
222 - }
223 -
224 - // Start with the room type, then append its details after a colon.
225 - $formatted_value = $room_type;
226 - if (!empty($details)) {
227 - $formatted_value .= ': ' . implode(', ', $details);
228 - }
229 -
230 - $formatted_rooms[] = $formatted_value;
231 - }
232 -
233 - // Join all room summaries with a pipe separator.
234 - $result = implode(' | ', $formatted_rooms);
235 -
236 - return $result;
237 - }
238 -
239 -
240 - /**
241 - * Deal with extra meta
242 - */
243 -
244 -
245 - /**
246 - * Deal with extra meta
247 - *
248 - * Iterates the property's selected RESO "extra" fields in memory-bounded batches,
249 - * routing each to a mapped custom post meta key, a mapped taxonomy term, or a
250 - * lowercased-name post meta. The RESO "Rooms" collection is flattened into one
251 - * meta. Returns an HTML history log (capped to conserve memory).
252 - *
253 - * @param int $property_id The property post ID.
254 - * @param array $property Parsed property payload (expects 'extra_meta').
255 - * @return array ['property_history' => string] HTML log of what was written.
256 - */
257 - public function mlsimportSaasSetExtraMeta($property_id, $property) {
258 - // Memory tracking
259 - // Record starting memory for the diagnostic diff at the end.
260 - $startMemory = memory_get_usage(true);
261 - $startMemoryFormatted = round($startMemory / 1048576, 2);
262 -
263 - $property_history = array();
264 - $answer = array();
265 -
266 - // Nothing to do without an extra_meta array -> return empty history.
267 - if (!isset($property['extra_meta']) || !is_array($property['extra_meta'])) {
268 - $answer['property_history'] = '';
269 -
270 - $endMemory = memory_get_usage(true);
271 - $endMemoryFormatted = round($endMemory / 1048576, 2);
272 - $memoryDiff = $endMemoryFormatted - $startMemoryFormatted;
273 -
274 - return $answer;
275 - }
276 -
277 - // Get options once and extract only what we need
278 - // Field-selection options decide which extra fields are imported and how.
279 - $options = get_option('mlsimport_admin_fields_select');
280 - $permited_meta = isset($options['mls-fields']) ? $options['mls-fields'] : array();
281 -
282 - // Process meta in smaller batches
283 - // Chunk the field keys so each batch can be GC'd, bounding peak memory.
284 - $meta_batches = array_chunk(array_keys($property['extra_meta']), 10, true);
285 -
286 - $batch_count = 0;
287 - // Process each batch of up to 10 field keys.
288 - foreach ($meta_batches as $meta_batch_keys) {
289 - $batch_count++;
290 -
291 - $batchMemory = memory_get_usage(true);
292 - $batchMemoryFormatted = round($batchMemory / 1048576, 2);
293 -
294 - // Handle each field in the current batch.
295 - foreach ($meta_batch_keys as $meta_name) {
296 - // Skip if meta doesn't exist in property extra_meta
297 - if (!isset($property['extra_meta'][$meta_name])) {
298 - continue;
299 - }
300 -
301 - $meta_value = $property['extra_meta'][$meta_name];
302 -
303 - // Check if extra meta is set to import
304 - if (!isset($permited_meta[$meta_name])) {
305 - // We do not have the extra meta
306 - continue;
307 - } elseif (isset($permited_meta[$meta_name]) && intval($permited_meta[$meta_name]) === 0) {
308 - // Meta exists but is set to no
309 - continue;
310 - }
311 -
312 - // Special-case the RESO "Rooms" collection -> single formatted meta.
313 - if ('Rooms' === $meta_name && is_array($meta_value)) {
314 - $formatted_rooms = $this->formatRoomsExtraMeta($meta_value, $property_id);
315 - if ($formatted_rooms !== '') {
316 - update_post_meta($property_id, 'rooms', $formatted_rooms);
317 - $property_history[] = 'Updated EXTRA Meta rooms';
318 - }
319 - continue;
320 - }
321 -
322 - // Normalize the raw value to a storable string.
323 - $normalized_meta_value = $this->normalizeExtraMetaValue($meta_value);
324 -
325 - $original_meta_name = $meta_name;
326 - $meta_name_lower = strtolower($meta_name);
327 -
328 - // Process custom postmeta mapping
329 - // Route 1: field mapped to an explicit custom post meta key.
330 - if (isset($options['mls-fields-map-postmeta'][$original_meta_name]) && $options['mls-fields-map-postmeta'][$original_meta_name] !== '') {
331 - $new_post_meta_key = $options['mls-fields-map-postmeta'][$original_meta_name];
332 -
333 - update_post_meta($property_id, $new_post_meta_key, $normalized_meta_value);
334 - $property_history[] = 'Updated CUSTOM post meta ' . $new_post_meta_key . ' original ' . $meta_name;
335 - }
336 - // Process custom taxonomy mapping
337 - // Route 2: field mapped to a taxonomy -> attach a labeled term.
338 - else if (isset($options['mls-fields-map-taxonomy'][$original_meta_name]) && $options['mls-fields-map-taxonomy'][$original_meta_name] !== '') {
339 - $new_taxonomy = $options['mls-fields-map-taxonomy'][$original_meta_name];
340 - $custom_label = isset($options['mls-fields-label'][$original_meta_name]) ? $options['mls-fields-label'][$original_meta_name] : '';
341 -
342 - // A label of 'none' means "no prefix".
343 - if ($custom_label == 'none') {
344 - $custom_label = '';
345 - }
346 -
347 - // Build the term name from optional label + value; skip if empty.
348 - $meta_value_with_label = array();
349 - $term_value = trim($custom_label . ' ' . $normalized_meta_value);
350 - if ($term_value !== '') {
351 - $meta_value_with_label[] = $term_value;
352 - }
353 -
354 - if (!empty($meta_value_with_label)) {
355 - // Append (append=true) the term to the property.
356 - wp_set_object_terms($property_id, $meta_value_with_label, $new_taxonomy, true);
357 -
358 - // Fix the clean_term_cache call to avoid SQL errors
359 - // Resolve created/matched term IDs before clearing their cache.
360 - $term_ids = array();
361 - foreach ($meta_value_with_label as $term_name) {
362 - $term = get_term_by('name', $term_name, $new_taxonomy);
363 - if ($term && !is_wp_error($term)) {
364 - $term_ids[] = $term->term_id;
365 - }
366 - }
367 -
368 - // Clear cache by term IDs (not the property ID).
369 - if (!empty($term_ids)) {
370 - clean_term_cache($term_ids, $new_taxonomy);
371 - }
372 - }
373 -
374 - $property_history[] = 'Updated CUSTOM TAX: ' . $new_taxonomy . ' original ' . $original_meta_name;
375 - }
376 - // Standard meta update
377 - // Route 3: no mapping -> store under the lowercased field name.
378 - else {
379 - update_post_meta($property_id, $meta_name_lower, $normalized_meta_value);
380 - $property_history[] = 'Updated EXTRA Meta ' . $meta_name_lower;
381 - }
382 -
383 - // Clear each variable after use
384 - $meta_value = null;
385 - $normalized_meta_value = null;
386 - }
387 -
388 - // Clean up after each batch
389 - // Flush caches and force GC between batches to keep memory flat.
390 - wp_cache_flush();
391 - gc_collect_cycles();
392 -
393 - $afterBatchMemory = memory_get_usage(true);
394 - $afterBatchMemoryFormatted = round($afterBatchMemory / 1048576, 2);
395 - $batchMemoryDiff = $afterBatchMemoryFormatted - $batchMemoryFormatted;
396 - }
397 -
398 - // Remove unused code that was referencing undefined variables
399 - // (The processed_field_values code wasn't being used)
400 -
401 - // Prepare the answer with limited history to conserve memory
402 - // Cap the history to the last 20 entries to bound the returned string.
403 - if (count($property_history) > 20) {
404 - $property_history = array_slice($property_history, -20);
405 - $property_history[] = '... [truncated history to save memory] ...';
406 - }
407 -
408 - // Join the history entries into a single HTML string.
409 - $answer['property_history'] = implode('</br>', $property_history);
410 -
411 - // Clean up variables
412 - $property_history = null;
413 - $options = null;
414 - $permited_meta = null;
415 -
416 - // Final memory tracking
417 - $endMemory = memory_get_usage(true);
418 - $endMemoryFormatted = round($endMemory / 1048576, 2);
419 - $memoryDiff = $endMemoryFormatted - $startMemoryFormatted;
420 -
421 - return $answer;
422 - }
423 -
424 -
425 -
426 -
427 -
428 115 /**
429 - * set hardcode fields after updated
116 + * return custom post field
430 117 *
431 - * On first insert, seed WpResidence's default meta (featured flag, map zoom,
432 - * country, assigned agent, and various "global" layout/sidebar settings), then
433 - * refresh the theme's hidden-address value for the property.
118 + * The post type(s) WpResidence uses for agents, agencies, and developers.
434 119 *
435 120 * @since 1.0.0
436 121 * @access protected
437 122 * @var string $plugin_name
438 - * @param string $is_insert 'yes' when the property was newly inserted.
439 - * @param int $property_id The property post ID.
440 - * @param array $global_extra_fields Extra fields (unused here).
441 - * @param mixed $new_agent Agent ID to assign to the property.
123 + * @return array The agent/agency/developer post-type slugs.
442 124 */
443 - public function correlationUpdateAfter( $is_insert, $property_id, $global_extra_fields, $new_agent ) {
444 - // Only seed defaults for freshly inserted properties.
445 - if ( 'yes' === $is_insert ) {
446 - // Not featured; default map zoom; default country.
447 - update_post_meta( $property_id, 'prop_featured', 0 );
448 - update_post_meta( $property_id, 'page_custom_zoom', 16 );
449 - update_post_meta( $property_id, 'property_country', 'United States' );
450 -
451 - // Assign the agent and reset the per-page design override.
452 - update_post_meta( $property_id, 'property_agent', $new_agent );
453 - update_post_meta( $property_id, 'property_page_desing_local', '' );
454 - // Layout/header/search/sidebar settings all inherit the theme "global" default.
455 - update_post_meta( $property_id, 'header_transparent', 'global' );
456 - update_post_meta( $property_id, 'page_show_adv_search', 'global' );
457 - update_post_meta( $property_id, 'page_show_adv_search', 'global' );
458 - update_post_meta( $property_id, 'header_type', 0 );
459 - update_post_meta( $property_id, 'sidebar_agent_option', 'global' );
460 - update_post_meta( $property_id, 'local_pgpr_slider_type', 'global' );
461 - update_post_meta( $property_id, 'local_pgpr_content_type', 'global' );
462 - update_post_meta( $property_id, 'sidebar_select', 'global' );
463 - update_post_meta( $property_id, 'sidebar_option', 'global' );
464 -
465 - // Let the theme recompute the hidden/approximate address if available.
466 - if ( function_exists( 'wpestate_update_hiddent_address_single' ) ) {
467 - wpestate_update_hiddent_address_single( $property_id );
468 - }
469 - }
125 + public function get_agent_post_type() {
126 + // WpResidence splits contacts across three post types.
127 + return array('estate_agent','estate_agency','estate_developer');
470 128 }
471 129
472 130
473 131
474 -
475 132 /**
476 133 * save custom fields per environment
477 134 *
478 135 * Syncs the plugin's selected MLS fields into WpResidence's custom-fields registry
@@ -483,9 +140,9 @@
483 140 * @access protected
484 141 * @var string $plugin_name
485 142 * @param string $option_name Option prefix used to read '{prefix}_admin_fields_select'.
486 143 */
487 - public function enviroment_custom_fields( $option_name ) {
144 + public function enviroment_custom_fields( $option_name ) {
488 145 // Load the theme option holding the custom-fields registry.
489 146 $theme_options = get_option( 'wpresidence_admin' );
490 147 $custom_fields = array();
491 148 if ( isset( $theme_options['wpestate_custom_fields_list'] ) ) {
@@ -504,9 +161,10 @@
504 161 }
505 162 }
506 163
507 164 // Load the plugin's field-selection options for this prefix.
508 - $options = get_option( $option_name . '_admin_fields_select' );
165 + $options = mlsimport_normalized_field_configuration();
166 + $active_fields = array_fill_keys( array_keys( mlsimport_field_configuration_metadata() ), true );
509 167
510 168 // Reconcile each MLS field against the theme registry.
511 169 foreach ( $options['mls-fields'] as $key => $value ) {
512 170 // Per-field flags: enabled, admin-managed, taxonomy target, order.
@@ -515,9 +173,9 @@
515 173 $taxonomy = isset( $options['mls-fields-map-taxonomy'][ $key ] ) ? $options['mls-fields-map-taxonomy'][ $key ] : '';
516 174 $order_value = isset( $options['field_order'][ $key ] ) ? intval( $options['field_order'][ $key ] ) + 100 : 100;
517 175
518 176 // Add/update only enabled, non-admin, non-taxonomy fields.
519 - if ( 1 === $import && 0 === $admin && '' === $taxonomy ) {
177 + if ( isset( $active_fields[ $key ] ) && 1 === $import && 0 === $admin && '' === $taxonomy ) {
520 178 // Is this field already registered?
521 179 $existing_index = array_search( $key, $custom_fields['add_field_name'], true );
522 180
523 181 if ( false === $existing_index ) {