Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_KEY=base64:O1PsaY4jsE62WEOT/GrRB4j14iRqAaseOVTDTAuQTx4=
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=sqlite
DB_DATABASE=database/database.sqlite
DB_CONNECTION=pgsql
DB_HOST=pgsql
DB_PORT=5432
DB_DATABASE=ooptimo_api
DB_USERNAME=sail
DB_PASSWORD=password

BROADCAST_DRIVER=log
CACHE_DRIVER=file
Expand Down
2 changes: 2 additions & 0 deletions .env.testing
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
25 changes: 25 additions & 0 deletions Docker/Dockerfile
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"]
190 changes: 190 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Copy link

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 port 8082 for the API. Please update to use consistent port numbers throughout the documentation.


### 🔧 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`)
22 changes: 22 additions & 0 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default error response for unhandled exceptions exposes the raw exception message ($e->getMessage()) which could leak sensitive information in production. Consider using a more generic error message for production environments and only showing detailed errors in development.

Suggested change
return $this->errorResponse($e->getMessage(), 500);
if (config('app.debug')) {
return $this->errorResponse($e->getMessage(), 500);
}
return $this->errorResponse('An unexpected error occurred', 500);

}

/**
* Register the exception handling callbacks for the application.
*/
Expand Down
65 changes: 65 additions & 0 deletions app/Http/Controllers/Api/ContactController.php
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);
}
}
}
29 changes: 29 additions & 0 deletions app/Http/Requests/ContactRequest.php
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'],
];
}
}
Loading