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-row.php

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

201 lines 8.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) flat-table row assembly.
4 *
5 * Single source of truth for turning a RESO field map into mlsimport_listings
6 * column values (via the §9 column targets + derivations) and upserting the row
7 * by listing_key. Shared by the live write adapter (fields from the import
8 * payload) and reindex (fields reconstructed from a post's meta + taxonomies).
9 *
10 * @package Mlsimport
11 */
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 require_once __DIR__ . '/class-mlsimport-standalone-reso-map.php';
18 require_once __DIR__ . '/class-mlsimport-standalone-derive.php';
19 require_once __DIR__ . '/class-mlsimport-standalone-table.php';
20
21 /**
22 * Builds and upserts mlsimport_listings rows.
23 */
24 class Mlsimport_Standalone_Row {
25
26 /**
27 * Assemble the flat-table column values from a RESO field => value map.
28 * Column targets come from the §9 map; bathrooms/lot_size/list_date are
29 * derived (overriding any raw copy).
30 *
31 * @param array $fields RESO field => value (scalars; arrays are skipped).
32 * @return array Column => value.
33 */
34 public static function build_columns( array $fields ): array {
35 $row = array();
36
37 // Walk every RESO field; copy the raw value into each of its column: targets.
38 foreach ( $fields as $field => $value ) {
39 // Only scalars map to flat columns; multi-value arrays go to taxonomies/meta.
40 if ( is_array( $value ) ) {
41 continue;
42 }
43 // A field may resolve to several targets; keep only the column: ones here.
44 foreach ( Mlsimport_Standalone_Reso_Map::targets_for( $field ) as $target ) {
45 if ( 0 === strpos( $target, 'column:' ) ) {
46 // Strip the "column:" prefix (7 chars) to get the bare column name.
47 $row[ substr( $target, 7 ) ] = $value;
48 }
49 }
50 }
51
52 // Derived columns override any raw copy: combine multiple RESO fields into one.
53 $bathrooms = Mlsimport_Standalone_Derive::derive_bathrooms( $fields );
54 if ( null !== $bathrooms ) {
55 $row['bathrooms'] = $bathrooms;
56 }
57 $lot_sqft = Mlsimport_Standalone_Derive::derive_lot_sqft( $fields );
58 if ( null !== $lot_sqft ) {
59 $row['lot_size'] = $lot_sqft;
60 }
61 $list_date = Mlsimport_Standalone_Derive::derive_list_date( $fields );
62 if ( null !== $list_date ) {
63 $row['list_date'] = $list_date;
64 }
65
66 // DATETIME columns: normalize RESO ISO-8601 to MySQL format.
67 foreach ( array( 'modification_timestamp', 'list_date' ) as $dt_col ) {
68 if ( isset( $row[ $dt_col ] ) ) {
69 $row[ $dt_col ] = Mlsimport_Standalone_Derive::normalize_datetime( $row[ $dt_col ] );
70 }
71 }
72
73 return $row;
74 }
75
76 /**
77 * Insert or update the row for a listing, keyed by listing_key (UNIQUE).
78 *
79 * The index mirrors published listings only: when the post is not published
80 * (trashed, draft, pending, private) its row is dropped instead of written, so
81 * a non-public listing can never sit in the search index. This is the single
82 * chokepoint every write path (live import + reindex) flows through.
83 *
84 * @param int $post_id Post ID.
85 * @param string $listing_key RESO ListingKey.
86 * @param array $row Column => value.
87 * @return bool True when the row was written, false when skipped/removed.
88 */
89 public static function upsert( $post_id, $listing_key, array $row ): bool {
90 global $wpdb;
91
92 // Index mirrors published listings only: drop the row for any non-public post.
93 if ( 'publish' !== get_post_status( $post_id ) ) {
94 self::delete( (int) $post_id );
95 return false;
96 }
97
98 // Stamp the identity columns onto the row before write.
99 $table = Mlsimport_Standalone_Table::table_name();
100 $row['listing_key'] = $listing_key;
101 $row['post_id'] = $post_id;
102
103 /** Filter the flat-table row before write. @since 6.3 */
104 $row = (array) apply_filters( 'mlsimport_listings_row', $row, $post_id, $listing_key );
105
106 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
107 $existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE listing_key = %s", $listing_key ) );
108
109 // Update in place when a row already exists for this listing_key, else insert.
110 if ( $existing ) {
111 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
112 $wpdb->update( $table, $row, array( 'id' => (int) $existing ) );
113 } else {
114 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
115 $wpdb->insert( $table, $row );
116 }
117
118 /** Fires after a listings row is inserted/updated. @since 6.3 */
119 do_action( 'mlsimport_after_listings_row_upsert', $post_id, $listing_key, $row );
120 return true;
121 }
122
123 /**
124 * Remove a listing's row from the index. The single row-deletion path, shared
125 * by the upsert guard, post deletion and the non-published status transition.
126 *
127 * @param int $post_id Post ID.
128 * @return void
129 */
130 public static function delete( $post_id ): void {
131 global $wpdb;
132 $post_id = (int) $post_id;
133
134 /** Fires before the listings row is deleted. @since 6.3 */
135 do_action( 'mlsimport_before_delete_listing_row', $post_id );
136
137 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
138 $wpdb->delete( Mlsimport_Standalone_Table::table_name(), array( 'post_id' => $post_id ) );
139
140 /** Fires after the listings row is deleted. @since 6.3 */
141 do_action( 'mlsimport_after_delete_listing_row', $post_id );
142 }
143
144 /**
145 * Clean up everything a property's raw-SQL delete leaves behind: its term
146 * relationships, the affected terms' cached counts, and its listings row.
147 *
148 * The reconciliation delete removes the post straight from wp_posts/wp_postmeta
149 * for speed (no wp_delete_post), so none of core's cleanup runs — the term
150 * relationships, the denormalized wp_term_taxonomy.count, and this table's row
151 * are all orphaned. This does that cleanup with the same SQL-first approach:
152 * capture the term links, delete them, recompute count for exactly those terms
153 * (WP's default published-post semantics: publish + mlsimport_property), clear
154 * the term cache so a persistent object cache stops serving the stale count,
155 * then drop the listings row. Call it before the raw wp_posts delete.
156 *
157 * @param int $post_id Post being deleted.
158 * @return void
159 */
160 public static function purge_post_relations( $post_id ): void {
161 global $wpdb;
162 $post_id = (int) $post_id;
163
164 // The terms this object is linked to, captured before we delete the links.
165 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
166 $links = $wpdb->get_results(
167 $wpdb->prepare(
168 "SELECT tr.term_taxonomy_id, tt.term_id FROM {$wpdb->term_relationships} tr INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tr.object_id = %d",
169 $post_id
170 )
171 );
172
173 if ( $links ) {
174 $ttids = array_map( 'intval', wp_list_pluck( $links, 'term_taxonomy_id' ) );
175 $term_ids = array_map( 'intval', wp_list_pluck( $links, 'term_id' ) );
176
177 // Drop the object's term relationships.
178 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
179 $wpdb->delete( $wpdb->term_relationships, array( 'object_id' => $post_id ) );
180
181 // Recompute count for exactly the affected terms — WP's default published
182 // post-count definition, so the number matches the published-only archive.
183 // Bounded to this post's terms and self-healing (fixes any prior drift).
184 $placeholders = implode( ', ', array_fill( 0, count( $ttids ), '%d' ) );
185 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
186 $wpdb->query(
187 $wpdb->prepare(
188 "UPDATE {$wpdb->term_taxonomy} tt SET tt.count = ( SELECT COUNT(*) FROM {$wpdb->term_relationships} tr INNER JOIN {$wpdb->posts} p ON p.ID = tr.object_id WHERE tr.term_taxonomy_id = tt.term_taxonomy_id AND p.post_status = 'publish' AND p.post_type = 'mlsimport_property' ) WHERE tt.term_taxonomy_id IN ({$placeholders})",
189 $ttids
190 )
191 );
192
193 // Drop the stale term cache (counts + relationships) so a persistent object
194 // cache doesn't keep serving the old numbers after the DB is corrected.
195 clean_term_cache( $term_ids );
196 }
197
198 self::delete( $post_id );
199 }
200 }
201