PHP Arrays Multidimensionais

Um array pode guardar qualquer valor, e isso inclui outro array. Quando os itens de um array são arrays, ele vira uma espécie de tabela, com linhas e colunas. É assim que o PHP representa a lista de alunos de uma turma, os produtos de uma loja ou as linhas que vêm de um banco de dados. O nome assusta (multidimensional = "de várias dimensões"), mas a ideia é simples: uma gaveta que tem um armário dentro.

Você vai aprender
  • O que é um array de arrays e como criá-lo.
  • Como ler e mudar um valor com dois colchetes: $alunos[0]["nome"].
  • Como acrescentar uma linha inteira à tabela.
  • Como percorrer com um laço, e com laços aninhados (um dentro do outro).
  • Como transformar o array numa tabela HTML de verdade.
  • Os erros mais comuns ao errar o nível do array.

Antes, é bom ter feito Arrays Indexados, Arrays Associativos e foreach: esta aula junta os três.


O mais simples: uma grade de jogo da velha

Um tabuleiro de jogo da velha tem 3 linhas, e cada linha tem 3 casas. Em PHP: um array com 3 itens, e cada item é um array com 3 casas.

<?php
$velha = [
    ["X", "O", "X"],     // linha 0
    [" ", "X", "O"],     // linha 1
    ["O", " ", "X"],     // linha 2
];
echo "Centro: ", $velha[1][1], "\n";
echo "Canto de baixo, à direita: ", $velha[2][2], "\n";
$velha[1][0] = "O";      // a jogada do O na casa vazia da linha 1
$velha
O array de fora: tem 3 itens (índices 0, 1 e 2). Cada item é uma linha.
["X", "O", "X"]
Um array de dentro: uma linha, com 3 casas (índices 0, 1 e 2).
$velha[1]
A linha 1 inteira: o array [" ", "X", "O"].
$velha[1][1]
Da linha 1, a casa 1: "X". Leia da esquerda para a direita: primeiro escolhe a linha, depois a casa dentro dela.
$velha[1][0] = "O";
Muda uma casa só, com a mesma escrita.

Pense nos dois colchetes como um endereço: "rua 1, casa 1". O primeiro número escolhe a rua (a linha); o segundo, a casa naquela rua (a coluna).

coluna 0coluna 1coluna 2
linha 0[0][0] X[0][1] O[0][2] X
linha 1[1][0] (vazia → O)[1][1] X[1][2] O
linha 2[2][0] O[2][1] (vazia)[2][2] X

Mostrar a grade: um laço dentro do outro

Para passar por todas as casas, você precisa de dois laços: o de fora anda pelas linhas; o de dentro, pelas casas de cada linha. Isso se chama laços aninhados (um dentro do outro, como ninhos).

for ($linha = 0; $linha < 3; $linha++) {
    for ($coluna = 0; $coluna < 3; $coluna++) {
        echo " ", $velha[$linha][$coluna], " ";
        if ($coluna < 2) {
            echo "|";
        }
    }
    echo "\n";
    if ($linha < 2) {
        echo "---+---+---\n";
    }
}
$ php velha.php
Centro: X
Canto de baixo, à direita: X
 X | O | X
---+---+---
 O | X | O               « a casa [1][0] agora tem O
---+---+---
 O |   | X 

Siga o caminho: com $linha = 0, o laço de dentro roda inteiro (colunas 0, 1 e 2) e só então a linha passa para 1, e o laço de dentro roda inteiro de novo. São 3 × 3 = 9 voltas no total. Os laços for estão explicados na aula for.


A tabela de alunos da Ana

O caso mais comum no dia a dia é uma lista de fichas: um array indexado (a lista) em que cada item é um array associativo (a ficha). A Ana dá aulas de informática e guarda a turma assim:

<?php
$alunos = [
    ["nome" => "Ana",    "idade" => 29, "cidade" => "Recife",    "nota" => 9.5],
    ["nome" => "Bruno",  "idade" => 34, "cidade" => "Olinda",    "nota" => 7.0],
    ["nome" => "Carla",  "idade" => 22, "cidade" => "Caruaru",   "nota" => 8.5],
    ["nome" => "Diego",  "idade" => 41, "cidade" => "Petrolina", "nota" => 6.0],
];
echo $alunos[0]["nome"], "\n";
echo $alunos[2]["cidade"], "\n";
echo "Alunos: ", count($alunos), "\n";
echo "Campos de cada aluno: ", count($alunos[0]), "\n";
print_r($alunos[1]);

Veja como uma tabela no papel:

Índice"nome""idade""cidade""nota"
0Ana29Recife9.5
1Bruno34Olinda7.0
2Carla22Caruaru8.5
3Diego41Petrolina6.0

Cada linha é um aluno (escolhida pelo número); cada coluna é um campo (escolhida pelo nome).

$alunos[0]
A ficha inteira da Ana: ["nome" => "Ana", "idade" => 29, …].
$alunos[0]["nome"]
Da ficha 0, o campo "nome": "Ana".
count($alunos)
Conta só o nível de fora: quantas fichas (4).
count($alunos[0])
Conta os campos de uma ficha (4).
$ php alunos.php
Ana
Caruaru
Alunos: 4
Campos de cada aluno: 4
Array
(
    [nome] => Bruno
    [idade] => 34
    [cidade] => Olinda
    [nota] => 7              « 7.0 aparece como 7 no print_r
)

Mudar um campo, acrescentar um campo, acrescentar um aluno

<?php
$alunos = [
    ["nome" => "Ana",   "idade" => 29, "cidade" => "Recife", "nota" => 9.5],
    ["nome" => "Bruno", "idade" => 34, "cidade" => "Olinda", "nota" => 7.0],
];
$alunos[1]["nota"] = 7.5;                       // muda um campo de uma ficha
$alunos[0]["email"] = "ana@exemplo.com";        // acrescenta um campo só na ficha 0
$alunos[] = ["nome" => "Elisa", "idade" => 19, "cidade" => "Garanhuns", "nota" => 10.0];
print_r($alunos);
$ php alunos-mudar.php
Array
(
    [0] => Array
        (
            [nome] => Ana
            [idade] => 29
            [cidade] => Recife
            [nota] => 9.5
            [email] => ana@exemplo.com
        )

    [1] => Array
        (
            [nome] => Bruno
            [idade] => 34
            [cidade] => Olinda
            [nota] => 7.5
        )

    [2] => Array
        (
            [nome] => Elisa
            [idade] => 19
            [cidade] => Garanhuns
            [nota] => 10
        )

)

No print_r, cada nível de array ganha mais recuo, e o array de dentro aparece como Array ( … ) no lugar do valor. A última linha do código usa o [] vazio que você conhece: acrescenta no fim do array de fora, e o que entra é uma ficha inteira.

Repare também que a Ana agora tem um campo "email" e os outros não. O PHP permite: cada ficha é um array independente. Mas é fonte de avisos (veja Quando dá errado); numa tabela, prefira que todas as fichas tenham os mesmos campos.


Percorrer a tabela

Um foreach: uma ficha por volta

Na maior parte do tempo, um laço basta: a cada volta, a variável recebe uma ficha inteira, e você lê os campos dela pelo nome.

<?php
$alunos = [
    ["nome" => "Ana",    "idade" => 29, "cidade" => "Recife",    "nota" => 9.5],
    ["nome" => "Bruno",  "idade" => 34, "cidade" => "Olinda",    "nota" => 7.0],
    ["nome" => "Carla",  "idade" => 22, "cidade" => "Caruaru",   "nota" => 8.5],
    ["nome" => "Diego",  "idade" => 41, "cidade" => "Petrolina", "nota" => 6.0],
];
foreach ($alunos as $aluno) {
    echo $aluno["nome"], " (", $aluno["cidade"], "): ", $aluno["nota"], "\n";
}
$soma = 0;
foreach ($alunos as $aluno) {
    $soma += $aluno["nota"];
}
echo "Média da turma: ", $soma / count($alunos), "\n";
$ php alunos-foreach.php
Ana (Recife): 9.5
Bruno (Olinda): 7
Carla (Caruaru): 8.5
Diego (Petrolina): 6
Média da turma: 7.75

O nome no singular ($aluno) para o item e no plural ($alunos) para a lista é uma convenção que ajuda muito: dentro do laço, $aluno["nome"] se lê quase como português. Para pegar uma coluna inteira de uma vez (todas as notas, todos os nomes), existe a função array_column, na aula Funções de Array.

Acrescente um aluno, mude a nota de outro e veja o resultado. Tente também mostrar a idade de cada um, ou só os alunos de uma cidade (com um if dentro do foreach).

<?php
$alunos = [
    ["nome" => "Ana",   "idade" => 29, "cidade" => "Recife",    "nota" => 9.5],
    ["nome" => "Bruno", "idade" => 34, "cidade" => "Olinda",    "nota" => 7.0],
    ["nome" => "Carla", "idade" => 22, "cidade" => "Caruaru",   "nota" => 8.5],
];

$alunos[] = ["nome" => "Diego", "idade" => 41, "cidade" => "Petrolina", "nota" => 6.0];
$alunos[1]["nota"] = 7.5;

foreach ($alunos as $aluno) {
    echo $aluno["nome"], " mora em ", $aluno["cidade"], " e tirou ", $aluno["nota"], "\n";
}

Dois foreach: passar por todos os campos

Quando você não sabe (ou não quer escrever) o nome de cada campo, coloque um segundo foreach dentro do primeiro. O de fora pega cada ficha; o de dentro percorre os pares chave => valor daquela ficha.

<?php
$alunos = [
    ["nome" => "Ana",   "idade" => 29, "cidade" => "Recife", "nota" => 9.5],
    ["nome" => "Bruno", "idade" => 34, "cidade" => "Olinda", "nota" => 7.0],
];
foreach ($alunos as $numero => $aluno) {
    echo "Aluno nº $numero\n";
    foreach ($aluno as $campo => $valor) {
        echo "  $campo: $valor\n";
    }
}
$ php alunos-aninhado.php
Aluno nº 0
  nome: Ana
  idade: 29
  cidade: Recife
  nota: 9.5
Aluno nº 1
  nome: Bruno
  idade: 34
  cidade: Olinda
  nota: 7
LaçoPercorreA variável recebe
de fora: foreach ($alunos as $numero => $aluno)a lista$numero: 0, 1…; $aluno: uma ficha (array)
de dentro: foreach ($aluno as $campo => $valor)uma ficha$campo: "nome", "idade"…; $valor: o dado
Use nomes diferentes nos dois laços

Se o laço de dentro reaproveitar a variável do de fora (por exemplo, foreach ($aluno as $aluno)), ele apaga a ficha que estava sendo usada. Dê a cada laço os seus próprios nomes.


Mais níveis e outras combinações

Não existe limite de níveis, e qualquer combinação vale. Duas muito comuns:

Um campo que é uma lista

<?php
$aluna = [
    "nome"  => "Ana",
    "notas" => [9.0, 8.5, 10.0],
];
echo "Segunda nota: ", $aluna["notas"][1], "\n";
echo "Quantidade de notas: ", count($aluna["notas"]), "\n";
$aluna["notas"][] = 7.5;
foreach ($aluna["notas"] as $n) {
    echo $n, " ";
}
echo "\n";
$ php notas3.php
Segunda nota: 8.5
Quantidade de notas: 3
9 8.5 10 7.5 

$aluna["notas"] é um array comum: dá para usar count, [] e foreach nele. Uma turma inteira com várias notas por aluno teria três níveis: $alunos[0]["notas"][1] (aluno 0, campo notas, nota 1).

Grupos com nome

<?php
$turmas = [
    "manhã" => ["Ana", "Bruno"],
    "noite" => ["Carla", "Diego", "Elisa"],
];
foreach ($turmas as $turno => $nomes) {
    echo "Turma da $turno (", count($nomes), "): ", implode(", ", $nomes), "\n";
}
echo $turmas["noite"][2], "\n";
$ php turmas.php
Turma da manhã (2): Ana, Bruno
Turma da noite (3): Carla, Diego, Elisa
Elisa

Aqui o array de fora é associativo (chaves "manhã" e "noite") e os de dentro são listas. O endereço $turmas["noite"][2] se lê "da turma da noite, o terceiro nome".

Isto é JSON

Quase todo dado que circula pela internet tem esse formato de "listas de fichas", e o formato JSON é exatamente isso escrito como texto. Na aula JSON, você vai transformar um array como $alunos em JSON e de volta com uma linha de código.


Gerar uma tabela HTML

Este é o motivo pelo qual arrays multidimensionais aparecem em todo site em PHP: a lista de fichas vira, quase sem esforço, uma tabela na página. A Ana quer publicar a turma no site do curso.

<?php
$alunos = [
    ["nome" => "Ana",   "idade" => 29, "cidade" => "Recife", "nota" => 9.5],
    ["nome" => "Bruno", "idade" => 34, "cidade" => "Olinda", "nota" => 7.0],
    ["nome" => "Carla", "idade" => 22, "cidade" => "Caruaru", "nota" => 8.5],
];
?>
<table border="1">
  <tr><th>Nome</th><th>Idade</th><th>Cidade</th><th>Nota</th></tr>
<?php foreach ($alunos as $aluno): ?>
  <tr>
    <td><?= htmlspecialchars($aluno["nome"]) ?></td>
    <td><?= $aluno["idade"] ?></td>
    <td><?= htmlspecialchars($aluno["cidade"]) ?></td>
    <td><?= number_format($aluno["nota"], 1, ",", ".") ?></td>
  </tr>
<?php endforeach; ?>
</table>
TrechoO que faz
?> depois do arraySai do modo PHP: o que vem depois é HTML, enviado como está (aula PHP Dentro do HTML).
<?php foreach ($alunos as $aluno): ?>O foreach na sintaxe alternativa: dois-pontos no lugar de {. Tudo até o endforeach se repete uma vez por aluno, inclusive o HTML.
<?= … ?>Atalho para <?php echo … ?>: escreve o valor ali no meio do HTML.
htmlspecialchars(…)Protege a página: se um nome tiver <, > ou &, eles aparecem como texto e não viram HTML. Sempre use em textos que vêm de dados (aula Escapar a Saída).
number_format(…, 1, ",", ".")Nota com uma casa e vírgula: 9,5.
<?php endforeach; ?>Fecha o laço.

Rodando no terminal, você vê o HTML que o PHP gera, que é o que o navegador recebe:

$ php tabela.php
<table border="1">
  <tr><th>Nome</th><th>Idade</th><th>Cidade</th><th>Nota</th></tr>
  <tr>
    <td>Ana</td>
    <td>29</td>
    <td>Recife</td>
    <td>9,5</td>
  </tr>
  <tr>
    <td>Bruno</td>
    <td>34</td>
    <td>Olinda</td>
    <td>7,0</td>
  </tr>
  <tr>
    <td>Carla</td>
    <td>22</td>
    <td>Caruaru</td>
    <td>8,5</td>
  </tr>
</table>

Três fichas no array, três <tr> no HTML. Se a Ana acrescentar um aluno ao array, a tabela ganha uma linha sem ela mexer em nada do HTML.

A outra forma: tudo com echo e dois laços

Também dá para gerar a tabela inteira de dentro do PHP. Com dois foreach, ela nem precisa saber o nome dos campos: o cabeçalho sai das chaves da primeira ficha (array_keys devolve a lista de chaves).

<?php
$alunos = [
    ["nome" => "Ana",   "idade" => 29, "cidade" => "Recife", "nota" => 9.5],
    ["nome" => "Bruno", "idade" => 34, "cidade" => "Olinda", "nota" => 7.0],
];
echo "<table border=\"1\">\n";
echo "  <tr>";
foreach (array_keys($alunos[0]) as $campo) {
    echo "<th>", htmlspecialchars($campo), "</th>";
}
echo "</tr>\n";
foreach ($alunos as $aluno) {
    echo "  <tr>";
    foreach ($aluno as $valor) {
        echo "<td>", htmlspecialchars((string) $valor), "</td>";
    }
    echo "</tr>\n";
}
echo "</table>\n";
$ php tabela2.php
<table border="1">
  <tr><th>nome</th><th>idade</th><th>cidade</th><th>nota</th></tr>
  <tr><td>Ana</td><td>29</td><td>Recife</td><td>9.5</td></tr>
  <tr><td>Bruno</td><td>34</td><td>Olinda</td><td>7</td></tr>
</table>

O (string) converte números em texto antes do htmlspecialchars, que só aceita strings (aula Conversão de Tipos).

Qual usar?

  • HTML com <?= ?> e foreach: … endforeach: quando a página tem um formato definido (cabeçalhos em português, notas formatadas). O HTML fica legível e fácil de mudar. É a forma mais usada em sites.
  • Tudo com echo: para uma tabela "genérica", que mostra qualquer array sem conhecer os campos, por exemplo para depurar.

Quando dá errado

Undefined array key 5 seguido de Trying to access array offset on null
<?php
$alunos = [
    ["nome" => "Ana",   "nota" => 9.5],
    ["nome" => "Bruno", "nota" => 7.0],
];
echo $alunos[5]["nome"], "\n";
echo $alunos[0]["idade"], "\n";
echo $alunos[0], "\n";
$ php erro-multi.php

Warning: Undefined array key 5 in /home/ana/site/erro-multi.php on line 6

Warning: Trying to access array offset on null in /home/ana/site/erro-multi.php on line 6


Warning: Undefined array key "idade" in /home/ana/site/erro-multi.php on line 7


Warning: Array to string conversion in /home/ana/site/erro-multi.php on line 8
Array

O que aconteceu, linha a linha:

  • Linha 6: não existe o aluno 5. $alunos[5] vale null (primeiro aviso), e pedir ["nome"] de null gera o segundo aviso. Resolva o primeiro e o segundo some.
  • Linha 7: o aluno 0 existe, mas não tem o campo "idade". O aviso cita a chave que falta, então você sabe em que nível olhar.
  • Linha 8: faltou o segundo colchete. $alunos[0] é uma ficha inteira (um array), e o echo não sabe escrevê-la.

Como resolver: faça print_r do array e confira cada nível do endereço. Se o dado pode faltar, $alunos[5]["nome"] ?? "?" resolve os dois níveis de uma vez, sem aviso.

TypeError: Cannot access offset of type string on string
<?php
$aluno = ["nome" => "Ana", "nota" => 9.5];
foreach ($aluno as $a) {
    echo $a["nome"], "\n";
}
$ php erro-string2.php

Fatal error: Uncaught TypeError: Cannot access offset of type string on string in /home/ana/site/erro-string2.php:4
Stack trace:
#0 {main}
  thrown in /home/ana/site/erro-string2.php on line 4

O que aconteceu: o foreach foi feito sobre uma ficha, não sobre a lista. Então $a recebeu "Ana" (uma string), e o código tentou ler ["nome"] de dentro de uma string. "Não dá para acessar uma posição do tipo string numa string." O mesmo erro aparece com um colchete a mais, como $aluno["nome"]["nota"].

Como resolver: confira em que nível você está. O foreach que dá fichas é o que percorre a lista ($alunos); o que percorre uma ficha dá os valores soltos. Um var_dump($a) dentro do laço mostra na hora o que a variável recebeu.


Referência rápida

Você quer…Código
Criar uma lista de fichas$alunos = [["nome" => "Ana", "nota" => 9.5], ["nome" => "Bruno", "nota" => 7.0]];
Ler um campo$alunos[0]["nome"]
Ler uma ficha inteira$alunos[0]
Mudar um campo$alunos[1]["nota"] = 8.0;
Acrescentar uma ficha$alunos[] = ["nome" => "Carla", "nota" => 8.5];
Quantas fichas / quantos camposcount($alunos) / count($alunos[0])
Percorrer as fichasforeach ($alunos as $aluno) { … $aluno["nome"] … }
Percorrer tudoforeach ($alunos as $aluno) { foreach ($aluno as $campo => $valor) { … } }
Grade com índicesfor ($l…) { for ($c…) { $grade[$l][$c] } }
Ler com valor padrão$alunos[9]["nome"] ?? "?"

Missão final

Missão: a página mostra a tabela da turma, mas só com o nome. Complete cada linha com mais duas células: a nota (com number_format, uma casa e vírgula) e a situação: Aprovado se a nota for 7 ou mais, Reprovado se for menor. Dica: o operador ternário cabe dentro de um <?= ?>. Veja o resultado na aba Resultado.

<?php
$alunos = [
    ["nome" => "Ana",   "idade" => 29, "cidade" => "Recife",    "nota" => 9.5],
    ["nome" => "Bruno", "idade" => 34, "cidade" => "Olinda",    "nota" => 7.0],
    ["nome" => "Carla", "idade" => 22, "cidade" => "Caruaru",   "nota" => 8.5],
    ["nome" => "Diego", "idade" => 41, "cidade" => "Petrolina", "nota" => 6.0],
];
?>
<h1>Notas da turma</h1>
<table border="1">
  <tr><th>Nome</th><th>Nota</th><th>Situação</th></tr>
<?php foreach ($alunos as $aluno): ?>
  <tr>
    <td><?= htmlspecialchars($aluno["nome"]) ?></td>
    <!-- complete aqui: a nota e a situação -->
  </tr>
<?php endforeach; ?>
</table>
<?php
$alunos = [
    ["nome" => "Ana",   "idade" => 29, "cidade" => "Recife",    "nota" => 9.5],
    ["nome" => "Bruno", "idade" => 34, "cidade" => "Olinda",    "nota" => 7.0],
    ["nome" => "Carla", "idade" => 22, "cidade" => "Caruaru",   "nota" => 8.5],
    ["nome" => "Diego", "idade" => 41, "cidade" => "Petrolina", "nota" => 6.0],
];
?>
<h1>Notas da turma</h1>
<table border="1">
  <tr><th>Nome</th><th>Nota</th><th>Situação</th></tr>
<?php foreach ($alunos as $aluno): ?>
  <tr>
    <td><?= htmlspecialchars($aluno["nome"]) ?></td>
    <td><?= number_format($aluno["nota"], 1, ",", ".") ?></td>
    <td><?= $aluno["nota"] >= 7 ? "Aprovado" : "Reprovado" ?></td>
  </tr>
<?php endforeach; ?>
</table>

No laboratório, a solução gera este HTML:

$ php notas.php
<h1>Notas da turma</h1>
<table border="1">
  <tr><th>Nome</th><th>Nota</th><th>Situação</th></tr>
  <tr>
    <td>Ana</td>
    <td>9,5</td>
    <td>Aprovado</td>
  </tr>
  <tr>
    <td>Bruno</td>
    <td>7,0</td>
    <td>Aprovado</td>
  </tr>
  <tr>
    <td>Carla</td>
    <td>8,5</td>
    <td>Aprovado</td>
  </tr>
  <tr>
    <td>Diego</td>
    <td>6,0</td>
    <td>Reprovado</td>
  </tr>
</table>

Exercícios

1. O endereço do dado

Complete para mostrar a cidade do segundo aluno da lista:

echo $alunos;

2. Uma ficha nova

Complete para acrescentar uma ficha no fim da lista:

$alunos = ["nome" => "Elisa", "nota" => 10.0];

3. O laço de dentro

Complete o segundo foreach para percorrer os campos de cada ficha:

foreach ($alunos as $aluno) {
  foreach ( as $campo => $valor) { … }
}

4. Com $m = [[1, 2, 3], [4, 5, 6]];, quanto vale $m[1][0]?

5. E count($m), com o mesmo array?

6. O código echo $alunos[0]; mostra Array e um aviso. Por quê?


Resumo

  • Um array multidimensional é um array cujos itens são arrays. O formato mais comum é a lista de fichas: indexado por fora, associativo por dentro.
  • Cada par de colchetes desce um nível: $alunos[0]["nome"] = ficha 0, campo nome.
  • Mudar e acrescentar usam a mesma escrita: $alunos[1]["nota"] = 8; e $alunos[] = [ … ];.
  • count conta só o nível em que você está.
  • Um foreach dá uma ficha por volta; dois foreach aninhados passam por todos os campos. Para grades com índices, dois for.
  • Para gerar HTML: foreach: … endforeach com <?= htmlspecialchars(…) ?>.
  • Erros de nível: Array to string conversion (faltou colchete), Cannot access offset of type string on string (colchete a mais ou laço no nível errado).