sexta-feira, 23 de setembro de 2016

A Filter Problem: Using Angular and Laravel

It’s a common problem, mainly in web development, filter requests of all kinds. As an example, imagine that you have a table with products and your users must be able to filter data using status, codes, availability, price and so on. A simple (but bad) solution is verify the fields, concatenate some strings and build a query at runtime. This can generate problems, especially if you’re not using patterns and concatenating strings to build the query. At some point, the query is a monster that you cannot maintain. In this post, i present a solution based in some techniques. In the client i’ll use Angular to make requests. I’ll change the Angular symbol, because i’m using Laravel and Blade{{}}.But you can use @{{}}. However, i'll change to [[]] with:
 (function () {  
  'use strict';  
  let signChange = function($interpolateProvider){  
   $interpolateProvider.startSymbol('[[');  
   $interpolateProvider.endSymbol(']]');  
  };  
  angular.module("productsApp", [], signChange);  
 })();  
Following, we need to build the API:
 (function() {  
   'use strict';  
   angular.module('productsApp').factory('productsApi', ['$http', '$q', productsApi]);  
   function productsApi($http, $q) {  
     function getItems(params) {  
       var deferred = $q.defer();          
       $http.get(params)  
         .success(function(data) {  
           deferred.resolve(data);  
         })  
         .error(function() {  
           deferred.reject();  
         });  
       return deferred.promise;  
     }  
     return {  
       getItems: getItems  
     };  
   };  
 })();  
In the code above, we build the factory, that will return our object (revealing module pattern). Now, with the getItems() method we can use HTTP GET and defer promises. With the API, we can build our ListProductsController:
 (function () {  
  'use strict';  
  angular.module("productsApp")  
  .controller("ListProductsController", ['productsApi', '$scope',  
   ListProductsController]);  
  function ListProductsController(api, vm) {  
   function filter(params){  
    vm.dataLoading = true;  
    api.getItems(params).then(function(data){  
     vm.products = data.data;  
    }).finally(_=>{  
     vm.dataLoading = false;  
    });  
   }  
   vm.filter = function(){  
    filter("/api/products?"+ buildParams());  
   }  
   vm.filter();  
  };  
 })();  
In the Controller, we inject the api and the scope, the vm has a filter method to be called from the view. A simple dataLoading is created to feedback the user. The getItems() method is called and when it’s deferred we can access the data. You can build the params with angular or jquery, the attributes will represent the fields to filter. Now, we can do the server side. We’ll make a route for ‘/api/products’ and call getProducts() in the controller, code below:
 public function getProducts(ProductFilters $filters){  
  $quant = request()->get('quant');  
  $quant = empty( $quant ) ? 5 : $quant;  
  $products = Product::filter($filters)->simplePaginate($quant);  
  return json_encode($products);  
 }  
In getProducts(), we get the number($quant) of items that we’ll put for page. By default we set 5. And we call filter passing the ProductsFilters that we’ll use. Next, with simplePaginate to generate pages and return json encoded. Now, the model Product:
 <?php  
 namespace App;  
 use Illuminate\Database\Eloquent\Model;  
 use App\Traits\Filterable;  
 class Product extends Model  
 {  
   use Filterable;  
 }  
In Product, we just state that we’ll use the Trait Filterable:
 <?php  
 namespace App\Traits;  
 use Illuminate\Database\Eloquent\Builder;  
 use App\Filters\QueryFilters;  
 trait Filterable  
 {  
   /**  
    * Filter a result set.  
    *  
    * @param Builder   $query  
    * @param QueryFilters $filters  
    * @return Builder  
    */  
   public function scopeFilter($query, QueryFilters $filters)  
   {  
     return $filters->apply($query);  
   }  
 }  
Now, we are using Laravel scope and calling the QueryFilters method apply, to apply the filters. QueryFilters is the base Filter for our project, we could call this a Filterable-Strategy Pattern.
 <?php  
 namespace App\Filters;  
 use Illuminate\Database\Eloquent\Builder;  
 use Illuminate\Http\Request;  
 abstract class QueryFilters  
 {  
   /**  
    * The request object.  
    *  
    * @var Request  
    */  
   protected $request;  
   /**  
    * The builder instance.  
    *  
    * @var Builder  
    */  
   protected $builder;  
   /**  
    * Create a new QueryFilters instance.  
    *  
    * @param Request $request  
    */  
   public function __construct(Request $request)  
   {  
     $this->request = $request;  
   }  
   /**  
    * Apply the filters to the builder.  
    *  
    * @param Builder $builder  
    * @return Builder  
    */  
   public function apply(Builder $builder)  
   {  
     $this->builder = $builder;  
     foreach ($this->filters() as $name => $value) {  
       if (! method_exists($this, $name)) {  
         continue;  
       }  
       if (strlen($value)) {  
         $this->$name($value);  
       }  
     }  
     return $this->builder;  
   }  
   /**  
    * Get all request filters data.  
    *  
    * @return array  
    */  
   public function filters()  
   {  
     return $this->request->all();  
   }  
 }  
The QueryFilters apply method look at the request params and verify if the method exists, if the method exists, the param value will be passed to execute. With it, you can make the simple question, where are those methods? And the response is: We need to create A ProductFilters class that extends QueryFilters to put our strategy. So:
 <?php  
 namespace App\Filters;  
 use Illuminate\Database\Eloquent\Builder;  
 class ProductFilters extends QueryFilters  
 {  
   public function price($param){  
     if ($param)  
       return $this->builder->orderBy('price', 'desc');   
     return $this->builder->orderBy('price');   
   }  
   public function name($param){  
     return $this->builder->where('name', 'like', '%'.$param.'%');  
   }  
 }  
In the ProductFilters we implement our filters, in the example i ordered by price and searched by name. So, with this strategy, you can create new filters and filter models, we’ll just create the class to extend QueryFilters and implement our filters. You can/must use repositories, i’ll make another post about repositories. No big deal, no big sql, of course this can be improved. So improve this. TY References:
https://github.com/JeffreyWay
https://laracasts.com/skills/laravel
https://app.pluralsight.com/library/courses/angularui-fundamentals/table-of-contents
https://app.pluralsight.com/paths/skills/angular-js
https://app.pluralsight.com/library/courses/patterns-library/table-of-contents
https://angularjs.org/
https://laravel.com/docs/5.2/queries

Nenhum comentário:

Postar um comentário