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.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 / enviroment / StandaloneClass.php

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

299 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 Stored mode theme adapter.
4 *
5 * Stored Listing Write owns shared listing decisions and persistence. This
6 * adapter retains the Standalone post type, gallery representation, RESO route
7 * map, taxonomies, derived flat-table row, content filters, and agent linkage.
8 * Its projection intentionally receives the raw RESO property at write time.
9 *
10 * @package MLSImport
11 */
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15
16 // Pull in the standalone helpers this adapter delegates to: the §9 RESO routing map,
17 // the derivation helpers, and the flat-table schema + row read/write classes.
18 require_once __DIR__ . '/../includes/standalone/class-mlsimport-standalone-reso-map.php';
19 require_once __DIR__ . '/../includes/standalone/class-mlsimport-standalone-derive.php';
20 require_once __DIR__ . '/../includes/standalone/class-mlsimport-standalone-table.php';
21 require_once __DIR__ . '/../includes/standalone/class-mlsimport-standalone-row.php';
22
23 /**
24 * Standalone (theme_id 990) write adapter.
25 *
26 * Implements the same adapter contract as ResidenceClass, but RESO-anchored: it
27 * routes a raw RESO property ($property['extra_meta'], PascalCase) into the
28 * mlsimport_property post (meta + 9 taxonomies + content) and the mlsimport_listings
29 * flat-table row, using the §9 routing map and the derivation helpers. The raw
30 * property and flat-table upsert remain together at this write point (ADR-0017).
31 */
32 class StandaloneClass {
33
34 /**
35 * Property CPT slug.
36 *
37 * @return string
38 */
39 public function get_property_post_type() {
40 return 'mlsimport_property';
41 }
42
43 /**
44 * Return the Standalone post type used for Managed Listing lookup/write.
45 *
46 * @return string Standalone Managed Listing post-type slug.
47 */
48 public function property_post_type(): string {
49 return 'mlsimport_property';
50 }
51
52 /**
53 * Agent CPT slug.
54 *
55 * @return string
56 */
57 public function get_agent_post_type() {
58 return 'mlsimport_agent';
59 }
60
61 /**
62 * Persist the gallery attachment IDs.
63 *
64 * @param int $property_id Post ID.
65 * @param array $post_attachments Attachment IDs.
66 * @return void
67 */
68 public function write_gallery( int $property_id, array $post_attachments ): bool {
69 update_post_meta( $property_id, 'mlsimport_gallery', $post_attachments );
70 return true;
71 }
72
73 /**
74 * Route the raw RESO property into post meta / taxonomies / content and the
75 * mlsimport_listings row.
76 *
77 * @param int $property_id Post ID.
78 * @param array $property Pipeline property; reads $property['extra_meta'].
79 * @param array $context Prepared fields and creation/agent choices.
80 * @return bool Whether the standalone projection completed.
81 */
82 public function write_theme_projection( int $property_id, array $property, array $context ): bool {
83 // Nothing to route without a RESO extra_meta array.
84 if ( ! isset( $property['extra_meta'] ) || ! is_array( $property['extra_meta'] ) ) {
85 return true;
86 }
87
88 // Raw RESO fields plus the accumulators used while routing them.
89 $extra = $property['extra_meta'];
90 $terms_by_tax = array();
91 $field_configuration = is_array( $context['field_configuration'] ?? null ) ? $context['field_configuration'] : array();
92 $opted_in = is_array( $field_configuration['mls-fields'] ?? null ) ? $field_configuration['mls-fields'] : array();
93 $listing_key = isset( $extra['ListingKey'] ) ? (string) $extra['ListingKey'] : (string) ( $property['ListingKey'] ?? '' );
94
95 // Route every incoming RESO field to its configured target(s).
96 foreach ( $extra as $field => $value ) {
97 $is_array = is_array( $value );
98
99 // A field may map to multiple targets (meta, tax, feature...).
100 foreach ( Mlsimport_Standalone_Reso_Map::targets_for( $field ) as $target ) {
101 // 'skip'/'content' are handled elsewhere (or not at all) here.
102 if ( 'skip' === $target || 'content' === $target ) {
103 continue;
104 }
105
106 // Target format is "<kind>:<name>" (e.g. "meta:x_foo", "tax:mlsimport_view").
107 $parts = explode( ':', $target, 2 );
108 $kind = $parts[0];
109 $name = isset( $parts[1] ) ? $parts[1] : '';
110
111 // Target kind 1: post meta.
112 if ( 'meta' === $kind ) {
113 // Meta key is namespaced; an "x_" name marks a passthrough field.
114 $meta_key = 'mlsimport_' . $name;
115 $is_passthrough = 0 === strpos( $name, 'x_' );
116 $raw = $value;
117
118 if ( $is_array ) {
119 // A multi-value RESO field (Flooring, Cooling, PoolFeatures...)
120 // stores as a comma-joined string, mapped or passthrough alike —
121 // that string is what the property page prints as the field's row.
122 // Arrays of records (Rooms, Media) have no scalar members and so
123 // store nothing.
124 $raw = implode( ', ', array_filter( $value, 'is_scalar' ) );
125 if ( '' === $raw ) {
126 continue;
127 }
128 }
129
130 /** Filter a property meta value before write. @since 6.3 */
131 $meta_value = apply_filters( 'mlsimport_property_meta_value', $raw, $meta_key, $property_id, $property );
132
133 if ( $is_passthrough ) {
134 // Passthrough: write only when the field is opted in via the selector.
135 if ( isset( $opted_in[ $field ] ) && 1 === intval( $opted_in[ $field ] ) ) {
136 update_post_meta( $property_id, $meta_key, $meta_value );
137 }
138 continue;
139 }
140 update_post_meta( $property_id, $meta_key, $meta_value );
141 // Target kind 2: taxonomy terms.
142 } elseif ( 'tax' === $kind ) {
143 // Multi-enum arrays (Appliances, View...) -> one term per value.
144 // Some feeds send the multi-enum as ONE comma-glued string; split
145 // it so each value still becomes its own term, never a glued term
146 // whose sanitized slug is a dead-end archive (fix #290).
147 foreach ( ( $is_array ? $value : array_map( 'trim', explode( ',', (string) $value ) ) ) as $term ) {
148 // Collect non-empty term names under their taxonomy.
149 if ( '' !== (string) $term ) {
150 $terms_by_tax[ $name ][] = (string) $term;
151 }
152 }
153 // Target kind 3: boolean "feature" flag -> label term when true.
154 } elseif ( 'feature' === $kind && ! $is_array && $this->is_truthy( $value ) ) {
155 // feature:<Label> -> add the label term only when the YN is true.
156 /** Filter the feature label term. @since 6.3 */
157 $terms_by_tax['mlsimport_feature'][] = (string) apply_filters( 'mlsimport_property_feature_label', $name, $field, $value );
158 }
159 }
160 }
161
162 // Flat-table columns (§9 column targets + derivations) — shared with reindex.
163 $row = Mlsimport_Standalone_Row::build_columns( $extra );
164
165 // VirtualTourURLUnbranded arrives as an <iframe> blob -> store the clean URL.
166 if ( isset( $extra['VirtualTourURLUnbranded'] ) ) {
167 $tour = Mlsimport_Standalone_Derive::extract_tour_src( (string) $extra['VirtualTourURLUnbranded'] );
168 // Only store when a src URL was successfully extracted.
169 if ( null !== $tour ) {
170 update_post_meta( $property_id, 'mlsimport_virtual_tour', $tour );
171 }
172 }
173
174 // Write the accumulated terms, one taxonomy at a time (replace, not append).
175 foreach ( $terms_by_tax as $taxonomy => $terms ) {
176 wp_set_object_terms( $property_id, $terms, $taxonomy, false );
177 }
178
179 // PublicRemarks arrives promoted to the top level (theme_id 990) -> post body.
180 // MLS feeds send remarks as one unbroken block, so break it into readable
181 // paragraphs before write; add-ons can still override via the filter.
182 $remarks = Mlsimport_Standalone_Derive::paragraphs( isset( $property['content'] ) ? (string) $property['content'] : '' );
183 /** Filter the post body before write. @since 6.3 */
184 $content = (string) apply_filters( 'mlsimport_property_post_content', $remarks, $property );
185 if ( '' !== $content ) {
186 wp_update_post(
187 array(
188 'ID' => $property_id,
189 'post_content' => $content,
190 )
191 );
192 }
193
194 // FULLTEXT source: title + remarks + address (§8).
195 // Combine the searchable pieces into the row's FULLTEXT column.
196 $search_parts = array(
197 get_the_title( $property_id ),
198 isset( $property['content'] ) ? (string) $property['content'] : '',
199 isset( $property['adr_title'] ) ? (string) $property['adr_title'] : '',
200 isset( $extra['UnparsedAddress'] ) ? (string) $extra['UnparsedAddress'] : '',
201 );
202 $row['search_text'] = trim( implode( ' ', array_filter( $search_parts ) ) );
203
204 // Upsert the flat-table row only when we have a stable ListingKey.
205 if ( '' !== $listing_key ) {
206 Mlsimport_Standalone_Row::upsert( $property_id, $listing_key, $row );
207 }
208
209 // The display-source choice is live task configuration. Assigned Agent is
210 // creation-time only, so updates never replace its stored relationship.
211 update_post_meta( $property_id, 'mlsimport_use_mls_agent', ! empty( $context['use_mls_agent'] ) ? 1 : 0 );
212 if ( ! empty( $context['is_new'] ) ) {
213 $agent_id = (int) ( $context['assigned_agent_id'] ?? 0 );
214 if ( $agent_id > 0 ) {
215 update_post_meta( $property_id, 'mlsimport_list_agent_id', $agent_id );
216 }
217 }
218
219 return true;
220 }
221
222 /**
223 * Whether a RESO boolean/YN value should count as true.
224 *
225 * @param mixed $value Raw YN value.
226 * @return bool
227 */
228 private function is_truthy( $value ) {
229 // A real boolean is returned as-is.
230 if ( is_bool( $value ) ) {
231 return $value;
232 }
233 // Otherwise accept common truthy string spellings.
234 return in_array( strtolower( trim( (string) $value ) ), array( '1', 'true', 'yes', 'y' ), true );
235 }
236
237 /**
238 * Removes the mlsimport_listings row when a property post is deleted
239 * (before_delete_post). Covers reconciliation, import-removal and manual
240 * admin deletes. Gated to mlsimport_property.
241 *
242 * @param int $post_id Post being deleted.
243 * @return void
244 */
245 public static function cleanup_on_delete( $post_id ) {
246 // Only act on our own property post type.
247 if ( 'mlsimport_property' !== get_post_type( $post_id ) ) {
248 return;
249 }
250 // Remove the associated flat-table row.
251 Mlsimport_Standalone_Row::delete( (int) $post_id );
252 }
253
254 /**
255 * Drop the mlsimport_listings row when a property leaves the published state
256 * (trashed, drafted, set pending/private). before_delete_post only fires on a
257 * permanent delete, so this covers a manual or reconciliation trash that keeps
258 * the post around — keeping the index to published listings only.
259 *
260 * @param string $new_status New post status.
261 * @param string $old_status Old post status.
262 * @param WP_Post $post The post.
263 * @return void
264 */
265 public static function cleanup_on_status_change( $new_status, $old_status, $post ) {
266 // Only act on our own property posts.
267 if ( ! $post instanceof WP_Post || 'mlsimport_property' !== $post->post_type ) {
268 return;
269 }
270 // Any non-published state drops the row (keeps the index published-only).
271 if ( 'publish' !== $new_status ) {
272 Mlsimport_Standalone_Row::delete( (int) $post->ID );
273 }
274 }
275
276 /**
277 * Theme custom-fields hook — intentionally empty for standalone.
278 *
279 * The other adapters mirror the field selector into a theme-owned option here
280 * because their themes read labels from their own settings. Standalone has no
281 * such option: the standalone render layer reads the module's active projection
282 * at render time, so there is no copied option to synchronize or make stale.
283 *
284 * @param string $option_name Option name.
285 * @return void
286 */
287 public function enviroment_custom_fields( $option_name ) {
288 }
289
290 /**
291 * Standalone sends theme_id 990, so there is no theme schema to return.
292 *
293 * @return string
294 */
295 public function return_theme_schema() {
296 return '';
297 }
298 }
299