Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Functional\Routines\Http\Controllers;
|
||||
|
||||
use Functional\Routines\Http\Requests\StoreRoutineRequest;
|
||||
use Functional\Routines\Http\Requests\UpdateRoutineRequest;
|
||||
use Functional\Routines\Http\Resources\RoutineResource;
|
||||
use Functional\Routines\Models\Routine;
|
||||
use Functional\Routines\Services\RoutineService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Http\Response as HttpResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* CRUD des routines de l'utilisateur authentifié, strictement scopées par auth()->id().
|
||||
*/
|
||||
class RoutinesController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoutineService $routineService,
|
||||
) {
|
||||
}
|
||||
|
||||
/** GET /api/routines */
|
||||
public function index(): AnonymousResourceCollection
|
||||
{
|
||||
$routines = Routine::query()
|
||||
->where('user_id', auth()->id())
|
||||
->with('routineExercises.exercise')
|
||||
->orderBy('name')
|
||||
->paginate();
|
||||
|
||||
return RoutineResource::collection($routines);
|
||||
}
|
||||
|
||||
/** POST /api/routines */
|
||||
public function store(StoreRoutineRequest $request): JsonResponse
|
||||
{
|
||||
$routine = $this->routineService->create($request->user(), $request->validated());
|
||||
|
||||
return (new RoutineResource($routine))
|
||||
->response()
|
||||
->setStatusCode(Response::HTTP_CREATED);
|
||||
}
|
||||
|
||||
/** GET /api/routines/{routine} */
|
||||
public function show(int $routine): RoutineResource
|
||||
{
|
||||
return new RoutineResource($this->findForUser($routine));
|
||||
}
|
||||
|
||||
/** PUT /api/routines/{routine} */
|
||||
public function update(UpdateRoutineRequest $request, int $routine): RoutineResource
|
||||
{
|
||||
$updated = $this->routineService->update($this->findForUser($routine), $request->validated());
|
||||
|
||||
return new RoutineResource($updated);
|
||||
}
|
||||
|
||||
/** DELETE /api/routines/{routine} */
|
||||
public function destroy(int $routine): HttpResponse
|
||||
{
|
||||
$this->findForUser($routine)->delete();
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
/** Charge la routine (avec ses exercices) en la scopant strictement à l'utilisateur courant. */
|
||||
private function findForUser(int $routineId): Routine
|
||||
{
|
||||
return Routine::query()
|
||||
->where('user_id', auth()->id())
|
||||
->with('routineExercises.exercise')
|
||||
->findOrFail($routineId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Functional\Routines\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* Validation de la création d'une routine, avec la liste (optionnelle) de ses exercices.
|
||||
*/
|
||||
class StoreRoutineRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
// Le scoping par utilisateur est géré par le contrôleur (auth:sanctum + user_id courant).
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'is_public' => ['sometimes', 'boolean'],
|
||||
'estimated_minutes' => ['nullable', 'integer', 'min:1'],
|
||||
'exercises' => ['sometimes', 'array'],
|
||||
'exercises.*.exercise_id' => ['required', 'integer', 'exists:exercises,id'],
|
||||
'exercises.*.position' => ['nullable', 'integer', 'min:0'],
|
||||
'exercises.*.sets' => ['required', 'integer', 'min:1'],
|
||||
'exercises.*.target_reps' => ['required', 'string', 'max:50'],
|
||||
'exercises.*.rest_seconds' => ['nullable', 'integer', 'min:0'],
|
||||
'exercises.*.tempo' => ['nullable', 'string', 'max:50'],
|
||||
'exercises.*.target_weight' => ['nullable', 'numeric', 'min:0'],
|
||||
'exercises.*.notes' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Functional\Routines\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* Validation de la mise à jour d'une routine. Les exercices, si fournis, remplacent
|
||||
* intégralement le pivot routine_exercises existant.
|
||||
*/
|
||||
class UpdateRoutineRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['sometimes', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'is_public' => ['sometimes', 'boolean'],
|
||||
'estimated_minutes' => ['nullable', 'integer', 'min:1'],
|
||||
'exercises' => ['sometimes', 'array'],
|
||||
'exercises.*.exercise_id' => ['required', 'integer', 'exists:exercises,id'],
|
||||
'exercises.*.position' => ['nullable', 'integer', 'min:0'],
|
||||
'exercises.*.sets' => ['required', 'integer', 'min:1'],
|
||||
'exercises.*.target_reps' => ['required', 'string', 'max:50'],
|
||||
'exercises.*.rest_seconds' => ['nullable', 'integer', 'min:0'],
|
||||
'exercises.*.tempo' => ['nullable', 'string', 'max:50'],
|
||||
'exercises.*.target_weight' => ['nullable', 'numeric', 'min:0'],
|
||||
'exercises.*.notes' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Functional\Routines\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* Une ligne du pivot routine_exercises, avec un sous-objet exercice minimal prêt-à-afficher.
|
||||
*/
|
||||
class RoutineExerciseResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'exercise_id' => $this->exercise_id,
|
||||
'position' => $this->position,
|
||||
'sets' => $this->sets,
|
||||
'target_reps' => $this->target_reps,
|
||||
'rest_seconds' => $this->rest_seconds,
|
||||
'tempo' => $this->tempo,
|
||||
'target_weight' => $this->target_weight,
|
||||
'notes' => $this->notes,
|
||||
'exercise' => [
|
||||
'id' => $this->exercise->id,
|
||||
'name' => $this->exercise->name,
|
||||
'image_url' => $this->exercise->image_url,
|
||||
'target' => $this->exercise->target,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Functional\Routines\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* Une routine, avec ses exercices ordonnés (chargés via routineExercises.exercise).
|
||||
*/
|
||||
class RoutineResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'is_public' => $this->is_public,
|
||||
'estimated_minutes' => $this->estimated_minutes,
|
||||
'exercises' => RoutineExerciseResource::collection($this->whenLoaded('routineExercises')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Functional\Routines\Models;
|
||||
|
||||
use Functional\Routines\Database\Factories\RoutineFactory;
|
||||
use Functional\Users\Models\User;
|
||||
use Illuminate\Database\Eloquent\Attributes\UseFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Une routine = liste ordonnée d'exercices avec séries/reps/repos, possédée par un utilisateur.
|
||||
*
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property string|null $description
|
||||
* @property bool $is_public
|
||||
* @property int|null $estimated_minutes
|
||||
*/
|
||||
#[UseFactory(RoutineFactory::class)]
|
||||
class Routine extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id', 'name', 'slug', 'description', 'is_public', 'estimated_minutes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_public' => 'boolean',
|
||||
'estimated_minutes' => 'integer',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function routineExercises(): HasMany
|
||||
{
|
||||
return $this->hasMany(RoutineExercise::class)->orderBy('position');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Functional\Routines\Models;
|
||||
|
||||
use Functional\Exercises\Models\Exercise;
|
||||
use Functional\Routines\Database\Factories\RoutineExerciseFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\UseFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* Pivot enrichi : un exercice positionné dans une routine, avec ses paramètres d'entraînement.
|
||||
*
|
||||
* @property int $position
|
||||
* @property int $sets
|
||||
* @property string $target_reps
|
||||
* @property int|null $rest_seconds
|
||||
* @property string|null $tempo
|
||||
* @property float|null $target_weight
|
||||
* @property string|null $notes
|
||||
*/
|
||||
#[UseFactory(RoutineExerciseFactory::class)]
|
||||
class RoutineExercise extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'routine_id', 'exercise_id', 'position', 'sets', 'target_reps',
|
||||
'rest_seconds', 'tempo', 'target_weight', 'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'position' => 'integer',
|
||||
'sets' => 'integer',
|
||||
'rest_seconds' => 'integer',
|
||||
'target_weight' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function routine(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Routine::class);
|
||||
}
|
||||
|
||||
public function exercise(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Exercise::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Functional\Routines\Providers;
|
||||
|
||||
use Functional\Routines\Database\Seeders\RoutinesSeeder;
|
||||
use Xefi\LaravelOSDD\LayerServiceProvider;
|
||||
|
||||
class RoutinesServiceProvider extends LayerServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
|
||||
$this->loadSeeders([RoutinesSeeder::class]);
|
||||
}
|
||||
|
||||
$this->withRouting(
|
||||
web: __DIR__ . '/../../routes/web.php',
|
||||
api: __DIR__ . '/../../routes/api.php',
|
||||
commands: __DIR__ . '/../../routes/console.php',
|
||||
channels: __DIR__ . '/../../routes/channels.php',
|
||||
);
|
||||
$this->loadSeeders([\Functional\Routines\Database\Seeders\RoutinesSeeder::class]);
|
||||
}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Functional\Routines\Services;
|
||||
|
||||
use Functional\Routines\Models\Routine;
|
||||
use Functional\Users\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Création/mise à jour d'une routine et synchronisation de son pivot routine_exercises.
|
||||
*/
|
||||
class RoutineService
|
||||
{
|
||||
/** Champs propres à la routine (les exercices sont synchronisés à part). */
|
||||
private const ROUTINE_FIELDS = ['name', 'description', 'is_public', 'estimated_minutes'];
|
||||
|
||||
public function create(User $user, array $data): Routine
|
||||
{
|
||||
$routine = new Routine($this->routineAttributes($data));
|
||||
$routine->user_id = $user->id;
|
||||
$routine->slug = Str::slug($data['name']);
|
||||
$routine->save();
|
||||
|
||||
$this->syncExercises($routine, $data['exercises'] ?? []);
|
||||
|
||||
return $routine->load('routineExercises.exercise');
|
||||
}
|
||||
|
||||
public function update(Routine $routine, array $data): Routine
|
||||
{
|
||||
$routine->fill($this->routineAttributes($data));
|
||||
|
||||
if (array_key_exists('name', $data)) {
|
||||
$routine->slug = Str::slug($data['name']);
|
||||
}
|
||||
|
||||
$routine->save();
|
||||
|
||||
if (array_key_exists('exercises', $data)) {
|
||||
$this->syncExercises($routine, $data['exercises']);
|
||||
}
|
||||
|
||||
return $routine->load('routineExercises.exercise');
|
||||
}
|
||||
|
||||
private function routineAttributes(array $data): array
|
||||
{
|
||||
return array_intersect_key($data, array_flip(self::ROUTINE_FIELDS));
|
||||
}
|
||||
|
||||
/** Remplace intégralement le pivot par la liste fournie (stratégie simple : purge puis recrée, dans l'ordre donné). */
|
||||
private function syncExercises(Routine $routine, array $exercises): void
|
||||
{
|
||||
$routine->routineExercises()->delete();
|
||||
|
||||
foreach ($exercises as $index => $exercise) {
|
||||
$routine->routineExercises()->create([
|
||||
'exercise_id' => $exercise['exercise_id'],
|
||||
'position' => $exercise['position'] ?? $index,
|
||||
'sets' => $exercise['sets'],
|
||||
'target_reps' => $exercise['target_reps'],
|
||||
'rest_seconds' => $exercise['rest_seconds'] ?? null,
|
||||
'tempo' => $exercise['tempo'] ?? null,
|
||||
'target_weight' => $exercise['target_weight'] ?? null,
|
||||
'notes' => $exercise['notes'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user