Files
sogoms/clients/prokov/api/controllers/ProjectController.php
Pierre 7e27f87d6f 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>
2025-12-15 19:09:00 +01:00

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(),
]);
}
}
}
}