Salvar imagem no banco de dados com PHP/MySQL

Quando precisamos fazer o upload de uma imagem, geralmente temos duas opções:

  • 1 – Copiar a imagem em uma pasta no servidor e salvar apenas o caminho no banco de dados, como é feito nesse artigo.
  • 2 – Converter a imagem para binário e salvá-la diretamente no banco de dados.

O primeiro método é o mais comum, pois é mais fácil de ser implementado, além de ter melhor desempenho. Porém, em alguns casos torna-se necessário salvar a imagem diretamente no banco de dados, e assim recorremos ao segundo método. A desvantagem é que é um pouco mais trabalhoso de ser implementado e o desempenho é menor se comparado ao primeiro método, pois será necessário converter a imagem de binário para seu formato original toda vez que for necessário mostrá-la ao usuário.

Nesse artigo veremos como implementar o segundo método. Criaremos um cadastro de fotos sem contexto, apenas para demostrar a ideia, vamos nessa?

Confira o exemplo em funcionamento, clicando aqui.

Banco de dados

Primeiramente, vamos criar nossa tabela.

CREATE TABLE `fotos` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `nome` varchar(60) NOT NULL,
 `conteudo` mediumblob NOT NULL,
 `tipo` varchar(20) NOT NULL,
 `tamanho` int(10) unsigned NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB;
  • Conteúdo: campo do tipo BLOB (utilizado para armazenar informações de arquivos em formato binário) e será utilizado para armazenar a foto;
  • Tipo: será utilizado para armazenar o tipo do arquivo, pois no nosso exemplo será permitido fazer upload de diferentes tipos (jpg, gif, png, bmp) e no momento de exibir a imagem iremos utilizá-lo;
  • Tamanho: O tamanho do arquivo em bytes.

O formulário de cadastro

Em seguida, vamos criar o formulário para cadastrar uma foto.

<form id="formulario" action="ajax/salvar.php" method="post">

    <div id="mensagem"></div>

    <div class="form-group">
        <label>Foto</label>
        <input class="form-control" type="file" name="foto"/>
    </div>

    <input id="salvar" class="btn btn-primary" type="submit" value="Salvar" data-loading-text="Salvando..."/>

</form>

O lado cliente

Para ficar mais interessante, vamos fazer o envio desse formulário utilizando AJAX.

// Quando enviado o formulário
$('#formulario').on('submit', function () {

    // Armazenando objetos em variáveis para utilizá-los posteriormente
    var formulario = $(this);
    var botao = $('#salvar');
    var mensagem = $('#mensagem');

    // Exibindo indicador de carregamento (Bootstrap)
    // Docs: http://getbootstrap.com/javascript/#buttons
    botao.button('loading');

    // Enviando formulário
    $(this).ajaxSubmit({

        // Definindo tipo de retorno do servidor
        dataType: 'json',

        // Se a requisição foi um sucesso
        success: function (retorno) {

            // Se cadastrado com sucesso
            if (retorno.sucesso) {
                // Definindo estilo da mensagem (sucesso)
                mensagem.attr('class', 'alert alert-success');

                // Limpando formulário
                formulario.resetForm();
            }
            else {
                // Definindo estilo da mensagem (erro)
                mensagem.attr('class', 'alert alert-danger');
            }

            // Exibindo mensagem
            mensagem.html(retorno.mensagem);

            // Escondendo indicador de carregamento
            botao.button('reset');

        },

        // Se houver algum erro na requisição
        error: function () {

            // Definindo estilo da mensagem (erro)
            mensagem.attr('class', 'alert alert-danger');

            // Exibindo mensagem
            mensagem.html(retorno.mensagem);

            // Escondendo indicador de carregamento
            botao.button('reset');
        }

    });

    // Retorna FALSE para que o formulário não seja enviado de forma convencional
    return false;

});

Uma explicação mais detalhada:

  • Linha 5, 6 e 7: Apenas armazenamos os objetos da página para ficar mais fácil e rápido de utilizá-los posteriormente. Por exemplo, ao invés de utilizar $("#mensagem").attr(), utilizamos mensagem.attr();
  • Linha 11: Utilizando um recurso do Bootstrap, exibimos a mensagem de carregamento definida na propriedade data-loading-text do botão, para que assim o usuário saiba que a foto está sendo enviada;
  • Linha 14: Utilizando o JQuery Form Plugin, fazemos o envio do formulário;
  • Linha 20: Se a requisição foi recebida e processada pelo servidor com sucesso, esse método será chamado;
  • Linha 23: O servidor irá retornar um JSON com as propriedades sucesso e mensagem (veremos isso mais adiante). Portanto, verificamos se a propriedade sucesso é verdadeira, que significa que a foto foi salva com sucesso no banco de dados;
  • Linha 25 e 28: Utilizamos um estilo do Bootstrap para a mensagem e limpamos o formulário para que o usuário possa enviar outra imagem;
  • Linha 36 e 39: Já definido o estilo da mensagem (sucesso ou erro), exibimos a mensagem retornada pelo servidor e então escondemos o indicador de carregamento do botão, pois o processamento acabou;
  • Linha 44 até 54: Esse método somente será chamado caso ocorra algum erro no servidor. Definimos o estilo de erro, exibimos uma mensagem genérica de erro e escondemos o indicador de carregamento.

O lado servidor

Conexão

Arquivo responsável pela conexão com o banco de dados utilizando PDO.

<?php

// Informações para conexão
$host = 'localhost';
$usuario = 'root';
$senha = 'root';
$banco = 'rafaelcouto';
$dsn = "mysql:host={$host};port=3306;dbname={$banco}";

try 
{
    // Conectando
    $pdo = new PDO($dsn, $usuario, $senha);
} 
catch (PDOException $e) 
{
    // Se ocorrer algum erro na conexão
    die($e->getMessage());
}

Caso você esteja se perguntando porque de não utilizar a função mysql_connect, recomendo a leitura dessa página.

Utilidade

Nesse arquivo vamos apenas criar uma função para facilitar a mensagem de retorno da requisição AJAX.

<?php

// Função que retorno um JSON com a propriedade sucesso e mensagem
function retorno($mensagem, $sucesso = false)
{
    // Criando vetor com a propriedades
    $retorno = array();
    $retorno['sucesso'] = $sucesso;
    $retorno['mensagem'] = $mensagem;

    // Convertendo para JSON e retornando
    return json_encode($retorno);
}

Salvar

Em seguida, criaremos o arquivo PHP responsável por receber, validar e incluir a foto no banco de dados.

<?php
// Incluindo arquivo de conexão
require_once('../config/conn.php');

// Funções de utilidade
require_once('../funcs/util.php');

// Constantes
define('TAMANHO_MAXIMO', (2 * 1024 * 1024));

// Verificando se selecionou alguma imagem
if (!isset($_FILES['foto']))
{
    echo retorno('Selecione uma imagem');
    exit;
}

// Recupera os dados dos campos
$foto = $_FILES['foto'];
$nome = $foto['name'];
$tipo = $foto['type'];
$tamanho = $foto['size'];

// Validações básicas
// Formato
if(!preg_match('/^image\/(pjpeg|jpeg|png|gif|bmp)$/', $tipo))
{
    echo retorno('Isso não é uma imagem válida');
    exit;
}

// Tamanho
if ($tamanho > TAMANHO_MAXIMO)
{
    echo retorno('A imagem deve possuir no máximo 2 MB');
    exit;
}

// Transformando foto em dados (binário)
$conteudo = file_get_contents($foto['tmp_name']);

// Preparando comando
$stmt = $pdo->prepare('INSERT INTO fotos (nome, conteudo, tipo, tamanho) VALUES (:nome, :conteudo, :tipo, :tamanho)');

// Definindo parâmetros
$stmt->bindParam(':nome', $nome, PDO::PARAM_STR);
$stmt->bindParam(':conteudo', $conteudo, PDO::PARAM_LOB);
$stmt->bindParam(':tipo', $tipo, PDO::PARAM_STR);
$stmt->bindParam(':tamanho', $tamanho, PDO::PARAM_INT);

// Executando e exibindo resultado
echo ($stmt->execute()) ? retorno('Foto cadastrada com sucesso', true) : retorno($stmt->errorInfo());

Algumas observações:

  • Linha 9: Criamos uma constante com o tamanho máximo da foto em bytes, no caso 2 MB;
  • Linha 40: Através da função file_get_contents() pegamos o conteúdo do arquivo, no caso como é uma imagem, será retornado o conteúdo binário dela;
  • Linha 43 até 49: Montamos o nosso comando SQL e passamos as informações da foto nos parâmetros. Vale ressaltar que devemos utilizar o tipo PDO::PARAM_LOB para o conteúdo pois a coluna no banco de dados é do tipo BLOB;
  • Linha 52: Executamos o comando SQL e testamos o resultado com o operador ternário do PHP; se for executado com sucesso retornamos o JSON {"sucesso":true,"mensagem":"Foto cadastrada com sucesso"} com a ajuda da função retorno() que foi criada anteriormente, caso contrário será retornado a mensagem de erro. Vale ressaltar que em ambiente de produção o ideal seria registrar essa mensagem em um arquivo de log e retornar uma mensagem de erro genérica ao usuário;

Convertendo imagem

Com os procedimento acima já conseguimos incluir as fotos em nosso banco de dados. Agora precisamos mostrá-las ao usuário, para isso é necessário converter o conteúdo binário para arquivo. O seguinte script será responsável por isso.

<?php
// Incluindo arquivo de conexão
require_once('config/conn.php');

$id = (int) $_GET['id'];

// Selecionando fotos
$stmt = $pdo->prepare('SELECT conteudo, tipo FROM fotos WHERE id = :id');
$stmt->bindParam(':id', $id, PDO::PARAM_INT);

// Se executado
if ($stmt->execute())
{
    // Alocando foto
    $foto = $stmt->fetchObject();
    
    // Se existir
    if ($foto != null)
    {
        // Definindo tipo do retorno
        header('Content-Type: '. $foto->tipo);
        
        // Retornando conteudo
        echo $foto->conteudo;
    }
}

Em resumo, recuperamos o ID da foto que é passado pelo método GET e buscamos o conteúdo e tipo da foto no banco de dados. Se encontrado a foto, utiliza a função header() para informar ao navegador que a saída desse script vai ser o tipo de arquivo da foto. Por fim, retorna o conteúdo da foto.

Dessa forma, podemos mostrar a imagem simplesmente incluindo uma tag img passando o ID da imagem, por exemplo:

<img src="imagem.php?id=1" />

Consulta

Para finalizar, vamos fazer uma consulta que vai buscar as fotos no banco de dados e mostrar para o usuário.

<?php
// Incluindo arquivo de conexão
require_once('config/conn.php');

// Selecionando fotos
$stmt = $pdo->query('SELECT id, nome, tipo, tamanho FROM fotos');
?>
<div class="row">

        <?php while ($foto = $stmt->fetchObject()): ?>

            <div class="col-sm-6 col-md-4">

                <div class="thumbnail">

                    <img src="imagem.php?id=<?php echo $foto->id ?>" />

                    <div class="caption">
                        <strong>Nome:</strong> <?php echo $foto->nome ?> <br/>
                        <strong>Tipo:</strong> <?php echo $foto->tipo ?> <br/>
                        <strong>Tamanho:</strong> <?php echo $foto->tamanho ?> bytes <br/>
                    </div>

                </div>

            </div>

        <?php endwhile ?>

    </div>

Basicamente, selecionamos as fotos cadastradas no banco de dados, percorremos elas e utilizando o script imagem.php que criamos anteriormente carregamos cada foto. Para ficar com uma aparência melhor, utilizamos o recurso de Thumbnail do Bootstrap.

Conclusão

O objetivo deste artigo foi apresentar uma solução para quem necessita gravar fotos no banco de dados utilizando PHP e MySQL. É bom lembrar que em um ambiente de produção o ideal seria reduzir o tamanho da imagem caso seja muito grande e gerar uma miniatura também, mas isso é assunto para outro artigo.

Outro detalhe é que esse método é válido para qualquer tipo de arquivo, só muda a validação, pois o que importa na hora da conversão é o tipo do arquivo.

Particularmente não recomento esse método, pois como foi dito no inicio, o desempenho normalmente é menor e a quantidade de fotos impacta no tamanho do banco de dados. Porém em determinados casos é uma boa saída. Portanto, se essa for sua necessidade, espero que este artigo tenha sido útil para você.

Código fonte disponível em: https://github.com/rafaelcouto/salvar-imagem-no-banco-de-dados-com-php-mysql