Files
homeproz/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-query.php
T
Hanson.xyz Dev 6556479417 Add MLS by HansonXyz plugin for MLS Grid API integration
Features:
- Full sync of NorthStar MLS properties via MLS Grid API v2
- Incremental sync using ModificationTimestamp
- Local media download and storage
- Rate limit compliance (2 req/sec, 7200/hr, 40000/day)
- Sync state tracking with resume capability
- WP-CLI commands: test, sync, status, stats, cache
- Admin settings page with manual sync triggers
- Public API functions: mls_get_properties, mls_get_property, etc.

Database tables:
- mls_properties: Listing data with full field mapping
- mls_media: Downloaded images
- mls_sync_state: Sync progress tracking
- mls_rate_limits: API usage tracking
- mls_sync_log: Debug logging

Documentation:
- docs/CLAUDE.md: AI development guide
- docs/API.md: MLS Grid API reference
- docs/USAGE.md: User documentation

Tested: Connection, auth, sync 10 records, media download verified

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 21:24:38 -06:00

485 lines
14 KiB
PHP

<?php
/**
* MLS Query Class
*
* Public API for querying cached MLS data
*/
if (!defined('ABSPATH')) {
exit;
}
class MLS_Query {
/**
* Database instance
*/
private $db;
/**
* Constructor
*/
public function __construct(MLS_DB $db) {
$this->db = $db;
}
/**
* Get properties matching criteria
*
* @param array $args Query arguments
* @return array Property objects
*/
public function get_properties($args = array()) {
global $wpdb;
$defaults = array(
'status' => null, // Active, Pending, Closed
'property_type' => null, // Residential, Land, Commercial
'city' => null,
'county' => null,
'postal_code' => null,
'min_price' => null,
'max_price' => null,
'min_beds' => null,
'max_beds' => null,
'min_baths' => null,
'min_sqft' => null,
'max_sqft' => null,
'year_built_min' => null,
'year_built_max' => null,
'listing_key' => null,
'listing_id' => null,
'search' => null, // Search in address/remarks
'limit' => 20,
'offset' => 0,
'orderby' => 'modification_timestamp',
'order' => 'DESC',
'include_media' => false,
'fields' => '*', // Specific fields or *
);
$args = wp_parse_args($args, $defaults);
// Build query
$table = $this->db->properties_table();
// Fields
if ($args['fields'] === '*') {
$select = '*';
} else {
$fields = array_map('sanitize_key', (array) $args['fields']);
$select = implode(', ', $fields);
}
$sql = "SELECT {$select} FROM {$table}";
// WHERE conditions
$where = array('mlg_can_view = 1');
$values = array();
if ($args['status']) {
$where[] = 'standard_status = %s';
$values[] = $args['status'];
}
if ($args['property_type']) {
$where[] = 'property_type = %s';
$values[] = $args['property_type'];
}
if ($args['city']) {
$where[] = 'city = %s';
$values[] = $args['city'];
}
if ($args['county']) {
$where[] = 'county = %s';
$values[] = $args['county'];
}
if ($args['postal_code']) {
$where[] = 'postal_code = %s';
$values[] = $args['postal_code'];
}
if ($args['min_price']) {
$where[] = 'list_price >= %d';
$values[] = (int) $args['min_price'];
}
if ($args['max_price']) {
$where[] = 'list_price <= %d';
$values[] = (int) $args['max_price'];
}
if ($args['min_beds']) {
$where[] = 'bedrooms_total >= %d';
$values[] = (int) $args['min_beds'];
}
if ($args['max_beds']) {
$where[] = 'bedrooms_total <= %d';
$values[] = (int) $args['max_beds'];
}
if ($args['min_baths']) {
$where[] = 'bathrooms_total >= %d';
$values[] = (int) $args['min_baths'];
}
if ($args['min_sqft']) {
$where[] = 'living_area >= %d';
$values[] = (int) $args['min_sqft'];
}
if ($args['max_sqft']) {
$where[] = 'living_area <= %d';
$values[] = (int) $args['max_sqft'];
}
if ($args['year_built_min']) {
$where[] = 'year_built >= %d';
$values[] = (int) $args['year_built_min'];
}
if ($args['year_built_max']) {
$where[] = 'year_built <= %d';
$values[] = (int) $args['year_built_max'];
}
if ($args['listing_key']) {
$where[] = 'listing_key = %s';
$values[] = $args['listing_key'];
}
if ($args['listing_id']) {
$where[] = 'listing_id = %s';
$values[] = $args['listing_id'];
}
if ($args['search']) {
$search_term = '%' . $wpdb->esc_like($args['search']) . '%';
$where[] = '(street_name LIKE %s OR city LIKE %s OR public_remarks LIKE %s OR listing_id LIKE %s)';
$values[] = $search_term;
$values[] = $search_term;
$values[] = $search_term;
$values[] = $search_term;
}
$sql .= ' WHERE ' . implode(' AND ', $where);
// ORDER BY
$allowed_orderby = array(
'modification_timestamp',
'list_price',
'bedrooms_total',
'bathrooms_total',
'living_area',
'year_built',
'days_on_market',
'city',
'created_at',
);
$orderby = in_array($args['orderby'], $allowed_orderby) ? $args['orderby'] : 'modification_timestamp';
$order = strtoupper($args['order']) === 'ASC' ? 'ASC' : 'DESC';
$sql .= " ORDER BY {$orderby} {$order}";
// LIMIT/OFFSET
$sql .= ' LIMIT %d OFFSET %d';
$values[] = (int) $args['limit'];
$values[] = (int) $args['offset'];
// Execute
$results = $wpdb->get_results($wpdb->prepare($sql, $values));
// Include media if requested
if ($args['include_media'] && $results) {
foreach ($results as &$property) {
$property->media = $this->get_property_media($property->listing_key);
}
}
return $results;
}
/**
* Get a single property
*
* @param string $identifier Listing key or listing ID
* @return object|null Property object
*/
public function get_property($identifier) {
global $wpdb;
$table = $this->db->properties_table();
// Try listing_key first
$property = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$table} WHERE listing_key = %s AND mlg_can_view = 1",
$identifier
));
// Try listing_id if not found
if (!$property) {
$property = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$table} WHERE listing_id = %s AND mlg_can_view = 1",
$identifier
));
}
return $property;
}
/**
* Get media for a property
*
* @param string $listing_key Listing key
* @return array Media objects
*/
public function get_property_media($listing_key) {
global $wpdb;
return $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$this->db->media_table()}
WHERE listing_key = %s
ORDER BY media_order ASC",
$listing_key
));
}
/**
* Get primary image URL
*
* @param string $listing_key Listing key
* @return string|null Image URL
*/
public function get_primary_image($listing_key) {
global $wpdb;
return $wpdb->get_var($wpdb->prepare(
"SELECT local_url FROM {$this->db->media_table()}
WHERE listing_key = %s AND local_url IS NOT NULL
ORDER BY media_order ASC
LIMIT 1",
$listing_key
));
}
/**
* Get distinct cities
*
* @param string|null $status Optional status filter
* @return array City names
*/
public function get_distinct_cities($status = null) {
global $wpdb;
$table = $this->db->properties_table();
if ($status) {
$cities = $wpdb->get_col($wpdb->prepare(
"SELECT DISTINCT city FROM {$table}
WHERE mlg_can_view = 1 AND standard_status = %s AND city IS NOT NULL
ORDER BY city ASC",
$status
));
} else {
$cities = $wpdb->get_col(
"SELECT DISTINCT city FROM {$table}
WHERE mlg_can_view = 1 AND city IS NOT NULL
ORDER BY city ASC"
);
}
return $cities;
}
/**
* Get distinct counties
*
* @param string|null $status Optional status filter
* @return array County names
*/
public function get_distinct_counties($status = null) {
global $wpdb;
$table = $this->db->properties_table();
if ($status) {
$counties = $wpdb->get_col($wpdb->prepare(
"SELECT DISTINCT county FROM {$table}
WHERE mlg_can_view = 1 AND standard_status = %s AND county IS NOT NULL
ORDER BY county ASC",
$status
));
} else {
$counties = $wpdb->get_col(
"SELECT DISTINCT county FROM {$table}
WHERE mlg_can_view = 1 AND county IS NOT NULL
ORDER BY county ASC"
);
}
return $counties;
}
/**
* Get property count
*
* @param array $args Filter arguments (same as get_properties)
* @return int Count
*/
public function get_count($args = array()) {
global $wpdb;
$table = $this->db->properties_table();
$where = array('mlg_can_view = 1');
$values = array();
if (!empty($args['status'])) {
$where[] = 'standard_status = %s';
$values[] = $args['status'];
}
if (!empty($args['property_type'])) {
$where[] = 'property_type = %s';
$values[] = $args['property_type'];
}
if (!empty($args['city'])) {
$where[] = 'city = %s';
$values[] = $args['city'];
}
$sql = "SELECT COUNT(*) FROM {$table} WHERE " . implode(' AND ', $where);
if (!empty($values)) {
return (int) $wpdb->get_var($wpdb->prepare($sql, $values));
}
return (int) $wpdb->get_var($sql);
}
/**
* Check if data exists
*
* @return bool
*/
public function has_data() {
global $wpdb;
$count = $wpdb->get_var(
"SELECT COUNT(*) FROM {$this->db->properties_table()} WHERE mlg_can_view = 1"
);
return (int) $count > 0;
}
/**
* Get property types with counts
*
* @param string|null $status Optional status filter
* @return array Property types with counts
*/
public function get_property_types($status = null) {
global $wpdb;
$table = $this->db->properties_table();
if ($status) {
return $wpdb->get_results($wpdb->prepare(
"SELECT property_type, COUNT(*) as count
FROM {$table}
WHERE mlg_can_view = 1 AND standard_status = %s AND property_type IS NOT NULL
GROUP BY property_type
ORDER BY count DESC",
$status
));
}
return $wpdb->get_results(
"SELECT property_type, COUNT(*) as count
FROM {$table}
WHERE mlg_can_view = 1 AND property_type IS NOT NULL
GROUP BY property_type
ORDER BY count DESC"
);
}
/**
* Get price range
*
* @param string|null $status Optional status filter
* @return object Min and max prices
*/
public function get_price_range($status = null) {
global $wpdb;
$table = $this->db->properties_table();
if ($status) {
return $wpdb->get_row($wpdb->prepare(
"SELECT MIN(list_price) as min_price, MAX(list_price) as max_price
FROM {$table}
WHERE mlg_can_view = 1 AND standard_status = %s AND list_price > 0",
$status
));
}
return $wpdb->get_row(
"SELECT MIN(list_price) as min_price, MAX(list_price) as max_price
FROM {$table}
WHERE mlg_can_view = 1 AND list_price > 0"
);
}
/**
* Get formatted address for a property
*
* @param object $property Property object
* @return string Formatted address
*/
public function format_address($property) {
$parts = array();
if ($property->street_number) {
$parts[] = $property->street_number;
}
if ($property->street_name) {
$parts[] = $property->street_name;
}
if ($property->street_suffix) {
$parts[] = $property->street_suffix;
}
if ($property->unit_number) {
$parts[] = '#' . $property->unit_number;
}
$street = implode(' ', $parts);
$location_parts = array();
if ($property->city) {
$location_parts[] = $property->city;
}
if ($property->state_or_province) {
$location_parts[] = $property->state_or_province;
}
if ($property->postal_code) {
$location_parts[] = $property->postal_code;
}
$location = implode(', ', $location_parts);
if ($street && $location) {
return $street . ', ' . $location;
}
return $street ?: $location;
}
}