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 :
- 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
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
- 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 ->