Laravel é um framework que tem se tornado bastante popular no mundo do PHP. Uma das vantagens dele sobre outros frameworks é que ele permite que seu código fique limpo e fluído, especialmente pela forma como ele trata as dependências e outras ajudas. Mas isso é assunto para outro momento. Agora, queria falar um pouco sobre a manipulação de arquivos carregados via upload.
Para começar, queria compartilhar uma situação hipotética: por causa da lógica de negócio por trás do projeto que você está trabalhando, é necessário garantir que arquivos duplicados não sejam carregados via upload. Usar o nome do arquivo nem sempre é a melhor forma de fazer isso porque o usuário pode simplesmente usar outro nome. Então quero compartilhar uma forma de fazer isso usando Laravel.
A documentação do framework não é muito clara a respeito da classe que representa um arquivo carregado. Procurando no código-fonte, porém, é possível encontrar evidências de que o objeto retornado pelo método $request->file()
é uma instância da classe SplFileInfo
. Isso significa que é possível manipular o arquivo sem grandes complicações. E o melhor: é possível fazer isso antes mesmo de gravar o arquivo no destino final.
Você pode encontrar essa evidência da SplFileInfo
na classe File:
class File extends \SplFileInfo {
Essa referência a uma classe do Symfony pode parecer estranha se você não tem familiriadade com a arquitetura do Laravel, mas ele usa vários componentes do Symfony internamente.
Bom, vamos ver como é possível fazer a manipulação na prática!
Resolvendo um upload
Uma action (método de um controller) simples para upload de arquivo geralmente pega a instância do arquivo do request, verifica se um arquivo foi enviado e grava ele no destino final. Provavelmente algo parecido com isso:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class FileController extends Controller { /** * Resolve o envio do arquivo. * * @param Request $request A instância do request. * @return Response A instância da response. */ public function upload(Request $request) { /* * O campo do form com o arquivo tinha o atributo name="file". */ $file = $request->file('file'); if (empty($file)) { abort(400, 'Nenhum arquivo foi enviado.'); } $path = $file->store('uploads'); // Faça qualquer coisa com o arquivo enviado... } }
Lendo e comparando o arquivo em partes
Usando aquela instância do arquivo que pegamos do request, podemos escrever um método que compara ela com uma outra instância da classe SplFileInfo
. Para agilizar o processamento, você não precisa comparar os arquivos inteiros. Ir por partes é a melhor estratégia. Você pode ler um pedaço do mesmo tamanho de ambos os arquivos e compará-los. Assim que uma diferença for encontrada, pare a execução do método.
/** * Compara o conteúdo de dois arquivos para verificar se há diferenças. * * Não verifica qual exatamente é a diferença entre os arquivos. Dessa * forma, uma diferenção é encontrada, a função para de ler os arquivos. * * @param SplFileInfo $a O primeiro arquivo para comparar. * @param SplFileInfo $b O segundo arquivo para comparar. * @return bool Indica se há qualquer diferença entre os arquivos. */ private function fileDiff($a, $b) { $diff = false; $fa = $a->openFile(); $fb = $b->openFile(); /* * Lê o mesmo número de bytes de cada arquivo. Quebra (break) o loop * assim que uma diferença for encontrada. */ while (!$fa->eof() && !$fb->eof()) { if ($fa->fread(4096) !== $fb->fread(4096)) { $diff = true; break; } } /* * Apenas um dos arquivos chegou ao fim. */ if ($fa->eof() !== $fb->eof()) { $diff = true; } /* * Closing handlers. */ $fa = null; $fb = null; return $diff; }
Melhorando a performance
A função que escrevemos acima é uma ótima maneira de comparar dois arquivos. Porém, abrir, ler e comparar o conteúdo de cada arquivo enviado anteriormente para a aplicação pode consumir muitos recursos (tempo, I/O, processamento). Podemos reduzir significamente esse consumo comparando o tamanho dos arquivos primeiro, e então apenas abrir e comparar o conteúdo dos arquivos que tiverem o mesmo tamanho. Isso ficaria assim:
/** * Verifica se o arquivo passado já foi enviado antes. * * Passa de arquivo em arquivo no diretório de uploads, conferindo se algum * pode ser igual ao arquivo sendo enviado. * * @param SplFileInfo $file O arquivo para verificar. * @return bool Indica se arquivo já foi enviando antes. */ private function isAlreadyUploaded($file) { $size = $file->getSize(); /* * O arquivo onde os arquivos são gravados. */ $path = storage_path('app/uploads/'); if (!is_dir($path)) { return false; } $files = scandir($path); foreach ($files as $f) { $filePath = $path . $f; if (!is_file($filePath)) { continue; } /* * Se ambos os arquivos tiverem o mesmo tamanho, compara o conteúdo. */ if (filesize($filePath) === $size) { /* * Verifica se há alguma diferença, usando a função que escrevemos * acima. */ $diff = $this->fileDiff(new \SplFileInfo($filePath), $file); /* * Retorna se os arquivos **não** são diferentes, ou seja, iguais. * Isso significa que já foi enviado antes. */ return !$diff; } } return false; }
Gravando o arquivo
Voltando à action que definimos no começo e usando esse último método que escrevemos, podemos finalmente gravar o arquivo. É importante lembrar de chamar o método de gravação (store
) apenas depois de ter verificado se o arquivo já não existe.
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class FileController extends Controller { /** * Resolve o envio do arquivo. * * @param Request $request A instância do request. * @return Response A instância da response. */ public function upload(Request $request) { /* * O campo do form com o arquivo tinha o atributo name="file". */ $file = $request->file('file'); if (empty($file)) { abort(400, 'Nenhum arquivo foi enviado.'); } /* * Já existe um arquivo igual ao que está sendo enviado? */ if ($this->isAlreadyUploaded($file)) { abort(400, 'Esse mesmo arquivo já foi enviado antes.'); } /* * Apenas grava o arquivo depois da verificação. */ $path = $file->store('uploads'); // Faça qualquer coisa com o arquivo enviado... } }
Conclusão
Essa é apenas uma maneira de trabalhar com o arquivo retornado pelo request, pensando no que ele é internamente. Existem provavelmente outras mil maneiras de utilizar isso para resolver outros problemas ou ao menos tornar seu código mais fluído.
Também é importante entender que essa é apenas uma forma de fazer isso usando o framework. Você provavelmente pode usar outras táticas para resolver o mesmo problema. Por exemplo, seria possível armazenar o arquivo enviado em um diretório temporário e então fazer as comparações necessárias. Ou você pode aproveitar a lógica descrita aqui e usar as funções nativas do PHP para upload ao invés das funções do Laravel.