Nossa meta é criar um gerenciador de conteúdos. Isto implica em guardar dados e, posteriormente, utilizá-los para montar um documento.
Iremos utilizar a função file_put_contents() para gravar um arquivo e file_get_contents() para ler o arquivo criado.
O problema é que um documento possui diversos “campos” distintos – título, conteúdo, nome do autor, data de criação, etc – que desejaríamos guardar e recuperar separadamente.
Como arquivos são lidos como strings (cadeia de caractéres), para gravar diversas variáveis precisaremos “serializá-las” – transformá-las em uma única série de caracteres – de tal forma que posteriormente seja possível separá-las novamente, reconstituindo cada campo.
O PHP oferece duas funções muito práticas para realizar esta tarefa: serialize() e unserialize().
Vamos considerar nosso documento como um array, e vamos preencher este array com os conteúdos do nosso documento:
$document['title'] = 'Olá mundo'; $document['content'] = 'Eu estou prestes a me tornar um programador de verdade!';
Vamos agora gravar este array em um arquivo:
file_put_contents ('teste.txt', serialize ($document));
O arquivo “teste.txt” será criado com o seguinte conteúdo:
a:2:{s:5:"title";s:9:"Olá mundo";s:7:"content";s:55:"Eu estou prestes a me tornar um programador de verdade!";}
Poderemos reconstituir o nosso array com a função unserialize() como se segue:
$document = unserialize (file_get_contents ('teste.txt'));
print '<html><body>';
print '<h1>' . $document['title'] . '</h1>';
print nl2br ($document['content']);
print '</body></html>'
Isto irá enviar o seguinte documento para o navegador:
Olá mundo Eu estou prestes a me tornar um programador de verdade!
Este método é rápido e prático para armazenar dados em arquivos e em bancos de dados. O inconveniente é que o arquivo gerado é confuso e difícil de alterar. Durante o desenvolvimento do nosso gerenciador de conteúdos, será necessário editar alguns arquivos de dados “manualmente“. Por isto, vamos criar nossa própria função para serializar e reconstituir arrays.
Os arquivos de dados deverão guardar, além dos textos, também algumas configurações. Sabemos, de antemão, que nossos arrays de dados poderão conter strings, números inteiros e arrays (com strings, inteiros ou arrays…). Embora o PHP possua outros tipos de dados – como booleanos, números de ponto flutuante, objetos… nós não iremos utilizá-los para armazenar dados – isto simplificará consideravelmente o processo de serialização e reconstituição.
A complexidade deste processo provêm do fato de que o valor de um array pode ser também um array, este, por sua vez, pode ter outros arrays como valores, e assim por diante. Para percorrer um array multi-dimensional, o processo é um pouco mais complexo que um simples “foreach”.
Imagine um edifício cheio de andares vazios. Vamos iniciar o nosso trabalho no primeiro andar. Espalharemos o array a ser processado no primeiro andar e começaremos a percorrer pelos seus itens com each(). Se por ventura o valor de um item for também um array, pegaremos este valor e o levaremos para o andar de cima (o segundo andar). Lá, iniciaremos o trabalho com o novo array até que ele termine. Ao terminar, desceremos novamente para o andar inferior (o primeiro andar) e continuaremos a partir de onde havíamos parado. Ao terminar, desceremos até o térreo e sairemos do edifício.
O nosso edifício pode ter alguns bilhões de andares, então podemos processar arrays com sub-arrays à vontade, estando limitados somente pela quantidade de memória disponível no computador.
Nosso edifício será uma variável – um array. O índice deste array corresponderá ao andar em que nos encontramos, então entenda
$building[$floor]
como sendo o edifício e o andar. Subir um andar:
$floor ++
descer:
$floor --
O processo de serialização será bem simples: em cada linha de um arquivo colocaremos uma chave e valor do array, como em:
'chave'='valor' 'outra_chave'='outro_valor'
O apóstrofo que envolve o nome da chave ou o valor nos permitirá escrever valores de múltiplas linhas, como em:
'chave'='um texto em múltiplas linhas'
Porém, se houver um apóstrofo dentro do valor, vamos duplicá-lo para que, durante o processo de reconstituição, possamos compreender que ali havia um apóstrofo escapado.
Chaves e valores que forem números inteiros não serão circundados com apóstrofo para que, durante a reconstituição, os valores sejam novamente convertidos para inteiros.
Se o valor de um item for um array, iremos abrir uma chave (“{“) e colocaremos os itens do subarray aí dentro. Ao terminar, fechamos com “}”. Assim,
'array'={
1='Estou aqui dentro'
}
Isto será a representação de
$array['array'][1] = 'Estou aqui dentro.';
Para reconstituir o array, o processo é semelhante: criamos um edifício e iniciamos no primeiro andar.
- Sempre que encontrarmos “{“, subiremos um andar ($floor ++) e passaremos a criar todos os itens ($key=$value) neste andar.
- Ao encontrar “}”, desceremos novamente para o nível anterior e copiaremos o array do nível superior como valor do item corrente.
Serializando os arrays com esta técnica, torna-se fácil criar e editar os arquivos de dados manualmente. Mas há ainda mais uma funcionalidade que pode nos ajudar quando quisermos editar os dados manualmente: trata-se de utilizar um “coringa” (“#”) para as chaves numéricas. Isto facilita a inserção e a remoção de itens. Para que as chaves numéricas sejam serializadas como coringas, chamamos a função array2string() com um segundo argumento “true“. Ao reconstituir, os coringas serão convertidos automaticamente para chaves numéricas.
Para testar, utilize os primeiros exemplos, substituindo serialize() por array2string() e unserialize() por string2array().
function array2string ($array, $use_counter=false)
{ // array to string
// Se nosso array não for de fato um array, abortaremos a operação
if (!is_array ($array)) return '';
// a string resultante
$string = '';
// o edifício
$building = array();
// o andar em que nos encontramos
$floor = 1;
// iniciaremos copiando o array para o primeiro andar do edifício
$building[$floor] = $array;
// nosso processo se dará em um loop "infinito"
while (1)
{ // loop
// capturaremos o item corrente do array no andar em que nos encontramos
// com each, após a captura, o ponteiro interno do array deverá avançar para o próximo item
$item = each ($building[$floor]);
// se o array tiver terminado, desceremos um andar
if ($item === false)
{ // end of array
unset ($building[$floor]);
$floor --;
// Se chegarmos ao "térreo", o processo terminou
if (!$floor) break;
// Se não terminou, imprime "}"
$string .= "}\r\n";
} // end of array
else
{ // array item
// capturamos a chave e o valor do item corrente
$key = $item[0];
$value = $item[1];
// imprimimos a chave do array
if (is_int ($key))
{ // int key
$use_counter ? $string .= '#' : $string .= $key;
} // int key
else
{ // string key
$string .= "'" . str_replace ("'", "''", $key) . "'";
} // string key
// Se o valor for um "subarray",
if (is_array ($value))
{ // value is array
// subimos um andar
$floor ++;
// copiamos o array deste valor para o andar em que nos encontramos
$building[$floor] = $value;
// Imprimimos "={"
$string .= "={\r\n";
} // value is array
// Caso o valor seja numérico
elseif (is_int ($value))
{ // numeric value
// imprimimos "=" e o valor sem os apóstrofos
$string .= '=' . strval($value) . "\r\n";
} // numeric value
// Ou consideraremos o valor como string
else
{ // string value
// imprimimos a string englobada por "'", e duplicamos os apóstrofos encontrados dentro dela
$string .= "='" . str_replace ("'", "''", $value) . "'\r\n";
} // string value
} // array item
} // loop
return $string;
} // array to string
function string2array ($string, $counter1=0)
{ // string to array
// Se a string não for de fato uma string, abortamos
if (!is_string ($string)) return array();
// acrescentamos à string recebida uma quebra de linha
$string .= "\n";
// o cumprimento total da string
$strlen = strlen ($string);
// o ponteiro que percorrerá a string
$pointer = 0;
// o edifício
$building = array ();
// o andar em que nos encontramos
$floor = 1;
$building[$floor] = array();
// O contador que substituirá os coringas encontrados
$counter[$floor] = $counter1;
// Sinalizadores
$buffer = '';
$buffer_enabled = false;
$buffer_string = false;
$key_enabled = false;
// iremos varrer a string até que o ponteiro alcance seu cumprimento total
while ($pointer < $strlen)
{ // get char
// capturamos o caractére da string que se encontra sob o ponteiro
$char = $string[$pointer];
// Verificamos que caractére é este
switch ($char)
{ // char
// ao encontrar uma quebra de linha, criamos um novo item de array
case "\n":
if (!$buffer_string) settype ($buffer, 'int');
if ($key_enabled)
{ // creates a value
$building[$floor][$key] = $buffer;
} // creates a value
$buffer = '';
$key_enabled = false;
$buffer_enabled = false;
$buffer_string = false;
break;
// ao encontrar "=", o que estiver no $buffer representa a chave de um item
case '=':
if ($buffer_enabled)
{ // creates key
if ($buffer == '#')
{ // counter
$counter[$floor] ++;
$buffer = $counter[$floor];
} // counter
elseif (!$buffer_string)
{ // numeric
settype ($buffer, 'int');
} // numeric
$key = $buffer;
$buffer = '';
$buffer_enabled = false;
$buffer_string = false;
$key_enabled = true;
} // creates key
break;
// ao encontrar "{", subiremos um andar
case '{':
if ($key_enabled)
{ // sublevel
$array_key[$floor] = $key;
$floor ++;
$building[$floor] = array();
$counter[$floor] = 0;
} // sublevel
$key = '';
$key_enabled = false;
$buffer = '';
$buffer_enabled = false;
$buffer_string = false;
break;
// ao encontrar "}", copiamos o array do andar corrente para o item atual do andar inferior e descemos um andar
case '}':
if ($floor <= 1) break 2;
$building[$floor - 1][$array_key[$floor - 1]] = $building[$floor];
$counter[$floor] = 0;
$floor --;
$buffer = '';
$buffer_enabled = false;
$buffer_string = false;
$key = '';
$key_enabled = false;
break;
// Ao encontrar "'", procuramos uma string com apóstrofos duplicados ("''")
case "'":
case '"':
loop_escape_char:
$n_close_string = strpos ($string, $char, $pointer + 1);
if ($n_close_string === false) break;
$buffer .= substr ($string, $pointer + 1, $n_close_string - ($pointer + 1));
$buffer_enabled = true;
$buffer_string = true;
$pointer = $n_close_string;
if ($string[$pointer + 1] != $char) break;
$buffer .= $char;
$pointer ++;
goto loop_escape_char;
// Caractéres ignorados
case "\r":
case "\t":
case ' ':
break;
// Ao encontrar "/" ou "<", seguiremos ignorando uma linha de comentário
case '<':
case '/':
$n_close_string = strpos ($string, "\n", $pointer + 1);
if ($n_close_string === false)
break;
else
{ // coment
$pointer = $n_close_string - 1;
} // coment
break;
// outros caractéres são guardados em $buffer
default:
$buffer .= $char;
$buffer_enabled = true;
break;
} // char
// avança o ponteiro para o próximo caractére
$pointer ++;
} // get char
return $building[1];
} // string to array