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 :
- Détecte l'erreur 401 (token expiré)
- Appelle
/refreshavec le refreshToken - Obtient un nouveau accessToken
- 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
- snippet.javascript
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
- 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
- useApi gère automatiquement l'authentification JWT
- Controller charge les objets Dolibarr
- Mapper convertit Dolibarr → DTO React
- useStates pour l'état local
- useGlobalStates pour l'état partagé
- Le refresh token est géré automatiquement
← Chapitre précédent | Retour au module | Module suivant : SmartCommon Composants →