├── .github └── issue_template.md ├── README.md ├── codesize.xml ├── composer.json ├── config ├── api.php └── tables.php ├── resources └── views │ └── emails │ └── export.blade.php ├── src ├── AppServiceProvider.php ├── Attributes │ ├── Button.php │ ├── Column.php │ ├── Controls.php │ ├── Filter.php │ ├── Number.php │ ├── Structure.php │ └── Style.php ├── Commands │ └── TemplateCacheClear.php ├── Contracts │ ├── AuthenticatesOnExport.php │ ├── ComputesArrayColumns.php │ ├── ComputesColumns.php │ ├── ComputesModelColumns.php │ ├── ConditionalActions.php │ ├── CustomCount.php │ ├── CustomCountCacheKey.php │ ├── CustomCssClasses.php │ ├── CustomFilter.php │ ├── CustomFilteredCount.php │ ├── DynamicTemplate.php │ ├── Filter.php │ ├── RawTotal.php │ └── Table.php ├── Exceptions │ ├── ArrayComputor.php │ ├── Button.php │ ├── Cache.php │ ├── Column.php │ ├── Control.php │ ├── Filter.php │ ├── Meta.php │ ├── ModelComputor.php │ ├── Query.php │ ├── Route.php │ └── Template.php ├── Exports │ ├── EnsoExcel.php │ ├── Excel.php │ └── Prepare.php ├── Jobs │ ├── EnsoExcel.php │ └── Excel.php ├── Notifications │ ├── ExportDone.php │ ├── ExportError.php │ └── ExportStarted.php ├── Services │ ├── Action.php │ ├── Data │ │ ├── ArrayComputors.php │ │ ├── Builders │ │ │ ├── Computor.php │ │ │ ├── Data.php │ │ │ ├── Meta.php │ │ │ ├── Pagination.php │ │ │ ├── Prepare.php │ │ │ └── Total.php │ │ ├── Computors.php │ │ ├── Computors │ │ │ ├── Cents.php │ │ │ ├── Date.php │ │ │ ├── DateTime.php │ │ │ ├── Enum.php │ │ │ ├── Method.php │ │ │ ├── Number.php │ │ │ ├── Resource.php │ │ │ └── Translator.php │ │ ├── Config.php │ │ ├── Fetcher.php │ │ ├── FilterAggregator.php │ │ ├── Filters.php │ │ ├── Filters │ │ │ ├── BaseFilter.php │ │ │ ├── CustomFilter.php │ │ │ ├── Filter.php │ │ │ ├── Interval.php │ │ │ ├── Search.php │ │ │ └── Searches.php │ │ ├── ModelComputors.php │ │ ├── Request.php │ │ ├── RequestArgument.php │ │ └── Sorts │ │ │ ├── CustomSort.php │ │ │ └── Sort.php │ ├── Template.php │ ├── Template │ │ ├── Builder.php │ │ ├── Builders │ │ │ ├── Buttons.php │ │ │ ├── Columns.php │ │ │ ├── Controls.php │ │ │ ├── Filters.php │ │ │ ├── Structure.php │ │ │ └── Style.php │ │ ├── Validator.php │ │ └── Validators │ │ │ ├── Buttons │ │ │ ├── Button.php │ │ │ └── Buttons.php │ │ │ ├── Columns │ │ │ ├── Column.php │ │ │ ├── Columns.php │ │ │ └── Meta.php │ │ │ ├── Controls.php │ │ │ ├── Filters │ │ │ ├── Filter.php │ │ │ └── Filters.php │ │ │ ├── Route.php │ │ │ └── Structure │ │ │ ├── Attributes.php │ │ │ └── Structure.php │ └── TemplateLoader.php └── Traits │ ├── Action.php │ ├── Data.php │ ├── Excel.php │ ├── Init.php │ ├── ProvidesData.php │ ├── ProvidesRequest.php │ ├── TableCache.php │ └── Tests │ └── Datatable.php ├── stubs └── Tables │ ├── Actions │ └── CustomAction.stub │ ├── Builders │ └── ModelTable.stub │ └── Templates │ └── template.stub └── tests └── units ├── Services ├── BuilderTestResource.php ├── SetUp.php ├── Table │ ├── Builders │ │ ├── DataTest.php │ │ ├── ExportTest.php │ │ ├── MetaTest.php │ │ └── lang │ │ │ └── lang.json │ └── Filters │ │ ├── FilterTest.php │ │ ├── IntervalTest.php │ │ └── SearchTest.php ├── Template │ ├── Builders │ │ ├── ButtonsTest.php │ │ ├── ColumnTest.php │ │ └── StructureTest.php │ └── Validators │ │ ├── AttributesTest.php │ │ ├── ButtonTest.php │ │ ├── ColumnTest.php │ │ ├── ControlTest.php │ │ ├── MetaTest.php │ │ ├── RouteTest.php │ │ └── StructureTest.php ├── TemplateLoaderTest.php ├── TestModel.php ├── TestTable.php └── templates │ └── template.json └── Traits └── TableCacheTest.php /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | This is a **bug | feature request**. 3 | 4 | 5 | ### Prerequisites 6 | * [ ] Are you running the latest version? 7 | * [ ] Are you reporting to the correct repository? 8 | * [ ] Did you check the documentation? 9 | * [ ] Did you perform a cursory search? 10 | 11 | ### Description 12 | 13 | 14 | ### Steps to Reproduce 15 | 20 | 21 | ### Expected behavior 22 | 23 | 24 | ### Actual behavior 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tables 2 | 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/edd421567e43456dbe6fe8ebe5210c74)](https://www.codacy.com/gh/laravel-enso/tables?utm_source=github.com&utm_medium=referral&utm_content=laravel-enso/tables&utm_campaign=Badge_Grade) 4 | [![StyleCI](https://github.styleci.io/repos/111688250/shield?branch=master)](https://github.styleci.io/repos/111688250) 5 | [![License](https://poser.pugx.org/laravel-enso/tables/license)](https://packagist.org/packages/laravel-enso/tables) 6 | [![Total Downloads](https://poser.pugx.org/laravel-enso/tables/downloads)](https://packagist.org/packages/laravel-enso/tables) 7 | [![Latest Stable Version](https://poser.pugx.org/laravel-enso/tables/version)](https://packagist.org/packages/laravel-enso/tables) 8 | 9 | Data Table package with server-side processing, unlimited exporting and VueJS components. 10 | Quickly build any complex table based on a JSON template. 11 | 12 | This package can work independently of the [Enso](https://github.com/laravel-enso/Enso) ecosystem. 13 | 14 | The front end assets that utilize this api are present in the [tables](https://github.com/enso-ui/tables) package. 15 | 16 | For live examples and demos, you may visit [laravel-enso.com](https://www.laravel-enso.com) 17 | 18 | [![Watch the demo](https://laravel-enso.github.io/tables/screenshots/bulma_001_thumb.png)](https://laravel-enso.github.io/tables/videos/bulma_demo_01.mp4) 19 | 20 | click on the photo to view a short demo in compatible browsers 21 | 22 | [![Themed screenshot](https://laravel-enso.github.io/tables/screenshots/bulma_002_thumb.png)](https://laravel-enso.github.io/tables/videos/bulma_demo_02.mp4) 23 | 24 | click on the photo to view an **export** demo in compatible browsers 25 | 26 | ### Installation, Configuration & Usage 27 | 28 | Be sure to check out the full documentation for this package available at [docs.laravel-enso.com](https://docs.laravel-enso.com/backend/tables.html) 29 | 30 | ### Contributions 31 | 32 | are welcome. Pull requests are great, but issues are good too. 33 | 34 | ### License 35 | 36 | This package is released under the MIT license. 37 | -------------------------------------------------------------------------------- /codesize.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | custom rules 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-enso/tables", 3 | "description": "Data Table library with server-side processing and a VueJS component", 4 | "keywords": [ 5 | "laravel-enso", 6 | "datatable", 7 | "data-table", 8 | "data-table-server-side" 9 | ], 10 | "homepage": "https://github.com/laravel-enso/tables", 11 | "type": "library", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Adrian Ocneanu", 16 | "email": "aocneanu@gmail.com", 17 | "homepage": "https://laravel-enso.com", 18 | "role": "Developer" 19 | } 20 | ], 21 | "require": { 22 | "php": "^8.0", 23 | "openspout/openspout": "^4.0", 24 | "laravel/framework": "^10.0|^11.0", 25 | "laravel-enso/enums": "^2.0", 26 | "laravel-enso/filters": "^2.0", 27 | "laravel-enso/helpers": "^3.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "LaravelEnso\\Tables\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "LaravelEnso\\Tables\\Tests\\": "tests/" 37 | } 38 | }, 39 | "extra": { 40 | "laravel": { 41 | "providers": [ 42 | "LaravelEnso\\Tables\\AppServiceProvider" 43 | ], 44 | "aliases": [] 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /config/api.php: -------------------------------------------------------------------------------- 1 | '3.4', 5 | ]; 6 | -------------------------------------------------------------------------------- /resources/views/emails/export.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | {{ __('Hi :name', ['name' => $name]) }}, 3 | 4 | {{ __('You will find the export attached to this email.') }} 5 | 6 | {{ __('Thank you') }},
7 | {{ __(config('app.name')) }} 8 | @endcomponent -------------------------------------------------------------------------------- /src/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | load() 14 | ->publish() 15 | ->commands(TemplateCacheClear::class); 16 | } 17 | 18 | private function load() 19 | { 20 | $this->mergeConfigFrom(__DIR__.'/../config/tables.php', 'enso.tables'); 21 | 22 | $this->mergeConfigFrom(__DIR__.'/../config/api.php', 'enso.tables'); 23 | 24 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'laravel-enso/tables'); 25 | 26 | return $this; 27 | } 28 | 29 | private function publish() 30 | { 31 | $this->publishes([ 32 | __DIR__.'/../config/tables.php' => config_path('enso/tables.php'), 33 | ], ['tables-config', 'enso-config']); 34 | 35 | $this->publishes([ 36 | __DIR__.'/../resources/views' => resource_path('views/vendor/laravel-enso/tables'), 37 | ], ['tables-mail', 'enso-mail']); 38 | 39 | $this->stubs()->each(fn ($ext, $stub) => $this->publishes([ 40 | __DIR__."/../stubs/{$stub}.stub" => app_path("{$stub}.{$ext}"), 41 | ])); 42 | 43 | return $this; 44 | } 45 | 46 | private function stubs() 47 | { 48 | return new Collection([ 49 | 'Tables/Actions/CustomAction' => 'php', 50 | 'Tables/Builders/ModelTable' => 'php', 51 | 'Tables/Templates/template' => 'json', 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Attributes/Button.php: -------------------------------------------------------------------------------- 1 | flush(); 20 | $this->info('Enso table cached templates cleared'); 21 | } elseif ($this->confirm("Your cache driver doesn't support tags, therefore we should flush the whole cache")) { 22 | Cache::flush(); 23 | $this->info('Application cache cleared'); 24 | } else { 25 | $this->warn('Enso Table cached templates were not cleared'); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Contracts/AuthenticatesOnExport.php: -------------------------------------------------------------------------------- 1 | $buttons] 20 | )); 21 | } 22 | 23 | public static function missingAttributes() 24 | { 25 | return new static(__( 26 | 'The following attributes are mandatory for buttons: ":attrs"', 27 | ['attrs' => implode('", "', Attributes::Mandatory)], 28 | )); 29 | } 30 | 31 | public static function unknownAttributes() 32 | { 33 | return new static(__( 34 | 'The following optional attributes are allowed for buttons: ":attrs"', 35 | ['attrs' => implode('", "', Attributes::Optional)] 36 | )); 37 | } 38 | 39 | public static function missingRoute() 40 | { 41 | return new static(__( 42 | 'When you set an action for a button you need to provide the fullRoute or routeSuffix' 43 | )); 44 | } 45 | 46 | public static function missingMethod() 47 | { 48 | return new static(__( 49 | 'When you set an ajax action for a button you need to provide the method aswell' 50 | )); 51 | } 52 | 53 | public static function missingName() 54 | { 55 | return new static(__( 56 | 'When you use conditionally rendered button you need to provide name as well' 57 | )); 58 | } 59 | 60 | public static function invalidType() 61 | { 62 | return new static(__( 63 | 'The following types are allowed for buttons: ":types"', 64 | ['types' => implode('", "', Attributes::Types)] 65 | )); 66 | } 67 | 68 | public static function invalidAction() 69 | { 70 | return new static(__( 71 | 'The following actions are allowed for buttons: ":actions"', 72 | ['actions' => implode('", "', Attributes::Actions)] 73 | )); 74 | } 75 | 76 | public static function routeNotFound(string $route) 77 | { 78 | return new static(__( 79 | 'Button route does not exist: ":route"', 80 | ['route' => $route] 81 | )); 82 | } 83 | 84 | public static function invalidMethod(string $method) 85 | { 86 | return new static(__( 87 | 'Method is incorrect: ":method"', 88 | ['method' => $method] 89 | )); 90 | } 91 | 92 | public static function noSelectable() 93 | { 94 | return new static(__( 95 | "You can't have a button with selection when the table is not selectable", 96 | )); 97 | } 98 | 99 | public static function rowSelection() 100 | { 101 | return new static(__( 102 | 'Selection works only on global buttons', 103 | )); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Exceptions/Cache.php: -------------------------------------------------------------------------------- 1 | $model] 14 | )); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/Column.php: -------------------------------------------------------------------------------- 1 | $attrs] 22 | )); 23 | } 24 | 25 | public static function unknownAttributes(string $attrs) 26 | { 27 | return new static(__( 28 | 'Unknown Column Attribute(s) Found: ":attrs"', 29 | ['attrs' => $attrs] 30 | )); 31 | } 32 | 33 | public static function enumNotFound(string $enum) 34 | { 35 | return new static(__( 36 | 'Provided enum does not exist: ":enum"', 37 | ['enum' => $enum] 38 | )); 39 | } 40 | 41 | public static function invalidEnum(string $enum) 42 | { 43 | return new static(__( 44 | 'Provided enum: ":enum" must implement the "Select" interface, or must be a subclass of "Enum" class', 45 | ['enum' => $enum] 46 | )); 47 | } 48 | 49 | public static function resourceNotFound(string $resource) 50 | { 51 | return new static(__( 52 | 'Provided resource does not exist: ":resource"', 53 | ['resource' => $resource] 54 | )); 55 | } 56 | 57 | public static function invalidTooltip(string $column) 58 | { 59 | return new static(__( 60 | 'The tooltip attribute provided for ":column" must be a string', 61 | ['column' => $column] 62 | )); 63 | } 64 | 65 | public static function invalidNumber(string $column) 66 | { 67 | return new static(__( 68 | 'Provided number attribute for ":column" must be an object', 69 | ['column' => $column] 70 | )); 71 | } 72 | 73 | public static function invalidClass(string $column) 74 | { 75 | return new static(__( 76 | 'The class attribute provided for ":column" must be a string', 77 | ['column' => $column] 78 | )); 79 | } 80 | 81 | public static function invalidAlign(string $column) 82 | { 83 | return new static(__( 84 | 'The align attribute provided for ":column" is incorrect', 85 | ['column' => $column] 86 | )); 87 | } 88 | 89 | public static function invalidNumberAttributes(string $column) 90 | { 91 | return new static(__( 92 | 'The number configuration provided for ":column" is invalid. Supported :attributes', 93 | ['column' => $column, 'attributes' => implode(', ', Number::Optional)] 94 | )); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Exceptions/Control.php: -------------------------------------------------------------------------------- 1 | $controls] 19 | )); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exceptions/Filter.php: -------------------------------------------------------------------------------- 1 | $class] 15 | )); 16 | } 17 | 18 | public static function invalidFormat() 19 | { 20 | return new static(__('The filters array may contain only objects')); 21 | } 22 | 23 | public static function missingAttributes() 24 | { 25 | return new static(__( 26 | 'The following attributes are mandatory for filters: ":attrs"', 27 | ['attrs' => implode('", "', Attributes::Mandatory)], 28 | )); 29 | } 30 | 31 | public static function unknownAttributes() 32 | { 33 | return new static(__( 34 | 'The following optional attributes are allowed for filters: ":attrs"', 35 | ['attrs' => implode('", "', Attributes::Optional)] 36 | )); 37 | } 38 | 39 | public static function missingRoute() 40 | { 41 | return new static(__( 42 | 'When you set a select filter you need to provide the options controller route' 43 | )); 44 | } 45 | 46 | public static function routeNotFound(string $route) 47 | { 48 | return new static(__( 49 | 'Filter route does not exist: ":route"', 50 | ['route' => $route] 51 | )); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Exceptions/Meta.php: -------------------------------------------------------------------------------- 1 | $attrs] 14 | )); 15 | } 16 | 17 | public static function unsupported(string $column) 18 | { 19 | return new static(__( 20 | 'Nested columns do not support "sortable": ":column"', 21 | ['column' => $column] 22 | )); 23 | } 24 | 25 | public static function cannotFilterIcon(string $column) 26 | { 27 | return new static(__( 28 | 'Icon columns do not support "fiterable": ":column"', 29 | ['column' => $column] 30 | )); 31 | } 32 | 33 | public static function missingInterface() 34 | { 35 | return new static(__( 36 | 'To use "rawTotal" the table builder must implement the "RawTotal" interface', 37 | )); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Exceptions/ModelComputor.php: -------------------------------------------------------------------------------- 1 | $route] 14 | )); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/Template.php: -------------------------------------------------------------------------------- 1 | $attrs] 14 | )); 15 | } 16 | 17 | public static function unknownAttributes(string $attrs) 18 | { 19 | return new static(__( 20 | 'Unknown Attribute(s) Found: ":attrs"', 21 | ['attrs' => $attrs] 22 | )); 23 | } 24 | 25 | public static function invalidLengthMenu() 26 | { 27 | return new static(__('"lengthMenu" attribute must be an array')); 28 | } 29 | 30 | public static function invalidAppends() 31 | { 32 | return new static(__('"appends" attribute must be an array')); 33 | } 34 | 35 | public static function invalidSearchModes() 36 | { 37 | return new static(__('"searchModes" attribute must be an associative array')); 38 | } 39 | 40 | public static function invalidSortDirection() 41 | { 42 | return new static(__('"defaultSortDirection" attribute must be either "asc" or "desc')); 43 | } 44 | 45 | public static function invalidDebounce() 46 | { 47 | return new static(__('"debounce" attribute must be an integer')); 48 | } 49 | 50 | public static function invalidMethod() 51 | { 52 | return new static(__('"method" attribute can be either "GET" or "POST"')); 53 | } 54 | 55 | public static function invalidSelectable() 56 | { 57 | return new static(__('"selectable" attribute must be a boolean')); 58 | } 59 | 60 | public static function invalidComparisonOperator() 61 | { 62 | return new static(__( 63 | '"comparisonOperator" attribute can be either "LIKE" or "ILIKE"' 64 | )); 65 | } 66 | 67 | public static function invalidSearchMode() 68 | { 69 | return new static(__( 70 | '"searchMode" attribute can be one of "full", "startsWith" or "endsWith"' 71 | )); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Exports/EnsoExcel.php: -------------------------------------------------------------------------------- 1 | user->preferences()->global->lang); 28 | 29 | $this->export->update([ 30 | 'status' => Statuses::Processing, 31 | 'total' => $this->count, 32 | ]); 33 | 34 | parent::process(); 35 | } 36 | 37 | protected function updateProgress(int $chunkSize): self 38 | { 39 | parent::updateProgress($chunkSize); 40 | 41 | $this->export->update(['entries' => $this->entryCount]); 42 | $this->cancelled = $this->export->fresh()->cancelled(); 43 | 44 | return $this; 45 | } 46 | 47 | protected function finalize(): void 48 | { 49 | $args = [ 50 | $this->export, $this->savedName, $this->filename, 51 | $this->export->getAttribute('created_by'), 52 | ]; 53 | 54 | $file = File::attach(...$args); 55 | 56 | $this->export->fill(['status' => Statuses::Finalized]) 57 | ->file()->associate($file) 58 | ->save(); 59 | 60 | $notification = new ExportDone($this->export, $this->emailSubject()); 61 | $queue = ConfigFacade::get('enso.tables.queues.notifications'); 62 | $this->user->notify($notification->onQueue($queue)); 63 | } 64 | 65 | protected function notifyError(): void 66 | { 67 | $this->export->update(['status' => Statuses::Failed]); 68 | 69 | parent::notifyError(); 70 | } 71 | 72 | private function emailSubject(): string 73 | { 74 | $name = $this->config->label(); 75 | 76 | return __(':name export done', ['name' => $name]); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Exports/Prepare.php: -------------------------------------------------------------------------------- 1 | user, $this->config, $this->table]; 24 | 25 | if ($this->config->isEnso()) { 26 | $args[] = $this->export(); 27 | EnsoExcel::dispatch(...$args); 28 | } else { 29 | Excel::dispatch(...$args); 30 | } 31 | } 32 | 33 | protected function export(): Export 34 | { 35 | return Export::factory()->create([ 36 | 'name' => $this->config->name(), 37 | 'status' => Statuses::Waiting, 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Jobs/EnsoExcel.php: -------------------------------------------------------------------------------- 1 | export = $export; 19 | } 20 | 21 | public function handle() 22 | { 23 | $args = [$this->user, $this->table(), $this->config, $this->export]; 24 | 25 | (new Service(...$args))->handle(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Jobs/Excel.php: -------------------------------------------------------------------------------- 1 | timeout = ConfigFacade::get('enso.tables.export.timeout'); 31 | $this->queue = ConfigFacade::get('enso.tables.queues.exports'); 32 | } 33 | 34 | public function handle() 35 | { 36 | (new Service($this->user, $this->table(), $this->config))->handle(); 37 | } 38 | 39 | protected function table() 40 | { 41 | return App::make($this->table, ['request' => $this->config->request()]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Notifications/ExportDone.php: -------------------------------------------------------------------------------- 1 | toArray() + [ 33 | 'level' => 'success', 34 | 'title' => $this->title(), 35 | ]))->onQueue($this->queue); 36 | } 37 | 38 | public function toMail($notifiable) 39 | { 40 | $appName = Config::get('app.name'); 41 | 42 | return (new MailMessage()) 43 | ->subject("[ {$appName} ] {$this->title()}") 44 | ->markdown('laravel-enso/tables::emails.export', [ 45 | 'name' => $notifiable->name, 46 | 'filename' => __($this->filename), 47 | 'entries' => $this->entries, 48 | ])->attach($this->path); 49 | } 50 | 51 | public function toArray() 52 | { 53 | return [ 54 | 'body' => $this->body(), 55 | ]; 56 | } 57 | 58 | protected function body(): string 59 | { 60 | return __('Export emailed: :filename', ['filename' => $this->filename]); 61 | } 62 | 63 | private function title(): string 64 | { 65 | return __('Table export done'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Notifications/ExportError.php: -------------------------------------------------------------------------------- 1 | intersect(['broadcast', 'database']) 26 | ->toArray(); 27 | } 28 | 29 | public function toBroadcast() 30 | { 31 | return (new BroadcastMessage($this->toArray() + [ 32 | 'level' => 'error', 33 | 'title' => __('Table export error'), 34 | ]))->onQueue($this->queue); 35 | } 36 | 37 | public function toArray() 38 | { 39 | return [ 40 | 'body' => __('The export :name could not be completed due to an unknown error', [ 41 | 'name' => $this->name, 42 | ]), 43 | 'path' => '#', 44 | 'icon' => 'file-excel', 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Notifications/ExportStarted.php: -------------------------------------------------------------------------------- 1 | intersect(['broadcast', 'database']) 26 | ->toArray(); 27 | } 28 | 29 | public function toBroadcast() 30 | { 31 | return (new BroadcastMessage($this->toArray() + [ 32 | 'level' => 'info', 33 | 'title' => __('Table export started'), 34 | ]))->onQueue($this->queue); 35 | } 36 | 37 | public function toArray() 38 | { 39 | return [ 40 | 'body' => __(':name export started', ['name' => $this->name]), 41 | 'path' => '#', 42 | 'icon' => 'file-excel', 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Services/Action.php: -------------------------------------------------------------------------------- 1 | fetcher = new Fetcher($table, $config); 18 | $this->request = $config->request(); 19 | } 20 | 21 | public function before(): void 22 | { 23 | } 24 | 25 | abstract public function process(array $row); 26 | 27 | public function after(): void 28 | { 29 | } 30 | 31 | public function handle(): void 32 | { 33 | $this->before(); 34 | 35 | $this->fetcher->next(); 36 | 37 | while ($this->fetcher->valid()) { 38 | $this->fetcher->current() 39 | ->each(fn ($row) => $this->process($row)); 40 | 41 | $this->fetcher->next(); 42 | } 43 | 44 | $this->after(); 45 | } 46 | 47 | public function request(): Request 48 | { 49 | return $this->request; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Services/Data/ArrayComputors.php: -------------------------------------------------------------------------------- 1 | Cents::class, 21 | 'enum' => Enum::class, 22 | 'date' => Date::class, 23 | 'datetime' => DateTime::class, 24 | 'number' => Number::class, 25 | 'translatable' => Translator::class, 26 | ]; 27 | 28 | public static function serverSide(): void 29 | { 30 | self::$serverSide = true; 31 | } 32 | 33 | protected static function computor($computor): ComputesArrayColumns 34 | { 35 | $computor = new static::$computors[$computor](); 36 | 37 | if (!$computor instanceof ComputesArrayColumns) { 38 | throw ArrayComputor::missingInterface(); 39 | } 40 | 41 | return $computor; 42 | } 43 | 44 | protected static function applicable(Config $config): Collection 45 | { 46 | return parent::applicable($config) 47 | ->when(!self::$serverSide, fn ($computors) => $computors 48 | ->reject(fn ($computor) => in_array($computor, ['enum', 'translatable']))); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Services/Data/Builders/Computor.php: -------------------------------------------------------------------------------- 1 | appends() 21 | ->modelCompute() 22 | ->sanitize() 23 | ->arrayCompute(); 24 | 25 | return $this->data; 26 | } 27 | 28 | private function appends(): self 29 | { 30 | if ($this->config->filled('appends')) { 31 | $this->data->each->setAppends( 32 | $this->config->get('appends')->toArray() 33 | ); 34 | } 35 | 36 | return $this; 37 | } 38 | 39 | private function modelCompute(): self 40 | { 41 | ModelComputors::handle($this->config, $this->data); 42 | 43 | return $this; 44 | } 45 | 46 | private function sanitize(): self 47 | { 48 | $this->data = new Collection($this->data->toArray()); 49 | 50 | return $this; 51 | } 52 | 53 | private function arrayCompute(): self 54 | { 55 | ArrayComputors::handle($this->config, $this->data); 56 | 57 | return $this; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Services/Data/Builders/Data.php: -------------------------------------------------------------------------------- 1 | query = $table->query(); 26 | } 27 | 28 | public function handle(): Collection 29 | { 30 | $this->filter() 31 | ->sort() 32 | ->limit() 33 | ->setData(); 34 | 35 | if ($this->data->isNotEmpty()) { 36 | $this->data = (new Computor($this->config, $this->data))->handle(); 37 | 38 | if (!$this->fetchMode) { 39 | $this->actions(); 40 | $this->style(); 41 | } 42 | 43 | $this->data = (new Prepare($this->config, $this->data))->handle(); 44 | } 45 | 46 | return $this->data; 47 | } 48 | 49 | public function toArray(): array 50 | { 51 | return ['data' => $this->handle()]; 52 | } 53 | 54 | private function filter(): self 55 | { 56 | (new Filters($this->table, $this->config, $this->query))->handle(); 57 | 58 | return $this; 59 | } 60 | 61 | private function sort(): self 62 | { 63 | (new Sort($this->config, $this->query))->handle(); 64 | 65 | return $this; 66 | } 67 | 68 | private function limit(): self 69 | { 70 | $this->query->skip($this->config->meta()->get('start')) 71 | ->take($this->config->meta()->get('length')); 72 | 73 | return $this; 74 | } 75 | 76 | private function setData(): self 77 | { 78 | $this->data = $this->query->get(); 79 | 80 | return $this; 81 | } 82 | 83 | private function actions(): void 84 | { 85 | if ($this->table instanceof ConditionalActions) { 86 | $this->data->transform(fn ($row) => $row + [ 87 | '_actions' => $this->rowActions($row), 88 | ]); 89 | } 90 | } 91 | 92 | private function style(): void 93 | { 94 | if ($this->table instanceof CustomCssClasses) { 95 | $this->data->transform(fn ($row) => $row + [ 96 | '_cssClasses' => $this->table->cssClasses($row), 97 | ]); 98 | } 99 | } 100 | 101 | private function rowActions(array $row): array 102 | { 103 | return $this->config->template()->buttons()->get('row') 104 | ->map(fn (Obj $action) => $action->get('name')) 105 | ->filter(fn (string $action) => $this->table->render($row, $action)) 106 | ->values() 107 | ->toArray(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Services/Data/Builders/Meta.php: -------------------------------------------------------------------------------- 1 | query = $table->query(); 34 | $this->total = []; 35 | $this->filters = false; 36 | } 37 | 38 | public function build(): self 39 | { 40 | $this->setCount() 41 | ->filter() 42 | ->detailedInfo() 43 | ->countFiltered() 44 | ->total(); 45 | 46 | return $this; 47 | } 48 | 49 | public function toArray(): array 50 | { 51 | $this->build(); 52 | 53 | return [ 54 | 'count' => $this->count, 55 | 'formattedCount' => number_format($this->count), 56 | 'filtered' => $this->filtered, 57 | 'formattedFiltered' => number_format($this->filtered), 58 | 'total' => $this->total, 59 | 'fullRecordInfo' => $this->fullRecordInfo, 60 | 'filters' => $this->filters, 61 | 'pagination' => $this->pagination()->toArray(), 62 | ]; 63 | } 64 | 65 | public function count($filtered = false): int 66 | { 67 | if ($this->table instanceof CustomCount && !$filtered) { 68 | return $this->table->count(); 69 | } 70 | 71 | if ($this->table instanceof CustomFilteredCount && $filtered) { 72 | return $this->table->filteredCount(); 73 | } 74 | 75 | return $this->query 76 | ->applyScopes() 77 | ->getQuery() 78 | ->getCountForPagination(); 79 | } 80 | 81 | public function filter(): self 82 | { 83 | $filters = new Filters($this->table, $this->config, $this->query); 84 | 85 | $this->filters = $filters->applies(); 86 | 87 | if ($this->filters) { 88 | $filters->handle(); 89 | } 90 | 91 | return $this; 92 | } 93 | 94 | private function setCount(): self 95 | { 96 | $this->filtered = $this->count = $this->cachedCount(); 97 | 98 | return $this; 99 | } 100 | 101 | private function detailedInfo(): self 102 | { 103 | $this->fullRecordInfo = $this->config->meta()->get('forceInfo') 104 | || $this->count <= $this->config->meta()->get('fullInfoRecordLimit') 105 | || (!$this->filters && !$this->config->meta()->get('total')); 106 | 107 | return $this; 108 | } 109 | 110 | private function countFiltered(): self 111 | { 112 | if ($this->filters && $this->fullRecordInfo) { 113 | $this->filtered = $this->count(true); 114 | } 115 | 116 | return $this; 117 | } 118 | 119 | private function total(): self 120 | { 121 | if ($this->fullRecordInfo && $this->config->meta()->get('total')) { 122 | $this->total = (new Total($this->table, $this->config, $this->query)) 123 | ->handle(); 124 | } 125 | 126 | return $this; 127 | } 128 | 129 | private function pagination(): Pagination 130 | { 131 | $args = [$this->config->meta(), $this->filtered, $this->fullRecordInfo]; 132 | 133 | return new Pagination(...$args); 134 | } 135 | 136 | private function cachedCount(): int 137 | { 138 | if (!$this->shouldCache()) { 139 | return $this->count(); 140 | } 141 | 142 | $cacheKey = $this->table instanceof CustomCountCacheKey 143 | ? $this->cacheKey($this->table->countCacheKey()) 144 | : $this->cacheKey(); 145 | 146 | if (!$this->cache($this->cacheKey())->has($cacheKey)) { 147 | $this->cache($this->cacheKey()) 148 | ->put($cacheKey, $this->count(), Carbon::now()->addHour()); 149 | } 150 | 151 | return $this->cache($this->cacheKey())->get($cacheKey); 152 | } 153 | 154 | private function cache(string $tag) 155 | { 156 | return Cache::getStore() instanceof TaggableStore 157 | ? Cache::tags($tag) 158 | : Cache::store(); 159 | } 160 | 161 | private function cacheKey(?string $suffix = null): string 162 | { 163 | $model = $this->query->getModel(); 164 | $key = (new $model())->tableCacheKey(); 165 | 166 | return Collection::wrap([$key, $suffix])->filter()->implode(':'); 167 | } 168 | 169 | private function shouldCache(): bool 170 | { 171 | $shouldCache = $this->config->has('countCache') 172 | ? $this->config->get('countCache') 173 | : ConfigFacade::get('enso.tables.cache.count'); 174 | 175 | if ($shouldCache) { 176 | $model = $this->query->getModel(); 177 | 178 | $instance = (new ReflectionClass($model)); 179 | 180 | $compatible = $instance->hasMethod('resetTableCache') 181 | && $instance->hasMethod('tableCacheKey'); 182 | 183 | if (!$compatible) { 184 | throw Exception::missingTrait($model::class); 185 | } 186 | } 187 | 188 | return $shouldCache 189 | && (Cache::getStore() instanceof TaggableStore 190 | || !$this->table instanceof CustomCountCacheKey); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Services/Data/Builders/Pagination.php: -------------------------------------------------------------------------------- 1 | pages = null; 26 | $this->atStart = false; 27 | $this->atEnd = false; 28 | $this->atMiddle = false; 29 | $this->middlePages = new Collection(); 30 | } 31 | 32 | public function toArray(): array 33 | { 34 | $this->handle(); 35 | 36 | return [ 37 | 'page' => $this->page, 38 | 'pages' => $this->pages, 39 | 'atStart' => $this->atStart, 40 | 'atEnd' => $this->atEnd, 41 | 'atMiddle' => $this->atMiddle, 42 | 'middlePages' => $this->middlePages, 43 | ]; 44 | } 45 | 46 | private function handle(): void 47 | { 48 | Collection::wrap(self::Computes) 49 | ->each(fn ($method) => $this->{$method}()); 50 | } 51 | 52 | private function page(): void 53 | { 54 | $this->page = $this->meta->get('start') / $this->meta->get('length') + 1; 55 | } 56 | 57 | private function pages(): void 58 | { 59 | if ($this->fullInfo) { 60 | $div = Decimals::div($this->filtered, $this->meta->get('length')); 61 | $this->pages = Decimals::ceil($div, 0); 62 | } 63 | } 64 | 65 | private function atStart(): void 66 | { 67 | if ($this->fullInfo) { 68 | $this->atStart = $this->page < 4; 69 | } 70 | } 71 | 72 | private function atEnd(): void 73 | { 74 | if ($this->fullInfo) { 75 | $this->atEnd = $this->pages - $this->page < 3; 76 | } 77 | } 78 | 79 | private function atMiddle(): void 80 | { 81 | if ($this->fullInfo) { 82 | $this->atMiddle = !$this->atStart && !$this->atEnd; 83 | } 84 | } 85 | 86 | private function middlePages(): void 87 | { 88 | if (!$this->fullInfo) { 89 | return; 90 | } 91 | 92 | if ($this->atStart) { 93 | $this->addStartPages(); 94 | } elseif ($this->atEnd) { 95 | $this->addEndPages(); 96 | } else { 97 | $this->addMiddlePages(); 98 | } 99 | } 100 | 101 | private function addStartPages(): void 102 | { 103 | $max = min($this->pages - 1, 4); 104 | 105 | for ($i = 2; $i <= $max; $i++) { 106 | $this->middlePages->push($i); 107 | } 108 | } 109 | 110 | private function addEndPages(): void 111 | { 112 | if ($this->pages > 4) { 113 | $this->middlePages->push($this->pages - 3); 114 | } 115 | 116 | $this->middlePages->push($this->pages - 2, $this->pages - 1); 117 | } 118 | 119 | private function addMiddlePages(): void 120 | { 121 | $this->middlePages->push($this->page - 1, $this->page, $this->page + 1); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Services/Data/Builders/Prepare.php: -------------------------------------------------------------------------------- 1 | strip() 20 | ->flatten(); 21 | 22 | return $this->data; 23 | } 24 | 25 | private function strip(): self 26 | { 27 | if (!$this->config->filled('strip')) { 28 | return $this; 29 | } 30 | 31 | $this->data->transform(function ($row) { 32 | foreach ($this->config->get('strip')->toArray() as $attr) { 33 | unset($row[$attr]); 34 | } 35 | 36 | return $row; 37 | }); 38 | 39 | return $this; 40 | } 41 | 42 | private function flatten(): void 43 | { 44 | if ($this->config->get('flatten')) { 45 | $this->data->transform(fn ($record) => Arr::dot($record)); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Services/Data/Builders/Total.php: -------------------------------------------------------------------------------- 1 | total = []; 24 | } 25 | 26 | public function handle(): array 27 | { 28 | $this->config->columns() 29 | ->filter(fn ($column) => $column->get('meta')->get('total') 30 | || $column->get('meta')->get('rawTotal') 31 | || $column->get('meta')->get('average')) 32 | ->each(fn ($column) => $this->compute($column)); 33 | 34 | return $this->total; 35 | } 36 | 37 | private function compute(Obj $column): void 38 | { 39 | if ($column->get('meta')->get('rawTotal')) { 40 | $this->total[$column->get('name')] = $this->rawTotal($column); 41 | } elseif ($column->get('meta')->get('average')) { 42 | $this->total[$column->get('name')] = $this->query->average($column->get('data')); 43 | } else { 44 | $this->total[$column->get('name')] = $this->query->sum($column->get('data')); 45 | } 46 | 47 | if ($column->get('meta')->get('cents')) { 48 | $this->total[$column->get('name')] /= 100; 49 | } 50 | 51 | if ($column->has('number')) { 52 | $this->total[$column->get('name')] = Number::format( 53 | $this->total[$column->get('name')], 54 | $column->get('number') 55 | ); 56 | } 57 | } 58 | 59 | private function rawTotal($column): string 60 | { 61 | if (!$this->table instanceof RawTotal) { 62 | throw Exception::missingInterface(); 63 | } 64 | 65 | $rawTotal = $this->table->rawTotal($column); 66 | 67 | if (is_numeric($rawTotal)) { 68 | return $rawTotal; 69 | } 70 | 71 | $raw = DB::raw("{$rawTotal} as {$column->get('name')}"); 72 | 73 | $result = $this->query->getQuery()->cloneWithoutBindings(['select']) 74 | ->select($raw)->first(); 75 | 76 | return $result?->{$column->get('name')} ?? 0; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Services/Data/Computors.php: -------------------------------------------------------------------------------- 1 | each(fn ($computor) => $data 17 | ->transform(fn ($row) => static::computor($computor)::handle($row))); 18 | } 19 | 20 | public static function columns(Config $config): void 21 | { 22 | static::applicable($config) 23 | ->each(fn ($computor) => static::computor($computor)::columns($config->columns())); 24 | } 25 | 26 | public static function computors(array $computors): void 27 | { 28 | static::$computors = $computors; 29 | } 30 | 31 | abstract protected static function computor($computor): ComputesColumns; 32 | 33 | protected static function applicable(Config $config): Collection 34 | { 35 | return $config->meta()->filter()->keys() 36 | ->intersect(Collection::wrap(static::$computors)->keys()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Services/Data/Computors/Cents.php: -------------------------------------------------------------------------------- 1 | filter(fn ($column) => $column->get('meta')->get('cents')) 16 | ->values(); 17 | } 18 | 19 | public static function handle(array $row): array 20 | { 21 | foreach (self::$columns as $column) { 22 | $row[$column->get('name')] /= 100; 23 | } 24 | 25 | return $row; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Services/Data/Computors/Date.php: -------------------------------------------------------------------------------- 1 | filter(fn ($column) => $column->get('meta')->get('date')) 19 | ->values(); 20 | } 21 | 22 | public static function handle(array $row): array 23 | { 24 | foreach (self::$columns as $column) { 25 | $rowValue = Arr::get($row, $column->get('name')); 26 | if ($rowValue !== null) { 27 | Arr::set($row, $column->get('name'), Carbon::parse($rowValue) 28 | ->setTimezone(Config::get('app.timezone')) 29 | ->format(self::format($column))); 30 | } 31 | } 32 | 33 | return $row; 34 | } 35 | 36 | private static function format($column) 37 | { 38 | return $column->has('dateFormat') 39 | ? $column->get('dateFormat') 40 | : Config::get('enso.tables.dateFormat'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Services/Data/Computors/DateTime.php: -------------------------------------------------------------------------------- 1 | filter(fn ($column) => $column->get('meta')->get('datetime')) 18 | ->values(); 19 | } 20 | 21 | public static function handle(array $row): array 22 | { 23 | foreach (self::$columns as $column) { 24 | if ($row[$column->get('name')] !== null) { 25 | $row[$column->get('name')] = Carbon::parse($row[$column->get('name')]) 26 | ->setTimezone(Config::get('app.timezone')) 27 | ->format(self::format($column)); 28 | } 29 | } 30 | 31 | return $row; 32 | } 33 | 34 | private static function format($column) 35 | { 36 | return $column->has('dateFormat') 37 | ? $column->get('dateFormat') 38 | : Config::get('enso.tables.dateTimeFormat'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Services/Data/Computors/Enum.php: -------------------------------------------------------------------------------- 1 | filter(fn ($column) => $column->get('enum')) 17 | ->values(); 18 | } 19 | 20 | public static function handle(array $row): array 21 | { 22 | foreach (self::$columns as $column) { 23 | if (enum_exists($column->get('enum'))) { 24 | $value = $column->get('enum')::from(Arr::get($row, $column->get('name'))) 25 | ?? null; 26 | } 27 | 28 | $value = $column->get('enum')[Arr::get($row, $column->get('name'))] 29 | ?? null; 30 | 31 | Arr::set($row, $column->get('name'), $value); 32 | } 33 | 34 | return $row; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Services/Data/Computors/Method.php: -------------------------------------------------------------------------------- 1 | filter(fn ($column) => $column->has('meta') 17 | && $column->get('meta')->get('method')) 18 | ->values(); 19 | } 20 | 21 | public static function handle(Model $row) 22 | { 23 | foreach (self::$columns as $column) { 24 | $row->{$column->get('name')} = $row->{$column->get('name')}(); 25 | } 26 | 27 | return $row; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Services/Data/Computors/Number.php: -------------------------------------------------------------------------------- 1 | filter(fn ($column) => $column->has('number')) 20 | ->values(); 21 | } 22 | 23 | public static function handle(array $row): array 24 | { 25 | foreach (self::$columns as $column) { 26 | $format = self::format( 27 | Arr::get($row, $column->get('name')), 28 | $column->get('number') 29 | ); 30 | 31 | Arr::set($row, $column->get('name'), $format); 32 | } 33 | 34 | return $row; 35 | } 36 | 37 | public static function format($value, Obj $number) 38 | { 39 | if (!isset(self::$formatter)) { 40 | self::$formatter = new Formatter(App::getLocale(), Formatter::DECIMAL); 41 | } 42 | 43 | self::$formatter->setAttribute(Formatter::FRACTION_DIGITS, $number->get('precision', 0)); 44 | 45 | if ($number->has('decimal')) { 46 | self::$formatter->setSymbol(Formatter::DECIMAL_SEPARATOR_SYMBOL, $number->get('decimal')); 47 | } 48 | 49 | if ($number->has('thousand')) { 50 | self::$formatter->setSymbol(Formatter::GROUPING_SEPARATOR_SYMBOL, $number->get('thousand')); 51 | } 52 | 53 | return self::$formatter->format($value); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Services/Data/Computors/Resource.php: -------------------------------------------------------------------------------- 1 | filter(fn ($column) => $column->get('resource')) 20 | ->values(); 21 | } 22 | 23 | public static function handle(Model $row) 24 | { 25 | foreach (self::$columns as $column) { 26 | $value = $row->{$column->get('name')}; 27 | $resource = $column->get('resource'); 28 | unset($row->{$column->get('name')}); 29 | 30 | $row->{$column->get('name')} = $value instanceof Collection 31 | ? self::collection($value, $resource) 32 | : self::resource($value, $resource); 33 | } 34 | 35 | return $row; 36 | } 37 | 38 | private static function collection($value, $resource) 39 | { 40 | return $value->isEmpty() 41 | ? $value 42 | : App::make($resource, [ 43 | 'resource' => new stdClass(), 44 | ])::collection($value); 45 | } 46 | 47 | private static function resource($value, $resource) 48 | { 49 | return $value === null 50 | ? null 51 | : App::make($resource, ['resource' => $value]); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Services/Data/Computors/Translator.php: -------------------------------------------------------------------------------- 1 | filter(fn ($column) => $column 15 | ->get('meta')->get('translatable')) 16 | ->values(); 17 | } 18 | 19 | public static function handle(array $row): array 20 | { 21 | foreach (self::$columns as $column) { 22 | $row[$column->get('name')] = __($row[$column->get('name')]); 23 | } 24 | 25 | return $row; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Services/Data/Config.php: -------------------------------------------------------------------------------- 1 | setMeta() 34 | ->setColumns(); 35 | } 36 | 37 | public function __call($method, $args) 38 | { 39 | if (isset($args[0]) && Collection::wrap(self::TemplateProxy)->contains($args[0])) { 40 | return $this->template->{$method}(...$args); 41 | } 42 | 43 | return $this->request->{$method}(...$args); 44 | } 45 | 46 | public function meta(): Obj 47 | { 48 | return $this->meta; 49 | } 50 | 51 | public function columns(): Obj 52 | { 53 | return $this->columns; 54 | } 55 | 56 | public function searches(): Obj 57 | { 58 | return $this->request->searches(); 59 | } 60 | 61 | public function filters(): Obj 62 | { 63 | return $this->request->filters(); 64 | } 65 | 66 | public function intervals(): Obj 67 | { 68 | return $this->request->intervals(); 69 | } 70 | 71 | public function params(): Obj 72 | { 73 | return $this->request->params(); 74 | } 75 | 76 | public function template(): Template 77 | { 78 | return $this->template; 79 | } 80 | 81 | public function request(): Request 82 | { 83 | return $this->request; 84 | } 85 | 86 | public function isEnso(): bool 87 | { 88 | return !empty(ConfigFacade::get('enso.config')); 89 | } 90 | 91 | public function name(): string 92 | { 93 | $name = Str::of($this->get('name'))->snake(); 94 | 95 | return preg_replace('/[^A-Za-z0-9_.-]/', '_', $name); 96 | } 97 | 98 | public function label(): string 99 | { 100 | return Str::of($this->name())->replace('_', ' ')->ucfirst(); 101 | } 102 | 103 | private function setMeta(): self 104 | { 105 | $requestMeta = new Collection(static::RequestMeta); 106 | 107 | $this->meta = $this->template->meta() 108 | ->forget(static::RequestMeta) 109 | ->merge($this->request->meta()->intersectByKeys($requestMeta->flip())); 110 | 111 | return $this; 112 | } 113 | 114 | private function setColumns(): void 115 | { 116 | $this->removeDefaults(); 117 | 118 | $this->columns = $this->template->columns() 119 | ->map(fn ($column) => $this->mergeColumnMeta($column)); 120 | 121 | ArrayComputors::columns($this); 122 | } 123 | 124 | private function mergeColumnMeta(Obj $templateColumn): Obj 125 | { 126 | $requestColumn = $this->request->column($templateColumn->get('name')); 127 | 128 | if ($requestColumn) { 129 | $meta = $templateColumn->get('meta', new Collection()) 130 | ->forget(static::RequestColumnMeta) 131 | ->merge($this->requestColumnMeta($requestColumn)); 132 | 133 | $templateColumn->set('meta', $meta); 134 | } 135 | 136 | return $templateColumn; 137 | } 138 | 139 | private function requestColumnMeta(Obj $requestColumn): Collection 140 | { 141 | return $requestColumn->get('meta', new Collection()) 142 | ->intersectByKeys(Collection::wrap(static::RequestColumnMeta)->flip()); 143 | } 144 | 145 | private function removeDefaults(): void 146 | { 147 | $this->template->columns() 148 | ->map(fn ($column) => tap($column, fn ($column) => $column->get('meta') 149 | ->forget(static::RemoveDefaultColumnMeta))); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Services/Data/Fetcher.php: -------------------------------------------------------------------------------- 1 | data = new Collection(); 23 | $this->page = 0; 24 | $this->ready = false; 25 | 26 | ArrayComputors::serverSide(); 27 | } 28 | 29 | public function current(): Collection 30 | { 31 | return $this->data; 32 | } 33 | 34 | public function chunkSize(): int 35 | { 36 | return $this->data->count(); 37 | } 38 | 39 | public function next(): void 40 | { 41 | if (!$this->ready) { 42 | $this->optimalChunk(); 43 | } 44 | 45 | $this->data = $this->fetch($this->page); 46 | $this->page++; 47 | } 48 | 49 | public function valid(): bool 50 | { 51 | return $this->data->isNotEmpty(); 52 | } 53 | 54 | public function count(): int 55 | { 56 | return $this->count ??= (new Meta($this->table, $this->config)) 57 | ->filter()->count(true); 58 | } 59 | 60 | private function fetch($page = 0): Collection 61 | { 62 | $start = $this->config->meta()->get('length') * $page; 63 | $this->config->meta()->set('start', $start); 64 | 65 | return (new Data($this->table, $this->config, true))->handle(); 66 | } 67 | 68 | private function optimalChunk(): void 69 | { 70 | $optimalChunk = OptimalChunk::get($this->count()); 71 | $this->config->meta()->set('length', $optimalChunk); 72 | 73 | $this->ready = true; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Services/Data/FilterAggregator.php: -------------------------------------------------------------------------------- 1 | internalFilters = new Obj(Argument::parse($internalFilters)); 24 | $this->searches = $this->filterInternal('string'); 25 | $this->filters = new Obj(Argument::parse($filters)); 26 | $this->intervals = new Obj(Argument::parse($intervals)); 27 | $this->params = new Obj(Argument::parse($params)); 28 | } 29 | 30 | public function searches(): Obj 31 | { 32 | return $this->searches; 33 | } 34 | 35 | public function filters(): Obj 36 | { 37 | return $this->filters; 38 | } 39 | 40 | public function intervals(): Obj 41 | { 42 | return $this->intervals; 43 | } 44 | 45 | public function params(): Obj 46 | { 47 | return $this->params; 48 | } 49 | 50 | public function __invoke(): self 51 | { 52 | $this->extractCustom() 53 | ->merge($this->filters, self::Filters) 54 | ->merge($this->filters, self::Intervals, $this->excludeArrays()) 55 | ->merge($this->intervals, self::Intervals, $this->onlyArrays()); 56 | 57 | return $this; 58 | } 59 | 60 | private function extractCustom(): self 61 | { 62 | $this->internalFilters 63 | ->filter(fn ($filter) => $filter->get('custom')) 64 | ->each(fn ($filter) => $this->set($this->params, $filter)); 65 | 66 | $this->internalFilters = $this->internalFilters 67 | ->reject(fn ($filter) => $filter->get('custom')); 68 | 69 | return $this; 70 | } 71 | 72 | private function merge(Obj $filters, array $types, ?Closure $filter = null): self 73 | { 74 | $this->filterInternal($types) 75 | ->when($filter, fn ($filters) => $filters->filter($filter)) 76 | ->each(fn ($filter) => $this->set($filters, $filter)); 77 | 78 | return $this; 79 | } 80 | 81 | private function set(Obj $filters, Obj $filter) 82 | { 83 | $array = []; 84 | 85 | Arr::set($array, $filter->get('data'), $filter->get('value')); 86 | 87 | $filters->set(key($array), $filters->has(key($array)) 88 | ? $filters->get(key($array))->merge(new Obj($array[key($array)])) 89 | : new Obj($array[key($array)])); 90 | } 91 | 92 | private function filterInternal($types): Obj 93 | { 94 | return $this->internalFilters 95 | ->filter(fn ($filter) => in_array($filter->get('type'), (array) $types)); 96 | } 97 | 98 | private function onlyArrays(): Closure 99 | { 100 | return fn ($filter) => $filter->get('value') instanceof Obj; 101 | } 102 | 103 | private function excludeArrays(): Closure 104 | { 105 | return fn ($filter) => !$filter->get('value') instanceof Obj; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Services/Data/Filters.php: -------------------------------------------------------------------------------- 1 | applicable() 33 | ->first(fn ($filter) => $this->filter($filter)->applies()) !== null; 34 | } 35 | 36 | public function handle(): void 37 | { 38 | $this->applicable()->each(fn ($filter) => $this->apply($filter)); 39 | } 40 | 41 | public static function filters($filters): void 42 | { 43 | self::$defaultFilters = $filters; 44 | } 45 | 46 | public static function customFilters($filters): void 47 | { 48 | self::$customFilters = $filters; 49 | } 50 | 51 | private function apply($filter): void 52 | { 53 | $filter = $this->filter($filter); 54 | 55 | if ($filter->applies()) { 56 | $filter->handle(); 57 | } 58 | } 59 | 60 | private function filter($filter): TableFilter 61 | { 62 | $instance = App::make($filter, [ 63 | 'table' => $this->table, 64 | 'config' => $this->config, 65 | 'query' => $this->query, 66 | ]); 67 | 68 | if (!$instance instanceof TableFilter) { 69 | throw Exception::missingContract($filter); 70 | } 71 | 72 | return $instance; 73 | } 74 | 75 | private function needsCustomFiltering(): bool 76 | { 77 | return $this->table instanceof TableCustomFilter; 78 | } 79 | 80 | private function applicable(): Collection 81 | { 82 | return Collection::wrap(self::$defaultFilters) 83 | ->merge($this->needsCustomFiltering() ? self::$customFilters : null); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Services/Data/Filters/BaseFilter.php: -------------------------------------------------------------------------------- 1 | table->filterApplies($this->config->params()); 10 | } 11 | 12 | public function handle(): void 13 | { 14 | $this->table->filter($this->query, $this->config->params()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Services/Data/Filters/Filter.php: -------------------------------------------------------------------------------- 1 | filters()->isNotEmpty(); 13 | } 14 | 15 | public function handle(): void 16 | { 17 | $this->query->where(fn ($query) => $this->filters() 18 | ->each(fn ($filters, $table) => $filters 19 | ->each(fn ($value, $column) => $query 20 | ->whereIn("{$table}.{$column}", $value)))); 21 | } 22 | 23 | private function filters(): Obj 24 | { 25 | return $this->config->filters()->map 26 | ->filter(fn ($value) => $this->isValid($value)) 27 | ->filter->isNotEmpty()->map 28 | ->map(fn ($value) => $this->value($value)); 29 | } 30 | 31 | private function isValid($value): bool 32 | { 33 | return !Collection::wrap([null, ''])->containsStrict($value) 34 | && (!$value instanceof Collection || $value->isNotEmpty()); 35 | } 36 | 37 | private function value($value): array 38 | { 39 | return $value instanceof Collection 40 | ? $value->toArray() 41 | : (array) $value; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Services/Data/Filters/Interval.php: -------------------------------------------------------------------------------- 1 | config->intervals() 12 | ->some(fn ($column) => $column 13 | ->some(fn ($interval) => $interval->filled('min') 14 | || $interval->filled('max'))); 15 | } 16 | 17 | public function handle(): void 18 | { 19 | $this->query->where(fn () => $this->config->intervals() 20 | ->each(fn ($interval, $table) => $interval 21 | ->each(fn ($interval, $column) => $this 22 | ->limit($table, $column, $interval)))); 23 | } 24 | 25 | private function limit($table, $column, Obj $interval): self 26 | { 27 | $attribute = "{$table}.{$column}"; 28 | 29 | if ($interval->filled('min')) { 30 | $this->query->where($attribute, '>=', $interval->get('min')); 31 | } 32 | 33 | if ($interval->filled('max')) { 34 | $this->query->where($attribute, '<=', $interval->get('max')); 35 | } 36 | 37 | return $this; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Services/Data/Filters/Search.php: -------------------------------------------------------------------------------- 1 | config->meta()->filled('search') 14 | && $this->searchable()->isNotEmpty(); 15 | } 16 | 17 | public function handle(): void 18 | { 19 | $search = $this->config->meta()->get('search'); 20 | 21 | (new Service($this->query, $this->attributes(), $search)) 22 | ->relations($this->relations()) 23 | ->comparisonOperator($this->config->get('comparisonOperator')) 24 | ->searchMode($this->config->meta()->get('searchMode')) 25 | ->handle(); 26 | } 27 | 28 | private function searchable(): Collection 29 | { 30 | return $this->config->columns()->filter(fn ($column) => $column 31 | ->get('meta')->get('searchable')); 32 | } 33 | 34 | private function attributes(): array 35 | { 36 | return $this->searchable() 37 | ->reject(fn ($column) => $this->isNested($column->get('name'))) 38 | ->map(fn ($column) => $column->get('data')) 39 | ->toArray(); 40 | } 41 | 42 | private function relations(): array 43 | { 44 | return $this->searchable() 45 | ->filter(fn ($column) => $this->isNested($column->get('name'))) 46 | ->map(fn ($column) => $column->get('data')) 47 | ->toArray(); 48 | } 49 | 50 | private function isNested($attribute): bool 51 | { 52 | return Str::of($attribute)->contains('.'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Services/Data/Filters/Searches.php: -------------------------------------------------------------------------------- 1 | filterable()->isNotEmpty() 15 | && $this->filters()->isNotEmpty(); 16 | } 17 | 18 | public function handle(): void 19 | { 20 | $this->query->where(fn () => $this->filters() 21 | ->each(fn ($filter) => $this->filter($filter))); 22 | } 23 | 24 | private function filter(Obj $filter): void 25 | { 26 | (new Search( 27 | $this->query, 28 | (array) $this->attribute($filter), 29 | $filter->get('value') 30 | ))->relations((array) $this->relation($filter)) 31 | ->comparisonOperator($this->config->get('comparisonOperator')) 32 | ->searchMode($filter->get('mode')) 33 | ->handle(); 34 | } 35 | 36 | private function attribute(Obj $filter): ?string 37 | { 38 | return $this->isNested($filter) ? null : $filter->get('data'); 39 | } 40 | 41 | private function relation(Obj $filter): ?string 42 | { 43 | return $this->isNested($filter) 44 | ? Str::of($filter->get('data'))->after('.') 45 | : null; 46 | } 47 | 48 | private function isNested(string $attribute): bool 49 | { 50 | return Str::of($attribute)->explode('.')->count() > 1; 51 | } 52 | 53 | private function filterable(): Collection 54 | { 55 | return $this->config->columns()->filter(fn ($column) => $column 56 | ->get('meta')->get('filterable')); 57 | } 58 | 59 | private function filters(): Obj 60 | { 61 | return $this->config->searches() 62 | ->map(fn ($filters) => $filters 63 | ->filter(fn ($value) => $this->isValid($value))) 64 | ->filter->isNotEmpty(); 65 | } 66 | 67 | private function isValid($value): bool 68 | { 69 | return !Collection::wrap([null, ''])->containsStrict($value) 70 | && (!$value instanceof Collection || $value->isNotEmpty()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Services/Data/ModelComputors.php: -------------------------------------------------------------------------------- 1 | Method::class, 14 | 'resource' => Resource::class, 15 | ]; 16 | 17 | protected static function computor($computor): ComputesModelColumns 18 | { 19 | $computor = new self::$computors[$computor](); 20 | 21 | if (!$computor instanceof ComputesModelColumns) { 22 | throw ModelComputor::missingInterface(); 23 | } 24 | 25 | return $computor; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Services/Data/Request.php: -------------------------------------------------------------------------------- 1 | columns = new Obj(Argument::parse($columns)); 20 | $this->meta = new Obj(Argument::parse($meta)); 21 | $this->searches = $aggregator->searches(); 22 | $this->filters = $aggregator->filters(); 23 | $this->intervals = $aggregator->intervals(); 24 | $this->params = $aggregator->params(); 25 | } 26 | 27 | public function columns(): Obj 28 | { 29 | return $this->columns; 30 | } 31 | 32 | public function meta(): Obj 33 | { 34 | return $this->meta; 35 | } 36 | 37 | public function searches(): Obj 38 | { 39 | return $this->searches; 40 | } 41 | 42 | public function filters(): Obj 43 | { 44 | return $this->filters; 45 | } 46 | 47 | public function intervals(): Obj 48 | { 49 | return $this->intervals; 50 | } 51 | 52 | public function params(): Obj 53 | { 54 | return $this->params; 55 | } 56 | 57 | public function column(string $name): ?Obj 58 | { 59 | return $this->columns 60 | ->first(fn ($column) => $column->get('name') === $name); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Services/Data/RequestArgument.php: -------------------------------------------------------------------------------- 1 | map(fn ($arg) => self::decode($arg))->toArray(); 14 | } 15 | 16 | public static function decode($arg) 17 | { 18 | if (is_array($arg)) { 19 | return $arg; 20 | } 21 | 22 | $decodedArg = json_decode($arg, true); 23 | 24 | return json_last_error() === JSON_ERROR_NONE 25 | ? $decodedArg 26 | : $arg; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Services/Data/Sorts/CustomSort.php: -------------------------------------------------------------------------------- 1 | config->meta()->get('sort') 21 | && $this->columns()->isNotEmpty(); 22 | } 23 | 24 | public function handle(): void 25 | { 26 | $this->columns() 27 | ->each(fn ($column) => $this->query->when( 28 | $column->get('meta')->get('nullLast'), 29 | fn ($query) => $query->orderByRaw($this->rawSort($column)), 30 | fn ($query) => $query 31 | ->orderBy($column->get('data'), $column->get('meta')->get('sort')) 32 | )); 33 | 34 | $template = $this->config->template(); 35 | 36 | $sort = ConfigFacade::get('enso.tables.dtRowId') === $template->get('dtRowId') 37 | ? "{$template->get('table')}.{$template->get('dtRowId')}" 38 | : $template->get('dtRowId'); 39 | 40 | $this->query->orderBy($sort, $template->get('defaultSortDirection')); 41 | } 42 | 43 | private function rawSort($column): string 44 | { 45 | $data = $column->get('data'); 46 | $sort = $column->get('meta')->get('sort'); 47 | 48 | return "({$data} IS NULL), {$data} {$sort}"; 49 | } 50 | 51 | protected function columns(): Obj 52 | { 53 | return $this->config->columns()->filter(fn ($column) => $column 54 | ->get('meta')->get('sortable') && $column->get('meta')->get('sort')); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Services/Data/Sorts/Sort.php: -------------------------------------------------------------------------------- 1 | config, $this->query); 19 | 20 | if ($sort->applies()) { 21 | $sort->handle(); 22 | } elseif (!$this->query->getQuery()->orders) { 23 | $column = $this->config->template()->get('defaultSort'); 24 | $direction = $this->config->template()->get('defaultSortDirection'); 25 | 26 | $this->query->orderBy($column, $direction); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Services/Template.php: -------------------------------------------------------------------------------- 1 | meta = new Obj(); 26 | } 27 | 28 | public function __call($method, $args) 29 | { 30 | return $this->template->{$method}(...$args); 31 | } 32 | 33 | public function load(Obj $template, Obj $meta) 34 | { 35 | $this->template = $template; 36 | $this->meta = $meta; 37 | 38 | return $this; 39 | } 40 | 41 | public function buildCacheable() 42 | { 43 | $this->template = $this->template(); 44 | 45 | $this->builder()->handleCacheable(); 46 | 47 | return $this; 48 | } 49 | 50 | public function buildNonCacheable() 51 | { 52 | $this->builder()->handleNonCacheable(); 53 | 54 | return $this; 55 | } 56 | 57 | public function toArray() 58 | { 59 | return [ 60 | 'template' => $this->template, 61 | 'meta' => $this->meta, 62 | 'apiVersion' => Config::get('enso.tables.apiVersion'), 63 | ]; 64 | } 65 | 66 | public function columns() 67 | { 68 | return $this->template->get('columns'); 69 | } 70 | 71 | public function buttons(): Collection 72 | { 73 | return $this->template->get('buttons'); 74 | } 75 | 76 | public function meta(): Obj 77 | { 78 | return $this->meta; 79 | } 80 | 81 | public function column(string $index) 82 | { 83 | return $this->columns()[$index]; 84 | } 85 | 86 | private function builder() 87 | { 88 | return $this->builder 89 | ??= new Builder($this->template, $this->meta); 90 | } 91 | 92 | private function template() 93 | { 94 | $template = $this->readJson($this->table->templatePath()); 95 | $model = $this->table->query()->getModel(); 96 | 97 | if (!$template->has('model')) { 98 | $this->setModel($template, $model); 99 | } 100 | 101 | $this->setTable($template, $model); 102 | 103 | return $template; 104 | } 105 | 106 | private function setModel(Obj $template, Model $model) 107 | { 108 | $model = (new ReflectionClass($model))->getShortName(); 109 | 110 | $template->set('model', Str::camel($model)); 111 | } 112 | 113 | private function setTable(Obj $template, Model $model): self 114 | { 115 | $template->set('table', $model->getTable()); 116 | 117 | return $this; 118 | } 119 | 120 | private function readJson($path) 121 | { 122 | $template = new Obj( 123 | (new JsonReader($path))->array() 124 | ); 125 | 126 | if ($this->needsValidation()) { 127 | (new Validator($template, $this->table))->run(); 128 | } 129 | 130 | return $template; 131 | } 132 | 133 | private function needsValidation() 134 | { 135 | $validations = Config::get('enso.tables.validations'); 136 | 137 | return in_array($validations, [App::environment(), 'always']); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Services/Template/Builder.php: -------------------------------------------------------------------------------- 1 | template, $this->meta))->build(); 24 | 25 | (new Columns($this->template, $this->meta))->build(); 26 | 27 | (new Style($this->template))->build(); 28 | 29 | (new Controls($this->template))->build(); 30 | } 31 | 32 | public function handleNonCacheable() 33 | { 34 | (new Buttons($this->template))->build(); 35 | 36 | (new Filters($this->template, $this->meta))->build(); 37 | 38 | $this->template->forget(['dataRouteSuffix', 'routePrefix']); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Services/Template/Builders/Buttons.php: -------------------------------------------------------------------------------- 1 | defaults = $this->defaults(); 19 | $this->template->set('actions', false); 20 | } 21 | 22 | public function build(): void 23 | { 24 | $buttons = $this->buttons(); 25 | 26 | $this->template->set('buttons', $buttons); 27 | $this->template->set('actions', $buttons->get('row')->isNotEmpty()); 28 | } 29 | 30 | private function buttons(): Obj 31 | { 32 | return $this->template->get('buttons') 33 | ->reduce(fn ($buttons, $button) => $this 34 | ->add($buttons, $button), $this->factory()); 35 | } 36 | 37 | private function add($buttons, $button): Collection 38 | { 39 | [$button, $type] = is_string($button) 40 | ? $this->default($button) 41 | : [$button, $button->get('type')]; 42 | 43 | if ($this->shouldDisplay($button, $type)) { 44 | $button->forget(['fullRoute', 'routeSuffix', 'type']); 45 | $buttons[$type]->push($button); 46 | } 47 | 48 | return $buttons; 49 | } 50 | 51 | private function shouldDisplay(Obj $button, string $type) 52 | { 53 | return !$button->has('action') 54 | && !$button->has('route') 55 | && !$button->has('routeSuffix') 56 | || $this->actionComputingSuccedes($button, $type); 57 | } 58 | 59 | private function default($button): array 60 | { 61 | return $this->defaults->get('global')->keys()->contains($button) 62 | ? [$this->defaults->get('global')->get($button), 'global'] 63 | : [$this->defaults->get('row')->get($button), 'row']; 64 | } 65 | 66 | private function actionComputingSuccedes($button, $type): bool 67 | { 68 | $route = $this->route($button); 69 | 70 | if ($this->routeForbidden($route)) { 71 | return false; 72 | } 73 | 74 | $this->pathOrRoute($button, $route, $type); 75 | 76 | return true; 77 | } 78 | 79 | private function route($button): ?string 80 | { 81 | if ( 82 | $button->has('fullRoute') 83 | && $button->get('fullRoute') !== null 84 | ) { 85 | return $button->get('fullRoute'); 86 | } 87 | 88 | return $button->has('routeSuffix') 89 | && $button->get('routeSuffix') !== null 90 | ? "{$this->template->get('routePrefix')}.{$button->get('routeSuffix')}" 91 | : null; 92 | } 93 | 94 | private function pathOrRoute($button, $route, $type) 95 | { 96 | if (in_array($button->get('action'), self::PathActions)) { 97 | $param = $type === 'row' ? 'dtRowId' : null; 98 | $absolute = Config::get('enso.tables.absoluteRoutes'); 99 | $button->set('path', route($route, [$param], $absolute)); 100 | } else { 101 | $button->set('route', $route); 102 | } 103 | } 104 | 105 | private function routeForbidden($route): bool 106 | { 107 | return $this->needAuthorization() 108 | && Auth::user()->cannot('access-route', $route); 109 | } 110 | 111 | private function needAuthorization() 112 | { 113 | return !empty(Config::get('enso.config')) 114 | && $this->template->get('auth') !== false; 115 | } 116 | 117 | private function defaults(): Obj 118 | { 119 | return (new Obj(Config::get('enso.tables.buttons'))) 120 | ->each(fn ($group) => $group 121 | ->each(fn ($button, $key) => $button->set('name', $key))); 122 | } 123 | 124 | private function factory(): Obj 125 | { 126 | return new Obj(['global' => [], 'row' => [], 'dropdown' => []]); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Services/Template/Builders/Columns.php: -------------------------------------------------------------------------------- 1 | template->set('columns', $this->columns()); 30 | } 31 | 32 | private function columns() 33 | { 34 | return $this->template->get('columns') 35 | ->reduce(fn ($columns, $column) => $columns 36 | ->push($this->compute($column)), new Obj()); 37 | } 38 | 39 | private function compute($column): Obj 40 | { 41 | $this->meta($column) 42 | ->enum($column) 43 | ->number($column) 44 | ->sort($column) 45 | ->total($column) 46 | ->visibility($column) 47 | ->defaults($column); 48 | 49 | return $column; 50 | } 51 | 52 | private function meta($column): self 53 | { 54 | $meta = Collection::wrap(Attributes::Meta) 55 | ->reduce(fn ($meta, $attribute) => $meta 56 | ->set($attribute, $column->get('meta')?->contains($attribute)), new Obj()); 57 | 58 | $meta->set('visible', true) 59 | ->set('hidden', false); 60 | 61 | $column->set('meta', $meta); 62 | 63 | return $this; 64 | } 65 | 66 | private function enum($column): self 67 | { 68 | if (!$column->has('enum')) { 69 | return $this; 70 | } 71 | 72 | if (enum_exists($column->get('enum'))) { 73 | $column->set('enum', Collection::wrap($column->get('enum')::cases()) 74 | ->mapWithKeys(fn ($value) => [$value->value => (new ReflectionEnum($column->get('enum'))) 75 | ->implementsInterface(Mappable::class) 76 | ? $value->map() 77 | : $value->name, ]))->toArray(); 78 | } else { 79 | $this->setLegacyEnum($column); 80 | } 81 | 82 | return $this; 83 | } 84 | 85 | private function setLegacyEnum($column): void 86 | { 87 | $enum = App::make($column->get('enum')); 88 | $enum::localisation(false); 89 | $column->set('enum', $enum::all()); 90 | $enum::localisation(true); 91 | } 92 | 93 | private function number($column): self 94 | { 95 | if ($column->has('number')) { 96 | $number = $column->get('number'); 97 | $number->set('symbol', $number->get('symbol', '')); 98 | $number->set('precision', $number->get('precision', 0)); 99 | $number->set('template', $number->get('template', '%s%v')); 100 | } 101 | 102 | return $this; 103 | } 104 | 105 | private function sort($column): self 106 | { 107 | $meta = $column->get('meta'); 108 | 109 | $templateSort = $this->templateSort($meta); 110 | 111 | $meta->set('sort', $templateSort); 112 | 113 | $meta->forget(['sort:ASC', 'sort:DESC']); 114 | 115 | if ($templateSort) { 116 | $this->meta->set('sort', true); 117 | } 118 | 119 | return $this; 120 | } 121 | 122 | private function templateSort($meta): ?string 123 | { 124 | if ($meta->get('sort:ASC')) { 125 | return 'ASC'; 126 | } 127 | 128 | if ($meta->get('sort:DESC')) { 129 | return 'DESC'; 130 | } 131 | 132 | return $meta->get('sort'); 133 | } 134 | 135 | private function total($column): self 136 | { 137 | $meta = $column->get('meta'); 138 | 139 | $hasTotal = $meta->get('total') || $meta->get('rawTotal') 140 | || $meta->get('customTotal') || $meta->get('average'); 141 | 142 | if ($hasTotal) { 143 | $this->meta->set('total', true); 144 | } 145 | 146 | return $this; 147 | } 148 | 149 | private function visibility($column): self 150 | { 151 | if ($column->get('meta')->get('notVisible')) { 152 | $column->get('meta')->set('visible', false); 153 | $column->get('meta')->forget('notVisible'); 154 | } 155 | 156 | return $this; 157 | } 158 | 159 | private function defaults($column): void 160 | { 161 | $this->fromColumn($column) 162 | ->merge($this->fromColumnMeta($column)) 163 | ->each(fn ($attribute) => $this->meta->set($attribute, true)); 164 | } 165 | 166 | private function fromColumn($column): Collection 167 | { 168 | return Collection::wrap(self::FromColumn) 169 | ->filter(fn ($attribute) => $column->get($attribute)); 170 | } 171 | 172 | private function fromColumnMeta($column): Collection 173 | { 174 | return Collection::wrap(self::FromMeta) 175 | ->filter(fn ($attribute) => $column->get('meta')->get($attribute)); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Services/Template/Builders/Controls.php: -------------------------------------------------------------------------------- 1 | template->has('controls')) { 18 | return; 19 | } 20 | 21 | $controls = Config::get('enso.tables.controls') ?? Attributes::List; 22 | $this->template->set('controls', $controls); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Services/Template/Builders/Filters.php: -------------------------------------------------------------------------------- 1 | template->has('filters')) { 20 | return; 21 | } 22 | 23 | $withRoute = fn ($filter) => $filter->has('route'); 24 | $allowed = fn ($filter) => !$withRoute($filter) || $this->routeAllowed($filter->get('route')); 25 | 26 | $filters = $this->template->get('filters')->filter($allowed) 27 | ->map(fn ($filter) => $this->compute($filter)); 28 | 29 | $this->template->set('filters', $filters); 30 | $this->meta->set('filterable', true); 31 | } 32 | 33 | private function compute(Obj $filter): Obj 34 | { 35 | $absolute = Config::get('enso.tables.absoluteRoutes'); 36 | 37 | return $filter->when($filter->has('route'), fn ($filter) => $filter 38 | ->set('path', route($filter->get('route'), [], $absolute)) 39 | ->forget('route')); 40 | } 41 | 42 | private function routeAllowed($route): bool 43 | { 44 | return !$this->needAuthorization() 45 | || Auth::user()->can('access-route', $route); 46 | } 47 | 48 | private function needAuthorization() 49 | { 50 | return !empty(Config::get('enso.config')) 51 | && $this->template->get('auth') !== false; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Services/Template/Builders/Structure.php: -------------------------------------------------------------------------------- 1 | customDtRowId = $this->template->has('dtRowId'); 37 | } 38 | 39 | public function build(): void 40 | { 41 | $this->defaults() 42 | ->defaultFromConfig() 43 | ->falseIfMissing() 44 | ->name() 45 | ->readPath() 46 | ->length() 47 | ->templateOrConfigToMeta() 48 | ->defaultSort(); 49 | } 50 | 51 | private function defaults(): self 52 | { 53 | $this->meta->set('start', 0); 54 | $this->meta->set('search', ''); 55 | 56 | Collection::wrap(self::DefaultFalse) 57 | ->each(fn ($attribute) => $this->meta->set($attribute, false)); 58 | 59 | return $this; 60 | } 61 | 62 | private function defaultFromConfig() 63 | { 64 | Collection::wrap(self::DefaultFromConfig) 65 | ->filter(fn ($attribute) => !$this->template->has($attribute)) 66 | ->each(fn ($attribute) => $this->template->set( 67 | $attribute, 68 | Config::get("enso.tables.{$attribute}") 69 | )); 70 | 71 | return $this; 72 | } 73 | 74 | private function falseIfMissing() 75 | { 76 | Collection::wrap(self::FalseIfMissing) 77 | ->filter(fn ($attribute) => !$this->template->has($attribute)) 78 | ->each(fn ($attribute) => $this->template->set($attribute, false)); 79 | 80 | return $this; 81 | } 82 | 83 | private function name() 84 | { 85 | if (!$this->template->has('name')) { 86 | $this->template->set('name', Str::plural($this->template->get('model'))); 87 | } 88 | 89 | return $this; 90 | } 91 | 92 | private function readPath(): self 93 | { 94 | $prefix = $this->template->get('routePrefix'); 95 | 96 | $suffix = $this->template->get('dataRouteSuffix') 97 | ?? Config::get('enso.tables.dataRouteSuffix'); 98 | 99 | $absolute = Config::get('enso.tables.absoluteRoutes'); 100 | 101 | $this->template->set('readPath', route("{$prefix}.{$suffix}", [], $absolute)); 102 | 103 | return $this; 104 | } 105 | 106 | private function length(): self 107 | { 108 | $this->meta->set( 109 | 'length', 110 | $this->template->get('lengthMenu')[0] 111 | ); 112 | 113 | return $this; 114 | } 115 | 116 | private function templateOrConfigToMeta(): self 117 | { 118 | Collection::wrap(self::TemplateOrConfigToMeta) 119 | ->each(fn ($attribute) => $this->metaFromTemplateOrConfig($attribute)); 120 | 121 | return $this; 122 | } 123 | 124 | private function defaultSort(): void 125 | { 126 | if (!$this->template->has('defaultSort')) { 127 | $defaultSort = $this->customDtRowId 128 | ? $this->template->get('dtRowId') 129 | : "{$this->template->get('table')}.{$this->template->get('dtRowId')}"; 130 | 131 | $this->template->set('defaultSort', $defaultSort); 132 | } 133 | 134 | if (!$this->template->has('defaultSortDirection')) { 135 | $this->template->set('defaultSortDirection', 'asc'); 136 | } 137 | } 138 | 139 | private function metaFromTemplateOrConfig(string $attribute): void 140 | { 141 | $value = $this->template->get($attribute) 142 | ?? Config::get("enso.tables.{$attribute}"); 143 | 144 | $this->meta->set($attribute, $value); 145 | 146 | $this->template->forget($attribute); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Services/Template/Builders/Style.php: -------------------------------------------------------------------------------- 1 | template = $template; 17 | $this->defaultStyle = new Obj(Config::get('enso.tables.style')); 18 | } 19 | 20 | public function build(): void 21 | { 22 | $this->template->set('align', $this->compute(Attributes::Align)) 23 | ->set('style', $this->compute(Attributes::Table)) 24 | ->set('aligns', $this->preset(Attributes::Align)) 25 | ->set('styles', $this->preset(Attributes::Table)) 26 | ->set('highlight', $this->defaultStyle->get('highlight')); 27 | } 28 | 29 | private function compute($style): Collection 30 | { 31 | return $this->defaultStyle->get('default') 32 | ->intersect($style) 33 | ->values() 34 | ->reduce(fn ($style, $param) => $style 35 | ->push($this->defaultStyle->get('mapping')->get($param)), new Collection()) 36 | ->unique(); 37 | } 38 | 39 | private function preset($style): Obj 40 | { 41 | return Collection::wrap($style) 42 | ->reduce(fn ($styles, $style) => $styles 43 | ->set($style, $this->defaultStyle->get('mapping')->get($style)), new Obj()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Services/Template/Validator.php: -------------------------------------------------------------------------------- 1 | template))->validate(); 26 | 27 | (new Attributes($this->template))->validate(); 28 | 29 | (new Route($this->template))->validate(); 30 | 31 | (new Buttons($this->template, $this->table))->validate(); 32 | 33 | (new Filters($this->template))->validate(); 34 | 35 | (new Controls($this->template))->validate(); 36 | 37 | (new Columns($this->template))->validate(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Services/Template/Validators/Buttons/Button.php: -------------------------------------------------------------------------------- 1 | each(fn ($validation) => $this->{$validation}()); 30 | } 31 | 32 | private function mandatory(): void 33 | { 34 | Collection::wrap(Attributes::Mandatory) 35 | ->diff($this->button->keys()) 36 | ->whenNotEmpty(fn () => throw Exception::missingAttributes()); 37 | } 38 | 39 | private function optional(): void 40 | { 41 | $this->button->keys() 42 | ->diff(Attributes::Mandatory) 43 | ->diff(Attributes::Optional) 44 | ->whenNotEmpty(fn () => throw Exception::unknownAttributes()); 45 | } 46 | 47 | private function type(): void 48 | { 49 | $invalid = !in_array($this->button->get('type'), Attributes::Types); 50 | 51 | if ($invalid) { 52 | throw Exception::invalidType(); 53 | } 54 | } 55 | 56 | private function action(): void 57 | { 58 | if (!$this->button->has('action')) { 59 | return; 60 | } 61 | 62 | $invalid = !in_array($this->button->get('action'), Attributes::Actions); 63 | 64 | if ($invalid) { 65 | throw Exception::invalidAction(); 66 | } 67 | 68 | $this->route() 69 | ->method(); 70 | } 71 | 72 | private function route(): self 73 | { 74 | $route = $this->button->get('fullRoute'); 75 | 76 | $route ??= $this->button->has('routeSuffix') 77 | ? "{$this->template->get('routePrefix')}.{$this->button->get('routeSuffix')}" 78 | : null; 79 | 80 | if ($route === null) { 81 | throw Exception::missingRoute(); 82 | } 83 | 84 | if (!Route::has($route) && $this->button->get('action') !== 'router') { 85 | throw Exception::routeNotFound($route); 86 | } 87 | 88 | return $this; 89 | } 90 | 91 | private function method(): void 92 | { 93 | if ($this->button->has('method')) { 94 | $invalid = !in_array($this->button->get('method'), Attributes::Methods); 95 | 96 | if ($invalid) { 97 | throw Exception::invalidMethod($this->button->get('method')); 98 | } 99 | } else { 100 | if ($this->button->get('action') === 'ajax') { 101 | throw Exception::missingMethod(); 102 | } 103 | } 104 | } 105 | 106 | private function name(): void 107 | { 108 | $missing = $this->table instanceof ConditionalActions 109 | && $this->button->get('type') === 'row' 110 | && !$this->button->has('name'); 111 | 112 | if ($missing) { 113 | throw Exception::missingName(); 114 | } 115 | } 116 | 117 | private function selection(): void 118 | { 119 | if (!$this->button->get('selection')) { 120 | return; 121 | } 122 | 123 | if ($this->button->get('type') === 'row') { 124 | throw Exception::rowSelection(); 125 | } 126 | 127 | if (!$this->template->get('selectable')) { 128 | throw Exception::noSelectable(); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Services/Template/Validators/Buttons/Buttons.php: -------------------------------------------------------------------------------- 1 | defaults = $this->configButtons(); 22 | } 23 | 24 | public function validate(): void 25 | { 26 | Collection::wrap(self::Validations) 27 | ->each(fn ($validation) => $this->{$validation}()); 28 | } 29 | 30 | private function format(): void 31 | { 32 | $invalid = $this->template->get('buttons') 33 | ->filter(fn ($button) => !is_string($button) && !$button instanceof Obj); 34 | 35 | if ($invalid->isNotEmpty()) { 36 | throw Exception::invalidFormat(); 37 | } 38 | } 39 | 40 | private function defaults(): void 41 | { 42 | $diff = $this->template->get('buttons') 43 | ->filter(fn ($button) => is_string($button)) 44 | ->diff($this->defaults->keys()); 45 | 46 | if ($diff->isNotEmpty()) { 47 | throw Exception::undefined($diff->implode('", "')); 48 | } 49 | } 50 | 51 | private function structure(): void 52 | { 53 | $this->template->get('buttons') 54 | ->map(fn ($button) => $this->map($button)) 55 | ->each(fn ($button) => (new Button($button, $this->table, $this->template)) 56 | ->validate()); 57 | } 58 | 59 | private function map($button) 60 | { 61 | return $button instanceof Obj 62 | ? $button 63 | : $this->defaults->get($button); 64 | } 65 | 66 | private function configButtons(): Obj 67 | { 68 | $global = (new Obj(Config::get('enso.tables.buttons.global'))) 69 | ->map(fn ($button, $key) => $button->set('type', 'global') 70 | ->set('name', $key)); 71 | 72 | $row = (new Obj(Config::get('enso.tables.buttons.row'))) 73 | ->map(fn ($button, $key) => $button->set('type', 'row') 74 | ->set('name', $key)); 75 | 76 | return $global->merge($row); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Services/Template/Validators/Columns/Column.php: -------------------------------------------------------------------------------- 1 | each(fn ($validation) => $this->{$validation}()); 31 | } 32 | 33 | private function mandatory(): void 34 | { 35 | $diff = Collection::wrap(Attributes::Mandatory) 36 | ->diff($this->column->keys()); 37 | 38 | if ($diff->isNotEmpty()) { 39 | throw Exception::missingAttributes($diff->implode('", "')); 40 | } 41 | } 42 | 43 | private function optional(): void 44 | { 45 | $attributes = Collection::wrap(Attributes::Mandatory) 46 | ->merge(Attributes::Optional); 47 | 48 | $diff = $this->column->keys()->diff($attributes); 49 | 50 | if ($diff->isNotEmpty()) { 51 | throw Exception::unknownAttributes($diff->implode('", "')); 52 | } 53 | } 54 | 55 | private function align(): void 56 | { 57 | if ($this->invalidAttribute('align', Style::Align)) { 58 | throw Exception::invalidAlign($this->column->get('name')); 59 | } 60 | } 61 | 62 | private function class(): void 63 | { 64 | if ($this->invalidString('class')) { 65 | throw Exception::invalidClass($this->column->get('name')); 66 | } 67 | } 68 | 69 | private function enum(): void 70 | { 71 | if ($this->column->has('enum')) { 72 | if ($this->enumNotFound()) { 73 | throw Exception::enumNotFound($this->column->get('enum')); 74 | } elseif ($this->invalidEnum()) { 75 | throw Exception::invalidEnum($this->column->get('enum')); 76 | } 77 | } 78 | } 79 | 80 | private function meta(): void 81 | { 82 | if ($this->column->has('meta')) { 83 | Meta::validate($this->column); 84 | } 85 | } 86 | 87 | private function number(): void 88 | { 89 | if ($this->invalidObject('number')) { 90 | throw Exception::invalidNumber($this->column->get('name')); 91 | } 92 | 93 | if ($this->invalidAttributes('number', Number::Optional)) { 94 | throw Exception::invalidNumberAttributes($this->column->get('name')); 95 | } 96 | } 97 | 98 | private function resource(): void 99 | { 100 | if ($this->missingClass('resource')) { 101 | throw Exception::resourceNotFound($this->column->get('resource')); 102 | } 103 | } 104 | 105 | private function tooltip(): void 106 | { 107 | if ($this->invalidString('tooltip')) { 108 | throw Exception::invalidTooltip($this->column->get('name')); 109 | } 110 | } 111 | 112 | private function missingClass(string $attribute): bool 113 | { 114 | return $this->column->has($attribute) 115 | && !class_exists($this->column->get($attribute)); 116 | } 117 | 118 | public function enumNotFound(): bool 119 | { 120 | return !class_exists($this->column->get('enum')) 121 | && !enum_exists($this->column->get('enum')); 122 | } 123 | 124 | public function invalidEnum(): bool 125 | { 126 | return enum_exists($this->column->get('enum')) 127 | ? !(new ReflectionEnum($this->column->get('enum'))) 128 | ->implementsInterface(Contract::class) 129 | : !(new ReflectionClass($this->column->get('enum'))) 130 | ->isSubclassOf(Enum::class); 131 | } 132 | 133 | private function invalidString(string $attribute): bool 134 | { 135 | return $this->column->has($attribute) 136 | && !is_string($this->column->get($attribute)); 137 | } 138 | 139 | private function invalidObject(string $attribute): bool //TODO can be aggregated with invalidAttributes 140 | { 141 | return $this->column->has($attribute) 142 | && !is_object($this->column->get($attribute)); 143 | } 144 | 145 | private function invalidAttribute(string $attribute, array $allowed): bool 146 | { 147 | return $this->column->has($attribute) 148 | && !in_array($this->column->get($attribute), $allowed); 149 | } 150 | 151 | private function invalidAttributes(string $attribute, array $allowed): bool 152 | { 153 | return $this->column->has($attribute) 154 | && Collection::wrap($this->column->get($attribute)) 155 | ->keys()->diff($allowed)->isNotEmpty(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Services/Template/Validators/Columns/Columns.php: -------------------------------------------------------------------------------- 1 | columns = $template->get('columns'); 15 | } 16 | 17 | public function validate() 18 | { 19 | $this->format() 20 | ->columns(); 21 | } 22 | 23 | private function columns() 24 | { 25 | $this->columns 26 | ->each(fn ($column) => (new Column($column))->validate()); 27 | } 28 | 29 | private function format() 30 | { 31 | if ($this->invalidFormat() || $this->invalidChild()) { 32 | throw Exception::invalidFormat(); 33 | } 34 | 35 | return $this; 36 | } 37 | 38 | private function invalidFormat() 39 | { 40 | return !$this->columns instanceof Obj 41 | || $this->columns->isEmpty(); 42 | } 43 | 44 | private function invalidChild() 45 | { 46 | return $this->columns 47 | ->some(fn ($column) => !$column instanceof Obj); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Services/Template/Validators/Columns/Meta.php: -------------------------------------------------------------------------------- 1 | get('meta'); 14 | 15 | $diff = $meta->diff(Attributes::Meta); 16 | 17 | if ($diff->isNotEmpty()) { 18 | throw Exception::unknownAttributes($diff->implode('", "')); 19 | } 20 | 21 | if ($meta->has('filterable') && $meta->has('icon')) { 22 | throw Exception::cannotFilterIcon($column->get('name')); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Services/Template/Validators/Controls.php: -------------------------------------------------------------------------------- 1 | controls = $template->get('controls'); 17 | $this->defaults = new Obj(Config::get('enso.tables.controls')); 18 | } 19 | 20 | public function validate() 21 | { 22 | if ($this->controls !== null) { 23 | $this->format() 24 | ->defaults(); 25 | } 26 | } 27 | 28 | private function format() 29 | { 30 | if ($this->invalidFormat()) { 31 | throw Exception::invalidFormat(); 32 | } 33 | 34 | return $this; 35 | } 36 | 37 | private function invalidFormat() 38 | { 39 | return !$this->controls instanceof Obj || $this->controls 40 | ->filter(fn ($control) => !is_string($control)) 41 | ->isNotEmpty(); 42 | } 43 | 44 | private function defaults() 45 | { 46 | $diff = $this->controls->diff($this->defaults); 47 | 48 | if ($diff->isNotEmpty()) { 49 | throw Exception::undefined($diff->implode('", "')); 50 | } 51 | 52 | return $this; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Services/Template/Validators/Filters/Filter.php: -------------------------------------------------------------------------------- 1 | each(fn ($validation) => $this->{$validation}()); 23 | } 24 | 25 | private function mandatory(): void 26 | { 27 | $missing = Collection::wrap(Attributes::Mandatory) 28 | ->diff($this->filter->keys()) 29 | ->isNotEmpty(); 30 | 31 | if ($missing) { 32 | throw Exception::missingAttributes(); 33 | } 34 | } 35 | 36 | private function optional(): void 37 | { 38 | $unknown = $this->filter->keys() 39 | ->diff(Attributes::Mandatory) 40 | ->diff(Attributes::Optional) 41 | ->isNotEmpty(); 42 | 43 | if ($unknown) { 44 | throw Exception::unknownAttributes(); 45 | } 46 | } 47 | 48 | private function complementary(): void 49 | { 50 | if ($this->filter->get('type') === 'select' && !$this->filter->has('route')) { 51 | throw Exception::missingRoute(); 52 | } 53 | } 54 | 55 | private function route(): void 56 | { 57 | $route = $this->filter->get('route'); 58 | 59 | if ($route !== null && !Route::has($route)) { 60 | throw Exception::routeNotFound($route); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Services/Template/Validators/Filters/Filters.php: -------------------------------------------------------------------------------- 1 | filters = $template->get('filters'); 15 | } 16 | 17 | public function validate(): void 18 | { 19 | if ($this->filters) { 20 | $this->format() 21 | ->structure(); 22 | } 23 | } 24 | 25 | private function format(): self 26 | { 27 | $invalid = $this->filters 28 | ->filter(fn ($filter) => !is_string($filter) && !$filter instanceof Obj); 29 | 30 | if ($invalid->isNotEmpty()) { 31 | throw Exception::invalidFormat(); 32 | } 33 | 34 | return $this; 35 | } 36 | 37 | private function structure(): self 38 | { 39 | $this->filters->each(fn ($filter) => (new Filter($filter))->validate()); 40 | 41 | return $this; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Services/Template/Validators/Route.php: -------------------------------------------------------------------------------- 1 | readRoute = $this->readRoute($template); 17 | } 18 | 19 | public function validate() 20 | { 21 | if (!Facade::has($this->readRoute)) { 22 | throw Exception::notFound($this->readRoute); 23 | } 24 | } 25 | 26 | private function readRoute(Obj $template) 27 | { 28 | $suffix = $template->has('dataRouteSuffix') 29 | ? $template->get('dataRouteSuffix') 30 | : Config::get('enso.tables.dataRouteSuffix'); 31 | 32 | return "{$template->get('routePrefix')}.{$suffix}"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Services/Template/Validators/Structure/Attributes.php: -------------------------------------------------------------------------------- 1 | lengthMenu() 19 | ->appends() 20 | ->searchMode() 21 | ->defaultSortDirection() 22 | ->debounce() 23 | ->method() 24 | ->selectable() 25 | ->comparisonOperator(); 26 | } 27 | 28 | private function lengthMenu() 29 | { 30 | if ( 31 | $this->template->has('lengthMenu') 32 | && !$this->template->get('lengthMenu') instanceof Obj 33 | ) { 34 | throw Exception::invalidLengthMenu(); 35 | } 36 | 37 | return $this; 38 | } 39 | 40 | private function appends() 41 | { 42 | if ( 43 | $this->template->has('appends') 44 | && !$this->template->get('appends') instanceof Obj 45 | ) { 46 | throw Exception::invalidAppends(); 47 | } 48 | 49 | return $this; 50 | } 51 | 52 | private function searchMode() 53 | { 54 | if ( 55 | $this->template->has('searchModes') 56 | && !$this->template->get('searchModes') instanceof Obj 57 | ) { 58 | throw Exception::invalidSearchModes(); 59 | } 60 | 61 | return $this; 62 | } 63 | 64 | private function defaultSortDirection() 65 | { 66 | $allowed = ['asc', 'desc']; 67 | 68 | if ( 69 | $this->template->has('defaultSortDirection') 70 | && !in_array(Str::lower($this->template->get('defaultSortDirection')), $allowed) 71 | ) { 72 | throw Exception::invalidSortDirection(); 73 | } 74 | 75 | return $this; 76 | } 77 | 78 | private function debounce() 79 | { 80 | if ( 81 | $this->template->has('debounce') 82 | && !is_int($this->template->get('debounce')) 83 | ) { 84 | throw Exception::invalidDebounce(); 85 | } 86 | 87 | return $this; 88 | } 89 | 90 | private function method() 91 | { 92 | $invalid = $this->template->has('method') 93 | && !in_array($this->template->get('method'), ['GET', 'POST']); 94 | 95 | if ($invalid) { 96 | throw Exception::invalidMethod(); 97 | } 98 | 99 | return $this; 100 | } 101 | 102 | private function selectable() 103 | { 104 | if ( 105 | $this->template->has('selectable') 106 | && !is_bool($this->template->get('selectable')) 107 | ) { 108 | throw Exception::invalidSelectable(); 109 | } 110 | 111 | return $this; 112 | } 113 | 114 | private function comparisonOperator() 115 | { 116 | $invalid = $this->template->has('comparisonOperator') 117 | && $this->template->get('comparisonOperator') !== ComparisonOperators::Like 118 | && $this->template->get('comparisonOperator') !== ComparisonOperators::ILike; 119 | 120 | if ($invalid) { 121 | throw Exception::invalidComparisonOperator(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Services/Template/Validators/Structure/Structure.php: -------------------------------------------------------------------------------- 1 | mandatoryAttributes() 19 | ->optionalAttributes(); 20 | } 21 | 22 | private function mandatoryAttributes() 23 | { 24 | $diff = Collection::wrap(Attributes::Mandatory) 25 | ->diff($this->template->keys()); 26 | 27 | if ($diff->isNotEmpty()) { 28 | throw Exception::missingAttributes($diff->implode('", "')); 29 | } 30 | 31 | return $this; 32 | } 33 | 34 | private function optionalAttributes() 35 | { 36 | $attributes = Collection::wrap(Attributes::Mandatory) 37 | ->merge(Attributes::Optional); 38 | 39 | $diff = $this->template->keys()->diff($attributes); 40 | 41 | if ($diff->isNotEmpty()) { 42 | throw Exception::unknownAttributes($diff->implode('", "')); 43 | } 44 | 45 | return $this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Services/TemplateLoader.php: -------------------------------------------------------------------------------- 1 | load(); 24 | 25 | return $this->template; 26 | } 27 | 28 | private function load() 29 | { 30 | $this->template = $this->fromCache() ?? $this->new(); 31 | 32 | if ($this->shouldCache()) { 33 | $this->cache()->put($this->cacheKey(), $this->template->toArray()); 34 | } 35 | 36 | $this->template->buildNonCacheable(); 37 | } 38 | 39 | private function fromCache() 40 | { 41 | if (!$this->cache()->has($this->cacheKey())) { 42 | return; 43 | } 44 | 45 | $this->cache = $this->cache()->get($this->cacheKey()); 46 | 47 | return (new Template($this->table)) 48 | ->load($this->cache['template'], $this->cache['meta']); 49 | } 50 | 51 | private function new() 52 | { 53 | return (new Template($this->table))->buildCacheable(); 54 | } 55 | 56 | private function shouldCache() 57 | { 58 | if (isset($this->cache)) { 59 | return false; 60 | } 61 | 62 | $type = $this->template->get( 63 | 'templateCache', 64 | Config::get('enso.tables.cache.template') 65 | ); 66 | 67 | switch ($type) { 68 | case 'never': 69 | return false; 70 | case 'always': 71 | return true; 72 | default: 73 | return app()->environment($type); 74 | } 75 | } 76 | 77 | private function cacheKey(): string 78 | { 79 | $configPrefix = Config::get('enso.tables.cache.prefix'); 80 | 81 | $prefix = $this->table instanceof DynamicTemplate 82 | ? "{$this->table->cachePrefix()}:" 83 | : null; 84 | 85 | return Str::of($this->table->templatePath()) 86 | ->replace(['/', '.'], [' ', ' ']) 87 | ->slug() 88 | ->prepend("{$configPrefix}:{$prefix}"); 89 | } 90 | 91 | private function cache() 92 | { 93 | return Cache::getStore() instanceof TaggableStore 94 | ? Cache::tags(Config::get('enso.tables.cache.tag')) 95 | : Cache::store(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Traits/Action.php: -------------------------------------------------------------------------------- 1 | actionClass, $this->data($request))->handle(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Traits/Data.php: -------------------------------------------------------------------------------- 1 | $table, 'config' => $config] = $this->data($request); 16 | 17 | return (new DataBuilder($table, $config))->toArray() 18 | + (new MetaBuilder($table, $config))->toArray(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Traits/Excel.php: -------------------------------------------------------------------------------- 1 | tableClass($request) 17 | : $this->tableClass; 18 | 19 | $user = $request->user(); 20 | ['config' => $config] = $this->data($request); 21 | $attrs = [$user, $config, $tableClass]; 22 | 23 | $user->notifyNow(new ExportStarted($config->label())); 24 | 25 | (new Prepare(...$attrs))->handle(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Traits/Init.php: -------------------------------------------------------------------------------- 1 | tableClass($request) 17 | : $this->tableClass; 18 | 19 | $table = App::make($tableClass, [ 20 | 'request' => $this->request($request), 21 | ]); 22 | 23 | $template = (new TemplateLoader($table))->handle(); 24 | 25 | return $template->toArray(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Traits/ProvidesData.php: -------------------------------------------------------------------------------- 1 | tableClass($request) 18 | : $this->tableClass; 19 | 20 | $request = $this->request($request); 21 | $table = App::make($tableClass, ['request' => $request]); 22 | $template = (new TemplateLoader($table))->handle(); 23 | $config = new Config($request, $template); 24 | 25 | return ['table' => $table, 'config' => $config]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Traits/ProvidesRequest.php: -------------------------------------------------------------------------------- 1 | get('internalFilters'), 15 | $request->get('filters'), 16 | $request->get('intervals'), 17 | $request->get('params') 18 | ); 19 | 20 | return new TableRequest($request->get('columns'), $request->get('meta'), $aggregator()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Traits/TableCache.php: -------------------------------------------------------------------------------- 1 | $model->resetTableCache()); 14 | 15 | self::deleted(fn ($model) => $model->resetTableCache()); 16 | } 17 | 18 | public function resetTableCache() 19 | { 20 | $key = $this->tableCacheKey(); 21 | 22 | if (Cache::getStore() instanceof TaggableStore) { 23 | Cache::tags($key)->flush(); 24 | } else { 25 | Cache::forget($key); 26 | } 27 | } 28 | 29 | public function tableCacheKey(): string 30 | { 31 | $prefix = Config::get('enso.tables.cache.prefix'); 32 | 33 | return "{$prefix}:{$this->getTable()}"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Traits/Tests/Datatable.php: -------------------------------------------------------------------------------- 1 | permissionGroup)) { 14 | throw new Exception('"permissionGroup" property is missing from your test'); 15 | } 16 | 17 | $absolute = Config::get('enso.tables.absoluteRoutes'); 18 | 19 | $init = $this->get(route($this->permissionGroup.'.initTable', [], $absolute)); 20 | 21 | $init->assertStatus(200) 22 | ->assertJsonStructure(['template', 'meta', 'apiVersion']); 23 | 24 | $params = [ 25 | 'columns' => [], 26 | 'meta' => '{"start":0,"length":10,"sort":false,"search": "","forceInfo":false,"searchMode":"full"}', 27 | ]; 28 | 29 | $this->get(route($this->permissionGroup.'.tableData', $params, $absolute)) 30 | ->assertStatus(200) 31 | ->assertJsonStructure(['data']); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /stubs/Tables/Actions/CustomAction.stub: -------------------------------------------------------------------------------- 1 | request() : Obj 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /stubs/Tables/Builders/ModelTable.stub: -------------------------------------------------------------------------------- 1 | 'relation', 13 | ]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/units/Services/SetUp.php: -------------------------------------------------------------------------------- 1 | setUpParralelTesting(); 31 | } 32 | 33 | $this->faker = Factory::create(); 34 | 35 | Route::any('route')->name('testTables.tableData'); 36 | Route::getRoutes()->refreshNameLookups(); 37 | 38 | TestModel::createTable(); 39 | 40 | $this->testModel = $this->createTestModel(); 41 | 42 | $columns = $internalFilters = $filters = $intervals = $params = []; 43 | 44 | $meta = ['length' => 10, 'search' => '', 'searchMode' => 'full']; 45 | $filters = [$internalFilters, $filters, $intervals, $params]; 46 | 47 | $aggregator = new FilterAggregator(...$filters); 48 | 49 | $request = new Request($columns, $meta, $aggregator()); 50 | 51 | $request->columns()->push(new Obj([ 52 | 'name' => 'name', 53 | 'data' => 'name', 54 | 'meta' => ['searchable' => true], 55 | ])); 56 | 57 | $this->table = new TestTable(); 58 | 59 | $template = (new Template($this->table))->buildCacheable() 60 | ->buildNonCacheable(); 61 | 62 | $this->config = new Config($request, $template); 63 | 64 | $this->query = $this->table->query(); 65 | } 66 | 67 | protected function createTestModel($name = null) 68 | { 69 | return TestModel::create([ 70 | 'name' => $name ?? $this->faker->name, 71 | 'price' => $this->faker->numberBetween(1000, 10000), 72 | ]); 73 | } 74 | 75 | protected function tearDown(): void 76 | { 77 | $token = ParallelTesting::token(); 78 | 79 | if ($token) { 80 | File::delete(Cache::get("table_{$token}_template")); 81 | 82 | Cache::forget("table_{$token}_template"); 83 | } 84 | 85 | parent::tearDown(); 86 | } 87 | 88 | private function setUpParralelTesting() 89 | { 90 | $token = ParallelTesting::token(); 91 | 92 | $base = base_path('vendor/laravel-enso/tables/tests/units/Services/templates'); 93 | 94 | $path = "{$base}/template_{$token}.json"; 95 | 96 | $template = new Collection(json_decode(File::get("{$base}/template.json"), true)); 97 | 98 | File::put($path, $template->toJson(JSON_PRETTY_PRINT)); 99 | 100 | Cache::put("table_{$token}_template", $path); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/units/Services/Table/Builders/ExportTest.php: -------------------------------------------------------------------------------- 1 | addJsonPath(__DIR__.'/lang'); 19 | 20 | App::setLocale('lang'); 21 | 22 | $this->testModel->update(['name' => 'should translate']); 23 | 24 | $this->config->meta()->set('translatable', true); 25 | 26 | $this->config->columns()->push(new Obj([ 27 | 'name' => 'name', 28 | 'data' => 'name', 29 | 'meta' => ['translatable' => true], 30 | ])); 31 | 32 | $response = $this->requestResponse(); 33 | 34 | $this->assertEquals('translation', $response[0]['name']); 35 | } 36 | 37 | private function requestResponse() 38 | { 39 | $fetcher = new Fetcher($this->table, $this->config); 40 | 41 | $fetcher->next(); 42 | 43 | return $fetcher->current(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/units/Services/Table/Builders/MetaTest.php: -------------------------------------------------------------------------------- 1 | requestResponse(); 22 | 23 | $this->assertEquals(TestModel::count(), $response->get('count')); 24 | } 25 | 26 | /** @test */ 27 | public function cannot_get_data_cache_count() 28 | { 29 | $this->config->put('countCache', false); 30 | 31 | $this->requestResponse(); 32 | 33 | $this->assertFalse(Cache::has('enso:tables:testModels')); 34 | } 35 | 36 | /** @test */ 37 | public function can_get_data_with_cache_when_table_cache_trait_used() 38 | { 39 | $key = 'enso:tables:test_models'; 40 | 41 | Config::set('enso.tables.cache.count', true); 42 | 43 | $this->requestResponse(); 44 | 45 | $this->assertEquals(1, $this->cache($key)->get($key)); 46 | } 47 | 48 | /** @test */ 49 | public function can_get_data_with_limit() 50 | { 51 | $this->config->meta()->put('length', 10); 52 | 53 | $response = $this->requestResponse(); 54 | 55 | $this->assertEquals(1, $response->get('filtered')); 56 | $this->assertEquals(1, $response->get('count')); 57 | } 58 | 59 | /** @test */ 60 | public function can_get_data_with_total() 61 | { 62 | $this->createTestModel(); 63 | 64 | $this->config->columns()->push(new Obj([ 65 | 'name' => 'price', 66 | 'data' => 'price', 67 | 'meta' => ['total' => true], 68 | ])); 69 | 70 | $this->config->meta()->put('total', true); 71 | 72 | $response = $this->requestResponse(); 73 | 74 | $this->assertEquals( 75 | TestModel::sum('price'), 76 | $response->get('total')->get('price') 77 | ); 78 | } 79 | 80 | /** @test */ 81 | public function can_use_full_info_record_limit() 82 | { 83 | $limit = 1; 84 | 85 | $this->createTestModel(); 86 | 87 | $this->testModel->update(['name' => 'User']); 88 | 89 | $this->config->columns()->push(new Obj([ 90 | 'name' => 'name', 91 | 'data' => 'name', 92 | 'meta' => ['searchable' => true], 93 | ])); 94 | 95 | $this->config->set('comparisonOperator', 'LIKE'); 96 | 97 | $this->config->meta()->set('search', $this->testModel->name) 98 | ->set('fullInfoRecordLimit', $limit) 99 | ->set('length', $limit) 100 | ->set('searchMode', 'full'); 101 | 102 | $response = $this->requestResponse(); 103 | 104 | $this->assertFalse($response->get('fullRecordInfo')); 105 | $this->assertEquals(2, $response->get('count')); 106 | $this->assertEquals(2, $response->get('filtered')); 107 | } 108 | 109 | private function requestResponse() 110 | { 111 | $builder = new Meta($this->table, $this->config); 112 | 113 | return new Obj($builder->toArray()); 114 | } 115 | 116 | private function cache($key) 117 | { 118 | return Cache::getStore() instanceof TaggableStore 119 | ? Cache::tags($key) 120 | : Cache::store(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/units/Services/Table/Builders/lang/lang.json: -------------------------------------------------------------------------------- 1 | {"should translate": "translation"} -------------------------------------------------------------------------------- /tests/units/Services/Table/Filters/FilterTest.php: -------------------------------------------------------------------------------- 1 | $this->testModel->name]); 18 | 19 | $this->config->filters()->set('test_models', $filters); 20 | 21 | $response = $this->requestResponse(); 22 | 23 | $this->assertCount(1, $response); 24 | 25 | $this->assertEquals( 26 | $response->first()->name, 27 | $this->testModel->name 28 | ); 29 | 30 | $filters->set('name', $this->testModel->name.'-'); 31 | 32 | $response = $this->requestResponse(); 33 | 34 | $this->assertCount(0, $response); 35 | } 36 | 37 | /** @test */ 38 | public function cannot_use_invalid_filters() 39 | { 40 | $filters = new Obj(['name' => null]); 41 | 42 | $this->config->filters()->set('test_models', $filters); 43 | 44 | $this->assertFalse($this->filter()->applies()); 45 | } 46 | 47 | private function requestResponse() 48 | { 49 | $this->filter()->handle(); 50 | 51 | return $this->query->get(); 52 | } 53 | 54 | private function filter() 55 | { 56 | return new Filter($this->table, $this->config, $this->query); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/units/Services/Table/Filters/IntervalTest.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'min' => $this->testModel->id - 1, 19 | 'max' => $this->testModel->id + 1, 20 | ]]); 21 | 22 | $this->config->intervals()->set('test_models', $intervals); 23 | 24 | $response = $this->requestResponse(); 25 | 26 | $this->assertCount(1, $response); 27 | 28 | $this->assertEquals( 29 | $this->testModel->name, 30 | $response->first()->name 31 | ); 32 | 33 | $intervals->get('id') 34 | ->set('min', $this->testModel->id - 2) 35 | ->set('max', $this->testModel->id - 1); 36 | 37 | $response = $this->requestResponse(); 38 | 39 | $this->assertCount(0, $response); 40 | } 41 | 42 | /** @test */ 43 | public function can_use_date_interval() 44 | { 45 | $intervals = new Obj(['created_at' => [ 46 | 'min' => $this->testModel->created_at->subDays(360)->format('Y-m-d'), 47 | 'max' => $this->testModel->created_at->addDays(1)->format('Y-m-d'), 48 | ]]); 49 | 50 | $this->config->intervals()->set('test_models', $intervals); 51 | 52 | $response = $this->requestResponse(); 53 | 54 | $this->assertCount(1, $response); 55 | 56 | $this->assertEquals( 57 | $this->testModel->name, 58 | $response->first()->name 59 | ); 60 | 61 | $intervals->get('created_at') 62 | ->set('min', $this->testModel->created_at->subDays(2)->format('Y-m-d')) 63 | ->set('max', $this->testModel->created_at->subDays(1)->format('Y-m-d')); 64 | 65 | $response = $this->requestResponse(); 66 | 67 | $this->assertCount(0, $response); 68 | } 69 | 70 | /** @test */ 71 | public function can_use_half_interval() 72 | { 73 | $intervals = new Obj([ 74 | 'id' => [ 75 | 'min' => $this->testModel->id - 1, 76 | 'max' => null, 77 | ], 78 | ]); 79 | 80 | $this->config->intervals()->set('test_models', $intervals); 81 | 82 | $response = $this->requestResponse(); 83 | 84 | $this->assertCount(1, $response); 85 | 86 | $this->assertEquals( 87 | $this->testModel->name, 88 | $response->first()->name 89 | ); 90 | } 91 | 92 | /** @test */ 93 | public function cannot_use_invalid_intervals() 94 | { 95 | $intervals = new Obj([ 96 | 'id' => [ 97 | 'min' => null, 98 | 'max' => null, 99 | ], 100 | ]); 101 | 102 | $this->config->intervals()->set('test_models', $intervals); 103 | 104 | $this->assertFalse($this->interval()->applies()); 105 | } 106 | 107 | private function requestResponse() 108 | { 109 | $this->interval()->handle(); 110 | 111 | return $this->query->get(); 112 | } 113 | 114 | private function interval() 115 | { 116 | return new Interval($this->table, $this->config, $this->query); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/units/Services/Table/Filters/SearchTest.php: -------------------------------------------------------------------------------- 1 | requestResponse(); 20 | 21 | $this->assertCount(TestModel::count(), $response); 22 | 23 | $this->assertTrue( 24 | $response->pluck('name') 25 | ->contains($this->testModel->name) 26 | ); 27 | } 28 | 29 | /** @test */ 30 | public function can_use_search() 31 | { 32 | $this->config->meta()->set('search', $this->testModel->name); 33 | 34 | $response = $this->requestResponse(); 35 | 36 | $this->assertCount(1, $response); 37 | 38 | $this->assertEquals( 39 | $response->first()->name, 40 | $this->testModel->name 41 | ); 42 | 43 | $this->config->meta()->set('search', $this->testModel->name.'-'); 44 | 45 | $response = $this->requestResponse(); 46 | 47 | $this->assertCount(0, $response); 48 | } 49 | 50 | /** @test */ 51 | public function can_use_starts_with_search() 52 | { 53 | $first = Collection::wrap(explode(' ', $this->testModel->name))->first(); 54 | 55 | $this->config->meta()->set('search', $first) 56 | ->set('searchMode', 'startsWith'); 57 | 58 | $response = $this->requestResponse(); 59 | 60 | $this->assertCount(1, $response); 61 | 62 | $this->assertEquals( 63 | $response->first()->name, 64 | $this->testModel->name 65 | ); 66 | } 67 | 68 | /** @test */ 69 | public function can_use_ends_with_search() 70 | { 71 | $this->config->meta() 72 | ->set('search', Collection::wrap(explode(' ', $this->testModel->name))->last()) 73 | ->set('searchMode', 'endsWith'); 74 | 75 | $response = $this->requestResponse(); 76 | 77 | $this->assertCount(1, $response); 78 | 79 | $this->assertEquals( 80 | $response->first()->name, 81 | $this->testModel->name 82 | ); 83 | } 84 | 85 | /** @test */ 86 | public function can_use_multi_argument_search() 87 | { 88 | $this->config->columns()->push(new Obj([ 89 | 'data' => 'price', 90 | 'name' => 'price', 91 | 'meta' => ['searchable' => true], 92 | ])); 93 | 94 | $response = $this->requestResponse(); 95 | 96 | $this->assertCount(1, $response); 97 | 98 | $this->assertEquals( 99 | $response->first()->name, 100 | $this->testModel->name 101 | ); 102 | } 103 | 104 | private function requestResponse() 105 | { 106 | $query = $this->table->query(); 107 | 108 | (new Search($this->table, $this->config, $query))->handle(); 109 | 110 | return $query->get(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/units/Services/Template/Builders/ButtonsTest.php: -------------------------------------------------------------------------------- 1 | meta = new Obj([]); 23 | 24 | $this->template = new Obj([ 25 | 'auth' => false, 26 | 'buttons' => [], 27 | ]); 28 | } 29 | 30 | /** @test */ 31 | public function can_build_with_type() 32 | { 33 | $this->template->get('buttons')->push(new Obj(['type' => 'row'])); 34 | 35 | $this->build(); 36 | 37 | $this->assertEquals(1, $this->template->get('buttons')->get('row')->count()); 38 | 39 | $this->assertEquals(0, $this->template->get('buttons')->get('global')->count()); 40 | 41 | $this->assertTrue($this->template->get('actions')); 42 | } 43 | 44 | /** @test */ 45 | public function cannot_build_when_user_cannot_access_to_route() 46 | { 47 | $user = Mockery::mock(Config::get('auth.providers.users.model'))->makePartial(); 48 | 49 | $this->actingAs($user); 50 | 51 | $this->template->get('buttons')->push(new Obj([ 52 | 'action' => '', 53 | 'type' => 'row', 54 | 'fullRoute' => 'test', 55 | ])); 56 | 57 | $this->template->get('buttons')->push('create'); 58 | 59 | $this->template->set('auth', true); 60 | 61 | $user->shouldReceive('cannot')->andReturn(true); 62 | 63 | $this->build(); 64 | 65 | $this->assertEmpty($this->template->get('buttons')->get('global')); 66 | 67 | $this->assertEmpty($this->template->get('buttons')->get('row')); 68 | } 69 | 70 | /** @test */ 71 | public function can_build_with_route() 72 | { 73 | Route::any('test')->name('test'); 74 | 75 | Route::getRoutes()->refreshNameLookups(); 76 | 77 | $this->template->get('buttons')->push(new Obj([ 78 | 'action' => 'ajax', 79 | 'type' => 'row', 80 | 'fullRoute' => 'test', 81 | ])); 82 | 83 | $this->build(); 84 | 85 | $this->assertEquals( 86 | '/test?dtRowId', 87 | $this->template->get('buttons') 88 | ->get('row') 89 | ->first() 90 | ->get('path') 91 | ); 92 | } 93 | 94 | /** @test */ 95 | public function can_build_with_predefined_buttons() 96 | { 97 | $this->template->set('buttons', new Collection(['create', 'show'])); 98 | 99 | $this->build(); 100 | 101 | $this->assertEquals( 102 | (new Obj(Config::get('enso.tables.buttons.global.create'))) 103 | ->put('name', 'create') 104 | ->except('routeSuffix'), 105 | $this->template->get('buttons')->get('global')->first()->except('route') 106 | ); 107 | 108 | $this->assertEquals( 109 | (new Obj(Config::get('enso.tables.buttons.row.show'))) 110 | ->put('name', 'show') 111 | ->except('routeSuffix'), 112 | $this->template->get('buttons')->get('row')->first()->except('route') 113 | ); 114 | 115 | $this->assertEquals( 116 | '.'.Config::get('enso.tables.buttons.global.create.routeSuffix'), 117 | $this->template->get('buttons')->get('global')->first()->get('route') 118 | ); 119 | 120 | $this->assertEquals( 121 | '.'.Config::get('enso.tables.buttons.row.show.routeSuffix'), 122 | $this->template->get('buttons')->get('row')->first()->get('route') 123 | ); 124 | } 125 | 126 | private function build(): void 127 | { 128 | (new Buttons($this->template))->build(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/units/Services/Template/Builders/ColumnTest.php: -------------------------------------------------------------------------------- 1 | meta = new Obj([]); 22 | $this->template = new Obj(['columns' => [[]]]); 23 | } 24 | 25 | /** @test */ 26 | public function can_build_basic() 27 | { 28 | $this->build(); 29 | 30 | $this->assertTrue($this->template->get('columns')->first()->has('meta')); 31 | } 32 | 33 | /** @test */ 34 | public function can_build_with_meta_attributes() 35 | { 36 | Collection::wrap(Column::Meta) 37 | ->each(fn ($attribute) => $this->assertPresent($attribute, $this->metaValue($attribute))); 38 | } 39 | 40 | private function assertPresent(string $attribute, $expected) 41 | { 42 | $this->template->set('columns', new Obj([['meta' => [$attribute]]])); 43 | $this->build(); 44 | 45 | $this->assertEquals( 46 | $expected['value'], 47 | $this->template->get('columns')->first()->get('meta')->get($expected['key']) 48 | ); 49 | } 50 | 51 | private function build(): void 52 | { 53 | (new Columns( 54 | $this->template, 55 | $this->meta 56 | ))->build(); 57 | } 58 | 59 | protected function metaValue($attribute): array 60 | { 61 | if ($attribute === 'notVisible') { 62 | return ['key' => 'visible', 'value' => false]; 63 | } 64 | 65 | if (Str::startsWith($attribute, 'sort:')) { 66 | return ['key' => 'sort', 'value' => Str::replaceFirst('sort:', '', $attribute)]; 67 | } 68 | 69 | return ['key' => $attribute, 'value' => true]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/units/Services/Template/Builders/StructureTest.php: -------------------------------------------------------------------------------- 1 | createRoute(); 20 | 21 | $this->template = new Obj([ 22 | 'routePrefix' => 'prefix', 23 | 'dataRouteSuffix' => 'suffix', 24 | 'model' => 'test', 25 | ]); 26 | 27 | $this->meta = new Obj([]); 28 | } 29 | 30 | /** @test */ 31 | public function can_build_with_route() 32 | { 33 | $this->build(); 34 | 35 | $this->assertEquals('/test', $this->template->get('readPath')); 36 | } 37 | 38 | /** @test */ 39 | public function can_build_with_length_menu() 40 | { 41 | $options = [12, 24]; 42 | 43 | $this->template->set('lengthMenu', $options); 44 | 45 | $this->build(); 46 | 47 | $this->assertEquals($options[0], $this->meta->get('length')); 48 | } 49 | 50 | private function createRoute($name = 'prefix.suffix', $path = '/test'): \Illuminate\Routing\Route 51 | { 52 | $route = Route::any($path)->name($name); 53 | 54 | Route::getRoutes()->refreshNameLookups(); 55 | 56 | return $route; 57 | } 58 | 59 | private function build(): void 60 | { 61 | (new Structure( 62 | $this->template, 63 | $this->meta 64 | ))->build(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/units/Services/Template/Validators/AttributesTest.php: -------------------------------------------------------------------------------- 1 | template = new Obj($this->mockedTemplate()); 21 | } 22 | 23 | /** @test */ 24 | public function cannot_validate_with_invalid_length_menu_format() 25 | { 26 | $this->template->set('lengthMenu', 'string'); 27 | 28 | $this->expectException(Exception::class); 29 | 30 | $this->expectExceptionMessage(Exception::invalidLengthMenu()->getMessage()); 31 | 32 | $this->validate(); 33 | } 34 | 35 | /** @test */ 36 | public function cannot_validate_with_non_numeric_debounce() 37 | { 38 | $this->template->set('debounce', 'string'); 39 | 40 | $this->expectException(Exception::class); 41 | 42 | $this->expectExceptionMessage(Exception::invalidDebounce()->getMessage()); 43 | 44 | $this->validate(); 45 | } 46 | 47 | /** @test */ 48 | public function cannot_validate_with_invalid_method() 49 | { 50 | $this->template->set('method', 'patch'); 51 | 52 | $this->expectException(Exception::class); 53 | 54 | $this->expectExceptionMessage(Exception::invalidMethod()->getMessage()); 55 | 56 | $this->validate(); 57 | } 58 | 59 | /** @test */ 60 | public function cannot_validate_with_non_boolean_selectable() 61 | { 62 | $this->template->set('selectable', 'string'); 63 | 64 | $this->expectException(Exception::class); 65 | 66 | $this->expectExceptionMessage(Exception::invalidSelectable()->getMessage()); 67 | 68 | $this->validate(); 69 | } 70 | 71 | /** @test */ 72 | public function cannot_validate_with_invalid_comparison_operator() 73 | { 74 | $this->template->set('comparisonOperator', ComparisonOperators::Equal); 75 | 76 | $this->expectException(Exception::class); 77 | 78 | $this->expectExceptionMessage(Exception::invalidComparisonOperator()->getMessage()); 79 | 80 | $this->validate(); 81 | } 82 | 83 | /** @test */ 84 | public function can_validate() 85 | { 86 | $this->validate(); 87 | 88 | $this->assertTrue(true); 89 | } 90 | 91 | private function validate() 92 | { 93 | $this->validator = new Attributes($this->template); 94 | 95 | $this->validator->validate(); 96 | } 97 | 98 | private function mockedTemplate() 99 | { 100 | return new Obj([ 101 | 'lengthMenu' => new Obj([]), 102 | 'debounce' => 10, 103 | 'method' => 'POST', 104 | 'selectable' => true, 105 | 'comparisonOperator' => 'LIKE', 106 | 'name' => 'name', 107 | 'columns' => [], 108 | 'buttons' => [], 109 | 'routePrefix' => 'prefix', 110 | ]); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/units/Services/Template/Validators/ButtonTest.php: -------------------------------------------------------------------------------- 1 | template = new Obj([ 24 | 'routePrefix' => 'mockedPrefix', 25 | 'buttons' => [$this->mockedButton()], 26 | ]); 27 | } 28 | 29 | /** @test */ 30 | public function cant_validateout_mandatory_attributes() 31 | { 32 | $this->template->get('buttons')->first()->forget('type'); 33 | 34 | $this->expectException(Exception::class); 35 | 36 | $this->expectExceptionMessage(Exception::missingAttributes()->getMessage()); 37 | 38 | $this->validate(); 39 | } 40 | 41 | /** @test */ 42 | public function cant_validate_invalid_attribute() 43 | { 44 | $this->template->get('buttons')->first()->set('invalid_attribute', 'invalid'); 45 | 46 | $this->expectException(Exception::class); 47 | 48 | $this->expectExceptionMessage(Exception::unknownAttributes()->getMessage()); 49 | 50 | $this->validate(); 51 | } 52 | 53 | /** @test */ 54 | public function cant_validate_invalid_type() 55 | { 56 | $this->template->get('buttons')->first()->set('type', 'unknown'); 57 | 58 | $this->expectException(Exception::class); 59 | 60 | $this->expectExceptionMessage(Exception::invalidType()->getMessage()); 61 | 62 | $this->validate(); 63 | } 64 | 65 | /** @test */ 66 | public function cant_validate_invalid_action() 67 | { 68 | $this->template->get('buttons')->first() 69 | ->set('action', 'unknown'); 70 | 71 | $this->expectException(Exception::class); 72 | 73 | $this->expectExceptionMessage(Exception::invalidAction()->getMessage()); 74 | 75 | $this->validate(); 76 | } 77 | 78 | /** @test */ 79 | public function cant_validate_action_with_missing_method() 80 | { 81 | $button = $this->template->get('buttons')->first(); 82 | 83 | $button->set('action', 'ajax'); 84 | $button->set('fullRoute', $this->createRoute()); 85 | 86 | $this->expectException(Exception::class); 87 | 88 | $this->expectExceptionMessage(Exception::missingMethod()->getMessage()); 89 | 90 | $this->validate(); 91 | } 92 | 93 | /** @test */ 94 | public function cant_validate_invalid_route() 95 | { 96 | $button = $this->template->get('buttons')->first(); 97 | 98 | $button->set('action', 'ajax'); 99 | $button->set('fullRoute', '/'); 100 | $button->set('method', 'GET'); 101 | 102 | $this->expectException(Exception::class); 103 | 104 | $this->expectExceptionMessage(Exception::routeNotFound('/')->getMessage()); 105 | 106 | $this->validate(); 107 | } 108 | 109 | /** @test */ 110 | public function cant_validate_invalid_method() 111 | { 112 | $button = $this->template->get('buttons')->first(); 113 | 114 | $button->set('action', 'ajax'); 115 | $button->set('fullRoute', $this->createRoute()); 116 | $button->set('method', 'invalid_method'); 117 | 118 | $this->expectException(Exception::class); 119 | 120 | $this->expectExceptionMessage(Exception::invalidMethod('invalid_method')->getMessage()); 121 | 122 | $this->validate(); 123 | } 124 | 125 | /** @test */ 126 | public function cant_validate_when_name_missing_for_conditional_actions() 127 | { 128 | $this->template->get('buttons')[0]->set('type', 'row'); 129 | 130 | $this->expectException(Exception::class); 131 | 132 | $this->expectExceptionMessage(Exception::missingName()->getMessage()); 133 | 134 | (new Buttons($this->template, $this->conditionalActionTable())) 135 | ->validate(); 136 | } 137 | 138 | /** @test */ 139 | public function cant_validate_invalid_button_type() 140 | { 141 | $this->template->set('buttons', new Obj(['UNKNOWN_TYPE'])); 142 | 143 | $this->expectException(Exception::class); 144 | 145 | $this->expectExceptionMessage(Exception::undefined('UNKNOWN_TYPE')->getMessage()); 146 | 147 | $this->validate(); 148 | } 149 | 150 | /** @test */ 151 | public function can_validate() 152 | { 153 | $button = $this->template->get('buttons')->first(); 154 | 155 | $button->set('action', 'ajax'); 156 | $button->set('fullRoute', $this->createRoute()); 157 | $button->set('method', 'GET'); 158 | 159 | $this->validate(); 160 | 161 | $this->assertTrue(true); 162 | } 163 | 164 | private function mockedButton() 165 | { 166 | return ['type' => 'global', 'icon' => 'icon']; 167 | } 168 | 169 | private function validate() 170 | { 171 | $this->validator = new Buttons($this->template, new TestTable()); 172 | 173 | $this->validator->validate(); 174 | } 175 | 176 | private function createRoute() 177 | { 178 | Route::any('/test.button.create')->name('test.create'); 179 | Route::getRoutes()->refreshNameLookups(); 180 | 181 | return 'test.create'; 182 | } 183 | 184 | private function conditionalActionTable(): Table 185 | { 186 | return new class() extends TestTable implements ConditionalActions { 187 | public function render(array $row, string $action): bool 188 | { 189 | return false; 190 | } 191 | }; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /tests/units/Services/Template/Validators/ColumnTest.php: -------------------------------------------------------------------------------- 1 | withoutExceptionHandling(); 23 | 24 | $this->template = new Obj(['columns' => [$this->mockedColumn()]]); 25 | } 26 | 27 | /** @test */ 28 | public function can_validate() 29 | { 30 | $this->validate(); 31 | 32 | $this->assertTrue(true); 33 | } 34 | 35 | /** @test */ 36 | public function cannot_validate_with_missing_mandatory_attribute() 37 | { 38 | $this->template->get('columns')->first()->forget('label'); 39 | 40 | $this->expectException(ColumnException::class); 41 | 42 | $this->expectExceptionMessage(ColumnException::missingAttributes('label')->getMessage()); 43 | 44 | $this->validate(); 45 | } 46 | 47 | /** @test */ 48 | public function cannot_validate_with_invalid_attribute() 49 | { 50 | $this->template->get('columns')->first()->set('invalid_attribute', 'invalid'); 51 | 52 | $this->expectException(ColumnException::class); 53 | 54 | $this->expectExceptionMessage(ColumnException::unknownAttributes('invalid_attribute')->getMessage()); 55 | 56 | $this->validate(); 57 | } 58 | 59 | /** @test */ 60 | public function cannot_validate_with_invalid_enum() 61 | { 62 | $this->template->get('columns')->first()->set('enum', 'MissingEnum'); 63 | 64 | $this->expectException(ColumnException::class); 65 | 66 | $this->expectExceptionMessage(ColumnException::enumNotFound('MissingEnum')->getMessage()); 67 | 68 | $this->validate(); 69 | } 70 | 71 | /** @test */ 72 | public function cannot_validate_with_invalid_resource() 73 | { 74 | $this->template->get('columns')->first()->set('resource', 'MissingResource'); 75 | 76 | $this->expectException(ColumnException::class); 77 | 78 | $this->expectExceptionMessage(ColumnException::resourceNotFound('MissingResource')->getMessage()); 79 | 80 | $this->validate(); 81 | } 82 | 83 | /** @test */ 84 | public function can_validate_meta() 85 | { 86 | $this->template->get('columns')->first()->set('meta', new Obj(['sortable'])); 87 | 88 | $this->validate(); 89 | 90 | $this->assertTrue(true); 91 | } 92 | 93 | /** @test */ 94 | public function cannot_validate_meta_with_invalid_attributes() 95 | { 96 | $this->template->get('columns')->first()->set('meta', new Obj(['invalid_attribute'])); 97 | 98 | $this->expectException(MetaException::class); 99 | 100 | $this->expectExceptionMessage(MetaException::unknownAttributes('invalid_attribute')->getMessage()); 101 | 102 | $this->validate(); 103 | } 104 | 105 | private function mockedColumn() 106 | { 107 | return Collection::wrap(Attributes::Mandatory) 108 | ->flip()->map(fn () => new Obj()); 109 | } 110 | 111 | private function validate() 112 | { 113 | $this->validator = new Columns($this->template); 114 | 115 | $this->validator->validate(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/units/Services/Template/Validators/ControlTest.php: -------------------------------------------------------------------------------- 1 | template = new Obj(['controls' => []]); 21 | } 22 | 23 | /** @test */ 24 | public function cannot_validate_with_invalid_button_type() 25 | { 26 | $this->template->get('controls')->push('invalid_action'); 27 | 28 | $this->expectException(Exception::class); 29 | 30 | $this->expectExceptionMessage(Exception::undefined('invalid_action')->getMessage()); 31 | 32 | $this->validate(); 33 | } 34 | 35 | /** @test */ 36 | public function can_validate() 37 | { 38 | $this->createRoute(); 39 | 40 | $this->template->get('controls')->push('columns'); 41 | 42 | $this->validate(); 43 | 44 | $this->assertTrue(true); 45 | } 46 | 47 | private function validate() 48 | { 49 | $this->validator = new Controls($this->template); 50 | 51 | $this->validator->validate(); 52 | } 53 | 54 | private function createRoute() 55 | { 56 | Route::any('/test.button.create')->name('test.create'); 57 | Route::getRoutes()->refreshNameLookups(); 58 | 59 | return 'test.create'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/units/Services/Template/Validators/MetaTest.php: -------------------------------------------------------------------------------- 1 | template = new Obj(['columns' => [$this->mockedColumn()]]); 22 | } 23 | 24 | /** @test */ 25 | public function can_validate_meta() 26 | { 27 | $this->template->get('columns')->first()->set('meta', new Obj(['sortable'])); 28 | 29 | $this->validate(); 30 | 31 | $this->assertTrue(true); 32 | } 33 | 34 | /** @test */ 35 | public function cannot_validate_meta_with_invalid_attributes() 36 | { 37 | $this->template->get('columns')->first()->set('meta', new Obj(['invalid_attribute'])); 38 | 39 | $this->expectException(Exception::class); 40 | 41 | $this->expectExceptionMessage(Exception::unknownAttributes('invalid_attribute')->getMessage()); 42 | 43 | $this->validate(); 44 | } 45 | 46 | private function mockedColumn() 47 | { 48 | return Collection::wrap(Attributes::Mandatory) 49 | ->flip()->map(fn () => new Obj()); 50 | } 51 | 52 | private function validate() 53 | { 54 | $this->validator = new Columns($this->template); 55 | 56 | $this->validator->validate(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/units/Services/Template/Validators/RouteTest.php: -------------------------------------------------------------------------------- 1 | template = new Obj(Collection::wrap(Attributes::Mandatory)->flip()); 22 | } 23 | 24 | /** @test */ 25 | public function cannot_validate_with_invalid_route() 26 | { 27 | $this->template->set('routePrefix', 'routePrefix'); 28 | $this->template->set('dataRouteSuffix', 'dataRouteSuffix'); 29 | 30 | $this->expectException(Exception::class); 31 | 32 | $this->expectExceptionMessage(Exception::notFound('routePrefix.dataRouteSuffix')->getMessage()); 33 | 34 | $this->validate(); 35 | } 36 | 37 | /** @test */ 38 | public function can_validate() 39 | { 40 | \Route::any('route')->name('route.test'); 41 | \Route::getRoutes()->refreshNameLookups(); 42 | 43 | $this->template->set('routePrefix', 'route'); 44 | $this->template->set('dataRouteSuffix', 'test'); 45 | 46 | $this->validate(); 47 | 48 | $this->assertTrue(true); 49 | } 50 | 51 | private function validate() 52 | { 53 | $this->validator = new Route($this->template); 54 | 55 | $this->validator->validate(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/units/Services/Template/Validators/StructureTest.php: -------------------------------------------------------------------------------- 1 | template = new Obj($this->mockedTemplate()); 20 | } 21 | 22 | /** @test */ 23 | public function cannot_validate_without_mandatory_attribute() 24 | { 25 | $this->template->forget('routePrefix'); 26 | 27 | $this->expectException(Exception::class); 28 | 29 | $this->expectExceptionMessage(Exception::missingAttributes('routePrefix')->getMessage()); 30 | 31 | $this->validate(); 32 | } 33 | 34 | /** @test */ 35 | public function cannot_validate_with_invalid_attribute() 36 | { 37 | $this->template->set('invalid_attributes', 'invalid'); 38 | 39 | $this->expectException(Exception::class); 40 | 41 | $this->expectExceptionMessage(Exception::unknownAttributes('invalid_attributes')->getMessage()); 42 | 43 | $this->validate(); 44 | } 45 | 46 | /** @test */ 47 | public function can_validate() 48 | { 49 | $this->validate(); 50 | 51 | $this->assertTrue(true); 52 | } 53 | 54 | private function validate() 55 | { 56 | $this->validator = new Structure($this->template); 57 | 58 | $this->validator->validate(); 59 | } 60 | 61 | private function mockedTemplate() 62 | { 63 | return new Obj([ 64 | 'lengthMenu' => new Obj([]), 65 | 'debounce' => 10, 66 | 'method' => 'POST', 67 | 'selectable' => true, 68 | 'comparisonOperator' => 'LIKE', 69 | 'name' => 'name', 70 | 'columns' => [], 71 | 'buttons' => [], 72 | 'routePrefix' => 'prefix', 73 | ]); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/units/Services/TemplateLoaderTest.php: -------------------------------------------------------------------------------- 1 | name('testTables.tableData'); 21 | Route::getRoutes()->refreshNameLookups(); 22 | 23 | $this->table = new TestTable(); 24 | 25 | Config::set('enso.tables.cache.prefix', 'prefix'); 26 | Config::set('enso.tables.cache.tag', 'tag'); 27 | Config::set('enso.tables.cache.template', 'never'); 28 | } 29 | 30 | /** @test */ 31 | public function can_get_template() 32 | { 33 | $this->assertTemplate((new TemplateLoader($this->table))->handle()); 34 | } 35 | 36 | /** @test */ 37 | public function can_cache_template() 38 | { 39 | TestTable::cache('always'); 40 | 41 | (new TemplateLoader($this->table))->handle(); 42 | 43 | $cache = Cache::tags(['tag'])->get($this->cacheKey()); 44 | 45 | $template = (new Template($this->table))->load($cache['template'], $cache['meta']); 46 | 47 | $this->assertTemplate($template); 48 | } 49 | 50 | /** @test */ 51 | public function cannot_cache_template_with_never_cache_config() 52 | { 53 | Config::set('enso.tables.cache.template', 'never'); 54 | TestTable::cache(null); 55 | 56 | (new TemplateLoader($this->table))->handle(); 57 | 58 | $this->assertNull(Cache::tags(['tag'])->get($this->cacheKey())); 59 | } 60 | 61 | /** @test */ 62 | public function cannot_cache_template_with_never_template_cache() 63 | { 64 | Config::set('enso.tables.cache.template', 'always'); 65 | TestTable::cache('never'); 66 | 67 | (new TemplateLoader($this->table))->handle(); 68 | 69 | $this->assertNull(Cache::tags(['tag'])->get($this->cacheKey())); 70 | } 71 | 72 | /** @test */ 73 | public function can_cache_with_environment() 74 | { 75 | Config::set('enso.tables.cache.template', app()->environment()); 76 | TestTable::cache(null); 77 | 78 | (new TemplateLoader($this->table))->handle(); 79 | 80 | $cache = Cache::tags(['tag'])->get($this->cacheKey()); 81 | 82 | $template = (new Template($this->table))->load($cache['template'], $cache['meta']); 83 | 84 | $this->assertTemplate($template); 85 | } 86 | 87 | private function assertTemplate($cache) 88 | { 89 | $this->assertEquals( 90 | 'name', 91 | $cache->get('columns')->first()->get('name') 92 | ); 93 | } 94 | 95 | private function cacheKey(): string 96 | { 97 | return Config::get('enso.tables.cache.prefix') 98 | .':'.Str::slug(str_replace( 99 | ['/', '.'], 100 | [' ', ' '], 101 | $this->table->templatePath() 102 | )); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/units/Services/TestModel.php: -------------------------------------------------------------------------------- 1 | 'name']; 18 | } 19 | 20 | public function customMethod() 21 | { 22 | return 'custom'; 23 | } 24 | 25 | public static function createTable() 26 | { 27 | Schema::create('test_models', function ($table) { 28 | $table->increments('id'); 29 | $table->string('name')->nullable(); 30 | $table->integer('price')->nullable(); 31 | $table->integer('color')->nullable(); 32 | $table->timestamps(); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/units/Services/TestTable.php: -------------------------------------------------------------------------------- 1 | forget('templateCache'); 42 | 43 | if ($type !== null) { 44 | $template['templateCache'] = $type; 45 | } 46 | 47 | File::put($path, $template->toJson(JSON_PRETTY_PRINT)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/units/Services/templates/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "routePrefix": "testTables", 3 | "buttons": [], 4 | "appends": [], 5 | "columns": [ 6 | { 7 | "label": "", 8 | "name": "name", 9 | "data": "name", 10 | "meta": [ 11 | "searchable" 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /tests/units/Traits/TableCacheTest.php: -------------------------------------------------------------------------------- 1 | key = 'prefix:test_models'; 27 | $this->faker = Factory::create(); 28 | 29 | $this->createTestModelTable(); 30 | 31 | $this->testModel = $this->createTestModel(); 32 | 33 | $this->cache()->put($this->key, 1, now()->addHour()); 34 | } 35 | 36 | /** @test */ 37 | public function should_forgot_cache_when_model_is_deleted() 38 | { 39 | $this->testModel->delete(); 40 | 41 | $this->assertFalse(Cache::has($this->key)); 42 | } 43 | 44 | /** @test */ 45 | public function should_forgot_cache_when_model_is_created() 46 | { 47 | $this->createTestModel(); 48 | 49 | $this->assertFalse(Cache::has($this->key)); 50 | } 51 | 52 | private function createTestModelTable() 53 | { 54 | Schema::create('test_models', function ($table) { 55 | $table->increments('id'); 56 | $table->string('name')->nullable(); 57 | $table->timestamps(); 58 | }); 59 | } 60 | 61 | private function createTestModel() 62 | { 63 | return TestModel::create([ 64 | 'name' => $this->faker->name, 65 | ]); 66 | } 67 | 68 | private function cache() 69 | { 70 | return Cache::getStore() instanceof TaggableStore 71 | ? Cache::tags($this->key) 72 | : Cache::store(); 73 | } 74 | } 75 | 76 | class TestModel extends Model 77 | { 78 | use TableCache; 79 | 80 | protected $fillable = ['name']; 81 | 82 | public function getTable() 83 | { 84 | return 'test_models'; 85 | } 86 | } 87 | --------------------------------------------------------------------------------