Construire un comparateur SaaS en PHP : retour d'expérience technique
Constituire un comparateur SaaS efficace, c'est plus complexe qu'il n'y paraît. Vous devez gérer des centaines de produits, des dizaines d'attributs, des filtres complexes, tout en gardant des performances web acceptables.
Après avoir construit plusieurs comparateurs SaaS en PHP pur (sans framework lourd), je vais partager les défis techniques réels, les patterns qui fonctionnent, et surtout — les erreurs à éviter.
1. Structure des données : au-delà des simples tableaux
Votre première tentation sera d'utiliser une base de données classique SQL avec une table products et une table product_attributes. Sauf que c'est un cauchemar pour les requêtes complexes.
Le problème normalisé :
products (id, name, vendor, price, currency)
attributes (id, product_id, attribute_name, attribute_value)
Quand vous essayez de filtrer : "Montrez-moi tous les SaaS avec stockage >= 100GB ET plan de paiement = 'annuel' ET nombre de seats >= 50", vous finissez avec une requête SQL atroce :
SELECT DISTINCT p.id, p.name
FROM products p
JOIN attributes a1 ON p.id = a1.product_id AND a1.attribute_name = 'storage' AND CAST(a1.attribute_value AS INT) >= 100
JOIN attributes a2 ON p.id = a2.product_id AND a2.attribute_name = 'payment_plan' AND a2.attribute_value = 'annuel'
JOIN attributes a3 ON p.id = a3.product_id AND a3.attribute_name = 'seats' AND CAST(a3.attribute_value AS INT) >= 50
ORDER BY p.price ASC;
Cette requête fait 3 JOIN sur la même table. Pour 500 produits, ça devient TRÈS lent.
La solution : dénormalisation stratégique
Au lieu de stocker chaque attribut dans une ligne distincte, stockez les attributs clés comme colonnes JSON :
products (
id INT PRIMARY KEY,
name VARCHAR(255),
vendor VARCHAR(255),
pricing DECIMAL(10,2),
currency VARCHAR(3),
attributes JSON, -- Contient storage, payment_plan, seats, etc.
metadata JSON, -- Contient features_count, user_rating, release_date
indexed_text FULLTEXT, -- Pour la recherche textuelle
created_at TIMESTAMP
)
Votre structure JSON pour un SaaS de collaboration pourrait ressembler à :
{
"storage": 100,
"payment_plan": ["monthly", "annual", "enterprise"],
"seats": {"min": 5, "max": null},
"sso": true,
"api_included": true,
"support_level": "email",
"uptime_sla": 99.9
}
Avec cette structure, votre requête MySQL devient :
SELECT id, name FROM products
WHERE JSON_EXTRACT(attributes, '$.storage') >= 100
AND JSON_CONTAINS(JSON_EXTRACT(attributes, '$.payment_plan'), '"annual"')
AND JSON_EXTRACT(attributes, '$.seats.min') <= 50
ORDER BY pricing ASC;
Beaucoup plus rapide. Et en PHP, vous parsez le JSON une seule fois.
2. Caching des filtres : le secret des performances
Quand vous avez 1000 produits et 50 attributs possibles, chaque combinaison de filtres crée une requête unique. Sans caching, c'est le chaos.
Implémentation simple avec Redis :
$filter_hash = md5(json_encode($_GET['filters'])); // Hash des filtres actuels
$cache_key = "filters:" . $filter_hash;
if ($cached = redis_get($cache_key)) {
$results = json_decode($cached);
} else {
$results = query_products($_GET['filters']);
redis_set($cache_key, json_encode($results), 3600); // Cache 1 heure
}
Le vrai truc sophistiqué : invalidez le cache au niveau des attributs, pas des filtres.
Quand quelqu'un met à jour un produit (ex: change le prix ou l'attribut "storage"), vous invalidez TOUS les caches qui concernent cet attribut :
function update_product($product_id, $new_data) {
// Mettre à jour la BDD
update_db($product_id, $new_data);
// Invalider les caches pertinents
redis_delete_pattern("filters:*storage*");
redis_delete_pattern("filters:*price*");
}
3. Les filtres multi-niveaux : la vraie complexité
Vous avez des filtres simples (oui/non : "a une API ?"), des filtres range (prix entre X et Y), et des filtres multi-select ("Stockage = 100GB OU 500GB OU Illimité").
Chaque type demande une logique MySQL différente. Voici comment structurer ça en PHP de manière maintenable :
class FilterBuilder {
private $conditions = [];
public function add_filter($attribute, $operator, $value) {
if ($operator === 'equals') {
$this->conditions[] = "JSON_EXTRACT(attributes, '$." . $attribute . "') = '" . $value . "'";
} elseif ($operator === 'gte') {
$this->conditions[] = "JSON_EXTRACT(attributes, '$." . $attribute . "') >= " . (int)$value;
} elseif ($operator === 'in') {
// Pour multi-select
$escaped = array_map(fn($v) => "'" . $v . "'", $value);
$this->conditions[] = "JSON_EXTRACT(attributes, '$." . $attribute . "') IN (" . implode(',', $escaped) . ")";
}
return $this;
}
public function build_sql() {
return "SELECT * FROM products WHERE " . implode(' AND ', $this->conditions);
}
}
$builder = new FilterBuilder();
$builder->add_filter('storage', 'gte', 100);
$builder->add_filter('payment_plan', 'in', ['monthly', 'annual']);
$sql = $builder->build_sql();
Cette approche (builder pattern) rend votre code extensible : ajouter un nouveau type de filtre ne demande qu'une nouvelle méthode.
4. SEO pour les comparateurs : le challenge invisible
Voici ce que personne ne vous dit sur le SEO d'un comparateur SaaS :
- Les pages de filtres sont considérées comme du duplicate content par Google. Si votre comparateur génère 10 000 pages (10 produits × 1000 combinaisons de filtres), vous allez avoir des problèmes.
Solution :
- Utilisez des URL canoniques intelligentes
- N'indexez que les filtres "importants" (évitez d'indexer une page filtrée par "prix décroissant")
- Utilisez
rel="noindex"sur les combinaisons de faible valeur
- Vous devez générer des pages statiques pour le SEO. Les filtres dynamiques en JavaScript ne sont pas crawlés efficacement.
Solution :
- Générez des pages HTML pré-rendues pour les combinaisons top (ex: "SaaS collaboratif vs SaaS CRM")
- Stockez ces pages en fichiers pour les servir rapidement
- Laissez les filtres avancés en JavaScript
- Les méta descriptions doivent être uniques. Une page filtrée sur "stockage >= 100GB" doit avoir une description différente de celle sur "stockage >= 500GB".
$meta_desc = "Comparez les meilleurs SaaS avec " . implode(', ', $active_filters) . ". Tableau comparatif complet, avis utilisateurs, prix.";
echo "<meta name='description' content='" . htmlspecialchars($meta_desc) . "'>";
5. Performance : servir des pages lourdes rapidement
Un tableau comparatif avec 100 produits et 30 attributs = énorme DOM. Sans optimisation :
- Rendu lent
- Interactions figées
- Utilisateurs qui quittent
Tactiques essentielles :
- Pagination côté serveur, pas côté client :
$per_page = 20; // Ne charger que 20 produits à la fois
$offset = ($_GET['page'] - 1) * $per_page;
$results = query_limited($filters, $offset, $per_page);
- Compression GZIP pour les données JSON :
header('Content-Encoding: gzip');
echo gzencode(json_encode($results), 9);
- Lazy-load des images : Les logos SaaS ne doivent charger que quand visibles
<img src="placeholder.svg" data-src="actual-logo.png" loading="lazy">
6. Cas d'usage réel : comparer-logiciels.fr
J'ai personnellement construit Comparer-Logiciels.fr, un comparateur SaaS français. Voici ce que j'ai appris :
- 500+ logiciels comparés, 250+ attributs différents
- Structure JSON pour les attributs = requêtes 40% plus rapides
- Redis caching = response time < 200ms même avec filtres complexes
- Génération de pages statiques pour le top 100 des comparaisons = SEO stable
Le coût ? Environ 50 EUR/mois en infrastructure (serveur PHP + MySQL + Redis). Aucun besoin de technos complexes.
Conclusion
Construire un comparateur SaaS en PHP pur, c'est possible et même préférable à une stack JavaScript lourd. Les points clés :
- Dénormalisez intelligemment avec JSON
- Cachey les résultats des filtres
- Pensez SEO dès le départ
- Paginéz pour la performance
- Utilisez des outils simples (MySQL, Redis, PHP) plutôt que une usine à gaz
Avec ces patterns, vous pouvez supporter des milliers de produits sans problème de performance.
Top comments (0)