Chapitre 3 : Flux de données

Vue d'ensemble

Comprendre comment les données circulent dans SmartMaker est essentiel. Ce chapitre trace le chemin complet d'une donnée, du clic utilisateur jusqu'au rendu final.

Les 3 types de données

Type Stockage Persistance Hook
État local Mémoire composant Non useState, useStates
État global Redux store Optionnel (localStorage) useGlobalStates
Données serveur API -> Dolibarr Base de données useApi

Flux d'une requête API

┌──────────────────────────────────────────────────────────────┐
│                        FRONTEND                               │
├──────────────────────────────────────────────────────────────┤
│  1. Utilisateur clique "Charger"                             │
│           ↓                                                   │
│  2. Composant appelle useApi()                               │
│           ↓                                                   │
│  3. api.private.get('items')                                 │
│           ↓                                                   │
│  4. ky ajoute automatiquement :                              │
│     - Authorization: Bearer <accessToken>                    │
│     - X-DEVICEID: <deviceId>                                 │
└──────────────────────────────────────────────────────────────┘
                            ↓ HTTPS
┌──────────────────────────────────────────────────────────────┐
│                        BACKEND                                │
├──────────────────────────────────────────────────────────────┤
│  5. pwa/api.php reçoit la requête                            │
│           ↓                                                   │
│  6. SmartAuth valide le JWT                                  │
│           ↓                                                   │
│  7. Router appelle ItemController::index()                   │
│           ↓                                                   │
│  8. Controller charge les objets Dolibarr                    │
│           ↓                                                   │
│  9. Mapper convertit en DTO                                  │
│           ↓                                                   │
│ 10. Retourne JSON                                            │
└──────────────────────────────────────────────────────────────┘
                            ↓ HTTPS
┌──────────────────────────────────────────────────────────────┐
│                        FRONTEND                               │
├──────────────────────────────────────────────────────────────┤
│ 11. ky parse la réponse JSON                                 │
│           ↓                                                   │
│ 12. Composant met à jour l'état                              │
│           ↓                                                   │
│ 13. React re-rend avec les nouvelles données                 │
└──────────────────────────────────────────────────────────────┘

Exemple concret : Liste de produits

1. Le composant React

// components/pages/private/ProductsPage/index.jsx
import { useEffect } from 'react';
import { Page, Block, List, ListItem, Spinner } from '@cap-rel/smartcommon';
import { useApi, useStates } from '@cap-rel/smartcommon';

export const ProductsPage = () => {
    const api = useApi();

    const st = useStates({
        initialStates: {
            products: [],
            loading: true,
            error: null
        }
    });

    // Chargement au montage
    useEffect(() => {
        loadProducts();
    }, []);

    const loadProducts = async () => {
        st.set('loading', true);
        st.set('error', null);

        try {
            // Requête API avec JWT automatique
            const data = await api.private.get('products').json();
            st.set('products', data.products);
        } catch (err) {
            st.set('error', err.message);
        } finally {
            st.set('loading', false);
        }
    };

    // Affichage conditionnel
    if (st.get('loading')) {
        return <Page><Spinner /></Page>;
    }

    if (st.get('error')) {
        return (
            <Page>
                <Block>Erreur : {st.get('error')}</Block>
            </Page>
        );
    }

    return (
        <Page title="Produits">
            <List>
                {st.get('products').map(product => (
                    <ListItem
                        key={product.id}
                        title={product.label}
                        subtitle={`${product.price} €`}
                    />
                ))}
            </List>
        </Page>
    );
};

2. Le routeur API (PHP)

<?php
// pwa/api.php
require_once '../smartmaker-api-prepend.php';

use SmartAuth\Api\AuthController;
use SmartAuth\Api\RouteController as Route;
use MonModule\Api\ProductController;

// Routes d'authentification
Route::get('login', AuthController::class, 'index');
Route::post('login', AuthController::class, 'login');
Route::get('refresh', AuthController::class, 'refresh');
Route::post('logout', AuthController::class, 'logout', true);

// Routes produits (protégées)
Route::get('products', ProductController::class, 'index', true);
Route::get('products/{id}', ProductController::class, 'show', true);
Route::post('products', ProductController::class, 'create', true);
Route::put('products/{id}', ProductController::class, 'update', true);
Route::delete('products/{id}', ProductController::class, 'delete', true);

// Fallback
json_reply('Access denied', 403);

3. Le Controller (PHP)

<?php
// smartmaker-api/Controllers/ProductController.php
namespace MonModule\Api;

use MonModule\Api\Mappers\dmProduct;

class ProductController
{
    public function index($payload = null)
    {
        global $db, $user;

        // Charger les produits Dolibarr
        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';

        $sql = "SELECT rowid FROM " . MAIN_DB_PREFIX . "product";
        $sql .= " WHERE entity IN (" . getEntity('product') . ")";
        $sql .= " ORDER BY label ASC";

        $result = $db->query($sql);
        $products = [];

        if ($result) {
            $mapper = new dmProduct();

            while ($obj = $db->fetch_object($result)) {
                $product = new \Product($db);
                $product->fetch($obj->rowid);

                // Mapper vers DTO React
                $products[] = $mapper->exportMappedData($product);
            }
        }

        return [['products' => $products], 200];
    }

    public function show($payload = null)
    {
        global $db;

        $id = $payload['id'];

        require_once DOL_DOCUMENT_ROOT . '/product/class/product.class.php';
        $product = new \Product($db);

        if ($product->fetch($id) <= 0) {
            return [['error' => 'Produit non trouvé'], 404];
        }

        $mapper = new dmProduct();
        return [$mapper->exportMappedData($product), 200];
    }
}

4. Le Mapper (PHP)

<?php
// smartmaker-api/Mappers/dmProduct.php
namespace MonModule\Api\Mappers;

class dmProduct
{
    public function exportMappedData($product)
    {
        return [
            'id' => (int) $product->id,
            'ref' => $product->ref,
            'label' => $product->label,
            'description' => $product->description,
            'price' => (float) $product->price,
            'price_ttc' => (float) $product->price_ttc,
            'stock' => (float) $product->stock_reel,
            'status' => (int) $product->status,
            'created_at' => $product->datec,
            'updated_at' => $product->tms
        ];
    }

    public function importMappedData($data)
    {
        // Conversion DTO -> Dolibarr pour création/modification
        return [
            'ref' => $data['ref'] ?? '',
            'label' => $data['label'] ?? '',
            'description' => $data['description'] ?? '',
            'price' => $data['price'] ?? 0
        ];
    }
}

Authentification JWT

Flux de login

1. Utilisateur saisit email/password
           ↓
2. api.login({ login, password })
           ↓
3. SmartAuth vérifie les credentials
           ↓
4. Retourne { accessToken, refreshToken, user }
           ↓
5. SmartCommon stocke dans session (localStorage)
           ↓
6. Toutes les requêtes futures ont le token

Exemple login

// components/pages/public/LoginPage/index.jsx
import { Page, Block, Form, Input, Button } from '@cap-rel/smartcommon';
import { useApi, useNavigation } from '@cap-rel/smartcommon';

export const LoginPage = () => {
    const api = useApi();
    const nav = useNavigation();

    const handleSubmit = async (values) => {
        try {
            // api.login gère tout automatiquement
            await api.login({
                login: values.email,
                password: values.password
            });

            // Redirige vers l'accueil
            nav.navigate('/');
        } catch (err) {
            alert('Identifiants incorrects');
        }
    };

    return (
        <Page title="Connexion">
            <Block>
                <Form onSubmit={handleSubmit}>
                    <Input name="email" label="Email" type="email" required />
                    <Input name="password" label="Mot de passe" type="password" required />
                    <Button type="submit">Connexion</Button>
                </Form>
            </Block>
        </Page>
    );
};

Refresh automatique

Le token d'accès expire après 15 minutes. SmartCommon gère automatiquement :

  1. Détecte l'erreur 401 (token expiré)
  2. Appelle /refresh avec le refreshToken
  3. Obtient un nouveau accessToken
  4. Relance la requête originale

Tout est transparent pour le développeur.

État global vs État local

Quand utiliser useStates (local)

  • Données propres à un composant
  • État de formulaire
  • État de chargement
  • Données temporaires
const st = useStates({
    initialStates: {
        loading: false,
        formData: { name: '', email: '' }
    }
});

Quand utiliser useGlobalStates

  • Données partagées entre composants
  • Session utilisateur
  • Préférences
  • Données métier principales
const gst = useGlobalStates();

const session = gst.get('session');
const cart = gst.get('cart');

// Écriture
gst.local.set('session', userData);
gst.set('cart', { items: [], total: 0 });

Création de données

Flux de création

1. Formulaire rempli
           ↓
2. api.private.post('products', { json: data })
           ↓
3. Controller::create() valide et crée
           ↓
4. Retourne le produit créé
           ↓
5. Mise à jour de l'état local/global

Exemple

const handleCreate = async (formData) => {
    try {
        const newProduct = await api.private
            .post('products', { json: formData })
            .json();

        // Ajouter à la liste locale
        st.set('products', [...st.get('products'), newProduct]);

        // Ou mettre à jour l'état global
        // const gst = useGlobalStates();
        // gst.set('products', [...gst.get('products'), newProduct]);

        nav.navigate('/products');
    } catch (err) {
        st.set('error', err.message);
    }
};

Résumé du flux complet

Étape Couche Action
1 React Utilisateur interagit
2 Hook useApi() prépare la requête
3 ky Ajoute JWT + envoie
4 PHP api.php route la requête
5 SmartAuth Valide le token
6 Controller Exécute la logique métier
7 Dolibarr Accès base de données
8 Mapper Convertit en DTO
9 PHP Retourne JSON
10 ky Parse la réponse
11 Hook Met à jour l'état
12 React Re-rend l'interface

Points clés à retenir

  1. useApi gère automatiquement l'authentification JWT
  2. Controller charge les objets Dolibarr
  3. Mapper convertit Dolibarr -> DTO React
  4. useStates pour l'état local
  5. useGlobalStates pour l'état partagé
  6. Le refresh token est géré automatiquement

← Chapitre précédent | Retour au module | Module suivant : SmartCommon Composants ->