Merge pull request 'developer' (#5) from developer into master

Reviewed-on: #5
pull/6/head
sundayenglish 14 hours ago
commit 1e9e7405b1
  1. 97
      app/Http/Controllers/Api/ExerciseController.php
  2. 58
      app/Http/Requests/CreateExerciseRequest.php
  3. 24
      app/Http/Resources/BlankResource.php
  4. 24
      app/Http/Resources/ChoiceResource.php
  5. 27
      app/Http/Resources/ExerciseResource.php
  6. 23
      app/Http/Resources/QuestionGroupResource.php
  7. 25
      app/Http/Resources/QuestionResource.php
  8. 22
      app/Http/Resources/SkillResource.php
  9. 16
      app/Models/Exercise.php
  10. 18
      app/Models/ExerciseSkill.php
  11. 4
      app/Models/QuestionBlank.php
  12. 2
      app/Models/User.php
  13. 74
      app/Services/ExerciseService.php
  14. 30
      app/Services/ExerciseSkillService.php
  15. 8
      app/Services/QuestionGroupService.php
  16. 144
      app/Services/QuestionService.php
  17. 21
      app/Services/SkillService.php
  18. 252
      composer.lock
  19. 30
      database/migrations/2025_07_11_013439_create_se_exercise_skills_table.php
  20. 7
      routes/api.php

@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\CreateExerciseRequest;
use App\Services\ExerciseService;
use App\Services\ExerciseSkillService;
use App\Services\QuestionService;
use App\Services\SkillService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ExerciseController extends Controller
{
public function __construct(
ExerciseService $exerciseService,
ExerciseSkillService $exerciseSkillService,
SkillService $skillService,
QuestionService $questionService,
)
{
$this->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.',
]);
}
}

@ -0,0 +1,58 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class CreateExerciseRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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));
}
}

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class BlankResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'position' => $this->position,
'correct_answer' => $this->correct_answer,
'other_answers' => $this->other_answers,
];
}
}

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ChoiceResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'content' => $this->content,
'is_correct' => $this->is_correct,
'position' => $this->position,
];
}
}

@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ExerciseResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
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')),
];
}
}

@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class QuestionGroupResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'content' => $this->content,
'questions' => QuestionResource::collection($this->whenLoaded('questions')),
];
}
}

@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class QuestionResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $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)),
];
}
}

@ -0,0 +1,22 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class SkillResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name
];
}
}

@ -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()

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class ExerciseSkill extends Model
{
use SoftDeletes;
protected $table = 'se_exercise_skills';
protected $fillable = [
'exercise_id',
'skill_id'
];
}

@ -17,6 +17,10 @@ class QuestionBlank extends Model
'other_answers',
];
protected $casts = [
'other_answers' => 'array', // Laravel tự động json_decode
];
public function question()
{
return $this->belongsTo(Question::class, 'question_id');

@ -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.
*

@ -0,0 +1,74 @@
<?php
namespace App\Services;
use App\Http\Resources\ExerciseResource;
use App\Models\Exercise;
class ExerciseService
{
public function getExercises($params)
{
$limit = isset($params['limit']) ? min((int)$params['limit'], 100) : 10;
$offset = isset($params['offset']) ? max((int)$params['offset'], 0) : 0;
$lessonName = $params['search'] ?? null;
$query = Exercise::select('id', 'lesson_name', 'description', 'status', 'level', 'year', 'created_at');
if (!empty($lessonName)) {
$query->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);
}
}

@ -0,0 +1,30 @@
<?php
namespace App\Services;
use App\Models\ExerciseSkill;
class ExerciseSkillService
{
/**
* Xử lý lưu các kỹ năng (skills) cho một bài tập (exercise)
*
* @param array $skills
* @param int $exerciseId
* @return void
*/
public function handleSkillsForExercise($skillIds, $exerciseId)
{
$data = [];
foreach ($skillIds as $skillId) {
$data[] = [
'exercise_id' => $exerciseId,
'skill_id' => $skillId,
];
}
if (!empty($data)) {
ExerciseSkill::insert($data);
}
}
}

@ -0,0 +1,8 @@
<?php
namespace App\Services;
class QuestionGroupService
{
}

@ -0,0 +1,144 @@
<?php
namespace App\Services;
use App\Models\Question;
use App\Models\QuestionBlank;
use App\Models\QuestionChoice;
use App\Models\QuestionGroup;
use App\Models\QuestionType;
use Illuminate\Support\Facades\DB;
class QuestionService
{
public function __construct(
QuestionGroupService $questionGroupService
)
{
$this->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;
}
}
}

@ -0,0 +1,21 @@
<?php
namespace App\Services;
use App\Models\Skill;
use Illuminate\Support\Str;
class SkillService
{
public function getSkillIds($skills)
{
$skillIds = [];
foreach ($skills as $value) {
$skill = Skill::where('code', $value)->first();
if ($skill) {
$skillIds[] = $skill->id;
}
}
return $skillIds;
}
}

252
composer.lock generated

@ -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",

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('se_exercise_skills', function (Blueprint $table) {
$table->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');
}
};

@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Api\ExerciseController;
use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;
use Psr\Http\Message\ServerRequestInterface;
@ -10,3 +11,9 @@ use Laravel\Passport\Passport;
Route::middleware(['auth:api', 'role:admin'])->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');
});

Loading…
Cancel
Save