Ethers.js can call fetchMarketItems() fine, but PHP (web3p/web3.php) fails to decode the tuple[] return — how do I read struct[] results?

I have an NFT marketplace contract running on a local Hardhat node. The function below returns an array of structs:

struct MarketItem {
    uint256 tokenId;
    address payable seller;
    address payable owner;
    uint256 price;
    bool sold;
}
function fetchMarketItems() public view returns (MarketItem[] memory) {
    // ...
}

ABI excerpt:


{
  "inputs": [],
  "name": "fetchMarketItems",
  "outputs": [
    {
      "components": [
        { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
        { "internalType": "address payable", "name": "seller", "type": "address" },
        { "internalType": "address payable", "name": "owner",  "type": "address" },
        { "internalType": "uint256", "name": "price",  "type": "uint256" },
        { "internalType": "bool",    "name": "sold",   "type": "bool" }
      ],
      "internalType": "struct NFTMarketplace.MarketItem[]",
      "name": "",
      "type": "tuple[]"
    }
  ],
  "stateMutability": "view",
  "type": "function"
}

What works (JS / ethers.js)

if (!NFTMarketplaceABI) await loadABI();
const provider = new ethers.providers.JsonRpcProvider(); // Hardhat default localhost
const contract  = new ethers.Contract(
  "0x5FbDB2315678afecb367f032d93F642f64180aa3",
  NFTMarketplaceABI,
  provider
);
const data = await contract.fetchMarketItems();
console.log(data); // OK: array of items

What fails (PHP / web3p/web3.php)

Environment: PHP 8.x, Laravel, web3p/web3.php (Contract/HttpProvider/HttpRequestManager), Hardhat node accessible at http://172.17.0.1:8545 from inside Docker.

$provider        = new HttpProvider(new HttpRequestManager('http://172.17.0.1:8545'));
$contractAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3';

$abiPath = app()->basePath('public/NFTMarketplace.json');
$abiJson = json_decode(file_get_contents($abiPath), true);
if (!isset($abiJson['abi'])) {
    throw new Exception('ABI not found');
}

$contract = new Contract($provider, $abiJson['abi']);
$contract->at($contractAddress);

// sanity check
$contract->eth->getCode($contractAddress, 'latest', function ($e, $code) {
    if ($e !== null) throw new Exception($e->getMessage());
    if ($code === '0x' || $code === '0x0') throw new Exception('Address is not a contract');
});

$items  = [];
$errMsg = null;

$contract->call('fetchMarketItems', [], function ($err, $result) use (&$items, &$errMsg) {
    if ($err !== null) { $errMsg = $err->getMessage(); return; }
    $items = $result ?? [];
});

if ($errMsg) {
    throw new RuntimeException("fetchMarketItems failed: {$errMsg}");
}

Actual result: the callback receives an error and $errMsg is set. Example message I see:
fetchMarketItems failed: unsupported or invalid solidity type “tuple[]” (message may vary; earlier I’ve also hit decode-related errors while stepping through the library’s ABI decoder).

Expected: an array of market items, same as in ethers.js.

What I’ve tried

Verified the contract address and eth_getCode (non-zero).

Confirmed the ABI file is the same one ethers.js uses.

Tried both 127.0.0.1:8545 and 172.17.0.1:8545 (Docker host).

Searched issues/docs for tuple[]/struct[] return support in web3p/web3.php.

Tested simpler functions (e.g., returns uint256) — those work.

Question

Does web3p/web3.php support decoding returns of dynamic arrays of structs (tuple[]) from Solidity? If yes, what is the correct way to call/define the ABI so that Contract->call(‘fetchMarketItems’) returns decoded data? If not, what are practical workarounds in PHP (e.g., returning parallel arrays, using another ABI decoder/package, or different PHP Web3 library) to read struct[] results?

Notes

Node: Hardhat local chain

Contract: 0x5FbDB2315678afecb367f032d93F642f64180aa3

Framework: Laravel

Library: web3p/web3.php

Everything up to $contract->call(‘fetchMarketItems’, …) works fine — the provider connects, the ABI loads, eth_getCode returns bytecode (non-zero), and simple view calls (e.g., listingPrice()) decode correctly. The error only appears when calling fetchMarketItems (returns tuple[]).