diff --git a/ANSWER.md b/ANSWER.md new file mode 100644 index 0000000..b78f161 --- /dev/null +++ b/ANSWER.md @@ -0,0 +1,107 @@ +# Code Refactor + +## Overview + +This document details the recent enhancements and updates made to the API endpoints, including validation, error handling, HTTP status codes, and OpenAPI annotations. + +## User Controller + +### Index Function + +#### Changes and Enhancements + +- *Error Handling:* + - Wrapped the user fetching logic in a `try-catch` block to handle exceptions. + - Returns an appropriate error message for better error handling. + +- *HTTP Status Codes:* + - Used `Response::HTTP_*` constants for readability and standard compliance. + +### Show Function + +#### Error Handling + +- *Exception Handling:* + - Wrapped the user fetching logic in a `try-catch` block. + - Handles specific exceptions (e.g., `ModelNotFoundException` for non-existent users) and general exceptions. + +#### HTTP Status Codes + +- Used Response: Used Response::HTTP_* constants for readability and standard compliance + +#### Validation + +- Ensured the id parameter is validated by trying to find the user and handling the ModelNotFoundException. + +## Store Function + +### FormRequest Class (StoreUserRequest) + +- *Validation Handling:* + - Utilizes `StoreUserRequest` to automatically handle validation. + - Returns a `400 Bad Request` response with validation errors if validation fails. + +- *User Creation:* + - Creates a user if validation passes. + - Returns a `201 Created` response upon successful creation. + +- *Error Handling:* + - Catches unexpected errors and returns a `500 Internal Server Error` response. + +## Update Function + +#### Validation Handling + +- *UpdateUserRequest:* + - Automatically validates incoming data. + - Updates only the provided fields. If the password is not included, it remains unchanged. + +#### Error Handling + +- *Exception Handling:* + - Catches exceptions and returns appropriate HTTP responses. + +#### Swagger/OpenAPI Annotations + +- *@OA\Put Annotation:* + - Defines the endpoint, operation, request body, and responses. + +- *Response Codes:* + - Includes common responses: `200` for success, `400` for validation errors, `404` for not found, and `500` for internal server errors. + +## Destroy Function + +#### Finding the User + +- *User Retrieval:* + - Utilizes `$this->user->findOrFail($id)` to retrieve the user by ID or throw a `ModelNotFoundException`. + +#### Deleting the User + +- *Deletion Logic:* + - Deletes the user if found. + +#### Exception Handling + +- *ModelNotFoundException:* + - Specifically catches cases where the user is not found. + +- *General Exception:* + - Catches any other unexpected errors. + +## User Model + +### Property Changes + +- *Removed Private Properties:* + - Changed private properties to protected to leverage Eloquent’s default behavior. + +### Updated Docblocks + +- *Docblock Adjustments:* + - Updated descriptions and types to align with actual usage. + +### Nullability + +- *Nullable Properties:* + - Added `?` to nullable `Carbon` properties where appropriate. \ No newline at end of file diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index f4a5826..4e12841 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -2,8 +2,12 @@ namespace App\Http\Controllers; +use App\Http\Requests\StoreUserRequest; +use App\Http\Requests\UpdateUserRequest; use App\Models\User; use Illuminate\Http\Request; +use Illuminate\Http\Response; +use Illuminate\Database\Eloquent\ModelNotFoundException; class UserController extends Controller { @@ -47,16 +51,21 @@ function __construct(User $user) * description="Forbidden" * ) * ) + * + * @return \Illuminate\Http\JsonResponse */ - public function index(Request $request) + public function index() { - return $this->user->get(); + try { + $users = $this->user->all(); + return response()->json($users, Response::HTTP_OK); + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to fetch users'], Response::HTTP_INTERNAL_SERVER_ERROR); + } } /** - * Show a specific user resource - * - * @return User + * Show a specific user resource. * * @OA\Get( * path="/users/{id}", @@ -72,6 +81,7 @@ public function index(Request $request) * description="User ID", * required=true, * in="path", + * @OA\Schema(type="integer") * ), * @OA\Response( * response=200, @@ -85,19 +95,35 @@ public function index(Request $request) * @OA\Response( * response=403, * description="Forbidden" + * ), + * @OA\Response( + * response=404, + * description="Not Found" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error" * ) * ) + * + * @param int $id + * @return \Illuminate\Http\JsonResponse */ - public function show(User $user) + public function show($id) { - return $user; + try { + $user = $this->user->findOrFail($id); + return response()->json($user, Response::HTTP_OK); + } catch (ModelNotFoundException $e) { + return response()->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND); + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to fetch user'], Response::HTTP_INTERNAL_SERVER_ERROR); + } } /** * Store a newly created user in storage. * - * @return User - * * @OA\Post( * path="/users", * operationId="storeUser", @@ -109,38 +135,60 @@ public function show(User $user) * }, * @OA\RequestBody( * required=true, - * @OA\JsonContent(ref="#/components/schemas/User") + * @OA\JsonContent( + * ref="#/components/schemas/User" + * ) * ), * @OA\Response( - * response=200, - * description="Successful operation", + * response=201, + * description="User successfully created", * @OA\JsonContent(ref="#/components/schemas/User") * ), * @OA\Response( + * response=400, + * description="Validation Error", + * @OA\JsonContent( + * @OA\Property(property="errors", type="object", example={"name": ["The name field is required."]}) + * ) + * ), + * @OA\Response( * response=401, * description="Unauthenticated", * ), * @OA\Response( * response=403, * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error" * ) * ) + * + * @param \App\Http\Requests\StoreUserRequest $request + * @return \Illuminate\Http\JsonResponse */ - public function store(Request $request) + public function store(StoreUserRequest $request) { - $data = $request->only([ - 'name', - 'email', - 'password', - ]); + try { + // Extract the required fields from the request + $data = $request->only(['name', 'email', 'password']); - return $this->user->create($data); + // Create a new user with the provided data + $user = $this->user->create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'password' => bcrypt($data['password']), + ]); + + return response()->json($user, Response::HTTP_CREATED); + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to create user'], Response::HTTP_INTERNAL_SERVER_ERROR); + } } /** - * Update a specific user resource - * - * @return User + * Update a specific user resource. * * @OA\Put( * path="/users/{id}", @@ -156,10 +204,13 @@ public function store(Request $request) * description="User ID", * required=true, * in="path", + * @OA\Schema(type="integer") * ), * @OA\RequestBody( * required=true, - * @OA\JsonContent(ref="#/components/schemas/User") + * @OA\JsonContent( + * ref="#/components/schemas/User" + * ) * ), * @OA\Response( * response=200, @@ -167,32 +218,70 @@ public function store(Request $request) * @OA\JsonContent(ref="#/components/schemas/User") * ), * @OA\Response( + * response=400, + * description="Validation Error", + * @OA\JsonContent( + * @OA\Property(property="errors", type="object", example={"email": ["The email field must be a valid email address."]}) + * ) + * ), + * @OA\Response( * response=401, - * description="Unauthenticated", + * description="Unauthenticated" * ), * @OA\Response( * response=403, * description="Forbidden" + * ), + * @OA\Response( + * response=404, + * description="User not found", + * @OA\JsonContent( + * @OA\Property(property="error", type="string", example="User not found") + * ) + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error" * ) * ) + * + * @param \App\Http\Requests\UpdateUserRequest $request + * @param int $id + * @return \Illuminate\Http\JsonResponse */ - public function update(Request $request, User $user) + public function update(UpdateUserRequest $request, $id) { - $data = $request->only([ - 'name', - 'email', - 'password', - ]); + try { + // Retrieve the user by ID + $user = $this->user->findOrFail($id); + + // Extract data from the request + $data = $request->only(['name', 'email', 'password']); + + // Check if the password is provided and needs to be hashed + if ($request->has('password')) { + $data['password'] = bcrypt($data['password']); + } else { + // Remove the password key if not provided + unset($data['password']); + } - $user->update($data); + // Update the user with the validated data + $user->update($data); - return $user; + // Return the updated user + return response()->json($user, Response::HTTP_OK); + } catch (ModelNotFoundException $e) { + // Return a JSON response if the user is not found + return response()->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND); + } catch (\Exception $e) { + // Return a JSON response for other errors + return response()->json(['error' => 'Failed to update user'], Response::HTTP_INTERNAL_SERVER_ERROR); + } } /** - * Remove a specific user resource - * - * @return User + * Remove a specific user resource. * * @OA\Delete( * path="/users/{id}", @@ -208,27 +297,57 @@ public function update(Request $request, User $user) * description="User ID", * required=true, * in="path", + * @OA\Schema(type="integer") * ), * @OA\Response( * response=200, * description="Successful operation", - * @OA\JsonContent(ref="#/components/schemas/User") + * @OA\JsonContent( + * @OA\Property(property="message", type="string", example="User successfully deleted") + * ) + * ), + * @OA\Response( + * response=404, + * description="User not found", + * @OA\JsonContent( + * @OA\Property(property="error", type="string", example="User not found") + * ) * ), * @OA\Response( * response=401, - * description="Unauthenticated", + * description="Unauthenticated" * ), * @OA\Response( * response=403, * description="Forbidden" + * ), + * @OA\Response( + * response=500, + * description="Internal Server Error" * ) * ) + * + * @param int $id + * @return \Illuminate\Http\JsonResponse */ - public function destroy(User $user) + public function destroy($id) { - $user->delete(); + try { + // Attempt to find the user by ID + $user = $this->user->findOrFail($id); + + // Delete the user + $user->delete(); - return $user; + // Return a JSON response indicating successful deletion + return response()->json(['message' => 'User successfully deleted'], Response::HTTP_OK); + } catch (ModelNotFoundException $e) { + // Return a JSON response if the user is not found + return response()->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND); + } catch (\Exception $e) { + // Return a JSON response for other errors + return response()->json(['error' => 'Failed to delete user'], Response::HTTP_INTERNAL_SERVER_ERROR); + } } } diff --git a/app/Http/Requests/StoreUserRequest.php b/app/Http/Requests/StoreUserRequest.php new file mode 100644 index 0000000..568df18 --- /dev/null +++ b/app/Http/Requests/StoreUserRequest.php @@ -0,0 +1,62 @@ + 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users', + 'password' => 'required|string|min:8', + ]; + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes() + { + return [ + 'name' => 'user name', + 'email' => 'email address', + 'password' => 'password', + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages() + { + return [ + 'name.required' => 'The user name is required.', + 'email.required' => 'The email address is required.', + 'email.unique' => 'The email address is already in use.', + 'password.required' => 'The password is required.', + ]; + } +} diff --git a/app/Http/Requests/UpdateUserRequest.php b/app/Http/Requests/UpdateUserRequest.php new file mode 100644 index 0000000..45a45a7 --- /dev/null +++ b/app/Http/Requests/UpdateUserRequest.php @@ -0,0 +1,64 @@ +route('id'); + return [ + 'name' => 'nullable|string|max:255', + 'email' => 'nullable|email|unique:users,email,' . $userId, + 'password' => 'nullable|string|min:8', + ]; + } + + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes() + { + return [ + 'name' => 'user name', + 'email' => 'email address', + 'password' => 'password', + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages() + { + return [ + 'name.required' => 'The user name is required.', + 'email.required' => 'The email address is required.', + 'email.unique' => 'The email address is already in use.', + 'password.required' => 'The password is required.', + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 8ceeafd..ef9e3c4 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -12,9 +12,7 @@ * @OA\Schema( * title="User", * description="User model", - * @OA\Xml( - * name="User" - * ) + * @OA\Xml(name="User") * ) */ class User extends Authenticatable @@ -24,7 +22,7 @@ class User extends Authenticatable /** * The attributes that are mass assignable. * - * @var array + * @var array */ protected $fillable = [ 'name', @@ -35,7 +33,7 @@ class User extends Authenticatable /** * The attributes that should be hidden for serialization. * - * @var array + * @var array */ protected $hidden = [ 'password', @@ -53,115 +51,115 @@ class User extends Authenticatable ]; /** - * User id + * User ID * * @OA\Property( - * property="id", - * description="User id", - * type="integer", - * example=1 + * property="id", + * description="User ID", + * type="integer", + * example=1 * ) * * @var int */ - private int $id; + protected int $id; /** * User name * * @OA\Property( - * property="name", - * description="User name", - * type="string", - * example="John Doe" + * property="name", + * description="User name", + * type="string", + * example="John Doe" * ) * * @var string */ - private string $name; + protected string $name; /** * User email * * @OA\Property( - * property="email", - * description="User email", - * type="string", - * example="example@elevensoft.dev" + * property="email", + * description="User email", + * type="string", + * example="example@elevensoft.dev" * ) * * @var string */ - private string $email; + protected string $email; /** - * User verified at + * User email verified at * * @OA\Property( - * property="email_verified_at", - * description="User email verified at", - * type="datetime", - * example="2021-01-01 00:00:00" + * property="email_verified_at", + * description="User email verified at", + * type="datetime", + * example="2021-01-01 00:00:00" * ) * - * @var Carbon + * @var Carbon|null */ - private Carbon $email_verified_at; + protected ?Carbon $email_verified_at; /** * User password * * @OA\Property( - * property="password", - * description="User password", - * type="string", - * example="password" + * property="password", + * description="User password", + * type="string", + * example="password" * ) * * @var string */ - private string $password; + protected $password; /** * User remember token * * @OA\Property( - * property="remember_token", - * description="User remember token", - * type="string", - * example="token" + * property="remember_token", + * description="User remember token", + * type="string", + * example="token" * ) * - * @var string + * @var string|null */ - private string $remember_token; + protected ?string $remember_token; /** * User created at * * @OA\Property( - * property="created_at", - * description="User created at", - * type="datetime", - * example="2021-01-01 00:00:00" + * property="created_at", + * description="User created at", + * type="datetime", + * example="2021-01-01 00:00:00" * ) * - * @var Carbon + * @var Carbon|null */ - private Carbon $created_at; + protected ?Carbon $created_at; /** * User updated at * * @OA\Property( - * property="updated_at", - * description="User updated at", - * type="datetime", - * example="2021-01-01 00:00:00" + * property="updated_at", + * description="User updated at", + * type="datetime", + * example="2021-01-01 00:00:00" * ) * - * @var Carbon + * @var Carbon|null */ - private Carbon $updated_at; + protected ?Carbon $updated_at; }