D
P
0

WordPress & PHP

PHP 8 Fatal: 'Attempt to read property on array' When a Getter Returns an Array

June 23, 2026·4 min read
PHP 8 Fatal: 'Attempt to read property on array' When a Getter Returns an Array

A WordPress booking platform I built had been running fine for months. Then the host bumped PHP from 7.x to 8, and the listing detail page died instantly — white screen, fatal error. The message was blunt:

Fatal error: Uncaught Error: Attempt to read property "price" on array

The trigger lived in the template part that renders a listing's price. It looked roughly like this:

$listing = get_listing_data( $id );
 
echo '<span class="price">' . $listing->price . '</span>';
echo '<span class="cap">'   . $listing->capacity . '</span>';

Nothing jumps out at first glance. $listing->price, $listing->capacity — they read like perfectly ordinary object property access. That's exactly the trap: $listing is not an object.

Why this happens

The plugin's getter, get_listing_data( $id ), returns an associative array, not an object. Something like:

$listing = array(
    'price'    => 250000,
    'capacity' => 4,
    'gallery'  => array( /* ... */ ),
    // and so on -- all meta fields
);

So the real price lives at $listing['price'], not $listing->price.

In PHP 7, reading an object property off an array was wrong, but it only emitted a notice and silently evaluated to null. The code kept running, the page kept rendering — the price just came out empty, and if anyone noticed at all, it was a warning buried in a log. The bug was latent: present from day one, but it never blew up.

PHP 8 changed the rules. Reading a property on an array is no longer a notice you can ignore — it's a fatal Error. The moment the interpreter hits $listing->price and finds $listing is an array, execution stops cold. No more silent null. A bug that had been sleeping for months became a site-down bug the instant we upgraded.

It got worse, because digging in surfaced a second problem. The checkout flow had a guard meant to validate the listing:

$listing = get_listing_data( $id );
 
if ( empty( $listing['id'] ) ) {
    // treat listing as invalid -> redirect out of checkout
    wp_safe_redirect( home_url() );
    exit;
}

The issue: get_listing_data() returns only meta fields. There is no 'id' key in it at all. So empty( $listing['id'] ) was true for every listing — including the perfectly valid ones. The guard treated all of them as invalid and mis-redirected people straight out of checkout. That's a standalone logic bug, nothing to do with the PHP version, but it sprang from the same root: assuming the shape of the data without ever checking it.

The fix

The fatal error itself is a one-liner: access with array syntax.

$listing = get_listing_data( $id );
 
echo '<span class="price">' . $listing['price'] . '</span>';
echo '<span class="cap">'   . $listing['capacity'] . '</span>';

For the checkout guard, I stopped leaning on a key that was never there. The correct way to confirm a listing exists is to ask WordPress directly via get_post(), not to guess at the contents of a meta array:

$post = get_post( $id );
 
if ( ! $post || $post->post_type !== 'listing' ) {
    wp_safe_redirect( home_url() );
    exit;
}
 
$listing = get_listing_data( $id );

get_post() returns a real post object (or null if there's none), so the check is accurate and checkout stops kicking out legitimate listings.

One more thing: some template parts genuinely need the listing's id and title. Rather than force them to read $listing->id (which doesn't exist) or call out elsewhere, I inject those fields into the array after the getter runs, so the template has everything it needs in one consistent shape:

$post = get_post( $id );
 
$listing          = get_listing_data( $id );
$listing['id']    = $post->ID;
$listing['title'] = get_the_title( $post );

Now template parts can read $listing['id'] and $listing['title'] safely, and nothing assumes the getter handed back an object.

The takeaway

Confirm a getter's return shape — array or object — before you access it. A name like get_listing_data() tells you nothing about this; the only way to know is to read the implementation or var_dump() the result once. I assumed the getter returned an object purely because it felt object-shaped, and that assumption rode along quietly for months.

PHP 8 promoted a whole class of old notices into fatal errors, and this is one of the most common ones to bite legacy projects. Reading a property on an array used to be a whisper in a log; now it kills the page. Which means a PHP upgrade is never just a syntax-compatibility chore — it audits every lazy assumption that PHP 7's permissive behavior had been papering over. A bug that sat latent for years can turn into a site outage from one minor version bump in a hosting panel.

The practical lesson: before a PHP 8 migration, sweep the code for -> used on anything that came from an array, and never validate existence through a key you haven't verified is present. If you want to know whether a post exists, ask get_post() — not whatever meta array you happen to be holding.