-
Notifications
You must be signed in to change notification settings - Fork 1
Prueba técnica Backend (Laravel) #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| DB_CONNECTION=sqlite | ||
| DB_DATABASE=:memory: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Laravel 11 + PHP 8.3 + PostgreSQL client + Node 20 | ||
| FROM php:8.3-fpm | ||
|
|
||
| # Install dependencies | ||
| RUN apt-get update && apt-get install -y \ | ||
| git zip unzip curl libpq-dev nodejs npm \ | ||
| && docker-php-ext-install pdo_pgsql | ||
|
|
||
| WORKDIR /var/www/html | ||
|
|
||
| # Copy composer and install dependencies | ||
| COPY --from=composer:latest /usr/bin/composer /usr/bin/composer | ||
| COPY composer.json composer.lock ./ | ||
|
|
||
| RUN composer install --no-interaction --prefer-dist --optimize-autoloader | ||
|
|
||
| # Copy app code | ||
| COPY . . | ||
|
|
||
| # Build assets (optional) | ||
| RUN npm install && npm run build || true | ||
|
|
||
| RUN chown -R www-data:www-data storage bootstrap/cache | ||
|
|
||
| CMD ["php-fpm"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,3 +54,193 @@ make test | |
| ## Notas | ||
| - Mantén el código simple y legible; prefiere claridad a “más features”. | ||
| - Si asumes algo, **descríbelo brevemente** en el README. | ||
|
|
||
|
|
||
|
|
||
| ## 🧩 Notas personales / Observaciones técnicas | ||
|
|
||
| Durante la implementación se utilizó **Laravel 11** con entorno **Dockerizado** mediante *Laravel Sail*, y base de datos **PostgreSQL 16**. | ||
| El contenedor principal expone el proyecto en el puerto `8080`, y se incluye un contenedor adicional de **Adminer** para inspeccionar fácilmente la base de datos (puerto `8081`). | ||
|
|
||
| ### 🔧 Entorno Docker | ||
| - PHP 8.3 (FPM) | ||
| - PostgreSQL 16 | ||
| - Adminer 4.8 | ||
| - Composer + Node integrados en build | ||
| - Makefile con comandos `make setup`, `make serve`, y `make test` | ||
| → simplifican la ejecución y validación del proyecto. | ||
|
|
||
| #### 🗄️ Acceso rápido a la base de datos | ||
| | Parámetro | Valor | | ||
| |------------|--------| | ||
| | **Host** | `pgsql` o `localhost` | | ||
| | **Puerto** | `5432` | | ||
| | **Usuario** | `sail` | | ||
| | **Contraseña** | `password` | | ||
| | **Base de datos** | `contacts_db` | | ||
| | **Adminer** | http://localhost:8081 | | ||
|
|
||
| --- | ||
|
|
||
| ### 🧪 Tests | ||
| Los tests se ejecutan dentro del contenedor usando: | ||
|
|
||
| ``` | ||
| make test | ||
| ``` | ||
|
|
||
| o directamente: | ||
|
|
||
| ``` | ||
| ./vendor/bin/sail artisan test | ||
| ``` | ||
|
|
||
| Los tests utilizan **SQLite en memoria** (archivo `.env.testing`) para un entorno de test rápido y aislado, cubriendo casos: | ||
| - ✅ Creación correcta de contacto | ||
| - 🚫 Email inválido | ||
| - 🚫 Email duplicado | ||
|
|
||
| --- | ||
|
|
||
| ### 📮 Postman | ||
| Se incluye la colección lista para importar: | ||
|
|
||
| 📁 `contacts_api.postman_collection.json` | ||
|
|
||
| Contiene las rutas principales: | ||
| - `POST /api/contacts` → crear contacto | ||
| - `GET /api/contacts` → listar todos | ||
| - `GET /api/contacts/{id}` → obtener uno | ||
| - `PUT /api/contacts/{id}` → actualizar | ||
| - `DELETE /api/contacts/{id}` → eliminar | ||
|
|
||
| Variable global: | ||
| ``` | ||
| {{base_url}} = http://localhost:8082 | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
|
|
||
| ### 🧱 Estructura de respuesta estándar | ||
|
|
||
| Se implementó un formato JSON uniforme para todas las respuestas, tanto en éxito como en error: | ||
|
|
||
| #### ✅ Éxito | ||
| ```json | ||
| { | ||
| "success": true, | ||
| "code": 200, | ||
| "message": "Contact retrieved successfully", | ||
| "data": { | ||
| "id": 1, | ||
| "name": "David Rodriguez", | ||
| "email": "[email protected]", | ||
| "phone": "610180280" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| #### ❌ Error | ||
| ```json | ||
| { | ||
| "success": false, | ||
| "code": 404, | ||
| "message": "Contact not found", | ||
| "errors": null | ||
| } | ||
| ``` | ||
|
|
||
| Este formato es gestionado mediante el trait `App\Traits\ApiResponse` y manejo centralizado de excepciones en `Handler.php`, incluyendo: | ||
| - Validaciones (`422`) | ||
| - Modelos no encontrados (`404`) | ||
| - Errores SQL o de tipo (`400`) | ||
| - Excepciones inesperadas (`500`) | ||
|
|
||
| --- | ||
|
|
||
| ## ⚙️ Guía para ejecutar el proyecto | ||
|
|
||
| A continuación se detallan los pasos necesarios para ejecutar correctamente la API en un entorno local utilizando **Docker**, **Laravel Sail** y **PostgreSQL**. | ||
|
|
||
| ### 🧩 1. Clonar el repositorio | ||
| ``` | ||
| git clone https://github.com/<tu-usuario>/<nombre-del-repo>.git | ||
| cd <nombre-del-repo> | ||
| ``` | ||
|
|
||
| ### 🧰 2. Copiar el archivo de entorno | ||
| ``` | ||
| cp .env.example .env | ||
| ``` | ||
|
|
||
| Asegúrate de configurar las credenciales correctas para PostgreSQL: | ||
|
|
||
| ``` | ||
| DB_CONNECTION=pgsql | ||
| DB_HOST=pgsql | ||
| DB_PORT=5432 | ||
| DB_DATABASE=ooptimo_api | ||
| DB_USERNAME=sail | ||
| DB_PASSWORD=password | ||
| ``` | ||
|
|
||
| ### 🐳 3. Construir y levantar los contenedores | ||
| ``` | ||
| docker compose up -d | ||
| ``` | ||
|
|
||
| Esto levantará: | ||
| - `app` → Contenedor PHP/Laravel | ||
| - `pgsql` → Base de datos PostgreSQL | ||
| - `adminer` → Panel de administración (http://localhost:8081) | ||
|
|
||
| ### 💾 4. Instalar dependencias | ||
| ``` | ||
| docker compose exec laravel.test composer install | ||
| ``` | ||
|
|
||
| ### 🧱 5. Generar la clave de aplicación | ||
| ``` | ||
| docker compose exec laravel.test php artisan key:generate | ||
| ``` | ||
|
|
||
| ### 🗃️ 6. Ejecutar migraciones y seeders | ||
| ``` | ||
| docker compose exec laravel.test php artisan migrate --seed | ||
| ``` | ||
|
|
||
| ### 🚀 7. Acceder a la aplicación | ||
| API → http://localhost:8082/api/contacts | ||
| Adminer → http://localhost:7780 | ||
|
|
||
| ### 🧪 8. Ejecutar tests | ||
| ``` | ||
| make test | ||
| ``` | ||
| O bien: | ||
| ``` | ||
| docker compose exec laravel.test php artisan test | ||
| ``` | ||
|
|
||
| ### 📬 9. Probar con Postman | ||
| Importar la colección `contacts_api.postman_collection.json` y usar: | ||
| ``` | ||
| {{base_url}} = http://localhost:8082 | ||
| ``` | ||
|
|
||
| ### 🔁 10. Detener el entorno | ||
| ``` | ||
| docker compose down | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ✅ **Autor:** David Rodríguez | ||
| **Branch:** `feature/david-rodriguez` | ||
| **Fecha:** Octubre 2025 | ||
| **Framework:** Laravel 11 | ||
| **Base de datos:** PostgreSQL 16 | ||
| **Testing:** PHPUnit + SQLite in-memory | ||
| **Infraestructura:** Docker / Laravel Sail | ||
| **Postman:** Incluido (`contacts_api.postman_collection.json`) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,11 +2,16 @@ | |||||||||||||
|
|
||||||||||||||
| namespace App\Exceptions; | ||||||||||||||
|
|
||||||||||||||
| use App\Traits\ApiResponse; | ||||||||||||||
| use Illuminate\Database\QueryException; | ||||||||||||||
| use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | ||||||||||||||
| use Illuminate\Validation\ValidationException; | ||||||||||||||
| use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; | ||||||||||||||
| use Throwable; | ||||||||||||||
|
|
||||||||||||||
| class Handler extends ExceptionHandler | ||||||||||||||
| { | ||||||||||||||
| use ApiResponse; | ||||||||||||||
| /** | ||||||||||||||
| * The list of the inputs that are never flashed to the session on validation exceptions. | ||||||||||||||
| * | ||||||||||||||
|
|
@@ -18,6 +23,23 @@ class Handler extends ExceptionHandler | |||||||||||||
| 'password_confirmation', | ||||||||||||||
| ]; | ||||||||||||||
|
|
||||||||||||||
| public function render($request, Throwable $e) | ||||||||||||||
| { | ||||||||||||||
| if ($e instanceof ValidationException) { | ||||||||||||||
| return $this->errorResponse('Validation error', 422, $e->errors()); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if ($e instanceof NotFoundHttpException) { | ||||||||||||||
| return $this->errorResponse('Resource not found', 404); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if ($e instanceof QueryException && str_contains($e->getMessage(), 'invalid input syntax for type bigint')) { | ||||||||||||||
| return $this->errorResponse('Invalid ID format. The ID must be a number.', 400); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| return $this->errorResponse($e->getMessage(), 500); | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The default error response for unhandled exceptions exposes the raw exception message (
Suggested change
|
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Register the exception handling callbacks for the application. | ||||||||||||||
| */ | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| <?php | ||
|
|
||
| namespace App\Http\Controllers\Api; | ||
|
|
||
| use App\Http\Controllers\Controller; | ||
| use App\Http\Requests\ContactRequest; | ||
| use App\Http\Resources\ContactResource; | ||
| use App\Models\Contact; | ||
| use App\Traits\ApiResponse; | ||
| use Illuminate\Http\JsonResponse; | ||
| use Illuminate\Database\Eloquent\ModelNotFoundException; | ||
|
|
||
| class ContactController extends Controller | ||
| { | ||
| use ApiResponse; | ||
|
|
||
| public function index(): JsonResponse | ||
| { | ||
| $contacts = ContactResource::collection(Contact::all()); | ||
| return $this->successResponse($contacts); | ||
| } | ||
|
|
||
| public function store(ContactRequest $request): JsonResponse | ||
| { | ||
| $contact = Contact::create($request->validated()); | ||
| return $this->successResponse( | ||
| new ContactResource($contact), | ||
| 'Contact created successfully', | ||
| 201 | ||
| ); | ||
| } | ||
|
|
||
| public function show($id): JsonResponse | ||
| { | ||
| try { | ||
| $contact = Contact::findOrFail($id); | ||
| return $this->successResponse( | ||
| new ContactResource($contact) | ||
| ); | ||
| } catch (ModelNotFoundException $e) { | ||
| return $this->errorResponse('Contact not found', 404); | ||
| } | ||
| } | ||
|
|
||
| public function update(ContactRequest $request, Contact $contact): JsonResponse | ||
| { | ||
| $contact->update($request->validated()); | ||
| return $this->successResponse( | ||
| new ContactResource($contact), | ||
| 'Contact updated successfully' | ||
| ); | ||
| } | ||
|
|
||
| public function destroy($id): JsonResponse | ||
| { | ||
| try { | ||
| $contact = Contact::findOrFail($id); | ||
| $contact->delete(); | ||
|
|
||
| return $this->successResponse(null, 'Contact deleted successfully'); | ||
| } catch (ModelNotFoundException $e) { | ||
| return $this->errorResponse('Contact not found', 404); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| <?php | ||
|
|
||
| namespace App\Http\Requests; | ||
|
|
||
| use Illuminate\Foundation\Http\FormRequest; | ||
|
|
||
| class ContactRequest extends FormRequest | ||
| { | ||
| public function authorize(): bool | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| public function rules(): array | ||
| { | ||
| $contact = $this->route('contact'); | ||
| $contactId = $contact ? $contact->id : null; | ||
|
|
||
| return [ | ||
| 'name' => ['required', 'string'], | ||
| 'email' => [ | ||
| 'required', | ||
| 'email', | ||
| 'unique:contacts,email,' . $contactId, | ||
| ], | ||
| 'phone' => ['nullable', 'string', 'max:25'], | ||
| ]; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's an inconsistency in the port numbers mentioned for the API. In line 63, you mention the project is exposed on port
8080, but in lines 119, 214, and 229, you reference port8082for the API. Please update to use consistent port numbers throughout the documentation.