From 8743aed709fae098882db0a69c7910218a362004 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Thu, 6 Mar 2025 09:26:48 +0100 Subject: [PATCH 1/3] fix typing --- src/Traits/HasTranslations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Traits/HasTranslations.php b/src/Traits/HasTranslations.php index 1f42dc8..69231be 100644 --- a/src/Traits/HasTranslations.php +++ b/src/Traits/HasTranslations.php @@ -60,7 +60,7 @@ public function setTranslation(string $key, string $value, ?string $locale = nul */ public function getTranslation(string $key, ?string $locale = null): ?Translatable { - /** @var Translatable $translatable */ + /** @var ?Translatable $translatable */ $translatable = $this->translations() ->where('key', $key) ->where('locale', app(LocaleResolver::class)->resolve($locale)) From 546d0025c2b2d44581ee97de0143b90662289a78 Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Thu, 6 Mar 2025 09:38:23 +0100 Subject: [PATCH 2/3] delete nova --- src/Nova/Fields/TranslatableField.php | 430 -------------------------- src/Traits/HasTranslations.php | 11 + tests/ArchTest.php | 26 +- 3 files changed, 14 insertions(+), 453 deletions(-) delete mode 100644 src/Nova/Fields/TranslatableField.php diff --git a/src/Nova/Fields/TranslatableField.php b/src/Nova/Fields/TranslatableField.php deleted file mode 100644 index d3d7604..0000000 --- a/src/Nova/Fields/TranslatableField.php +++ /dev/null @@ -1,430 +0,0 @@ -toArray(); - } - - if (empty($value)) { - return null; - } - - // Create a value map locale => translation - $value = collect($value)->mapWithKeys(function ($item) { - return [$item['locale'] => $item['text']]; - })->toArray(); - - return $value; - - }); - - $this->key($key); - } - - /** - * Set the validation rules for the field. - * - * @param (callable(\Laravel\Nova\Http\Requests\NovaRequest):(array|\Stringable|string|callable))|array|\Stringable|string ...$rules - * @return @return array> - */ - public function getRules(NovaRequest $request) - { - return $this->getLocaleRules($request, true); - } - - protected function getLocaleRules(NovaRequest $request, bool $withLocales = false): array - { - if (! $withLocales) { - $rules = is_callable($this->rules) ? call_user_func($this->rules, $request) : $this->rules; - - return [ - $this->attribute => $rules, - ]; - } - - // Validate the field for each locale - return [ - $this->attribute => function (string $attribute, mixed $value, \Closure $fail) use ($request) { - $value = json_decode($value, true); - if (empty($value) || ! is_array($value)) { - $fail("The {$attribute} is invalid."); - - return; - } - - // Create a rule map for each locale - $localeRuleMap = array_reduce($this->meta['locales'], function ($carry, $locale) use ($request) { - $rules = $this->getRulesForLocale($locale, $request); - - $carry[$locale] = $rules; - - return $carry; - }, []); - - // Use the rule map to validate the value - $validator = Validator::make($value, $localeRuleMap); - - if ($validator->fails()) { - $fail( - collect($validator->messages()) - ->map(function ($message, $locale) { - $localeKey = str_replace('_', ' ', Str::snake($locale)); - - $attributeName = "{$this->name} ({$locale})"; - - return Str::replace($localeKey, $attributeName, $message[0]); - }) - ); - } - }, - ]; - } - - /** - * Fill the model's attribute with data. - * - * @param \Illuminate\Database\Eloquent\Model|\Laravel\Nova\Support\Fluent $model - * @return void - */ - public function fillModelWithData(mixed $model, mixed $value, string $attribute) - { - // skip if the model is a Fluent instance - if ($model instanceof \Laravel\Nova\Support\Fluent) { - $attributes = $model->getAttributes(); - - $findModel = $this->model::find($attributes['id']); - - if (! $findModel) { - // create a uuid to identify the model later - $uuid = $model->uuid ?? Str::uuid()->toString(); - - $model->uuid = $uuid; - // wait for a model to be created - $this->model::created(function ($created) use ($uuid, $value, $attribute) { - // skip if the model is not the one we are looking for - if (! $created->uuid || $created->uuid !== $uuid) { - return; - } - - $this->upsertModelTranslation($created, $value, $attribute); - }); - - return; - } - - $this->upsertModelTranslation($findModel, $value, $attribute); - - return; - } - - // If the model does not implement the IsTranslatable interface, throw an exception - if (! $model instanceof \mindtwo\LaravelTranslatable\Contracts\IsTranslatable) { - throw new \InvalidArgumentException('The model must implement the IsTranslatable interface.'); - } - - $this->upsertModelTranslation($model, $value, $attribute); - } - - /** - * Create or update translations for the given model. - * - * @param \mindtwo\LaravelTranslatable\Contracts\IsTranslatable & Model $model - */ - protected function upsertModelTranslation(IsTranslatable $model, mixed $value, string $attribute): void - { - // If the value is a string, try to decode it - if (is_string($value)) { - $value = json_decode($value, true); - } - - // Skip if the value is not an array - if (! is_array($value)) { - return; - } - - if (! $model->exists) { - // create a uuid to identify the model later - $uuid = $model->uuid ?? Str::uuid()->toString(); - - $model->uuid = $uuid; - // wait for a model to be created - $model::created(function ($model) use ($value, $attribute, $uuid) { - // skip if the model is not the one we are looking for - if (! $model->uuid || $model->uuid !== $uuid) { - return; - } - - $this->upsertModelTranslation($model, $value, $attribute); - }); - - return; - } - - // Create or update translations from the given value - foreach ($value as $locale => $translation) { - if (empty($translation)) { - $model->translations()->where([ - 'locale' => $locale, - 'key' => $attribute, - ])->delete(); - - continue; - } - - $model->translations()->updateOrCreate( - [ - 'locale' => $locale, - 'key' => $attribute, - ], - ['text' => $translation] - ); - } - } - - /** - * Determine if the field is required. - * - * @return bool - */ - public function isRequired(NovaRequest $request) - { - // Get the base rules - $rules = is_callable($this->rules) ? call_user_func($this->rules, $request) : $this->rules; - - if (! empty($this->attribute)) { - if ($request->isResourceIndexRequest() || $request->isLensRequest() || $request->isActionRequest()) { - return in_array('required', $this->getLocaleRules($request)[$this->attribute]); - } - - if ($request->isCreateOrAttachRequest()) { - return in_array('required', $this->getLocaleRules($request)[$this->attribute]); - } - - if ($request->isUpdateOrUpdateAttachedRequest()) { - return in_array('required', $this->getLocaleRules($request)[$this->attribute]); - } - } - - // If the field is not required, return false - return in_array('required', $rules); - } - - /** - * Resolve the given attribute from the given resource. - * - * @param mixed $resource - * @param string $attribute - * @return mixed - */ - protected function resolveAttribute($resource, $attribute) - { - if (is_array($resource)) { - return $resource; - } - - // Get translations for the given key - return $resource->getTranslations($this->meta['key']); - } - - /** - * Set the locales for the field. - * - * TODO closure shape - * - * @return $this - */ - public function locales(array|\Closure $locales) - { - if (is_callable($locales)) { - $locales = call_user_func($locales); - } - - $this->locales = $locales; - - return $this->withMeta(['locales' => $locales]); - } - - /** - * Set the model for the field. - * - * @return $this - */ - public function model(string $model) - { - $this->model = $model; - - return $this; - } - - /** - * Set the key for the field. - * - * @param string $key - * @return $this - */ - public function key($key) - { - return $this->withMeta(['key' => $key]); - } - - /** - * Set the input type for the field to markdown. - * - * @return $this - */ - public function markdown() - { - return $this->inputType('markdown'); - } - - /** - * Set the input type for the field to textarea. - * - * @return $this - */ - public function textarea() - { - return $this->inputType('textarea'); - } - - /** - * Set the input component for the field. - * - * @param string $component - * @return $this - */ - public function inputType($component) - { - $this->inputType = $component; - - return $this; - } - - /** - * Prepare the field for JSON serialization. - * - * @return array - */ - public function jsonSerialize(): array - { - return array_merge(parent::jsonSerialize(), [ - 'inputType' => $this->inputType, - ]); - } - - /** - * Resolve the element's value. - * - * @param mixed $resource - * @param string|null $attribute - * @return void - */ - public function resolve($resource, $attribute = null) - { - $this->resource = $resource; - $attribute = $attribute ?? $this->attribute; - - with(app(NovaRequest::class), function ($request) use ($resource, $attribute) { - if (! $request->isFormRequest() || ! in_array($request->method(), ['PUT', 'PATCH'])) { - parent::resolve($resource, $attribute); - - return; - } - - $value = $request->input($this->attribute); - - if (is_string($value)) { - $value = json_decode($value, true) ?? $value; - } - - $this->value = $value; - }); - } - - /** - * Sync depends on logic. - * - * @return $this - */ - public function syncDependsOn(NovaRequest $request) - { - - $currentValue = $this->value; - $currentLocales = ! empty($this->locales) ? $this->locales : array_keys($currentValue); - - $result = parent::syncDependsOn($request); - - if ($result->value === null && count($currentLocales) !== count($result->locales)) { - $result->value = array_reduce($result->locales, function ($carry, $locale) use ($currentValue) { - $carry[$locale] = $currentValue[$locale] ?? null; - - return $carry; - }, []); - - $result->dependentShouldEmitChangesEvent = true; - } - - return $result; - } - - /** - * Set the rules for the field for a specific language. - * - * @return $this - */ - public function rulesFor(string $locale, array|\Closure $rules) - { - $this->localeRules[$locale] = $rules; - - return $this; - } - - /** - * Get the rules for the field for a specific language. - */ - protected function getRulesForLocale(string $locale, NovaRequest $request): array - { - $rules = $this->localeRules[$locale] ?? $this->rules ?? []; - - if (is_callable($rules)) { - return call_user_func($rules, $request); - } - - return $rules; - } -} diff --git a/src/Traits/HasTranslations.php b/src/Traits/HasTranslations.php index 69231be..6cdc681 100644 --- a/src/Traits/HasTranslations.php +++ b/src/Traits/HasTranslations.php @@ -60,6 +60,17 @@ public function setTranslation(string $key, string $value, ?string $locale = nul */ public function getTranslation(string $key, ?string $locale = null): ?Translatable { + // If translations are already loaded, we can use the collection to find the translation. + if ($this->relationLoaded('translations') && $this->translations->isNotEmpty()) { + /** @var ?Translatable $translatable */ + $translatable = $this->translations + ->where('key', $key) + ->where('locale', app(LocaleResolver::class)->resolve($locale)) + ->first(); + + return $translatable; + } + /** @var ?Translatable $translatable */ $translatable = $this->translations() ->where('key', $key) diff --git a/tests/ArchTest.php b/tests/ArchTest.php index bef65ad..6008533 100644 --- a/tests/ArchTest.php +++ b/tests/ArchTest.php @@ -1,25 +1,5 @@ expect('mindtwo\LaravelTranslatable\Traits') - ->not->toUse('dd'); - -arch('globals dump') - ->expect('mindtwo\LaravelTranslatable\Traits') - ->not->toUse('dump'); - -arch('globals ray') - ->expect('mindtwo\LaravelTranslatable\Traits') - ->not->toUse('ray'); - -arch('globals dd 2') - ->expect('mindtwo\LaravelTranslatable\Models') - ->not->toUse('dd'); - -arch('globals dump 2') - ->expect('mindtwo\LaravelTranslatable\Models') - ->not->toUse('dump'); - -arch('globals ray 2') - ->expect('mindtwo\LaravelTranslatable\Models') - ->not->toUse('ray'); +arch('globals') + ->expect('mindtwo\LaravelTranslatable') + ->not->toUse(['dd', 'dump', 'ray']); From 8cc14c2eded0209b36df6da7aa9f38df1c1bd78e Mon Sep 17 00:00:00 2001 From: Andreas Schneider Date: Thu, 6 Mar 2025 09:46:07 +0100 Subject: [PATCH 3/3] add test for resolve --- tests/ExampleTest.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php index 56cdad4..7aa0645 100644 --- a/tests/ExampleTest.php +++ b/tests/ExampleTest.php @@ -241,3 +241,27 @@ protected function getTitleAttribute() expect($searchResults->count())->toBe(2); expect($searchResults->pluck('title')->toArray())->toContain('bar', 'baz'); }); + +test('expect translations to resolved via loaded relation', function () { + $model = TestModelWithTranslations::create(); + + $model->translations()->create([ + 'key' => 'title', + 'locale' => 'en', + 'text' => 'Test Title', + ]); + + // Check if the relation is not loaded but the translation is still resolved + expect($model->relationLoaded('translations'))->toBeFalse(); + expect($model->getTranslation('title', 'en')->text)->toBe('Test Title'); + + expect($model->relationLoaded('translations'))->toBeFalse(); + + // Load the relation and check if the translation is still resolved even if the relation is loaded + $model->load('translations'); + expect($model->relationLoaded('translations'))->toBeTrue(); + Translatable::query()->delete(); + + expect(Translatable::count())->toBe(0); + expect($model->getTranslation('title', 'en')->text)->toBe('Test Title'); +});