PHP Servizos Web
Sumario
Introdución
- Arquitectura REST.
- Arquitectura REST e regras que debería cumprirse.
- Que é un servizo REST (en español) e regras que debe cumprir.
- Neste manual non aplica ben as regras para que sexa Rest (os código de erro de resposta HTTP).
Servizo REST: Características
- Información obtida deste enlace.
- 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
- Con unhas 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 composta polas seguintes táboas:
Táboa MARCAS:
1 CREATE TABLE `BELLEZA`.`MARCAS` ( 2 `id_marca` INT NOT NULL AUTO_INCREMENT, 3 `descripcion` VARCHAR(100) NOT NULL, 4 PRIMARY KEY (`id_marca`));
Táboa PERFUMES:
1 CREATE TABLE `PERFUMES` ( 2 `id_perfume` int(11) NOT NULL AUTO_INCREMENT, 3 `descripcion` varchar(45) COLLATE utf8_spanish2_ci NOT NULL, 4 `prezo` decimal(8,2) NOT NULL, 5 `data_compra` date DEFAULT NULL, 6 `marca_id` int(11) NOT NULL, 7 PRIMARY KEY (`id_perfume`), 8 KEY `fk_PERFUMES_1_idx` (`id_perfume`), 9 KEY `fk_PERFUMES_MARCA_idx` (`marca_id`), 10 CONSTRAINT `fk_PERFUMES_MARCA` FOREIGN KEY (`marca_id`) REFERENCES `MARCAS` (`id_marca`) ON DELETE NO ACTION ON UPDATE CASCADE 11 ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8 COLLATE=utf8_spanish2_ci;
- Algúns datos de exemplo:
1 INSERT INTO `MARCAS` VALUES (1,'CHANNEL'),(2,'ADIDAS'),(3,'ARAMIS');
1 INSERT INTO `PERFUMES` VALUES (1,'Channel 1',46.22,'2017-02-01',1),(2,'Adidas',1.33,NULL,2),(18,'Best Aramis',54.32,'0000-00-00',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:
- Arquivo /app/Marca.php
1 <?php 2 3 namespace App; 4 5 use Illuminate\Database\Eloquent\Model; 6 7 class Marca extends Model 8 { 9 protected $table ='MARCAS'; 10 protected $primaryKey='id_marca'; 11 12 public $timestamps = false; 13 14 protected $fillable = [ 15 'descipcion' 16 ]; 17 // Se queremos que certos campos non sexan enviados en formato json os poñemos na propiedade $hidden 18 // protected $hidden = ['password']; 19 20 21 public function perfumes(){ 22 return $this->hasMany('App\Perfume','marca_id','id_marca'); 23 } 24 }
- Arquivo /app/Perfume.php
1 <?php 2 3 namespace App; 4 5 use Illuminate\Database\Eloquent\Model; 6 7 class Perfume extends Model 8 { 9 protected $table ='PERFUMES'; 10 protected $primaryKey='id_perfume'; 11 12 public $timestamps = false; 13 14 protected $dates = ['data_compra']; 15 16 protected $fillable = [ 17 'descipcion', 18 'prezo', 19 'data_compra', 20 'marca_id' 21 ]; 22 // Se queremos que certos campos non sexan enviados en formato json os poñemos na propiedade $hidden 23 // protected $hidden = ['password']; 24 25 public function marca(){ 26 return $this->belongsTo('App\Marca','marca_id','id_marca'); 27 } 28 }
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/{marcas} → Para obter a información dunha marca concreta
- PUT http://meusitio.es/marcas/{marcas} → Para modificar os datos dunha marca concreta
- DELETE http://meusitio.es/marcas/{marcas} → Para eliminar unha marca concreta
- GET http://meusitio.es/marcas → Para obter o listado de marcas.
- POST http://meusitio.es/marcas/{marcas}/perfumes → Para crear un perfume
- PUT http://meusitio.es/marca/{marcas}/perfumes/{perfumes} → Para modificar os datos dun perfume concreto
- GET http://meusitio.es/marcas/{marcas}/perfumes → Para obter o listado de perfumes dunha marca concreta.
- DELETE http://meusitio.es/marcas/{marcas}/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 tres controladores asociados a estas rutas:
- MarcasController
- MarcasPerfumesController
- PerfumesController
- Os comandos para crear ditos controladores serán:
- Versión 5.1 ou anteriores:
1 php artisan make:controller MarcaController 2 php artisan make:controller PerfumeController 3 php artisan make:controller MarcaPerfumeController
- Versión 5.2 ou posteriores:
1 php artisan make:controller MarcaController --resource 2 php artisan make:controller PerfumeController --resource 3 php artisan make:controller MarcaPerfumeController --resource
- Editamos o arquivo /app/Http/routes.php
1 Route::resource('marcas','MarcasController', 2 ['only' => ['store', 'show', 'update', 'destroy','index']]); 3 4 Route::resource('marcas.perfumes','MarcasPerfumesController', 5 ['only' => ['store', 'update', 'index', 'destroy','']]); 6 7 Route::resource('perfumes','PerfumesController', 8 ['only' => ['index', 'show']]);
- Nota: Información sobre as rutas neste enlace.
- 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}.
- Unha vez creadas as rutas podemos comprobalas có comando: php artisan route:list
Probando as rutas
- Podemos facer que dende os controladores amosen unha cadea de texto para comprobar que as rutas funcionan correctamente.
- Para comprobalas podemos facer uso:
- Da orde curl
- Máis información neste enlace.
- Complemento de Firefox restclient ou httprequest.
- Importante: Por defecto Laravel espera recibir un token dos formularios para evitar ataques CSRF. No caso dos servizos, estes non empregan formularios polo que desactivaremos o envío do token no arquivo /app/Http/kernel.php
1 ........... 2 protected $middleware = [ 3 \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, 4 \App\Http\Middleware\EncryptCookies::class, 5 \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 6 \Illuminate\Session\Middleware\StartSession::class, 7 \Illuminate\View\Middleware\ShareErrorsFromSession::class, 8 // \App\Http\Middleware\VerifyCsrfToken::class, 9 ];
- Arquivo: /app/Http/Controller/MarcaController.php
1 <?php 2 3 namespace App\Http\Controllers; 4 5 use Illuminate\Http\Request; 6 7 use App\Http\Requests; 8 use App\Http\Controllers\Controller; 9 10 class MarcasController extends Controller 11 { 12 /** 13 * Display a listing of the resource. 14 * 15 * @return \Illuminate\Http\Response 16 */ 17 public function index() 18 { 19 return 'Amosamos as marcas'; 20 } 21 22 23 /** 24 * Store a newly created resource in storage. 25 * 26 * @param \Illuminate\Http\Request $request 27 * @return \Illuminate\Http\Response 28 */ 29 public function store(Request $request) 30 { 31 return "Creando unha nova marca"; 32 } 33 34 /** 35 * Display the specified resource. 36 * 37 * @param int $id 38 * @return \Illuminate\Http\Response 39 */ 40 public function show($id) 41 { 42 return "Amosando datos da marca $id"; 43 } 44 45 /** 46 * Update the specified resource in storage. 47 * 48 * @param \Illuminate\Http\Request $request 49 * @param int $id 50 * @return \Illuminate\Http\Response 51 */ 52 public function update(Request $request, $id) 53 { 54 return "Actualizando os datos da marca $id"; 55 } 56 57 /** 58 * Remove the specified resource from storage. 59 * 60 * @param int $id 61 * @return \Illuminate\Http\Response 62 */ 63 public function destroy($id) 64 { 65 return "Eliminando marca $id"; 66 } 67 }
Arquivo: /app/Http/Controller/MarcaPerfumeController.php
1 <?php 2 3 namespace App\Http\Controllers; 4 5 use Illuminate\Http\Request; 6 7 use App\Http\Requests; 8 use App\Http\Controllers\Controller; 9 10 class MarcasPerfumesController extends Controller 11 { 12 /** 13 * Display a listing of the resource. 14 * 15 * @return \Illuminate\Http\Response 16 */ 17 public function index($idMarca) 18 { 19 return "Amosando os perfumes da marca $idMarca"; 20 } 21 22 23 /** 24 * Store a newly created resource in storage. 25 * 26 * @param \Illuminate\Http\Request $request 27 * @return \Illuminate\Http\Response 28 */ 29 public function store(Request $request,$idMarca) 30 { 31 return "Se crea un novo perfume da marca $idMarca"; 32 } 33 34 35 36 /** 37 * Update the specified resource in storage. 38 * 39 * @param \Illuminate\Http\Request $request 40 * @param int $id 41 * @return \Illuminate\Http\Response 42 */ 43 public function update(Request $request, $idMarca,$idPerfume) 44 { 45 return "Se actualizan os datos do perfume $idPerfume da marca $idMarca"; 46 } 47 48 /** 49 * Remove the specified resource from storage. 50 * 51 * @param int $id 52 * @return \Illuminate\Http\Response 53 */ 54 public function destroy($idMarca,$idPerfume) 55 { 56 return "Se elimina perfume $idPerfume da marca $idMarca"; 57 } 58 }
Arquivo: /app/Http/Controller/PerfumeController.php
1 <?php 2 3 namespace App\Http\Controllers; 4 5 use Illuminate\Http\Request; 6 7 use App\Http\Requests; 8 use App\Http\Controllers\Controller; 9 10 class PerfumesController extends Controller 11 { 12 /** 13 * Display a listing of the resource. 14 * 15 * @return \Illuminate\Http\Response 16 */ 17 public function index() 18 { 19 return "Amósanse todos os perfumes"; 20 } 21 22 23 /** 24 * Display the specified resource. 25 * 26 * @param int $id 27 * @return \Illuminate\Http\Response 28 */ 29 public function show($idPerfume) 30 { 31 return "Amosa os datos do perfume $idPerfume"; 32 } 33 34 }
- Exemplos para probar as rutas:
- Comando:
1 curl -i "http://localhost:8000/marcas"
- Resultado:
1 HTTP/1.1 200 OK 2 Host: localhost:8000 3 Connection: close 4 X-Powered-By: PHP/5.5.9-1ubuntu4.19 5 Cache-Control: no-cache 6 Date: Mon, 17 Apr 2017 09:25:34 GMT 7 Content-Type: text/html; charset=UTF-8 8 Set-Cookie: laravel_session=eyJpdiI6IjIrbzRrS08yaU51bjVwdk1WY2VrcGc9PSIsInZhbHVlIjoiQmhUVU9SdVhKVzBVckNqUitJV0JlR2tSNkkydWFmY2hGSWdmWUV0Q053MlV1QTVyb21FcG1tMDFveGREaDZmTk55Nnk1VDNkclBCQng5OExXM1ZnQmc9PSIsIm1hYyI6Ijc2YWQ5YTY0MmZjOWRkZjRmNTU4ZTllNWNjZTA0NjRkZGUwMmEzNTIyNjhiMmJkMTBiZWMzYzczYTQ0Zjk2ZGEifQ%3D%3D; expires=Mon, 17-Apr-2017 11:25:34 GMT; Max-Age=7200; path=/; httponly 9 10 Amosamos as marcas
- Comando:
1 curl -i -H "Accept: application/json" -X POST http://localhost:8000/marcas
- Resultado:
1 HTTP/1.1 200 OK 2 Host: localhost:8000 3 Connection: close 4 X-Powered-By: PHP/5.5.9-1ubuntu4.19 5 Cache-Control: no-cache 6 Date: Mon, 17 Apr 2017 09:26:43 GMT 7 Content-Type: text/html; charset=UTF-8 8 Set-Cookie: laravel_session=eyJpdiI6InYrZXU0Y1BsbGM0NGdWR212aUNlQXc9PSIsInZhbHVlIjoiNG1QM0lEaG02QmFkS2p1XC9UZzZ6R2dCT2FxUFwvbURSNWNJN21ibUpvZzFvWVE1b3JBaFB4a0F3ZjFIc0NVZzNPVWxOSmhsT2wwdnFIdTJhck5PeHBCdz09IiwibWFjIjoiODg0YTNjOTQ2NjAyOTNhNTI0OGI5MjZkYjIwZWU0YmJiMTM1ODFmMDkzNTU3MmZjMzg5OGY3NWE0NzNjZDlkZiJ9; expires=Mon, 17-Apr-2017 11:26:43 GMT; Max-Age=7200; path=/; httponly 9 10 Creando unha 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:
1 $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.
- Arquivo app/Http/Controller/MarcasController.php
1 <?php 2 3 namespace App\Http\Controllers; 4 5 use Illuminate\Http\Request; 6 7 use App\Http\Requests; 8 use App\Http\Controllers\Controller; 9 10 use App\Marca; 11 use Validator; 12 13 class MarcasController extends Controller 14 { 15 /** 16 * Display a listing of the resource. 17 * 18 * @return \Illuminate\Http\Response 19 */ 20 public function index() 21 { 22 23 return response()->json(['status'=>'ok','data'=>Marca::all()],200); 24 } 25 26 27 /** 28 * Store a newly created resource in storage. 29 * 30 * @param \Illuminate\Http\Request $request 31 * @return \Illuminate\Http\Response 32 */ 33 public function store(Request $request) 34 { 35 if(!$request->input('descripcion')){ 36 return response()->json(['errors'=>['status'=>'422','title'=>'Datos incompletos','detail'=>'Falta a descripcion da marca']],400); 37 } 38 return response()->json(['status'=>'ok','data'=>$novaMarca],201)->header('Location', 'http://www.dominio.es/marcas/'.$novaMarca->id_marca)->header('Content-Type', 'application/json'); 39 } 40 41 /** 42 * Display the specified resource. 43 * 44 * @param int $id 45 * @return \Illuminate\Http\Response 46 */ 47 public function show($idMarca) 48 { 49 $marca = Marca::find($idMarca); 50 if (!$marca){ 51 return response()->json(['errors'=>['status'=>'404','title'=>'Marca non atopada']],404); 52 } 53 54 return response()->json(['status'=>'ok','data'=>$marca],200); 55 } 56 57 /** 58 * Update the specified resource in storage. 59 * 60 * @param \Illuminate\Http\Request $request 61 * @param int $id 62 * @return \Illuminate\Http\Response 63 */ 64 public function update(Request $request, $idMarca) 65 { 66 // Validamos os datos que nos chegan: 67 $validator = Validator::make($request->all(),[ 68 'descripcion' => 'required|min:1|max:100' 69 ]); 70 if ($validator->fails()){ 71 return response()->json(['errors'=>['status'=>'422','title'=>'Datos incompletos','detail'=>'Falta a descripcion da marca ou ten mais de 100 caracteres']],400); 72 } 73 74 // Pode vir polo método PUT ou polo método PATCH 75 // Non imos facer distinción. 76 $modificado=false; 77 $marcaModificar = Marca::find($idMarca); 78 if (!$marcaModificar){ 79 return response()->json(['errors'=>['status'=>'404','title'=>'Marca non atopada']],404); 80 } 81 if($request->has('descripcion')){ 82 $modificado=true; 83 $marcaModificar->descripcion=$request->input('descripcion'); 84 } 85 86 // 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. 87 if(!$modificado) { 88 return response()->json(['errors'=>['status'=>'304','title'=>'Marca non modificada']],304); 89 } 90 91 $marcaModificar->save(); 92 return response()->json(['status'=>'ok'],200); 93 94 } 95 96 /** 97 * Remove the specified resource from storage. 98 * 99 * @param int $id 100 * @return \Illuminate\Http\Response 101 */ 102 public function destroy($idMarca) 103 { 104 $marcaBorrar = Marca::find($idMarca); 105 if (!$marcaBorrar){ 106 return response()->json(['errors'=>['status'=>'404','title'=>'Marca non atopada']],404); 107 } 108 109 // Comprobamos se temos perfumes asociados á marca 110 if($marcaBorrar->perfumes && $marcaBorrar->perfumes->count()>0){ 111 return response()->json(['errors'=>['status'=>'409','title'=>'Existen perfumes asociados']],409); 112 } 113 114 $marcaBorrar->delete(); 115 return response()->json(['errors'=>['status'=>'204','title'=>'Marca eliminada']],204); 116 117 118 } 119 }
-- Ángel D. Fernández González -- (2017).