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.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 6.0.5 All 36 releases
mlsimport / includes / live / live-parse.php

live-parse.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.0.4, at includes/live/live-parse.php

151 lines 5.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Live mode: normalize a provider response body into { records, total }.
4 *
5 * Pure functions — no WordPress, no network. Family 1 providers all answer
6 * with the standard OData envelope: @odata.count + value[]. Provider-family
7 * variants (Rapattoni, Realtor.ca) extend this file when they are ported.
8 *
9 * @package Mlsimport
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 /**
17 * Parse a raw response body into records + total.
18 *
19 * @param string $body Raw HTTP response body.
20 * @param array $config Per-MLS config (type, …) — kept as the family-dispatch
21 * seam; every current provider answers one of the two
22 * envelopes below (Rapattoni's PropertyPictures quirk is
23 * normalized later, in live-map-reso).
24 * @return array{records:array,total:int}|null Null when the body is not a
25 * usable listing payload (bad JSON or a provider error envelope) —
26 * callers treat null as "keep the warm cache".
27 */
28 function mlsimport_live_parse_response( string $body, array $config ): ?array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- $config is the family-dispatch seam.
29 // Bad JSON or an error envelope: null tells callers to keep the warm cache.
30 $data = json_decode( $body, true );
31 if ( ! is_array( $data ) || isset( $data['error'] ) ) {
32 return null;
33 }
34
35 // Bridge native listings envelope: success/total/bundle. A single-listing
36 // fetch returns bundle as one object instead of a list.
37 if ( array_key_exists( 'success', $data ) || array_key_exists( 'bundle', $data ) ) {
38 // Reject an unsuccessful or malformed bundle.
39 if ( empty( $data['success'] ) || ! isset( $data['bundle'] ) || ! is_array( $data['bundle'] ) ) {
40 return null;
41 }
42 $bundle = $data['bundle'];
43 // A list-shaped bundle stays a list; a single object is wrapped into one.
44 $records = array_keys( $bundle ) === range( 0, count( $bundle ) - 1 ) || array() === $bundle
45 ? array_values( $bundle )
46 : array( $bundle );
47 // Use the envelope's total when given, else the returned record count.
48 $total = isset( $data['total'] ) && is_numeric( $data['total'] ) ? (int) $data['total'] : count( $records );
49
50 return array(
51 'records' => $records,
52 'total' => $total,
53 );
54 }
55
56 // Generic RESO OData envelope: @odata.count + value[].
57 $records = isset( $data['value'] ) && is_array( $data['value'] ) ? array_values( $data['value'] ) : array();
58 // @odata.count when present, otherwise the page's own record count.
59 $total = isset( $data['@odata.count'] ) && is_numeric( $data['@odata.count'] )
60 ? (int) $data['@odata.count']
61 : count( $records );
62
63 return array(
64 'records' => $records,
65 'total' => $total,
66 );
67 }
68
69 /**
70 * Group BrightMedia rows into standard RESO Media arrays, keyed by their
71 * listing's ResourceRecordKey — the shape mlsimport_live_media_urls() reads.
72 * HiRes URL preferred, plain MediaURL the fallback (as the AWS media fetch
73 * normalizes).
74 *
75 * @param array $rows BrightMedia value[] rows.
76 * @return array<string,array> ListingKey => Media[].
77 */
78 function mlsimport_live_brightmls_media_map( array $rows ): array {
79 $map = array();
80 // Group every media row under its listing's ResourceRecordKey.
81 foreach ( $rows as $row ) {
82 // Skip rows we can't attribute to a listing.
83 if ( ! is_array( $row ) || ! isset( $row['ResourceRecordKey'] ) ) {
84 continue;
85 }
86 // Prefer the hi-res URL; fall back to the plain one; skip if neither.
87 $url = ! empty( $row['MediaURLHiRes'] ) ? $row['MediaURLHiRes'] : ( $row['MediaURL'] ?? '' );
88 if ( '' === (string) $url ) {
89 continue;
90 }
91 // Append a standard RESO-Media-shaped entry under this listing's key.
92 $map[ (string) $row['ResourceRecordKey'] ][] = array(
93 'MediaURL' => (string) $url,
94 'Order' => $row['MediaDisplayOrder'] ?? null,
95 'MediaCategory' => $row['MediaCategory'] ?? null,
96 'MediaType' => $row['MediaType'] ?? null,
97 );
98 }
99 return $map;
100 }
101
102 /**
103 * The SaaS /clusters answer as the map's { type: clusters } payload — the
104 * same shape stored mode emits, so the map JS needs nothing new. Anything
105 * off (failed, missing pieces, malformed rows) yields null and the caller
106 * keeps the v1 zoom-in state.
107 *
108 * @param mixed $answer Decoded /clusters response body.
109 * @return array|null
110 */
111 function mlsimport_live_clusters_payload( $answer ): ?array {
112 // Anything missing or malformed: null keeps the caller on the zoom-in state.
113 if ( ! is_array( $answer ) || empty( $answer['success'] ) || ! isset( $answer['total'] ) || ! is_array( $answer['clusters'] ?? null ) ) {
114 return null;
115 }
116
117 $clusters = array();
118 // Keep only well-formed cluster rows, coercing their types.
119 foreach ( $answer['clusters'] as $row ) {
120 if ( ! is_array( $row ) || ! isset( $row['lat'], $row['lng'], $row['count'] ) ) {
121 continue;
122 }
123 $clusters[] = array(
124 'lat' => (float) $row['lat'],
125 'lng' => (float) $row['lng'],
126 'count' => (int) $row['count'],
127 );
128 }
129
130 // The map JS's { type: clusters } shape: total + the cleaned bubbles.
131 $payload = array(
132 'type' => 'clusters',
133 'total' => (int) $answer['total'],
134 'clusters' => $clusters,
135 );
136
137 // The index's exact in-bbox bounds: lets an unfiltered map block fit
138 // itself from the same cached answer, no record fetch.
139 $bounds = $answer['bounds'] ?? null;
140 if ( is_array( $bounds ) && isset( $bounds['lat_min'], $bounds['lat_max'], $bounds['lng_min'], $bounds['lng_max'] ) ) {
141 $payload['bounds'] = array(
142 'lat_min' => (float) $bounds['lat_min'],
143 'lat_max' => (float) $bounds['lat_max'],
144 'lng_min' => (float) $bounds['lng_min'],
145 'lng_max' => (float) $bounds['lng_max'],
146 );
147 }
148
149 return $payload;
150 }
151