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
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 sectionsmode- Mode actuel ('edit' | 'preview')titleBuilder- Titre du builder
Méthodes principales
addSection($columns)- Ajouter une sectiontoggleMode()- Basculer entre édition et prévisualisationsyncOrderWithSections()- 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 blocksloadBlocks($model)- Charger les blocksblockToArray($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
•
$model - Le modèle parent (Post, Product, etc.)•
$sections - Les sections depuis le BuilderRetour : void
loadBlocks(Model $model)
Charge les blocks depuis la base de données vers le format du Builder
•
$model - Le modèle parentRetour : array (sections formatées pour le Builder)
blockToArray(Block $block, ?string $uuid)
Convertit un Block en tableau pour le Builder
•
$block - Le block à convertir•
$uuid - UUID personnalisé (optionnel)Retour : array (format Builder)
Builder API
Méthodes publiques
addSection($columns)- Ajouter une sectiontoggleMode()- Basculer le modesyncOrderWithSections()- 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()- MorphToparent()- BelongsTochildren()- HasMany
Scopes
rootBlocks()- Sections racinesofType($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:keyuniques - • 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_tableest 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);
});