PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.1.1
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.1.1
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 / standalone / class-mlsimport-standalone-derive.php

class-mlsimport-standalone-derive.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.1.1, at includes/standalone/class-mlsimport-standalone-derive.php

197 lines 6.2 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) derivation helpers.
4 *
5 * Pure transformations from a raw RESO property array into the normalized
6 * scalar a flat-table column expects. No WordPress, no DB. RESO scalars are
7 * frequently delivered as strings, so numeric inputs are coerced.
8 *
9 * @package Mlsimport
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 /**
17 * Pure RESO-value derivation helpers for the standalone write adapter.
18 */
19 class Mlsimport_Standalone_Derive {
20
21 /**
22 * Bathrooms as a decimal, preferring the RESO BathroomsTotalDecimal field.
23 *
24 * @param array $property Raw RESO property (PascalCase keys).
25 * @return float|null Decimal bathroom count, or null when unknown.
26 */
27 public static function derive_bathrooms( array $property ): ?float {
28 $decimal = self::numeric( $property, 'BathroomsTotalDecimal' );
29 if ( null !== $decimal ) {
30 return $decimal;
31 }
32
33 $full = self::numeric( $property, 'BathroomsFull' );
34 $half = self::numeric( $property, 'BathroomsHalf' );
35
36 if ( null !== $full || null !== $half ) {
37 return (float) $full + ( (float) $half * 0.5 );
38 }
39
40 return null;
41 }
42
43 /**
44 * Lot size in square feet, preferring the RESO LotSizeSquareFeet field.
45 *
46 * @param array $property Raw RESO property (PascalCase keys).
47 * @return float|null Lot size in sqft, or null when unknown.
48 */
49 public static function derive_lot_sqft( array $property ): ?float {
50 $sqft = self::numeric( $property, 'LotSizeSquareFeet' );
51 if ( null !== $sqft ) {
52 return $sqft;
53 }
54
55 $acres = self::numeric( $property, 'LotSizeAcres' );
56 if ( null !== $acres ) {
57 return $acres * 43560;
58 }
59
60 return null;
61 }
62
63 /**
64 * Listing date, preferring ListingContractDate and falling back to OnMarketDate.
65 *
66 * @param array $property Raw RESO property (PascalCase keys).
67 * @return string|null Raw RESO date string, or null when unknown.
68 */
69 public static function derive_list_date( array $property ): ?string {
70 return self::non_empty_string( $property, 'ListingContractDate' )
71 ?? self::non_empty_string( $property, 'OnMarketDate' );
72 }
73
74 /**
75 * Clean virtual-tour URL from the RESO VirtualTourURLUnbranded value.
76 *
77 * The provider often delivers an <iframe> HTML blob rather than a bare URL;
78 * extract the src attribute. A blob with no iframe src falls back to the
79 * raw trimmed value (assumed to already be a URL).
80 *
81 * @param string|null $blob Raw VirtualTourURLUnbranded value.
82 * @return string|null Clean URL, or null when empty.
83 */
84 public static function extract_tour_src( ?string $blob ): ?string {
85 if ( preg_match( '/src="([^"]+)"/', (string) $blob, $matches ) ) {
86 return $matches[1];
87 }
88
89 $raw = trim( (string) $blob );
90
91 return '' === $raw ? null : $raw;
92 }
93
94 /**
95 * Normalize a RESO date/timestamp to MySQL DATETIME (Y-m-d H:i:s).
96 *
97 * RESO sends ISO-8601 (e.g. 2024-01-15T10:30:00Z); the literal date/time is
98 * preserved (T->space, fractional seconds + timezone offset dropped) without
99 * a timezone shift. Date-only values get 00:00:00. Returns null when the
100 * value is empty or not a recognizable date.
101 *
102 * @param mixed $value Raw date string.
103 * @return string|null
104 */
105 public static function normalize_datetime( $value ): ?string {
106 if ( ! is_scalar( $value ) ) {
107 return null;
108 }
109
110 $value = trim( (string) $value );
111 if ( '' === $value ) {
112 return null;
113 }
114
115 if ( ! preg_match( '/^(\d{4}-\d{2}-\d{2})(?:[T ](\d{2}:\d{2}:\d{2}))?/', $value, $matches ) ) {
116 return null;
117 }
118
119 $time = isset( $matches[2] ) ? $matches[2] : '00:00:00';
120
121 return $matches[1] . ' ' . $time;
122 }
123
124 /**
125 * Break a single-block listing remark into readable paragraphs.
126 *
127 * MLS feeds deliver PublicRemarks as one unbroken run of text with no line
128 * breaks, so wpautop() (or any theme) renders it as one giant paragraph. This
129 * groups the remark into paragraphs of roughly $per sentences each, joined by
130 * blank lines so wpautop wraps each in its own <p>. Remarks that already carry
131 * their own structure (blank lines or HTML block tags) are returned untouched.
132 *
133 * @param string $text Raw remark text.
134 * @param int $per Sentences per paragraph (default 3).
135 * @return string Text with blank-line paragraph breaks, or the original.
136 */
137 public static function paragraphs( string $text, int $per = 3 ): string {
138 $text = trim( $text );
139 if ( '' === $text ) {
140 return '';
141 }
142
143 // Respect remarks that already provide their own paragraphing.
144 if ( preg_match( '/\n\s*\n/', $text ) || preg_match( '/<\s*(?:p|br|div|ul|ol)\b/i', $text ) ) {
145 return $text;
146 }
147
148 // MLS feeds pad sentence gaps with single/double spaces -> normalize.
149 $text = (string) preg_replace( '/\s+/', ' ', $text );
150 $parts = preg_split( '/(?<=[.!?])\s+(?=[A-Z0-9"\'(])/', $text );
151 if ( ! is_array( $parts ) || count( $parts ) < 2 ) {
152 return $text;
153 }
154
155 // Re-join fragments split mid-sentence after a common abbreviation
156 // (e.g. "Mt. Hood") so paragraph breaks only land at real sentence ends.
157 $sentences = array();
158 foreach ( $parts as $part ) {
159 $prev = count( $sentences ) - 1;
160 if ( $prev >= 0 && preg_match( '/\b(?:Mt|St|Ave|Rd|Blvd|Ln|Ct|Apt|Ste|Sq|Ft|Approx|No|Jr|Sr|Dr)\.$/i', $sentences[ $prev ] ) ) {
161 $sentences[ $prev ] .= ' ' . $part;
162 continue;
163 }
164 $sentences[] = $part;
165 }
166
167 $paragraphs = array();
168 foreach ( array_chunk( $sentences, max( 1, $per ) ) as $chunk ) {
169 $paragraphs[] = implode( ' ', $chunk );
170 }
171
172 return implode( "\n\n", $paragraphs );
173 }
174
175 /**
176 * A numeric field coerced to float (RESO often sends numbers as strings).
177 *
178 * @param array $property Raw RESO property.
179 * @param string $key Field name.
180 * @return float|null Float value, or null when absent/non-numeric.
181 */
182 private static function numeric( array $property, string $key ): ?float {
183 return isset( $property[ $key ] ) && is_numeric( $property[ $key ] ) ? (float) $property[ $key ] : null;
184 }
185
186 /**
187 * A non-empty string field, or null when absent/empty.
188 *
189 * @param array $property Raw RESO property.
190 * @param string $key Field name.
191 * @return string|null String value, or null when absent/empty.
192 */
193 private static function non_empty_string( array $property, string $key ): ?string {
194 return isset( $property[ $key ] ) && '' !== $property[ $key ] ? (string) $property[ $key ] : null;
195 }
196 }
197