├── .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 | [](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 | [](https://github.styleci.io/repos/111688250)
5 | [](https://packagist.org/packages/laravel-enso/tables)
6 | [](https://packagist.org/packages/laravel-enso/tables)
7 | [](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 | [](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 | [](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 |
--------------------------------------------------------------------------------