Retour

documentation_

Tout ce que tu dois savoir pour démarrer avec le boilerplate Spark.

Installation

1. Clone le repo
git clone https://github.com/kainovaii/spark-skeleton.git
cd spark-skeleton
3. Lance l'application
./build.bat
⚠️ Prérequis
  • • Java 17 ou supérieur
  • • Maven 3.6+
  • • MySQL/PostgreSQL/SQLite

Routing avec annotations

Fini les routes déclarées manuellement. Utilise les annotations directement sur tes méthodes.

Annotations disponibles
@GET(value = "/articles", name = "articles.index")
@POST(value = "/articles", name = "articles.store")
@PUT(value = "/articles/:id", name = "articles.update")
@DELETE(value = "/articles/:id", name = "articles.delete")
@PATCH(value = "/articles/:id", name = "articles.patch")
Paramètres de route
@GET(value = "/articles/:id", name = "articles.show")
private Object show(Request req, Response res) {
    String id = req.params(":id");
    // ...
}
Query parameters
@GET(value = "/search", name = "search")
private Object search(Request req, Response res) {
    String query = req.queryParams("q");
    String page = req.queryParams("page");
    // ...
}

Controllers

Tous tes controllers doivent hériter de BaseController et porter l'annotation @Controller.

Controller basique
@Controller
public class ArticleController extends BaseController {

    @GET(value = "/articles", name = "articles.index")
    private Object index(Request req, Response res) {
        return render("articles/index.html", Map.of(
            "title", "Mes articles"
        ));
    }

    @POST(value = "/articles", name = "articles.store")
    private Object store(Request req, Response res) {
        String title = req.queryParams("title");

        Article article = new Article();
        article.set("title", title);
        article.saveIt();

        res.redirect("/articles");
        return null;
    }
}
Injection de dépendances
@GET(value = "/articles", name = "articles.index")
private Object index(ArticleRepository articleRepo) {
    List<Article> articles = DB.withConnection(() ->
        articleRepo.findAll().stream().toList()
    );

    return render("articles/index.html", Map.of(
        "articles", articles
    ));
}
💡 Astuce

L'injection fonctionne avec n'importe quelle classe annotée @Repository. Tu peux injecter autant de dépendances que nécessaire.

Models ActiveJDBC

Les models utilisent ActiveJDBC, un ORM léger qui suit le pattern ActiveRecord.

Créer un model
public class Article extends Model {

    // Getters
    public String getTitle() {
        return getString("title");
    }

    public String getContent() {
        return getString("content");
    }

    public Integer getStatus() {
        return getInteger("status");
    }

    // Setters
    public void setTitle(String title) {
        set("title", title);
    }

    public void setContent(String content) {
        set("content", content);
    }

    public void setStatus(Integer status) {
        set("status", status);
    }
}
Utilisation
// Créer
Article article = new Article();
article.setTitle("Mon titre");
article.setContent("Mon contenu");
article.saveIt();

// Récupérer
Article article = Article.findById(1);
LazyList<Article> articles = Article.findAll();
LazyList<Article> published = Article.where("status = ?", 1);

// Modifier
article.setTitle("Nouveau titre");
article.saveIt();

// Supprimer
article.delete();
Relations
public class Article extends Model {

    // Un article appartient à un user
    public User getAuthor() {
        return parent(User.class);
    }

    // Un article a plusieurs commentaires
    public LazyList<Comment> getComments() {
        return get(Comment.class, "article_id = ?", getId());
    }
}

Migrations

Un système de migrations fluide inspiré de Laravel pour gérer ta structure de base de données.

Créer une table
public class CreateArticlesTable extends Migration {

    @Override
    public void up() {
        createTable("articles", table -> {
            table.id();
            table.string("title").notNull();
            table.text("content").notNull();
            table.integer("status").defaultValue(1);
            table.integer("user_id");
            table.timestamps();
        });
    }

    @Override
    public void down() {
        dropTable("articles");
    }
}
Types de colonnes disponibles
table.id()                              // PRIMARY KEY AUTO_INCREMENT
table.string("name")                    // VARCHAR(255)
table.string("code", 10)                // VARCHAR(10)
table.text("content")                   // TEXT
table.integer("count")                  // INT
table.bigInteger("big_count")           // BIGINT
table.decimal("price", 10, 2)           // DECIMAL(10,2)
table.boolean("active")                 // BOOLEAN
table.date("birth_date")                // DATE
table.datetime("event_at")              // DATETIME
table.timestamp("logged_at")            // TIMESTAMP
table.timestamps()                      // created_at + updated_at
Modificateurs
table.string("email").notNull()
table.integer("count").defaultValue(0)
table.string("code").unique()
table.integer("user_id").foreignKey("users", "id")

Repositories

Sépare ta logique de données avec le pattern Repository.

Créer un repository
@Repository
public class ArticleRepository {

    public LazyList<Article> findAll() {
        return Article.findAll();
    }

    public LazyList<Article> findPublished() {
        return Article.where("status = ?", 1);
    }

    public LazyList<Article> findByAuthor(Integer userId) {
        return Article.where("user_id = ?", userId);
    }

    public Article findById(Integer id) {
        return Article.findById(id);
    }

    public void create(String title, String content) {
        Article article = new Article();
        article.setTitle(title);
        article.setContent(content);
        article.setStatus(1);
        article.saveIt();
    }
}
💡 Bonne pratique

Utilise les repositories pour toute logique complexe de récupération de données. Ça garde tes controllers propres et facilite les tests.

Templates Pebble

Utilise Pebble pour tes vues. Syntaxe simple et puissante.

Template de base
{% extends "layout.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}    <h1>{{ title }}</h1>

    {% for article in articles %}        <article>
            <h2>{{ article.title }}</h2>
            <p>{{ article.content }}</p>
        </article>
    {% endfor %}{% endblock %}
Render depuis un controller
return render("articles/index.html", Map.of(
    "title", "Mes articles",
    "articles", articles
));
Filtres utiles
{{ article.title | upper }}{{ article.content | truncate(100) }}{{ article.createdAt | date("Y-m-d") }}{{ article.price | number_format }}

Configuration Database

Utilisation dans le code
// Toutes les opérations DB doivent être wrapped
List<Article> articles = DB.withConnection(() ->
    Article.findAll().stream().toList()
);

// Ou pour plusieurs opérations
DB.withConnection(() -> {
    Article article = new Article();
    article.setTitle("Test");
    article.saveIt();

    return article;
});

Security & Authentication

Le système d'authentification est basé sur UserDetailsService, une abstraction qui te laisse libre d'organiser ton modèle User comme tu veux.

1. Créer ton UserDetailsService
@UserDetailsServiceImpl
public class AppUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    public AppUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadByUsername(String username) {
        return DB.withConnection(() -> {
            if (!UserRepository.userExist(username)) return null;
            User user = userRepository.findByUsername(username);
            return adapt(user);
        });
    }

    @Override
    public UserDetails loadById(Object id) {
        return DB.withConnection(() -> {
            User user = User.findById(id);
            return adapt(user);
        });
    }

    private UserDetails adapt(User user) {
        if (user == null) return null;

        return new UserDetails() {
            public Object getId() { return user.getId(); }
            public String getUsername() { return user.getUsername(); }
            public String getPassword() { return user.getPassword(); }
            public String getRole() { return user.getRole(); }
            public boolean isEnabled() { return true; }
        };
    }
}
3. Utiliser dans tes controllers
@Controller
public class LoginController extends BaseController {

    @POST("/users/login")
    private Object loginBack(Request req, Response res) {
        String username = req.queryParams("username");
        String password = req.queryParams("password");
        Session session = req.session(true);

        if (login(username, password, session)) {
            res.redirect("/dashboard");
            return true;
        }

        return redirectWithFlash(req, res, "error",
            "Incorrect login", "/users/login");
    }

    @HasRole("DEFAULT")
    @GET("/users/logout")
    private Object logout(Request req, Response res) {
        logout(req.session(true));
        return redirectWithFlash(req, res, "success",
            "Logged out", "/users/login");
    }
}
Méthodes disponibles dans BaseController
// Authentification
login(username, password, session)  // Authentifie et crée la session
logout(session)                     // Déconnecte l'utilisateur

// Vérifications
isLogged(req)                       // Vérifie si connecté
hasRole(req, "ADMIN")               // Vérifie un rôle
requireLogin(req, res)              // Force la connexion (redirige sinon)

// Récupération
getLoggedUser(req)                  // Récupère le UserDetails
Protection par rôle
@HasRole("ADMIN")
@GET("/admin/dashboard")
private Object adminDashboard(Request req, Response res) {
    // Accessible uniquement aux ADMIN
    return render("admin/dashboard.html", Map.of());
}

@HasRole("DEFAULT")
@GET("/profile")
private Object profile(Request req, Response res) {
    // Accessible à tous les utilisateurs connectés
    UserDetails user = getLoggedUser(req);
    return render("profile.html", Map.of("user", user));
}
💡 Astuce

L'annotation @UserDetailsServiceImpl permet au framework de détecter automatiquement ton implémentation. Pas besoin de config manuelle !

Le rôle "DEFAULT" signifie "n'importe quel utilisateur connecté".