API con Laravel

Algunos enlaces interesantes:

Un servicio web (en inglés, web service o web services) es una tecnología que utiliza un conjunto de protocolos y estándares que sirven para intercambiar datos entre aplicaciones.

  • Se trata de que una aplicación web intercambie información con otra aplicación mediante el protocolo HTTP.

  • Durante bastante tiempo se usó XML y el estándar SOAP para esta finalidad.

  • Actualmente se usa JSON y el estándar de facto API REST

¿Qué es JSON?

  • JavaScript Object Notation

  • Es una sintáxis creada para codificar objetos en JavaScript

  • Su uso se ha extendido mucho como alternativa a XML por lo que se considera un formato independiente de dicho lenguaje.

¿Qué es un API?

  • Application Programming Interface, Interfaz de Programación de Aplicaciones.

  • Es decir una interfaz que permite crear aplicaciones que usan determinado recurso.

  • Una API web usar el protocolo HTTP para lograr dicho objetivo.

¿Qué es un API REST?

  • Es una API web que sigue determinado stándar de facto.

  • El intercambio de información se basa en el uso de HTTP.

  • Usa un protocolo cliente-servidor sin estado. No pueden usarse cookies para mantener el estado de la sesión.

  • Cacheable. Una respuesta puede definirse como cacheable para evitar peticiones recurrentes.

  • Hipermedia como motor del estado. A partir de una dirección principal pueden obtenerse enlaces a todos los recursos del servicio.

  • Se define un conjunto de operaciones: GET, POST, PUT, DELETE.

  • Las operaciones CRUD se pueden asimilar al anterior conjunto y a unas rutas determinadas:

Ruta

Verbo

Descripción

/studies

GET

Obtenemos todos los estudios

/studies

POST

Creamos un estudio

/studies/{id}

GET

Obtenemos un estudio

/studies/{id}

PUT

Modificamos un estudio

/studies/{id}

PATCH

Modificación parcial

/studies/{id}

DELETE

Borramos un estudio

Se pueden añadir otras rutas para complementar el acceso:

  • Podemos definir búsquedas por un campo:

    GET /studies?code=IFC303
  • Podemos definir búsquedas más complejas:

    GET /studies/search?family=IFC&level=GS
  • Podemos definir ordenaciones, en este caso descendente:

    GET /studies?sort=-updated_at

Y podemos reflejar las relaciones:

//Lista de módulos de un estudio
GET /studies/1/modules 
//Busqueda de un módulo dentro de un estudio
GET /studies/1/modules/3
//Creación de un módulo dentro de un estudio
POST /studies/1/modules
//Modificación de un módulo dentro de un estudio
PUT /studies/1/modules/3
//Borrado de un módulo dentro de un estudio
DELETE /studies/1/modules/3

  • Utilizaremos Postman para testear nuestra API

    • Independiente de que hagamos tests por otro lado

  • Es una herramienta muy extendida

Comprobación API inicial

  • Vamos a crear una ruta inicial:

Route::get('/', function() {
    return "Bienvenido a la API Laravel 20";
});
  • Abre Postman y prueba su funcionamiento.

  • El anterior ejemplo funciona pero es más correcto de la siguiente manera:

    • Definimos el código de estado Http

    • Los arrays ordenados se covierten en arrays JSON

    • Los arrays asociativos se convierten en objetos

    • Los objetos se convierten en objetos:

//usar la función response() y el método json para cambiar el status
//el $data que enviamos podría ser cualqueir cosa: objeto, array,...
Route::get('/', function() {
  $data = ['message' => 'Bienvenido a la API'];
  return response()->json($data, 200);
});

Códigos de estado:

  • 200's usados para respuestas con éxito.

  • 300's usados para redirecciones.

  • 400's usados cuando hay algún problema con la petición.

  • 500's usados cuando hay algún problema con el servidor.

  • Lista de códigos HTTP que se deberían utilizar en la API RESTful:

    • 200 OK - Respuesta a un exitoso GET, PUT, PATCH o DELETE. Puede ser usado también para un POST que no resulta en una creación.

    • 201 Created – [Creada] Respuesta a un POST que resulta en una creación. Debería ser combinado con un encabezado Location, apuntando a la ubicación del nuevo recurso.

    • 204 No Content – [Sin Contenido] Respuesta a una petición exitosa que no devuelve un body (por ejemplo en una petición DELETE)

  • 304 Not Modified – [No Modificada] Usado cuando el cacheo de encabezados HTTP está activo y el cliente puede usar datos cacheados.

  • 400 Bad Request – [Petición Errónea] La petición está malformada, como por ejemplo, si el contenido no fue bien parseado. El error se debe mostrar también en el JSON de respuesta.

  • 401 Unauthorized – [Desautorizada] No se ha podido autenticar al usuario. Sin credenciales o credenciales no válidas.

  • 403 Forbidden – [Prohibida] Cuando la autenticación es exitosa pero el usuario no tiene permiso al recurso en cuestión.

  • 404 Not Found – [No encontrada] Cuando un recurso se solicita un recurso no existente.

  • 405 Method Not Allowed – [Método no permitido] Cuando un método HTTP que está siendo pedido no está permitido para el usuario autenticado.

  • 409 Conflict - [Conflicto] Cuando hay algún conflicto al procesar una petición, por ejemplo en PATCH, POST o DELETE.

  • 410 Gone – [Retirado] Indica que el recurso en ese endpoint ya no está disponible. Útil como una respuesta en blanco para viejas versiones de la API

  • 415 Unsupported Media Type – [Tipo de contenido no soportado] Si el tipo de contenido que solicita la petición es incorrecto

  • 422 Unprocessable Entity – [Entidad improcesable] Utilizada para errores de validación, o cuando por ejemplo faltan campos en una petición.

  • 429 Too Many Requests – [Demasiadas peticiones] Cuando una petición es rechazada debido a la tasa límite .

  • 500 – Internal Server Error – [Error Interno del servidor] Los desarrolladores de API NO deberían usar este código. En su lugar se debería loguear el fallo y no devolver respuesta.

CRUD de Estudios en API

  • Antes de empezar, prueba otras rutas y fuerza la página de error "No encontrado".

  • Obtenemos un error 404 html.

  • Mejor un error 404 limpio en JSON.

  • Debemos definir una ruta de fallback, o ruta por defecto si ninguna otra es la solicitada

Route::fallback(function () {
  return response()->json(['error' => 'No encontrado'], 404);
});

Controlador y rutas

  • Vamos a necesitar un controlador para gestionar nuestro recurso.

  • Ojo, vamos separar los controladores API del resto:

php artisan make:controller Api/StudyController
  • Basta con incluir una ruta resource y añadir el modificador except:

Route::resource('studies', StudyController::class)->except([
    'create', 'edit'
]);

Index

public function index()
{
  return Study::all();  
  //No es lo más correcto porque se devolverían todos los registros. Se recomienda usar Filtros o paginación.
}
// Se debería devolver un objeto con una propiedad como mínimo data y el array de resultados en esa propiedad.
// A su vez también es necesario devolver el código HTTP de la respuesta.
public function index()
{
    // return Study::all();
    $studies = Study::all();

    return response()->json(['status' => 'ok', 'data' => $studies], 200);
}

Show

public function show($id)
{
  // Corresponde con la ruta /studies/{study}
  // Buscamos un study por el ID.
  $study=Study::find($id);

  // Chequeamos si encontró o no el study
  if (! $study)
  {
    // Se devuelve un array errors con los errores detectados y código 404
    return response()->json(['errors'=>(['code'=>404,'message'=>'No se encuentra un studio con ese código.'])],404);
  }

  // Devolvemos la información encontrada.
  return response()->json(['status'=>'ok','data'=>$study],200);
}

Create

    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'code' => 'required|unique:studies,code|max:6',
            'name' => 'required',
            'abreviation' => 'required',
        ]);

        if ($validator->fails()) {
            return response()->json(['errors' => $validator->errors()], 422);            
        }

        $new = Study::create($request->all());
        return response()->json($new, 201);
    }

Update: PUT

public function update(Request $request, $id)
{
    $study = Study::find($id);
    //si no se encuentra 404
    if (!$study) {
        return response()->json([
            'status' => 404,
            'message' => 'No se ha encontrado un estudio con ese código'
        ], 404);
    }
    //si no valida 422
    $validator = Validator::make($request->all(), [
        'code' => 'required|unique:studies,code|max:6',
        'name' => 'required',
        'abreviation' => 'required',
    ]);

    if ($validator->fails()) {
        return response()->json(['errors' => $validator->errors()], 422);            
    }

    //todo ok 201
    $study->fill($request->all());
    $study->save();
    return response()->json([
        'status' => 'ok',
        'data' => $study
    ], 200);
}

Delete

  • Estamos repitiendo la comprobación de 404 una y otra vez.

  • Sería interesante crear un método para no repetir código de esta manera.

//DRY. Don't Repeat Yourself
public function check404($study)
{
    if (!$study) {
        response()->json([
            'status' => 404,
            'message' => 'No se ha encontrado un estudio con ese id'
        ], 404)->send();
        die();
    }
}
public function destroy($id)
{
    $study = Study::find($id);
    $this->check404($study);

    try {
        //status 204: No content
        $study->delete();
        return response()->json([
            'Sin contenido'
        ], 204);
    } catch (\Throwable $th) {
        return response()->json([
            'status' => 'error',
            'message' => 'Borrado fallido',
            'error_message' => $th->getMessage()
        ], 409);
    }
}

Autenticación: JWT

  • REST se define como sin estado. Esto implica la no utilización de cookies y por ende sesiones.

  • JSON Web Token es un estándar usado para este fin.

  • Cuando un usuario hace login recibe un token que debe guardar en su aplicación cliente.

  • En el resto de peticiones debe entregar ese token para ser identificado.

  • Para JWT vamos a usar la librería tymondesigns/jwt-auth

  • Veamos como usarla para:

    • Hacer login

    • Hacer logout

    • Registrar un usuario

    • ...

Instalación

  • Instalar dependencias:

composer require tymon/jwt-auth
  • Tomar fichero de configuración y crear clave de cifrado

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret
  • La clave de cifrado se guarda en .env. Deberemos repetirlo en cada instalación.

  • Debemos modificar el fichero config/auth.php. Apartado guards

  'api' => [
      'driver' => 'jwt',
      'provider' => 'users'   
  ],

Modificar el modelo User

  • Añadimos un "use" en la cabecera

use Tymon\JWTAuth\Contracts\JWTSubject;
  • Más abajo en la definición de clase, implements ...

class User extends Authenticatable implements JWTSubject
  • Por último, añadimos estos métodos:

    /**
    * Get the identifier that will be stored in the subject claim of the JWT.
    *
    * @return mixed
    */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }
    /**
    * Return a key value array, containing any custom claims to be added to the JWT.
    *
    * @return array
    */
    public function getJWTCustomClaims()
    {
        return [];
    }

Rutas y controlador

  • Usaremos un controlador. Lo creamos:

php artisan make:controller Api/AuthController
  • Necesitamos unas rutas en api.php para los métodos de autenticación:

//En la parte superiror:
use App\Http\Controllers\Api\AuthController;

// rutas con este prefijo: /api/auth/....
Route::group([
    'middleware' => 'api',
    'prefix' => 'auth'
    ], function ($router) {
        Route::post('register', [AuthController::class, 'register']);
        Route::post('login', [AuthController::class, 'login']);
        Route::post('logout', [AuthController::class, 'logout']);
        Route::post('refresh',  [AuthController::class, 'refresh']);
        Route::post('me',  [AuthController::class, 'me']);
});

Métodos del controlador

  • Necesitamos añadir algunos "use":

use JWTAuth;
use Auth;
use App\Models\User;
use Illuminate\Support\Facades\Validator;
use Tymon\JWTAuth\Exceptions\JWTException;
  • DRY. Para enviar el token al cliente usaremos este método:

protected function respondWithToken($token, $status=200)
{        
    return response()->json([
        'access_token' => $token,
        'token_type' => 'bearer',
        'expires_in' => auth('api')->factory()->getTTL() * 60
    ], $status);
}
  • Veamos ahora los métodos:

  • El constructor con un middleware.

    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login', 'register']]);
    }
  • Register

    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:6|confirmed',
        ]);
    
        if($validator->fails()){
            return response()->json($validator->errors(), 400);
        };
    
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => bcrypt($request->password),
        ]);
    
        $token = JWTAuth::fromUser($user);
        return response()->json(compact('user','token'), 201);
    }
  • Login

    public function login(Request $request)
    {        
        $credentials = $request->only('email', 'password');
        try {
            if ($token = JWTAuth::attempt($credentials)) {
                return $this->respondWithToken($token); //OK
            } else {
                return response()->json(['error' => 'Credenciales inválidas'], 400);                
            }
        } catch (JWTException $e) {
              \Log::error($e->getMessage());
            return response()->json(['error' => 'No se pudo crear el token'], 500);
        }
    }
  • Logout

    public function logout()
    {
        // auth()->logout();
        Auth::logout();
    
        return response()->json(['message' => 'Salió con éxito']);
    }
  • La información del usuario: me().

    public function me()
    {
        return response()->json(Auth::user());
    }
  • Refresh

    public function refresh()
    {
        return $this->respondWithToken(JWTAuth::refresh(JWTAuth::getToken()));
    }

Autorización

  • Autenticar es identificar al usuario:

    • En web usamos sesión y los controladores que nos brinda Laravel

    • En api acabamos de crear el controalador Api/AuthController

  • El middleware Auth nos permite limitar el acceso a una ruta a usuarios identificados.

  • Pero no nos permite un control de acceso más elaborado

  • Laravel ofrece varias soluciones para esto. Vamos a estudiar las políticas.

  • Una política es una clase guardada den app/Policies

  • Una política va siempre asociada a un modelo.

  • Su creación:

php artisan make:policy StudyPolicy --model=Study

Registro

  • Una vez creada debemos registrarla en el array $policies en AuthServiceProvider

    protected $policies = [
        'App\Study' => 'App\Policies\StudyPolicy',
    ]
  • Si no hacemos esto siempre obtendremos un estado 403 No autorizado

Definir reglas

  • Cada tipo de acceso se asocia a una regla. Una regla será un método dentro de la política.

  • Por defecto las polítcas traen una colección de reglas/métodos.

  • Por ejemplo el método update lo podemos usar para actualizar un estudio. Sus argumentos deben ser el user y un objeto del modelo protegido (Study):

public function update(User $user, Study $study)
{
    return $user->id === $study->user_id;
}
  • Otros métodos se asocian a la clase en vez de a un objeto (create, viewAny, ...). Observa, crear o ver todos los estudios hace referencia a la clase, no a un estudio concreto.

public function create(User $user)
{
    return $user->isAdmin();
}

Autorización web.

  • Para autorizar basta con usar el método autorize() del controlador:

  • Ejemplo sobre una regla de clase:

public function create()
{
    $this->authorize('create', Study::class);
    return view('study.create');
}
  • Ejemplo sobre una regla de objeto

    public function update(Request $request, Study $study)
    {
        $this->authorize('update', $study);
        //resto del código

    }

Autorización web en blade.

  • Podemos ocultar enlaces usando directivas específicas:

@can('create', App\Study::class)
<div class="float-right>
<a href="/studies">Nuevo estudio</a>
</div>
@endcan

.....

@can('update', $study)
<div class="float-right>
<a href="/studies/{{$study->id}}">Editar</a>
</div>
@endcan

Modelos un poco más a fondo:

  • Setters o mutators

  • Nomenclatura:

      set{Attribute}Attribute
  • Ejemplo:

      class User extends Model
      {
          public function setFirstNameAttribute($value)
          {
              $this->attributes['first_name'] = strtolower($value);
          }
      }
  • Getters o accessors

  • Permiten modificar o formatear los datos según están recogidos en la base de datos.

  • Nomenclatura:

      get{Attribute}Attribute
  • Ejemplo:

class User extends Model
{
    public function getFirstNameAttribute($value)
    {
        return ucfirst($value);
    }
}
  • Podemos añadir campos calculados:

      public function getFullNameAttribute()
      {
          return "{$this->first_name} {$this->last_name}";
      }
  • Podemos incluso hacer que esos campos se añadan cuando serializamos objetos (conversión a JSON).

      class User extends Model
      {
          /**
          * The accessors to append to the model's array form.
          *
          * @var array
          */
          protected $appends = ['is_admin', 'fullName'];
      }

Fechas

  • Las fechas se leen como texto de la base de datos

  • Laravel tiene una clase específica para tratar fechas y horas llamada Carbon

  • Podemos forzar que nuestras fechas hagan casting a esta clase cada vez que se leen de la BBDD.

  • Para hacerlo basta con definir la variable $dates:

      class User extends Model
      {
          /**
          * The attributes that should be mutated to dates.
          *
          * @var array
          */
          protected $dates = [
              'seen_at',
          ];
      }
  • Una vez hecho esto podemos usar dicho campo como una fecha:

      {{ $user->seen_at->format('d-m-Y') }}
      {{ $user->seen_at->addDays(30) }}
      {{ $user_seen_at->diffInDays($dt->copy()->addMonth()) }}

Laravel como cliente Http

  • Una aplicación web también puede ser un cliente Http

  • Típicamente podemos hacer que una aplicación web consuma los recursos ofrecidos por una API Rest

  • Vamos a crear una nueva instalación de Laravel (laravel20client)

  • Vamos a crear un controlador StudyController que consuma los recursos de la API que ya hemos creado.

  • Por simplificar, vamos a prescindier de autenticación y autorización

  • Ruta api:

      // Route::resource('studies', StudyController::class);
      Route::resource('studies', EstudioController::class);
  • Código del controlador en la API

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Study;
use Illuminate\Support\Facades\Validator;

class EstudioController extends Controller
{
    public function index()
    {
        $studies = Study::all();
        // return $studies;
        return response()->json([
            'status' => 200,
            'data' => $studies
        ], 200);
    }

    /**
    * Store a newly created resource in storage.
    *
    * @param  \Illuminate\Http\Request  $request
    * @return \Illuminate\Http\Response
    */
    public function store(Request $request)
    {        
        $rules = [
            'code' => 'required|unique:studies,code|max:6',
            'name' => 'required|max:255',
            'abreviation' => 'required|max:255'
        ];        
        $validator = Validator::make($request->all(), $rules);

        if ($validator->fails()) {
            return response()->json([
                'status' => 'error',
                'errors' => $validator->errors()
            ], 422);                
        }

        $study = Study::create($request->all());
        return response()->json([
            'status' => 'ok',
            'data' => $study
        ], 201);
    }

    /**
    * Display the specified resource.
    *
    * @param  int  $id
    * @return \Illuminate\Http\Response
    */
    public function show($id)
    {
        $study = Study::find($id);
        $this->check404($study);

        return response()->json([
            'status' => 'ok',
            'data' => $study
        ], 200);
    }

    public function update(Request $request, $id)
    {
        $study = Study::find($id);     
        $this->check404($study);

        //si no pasa validación 422
        $rules = [
            'code' => 'required|max:6|unique:studies,code,' . $study->id,
            'name' => 'required|max:255',
            'abreviation' => 'required|max:255'
        ];        
        $validator = Validator::make($request->all(), $rules);
        if ($validator->fails()) {
            return response()->json([
                'status' => 'error',
                'errors' => $validator->errors()
            ], 422);                
        }
        $study->fill($request->all());
        $study->save();
        return response()->json([
            'status' => 'ok',
            'data' => $study
        ], 200);                
    }

    /**
    * Remove the specified resource from storage.
    *
    * @param  int  $id
    * @return \Illuminate\Http\Response
    */
    public function destroy($id)
    {
        $study = Study::find($id);
        $this->check404($study);

        try {
            //status 204: No content
            $study->delete();
            return response()->json([
                'Sin contenido'
            ], 204);
        } catch (\Throwable $th) {
            return response()->json([
                'status' => 'error',
                'message' => 'Borrado fallido. Conflicto',
            ], 409);
        }
    }
    //DRY. Don't Repeat Yourself
    public function check404($study)
    {
        if (!$study) {
            response()->json([
                'status' => 404,
                'message' => 'No se ha encontrado un estudio con ese id'
            ], 404)->send();
            die();
        }
    }
  • Crearmos un nuevo proyecto laravel20client

      composer create-project laravel/laravel laravel20cliente
  • Añadimos un controlador StudioController que va a gestionar los estudios de nuestra API (en español por diferenciar):

      php artisan make:controller StudioController
  • Ahora se trata de añadir rutas y métodos a este controlador para completar el CRUD a través de nuestra API

Index

Show

Store

Update

Delete

Index

Last updated