Diferencia entre revisiones de «PHP Servizos Web»
Ir a la navegación
Ir a la búsqueda
Probando rutas con Chrome
| Línea 621: | Línea 621: | ||
:* Empregando a extensión de Chrome. | :* Empregando a extensión de Chrome. | ||
| − | ::<gallery caption="Probando rutas con | + | ::<gallery caption="Probando rutas con Chrome" widths="600" heights="300px" perrow="2"> |
Image:Php_laravel_1.jpg |Unha vez instalado, o executamos. Prememos no nome da extensión. | Image:Php_laravel_1.jpg |Unha vez instalado, o executamos. Prememos no nome da extensión. | ||
Image:php_laravel_servizos_1.JPG | Exemplo de chamada empregando o verbo GET. | Image:php_laravel_servizos_1.JPG | Exemplo de chamada empregando o verbo GET. | ||
Revisión del 16:48 22 feb 2024
Sumario
Introdución
- Para entender esta sección, os alumnos deben ter realizados os exercicios e visto os vídeos nos que se explica o funcionamento de Laravel.
- Partimos dun proyecto de Laravel de nome beleza, creado coa orde: composer create-project laravel/laravel beleza "10.*"
- Servizo Rest: Esta parte do manual tomou como base o exemplo de creación dun servizo REST de Rafael Veiga.
Manual de exemplo
- Aclaracións ao manual anterior:
- Previamente tedes que ter creada unha base de datos en MySql (eu vou poñer de nome api-new) cun usuario / password de acceso para poder facer operacións de create, alter, drop, insert, update, delete e select sobre a base de datos.
- Unha vez feito, modificade o arquivo .env nas entradas DB_YYYYYYYYY que se atopen.
- Paso 1: Para crear o proxecto: composer create-project laravel/laravel api-new
- Paso 2.1:
- O arquivo para modificar o método run se atopa en: /database/seeders/UserSeeder.php
- Hai que empregar estos uses para que acceda ás clases User e Hash:
- use App\Models\User;
- use Illuminate\Support\Facades\Hash;
- Paso 2.1: O comando é con dous guións: php artisan migrate --seed
- Paso 3.1: O comando é con dous guións: php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
Servizo REST: Características
- REST é o acrónimo de Representational State Transfer.
- Para que sexa considerado REST ten que cumprir estas características:
- Baseado nun protocolo de cliente/servidor sen estado (protocolo Http)
- Cacheable
- Escalable
- Cunhas operacións ben definidas no que os recursos estean identificados de forma única por URIs.
- Ao utilizar o protocolo HTTP, xa vimos que non garda o estado polo que deberemos de enviar os datos necesarios para a autenticación en cada petición que fagamos.
- Cada petición que fagamos levará asociada o uso dun verbo que indicará un tipo de acción a realizar:
- GET: Utilízase para acceder a un recurso
- POST: Envía datos para crear un recurso. Os datos non van na URI, se non no corpo da mensaxe.
- PUT: Utilizado para editar un recurso.
- DELETE: Elimina un recurso
- PATCH: Utilízase para modificar parcialmente un recurso. Normalmente faise con PUT.
- Cando fagamos algunha das operacións anteriores deberemos de informar a quen fixo a petición do éxito ou fracaso da operación. Non existe un estándar que nos obrigue a utilizar uns códigos ou outros, pero deberemos de aproveitar' os código HTML para facilitar o 'consumo' do noso servizo REST:
- 200 → OK : Petición recibida e procesada de forma correcta
- 201 → Created : Petición completada. Creouse un novo recurso
- 204 → No Content: Petición procesada correctamente, pero a resposta non ten ningún contido
- 401 → Unauthorized: A información de autenticación non é válida
- 404 → Not found: O recurso non se atopou.
- Temos neste enlace os código de resposta do protocolo HTTP.
- Temos neste enlace a sintaxe da resposta que debe dar un servizo e cando se deben enviar cada un dos códigos de resposta do protocolo HTTP.
- Cacheable:
- Isto quere dicir que ten que ter a posibilidade de poder 'cachear' os resultados para ter un mellor desempeño.
- As peticións deben indicar se o resultado pode ou non ser cacheado.
- Escalable:
- O servidor encargado de recibir e procesar as respostas debe ser capaz de ser dividido en capas (cada capa se encargará de procesar as peticións de diferentes recursos). Desta forma podemos ter diferentes políticas de seguridade.
- Identificación de recursos mediante URIs:
- Normalmente un servizo REST vai permitir realizar operacións de consulta, actualización, engadir e borrado sobre diferentes elementos de informacións. Es estes elementos denomínanse recursos (por exemplo, engadir un novo libro se xestionamos unha librería, obter o listado de clientes, obter o prezo de todas as pantallas de 14 se xestionamos unha tenda de computadores,...).
- Cando fagamos unha operación sobre un destes recursos, temos que utilizar unha URI que deberá cumprir as seguintes propiedades:
- Deben ser únicas, non pode existir máis dunha URI para identificar o mesmo recurso.
- Deben ser independentes do formato no que queiramos consultar el recurso (a URI debe ser a mesma se devolvemos a información en JSON ou XML, por exemplo)
- Deben manter una xerarquía na ruta do recurso
- No deben indicar accións, polo que non debemos usar verbos á hora de definir unha URI (como viñamos facendo nas aplicacións WEB)
- Por exemplo, se queremos xestionar un recurso 'libros':
- POST http://meusitio.es/libros → Para crear un libro
- GET http://meusitio.es/libros/{id} → Para obter a información dun libro concreto
- PUT http://meusitio.es/libros/{id} → Para modificar os datos dun libro concreto
- DELETE http://meusitio.es/libros/{id} → Para eliminar un libro concreto
- GET http://meusitio.es/libros → Para obter o listado de libros.
- Nota: Normalmente o recurso estará en minúsculas e en plural a no ser que o recurso sexa único (por exemplo a configuración).
- En caso de querer filtrar os recursos faremos uso da URI e enviaremos a información de filtrado usando parámetros da forma: ?param1=valor1¶m2=valor.
- Por exemplo, se queremos obter a lista de libros de informática: http://meusitio.es/libros?tematica=informatica.
- Temos que ter coidado coa xerarquía de recursos e acceder atendendo a dita xerarquía.
- Así se xestiono varias tendas de libros, o acceso a un libro dunha tenda debería ser: http://meusitio.es/tendas/1/libros/5 e non ao revés (http://meusitio.es/libros/5/tendas/1).
- Outras características que deberían cumprir as URI's:
- Utilizar minúsculas y guións ou guións baixos (snake-case) no canto de maiúsculas y minúsculas (CamelCase)
- Non utilizar caracteres que necesiten codificación URL como por exemplo espazos en branco, comillas, etc.
- Non utilizar parámetros de consulta (?tipo=1) en peticións que no sexan de consulta.
- Formato de resposta:
- Normalmente o formato vai ser XML ou JSON.
- Como comentamos anteriormente na URI non debe ir ningunha información sobre o tipo de formato da resposta.
- O tipo de formato da resposta será indicado na cabeceira da petición.
Creación dun servizo web
Base de datos
- Partimos da seguinte base de datos de nome belleza.
CREATE SCHEMA `belleza` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_spanish2_ci ;
- A base de datos está composta polas seguintes táboas:
Táboa MARCAS:
CREATE TABLE `belleza`.`marcas` ( `id` INT NOT NULL AUTO_INCREMENT, `descripcion` VARCHAR(100) NOT NULL, PRIMARY KEY (`id`));
Táboa perfumes:
CREATE TABLE `belleza`.`perfumes` ( `id` int NOT NULL AUTO_INCREMENT, `descripcion` varchar(45) NOT NULL, `prezo` decimal(8,2) NOT NULL, `data_compra` date DEFAULT NULL, `marca_id` int NOT NULL, PRIMARY KEY (`id`), KEY `fk_PERFUMES_1_idx` (`id`), KEY `fk_PERFUMES_MARCA_idx` (`marca_id`), CONSTRAINT `fk_PERFUMES_MARCA` FOREIGN KEY (`marca_id`) REFERENCES `marcas` (`id`) ON DELETE NO ACTION ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_spanish2_ci;
- Algúns datos de exemplo:
INSERT INTO `marcas` VALUES (1,'CHANNEL'),(2,'ADIDAS'),(3,'ARAMIS');
INSERT INTO `perfumes` VALUES (1,'Channel 1',46.22,'2017-02-01',1),(2,'Adidas',1.33,NULL,2),(18,'Best Aramis',54.32,'2024-01-18',3);
Nota: Indicar que Laravel como xa comentamos anteriormente permite a creación de táboas usando migracións e enchendo de datos con seeders.
- Agora en Laravel creamos os modelos correspondentes a cada táboa:
- Podemos crealos dende consola, coa orde: php artisan make:model NomeDoModelo
- Arquivo /app/Models/Marca.php
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Marca extends Model { protected $table ='marcas'; // Non faria falta poñelo protected $primaryKey='id'; // Non faria falta poñelo public $timestamps = false; protected $fillable = [ 'descipcion' ]; // Se queremos que certos campos non sexan enviados en formato json os poñemos na propiedade $hidden // protected $hidden = ['password']; public function perfumes(){ return $this->hasMany('App\Perfume','marca_id','id'); } }
- Arquivo /app/Perfume.php
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Perfume extends Model { protected $table ='perfumes'; protected $primaryKey='id'; public $timestamps = false; protected $dates = ['data_compra']; //protected $dateFormat = 'd-m-Y'; protected $fillable = [ 'descipcion', 'prezo', 'data_compra', 'marca_id' ]; // Se queremos que certos campos non sexan enviados en formato json os poñemos na propiedade $hidden // protected $hidden = ['password']; public function marca(){ return $this->belongsTo('App\Marca','marca_id','id'); } }
- Liña 15: Se devolvemos datos en json podemos facer uso do atributo $dateFormat para que os campos de tipo Date teñan un formato específico. O problema desta solución é que despois da un erro cando engadimos datos....
Rutas
- Nota: Lembrar que nun servizo non existen formularios de entrada de datos, polo que as rutas/funcións que dan acceso a ditos formularios sobran.
- Partimos da idea de que un perfume sempre vai pertencer a unha marca e polo tanto non existe se non existe a marca.
- As rutas que imos ter van ser:
- POST http://meusitio.es/marcas → Para crear unha marca
- GET http://meusitio.es/marcas/{marca} → Para obter a información dunha marca concreta
- PUT http://meusitio.es/marcas/{marca} → Para modificar os datos dunha marca concreta
- DELETE http://meusitio.es/marcas/{marca} → Para eliminar unha marca concreta
- GET http://meusitio.es/marcas → Para obter o listado de marcas.
- POST http://meusitio.es/marcas/{marca}/perfumes → Para crear un perfume
- PUT http://meusitio.es/marca/{marca}/perfumes/{perfumes} → Para modificar os datos dun perfume concreto
- GET http://meusitio.es/marcas/{marca}/perfumes → Para obter o listado de perfumes dunha marca concreta.
- DELETE http://meusitio.es/marcas/{marca}/perfumes/{id} → Para eliminar un perfume concreto
- GET http://meusitio.es/perfumes → Para obter o listado de perfumes.
- GET http://meusitio.es/perfumes/{id} → Para obter a información dun perfume concreto
- Imos ter polo tanto dous controladores asociados a estas rutas:
- MarcaController
- PerfumeController
- Ningún deles vai ter unha vista que vaia a devolver ao usuario, polo tanto nas rutas podemos non crear as opcións 'create' e 'edit' xa que estas son empregadas para amosar unha vista (formulario) para crear un modelo e modificar un modelo.
- Os comandos para crear ditos controladores serán:
- Versión 5.1 ou anteriores:
php artisan make:controller MarcaController php artisan make:controller PerfumeController
- Versión 10 ou posteriores:
php artisan make:controller MarcaController --api --model=Marca php artisan make:controller PerfumeController --api --model=Perfume
- O que estamos a facer poñendo --api é que o controlador non teña os métodos create e edit.
- O que estamos a facer poñendo --model=Modelo é que os métodos do controlador van ter como entrada un obxecto da clase asociada.
- Por exemplo, no caso de Marca:
public function destroy(Marca $marca) { // }
- Se non temos --model=Modelo, o que esperaría destroy sería o valor numérico id que se correspondería coa clave primaria da marca a borrar (se pode facer con calquera das dúas formas)
- Agora necesitamos crear as rutas que van a relacionar unha url cun método do controlador.
- Importante: A partires de Laravel 5.3 as rutas do servizo REST deberían gardarse no arquivo routes/api.php e as rutas 'normais' no arquivo routes/web.php.
- Ao facelo desta forma, se optimizan os recursos empregados por Laravel para atender as peticións (nas peticións normais se están aplicando filtros para inicializar a sesión, as cookies, os bindings e a protección CSRF).
- Se queremos acceder a unha ruta que se atopa definida en api.php, teremos que empregar a palabra api na ruta desta forma: http://meusitio.es/api/marcas/{marcas}.
- O podemos facer de forma automática ou ben manualmente.
- Automaticamente:
- Editamos o arquivo /app/routes/api.php e escribimos esta liña:
use App\Http\Controllers\MarcaController; .... Route::apiResource('marcas', MarcaController::class); // Ao ser apiResource xa non engade as rutas create e edit
- O anterior crea as seguintes rutas de forma automática:
GET|HEAD api/marcas ............................................................................ marcas.index › MarcaController@index POST api/marcas ............................................................................ marcas.store › MarcaController@store GET|HEAD api/marcas/{marca} ...................................................................... marcas.show › MarcaController@show PUT|PATCH api/marcas/{marca} .................................................................. marcas.update › MarcaController@update DELETE api/marcas/{marca} ................................................................ marcas.destroy › MarcaController@destroy
- Manualmente, editando o arquivo /app/routes/api.php
Route::get('/marcas', [MarcaController::class, 'index']); Route::post('/marcas', [MarcaController::class, 'store']); Route::get('/marcas/{marca}', [MarcaController::class, 'show']); Route::put('/marcas/{marca}', [MarcaController::class, 'update']); Route::delete('/marcas/{marca}', [MarcaController::class, 'destroy']);
- É unha práctica recomendable antepoñer un prefixo ás rutas para indicar a versión da API que estamos a definir. De tal forma que se cambiamos as rutas ou parámetros nunha nova versión, poderíamos deixar funcionando a versión anterior da api. Para poñer un prefixo temos que facer no arquivo /app/routes/api.php:
use App\Http\Controllers\MarcaController; ..... Route::group(['prefix'=>'v1'], function() { Route::apiResource('marcas', MarcaController::class); });
- Utilizando números de versións podemos facer que diferentes clientes utilicen diferentes versións nas que podemos aumentar as funcionalidades. Por exemplo nunha versión v2 poderíamos ter unha nova ruta (teríamos que repetir todas as rutas anteriores máis a nova).
- Unha vez creadas as rutas podemos comprobalas có comando: php artisan route:list
- Polo de agora có anterior cubrimos as rutas que teñen que ver coa marca.
- Faltan as rutas dos perfumes.
- Primeiro debemos crear o controlador para os perfumes se non o temos feito anteriormente: php artisan make:controller PerfumeController --api --model=Perfume
- Agora para as rutas, poderíamos facer o mesmo que con marca, pero desa forma a ruta creada para dar de alta un perfume sería: POST /api/v1/perfumes.
- Sen embargo, se queremos empregar o nomeado de rutas nun Restful debería ser algo parecido a isto: POST /api/v1/marcas/XX/perfumes, indicando que se quere dar de alta un perfume da marca XX.
- Se lembrades, iso é o que estaba posto ao principio de todo:
- POST http://meusitio.es/marcas/{marca}/perfumes → Para crear un perfume
- PUT http://meusitio.es/marca/{marca}/perfumes/{perfumes} → Para modificar os datos dun perfume concreto
- GET http://meusitio.es/marcas/{marca}/perfumes → Para obter o listado de perfumes dunha marca concreta.
- DELETE http://meusitio.es/marcas/{marca}/perfumes/{id} → Para eliminar un perfume concreto
- Como podemos lograr isto ?
- Pois indicando que perfume depende de marca no arquivo onde definimos as rutas.
- Arquivo /app/routes/api.php
use App\Http\Controllers\MarcaController; use App\Http\Controllers\PerfumeController; ..... Route::group(['prefix'=>'v1'], function() { Route::apiResource('marcas', MarcaController::class); Route::apiResource('marcas.perfumes', PerfumeController::class); });
- Fixarse como está posto 'marcas.perfumes'. Isto fai que se cre as seguintes rutas:
GET|HEAD api/v1/marcas/{marca}/perfumes ............................................. marcas.perfumes.index › PerfumeController@index POST api/v1/marcas/{marca}/perfumes ............................................. marcas.perfumes.store › PerfumeController@store GET|HEAD api/v1/marcas/{marca}/perfumes/{perfume} ..................................... marcas.perfumes.show › PerfumeController@show PUT|PATCH api/v1/marcas/{marca}/perfumes/{perfume} ................................. marcas.perfumes.update › PerfumeController@update DELETE api/v1/marcas/{marca}/perfumes/{perfume} ............................... marcas.perfumes.destroy › PerfumeController@destroy
- Pero sobre isto podemos facer unha mellora. Se vos fixades, para borrar un perfume concreto estou poñendo como ruta: api/v1/marcas/{marca}/perfumes/{perfume} pero isto non ten moito sentido, xa que {perfume} vai ser o perfume concreto. Non necesitaría enviarlle a marca á que pertence dito perfume.
- As rutas de show, update e delete non necesitan a marca. Para simplificar ditas rutas podemos empregar un método shallow() da forma:
use App\Http\Controllers\MarcaController; use App\Http\Controllers\PerfumeController; ..... Route::group(['prefix'=>'v1'], function() { Route::apiResource('marcas', MarcaController::class); Route::apiResource('marcas.perfumes', PerfumeController::class)->shallow(); });
- Desta forma se crean estas rutas:
GET|HEAD api/v1/marcas/{marca}/perfumes ............................................. marcas.perfumes.index › PerfumeController@index POST api/v1/marcas/{marca}/perfumes ............................................. marcas.perfumes.store › PerfumeController@store GET|HEAD api/v1/perfumes/{perfume} ........................................................... perfumes.show › PerfumeController@show PUT|PATCH api/v1/perfumes/{perfume} ....................................................... perfumes.update › PerfumeController@update DELETE api/v1/perfumes/{perfume} ..................................................... perfumes.destroy › PerfumeController@destroy
- Se vos fixades, tanto store (crea un perfume da marca indicada) como index (amosa todos os perfumes da marca indicada) van necesitar o id da marca ao que pertence o perfume.
- Polo tanto imos necesitar modificar os métodos do controlador para que reciban dita información dende a url.
- Arquivo /app/Http/Controllers/PerfumeController.php
.... /** * Display a listing of the resource. */ public function index($marca) { // } /** * Store a newly created resource in storage. */ public function store(Request $request,$marca) { // } .....
- Agora soamente quedan por definir a seguinte ruta:
- GET http://meusitio.es/perfumes → Para obter o listado de perfumes.
- Creamos unha función no controlador de Perfumes (/app/Http/Controllers/PerfumeController.php) para esta rutas:
- public function index_todos() {}: Que amosará todos os perfumes.
- Creamos a ruta no arquivo /app/routes/api.php:
.... Route::group(['prefix'=>'v1'], function() { Route::apiResource('marcas', MarcaController::class); Route::apiResource('marcas.perfumes', PerfumeController::class)->shallow(); Route::get('perfumes',[PerfumeController::class,'index_todos']); }); .....
- Có anterior temos todas estas rutas:
GET|HEAD api/v1/marcas ......................................................................... marcas.index › MarcaController@index POST api/v1/marcas ......................................................................... marcas.store › MarcaController@store GET|HEAD api/v1/marcas/{marca} ................................................................... marcas.show › MarcaController@show PUT|PATCH api/v1/marcas/{marca} ............................................................... marcas.update › MarcaController@update DELETE api/v1/marcas/{marca} ............................................................. marcas.destroy › MarcaController@destroy GET|HEAD api/v1/marcas/{marca}/perfumes ............................................. marcas.perfumes.index › PerfumeController@index POST api/v1/marcas/{marca}/perfumes ............................................. marcas.perfumes.store › PerfumeController@store GET|HEAD api/v1/perfumes .............................................................................. PerfumeController@index_todos GET|HEAD api/v1/perfumes/{perfume} ........................................................... perfumes.show › PerfumeController@show PUT|PATCH api/v1/perfumes/{perfume} ....................................................... perfumes.update › PerfumeController@update DELETE api/v1/perfumes/{perfume} ..................................................... perfumes.destroy › PerfumeController@destroy
Probando as rutas
- IMPORTANTE:
- Lembrar que debe estar iniciado o servidor web de Laravel, executando dende consola e dentro do cartafol do proxecto: php artisan serve
- Lembrar que a ruta, se a tedes 'agrupada' polo número de versión será: http://localhost:8000/api/v1/???????
- Antes de probar as rutas imos a modificar o Controller para que amose un texto en cada un dos métodos que vai responder a cada unha das rutas.
- Arquivo: /app/Http/Controller/MarcaController.php
<?php namespace App\Http\Controllers; use App\Models\Marca; use Illuminate\Http\Request; class MarcaController extends Controller { /** * Display a listing of the resource. */ public function index() { return "Amosa todas as marcas"; } /** * Store a newly created resource in storage. */ public function store(Request $request) { return "Crea una nova marca: $request->descripcion"; } /** * Display the specified resource. */ public function show(Marca $marca) { return "Amosa os datos da marca: $marca->descripcion"; } /** * Update the specified resource in storage. */ public function update(Request $request, Marca $marca) { return "Modifica os datos ($request->descripcion) da marca $marca->id"; } /** * Remove the specified resource from storage. */ public function destroy(Marca $marca) { return "Borra a marca $marca->id"; } }
Arquivo: /app/Http/Controller/PerfumeController.php
<?php namespace App\Http\Controllers; use App\Models\Perfume; use Illuminate\Http\Request; class PerfumeController extends Controller { public function index_todos(){ return 'Amosa todos os perfumes'; } /** * Display a listing of the resource. */ public function index($marca) { return "Amosa todos os perfumes da marca indicada $marca->id"; } /** * Store a newly created resource in storage. */ public function store(Request $request,$marca) { return "Garda un perfume novo ($request->descripcion) que pertence á marca indicada: $marca->id"; } /** * Display the specified resource. */ public function show(Perfume $perfume) { return "Amosa os datos do perfume indicado: $perfume->descripcion"; } /** * Update the specified resource in storage. */ public function update(Request $request, Perfume $perfume) { return "Modifica os datos ($request->descripcion) do perfume indicado ($perfume->id)"; } /** * Remove the specified resource from storage. */ public function destroy(Perfume $perfume) { return "Borra o perfume: $perfume->id"; } }
- Para comprobalas podemos facer uso:
- Da orde curl
- Máis información neste enlace.
- Complemento para:
- Firefox:
- Chome::
- Exemplos para probar as rutas:
- Empregando a extensión de Chrome.
- Empregando o comando curl:
curl -i "http://localhost:8000/api/v1/marcas"
- Resultado:
HTTP/1.1 200 OK Host: localhost:8000 Date: Thu, 22 Feb 2024 16:39:38 GMT Connection: close X-Powered-By: PHP/8.1.2-1ubuntu2.14 Content-Type: text/html; charset=UTF-8 Cache-Control: no-cache, private Date: Thu, 22 Feb 2024 16:39:38 GMT X-RateLimit-Limit: 60 X-RateLimit-Remaining: 59 Access-Control-Allow-Origin: * Amosa todas as marcas
- Comando:
curl -i -H "Accept: application/json" -X POST "http://localhost:8000/api/v1/marcas"
- Resultado:
HTTP/1.1 200 OK Host: localhost:8000 Date: Thu, 22 Feb 2024 16:40:34 GMT Connection: close X-Powered-By: PHP/8.1.2-1ubuntu2.14 Content-Type: text/html; charset=UTF-8 Cache-Control: no-cache, private Date: Thu, 22 Feb 2024 16:40:34 GMT X-RateLimit-Limit: 60 X-RateLimit-Remaining: 58 Access-Control-Allow-Origin: * Crea una nova marca
Devolvendo datos json
- Agora é o momento de implementar o código dos controladores para que devolvan en formato json os datos a quen faga uso da API REST que estamos a implementar.
- Soamente temos que chamar á función toJson para converter os datos a dito modelo da forma:
$jsonPerfumes = Perfumes::all()->toJson();
- Unha vez convertido temos que enviar a resposta ao cliente.
- Isto o faremos có obxecto response da forma: response()->json(['variable'=>'valor']);
- Desta forma convirte a json.
- Poderíamos converter os datos primeiro a json da forma:
- $marcasjson= Marca::all()->toJson();
- Pero iso xa o fai o response...
- Se ademais queremos devolver un código de resposta HTTP o poñeremos ao final: response()->json(['variable'=>'valor','status'=>'ok'],200);
- Máis información neste enlace.
- Todas as respostas e formatos deste exercicio están seguindo as normas indicadas neste enlace.
- Arquivo app/Http/Controller/MarcasController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Requests; use App\Http\Controllers\Controller; use App\Marca; use Validator; class MarcasController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { return response()->json(['status'=>'ok','data'=>Marca::all()],200); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { if(!$request->input('descripcion')){ return response()->json(['errors'=>['status'=>'422','title'=>'Datos incompletos','detail'=>'Falta a descripcion da marca']],400); } $novaMarca = new Marca; $novaMarca->descripcion=$request->input("descripcion"); $novaMarca->save(); return response()->json(['status'=>'ok','data'=>$novaMarca],201)->header('Location', 'http://www.dominio.es/marcas/'.$novaMarca->id_marca)->header('Content-Type', 'application/json'); } /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($idMarca) { $marca = Marca::find($idMarca); if (!$marca){ return response()->json(['errors'=>['status'=>'404','title'=>'Marca non atopada']],404); } return response()->json(['status'=>'ok','data'=>$marca],200); } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $idMarca) { // Validamos os datos que nos chegan: $validator = Validator::make($request->all(),[ 'descripcion' => 'required|min:1|max:100' ]); if ($validator->fails()){ return response()->json(['errors'=>['status'=>'422','title'=>'Datos incompletos','detail'=>'Falta a descripcion da marca ou ten mais de 100 caracteres']],400); } // Pode vir polo método PUT ou polo método PATCH // Non imos facer distinción. $modificado=false; $marcaModificar = Marca::find($idMarca); if (!$marcaModificar){ return response()->json(['errors'=>['status'=>'404','title'=>'Marca non atopada']],404); } if($request->has('descripcion')){ $modificado=true; $marcaModificar->descripcion=$request->input('descripcion'); } // Non se modificou nada. Neste caso non pode darse o caso xa que obrigamos a enviar a descripcion no caso de modificación. Pero se tivésemos máis campos, teríamos que quitar o atribute required. if(!$modificado) { return response()->json(['errors'=>['status'=>'304','title'=>'Marca non modificada']],304); } $marcaModificar->save(); return response()->json(['status'=>'ok'],200); } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($idMarca) { $marcaBorrar = Marca::find($idMarca); if (!$marcaBorrar){ return response()->json(['errors'=>['status'=>'404','title'=>'Marca non atopada']],404); } // Comprobamos se temos perfumes asociados á marca if($marcaBorrar->perfumes && $marcaBorrar->perfumes->count()>0){ return response()->json(['errors'=>['status'=>'409','title'=>'Existen perfumes asociados']],409); } $marcaBorrar->delete(); return response()->json('',204); } }
- Arquivo app/Http/Controller/PerfumesController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Requests; use App\Http\Controllers\Controller; use App\Perfume; class PerfumesController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { return response()->json(['status'=>'ok','data'=>Perfumes::all()],200); } /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($idPerfume) { $perfume=Perfume::find($idPerfume); if (!$perfume){ return response()->json(['errors'=>['status'=>'404','title'=>'Perfume non atopado']],404); } return response()->json(['status'=>'ok','data'=>$perfume],200); } }
- Arquivo app/Http/Controller/MarcasPerfumesController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Requests; use App\Http\Controllers\Controller; use App\Marca; use App\Perfume; use Validator; class MarcasPerfumesController extends Controller { // Formato dd/mm/YYYY => devuelve YYYY-mm-dd function cambiaf_a_mysql($fecha){ if (empty($fecha)) return null; $numeroDia = substr($fecha,0,2); $mes = substr($fecha,3,2); $anio = substr($fecha,6,4); return $anio . '-' . $mes . '-' . $numeroDia; } /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index($idMarca) { $marca = Marca::find($idMarca); if (!$marca){ return response()->json(['errors'=>['status'=>'404','title'=>'Marca non atopada']],404); } $perfumes = $marca->perfumes()->get(); return response()->json(['status'=>'ok','data'=>$perfumes],200); } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request,$idMarca) { // Validamos os datos que nos chegan: $validator = Validator::make($request->all(),[ 'descripcion' => 'required|max:45', 'prezo' => 'required|regex:/[0-9]{1,3},[0-9]{2}/', 'data_compra' => 'date_format:"d/m/Y"' ]); if ($validator->fails()){ return response()->json(['errors'=>['status'=>'422','title'=>'Datos incompletos','detail'=>'Faltan datos ou con formato incorrecto: descripcion, prezo, data_compra']],400); } // Comprobamos se a marca existe $marca = Marca::find($idMarca); if (!$marca){ return response()->json(['errors'=>['status'=>'404','title'=>'Marca non atopada']],404); } //SE NON TIVISEMOS QUE FORMATEAR OS DATOS PODERÍAMOS FACELO ASÍ: // $marca->perfumes()->create($request->all()); // pero desta forma teríamos que enviar o id da marca tamén como dato e comprobar que sexa o mesmo que $idMarca $novoPerfume=new Perfume; $novoPerfume->descripcion=$request->input('descripcion'); $novoPerfume->prezo=str_replace(',','.',$request->input('prezo')); $novoPerfume->data_compra=$this->cambiaf_a_mysql($request->input('data_compra')); $novoPerfume->marca_id=$idMarca; $novoPerfume->save(); return response()->json(['status'=>'ok','data'=>$novoPerfume],201)->header('Location', 'http://www.dominio.es/perfumes/' . $novoPerfume->id_perfume)->header('Content-Type', 'application/json'); } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $idMarca,$idPerfume) { // Validamos os datos que nos chegan: $validator = Validator::make($request->all(),[ 'descripcion' => 'min:1|max:45', 'prezo' => 'regex:/[0-9]{1,3},[0-9]{2}/', 'data_compra' => 'date_format:"d/m/Y"', 'marca_id' => 'integer' ]); if ($validator->fails()){ return response()->json(['errors'=>['status'=>'422','title'=>'Datos incompletos','detail'=>'Faltan datos ou con formato incorrecto: descripcion, prezo, data_compra']],400); } if($request->has('marca_id')){ // Comprobamos se a marca nova existe $marcadestino = Marca::find($request->input('marca_id')); if (!$marcadestino){ return response()->json(['errors'=>['status'=>'422','title'=>'Datos incompletos','detail'=>'A marca non existe']],400); } } // Comprobamos se a marca existe $marca = Marca::find($idMarca); if (!$marca){ return response()->json(['errors'=>['status'=>'404','title'=>'Marca non atopada']],404); } // Comprobamos se o perfume existe $perfume=$marca->perfumes()->find($idPerfume); if (!$perfume){ return response()->json(['errors'=>['status'=>'404','title'=>'Perfume non atopado']],404); } $modificado=false; if($request->has('descripcion')){ $perfume->descripcion=$request->input('descripcion'); $modificado=true; } if($request->has('prezo')){ $perfume->prezp=str_replace(',','.',$request->input('prezo')); $modificado=true; } if($request->has('data_compra')){ $perfume->data_compra=$this->cambiaf_a_mysql($request->input('data_compra')); $modificado=true; } if($request->has('marca_id')){ $perfume->marca_id=$request->input('marca_id'); $modificado=true; } if(!$modificado) { return response()->json(['errors'=>['status'=>'304','title'=>'Marca non modificada']],304); } $perfume->save(); return response()->json(['status'=>'ok','data'=>$perfume],200); } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($idMarca,$idPerfume) { $marca = Marca::find($idMarca); if (!$marca){ return response()->json(['errors'=>['status'=>'404','title'=>'Marca non atopada']],404); } // Comprobamos se o perfume existe $perfumeBorrar=$marca->perfumes()->find($idPerfume); if (!$perfumeBorrar){ return response()->json(['errors'=>['status'=>'404','title'=>'Perfume non atopado']],404); } //$perfumeBorrar->delete(); return response()->json('',204); } }
Autenticar
- Tedes máis información en:
- ajgallego.gitbooks.io => Punto Autenticación HTTP básica sin estado.
- Documentación oficial => Punto Autenticación HTTP básica sin estado.
- Como comentamos anteriormente, un servizo REST non ten estado, polo que teremos que enviar en cada petición os datos necesarios para autenticar ao usuario.
- Normalmente as petición que realizan consulta non necesitan autenticación, pero si as que van a facer operacións de modificación na base de datos.
Autenticación manual
- Neste caso teremos que ter creada unha táboa 'Usuarios' cos campos que nos interesen que se vaian a utilizar para validar.
- Normalmente teremos un campo password 'encriptado' coa orde bcrypt de Laravel.
- Os pasos son so seguintes:
- Imos crear unha táboa que teña un campo password que é o que imos a utilizar para deixar ou non realizar unha operación.
- Creamos a táboa en MYSQL:
CREATE TABLE `USERS_LARAVEL` ( `id` int(11) NOT NULL AUTO_INCREMENT, `password` varchar(100) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1;
- Modificamos o arquivo app\User.php e quitamos a opción de que o usuario poida resetear o password e indicamos que campos van poder ser 'escritos' (fillable) e que campos non van poder enviarse en formato json (hidden).
<?php namespace App; use Illuminate\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; //use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Foundation\Auth\Access\Authorizable; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; //use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; class User extends Model implements AuthenticatableContract, AuthorizableContract //,CanResetPasswordContract { use Authenticatable, Authorizable; //, CanResetPassword; /** * The database table used by the model. * * @var string */ protected $table = 'USERS_LARAVEL'; public $timestamps = false; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = ['password']; /** * The attributes excluded from the model's JSON form. * * @var array */ protected $hidden = ['password']; }
- Creamos un usuario. Para iso o podemos facer dende Laravel facendo un 'create' do modelo User (User::create(['password' => bcrypt('password')]);) ou ben podemos ir á consola e poñer o comando: php artisan tinker
- A continuación teclearemos: bcrypt('password')
- O resultado o copiaremos en Mysql nun rexistro novo dun usuario novo.
- Premeremos exit para saír.
- Agora na función dentro do controlador, que queiramos protexer teremos que escribir:
public function store(Request $request) { if (Auth::attempt(['password' => $request->password])){ // Retornamos a información } else{ return response('Unauthorized.', 401); } }
- Como vemos o usuario terá que enviar como parámetro o password para poder facer operación.
- Poderíamos poñer máis campos dentro do array da forma: Auth::attempt(['user' => $request->user, 'password' => $request->password])
Autenticación proporcionada por Laravel
- Os pasos son so seguintes:
- Escribimos o seguinte comando dende consola: php artisan migrate
- Isto crea tres táboas: migrate, users, password_resets (máis información sobre as migracións neste enlace.
- A nos soamente importa a táboa users (as outras poderíamos borralas se non imos facer uso de formularios para o reseteo de password e rexistro de usuarios e a de migrate se non imos traballar con migraciones).
- Creamos un middleware có comando: php artisan make:middleware AutenticacionBasica
- Escribimos o seguinte código:
<?php namespace App\Http\Middleware; use Illuminate\Support\Facades\Auth; use Closure; class AutenticacionBasica { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { return Auth::onceBasic() ?: $next($request); } }
- Usamos un tipo de autenticación básica no que se pide un login-password para ademais non utilizamos cookies para gardar a autenticación no caso dos servizos (stateless). Máis información neste enlace.
- Modificamos o arquivo /app/Http/Kernel.php e rexistramos o middleware:
.............. protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'auth.basic.once' => \App\Http\Middleware\AutenticacionBasica::class, ];
- Agora teremos que enviar como login o email e o password (os campos que utiliza por defecto para autenticarse).
- Podemos modificar isto cambiando o middleware: return Auth::onceBasic('password') ?: $next($request);
- Agora soamente verificará que o password sexa correcto.
- Tamén podemos poñer outro campo para o 'login', por exemplo: return Auth::onceBasic('emial','password') ?: $next($request);
- Esto é igual ao comportamento por defecto...
- Debemos rexistrar o middleware no controlador. Existen varias formas de facelo, unha delas é no constructor:
.................. public function __construct(){ $this->middleware('auth.basic.once'); }
- Se queremos que o middleware afecta soamente a algún/s método/s: $this->middleware('auth.basic.once',['only' => ['store']]);
- Agora no controlador, se queremos verificar se o usuario pasou a autenticación podemos facelo coa orde: Auth::check() (facendo un use da clase Auth).
- Para enviar a información do login/password:
- Para facelo, esta información ten que ir na cabeceira.
- Se estamos a utilizar o programa HTTP REQUEST debemos premer no botón Authentification e aparecerán dúas caixas de texto, unha para o login e outra para o password.
- Esta información irá na cabeceira codificada en base64 polo que pode ser facilmente descifrada se os paquetes son capturados.
- Se queremos utilizar un cliente que se conecte á nosa API (isto o veremos no seguinte punto):
$client = new \GuzzleHttp\Client(); $res = $client->get('http://localhost:8000/marcas',[ 'auth' => ['username', 'password' ] ]); echo $res->getStatusCode(); // 200 $datos = json_decode($res->getBody(),true);
Usar un servizo externo
- * Comando para instalación: composer require guzzlehttp/guzzle
Exemplo de código
- Neste código imos amosar como facer unha chamada a unha API Externa.
........ use \GuzzleHttp\Client; class ApiExternaController extends Controller { public function index() { $client = new \GuzzleHttp\Client(); $res = $client->get('https://XXXX.XXXXX.XX/api/XXXXXXXX/XXXXXX/json?key=XXXXXXXXXXXXXX&query=parametro&language=es'); echo $res->getStatusCode(); // 200 $datos = json_decode($res->getBody(),true); var_dump($datos); } ..................
- Liña 3: Temos que facer uso da clase Client de GuzzleHttp.
- Liña 10: Facemos a petición a API Externa. Fixarse que normalmente teredes que enviar algún tipo de autenticación. Neste caso enviamos unha API Key xa que o servizo pertence a Google.
- Dita API espera recibir na URL o formato de saída (xml ou json), e tamén pode levar parámetros como uha cadea de busca ou a linguaxe do resultado.
- Liña 11: Podemos obter o código de erro devolto pola API (200 é correcto como xa vimos anteriormente).
- Liña 12: Utilizamos a función json_decode có parámetro a true para que devolva os datos como un array asociativo.
- O var_dump dará como resultado:
array (size=4) 'html_attributions' => array (size=0) empty 'next_page_token' => string 'CvQB5QAAAB-Q718qEtEfzZnccxK0THwIjfCrwGbkVsvEoQaiGyMWBj6Z3-DW1_9UWJaKANfpAahtoMP7AEjcb1JzLzRvJa9gBIueMXhxGzSPhHwKXxiI6Gp2NVQUHRcr0wW2VgAmVHKCGYid01wI8C_r0SaD3iHIB-0Fr5h2IRy4DsmjN6L9oJMaOQTBtAgCN7sCH6imRmMd2lq5XZW9pWHA3JeOAJt4HhKb34X8fjNNKgiIvOi0XPPcRPoIYJ-NcphGShTgA_Fhnl02vyqnn8mbzGMA9xU4yBObOx-cfd5qMKHYLdb5p0L6ctNa_SIV5YM_YUSFwRIQthy2TYyPTmUNwbBfFDMP5xoUPNoKN8AtwpDHFVgOb44DjIBGffU' (length=383) 'results' => array (size=20) 0 => array (size=10) 'formatted_address' => string 'Rúa MarÃa, 2, 15402 Ferrol, A Coruña, España' (length=48) 'geometry' => array (size=2) ... 'icon' => string 'https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png' (length=67) 'id' => string '7c7088e7c654ba30efa1e384e0881bebee8cf065' (length=40) 'name' => string 'Asador Gavia' (length=12) 'photos' => array (size=1) ... 'place_id' => string 'ChIJ22OacwV2Lg0RLObpSmq3D6k' (length=27) 'rating' => float 4.2 'reference' => string 'CmRSAAAAG-LM_2yc9TTbC8l4HoczU2OPnFPs6eYHzoLwJBwGeSiFT9XvalSDcOb0bLZXHDxp3tSAswl57eXpcZGvAIDiYfckbnl7eIxhLl722qHdn4qv-py6_umtqt2pvzoWGC3EEhA8MgKa1gD9lEzOD_EGSqlgGhT8YjPOdYP3d9C9oB9IdHEr92yvEQ' (length=190) 'types' => array (size=4) ... 1 => array (size=10) 'formatted_address' => string 'Lugar Bosque, 73 - Bajo, 15405 Ferrol, A Coruña, España' (length=57) 'geometry' => array (size=2) ... 'icon' => string 'https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png' (length=67) 'id' => string 'd0d3a7fb96efe558e00268ba18bf7c9f7e003d67' (length=40) 'name' => string 'Medulio' (length=7) 'photos' => array (size=1) ... 'place_id' => string 'ChIJI7bO3KPYLQ0R5CVGKcTigTg' (length=27) 'rating' => float 4.2 'reference' => string 'CmRRAAAA_pB5EDe4puf3vqpGI2ACBvg5C5AchtIyS306-m7j_TBzbYsN9KNz8zU-W2u7qhPh1WptaIC5La2JXJxOeGfIlqekxmtKF27PQwz8q503Nbizc7sNH7OKcIhbKm_rssA1EhC9ikBsyUqi3ruykyou0NHGGhTpsMBsUvdbBYkcutfmME-AuOwzWA' (length=190) 'types' => array (size
- Liña 6: A nos interésanos a clave 'results', polo tanto: $datos['results'] (que é un array asociativo) accederemos ao conxunto de resultados. Soamente teremos que percorrer dito array (foreach) e por cada elemento ($dato) acceder ás propiedades que nos interesen ($dato['name'] ou $dato['formatted_address'] por exemplo).
- Esta información sería gardada nun array que será enviado á vista para que amose o resultado.
- Se a API espera recibir un login e ou password, debemos envialo desta forma:
..................... $res = $client->get('url',[ 'auth' => ['username', 'password' ] ]);
- Lembrar que esta información envíase cifrada con base64 e se se captura o tráfico pódese descifrar facilmente.
- O ideal sería utilizar unha comunicación https cifrada con SSL.
Seguridade
- Tedes neste enlace unha explicación das diferentes formas de seguridade no envío de contrasinais para acceder a recursos.
- Neste enlace explica en que consiste a seguridade baseada en OAuth2.
- Tedes neste enlace un exemplo para Laravel de OAuth2 (versión Laravel 5.1 ou anterior).
- A partires da versión 5.3 é mellor empregar o indicado na documentación oficial (mirade a versión escollida na parte superior dereita).
-- Ángel D. Fernández González -- (2017).