├── LICENSE ├── README.md ├── composer.json └── src ├── Builders ├── BaseBuilder.php ├── CollectionVuetableBuilder.php └── EloquentVuetableBuilder.php ├── Vuetable.php ├── VuetableFacade.php └── VuetableServiceProvider.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Santiago García 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Vuetable (Laravel 8.x/7.x/6.x/5.x Package) 2 | 3 | [![Build Status](https://travis-ci.org/santigarcor/laravel-vuetable.svg?branch=master)](https://travis-ci.org/santigarcor/laravel-vuetable) 4 | [![Latest Stable Version](https://poser.pugx.org/santigarcor/laravel-vuetable/v/stable)](https://packagist.org/packages/santigarcor/laravel-vuetable) 5 | [![Total Downloads](https://poser.pugx.org/santigarcor/laravel-vuetable/downloads)](https://packagist.org/packages/santigarcor/laravel-vuetable) 6 | [![StyleCI](https://styleci.io/repos/99027423/shield?branch=master)](https://styleci.io/repos/99027423) 7 | [![License](https://poser.pugx.org/santigarcor/laravel-vuetable/license)](https://packagist.org/packages/santigarcor/laravel-vuetable) 8 | 9 | Laravel Vuetable is the backend component that can work with the [Vuetable component](https://github.com/ratiw/vuetable-2). 10 | 11 | The latest release requires [PHP](https://php.net) 7.2.5-7.4 and supports Laravel 5.7, 5.8, 6.* ,7.* and 8.* 12 | 13 | | Laravel Vuetable | L5.4 | L5.5 | L5.6 | L5.7 | L5.8 | L6 | L7 | L8 | 14 | |-------------------|------------------|------------------|------------------|------------------|------------------|------------------|------------------|------------------| 15 | | < 1.0 |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:x: |:x: |:x: | 16 | | \> 1.0 |:x: |:x: |:x: |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| 17 | 18 | ## Installation 19 | 1. Run the composer require command from your terminal: 20 | 21 | composer require "santigarcor/laravel-vuetable" 22 | 23 | 2. If you laravel version not supported the package discovery, set in your `config/app.php`: 24 | - In the providers array add: 25 | 26 | Vuetable\VuetableServiceProvider::class, 27 | 28 | - In the aliases array add: 29 | 30 | 'Vuetable' => Vuetable\VuetableFacade::class, 31 | 32 | ## Usage 33 | Your request to the controller should have this data: 34 | 35 | ```javascript 36 | { 37 | sort: '', // column_name|asc or column_name|desc 38 | page: 1, 39 | per_page: 10, 40 | searchable: [ 41 | // This array should have the names of the columns in the database 42 | ], 43 | filter: '' //The text that is going to be used to filter the data 44 | } 45 | ``` 46 | 47 | You can also specify the sorting order using the "order" attribute (required by https://mannyyang.github.io/vuetable-3/ ): 48 | ```javascript 49 | { 50 | sort: '', // column_name 51 | order: '', // asc or desc 52 | } 53 | ``` 54 | 55 | 56 | So for example lets create the table for the users with their companies. Then in the javascript we should have: 57 | 58 | ```javascript 59 | data = { 60 | sort: 'users.name|asc', 61 | page: 1, 62 | per_page: 10, 63 | searchable: [ // This means the 'users.name', 'users.email' and 'companies.name' columns can be filtered through the 'filter' attribute in the data. 64 | 'users.name', 65 | 'users.email', 66 | 'companies.name', 67 | ] 68 | } 69 | 70 | axios.get('http://url.com/users-with-companies', data) 71 | ``` 72 | 73 | In Controller we can provide Eloquent: 74 | 75 | ```php 76 | class UsersDataController extends Controller 77 | { 78 | public function index() { 79 | 80 | $query = User::select([ 81 | 'users.id', 82 | 'users.name', 83 | 'users.email', 84 | 'companies.name as company', 85 | 'companies.company_id' 86 | ]) 87 | ->leftJoin('companies', 'users.company_id', '=', 'companies.id'); 88 | 89 | return Vuetable::of($query) 90 | ->editColumn('company', function ($user) { 91 | if ($user->company) { 92 | return $user->company; 93 | } 94 | 95 | return '-'; 96 | }) 97 | ->addColumn('urls', function ($user) { 98 | return [ 99 | 'edit' => route('users.edit', $user->id), 100 | 'delete' => route('users.destroy', $user->id), 101 | ]; 102 | }) 103 | ->make(); 104 | } 105 | } 106 | ``` 107 | 108 | Or Collection 109 | ```php 110 | class UsersDataController extends Controller 111 | { 112 | public function index() { 113 | 114 | $query = new Collection([ 115 | ['name' => 'John Doe', 'email' => 'john@mail.com'], 116 | ['name' => 'Jane Doe', 'email' => 'jane@mail.com'], 117 | ['name' => 'Test John', 'email' => 'test@mail.com'] 118 | ]); 119 | 120 | return Vuetable::of($query) 121 | ->editColumn('name', function ($user) { 122 | return Str::lower($user['name']); 123 | }) 124 | ->addColumn('urls', function ($user) { 125 | return [ 126 | 'edit' => route('users.edit', $user['id']), 127 | 'delete' => route('users.destroy', $user['id']), 128 | ]; 129 | }) 130 | ->make(); 131 | } 132 | } 133 | ``` 134 | This controller is going to return: 135 | ```json 136 | { 137 | "current_page": 1, 138 | "from": 1, 139 | "to": 10, 140 | "total": 150, 141 | "per_page": 10, 142 | "last_page": 15, 143 | "first_page_url": "http://url.com/users-with-companies?page=1", 144 | "last_page_url": "http://url.com/users-with-companies?page=15", 145 | "next_page_url": "http://url.com/users-with-companies?page=2", 146 | "prev_page_url": null, 147 | "path": "http://url.com/users-with-companies", 148 | "data": [ 149 | { 150 | "id": 1, 151 | "name": "Administrator", 152 | "email": "administrator@app.com", 153 | "company": "-", 154 | "company_id": null, 155 | "urls": { 156 | "edit": "http://url.com//users/1/edit", 157 | "delete": "http://url.com//users/1" 158 | }, 159 | }, 160 | { 161 | "id": 2, 162 | "name": "Company Administrator", 163 | "email": "company_administrator@app.com", 164 | "company": "-", 165 | "company_id": null, 166 | "urls": { 167 | "edit": "http://url.com//users/2/edit", 168 | "delete": "http://url.com//users/2" 169 | }, 170 | ... 171 | } 172 | ], 173 | } 174 | ``` 175 | 176 | ## What does Laravel Vuetable support? 177 | 178 | Using the Eloquent Builder you can: 179 | - Filter/Sort by model columns. 180 | - Make joins and filter/sort by them. 181 | - Define the length of the pagination. 182 | - Add columns. 183 | - Edit columns (if the column has a cast defined, it doesn't work). 184 | 185 | Using the Collection you can: 186 | - Filter/Sort by model columns. 187 | - Define the length of the pagination. 188 | - Add columns. 189 | - Edit columns. 190 | 191 | ## License 192 | 193 | Laravel Vuetable is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). 194 | 195 | ## Contributing 196 | 197 | Please report any issue you find in the issues page. Pull requests are more than welcome. 198 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "santigarcor/laravel-vuetable", 3 | "description": "Vuetable laravel backend package", 4 | "keywords": ["laravel", "vuetable", "vue", "table"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Santiago Garcia", 9 | "homepage": "http://santigarcor.me" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.2.5", 14 | "illuminate/database": "^5.7|^6.0|^7.0|^8.0", 15 | "illuminate/http": "^5.7|^6.0|^7.0|^8.0", 16 | "illuminate/support": "^5.7|^6.0|^7.0|^8.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^9.0", 20 | "orchestra/testbench": "^3.7|^6.0", 21 | "mockery/mockery": "^1.2.0" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Vuetable\\": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Vuetable\\Tests\\": "tests/" 31 | } 32 | }, 33 | "config": { 34 | "sort-packages": true 35 | }, 36 | "prefer-stable": true, 37 | "minimum-stability": "dev", 38 | "extra": { 39 | "laravel": { 40 | "providers": [ 41 | "Vuetable\\VuetableServiceProvider" 42 | ], 43 | "aliases": { 44 | "Vuetable": "Vuetable\\VuetableFacade" 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Builders/BaseBuilder.php: -------------------------------------------------------------------------------- 1 | columnsToEdit[$column] = $content; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * Add a new column to the columns to add. 46 | * 47 | * @param string $column 48 | * @param string|\Closure $content 49 | */ 50 | public function addColumn($column, $content) 51 | { 52 | $this->columnsToAdd[$column] = $content; 53 | 54 | return $this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Builders/CollectionVuetableBuilder.php: -------------------------------------------------------------------------------- 1 | request = $request; 26 | $this->collection = $collection; 27 | } 28 | 29 | /** 30 | * Make the vuetable data. The data is sorted, filtered and paginated. 31 | * 32 | * @return \Illuminate\Pagination\LengthAwarePaginator 33 | */ 34 | public function make() 35 | { 36 | $results = $this 37 | ->filter() 38 | ->sort() 39 | ->paginate(); 40 | 41 | return $this->applyChangesTo($results); 42 | } 43 | 44 | /** 45 | * Paginate the query. 46 | * 47 | * @return LengthAwarePaginator 48 | */ 49 | public function paginate() 50 | { 51 | $perPage = ($this->request->input('per_page') > 0) ? $this->request->input('per_page') : 15; 52 | $count = $this->collection->count(); 53 | $page = $this->request->input('page'); 54 | $offset = $perPage * ($page - 1); 55 | 56 | $this->collection = $this->collection->slice( 57 | $offset, 58 | (int) $perPage 59 | )->values(); 60 | 61 | $paginator = new LengthAwarePaginator($this->collection, $count, $perPage ?: 15); 62 | 63 | return $paginator; 64 | } 65 | 66 | /** 67 | * Add the order by statement to the query. 68 | * 69 | * @return $this 70 | */ 71 | public function sort() 72 | { 73 | if (!$this->request->input('sort')) { 74 | return $this; 75 | } 76 | 77 | $sort_parts = explode('|', $this->request->input('sort')); 78 | 79 | $field = $sort_parts[0]; 80 | 81 | $direction = count($sort_parts) > 1 82 | ? $sort_parts[1] 83 | : $this->request->input('order', 'desc'); 84 | 85 | if ($field) { 86 | $comparer = function ($a, $b) use ($field, $direction) { 87 | if ($direction === 'desc') { 88 | $first = $b; 89 | $second = $a; 90 | } else { 91 | $first = $a; 92 | $second = $b; 93 | } 94 | $cmp = strnatcasecmp($first[$field], $second[$field]); 95 | 96 | if ($cmp != 0) { 97 | return $cmp; 98 | } 99 | // all elements were equal 100 | return 0; 101 | }; 102 | 103 | $this->collection = $this->collection 104 | ->map(function ($data) { 105 | return Arr::dot($data); 106 | }) 107 | ->sort($comparer) 108 | ->map(function ($data) { 109 | foreach ($data as $key => $value) { 110 | unset($data[$key]); 111 | Arr::set($data, $key, $value); 112 | } 113 | 114 | return $data; 115 | }); 116 | } 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * Add the where clauses to the query. 123 | * 124 | * @return $this 125 | */ 126 | public function filter() 127 | { 128 | if (!$this->request->input('searchable') || !$this->request->input('filter')) { 129 | return $this; 130 | } 131 | 132 | $filterText = Str::lower($this->request->input('filter')); 133 | $columns = $this->request->input('searchable'); 134 | 135 | $this->collection = $this->collection->filter( 136 | function ($row) use ($columns, $filterText) { 137 | $data = $this->serialize($row); 138 | foreach ($columns as $column) { 139 | if (! $value = Arr::get($data, $column)) { 140 | continue; 141 | } 142 | 143 | if (is_array($value)) { 144 | continue; 145 | } 146 | 147 | $value = Str::lower($value); 148 | 149 | if (Str::contains($value, $filterText)) { 150 | return true; 151 | } 152 | } 153 | 154 | return false; 155 | } 156 | ); 157 | 158 | return $this; 159 | } 160 | 161 | /** 162 | * Edit the results inside the pagination object. 163 | * 164 | * @param \Illuminate\Pagination\LengthAwarePaginator $results 165 | * @return \Illuminate\Pagination\LengthAwarePaginator 166 | */ 167 | public function applyChangesTo($results) 168 | { 169 | if (empty($this->columnsToEdit) && empty($this->columnsToAdd)) { 170 | return $results; 171 | } 172 | 173 | $newData = $results 174 | ->getCollection() 175 | ->map(function ($item) { 176 | $item = $this->editItem($item); 177 | $item = $this->addItem($item); 178 | 179 | return $item; 180 | }); 181 | 182 | return $results->setCollection($newData); 183 | } 184 | 185 | /** 186 | * @param array $item 187 | * @throws \Exception 188 | * 189 | * @return array 190 | */ 191 | public function addItem($item) 192 | { 193 | foreach ($this->columnsToAdd as $column => $value) { 194 | if (Arr::has($item, $column)) { 195 | throw new \Exception("Can not add the '{$column}' column, the results already have that column."); 196 | } 197 | 198 | $item = $this->applyColumn($item, $column, $value); 199 | } 200 | 201 | return $item; 202 | } 203 | 204 | 205 | public function editItem($item) 206 | { 207 | foreach ($this->columnsToEdit as $column => $value) { 208 | if (Arr::has($item, $column) === false) { 209 | throw new \Exception("Column {$column} not exist in array"); 210 | } 211 | 212 | $item = $this->applyColumn($item, $column, $value); 213 | } 214 | 215 | return $item; 216 | } 217 | 218 | /** 219 | * Change a model attribe 220 | * 221 | * @param array $model 222 | * @param string $attribute 223 | * @param string|\Closure $value 224 | * @return array 225 | */ 226 | public function applyColumn($item, $attribute, $value) 227 | { 228 | if ($value instanceof \Closure) { 229 | $item[$attribute] = $value($item); 230 | } else { 231 | $item[$attribute] = $value; 232 | } 233 | 234 | return $item; 235 | } 236 | 237 | /** 238 | * Serialize collection 239 | * 240 | * @param mixed $collection 241 | * @return mixed|null 242 | */ 243 | protected function serialize($collection) 244 | { 245 | return $collection instanceof Arrayable ? $collection->toArray() : (array) $collection; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/Builders/EloquentVuetableBuilder.php: -------------------------------------------------------------------------------- 1 | request = $request; 22 | $this->query = $query; 23 | } 24 | 25 | /** 26 | * Make the vuetable data. The data is sorted, filtered and paginated. 27 | * 28 | * @return \Illuminate\Pagination\LengthAwarePaginator 29 | */ 30 | public function make() 31 | { 32 | $results = $this 33 | ->sort() 34 | ->filter() 35 | ->paginate(); 36 | 37 | return $this->applyChangesTo($results); 38 | } 39 | 40 | /** 41 | * Paginate the query. 42 | * 43 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 44 | */ 45 | public function paginate() 46 | { 47 | $perPage = $this->request->input('per_page'); 48 | 49 | return $this->query->paginate($perPage ?: 15); 50 | } 51 | 52 | /** 53 | * Add the order by statement to the query. 54 | * 55 | * @return $this 56 | */ 57 | public function sort() 58 | { 59 | if (!$this->request->input('sort')) { 60 | return $this; 61 | } 62 | 63 | $sort_parts = explode('|', $this->request->input('sort')); 64 | 65 | $field = $sort_parts[0]; 66 | 67 | $direction = count($sort_parts) > 1 68 | ? $sort_parts[1] 69 | : $this->request->input('order', 'desc'); 70 | 71 | $this->query->orderBy($field, $direction); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Add the where clauses to the query. 78 | * 79 | * @return $this 80 | */ 81 | public function filter() 82 | { 83 | if (!$this->request->input('searchable') || !$this->request->input('filter')) { 84 | return $this; 85 | } 86 | 87 | $filterText = "%{$this->request->input('filter')}%"; 88 | 89 | $this->query->where(function ($query) use ($filterText) { 90 | foreach ($this->request->input('searchable') as $column) { 91 | $query->orWhere($column, 'like', $filterText); 92 | } 93 | }); 94 | 95 | return $this; 96 | } 97 | 98 | 99 | /** 100 | * Edit the results inside the pagination object. 101 | * 102 | * @param \Illuminate\Pagination\LengthAwarePaginator $results 103 | * @return \Illuminate\Pagination\LengthAwarePaginator 104 | */ 105 | public function applyChangesTo($results) 106 | { 107 | if (empty($this->columnsToEdit) && empty($this->columnsToAdd)) { 108 | return $results; 109 | } 110 | 111 | $newData = $results 112 | ->getCollection() 113 | ->map(function ($model) { 114 | $model = $this->editModelAttibutes($model); 115 | $model = $this->addModelAttibutes($model); 116 | 117 | return $model; 118 | }); 119 | 120 | return $results->setCollection($newData); 121 | } 122 | 123 | /** 124 | * Edit the model attributes acording to the columnsToEdit attribute. 125 | * 126 | * @param \Illuminate\Database\Eloquent\Model $model 127 | * @return \Illuminate\Database\Eloquent\Model 128 | */ 129 | public function editModelAttibutes($model) 130 | { 131 | foreach ($this->columnsToEdit as $column => $value) { 132 | if ($model->hasCast($column)) { 133 | throw new \Exception("Can not edit the '{$column}' attribute, it has a cast defined in the model."); 134 | } 135 | 136 | $model = $this->changeAttribute($model, $column, $value); 137 | } 138 | 139 | return $model; 140 | } 141 | 142 | /** 143 | * Add the model attributes acording to the columnsToAdd attribute. 144 | * 145 | * @param \Illuminate\Database\Eloquent\Model $model 146 | * @return \Illuminate\Database\Eloquent\Model 147 | */ 148 | public function addModelAttibutes($model) 149 | { 150 | foreach ($this->columnsToAdd as $column => $value) { 151 | if ($model->relationLoaded($column) || $model->getAttributeValue($column) != null) { 152 | throw new \Exception("Can not add the '{$column}' column, the results already have that column."); 153 | } 154 | 155 | $model = $this->changeAttribute($model, $column, $value); 156 | } 157 | 158 | return $model; 159 | } 160 | 161 | /** 162 | * Change a model attribe 163 | * 164 | * @param \Illuminate\Database\Eloquent\Model $model 165 | * @param string $attribute 166 | * @param string|Closure $value 167 | * @return \Illuminate\Database\Eloquent\Model 168 | */ 169 | public function changeAttribute($model, $attribute, $value) 170 | { 171 | if ($value instanceof Closure) { 172 | $model->setAttribute($attribute, $value($model)); 173 | } else { 174 | $model->setAttribute($attribute, $value); 175 | } 176 | 177 | if ($model->relationLoaded($attribute)) { 178 | $model->setRelation($attribute, 'removed'); 179 | } 180 | 181 | return $model; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Vuetable.php: -------------------------------------------------------------------------------- 1 | request = $request; 16 | } 17 | 18 | /** 19 | * Handle automatic builder 20 | * 21 | * @param mixed $source 22 | * @return CollectionVuetableBuilder|EloquentVuetableBuilder 23 | * @throws \Exception 24 | */ 25 | public static function of($source) 26 | { 27 | $request = app('request'); 28 | 29 | if ($source instanceof \Illuminate\Database\Eloquent\Builder) { 30 | return new EloquentVuetableBuilder($request, $source); 31 | } elseif ($source instanceof \Illuminate\Support\Collection) { 32 | return new CollectionVuetableBuilder($request, $source); 33 | } else { 34 | throw new \Exception('Unsupported builder type: ' . gettype($source)); 35 | } 36 | } 37 | 38 | /** 39 | * Return the Eloquent Vuetable Builder 40 | * 41 | * @param \Illuminate\Database\Eloquent\Builder $query 42 | * @return \Vuetable\Builders\EloquentVuetableBuilder 43 | */ 44 | public function eloquent($query) 45 | { 46 | return new EloquentVuetableBuilder($this->request, $query); 47 | } 48 | 49 | /** 50 | * @param \Illuminate\Support\Collection $collection 51 | */ 52 | public function collection($collection) 53 | { 54 | return new CollectionVuetableBuilder($this->request, $collection); 55 | } 56 | 57 | public function getRequest() 58 | { 59 | return $this->request; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/VuetableFacade.php: -------------------------------------------------------------------------------- 1 | app->alias('vuetable', Vuetable::class); 26 | $this->app->singleton('vuetable', function () { 27 | return new Vuetable(app('request')); 28 | }); 29 | } 30 | } 31 | --------------------------------------------------------------------------------