Initial commit - SOGOMS v1.0.0
- 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>
This commit is contained in:
359
clients/prokov/api/controllers/ProjectController.php
Normal file
359
clients/prokov/api/controllers/ProjectController.php
Normal file
@@ -0,0 +1,359 @@
|
||||
<?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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user