neighborhoods/phrets
A PHP RETS client for connecting to MLS servers, running property searches, retrieving metadata, and downloading listing photos. Includes session handling and a straightforward API for integrating real estate data into PHP applications, with support for standard RETS transactions.
PHRETS is a PHP client for the RETS (Real Estate Transaction Standard) protocol used by most North American MLS systems. If you're syncing listings, photos, or agent rosters into a Laravel app, this is the library you'll reach for.
Minimal setup:
composer require troyhall/phrets (or neighborhoods/phrets — the 2.7.1 release comes from the neighborhoods/PHRETS fork, so pin the fork if you rely on the MultipleStreamed parser fix).config/rets.php) with login_url, username, password, user_agent, and optionally user_agent_password. Don't inline credentials in code — use .env.SearchQuery against the Property class ((ModificationTimestamp=2024-01-01T00:00:00Z+)) to confirm auth, metadata, and formatting work before you build a full sync.Look first at: the RETS\Session class (login/search/getObject/disconnect), RETS\Metadata for discovering the actual field/class names your MLS exposes, and the SearchQuery/SearchResult helpers if you're doing anything non-trivial with result counting.
Singleton client with explicit lifecycle. RETS sessions are stateful and server-side; treat RETS\Session as a request-scoped or job-scoped resource. Bind it as a singleton and register a disconnect() in a terminating callback, or better, use a small wrapper class you inject that handles login/logout in a try/finally.
Metadata caching is non-negotiable. GetMetadata is slow and expensive against live servers. Fetch it once per environment, serialize it (JSON or cache driver), and refresh on a schedule rather than per-run. The class list, field definitions, and lookup values rarely change.
Queue everything. Searches and GetObject calls are I/O-bound and slow. Dispatch a SyncListings job, use SearchQuery with pagination (offset/limit), and process results in chunks. RETS servers frequently cap returned rows (often 100–1000), so never assume a single search returns everything.
MultipleStreamed for large batches. If your MLS returns large Property result sets, the MultipleStreamed parser streams the multipart response instead of buffering it all in memory. This is exactly what the 2.7.1 Guzzle fix affects — if you're on 2.7.0 and see deprecation notices from GuzzleHttp\Psr7\parse_response, upgrading to 2.7.1 silences them without changing behavior.
Idempotent upserts. RETS data is dirty: duplicates, whitespace, 0 vs null, and inconsistent datetimes. Key your local listings table on the MLS ListingId and use updateOrCreate. Store the raw ModificationTimestamp so incremental syncs ((ModificationTimestamp={lastSync}+)) can resume cleanly.
Photos via GetObject. Photos are a separate call (GetObject($resource, $type, $id)) and often return multiple images per listing. Download to a temporary disk, then Storage::disk('s3')->put(...) — don't pipe directly to storage streams because the parser expects a writable local path in most configurations.
Rate limiting at the job layer. Use Laravel's RateLimiter or ->middleware(RateLimited::class) on sync jobs. MLS servers throttle aggressively and will ban IPs on abuse; a per-second cap tuned to your vendor's contract is safer than retry-on-429 (which many RETS servers don't return properly).
parse_response deprecation fixed in 2.7.1. If you're on 2.7.0 and use the MultipleStreamed parser with a newer Guzzle, you'll see deprecation noise. The upgrade is drop-in — no API change — but verify your Guzzle version matches the neighborhoods/phrets fork's constraints.neighborhoods/PHRETS is an actively maintained fork; the upstream troyhall/PHRETS has been quieter. Confirm which one your composer.json resolves to, and pin the minor version if you need stability.RETS\Exceptions\RetsException and re-login once; don't blindly retry loops.User-Agent header, and some require the RETS-Version header too. Set both in config; the failure mode is an opaque "Login failed" with no diagnostics.YYYY-MM-DDTHH:MM:SS with an offset but no timezone name. Normalize to UTC with Carbon before comparison, or your incremental sync will silently skip or duplicate rows across DST changes.SearchQuery is your friend. The lower-level RETS\Session::Search() returns a SearchResult; the query builder adds pagination, field selection, and query construction. Prefer it for anything beyond a one-off exploratory call.Encoding="UTF-8" vs Latin-1) are a recurring culprit.RETS\Session accepts an injected PSR-7 client, so you can add Guzzle middleware for request logging, retries, or metrics without forking the package. That's the cleanest place to add observability.login_url and username/password, but the user-agent password (if your vendor uses one) goes in a separate key. Double-check against your vendor's credentials sheet — a mislabeled user_agent_password is a common source of "unauthorized" errors.How can I help you explore Laravel packages today?