PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.0.4
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.0.4
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 / enviroment / ResidenceClass.php

ResidenceClass.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.0.4, at enviroment/ResidenceClass.php

600 lines 26.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ResidenceClass — theme adapter for the WpResidence (WpEstate family) WordPress theme.
4 *
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.
11 *
12 * @package MLSImport
13 */
14 // Abort if the file is accessed directly outside of WordPress.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly
17 }
18
19
20 /**
21 * Description of ResidenceClass
22 *
23 * @author mlsimport
24 */
25 class ResidenceClass {
26
27 /**
28 * Constructor — no setup required for this adapter.
29 */
30 public function __construct() {
31
32 }
33
34
35 /**
36 * return custom post field
37 *
38 * The post type WpResidence uses to store property listings.
39 *
40 * @since 1.0.0
41 * @access protected
42 * @var string $plugin_name
43 * @return string The 'estate_property' post-type slug.
44 */
45 public function get_property_post_type() {
46 // WpResidence stores listings in the 'estate_property' post type.
47 return 'estate_property';
48 }
49
50
51
52 /**
53 * return custom post field
54 *
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.
61 */
62 public function get_agent_post_type() {
63 // WpResidence splits contacts across three post types.
64 return array('estate_agent','estate_agency','estate_developer');
65 }
66
67
68 /**
69 * image save
70 *
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).
79 */
80 public function enviroment_image_save( $property_id, $attach_id ) {
81 // Intentionally does nothing for this theme.
82 return;
83 }
84
85
86
87 /**
88 * image save
89 *
90 * Save the full gallery for a property in the theme's gallery meta.
91 *
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.
97 */
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);
103
104 }
105
106
107
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 /**
429 * set hardcode fields after updated
430 *
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.
434 *
435 * @since 1.0.0
436 * @access protected
437 * @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.
442 */
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 }
470 }
471
472
473
474
475 /**
476 * save custom fields per environment
477 *
478 * Syncs the plugin's selected MLS fields into WpResidence's custom-fields registry
479 * (stored in 'wpresidence_admin' -> 'wpestate_custom_fields_list'). Enabled,
480 * non-admin, non-taxonomy fields are added/updated; others removed; list re-sorted.
481 *
482 * @since 1.0.0
483 * @access protected
484 * @var string $plugin_name
485 * @param string $option_name Option prefix used to read '{prefix}_admin_fields_select'.
486 */
487 public function enviroment_custom_fields( $option_name ) {
488 // Load the theme option holding the custom-fields registry.
489 $theme_options = get_option( 'wpresidence_admin' );
490 $custom_fields = array();
491 if ( isset( $theme_options['wpestate_custom_fields_list'] ) ) {
492 $custom_fields = $theme_options['wpestate_custom_fields_list'];
493 }
494
495 // Ensure we have an array to work with.
496 if ( ! is_array( $custom_fields ) ) {
497 $custom_fields = array();
498 }
499
500 // Guarantee every parallel column exists as an array.
501 foreach ( array( 'add_field_name', 'add_field_label', 'add_field_type', 'add_field_order', 'add_dropdown_order' ) as $field_key ) {
502 if ( ! isset( $custom_fields[ $field_key ] ) || ! is_array( $custom_fields[ $field_key ] ) ) {
503 $custom_fields[ $field_key ] = array();
504 }
505 }
506
507 // Load the plugin's field-selection options for this prefix.
508 $options = get_option( $option_name . '_admin_fields_select' );
509
510 // Reconcile each MLS field against the theme registry.
511 foreach ( $options['mls-fields'] as $key => $value ) {
512 // Per-field flags: enabled, admin-managed, taxonomy target, order.
513 $import = intval( $value );
514 $admin = isset( $options['mls-fields-admin'][ $key ] ) ? intval( $options['mls-fields-admin'][ $key ] ) : 0;
515 $taxonomy = isset( $options['mls-fields-map-taxonomy'][ $key ] ) ? $options['mls-fields-map-taxonomy'][ $key ] : '';
516 $order_value = isset( $options['field_order'][ $key ] ) ? intval( $options['field_order'][ $key ] ) + 100 : 100;
517
518 // Add/update only enabled, non-admin, non-taxonomy fields.
519 if ( 1 === $import && 0 === $admin && '' === $taxonomy ) {
520 // Is this field already registered?
521 $existing_index = array_search( $key, $custom_fields['add_field_name'], true );
522
523 if ( false === $existing_index ) {
524 // New field: append across all parallel columns.
525 $custom_fields['add_field_name'][] = $key;
526 $label = isset( $options['mls-fields-label'][ $key ] ) && '' !== $options['mls-fields-label'][ $key ] ? $options['mls-fields-label'][ $key ] : $key;
527 $custom_fields['add_field_label'][] = $label;
528 $custom_fields['add_field_type'][] = 'short text';
529 $custom_fields['add_field_order'][] = $order_value;
530 $custom_fields['add_dropdown_order'][] = '';
531 } else {
532 // Existing field: refresh its label and order in place.
533 $label = isset( $options['mls-fields-label'][ $key ] ) && '' !== $options['mls-fields-label'][ $key ] ? $options['mls-fields-label'][ $key ] : $key;
534 $custom_fields['add_field_label'][ $existing_index ] = $label;
535 $custom_fields['add_field_order'][ $existing_index ] = $order_value;
536 }
537 } else {
538 // Not eligible: remove it from every parallel column if present.
539 $remove_index = array_search( $key, $custom_fields['add_field_name'], true );
540 if ( false !== $remove_index ) {
541 unset( $custom_fields['add_field_name'][ $remove_index ] );
542 unset( $custom_fields['add_field_label'][ $remove_index ] );
543 unset( $custom_fields['add_field_type'][ $remove_index ] );
544 unset( $custom_fields['add_field_order'][ $remove_index ] );
545 unset( $custom_fields['add_dropdown_order'][ $remove_index ] );
546 }
547 }
548 }
549
550 // Re-sort all columns by field order so the registry stays ordered.
551 if ( ! empty( $custom_fields['add_field_order'] ) ) {
552 // Sort the order column, preserving keys as the new sequence.
553 asort( $custom_fields['add_field_order'] );
554 $ordered = array(
555 'add_field_name' => array(),
556 'add_field_label' => array(),
557 'add_field_type' => array(),
558 'add_field_order' => array(),
559 'add_dropdown_order' => array(),
560 );
561 // Rebuild each column following the sorted order of indices.
562 foreach ( array_keys( $custom_fields['add_field_order'] ) as $idx ) {
563 $ordered['add_field_name'][] = $custom_fields['add_field_name'][ $idx ];
564 $ordered['add_field_label'][] = $custom_fields['add_field_label'][ $idx ];
565 $ordered['add_field_type'][] = $custom_fields['add_field_type'][ $idx ];
566 $ordered['add_field_order'][] = $custom_fields['add_field_order'][ $idx ];
567 $ordered['add_dropdown_order'][] = $custom_fields['add_dropdown_order'][ $idx ];
568 }
569 $custom_fields = $ordered;
570 }
571
572 // Persist the updated registry back to the theme option.
573 $theme_options['wpestate_custom_fields_list'] = $custom_fields;
574 update_option( 'wpresidence_admin', $theme_options );
575
576 }
577
578
579
580
581
582
583 /**
584 * return theme schema
585 *
586 * No-op placeholder: WpResidence provides no field schema through this adapter.
587 *
588 * @since 1.0.0
589 * @access protected
590 * @var string $plugin_name
591 */
592 public function return_theme_schema() {
593 // No schema to return for this theme.
594 return;
595 }
596
597
598
599 }
600