Skip to content

Instantly share code, notes, and snippets.

@DaveyJake
Created October 10, 2024 09:17
Show Gist options
  • Save DaveyJake/f94944ce67920b058b713a3d71a916a2 to your computer and use it in GitHub Desktop.
Save DaveyJake/f94944ce67920b058b713a3d71a916a2 to your computer and use it in GitHub Desktop.
RETS API Integration
<?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