# Guide développeur : Création de champs ACF personnalisés

## 🎯 Principes de base

Pour créer un champ ACF custom qui **sauvegarde correctement** en base de
données, suivez ces règles essentielles :

### 1. Structure de base

```php
class MonChampCustom extends \acf_field {

    public function __construct() {
        $this->name     = 'mon_champ_custom';
        $this->label    = __('Mon Champ Custom', 'pilo-blocks');
        $this->category = 'Pilo\'Blocks';

        parent::__construct();
    }

    public function render_field( $field ) {
        // Voir section suivante
    }

    public function update_value( $value, $post_id, $field ) {
        // Nettoyer et valider les données
        return $value;
    }
}
```

## ⚠️ RÈGLE CRITIQUE : Sous-champs avec prefix

### ❌ INCORRECT (ne sauvegarde pas)

```php
public function render_field( $field ) {
    $value = $field['value'];

    // ❌ Concaténer le name avec des crochets
    $link_field = array(
        'type'  => 'acfe_advanced_link',
        'name'  => $field['name'] . '[link]',  // ERREUR
        'key'   => $field['key'] . '_link',
        'value' => $value['link'] ?? array(),
    );

    acf_render_field( $link_field );
}
```

**Problème** : Les sous-champs du champ Advanced Link génèrent des `name`
incorrects et ACF ne peut pas parser les données POST.

### ✅ CORRECT (sauvegarde automatiquement)

```php
public function render_field( $field ) {
    $value = is_array( $field['value'] ) ? $field['value'] : array();

    echo '<div class="mon-champ-wrapper">';

    // ✅ ÉTAPE 1 : Hidden input parent (OBLIGATOIRE)
    acf_hidden_input(array('name' => $field['name']));

    // ✅ ÉTAPE 2 : Sous-champs avec 'prefix' au lieu de concaténer 'name'
    $link_field = array(
        'type'   => 'acfe_advanced_link',
        'name'   => 'link',              // ✅ Simple, sans concaténation
        'key'    => $field['key'] . '_link',
        'prefix' => $field['name'],      // ✅ Le prefix gère le nested array
        'value'  => $value['link'] ?? array(),
    );

    acf_render_field_wrap( $link_field );

    echo '</div>';
}
```

## 📋 Checklist pour chaque sous-champ

```php
$sous_champ = array(
    'name'   => 'nom_simple',           // ✅ PAS de $field['name'] . '[...]'
    'key'    => $field['key'] . '_suffix',
    'prefix' => $field['name'],         // ✅ OBLIGATOIRE
    'value'  => $value['nom_simple'] ?? $default,
    'type'   => 'text',
    // ... autres propriétés
);

// Utiliser acf_render_field_wrap() pour un wrapper complet
acf_render_field_wrap( $sous_champ );

// OU acf_render_field() pour juste l'input
acf_render_field( $sous_champ );
```

## 🏗️ Structure HTML générée

### Avec la méthode correcte :

```html
<!-- Hidden input parent -->
<input type="hidden" name="acf[field_123]" value="" />

<!-- Sous-champs -->
<input type="text" name="acf[field_123][link][url]" value="https://..." />
<select name="acf[field_123][btn_type]">
  ...
</select>
<input type="text" name="acf[field_123][icon]" value="arrow-right" />
```

### Ce que reçoit PHP :

```php
$_POST['acf']['field_123'] = [
    'link' => [
        'url'    => 'https://...',
        'title'  => '...',
        'target' => false
    ],
    'btn_type' => 'btn-primary',
    'icon'     => 'arrow-right'
];
```

## 🔧 Nested arrays complexes (3+ niveaux)

Pour des structures comme `[query][post_type]` :

```php
$post_type_field = array(
    'type'   => 'select',
    'name'   => 'post_type',              // ✅ Juste le dernier niveau
    'key'    => $field['key'] . '_post_type',
    'prefix' => $field['name'] . '[query]', // ✅ Prefix inclut les parents
    'value'  => $value['query']['post_type'] ?? 'post',
    'choices' => $post_types,
);
```

Génère automatiquement : `name="acf[field_123][query][post_type]"`

## 🎨 update_value() et format_value()

### update_value() - Nettoyer avant sauvegarde

> ⚠️ **CRITIQUE** : ACF préfixe automatiquement les clés des sous-champs avec
> `$field['key'] . '_'` quand ils utilisent le système de prefix !

```php
public function update_value( $value, $post_id, $field ) {

    // S'assurer que c'est un tableau
    if ( !is_array( $value ) ) {
        return array();
    }

    // ✅ OBLIGATOIRE : Utiliser le préfixe de key pour extraire les valeurs
    $key_prefix = $field['key'] . '_';

    // ❌ INCORRECT : Chercher par nom simple
    // $clean_value = array(
    //     'link' => $value['link'] ?? array(),  // Ne trouvera JAMAIS la valeur !
    // );

    // ✅ CORRECT : Chercher avec le préfixe de key
    $clean_value = array(
        'link'     => $value[$key_prefix . 'link'] ?? array(),
        'btn_type' => sanitize_text_field( $value[$key_prefix . 'btn_type'] ?? 'btn-primary' ),
        'icon'     => sanitize_text_field( $value[$key_prefix . 'icon'] ?? '' ),
    );

    // ✅ Retourner empty string si vide (ne pas polluer la DB)
    if ( empty( $clean_value['link'] ) && empty( $clean_value['btn_type'] ) ) {
        return '';
    }

    return $clean_value;
}
```

**Pourquoi ce préfixe ?**

Dans `render_field()`, vous créez des sous-champs avec :

```php
$btn_type_field = array(
    'name'   => 'btn_type',
    'key'    => $field['key'] . '_btn_type',  // 'field_691b08af089e9_btn_type'
    'prefix' => $field['name'],
);
```

ACF soumet alors le formulaire avec les clés incluant le key complet :

```php
$_POST['acf']['field_691b08af089e9'] = array(
    'field_691b08af089e9_btn_type' => 'btn-secondary',  // ← Key complète !
    'field_691b08af089e9_icon' => 'arrow-left',
);
```

Sans le `$key_prefix`, vous cherchez `$value['btn_type']` mais c'est stocké dans
`$value['field_xxx_btn_type']` → **Sauvegarde toujours les valeurs par défaut**
!

### format_value() - Formater pour l'affichage

> ⚠️ **RÈGLE IMPORTANTE** : Ne PAS utiliser `is_admin()` ou `REST_REQUEST` pour
> bloquer le formatage ! Cela empêche le bon fonctionnement dans l'éditeur
> Gutenberg et les previews de blocs.

```php
public function format_value( $value, $post_id, $field ) {

    if ( empty( $value ) || !is_array( $value ) ) {
        return null;
    }

    // ✅ TOUJOURS formater la valeur
    // Contrairement à ce qu'on pourrait croire, le check is_admin() empêche
    // le bon fonctionnement dans l'éditeur Gutenberg et les previews.
    // ACF gère correctement le contexte d'affichage sans ce check.

    $return_format = $field['return_format'] ?? 'array';

    if ( $return_format === 'output' ) {
        // Générer le HTML
        ob_start();
        mon_helper_function( $value );
        return ob_get_clean();
    }

    // Format 'array' : transformer la structure pour l'usage en template
    return array(
        'link' => $value['link'] ?? array(),
        'args' => array(
            'type' => $value['btn_type'] ?? 'btn-primary',
            'icon' => $value['icon'] ?? 'arrow-right',
        )
    );
}
```

**Comment gérer la transformation de structure ?**

Si vous devez transformer la structure pour l'affichage frontend, faites-le
**uniquement** pour le format 'output', pas pour le format 'array' par défaut :

```php
if ( $return_format === 'output' ) {
    // Générer le HTML directement
    ob_start();
    pib_btn( $value['link'], array(
        'type' => $value['btn_type'] ?? 'btn-primary',
        'icon' => $value['icon'] ?? 'arrow-right',
    ));
    return ob_get_clean();
}

// Format 'array' : retourner tel quel pour usage manuel en template
// La structure reste identique à celle stockée en DB
return $value;
```

Ainsi, `render_field()` peut toujours accéder à `$field['value']['btn_type']`
correctement, que ce soit lors de la création ou de l'édition du champ.

## 📚 Exemples de référence

### ACF natif

- `/advanced-custom-fields-pro/includes/fields/class-acf-field-link.php`
- Ligne 115-125 : Structure des hidden inputs
- Ligne 241-250 : Méthode update_value()

### ACF Extended

- `/acf-extended-pro/includes/fields/field-advanced-link.php`
- Ligne 98 : Hidden input parent
- Ligne 276-294 : Configuration des sous-champs avec prefix
- Ligne 428-478 : Méthode update_value() complète

## ⚡ Pourquoi pas de JavaScript ?

La **sauvegarde en base de données fonctionne sans JavaScript** :

1. L'utilisateur remplit le formulaire → HTML natif
2. Clique sur "Mettre à jour" → POST standard
3. WordPress reçoit `$_POST` → PHP natif
4. ACF appelle `update_value()` → Votre code
5. Sauvegarde avec `update_post_meta()` → WordPress natif

**Le JavaScript n'est utile que pour :**

- Interface utilisateur (modals, previews)
- Chargement dynamique de données (AJAX)
- Validation côté client (UX)

⚠️ **ATTENTION** : N'utilisez PAS JavaScript pour gérer le show/hide de champs !

## 🎯 Affichage conditionnel avec conditional_logic

Pour afficher/masquer des champs selon une condition, utilisez
`conditional_logic` natif ACF :

```php
// Dans render_field()
$mode_field = array(
    'type'    => 'radio',
    'name'    => 'mode',
    'key'     => $field['key'] . '_mode',
    'prefix'  => $field['name'],
    'choices' => array(
        'auto'   => 'Automatique',
        'custom' => 'Personnalisé',
    ),
);
acf_render_field_wrap($mode_field);

// Champ qui s'affiche conditionnellement
$custom_field = array(
    'type'              => 'text',
    'name'              => 'custom_value',
    'key'               => $field['key'] . '_custom',
    'prefix'            => $field['name'],
    'conditional_logic' => array(
        array(
            array(
                'field'    => $field['key'] . '_mode',
                'operator' => '==',
                'value'    => 'custom',
            ),
        ),
    ),
);
acf_render_field_wrap($custom_field);
```

**Pourquoi utiliser conditional_logic plutôt que JavaScript ?**

- ✅ Géré nativement par ACF (pas de JavaScript custom nécessaire)
- ✅ Fonctionne dans tous les contextes (admin, Gutenberg, repeater, flexible
  content)
- ✅ Pas de risque de conflits avec d'autres champs ou plugins
- ✅ Code plus propre, maintenable et performant
- ✅ Gère automatiquement la validation et la sauvegarde des champs masqués

**Exemple réel :** `ArchivePostsField` utilise `conditional_logic` pour afficher
le champ custom WPGB ID uniquement quand mode='custom' est sélectionné (lignes
248-262).

## ✅ Checklist finale

Avant de créer un champ custom, vérifiez :

- [ ] `acf_hidden_input(['name' => $field['name']])` au début de
      `render_field()`
- [ ] Tous les sous-champs utilisent `'prefix' => $field['name']`
- [ ] Aucun `'name' => $field['name'] . '[...]'` dans les sous-champs
- [ ] `update_value()` retourne `''` si les valeurs sont vides
- [ ] `format_value()` gère les cas où `$value` est null/vide
- [ ] Tester avec un champ vide (doit se sauvegarder sans erreur)
- [ ] Tester dans un repeater/flexible content

## 🐛 Debugging

### Le champ ne sauvegarde pas

1. Inspecter le HTML généré (DevTools)
2. Vérifier que les `name` attributes sont corrects :

   ```html
   <!-- ✅ Bon -->
   <input name="acf[field_123][sous_champ]" />

   <!-- ❌ Mauvais -->
   <input name="acf[field_123][sous_champ][sous_champ]" />
   ```

3. Vérifier que `update_value()` est appelé :
   ```php
   error_log('UPDATE VALUE: ' . print_r($value, true));
   ```

### Erreur de validation

- Vérifier que tous les champs requis ont une valeur
- S'assurer que `validate_value()` retourne `true` ou un string d'erreur

## 🎯 Résumé

**2 règles d'or :**

1. **Hidden input parent** : `acf_hidden_input(['name' => $field['name']])`
2. **Prefix sur les sous-champs** : `'prefix' => $field['name']` et
   `'name' => 'simple'`

Suivez ces règles et la sauvegarde fonctionnera automatiquement ! 🎉
