Table des matières

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

snippet.javascript
// 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)

snippet.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)

snippet.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)

snippet.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

snippet.javascript
// 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 navigate = 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
            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)

snippet.javascript
const st = useStates({
    initialStates: {
        loading: false,
        formData: { name: '', email: '' }
    }
});

Quand utiliser useGlobalStates

snippet.javascript
const [session, setSession] = useGlobalStates('session');
const [cart, setCart] = useGlobalStates('cart');

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

snippet.javascript
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 [products, setProducts] = useGlobalStates('products');
        // setProducts([...products, newProduct]);
 
        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 →