Vue d'ensemble

Block Builder

Système complet de gestion de blocks construit avec Livewire et Filament. Permet de créer, gérer et organiser des blocks de manière flexible et réutilisable avec une interface drag & drop intuitive.

Fonctionnalités principales

Interface drag & drop
Sections multi-colonnes
Blocks personnalisables
Sauvegarde en base de données
Mode édition et prévisualisation
Gestion des médias

Architecture

Structure des fichiers

app/
├── Models/
│   └── Block.php                    # Modèle principal des blocks
├── Enums/
│   └── BlockType.php               # Types de blocks disponibles
├── Services/BlockBuilder/
│   └── BlockManager.php            # Service de gestion des blocks
├── Livewire/BlockBuilder/
│   ├── Builder.php                  # Composant principal du builder
│   ├── Render.php                   # Composant de rendu
│   └── Blocks/                     # Composants de blocks individuels
│       ├── Heading.php
│       ├── Text.php
│       ├── Image.php
│       └── Layout.php

resources/views/livewire/block-builder/
├── builder.blade.php               # Vue principale du builder
├── render.blade.php                # Vue de rendu
└── blocks/                         # Vues des blocks individuels
    ├── heading.blade.php
    ├── text.blade.php
    ├── image.blade.php
    └── layout.blade.php

database/migrations/
└── create_blocks_table.php         # Migration de la table blocks

Flux de données

Builder

Interface

BlockManager

Service

Base de données

Stockage

Composants

Builder (Composant principal)

Le composant principal qui gère l'interface d'édition des blocks.

Propriétés principales

  • sections - Array des sections (map: id => section)
  • order - Ordre d'affichage des sections
  • mode - Mode actuel ('edit' | 'preview')
  • titleBuilder - Titre du builder

Méthodes principales

  • addSection($columns) - Ajouter une section
  • toggleMode() - Basculer entre édition et prévisualisation
  • syncOrderWithSections() - Synchroniser l'ordre

Render (Composant de rendu)

Composant pour afficher les blocks en mode lecture seule.

Utilisation

<!-- Charger depuis un modèle -->
<livewire:block-builder.render
    :modelId="$post->id"
    :modelType="App\Models\Post::class"
/>

<!-- Ou passer les sections directement -->
<livewire:block-builder.render
    :sections="$sections"
    :order="$order"
/>

BlockManager (Service)

Service central pour gérer la sauvegarde et le chargement des blocks.

Méthodes principales

  • saveBlocks($model, $sections) - Sauvegarder les blocks
  • loadBlocks($model) - Charger les blocks
  • blockToArray($block, $uuid) - Convertir un block en array

Types de blocks

Types disponibles

SECTION

Conteneur de layout avec colonnes

  • • Support multi-colonnes (1-3)
  • • Contient d'autres blocks
  • • Gestion de l'ordre

HEADING

Titre avec personnalisation avancée

  • • Niveaux H1-H6
  • • Sous-titre optionnel
  • • Couleurs personnalisées
  • • Classes CSS personnalisées

TEXT

Zone de texte riche

  • • Éditeur WYSIWYG
  • • Formatage avancé
  • • Classes CSS personnalisées

IMAGE

Gestion d'images avec MediaLibrary

  • • Upload multiple
  • • Médias privés/publics
  • • Redimensionnement
  • • Alt text et légendes

Créer des blocks

1. Ajouter un nouveau type dans l'enum

// app/Enums/BlockType.php
enum BlockType: string
{
    case SECTION = 'SECTION';
    case HEADING = 'HEADING';
    case TEXT = 'TEXT';
    case IMAGE = 'IMAGE';
    case VIDEO = 'VIDEO'; // Nouveau type

    public function label(): string
    {
        return match ($this) {
            self::SECTION => 'Section',
            self::HEADING => 'Titre',
            self::TEXT => 'Zone de texte',
            self::IMAGE => 'Image',
            self::VIDEO => 'Vidéo', // Nouveau label
        };
    }
}

2. Créer le composant Livewire

// app/Livewire/BlockBuilder/Blocks/Video.php
<?php

namespace App\Livewire\BlockBuilder\Blocks;

use Livewire\Attributes\Modelable;
use Livewire\Component;

class Video extends Component
{
    #[Modelable]
    public array $block = [];

    public function mount(): void
    {
        // Initialiser les valeurs par défaut
        $this->block['data']['url'] = $this->block['data']['url'] ?? '';
        $this->block['data']['title'] = $this->block['data']['title'] ?? '';
        $this->block['data']['autoplay'] = $this->block['data']['autoplay'] ?? false;
        $this->block['data']['controls'] = $this->block['data']['controls'] ?? true;
    }

    public function render()
    {
        return view('livewire.block-builder.blocks.video');
    }
}

3. Créer la vue Blade

<!-- resources/views/livewire/block-builder/blocks/video.blade.php -->
<div class="block-video">
    <!-- Mode édition -->
    @if($mode === 'edit')
        <div class="p-4 border border-gray-300 rounded-lg">
            <h4 class="font-semibold mb-3">Configuration Vidéo</h4>

            <div class="space-y-4">
                <div>
                    <label class="block text-sm font-medium mb-1">URL de la vidéo</label>
                    <input type="url"
                           wire:model="block.data.url"
                           class="w-full px-3 py-2 border rounded-md">
                </div>

                <div>
                    <label class="block text-sm font-medium mb-1">Titre</label>
                    <input type="text"
                           wire:model="block.data.title"
                           class="w-full px-3 py-2 border rounded-md">
                </div>

                <div class="flex items-center space-x-4">
                    <label class="flex items-center">
                        <input type="checkbox"
                               wire:model="block.data.autoplay">
                        <span class="ml-2 text-sm">Lecture automatique</span>
                    </label>

                    <label class="flex items-center">
                        <input type="checkbox"
                               wire:model="block.data.controls">
                        <span class="ml-2 text-sm">Contrôles</span>
                    </label>
                </div>
            </div>
        </div>
    @else
        <!-- Mode rendu -->
        @if($block['data']['url'])
            <div class="video-container">
                @if($block['data']['title'])
                    <h3 class="text-lg font-semibold mb-2"></h3>
                @endif

                <video
                    src=""
                    @if($block['data']['autoplay']) autoplay @endif
                    @if($block['data']['controls']) controls @endif
                    class="w-full rounded-lg">
                    Votre navigateur ne supporte pas la lecture vidéo.
                </video>
            </div>
        @endif
    @endif
</div>

4. Enregistrer dans le Builder

// Dans resources/views/livewire/block-builder/builder.blade.php
// Ajouter le nouveau type dans la liste des blocks disponibles

<div class="block-types">
    <!-- Types existants -->
    <div class="block-type" data-type="heading">Titre</div>
    <div class="block-type" data-type="text">Texte</div>
    <div class="block-type" data-type="image">Image</div>

    <!-- Nouveau type -->
    <div class="block-type" data-type="video">Vidéo</div>
</div>

Rendu

Utilisation basique

<!-- Dans une vue Blade -->
<livewire:block-builder.render
    :modelId="$post->id"
    :modelType="App\Models\Post::class"
/>

Avec sections personnalisées

<!-- Passer des sections directement -->
<livewire:block-builder.render
    :sections="$customSections"
    :order="$customOrder"
/>

Dans un contrôleur

// Dans un contrôleur
use App\Services\BlockBuilder\BlockManager;

public function show(Post $post)
{
    $blockManager = app(BlockManager::class);
    $sections = $blockManager->loadBlocks($post);

    return view('posts.show', compact('post', 'sections'));
}

Personnalisation

Styles CSS personnalisés

/* Dans votre CSS */
.block-builder {
    /* Styles pour le builder */
}

.block-builder .section {
    /* Styles pour les sections */
}

.block-builder .block {
    /* Styles pour les blocks individuels */
}

/* Styles spécifiques par type */
.block-heading h1, .block-heading h2, .block-heading h3 {
    /* Styles pour les titres */
}

.block-text {
    /* Styles pour les zones de texte */
}

.block-image img {
    /* Styles pour les images */
}

Classes CSS dynamiques

<!-- Dans une vue de block -->
<div class="block-heading ">
    <h
       class=""
       style="color: ">
            >

        @if($block['data']['subtitle'])
        <h
           class=""
           style="color: ">
                >
    @endif
</div>

Configuration avancée

// Créer un fichier de configuration
// config/block-builder.php

return [
    'defaults' => [
        'heading' => [
            'level' => 2,
            'size' => 'xl',
            'color' => 'none',
        ],
        'text' => [
            'class' => 'prose prose-lg',
        ],
        'image' => [
            'max_files' => 5,
            'allowed_types' => ['image/jpeg', 'image/png', 'image/webp'],
        ],
    ],

    'styles' => [
        'heading_sizes' => [
            'sm' => 'text-lg',
            'md' => 'text-xl',
            'lg' => 'text-2xl',
            'xl' => 'text-3xl',
            '2xl' => 'text-4xl',
        ],
        'colors' => [
            'primary' => '#3B82F6',
            'secondary' => '#6B7280',
            'success' => '#10B981',
            'warning' => '#F59E0B',
            'danger' => '#EF4444',
        ],
    ],
];

Base de données

Structure de la table blocks

Champ Type Description
id bigint Identifiant unique
blockable_id bigint ID du modèle parent
blockable_type string Type du modèle parent
type string Type de block (SECTION, HEADING, etc.)
block_name string Nom personnalisé du block
data json Données du block
parent_id bigint nullable ID du block parent (pour les enfants)
order int Ordre d'affichage
column_index tinyint nullable Index de colonne

Relations

blockable() - MorphTo

Relation polymorphique vers le modèle parent (Post, Product, etc.)

parent() - BelongsTo

Relation vers le block parent (pour les blocks enfants)

children() - HasMany

Relation vers les blocks enfants

Trait HasBlocks

// Dans votre modèle
use App\Models\Concerns\HasBlocks;

class Post extends Model
{
    use HasBlocks;

    // Le trait fournit automatiquement :
    // - blocks(): MorphMany - Tous les blocks
    // - rootBlocks(): MorphMany - Sections racines uniquement
    // - hasBlocks(): bool - Vérifie si le modèle a des blocks
}

API Reference

BlockManager API

saveBlocks(Model $model, array $sections)

Sauvegarde les sections du builder vers la base de données

Paramètres :
$model - Le modèle parent (Post, Product, etc.)
$sections - Les sections depuis le Builder
Retour : void

loadBlocks(Model $model)

Charge les blocks depuis la base de données vers le format du Builder

Paramètres :
$model - Le modèle parent
Retour : array (sections formatées pour le Builder)

blockToArray(Block $block, ?string $uuid)

Convertit un Block en tableau pour le Builder

Paramètres :
$block - Le block à convertir
$uuid - UUID personnalisé (optionnel)
Retour : array (format Builder)

Builder API

Méthodes publiques

  • addSection($columns) - Ajouter une section
  • toggleMode() - Basculer le mode
  • syncOrderWithSections() - Synchroniser l'ordre

Propriétés publiques

  • $sections - Array des sections
  • $order - Ordre d'affichage
  • $mode - Mode actuel
  • $titleBuilder - Titre du builder

Block Model API

Relations

  • blockable() - MorphTo
  • parent() - BelongsTo
  • children() - HasMany

Scopes

  • rootBlocks() - Sections racines
  • ofType($type) - Par type

Exemples pratiques

Exemple 1 : Page d'article avec blocks

<!-- resources/views/posts/show.blade.php -->
<div class="max-w-4xl mx-auto py-8">
    <h1 class="text-3xl font-bold mb-4">title ); ?></h1>

    <!-- Rendu des blocks -->
    <livewire:block-builder.render
        :modelId="$post->id"
        :modelType="App\Models\Post::class"
    />
</div>

Exemple 2 : Création d'un article avec builder

<!-- resources/views/posts/create.blade.php -->
<form wire:submit="save">
    <div class="mb-6">
        <label class="block text-sm font-medium mb-2">Titre</label>
        <input type="text" wire:model="title" class="w-full px-3 py-2 border rounded-md">
    </div>

    <!-- Builder de blocks -->
    <livewire:block-builder.builder
        wire:model="sections"
        :titleBuilder="'Contenu de l\'article'"
    />

    <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">
        Créer l'article
    </button>
</form>

Exemple 3 : Création programmatique de blocks

// Dans un contrôleur ou service
use App\Services\BlockBuilder\BlockManager;
use Illuminate\Support\Str;

public function createDefaultContent(Post $post)
{
    $blockManager = app(BlockManager::class);

    $sections = [
        Str::uuid()->toString() => [
            'id' => Str::uuid()->toString(),
            'type' => 'layout',
            'blockName' => 'Section Introduction',
            'data' => [
                'columns' => 1,
                'widths' => [1],
                'version' => 0,
                'children' => [
                    [
                        'heading-1' => [
                            'id' => 'heading-1',
                            'type' => 'heading',
                            'blockName' => 'Titre',
                            'data' => [
                                'text' => '<h2>Bienvenue</h2>',
                                'level' => 2,
                            ],
                        ],
                        'text-1' => [
                            'id' => 'text-1',
                            'type' => 'text',
                            'blockName' => 'Introduction',
                            'data' => [
                                'text' => '<p>Contenu par défaut de l\'article.</p>',
                            ],
                        ],
                    ],
                ],
                'orders' => [
                    ['heading-1', 'text-1'],
                ],
            ],
        ],
    ];

    $blockManager->saveBlocks($post, $sections);
}

Exemple 4 : Block personnalisé avec validation

// app/Livewire/BlockBuilder/Blocks/CustomForm.php
<?php

namespace App\Livewire\BlockBuilder\Blocks;

use Livewire\Attributes\Modelable;
use Livewire\Component;
use Livewire\Attributes\Rule;

class CustomForm extends Component
{
    #[Modelable]
    public array $block = [];

    #[Rule('required|email')]
    public string $email = '';

    #[Rule('required|min:3')]
    public string $name = '';

    public function mount(): void
    {
        $this->email = $this->block['data']['email'] ?? '';
        $this->name = $this->block['data']['name'] ?? '';
    }

    public function save()
    {
        $this->validate();

        $this->block['data']['email'] = $this->email;
        $this->block['data']['name'] = $this->name;

        $this->dispatch('block-saved', $this->block);
    }

    public function render()
    {
        return view('livewire.block-builder.blocks.custom-form');
    }
}

Dépannage

Problèmes courants

Erreur "Snapshot missing"

Cette erreur survient avec les composants Livewire imbriqués :

  • • Utilisez des UUIDs uniques pour les IDs de blocks
  • • Ajoutez des wire:key uniques
  • • Vérifiez que les composants enfants ont des IDs stables

Blocks non sauvegardés

Si les blocks ne se sauvegardent pas :

  • • Vérifiez que le modèle utilise le trait HasBlocks
  • • Assurez-vous que la migration create_blocks_table est exécutée
  • • Vérifiez les logs pour les erreurs de sauvegarde

Images non affichées

Si les images ne s'affichent pas :

  • • Vérifiez que Spatie MediaLibrary est configuré
  • • Assurez-vous que les routes de médias sont définies
  • • Vérifiez les permissions de fichiers

Bonnes pratiques

  • • Utilisez des UUIDs pour les IDs de blocks dans le Builder
  • • Testez vos nouveaux types de blocks avec des données variées
  • • Utilisez des classes CSS cohérentes pour le styling
  • • Validez les données dans les composants de blocks
  • • Documentez vos blocks personnalisés

Tests recommandés

// Test de création de blocks
it('can create blocks programmatically', function () {
    $post = Post::factory()->create();
    $blockManager = app(BlockManager::class);

    $sections = [
        'section-1' => [
            'id' => 'section-1',
            'type' => 'layout',
            'blockName' => 'Test Section',
            'data' => [
                'columns' => 1,
                'widths' => [1],
                'version' => 0,
                'children' => [],
                'orders' => [[]],
            ],
        ],
    ];

    $blockManager->saveBlocks($post, $sections);

    expect($post->blocks()->count())->toBe(1);
});

// Test de rendu des blocks
it('can render blocks correctly', function () {
    $post = Post::factory()->create();

    Livewire::test(BlockBuilder\Render::class, [
        'modelId' => $post->id,
        'modelType' => Post::class,
    ])->assertStatus(200);
});