jdw5 / vanguard
Extend filters, actions and table data for your Laravel applications.
Requires
- php: ^8.0
- illuminate/console: >=10.0.0
- illuminate/database: >=10.0.0
- illuminate/support: >=10.0.0
Requires (Dev)
- laravel/pint: dev-main
- orchestra/testbench: ^8.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.5
README
Vanguard is a fullstack datatable and search query builder package for Laravel, with a companion Vue frontend composable using the InertiaJS HTTP protocol specification. It provides an elegant API to define tables, columns, actions and refiners for your data.
Table of Contents
Installation
Install the package via composer.
composer require jdw5/vanguard
There are no configuration files needed. You may want to publish the stub and command to customise the default table generated.
php artisan vendor:publish --tag=vanguard-stubs
Frontend Companion
There is a Vue-Inertia client library available via npm
that provides a composable around the refiners and table data. View the documentation for that here.
npm install vanguard-client
And use as
import { useTable } from 'vanguard-client' defineProps({ propName: Object }) const table = useTable('propName')
It comes with in-built query string parsing and data refreshing, bulk selection of table items and generating functions in Javascript for the table.
See the repository for more information, and API documentation.
Anatomy
The core functionality is the ability to define your tables and perform filtering automatically. The refinements and actions can also be used separately, without the table, but the anatomy will focus on the Table
class.
When using the make:table
command, the following boilerplate will be generated:
You must define the model
, table
attributes or complete the defineQuery
method to tell the table what data to fetch. You can also exclude this, and pass in a Builder
instance to the UserTable::make(Model::query())
method.
The table is expected a unique column, or identifier for each element. This is particularly useful for generating modals: the based/momentum-modal
is recommended for providing modal endpoints. The key
can be defined as an attribute on the model, or you can chain asKey()
onto a column when defining.
The table will default to performing a get()
when retrieving records if not collection method is provided. These can be provided when creating the class as such:
$table = UserTable::make()->paginate(10);
Or, the recommended method is to complete the public function paginate()
method. This can return either a single integer or an array of integers. If an array is provided, you have opted in for dynamic pagination and a show
property will be generated on the table to the frontend. This allows for users to change the number of records per page.
defineColumns
returns an array where you can specify the columns to display on the frontend. The selected data from the query will be reduced such that only the column properties here are passed to the client. It provides functionality to define the visibility, breakpoints, transform data and more on each column.
defineRefinements
returns an array of Filter
and Sort
child classes to define the refinements available to the user. The documentation contains a complete specification of the provided APIs for this.
defineActions
returns an array where you can specify the actions to perform on the data. There are three actions types available PageAction
, InlineAction
and BulkAction
to use to separate them.
Core
We provide a complete API documentation for the classes and traits provided by Vanguard, including the relevant namespacing. Abstract classes are not included in the documentation, but are available in the source code.
Table
Tables can be generated from the command line using php artisan make:table
.
Defining the Query
The table requires a query to fetch data from. This can be supplied in a number of ways, listed below in order of precendence:
- Passed as the only argument to
Table::make($argument)
- Overriding the method
protected function defineQuery()
on your table class, returing aBuilder|QueryBuilder
instance - Setting the property
protected $model
to be the model class to fetch data from
- Setting the property
protected $getModelClassesUsing
to be a function to resolve the model class - It will attempt to resolve the model class from the table name if none of the above are provided
DEPRECATED
To modify the query between the query you provide and the data being fetched, you can add another method protected function beforeFetch(Builder|QueryBuilder $query)
to apply any additional constraints or modifications, most notably for changing sort orders.
You should also define a unique key for each row in the table. This can be done by setting the property protected $key
to the column name, or by chaining the method asKey()
onto a column when defining it.
Defining columns
Columns define what data is passed to the frontend, and can be used to mutate the data before it is passed among many other things.
Columns are defined using the protected function defineColumns(): array
method, which should return an array of Column
instances. See the Column documentation for the API.
There is an additional property protected $applyColumns
which can be set to false
to disable the application of columns to the query. This is useful when you don't want to reduce the data, or are happy with the default columns. This, by default, is set to true.
Defining refiners
Vanguard provides a macro on both the Query\Builder
and Eloquent\Builder
's to apply the Refinement
class to a given query. This is used in the pipeline for generating the table, allowing you to fluently define refinements for the table.
To add refinements to your table, you can define the protected function defineRefinements(): array
method, which should return an array of Refinement
instances. See the Refinement documentation for the API.
Defining pagination and meta
Vanguard provides a nearly identical API to the default fetching methods provided by Laravel's query builder. You can define the fetching method when making the table, defining the attribute or defining the method. The supported methods are: get
, paginate
, cursorPaginate
and dynamicPaginate
. In order of precendence:
- Setting the method when making
Table::make()->paginate()
- Setting
protected $paginateType
toget
,paginate
,cursor
ordynamic
- Overriding the method
protected function paginate(): int|array
to return the number of records per page
The chaining method provides an identical API to Laravel's in-built paginate mechanisms. If you don't do this at the make level, you can override the relevant attributes on the table. These are protected $pageName
, protected $page
, protected $columns
, and then the protected $paginateType
.
There is a dynamicPaginate
option available. This allows for users to change the number of records per page. This is done safely, and they cannot arbitrarily change the number of records per page - it must be one of the provided options. To enable this, you must return an array of integers from the protected function definePaginate()
method, where each integer is a valid number of records per page. Alternatively, you can manually set the perPage
attribute as an array and change the paginateType
to dynamic
- but this is not recommended.
The default number of records is set to 10, but can be overriden by changing the attribute protected $defaultPerPage
. It is recommended that the array provided as page number options contains the default number of records you set. You can also override the query parameter term by changing the attribute protected $showKey
. By default, the term is show
.
Defining actions
Actions are defined using the protected defineActions(): array
method, which should return an array of Action
instances. See the Action documentation for the API. These are then grouped by their type for access on your frontend.
Defining preferences
Preferences are a way to dynamically change the data that is sent to the frontend based on a user changing the selection. This behaviour is not enabled by default. It will add a paging_options
property to the table data object, containing columns to be used as preferences.
To enable preferences, you must define the the key to be used for the preferences in the search query. This can be done by setting the property protected $preferencesKey
to the name of your choice (cols
is a common name), or by overriding the method protected definePreferenceKey(): string
to return the key.
The columns you have defined can then have the preference()
method chained onto them to enable them as preferences. If you have columns applied, this will then prevent any data not in those preferences from being sent to the frontend. However, the query must select the necessary columns for all possible preferences - as the preferencing is done at the server level, not database.
Additionally, Vanguard provides functionality to store a user's preferences for a given table through a cookie. To enable this, the cookie name must be defined in the table class. This can be done by setting the property protected $preferenceCookie
to the name of your choice, or by overriding the method protected definePreferenceCookie(): string
to return the name. It is critical that this key is unique amongst all your tables, and cookies, to prevent conflicts.
Actions
Actions allow for you to define a group of actions, not part of a table. To define an Actions
class, you can create it inline with the Actions::make(...BaseAction $actions)
method. The arguments must be InlineAction
, PageAction
or BulkAction
instances. The actions are then grouped by their type for access on your frontend.
Refiners
Refiners are a way to define search parameters, and apply them to a query using a fluent API. Refiners are then grouped by their type for access on your frontend. You can group them, not part of a table, using the Refiners
class. Generate it in-line using the Refiners::make(...Refinement $refiners)
method. The arguments must be Refinement
instances.
Component Documentation
Columns
Columns are used exclusively in the Table
class to define the data that is passed to the frontend. They can be used to mutate the data before it is passed, define the visibility, breakpoints and more. Columns are generated using Column::make($name)
. The $name
parameter should be the name of the column in the database if you are applying columns. The remaining properties are autogenerated, or require chaining using the following methods:
Actions
Classes which extend BaseAction
are used to define simple methods users can perform on a table. To generate an action, InlineAction::make(string $name)
. The name is used as the key for the action in it's group or table. The remaining properties are autogenerated, or require chaining using the following methods:
Inline Action
Inline action has an additional property for default, often used to denote the default action to be performed when a row is clicked.
Page Action
Currently, only page actions have access to the endpoint API as these pages do not generally require data to be passed and can be built with only server data.
Refinements
Refinements are a way to define search parameters, and apply them to a query using a fluent API. The constructor for all refinements is slightly more complex:
Refinement::make(string $property, ?string $name)
The property field is required, and denotes the specific database column. The name field is optional, and is used to display the refinement on the frontend. If no name is provided, the property is used as the name. The constructor also generates a label, which fallbacks to the name then property but is often overriden. The remaining API for the refinement is provided below:
There are some specific refiners created for you, which should cover almost every use case in reality. These are formed from bases:
Sorts
The BaseSort
API adds a direction to the refinement to accomodate for the ordering of the data. The standard Sort
option extends this base sort with no additional parameters.
The ToggleSort
API is not available for extension, and so not provided. There are no chaining methods, as it is all handled internally.
Filters
Filters provide a way to filter the data based on a specific property. The BaseFilter
API adds a value to the refinement to accomodate for the filtering of the data.
Filter
Filter extends the base filter, and allows for a mode and operator to be defined.
SelectFilter
Select filter allows for whereIn
clauses to be applied, that is it allows for an array to be checked. It only has the operator methods available, see above.
QueryFilter
QueryFilter allows for a custom query to be applied to the filter. This is useful for complex queries, or queries that cannot be expressed using the standard operators.
Options
Options are available values a query term can take on. They can be generated in a number of ways because of this, detailed underneath the chain methods available. The standard way to create an option is to use the Option::make(string $value, ?string $label)
method.
As mentioned, the standard way to create an option is to use the Option::make(string $value, ?string $label)
method where the value of the option is a required parameter, and a label can be optionally passed. There are instances where you may want to create options from a source, these are also covered.
From a collection, such as performing a database query, Option::collection(Collection $collection, string\|callable $asValue, string\|callable $asLabel)
can be used. The collection is the collection of data to be used, and the two callables are used to extract the value and label from the collection. This provides a way to map the data and create a value, label pair. The callable functions should accept an element of the collection and return the value or label respectively. The method defaults to indexing the associative array with value
and label
to retrieve the data.
From an array, such as a list of options, Option::array(array $array, string\|callable $asValue, string\|callable $asLabel)
can be used. This uses the same API as the collection method, but for an array.
Finally, enums can be used to generate options using Option::enum(string $enum, string\|callable $asLabel)
. This will check for a BackedEnum
at the given enum string, the value is taken as the enum value. The label is then generated from a function, with an instance of BackedEnum
sent to it or using a method defined on the enum if a string is passed.
Testing
Before testing ensure that your environment or container has the PHP Sqlite adapter, on Linux:
sudo apt-get install php-sqlite3
Perform a composer install
to retrieve the required packages. Tests can then be executed as:
vendor/bin/phpunit
Specify a testsuite as such:
vendor/bin/phpunit --testsuite=Feature