Combien coûte vraiment un crédit ? Une calculatrice de prêt en PHP répond en trois champs : le montant, le taux et la durée donnent la mensualité, le coût des intérêts et le tableau d'amortissement mois par mois. C'est un projet idéal pour pratiquer PHP côté serveur : formulaire, validation, calcul et affichage sécurisé. Durée : environ 45 minutes.

Ce que tu vas construire

  • un formulaire qui accepte les nombres « à la française » (200 000, 3,5) ;
  • une validation avec des messages d'erreur clairs sous chaque champ ;
  • la mensualité, le coût total des intérêts et le montant total remboursé ;
  • le tableau d'amortissement complet, replié par défaut.

Il te faut PHP 8 ou plus. Crée un dossier avec calcul.php, index.php et style.css, puis lance le serveur intégré de PHP dans ce dossier : php -S localhost:8000, et ouvre http://localhost:8000.

1. La formule de la mensualité

Pour un prêt à taux fixe, la mensualité M se calcule ainsi :

M = C × t / (1 − (1 + t)−n)

  • C : le capital emprunté ;
  • t : le taux mensuel, c'est-à-dire le taux annuel divisé par 12 (3,5 % par an donne 0,035 / 12) ;
  • n : le nombre de mensualités (25 ans donnent 300 mois).

Chaque mois, une partie de la mensualité paie les intérêts du capital restant, le reste rembourse le capital. Au début, les intérêts pèsent lourd ; à la fin, presque toute la mensualité rembourse le capital. C'est ce que montre le tableau d'amortissement.

2. Les fonctions de calcul

<?php

declare(strict_types=1);

/**
 * Mensualité d'un prêt à taux fixe (hors assurance).
 */
function mensualite(float $capital, float $tauxAnnuel, int $mois): float
{
    $tauxMensuel = $tauxAnnuel / 100 / 12;

    // Prêt à taux zéro : la formule diviserait par zéro, on partage simplement le capital.
    if ($tauxMensuel == 0) {
        return $capital / $mois;
    }

    return $capital * $tauxMensuel / (1 - (1 + $tauxMensuel) ** -$mois);
}

/**
 * Tableau d'amortissement : chaque mois, la part d'intérêts, la part de
 * capital remboursé et le capital qui reste dû.
 *
 * @return list<array{mois: int, interets: float, capital: float, restant: float}>
 */
function amortissement(float $capital, float $tauxAnnuel, int $mois): array
{
    $mensualite = mensualite($capital, $tauxAnnuel, $mois);
    $tauxMensuel = $tauxAnnuel / 100 / 12;
    $restant = $capital;
    $lignes = [];

    for ($m = 1; $m <= $mois; $m++) {
        $interets = $restant * $tauxMensuel;
        $rembourse = $mensualite - $interets;
        $restant = max(0.0, $restant - $rembourse);

        $lignes[] = ['mois' => $m, 'interets' => $interets, 'capital' => $rembourse, 'restant' => $restant];
    }

    return $lignes;
}

/**
 * Montant au format français : 1 001,25 €.
 */
function euros(float $montant): string
{
    return number_format($montant, 2, ',', ' ') . ' €';
}

Les calculs vivent dans un fichier à part, sans HTML : tu peux les réutiliser ailleurs (une API, une commande) et les tester seuls. Trois détails :

  • à taux zéro, la formule diviserait par zéro : on partage simplement le capital par le nombre de mois ;
  • ** est l'opérateur puissance de PHP : (1 + $tauxMensuel) ** -$mois ;
  • max(0.0, …) évite d'afficher « −0,00 € » au dernier mois à cause des arrondis.

3. Le formulaire et la validation

<?php

declare(strict_types=1);

require __DIR__ . '/calcul.php';

function e(string $texte): string
{
    return htmlspecialchars($texte, ENT_QUOTES, 'UTF-8');
}

$valeurs = ['capital' => '', 'taux' => '', 'annees' => ''];
$erreurs = [];
$resultat = null;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    foreach ($valeurs as $champ => $_) {
        $valeurs[$champ] = trim((string) ($_POST[$champ] ?? ''));
    }

    // On accepte « 200 000 » et « 3,5 », comme on les écrit en France.
    $capital = filter_var(str_replace([' ', ','], ['', '.'], $valeurs['capital']), FILTER_VALIDATE_FLOAT);
    $taux = filter_var(str_replace(',', '.', $valeurs['taux']), FILTER_VALIDATE_FLOAT);
    $annees = filter_var($valeurs['annees'], FILTER_VALIDATE_INT);

    if ($capital === false || $capital <= 0 || $capital > 10_000_000) {
        $erreurs['capital'] = 'Indique un montant entre 1 et 10 000 000 €.';
    }
    if ($taux === false || $taux < 0 || $taux > 20) {
        $erreurs['taux'] = 'Indique un taux entre 0 et 20 %.';
    }
    if ($annees === false || $annees < 1 || $annees > 30) {
        $erreurs['annees'] = 'Indique une durée entre 1 et 30 ans.';
    }

    if ($erreurs === []) {
        $mois = $annees * 12;
        $mensualite = mensualite($capital, $taux, $mois);

        $resultat = [
            'mensualite' => $mensualite,
            'total' => $mensualite * $mois,
            'interets' => $mensualite * $mois - $capital,
            'tableau' => amortissement($capital, $taux, $mois),
        ];
    }
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Calculatrice de prêt</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="app">
    <h1>Calculatrice de prêt</h1>

    <form method="post" novalidate>
      <?php foreach (['capital' => 'Montant emprunté (€)', 'taux' => 'Taux annuel (%)', 'annees' => 'Durée (années)'] as $champ => $libelle): ?>
        <label for="<?= $champ ?>"><?= $libelle ?></label>
        <input id="<?= $champ ?>" name="<?= $champ ?>" inputmode="decimal" value="<?= e($valeurs[$champ]) ?>"
          <?= isset($erreurs[$champ]) ? 'aria-invalid="true" aria-describedby="' . $champ . '-erreur"' : '' ?>>
        <?php if (isset($erreurs[$champ])): ?>
          <p id="<?= $champ ?>-erreur" class="erreur"><?= e($erreurs[$champ]) ?></p>
        <?php endif; ?>
      <?php endforeach; ?>
      <button type="submit">Calculer</button>
    </form>

    <?php if ($resultat !== null): ?>
      <section class="resultat" aria-live="polite">
        <p>Mensualité : <strong><?= euros($resultat['mensualite']) ?></strong></p>
        <p>Coût des intérêts : <?= euros($resultat['interets']) ?></p>
        <p>Total remboursé : <?= euros($resultat['total']) ?></p>

        <details>
          <summary>Tableau d'amortissement</summary>
          <table>
            <thead>
              <tr><th>Mois</th><th>Intérêts</th><th>Capital</th><th>Reste dû</th></tr>
            </thead>
            <tbody>
              <?php foreach ($resultat['tableau'] as $ligne): ?>
                <tr>
                  <td><?= $ligne['mois'] ?></td>
                  <td><?= euros($ligne['interets']) ?></td>
                  <td><?= euros($ligne['capital']) ?></td>
                  <td><?= euros($ligne['restant']) ?></td>
                </tr>
              <?php endforeach; ?>
            </tbody>
          </table>
        </details>
      </section>
    <?php endif; ?>
  </main>
</body>
</html>

Ce qu'il faut retenir :

  • Ne jamais faire confiance aux données envoyées. filter_var() avec FILTER_VALIDATE_FLOAT ou FILTER_VALIDATE_INT renvoie false si la valeur n'est pas un nombre, puis on vérifie des bornes réalistes.
  • Accepter les habitudes françaises : on enlève les espaces et on remplace la virgule par un point avant de valider.
  • Échapper tout ce qu'on réaffiche avec htmlspecialchars() (la fonction e()) : une saisie comme "><script> reste du texte dans le champ.
  • Garder les valeurs saisies après l'envoi, pour corriger une erreur sans tout retaper.
  • Relier chaque erreur à son champ avec aria-invalid et aria-describedby : un lecteur d'écran lit le message quand on revient sur le champ.
  • novalidate sur le formulaire désactive les bulles d'erreur du navigateur, pour afficher nos propres messages. La vraie validation est toujours celle du serveur.

4. Le CSS

body {
  margin: 0;
  background: #f6f4ef;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

.app {
  max-width: 36rem;
  margin: 0 auto;
  padding: 2rem 1rem;
}

label {
  display: block;
  margin-top: 1rem;
  font-weight: 600;
}

input {
  box-sizing: border-box;
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #bbb;
  border-radius: 6px;
  font: inherit;
}

input[aria-invalid="true"] {
  border-color: #b3261e;
}

.erreur {
  margin: 0.25rem 0 0;
  color: #b3261e;
  font-size: 0.875rem;
}

button {
  margin-top: 1.5rem;
  padding: 0.6rem 1.25rem;
  border: 0;
  border-radius: 6px;
  background: #25737d;
  color: #fff;
  font: inherit;
  cursor: pointer;
}

.resultat {
  margin-top: 2rem;
  padding: 1rem 1.25rem;
  border-radius: 8px;
  background: #fff;
}

.resultat strong {
  color: #25737d;
  font-size: 1.5rem;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
}

th,
td {
  padding: 0.25rem 0.5rem;
  border-bottom: 1px solid #eee;
  text-align: right;
}

font-variant-numeric: tabular-nums donne la même largeur à tous les chiffres : les montants s'alignent dans le tableau.

5. Tester

  • Saisis 200 000 €, 3,5 % sur 25 ans : la mensualité est de 1 001,25 €, pour 100 374,14 € d'intérêts.
  • Ouvre le tableau : le premier mois, 583,33 € d'intérêts ; le reste dû tombe à 0,00 € au 300e mois.
  • Essaie un taux de 0 % : 12 000 € sur 2 ans donnent 500,00 € par mois.
  • Envoie des valeurs absurdes (« abc », 25 %, 0 an) : trois messages d'erreur, et les champs gardent ta saisie.

Ce calcul ne tient compte ni de l'assurance emprunteur ni des frais de dossier et de garantie : une vraie offre de prêt coûte un peu plus cher.

Pour aller plus loin

  • Un résumé par année plutôt que 300 lignes : regroupe le tableau avec array_chunk($tableau, 12) et additionne les intérêts de chaque année.
  • L'assurance emprunteur : un champ en pourcentage du capital, ajouté à chaque mensualité.
  • Le calcul inverse : combien emprunter pour une mensualité donnée ? Il suffit d'isoler C dans la formule.
  • Un résultat instantané : refais le calcul en JavaScript pendant la saisie, et garde le PHP pour les navigateurs sans JavaScript. Les bases sont dans le tutoriel validation de formulaires en JavaScript.

Tu calcules un vrai projet immobilier ? Au-delà de la mensualité, ma checklist du premier investissement locatif liste les points à vérifier avant de signer. Et si ta calculatrice est en ligne, partage-la dans la vitrine des lecteurs.