diff --git a/app/Http/Controllers/Api/ExerciseController.php b/app/Http/Controllers/Api/ExerciseController.php new file mode 100644 index 0000000..1d4a14a --- /dev/null +++ b/app/Http/Controllers/Api/ExerciseController.php @@ -0,0 +1,97 @@ +exerciseService = $exerciseService; + $this->skillService = $skillService; + $this->exerciseSkillService = $exerciseSkillService; + $this->questionService = $questionService; + } + + public function index(Request $request) + { + $exercises = $this->exerciseService->getExercises($request->all()); + + if ($exercises->isEmpty()) { + return response()->json([ + 'status' => true, + 'data' => $exercises, + 'message' => 'Không có dữ liệu.', + ]); + } + + return response()->json([ + 'status' => true, + 'data' => $exercises, + 'message' => 'Lấy danh sách đề thi thành công.', + ]); + } + + public function create(CreateExerciseRequest $request) + { + DB::beginTransaction(); + try { + $data = $request->all(); + $groups = json_decode($data['groups']); + $dataExercises = $request->except('groups','skill'); + $dataSkills = $data['skill']; + + $exerciseId = $this->exerciseService->createExercise($dataExercises); + $skillIds = $this->skillService->getSkillIds($dataSkills); + $this->exerciseSkillService->handleSkillsForExercise($skillIds, $exerciseId); + $this->questionService->createQuestion($groups, $exerciseId); + + DB::commit(); + + return response()->json([ + 'success' => true, + 'message' => 'Tạo đề thi thành công.', + 'exercise_id' => $exerciseId + ], 201); + } catch (\Exception $e) { + DB::rollBack(); + return response()->json([ + 'success' => false, + 'message' => 'Tạo đề thi thất bại.', + 'error' => $e->getMessage() + ], 500); + } + } + + public function detail($id) + { + $exercise = $this->exerciseService->detail($id); + + if (empty($exercise)) { + return response()->json([ + 'status' => true, + 'data' => $exercise, + 'message' => 'Không có dữ liệu.', + ]); + } + + return response()->json([ + 'status' => true, + 'data' => $exercise, + 'message' => 'Lấy danh sách đề thi thành công.', + ]); + } +} diff --git a/app/Http/Requests/CreateExerciseRequest.php b/app/Http/Requests/CreateExerciseRequest.php new file mode 100644 index 0000000..c6453cf --- /dev/null +++ b/app/Http/Requests/CreateExerciseRequest.php @@ -0,0 +1,58 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string', + 'grade_id' => 'required|integer', + 'subject_id' => 'required|integer', + 'category_id' => 'required|integer', + 'skill' => 'required|array', + 'level' => 'required|integer', + 'groups' => 'required' + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Vui lòng nhập tên bài.', + 'grade_id.required' => 'Vui lòng nhập khối lớp.', + 'subject_id.required' => 'Vui lòng nhập môn học.', + 'category_id.required' => 'Vui lòng nhập danh mục', + 'skill.required' => 'Vui lòng nhập tên kĩ năng.', + 'level.required' => 'Vui lòng nhập trình độ.', + 'groups.required' => 'Vui lòng nhập danh sách các nhóm câu hỏi.' + ]; + } + + protected function failedValidation(Validator $validator) + { + throw new HttpResponseException(response()->json([ + 'success' => false, + 'message' => 'Dữ liệu không hợp lệ', + 'errors' => $validator->errors() + ], 422)); + } +} diff --git a/app/Http/Resources/BlankResource.php b/app/Http/Resources/BlankResource.php new file mode 100644 index 0000000..43becc4 --- /dev/null +++ b/app/Http/Resources/BlankResource.php @@ -0,0 +1,24 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'position' => $this->position, + 'correct_answer' => $this->correct_answer, + 'other_answers' => $this->other_answers, + ]; + } +} diff --git a/app/Http/Resources/ChoiceResource.php b/app/Http/Resources/ChoiceResource.php new file mode 100644 index 0000000..850db6d --- /dev/null +++ b/app/Http/Resources/ChoiceResource.php @@ -0,0 +1,24 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'content' => $this->content, + 'is_correct' => $this->is_correct, + 'position' => $this->position, + ]; + } +} diff --git a/app/Http/Resources/ExerciseResource.php b/app/Http/Resources/ExerciseResource.php new file mode 100644 index 0000000..494c69c --- /dev/null +++ b/app/Http/Resources/ExerciseResource.php @@ -0,0 +1,27 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->lesson_name, + 'description' => $this->description, + 'level' => $this->level_label, + 'year' => $this->year, + 'skills' => SkillResource::collection($this->whenLoaded('skills')), + 'question_groups' => QuestionGroupResource::collection($this->whenLoaded('questionGroups')), + ]; + } +} diff --git a/app/Http/Resources/QuestionGroupResource.php b/app/Http/Resources/QuestionGroupResource.php new file mode 100644 index 0000000..2731831 --- /dev/null +++ b/app/Http/Resources/QuestionGroupResource.php @@ -0,0 +1,23 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'content' => $this->content, + 'questions' => QuestionResource::collection($this->whenLoaded('questions')), + ]; + } +} diff --git a/app/Http/Resources/QuestionResource.php b/app/Http/Resources/QuestionResource.php new file mode 100644 index 0000000..c4979b7 --- /dev/null +++ b/app/Http/Resources/QuestionResource.php @@ -0,0 +1,25 @@ + $this->id, + 'content' => $this->content, + 'score' => $this->score, + 'explanation' => $this->explanation, + 'hint' => $this->hint, + 'type' => [ + 'code' => $this->type->code ?? null, + 'name' => $this->type->name ?? null, + ], + 'choices' => $this->when($this->type->code === 'multiple_choice', ChoiceResource::collection($this->choices)), + 'blanks' => $this->when($this->type->code !== 'multiple_choice', BlankResource::collection($this->blanks)), + ]; + } +} diff --git a/app/Http/Resources/SkillResource.php b/app/Http/Resources/SkillResource.php new file mode 100644 index 0000000..8001e20 --- /dev/null +++ b/app/Http/Resources/SkillResource.php @@ -0,0 +1,22 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name + ]; + } +} diff --git a/app/Models/Exercise.php b/app/Models/Exercise.php index 4332595..1cef5f0 100644 --- a/app/Models/Exercise.php +++ b/app/Models/Exercise.php @@ -23,6 +23,18 @@ class Exercise extends Model 'media_object_id', ]; + protected $appends = ['level_label']; + + public function getLevelLabelAttribute() + { + return match ($this->level) { + 0 => 'easy', + 1 => 'normal', + 2 => 'hard', + default => 'normal', + }; + } + public function subject() { return $this->belongsTo(Subject::class, 'subject_id'); @@ -33,9 +45,9 @@ class Exercise extends Model return $this->belongsTo(Category::class, 'category_id'); } - public function skill() + public function skills() { - return $this->belongsTo(Skill::class, 'skill_id'); + return $this->belongsToMany(Skill::class, 'se_exercise_skills', 'exercise_id', 'skill_id'); } public function questionGroups() diff --git a/app/Models/ExerciseSkill.php b/app/Models/ExerciseSkill.php new file mode 100644 index 0000000..a3c6761 --- /dev/null +++ b/app/Models/ExerciseSkill.php @@ -0,0 +1,18 @@ + 'array', // Laravel tự động json_decode + ]; + public function question() { return $this->belongsTo(Question::class, 'question_id'); diff --git a/app/Models/User.php b/app/Models/User.php index caead80..306dcfd 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -15,6 +15,8 @@ class User extends Authenticatable /** @use HasFactory<\Database\Factories\UserFactory> */ use HasApiTokens, HasFactory, Notifiable, HasRoles; + protected $table = 'users_laravel'; + /** * The attributes that are mass assignable. * diff --git a/app/Services/ExerciseService.php b/app/Services/ExerciseService.php new file mode 100644 index 0000000..9c3f08c --- /dev/null +++ b/app/Services/ExerciseService.php @@ -0,0 +1,74 @@ +where('lesson_name', 'like', '%' . $lessonName . '%'); + } + + // Lấy dữ liệu chính + $data = $query->orderByDesc('created_at') + ->skip($offset) + ->take($limit) + ->get() + ->map(function ($item) { + $item->level = match ((int)$item->level) { + 0 => 'easy', + 1 => 'normal', + 2 => 'hard', + default => 'unknown', + }; + return $item; + }); + + return $data; + } + public function createExercise($dataExercise) + { + $dataCreateExercise = [ + 'lesson_name' => $dataExercise['name'] ?? null, + 'description' => $dataExercise['description'] ?? null, + 'subject_id' => $dataExercise['subject_id'] ?? null, + 'level' => $dataExercise['level'] ?? null, + 'status' => $dataExercise['status'] ?? null, + 'category_id' => $dataExercise['category_id'] ?? null, + 'year' => $dataExercise['year'] ?? null, + ]; + + $exercise = Exercise::create($dataCreateExercise); + + return $exercise->id; + } + + public function detail($id) + { + $exercise = Exercise::with([ + 'skills:id,name', + 'questionGroups' => function ($q) { + $q->select('id', 'exercise_id', 'content'); + }, + 'questionGroups.questions' => function ($q) { + $q->select('id', 'content', 'group_id', 'question_type_id', 'description', 'score', 'explanation', 'hint'); + }, + 'questionGroups.questions.type:id,code,name', + 'questionGroups.questions.choices', + 'questionGroups.questions.blanks', + ])->findOrFail($id); + + return new ExerciseResource($exercise); + } +} diff --git a/app/Services/ExerciseSkillService.php b/app/Services/ExerciseSkillService.php new file mode 100644 index 0000000..71895b9 --- /dev/null +++ b/app/Services/ExerciseSkillService.php @@ -0,0 +1,30 @@ + $exerciseId, + 'skill_id' => $skillId, + ]; + } + + if (!empty($data)) { + ExerciseSkill::insert($data); + } + } +} diff --git a/app/Services/QuestionGroupService.php b/app/Services/QuestionGroupService.php new file mode 100644 index 0000000..1ceaaa5 --- /dev/null +++ b/app/Services/QuestionGroupService.php @@ -0,0 +1,8 @@ +questionGroupService = $questionGroupService; + } + + public function createQuestion($data, $exerciseId) + { + $now = now(); + $groupData = []; + $dataQuestion = []; + $dataQuestionChoices = []; + $dataQuestionBlank = []; + $questionMap = []; + + DB::beginTransaction(); + try { + // 1. Chuẩn bị groupData + foreach ($data as $groupKey => $value) { + $groupData[] = [ + 'content' => $value->title, + 'exercise_id' => $exerciseId, + 'is_question_order_fixed' => $value->is_question_order_fixed, + 'is_option_order_fixed' => $value->is_option_order_fixed, + 'position' => $groupKey + 1, + 'paragraph' => $value->paragraph, + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + + // 2. Insert group & lấy lại id + QuestionGroup::insert($groupData); + $groupIds = QuestionGroup::where('exercise_id', $exerciseId) + ->orderByDesc('id') + ->take(count($groupData)) + ->get() + ->reverse() + ->pluck('id') + ->values() + ->toArray(); + + // 3. Chuẩn bị question + $questionIndex = 0; + foreach ($data as $groupKey => $value) { + $groupId = $groupIds[$groupKey]; + foreach ($value->questions as $qIndex => $question) { + $questionType = QuestionType::where('code', $question->type)->first(); + $questionTypeId = $questionType ? $questionType->id : 1; + $dataQuestion[] = [ + 'exercise_id' => $exerciseId, + 'content' => $question->content, + 'description' => $question->description ?? null, + 'group_id' => $groupId, + 'question_type_id' => $questionTypeId, + 'level' => 2, + 'score' => $question->score ?? null, + 'explanation' => $question->explanation ?? null, + 'hint' => $question->hint ?? null, + 'created_at' => $now, + 'updated_at' => $now + ]; + $questionMap["{$groupKey}_{$qIndex}"] = [ + 'type' => $question->type, + 'options' => $question->options ?? [], + 'answers' => $question->answers ?? [], + ]; + $questionIndex++; + } + } + + // 4. Insert question & lấy lại theo custom_key + Question::insert($dataQuestion); + + $questions = Question::where('exercise_id', $exerciseId)->get(); +// $questionsByKey = $questions->keyBy(function ($item) { +// return "{$item->group_id}_{$item->position}"; // hoặc dùng custom_key nếu có +// }); + + // 5. Mapping lại options/answers + $qIndex = 0; + foreach ($questionMap as $key => $meta) { + $questionId = $questions[$qIndex]->id ?? null; + if (!$questionId) continue; + + if ($meta['type'] == 'multiple_choice') { + foreach ($meta['options'] as $optKey => $option) { + $dataQuestionChoices[] = [ + 'question_id' => $questionId, + 'label' => $option->label, + 'content' => $option->content, + 'is_correct' => $option->is_correct, + 'position' => $optKey + 1, + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + } else { + foreach ($meta['answers'] as $ansKey => $answer) { + $dataQuestionBlank[] = [ + 'question_id' => $questionId, + 'correct_answer' => $answer->answer_true, + 'other_answers' => json_encode($answer->other_answers), + 'position' => $ansKey + 1, + 'created_at' => $now, + 'updated_at' => $now, + ]; + } + } + + $qIndex++; + } + + if (!empty($dataQuestionChoices)) { + QuestionChoice::insert($dataQuestionChoices); + } + + if (!empty($dataQuestionBlank)) { + QuestionBlank::insert($dataQuestionBlank); + } + + DB::commit(); + } catch (\Throwable $e) { + DB::rollBack(); + throw $e; + } + } + +} diff --git a/app/Services/SkillService.php b/app/Services/SkillService.php new file mode 100644 index 0000000..c8b314b --- /dev/null +++ b/app/Services/SkillService.php @@ -0,0 +1,21 @@ +first(); + if ($skill) { + $skillIds[] = $skill->id; + } + } + return $skillIds; + } +} diff --git a/composer.lock b/composer.lock index 17384dd..881c691 100644 --- a/composer.lock +++ b/composer.lock @@ -2043,16 +2043,16 @@ }, { "name": "league/flysystem", - "version": "3.29.1", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", "shasum": "" }, "require": { @@ -2076,13 +2076,13 @@ "composer/semver": "^3.0", "ext-fileinfo": "*", "ext-ftp": "*", - "ext-mongodb": "^1.3", + "ext-mongodb": "^1.3|^2", "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "guzzlehttp/psr7": "^2.6", "microsoft/azure-storage-blob": "^1.1", - "mongodb/mongodb": "^1.2", + "mongodb/mongodb": "^1.2|^2", "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", @@ -2120,22 +2120,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" }, - "time": "2024-10-08T08:58:34+00:00" + "time": "2025-06-25T13:29:59+00:00" }, { "name": "league/flysystem-local", - "version": "3.29.0", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", "shasum": "" }, "require": { @@ -2169,9 +2169,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" }, - "time": "2024-08-09T21:24:39+00:00" + "time": "2025-05-21T10:34:19+00:00" }, { "name": "league/mime-type-detection", @@ -2672,16 +2672,16 @@ }, { "name": "nesbot/carbon", - "version": "3.10.0", + "version": "3.10.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9" + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/c1397390dd0a7e0f11660f0ae20f753d88c1f3d9", - "reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00", "shasum": "" }, "require": { @@ -2773,7 +2773,7 @@ "type": "tidelift" } ], - "time": "2025-06-12T10:24:28+00:00" + "time": "2025-06-21T15:19:35+00:00" }, { "name": "nette/schema", @@ -3070,16 +3070,16 @@ }, { "name": "nwidart/laravel-modules", - "version": "v12.0.3", + "version": "v12.0.4", "source": { "type": "git", "url": "https://github.com/nWidart/laravel-modules.git", - "reference": "ffad5c797e6a11d0e2d9a1bad422fa456589531e" + "reference": "6e1f50de63366206b06ec53bbc823282977ddd06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nWidart/laravel-modules/zipball/ffad5c797e6a11d0e2d9a1bad422fa456589531e", - "reference": "ffad5c797e6a11d0e2d9a1bad422fa456589531e", + "url": "https://api.github.com/repos/nWidart/laravel-modules/zipball/6e1f50de63366206b06ec53bbc823282977ddd06", + "reference": "6e1f50de63366206b06ec53bbc823282977ddd06", "shasum": "" }, "require": { @@ -3143,7 +3143,7 @@ ], "support": { "issues": "https://github.com/nWidart/laravel-modules/issues", - "source": "https://github.com/nWidart/laravel-modules/tree/v12.0.3" + "source": "https://github.com/nWidart/laravel-modules/tree/v12.0.4" }, "funding": [ { @@ -3155,7 +3155,7 @@ "type": "github" } ], - "time": "2025-04-28T07:57:29+00:00" + "time": "2025-06-29T09:23:53+00:00" }, { "name": "nyholm/psr7", @@ -3429,16 +3429,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.44", + "version": "3.0.46", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "1d0b5e7e1434678411787c5a0535e68907cf82d9" + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/1d0b5e7e1434678411787c5a0535e68907cf82d9", - "reference": "1d0b5e7e1434678411787c5a0535e68907cf82d9", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", "shasum": "" }, "require": { @@ -3519,7 +3519,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.44" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.46" }, "funding": [ { @@ -3535,7 +3535,7 @@ "type": "tidelift" } ], - "time": "2025-06-15T09:59:26+00:00" + "time": "2025-06-26T16:29:55+00:00" }, { "name": "psr/clock", @@ -3951,16 +3951,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.8", + "version": "v0.12.9", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625" + "reference": "1b801844becfe648985372cb4b12ad6840245ace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/85057ceedee50c49d4f6ecaff73ee96adb3b3625", - "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/1b801844becfe648985372cb4b12ad6840245ace", + "reference": "1b801844becfe648985372cb4b12ad6840245ace", "shasum": "" }, "require": { @@ -4024,9 +4024,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.8" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.9" }, - "time": "2025-03-16T03:05:19+00:00" + "time": "2025-06-23T02:35:06+00:00" }, { "name": "ralouphie/getallheaders", @@ -4150,21 +4150,20 @@ }, { "name": "ramsey/uuid", - "version": "4.8.1", + "version": "4.9.0", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28" + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28", - "reference": "fdf4dd4e2ff1813111bd0ad58d7a1ddbb5b56c28", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", "shasum": "" }, "require": { "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", - "ext-json": "*", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -4223,9 +4222,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.8.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.0" }, - "time": "2025-06-01T06:28:46+00:00" + "time": "2025-06-25T14:20:11+00:00" }, { "name": "spatie/laravel-permission", @@ -4386,16 +4385,16 @@ }, { "name": "symfony/console", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44" + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44", - "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44", + "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", "shasum": "" }, "require": { @@ -4460,7 +4459,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.0" + "source": "https://github.com/symfony/console/tree/v7.3.1" }, "funding": [ { @@ -4476,7 +4475,7 @@ "type": "tidelift" } ], - "time": "2025-05-24T10:34:04+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/css-selector", @@ -4612,16 +4611,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "cf68d225bc43629de4ff54778029aee6dc191b83" + "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/cf68d225bc43629de4ff54778029aee6dc191b83", - "reference": "cf68d225bc43629de4ff54778029aee6dc191b83", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/35b55b166f6752d6aaf21aa042fc5ed280fce235", + "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235", "shasum": "" }, "require": { @@ -4669,7 +4668,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.3.0" + "source": "https://github.com/symfony/error-handler/tree/v7.3.1" }, "funding": [ { @@ -4685,7 +4684,7 @@ "type": "tidelift" } ], - "time": "2025-05-29T07:19:49+00:00" + "time": "2025-06-13T07:48:40+00:00" }, { "name": "symfony/event-dispatcher", @@ -4909,16 +4908,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "4236baf01609667d53b20371486228231eb135fd" + "reference": "23dd60256610c86a3414575b70c596e5deff6ed9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/4236baf01609667d53b20371486228231eb135fd", - "reference": "4236baf01609667d53b20371486228231eb135fd", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/23dd60256610c86a3414575b70c596e5deff6ed9", + "reference": "23dd60256610c86a3414575b70c596e5deff6ed9", "shasum": "" }, "require": { @@ -4968,7 +4967,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.3.0" + "source": "https://github.com/symfony/http-foundation/tree/v7.3.1" }, "funding": [ { @@ -4984,20 +4983,20 @@ "type": "tidelift" } ], - "time": "2025-05-12T14:48:23+00:00" + "time": "2025-06-23T15:07:14+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f" + "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ac7b8e163e8c83dce3abcc055a502d4486051a9f", - "reference": "ac7b8e163e8c83dce3abcc055a502d4486051a9f", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1644879a66e4aa29c36fe33dfa6c54b450ce1831", + "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831", "shasum": "" }, "require": { @@ -5082,7 +5081,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.3.0" + "source": "https://github.com/symfony/http-kernel/tree/v7.3.1" }, "funding": [ { @@ -5098,20 +5097,20 @@ "type": "tidelift" } ], - "time": "2025-05-29T07:47:32+00:00" + "time": "2025-06-28T08:24:55+00:00" }, { "name": "symfony/mailer", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "0f375bbbde96ae8c78e4aa3e63aabd486e33364c" + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/0f375bbbde96ae8c78e4aa3e63aabd486e33364c", - "reference": "0f375bbbde96ae8c78e4aa3e63aabd486e33364c", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368", "shasum": "" }, "require": { @@ -5162,7 +5161,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.3.0" + "source": "https://github.com/symfony/mailer/tree/v7.3.1" }, "funding": [ { @@ -5178,7 +5177,7 @@ "type": "tidelift" } ], - "time": "2025-04-04T09:51:09+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/mime", @@ -6298,16 +6297,16 @@ }, { "name": "symfony/translation", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "4aba29076a29a3aa667e09b791e5f868973a8667" + "reference": "241d5ac4910d256660238a7ecf250deba4c73063" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/4aba29076a29a3aa667e09b791e5f868973a8667", - "reference": "4aba29076a29a3aa667e09b791e5f868973a8667", + "url": "https://api.github.com/repos/symfony/translation/zipball/241d5ac4910d256660238a7ecf250deba4c73063", + "reference": "241d5ac4910d256660238a7ecf250deba4c73063", "shasum": "" }, "require": { @@ -6374,7 +6373,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.3.0" + "source": "https://github.com/symfony/translation/tree/v7.3.1" }, "funding": [ { @@ -6390,7 +6389,7 @@ "type": "tidelift" } ], - "time": "2025-05-29T07:19:49+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/translation-contracts", @@ -6472,16 +6471,16 @@ }, { "name": "symfony/uid", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "7beeb2b885cd584cd01e126c5777206ae4c3c6a3" + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/7beeb2b885cd584cd01e126c5777206ae4c3c6a3", - "reference": "7beeb2b885cd584cd01e126c5777206ae4c3c6a3", + "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", "shasum": "" }, "require": { @@ -6526,7 +6525,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.3.0" + "source": "https://github.com/symfony/uid/tree/v7.3.1" }, "funding": [ { @@ -6542,20 +6541,20 @@ "type": "tidelift" } ], - "time": "2025-05-24T14:28:13+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e" + "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e", - "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", + "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", "shasum": "" }, "require": { @@ -6610,7 +6609,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.0" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.1" }, "funding": [ { @@ -6626,7 +6625,7 @@ "type": "tidelift" } ], - "time": "2025-04-27T18:39:23+00:00" + "time": "2025-06-27T19:55:54+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -7308,16 +7307,16 @@ }, { "name": "laravel/pint", - "version": "v1.22.1", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "941d1927c5ca420c22710e98420287169c7bcaf7" + "reference": "9ab851dba4faa51a3c3223dd3d07044129021024" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/941d1927c5ca420c22710e98420287169c7bcaf7", - "reference": "941d1927c5ca420c22710e98420287169c7bcaf7", + "url": "https://api.github.com/repos/laravel/pint/zipball/9ab851dba4faa51a3c3223dd3d07044129021024", + "reference": "9ab851dba4faa51a3c3223dd3d07044129021024", "shasum": "" }, "require": { @@ -7328,10 +7327,10 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.75.0", - "illuminate/view": "^11.44.7", - "larastan/larastan": "^3.4.0", - "laravel-zero/framework": "^11.36.1", + "friendsofphp/php-cs-fixer": "^3.76.0", + "illuminate/view": "^11.45.1", + "larastan/larastan": "^3.5.0", + "laravel-zero/framework": "^11.45.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.3.1", "pestphp/pest": "^2.36.0" @@ -7341,6 +7340,9 @@ ], "type": "project", "autoload": { + "files": [ + "overrides/Runner/Parallel/ProcessFactory.php" + ], "psr-4": { "App\\": "app/", "Database\\Seeders\\": "database/seeders/", @@ -7370,7 +7372,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2025-05-08T08:38:12+00:00" + "time": "2025-07-03T10:37:47+00:00" }, { "name": "laravel/sail", @@ -7520,16 +7522,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.1", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", "shasum": "" }, "require": { @@ -7568,7 +7570,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" }, "funding": [ { @@ -7576,20 +7578,20 @@ "type": "tidelift" } ], - "time": "2025-04-29T12:36:36+00:00" + "time": "2025-07-05T12:25:42+00:00" }, { "name": "nunomaduro/collision", - "version": "v8.8.1", + "version": "v8.8.2", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "44ccb82e3e21efb5446748d2a3c81a030ac22bd5" + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/44ccb82e3e21efb5446748d2a3c81a030ac22bd5", - "reference": "44ccb82e3e21efb5446748d2a3c81a030ac22bd5", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", "shasum": "" }, "require": { @@ -7675,7 +7677,7 @@ "type": "patreon" } ], - "time": "2025-06-11T01:04:21+00:00" + "time": "2025-06-25T02:12:12+00:00" }, { "name": "phar-io/manifest", @@ -8202,16 +8204,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.23", + "version": "11.5.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "86ebcd8a3dbcd1857d88505109b2a2b376501cde" + "reference": "4ad8fe263a0b55b54a8028c38a18e3c5bef312e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/86ebcd8a3dbcd1857d88505109b2a2b376501cde", - "reference": "86ebcd8a3dbcd1857d88505109b2a2b376501cde", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4ad8fe263a0b55b54a8028c38a18e3c5bef312e0", + "reference": "4ad8fe263a0b55b54a8028c38a18e3c5bef312e0", "shasum": "" }, "require": { @@ -8225,7 +8227,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.9", + "phpunit/php-code-coverage": "^11.0.10", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", @@ -8283,7 +8285,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.26" }, "funding": [ { @@ -8307,7 +8309,7 @@ "type": "tidelift" } ], - "time": "2025-06-13T05:47:49+00:00" + "time": "2025-07-04T05:58:21+00:00" }, { "name": "sebastian/cli-parser", @@ -9301,16 +9303,16 @@ }, { "name": "symfony/yaml", - "version": "v7.3.0", + "version": "v7.3.1", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "cea40a48279d58dc3efee8112634cb90141156c2" + "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/cea40a48279d58dc3efee8112634cb90141156c2", - "reference": "cea40a48279d58dc3efee8112634cb90141156c2", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0c3555045a46ab3cd4cc5a69d161225195230edb", + "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb", "shasum": "" }, "require": { @@ -9353,7 +9355,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.0" + "source": "https://github.com/symfony/yaml/tree/v7.3.1" }, "funding": [ { @@ -9369,7 +9371,7 @@ "type": "tidelift" } ], - "time": "2025-04-04T10:10:33+00:00" + "time": "2025-06-03T06:57:57+00:00" }, { "name": "theseer/tokenizer", diff --git a/database/migrations/2025_07_11_013439_create_se_exercise_skills_table.php b/database/migrations/2025_07_11_013439_create_se_exercise_skills_table.php new file mode 100644 index 0000000..41749ef --- /dev/null +++ b/database/migrations/2025_07_11_013439_create_se_exercise_skills_table.php @@ -0,0 +1,30 @@ +id(); + $table->unsignedBigInteger('exercise_id'); + $table->unsignedBigInteger('skill_id'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('se_exercise_skills'); + } +}; diff --git a/routes/api.php b/routes/api.php index fcd12d9..495a596 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@ get('/user', function (Request $request) { return $request->user(); }); + +Route::middleware('auth:api')->group(function () { + Route::get('/exercises', [ExerciseController::class, 'index'])->name('exercise.index'); + Route::post('/exercise/create', [ExerciseController::class, 'create'])->name('exercise.create'); + Route::get('/exercise/{id}', [ExerciseController::class, 'detail'])->name('exercise.detail'); +});