PHP Type Checking Pitfall: is_string(‘literal’) Always True

πŸ“– 2 minutes read

I just spent 2 hours debugging a “works fine without this condition” bug, only to discover the most face-palm PHP mistake I’ve made in years: checking is_string('field_name') instead of is_string($data['field_name']).

The Bug

The code was supposed to merge currency codes from multiple sources, but only if a specific field contained a valid string:

$currencyCodes = [];

foreach ($batches as $batch) {
    // ... collect from batches ...
}

// Merge in historical data if it exists
if (is_string('historical_currencies')) {  // πŸ› BUG HERE
    $currencyCodes = array_unique(array_merge(
        $currencyCodes,
        explode(',', $batch['historical_currencies'])
    ));
}

See the problem? I’m checking if the literal string 'historical_currencies' is a string (it always is), not whether $batch['historical_currencies'] contains string data.

The Result

When $batch['historical_currencies'] was NULL:

  1. The condition is_string('historical_currencies') evaluated to TRUE (literal strings are always strings)
  2. The code tried to explode(',', NULL)
  3. PHP 8.1+ throws a deprecation warning, but older versions silently return an empty array
  4. Result: missing data, no error logs, mystery bug

The Fix

// Correct version
if (is_string($batch['historical_currencies'])) {
    $currencyCodes = array_unique(array_merge(
        $currencyCodes,
        explode(',', $batch['historical_currencies'])
    ));
}

Or better yet, use optional chaining with type safety:

if (!empty($batch['historical_currencies']) && is_string($batch['historical_currencies'])) {
    $currencyCodes = array_unique(array_merge(
        $currencyCodes,
        explode(',', $batch['historical_currencies'])
    ));
}

How This Slipped Through

  1. No static analysis – PHPStan/Psalm would catch this immediately
  2. Conditional always TRUE – so tests with valid data passed
  3. Silent failure – no exception thrown, just missing results

The Lesson

Type-checking functions operate on values, not field names.

This applies to all type checks:

  • is_array('items') ❌ vs is_array($data['items']) βœ…
  • is_numeric('total') ❌ vs is_numeric($data['total']) βœ…
  • is_null('deleted_at') ❌ vs is_null($data['deleted_at']) βœ…

When in doubt, echo the value you’re checking. If you see a field name instead of actual data, you’re checking the wrong thing.

And seriously, run PHPStan. It would have caught this in 0.2 seconds.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *