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

Zillow Laravel Package

yajra/zillow

Laravel wrapper for the Zillow API, letting you query property data, Zestimates, and listing details through a clean, fluent interface. Integrate real estate info into your PHP apps with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Clear the dependency gate before writing any code. This is a Laravel 5.x-era package. Run the install as a throwaway spike on a branch, not on main:

composer require yajra/zillow --dry-run

Expect illuminate/* and Guzzle constraint conflicts on Laravel 9+. If Composer resolves it anyway, do not report that as progress — resolution only proves the version strings agree, not that the call path works. Record the output in a short ADR appendix either way; that artifact is the deliverable.

2. Publish and configure. With auto-discovery the provider registers itself. Publish the config and set the key:

php artisan vendor:publish --provider="Yajra\Zillow\ZillowServiceProvider" --tag=config
ZILLOW_API_KEY=your-key-here

Read config/zillow.php first — that file, plus the facade class and the client class behind it, is the entire package. There are maybe three files worth reading and you can finish them over coffee. Look at the client to confirm the exact method names and endpoint paths before you trust anything you read in a blog post; documentation is thin and the response shapes are XML-derived.

3. Smoke-test the boundary, not the feature. Make one call in php artisan tinker:

Zillow::getSearchResults('2114 Bigelow Ave', 'Seattle, WA');

The expected outcome on a live install today is a 410 Gone or 404. That is the finding. Treat the package as a structural reference — a worked example of config file + singleton binding + facade + response mapper — not as a data source.

First real use case: a property detail page or an address-lookup widget is the natural first slice, because it exercises exactly one call (GetDeepSearchResults-style) and one mapping step. But scope it as a prototype behind a first-party interface, never as a production dependency.

Implementation Patterns

Bind behind your own interface on day one. This is the single decision that survives a vendor swap. Do it before the first controller line:

// app/Contracts/PropertyData.php
interface PropertyData
{
    public function lookup(string $address, string $cityStateZip): ?PropertyRecord;
}
// app/Services/ZillowPropertyData.php
class ZillowPropertyData implements PropertyData
{
    public function lookup(string $address, string $cityStateZip): ?PropertyRecord
    {
        $raw = Zillow::getDeepSearchResults($address, $cityStateZip);

        return $raw ? PropertyRecord::fromZillow($raw) : null;
    }
}
// app/Providers/AppServiceProvider.php
$this->app->bind(PropertyData::class, ZillowPropertyData::class);

Controllers and Blade templates type-hint PropertyData, never Zillow. When the vendor changes — and it will — you rewrite one class.

Map the raw payload to a DTO immediately. The wrapper hands back loosely-typed structures with the vendor's naming. Normalize into your own value object at the boundary so a response-shape change is a one-file fix:

final class PropertyRecord
{
    public function __construct(
        public readonly string $address,
        public readonly ?int $zestimate,
        public readonly ?int $rentZestimate,
        public readonly ?float $latitude,
        public readonly ?float $longitude,
        public readonly array $raw,
    ) {}
}

Keep $raw for debugging and reprocessing; never read it in application code.

Cache aggressively at the service layer. The package has no caching, and every provider you might actually use bills per lookup. Wrap reads:

public function lookup(string $address, string $cityStateZip): ?PropertyRecord
{
    $key = 'property:' . md5(strtolower("$address|$cityStateZip"));

    return Cache::remember($key, now()->addDay(), function () use ($address, $cityStateZip) {
        return $this->fetchFromVendor($address, $cityStateZip);
    });
}

A property record is stable for days; a search result is not. Set TTLs per call type deliberately rather than defaulting to one value.

Test against the interface, not the facade. Facade mocking (Zillow::shouldReceive(...)) couples every test to the vendor. Stub the contract instead:

$this->mock(PropertyData::class, fn ($m) => $m
    ->shouldReceive('lookup')
    ->once()
    ->andReturn(new PropertyRecord('2114 Bigelow Ave', 1250000, null, 47.6, -122.3, [])));

For the adapter itself, isolate one test that asserts your mapping against a committed XML/JSON fixture. That test is your early warning if the vendor changes a field name.

Do bulk work in jobs, not in the request cycle. Any batch (warming a market, backfilling a comps table) goes through a queued job with RateLimited middleware and retryUntil. There is no backoff, queueing, or circuit-breaking in the package — you supply all of it.

Add a thin resilience decorator. One class around the adapter handles timeout, retry-with-backoff, and a fallback to null:

public function lookup(string $address, string $cityStateZip): ?PropertyRecord
{
    return retry(3, fn () => $this->inner->lookup($address, $cityStateZip), 200, fn ($e) => $e instanceof ConnectionException);
}

Never let a ConnectionException from a third-party client bubble into a Blade template. Degrade to a "valuation unavailable" state.

Log the raw response on failure, sampled not on every call. When the endpoint starts returning 410, you want one log line proving it rather than fifty.

Gotchas and Tips

The endpoints are retired. This is the headline, not a footnote. Zillow shut down public access to the legacy GetSearchResults / GetDeepSearchResults / GetZestimate routes this wrapper targets. Expect 410 Gone. No amount of code fixes it — access now requires a Zillow Group Bridge/Data Connect partner agreement. Verify the contract in writing before you build anything on top, and route the requirement to ATTOM, CoreLogic/HouseCanary, RentCast, Estated, or an MLS/RESO feed.

A successful composer require is a false positive. Stakeholders will read "it installed" as "it works." Frame the install spike as compatibility discovery, and pair every install result with a live-call result so nobody is misled.

env() outside config returns null under config:cache. Standard Laravel trap, and it bites here because the API key is the only thing that matters. Read the key only in config/zillow.php, then config('zillow.api_key') everywhere else. Run php artisan config:cache once in staging specifically to catch this.

Nulls, strings, and missing nodes. Legacy XML-to-array conversion produces inconsistent types: a numeric field that's sometimes a string, absent nodes that become null versus [], empty <zestimate> blocks. Coerce explicitly in your DTO ((int), (float), filled()), and never do arithmetic on a raw value. Add a fixture test with a sparsely-populated property — those are the ones that throw.

Don't mock the facade in feature tests. Facade mocks leak across tests if Mockery::close() isn't wired up, and they tie your suite to a class you intend to delete. Bind against PropertyData and mock that.

Vendor the source if you prototype. With 15 stars and zero dependents there is no upgrade path, no fork community, and no issue triage. If you keep any of it, copy it into the repo, pin the commit, and put your team's name on it. The MIT license permits this; the license covers the code, not the data.

Attribution and display rights are a separate legal question. MIT covers the source. It does not cover showing a Zestimate or listing data in your UI. Confirm display and redistribution rights with legal before a single value reaches a template.

Plan the exit before you need it. Name an owner, write the runbook, and document what "the vendor is down" looks like in the UI — because the first on-call page for this dependency has no upstream fix and no community to consult. That runbook, not the wrapper, is the reusable asset.

Copy the pattern even though you drop the package. Publishable config holding credentials, a singleton binding, a facade at the edge, response mapping to DTOs, an interface between your app and the vendor. That shape is correct and worth reproducing against whichever provider you actually license — and it makes the next swap a config change rather than a rewrite.

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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style