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 arrayThe 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 and $listing->capacity 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.