git clone https://github.com/kainovaii/spark-skeleton.git
cd spark-skeleton
./build.bat
Fini les routes déclarées manuellement. Utilise les annotations directement sur tes méthodes.
@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")
@GET(value = "/articles/:id", name = "articles.show")
private Object show(Request req, Response res) {
String id = req.params(":id");
// ...
}
@GET(value = "/search", name = "search")
private Object search(Request req, Response res) {
String query = req.queryParams("q");
String page = req.queryParams("page");
// ...
}
Tous tes controllers doivent hériter de BaseController et porter l'annotation @Controller.
@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;
}
}
@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
));
}
L'injection fonctionne avec n'importe quelle classe annotée @Repository. Tu peux injecter autant de dépendances que nécessaire.
Les models utilisent ActiveJDBC, un ORM léger qui suit le pattern ActiveRecord.
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);
}
}
// 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();
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());
}
}
Un système de migrations fluide inspiré de Laravel pour gérer ta structure de base de données.
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");
}
}
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
table.string("email").notNull()
table.integer("count").defaultValue(0)
table.string("code").unique()
table.integer("user_id").foreignKey("users", "id")
Sépare ta logique de données avec le pattern 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();
}
}
Utilise les repositories pour toute logique complexe de récupération de données. Ça garde tes controllers propres et facilite les tests.
Utilise Pebble pour tes vues. Syntaxe simple et puissante.
{% 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 %}
return render("articles/index.html", Map.of(
"title", "Mes articles",
"articles", articles
));
{{ article.title | upper }}{{ article.content | truncate(100) }}{{ article.createdAt | date("Y-m-d") }}{{ article.price | number_format }}
// 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;
});
Le système d'authentification est basé sur UserDetailsService, une abstraction qui te laisse libre d'organiser ton modèle User comme tu veux.
@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; }
};
}
}
@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");
}
}
// 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
@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));
}
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é".