Created
October 10, 2024 09:17
-
-
Save DaveyJake/f94944ce67920b058b713a3d71a916a2 to your computer and use it in GitHub Desktop.
RETS API Integration
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* RETS API: Properties | |
* | |
* Make a request to retrieve any/all specified properties. | |
* | |
* @package Search | |
* @subpackage RETS | |
*/ | |
(defined( 'ABSPATH' ) && defined( 'RETS_API_URL' ) && defined( 'RETS_AUTH_KEY' )) || exit; | |
// Nonce support. | |
require ABSPATH . 'wp-includes/pluggable.php'; | |
/** | |
* Initialize the API class. | |
* | |
* @since 1.0.0 | |
*/ | |
class Search_RETS { | |
/** | |
* Property or pricing type. Default 'all'. | |
* | |
* @since 1.0.0 | |
* | |
* @var string | |
*/ | |
public $pr_type = 'all'; | |
/** | |
* Listing status. Default 'all'. | |
* | |
* @since 1.0.0 | |
* | |
* @var string | |
*/ | |
public $listing_status = 'all'; | |
/** | |
* RETS API default query parameters. | |
* | |
* @since 1.0.0 | |
* | |
* @var array | |
*/ | |
public $query = array( | |
'q' => '', | |
'status' => 'all', | |
'type' => 'all', | |
'minbaths' => '', | |
'minbeds' => '', | |
'minprice' => '', | |
'maxprice' => '', | |
'minyear' => '', | |
'maxyear' => '', | |
'page' => '', | |
); | |
/** | |
* RETS API URL. | |
* | |
* @since 1.0.0 | |
* | |
* @var string | |
*/ | |
public $api_url = RETS_API_URL; | |
/** | |
* RETS API property statuses. | |
* | |
* @since 1.0.0 | |
* | |
* @var array | |
*/ | |
public $property_statuses = array( 'Active', 'ActiveUnderContract' ); | |
/** | |
* RETS API property types. | |
* | |
* @since 1.0.0 | |
* | |
* @var array | |
*/ | |
public $property_types = array( | |
'RES' => 'residential', | |
'MLF' => 'multifamily', | |
'MBL' => 'mobilehome', | |
'CND' => 'condominium', | |
'CRE' => 'commercial', | |
'LND' => 'land', | |
'FRM' => 'farm', | |
); | |
/** | |
* Final query parameters. | |
* | |
* @since 1.0.0 | |
* | |
* @var array | |
*/ | |
public $args = array(); | |
/** | |
* Final endpoint URL. | |
* | |
* @since 1.0.0 | |
* | |
* @var string | |
*/ | |
public $url; | |
/** | |
* Flag for property view type. True if single. Default false. | |
* | |
* @since 1.0.0 | |
* | |
* @var bool | |
*/ | |
public $is_single = false; | |
/** | |
* MLS ID for viewing a single property only. | |
* | |
* @since 1.0.0 | |
* | |
* @var string | |
*/ | |
public $mls_id; | |
/** | |
* Primary constructor. | |
* | |
* @since 1.0.0 | |
*/ | |
public function __construct() { | |
if ( isset( $_REQUEST['nonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['nonce'] ), 'search-rets' ) ) { | |
// Check for MLS ID. | |
if ( isset( $_REQUEST['mls_id'] ) ) { | |
$this->mls_id = sanitize_text_field( wp_unslash( $_REQUEST['mls_id'] ) ); | |
$this->is_single = true; | |
} | |
// If we're looking at multiple properties on a Google Map... | |
if ( false === $this->is_single ) { | |
// Property status. | |
if ( isset( $_REQUEST['listing_status'] ) ) { | |
$this->listing_status = sanitize_text_field( wp_unslash( $_REQUEST['listing_status'] ) ); | |
} | |
// Property or pricing type. | |
if ( isset( $_REQUEST['pr_type'] ) ) { | |
$this->pr_type = sanitize_text_field( wp_unslash( $_REQUEST['pr_type'] ) ); | |
} | |
// Default parameters. | |
if ( 'all' === $this->listing_status ) { | |
$this->args['status'] = implode( '&status=', $this->property_statuses ); | |
} else { | |
$this->args['status'] = $this->listing_status; | |
} | |
if ( 'all' === $this->pr_type ) { | |
$this->args['type'] = implode( '&type=', array_values( $this->property_types ) ); | |
} else { | |
$this->args['type'] = $this->pr_type; | |
} | |
if ( isset( $_REQUEST['min_baths'] ) ) { | |
$this->args['minbaths'] = sanitize_text_field( wp_unslash( $_REQUEST['min_baths'] ) ); | |
} else { | |
$this->args['minbaths'] = ''; | |
} | |
if ( isset( $_REQUEST['max_baths'] ) ) { | |
$this->args['maxbaths'] = sanitize_text_field( wp_unslash( $_REQUEST['max_baths'] ) ); | |
} else { | |
$this->args['maxbaths'] = ''; | |
} | |
if ( isset( $_REQUEST['min_beds'] ) ) { | |
$this->args['minbeds'] = sanitize_text_field( wp_unslash( $_REQUEST['min_beds'] ) ); | |
} else { | |
$this->args['minbeds'] = ''; | |
} | |
if ( isset( $_REQUEST['max_beds'] ) ) { | |
$this->args['maxbeds'] = sanitize_text_field( wp_unslash( $_REQUEST['max_beds'] ) ); | |
} else { | |
$this->args['maxbeds'] = ''; | |
} | |
if ( isset( $_REQUEST['min_price'] ) ) { | |
$this->args['minprice'] = sanitize_text_field( wp_unslash( $_REQUEST['min_price'] ) ); | |
} else { | |
$this->args['minprice'] = ''; | |
} | |
if ( isset( $_REQUEST['max_price'] ) ) { | |
$this->args['maxprice'] = sanitize_text_field( wp_unslash( $_REQUEST['max_price'] ) ); | |
} else { | |
$this->args['maxprice'] = ''; | |
} | |
if ( isset( $_REQUEST['year_min'] ) ) { | |
$this->args['minyear'] = sanitize_text_field( wp_unslash( $_REQUEST['year_min'] ) ); | |
} else { | |
$this->args['minyear'] = ''; | |
} | |
if ( isset( $_REQUEST['year_max'] ) ) { | |
$this->args['maxyear'] = sanitize_text_field( wp_unslash( $_REQUEST['year_max'] ) ); | |
} else { | |
$this->args['maxyear'] = ''; | |
} | |
if ( isset( $_REQUEST['location'] ) ) { | |
$this->args['q'] = sanitize_text_field( wp_unslash( $_REQUEST['location'] ) ); | |
} else { | |
$this->args['q'] = ''; | |
} | |
if ( isset( $_REQUEST['post_type'] ) ) { | |
$this->args['post_type'] = sanitize_text_field( wp_unslash( $_REQUEST['post_type'] ) ); | |
} else { | |
$this->args['post_type'] = ''; | |
} | |
// Max results limit. | |
$this->args['limit'] = '27'; | |
// Don't count. | |
$this->args['count'] = 'false'; | |
// Pagination. | |
if ( isset( $_REQUEST['page'] ) ) { | |
$this->args['page'] = sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ); | |
} else { | |
$this->args['page'] = '1'; | |
} | |
// Remove all empty, NULL and boolean false values. | |
$this->args = array_filter( $this->args ); | |
// Final collection endpoint URL. | |
$this->url = $this->parse_api_url( $this->api_url, $this->args ); | |
} else { | |
// If we're looking at a single property on its own page... | |
$this->url = sprintf( '%s/%s', $this->api_url, $this->mls_id ); | |
}//end if | |
add_action( 'wp_ajax_search_rets', array( $this, 'rets_api_request' ) ); | |
add_action( 'wp_ajax_nopriv_search_rets', array( $this, 'rets_api_request' ) ); | |
}//end if | |
} | |
/** | |
* Make the request and parse the response. | |
* | |
* @since 1.0.0 | |
*/ | |
public function rets_api_request() { | |
if ( defined( 'DOING_AJAX' ) && DOING_AJAX | |
&& isset( $_REQUEST['nonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['nonce'] ), 'search-rets' ) | |
) { | |
$args = array(); | |
$transient_key = ''; | |
$parts = wp_parse_url( $this->url ); | |
if ( ! empty( $parts['query'] ) ) { | |
$queries = preg_split( '/\&/', $parts['query'] ); | |
foreach ( $queries as $_query ) { | |
$query = preg_split( '/=/', $_query ); | |
$args[ $query[0] ] = $query[1]; | |
} | |
} | |
if ( ! empty( $args ) ) { | |
if ( isset( $args['q'] ) ) { | |
$transient_key .= preg_replace( '/[^a-zA-Z0-9]/', '_', $args['q'] ) . '_'; | |
} | |
if ( isset( $_REQUEST['page'] ) ) { | |
$transient_key .= sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ); | |
} | |
} else { | |
$transient_key = 'default'; | |
} | |
$transient = sprintf( 'listings_%s', $transient_key ); | |
$result = get_transient( $transient ); | |
if ( false === $result ) { | |
// Ensure nothing's cached. | |
delete_transient( $transient ); | |
// Request headers. | |
$auth = array( | |
'headers' => array( | |
'Authorization' => 'Basic ' . RETS_AUTH_KEY, | |
'Accept' => 'application/vnd.simplyrets-v0.1+json', | |
), | |
); | |
$request = wp_remote_get( $this->url, $auth ); | |
if ( empty( $request ) || is_wp_error( $request ) ) { | |
if ( empty( $request ) ) { | |
$error_data = array( 'message' => wp_remote_retrieve_response_message( $request ) ); | |
} elseif ( is_wp_error( $request ) ) { | |
$error_data = array( 'message' => $request->get_error_message() ); | |
} | |
wp_send_json_error( $error_data ); | |
wp_die(); | |
} | |
$response = wp_remote_retrieve_body( $request ); | |
if ( empty( $response ) ) { | |
wp_send_json_error( array( 'message' => wp_remote_retrieve_response_message( $response ) ) ); | |
wp_die(); | |
} elseif ( is_wp_error( $response ) ) { | |
wp_send_json_error( array( 'message' => $response->get_error_message() ) ); | |
wp_die(); | |
} else { | |
set_transient( $transient, $response, 2 * DAY_IN_SECONDS ); | |
$api = $this->format_api_response( $response ); | |
wp_send_json_success( $api ); | |
wp_die(); | |
} | |
} elseif ( empty( $result ) ) { | |
delete_transient( $transient ); | |
wp_send_json_error( array( 'message' => 'Response was good but contained no data.' ) ); | |
wp_die(); | |
} else { | |
$api = $this->format_api_response( $result ); | |
wp_send_json_success( $api ); | |
wp_die(); | |
}//end if | |
}//end if | |
wp_die(); | |
} | |
/** | |
* Format the raw API response to suit our needs and update the `properties` | |
* database table. | |
* | |
* @since 1.0.0 | |
* | |
* @param string $response Raw JSON data. | |
* | |
* @return object Custom, formatted API response. | |
*/ | |
private function format_api_response( $response ) { | |
$data = json_decode( $response ); | |
$api = array(); | |
$slug2id = array(); | |
$already_parsed = array(); | |
foreach ( $data as $i => $d ) { | |
if ( ! is_object( $d ) ) { | |
continue; | |
} | |
if ( ! in_array( $d->property->type, array_keys( $this->property_types ), true ) ) { | |
continue; | |
} | |
// Ensure there are no duplicate entries by checking the `$already_parsed` array. | |
if ( empty( $d->mlsId ) || in_array( $d->mlsId, $already_parsed, true ) ) { | |
continue; | |
} | |
if ( $d->listPrice < 1 ) { | |
continue; | |
} | |
$mls_id = $d->mlsId; | |
if ( ! empty( $d->geo ) ) { | |
$geo = array( | |
'lat' => isset( $d->geo->lat ) ? $d->geo->lat : '', | |
'lng' => isset( $d->geo->lng ) ? $d->geo->lng : '', | |
); | |
} | |
$title = preg_replace( array_keys( $this->keywords ), array_values( $this->keywords ), $d->address->full ) . ', ' . $d->address->city . ', ' . $d->address->state . ' ' . $d->address->postalCode; | |
$slug = sanitize_title( $title ); | |
$index = 0; | |
$address = preg_replace( array_keys( $this->keywords ), array_values( $this->keywords ), $d->address->full ) . ',<br />' . $d->address->city . ', ' . $d->address->state . ' ' . $d->address->postalCode; | |
if ( ! empty( $d->photos ) ) { | |
$total = count( $d->photos ); | |
if ( $total > 1 ) { | |
$_index = wp_rand( 1, $total ); | |
$index = $_index - 1; | |
} | |
} | |
$api[] = array( | |
'mls_id' => (string) $mls_id, | |
'listing_id' => $d->listingId, | |
'slug' => $slug, | |
'agent' => $d->agent, | |
'title' => $title, | |
'address' => $address, | |
'baths' => ! empty( $d->property->bathrooms ) ? $d->property->bathrooms : '', | |
'beds' => ! empty( $d->property->bedrooms ) ? $d->property->bedrooms : '', | |
'city' => $d->address->city, | |
'state' => $d->address->state, | |
'zip' => $d->address->postalCode, | |
'geo' => ! empty( $geo ) ? $geo : null, | |
'move_in' => get_gmt_from_date( $d->listDate, 'U' ), | |
'office' => ! empty( $d->office ) ? $d->office : '', | |
'photos' => ! empty( $d->photos ) ? $d->photos : '', | |
'featured' => isset( $d->photos[ $index ] ) ? $d->photos[ $index ] : '', | |
'price_int' => absint( preg_replace( '/[^0-9]+/', '', $d->listPrice ) ), | |
'price_usd' => number_format_i18n( $d->listPrice ), | |
'remarks' => ! empty( $d->remarks ) ? $d->remarks : '', | |
'sqft' => ! empty( $d->property->area ) ? $d->property->area : '', | |
'status' => ! empty( $d->mls->status ) ? $d->mls->status : '', | |
'type' => $this->property_types[ $d->property->type ], | |
'type_text' => $d->property->subTypeText, | |
); | |
$slug2id[ $slug ] = $mls_id; | |
$already_parsed[] = $mls_id; | |
}//end foreach | |
foreach ( $slug2id as $slug => $mls ) { | |
update_property( $slug, $mls ); | |
} | |
return $api; | |
} | |
/** | |
* Build the URL. | |
* | |
* @since 1.0.0 | |
* @access private | |
* | |
* @see Search_RETS::parse_query_params() | |
* | |
* @param string $url The API URL. | |
* @param array|int $params Filter input values. | |
* | |
* @return string|bool The API URL if successful. False if not. | |
*/ | |
private function parse_api_url( $url, $params ) { | |
$params = $this->parse_query_params( $params ); | |
if ( is_int( $params ) ) { | |
return sprintf( '%s/%d', $url, $params ); | |
} elseif ( ! empty( $params ) ) { | |
return add_query_arg( $params, $url ); | |
} else { | |
return $url; | |
} | |
} | |
/** | |
* Parse the API query parameters. | |
* | |
* @since 1.0.0 | |
* @access private | |
* | |
* @see Search_RETS::parse_url() | |
* | |
* @param array $params URL query parameters. | |
* | |
* @return int|array The parameter values. | |
*/ | |
private function parse_query_params( $params ) { | |
if ( is_int( $params ) ) { | |
return $params; | |
} | |
$final = array(); | |
// Begin parsing keyword search. | |
foreach ( (array) $params as $param => $value ) { | |
if ( 'q' === $param && ! empty( $value ) ) { | |
if ( preg_match( '/\+/', $value ) ) { | |
$parts = array_map( 'trim', explode( '+', $value ) ); | |
$final['q'] = implode( '&q=', $parts ); | |
} elseif ( is_string( $value ) ) { | |
$final['q'] = $value; | |
} elseif ( absint( $value ) > 0 ) { | |
$final['q'] = absint( $value ); | |
} | |
} elseif ( ( ! empty( $value ) || 'all' !== $value ) && 'post_type' !== $param && 'page' !== $param ) { | |
$final[ $param ] = $value; | |
} | |
} | |
return $final; | |
} | |
} | |
$GLOBALS['search_rets'] = new Search_RETS(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment