New_Micro_Learning/backend/app/Modules/Courses/Http/CourseStructureController.php

305 خطوط
14 KiB
PHP

<?php
namespace App\Modules\Courses\Http;
use App\Http\Controllers\Controller;
use App\Modules\Courses\Application\CourseVersionEditor;
use App\Modules\Courses\Domain\Block;
use App\Modules\Courses\Domain\CourseModule;
use App\Modules\Courses\Domain\CourseVersion;
use App\Modules\Courses\Domain\Lesson;
use App\Modules\Courses\Http\Requests\ReorderStructureRequest;
use App\Modules\Courses\Http\Requests\StoreLessonRequest;
use App\Modules\Courses\Http\Requests\StoreModuleRequest;
use App\Modules\Courses\Http\Requests\UpdateLessonRequest;
use App\Modules\Courses\Http\Requests\UpdateModuleRequest;
use App\Modules\Identity\Application\RolePermissions;
use App\Modules\Identity\Domain\Enums\Permission;
use App\Modules\Tenancy\Application\TenantContext;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
final class CourseStructureController extends Controller
{
public function __construct(private readonly TenantContext $tenant, private readonly RolePermissions $permissions, private readonly CourseVersionEditor $versions) {}
public function storeModule(StoreModuleRequest $request, string $version): JsonResponse
{
$this->authorize($request);
$versionModel = $this->version($version);
$this->versions->assertDraft($versionModel);
$module = CourseModule::query()->create(['organization_id' => $this->tenant->id(), 'course_version_id' => $versionModel->getKey(), 'title' => $request->validated('title'), 'position' => ((int) $versionModel->modules()->max('position')) + 1]);
return response()->json(['data' => $this->modulePayload($module)], 201);
}
public function updateModule(UpdateModuleRequest $request, string $module): JsonResponse
{
$this->authorize($request);
$model = $this->module($module);
$this->versions->assertDraft($this->version($model->course_version_id));
$data = $request->validated();
if ($model->is_locked && (($data['locked'] ?? null) !== false || count($data) !== 1)) {
$this->locked('module');
}
$attributes = [];
if (isset($data['title'])) {
$attributes['title'] = $data['title'];
}
if (array_key_exists('locked', $data)) {
$attributes['is_locked'] = $data['locked'];
}
$model->update($attributes);
return response()->json(['data' => $this->modulePayload($model)]);
}
public function destroyModule(Request $request, string $module): JsonResponse
{
$this->authorize($request);
$model = $this->module($module);
$this->versions->assertDraft($this->version($model->course_version_id));
$this->assertUnlocked($model);
if ($model->lessons()->where('is_locked', true)->exists()) {
$this->locked('lesson');
}
DB::transaction(function () use ($model) {
$versionId = $model->course_version_id;
$position = $model->position;
$model->delete();
CourseModule::query()->where('course_version_id', $versionId)->where('position', '>', $position)->decrement('position');
});
return response()->json(status: 204);
}
public function reorderModules(ReorderStructureRequest $request, string $version): JsonResponse
{
$this->authorize($request);
$model = $this->version($version);
$this->versions->assertDraft($model);
$ids = $request->validated('ids');
$this->assertComplete($ids, $model->modules()->pluck('id')->all());
if ($model->modules()->where('is_locked', true)->exists()) {
$this->locked('module order');
}
$this->reorder(CourseModule::class, 'course_version_id', $model->getKey(), $ids);
return response()->json(['data' => ['ids' => $ids]]);
}
public function storeLesson(StoreLessonRequest $request, string $module): JsonResponse
{
$this->authorize($request);
$moduleModel = $this->module($module);
$version = $this->version($moduleModel->course_version_id);
$this->versions->assertDraft($version);
$this->assertUnlocked($moduleModel);
$lesson = Lesson::query()->create(['organization_id' => $this->tenant->id(), 'course_version_id' => $version->getKey(), 'course_module_id' => $moduleModel->getKey(), 'title' => $request->validated('title'), 'presentation_mode' => $request->validated('presentationMode', 'flow'), 'position' => ((int) $moduleModel->lessons()->max('position')) + 1]);
return response()->json(['data' => $this->lessonPayload($lesson)], 201);
}
public function updateLesson(UpdateLessonRequest $request, string $lesson): JsonResponse
{
$this->authorize($request);
$model = $this->lesson($lesson);
$version = $this->version($model->course_version_id);
$this->versions->assertDraft($version);
$data = $request->validated();
$currentModule = $this->module($model->course_module_id);
$this->assertUnlocked($currentModule);
if ($model->is_locked && (($data['locked'] ?? null) !== false || count($data) !== 1)) {
$this->locked('lesson');
}
DB::transaction(function () use ($model, $version, $data) {
$attributes = [];
if (isset($data['title'])) {
$attributes['title'] = $data['title'];
}
if (isset($data['presentationMode'])) {
$attributes['presentation_mode'] = $data['presentationMode'];
}
if (array_key_exists('locked', $data)) {
$attributes['is_locked'] = $data['locked'];
}
if (isset($data['moduleId']) && $data['moduleId'] !== $model->course_module_id) {
$target = CourseModule::query()->where('organization_id', $this->tenant->id())->where('course_version_id', $version->getKey())->findOrFail($data['moduleId']);
$this->assertUnlocked($target);
$oldModule = $model->course_module_id;
$oldPosition = $model->position;
$attributes['course_module_id'] = $target->getKey();
$attributes['position'] = ((int) $target->lessons()->max('position')) + 1;
$model->update($attributes);
Lesson::query()->where('course_module_id', $oldModule)->where('position', '>', $oldPosition)->decrement('position');
return;
}
$model->update($attributes);
});
return response()->json(['data' => $this->lessonPayload($model->fresh())]);
}
public function destroyLesson(Request $request, string $lesson): JsonResponse
{
$this->authorize($request);
$model = $this->lesson($lesson);
$this->versions->assertDraft($this->version($model->course_version_id));
$this->assertUnlocked($this->module($model->course_module_id));
$this->assertUnlocked($model);
DB::transaction(function () use ($model) {
$moduleId = $model->course_module_id;
$position = $model->position;
$model->delete();
Lesson::query()->where('course_module_id', $moduleId)->where('position', '>', $position)->decrement('position');
});
return response()->json(status: 204);
}
public function reorderLessons(ReorderStructureRequest $request, string $module): JsonResponse
{
$this->authorize($request);
$moduleModel = $this->module($module);
$this->versions->assertDraft($this->version($moduleModel->course_version_id));
$ids = $request->validated('ids');
$this->assertComplete($ids, $moduleModel->lessons()->pluck('id')->all());
if ($moduleModel->is_locked || $moduleModel->lessons()->where('is_locked', true)->exists()) {
$this->locked('lesson order');
}
$this->reorder(Lesson::class, 'course_module_id', $moduleModel->getKey(), $ids);
return response()->json(['data' => ['ids' => $ids]]);
}
private function reorder(string $model, string $parentKey, string $parentId, array $ids): void
{
DB::transaction(function () use ($model, $parentKey, $parentId, $ids) {
$model::query()->where($parentKey, $parentId)->update(['position' => DB::raw('position + 100000')]);
foreach ($ids as $index => $id) {
$model::query()->whereKey($id)->update(['position' => $index + 1]);
}
});
}
private function assertComplete(array $ids, array $actual): void
{
if (count($ids) !== count($actual) || array_diff($ids, $actual) || array_diff($actual, $ids)) {
throw ValidationException::withMessages(['ids' => ['The order must contain every item exactly once.']]);
}
}
public function duplicateModule(Request $request, string $module): JsonResponse
{
$this->authorize($request);
$source = $this->module($module);
$version = $this->version($source->course_version_id);
$this->versions->assertDraft($version);
$this->assertUnlocked($source);
$copy = DB::transaction(function () use ($source, $version) {
$source->load('lessons.blocks');
$module = CourseModule::query()->create([
'organization_id' => $this->tenant->id(), 'course_version_id' => $version->getKey(),
'title' => $source->title.' — کپی', 'position' => ((int) $version->modules()->max('position')) + 1,
'settings' => $source->settings, 'is_locked' => false,
]);
foreach ($source->lessons->sortBy('position') as $lesson) {
$newLesson = Lesson::query()->create([
'organization_id' => $this->tenant->id(), 'course_version_id' => $version->getKey(),
'course_module_id' => $module->getKey(), 'title' => $lesson->title,
'position' => $lesson->position, 'presentation_mode' => $lesson->presentation_mode,
'settings' => $lesson->settings, 'is_locked' => false,
]);
$this->copyBlocks($lesson, $newLesson);
}
return $module;
});
return response()->json(['data' => $this->modulePayload($copy)], 201);
}
public function duplicateLesson(Request $request, string $lesson): JsonResponse
{
$this->authorize($request);
$source = $this->lesson($lesson);
$version = $this->version($source->course_version_id);
$this->versions->assertDraft($version);
$module = $this->module($source->course_module_id);
$this->assertUnlocked($module);
$this->assertUnlocked($source);
$copy = DB::transaction(function () use ($source, $version, $module) {
$lesson = Lesson::query()->create([
'organization_id' => $this->tenant->id(), 'course_version_id' => $version->getKey(),
'course_module_id' => $module->getKey(), 'title' => $source->title.' — کپی',
'position' => ((int) $module->lessons()->max('position')) + 1,
'presentation_mode' => $source->presentation_mode, 'settings' => $source->settings, 'is_locked' => false,
]);
$this->copyBlocks($source, $lesson);
return $lesson;
});
return response()->json(['data' => $this->lessonPayload($copy)], 201);
}
private function copyBlocks(Lesson $source, Lesson $target): void
{
$source->loadMissing('blocks');
foreach ($source->blocks->sortBy('position') as $block) {
Block::query()->create([
'organization_id' => $this->tenant->id(), 'course_version_id' => $target->course_version_id,
'lesson_id' => $target->getKey(), 'type' => $block->type, 'schema_version' => $block->schema_version,
'data' => $block->data, 'style' => $block->style, 'behavior' => $block->behavior,
'responsive' => $block->responsive, 'accessibility' => $block->accessibility,
'position' => $block->position, 'revision' => 1,
]);
}
}
private function assertUnlocked(CourseModule|Lesson $model): void
{
if ($model->is_locked) {
$this->locked($model instanceof CourseModule ? 'module' : 'lesson');
}
}
private function locked(string $target): never
{
throw ValidationException::withMessages(['locked' => ["The {$target} is locked."]]);
}
private function version(string $id): CourseVersion
{
return CourseVersion::query()->where('organization_id', $this->tenant->id())->findOrFail($id);
}
private function module(string $id): CourseModule
{
return CourseModule::query()->where('organization_id', $this->tenant->id())->findOrFail($id);
}
private function lesson(string $id): Lesson
{
return Lesson::query()->where('organization_id', $this->tenant->id())->findOrFail($id);
}
private function modulePayload(CourseModule $module): array
{
return ['id' => $module->getKey(), 'title' => $module->title, 'position' => $module->position, 'locked' => $module->is_locked, 'lessons' => []];
}
private function lessonPayload(Lesson $lesson): array
{
return ['id' => $lesson->getKey(), 'title' => $lesson->title, 'position' => $lesson->position, 'presentationMode' => $lesson->presentation_mode, 'locked' => $lesson->is_locked, 'blockCount' => $lesson->blocks()->count()];
}
private function authorize(Request $request): void
{
abort_unless($this->permissions->allows($request->user(), Permission::CoursesAuthor), 403);
}
}