From 18c711686c079a794a8435f5e96f709abba12c47 Mon Sep 17 00:00:00 2001 From: Witel Group Date: Sun, 14 Jul 2024 10:44:55 +0000 Subject: [PATCH] Added Datatable package --- app/Facades/DataTable/DataTable.php | 64 ++++++++++ app/Facades/DataTable/DataTableFacade.php | 13 +++ app/Providers/DataTableServiceProvider.php | 26 +++++ app/Services/DataTable/DataTableInput.php | 74 ++++++++++++ app/Services/DataTable/DataTableService.php | 110 ++++++++++++++++++ app/Services/DataTable/Enums/DataType.php | 15 +++ app/Services/DataTable/Enums/SearchType.php | 19 +++ .../Exceptions/InvalidFilterException.php | 21 ++++ .../Exceptions/InvalidParameterInterface.php | 7 ++ .../Exceptions/InvalidRelationException.php | 21 ++++ .../Exceptions/InvalidSortingException.php | 21 ++++ app/Services/DataTable/Filter/ApplyFilter.php | 84 +++++++++++++ app/Services/DataTable/Filter/Filter.php | 73 ++++++++++++ .../Filter/SearchFunctions/FilterBetween.php | 27 +++++ .../Filter/SearchFunctions/FilterContains.php | 31 +++++ .../Filter/SearchFunctions/FilterEquals.php | 30 +++++ .../SearchFunctions/FilterGreaterThan.php | 18 +++ .../FilterGreaterThanOrEqual.php | 18 +++ .../Filter/SearchFunctions/FilterLessThan.php | 18 +++ .../SearchFunctions/FilterLessThanOrEqual.php | 18 +++ .../SearchFunctions/FilterNotEquals.php | 17 +++ .../Filter/SearchFunctions/SearchFilter.php | 22 ++++ app/Services/DataTable/Sort/ApplySort.php | 31 +++++ app/Services/DataTable/Sort/Sort.php | 39 +++++++ .../DataTable/Validators/FilterValidator.php | 69 +++++++++++ .../Validators/RelationValidator.php | 25 ++++ .../DataTable/Validators/SortingValidator.php | 39 +++++++ bootstrap/cache/services.php | 2 + 28 files changed, 952 insertions(+) create mode 100644 app/Facades/DataTable/DataTable.php create mode 100644 app/Facades/DataTable/DataTableFacade.php create mode 100644 app/Providers/DataTableServiceProvider.php create mode 100644 app/Services/DataTable/DataTableInput.php create mode 100644 app/Services/DataTable/DataTableService.php create mode 100644 app/Services/DataTable/Enums/DataType.php create mode 100644 app/Services/DataTable/Enums/SearchType.php create mode 100644 app/Services/DataTable/Exceptions/InvalidFilterException.php create mode 100644 app/Services/DataTable/Exceptions/InvalidParameterInterface.php create mode 100644 app/Services/DataTable/Exceptions/InvalidRelationException.php create mode 100644 app/Services/DataTable/Exceptions/InvalidSortingException.php create mode 100644 app/Services/DataTable/Filter/ApplyFilter.php create mode 100644 app/Services/DataTable/Filter/Filter.php create mode 100644 app/Services/DataTable/Filter/SearchFunctions/FilterBetween.php create mode 100644 app/Services/DataTable/Filter/SearchFunctions/FilterContains.php create mode 100644 app/Services/DataTable/Filter/SearchFunctions/FilterEquals.php create mode 100644 app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThan.php create mode 100644 app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThanOrEqual.php create mode 100644 app/Services/DataTable/Filter/SearchFunctions/FilterLessThan.php create mode 100644 app/Services/DataTable/Filter/SearchFunctions/FilterLessThanOrEqual.php create mode 100644 app/Services/DataTable/Filter/SearchFunctions/FilterNotEquals.php create mode 100644 app/Services/DataTable/Filter/SearchFunctions/SearchFilter.php create mode 100644 app/Services/DataTable/Sort/ApplySort.php create mode 100644 app/Services/DataTable/Sort/Sort.php create mode 100644 app/Services/DataTable/Validators/FilterValidator.php create mode 100644 app/Services/DataTable/Validators/RelationValidator.php create mode 100644 app/Services/DataTable/Validators/SortingValidator.php diff --git a/app/Facades/DataTable/DataTable.php b/app/Facades/DataTable/DataTable.php new file mode 100644 index 00000000..ae637a86 --- /dev/null +++ b/app/Facades/DataTable/DataTable.php @@ -0,0 +1,64 @@ +filters); + $sorting = json_decode($request->sorting); + $rels = $request->rels ?: array(); + + $dataTableInput = new DataTableInput( + $request->start, + $request->size, + $filters, + $sorting, + $rels, + $allowedFilters, + $allowedSortings + ); + + $query = $this->makeQueryFromModel($mixed); + + $dataTableService = (new DataTableService($query, $dataTableInput)) + ->setAllowedFilters($allowedFilters) + ->setAllowedRelations($allowedRelations) + ->setAllowedSortings($allowedSortings) + ->setAllowedSelects($allowedSelects); + + return $dataTableService->getData(); + } + + protected function makeQueryFromModel(Model|Builder $mixed): Builder + { + return ($mixed instanceof Model) ? $mixed->query() : $mixed; + } +} diff --git a/app/Facades/DataTable/DataTableFacade.php b/app/Facades/DataTable/DataTableFacade.php new file mode 100644 index 00000000..0e94ab5c --- /dev/null +++ b/app/Facades/DataTable/DataTableFacade.php @@ -0,0 +1,13 @@ +app->bind('datatable', function () { + return new DataTable(); + }); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + } +} diff --git a/app/Services/DataTable/DataTableInput.php b/app/Services/DataTable/DataTableInput.php new file mode 100644 index 00000000..69172f9a --- /dev/null +++ b/app/Services/DataTable/DataTableInput.php @@ -0,0 +1,74 @@ +start; + } + + public function getSize(): ?int + { + return $this->size; + } + + /** + * @return array returns an array of Filter objects + */ + public function getFilters(): array + { + $filters = array(); + + foreach ($this->filters as $filter) { + $filters[] = new Filter( + $filter->id, + $filter->value, + $filter->fn, + $filter->datatype, + $this->allowedFilters + ); + } + + return $filters; + } + + /** + * @return Sort|null + */ + public function getSorting(): ?Sort + { + return !empty($this->sorting) ? + new Sort($this->sorting[0]->id, $this->sorting[0]->desc, $this->allowedSortings) : null; + } + + public function getRelations(): array + { + return $this->rels; + } + + +} diff --git a/app/Services/DataTable/DataTableService.php b/app/Services/DataTable/DataTableService.php new file mode 100644 index 00000000..f8b54d09 --- /dev/null +++ b/app/Services/DataTable/DataTableService.php @@ -0,0 +1,110 @@ +allowedFilters = $allowedFilters; + return $this; + } + + public function setAllowedRelations(array $allowedRelations): DataTableService + { + $this->allowedRelations = $allowedRelations; + return $this; + } + + public function setAllowedSortings(array $allowedSortings): DataTableService + { + $this->allowedSortings = $allowedSortings; + return $this; + } + + public function setAllowedSelects(array $allowedSelects): DataTableService + { + $this->allowedSelects = $allowedSelects; + return $this; + } + + /** + * Handle 'getData' operations + * @return array + */ + public function getData(): array + { + $query = $this->buildQuery(); + $data = $query->get(); + + return array( + 'data' => $data, + 'meta' => [ + 'totalRowCount' => $this->totalRowCount + ] + ); + } + + protected function buildQuery(): Builder + { + $query = $this->query; + + foreach ($this->dataTableInput->getFilters() as $filter) { + $query = (new ApplyFilter($query, $filter))->apply(); + } + + $query = $this->applySelect($query, $this->allowedSelects); + $query = $this->includeRelationsInQuery($query, $this->allowedRelations); + + $this->totalRowCount = $query->count(); + + $query->offset($this->dataTableInput->getStart()); + + if(!is_null($this->dataTableInput->getSize())){ + $query->limit($this->dataTableInput->getSize()); + } + + $sorting = $this->dataTableInput->getSorting(); + $query = (new ApplySort($query, $sorting))->apply(); + return $query; + } + + protected function applySelect(Builder $query, array $selectedFields): Builder + { + if (!empty($selectedFields)) { + $query->select($selectedFields); + } + + return $query; + } + + protected function includeRelationsInQuery(Builder $query, array $rels): Builder + { + if (!empty($rels)) { + $query->with($rels); + } + + return $query; + } + + // (later) define mapping of relation names to prevent relation name expose. + // (later) define mapping of column names to prevent column name expose. +} diff --git a/app/Services/DataTable/Enums/DataType.php b/app/Services/DataTable/Enums/DataType.php new file mode 100644 index 00000000..e16e98cd --- /dev/null +++ b/app/Services/DataTable/Enums/DataType.php @@ -0,0 +1,15 @@ +fieldName = $fieldName; + parent::__construct($message, $code, $previous); + } + + public function getFieldName() + { + return $this->fieldName; + } +} diff --git a/app/Services/DataTable/Exceptions/InvalidParameterInterface.php b/app/Services/DataTable/Exceptions/InvalidParameterInterface.php new file mode 100644 index 00000000..3e7b3c73 --- /dev/null +++ b/app/Services/DataTable/Exceptions/InvalidParameterInterface.php @@ -0,0 +1,7 @@ +fieldName = $fieldName; + parent::__construct($message, $code, $previous); + } + + public function getFieldName() + { + return $this->fieldName; + } +} diff --git a/app/Services/DataTable/Exceptions/InvalidSortingException.php b/app/Services/DataTable/Exceptions/InvalidSortingException.php new file mode 100644 index 00000000..d1704699 --- /dev/null +++ b/app/Services/DataTable/Exceptions/InvalidSortingException.php @@ -0,0 +1,21 @@ +fieldName = $fieldName; + parent::__construct($message, $code, $previous); + } + + public function getFieldName() + { + return $this->fieldName; + } +} diff --git a/app/Services/DataTable/Filter/ApplyFilter.php b/app/Services/DataTable/Filter/ApplyFilter.php new file mode 100644 index 00000000..971abccb --- /dev/null +++ b/app/Services/DataTable/Filter/ApplyFilter.php @@ -0,0 +1,84 @@ +filter; + $query = $this->query; + + $searchType = SearchType::from($filter->getFn()); + switch ($searchType) { + case SearchType::CONTAINS: + $this->searchFilter = new FilterContains($query, $filter); + break; + + case SearchType::EQUALS: + $this->searchFilter = new FilterEquals($query, $filter); + break; + + case SearchType::NOT_EQUALS: + $this->searchFilter = new FilterNotEquals($query, $filter); + break; + + case SearchType::BETWEEN: + $this->searchFilter = new FilterBetween($query, $filter); + break; + + case SearchType::GREATER_THAN: + $this->searchFilter = new FilterGreaterThan($query, $filter); + break; + + case SearchType::LESS_THAN: + $this->searchFilter = new FilterLessThan($query, $filter); + break; + + default: + $searchFunction = $filter->getFn(); + throw new InvalidFilterException($searchFunction, "search function `$searchFunction` is invalid."); + + } + + $relation = $this->filter->getRelation(); + return $relation ? $this->applyFilterToRelation($relation) : $this->searchFilter->apply(); + } + + protected function applyFilterToRelation(string $relation): Builder + { + return $this->query->whereHas($relation, function (Builder $query) { + $this->filter->removeRelationFromId(); + $this->applyFilter($query, $this->filter); + }); + } + + private function applyFilter(Builder $query, Filter $filter): Builder + { + return (new ApplyFilter($query, $filter))->apply(); + } +} diff --git a/app/Services/DataTable/Filter/Filter.php b/app/Services/DataTable/Filter/Filter.php new file mode 100644 index 00000000..22362a1d --- /dev/null +++ b/app/Services/DataTable/Filter/Filter.php @@ -0,0 +1,73 @@ +isValid($this, $this->allowedFilters); + } + + public function getId(): string + { + return $this->id; + } + + public function getValue(): array|int|string + { + return $this->value; + } + + public function getFn(): string + { + return $this->fn; + } + + public function getDatatype(): string + { + return $this->datatype; + } + + public function getRelation(): string + { + $fieldArray = explode('.', $this->id); + return count($fieldArray) > 1 ? $fieldArray[0] : ''; + } + + public function getColumn(): string + { + $fieldArray = explode('.', $this->id); + return array_pop($fieldArray); + } + + public function removeRelationFromId(): void + { + $this->id = $this->getColumn(); + } + + public function setValue(int|array|string $value): void + { + $this->value = $value; + } + +} diff --git a/app/Services/DataTable/Filter/SearchFunctions/FilterBetween.php b/app/Services/DataTable/Filter/SearchFunctions/FilterBetween.php new file mode 100644 index 00000000..7b91d5e6 --- /dev/null +++ b/app/Services/DataTable/Filter/SearchFunctions/FilterBetween.php @@ -0,0 +1,27 @@ +query; + [$minVal, $maxVal] = $this->filter->getValue(); + + if ($minVal) { + $this->filter->setValue($minVal); + $query = (new FilterGreaterThanOrEqual($this->query, $this->filter))->apply(); + } + + if ($maxVal) { + $this->filter->setValue($maxVal); + $query = (new FilterLessThanOrEqual($this->query, $this->filter))->apply(); + } + + return $query; + } +} diff --git a/app/Services/DataTable/Filter/SearchFunctions/FilterContains.php b/app/Services/DataTable/Filter/SearchFunctions/FilterContains.php new file mode 100644 index 00000000..686fe9f1 --- /dev/null +++ b/app/Services/DataTable/Filter/SearchFunctions/FilterContains.php @@ -0,0 +1,31 @@ +filter->getId(); + $value = '%' . $this->filter->getValue() . '%'; + + if ($this->filter->getDatatype() == DataType::TEXT->value) { + $query = $this->searchIgnoreCase($column, $value); + + } else { + $query = $this->query->where($column, 'LIKE', $value); + } + + return $query; + } + + private function searchIgnoreCase(string $column, string $value): Builder + { + $value = strtolower($value); + return $this->query->whereRaw("LOWER($column) LIKE ?", [$value]); + } +} diff --git a/app/Services/DataTable/Filter/SearchFunctions/FilterEquals.php b/app/Services/DataTable/Filter/SearchFunctions/FilterEquals.php new file mode 100644 index 00000000..594ce87d --- /dev/null +++ b/app/Services/DataTable/Filter/SearchFunctions/FilterEquals.php @@ -0,0 +1,30 @@ +filter->getId(); + $value = $this->filter->getValue(); + + if ($this->filter->getDatatype() == DataType::TEXT->value) { + $query = $this->searchIgnoreCase($column, $value); + } else { + $query = $this->query->where($column, $value); + } + + return $query; + } + + private function searchIgnoreCase(string $column, string $value): Builder + { + $value = strtolower($value); + return $this->query->whereRaw("LOWER($column) = ?", [$value]); + } +} diff --git a/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThan.php b/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThan.php new file mode 100644 index 00000000..30da5d32 --- /dev/null +++ b/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThan.php @@ -0,0 +1,18 @@ +filter->getDatatype() == DataType::NUMERIC) ? + (float)$this->filter->getValue() : $this->filter->getValue(); + + return $this->query->where($this->filter->getId(), '>', $value); + } +} diff --git a/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThanOrEqual.php b/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThanOrEqual.php new file mode 100644 index 00000000..8e97248a --- /dev/null +++ b/app/Services/DataTable/Filter/SearchFunctions/FilterGreaterThanOrEqual.php @@ -0,0 +1,18 @@ +filter->getDatatype() == DataType::NUMERIC) ? + (float)$this->filter->getValue() : $this->filter->getValue(); + + return $this->query->where($this->filter->getId(), '>=', $value); + } +} diff --git a/app/Services/DataTable/Filter/SearchFunctions/FilterLessThan.php b/app/Services/DataTable/Filter/SearchFunctions/FilterLessThan.php new file mode 100644 index 00000000..4069b9e0 --- /dev/null +++ b/app/Services/DataTable/Filter/SearchFunctions/FilterLessThan.php @@ -0,0 +1,18 @@ +filter->getDatatype() == DataType::NUMERIC) ? + (float)$this->filter->getValue() : $this->filter->getValue(); + + return $this->query->where($this->filter->getId(), '<', $value); + } +} diff --git a/app/Services/DataTable/Filter/SearchFunctions/FilterLessThanOrEqual.php b/app/Services/DataTable/Filter/SearchFunctions/FilterLessThanOrEqual.php new file mode 100644 index 00000000..84409052 --- /dev/null +++ b/app/Services/DataTable/Filter/SearchFunctions/FilterLessThanOrEqual.php @@ -0,0 +1,18 @@ +filter->getDatatype() == DataType::NUMERIC) ? + (float)$this->filter->getValue() : $this->filter->getValue(); + + return $this->query->where($this->filter->getId(), '<=', $value); + } +} diff --git a/app/Services/DataTable/Filter/SearchFunctions/FilterNotEquals.php b/app/Services/DataTable/Filter/SearchFunctions/FilterNotEquals.php new file mode 100644 index 00000000..e525f2b7 --- /dev/null +++ b/app/Services/DataTable/Filter/SearchFunctions/FilterNotEquals.php @@ -0,0 +1,17 @@ +filter->getId(); + $value = $this->filter->getValue(); + + return $this->query->whereNot($column, $value); + } +} diff --git a/app/Services/DataTable/Filter/SearchFunctions/SearchFilter.php b/app/Services/DataTable/Filter/SearchFunctions/SearchFilter.php new file mode 100644 index 00000000..d4dcbf6a --- /dev/null +++ b/app/Services/DataTable/Filter/SearchFunctions/SearchFilter.php @@ -0,0 +1,22 @@ +query; + + if (!is_null($this->sort)) { + $query->orderBy($this->sort->getId(), $this->sort->getDirection()); + } + + return $query; + } +} diff --git a/app/Services/DataTable/Sort/Sort.php b/app/Services/DataTable/Sort/Sort.php new file mode 100644 index 00000000..5196797a --- /dev/null +++ b/app/Services/DataTable/Sort/Sort.php @@ -0,0 +1,39 @@ +isValid($this, $this->allowedSortings); + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + public function getDirection(): string + { + return $this->desc === true ? 'desc' : 'asc'; + } +} diff --git a/app/Services/DataTable/Validators/FilterValidator.php b/app/Services/DataTable/Validators/FilterValidator.php new file mode 100644 index 00000000..cc45813e --- /dev/null +++ b/app/Services/DataTable/Validators/FilterValidator.php @@ -0,0 +1,69 @@ +isAllowed($filter, $allowedFilters)) { + $filterId = $filter->getId(); + throw new InvalidFilterException($filter->getId(), "filtering field `$filterId` is not allowed."); + } + + if (!$this->isValidSearchFunction($filter)) { + $searchFunction = $filter->getFn(); + throw new InvalidFilterException($searchFunction, "search function `$searchFunction` is invalid."); + } + + if ($this->isValidDataType($filter) == -1) { + throw new InvalidFilterException(null, "datatype property is not set in `filters` array."); + } + + if (!$this->isValidDataType($filter)) { + $datatype = $filter->getDatatype(); + throw new InvalidFilterException($datatype, "datatype `$datatype` is invalid."); + } + + return true; + } + + protected function isAllowed(Filter $filter, array $allowedFilters): bool + { + return in_array($filter->getId(), $allowedFilters); + } + + protected function isValidSearchFunction(Filter $filter): bool + { + $searchFunction = $filter->getFn(); + return isset($searchFunction) && in_array($searchFunction, SearchType::values()); + } + + protected function isValidDataType(Filter $filter): int + { + if (!property_exists($filter, 'datatype')) + return -1; + + return $filter->getDatatype() && in_array($filter->getDatatype(), DataType::values()) ? 1 : 0; + } +} diff --git a/app/Services/DataTable/Validators/RelationValidator.php b/app/Services/DataTable/Validators/RelationValidator.php new file mode 100644 index 00000000..ab87382b --- /dev/null +++ b/app/Services/DataTable/Validators/RelationValidator.php @@ -0,0 +1,25 @@ +isAllowed($sorting, $allowedSortings)) { + $sortId = $sorting->getId(); + throw new InvalidSortingException($sortId, "sorting field `$sortId` is not allowed."); + } + + return true; + } + + protected function isAllowed(Sort $sorting, array $allowedSortings): bool + { + return in_array($sorting->getId(), $allowedSortings); + } +} diff --git a/bootstrap/cache/services.php b/bootstrap/cache/services.php index 1ae78e1a..af155298 100755 --- a/bootstrap/cache/services.php +++ b/bootstrap/cache/services.php @@ -44,6 +44,7 @@ 40 => 'App\\Providers\\RouteServiceProvider', 41 => 'App\\Providers\\TelescopeServiceProvider', 42 => 'Barryvdh\\Debugbar\\ServiceProvider', + 43 => 'App\\Providers\\DataTableServiceProvider', ), 'eager' => array ( @@ -76,6 +77,7 @@ 26 => 'App\\Providers\\RouteServiceProvider', 27 => 'App\\Providers\\TelescopeServiceProvider', 28 => 'Barryvdh\\Debugbar\\ServiceProvider', + 29 => 'App\\Providers\\DataTableServiceProvider', ), 'deferred' => array (