Tutoriel Vidéo Laravel Création d'un système de fichiers attachés

Télécharger la vidéo Télécharger les sources

Dans ce tutoriel je vous propose de mettre en place un système de fichiers attachés pour vos contenus. Le principe est d'offrir à l'administrateur du site la possibilité d'associer une ou plusieurs images à un article.

Sommaire

  • 00:00 - Présentation du projet
  • 02:05 - Initialisation du projet
  • 04:25 - Découverte de la relation Polymorphique
  • 11:34 - TDD, Test-driven development
  • 53:03 - Intégration avec TinyMCE
  • 1:03:15 - Intégration avec Trumbowyg
  • 1:10:35 - Intégration avec Alloy Editor
  • 1:16:36 - Gestion de la suppression de contenu

Structure

Avant de se lancer dans le code il est important d'avoir en tête le fonctionnement général de notre système. Pour gérer les fichiers attachés nous allons utiliser une relation polymorphique. Ce type de relation permet de créer une liaison multi-table (ce qui permettra d'attacher une image à plusieurs types de contenus). Du coup la table aura la structure suivante :

Schema::create('attachments', function (Blueprint $table) {
  $table->increments('id');
  $table->string('name');
  $table->string('attachable_type');
  $table->integer('attachable_id')->unsigned();
  $table->timestamps();
});

Ce qui donnera :

attachable_type attachable_id name
App\Post 3 demo.jpg
App\Tutoriel 2 php.jpg
App\Post 4 demo2.jpg

Feature Testing

Afin de s'assurer que tout fonctionne comme attendu nous allons décrire les différents comportements attendus à travers des tests fonctionnels. Vous pouvez utiliser ces tests pour essayer d'écrire le code par vous même avant d'attaquer la vidéo.

<?php

namespace Tests\Feature;

use App\Attachment;
use App\Post;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class AttachmentTest extends TestCase {

    public function setUp() {
        parent::setUp();
        Artisan::call('migrate');
        $this->cleanDirectories();
    }

    public function tearDown() {
        parent::tearDown();
        $this->cleanDirectories();
    }

    public function cleanDirectories () {
        Storage::disk('public')->deleteDirectory('uploads');
    }

    public function getFileForAttachment($attachment) {
        return dirname(__DIR__) . '/fixtures/uploads/' . $attachment['name'];
    }

    private function callController($data = []) {
        $path = dirname(__DIR__) . '/fixtures/demo.jpg';
        $file = new UploadedFile($path, 'demo.jpg', filesize($path), 'image/jpeg', null, true);
        $post = Post::create(['name' => 'demo', 'content' => 'demo']);
        $default = [
            'attachable_type' => Post::class,
            'attachable_id' => $post->id,
            'image' => $file
        ];
        return $this->post(route('attachments.store'), array_merge($default, $data));
    }

    public function testIncorrectDataAttachbleType () {
        $response = $this->callController(['attachable_type' => 'POOO']);
        $response->assertJsonStructure(['attachable_type']);
        $response->assertStatus(422);
    }

    public function testIncorrectDataAttachbleId () {
        $response = $this->callController(['attachable_id' => 3]);
        $response->assertJsonStructure(['attachable_id']);
        $response->assertStatus(422);
    }

    public function testCorrectData () {
        $response = $this->callController();
        $attachment = $response->json();
        $this->assertFileExists($this->getFileForAttachment($attachment));
        $response->assertJsonStructure(['id', 'url']);
        $this->assertContains('/uploads/', $attachment['url']);
        $response->assertStatus(200);
    }

    public function testDeleteAttachmentDeleteFile() {
        $response = $this->callController();
        $attachment = $response->json();
        $this->assertFileExists($this->getFileForAttachment($attachment));
        Attachment::find($attachment['id'])->delete();
        $this->assertFileNotExists($this->getFileForAttachment($attachment));
    }

    public function testDeletePostDeleteAllAttachments() {
        $response = $this->callController();
        $attachment = $response->json();
        factory(Attachment::class, 3)->create();
        $this->assertFileExists($this->getFileForAttachment($attachment));
        $this->assertEquals(4, Attachment::count());
        Post::first()->delete();
        $this->assertFileNotExists($this->getFileForAttachment($attachment));
        $this->assertEquals(3, Attachment::count());
    }

    public function testChangePostContentAttachmentsAreDeleted() {
        $response = $this->callController();
        $attachment = $response->json();
        factory(Attachment::class, 3)->create();
        $this->assertFileExists($this->getFileForAttachment($attachment));
        $this->assertEquals(4, Attachment::count());
        $post = Post::first();
        $post->content = "<img src=\"#{$attachment['url']}\"/> balbalbabla";
        $post->save();
        $this->assertEquals(4, Attachment::count());
        $post->content = "";
        $post->save();
        $this->assertEquals(3, Attachment::count());
        $this->assertFileNotExists($this->getFileForAttachment($attachment));
    }

    public function testChangePostContentAttachmentsAreDeletedIfImageChanged() {
        $response = $this->callController();
        $attachment = $response->json();
        factory(Attachment::class, 3)->create();
        $this->assertFileExists($this->getFileForAttachment($attachment));
        $this->assertEquals(4, Attachment::count());
        $post = Post::first();
        $post->content = "<img src=\"#{$attachment['url']}\"/> balbalbabla";
        $post->save();
        $this->assertEquals(4, Attachment::count());
        $post->content = "<img src=\"azeeaze/eazeazeazeaz/azeezaze.jpg\"/>";
        $post->save();
        $this->assertEquals(3, Attachment::count());
        $this->assertFileNotExists($this->getFileForAttachment($attachment));
    }

}