Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phrets Laravel Package

troydavisson/phrets

A PHP client for the RETS protocol used by real estate MLS servers, making it simple to authenticate, query listings, download photos, and pull metadata from RETS feeds through a clean, object-oriented API.

Deep Wiki
Context7

Getting Started

Install and gate it

composer require troydavisson/phrets

Requires ext-curl (and SimpleXML for metadata parsing). Last release was July 2023, so pin an explicit version in composer.json, confirm it installs cleanly on your PHP version before writing app code, and add a composer outdated check to your CI routine — this is a maintenance-mode library.

Credentials you will actually need from the MLS

Not just URL/user/pass. Ask for: the login URL (https://.../rets/login), the RETS version to negotiate (1.8 or 1.7.2), a User-Agent string they have whitelisted, a User-Agent password (required on 1.7+), whether HTTP Basic/Digest auth is layered on top, and which Resource/Class names you are licensed for (Property/RE_1, etc.). Getting the User-Agent wrong produces confusing 401s that look like bad passwords.

First boot

$config = new \PHRETS\Configuration;
$config->setLoginUrl(config('rets.login_url'))
       ->setUsername(config('rets.username'))
       ->setPassword(config('rets.password'))
       ->setRetsVersion('1.8')
       ->setUserAgent(config('rets.user_agent'))
       ->setUserAgentPassword(config('rets.user_agent_password'));

$rets = new \PHRETS\Session($config);
$rets->Login();

Look at metadata before you look at listings. Every hour spent in GetMetadata('METADATA-RESOURCE'), METADATA-CLASS, and METADATA-TABLE saves a day of guessing field names. MLS field names are non-standard (L_Status, LM_Char10_15), so dump the table metadata for your class and keep it as a reference file. Only then write your first DMQL query.

First use case: a rets:probe Artisan command that logs in, prints GetMetadataTypes(), then pulls 5 listings:

$results = $rets->Search('Property', 'RE_1', '(ModificationTimestamp=2024-01-01+)',
    ['QueryType' => 'DMQL2', 'Format' => 'COMPACT-DECODED', 'Limit' => 5, 'Offset' => 0]);

dump($results->getTotalResultsCount(), $results->toArray());

In Laravel, register the Session as a singleton in a small service provider and read credentials from config/rets.php backed by env vars — never hardcode them in the wrapper.

Implementation Patterns

Wrap the session, don't sprinkle it. Bind PHRETS\Session as a singleton, then put all calls behind a RetsClient/ListingRepository interface. This is what makes the rest of your app testable and is your escape hatch when you eventually migrate to RESO Web API.

Metadata as a cached schema. Run a nightly rets:metadata command that stores Resource → Class → Table definitions (and lookups) in a database table or cache with a long TTL. Metadata calls are slow and heavy; calling GetMetadata('METADATA-TABLE', ...) per request will get you throttled. Build a per-class field map from the cached metadata so your sync jobs can translate system names to Eloquent columns without hitting the server.

Paginate deliberately. Always pass Limit and Offset, always use COMPACT-DECODED, and always inspect getReturnedResultsCount() — an empty page means stop, not "retry forever". getTotalResultsCount() is your progress bar. Request only the columns you need with Select to cut payload size dramatically on photo-heavy MLSs.

Incremental sync beats full sync. Track a ModificationTimestamp watermark per Resource/Class. Query (ModificationTimestamp=YYYY-MM-DDTHH:MM:SSZ+), and overlap the watermark by a few minutes on each run to absorb clock skew and in-flight transactions. Never use Offset as your sole cursor for very large datasets — it is slow and, on some servers, silently unsupported.

Push the work to queues. A full sync is hours of I/O. Structure as: SyncMetadataJob → SyncListingsChunkJob(offset) that re-dispatches itself → FetchPhotoJob(listingId). Use RateLimited middleware or Redis::throttle to match the MLS's concurrency cap, and $schedule->command('rets:sync')->hourly()->withoutOverlapping().

Photos are a separate pipeline. GetObject($resource, $class, 'Photo', $ids, '*') accepts comma-separated IDs, which is the only way to make photo backfill tolerable. Iterate the returned BaseObject models, read getContentId()/getObjectId() to order them, and stream content to Storage::disk('s3') keyed by listing + object ID so re-runs are idempotent. Store the mime type; some servers omit it.

Error handling per reply code. Catch PHRETS\Exceptions\RetsException (and the MLS, Search, Object, Metadata subclasses) and inspect the RETS ReplyCode/ReplyText. Distinguish "server busy, back off" from "your query syntax is wrong" from "credential expired" — retry only the first. On repeated timeouts, rebuild the session and log in again.

Testing. Because the real server is slow, live, and rate-limited, fake the repository interface in tests and keep a fixture set of recorded toArray() payloads. Reserve real-server calls for a manual smoke command.

Gotchas and Tips

  • RETS is a sunset protocol. RESO Web API is the replacement and many MLSs are dual-running or announcing retirement. Treat this as a bridge: keep the repository boundary thin so a RESO client can slot in later.
  • Cookie jar and temp paths must be writable by the PHP user. Two processes sharing one cookie file will corrupt the session — one Session per process, always call Disconnect() in a finally.
  • Sessions expire (typically ~60 minutes idle). Long-running jobs must detect re-login failures and rebuild the session rather than dying halfway.
  • Long DMQL queries via GET can be truncated. If your server supports it, enable the POST-method option on Configuration so multi-clause queries survive.
  • Some servers cap Limit, ignore Offset, or reject Count-only searches. Verify with a probe script on day one; don't assume the spec.
  • Memory. A single Search with a huge Limit loads everything into the result collection. Keep Limit modest (a few hundred), stream to the DB, and free the result between pages.
  • Timezone traps. RETS timestamps are UTC. Store UTC in the DB and convert at the presentation layer; ModificationTimestamp comparisons made with local time silently skip records.
  • Debugging lives in the last exchange. GetLastRequest(), GetLastResponse(), GetLastResponseHeader(), GetLastResponseBody() turn a silent failure into a readable DMQL/HTTP exchange. Wrap these into a rets:debug --query="..." command — it will be your most-used tool. Scrub credentials before logging.
  • Cache metadata aggressively (days), cache listings never. Metadata changes rarely; listing data changes constantly.
  • Pin the library version and read its changelog on upgrade. With a single 2023 release and a low opportunity score, upstream fixes for new PHP versions and obscure MLS quirks are unlikely to arrive quickly. Keep a local patch path documented, and isolate any subclassing of Session behind your wrapper so you can drop it fast.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route