Skip to content

fix: resolve currency code correctly for both list and associative-map shapes#201

Merged
roncodes merged 2 commits intomainfrom
fix/get-currency-from-country-code-return-type
Apr 16, 2026
Merged

fix: resolve currency code correctly for both list and associative-map shapes#201
roncodes merged 2 commits intomainfrom
fix/get-currency-from-country-code-return-type

Conversation

@roncodes
Copy link
Copy Markdown
Member

Problem

Two related errors were reported in production:

DEPRECATED  mb_strtolower(): Passing null to parameter #1 ($string) of type string is deprecated
            in vendor/laravel/framework/src/Illuminate/Support/Str.php on line 611.

TypeError: Fleetbase\Support\Utils::getCurrenyFromCountryCode(): Return value must be of type
           ?string, array returned  (Utils.php:1235)

Root cause

The fleetbase/countries package (a fork of PragmaRX Countries) returns the currencies field in two distinct shapes depending on the underlying data source:

Shape Example Scope
Sequential list of ISO 4217 strings ["USD"] Most countries (239)
Associative map — code as key, detail object as value {"USD": {"name": "United States dollar", "symbol": "$"}} 10 territories: BQ, CC, CX, GP, GF, MQ, YT, RE, SJ, TK

The previous code used Arr::first() unconditionally on the currencies collection:

// Before
'currency' => Arr::first(static::get($country, 'currencies', [])),
  • List shapeArr::first() returns the first element (the code string). ✅
  • Map shapeArr::first() returns the first value (an array like ["name" => "...", "symbol" => "..."]). ❌

This array then propagated into the cached country data and was returned from getCurrenyFromCountryCode(), whose return type is ?string, causing the TypeError.

A secondary issue existed in getCountryCodeByCurrency() which used the dot-notation shorthand currencies.0 (only valid for list-shape countries) and then called strtolower($country['currency']) without a null guard, triggering the mb_strtolower(null) deprecation for map-shape territories.

Fix

Introduce a new Utils::resolveCurrencyCode($currencies): ?string helper that normalises both shapes:

public static function resolveCurrencyCode($currencies): ?string
{
    if (empty($currencies)) {
        return null;
    }

    // Convert Coollection / Collection objects to a plain array.
    if (is_object($currencies) && method_exists($currencies, 'toArray')) {
        $currencies = $currencies->toArray();
    }

    if (!is_array($currencies)) {
        return null;
    }

    // Sequential list shape: ["USD", "EUR", ...]
    if (array_is_list($currencies)) {
        $code = Arr::first($currencies);
        return is_string($code) ? $code : null;
    }

    // Associative map shape: {"USD": {"name": "...", "symbol": "..."}, ...}
    $code = array_key_first($currencies);
    return is_string($code) ? $code : null;
}

Changes

  • getCountryData() — replace Arr::first(...currencies...) with resolveCurrencyCode() so the cached country data always stores a string currency code, never an array.
  • getCountryCodeByCurrency() — replace currencies.0 dot-notation with resolveCurrencyCode() (fixes map-shape countries), and add an is_string() guard before strtolower() to eliminate the mb_strtolower(null) deprecation.

Affected territories

The following 10 territories had associative-map currencies and were broken before this fix:

cca2 Territory Currency
BQ Caribbean Netherlands USD
CC Cocos (Keeling) Islands AUD
CX Christmas Island AUD
GP Guadeloupe EUR
GF French Guiana EUR
MQ Martinique EUR
YT Mayotte EUR
RE Réunion EUR
SJ Svalbard and Jan Mayen NOK
TK Tokelau NZD

roncodes and others added 2 commits April 15, 2026 03:44
The PragmaRX Countries package returns the 'currencies' field in two
distinct shapes depending on the underlying data source:

  1. A sequential list of ISO 4217 code strings, e.g. ["USD", "EUR"]
  2. An associative map keyed by ISO 4217 code with detail objects as
     values, e.g. {"USD": {"name": "United States dollar", "symbol": "$"}}

The previous code used Arr::first() unconditionally. For the list shape
this works correctly — Arr::first() returns the first element, which is
the code string. For the associative map shape, however, Arr::first()
returns the first *value* (an array), not the first *key* (the code
string). This caused a TypeError in getCurrenyFromCountryCode() because
the function's return type is declared as ?string but an array was
returned instead.

Affected territories (cca2): BQ, CC, CX, GP, GF, MQ, YT, RE, SJ, TK.

Changes:
- Introduce Utils::resolveCurrencyCode() which normalises both shapes:
  * Sequential list  → Arr::first() on values (existing behaviour)
  * Associative map  → array_key_first() to extract the code from keys
  Coollection / Collection objects are converted via toArray() first so
  that array_is_list() and array_key_first() work reliably.
- Replace Arr::first(...currencies...) with resolveCurrencyCode() in
  getCountryData() (line 1202) so the cached country data always stores
  a string currency code, never an array.
- Apply the same fix in getCountryCodeByCurrency() (line 1131) which
  used the dot-notation shorthand currencies.0 — also broken for the
  associative map shape.
- Guard the strtolower() comparison in getCountryCodeByCurrency() with
  is_string() to prevent the mb_strtolower(null) deprecation warning
  that surfaced when currency resolved to null for map-shape countries.

Fixes: TypeError: getCurrenyFromCountryCode(): Return value must be of
type ?string, array returned (Utils.php:1235)
@roncodes roncodes merged commit e19c142 into main Apr 16, 2026
3 checks passed
@roncodes roncodes deleted the fix/get-currency-from-country-code-return-type branch April 16, 2026 01:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant