Data Types And Standard Library

Arrays

PHP arrays are one of the most flexible parts of the language. The same data type can represent a sequential list, a keyed map, a lightweight record, or a list of records returned from a database or decoded from JSON. That flexibility is useful, but it also means array code needs clear names, clear shape assumptions, and validation at the boundary.

You already met basic arrays in the PHP language track. This lesson focuses on the standard-library functions you will use when data has already arrived from a request, database query, API response, CSV file, or configuration file. The goal is not to memorize every array function. The goal is to recognize the shape you have, choose a function that matches that shape, and avoid silent data loss when keys or values do not mean what you think they mean.

Start with the array shape

A list uses numeric positions and is normally read in order. Product names, selected IDs, lines in a file, and validation messages are often lists.

PHP example
<?php

declare(strict_types=1);

$productNames = ['keyboard', 'mouse', 'monitor'];

echo $productNames[0] . PHP_EOL;

// Prints:
// keyboard

A map uses meaningful keys. Stock by SKU, configuration by option name, and errors by field name are maps.

PHP example
<?php

declare(strict_types=1);

$stockBySku = [
    'KB-101' => 12,
    'MS-202' => 0,
];

echo $stockBySku['KB-101'] . PHP_EOL;

// Prints:
// 12

A record is usually an associative array with a known set of keys. A list of records is an array where each item is one associative array.

PHP example
<?php

declare(strict_types=1);

$orders = [
    ['id' => 1, 'status' => 'paid', 'total' => 1250],
    ['id' => 2, 'status' => 'draft', 'total' => 800],
];

echo $orders[0]['status'] . PHP_EOL;

// Prints:
// paid

Many array bugs come from treating one shape as another. Before you reach for array_map(), array_column(), or array_merge(), ask: is this a plain list, a keyed map, a record, or a list of records? Variable names should answer that question. $paidOrders, $stockBySku, $totals, and $errorsByField are more useful than $data.

Pulling a column from records

array_column() is useful when you have a list of records and need one field from each record.

PHP example
<?php

declare(strict_types=1);

$orders = [
    ['id' => 1, 'status' => 'paid', 'total' => 1250],
    ['id' => 2, 'status' => 'paid', 'total' => 800],
];

$totals = array_column($orders, 'total');
$grandTotal = array_sum($totals);

echo 'Grand total: ' . $grandTotal . ' pennies' . PHP_EOL;

// Prints:
// Grand total: 2050 pennies

This is common after fetching rows from a database or decoding JSON. The code is short, but it only makes sense if every record is expected to contain a total key. When the records come from outside your code, validate that shape before extracting columns.

array_column() can also use one field as the key in the returned array.

PHP example
<?php

declare(strict_types=1);

$products = [
    ['sku' => 'KB-101', 'name' => 'Keyboard'],
    ['sku' => 'MS-202', 'name' => 'Mouse'],
];

$namesBySku = array_column($products, 'name', 'sku');

echo $namesBySku['MS-202'] . PHP_EOL;

// Prints:
// Mouse

That is useful, but it has a policy hidden inside it: if two records have the same key, the later value wins. If duplicate SKUs should be an error, check for duplicates before building the map.

Filtering rows

array_filter() keeps values that pass a test. When filtering a list, call array_values() afterwards if the result should remain a clean zero-based list.

PHP example
<?php

declare(strict_types=1);

$orders = [
    ['id' => 1, 'status' => 'paid', 'total' => 1250],
    ['id' => 2, 'status' => 'draft', 'total' => 800],
    ['id' => 3, 'status' => 'paid', 'total' => 2199],
];

$paidOrders = array_values(array_filter($orders, function (array $order): bool {
    return $order['status'] === 'paid';
}));

echo count($paidOrders) . ' paid orders' . PHP_EOL;

// Prints:
// 2 paid orders

array_filter() preserves the original keys. That is useful for maps because the keys still identify the values. It can surprise you with lists because the first remaining item may no longer be at index 0, and JSON encoding may then produce an object shape instead of a list shape.

Be careful when calling array_filter() without a callback. It removes falsy values, including 0, '0', false, null, and ''. That can be useful for some cleanup tasks, but it is dangerous when zero is a valid value.

PHP example
<?php

declare(strict_types=1);

$quantities = [3, 0, 5];
$nonNegative = array_values(array_filter($quantities, function (int $quantity): bool {
    return $quantity >= 0;
}));

echo implode(', ', $nonNegative) . PHP_EOL;

// Prints:
// 3, 0, 5

A callback makes the rule explicit and keeps valid zero values.

Mapping values

array_map() transforms each value and returns a new array. Use it when you want the same operation applied to each item.

PHP example
<?php

declare(strict_types=1);

$names = ['keyboard', 'mouse', 'monitor'];

$labels = array_map(function (string $name): string {
    return ucfirst($name);
}, $names);

echo implode(', ', $labels) . PHP_EOL;

// Prints:
// Keyboard, Mouse, Monitor

Mapping should not normally change external state. If the callback is sending emails, writing files, logging audit events, or updating a database, a foreach loop is usually clearer because the purpose is an action, not a transformation.

When mapping records, return a new record shape intentionally. Do not modify a nested array just because it is convenient if the caller still expects the original shape.

PHP example
<?php

declare(strict_types=1);

$products = [
    ['sku' => 'KB-101', 'name' => 'keyboard'],
    ['sku' => 'MS-202', 'name' => 'mouse'],
];

$labels = array_map(function (array $product): string {
    return $product['sku'] . ': ' . ucfirst($product['name']);
}, $products);

echo implode(' | ', $labels) . PHP_EOL;

// Prints:
// KB-101: Keyboard | MS-202: Mouse

Reducing values

array_reduce() turns an array into one final value. Totals and summaries are common examples.

PHP example
<?php

declare(strict_types=1);

$lines = [
    ['quantity' => 2, 'unitPrice' => 500],
    ['quantity' => 1, 'unitPrice' => 1299],
];

$total = array_reduce(
    $lines,
    function (int $carry, array $line): int {
        return $carry + ($line['quantity'] * $line['unitPrice']);
    },
    0
);

echo 'Basket total: ' . $total . ' pennies' . PHP_EOL;

// Prints:
// Basket total: 2299 pennies

Always provide the initial value. Without it, the behavior for an empty array may not match your domain rule, and the inferred type of the carry value becomes harder to reason about. If the reducer callback becomes hard to read, use a small named function or a foreach loop instead.

A loop is often the clearest choice when the summary has more than one value.

PHP example
<?php

declare(strict_types=1);

$orders = [
    ['status' => 'paid', 'total' => 1250],
    ['status' => 'draft', 'total' => 800],
    ['status' => 'paid', 'total' => 500],
];

$count = 0;
$total = 0;

foreach ($orders as $order) {
    if ($order['status'] !== 'paid') {
        continue;
    }

    $count++;
    $total += $order['total'];
}

echo $count . ' paid orders, ' . $total . ' pennies' . PHP_EOL;

// Prints:
// 2 paid orders, 1750 pennies

Readable array code matters more than using the most compact function chain.

Sorting arrays

Different sort functions preserve different things. sort() sorts values and reindexes numeric keys. asort() sorts values while preserving keys. ksort() sorts by keys.

PHP example
<?php

declare(strict_types=1);

$stockBySku = [
    'KB-101' => 12,
    'MS-202' => 3,
    'MN-303' => 8,
];

asort($stockBySku);

foreach ($stockBySku as $sku => $stock) {
    echo $sku . ': ' . $stock . PHP_EOL;
}

// Prints:
// MS-202: 3
// MN-303: 8
// KB-101: 12

Choose the sorting function based on whether the keys matter. Product IDs, slugs, database IDs, and field names usually matter. If the keys matter, do not use a function that discards or renumbers them unless that is the point of the operation.

For records, use usort() with a comparison callback.

PHP example
<?php

declare(strict_types=1);

$products = [
    ['sku' => 'KB-101', 'stock' => 12],
    ['sku' => 'MS-202', 'stock' => 3],
    ['sku' => 'MN-303', 'stock' => 8],
];

usort($products, function (array $a, array $b): int {
    return $a['stock'] <=> $b['stock'];
});

echo $products[0]['sku'] . PHP_EOL;

// Prints:
// MS-202

The spaceship operator, <=>, returns the comparison value expected by sort callbacks.

Merging arrays

array_merge() appends numeric-keyed values and lets later string-keyed values replace earlier ones.

PHP example
<?php

declare(strict_types=1);

$defaults = [
    'currency' => 'GBP',
    'perPage' => 20,
];

$requestOptions = [
    'perPage' => 50,
];

$options = array_merge($defaults, $requestOptions);

echo $options['currency'] . ' ' . $options['perPage'] . PHP_EOL;

// Prints:
// GBP 50

This is useful for options and configuration. Be careful when arrays contain numeric keys because merging a list is different from replacing named options.

The array union operator, +, has different behavior: it keeps keys from the left-hand array when the same key exists on both sides.

PHP example
<?php

declare(strict_types=1);

$provided = ['perPage' => 50];
$defaults = ['currency' => 'GBP', 'perPage' => 20];

$options = $provided + $defaults;

echo $options['currency'] . ' ' . $options['perPage'] . PHP_EOL;

// Prints:
// GBP 50

Both tools are valid. Pick the one whose precedence rule matches the situation, and make that rule obvious in variable names and tests.

Handling missing keys

Do not assume a key exists when data comes from outside your code. Check it at the boundary and fail with a useful message.

PHP example
<?php

declare(strict_types=1);

function requireOrderTotal(array $order): int
{
    if (!array_key_exists('total', $order)) {
        throw new InvalidArgumentException('Order total is required.');
    }

    if (!is_int($order['total'])) {
        throw new InvalidArgumentException('Order total must be an integer.');
    }

    return $order['total'];
}

echo requireOrderTotal(['id' => 1, 'total' => 1250]) . PHP_EOL;

// Prints:
// 1250

Use array_key_exists() when null is a possible value and you need to know whether the key exists. Use isset() when null should behave like a missing value. For example, an optional middle name may exist with a null value, while a required total should not be null at all.

Shape validation should happen before transformation. Once code starts mapping, filtering, and reducing records, error messages become harder to connect to the original bad input.

Choosing loops or array helpers

Array helpers are excellent when the operation has one clear purpose. Use array_map() when every item becomes one new item. Use array_filter() when every item is either kept or discarded. Use array_reduce() when many items become one value. That vocabulary makes code compact and expressive when the shape is simple.

A foreach loop is often better when the code validates input, builds more than one output, needs detailed error messages, or has several domain rules. There is no prize for turning every array operation into a chain of callbacks. A loop with named variables can be easier to debug, easier to step through, and easier for a teammate to change safely.

The dividing line is reviewability. If you can name the intermediate shape in one phrase, such as "paid orders" or "totals by SKU", creating that variable usually improves the code. If a chain reads as filter, reindex, column, reduce, then the separate variables may be worth the extra lines because they document each decision. If the chain is one obvious transformation, a helper function may be clearer than a loop.

Avoid mixing validation and transformation too deeply. First prove that external records have the required keys and types. Then transform them. That order gives users and logs better error messages and prevents later array functions from hiding which record was malformed.

Testing array transformations

Test array code with the shape it claims to accept. For lists, include empty lists, one-item lists, multiple items, and items that should remain at index 0 after filtering. For maps, include missing keys, duplicate keys where the source format allows them, and keys whose values are valid zeros. For records, include missing fields, fields with the wrong type, and extra fields that should be ignored or rejected according to the domain.

For order-style transformations, useful tests include:

  • no paid orders returns an empty list and a zero total;
  • paid orders at nonzero original indexes are reindexed when the output is a list;
  • a total of 0 is preserved when zero is valid;
  • missing required keys fail before totals are calculated;
  • keyed maps keep their keys after sorting or filtering.

These tests are not busywork. They document whether code is handling a list, map, record, or list of records. That is the main design decision in most PHP array code.

Review checklist

Before approving array code, identify the shape of every array variable. Check whether the function used preserves keys, discards keys, reindexes values, overwrites string keys, or silently accepts missing record fields. Look for array_filter() without a callback, array_merge() on keyed data where left/right precedence matters, and long chains of array functions whose intermediate shapes are not named.

Prefer a clear foreach loop when it makes validation, error handling, or multiple outputs easier to understand. Array helpers are valuable, but they should make the data flow clearer, not hide the rule being implemented.

What to remember

Arrays are flexible, but that flexibility is also the risk. Name variables after the shape they contain, such as $paidOrders, $stockBySku, or $totals. Validate records from external sources before transforming them. Know which functions preserve keys and which functions reindex. Prefer readable loops when nested array functions become difficult to explain in a code review.

Practice

Task: Build an order summary

Build a small order summary from an array of order records.

Requirements

  • Use declare(strict_types=1);.
  • Start with an array containing at least three orders.
  • Each order should contain id, status, and total.
  • Keep only orders with the status paid.
  • Reindex the filtered list.
  • Pull the paid order IDs into their own array.
  • Pull the paid totals into their own array and sum them.
  • Print the paid order IDs and the grand total.
  • Include the expected output as comments in the same PHP code block.

The finished example should make it clear when you are filtering records, when you are extracting columns, and when you are calculating the final total.

Show solution
PHP example
<?php

declare(strict_types=1);

$orders = [
    ['id' => 1, 'status' => 'paid', 'total' => 1250],
    ['id' => 2, 'status' => 'draft', 'total' => 800],
    ['id' => 3, 'status' => 'paid', 'total' => 2199],
];

$paidOrders = array_values(array_filter($orders, function (array $order): bool {
    return $order['status'] === 'paid';
}));

$paidOrderIds = array_column($paidOrders, 'id');
$paidTotals = array_column($paidOrders, 'total');
$grandTotal = array_sum($paidTotals);

echo 'Paid orders: ' . implode(', ', $paidOrderIds) . PHP_EOL;
echo 'Grand total: ' . $grandTotal . ' pennies' . PHP_EOL;

// Prints:
// Paid orders: 1, 3
// Grand total: 3449 pennies

This solution separates the work into clear stages. array_filter() chooses the records, array_values() turns the result back into a normal list, array_column() extracts the fields needed for output and calculation, and array_sum() produces the final total.

Task: Merge request options with defaults

Build a small options helper for a paginated product list.

Requirements

  • Use declare(strict_types=1);.
  • Start with default options containing currency, perPage, and sort.
  • Start with request options that override perPage and sort.
  • Merge the arrays so request options take precedence over defaults.
  • Print the final currency, per-page limit, and sort mode.
  • Add a second example using the array union operator where provided options also take precedence.
  • Include expected output comments in the PHP code block.
Show solution
PHP example
<?php

declare(strict_types=1);

$defaults = [
    'currency' => 'GBP',
    'perPage' => 20,
    'sort' => 'name',
];

$requestOptions = [
    'perPage' => 50,
    'sort' => 'stock',
];

$mergedOptions = array_merge($defaults, $requestOptions);

printf(
    "%s %d %s\n",
    $mergedOptions['currency'],
    $mergedOptions['perPage'],
    $mergedOptions['sort']
);

$unionOptions = $requestOptions + $defaults;

printf(
    "%s %d %s\n",
    $unionOptions['currency'],
    $unionOptions['perPage'],
    $unionOptions['sort']
);

// Prints:
// GBP 50 stock
// GBP 50 stock

array_merge() works here because these are string-keyed options and later values should override earlier values. The union example uses the opposite ordering: provided options are on the left, so they win when a key exists in both arrays.

Task: Validate product rows before indexing

Create a helper that validates product records before turning them into a map keyed by SKU.

Requirements

  • Use declare(strict_types=1);.
  • Accept a list of product arrays.
  • Require each product to contain a string sku, a string name, and an integer stock.
  • Reject duplicate SKUs with a clear exception message.
  • Return an array keyed by SKU.
  • Print one product name and stock count from the returned map.
  • Include one duplicate-SKU case and print the exception message.
  • Include expected output comments in the PHP code block.
Show solution
PHP example
<?php

declare(strict_types=1);

/**
 * @param list<array<string, mixed>> $products
 * @return array<string, array{sku: string, name: string, stock: int}>
 */
function productsBySku(array $products): array
{
    $indexed = [];

    foreach ($products as $product) {
        foreach (['sku', 'name', 'stock'] as $key) {
            if (!array_key_exists($key, $product)) {
                throw new InvalidArgumentException("Product {$key} is required.");
            }
        }

        if (!is_string($product['sku']) || $product['sku'] === '') {
            throw new InvalidArgumentException('Product sku must be a non-empty string.');
        }

        if (!is_string($product['name']) || $product['name'] === '') {
            throw new InvalidArgumentException('Product name must be a non-empty string.');
        }

        if (!is_int($product['stock'])) {
            throw new InvalidArgumentException('Product stock must be an integer.');
        }

        if (array_key_exists($product['sku'], $indexed)) {
            throw new InvalidArgumentException('Duplicate SKU: ' . $product['sku']);
        }

        $indexed[$product['sku']] = [
            'sku' => $product['sku'],
            'name' => $product['name'],
            'stock' => $product['stock'],
        ];
    }

    return $indexed;
}

$products = productsBySku([
    ['sku' => 'KB-101', 'name' => 'Keyboard', 'stock' => 12],
    ['sku' => 'MS-202', 'name' => 'Mouse', 'stock' => 0],
]);

echo $products['MS-202']['name'] . ': ' . $products['MS-202']['stock'] . PHP_EOL;

try {
    productsBySku([
        ['sku' => 'KB-101', 'name' => 'Keyboard', 'stock' => 12],
        ['sku' => 'KB-101', 'name' => 'Spare keyboard', 'stock' => 3],
    ]);
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage() . PHP_EOL;
}

// Prints:
// Mouse: 0
// Duplicate SKU: KB-101

The helper validates the record shape before building the keyed map. That prevents a later duplicate SKU from silently replacing an earlier product.