- sogoctl: supervisor avec health checks et restart auto - sogoway: gateway HTTP, auth JWT, routing par hostname - sogoms-db: microservice MariaDB avec pool par application - Protocol IPC Unix socket JSON length-prefixed - Config YAML multi-application (prokov) - Deploy script pour container Alpine gw3 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
360 lines
9.7 KiB
PHP
360 lines
9.7 KiB
PHP
<?php
|
|
/**
|
|
* Contrôleur des projets
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
class ProjectController extends Controller
|
|
{
|
|
/**
|
|
* GET /projects
|
|
* Liste tous les projets de l'utilisateur (arborescence)
|
|
*/
|
|
public function index(): void
|
|
{
|
|
$this->requireAuth();
|
|
|
|
$db = Database::getInstance();
|
|
|
|
// Récupérer tous les projets de l'utilisateur
|
|
$stmt = $db->prepare('
|
|
SELECT p.*,
|
|
GROUP_CONCAT(t.id) as tag_ids,
|
|
GROUP_CONCAT(t.name) as tag_names
|
|
FROM projects p
|
|
LEFT JOIN project_tags pt ON p.id = pt.project_id
|
|
LEFT JOIN tags t ON pt.tag_id = t.id
|
|
WHERE p.user_id = :user_id
|
|
GROUP BY p.id
|
|
ORDER BY p.parent_id ASC, p.position ASC, p.name ASC
|
|
');
|
|
|
|
$stmt->execute(['user_id' => $this->getUserId()]);
|
|
$projects = $stmt->fetchAll();
|
|
|
|
// Construire l'arborescence
|
|
$tree = $this->buildTree($projects);
|
|
|
|
Response::success($tree);
|
|
}
|
|
|
|
/**
|
|
* GET /projects/{id}
|
|
*/
|
|
public function show(): void
|
|
{
|
|
$this->requireAuth();
|
|
|
|
$id = (int) $this->request->getParam('id');
|
|
$project = $this->findOrFail($id);
|
|
|
|
// Récupérer les tags
|
|
$project['tags'] = $this->getProjectTags($id);
|
|
|
|
// Récupérer les sous-projets
|
|
$project['children'] = $this->getChildren($id);
|
|
|
|
Response::success($project);
|
|
}
|
|
|
|
/**
|
|
* POST /projects
|
|
*/
|
|
public function store(): void
|
|
{
|
|
$this->requireAuth();
|
|
|
|
$data = $this->validate([
|
|
'name' => 'required|min:1|max:100',
|
|
'description' => 'max:65535',
|
|
'parent_id' => 'int',
|
|
'position' => 'int',
|
|
]);
|
|
|
|
// Vérifier que le parent appartient à l'utilisateur
|
|
if (!empty($data['parent_id'])) {
|
|
$this->findOrFail((int) $data['parent_id']);
|
|
}
|
|
|
|
$db = Database::getInstance();
|
|
|
|
$stmt = $db->prepare('
|
|
INSERT INTO projects (user_id, parent_id, name, description, position)
|
|
VALUES (:user_id, :parent_id, :name, :description, :position)
|
|
');
|
|
|
|
$stmt->execute([
|
|
'user_id' => $this->getUserId(),
|
|
'parent_id' => $data['parent_id'] ?: null,
|
|
'name' => $data['name'],
|
|
'description' => $data['description'] ?? null,
|
|
'position' => $data['position'] ?? 0,
|
|
]);
|
|
|
|
$projectId = (int) $db->lastInsertId();
|
|
|
|
// Gérer les tags si fournis
|
|
$tags = $this->request->get('tags');
|
|
if (is_array($tags)) {
|
|
$this->syncTags($projectId, $tags);
|
|
}
|
|
|
|
$project = $this->findOrFail($projectId);
|
|
$project['tags'] = $this->getProjectTags($projectId);
|
|
|
|
Response::success($project, 'Projet créé', 201);
|
|
}
|
|
|
|
/**
|
|
* PUT /projects/{id}
|
|
*/
|
|
public function update(): void
|
|
{
|
|
$this->requireAuth();
|
|
|
|
$id = (int) $this->request->getParam('id');
|
|
$this->findOrFail($id);
|
|
|
|
$data = $this->validate([
|
|
'name' => 'min:1|max:100',
|
|
'description' => 'max:65535',
|
|
'parent_id' => 'int',
|
|
'position' => 'int',
|
|
]);
|
|
|
|
// Vérifier que le parent n'est pas le projet lui-même ou un de ses enfants
|
|
if (!empty($data['parent_id'])) {
|
|
$parentId = (int) $data['parent_id'];
|
|
if ($parentId === $id) {
|
|
Response::error('Un projet ne peut pas être son propre parent', 422);
|
|
}
|
|
$this->findOrFail($parentId);
|
|
|
|
// Vérifier que le parent n'est pas un enfant du projet
|
|
if ($this->isDescendant($parentId, $id)) {
|
|
Response::error('Le parent ne peut pas être un sous-projet', 422);
|
|
}
|
|
}
|
|
|
|
$db = Database::getInstance();
|
|
|
|
$fields = [];
|
|
$params = ['id' => $id];
|
|
|
|
if (isset($data['name'])) {
|
|
$fields[] = 'name = :name';
|
|
$params['name'] = $data['name'];
|
|
}
|
|
|
|
if (array_key_exists('description', $data)) {
|
|
$fields[] = 'description = :description';
|
|
$params['description'] = $data['description'];
|
|
}
|
|
|
|
if (array_key_exists('parent_id', $data)) {
|
|
$fields[] = 'parent_id = :parent_id';
|
|
$params['parent_id'] = $data['parent_id'] ?: null;
|
|
}
|
|
|
|
if (isset($data['position'])) {
|
|
$fields[] = 'position = :position';
|
|
$params['position'] = $data['position'];
|
|
}
|
|
|
|
if (!empty($fields)) {
|
|
$sql = 'UPDATE projects SET ' . implode(', ', $fields) . ' WHERE id = :id';
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute($params);
|
|
}
|
|
|
|
// Gérer les tags si fournis
|
|
$tags = $this->request->get('tags');
|
|
if (is_array($tags)) {
|
|
$this->syncTags($id, $tags);
|
|
}
|
|
|
|
$project = $this->findOrFail($id);
|
|
$project['tags'] = $this->getProjectTags($id);
|
|
|
|
Response::success($project, 'Projet mis à jour');
|
|
}
|
|
|
|
/**
|
|
* DELETE /projects/{id}
|
|
*/
|
|
public function destroy(): void
|
|
{
|
|
$this->requireAuth();
|
|
|
|
$id = (int) $this->request->getParam('id');
|
|
$this->findOrFail($id);
|
|
|
|
$db = Database::getInstance();
|
|
|
|
// Les sous-projets et tâches seront supprimés en cascade (FK)
|
|
$stmt = $db->prepare('DELETE FROM projects WHERE id = :id');
|
|
$stmt->execute(['id' => $id]);
|
|
|
|
Response::success(null, 'Projet supprimé');
|
|
}
|
|
|
|
/**
|
|
* Trouver un projet ou retourner 404
|
|
*/
|
|
private function findOrFail(int $id): array
|
|
{
|
|
$db = Database::getInstance();
|
|
|
|
$stmt = $db->prepare('
|
|
SELECT * FROM projects
|
|
WHERE id = :id AND user_id = :user_id
|
|
');
|
|
|
|
$stmt->execute([
|
|
'id' => $id,
|
|
'user_id' => $this->getUserId(),
|
|
]);
|
|
|
|
$project = $stmt->fetch();
|
|
|
|
if (!$project) {
|
|
Response::notFound('Projet non trouvé');
|
|
}
|
|
|
|
return $project;
|
|
}
|
|
|
|
/**
|
|
* Construire l'arborescence des projets
|
|
*/
|
|
private function buildTree(array $projects, ?int $parentId = null): array
|
|
{
|
|
$tree = [];
|
|
|
|
foreach ($projects as $project) {
|
|
if ($project['parent_id'] == $parentId) {
|
|
// Parser les tags
|
|
$project['tags'] = [];
|
|
if (!empty($project['tag_ids'])) {
|
|
$ids = explode(',', $project['tag_ids']);
|
|
$names = explode(',', $project['tag_names']);
|
|
foreach ($ids as $i => $tagId) {
|
|
$project['tags'][] = [
|
|
'id' => (int) $tagId,
|
|
'name' => $names[$i] ?? '',
|
|
];
|
|
}
|
|
}
|
|
unset($project['tag_ids'], $project['tag_names']);
|
|
|
|
// Récursion pour les enfants
|
|
$project['children'] = $this->buildTree($projects, (int) $project['id']);
|
|
|
|
$tree[] = $project;
|
|
}
|
|
}
|
|
|
|
return $tree;
|
|
}
|
|
|
|
/**
|
|
* Récupérer les tags d'un projet
|
|
*/
|
|
private function getProjectTags(int $projectId): array
|
|
{
|
|
$db = Database::getInstance();
|
|
|
|
$stmt = $db->prepare('
|
|
SELECT t.id, t.name, t.color
|
|
FROM tags t
|
|
JOIN project_tags pt ON t.id = pt.tag_id
|
|
WHERE pt.project_id = :project_id
|
|
');
|
|
|
|
$stmt->execute(['project_id' => $projectId]);
|
|
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
/**
|
|
* Récupérer les sous-projets directs
|
|
*/
|
|
private function getChildren(int $projectId): array
|
|
{
|
|
$db = Database::getInstance();
|
|
|
|
$stmt = $db->prepare('
|
|
SELECT * FROM projects
|
|
WHERE parent_id = :parent_id AND user_id = :user_id
|
|
ORDER BY position ASC, name ASC
|
|
');
|
|
|
|
$stmt->execute([
|
|
'parent_id' => $projectId,
|
|
'user_id' => $this->getUserId(),
|
|
]);
|
|
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
/**
|
|
* Vérifier si un projet est un descendant d'un autre
|
|
*/
|
|
private function isDescendant(int $projectId, int $ancestorId): bool
|
|
{
|
|
$db = Database::getInstance();
|
|
|
|
$stmt = $db->prepare('
|
|
SELECT parent_id FROM projects
|
|
WHERE id = :id AND user_id = :user_id
|
|
');
|
|
|
|
$stmt->execute([
|
|
'id' => $projectId,
|
|
'user_id' => $this->getUserId(),
|
|
]);
|
|
|
|
$project = $stmt->fetch();
|
|
|
|
if (!$project || $project['parent_id'] === null) {
|
|
return false;
|
|
}
|
|
|
|
if ((int) $project['parent_id'] === $ancestorId) {
|
|
return true;
|
|
}
|
|
|
|
return $this->isDescendant((int) $project['parent_id'], $ancestorId);
|
|
}
|
|
|
|
/**
|
|
* Synchroniser les tags d'un projet
|
|
*/
|
|
private function syncTags(int $projectId, array $tagIds): void
|
|
{
|
|
$db = Database::getInstance();
|
|
|
|
// Supprimer les associations existantes
|
|
$stmt = $db->prepare('DELETE FROM project_tags WHERE project_id = :project_id');
|
|
$stmt->execute(['project_id' => $projectId]);
|
|
|
|
// Ajouter les nouvelles associations
|
|
if (!empty($tagIds)) {
|
|
$stmt = $db->prepare('
|
|
INSERT INTO project_tags (project_id, tag_id)
|
|
SELECT :project_id, id FROM tags
|
|
WHERE id = :tag_id AND user_id = :user_id
|
|
');
|
|
|
|
foreach ($tagIds as $tagId) {
|
|
$stmt->execute([
|
|
'project_id' => $projectId,
|
|
'tag_id' => (int) $tagId,
|
|
'user_id' => $this->getUserId(),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|