lara-zeus / sky

CMS for your website. it include posts, pages, tags, and categories. with a frontend scaffolding ready to use
https://larazeus.com/sky
MIT License
136 stars 25 forks source link

Cached translation support for LibraryTypes and TagTypes. #163

Closed BurakBoz closed 11 months ago

BurakBoz commented 11 months ago

This PR adds fast cached translation support for libraryTypes and tagTypes.

atmonshi commented 11 months ago

Thank you @BurakBoz How to use the translatable? because I get:

Target class [translator] does not exist.

when adding translatable:

->libraryTypes([
      'FILE' => __('File'),
      'IMAGE' => __('Image'),
      'VIDEO' => __('Video'),
  ])

also won't be better to allow to pass a Closure!

BurakBoz commented 11 months ago

Hello! Here is my config/app.php. I believe this will resolve your error.

'providers' => ServiceProvider::defaultProviders()->merge([
        /*
         * Package Service Providers...
         */
        Illuminate\Translation\TranslationServiceProvider::class, // <== Translation Service

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\Filament\AdminPanelProvider::class,
        App\Providers\RouteServiceProvider::class,
    ])->toArray(),

When it comes to why I'm using cache, I noticed that it was being called three times. I implemented this structure to avoid unnecessary repetitive processing.

I'm using without __ function on my AdminPanelProvider

->libraryTypes([
                    'FILE' => 'File',
                    'IMAGE' => 'Image',
                    'VIDEO' => 'Video',
                ])
                ->tagTypes([
                    'tag' => 'Tag',
                    'category' => 'Category',
                    'library' => 'Library',
                    'faq' => 'Faq',
                ]))

I've also created a Trait for this that adds support for untranslatable plugin translations:

<?php

namespace BurakBoz\Filament\Support;

use Filament\Forms\Components\Field;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Toggle;
use Filament\Navigation\NavigationGroup;
use Filament\Navigation\NavigationItem;
use Filament\Tables\Columns\Column;
use Filament\Tables\Filters\BaseFilter;

trait TranslateLabels
{
    public function autoTranslate(): void
    {
        $this->translateLabels([
            NavigationGroup::class,
            NavigationItem::class,
            Field::class,
            BaseFilter::class,
            Placeholder::class,
            Column::class,
            Toggle::class
        ]);
    }

    private function translateLabels(array $components = []): void
    {
        foreach($components as $component)
        {
            if(class_exists($component))
            {
                $component::configureUsing(static function ($c): void
                {
                    method_exists($c, 'translateLabel') && $c->translateLabel();
                    method_exists($c, 'label') && $c->label(__($c->getLabel()));
                });
            }
        }
    }
}

Using it in my AppServiceProvider

class AppServiceProvider extends ServiceProvider
{
    use TranslateLabels;

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        $this->autoTranslate();
    }

Perhaps there are better alternatives to accomplish this; I would appreciate suggestions.

atmonshi commented 11 months ago

that is a nice solution for translating packages :)

I would do it by passing a Closure the same way for other configurations like skyPrefix

    protected Closure| array | null $libraryTypes = [
        'FILE' => 'File',
        'IMAGE' => 'Image',
        'VIDEO' => 'Video',
    ];

    public function libraryTypes(Closure|array $types): static
    {
        $this->libraryTypes = $types;

        return $this;
    }

    public function getLibraryTypes(): Closure|array|null
    {
        return $this->evaluate($this->libraryTypes);
    }

and then in your panel provider:

->libraryTypes(fn()=>[
      'FILE' => __('File'),
      'IMAGE' => __('Image'),
      'VIDEO' => __('Video'),
  ])

but I'm not sure about being called three times, does it happen even if you cache the views?

BurakBoz commented 11 months ago

Closures

This is the way

I believe this approach makes more sense. I will test the cache

BurakBoz commented 11 months ago
    public function getLibraryTypes(): ?array
    {
        !isset($GLOBALS["i"]) && $GLOBALS["i"] = 0; dump($GLOBALS["i"]++, debug_backtrace(limit:1));
        return $this->libraryTypes;
    }
php artisan cache:clear
php artisan optimize:clear
php artisan optimize
php artisan config:cache
php artisan route:cache
php artisan event:cache
php artisan icons:clear
php artisan icons:cache
php artisan view:cache

In a cached app, this results in 7 calls to the /admin/library/create URL in LibraryResource.php:79 and LibraryResource.php:78.

If each call executes the translate function, it can significantly slow down the application.

Here is the patched version of LibraryResource that reduces the number of calls to only 3.

<?php

namespace LaraZeus\Sky\Filament\Resources;

use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
use Filament\Forms\Components\SpatieTagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Tables\Actions\Action;
use Filament\Tables\Actions\ActionGroup;
use Filament\Tables\Actions\DeleteAction;
use Filament\Tables\Actions\EditAction;
use Filament\Tables\Columns\SpatieTagsColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use LaraZeus\Sky\Filament\Resources\LibraryResource\Pages;
use LaraZeus\Sky\Models\Library;
use LaraZeus\Sky\SkyPlugin;
use Wallo\FilamentSelectify\Components\ButtonGroup;

class LibraryResource extends SkyResource
{
    protected static ?string $slug = 'library';

    protected static ?string $navigationIcon = 'heroicon-o-folder';

    protected static ?int $navigationSort = 4;

    public static function getModel(): string
    {
        return SkyPlugin::get()->getModel('Library');
    }

    public static function form(Form $form): Form
    {
        $libraryTypes = SkyPlugin::get()->getLibraryTypes();
        return $form
            ->schema([
                Section::make(__('Library File'))
                    ->columns(2)
                    ->schema([
                        TextInput::make('title')
                            ->label(__('Library Title'))
                            ->required()
                            ->maxLength(255)
                            ->live(onBlur: true)
                            ->afterStateUpdated(function (Set $set, $state, $context) {
                                if ($context === 'edit') {
                                    return;
                                }

                                $set('slug', Str::slug($state));
                            }),

                        TextInput::make('slug')
                            ->unique(ignorable: fn (?Library $record): ?Library => $record)
                            ->required()
                            ->maxLength(255)
                            ->label(__('Library Slug')),

                        Textarea::make('description')
                            ->maxLength(255)
                            ->label(__('Library Description'))
                            ->columnSpan(2),

                        SpatieTagsInput::make('category')
                            ->type('library')
                            ->label(__('Library Categories')),

                        Select::make('type')
                            ->label(__('Library Type'))
                            ->visible($libraryTypes !== null)
                            ->options($libraryTypes),
                    ]),

                Section::make(__('Library File'))
                    ->collapsible()
                    ->compact()
                    ->schema([
                        ButtonGroup::make('upload_or_url')
                            ->live()
                            ->dehydrated(false)
                            ->afterStateHydrated(function (Set $set, Get $get) {
                                $setVal = ($get('file_path') === null) ? 'upload' : 'url';
                                $set('upload_or_url', $setVal);
                            })
                            ->options([
                                'upload' => __('upload'),
                                'url' => __('url'),
                            ])
                            ->default('upload'),
                        SpatieMediaLibraryFileUpload::make('file_path_upload')
                            ->disk(SkyPlugin::get()->getUploadDisk())
                            ->directory(SkyPlugin::get()->getUploadDirectory())
                            ->collection('library')
                            ->multiple()
                            ->reorderable()
                            ->visible(fn (Get $get) => $get('upload_or_url') === 'upload')
                            ->label(''),

                        TextInput::make('file_path')
                            ->label(__('file url'))
                            ->visible(fn (Get $get) => $get('upload_or_url') === 'url')
                            ->url(),
                    ]),
            ]);
    }

    public static function table(Table $table): Table
    {
        $libraryTypes = SkyPlugin::get()->getLibraryTypes();
        return $table
            ->columns([
                TextColumn::make('title')->label(__('Library Title'))->searchable()->sortable()->toggleable(),
                TextColumn::make('slug')->label(__('Library Slug'))->searchable()->sortable()->toggleable(),

                TextColumn::make('type')
                    ->label(__('Library Type'))
                    ->searchable()
                    ->sortable()
                    ->visible($libraryTypes !== null)
                    ->formatStateUsing(fn (string $state): string => str($state)->title())
                    ->color('')
                    ->color(fn (string $state) => match ($state) {
                        'IMAGE' => 'primary',
                        'FILE' => 'success',
                        'VIDEO' => 'warning',
                        default => '',
                    })
                    ->icon(fn (string $state) => match ($state) {
                        'IMAGE' => 'heroicon-o-photo',
                        'FILE' => 'heroicon-o-document',
                        'VIDEO' => 'heroicon-o-film',
                        default => 'heroicon-o-document-magnifying-glass',
                    })
                    ->toggleable(),

                SpatieTagsColumn::make('tags')
                    ->label(__('Library Tags'))
                    ->toggleable()
                    ->type('library'),
            ])
            ->actions([
                ActionGroup::make([
                    EditAction::make('edit')->label(__('Edit')),
                    Action::make('Open')
                        ->color('warning')
                        ->icon('heroicon-o-arrow-top-right-on-square')
                        ->label(__('Open'))
                        ->url(fn (Library $record): string => route('library.item', ['slug' => $record->slug]))
                        ->openUrlInNewTab(),
                    DeleteAction::make('delete')
                        ->label(__('Delete')),
                ]),
            ])
            ->filters([
                SelectFilter::make('type')
                    ->visible()
                    ->options($libraryTypes)
                    ->visible($libraryTypes !== null)
                    ->label(__('type')),
                SelectFilter::make('tags')
                    ->multiple()
                    ->relationship('tags', 'name')
                    ->label(__('Tags')),
            ])
            ->defaultSort('id', 'desc');
    }

    public static function getPages(): array
    {
        return [
            'index' => Pages\ListLibrary::route('/'),
            'create' => Pages\CreateLibrary::route('/create'),
            'edit' => Pages\EditLibrary::route('/{record}/edit'),
        ];
    }

    public static function getLabel(): string
    {
        return __('Library');
    }

    public static function getPluralLabel(): string
    {
        return __('Libraries');
    }

    public static function getNavigationLabel(): string
    {
        return __('Libraries');
    }
}

Closures can be useful for configuration, but they still require a temporary caching solution for translations.

I won't be submitting any more PRs or comments on this topic.

Let's, at the very least, include the Turkish language translation that I have submitted to the package.

atmonshi commented 11 months ago

No problem :). Thank you again, I will re-visit this and see how to improve it more and enhance the performance